1"""
2Helper code for implementing unittests.
3
4This module is unsupported and is primairily used in the PyObjC
5testsuite.
6"""
7import plistlib as _pl
8import unittest as _unittest
9import objc
10import os as _os
11import gc as _gc
12import subprocess as _subprocess
13import sys as _sys
14import struct as _struct
15from distutils.sysconfig import get_config_var as _get_config_var
16import re as _re
17
18# Have a way to disable the autorelease pool behaviour
19_usepool = not _os.environ.get('PYOBJC_NO_AUTORELEASE')
20_useleaks = bool(_os.environ.get('PyOBJC_USE_LEAKS'))
21_leaksVerbose = True
22
23def sdkForPython(_cache=[]):
24    """
25    Return the SDK version used to compile Python itself,
26    or None if no framework was used
27    """
28    if not _cache:
29
30        cflags = _get_config_var('CFLAGS')
31        m = _re.search('-isysroot ([^ ]*) ', cflags)
32        if m is None:
33            return None
34
35        path = m.group(1)
36        bn = _os.path.basename(path)
37        version = bn[6:-4]
38        if version.endswith('u'):
39            version = version[:-1]
40
41        return map(int, version.split('.'))
42
43    return _cache[0]
44
45def fourcc(v):
46    """
47    Decode four-character-code integer definition
48
49    (e.g. 'abcd')
50    """
51    return _struct.unpack('>i', v)[0]
52
53def cast_int(value):
54    """
55    Cast value to 32bit integer
56
57    Usage:
58        cast_int(1 << 31) == -1
59
60    (where as: 1 << 31 == 2147483648)
61    """
62    value = value & 0xffffffff
63    if value & 0x80000000:
64        value =   ~value + 1 & 0xffffffff
65        return -value
66    else:
67        return value
68
69_os_release = None
70def os_release():
71    """
72    Returns '10.5' on all releases of Leopard, simularly for other
73    major releases.
74    """
75    global _os_release
76    if _os_release is not None:
77        return _os_release
78
79    pl = _pl.readPlist('/System/Library/CoreServices/SystemVersion.plist')
80    v = pl['ProductVersion']
81    return tuple(map(int, v.split('.')[:2]))
82
83def onlyOn32Bit(function):
84    """
85    Usage::
86
87        class Tests (unittest.TestCase):
88
89            @onlyOn32Bit
90            def test32BitOnly(self):
91                pass
92
93    The test runs only on 32-bit systems
94    """
95    if _sys.maxint > 2 ** 32:
96        return None
97    else:
98        return function
99
100
101def min_os_level(release):
102    """
103    Usage::
104
105        class Tests (unittest.TestCase):
106
107            @min_os_level('10.6')
108            def testSnowLeopardCode(self):
109                pass
110    """
111    if os_release() >= tuple(map(int, release.split('.'))):
112        def decorator(function):
113            return function
114
115    else:
116        def decorator(function):
117            return None
118
119    return decorator
120
121
122
123def _leaks():
124    data = _subprocess.Popen(
125            ['/usr/bin/leaks', str(_os.getpid())], stdout=_subprocess.PIPE
126        ).communicate()[0]
127    return data.splitlines()
128
129
130_poolclass = objc.lookUpClass('NSAutoreleasePool')
131_nscftype = objc.lookUpClass('NSCFType')
132
133class TestCase (_unittest.TestCase):
134    """
135    A version of TestCase that wraps every test into its own
136    autorelease pool.
137
138    This also adds a number of useful assertion methods
139    """
140    def failUnlessIsCFType(self, tp, message = None):
141        if not isinstance(tp, objc.objc_class):
142            self.fail(message or "%r is not a CFTypeRef type"%(tp,))
143
144        if tp is _nscftype:
145            self.fail(message or "%r is not a unique CFTypeRef type"%(tp,))
146
147    def failUnlessIsOpaquePointer(self, tp, message = None):
148        if not hasattr(tp, "__pointer__"):
149            self.fail(message or "%r is not an opaque-pointer"%(tp,))
150
151        if not hasattr(tp, "__typestr__"):
152            self.fail(message or "%r is not an opaque-pointer"%(tp,))
153
154
155    def failUnlessIsNone(self, value, message = None):
156        if value is not None:
157            sel.fail(message or "%r is not %r"%(value, test))
158
159    def failIfIsNone(self, value,  message = None):
160        if value is None:
161            sel.fail(message, "%r is not %r"%(value, test))
162
163    def failUnlessResultIsNullTerminated(self, method, message = None):
164        info = method.__metadata__()
165        if not info['retval'].get('c_array_delimited_by_null'):
166            self.fail(message or "argument %d of %r is not a nul-terminated array"%(argno, method))
167
168    def failUnlessIsNullTerminated(self, method, message = None):
169        info = method.__metadata__()
170        if not info.get('c_array_delimited_by_null') or not info.get('variadic'):
171            self.fail(message or "%s is not a variadic function with a null-terminated list of arguments"%(method,))
172
173    def failUnlessArgIsNullTerminated(self, method, argno, message = None):
174        if isinstance(method, objc.selector):
175            offset = 2
176        else:
177            offset = 0
178        info = method.__metadata__()
179        if not info['arguments'][argno+offset].get('c_array_delimited_by_null'):
180            self.fail(message or "argument %d of %r is not a nul-terminated array"%(argno, method))
181
182    def failUnlessArgIsVariableSize(self, method, argno, message = None):
183        if isinstance(method, objc.selector):
184            offset = 2
185        else:
186            offset = 0
187        info = method.__metadata__()
188        if not info['arguments'][argno+offset].get('c_array_of_variable_length'):
189            self.fail(message or "argument %d of %r is not a variable sized array"%(argno, method))
190
191    def failUnlessResultIsVariableSize(self, method, message = None):
192        info = method.__metadata__()
193        if not info['retval'].get('c_array_of_variable_length'):
194            self.fail(message or "result of %r is not a variable sized array"%(argno, method))
195
196    def failUnlessArgSizeInResult(self, method, argno, message = None):
197        if isinstance(method, objc.selector):
198            offset = 2
199        else:
200            offset = 0
201        info = method.__metadata__()
202        if not info['arguments'][argno+offset].get('c_array_length_in_result'):
203            self.fail(message or "argument %d of %r does not have size in result"%(argno, method))
204
205    def failUnlessArgIsPrintf(self, method, argno, message = None):
206        if isinstance(method, objc.selector):
207            offset = 2
208        else:
209            offset = 0
210        info = method.__metadata__()
211        if not info.get('variadic'):
212            self.fail(message or "%r is not a variadic function"%(method,))
213
214        if not info['arguments'][argno+offset].get('printf_format'):
215            self.fail(message or "%r argument %d is not a printf format string"%(method, argno))
216
217    def failUnlessArgIsCFRetained(self, method, argno, message = None):
218        if isinstance(method, objc.selector):
219            offset = 2
220        else:
221            offset = 0
222        info = method.__metadata__()
223        if not info['arguments'][argno+offset]['already_cfretained']:
224            self.fail(message or "%r is not cfretained"%(method,))
225
226    def failIfArgIsCFRetained(self, method, argno, message = None):
227        if isinstance(method, objc.selector):
228            offset = 2
229        else:
230            offset = 0
231        info = method.__metadata__()
232        if info['arguments'][argno+offset]['already_cfretained']:
233            self.fail(message or "%r is cfretained"%(method,))
234
235    def failUnlessResultIsCFRetained(self, method, message = None):
236        info = method.__metadata__()
237        if not info['retval']['already_cfretained']:
238            self.fail(message or "%r is not cfretained"%(method,))
239
240    def failIfResultIsCFRetained(self, method, message = None):
241        info = method.__metadata__()
242        if info['retval']['already_cfretained']:
243            self.fail(message or "%r is cfretained"%(method,))
244
245    def failUnlessResultIsRetained(self, method, message = None):
246        info = method.__metadata__()
247        if not info['retval']['already_retained']:
248            self.fail(message or "%r is not retained"%(method,))
249
250    def failIfResultIsRetained(self, method, message = None):
251        info = method.__metadata__()
252        if info['retval']['already_retained']:
253            self.fail(message or "%r is retained"%(method,))
254
255    def failUnlessResultHasType(self, method, tp, message=None):
256        info = method.__metadata__()
257        type = info['retval']['type']
258        if type != tp:
259            self.fail(message or "result of %r is not of type %r, but %r"%(
260                method, tp, type))
261
262    def failUnlessArgHasType(self, method, argno, tp, message=None):
263        if isinstance(method, objc.selector):
264            offset = 2
265        else:
266            offset = 0
267        info = method.__metadata__()
268        type = info['arguments'][argno+offset]['type']
269        if type != tp:
270            self.fail(message or "arg %d of %s is not of type %r, but %r"%(
271                argno, method, tp, type))
272
273    def failUnlessArgIsFunction(self, method, argno, sel_type, retained, message=None):
274        if isinstance(method, objc.selector):
275            offset = 2
276        else:
277            offset = 0
278        info = method.__metadata__()
279        type = info['arguments'][argno+offset]['type']
280        if type != '^?':
281            self.fail(message or "arg %d of %s is not of type function_pointer"%(
282                argno, method))
283
284        st = info['arguments'][argno+offset].get('callable')
285        if st is None:
286            self.fail(message or "arg %d of %s is not of type function_pointer"%(
287                argno, method))
288
289        iface = st['retval']['type']
290        for a in st['arguments']:
291            iface += a['type']
292
293        if iface != sel_type:
294            self.fail(message or "arg %d of %s is not a function_pointer with type %r, but %r"%(argno, method, sel_type, iface))
295
296
297        st = info['arguments'][argno+offset]['callable_retained']
298        if bool(st) != bool(retained):
299            self.fail(message or "arg %d of %s; retained: %r, expected: %r"%(
300                argno, method, st, retained))
301
302
303    def failUnlessArgIsSEL(self, method, argno, sel_type, message=None):
304        if isinstance(method, objc.selector):
305            offset = 2
306        else:
307            offset = 0
308        info = method.__metadata__()
309        type = info['arguments'][argno+offset]['type']
310        if type != objc._C_SEL:
311            self.fail(message or "arg %d of %s is not of type SEL"%(
312                argno, method))
313
314        st = info['arguments'][argno+offset].get('sel_of_type')
315        if st != sel_type:
316            self.fail(message or "arg %d of %s doesn't have sel_type %r but %r"%(
317                argno, method, sel_type, st))
318
319    def failUnlessResultIsBOOL(self, method, message=None):
320        info = method.__metadata__()
321        type = info['retval']['type']
322        if type != objc._C_NSBOOL:
323            self.fail(message or "result of %s is not of type BOOL"%(
324                method))
325
326    def failUnlessArgIsBOOL(self, method, argno, message=None):
327        if isinstance(method, objc.selector):
328            offset = 2
329        else:
330            offset = 0
331        info = method.__metadata__()
332        type = info['arguments'][argno+offset]['type']
333        if type != objc._C_NSBOOL:
334            self.fail(message or "arg %d of %s is not of type BOOL"%(
335                argno, method))
336
337    def failUnlessArgIsFixedSize(self, method, argno, count, message=None):
338        if isinstance(method, objc.selector):
339            offset = 2
340        else:
341            offset = 0
342        info = method.__metadata__()
343        cnt = info['arguments'][argno+offset]['c_array_of_fixed_length']
344        if cnt != count:
345            self.fail(message or "arg %d of %s is not a C-array of length %d"%(
346                argno, method, count))
347
348    def failUnlessArgSizeInArg(self, method, argno, count, message=None):
349        if isinstance(method, objc.selector):
350            offset = 2
351        else:
352            offset = 0
353        info = method.__metadata__()
354        cnt = info['arguments'][argno+offset]['c_array_length_in_arg']
355        if isinstance(count, (list, tuple)):
356            count2 = tuple(x + offset for x in count)
357        else:
358            count2 = count + offset
359        if cnt != count2:
360            self.fail(message or "arg %d of %s is not a C-array of with length in arg %d"%(
361                argno, method, count))
362
363    def failUnlessResultSizeInArg(self, method, count, message=None):
364        if isinstance(method, objc.selector):
365            offset = 2
366        else:
367            offset = 0
368        info = method.__metadata__()
369        cnt = info['retval']['c_array_length_in_arg']
370        if cnt != count + offset:
371            self.fail(message or "result %s is not a C-array of with length in arg %d"%(
372                argno, method, count))
373
374
375    def failUnlessArgIsOut(self, method, argno, message=None):
376        if isinstance(method, objc.selector):
377            offset = 2
378        else:
379            offset = 0
380        info = method.__metadata__()
381        type = info['arguments'][argno+offset]['type']
382        if not type.startswith('o^'):
383            self.fail(message or "arg %d of %s is not an 'out' argument"%(
384                argno, method))
385
386    def failUnlessArgIsInOut(self, method, argno, message=None):
387        if isinstance(method, objc.selector):
388            offset = 2
389        else:
390            offset = 0
391        info = method.__metadata__()
392        type = info['arguments'][argno+offset]['type']
393        if not type.startswith('N^'):
394            self.fail(message or "arg %d of %s is not an 'inout' argument"%(
395                argno, method))
396
397    def failUnlessArgIsIn(self, method, argno, message=None):
398        if isinstance(method, objc.selector):
399            offset = 2
400        else:
401            offset = 0
402        info = method.__metadata__()
403        type = info['arguments'][argno+offset]['type']
404        if not type.startswith('n^'):
405            self.fail(message or "arg %d of %s is not an 'in' argument"%(
406                argno, method))
407
408
409    def failUnlessStartswith(self, value, check, message=None):
410        if not value.startswith(check):
411            self.fail(message or "not %r.startswith(%r)"%(value, check))
412
413    def failUnlessIsInstance(self, value, types, message=None):
414        if not isinstance(value, types):
415            self.fail(message or "%s is not an instance of %r"%(value, types))
416
417    def failIfIsInstance(self, value, types, message=None):
418        if isinstance(value, types):
419            self.fail(message or "%s is an instance of %r"%(value, types))
420
421    assertIsInstance = failUnlessIsInstance
422
423    if not hasattr(_unittest.TestCase, "assertAlmostEquals"):
424        def assertAlmostEquals(self, val1, val2, message=None):
425            self.failUnless(abs (val1 - val2) < 0.00001, message)
426
427
428
429    def run(self, *args):
430        if _useleaks:
431            leaksBefore = _leaks()
432        if _usepool:
433            p = _poolclass.alloc().init()
434        else:
435            p = 1
436
437        try:
438            _unittest.TestCase.run(self, *args)
439        finally:
440            _gc.collect()
441            del p
442            _gc.collect()
443
444            if _useleaks:
445                leaksAfter = _leaks()
446                if len(leaksBefore) != len(leaksAfter):
447                    print "\ntest %s is leaking [%d lines]"%(self, len(leaksAfter) - len(leaksBefore))
448                    if _leaksVerbose:
449                        # XXX: add a smartish filter the surpresses the leaks
450                        # in leaksBefore.
451                        for ln in leaksAfter:
452                            print ln
453
454main = _unittest.main
455
456if hasattr(_unittest, 'expectedFailure'):
457    expectedFailure = _unittest.expectedFailure
458else:
459    def expectedFailure(func):
460        def test(self):
461            try:
462                func(self)
463
464            except AssertionError:
465                return
466
467            self.fail("test unexpectedly passed")
468        test.__name__ == func.__name__
469
470        return test
471
472