diff --git a/NEWS b/NEWS index 80cba69..4f99759 100644 --- a/NEWS +++ b/NEWS @@ -51,6 +51,11 @@ Improvements separate consumer will be created to support that. (Robert Collins) +* New support class ``StreamToExtendedDecorator`` which translates + ``StreamResult`` API calls into ``ExtendedTestResult`` (or any older + ``TestResult``) calls. This permits using un-migrated result objects with + new runners / tests. (Robert Collins) + * New ``TestCase`` decorator ``DecorateTestCaseResult`` that adapts the ``TestResult`` or ``StreamResult`` a case will be run with, for ensuring that a particular result object is used even if the runner running the test doesn't diff --git a/doc/for-framework-folk.rst b/doc/for-framework-folk.rst index beb9b01..5c9ffe8 100644 --- a/doc/for-framework-folk.rst +++ b/doc/for-framework-folk.rst @@ -213,6 +213,14 @@ This is useful when a ``StreamResult`` stream is desired, but you cannot be sure that the tests which will run have been updated to the ``StreamResult`` API. +StreamToExtendedDecorator +------------------------- + +This is a simple converter that emits the ``ExtendedTestResult`` API in +response to events from the ``StreamResult`` API. Useful when outputting +``StreamResult`` events from a ``TestCase`` but the supplied ``TestResult`` +does not support the ``status`` and ``file`` methods. + ThreadsafeStreamResult ---------------------- diff --git a/testtools/__init__.py b/testtools/__init__.py index 4d91c24..2bde458 100644 --- a/testtools/__init__.py +++ b/testtools/__init__.py @@ -32,6 +32,7 @@ __all__ = [ 'StreamResult', 'StreamSummary', 'StreamToDict', + 'StreamToExtendedDecorator', 'TestControl', 'ThreadsafeForwardingResult', 'try_import', @@ -81,6 +82,7 @@ else: StreamResult, StreamSummary, StreamToDict, + StreamToExtendedDecorator, Tagger, TestByTestResult, TestControl, diff --git a/testtools/testresult/__init__.py b/testtools/testresult/__init__.py index b0f22d6..404aa3c 100644 --- a/testtools/testresult/__init__.py +++ b/testtools/testresult/__init__.py @@ -11,6 +11,7 @@ __all__ = [ 'StreamResult', 'StreamSummary', 'StreamToDict', + 'StreamToExtendedDecorator', 'Tagger', 'TestByTestResult', 'TestControl', @@ -29,6 +30,7 @@ from testtools.testresult.real import ( StreamResult, StreamSummary, StreamToDict, + StreamToExtendedDecorator, Tagger, TestByTestResult, TestControl, diff --git a/testtools/testresult/real.py b/testtools/testresult/real.py index 7b8fed4..d9369d2 100644 --- a/testtools/testresult/real.py +++ b/testtools/testresult/real.py @@ -11,6 +11,7 @@ __all__ = [ 'StreamResult', 'StreamSummary', 'StreamToDict', + 'StreamToExtendedDecorator', 'Tagger', 'TestControl', 'TestResult', @@ -540,7 +541,8 @@ def test_dict_to_case(test_dict): from testtools.testcase import PlaceHolder outcome = _status_map[test_dict['status']] return PlaceHolder(test_dict['id'], outcome=outcome, - details=test_dict['details'], tags=test_dict['tags']) + details=test_dict['details'], tags=test_dict['tags'], + timestamps=test_dict['timestamps']) class StreamSummary(StreamToDict): @@ -1260,6 +1262,44 @@ class ExtendedToStreamDecorator(CopyStreamResult, StreamSummary, TestControl): return super(ExtendedToStreamDecorator, self).wasSuccessful() +class StreamToExtendedDecorator(StreamResult): + """Convert StreamResult API calls into ExtendedTestResult calls. + + This will buffer all calls for all concurrently active tests, and + then flush each test as they complete. + + Incomplete tests will be flushed as errors when the test run stops. + + Non test file attachments are accumulated into a test called + 'testtools.extradata' flushed at the end of the run. + """ + + def __init__(self, decorated): + # ExtendedToOriginalDecorator takes care of thunking details back to + # exceptions/reasons etc. + self.decorated = ExtendedToOriginalDecorator(decorated) + # StreamToDict buffers and gives us individual tests. + self.hook = StreamToDict(self._handle_tests) + + def status(self, test_id=None, test_status=None, *args, **kwargs): + if test_status == 'exists': + return + self.hook.status( + test_id=test_id, test_status=test_status, *args, **kwargs) + + def startTestRun(self): + self.decorated.startTestRun() + self.hook.startTestRun() + + def stopTestRun(self): + self.hook.stopTestRun() + self.decorated.stopTestRun() + + def _handle_tests(self, test_dict): + case = test_dict_to_case(test_dict) + case.run(self.decorated) + + class TestResultDecorator(object): """General pass-through decorator. diff --git a/testtools/tests/test_testresult.py b/testtools/tests/test_testresult.py index 9716802..c35a231 100644 --- a/testtools/tests/test_testresult.py +++ b/testtools/tests/test_testresult.py @@ -28,6 +28,7 @@ from testtools import ( StreamResult, StreamSummary, StreamToDict, + StreamToExtendedDecorator, Tagger, TestCase, TestControl, @@ -467,6 +468,15 @@ class TestTestResultDecoratorContract(TestCase, StartTestRunContract): return TestResultDecorator(TestResult()) +# DetailsContract because ExtendedToStreamDecorator follows Python for +# uxsuccess handling. +class TestStreamToExtendedContract(TestCase, DetailsContract): + + def makeResult(self): + return ExtendedToStreamDecorator( + StreamToExtendedDecorator(ExtendedTestResult())) + + class TestStreamResultContract(object): def _make_result(self): @@ -559,6 +569,12 @@ class TestStreamToDictContract(TestCase, TestStreamResultContract): return StreamToDict(lambda x:None) +class TestStreamToExtendedDecoratorContract(TestCase, TestStreamResultContract): + + def _make_result(self): + return StreamToExtendedDecorator(ExtendedTestResult()) + + class TestStreamFailFastContract(TestCase, TestStreamResultContract): def _make_result(self):