From ad518e2089ddd9051ec3beaeaaf624028efee574 Mon Sep 17 00:00:00 2001 From: Jonathan Lange Date: Mon, 2 Nov 2015 19:14:35 +0000 Subject: [PATCH 01/24] Aspirational documentation --- requirements.txt | 1 + testtools/testresult/real.py | 66 ++++++++++++++++++++++++++++++++++++ 2 files changed, 67 insertions(+) diff --git a/requirements.txt b/requirements.txt index a944636a..92adc5fa 100644 --- a/requirements.txt +++ b/requirements.txt @@ -1,5 +1,6 @@ pbr>=0.11 extras +pyrsistent # 'mimeparse' has not been uploaded by the maintainer with Python3 compat # but someone kindly uploaded a fixed version as 'python-mimeparse'. python-mimeparse diff --git a/testtools/testresult/real.py b/testtools/testresult/real.py index 8773c65f..43edfc07 100644 --- a/testtools/testresult/real.py +++ b/testtools/testresult/real.py @@ -32,6 +32,8 @@ parse_mime_type = try_import('mimeparse.parse_mime_type') Queue = try_imports(['Queue.Queue', 'queue.Queue']) +from pyrsistent import PRecord, field, pmap_field, pset_field + from testtools.compat import str_is_unicode, _u, _b from testtools.content import ( Content, @@ -267,6 +269,44 @@ def done(self): """ +"""Interim states: + +* None - no particular status is being reported, or status being reported is + not associated with a test (e.g. when reporting on stdout / stderr chatter). + +* inprogress - the test is currently running. Emitted by tests when they start + running and at any intermediary point they might choose to indicate their + continual operation. +""" +INTERIM_STATES = frozenset({None, 'inprogress'}) + +"""Final states: + +* exists - the test exists. This is used when a test is not being executed. + Typically this is when querying what tests could be run in a test run (which + is useful for selecting tests to run). + +* xfail - the test failed but that was expected. This is purely informative - + the test is not considered to be a failure. + +* uxsuccess - the test passed but was expected to fail. The test will be + considered a failure. + +* success - the test has finished without error. + +* fail - the test failed (or errored). The test will be considered a failure. + +* skip - the test was selected to run but chose to be skipped. e.g. a test + dependency was missing. This is purely informative: the test is not + considered to be a failure. +""" +FINAL_STATES = frozenset( + {'exists', 'xfail', 'uxsuccess', 'success', 'fail', 'skip'}) + + +STATES = INTERIM_STATES | FINAL_STATES + + class StreamResult(object): """A test result for reporting the activity of a test run. @@ -592,6 +632,32 @@ def status(self, *args, **kwargs): super(StreamTagger, self).status(*args, **kwargs) +class TestRecord(PRecord): + """Representation of a test.""" + + """The test id.""" + id = field(unicode, mandatory=True) + + """Tags for the test.""" + tags = pset_field(unicode, optional=False) + + """File attachments.""" + details = pmap_field(unicode, Content, optional=False) + + """One of the StreamResult status codes.""" + status = field(unicode, mandatory=True, invariant=lambda x: x in STATES) + + """Pair of timestamps (x, y). + + x is the first timestamp we received for this test, y is the one that + triggered the notification. y can be None if the test hanged. + + Timestamps are not compared - their ordering is purely order received in + the stream. + """ + timestamps = field(tuple, mandatory=True) + + class StreamToDict(StreamResult): """A specialised StreamResult that emits a callback as tests complete. From eda634568e3b6c94e0bfea7d9c0f324e96429178 Mon Sep 17 00:00:00 2001 From: Jonathan Lange Date: Mon, 2 Nov 2015 19:15:59 +0000 Subject: [PATCH 02/24] Use name for interim states --- testtools/testresult/real.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/testtools/testresult/real.py b/testtools/testresult/real.py index 43edfc07..7d9348e0 100644 --- a/testtools/testresult/real.py +++ b/testtools/testresult/real.py @@ -731,7 +731,7 @@ def status(self, test_id=None, test_status=None, test_tags=None, if test_tags is not None: self._inprogress[key]['tags'] = test_tags # notify completed tests. - if test_status not in (None, 'inprogress'): + if test_status not in INTERIM_STATES: self.on_test(self._inprogress.pop(key)) def stopTestRun(self): From 37afc36a952e07c9484cd380d1dcdd6597b50716 Mon Sep 17 00:00:00 2001 From: Jonathan Lange Date: Mon, 2 Nov 2015 19:23:47 +0000 Subject: [PATCH 03/24] Use `case` consistently, over `_inprogress[key]` --- testtools/testresult/real.py | 18 +++++++++++++----- 1 file changed, 13 insertions(+), 5 deletions(-) diff --git a/testtools/testresult/real.py b/testtools/testresult/real.py index 7d9348e0..176f0197 100644 --- a/testtools/testresult/real.py +++ b/testtools/testresult/real.py @@ -707,14 +707,16 @@ def status(self, test_id=None, test_status=None, test_tags=None, # update fields if not key: return - if test_status is not None: - self._inprogress[key]['status'] = test_status - self._inprogress[key]['timestamps'][1] = timestamp case = self._inprogress[key] + if test_status is not None: + case['status'] = test_status + case['timestamps'][1] = timestamp if file_name is not None: if file_name not in case['details']: + if mime_type is None: mime_type = 'application/octet-stream' + primary, sub, parameters = parse_mime_type(mime_type) if 'charset' in parameters: if ',' in parameters['charset']: @@ -723,16 +725,22 @@ def status(self, test_id=None, test_status=None, test_tags=None, # this in a few releases. parameters['charset'] = parameters['charset'][ :parameters['charset'].find(',')] + content_type = ContentType(primary, sub, parameters) content_bytes = [] case['details'][file_name] = Content( content_type, lambda: content_bytes) case['details'][file_name].iter_bytes().append(file_bytes) + if test_tags is not None: - self._inprogress[key]['tags'] = test_tags + case['tags'] = test_tags # notify completed tests. if test_status not in INTERIM_STATES: - self.on_test(self._inprogress.pop(key)) + # XXX: This isn't actually desirable end-state code, but I just + # want to verify that we are re-using this correctly. + popped_case = self._inprogress.pop(key) + assert case == popped_case + self.on_test(popped_case) def stopTestRun(self): super(StreamToDict, self).stopTestRun() From 131161fa7a7c348d56aa61614890a2ec4beed7ce Mon Sep 17 00:00:00 2001 From: Jonathan Lange Date: Mon, 2 Nov 2015 19:29:49 +0000 Subject: [PATCH 04/24] Extract _make_content_type --- testtools/testresult/real.py | 36 ++++++++++++++++++++++-------------- 1 file changed, 22 insertions(+), 14 deletions(-) diff --git a/testtools/testresult/real.py b/testtools/testresult/real.py index 176f0197..62b97166 100644 --- a/testtools/testresult/real.py +++ b/testtools/testresult/real.py @@ -713,20 +713,7 @@ def status(self, test_id=None, test_status=None, test_tags=None, case['timestamps'][1] = timestamp if file_name is not None: if file_name not in case['details']: - - if mime_type is None: - mime_type = 'application/octet-stream' - - primary, sub, parameters = parse_mime_type(mime_type) - if 'charset' in parameters: - if ',' in parameters['charset']: - # testtools was emitting a bad encoding, workaround it, - # Though this does lose data - probably want to drop - # this in a few releases. - parameters['charset'] = parameters['charset'][ - :parameters['charset'].find(',')] - - content_type = ContentType(primary, sub, parameters) + content_type = _make_content_type(mime_type) content_bytes = [] case['details'][file_name] = Content( content_type, lambda: content_bytes) @@ -763,6 +750,27 @@ def _ensure_key(self, test_id, route_code, timestamp): return key +def _make_content_type(mime_type=None): + """Return ContentType for a given mime type. + + testtools was emitting a bad encoding, and this works around it. + Unfortunately, is also loses data - probably want to drop this in a few + releases. + """ + # XXX: Not sure what release this was added, so "in a few releases" is + # unactionable. + if mime_type is None: + mime_type = 'application/octet-stream' + + primary, sub, parameters = parse_mime_type(mime_type) + if 'charset' in parameters: + if ',' in parameters['charset']: + parameters['charset'] = parameters['charset'][ + :parameters['charset'].find(',')] + + return ContentType(primary, sub, parameters) + + _status_map = { 'inprogress': 'addFailure', 'unknown': 'addFailure', From c20de458a10d3aaa586edbf302c06d057d1a06a5 Mon Sep 17 00:00:00 2001 From: Jonathan Lange Date: Mon, 2 Nov 2015 19:36:45 +0000 Subject: [PATCH 05/24] Extract methods for making and updating record --- testtools/testresult/real.py | 38 ++++++++++++++++++++++++------------ 1 file changed, 25 insertions(+), 13 deletions(-) diff --git a/testtools/testresult/real.py b/testtools/testresult/real.py index 62b97166..b8d57a2a 100644 --- a/testtools/testresult/real.py +++ b/testtools/testresult/real.py @@ -708,6 +708,29 @@ def status(self, test_id=None, test_status=None, test_tags=None, if not key: return case = self._inprogress[key] + self._inprogress[key] = self._update_case( + case, test_status, test_tags, file_name, file_bytes, + mime_type, timestamp) + # notify completed tests. + if test_status not in INTERIM_STATES: + # XXX: This isn't actually desirable end-state code, but I just + # want to verify that we are re-using this correctly. + popped_case = self._inprogress.pop(key) + assert case == popped_case + self.on_test(popped_case) + + def _make_case(self, test_id, timestamp): + return { + 'id': test_id, + 'tags': set(), + 'details': {}, + 'status': 'unknown', + 'timestamps': [timestamp, None], + } + + def _update_case(self, case, test_status=None, test_tags=None, + file_name=None, file_bytes=None, mime_type=None, + timestamp=None): if test_status is not None: case['status'] = test_status case['timestamps'][1] = timestamp @@ -721,13 +744,7 @@ def status(self, test_id=None, test_status=None, test_tags=None, if test_tags is not None: case['tags'] = test_tags - # notify completed tests. - if test_status not in INTERIM_STATES: - # XXX: This isn't actually desirable end-state code, but I just - # want to verify that we are re-using this correctly. - popped_case = self._inprogress.pop(key) - assert case == popped_case - self.on_test(popped_case) + return case def stopTestRun(self): super(StreamToDict, self).stopTestRun() @@ -741,12 +758,7 @@ def _ensure_key(self, test_id, route_code, timestamp): return key = (test_id, route_code) if key not in self._inprogress: - self._inprogress[key] = { - 'id': test_id, - 'tags': set(), - 'details': {}, - 'status': 'unknown', - 'timestamps': [timestamp, None]} + self._inprogress[key] = self._make_case(test_id, timestamp) return key From f88d76a4a2c6c01d8063db46e04a2df5c8d3f7e3 Mon Sep 17 00:00:00 2001 From: Jonathan Lange Date: Mon, 2 Nov 2015 19:49:49 +0000 Subject: [PATCH 06/24] Actually use TestRecord internally --- testtools/testresult/real.py | 84 +++++++++++++++++++++++++----------- 1 file changed, 59 insertions(+), 25 deletions(-) diff --git a/testtools/testresult/real.py b/testtools/testresult/real.py index b8d57a2a..ac45b195 100644 --- a/testtools/testresult/real.py +++ b/testtools/testresult/real.py @@ -32,7 +32,7 @@ parse_mime_type = try_import('mimeparse.parse_mime_type') Queue = try_imports(['Queue.Queue', 'queue.Queue']) -from pyrsistent import PRecord, field, pmap_field, pset_field +from pyrsistent import PRecord, field, pmap_field, pset_field, pmap, pset, thaw from testtools.compat import str_is_unicode, _u, _b from testtools.content import ( @@ -299,9 +299,11 @@ def done(self): * skip - the test was selected to run but chose to be skipped. e.g. a test dependency was missing. This is purely informative: the test is not considered to be a failure. + +* unknown - we don't know what state the test is in """ FINAL_STATES = frozenset( - {'exists', 'xfail', 'uxsuccess', 'success', 'fail', 'skip'}) + {'exists', 'xfail', 'uxsuccess', 'success', 'fail', 'skip', 'unknown'}) STATES = INTERIM_STATES | FINAL_STATES @@ -636,16 +638,19 @@ class TestRecord(PRecord): """Representation of a test.""" """The test id.""" - id = field(unicode, mandatory=True) + id = field((str, unicode), mandatory=True) """Tags for the test.""" - tags = pset_field(unicode, optional=False) + tags = pset_field((str, unicode), optional=False) """File attachments.""" - details = pmap_field(unicode, Content, optional=False) + # XXX: Documentation says these are unicode, but tests pass in str. + details = pmap_field((str, unicode), Content, optional=False) """One of the StreamResult status codes.""" - status = field(unicode, mandatory=True, invariant=lambda x: x in STATES) + status = field( + (str, unicode), mandatory=True, + invariant=lambda x: (x in STATES, 'Invalid state')) """Pair of timestamps (x, y). @@ -657,6 +662,31 @@ class TestRecord(PRecord): """ timestamps = field(tuple, mandatory=True) + def to_dict(self): + """Convert record into a "test dict". + + A "test dict" is a concept used in other parts of the code-base. It + has the following keys: + + * id: the test id. + * tags: The tags for the test. A set of unicode strings. + * details: A dict of file attachments - ``testtools.content.Content`` + objects. + * status: One of the StreamResult status codes (including inprogress) or + 'unknown' (used if only file events for a test were received...) + * timestamps: A pair of timestamps - the first one received with this + test id, and the one in the event that triggered the notification. + Hung tests have a None for the second end event. Timestamps are not + compared - their ordering is purely order received in the stream. + """ + return { + 'id': self.id, + 'tags': thaw(self.tags), + 'details': thaw(self.details), + 'status': self.status, + 'timestamps': list(self.timestamps), + } + class StreamToDict(StreamResult): """A specialised StreamResult that emits a callback as tests complete. @@ -715,43 +745,47 @@ def status(self, test_id=None, test_status=None, test_tags=None, if test_status not in INTERIM_STATES: # XXX: This isn't actually desirable end-state code, but I just # want to verify that we are re-using this correctly. - popped_case = self._inprogress.pop(key) - assert case == popped_case - self.on_test(popped_case) + case = self._inprogress.pop(key) + self.on_test(case.to_dict()) def _make_case(self, test_id, timestamp): - return { - 'id': test_id, - 'tags': set(), - 'details': {}, - 'status': 'unknown', - 'timestamps': [timestamp, None], - } + return TestRecord( + id=test_id, + tags=pset(), + details=pmap(), + status='unknown', + timestamps=(timestamp, None), + ) def _update_case(self, case, test_status=None, test_tags=None, file_name=None, file_bytes=None, mime_type=None, timestamp=None): if test_status is not None: - case['status'] = test_status - case['timestamps'][1] = timestamp + case = case.set(status=test_status) + + case = case.set(timestamps=(case.timestamps[0], timestamp)) + if file_name is not None: - if file_name not in case['details']: + if file_name not in case.details: content_type = _make_content_type(mime_type) content_bytes = [] - case['details'][file_name] = Content( - content_type, lambda: content_bytes) - case['details'][file_name].iter_bytes().append(file_bytes) + case = case.transform( + ['details', file_name], + Content(content_type, lambda: content_bytes)) + + case.details[file_name].iter_bytes().append(file_bytes) if test_tags is not None: - case['tags'] = test_tags + case = case.set('tags', test_tags) + return case def stopTestRun(self): super(StreamToDict, self).stopTestRun() while self._inprogress: case = self._inprogress.popitem()[1] - case['timestamps'][1] = None - self.on_test(case) + case = case.set(timestamps=(case.timestamps[0], None)) + self.on_test(case.to_dict()) def _ensure_key(self, test_id, route_code, timestamp): if test_id is None: From 4b8a8459076efacecc4169f1680826d4e6e969f6 Mon Sep 17 00:00:00 2001 From: Jonathan Lange Date: Mon, 2 Nov 2015 19:51:10 +0000 Subject: [PATCH 07/24] Move creation logic to TestRecord itself --- testtools/testresult/real.py | 21 +++++++++++---------- 1 file changed, 11 insertions(+), 10 deletions(-) diff --git a/testtools/testresult/real.py b/testtools/testresult/real.py index ac45b195..26a1fd63 100644 --- a/testtools/testresult/real.py +++ b/testtools/testresult/real.py @@ -662,6 +662,16 @@ class TestRecord(PRecord): """ timestamps = field(tuple, mandatory=True) + @classmethod + def create(cls, test_id, timestamp): + return cls( + id=test_id, + tags=pset(), + details=pmap(), + status='unknown', + timestamps=(timestamp, None), + ) + def to_dict(self): """Convert record into a "test dict". @@ -748,15 +758,6 @@ def status(self, test_id=None, test_status=None, test_tags=None, case = self._inprogress.pop(key) self.on_test(case.to_dict()) - def _make_case(self, test_id, timestamp): - return TestRecord( - id=test_id, - tags=pset(), - details=pmap(), - status='unknown', - timestamps=(timestamp, None), - ) - def _update_case(self, case, test_status=None, test_tags=None, file_name=None, file_bytes=None, mime_type=None, timestamp=None): @@ -792,7 +793,7 @@ def _ensure_key(self, test_id, route_code, timestamp): return key = (test_id, route_code) if key not in self._inprogress: - self._inprogress[key] = self._make_case(test_id, timestamp) + self._inprogress[key] = TestRecord.create(test_id, timestamp) return key From 37040d577d4b011e4f79d2afe663889ff435e1c4 Mon Sep 17 00:00:00 2001 From: Jonathan Lange Date: Mon, 2 Nov 2015 22:15:28 +0000 Subject: [PATCH 08/24] Extract got_timestamp function --- testtools/testresult/real.py | 16 ++++++++++------ 1 file changed, 10 insertions(+), 6 deletions(-) diff --git a/testtools/testresult/real.py b/testtools/testresult/real.py index 26a1fd63..70800ddc 100644 --- a/testtools/testresult/real.py +++ b/testtools/testresult/real.py @@ -656,9 +656,6 @@ class TestRecord(PRecord): x is the first timestamp we received for this test, y is the one that triggered the notification. y can be None if the test hanged. - - Timestamps are not compared - their ordering is purely order received in - the stream. """ timestamps = field(tuple, mandatory=True) @@ -697,6 +694,14 @@ def to_dict(self): 'timestamps': list(self.timestamps), } + def got_timestamp(self, timestamp): + """Called when we receive a timestamp. + + This will always update the second element of the 'timestamps' tuple. + It doesn't compare timestamps at all. + """ + return self.set(timestamps=(self.timestamps[0], timestamp)) + class StreamToDict(StreamResult): """A specialised StreamResult that emits a callback as tests complete. @@ -764,7 +769,7 @@ def _update_case(self, case, test_status=None, test_tags=None, if test_status is not None: case = case.set(status=test_status) - case = case.set(timestamps=(case.timestamps[0], timestamp)) + case = case.got_timestamp(timestamp) if file_name is not None: if file_name not in case.details: @@ -785,8 +790,7 @@ def stopTestRun(self): super(StreamToDict, self).stopTestRun() while self._inprogress: case = self._inprogress.popitem()[1] - case = case.set(timestamps=(case.timestamps[0], None)) - self.on_test(case.to_dict()) + self.on_test(case.got_timestamp(None).to_dict()) def _ensure_key(self, test_id, route_code, timestamp): if test_id is None: From 291e02cd09db3afe2860c01a35699f2463dd8f46 Mon Sep 17 00:00:00 2001 From: Jonathan Lange Date: Mon, 2 Nov 2015 19:59:04 +0000 Subject: [PATCH 09/24] Extract got_file --- testtools/testresult/real.py | 27 +++++++++++++++++++-------- 1 file changed, 19 insertions(+), 8 deletions(-) diff --git a/testtools/testresult/real.py b/testtools/testresult/real.py index 70800ddc..e2963aed 100644 --- a/testtools/testresult/real.py +++ b/testtools/testresult/real.py @@ -702,6 +702,24 @@ def got_timestamp(self, timestamp): """ return self.set(timestamps=(self.timestamps[0], timestamp)) + def got_file(self, file_name, file_bytes, mime_type=None): + """Called when we receive file information. + + ``mime_type`` is only used when this is the first time we've seen data + from this file. + """ + if file_name in self.details: + case = self + else: + content_type = _make_content_type(mime_type) + content_bytes = [] + case = self.transform( + ['details', file_name], + Content(content_type, lambda: content_bytes)) + + case.details[file_name].iter_bytes().append(file_bytes) + return case + class StreamToDict(StreamResult): """A specialised StreamResult that emits a callback as tests complete. @@ -772,14 +790,7 @@ def _update_case(self, case, test_status=None, test_tags=None, case = case.got_timestamp(timestamp) if file_name is not None: - if file_name not in case.details: - content_type = _make_content_type(mime_type) - content_bytes = [] - case = case.transform( - ['details', file_name], - Content(content_type, lambda: content_bytes)) - - case.details[file_name].iter_bytes().append(file_bytes) + case = case.got_file(file_name, file_bytes, mime_type) if test_tags is not None: case = case.set('tags', test_tags) From 528d86c2e4ca4653b9239b3f006a210e554583a7 Mon Sep 17 00:00:00 2001 From: Jonathan Lange Date: Mon, 2 Nov 2015 20:01:30 +0000 Subject: [PATCH 10/24] Remove temporary variable 'case' --- testtools/testresult/real.py | 15 +++++++-------- 1 file changed, 7 insertions(+), 8 deletions(-) diff --git a/testtools/testresult/real.py b/testtools/testresult/real.py index e2963aed..f82667a1 100644 --- a/testtools/testresult/real.py +++ b/testtools/testresult/real.py @@ -766,20 +766,19 @@ def status(self, test_id=None, test_status=None, test_tags=None, test_tags=test_tags, runnable=runnable, file_name=file_name, file_bytes=file_bytes, eof=eof, mime_type=mime_type, route_code=route_code, timestamp=timestamp) + key = self._ensure_key(test_id, route_code, timestamp) - # update fields if not key: return - case = self._inprogress[key] + + # update fields self._inprogress[key] = self._update_case( - case, test_status, test_tags, file_name, file_bytes, - mime_type, timestamp) + self._inprogress[key], test_status, test_tags, file_name, + file_bytes, mime_type, timestamp) + # notify completed tests. if test_status not in INTERIM_STATES: - # XXX: This isn't actually desirable end-state code, but I just - # want to verify that we are re-using this correctly. - case = self._inprogress.pop(key) - self.on_test(case.to_dict()) + self.on_test(self._inprogress.pop(key).to_dict()) def _update_case(self, case, test_status=None, test_tags=None, file_name=None, file_bytes=None, mime_type=None, From cb266059b8882207f353a5d5899c661a2c1e86a2 Mon Sep 17 00:00:00 2001 From: Jonathan Lange Date: Mon, 2 Nov 2015 20:03:49 +0000 Subject: [PATCH 11/24] Move test_dict_to_case closer to test_dict definition --- testtools/testresult/real.py | 54 ++++++++++++++++++------------------ 1 file changed, 27 insertions(+), 27 deletions(-) diff --git a/testtools/testresult/real.py b/testtools/testresult/real.py index f82667a1..c33c1c60 100644 --- a/testtools/testresult/real.py +++ b/testtools/testresult/real.py @@ -721,6 +721,33 @@ def got_file(self, file_name, file_bytes, mime_type=None): return case +_status_map = { + 'inprogress': 'addFailure', + 'unknown': 'addFailure', + 'success': 'addSuccess', + 'skip': 'addSkip', + 'fail': 'addFailure', + 'xfail': 'addExpectedFailure', + 'uxsuccess': 'addUnexpectedSuccess', + } + + +def test_dict_to_case(test_dict): + """Convert a test dict into a TestCase object. + + :param test_dict: A test dict as generated by StreamToDict. + :return: A PlaceHolder test object. + """ + # Circular import. + global PlaceHolder + if PlaceHolder is None: + 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'], timestamps=test_dict['timestamps']) + + class StreamToDict(StreamResult): """A specialised StreamResult that emits a callback as tests complete. @@ -832,33 +859,6 @@ def _make_content_type(mime_type=None): return ContentType(primary, sub, parameters) -_status_map = { - 'inprogress': 'addFailure', - 'unknown': 'addFailure', - 'success': 'addSuccess', - 'skip': 'addSkip', - 'fail': 'addFailure', - 'xfail': 'addExpectedFailure', - 'uxsuccess': 'addUnexpectedSuccess', - } - - -def test_dict_to_case(test_dict): - """Convert a test dict into a TestCase object. - - :param test_dict: A test dict as generated by StreamToDict. - :return: A PlaceHolder test object. - """ - # Circular import. - global PlaceHolder - if PlaceHolder is None: - 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'], timestamps=test_dict['timestamps']) - - class StreamSummary(StreamToDict): """A specialised StreamResult that summarises a stream. From 9cd914710bbdacb06af7c21f61066a7ed46507a7 Mon Sep 17 00:00:00 2001 From: Jonathan Lange Date: Mon, 2 Nov 2015 20:04:19 +0000 Subject: [PATCH 12/24] Make _status_map persistent. No need to ever change it. --- testtools/testresult/real.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/testtools/testresult/real.py b/testtools/testresult/real.py index c33c1c60..38aaa2a5 100644 --- a/testtools/testresult/real.py +++ b/testtools/testresult/real.py @@ -721,7 +721,7 @@ def got_file(self, file_name, file_bytes, mime_type=None): return case -_status_map = { +_status_map = pmap({ 'inprogress': 'addFailure', 'unknown': 'addFailure', 'success': 'addSuccess', @@ -729,7 +729,7 @@ def got_file(self, file_name, file_bytes, mime_type=None): 'fail': 'addFailure', 'xfail': 'addExpectedFailure', 'uxsuccess': 'addUnexpectedSuccess', - } +}) def test_dict_to_case(test_dict): From b6c4968d32959bebd59817606b8e6ebd0419d5ee Mon Sep 17 00:00:00 2001 From: Jonathan Lange Date: Mon, 2 Nov 2015 20:13:22 +0000 Subject: [PATCH 13/24] Put to_test_case logic on TestRecord --- testtools/testresult/real.py | 33 +++++++++++++++++++++++++-------- 1 file changed, 25 insertions(+), 8 deletions(-) diff --git a/testtools/testresult/real.py b/testtools/testresult/real.py index 38aaa2a5..9277075d 100644 --- a/testtools/testresult/real.py +++ b/testtools/testresult/real.py @@ -720,6 +720,24 @@ def got_file(self, file_name, file_bytes, mime_type=None): case.details[file_name].iter_bytes().append(file_bytes) return case + def to_test_case(self): + """Convert into a TestCase object. + + :return: A PlaceHolder test object. + """ + # Circular import. + global PlaceHolder + if PlaceHolder is None: + from testtools.testcase import PlaceHolder + outcome = _status_map[self.status] + return PlaceHolder( + self.id, + outcome=outcome, + details=thaw(self.details), + tags=thaw(self.tags), + timestamps=self.timestamps, + ) + _status_map = pmap({ 'inprogress': 'addFailure', @@ -738,14 +756,13 @@ def test_dict_to_case(test_dict): :param test_dict: A test dict as generated by StreamToDict. :return: A PlaceHolder test object. """ - # Circular import. - global PlaceHolder - if PlaceHolder is None: - 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'], timestamps=test_dict['timestamps']) + return TestRecord( + id=test_dict['id'], + tags=test_dict['tags'], + details=test_dict['details'], + status=test_dict['status'], + timestamps=tuple(test_dict['timestamps']), + ).to_test_case() class StreamToDict(StreamResult): From 7d43c55b4e9472841b94df7a445f0f37d377eed5 Mon Sep 17 00:00:00 2001 From: Jonathan Lange Date: Mon, 2 Nov 2015 20:22:36 +0000 Subject: [PATCH 14/24] Move _make_content_type closer to where it is used --- testtools/testresult/real.py | 42 ++++++++++++++++++------------------ 1 file changed, 21 insertions(+), 21 deletions(-) diff --git a/testtools/testresult/real.py b/testtools/testresult/real.py index 9277075d..5258628f 100644 --- a/testtools/testresult/real.py +++ b/testtools/testresult/real.py @@ -739,6 +739,27 @@ def to_test_case(self): ) +def _make_content_type(mime_type=None): + """Return ContentType for a given mime type. + + testtools was emitting a bad encoding, and this works around it. + Unfortunately, is also loses data - probably want to drop this in a few + releases. + """ + # XXX: Not sure what release this was added, so "in a few releases" is + # unactionable. + if mime_type is None: + mime_type = 'application/octet-stream' + + primary, sub, parameters = parse_mime_type(mime_type) + if 'charset' in parameters: + if ',' in parameters['charset']: + parameters['charset'] = parameters['charset'][ + :parameters['charset'].find(',')] + + return ContentType(primary, sub, parameters) + + _status_map = pmap({ 'inprogress': 'addFailure', 'unknown': 'addFailure', @@ -855,27 +876,6 @@ def _ensure_key(self, test_id, route_code, timestamp): return key -def _make_content_type(mime_type=None): - """Return ContentType for a given mime type. - - testtools was emitting a bad encoding, and this works around it. - Unfortunately, is also loses data - probably want to drop this in a few - releases. - """ - # XXX: Not sure what release this was added, so "in a few releases" is - # unactionable. - if mime_type is None: - mime_type = 'application/octet-stream' - - primary, sub, parameters = parse_mime_type(mime_type) - if 'charset' in parameters: - if ',' in parameters['charset']: - parameters['charset'] = parameters['charset'][ - :parameters['charset'].find(',')] - - return ContentType(primary, sub, parameters) - - class StreamSummary(StreamToDict): """A specialised StreamResult that summarises a stream. From 689b04b49ce8458cf5d54e6eb054b41430a59cc4 Mon Sep 17 00:00:00 2001 From: Jonathan Lange Date: Mon, 2 Nov 2015 20:37:55 +0000 Subject: [PATCH 15/24] Extract StreamToTestRecord base class --- testtools/testresult/real.py | 90 +++++++++++++++++++++++++++--------- 1 file changed, 68 insertions(+), 22 deletions(-) diff --git a/testtools/testresult/real.py b/testtools/testresult/real.py index 5258628f..059a124f 100644 --- a/testtools/testresult/real.py +++ b/testtools/testresult/real.py @@ -786,47 +786,36 @@ def test_dict_to_case(test_dict): ).to_test_case() -class StreamToDict(StreamResult): +class StreamToTestRecord(StreamResult): """A specialised StreamResult that emits a callback as tests complete. Top level file attachments are simply discarded. Hung tests are detected by stopTestRun and notified there and then. - The callback is passed a dict with the following keys: - - * id: the test id. - * tags: The tags for the test. A set of unicode strings. - * details: A dict of file attachments - ``testtools.content.Content`` - objects. - * status: One of the StreamResult status codes (including inprogress) or - 'unknown' (used if only file events for a test were received...) - * timestamps: A pair of timestamps - the first one received with this - test id, and the one in the event that triggered the notification. - Hung tests have a None for the second end event. Timestamps are not - compared - their ordering is purely order received in the stream. + The callback is passed a ``TestRecord`` object. Only the most recent tags observed in the stream are reported. """ def __init__(self, on_test): - """Create a StreamToDict calling on_test on test completions. + """Create a StreamToTestRecord calling on_test on test completions. - :param on_test: A callback that accepts one parameter - a dict - describing a test. + :param on_test: A callback that accepts one parameter: + a ``TestRecord`` object describing a test. """ - super(StreamToDict, self).__init__() + super(StreamToTestRecord, self).__init__() self.on_test = on_test if parse_mime_type is None: raise ImportError("mimeparse module missing.") def startTestRun(self): - super(StreamToDict, self).startTestRun() + super(StreamToTestRecord, self).startTestRun() self._inprogress = {} 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): - super(StreamToDict, self).status( + super(StreamToTestRecord, self).status( test_id, test_status, test_tags=test_tags, runnable=runnable, file_name=file_name, file_bytes=file_bytes, eof=eof, mime_type=mime_type, @@ -843,7 +832,7 @@ def status(self, test_id=None, test_status=None, test_tags=None, # notify completed tests. if test_status not in INTERIM_STATES: - self.on_test(self._inprogress.pop(key).to_dict()) + self.on_test(self._inprogress.pop(key)) def _update_case(self, case, test_status=None, test_tags=None, file_name=None, file_bytes=None, mime_type=None, @@ -862,10 +851,10 @@ def _update_case(self, case, test_status=None, test_tags=None, return case def stopTestRun(self): - super(StreamToDict, self).stopTestRun() + super(StreamToTestRecord, self).stopTestRun() while self._inprogress: case = self._inprogress.popitem()[1] - self.on_test(case.got_timestamp(None).to_dict()) + self.on_test(case.got_timestamp(None)) def _ensure_key(self, test_id, route_code, timestamp): if test_id is None: @@ -876,6 +865,63 @@ def _ensure_key(self, test_id, route_code, timestamp): return key +class StreamToDict(StreamResult): + """A specialised StreamResult that emits a callback as tests complete. + + Top level file attachments are simply discarded. Hung tests are detected + by stopTestRun and notified there and then. + + The callback is passed a dict with the following keys: + + * id: the test id. + * tags: The tags for the test. A set of unicode strings. + * details: A dict of file attachments - ``testtools.content.Content`` + objects. + * status: One of the StreamResult status codes (including inprogress) or + 'unknown' (used if only file events for a test were received...) + * timestamps: A pair of timestamps - the first one received with this + test id, and the one in the event that triggered the notification. + Hung tests have a None for the second end event. Timestamps are not + compared - their ordering is purely order received in the stream. + + Only the most recent tags observed in the stream are reported. + """ + + # XXX: This could actually be replaced by a very simple function. + # Unfortunately, subclassing is a supported API. + + # XXX: Alternative simplification is to extract a StreamAdapter base + # class, and have this inherit from that. + + def __init__(self, on_test): + """Create a StreamToTestRecord calling on_test on test completions. + + :param on_test: A callback that accepts one parameter: + a ``TestRecord`` object describing a test. + """ + super(StreamToDict, self).__init__() + self._streamer = StreamToTestRecord(self._handle_test) + # XXX: Not clear whether its part of the supported interface for + # self.on_test to be the passed-in on_test. If not, we could reduce + # the boilerplate by subclassing StreamToTestRecord. + self.on_test = on_test + + def _handle_test(self, test_record): + self.on_test(test_record.to_dict()) + + def startTestRun(self): + super(StreamToDict, self).startTestRun() + self._streamer.startTestRun() + + def status(self, *args, **kwargs): + super(StreamToDict, self).status(*args, **kwargs) + self._streamer.status(*args, **kwargs) + + def stopTestRun(self): + super(StreamToDict, self).stopTestRun() + self._streamer.stopTestRun() + + class StreamSummary(StreamToDict): """A specialised StreamResult that summarises a stream. From 87136ad6019f2e80a705f359c45e301dd18e7536 Mon Sep 17 00:00:00 2001 From: Jonathan Lange Date: Mon, 2 Nov 2015 20:40:43 +0000 Subject: [PATCH 16/24] StreamSummary with composition, not inheritance --- testtools/testresult/real.py | 14 ++++++++++++-- 1 file changed, 12 insertions(+), 2 deletions(-) diff --git a/testtools/testresult/real.py b/testtools/testresult/real.py index 059a124f..b7daf485 100644 --- a/testtools/testresult/real.py +++ b/testtools/testresult/real.py @@ -922,7 +922,7 @@ def stopTestRun(self): self._streamer.stopTestRun() -class StreamSummary(StreamToDict): +class StreamSummary(StreamResult): """A specialised StreamResult that summarises a stream. The summary uses the same representation as the original @@ -931,7 +931,8 @@ class StreamSummary(StreamToDict): """ def __init__(self): - super(StreamSummary, self).__init__(self._gather_test) + super(StreamSummary, self).__init__() + self._streamer = StreamToDict(self._gather_test) self._handle_status = { 'success': self._success, 'skip': self._skip, @@ -951,6 +952,15 @@ def startTestRun(self): self.skipped = [] self.expectedFailures = [] self.unexpectedSuccesses = [] + self._streamer.startTestRun() + + def status(self, *args, **kwargs): + super(StreamSummary, self).status(*args, **kwargs) + self._streamer.status(*args, **kwargs) + + def stopTestRun(self): + super(StreamSummary, self).stopTestRun() + self._streamer.stopTestRun() def wasSuccessful(self): """Return False if any failure has occured. From 7e237fbccec44ca2074aeefa1f7ed6af23e16c38 Mon Sep 17 00:00:00 2001 From: Jonathan Lange Date: Mon, 2 Nov 2015 20:45:13 +0000 Subject: [PATCH 17/24] _streamer -> _hook --- testtools/testresult/real.py | 16 ++++++++-------- 1 file changed, 8 insertions(+), 8 deletions(-) diff --git a/testtools/testresult/real.py b/testtools/testresult/real.py index b7daf485..5c2d0fd5 100644 --- a/testtools/testresult/real.py +++ b/testtools/testresult/real.py @@ -900,7 +900,7 @@ def __init__(self, on_test): a ``TestRecord`` object describing a test. """ super(StreamToDict, self).__init__() - self._streamer = StreamToTestRecord(self._handle_test) + self._hook = StreamToTestRecord(self._handle_test) # XXX: Not clear whether its part of the supported interface for # self.on_test to be the passed-in on_test. If not, we could reduce # the boilerplate by subclassing StreamToTestRecord. @@ -911,15 +911,15 @@ def _handle_test(self, test_record): def startTestRun(self): super(StreamToDict, self).startTestRun() - self._streamer.startTestRun() + self._hook.startTestRun() def status(self, *args, **kwargs): super(StreamToDict, self).status(*args, **kwargs) - self._streamer.status(*args, **kwargs) + self._hook.status(*args, **kwargs) def stopTestRun(self): super(StreamToDict, self).stopTestRun() - self._streamer.stopTestRun() + self._hook.stopTestRun() class StreamSummary(StreamResult): @@ -932,7 +932,7 @@ class StreamSummary(StreamResult): def __init__(self): super(StreamSummary, self).__init__() - self._streamer = StreamToDict(self._gather_test) + self._hook = StreamToDict(self._gather_test) self._handle_status = { 'success': self._success, 'skip': self._skip, @@ -952,15 +952,15 @@ def startTestRun(self): self.skipped = [] self.expectedFailures = [] self.unexpectedSuccesses = [] - self._streamer.startTestRun() + self._hook.startTestRun() def status(self, *args, **kwargs): super(StreamSummary, self).status(*args, **kwargs) - self._streamer.status(*args, **kwargs) + self._hook.status(*args, **kwargs) def stopTestRun(self): super(StreamSummary, self).stopTestRun() - self._streamer.stopTestRun() + self._hook.stopTestRun() def wasSuccessful(self): """Return False if any failure has occured. From 0211422650d4136e53c4ca533a8fb97615c238a3 Mon Sep 17 00:00:00 2001 From: Jonathan Lange Date: Mon, 2 Nov 2015 20:48:46 +0000 Subject: [PATCH 18/24] Don't use StreamToDict internally --- testtools/testresult/real.py | 18 +++++++++--------- 1 file changed, 9 insertions(+), 9 deletions(-) diff --git a/testtools/testresult/real.py b/testtools/testresult/real.py index 5c2d0fd5..46198572 100644 --- a/testtools/testresult/real.py +++ b/testtools/testresult/real.py @@ -932,7 +932,7 @@ class StreamSummary(StreamResult): def __init__(self): super(StreamSummary, self).__init__() - self._hook = StreamToDict(self._gather_test) + self._hook = StreamToTestRecord(self._gather_test) self._handle_status = { 'success': self._success, 'skip': self._skip, @@ -970,12 +970,12 @@ def wasSuccessful(self): """ return (not self.failures and not self.errors) - def _gather_test(self, test_dict): - if test_dict['status'] == 'exists': + def _gather_test(self, test_record): + if test_record.status == 'exists': return self.testsRun += 1 - case = test_dict_to_case(test_dict) - self._handle_status[test_dict['status']](case) + case = test_record.to_test_case() + self._handle_status[test_record.status](case) def _incomplete(self, case): self.errors.append((case, "Test did not complete")) @@ -1688,8 +1688,8 @@ 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) + # StreamToTestRecord buffers and gives us individual tests. + self.hook = StreamToTestRecord(self._handle_tests) def status(self, test_id=None, test_status=None, *args, **kwargs): if test_status == 'exists': @@ -1705,8 +1705,8 @@ def stopTestRun(self): self.hook.stopTestRun() self.decorated.stopTestRun() - def _handle_tests(self, test_dict): - case = test_dict_to_case(test_dict) + def _handle_tests(self, test_record): + case = test_record.to_test_case() case.run(self.decorated) From 4291b0a52ff6b08531e68d8e4dff5f8072dcad8c Mon Sep 17 00:00:00 2001 From: Jonathan Lange Date: Mon, 2 Nov 2015 20:49:20 +0000 Subject: [PATCH 19/24] Move test_dict_to_case near StreamToDict --- testtools/testresult/real.py | 30 +++++++++++++++--------------- 1 file changed, 15 insertions(+), 15 deletions(-) diff --git a/testtools/testresult/real.py b/testtools/testresult/real.py index 46198572..86c6ae68 100644 --- a/testtools/testresult/real.py +++ b/testtools/testresult/real.py @@ -771,21 +771,6 @@ def _make_content_type(mime_type=None): }) -def test_dict_to_case(test_dict): - """Convert a test dict into a TestCase object. - - :param test_dict: A test dict as generated by StreamToDict. - :return: A PlaceHolder test object. - """ - return TestRecord( - id=test_dict['id'], - tags=test_dict['tags'], - details=test_dict['details'], - status=test_dict['status'], - timestamps=tuple(test_dict['timestamps']), - ).to_test_case() - - class StreamToTestRecord(StreamResult): """A specialised StreamResult that emits a callback as tests complete. @@ -922,6 +907,21 @@ def stopTestRun(self): self._hook.stopTestRun() +def test_dict_to_case(test_dict): + """Convert a test dict into a TestCase object. + + :param test_dict: A test dict as generated by StreamToDict. + :return: A PlaceHolder test object. + """ + return TestRecord( + id=test_dict['id'], + tags=test_dict['tags'], + details=test_dict['details'], + status=test_dict['status'], + timestamps=tuple(test_dict['timestamps']), + ).to_test_case() + + class StreamSummary(StreamResult): """A specialised StreamResult that summarises a stream. From 76064bdf8f2a8eb65c48be5263e8b2b8b8a1a969 Mon Sep 17 00:00:00 2001 From: Jonathan Lange Date: Mon, 2 Nov 2015 20:50:40 +0000 Subject: [PATCH 20/24] Change PRecord to PClass We don't want to accidentally iterate on TestRecord, say. --- testtools/testresult/real.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/testtools/testresult/real.py b/testtools/testresult/real.py index 86c6ae68..efc78f1a 100644 --- a/testtools/testresult/real.py +++ b/testtools/testresult/real.py @@ -32,7 +32,7 @@ parse_mime_type = try_import('mimeparse.parse_mime_type') Queue = try_imports(['Queue.Queue', 'queue.Queue']) -from pyrsistent import PRecord, field, pmap_field, pset_field, pmap, pset, thaw +from pyrsistent import PClass, field, pmap_field, pset_field, pmap, pset, thaw from testtools.compat import str_is_unicode, _u, _b from testtools.content import ( @@ -634,7 +634,7 @@ def status(self, *args, **kwargs): super(StreamTagger, self).status(*args, **kwargs) -class TestRecord(PRecord): +class TestRecord(PClass): """Representation of a test.""" """The test id.""" From d3860427444dae803243dfb51cababa35bfa16e2 Mon Sep 17 00:00:00 2001 From: Jonathan Lange Date: Tue, 3 Nov 2015 11:01:37 +0000 Subject: [PATCH 21/24] Add pyrsistent dependency to Travis CI --- .travis.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.travis.yml b/.travis.yml index 8e62b21f..0ca34e9c 100644 --- a/.travis.yml +++ b/.travis.yml @@ -37,7 +37,7 @@ matrix: env: SPHINX="<1.3" install: - - pip install fixtures $JINJA_REQ sphinx$SPHINX $TWISTED_REQ + - pip install fixtures $JINJA_REQ sphinx$SPHINX pyrsistent $TWISTED_REQ - python setup.py install script: From 1eccee8c28612c3dd289e78aa0aa0d66a515728b Mon Sep 17 00:00:00 2001 From: Jonathan Lange Date: Tue, 3 Nov 2015 11:24:45 +0000 Subject: [PATCH 22/24] Abstract text_or_bytes --- testtools/compat.py | 5 ++++- testtools/testresult/real.py | 10 +++++----- 2 files changed, 9 insertions(+), 6 deletions(-) diff --git a/testtools/compat.py b/testtools/compat.py index f4b9754b..3f0bfabf 100644 --- a/testtools/compat.py +++ b/testtools/compat.py @@ -1,4 +1,4 @@ -# Copyright (c) 2008-2011 testtools developers. See LICENSE for details. +# Copyright (c) 2008-2015 testtools developers. See LICENSE for details. """Compatibility support for python 2 and 3.""" @@ -14,6 +14,7 @@ 'StringIO', 'reraise', 'unicode_output_stream', + 'text_or_bytes', ] import codecs @@ -66,6 +67,7 @@ def istext(x): def classtypes(): return (type,) str_is_unicode = True + text_or_bytes = (str, bytes) else: import __builtin__ as builtins def _u(s): @@ -83,6 +85,7 @@ def classtypes(): import types return (type, types.ClassType) str_is_unicode = sys.platform == "cli" + text_or_bytes = (unicode, str) _u.__doc__ = __u_doc diff --git a/testtools/testresult/real.py b/testtools/testresult/real.py index efc78f1a..bf1e2592 100644 --- a/testtools/testresult/real.py +++ b/testtools/testresult/real.py @@ -34,7 +34,7 @@ from pyrsistent import PClass, field, pmap_field, pset_field, pmap, pset, thaw -from testtools.compat import str_is_unicode, _u, _b +from testtools.compat import str_is_unicode, text_or_bytes, _u, _b from testtools.content import ( Content, text_content, @@ -638,18 +638,18 @@ class TestRecord(PClass): """Representation of a test.""" """The test id.""" - id = field((str, unicode), mandatory=True) + id = field(text_or_bytes, mandatory=True) """Tags for the test.""" - tags = pset_field((str, unicode), optional=False) + tags = pset_field(text_or_bytes, optional=False) """File attachments.""" # XXX: Documentation says these are unicode, but tests pass in str. - details = pmap_field((str, unicode), Content, optional=False) + details = pmap_field(text_or_bytes, Content, optional=False) """One of the StreamResult status codes.""" status = field( - (str, unicode), mandatory=True, + text_or_bytes, mandatory=True, invariant=lambda x: (x in STATES, 'Invalid state')) """Pair of timestamps (x, y). From bc8b162510b18f3eb1ca98db55eafbf58e299497 Mon Sep 17 00:00:00 2001 From: Jonathan Lange Date: Tue, 3 Nov 2015 11:26:37 +0000 Subject: [PATCH 23/24] Old-fashioned set syntax --- testtools/testresult/real.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/testtools/testresult/real.py b/testtools/testresult/real.py index bf1e2592..7380e17b 100644 --- a/testtools/testresult/real.py +++ b/testtools/testresult/real.py @@ -278,7 +278,7 @@ def done(self): running and at any intermediary point they might choose to indicate their continual operation. """ -INTERIM_STATES = frozenset({None, 'inprogress'}) +INTERIM_STATES = frozenset([None, 'inprogress']) """Final states: @@ -303,7 +303,7 @@ def done(self): * unknown - we don't know what state the test is in """ FINAL_STATES = frozenset( - {'exists', 'xfail', 'uxsuccess', 'success', 'fail', 'skip', 'unknown'}) + ['exists', 'xfail', 'uxsuccess', 'success', 'fail', 'skip', 'unknown']) STATES = INTERIM_STATES | FINAL_STATES From 7c9906eb8bc2c931e65d2001f871301e037e08a8 Mon Sep 17 00:00:00 2001 From: Jonathan Lange Date: Thu, 12 Nov 2015 10:20:51 +0000 Subject: [PATCH 24/24] Hide TestRecord and StreamToTestRecord While they are probably OK to be exposed, let's keep this patch minimal & not news-worthy --- testtools/testresult/real.py | 40 ++++++++++++++++++------------------ 1 file changed, 20 insertions(+), 20 deletions(-) diff --git a/testtools/testresult/real.py b/testtools/testresult/real.py index 7380e17b..e9fb5402 100644 --- a/testtools/testresult/real.py +++ b/testtools/testresult/real.py @@ -634,7 +634,7 @@ def status(self, *args, **kwargs): super(StreamTagger, self).status(*args, **kwargs) -class TestRecord(PClass): +class _TestRecord(PClass): """Representation of a test.""" """The test id.""" @@ -679,8 +679,8 @@ def to_dict(self): * tags: The tags for the test. A set of unicode strings. * details: A dict of file attachments - ``testtools.content.Content`` objects. - * status: One of the StreamResult status codes (including inprogress) or - 'unknown' (used if only file events for a test were received...) + * status: One of the StreamResult status codes (including inprogress) + or 'unknown' (used if only file events for a test were received...) * timestamps: A pair of timestamps - the first one received with this test id, and the one in the event that triggered the notification. Hung tests have a None for the second end event. Timestamps are not @@ -771,36 +771,36 @@ def _make_content_type(mime_type=None): }) -class StreamToTestRecord(StreamResult): +class _StreamToTestRecord(StreamResult): """A specialised StreamResult that emits a callback as tests complete. Top level file attachments are simply discarded. Hung tests are detected by stopTestRun and notified there and then. - The callback is passed a ``TestRecord`` object. + The callback is passed a ``_TestRecord`` object. Only the most recent tags observed in the stream are reported. """ def __init__(self, on_test): - """Create a StreamToTestRecord calling on_test on test completions. + """Create a _StreamToTestRecord calling on_test on test completions. :param on_test: A callback that accepts one parameter: - a ``TestRecord`` object describing a test. + a ``_TestRecord`` object describing a test. """ - super(StreamToTestRecord, self).__init__() + super(_StreamToTestRecord, self).__init__() self.on_test = on_test if parse_mime_type is None: raise ImportError("mimeparse module missing.") def startTestRun(self): - super(StreamToTestRecord, self).startTestRun() + super(_StreamToTestRecord, self).startTestRun() self._inprogress = {} 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): - super(StreamToTestRecord, self).status( + super(_StreamToTestRecord, self).status( test_id, test_status, test_tags=test_tags, runnable=runnable, file_name=file_name, file_bytes=file_bytes, eof=eof, mime_type=mime_type, @@ -836,7 +836,7 @@ def _update_case(self, case, test_status=None, test_tags=None, return case def stopTestRun(self): - super(StreamToTestRecord, self).stopTestRun() + super(_StreamToTestRecord, self).stopTestRun() while self._inprogress: case = self._inprogress.popitem()[1] self.on_test(case.got_timestamp(None)) @@ -846,7 +846,7 @@ def _ensure_key(self, test_id, route_code, timestamp): return key = (test_id, route_code) if key not in self._inprogress: - self._inprogress[key] = TestRecord.create(test_id, timestamp) + self._inprogress[key] = _TestRecord.create(test_id, timestamp) return key @@ -879,16 +879,16 @@ class StreamToDict(StreamResult): # class, and have this inherit from that. def __init__(self, on_test): - """Create a StreamToTestRecord calling on_test on test completions. + """Create a _StreamToTestRecord calling on_test on test completions. :param on_test: A callback that accepts one parameter: - a ``TestRecord`` object describing a test. + a dictionary describing a test. """ super(StreamToDict, self).__init__() - self._hook = StreamToTestRecord(self._handle_test) + self._hook = _StreamToTestRecord(self._handle_test) # XXX: Not clear whether its part of the supported interface for # self.on_test to be the passed-in on_test. If not, we could reduce - # the boilerplate by subclassing StreamToTestRecord. + # the boilerplate by subclassing _StreamToTestRecord. self.on_test = on_test def _handle_test(self, test_record): @@ -913,7 +913,7 @@ def test_dict_to_case(test_dict): :param test_dict: A test dict as generated by StreamToDict. :return: A PlaceHolder test object. """ - return TestRecord( + return _TestRecord( id=test_dict['id'], tags=test_dict['tags'], details=test_dict['details'], @@ -932,7 +932,7 @@ class StreamSummary(StreamResult): def __init__(self): super(StreamSummary, self).__init__() - self._hook = StreamToTestRecord(self._gather_test) + self._hook = _StreamToTestRecord(self._gather_test) self._handle_status = { 'success': self._success, 'skip': self._skip, @@ -1688,8 +1688,8 @@ def __init__(self, decorated): # ExtendedToOriginalDecorator takes care of thunking details back to # exceptions/reasons etc. self.decorated = ExtendedToOriginalDecorator(decorated) - # StreamToTestRecord buffers and gives us individual tests. - self.hook = StreamToTestRecord(self._handle_tests) + # _StreamToTestRecord buffers and gives us individual tests. + self.hook = _StreamToTestRecord(self._handle_tests) def status(self, test_id=None, test_status=None, *args, **kwargs): if test_status == 'exists':