testtools-dev team mailing list archive
-
testtools-dev team
-
Mailing list archive
-
Message #01146
[Merge] lp:~lifeless/testtools/bug-1091512 into lp:testtools
Robert Collins has proposed merging lp:~lifeless/testtools/bug-1091512 into lp:testtools.
Requested reviews:
testtools committers (testtools-committers)
Related bugs:
Bug #1091512 in testtools: "load-list order is arbitrary"
https://bugs.launchpad.net/testtools/+bug/1091512
For more details, see:
https://code.launchpad.net/~lifeless/testtools/bug-1091512/+merge/140369
This works around an annoying defect in the python discover implementation, giving more reliable test ordering for folk using discovery.
I took care to make it possible for things like testresources.OptimisingTestSuite to stay intact, though we still have an issue with --load-list and such suites. (bug 827175)
--
https://code.launchpad.net/~lifeless/testtools/bug-1091512/+merge/140369
Your team testtools developers is subscribed to branch lp:testtools.
=== modified file 'NEWS'
--- NEWS 2012-12-17 01:05:23 +0000
+++ NEWS 2012-12-18 09:13:25 +0000
@@ -6,6 +6,15 @@
NEXT
~~~~
+Changes
+-------
+
+* ``testtools.run discover`` will now sort the tests it discovered. This is a
+ workaround for http://bugs.python.org/issue16709. Non-standard test suites
+ are preserved, and their ``sort_tests()`` method called (if they have such an
+ attribute). ``testtools.testsuite.sorted_tests(suite, True)`` can be used by
+ such suites to do a local sort. (Robert Collins, #1091512)
+
0.9.23
~~~~~~
=== modified file 'doc/for-framework-folk.rst'
--- doc/for-framework-folk.rst 2012-04-11 18:11:03 +0000
+++ doc/for-framework-folk.rst 2012-12-18 09:13:25 +0000
@@ -222,6 +222,17 @@
it down after all of the tests are run. The fixture is *not* made available to
any of the tests.
+sorted_tests
+------------
+
+Given the composite structure of TestSuite / TestCase, sorting tests is
+problematic - you can't tell what functionality is embedded into custom Suite
+implementations. In order to deliver consistent test orders when using test
+discovery (see http://bugs.python.org/issue16709), testtools flattens and
+sorts tests that have the standard TestSuite, defines a new method sort_tests,
+which can be used by non-standard TestSuites to know when they should sort
+their tests.
+
.. _`testtools API docs`: http://mumak.net/testtools/apidocs/
.. _unittest: http://docs.python.org/library/unittest.html
.. _fixture: http://pypi.python.org/pypi/fixtures
=== modified file 'testtools/run.py'
--- testtools/run.py 2012-12-16 08:46:30 +0000
+++ testtools/run.py 2012-12-18 09:13:25 +0000
@@ -14,7 +14,7 @@
from testtools import TextTestResult
from testtools.compat import classtypes, istext, unicode_output_stream
-from testtools.testsuite import iterate_tests
+from testtools.testsuite import iterate_tests, sorted_tests
defaultTestLoader = unittest.defaultTestLoader
@@ -75,6 +75,8 @@
# - --load-list has been added which can reduce the tests used (should be
# upstreamed).
# - The limitation of using getopt is declared to the user.
+# - http://bugs.python.org/issue16709 is worked around, by sorting tests when
+# discover is used.
FAILFAST = " -f, --failfast Stop on first failure\n"
CATCHBREAK = " -c, --catch Catch control-C and display results\n"
@@ -307,7 +309,17 @@
top_level_dir = options.top
loader = Loader()
- self.test = loader.discover(start_dir, pattern, top_level_dir)
+ # See http://bugs.python.org/issue16709
+ # While sorting here is intrusive, its better than being random.
+ # Rules for the sort:
+ # - standard suites are flattened, and the resulting tests sorted by
+ # id.
+ # - non-standard suites are preserved as-is, and sorted into position
+ # by the first test found by iterating the suite.
+ # We do this by a DSU process: flatten and grab a key, sort, strip the
+ # keys.
+ loaded = loader.discover(start_dir, pattern, top_level_dir)
+ self.test = sorted_tests(loaded)
def runTests(self):
if (self.catchbreak
=== modified file 'testtools/tests/test_run.py'
--- testtools/tests/test_run.py 2012-12-17 00:57:44 +0000
+++ testtools/tests/test_run.py 2012-12-18 09:13:25 +0000
@@ -57,6 +57,27 @@
testtools.runexample.TestFoo.test_quux
""", out.getvalue())
+ def test_run_orders_tests(self):
+ self.useFixture(SampleTestFixture())
+ out = StringIO()
+ # We load two tests - one that exists and one that doesn't, and we
+ # should get the one that exists and neither the one that doesn't nor
+ # the unmentioned one that does.
+ tempdir = self.useFixture(fixtures.TempDir())
+ tempname = tempdir.path + '/tests.list'
+ f = open(tempname, 'wb')
+ try:
+ f.write(_b("""
+testtools.runexample.TestFoo.test_bar
+testtools.runexample.missingtest
+"""))
+ finally:
+ f.close()
+ run.main(['prog', '-l', '--load-list', tempname,
+ 'testtools.runexample.test_suite'], out)
+ self.assertEqual("""testtools.runexample.TestFoo.test_bar
+""", out.getvalue())
+
def test_run_load_list(self):
self.useFixture(SampleTestFixture())
out = StringIO()
=== modified file 'testtools/tests/test_testsuite.py'
--- testtools/tests/test_testsuite.py 2012-04-17 14:24:02 +0000
+++ testtools/tests/test_testsuite.py 2012-12-18 09:13:25 +0000
@@ -9,10 +9,11 @@
from testtools import (
ConcurrentTestSuite,
iterate_tests,
+ PlaceHolder,
TestCase,
)
from testtools.helpers import try_import
-from testtools.testsuite import FixtureSuite
+from testtools.testsuite import FixtureSuite, iterate_tests, sorted_tests
from testtools.tests.helpers import LoggingResult
FunctionFixture = try_import('fixtures.FunctionFixture')
@@ -93,6 +94,35 @@
self.assertEqual(['setUp', 1, 2, 'tearDown'], log)
+class TestSortedTests(TestCase):
+
+ def test_sorts_custom_suites(self):
+ a = PlaceHolder('a')
+ b = PlaceHolder('b')
+ class Subclass(unittest.TestSuite):
+ def sort_tests(self):
+ self._tests = sorted_tests(self, True)
+ input_suite = Subclass([b, a])
+ suite = sorted_tests(input_suite)
+ self.assertEqual([a, b], list(iterate_tests(suite)))
+ self.assertEqual([input_suite], list(iter(suite)))
+
+ def test_custom_suite_without_sort_tests_works(self):
+ a = PlaceHolder('a')
+ b = PlaceHolder('b')
+ class Subclass(unittest.TestSuite):pass
+ input_suite = Subclass([b, a])
+ suite = sorted_tests(input_suite)
+ self.assertEqual([b, a], list(iterate_tests(suite)))
+ self.assertEqual([input_suite], list(iter(suite)))
+
+ def test_sorts_simple_suites(self):
+ a = PlaceHolder('a')
+ b = PlaceHolder('b')
+ suite = sorted_tests(unittest.TestSuite([b, a]))
+ self.assertEqual([a, b], list(iterate_tests(suite)))
+
+
def test_suite():
from unittest import TestLoader
return TestLoader().loadTestsFromName(__name__)
=== modified file 'testtools/testsuite.py'
--- testtools/testsuite.py 2012-04-17 14:25:20 +0000
+++ testtools/testsuite.py 2012-12-18 09:13:25 +0000
@@ -6,9 +6,10 @@
__all__ = [
'ConcurrentTestSuite',
'iterate_tests',
+ 'sorted_tests',
]
-from testtools.helpers import try_imports
+from testtools.helpers import safe_hasattr, try_imports
Queue = try_imports(['Queue.Queue', 'queue.Queue'])
@@ -114,3 +115,40 @@
super(FixtureSuite, self).run(result)
finally:
self._fixture.cleanUp()
+
+ def sort_tests(self):
+ self._tests = sorted_tests(self, True)
+
+
+def _flatten_tests(suite_or_case, unpack_outer=False):
+ try:
+ tests = iter(suite_or_case)
+ except TypeError:
+ # Not iterable, assume its a test case.
+ return [(suite_or_case.id(), suite_or_case)]
+ if (type(suite_or_case) in (unittest.TestSuite,) or
+ unpack_outer):
+ # Plain old test suite (or any others we may add).
+ result = []
+ for test in tests:
+ # Recurse to flatten.
+ result.extend(_flatten_tests(test))
+ return result
+ else:
+ # Find any old actual test and grab its id.
+ suite_id = None
+ tests = iterate_tests(suite_or_case)
+ for test in tests:
+ suite_id = test.id()
+ break
+ # If it has a sort_tests method, call that.
+ if safe_hasattr(suite_or_case, 'sort_tests'):
+ suite_or_case.sort_tests()
+ return [(suite_id, suite_or_case)]
+
+
+def sorted_tests(suite_or_case, unpack_outer=False):
+ """Sort suite_or_case while preserving non-vanilla TestSuites."""
+ tests = _flatten_tests(suite_or_case, unpack_outer=unpack_outer)
+ tests.sort()
+ return unittest.TestSuite([test for (sort_key, test) in tests])
Follow ups