1 | """ |
---|
2 | Test Loader |
---|
3 | ----------- |
---|
4 | |
---|
5 | nose's test loader implements the same basic functionality as its |
---|
6 | superclass, unittest.TestLoader, but extends it by more liberal |
---|
7 | interpretations of what may be a test and how a test may be named. |
---|
8 | """ |
---|
9 | from __future__ import generators |
---|
10 | |
---|
11 | import logging |
---|
12 | import os |
---|
13 | import sys |
---|
14 | import unittest |
---|
15 | from inspect import isfunction, ismethod |
---|
16 | from nose.case import FunctionTestCase, MethodTestCase |
---|
17 | from nose.failure import Failure |
---|
18 | from nose.config import Config |
---|
19 | from nose.importer import Importer, add_path, remove_path |
---|
20 | from nose.selector import defaultSelector, TestAddress |
---|
21 | from nose.util import cmp_lineno, getpackage, isclass, isgenerator, ispackage, \ |
---|
22 | match_last, resolve_name, transplant_func, transplant_class, test_address |
---|
23 | from nose.suite import ContextSuiteFactory, ContextList, LazySuite |
---|
24 | |
---|
25 | |
---|
26 | log = logging.getLogger(__name__) |
---|
27 | #log.setLevel(logging.DEBUG) |
---|
28 | |
---|
29 | # for efficiency and easier mocking |
---|
30 | op_normpath = os.path.normpath |
---|
31 | op_abspath = os.path.abspath |
---|
32 | op_join = os.path.join |
---|
33 | op_isdir = os.path.isdir |
---|
34 | op_isfile = os.path.isfile |
---|
35 | |
---|
36 | |
---|
37 | __all__ = ['TestLoader', 'defaultTestLoader'] |
---|
38 | |
---|
39 | |
---|
40 | class TestLoader(unittest.TestLoader): |
---|
41 | """Test loader that extends unittest.TestLoader to: |
---|
42 | |
---|
43 | * Load tests from test-like functions and classes that are not |
---|
44 | unittest.TestCase subclasses |
---|
45 | * Find and load test modules in a directory |
---|
46 | * Support tests that are generators |
---|
47 | * Support easy extensions of or changes to that behavior through plugins |
---|
48 | """ |
---|
49 | config = None |
---|
50 | importer = None |
---|
51 | workingDir = None |
---|
52 | selector = None |
---|
53 | suiteClass = None |
---|
54 | |
---|
55 | def __init__(self, config=None, importer=None, workingDir=None, |
---|
56 | selector=None): |
---|
57 | """Initialize a test loader. |
---|
58 | |
---|
59 | Parameters (all optional): |
---|
60 | |
---|
61 | * config: provide a `nose.config.Config`_ or other config class |
---|
62 | instance; if not provided a `nose.config.Config`_ with |
---|
63 | default values is used. |
---|
64 | * importer: provide an importer instance that implements |
---|
65 | `importFromPath`. If not provided, a |
---|
66 | `nose.importer.Importer`_ is used. |
---|
67 | * workingDir: the directory to which file and module names are |
---|
68 | relative. If not provided, assumed to be the current working |
---|
69 | directory. |
---|
70 | * selector: a selector class or instance. If a class is |
---|
71 | provided, it will be instantiated with one argument, the |
---|
72 | current config. If not provided, a `nose.selector.Selector`_ |
---|
73 | is used. |
---|
74 | """ |
---|
75 | if config is None: |
---|
76 | config = Config() |
---|
77 | if importer is None: |
---|
78 | importer = Importer(config=config) |
---|
79 | if workingDir is None: |
---|
80 | workingDir = config.workingDir |
---|
81 | if selector is None: |
---|
82 | selector = defaultSelector(config) |
---|
83 | elif isclass(selector): |
---|
84 | selector = selector(config) |
---|
85 | self.config = config |
---|
86 | self.importer = importer |
---|
87 | self.workingDir = op_normpath(op_abspath(workingDir)) |
---|
88 | self.selector = selector |
---|
89 | if config.addPaths: |
---|
90 | add_path(workingDir, config) |
---|
91 | self.suiteClass = ContextSuiteFactory(config=config) |
---|
92 | unittest.TestLoader.__init__(self) |
---|
93 | |
---|
94 | def getTestCaseNames(self, testCaseClass): |
---|
95 | """Override to select with selector, unless |
---|
96 | config.getTestCaseNamesCompat is True |
---|
97 | """ |
---|
98 | if self.config.getTestCaseNamesCompat: |
---|
99 | return unittest.TestLoader.getTestCaseNames(self, testCaseClass) |
---|
100 | |
---|
101 | def wanted(attr, cls=testCaseClass, sel=self.selector): |
---|
102 | item = getattr(cls, attr, None) |
---|
103 | if not ismethod(item): |
---|
104 | return False |
---|
105 | return sel.wantMethod(item) |
---|
106 | cases = filter(wanted, dir(testCaseClass)) |
---|
107 | for base in testCaseClass.__bases__: |
---|
108 | for case in self.getTestCaseNames(base): |
---|
109 | if case not in cases: |
---|
110 | cases.append(case) |
---|
111 | # add runTest if nothing else picked |
---|
112 | if not cases and hasattr(testCaseClass, 'runTest'): |
---|
113 | cases = ['runTest'] |
---|
114 | if self.sortTestMethodsUsing: |
---|
115 | cases.sort(self.sortTestMethodsUsing) |
---|
116 | return cases |
---|
117 | |
---|
118 | def loadTestsFromDir(self, path): |
---|
119 | """Load tests from the directory at path. This is a generator |
---|
120 | -- each suite of tests from a module or other file is yielded |
---|
121 | and is expected to be executed before the next file is |
---|
122 | examined. |
---|
123 | """ |
---|
124 | log.debug("load from dir %s", path) |
---|
125 | plugins = self.config.plugins |
---|
126 | plugins.beforeDirectory(path) |
---|
127 | if self.config.addPaths: |
---|
128 | paths_added = add_path(path, self.config) |
---|
129 | |
---|
130 | entries = os.listdir(path) |
---|
131 | entries.sort(lambda a, b: match_last(a, b, self.config.testMatch)) |
---|
132 | for entry in entries: |
---|
133 | # this hard-coded initial-dot test will be removed: |
---|
134 | # http://code.google.com/p/python-nose/issues/detail?id=82 |
---|
135 | if entry.startswith('.'): |
---|
136 | continue |
---|
137 | entry_path = op_abspath(op_join(path, entry)) |
---|
138 | is_file = op_isfile(entry_path) |
---|
139 | wanted = False |
---|
140 | if is_file: |
---|
141 | is_dir = False |
---|
142 | wanted = self.selector.wantFile(entry_path) |
---|
143 | else: |
---|
144 | is_dir = op_isdir(entry_path) |
---|
145 | if is_dir: |
---|
146 | # this hard-coded initial-underscore test will be removed: |
---|
147 | # http://code.google.com/p/python-nose/issues/detail?id=82 |
---|
148 | if entry.startswith('_'): |
---|
149 | continue |
---|
150 | wanted = self.selector.wantDirectory(entry_path) |
---|
151 | is_package = ispackage(entry_path) |
---|
152 | if wanted: |
---|
153 | if is_file: |
---|
154 | plugins.beforeContext() |
---|
155 | if entry.endswith('.py'): |
---|
156 | yield self.loadTestsFromName( |
---|
157 | entry_path, discovered=True) |
---|
158 | else: |
---|
159 | yield self.loadTestsFromFile(entry_path) |
---|
160 | plugins.afterContext() |
---|
161 | elif is_package: |
---|
162 | # Load the entry as a package: given the full path, |
---|
163 | # loadTestsFromName() will figure it out |
---|
164 | yield self.loadTestsFromName( |
---|
165 | entry_path, discovered=True) |
---|
166 | else: |
---|
167 | # Another test dir in this one: recurse lazily |
---|
168 | yield self.suiteClass( |
---|
169 | lambda: self.loadTestsFromDir(entry_path)) |
---|
170 | tests = [] |
---|
171 | for test in plugins.loadTestsFromDir(path): |
---|
172 | tests.append(test) |
---|
173 | # TODO: is this try/except needed? |
---|
174 | try: |
---|
175 | if tests: |
---|
176 | yield self.suiteClass(tests) |
---|
177 | except (KeyboardInterrupt, SystemExit): |
---|
178 | raise |
---|
179 | except: |
---|
180 | yield self.suiteClass([Failure(*sys.exc_info())]) |
---|
181 | |
---|
182 | # pop paths |
---|
183 | if self.config.addPaths: |
---|
184 | map(remove_path, paths_added) |
---|
185 | plugins.afterDirectory(path) |
---|
186 | |
---|
187 | def loadTestsFromFile(self, filename): |
---|
188 | """Load tests from a non-module file. Default is to raise a |
---|
189 | ValueError; plugins may implement `loadTestsFromFile` to |
---|
190 | provide a list of tests loaded from the file. |
---|
191 | """ |
---|
192 | log.debug("Load from non-module file %s", filename) |
---|
193 | try: |
---|
194 | tests = [test for test in |
---|
195 | self.config.plugins.loadTestsFromFile(filename)] |
---|
196 | if tests: |
---|
197 | # Plugins can yield False to indicate that they were |
---|
198 | # unable to load tests from a file, but it was not an |
---|
199 | # error -- the file just had no tests to load. |
---|
200 | tests = filter(None, tests) |
---|
201 | return self.suiteClass(tests) |
---|
202 | else: |
---|
203 | # Nothing was able to even try to load from this file |
---|
204 | open(filename, 'r').close() # trigger os error |
---|
205 | raise ValueError("Unable to load tests from file %s" |
---|
206 | % filename) |
---|
207 | except (KeyboardInterrupt, SystemExit): |
---|
208 | raise |
---|
209 | except: |
---|
210 | exc = sys.exc_info() |
---|
211 | return self.suiteClass( |
---|
212 | [Failure(exc[0], exc[1], exc[2], |
---|
213 | address=(filename, None, None))]) |
---|
214 | |
---|
215 | def loadTestsFromGenerator(self, generator, module): |
---|
216 | """Lazy-load tests from a generator function. The generator function |
---|
217 | may yield either: |
---|
218 | |
---|
219 | * a callable, or |
---|
220 | * a function name resolvable within the same module |
---|
221 | """ |
---|
222 | def generate(g=generator, m=module): |
---|
223 | try: |
---|
224 | for test in g(): |
---|
225 | test_func, arg = self.parseGeneratedTest(test) |
---|
226 | if not callable(test_func): |
---|
227 | test_func = getattr(m, test_func) |
---|
228 | yield FunctionTestCase(test_func, arg=arg, descriptor=g) |
---|
229 | except KeyboardInterrupt: |
---|
230 | raise |
---|
231 | except: |
---|
232 | exc = sys.exc_info() |
---|
233 | yield Failure(exc[0], exc[1], exc[2], |
---|
234 | address=test_address(generator)) |
---|
235 | return self.suiteClass(generate, context=generator, can_split=False) |
---|
236 | |
---|
237 | def loadTestsFromGeneratorMethod(self, generator, cls): |
---|
238 | """Lazy-load tests from a generator method. |
---|
239 | |
---|
240 | This is more complicated than loading from a generator function, |
---|
241 | since a generator method may yield: |
---|
242 | |
---|
243 | * a function |
---|
244 | * a bound or unbound method, or |
---|
245 | * a method name |
---|
246 | """ |
---|
247 | # convert the unbound generator method |
---|
248 | # into a bound method so it can be called below |
---|
249 | cls = generator.im_class |
---|
250 | inst = cls() |
---|
251 | method = generator.__name__ |
---|
252 | generator = getattr(inst, method) |
---|
253 | |
---|
254 | def generate(g=generator, c=cls): |
---|
255 | try: |
---|
256 | for test in g(): |
---|
257 | test_func, arg = self.parseGeneratedTest(test) |
---|
258 | if not callable(test_func): |
---|
259 | test_func = getattr(c, test_func) |
---|
260 | if ismethod(test_func): |
---|
261 | yield MethodTestCase(test_func, arg=arg, descriptor=g) |
---|
262 | elif isfunction(test_func): |
---|
263 | # In this case we're forcing the 'MethodTestCase' |
---|
264 | # to run the inline function as its test call, |
---|
265 | # but using the generator method as the 'method of |
---|
266 | # record' (so no need to pass it as the descriptor) |
---|
267 | yield MethodTestCase(g, test=test_func, arg=arg) |
---|
268 | else: |
---|
269 | yield Failure( |
---|
270 | TypeError, |
---|
271 | "%s is not a function or method" % test_func) |
---|
272 | except KeyboardInterrupt: |
---|
273 | raise |
---|
274 | except: |
---|
275 | exc = sys.exc_info() |
---|
276 | yield Failure(exc[0], exc[1], exc[2], |
---|
277 | address=test_address(generator)) |
---|
278 | return self.suiteClass(generate, context=generator, can_split=False) |
---|
279 | |
---|
280 | def loadTestsFromModule(self, module, path=None, discovered=False): |
---|
281 | """Load all tests from module and return a suite containing |
---|
282 | them. If the module has been discovered and is not test-like, |
---|
283 | the suite will be empty by default, though plugins may add |
---|
284 | their own tests. |
---|
285 | """ |
---|
286 | log.debug("Load from module %s", module) |
---|
287 | tests = [] |
---|
288 | test_classes = [] |
---|
289 | test_funcs = [] |
---|
290 | # For *discovered* modules, we only load tests when the module looks |
---|
291 | # testlike. For modules we've been directed to load, we always |
---|
292 | # look for tests. (discovered is set to True by loadTestsFromDir) |
---|
293 | if not discovered or self.selector.wantModule(module): |
---|
294 | for item in dir(module): |
---|
295 | test = getattr(module, item, None) |
---|
296 | # print "Check %s (%s) in %s" % (item, test, module.__name__) |
---|
297 | if isclass(test): |
---|
298 | if self.selector.wantClass(test): |
---|
299 | test_classes.append(test) |
---|
300 | elif isfunction(test) and self.selector.wantFunction(test): |
---|
301 | test_funcs.append(test) |
---|
302 | test_classes.sort(lambda a, b: cmp(a.__name__, b.__name__)) |
---|
303 | test_funcs.sort(cmp_lineno) |
---|
304 | tests = map(lambda t: self.makeTest(t, parent=module), |
---|
305 | test_classes + test_funcs) |
---|
306 | |
---|
307 | # Now, descend into packages |
---|
308 | # FIXME can or should this be lazy? |
---|
309 | # is this syntax 2.2 compatible? |
---|
310 | module_paths = getattr(module, '__path__', []) |
---|
311 | if path: |
---|
312 | path = os.path.realpath(path) |
---|
313 | for module_path in module_paths: |
---|
314 | if (self.config.traverseNamespace or not path) or \ |
---|
315 | os.path.realpath(module_path).startswith(path): |
---|
316 | tests.extend(self.loadTestsFromDir(module_path)) |
---|
317 | |
---|
318 | for test in self.config.plugins.loadTestsFromModule(module, path): |
---|
319 | tests.append(test) |
---|
320 | |
---|
321 | return self.suiteClass(ContextList(tests, context=module)) |
---|
322 | |
---|
323 | def loadTestsFromName(self, name, module=None, discovered=False): |
---|
324 | """Load tests from the entity with the given name. |
---|
325 | |
---|
326 | The name may indicate a file, directory, module, or any object |
---|
327 | within a module. See `nose.util.split_test_name` for details on |
---|
328 | test name parsing. |
---|
329 | """ |
---|
330 | # FIXME refactor this method into little bites? |
---|
331 | log.debug("load from %s (%s)", name, module) |
---|
332 | |
---|
333 | suite = self.suiteClass |
---|
334 | |
---|
335 | # give plugins first crack |
---|
336 | plug_tests = self.config.plugins.loadTestsFromName(name, module) |
---|
337 | if plug_tests: |
---|
338 | return suite(plug_tests) |
---|
339 | |
---|
340 | addr = TestAddress(name, workingDir=self.workingDir) |
---|
341 | if module: |
---|
342 | # Two cases: |
---|
343 | # name is class.foo |
---|
344 | # The addr will be incorrect, since it thinks class.foo is |
---|
345 | # a dotted module name. It's actually a dotted attribute |
---|
346 | # name. In this case we want to use the full submitted |
---|
347 | # name as the name to load from the module. |
---|
348 | # name is module:class.foo |
---|
349 | # The addr will be correct. The part we want is the part after |
---|
350 | # the :, which is in addr.call. |
---|
351 | if addr.call: |
---|
352 | name = addr.call |
---|
353 | parent, obj = self.resolve(name, module) |
---|
354 | if (isclass(parent) |
---|
355 | and getattr(parent, '__module__', None) != module.__name__): |
---|
356 | parent = transplant_class(parent, module.__name__) |
---|
357 | obj = getattr(parent, obj.__name__) |
---|
358 | log.debug("parent %s obj %s module %s", parent, obj, module) |
---|
359 | if isinstance(obj, Failure): |
---|
360 | return suite([obj]) |
---|
361 | else: |
---|
362 | return suite(ContextList([self.makeTest(obj, parent)], |
---|
363 | context=parent)) |
---|
364 | else: |
---|
365 | if addr.module: |
---|
366 | try: |
---|
367 | if addr.filename is None: |
---|
368 | module = resolve_name(addr.module) |
---|
369 | else: |
---|
370 | self.config.plugins.beforeImport( |
---|
371 | addr.filename, addr.module) |
---|
372 | # FIXME: to support module.name names, |
---|
373 | # do what resolve-name does and keep trying to |
---|
374 | # import, popping tail of module into addr.call, |
---|
375 | # until we either get an import or run out of |
---|
376 | # module parts |
---|
377 | try: |
---|
378 | module = self.importer.importFromPath( |
---|
379 | addr.filename, addr.module) |
---|
380 | finally: |
---|
381 | self.config.plugins.afterImport( |
---|
382 | addr.filename, addr.module) |
---|
383 | except (KeyboardInterrupt, SystemExit): |
---|
384 | raise |
---|
385 | except: |
---|
386 | exc = sys.exc_info() |
---|
387 | return suite([Failure(exc[0], exc[1], exc[2], |
---|
388 | address=addr.totuple())]) |
---|
389 | if addr.call: |
---|
390 | return self.loadTestsFromName(addr.call, module) |
---|
391 | else: |
---|
392 | return self.loadTestsFromModule( |
---|
393 | module, addr.filename, |
---|
394 | discovered=discovered) |
---|
395 | elif addr.filename: |
---|
396 | path = addr.filename |
---|
397 | if addr.call: |
---|
398 | package = getpackage(path) |
---|
399 | if package is None: |
---|
400 | return suite([ |
---|
401 | Failure(ValueError, |
---|
402 | "Can't find callable %s in file %s: " |
---|
403 | "file is not a python module" % |
---|
404 | (addr.call, path), |
---|
405 | address=addr.totuple())]) |
---|
406 | return self.loadTestsFromName(addr.call, module=package) |
---|
407 | else: |
---|
408 | if op_isdir(path): |
---|
409 | # In this case we *can* be lazy since we know |
---|
410 | # that each module in the dir will be fully |
---|
411 | # loaded before its tests are executed; we |
---|
412 | # also know that we're not going to be asked |
---|
413 | # to load from . and ./some_module.py *as part |
---|
414 | # of this named test load* |
---|
415 | return LazySuite( |
---|
416 | lambda: self.loadTestsFromDir(path)) |
---|
417 | elif op_isfile(path): |
---|
418 | return self.loadTestsFromFile(path) |
---|
419 | else: |
---|
420 | return suite([ |
---|
421 | Failure(OSError, "No such file %s" % path, |
---|
422 | address=addr.totuple())]) |
---|
423 | else: |
---|
424 | # just a function? what to do? I think it can only be |
---|
425 | # handled when module is not None |
---|
426 | return suite([ |
---|
427 | Failure(ValueError, "Unresolvable test name %s" % name, |
---|
428 | address=addr.totuple())]) |
---|
429 | |
---|
430 | def loadTestsFromNames(self, names, module=None): |
---|
431 | """Load tests from all names, returning a suite containing all |
---|
432 | tests. |
---|
433 | """ |
---|
434 | plug_res = self.config.plugins.loadTestsFromNames(names, module) |
---|
435 | if plug_res: |
---|
436 | suite, names = plug_res |
---|
437 | if suite: |
---|
438 | return self.suiteClass([ |
---|
439 | self.suiteClass(suite), |
---|
440 | unittest.TestLoader.loadTestsFromNames(self, names, module) |
---|
441 | ]) |
---|
442 | return unittest.TestLoader.loadTestsFromNames(self, names, module) |
---|
443 | |
---|
444 | def loadTestsFromTestCase(self, testCaseClass): |
---|
445 | """Load tests from a unittest.TestCase subclass. |
---|
446 | """ |
---|
447 | cases = [] |
---|
448 | plugins = self.config.plugins |
---|
449 | for case in plugins.loadTestsFromTestCase(testCaseClass): |
---|
450 | cases.append(case) |
---|
451 | # For efficiency in the most common case, just call and return from |
---|
452 | # super. This avoids having to extract cases and rebuild a context |
---|
453 | # suite when there are no plugin-contributed cases. |
---|
454 | if not cases: |
---|
455 | return super(TestLoader, self).loadTestsFromTestCase(testCaseClass) |
---|
456 | cases.extend( |
---|
457 | [case for case in |
---|
458 | super(TestLoader, self).loadTestsFromTestCase(testCaseClass)]) |
---|
459 | return self.suiteClass(cases) |
---|
460 | |
---|
461 | def loadTestsFromTestClass(self, cls): |
---|
462 | """Load tests from a test class that is *not* a unittest.TestCase |
---|
463 | subclass. |
---|
464 | |
---|
465 | In this case, we can't depend on the class's `__init__` taking method |
---|
466 | name arguments, so we have to compose a MethodTestCase for each |
---|
467 | method in the class that looks testlike. |
---|
468 | """ |
---|
469 | def wanted(attr, cls=cls, sel=self.selector): |
---|
470 | item = getattr(cls, attr, None) |
---|
471 | if not ismethod(item): |
---|
472 | return False |
---|
473 | return sel.wantMethod(item) |
---|
474 | cases = [self.makeTest(getattr(cls, case), cls) |
---|
475 | for case in filter(wanted, dir(cls))] |
---|
476 | for test in self.config.plugins.loadTestsFromTestClass(cls): |
---|
477 | cases.append(test) |
---|
478 | return self.suiteClass(ContextList(cases, context=cls)) |
---|
479 | |
---|
480 | def makeTest(self, obj, parent=None): |
---|
481 | """Given a test object and its parent, return a test case |
---|
482 | or test suite. |
---|
483 | """ |
---|
484 | plug_tests = [] |
---|
485 | try: |
---|
486 | addr = test_address(obj) |
---|
487 | except KeyboardInterrupt: |
---|
488 | raise |
---|
489 | except: |
---|
490 | addr = None |
---|
491 | for test in self.config.plugins.makeTest(obj, parent): |
---|
492 | plug_tests.append(test) |
---|
493 | # TODO: is this try/except needed? |
---|
494 | try: |
---|
495 | if plug_tests: |
---|
496 | return self.suiteClass(plug_tests) |
---|
497 | except (KeyboardInterrupt, SystemExit): |
---|
498 | raise |
---|
499 | except: |
---|
500 | exc = sys.exc_info() |
---|
501 | return Failure(exc[0], exc[1], exc[2], address=addr) |
---|
502 | |
---|
503 | if isinstance(obj, unittest.TestCase): |
---|
504 | return obj |
---|
505 | elif isclass(obj): |
---|
506 | if parent and obj.__module__ != parent.__name__: |
---|
507 | obj = transplant_class(obj, parent.__name__) |
---|
508 | if issubclass(obj, unittest.TestCase): |
---|
509 | return self.loadTestsFromTestCase(obj) |
---|
510 | else: |
---|
511 | return self.loadTestsFromTestClass(obj) |
---|
512 | elif ismethod(obj): |
---|
513 | if parent is None: |
---|
514 | parent = obj.__class__ |
---|
515 | if issubclass(parent, unittest.TestCase): |
---|
516 | return parent(obj.__name__) |
---|
517 | else: |
---|
518 | if isgenerator(obj): |
---|
519 | return self.loadTestsFromGeneratorMethod(obj, parent) |
---|
520 | else: |
---|
521 | return MethodTestCase(obj) |
---|
522 | elif isfunction(obj): |
---|
523 | if parent and obj.__module__ != parent.__name__: |
---|
524 | obj = transplant_func(obj, parent.__name__) |
---|
525 | if isgenerator(obj): |
---|
526 | return self.loadTestsFromGenerator(obj, parent) |
---|
527 | else: |
---|
528 | return FunctionTestCase(obj) |
---|
529 | else: |
---|
530 | return Failure(TypeError, |
---|
531 | "Can't make a test from %s" % obj, |
---|
532 | address=addr) |
---|
533 | |
---|
534 | def resolve(self, name, module): |
---|
535 | """Resolve name within module |
---|
536 | """ |
---|
537 | obj = module |
---|
538 | parts = name.split('.') |
---|
539 | for part in parts: |
---|
540 | parent, obj = obj, getattr(obj, part, None) |
---|
541 | if obj is None: |
---|
542 | # no such test |
---|
543 | obj = Failure(ValueError, "No such test %s" % name) |
---|
544 | return parent, obj |
---|
545 | |
---|
546 | def parseGeneratedTest(self, test): |
---|
547 | """Given the yield value of a test generator, return a func and args. |
---|
548 | |
---|
549 | This is used in the two loadTestsFromGenerator* methods. |
---|
550 | |
---|
551 | """ |
---|
552 | if not isinstance(test, tuple): # yield test |
---|
553 | test_func, arg = (test, tuple()) |
---|
554 | elif len(test) == 1: # yield (test,) |
---|
555 | test_func, arg = (test[0], tuple()) |
---|
556 | else: # yield test, foo, bar, ... |
---|
557 | assert len(test) > 1 # sanity check |
---|
558 | test_func, arg = (test[0], test[1:]) |
---|
559 | return test_func, arg |
---|
560 | |
---|
561 | defaultTestLoader = TestLoader |
---|
562 | |
---|