Anonymous avatar Anonymous committed 084f75b Merge

Merged upstream

Comments (0)

Files changed (10)

 replaced. executeTests in startTestRun should only be able to be set by one
 plugin.
 
+Inserted tests may have the "wrong" class and module, causing class and module
+level setup / teardown to be re-executed. A way of faking this, or indicating
+that they should be ignored for this purpose, should be available.
+
 Add an epilogue to the optparse help messages.
 
 In the test generator plugin exceptions raised whilst loading the tests should
 
 Output by the TextTestRunner and the TextTestResult goes through the 'message' event. To support this the ``_WritelnDecorator`` takes an optional 'runner' argument in the constructor and has a new `write` method. If the 'runner' argument is used then all calls to `write` and `writeln` are sent on to the ``runner.message`` method (which writes directly to the underlying stream).
 
-TextTestResult has a new addReport method used by the TextTestRunner for test reporting. This adds report objects to the ``.reports`` attribute and also delegates to the standard ``addError`` / ``addFailure`` etc methods for tests with standard outcomes. For non-standard outcomes ``addReport`` reports whatever information is specified in the 
+TextTestResult has a new addReport method used by the TextTestRunner for test reporting. This adds report objects to the ``.reports`` attribute and also delegates to the standard ``addError`` / ``addFailure`` etc methods for tests with standard outcomes. For non-standard outcomes ``addReport`` reports whatever information is specified in the XXXXX
+
+Test discovery has been improved. The initial implementation in Python 2.7 was very conservative. The new implementation supports many more common package layouts. It supports the package code being in a 'src' or a 'lib' subdirectory (that isn't itself a package). It also supports tests being in any top level directory that isn't a package, so long as the directory name contains 'test' in it.
 
 TestLoader has a new attribute ``DEFAULT_PATTERN``. This is so that the
 regex matching plugin can change the default pattern used for test discovery
 always-on = False
 
 [generators]
-always-on = False
+always-on = True
+
+[parameters]
+always-on = True
 
 [checker]
 always-on = False
 
 [count]
 always-on = True
-enhanced = False
-
-[parameters]
-always-on = False
+enhanced = False
 ##################################################################
 [project attributes]
 proj.directory-list = [{'dirloc': loc('.'),
-                        'excludes': [u'unittest2.egg-info',
+                        'excludes': [u'dist',
+                                     u'temp',
+                                     u'htmlcov',
                                      u'unittest2/__pycache__',
-                                     u'dist',
                                      u'build',
-                                     u'temp'],
+                                     u'unittest2.egg-info'],
                         'filter': '*',
                         'include_hidden': False,
                         'recursive': True,

unittest2/config.py

 from ConfigParser import SafeConfigParser
 from ConfigParser import Error as ConfigParserError
 
-CFG_NAME = 'unittest.cfg'
+__all__ = (
+    'loadConfig',
+    'getConfig',
+)
+
+USER_DIR = '~'
+CFG_NAMES = (
+    'unittest.cfg',
+    '.unittest.cfg',
+)
 
 DEFAULT = object()
 RETURN_DEFAULT = object()
 
 _config = None
 
-__all__ = (
-    'loadConfig',
-    'getConfig',
-)
-
 def getConfig(section=None):
     # warning! mutable
     if section is None:
     
     configs = []
     if not noUserConfig:
-        cfgPath = os.path.join(os.path.expanduser('~'), CFG_NAME)
+        for CFG_NAME in CFG_NAMES:
+            cfgPath = os.path.join(os.path.expanduser(USER_DIR), CFG_NAME)
+            if os.path.isfile(cfgPath):
+                break
         userParser, userPlugins, userExcludedPlugins = loadPluginsConfigFile(cfgPath)
         configs.append((userPlugins, userParser, userExcludedPlugins))
     
     if configLocations is None:
-        cfgPath = os.path.join(os.getcwd(), CFG_NAME)
+        for CFG_NAME in CFG_NAMES:
+            cfgPath = os.path.join(os.getcwd(), CFG_NAME)
+            if os.path.isfile(cfgPath):
+                break
         localParser, localPlugins, localExcludedPlugins = loadPluginsConfigFile(cfgPath)
         configs.append((localPlugins, localParser, localExcludedPlugins))
         configLocations = []
     for entry in configLocations:
         path = entry
         if not os.path.isfile(path):
-            path = os.path.join(path, CFG_NAME)
-            if not os.path.isfile(path):
+            for CFG_NAME in CFG_NAMES:
+                path = os.path.join(path, CFG_NAME)
+                if os.path.isfile(path):
+                    break
+            else:
                 # exception type?
                 raise Exception('Config file location %r could not be found'
                                 % entry)
     parsers = [parser for _, parser, __ in configs]
     excludedPlugins = set(sum([excluded for _, __, excluded in configs], []))
     _config = combineConfigs(parsers)
-    return plugins - excludedPlugins
+    finalPlugins = plugins - excludedPlugins
+    unittestSection = _config.get('unittest', Section('unittest'))
+    unittestSection['plugins'] = finalPlugins
+    unittestSection['excluded-plugins'] = excludedPlugins
+    return finalPlugins
 
 
 class Section(dict):

unittest2/loader.py

         packages can continue discovery themselves. top_level_dir is stored so
         load_tests does not need to pass this argument in to loader.discover().
         """
+        implicit_start = False
+        if start_dir is None:
+            start_dir = '.'
+            implicit_start = True
         if pattern is None:
             pattern = DEFAULT_PATTERN
         set_implicit_top = False
         if is_not_importable:
             raise ImportError('Start directory is not importable: %r' % start_dir)
 
+        def check_dir(the_dir):
+            if not implicit_start:
+                return
+            full_path =  os.path.join(start_dir, the_dir)
+            if (os.path.isdir(full_path) and not 
+                os.path.isfile(os.path.join(full_path, '__init__.py'))):
+                sys.path.append(full_path)
+                return full_path
+
+        src_dir = check_dir('src')
+        lib_dir = check_dir('lib')
         tests = list(self._find_tests(start_dir, pattern))
+        real_top_level = self._top_level_dir
+        if lib_dir is not None:
+            self._top_level_dir = lib_dir
+            tests.extend(list(self._find_tests(lib_dir, pattern)))
+        if src_dir is not None:
+            self._top_level_dir = src_dir
+            tests.extend(list(self._find_tests(src_dir, pattern)))
+        if implicit_start:
+            for entry in os.listdir(start_dir):
+                if not 'test' in entry.lower():
+                    continue
+                full = check_dir(entry)
+                if full is None:
+                    continue
+                self._top_level_dir = full
+                tests.extend(list(self._find_tests(full, pattern)))
+        
+        self._top_level_dir = real_top_level
         return self.suiteClass(tests)
 
     def _get_name_from_path(self, path):

unittest2/main.py

         if (not forDiscovery and not args and self.module is None and 
             self.defaultTest is None):
             # launched with no args from script
-            options.start = '.'
+            options.start = None
             options.top = options.pattern = None
             forDiscovery =True
 
                               action='store_true')
 
         if forDiscovery:
-            parser.add_option('-s', '--start-directory', dest='start', default='.',
+            parser.add_option('-s', '--start-directory', dest='start', default=None,
                               help="Directory to start discovery ('.' default)")
             parser.add_option('-p', '--pattern', dest='pattern', default=None,
                               help="Pattern to match tests ('test*.py' default)")

unittest2/plugins/checker.py

 from unittest2.events import Plugin
 
 import sys
+__unittest = True
 
 try:
     from pyflakes.scripts.pyflakes import check as pyflakes_check

unittest2/plugins/filtertests.py

-from unittest2.events import Plugin, addOption
+from unittest2 import Plugin, addOption, TestSuite
 
 import re
 
+def flatten(thing):
+    try:
+        for sub_thing in thing:
+            for test in flatten(sub_thing):
+                yield test
+    except TypeError:
+        yield thing
+
 class FilterTests(Plugin):
     """
     Filter which test methods on TestCase classes
         self.regex = [re.compile(regex) for regex in self.regex]
         if not self.regex:
             self.unregister()
-    
-    def getTestCaseNames(self, event):
-        testCase = event.testCase
-        
-        def is_invalid(attr):
-            for regex in self.regex:
-                if regex.match(attr):
-                    return False
-            return True
-        
-        excluded = filter(is_invalid, dir(testCase))
-        event.excludedNames.extend(excluded)
+
+    def startTestRun(self, event):
+        suite = TestSuite()
+        regexes = self.regex
+        for test in flatten(event.suite):
+            try:
+                name = test.id()
+            except AttributeError:
+                name = test.__name__
+            
+            for regex in regexes:
+                if regex.search(name):
+                    suite.addTest(test)
+                    break
+        event.suite = suite

unittest2/plugins/moduleloading.py

+import sys
 from unittest2 import Plugin, FunctionTestCase, TestCase, TestSuite
 from unittest2.util import getObjectFromName
 
 import types
 
+__unittest = True
+
 
 def setUp(setupFunction):
     def decorator(func):
     return decorator
 
 
+def _make_load_test_failure(testname, exc_info):
+    def testFailure(self):
+        raise exc_info[0], exc_info[1], exc_info[2]
+    classname = 'LoadingGeneratedTestFail'
+    attrs = {testname: testFailure}
+    TestClass = type(classname, (TestCase,), attrs)
+    return TestSuite((TestClass(testname),))
+
+class TestNotFoundError(Exception):
+    pass
+
+def testFromName(name, module):
+    pos = name.find(':')
+    index = None
+    if pos != -1:
+        real_name, digits = name[:pos], name[pos+1:]
+        try:
+            index = int(digits)
+        except ValueError:
+            pass
+        else:
+            name = real_name
+
+    try:
+        parent, obj = getObjectFromName(name, module)
+    except AttributeError, ImportError:
+        return None
+    return parent, obj, name, index
+
+
 class Functions(Plugin):
     
     generatorsEnabled = False
     commandLineSwitch = (None, 'functions', 'Load tests from functions')
 
     def loadTestsFromName(self, event):
-        parent, obj = getObjectFromName(event.name, event.module)
+        name = event.name
+        module = event.module
+        result = testFromName(name, module)
+        if result is None:
+            return
+        parent, obj, name, index = result
+
         if isinstance(obj, types.FunctionType):
             suite = TestSuite()
-            for test in self.createTests(obj):
-                suite.addTest(test)
+            suite.addTests(self.createTests(obj, index))
             event.handled = True
             return suite
 
+
     def loadTestsFromModule(self, event):
         loader = event.loader
         module = event.module
                 tests.extend(self.createTests(obj))
         event.extraTests.extend(tests)
 
-    def createTests(self, obj):
+    def createTests(self, obj, testIndex=None):
         tests = []
         args = {}
         setUp = getattr(obj, 'setUp', None)
         else:
             case = FunctionTestCase(obj, **args)
             tests.append(case)
+        if testIndex is not None:
+            # what if this doesn't exist?
+            return [tests[testIndex-1]]
         return tests
 
+
 class GeneratorFunctionCase(FunctionTestCase):
     def __init__(self, name, **args):
         self._name = name
 
     id = __str__ = __repr__
 
+
 class ParamsFunctionCase(FunctionTestCase):
     def __init__(self, name, func, **args):
         self._name = name
 
     id = __str__ = __repr__
 
+
 class Generators(Plugin):
 
     configSection = 'generators'
             method = getattr(testCaseClass, name)
             if getattr(method, 'testGenerator', None) is not None:
                 instance = testCaseClass(name)
-                tests = list(method(instance))
                 event.extraTests.extend(
-                    testsFromGenerator(name, tests, testCaseClass)
+                    testsFromGenerator(name, method(instance), testCaseClass)
                 )
 
     def getTestCaseNames(self, event):
             if getattr(method, 'testGenerator', None) is not None:
                 event.excludedNames.append(name)
 
-def testsFromGenerator(name, tests, testCaseClass):
-    for index, (func, args) in enumerate(tests):
-        method_name = name_from_args(name, index, args)
-        setattr(testCaseClass, method_name, None)
-        instance = testCaseClass(method_name)
-        delattr(testCaseClass, method_name)
-        def method(func=func, args=args):
-            return func(*args)
-        setattr(instance, method_name, method)
-        yield instance
+    def loadTestsFromName(self, event):
+        original_name = name = event.name
+        module = event.module
+        result = testFromName(name, module)
+        if result is None:
+            # we can't find it - let the default case handle it
+            return
+        
+        parent, obj, name, index = result
+        if (index is None or not isinstance(parent, type) or 
+            not issubclass(parent, TestCase) or 
+            not getattr(obj, 'testGenerator', False)):
+            # we're only handling TestCase generator methods here
+            return
+
+        instance = parent(obj.__name__)
+        
+        try:
+            test = list(testsFromGenerator(name, obj(instance), parent))[index-1]
+        except IndexError:
+            raise TestNotFoundError(original_name)
+        
+        suite = TestSuite()
+        suite.addTest(test)
+        event.handled = True
+        return suite
+
+
+def testsFromGenerator(name, generator, testCaseClass):
+    try:
+        for index, (func, args) in enumerate(generator):
+            method_name = name_from_args(name, index, args)
+            setattr(testCaseClass, method_name, None)
+            instance = testCaseClass(method_name)
+            delattr(testCaseClass, method_name)
+            def method(func=func, args=args):
+                return func(*args)
+            setattr(instance, method_name, method)
+            yield instance
+    except:
+        exc_info = sys.exc_info()
+        test_name = '%s.%s.%s' % (testCaseClass.__module__,
+                                  testCaseClass.__name__,
+                                  name)
+        yield _make_load_test_failure(test_name, exc_info)
 
 def name_from_args(name, index, args):
     summary = ', '.join(repr(arg) for arg in args)
-    return '%s_%s\n%s' % (name, index + 1, summary[:79])
+    return '%s:%s\n%s' % (name, index + 1, summary[:79])
 
 
 class Parameters(Plugin):
                     return method(self, *args)
                 method_name = name_from_args(name, index, args)
                 setattr(klass, method_name, _method)
+
+    def loadTestsFromName(self, event):
+        original_name = name = event.name
+        module = event.module
+        result = testFromName(name, module)
+        if result is None:
+            return
+        parent, obj, name, index = result
+        if (index is None or not isinstance(parent, type) or 
+            not issubclass(parent, TestCase)):
+            # we're only handling TestCase methods here
+            return
+        
+        paramList = getattr(obj, 'paramList', None)
+        if paramList is None:
+            return
+        instance = parent(obj.__name__)
+        method = getattr(instance, obj.__name__)
+        
+        try:
+            args = list(method.paramList)[index-1]
+        except IndexError:
+            raise TestNotFoundError(original_name)
+        def _method(self, method=method, args=args):
+            return method(*args)
+        method_name = name_from_args(name, index-1, args)
+        setattr(parent, method_name, _method)
+        
+        suite = TestSuite()
+        suite.addTest(parent(method_name))
+        event.handled = True
+        return suite
+        

unittest2/test/test_discovery.py

         program.parseArgs(['unittest2', 'discover', '-v'])
         self.assertEqual(program.verbosity, 2)
         self.assertEqual(program.test, 'tests')
-        self.assertEqual(Loader.args, [('.', None, None)])
+        self.assertEqual(Loader.args, [(None, None, None)])
 
         program = makeProgram()
         program.parseArgs(['unittest2', 'discover', '--verbose'])
         self.assertEqual(program.test, 'tests')
-        self.assertEqual(Loader.args, [('.', None, None)])
+        self.assertEqual(Loader.args, [(None, None, None)])
 
         program = makeProgram()
         program.parseArgs(['unittest2'])
         self.assertEqual(program.test, 'tests')
-        self.assertEqual(Loader.args, [('.', None, None)])
+        self.assertEqual(Loader.args, [(None, None, None)])
 
         program = makeProgram()
         program.parseArgs(['unittest2', 'discover', 'fish'])
Tip: Filter by directory path e.g. /media app.js to search for public/media/app.js.
Tip: Use camelCasing e.g. ProjME to search for ProjectModifiedEvent.java.
Tip: Filter by extension type e.g. /repo .js to search for all .js files in the /repo directory.
Tip: Separate your search with spaces e.g. /ssh pom.xml to search for src/ssh/pom.xml.
Tip: Use ↑ and ↓ arrow keys to navigate and return to view the file.
Tip: You can also navigate files with Ctrl+j (next) and Ctrl+k (previous) and view the file with Ctrl+o.
Tip: You can also navigate files with Alt+j (next) and Alt+k (previous) and view the file with Alt+o.