testtools-dev team mailing list archive
-
testtools-dev team
-
Mailing list archive
-
Message #01163
[Merge] lp:~lifeless/testtools/preserve-suites-load-list into lp:testtools
Robert Collins has proposed merging lp:~lifeless/testtools/preserve-suites-load-list into lp:testtools.
Requested reviews:
testtools committers (testtools-committers)
For more details, see:
https://code.launchpad.net/~lifeless/testtools/preserve-suites-load-list/+merge/144011
Foinally.
--
https://code.launchpad.net/~lifeless/testtools/preserve-suites-load-list/+merge/144011
Your team testtools developers is subscribed to branch lp:testtools.
=== modified file 'NEWS'
--- NEWS 2013-01-18 11:24:57 +0000
+++ NEWS 2013-01-20 07:34:21 +0000
@@ -9,6 +9,11 @@
Changes
-------
+* ``python -m testtools.run --load-list`` will now preserve any custom suites
+ (such as ``testtools.FixtureSuite`` or ``testresources.OptimisingTestSuite``)
+ rather than flattening them.
+ (Robert Collins, #827175)
+
* Testtools now depends on extras, a small library split out from it to contain
generally useful non-testing facilities. Since extras has been around for a
couple of testtools releases now, we're making this into a hard dependency of
=== modified file 'doc/for-framework-folk.rst'
--- doc/for-framework-folk.rst 2012-12-18 09:03:19 +0000
+++ doc/for-framework-folk.rst 2013-01-20 07:34:21 +0000
@@ -21,6 +21,10 @@
Extensions to TestCase
======================
+In addition to the ``TestCase`` specific methods, we have extensions for
+``TestSuite`` that also apply to ``TestCase`` (because ``TestCase`` and
+``TestSuite`` follow the Composite pattern).
+
Custom exception handling
-------------------------
@@ -220,7 +224,10 @@
A test suite that sets up a fixture_ before running any tests, and then tears
it down after all of the tests are run. The fixture is *not* made available to
-any of the tests.
+any of the tests due to there being no standard channel for suites to pass
+information to the tests they contain (and we don't have enough data on what
+such a channel would need to achieve to design a good one yet - or even decide
+if it is a good idea).
sorted_tests
------------
@@ -229,9 +236,21 @@
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.
+sorts tests that have the standard TestSuite, and defines a new method
+sort_tests, which can be used by non-standard TestSuites to know when they
+should sort their tests. An example implementation can be seen at
+``FixtureSuite.sorted_tests``.
+
+filter_by_ids
+-------------
+
+Simiarly to ``sorted_tests`` running a subset of tests is problematic - the
+standard run interface provides no way to limit what runs. Rather than
+confounding the two problems (selection and execution) we defined a method
+that filters the tests in a suite (or a case) by their unique test id.
+If you a writing custom wrapping suites, consider implementing filter_by_ids
+to support this (though most wrappers that subclass ``unittest.TestSuite`` will
+work just fine [see ``testtools.testsuite.filter_by_ids`` for details.]
.. _`testtools API docs`: http://mumak.net/testtools/apidocs/
.. _unittest: http://docs.python.org/library/unittest.html
=== modified file 'testtools/run.py'
--- testtools/run.py 2012-12-18 09:03:19 +0000
+++ testtools/run.py 2013-01-20 07:34:21 +0000
@@ -14,7 +14,7 @@
from testtools import TextTestResult
from testtools.compat import classtypes, istext, unicode_output_stream
-from testtools.testsuite import iterate_tests, sorted_tests
+from testtools.testsuite import filter_by_ids, iterate_tests, sorted_tests
defaultTestLoader = unittest.defaultTestLoader
@@ -173,11 +173,7 @@
finally:
source.close()
test_ids = set(line.strip().decode('utf-8') for line in lines)
- filtered = unittest.TestSuite()
- for test in iterate_tests(self.test):
- if test.id() in test_ids:
- filtered.addTest(test)
- self.test = filtered
+ self.test = filter_by_ids(self.test, test_ids)
if not self.listtests:
self.runTests()
else:
=== modified file 'testtools/tests/test_run.py'
--- testtools/tests/test_run.py 2013-01-18 09:17:19 +0000
+++ testtools/tests/test_run.py 2013-01-20 07:34:21 +0000
@@ -5,15 +5,15 @@
from unittest import TestSuite
from extras import try_import
+fixtures = try_import('fixtures')
+testresources = try_import('testresources')
+import testtools
+from testtools import TestCase, run
from testtools.compat import (
_b,
StringIO,
)
-fixtures = try_import('fixtures')
-
-import testtools
-from testtools import TestCase, run
from testtools.matchers import Contains
@@ -43,6 +43,50 @@
self.addCleanup(testtools.__path__.remove, self.package.base)
+if fixtures and testresources:
+ class SampleResourcedFixture(fixtures.Fixture):
+ """Creates a test suite that uses testresources."""
+
+ def __init__(self):
+ super(SampleResourcedFixture, self).__init__()
+ self.package = fixtures.PythonPackage(
+ 'resourceexample', [('__init__.py', _b("""
+from fixtures import Fixture
+from testresources import (
+ FixtureResource,
+ OptimisingTestSuite,
+ ResourcedTestCase,
+ )
+from testtools import TestCase
+
+class Printer(Fixture):
+
+ def setUp(self):
+ super(Printer, self).setUp()
+ print('Setting up Printer')
+
+class TestFoo(TestCase, ResourcedTestCase):
+ # When run, this will print just one Setting up Printer, unless the
+ # OptimisingTestSuite is not honoured, when one per test case will print.
+ resources=[('res', FixtureResource(Printer()))]
+ def test_bar(self):
+ pass
+ def test_foo(self):
+ pass
+ def test_quux(self):
+ pass
+def test_suite():
+ from unittest import TestLoader
+ return OptimisingTestSuite(TestLoader().loadTestsFromName(__name__))
+"""))])
+
+ def setUp(self):
+ super(SampleResourcedFixture, self).setUp()
+ self.useFixture(self.package)
+ self.addCleanup(testtools.__path__.remove, self.package.base)
+ testtools.__path__.append(self.package.base)
+
+
class TestRun(TestCase):
def setUp(self):
@@ -100,6 +144,33 @@
self.assertEqual("""testtools.runexample.TestFoo.test_bar
""", out.getvalue())
+ def test_load_list_preserves_custom_suites(self):
+ if testresources is None:
+ self.skipTest("Need testresources")
+ self.useFixture(SampleResourcedFixture())
+ # We load two tests, not loading one. Both share a resource, so we
+ # should see just one resource setup occur.
+ tempdir = self.useFixture(fixtures.TempDir())
+ tempname = tempdir.path + '/tests.list'
+ f = open(tempname, 'wb')
+ try:
+ f.write(_b("""
+testtools.resourceexample.TestFoo.test_bar
+testtools.resourceexample.TestFoo.test_foo
+"""))
+ finally:
+ f.close()
+ stdout = self.useFixture(fixtures.StringStream('stdout'))
+ with fixtures.MonkeyPatch('sys.stdout', stdout.stream):
+ try:
+ run.main(['prog', '--load-list', tempname,
+ 'testtools.resourceexample.test_suite'], stdout.stream)
+ except SystemExit:
+ # Evil resides in TestProgram.
+ pass
+ out = stdout.getDetails()['stdout'].as_text()
+ self.assertEqual(1, out.count('Setting up Printer'), "%r" % out)
+
def test_run_failfast(self):
stdout = self.useFixture(fixtures.StringStream('stdout'))
=== modified file 'testtools/testsuite.py'
--- testtools/testsuite.py 2013-01-18 09:17:19 +0000
+++ testtools/testsuite.py 2013-01-20 07:34:21 +0000
@@ -5,6 +5,7 @@
__metaclass__ = type
__all__ = [
'ConcurrentTestSuite',
+ 'filter_by_ids',
'iterate_tests',
'sorted_tests',
]
@@ -147,6 +148,66 @@
return [(suite_id, suite_or_case)]
+def filter_by_ids(suite_or_case, test_ids):
+ """Remove tests from suite_or_case where their id is not in test_ids.
+
+ :param suite_or_case: A test suite or test case.
+ :param test_ids: Something that supports the __contains__ protocol.
+ :return: suite_or_case, unless suite_or_case was a case that itself
+ fails the predicate when it will return a new unittest.TestSuite with
+ no contents.
+
+ This helper exists to provide backwards compatability with older versions
+ of Python (currently all versions :)) that don't have a native
+ filter_by_ids() method on Test(Case|Suite).
+
+ For subclasses of TestSuite, filtering is done by:
+ - attempting to call suite.filter_by_ids(test_ids)
+ - if there is no method, iterating the suite and identifying tests to
+ remove, then removing them from _tests, manually recursing into
+ each entry.
+
+ For objects with an id() method - TestCases, filtering is done by:
+ - attempting to return case.filter_by_ids(test_ids)
+ - if there is no such method, checking for case.id() in test_ids
+ and returning case if it is, or TestSuite() if it is not.
+
+ For anything else, it is not filtered - it is returned as-is.
+
+ To provide compatability with this routine for a custom TestSuite, just
+ define a filter_by_ids() method that will return a TestSuite equivalent to
+ the original minus any tests not in test_ids.
+ Similarly to provide compatability for a custom TestCase that does
+ something unusual define filter_by_ids to return a new TestCase object
+ that will only run test_ids that are in the provided container. If none
+ would run, return an empty TestSuite().
+
+ The contract for this function does not require mutation - each filtered
+ object can choose to return a new object with the filtered tests. However
+ because existing custom TestSuite classes in the wild do not have this
+ method, we need a way to copy their state correctly which is tricky:
+ thus the backwards-compatible code paths attempt to mutate in place rather
+ than guessing how to reconstruct a new suite.
+ """
+ # Compatible objects
+ if safe_hasattr(suite_or_case, 'filter_by_ids'):
+ return suite_or_case.filter_by_ids(test_ids)
+ # TestCase objects.
+ if safe_hasattr(suite_or_case, 'id'):
+ if suite_or_case.id() in test_ids:
+ return suite_or_case
+ else:
+ return unittest.TestSuite()
+ # Standard TestSuites or derived classes [assumed to be mutable].
+ if isinstance(suite_or_case, unittest.TestSuite):
+ filtered = []
+ for item in suite_or_case:
+ filtered.append(filter_by_ids(item, test_ids))
+ suite_or_case._tests[:] = filtered
+ # Everything else:
+ return 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)
Follow ups