From 01d4a9b59ecb93701838b8df5ee8fbbd3d22b0f4 Mon Sep 17 00:00:00 2001 From: Free Ekanayaka Date: Tue, 11 Apr 2017 15:13:10 +0200 Subject: [PATCH] Add ResourcedToStreamDecorator test result decorator for testresources integration (#243) This new decorator implements the TestResult protocol extension supported by test resources. For example, tt makes it possible to easily have resource-related events streamed to subunit. --- NEWS | 6 +++ setup.cfg | 1 + testtools/__init__.py | 2 + testtools/testresult/__init__.py | 2 + testtools/testresult/doubles.py | 21 +++++--- testtools/testresult/real.py | 53 ++++++++++++++++++++ testtools/tests/test_testresult.py | 77 +++++++++++++++++++++++++++++- 7 files changed, 154 insertions(+), 8 deletions(-) diff --git a/NEWS b/NEWS index e459a98a..07c6fefd 100644 --- a/NEWS +++ b/NEWS @@ -6,6 +6,12 @@ Changes and improvements to testtools_, grouped by release. NEXT ~~~~ +Improvements +------------ + +* New ``ResourcedToStreamDecorator`` for tracking lifecycle events of + test resources, and possibly integrate with subunit. + 2.2.0 ~~~~~ diff --git a/setup.cfg b/setup.cfg index c1d6b519..2ad0da56 100644 --- a/setup.cfg +++ b/setup.cfg @@ -12,6 +12,7 @@ classifier = [extras] test = testscenarios + testresources unittest2>=1.1.0 [files] diff --git a/testtools/__init__.py b/testtools/__init__.py index d6c15d2a..5d6193a9 100644 --- a/testtools/__init__.py +++ b/testtools/__init__.py @@ -18,6 +18,7 @@ 'MultiTestResult', 'PlaceHolder', 'run_test_with', + 'ResourcedToStreamDecorator', 'Tagger', 'TestCase', 'TestCommand', @@ -85,6 +86,7 @@ ExtendedToOriginalDecorator, ExtendedToStreamDecorator, MultiTestResult, + ResourcedToStreamDecorator, StreamFailFast, StreamResult, StreamResultRouter, diff --git a/testtools/testresult/__init__.py b/testtools/testresult/__init__.py index 5bf8f9c6..83d8cc7d 100644 --- a/testtools/testresult/__init__.py +++ b/testtools/testresult/__init__.py @@ -7,6 +7,7 @@ 'ExtendedToOriginalDecorator', 'ExtendedToStreamDecorator', 'MultiTestResult', + 'ResourcedToStreamDecorator', 'StreamFailFast', 'StreamResult', 'StreamResultRouter', @@ -30,6 +31,7 @@ ExtendedToOriginalDecorator, ExtendedToStreamDecorator, MultiTestResult, + ResourcedToStreamDecorator, StreamFailFast, StreamResult, StreamResultRouter, diff --git a/testtools/testresult/doubles.py b/testtools/testresult/doubles.py index 3f1bf53c..e37a5d4a 100644 --- a/testtools/testresult/doubles.py +++ b/testtools/testresult/doubles.py @@ -2,6 +2,10 @@ """Doubles of test result objects, useful for testing unittest code.""" +from collections import namedtuple + +from testtools.tags import TagContext + __all__ = [ 'Python26TestResult', 'Python27TestResult', @@ -11,9 +15,6 @@ ] -from testtools.tags import TagContext - - class LoggingBase(object): """Basic support for logging of results.""" @@ -219,6 +220,14 @@ def status(self, test_id=None, test_status=None, test_tags=None, runnable=True, file_name=None, file_bytes=None, eof=False, mime_type=None, route_code=None, timestamp=None): self._events.append( - ('status', test_id, test_status, test_tags, - runnable, file_name, file_bytes, eof, mime_type, route_code, - timestamp)) + _StatusEvent( + 'status', test_id, test_status, test_tags, runnable, + file_name, file_bytes, eof, mime_type, route_code, + timestamp)) + + +# Convenience for easier access to status fields +_StatusEvent = namedtuple( + "_Event", [ + "name", "test_id", "test_status", "test_tags", "runnable", "file_name", + "file_bytes", "eof", "mime_type", "route_code", "timestamp"]) diff --git a/testtools/testresult/real.py b/testtools/testresult/real.py index 2c864c54..8c5aefca 100644 --- a/testtools/testresult/real.py +++ b/testtools/testresult/real.py @@ -6,6 +6,7 @@ 'ExtendedToOriginalDecorator', 'ExtendedToStreamDecorator', 'MultiTestResult', + 'ResourcedToStreamDecorator', 'StreamFailFast', 'StreamResult', 'StreamSummary', @@ -1688,6 +1689,58 @@ def wasSuccessful(self): return super(ExtendedToStreamDecorator, self).wasSuccessful() +class ResourcedToStreamDecorator(ExtendedToStreamDecorator): + """Report ``testresources``-related activity to StreamResult objects. + + Implement the resource lifecycle TestResult protocol extension supported + by the ``testresources.TestResourceManager`` class. At each stage of a + resource's lifecycle, a stream event with relevant details will be + emitted. + + Each stream event will have its test_id field set to the resource manager's + identifier (see ``testresources.TestResourceManager.id()``) plus the method + being executed (either 'make' or 'clean'). + + The test_status will be either 'inprogress' or 'success'. + + The runnable flag will be set to False. + """ + + def startMakeResource(self, resource): + self._convertResourceLifecycle(resource, 'make', 'start') + + def stopMakeResource(self, resource): + self._convertResourceLifecycle(resource, 'make', 'stop') + + def startCleanResource(self, resource): + self._convertResourceLifecycle(resource, 'clean', 'start') + + def stopCleanResource(self, resource): + self._convertResourceLifecycle(resource, 'clean', 'stop') + + def _convertResourceLifecycle(self, resource, method, phase): + """Convert a resource lifecycle report to a stream event.""" + + # If the resource implements the TestResourceManager.id() API, let's + # use it, otherwise fallback to the class name. + if safe_hasattr(resource, "id"): + resource_id = resource.id() + else: + resource_id = "%s.%s" % ( + resource.__class__.__module__, resource.__class__.__name__) + + test_id = '%s.%s' % (resource_id, method) + + if phase == 'start': + test_status = 'inprogress' + else: + test_status = 'success' + + self.status( + test_id=test_id, test_status=test_status, runnable=False, + timestamp=self._now()) + + class StreamToExtendedDecorator(StreamResult): """Convert StreamResult API calls into ExtendedTestResult calls. diff --git a/testtools/tests/test_testresult.py b/testtools/tests/test_testresult.py index 7357fa2a..7b0b212d 100644 --- a/testtools/tests/test_testresult.py +++ b/testtools/tests/test_testresult.py @@ -15,17 +15,19 @@ import tempfile import threading from unittest import TestSuite - -from extras import safe_hasattr, try_imports +from extras import safe_hasattr, try_imports, try_import Queue = try_imports(['Queue.Queue', 'queue.Queue']) +testresources = try_import('testresources') + from testtools import ( CopyStreamResult, ExtendedToOriginalDecorator, ExtendedToStreamDecorator, MultiTestResult, PlaceHolder, + ResourcedToStreamDecorator, StreamFailFast, StreamResult, StreamResultRouter, @@ -590,6 +592,12 @@ def _make_result(self): return ExtendedToStreamDecorator(StreamResult()) +class TestResourcedToStreamDecoratorContract(TestCase, TestStreamResultContract): + + def _make_result(self): + return ResourcedToStreamDecorator(StreamResult()) + + class TestStreamSummaryResultContract(TestCase, TestStreamResultContract): def _make_result(self): @@ -932,6 +940,71 @@ def test_empty_detail_status_correct(self): ('stopTestRun',)], log._events) +class TestResourcedToStreamDecorator(TestCase): + + def setUp(self): + super(TestResourcedToStreamDecorator, self).setUp() + if testresources is None: + self.skipTest('Need testresources') + + def test_startMakeResource(self): + log = LoggingStreamResult() + result = ResourcedToStreamDecorator(log) + timestamp = datetime.datetime.utcfromtimestamp(3.476) + result.startTestRun() + result.time(timestamp) + resource = testresources.TestResourceManager() + result.startMakeResource(resource) + [_, event] = log._events + self.assertEqual( + 'testresources.TestResourceManager.make', event.test_id) + self.assertEqual('inprogress', event.test_status) + self.assertFalse(event.runnable) + self.assertEqual(timestamp, event.timestamp) + + def test_startMakeResource_with_custom_id_method(self): + log = LoggingStreamResult() + result = ResourcedToStreamDecorator(log) + resource = testresources.TestResourceManager() + resource.id = lambda: 'nice.resource' + result.startTestRun() + result.startMakeResource(resource) + self.assertEqual('nice.resource.make', log._events[1].test_id) + + def test_stopMakeResource(self): + log = LoggingStreamResult() + result = ResourcedToStreamDecorator(log) + resource = testresources.TestResourceManager() + result.startTestRun() + result.stopMakeResource(resource) + [_, event] = log._events + self.assertEqual( + 'testresources.TestResourceManager.make', event.test_id) + self.assertEqual('success', event.test_status) + + def test_startCleanResource(self): + log = LoggingStreamResult() + result = ResourcedToStreamDecorator(log) + resource = testresources.TestResourceManager() + result.startTestRun() + result.startCleanResource(resource) + [_, event] = log._events + self.assertEqual( + 'testresources.TestResourceManager.clean', event.test_id) + self.assertEqual('inprogress', event.test_status) + + def test_stopCleanResource(self): + log = LoggingStreamResult() + result = ResourcedToStreamDecorator(log) + resource = testresources.TestResourceManager() + result.startTestRun() + result.stopCleanResource(resource) + [_, event] = log._events + self.assertEqual( + 'testresources.TestResourceManager.clean', event.test_id) + self.assertEqual('success', event.test_status) + + class TestStreamFailFast(TestCase): def test_inprogress(self):