Changeset 14

Show
Ignore:
Timestamp:
Sun Mar 26 22:47:22 2006
Author:
jpellerin
Message:
  • Document plugin interface (in progress) (#13)
  • Camel case attributes for better consistency w/unittest (closes #26)
  • Move test loading to unittest-compatible test loader class (closes #23)
  • Refactor to reduce source file sizes for readability: LazySuite? moved to nose.suite, TestLoader?, FunctionTestCase? and MethodTestCase? moved to nose.loader
  • Move file_like, split_test_name, test_address from selector class to top-level funcs in nose.util


Files:

Legend:

Unmodified
Added
Removed
Modified
  • trunk/nose/core.py

    r13 r14  
    1 1 """Implements core nose test discovery functions.  
    2 2 """  
    3   import inspect  
    4 3 import logging  
    5 4 import os  
     
    15 14 from nose.config import Config  
    16 15 from nose.importer import _import  
    17   from nose.selector import Selector  
    18   from nose.util import absdir, absfile  
      16 from nose.loader import defaultTestLoader  
      17 from nose.selector import defaultSelector  
      18 from nose.suite import LazySuite  
      19 from nose.util import absdir, absfile, try_run  
    19 20  
    20   defaultSelector = Selector  
    21   log = logging.getLogger('nose')  
      21 log = logging.getLogger('nose.core')  
    22 22  
    23 23 # backwards compatibility  
     
    50 50     """  
    51 51     def __init__(self, testFunc, setUp=None, tearDown=None, description=None,  
    52                    from_directory=None):  
      52                  fromDirectory=None):  
    52 52         TestCase.__init__(self)  
    53 53         self.testFunc = testFunc  
     
    56 56         self.tearDownFunc = tearDown  
    57 57         self.description = description  
    58           self.from_directory = from_directory  
      58         self.fromDirectory = fromDirectory  
    58 58  
    59 59     def id(self):  
    60 60         name = self.testFunc.__name__  
    61           if self.from_directory is None:  
      61         if self.fromDirectory is None:  
    61 61             return name  
    62 62         else:  
    63               return "%s: %s" % (self.from_directory, name)  
      63             return "%s: %s" % (self.fromDirectory, name)  
    63 63      
    64 64     def runTest(self):  
     
    92 92             name = self.testFunc.__name__  
    93 93         name = "%s.%s" % (self.testFunc.__module__, name)  
    94           if self.from_directory is None:  
      94         if self.fromDirectory is None:  
    94 94             return name  
    95 95         else:  
    96               return "%s: %s" % (self.from_directory, name)  
      96             return "%s: %s" % (self.fromDirectory, name)  
    96 96     __repr__ = __str__  
    97 97      
     
    101 101         pass # FIXME  
    102 102  
    103        
      103  
      104          
    104 105 class MethodTestCase(TestCase):  
    105 106     """Test case that wraps one method in a test class.  
     
    147 148      
    148 149  
    149   class TestCollector(unittest.TestSuite):  
    150       """Discovery-based test suite.  
      150 class TestCollector(LazySuite):  
      151     """Discovery-based test suite. FIXME docs out of date  
    151 152  
    152 153     Unless told not to, the collector looks in the current directory for  
     
    160 161       * And file with 'test' in the name (will be loaded as a test module)  
    161 162     """  
    162       failureException = unittest.TestCase.failureException  
    163       ignore = ['CVS']  
    164       selector = None  
    165 163      
    166       def __init__(self, conf):  
      164     def __init__(self, conf, loader=None):  
      165         if loader is None:  
      166             loader = defaultTestLoader(conf)  
    167 167         self.conf = conf  
      168         self.loader = loader  
    168 169         self.path = conf.where  
    169 170         self.plugins = conf.plugins  
    170           if self.selector is None:  
    171               self.selector = defaultSelector(conf)  
    172            
    173       def __call__(self, *args, **kwds):  
    174           """For backwards compatibility. Between python 2.3 and 2.4  
    175           unittest reversed its usage of __call__ and run(). nose expects  
    176           the 2.4 implementation.  
    177           """  
    178           self.run(*args, **kwds)  
    179    
    180       def __iter__(self):  
    181           return self.collect()  
    182 171          
    183       # FIXME  
      172     def loadtests(self):  
      173         for test in self.loader.loadTestsFromDir(self.path):  
      174             yield test  
      175              
    184 176     def __repr__(self):  
    185 177         return "collector in %s" % self.path  
    186 178     __str__ = __repr__  
    187            
    188       def collect(self):  
    189           """Collect tests to run. This method is a generator.  
    190           """  
    191           for test in self.find_tests():  
    192               yield self.load_from_name(test)  
    193                
    194       def exc_info(self):  
    195           return sys.exc_info()  
    196    
    197       def find_tests(self):  
    198           """Find tests in the target directory.  
    199           """  
    200           return self.find_in_dir(self.path)             
    201    
    202       def find_in_dir(self, dirname, package=None):  
    203           """Find tests in a directory.  
    204    
    205           Each item in the directory is tested against self.selector, want_file  
    206           or want_directory as appropriate. Those that are wanted are returned.  
    207           """  
    208           log.info("%s looking for tests in %s [%s]", self, dirname, package)  
    209    
    210           def test_last(a, b, m=self.conf.testMatch):  
    211               if m.search(a) and not m.search(b):  
    212                   return 1  
    213               elif m.search(b) and not m.search(a):  
    214                   return -1  
    215               return cmp(a, b)  
    216            
    217           if not os.path.isabs(dirname):  
    218               raise ValueError("Directory paths must be specified as "  
    219                                "absolute paths (%s)" % dirname)  
    220           tests = []  
    221    
    222           entries = os.listdir(dirname)  
    223    
    224           # to ensure that lib paths are set up correctly before tests are  
    225           # run, examine directories that look like lib or package  
    226           # directories first and tests last  
    227           entries.sort(test_last)  
    228           for item in entries:  
    229               log.debug("candidate %s in %s", item, dirname)  
    230               path = os.path.join(dirname, item)  
    231               if os.path.isfile(path):  
    232                   if self.selector.want_file(path, package):  
    233                       tests.append(path)  
    234               elif os.path.isdir(path):  
    235                   if self.selector.want_directory(path):  
    236                       tests.append(path)  
    237               else:  
    238                   # ignore non-file, non-path item  
    239                   log.warning("%s in %s is neither file nor path", item, dirname)  
    240           if tests:  
    241               log.info("Collected tests %s in %s", tests, dirname)  
    242           return tests  
    243    
    244       def load_from_name(self, filename, path=None, package=None):  
    245           """Load tests from a file, with an optional package prefix and load  
    246           path.  
    247            
    248           """  
    249           log.debug("Load tests from %s", filename)  
    250           head, test = os.path.split(filename)  
    251           if path is None:  
    252               path = head  
    253            
    254           log.debug("Name %s is %s in %s", filename, test, path)  
    255           if os.path.isfile(filename):  
    256               if test.endswith('.py'):  
    257                   # trim the extension of python files  
    258                   test = test[:-3]  
    259                   if package is not None:  
    260                       test = "%s.%s" % (package, test)  
    261                   return TestModule(test, path, self.selector, self.conf)  
    262               # give plugins a chance  
    263               res = call_plugins(self.plugins, 'load_from_name',  
    264                                  filename, path, package)  
    265               if res is not None:  
    266                   return res  
    267               else:  
    268                   raise Exception("%s is not a python source file" % filename)  
    269           elif os.path.isdir(filename):  
    270               init = os.path.join(filename, '__init__.py')  
    271               if not os.path.exists(init):  
    272                   return TestDirectory(filename, self.selector, self.conf)  
    273               else:  
    274                   if package is not None:  
    275                       test = "%s.%s" % (package, test)  
    276                   return TestModule(test, path, self.selector, self.conf)  
    277           else:  
    278               # FIXME give plugins a chance?  
    279               raise Exception("%s is not a directory or python source file" %  
    280                               filename)  
    281    
    282            
    283       def run(self, result):  
    284           """Collect and run all tests, running setup before and teardown  
    285           after.  
    286           """  
    287           try:  
    288               self.setUp()  
    289           except KeyboardInterrupt:  
    290               raise  
    291           except:  
    292               result.addError(self, self.exc_info())  
    293               return  
    294           for test in self.collect():  
    295               log.debug("running test %s", test)  
    296               if result.shouldStop:  
    297                   break  
    298               test(result)  
    299           try:  
    300               self.tearDown()  
    301           except KeyboardInterrupt:  
    302               raise  
    303           except:  
    304               result.addError(self, self.exc_info())             
    305           return result  
    306    
    307       def setUp(self):  
    308           pass  
    309    
    310       def shortDescription(self):  
    311           return str(self) # FIXME  
    312    
    313       def tearDown(self):  
    314           pass  
    315 179      
    316 180 defaultTestCollector = TestCollector  
    317 181  
      182  
    318 183 def collector():  
    319 184     """TestSuite replacement entry point. Use anywhere you might use a  
     
    323 188     """  
    324 189     conf = configure(env=os.environ)  
    325       result.install_patch(conf)     
    326       return TestCollector(conf)  
    327    
    328    
    329   class TestDirectory(TestCollector):  
    330       """Test collector that collects tests from python modules in a  
    331       non-package directory.  
    332       """  
    333       def __init__(self, dirname, selector, conf):  
    334           self.dirname = dirname  
    335           self.selector = selector  
    336           self.conf = conf  
    337            
    338       def __repr__(self):  
    339           return "test directory %s" % self.dirname  
    340       __str__ = __repr__  
    341        
    342       def collect(self):  
    343           """Collect tests from the directory  
    344           """  
    345           log.info("Collect tests in directory %s", self.dirname)  
    346           for test in self.find_in_dir(self.dirname):  
    347               test_module = self.load_from_name(test)  
    348               # otherwise names for tests in test dirs lack context  
    349               test_module.from_directory = self.dirname  
    350               yield test_module  
    351                
    352       def id(self):  
    353           return self.__str__()  
      190     result.install_patch(conf)  
    354 191  
    355        
    356   class TestModule(TestCollector):  
    357       """Test collector that collects tests from modules and packages.  
    358    
    359       This collector collects module members that match the testMatch  
    360       regular expression. For packages, it also collects any modules or  
    361       packages found in the package __path__ that match testMatch. For  
    362       modules that themselves do not match testMatch, the collector collects  
    363       doctests instead of test functions.  
    364    
    365       Before returning the first collected test, any defined setup method  
    366       will be run. Packages may define setup, setUp, setup_package or  
    367       setUpPackage, modules setup, setUp, setup_module, setupModule or  
    368       setUpModule. Likewise, teardown will be run if defined and if setup  
    369       ran successfully; teardown methods follow the same naming rules as  
    370       setup methods.  
    371       """  
    372       from_directory = None  
    373        
    374       def __init__(self, module_name, path, selector, conf):  
    375           self.module_name = module_name  
    376           self.path = path  
    377           self.module = None  
    378           self.selector = selector  
    379           self.conf = conf  
    380           self.plugins = conf.plugins  
    381            
    382       def __repr__(self):  
    383           return "test module %s in %s" % (self.module_name, self.path)  
    384       __str__ = __repr__  
    385        
    386       def collect(self):  
    387           """Collect tests in the module. Packages will also collect tests  
    388           in the package directory. Non-test modules will collect only  
    389           doctests.  
    390           """  
    391           # find tests defined in the module proper -- if it's a test module  
    392           # and we want to examine it  
    393           if self.selector.want_module_tests(self.module):  
    394               log.debug("collect tests in %s", self.module.__name__)  
    395               for test in self.find_in_module(self.module):  
    396                   yield test  
    397                        
    398           # give plugins a chance  
    399           for plug in self.plugins:  
    400               # plugins need not define want_module_tests; in that case  
    401               # they will try to find tests in all modules  
    402               if hasattr(plug, 'tests_in_module'):  
    403                   log.debug("collect tests in %s with plugin %s",  
    404                             self.module.__name__, plug.name)  
    405                   for test in plug.tests_in_module(self.module):  
    406                       yield test  
    407                        
    408           # recurse into all modules  
    409           if hasattr(self.module, '__path__'):  
    410               path = self.module.__path__[0]  
    411               for test in self.find_in_dir(path,  
    412                                            package=self.module.__name__):  
    413                   # setting the package prefix means that we're  
    414                   # loading from our own parent directory, since we're  
    415                   # loading xxx.yyy, not just yyy, so ask the loader  
    416                   # to load from self.path (the path we loaded from),  
    417                   # not path (the path we're at now)  
    418                   yield self.load_from_name(test, self.path,  
    419                                             self.module.__name__)  
    420                    
    421       def find_in_module(self, module):  
    422           """Find functions and classes matching testMatch, as well as  
    423           classes that descend from unittest.TestCase, return all found  
    424           (properly wrapped) as tests.  
    425           """  
    426           def cmp_line(a, b):  
    427               """Compare functions by their line numbers  
    428               """  
    429               try:  
    430                   a_ln = a.func_code.co_firstlineno  
    431                   b_ln = b.func_code.co_firstlineno  
    432               except AttributeError:  
    433                   return 0  
    434               return cmp(a_ln, b_ln)  
    435            
    436           entries = dir(module)  
    437           tests = []  
    438           func_tests = []  
    439           for item in entries:  
    440               test = getattr(module, item)  
    441               if isinstance(test, (type, types.ClassType)):  
    442                   if self.selector.want_class(test):  
    443                       tests.append(TestClass(test, self.selector, self.conf))  
    444               elif callable(test):  
    445                   if not self.selector.want_function(test):  
    446                       continue  
    447                   # might be a generator  
    448                   if self.is_generator(test):  
    449                       func_tests.extend(self.generate_tests(test))  
    450                   else:  
    451                       # nope, simple functional test  
    452                       func_tests.append(test)  
    453    
    454           # run functional tests in the order in which they are defined  
    455           func_tests.sort(cmp_line)  
    456           tests.extend([ FunctionTestCase(test,  
    457                                           from_directory=self.from_directory)  
    458                        for test in func_tests ])  
    459           return tests  
    460    
    461       def generate_tests(self, test):  
    462           cases = []  
    463           for expr in test():  
    464               # build a closure to run the test, and give it a nice name  
    465               def run(expr=expr):  
    466                   expr[0](*expr[1:])  
    467               run.__module__ = test.__module__  
    468               try:  
    469                   run.__name__ = '%s:%s' % (test.__name__, expr[1:])  
    470               except TypeError:  
    471                   # can't set func name in python 2.3  
    472                   run.compat_func_name = '%s:%s' % (test.__name__, expr[1:])  
    473                   pass  
    474               setup = ('setup', 'setUp', 'setUpFunc')  
    475               teardown = ('teardown', 'tearDown', 'tearDownFunc')  
    476               for name in setup:  
    477                   if hasattr(test, name):  
    478                       setattr(run, name, getattr(test, name))  
    479                       break  
    480               for name in teardown:  
    481                   if hasattr(test, name):  
    482                       setattr(run, name, getattr(test, name))  
    483                       break  
    484               cases.append(run)  
    485           return cases  
    486    
    487       def id(self):  
    488           return self.__str__()  
    489        
    490       def is_generator(self, test):  
    491           from compiler.consts import CO_GENERATOR  
    492           try:  
    493               return test.func_code.co_flags & CO_GENERATOR != 0  
    494           except AttributeError:  
    495               return False  
    496        
    497       def setUp(self):  
    498           """Run any package or module setup function found. For packages, setup  
    499           functions may be named 'setupPackage', 'setup_package', 'setUp',  
    500           or 'setup'. For modules, setup functions may be named  
    501           'setupModule', 'setup_module', 'setUp', or 'setup'. The setup  
    502           function may optionally accept a single argument; in that case,  
    503           the test package or module will be passed to the setup function.  
    504           """  
    505           self.module = _import(self.module_name, [self.path], self.conf)  
      192     # FIXME if any plugin implements prepareTest, here must patch  
      193     # unittest.TextTestRunner.run with run like nose's TextTestRunner  
      194     return TestCollector(conf)  
    506 195  
    507           if hasattr(self.module, '__path__'):  
    508               names = ['setupPackage', 'setUpPackage', 'setup_package']  
    509           else:  
    510               names = ['setupModule', 'setUpModule', 'setup_module']  
    511           names += ['setUp', 'setup']  
    512           try_run(self.module, names)  
    513 196              
    514       def tearDown(self):  
    515           """Run any package or module teardown function found. For packages,  
    516           teardown functions may be named 'teardownPackage',  
    517           'teardown_package' or 'teardown'. For modules, teardown functions  
    518           may be named 'teardownModule', 'teardown_module' or  
    519           'teardown'. The teardown function may optionally accept a single  
    520           argument; in that case, the test package or module will be passed  
    521           to the teardown function.  
    522    
    523           The teardown function will be run only if any package or module  
    524           setup function completed successfully.  
    525           """  
    526           if hasattr(self.module, '__path__'):  
    527               names = ['teardownPackage', 'teardown_package']  
    528           else:  
    529               names = ['teardownModule', 'teardown_module']  
    530           names += ['tearDown', 'teardown']         
    531           try_run(self.module, names)  
    532    
    533    
    534   class TestClass(TestCollector):  
    535       """Collects tests from a class.  
    536       """  
    537       def __init__(self, cls, selector, conf):  
    538           self.cls = cls  
    539           self.selector = selector  
    540           self.conf = conf  
    541    
    542       def __str__(self):  
    543           return self.__repr__()  
    544        
    545       def __repr__(self):  
    546           return 'test class %s' % self.cls  
    547            
    548       def collect(self):  
    549           cls = self.cls  
    550           log.debug("collect tests in class %s", cls)  
    551           for item in dir(cls):  
    552               attr = getattr(cls, item)  
    553               wanted = False  
    554               if callable(attr) and self.selector.want_method(attr):  
    555                   if issubclass(cls, unittest.TestCase):                     
    556                       yield cls(item)  
    557                   else:  
    558                       yield MethodTestCase(cls, item)  
    559                                
    560                    
    561 197 class TextTestRunner(unittest.TextTestRunner):  
    562 198  
     
    573 209  
    574 210     def run(self, test):  
    575           wrapper = call_plugins(self.conf.plugins, 'prepare_test', test)  
      211         wrapper = call_plugins(self.conf.plugins, 'prepareTest', test)  
    575 211         if wrapper is not None:  
    576 212             test = wrapper  
    577 213         return super(TextTestRunner, self).run(test)  
    578                
      214  
      215      
    579 216 class TestProgram(unittest.TestProgram):  
    580 217     """usage: %prog [options] [names]  
     
    712 349     parser.add_option("-d", "--detailed-errors", action="store_true",  
    713 350                       default=env.get('NOSE_DETAILED_ERRORS'),  
    714                         dest="detailed_errors", help="Add detail to error"  
      351                       dest="detailedErrors", help="Add detail to error"  
    714 351                       " output by attempting to evaluate failed"  
    715 352                       " asserts [NOSE_DETAILED_ERRORS]")  
    716       parser.add_option("--pdb", action="store_true", dest="debug_errors",  
      353     parser.add_option("--pdb", action="store_true", dest="debugErrors",  
    716 353                       default=env.get('NOSE_PDB'), help="Drop into debugger "  
    717 354                       "on errors")  
    718 355     parser.add_option("--pdb-failures", action="store_true",  
    719                         dest="debug_failures",  
      356                       dest="debugFailures",  
    719 356                       default=env.get('NOSE_PDB_FAILURES'),  
    720 357                       help="Drop into debugger on failures")  
    721 358     parser.add_option("-P", "--no-path-adjustment", action="store_false",  
    722                         dest="add_paths",  
      359                       dest="addPaths",  
    722 359                       default=not env.get('NOSE_NOPATH'),  
    723 360                       help="Don't make any changes to sys.path when "  
     
    762 399     # in Test and properties in runner and loader  
    763 400  
    764       conf.add_paths = options.add_paths  
      401     conf.addPaths = options.addPaths  
    764 401     conf.capture = options.capture  
    765       conf.detailed_errors = options.detailed_errors  
    766       conf.debug_errors = options.debug_errors  
    767       conf.debug_failures = options.debug_failures  
      402     conf.detailedErrors = options.detailedErrors  
      403     conf.debugErrors = options.debugErrors  
      404     conf.debugFailures = options.debugFailures  
    768 405     conf.plugins = [ plug for plug in all_plugins if plug.enabled ]  
    769 406     conf.verbosity = options.verbosity  
     
    798 435  
    799 436 def configure_logging(options):  
    800       loggers = [ '', 'nose', 'nose.importer', 'nose.inspector',  
    801                   'nose.plugins', 'nose.result', 'nose.selector' ]  
      437     loggers = [ '', 'nose', 'nose.core', 'nose.importer', 'nose.inspector',  
      438                 'nose.loader', 'nose.plugins', 'nose.result',  
      439                 'nose.selector', 'nose.suite' ]  
    802 440     lvl = logging.WARNING  
    803 441     if options.verbosity >= 5:  
     
    838 476     """  
    839 477     sys.exit(not main(*arg, **kw))  
    840    
    841 478      
    842   # FIXME move to util     
    843   def try_run(obj, names):  
    844       """Given a list of possible method names, try to run them with the  
    845       provided object. Keep going until something works. Used to run  
    846       setup/teardown methods for module, package, and function tests.  
    847       """  
    848       for name in names:  
    849           func = getattr(obj, name, None)  
    850           if func is not None:  
    851               if type(obj) == types.ModuleType:  
    852                   # py.test compatibility  
    853                   args, varargs, varkw, defaults = inspect.getargspec(func)  
    854                   if len(args):  
    855                       log.debug("call fixture %s.%s(%s)", obj, name, obj)     
    856                       return func(obj)  
    857               log.debug("call fixture %s.%s", obj, name)  
    858               return func()  
    859    
    860 479          
    861 480 # FIXME move to util  
  • trunk/nose/plugins/doctests.py

    r9 r14  
    18 18 import logging  
    19 19 import os  
    20   from nose.plugins.base import Collector, Selector  
      20 from nose.plugins.base import Plugin  
    20 20 from nose.util import anyp  
    21 21  
    22 22 log = logging.getLogger(__name__)  
    23 23  
    24   class Doctest(Collector, Selector):  
      24 class Doctest(Plugin):  
    24 24     """Activate doctest plugin to find and run doctest in non-test modules.  
    25 25     """  
     
    34 34             doctest.DocFileSuite  
    35 35             parser.add_option('--doctest-extension', action="append",  
    36                                 dest="doctest_extension",  
      36                               dest="doctestExtension",  
    36 36                               default=env.get('NOSE_DOCTEST_EXTENSION'),  
    37 37                               help="Also look for doctests in files with "  
     
    44 44         super(Doctest, self).configure(options, config)  
    45 45         try:  
    46               self.extension = self.tolist(options.doctest_extension)  
      46             self.extension = self.tolist(options.doctestExtension)  
    46 46         except AttributeError:  
    47 47             # 2.3, no other-file option  
    48 48             self.extension = None  
      49  
      50     def loadTestsFromModule(self, module):  
      51         if not self.matches(module.__name__):  
      52             log.debug("Doctest doesn't want module %s", module)  
      53             return  
      54         try:  
      55             doctests = doctest.DocTestSuite(module)  
      56         except ValueError:  
      57             log.debug("No doctests in %s", module)  
      58             return  
      59         else:  
      60             # < 2.4 doctest (and unittest) suites don't have iterators  
      61             log.debug("Doctests found in %s", module)  
      62             if hasattr(doctests, '__iter__'):