Changeset 15

Show
Ignore:
Timestamp:
Mon Mar 27 22:59:52 2006
Author:
jpellerin
Message:
  • Add units for attrib plugin option handling (closes #16)
  • Implement additional hooks for plugins to access and control test results and output (closes #19, #24)
  • Add example html output plugin demonstrating those hooks
  • Various bugfixes and doc improvements


Files:

Legend:

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

    r14 r15  
    13 13     TextTestResult  
    14 14 from nose.config import Config  
    15   from nose.importer import _import  
    16 15 from nose.loader import defaultTestLoader  
    17 16 from nose.selector import defaultSelector  
     
    21 20 log = logging.getLogger('nose.core')  
    22 21  
    23   # backwards compatibility  
    24   TestCase = unittest.TestCase  
    25    
    26   class FunctionTestCase(TestCase):  
    27       """TestCase wrapper for functional tests.  
    28    
    29       Don't use this class directly; it is used internally in nose to  
    30       create test cases for functional tests.  
    31        
    32       This class is very similar to unittest.FunctionTestCase, with a few  
    33       extensions:  
    34         * It descends from nose.TestCase, and so provides output  
    35           capture  
    36         * It allows setup and teardown functions to be defined as attributes  
    37           of the test function. A convenient way to set this up is via the  
    38           provided with_setup decorator:  
    39    
    40           def setup_func():  
    41               # ...  
    42    
    43           def teardown_func():  
    44               # ...  
    45            
    46           @with_setup(setup_func, teardown_func)         
    47           def test_something():  
    48               # ...  
    49    
    50       """  
    51       def __init__(self, testFunc, setUp=None, tearDown=None, description=None,  
    52                    fromDirectory=None):  
    53           TestCase.__init__(self)  
    54           self.testFunc = testFunc  
    55           self.setUpFunc = setUp  
    56           self.tearDownFunc = tearDown  
    57           self.description = description  
    58           self.fromDirectory = fromDirectory  
    59    
    60       def id(self):  
    61           name = self.testFunc.__name__  
    62           if self.fromDirectory is None:  
    63               return name  
    64           else:  
    65               return "%s: %s" % (self.fromDirectory, name)  
    66        
    67       def runTest(self):  
    68           self.testFunc()  
    69            
    70       def setUp(self):  
    71           """Run any setup function attached to the test function  
    72           """  
    73           if self.setUpFunc:  
    74               self.setUpFunc()  
    75           else:  
    76               names = ('setup', 'setUp', 'setUpFunc')  
    77               try_run(self.testFunc, names)  
    78    
    79       def tearDown(self):  
    80           """Run any teardown function attached to the test function  
    81           """  
    82           if self.tearDownFunc:  
    83               self.tearDownFunc()  
    84           else:  
    85               names = ('teardown', 'tearDown', 'tearDownFunc')  
    86               try_run(self.testFunc, names)  
    87            
    88       def __str__(self):  
    89           if hasattr(self.testFunc, 'compat_func_name'):  
    90               name = self.testFunc.compat_func_name  
    91           else:  
    92               name = self.testFunc.__name__  
    93           name = "%s.%s" % (self.testFunc.__module__, name)  
    94           if self.fromDirectory is None:  
    95               return name  
    96           else:  
    97               return "%s: %s" % (self.fromDirectory, name)  
    98       __repr__ = __str__  
    99        
    100       def shortDescription(self):  
    101           pass # FIXME  
    102    
    103    
    104            
    105   class MethodTestCase(TestCase):  
    106       """Test case that wraps one method in a test class.  
    107       """  
    108       def __init__(self, cls, method):  
    109           self.cls = cls  
    110           self.method = method  
    111           self.testInstance = self.cls()  
    112           self.testCase = getattr(self.testInstance, self.method)         
    113           super(MethodTestCase, self).__init__()  
    114            
    115       def __str__(self):  
    116           return self.id()  
    117            
    118       def id(self):  
    119           return "%s.%s.%s" % (self.cls.__module__,  
    120                                self.cls.__name__,  
    121                                self.method)  
    122    
    123       def setUp(self):  
    124           """Run any setup method declared in the test class to which this  
    125           method belongs  
    126           """  
    127           names = ('setup', 'setUp')  
    128           try_run(self.testInstance, names)  
    129    
    130       def runTest(self):  
    131           self.testCase()  
    132            
    133       def tearDown(self):  
    134           """Run any teardown method declared in the test class to which  
    135           this method belongs  
    136           """  
    137           if self.testInstance is not None:  
    138               names = ('teardown', 'tearDown')  
    139               try_run(self.testInstance, names)  
    140    
    141       def shortDescription(self):  
    142           # FIXME ... diff output if is TestCase subclass, for back compat  
    143           if self.testCase.__doc__ is not None:             
    144               return '(%s.%s) "%s"' % (self.cls.__module__,  
    145                                        self.cls.__name__,  
    146                                        self.testCase.__doc__)  
    147           return None  
    148        
    149 22  
    150 23 class TestCollector(LazySuite):  
     
    168 41         self.loader = loader  
    169 42         self.path = conf.where  
    170           self.plugins = conf.plugins  
    171 43          
    172 44     def loadtests(self):  
     
    212 84         if wrapper is not None:  
    213 85             test = wrapper  
    214           return super(TextTestRunner, self).run(test)  
      86          
      87         # plugins can decorate or capture the output stream  
      88         wrapped = call_plugins(self.conf.plugins, 'setOutputStream',  
      89                                self.stream)  
      90         if wrapped is not None:  
      91             self.stream = wrapped  
      92              
      93         result = super(TextTestRunner, self).run(test)  
      94         call_plugins(self.conf.plugins, 'finalize', result)  
      95         return result  
    215 96  
    216 97      
  • trunk/nose/suite.py

    r14 r15  
    1 1 import logging  
      2 import sys  
    2 3 import unittest  
    3 4 from nose.config import Config  
  • trunk/nose/plugins/doctests.py

    r14 r15  
    68 68                 yield test  
    69 69              
    70       def loadTestsFromName(self, filename, package=None, importPath=None):  
      70     def loadTestsFromPath(self, filename, package=None, importPath=None):  
    70 70         if self.extension and anyp(filename.endswith, self.extension):  
    71 71             try:  
  • trunk/nose/plugins/base.py

    r14 r15  
    37 37         """  
    38 38         env_opt = 'NOSE_WITH_%s' % self.name.upper()  
      39         env_opt.replace('-', '_')  
    39 40         parser.add_option("--with-%s" % self.name,  
    40 41                           action="store_true",  
     
    96 97     When plugins are called, the first plugin that implements a method  
    97 98     and returns a non-None value wins, and plugin processing ends.  
      99  
      100     In general, plugin methods correspond directly to the methods of  
      101     nose.selector.Selector, nose.loader.TestLoader and  
      102     nose.result.TextTestResult where they are called. In some cases, the  
      103     plugin hook doesn't neatly match the method in which it is called;  
      104     for those, the documentation for the hook will tell you where in the  
      105     test process it is available.  
      106  
      107     Plugin hooks fall into two broad categories: selecting and loading  
      108     tests, and watching and reporting on test results.  
      109      
      110     Selecting and loading tests  
      111     ===========================  
      112  
      113     To alter test selection behavior, implement any necessary want*  
      114     methods as outlined below. Keep in mind, though, that when your  
      115     plugin returns True from a want* method, you will send the requested  
      116     object through the normal test collection process. If the object  
      117     represents something from which normal tests can't be collected, you  
      118     must also implement a loader method to load the tests.  
      119  
      120     Examples:  
      121      * The builtin doctests plugin, for python 2.4 only, implements  
      122        `wantFile` to enable loading of doctests from files that are not  
      123        python modules. It also implements `loadTestsFromModule` to load  
      124        doctests from python modules, and `loadTestsFromPath` to load tests  
      125        from the non-module files selected by `wantFile`.  
      126      * The builtin attrib plugin implements `wantFunction` and  
      127        `wantMethod` so that it can reject tests that don't match the  
      128        specified attributes.  
      129      
      130     Watching or reporting on tests  
      131     ==============================  
      132  
      133     To record information about tests or other modules imported during  
      134     the testing process, output additional reports, or entirely change  
      135     test report output, implement any of the methods outlined below that  
      136     correspond to TextTestResult methods.  
      137  
      138     Examples:  
      139      * The builtin cover plugin implements `begin` and `report` to  
      140        capture and report code coverage metrics for all or selected modules  
      141        loaded during testing.  
      142      * The builtin profile plugin implements `begin`, `prepareTest` and  
      143        `report` to record and output profiling information. In this  
      144        case, the plugin's `prepareTest` method constructs a function that  
      145        runs the test through the hotshot profiler's runcall() method.       
    98 146     """  
    99 147     def __new__(cls, *arg, **kw):  
     
    124 172         pass  
    125 173  
    126       def addFailure(self, test, err):  
    127           """Called when a test fails. DO NOT return a value unless you want to  
    128           stop other plugins from seeing that the test has failed.  
      174     def addFailure(self, test, err, tb_info):  
      175         """Called when a test fails. DO NOT return a value unless you  
      176         want to stop other plugins from seeing that the test has failed.  
    129 177  
    130 178         Parameters:  
     
    133 181          * err:  
    134 182            sys.exc_info() tuple  
      183          * tb_info:  
      184            Introspected traceback info, if any  
    135 185         """  
    136 186         pass  
    137 187  
    138 188     def addSkip(self, test):  
    139           """Called when a test is skipped. DO NOT return a value unless you want  
    140           to stop other plugins from seeing the skipped test.  
      189         """Called when a test is skipped. DO NOT return a value unless  
      190         you want to stop other plugins from seeing the skipped test.  
    141 191  
    142 192         Parameters:  
     
    147 197  
    148 198     def addSuccess(self, test):  
    149           """Called when a test passes. DO NOT return a value unless you want to  
    150           stop other plugins from seeing the passing test.  
      199         """Called when a test passes. DO NOT return a value unless you  
      200         want to stop other plugins from seeing the passing test.  
    151 201  
    152 202         Parameters:  
     
    162 212         pass  
    163 213  
      214     def finalize(self, result):  
      215         """Called after all report output, including output from all plugins,  
      216         has been sent to the stream. Use this to print final test  
      217         results. Return None to allow other plugins to continue  
      218         printing, any other value to stop them.  
      219         """  
      220         pass  
      221      
    164 222     def loadTestsFromModule(self, module):  
    165           """Return iterable of tests in a module. May be a generator. Each item  
    166           returned must be a runnable unittest.TestCase subclass. Return  
    167           None if your plugin cannot collect any tests from module.  
      223         """Return iterable of tests in a module. May be a  
      224         generator. Each item returned must be a runnable  
      225         unittest.TestCase subclass. Return None if your plugin cannot  
      226         collect any tests from module.  
    168 227  
    169 228         Parameters:  
     
    205 264      
    206 265     def loadTestsFromTestCase(self, cls):  
    207           """Return tests in this test case class. Return None if you are not able  
    208           to load any tests, or an iterable if you are. May be a  
      266         """Return tests in this test case class. Return None if you are  
      267         not able to load any tests, or an iterable if you are. May be a  
    209 268         generator.  
    210 269  
     
    235 294         """  
    236 295         pass  
      296  
      297     def setOutputStream(self, stream):  
      298         """Called before test output begins. To direct test output to a  
      299         new stream, return a stream object, which must implement a  
      300         write(msg) method. If you only want to note the stream, not  
      301         capture or redirect it, then return None.  
      302  
      303         Parameters:  
      304          * stream:  
      305            the original output stream  
      306         """  
    237 307      
    238 308     def startTest(self, test):  
     
    269 339         directory, false if you do not, and None if you don't care.  
    270 340  
      341         FIXME: WARNING: Hook not implemented!  
      342          
    271 343         Parameters:  
    272 344          * dirname:  
     
    310 382         """Return true if you want to collection to descend into this  
    311 383         module, false to prevent the collector from descending into the  
    312           module, and None if your don't care.  
      384         module, and None if you don't care.  
    312 384  
    313 385         Parameters:  
  • trunk/nose/plugins/attrib.py

    r14 r15  
    68 68         attr and eval_attr may each be lists.  
    69 69         """  
    70    
      70         self.attribs = []  
      71          
    71 72         # handle python eval-expression parameter  
    72 73         if options.eval_attr:  
     
    101 102                             # -> 'bool(obj.name)' must be True  
    102 103                             value = True  
    103                   self.attribs.append((key, value))  
      104                     self.attribs.append((key, value))  
    103 104         if self.attribs:  
    104 105             self.enabled = True  
  • trunk/nose/util.py

    r14 r15  
    2 2 """  
    3 3 import inspect  
      4 import logging  
    4 5 import os  
    5 6 import sys  
    6 7 import types  
    7 8  
      9 log = logging.getLogger('nose')  
      10  
    8 11 try:  
    9 12     from cStringIO import StringIO  
  • trunk/nose/result.py

    r14 r15  
    17 17 _TextTestResult = unittest._TextTestResult  
    18 18  
    19   # FIXME addSuccess needed here  
    20 19  
    21 20 class TextTestResult(_TextTestResult):  
     
    40 39         whole to be considered non-successful.  
    41 40     """  
    42       def __init__(self, stream, descriptions, verbosity, conf):  
    43           _TextTestResult.__init__(self, stream, descriptions, verbosity)  
      41     separator3 = '.' * 70  
      42      
      43     def __init__(self, stream, descriptions, verbosity, conf):         
    44 44         self.deprecated = []  
    45 45         self.skip = []  
    46 46         self.conf = conf  
    47 47         self.capture = conf.capture  
    48        
    49       def startTest(self, test):  
    50           if self.capture:  
    51               self.resetBuffer()  
    52           super(TextTestResult, self).startTest(test)  
    53           call_plugins(self.conf.plugins, 'startTest', test)  
    54            
    55       def stopTest(self, test):  
    56           if self.capture:  
    57               self.resetBuffer()  
    58           super(TextTestResult, self).stopTest(test)  
    59           call_plugins(self.conf.plugins, 'stopTest', test)  
    60        
      48         _TextTestResult.__init__(self, stream, descriptions, verbosity)  
      49                  
    61 50     def addDeprecated(self, test):  
    62 51         self.deprecated.append((test, '', ''))  
     
    82 71             self.resetBuffer()  
    83 72             self.writeRes('ERROR','E')  
    84               call_plugins(self.conf.plugins, 'addError', test)  
      73             call_plugins(self.conf.plugins, 'addError', test, err)  
    84 73              
    85 74     def addFailure(self, test, err):  
     
    103 92         self.resetBuffer()  
    104 93         self.writeRes('FAIL','F')  
    105           call_plugins(self.conf.plugins, 'addFailure', test)  
      94         call_plugins(self.conf.plugins, 'addFailure', test, err, tb)  
    105 94          
    106 95     def addSkip(self, test):  
     
    110 99         self.writeRes('SKIP','S')  
    111 100         call_plugins(self.conf.plugins, 'addSkip', test)  
    112            
    113       def resetBuffer(self):  
    114           buffer.truncate(0)         
    115 101  
      102     def addSuccess(self, test):  
      103         self.resetBuffer()  
      104         self.writeRes('ok', '.')  
      105         call_plugins(self.conf.plugins, 'addSuccess', test)  
      106          
    116 107     def printErrors(self):  
    117 108         super(TextTestResult, self).printErrors()  
     
    131 122                 self.stream.writeln(ln('>> end captured stdout <<'))  
    132 123  
      124     def resetBuffer(self):  
      125         buffer.truncate(0)         
      126  
      127     def startTest(self, test):  
      128         if self.capture:  
      129             self.resetBuffer()  
      130         super(TextTestResult, self).startTest(test)  
      131         call_plugins(self.conf.plugins, 'startTest', test)  
      132          
      133     def stopTest(self, test):  
      134         if self.capture:  
      135             self.resetBuffer()  
      136         super(TextTestResult, self).stopTest(test)  
      137         call_plugins(self.conf.plugins, 'stopTest', test)  
      138                  
    133 139     def writeRes(self, long, short):  
    134 140         if self.showAll:  
  • trunk/nose/loader.py

    r14 r15  
    8 8 from nose.plugins import call_plugins  
    9 9 from nose.suite import LazySuite, TestClass, TestModule  
    10   from nose.util import split_test_name  
      10 from nose.util import split_test_name, try_run  
    10 10  
    11 11 log = logging.getLogger(__name__)  
     
    39 39     def __init__(self, testFunc, setUp=None, tearDown=None, description=None,  
    40 40                  fromDirectory=None):  
    41           TestCase.__init__(self)  
      41         super(FunctionTestCase, self).__init__()  
    41 41         self.testFunc = testFunc  
    42 42         self.setUpFunc = setUp  
     
    186 186         if self.selector.wantModuleTests(module):  
    187 187             log.debug("load tests from %s", module.__name__)  
    188               for test in self.testsInModule(module):  
      188             for test in self.testsInModule(module, importPath):  
    188 188                 tests.append(test)  
    189 189                      
     
    352 352                     yield MethodTestCase(cls, item)         
    353 353              
    354       def testsInModule(self, module):  
      354     def testsInModule(self, module, importPath=None):  
    354 354         """Find functions and classes matching testMatch, as well as  
    355 355         classes that descend from unittest.TestCase, return all found  
     
    389 389         # run functional tests in the order in which they are defined  
    390 390         func_tests.sort(cmp_line)  
    391           tests.extend([ FunctionTestCase(test,  
    392                                           fromDirectory=self.fromDirectory)  
    393                        for test in func_tests ])  
      391         tests.extend([ FunctionTestCase(test, fromDirectory=importPath)  
      392                        for test in func_tests ])  
    394 393         return tests  
    395 394  
  • trunk/st/unit_tests/test_plugins.py

    r14 r15  
    157 157         plug.configure(opt, conf)  
    158 158         plug.extension = ['.txt']  
    159           suite = plug.loadTestsFromName(fn)  
      159         suite = plug.loadTestsFromPath(fn)  
    159 159         for test in suite:  
    160 160             assert str(test).endswith('doctests.txt')  
     
    163 163 class TestAttribPlugin(unittest.TestCase):  
    164 164  
      165     def test_add_options(self):  
      166         plug = AttributeSelector()  
      167         parser = MockOptParser()  
      168         plug.add_options(parser)  
      169  
      170         expect = [(('-a', '--attr'),  
      171                    {'dest': 'attr', 'action': 'append', 'default': None,  
      172                     'help': 'Run only tests that have attributes '  
      173                     'specified by ATTR [NOSE_ATTR]'}),  
      174                   (('-A', '--eval-attr'),  
      175                    {'dest': 'eval_attr', 'action': 'append',  
      176                     'default': None, 'metavar': 'EXPR',  
      177                     'help': 'Run only tests for whose attributes the '  
      178                     'Python expression EXPR evaluates to True '  
      179                     '[NOSE_EVAL_ATTR]'})]  
      180         self.assertEqual(parser.opts, expect)  
      181  
      182         opt = Bucket()  
      183         opt.attr = ['!slow']  
      184         plug.configure(opt, Config())  
      185         assert plug.enabled  
      186         self.assertEqual(plug.attribs, [('slow',False)])  
      187  
      188         opt.attr = ['fast,quick', 'weird=66']  
      189         plug.configure(opt, Config())  
      190         self.assertEqual(plug.attribs, [('fast', True),  
      191                                         ('quick', True),  
      192                                         ('weird', '66')])  
      193  
      194         opt.attr = None  
      195         opt.eval_attr = [ 'weird >= 66' ]  
      196         plug.configure(opt, Config())  
      197         self.assertEqual(plug.attribs[0][0], 'weird >= 66')  
      198         self.assertTrue(callable(plug.attribs[0][1]))  
      199                          
    165 200     def test_basic_attr(self):  
    166 201         def f():