Commits

Michael Foord committed 520b3c2

Support more common package layouts for test discovery.

Comments (0)

Files changed (2)

 
 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

unittest2/loader.py

         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 src_dir is not None:
+            self._top_level_dir = src_dir
+            tests.extend(list(self._find_tests(src_dir, pattern)))
+        if lib_dir is not None:
+            self._top_level_dir = lib_dir
+            tests.extend(list(self._find_tests(lib_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):