From 255d00653139619a8d1eb6a0efaefbbc842e5bbb Mon Sep 17 00:00:00 2001 From: Jonathan Lange Date: Sat, 12 Feb 2011 12:10:06 +0000 Subject: Get started, fix a bug in subunit's decorator. --- python/subunit/test_results.py | 6 +++++- python/subunit/tests/test_test_results.py | 13 +++++++++++++ 2 files changed, 18 insertions(+), 1 deletion(-) (limited to 'python') diff --git a/python/subunit/test_results.py b/python/subunit/test_results.py index a6ad0c6..d248846 100644 --- a/python/subunit/test_results.py +++ b/python/subunit/test_results.py @@ -82,7 +82,7 @@ class TestResultDecorator(object): return self.decorated.stop() def tags(self, new_tags, gone_tags): - return self.decorated.time(new_tags, gone_tags) + return self.decorated.tags(new_tags, gone_tags) def time(self, a_datetime): return self.decorated.time(a_datetime) @@ -195,6 +195,10 @@ class AutoTimingTestResultDecorator(HookedTestResultDecorator): return self.decorated.time(a_datetime) +class TagCollapsingDecorator(TestResultDecorator): + """Collapses many 'tags' calls into one where possible.""" + + class TestResultFilter(TestResultDecorator): """A pyunit TestResult interface implementation which filters tests. diff --git a/python/subunit/tests/test_test_results.py b/python/subunit/tests/test_test_results.py index a2dad96..0bc7e24 100644 --- a/python/subunit/tests/test_test_results.py +++ b/python/subunit/tests/test_test_results.py @@ -17,6 +17,9 @@ import datetime import unittest +from testtools import TestCase +from testtools.testresult.doubles import ExtendedTestResult + import subunit import subunit.iso8601 as iso8601 import subunit.test_results @@ -187,6 +190,16 @@ class TestAutoTimingTestResultDecorator(unittest.TestCase): self.assertNotEqual(None, self.decorated._calls[2]) +class TestTagCollapsingDecorator(TestCase): + + def test_tags_forwarded_outside_of_tests(self): + result = ExtendedTestResult() + tag_collapser = subunit.test_results.TagCollapsingDecorator(result) + tag_collapser.tags(set(['a', 'b']), set()) + self.assertEquals( + [('tags', set(['a', 'b']), set([]))], result._events) + + def test_suite(): loader = subunit.tests.TestUtil.TestLoader() result = loader.loadTestsFromName(__name__) -- cgit v1.2.1 From 0f644eda8f3af105622ef30c7c6739c6b26fe7e8 Mon Sep 17 00:00:00 2001 From: Jonathan Lange Date: Sat, 12 Feb 2011 12:16:36 +0000 Subject: Implement the tag collapsing logic by stealing stuff from TRF and fixing it. --- python/subunit/test_results.py | 48 +++++++++++++++++++++++++++++++ python/subunit/tests/test_test_results.py | 15 ++++++++++ 2 files changed, 63 insertions(+) (limited to 'python') diff --git a/python/subunit/test_results.py b/python/subunit/test_results.py index d248846..662585b 100644 --- a/python/subunit/test_results.py +++ b/python/subunit/test_results.py @@ -198,6 +198,54 @@ class AutoTimingTestResultDecorator(HookedTestResultDecorator): class TagCollapsingDecorator(TestResultDecorator): """Collapses many 'tags' calls into one where possible.""" + def __init__(self, result): + TestResultDecorator.__init__(self, result) + # The current test (for filtering tags) + self._current_test = None + # The (new, gone) tags for the current test. + self._current_test_tags = None + + def startTest(self, test): + """Start a test. + + Not directly passed to the client, but used for handling of tags + correctly. + """ + self.decorated.startTest(test) + self._current_test = test + self._current_test_tags = set(), set() + + def stopTest(self, test): + """Stop a test. + + Not directly passed to the client, but used for handling of tags + correctly. + """ + # Tags to output for this test. + if self._current_test_tags[0] or self._current_test_tags[1]: + self.decorated.tags(*self._current_test_tags) + self.decorated.stopTest(test) + self._current_test = None + self._current_test_tags = None + + def tags(self, new_tags, gone_tags): + """Handle tag instructions. + + Adds and removes tags as appropriate. If a test is currently running, + tags are not affected for subsequent tests. + + :param new_tags: Tags to add, + :param gone_tags: Tags to remove. + """ + if self._current_test is not None: + # gather the tags until the test stops. + self._current_test_tags[0].update(new_tags) + self._current_test_tags[0].difference_update(gone_tags) + self._current_test_tags[1].update(gone_tags) + self._current_test_tags[1].difference_update(new_tags) + else: + return self.decorated.tags(new_tags, gone_tags) + class TestResultFilter(TestResultDecorator): """A pyunit TestResult interface implementation which filters tests. diff --git a/python/subunit/tests/test_test_results.py b/python/subunit/tests/test_test_results.py index 0bc7e24..7aed8a4 100644 --- a/python/subunit/tests/test_test_results.py +++ b/python/subunit/tests/test_test_results.py @@ -199,6 +199,21 @@ class TestTagCollapsingDecorator(TestCase): self.assertEquals( [('tags', set(['a', 'b']), set([]))], result._events) + def test_tags_collapsed_inside_of_tests(self): + result = ExtendedTestResult() + tag_collapser = subunit.test_results.TagCollapsingDecorator(result) + test = subunit.RemotedTestCase('foo') + tag_collapser.startTest(test) + tag_collapser.tags(set(['a']), set()) + tag_collapser.tags(set(['b']), set(['a'])) + tag_collapser.tags(set(['c']), set()) + tag_collapser.stopTest(test) + self.assertEquals( + [('startTest', test), + ('tags', set(['b', 'c']), set(['a'])), + ('stopTest', test)], + result._events) + def test_suite(): loader = subunit.tests.TestUtil.TestLoader() -- cgit v1.2.1 From 83942a36ce48d3761b4ade166ccb0bf286578d2e Mon Sep 17 00:00:00 2001 From: Jonathan Lange Date: Sat, 12 Feb 2011 12:21:07 +0000 Subject: Simplify TRF by using TagCollapsingDecorator. --- python/subunit/test_results.py | 24 +----------------------- 1 file changed, 1 insertion(+), 23 deletions(-) (limited to 'python') diff --git a/python/subunit/test_results.py b/python/subunit/test_results.py index 662585b..6a89200 100644 --- a/python/subunit/test_results.py +++ b/python/subunit/test_results.py @@ -275,6 +275,7 @@ class TestResultFilter(TestResultDecorator): as 'success' or 'failure'. """ TestResultDecorator.__init__(self, result) + self.decorated = TagCollapsingDecorator(self.decorated) self._filter_error = filter_error self._filter_failure = filter_failure self._filter_success = filter_success @@ -286,8 +287,6 @@ class TestResultFilter(TestResultDecorator): self._current_test = None # Has the current test been filtered (for outputting test tags) self._current_test_filtered = None - # The (new, gone) tags for the current test. - self._current_test_tags = None # Calls to this result that we don't know whether to forward on yet. self._buffered_calls = [] @@ -345,7 +344,6 @@ class TestResultFilter(TestResultDecorator): """ self._current_test = test self._current_test_filtered = False - self._current_test_tags = set(), set() self._buffered_calls.append(('startTest', [test], {})) def stopTest(self, test): @@ -358,31 +356,11 @@ class TestResultFilter(TestResultDecorator): # Tags to output for this test. for method, args, kwargs in self._buffered_calls: getattr(self.decorated, method)(*args, **kwargs) - if self._current_test_tags[0] or self._current_test_tags[1]: - self.decorated.tags(*self._current_test_tags) self.decorated.stopTest(test) self._current_test = None self._current_test_filtered = None - self._current_test_tags = None self._buffered_calls = [] - def tags(self, new_tags, gone_tags): - """Handle tag instructions. - - Adds and removes tags as appropriate. If a test is currently running, - tags are not affected for subsequent tests. - - :param new_tags: Tags to add, - :param gone_tags: Tags to remove. - """ - if self._current_test is not None: - # gather the tags until the test stops. - self._current_test_tags[0].update(new_tags) - self._current_test_tags[0].difference_update(gone_tags) - self._current_test_tags[1].update(gone_tags) - self._current_test_tags[1].difference_update(new_tags) - return self.decorated.tags(new_tags, gone_tags) - def time(self, a_time): if self._current_test is not None: self._buffered_calls.append(('time', [a_time], {})) -- cgit v1.2.1 From cd10a295cc588e4ca93e741b094b6c2997153f37 Mon Sep 17 00:00:00 2001 From: Jonathan Lange Date: Sat, 12 Feb 2011 12:36:50 +0000 Subject: Only one mechanism for checking to see if a thing is filtered. --- python/subunit/test_results.py | 42 +++++++++++++++++++++++++++--------------- 1 file changed, 27 insertions(+), 15 deletions(-) (limited to 'python') diff --git a/python/subunit/test_results.py b/python/subunit/test_results.py index 6a89200..12849f5 100644 --- a/python/subunit/test_results.py +++ b/python/subunit/test_results.py @@ -247,6 +247,14 @@ class TagCollapsingDecorator(TestResultDecorator): return self.decorated.tags(new_tags, gone_tags) +def all_true(bools): + """Return True if all of 'bools' are True. False otherwise.""" + for b in bools: + if not b: + return False + return True + + class TestResultFilter(TestResultDecorator): """A pyunit TestResult interface implementation which filters tests. @@ -276,13 +284,20 @@ class TestResultFilter(TestResultDecorator): """ TestResultDecorator.__init__(self, result) self.decorated = TagCollapsingDecorator(self.decorated) - self._filter_error = filter_error - self._filter_failure = filter_failure - self._filter_success = filter_success - self._filter_skip = filter_skip - if filter_predicate is None: - filter_predicate = lambda test, outcome, err, details: True - self.filter_predicate = filter_predicate + predicates = [] + if filter_error: + predicates.append(lambda t, outcome, e, d: outcome != 'error') + if filter_failure: + predicates.append(lambda t, outcome, e, d: outcome != 'failure') + if filter_success: + predicates.append(lambda t, outcome, e, d: outcome != 'success') + if filter_skip: + predicates.append(lambda t, outcome, e, d: outcome != 'skip') + if filter_predicate is not None: + predicates.append(filter_predicate) + self.filter_predicate = ( + lambda test, outcome, err, details: + all_true(p(test, outcome, err, details) for p in predicates)) # The current test (for filtering tags) self._current_test = None # Has the current test been filtered (for outputting test tags) @@ -291,32 +306,28 @@ class TestResultFilter(TestResultDecorator): self._buffered_calls = [] def addError(self, test, err=None, details=None): - if (not self._filter_error and - self.filter_predicate(test, 'error', err, details)): + if (self.filter_predicate(test, 'error', err, details)): self._buffered_calls.append( ('addError', [test, err], {'details': details})) else: self._filtered() def addFailure(self, test, err=None, details=None): - if (not self._filter_failure and - self.filter_predicate(test, 'failure', err, details)): + if (self.filter_predicate(test, 'failure', err, details)): self._buffered_calls.append( ('addFailure', [test, err], {'details': details})) else: self._filtered() def addSkip(self, test, reason=None, details=None): - if (not self._filter_skip and - self.filter_predicate(test, 'skip', reason, details)): + if (self.filter_predicate(test, 'skip', reason, details)): self._buffered_calls.append( ('addSkip', [reason], {'details': details})) else: self._filtered() def addSuccess(self, test, details=None): - if (not self._filter_success and - self.filter_predicate(test, 'success', None, details)): + if (self.filter_predicate(test, 'success', None, details)): self._buffered_calls.append( ('addSuccess', [test], {'details': details})) else: @@ -362,6 +373,7 @@ class TestResultFilter(TestResultDecorator): self._buffered_calls = [] def time(self, a_time): + # XXX: Make a TimeCollapsingDecorator. if self._current_test is not None: self._buffered_calls.append(('time', [a_time], {})) else: -- cgit v1.2.1 From 7fd32930b2d9bff0c4f8f823a313c03beedce451 Mon Sep 17 00:00:00 2001 From: Jonathan Lange Date: Sun, 13 Feb 2011 14:39:43 +0000 Subject: Add a time collapsing decorator. --- python/subunit/test_results.py | 19 +++++++++++++++++++ python/subunit/tests/test_test_results.py | 25 +++++++++++++++++++++++++ 2 files changed, 44 insertions(+) (limited to 'python') diff --git a/python/subunit/test_results.py b/python/subunit/test_results.py index 12849f5..b3228c6 100644 --- a/python/subunit/test_results.py +++ b/python/subunit/test_results.py @@ -247,6 +247,25 @@ class TagCollapsingDecorator(TestResultDecorator): return self.decorated.tags(new_tags, gone_tags) +class TimeCollapsingDecorator(HookedTestResultDecorator): + """Only pass on the first and last of a consecutive sequence of times.""" + + def __init__(self, decorated): + HookedTestResultDecorator.__init__(self, decorated) + self._last_time = None + + def _before_event(self): + self.decorated.time(self._last_time) + self._last_time = None + + def time(self, a_time): + # Don't upcall, because we don't want to call _before_event, it's only + # for non-time events. + if self._last_time is None: + self.decorated.time(a_time) + self._last_time = a_time + + def all_true(bools): """Return True if all of 'bools' are True. False otherwise.""" for b in bools: diff --git a/python/subunit/tests/test_test_results.py b/python/subunit/tests/test_test_results.py index 7aed8a4..8535d56 100644 --- a/python/subunit/tests/test_test_results.py +++ b/python/subunit/tests/test_test_results.py @@ -215,6 +215,31 @@ class TestTagCollapsingDecorator(TestCase): result._events) +class TestTimeCollapsingDecorator(TestCase): + + def make_time(self): + # Heh heh. + return datetime.datetime( + 2000, 1, self.getUniqueInteger(), tzinfo=iso8601.UTC) + + def test_initial_time_forwarded(self): + result = ExtendedTestResult() + tag_collapser = subunit.test_results.TimeCollapsingDecorator(result) + a_time = self.make_time() + tag_collapser.time(a_time) + self.assertEquals([('time', a_time)], result._events) + + def test_time_collapsed(self): + result = ExtendedTestResult() + tag_collapser = subunit.test_results.TimeCollapsingDecorator(result) + times = [self.make_time() for i in range(5)] + for a_time in times: + tag_collapser.time(a_time) + tag_collapser.startTest(subunit.RemotedTestCase('foo')) + self.assertEquals( + [('time', times[0]), ('time', times[-1])], result._events[:-1]) + + def test_suite(): loader = subunit.tests.TestUtil.TestLoader() result = loader.loadTestsFromName(__name__) -- cgit v1.2.1 From d86c97154604bd029774d8095b394de26af4867c Mon Sep 17 00:00:00 2001 From: Jonathan Lange Date: Sun, 13 Feb 2011 14:48:21 +0000 Subject: Properly handle multiple events. --- python/subunit/test_results.py | 14 +++++++++----- python/subunit/tests/test_test_results.py | 26 +++++++++++++++++++++++++- 2 files changed, 34 insertions(+), 6 deletions(-) (limited to 'python') diff --git a/python/subunit/test_results.py b/python/subunit/test_results.py index b3228c6..cf051ba 100644 --- a/python/subunit/test_results.py +++ b/python/subunit/test_results.py @@ -252,18 +252,22 @@ class TimeCollapsingDecorator(HookedTestResultDecorator): def __init__(self, decorated): HookedTestResultDecorator.__init__(self, decorated) - self._last_time = None + self._last_received_time = None + self._last_sent_time = None def _before_event(self): - self.decorated.time(self._last_time) - self._last_time = None + if self._last_received_time != self._last_sent_time: + self.decorated.time(self._last_received_time) + self._last_sent_time = self._last_received_time + self._last_received_time = None def time(self, a_time): # Don't upcall, because we don't want to call _before_event, it's only # for non-time events. - if self._last_time is None: + if self._last_received_time is None: self.decorated.time(a_time) - self._last_time = a_time + self._last_sent_time = a_time + self._last_received_time = a_time def all_true(bools): diff --git a/python/subunit/tests/test_test_results.py b/python/subunit/tests/test_test_results.py index 8535d56..8633b44 100644 --- a/python/subunit/tests/test_test_results.py +++ b/python/subunit/tests/test_test_results.py @@ -223,13 +223,16 @@ class TestTimeCollapsingDecorator(TestCase): 2000, 1, self.getUniqueInteger(), tzinfo=iso8601.UTC) def test_initial_time_forwarded(self): + # We always forward the first time event we see. result = ExtendedTestResult() tag_collapser = subunit.test_results.TimeCollapsingDecorator(result) a_time = self.make_time() tag_collapser.time(a_time) self.assertEquals([('time', a_time)], result._events) - def test_time_collapsed(self): + def test_time_collapsed_to_first_and_last(self): + # If there are many consecutive time events, only the first and last + # are sent through. result = ExtendedTestResult() tag_collapser = subunit.test_results.TimeCollapsingDecorator(result) times = [self.make_time() for i in range(5)] @@ -239,6 +242,27 @@ class TestTimeCollapsingDecorator(TestCase): self.assertEquals( [('time', times[0]), ('time', times[-1])], result._events[:-1]) + def test_only_one_time_sent(self): + # If we receive a single time event followed by a non-time event, we + # send exactly one time event. + result = ExtendedTestResult() + tag_collapser = subunit.test_results.TimeCollapsingDecorator(result) + a_time = self.make_time() + tag_collapser.time(a_time) + tag_collapser.startTest(subunit.RemotedTestCase('foo')) + self.assertEquals([('time', a_time)], result._events[:-1]) + + def test_duplicate_times_not_sent(self): + # Many time events with the exact same time are collapsed into one + # time event. + result = ExtendedTestResult() + tag_collapser = subunit.test_results.TimeCollapsingDecorator(result) + a_time = self.make_time() + for i in range(5): + tag_collapser.time(a_time) + tag_collapser.startTest(subunit.RemotedTestCase('foo')) + self.assertEquals([('time', a_time)], result._events[:-1]) + def test_suite(): loader = subunit.tests.TestUtil.TestLoader() -- cgit v1.2.1 From e0581e6819db7c2872ced4d43e803ae7b5e7bd05 Mon Sep 17 00:00:00 2001 From: Jonathan Lange Date: Sun, 13 Feb 2011 14:52:42 +0000 Subject: Make sure that we don't send time if there are none to send. --- python/subunit/test_results.py | 2 ++ python/subunit/tests/test_test_results.py | 15 +++++++++++++++ 2 files changed, 17 insertions(+) (limited to 'python') diff --git a/python/subunit/test_results.py b/python/subunit/test_results.py index cf051ba..a3f640d 100644 --- a/python/subunit/test_results.py +++ b/python/subunit/test_results.py @@ -256,6 +256,8 @@ class TimeCollapsingDecorator(HookedTestResultDecorator): self._last_sent_time = None def _before_event(self): + if self._last_received_time is None: + return if self._last_received_time != self._last_sent_time: self.decorated.time(self._last_received_time) self._last_sent_time = self._last_received_time diff --git a/python/subunit/tests/test_test_results.py b/python/subunit/tests/test_test_results.py index 8633b44..bed2bd8 100644 --- a/python/subunit/tests/test_test_results.py +++ b/python/subunit/tests/test_test_results.py @@ -263,6 +263,21 @@ class TestTimeCollapsingDecorator(TestCase): tag_collapser.startTest(subunit.RemotedTestCase('foo')) self.assertEquals([('time', a_time)], result._events[:-1]) + def test_no_times_inserted(self): + result = ExtendedTestResult() + tag_collapser = subunit.test_results.TimeCollapsingDecorator(result) + a_time = self.make_time() + tag_collapser.time(a_time) + foo = subunit.RemotedTestCase('foo') + tag_collapser.startTest(foo) + tag_collapser.addSuccess(foo) + tag_collapser.stopTest(foo) + self.assertEquals( + [('time', a_time), + ('startTest', foo), + ('addSuccess', foo), + ('stopTest', foo)], result._events) + def test_suite(): loader = subunit.tests.TestUtil.TestLoader() -- cgit v1.2.1 From 7e0f91fee35ac314e52043f01502857871f75cd8 Mon Sep 17 00:00:00 2001 From: Jonathan Lange Date: Sun, 13 Feb 2011 14:53:05 +0000 Subject: Use the time collapsing decorator as well. --- python/subunit/test_results.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) (limited to 'python') diff --git a/python/subunit/test_results.py b/python/subunit/test_results.py index a3f640d..e1e59df 100644 --- a/python/subunit/test_results.py +++ b/python/subunit/test_results.py @@ -308,7 +308,8 @@ class TestResultFilter(TestResultDecorator): as 'success' or 'failure'. """ TestResultDecorator.__init__(self, result) - self.decorated = TagCollapsingDecorator(self.decorated) + self.decorated = TimeCollapsingDecorator( + TagCollapsingDecorator(self.decorated)) predicates = [] if filter_error: predicates.append(lambda t, outcome, e, d: outcome != 'error') @@ -398,7 +399,6 @@ class TestResultFilter(TestResultDecorator): self._buffered_calls = [] def time(self, a_time): - # XXX: Make a TimeCollapsingDecorator. if self._current_test is not None: self._buffered_calls.append(('time', [a_time], {})) else: -- cgit v1.2.1 From 2d2aa86675f7e320ad146a6f42bca8f86f51682c Mon Sep 17 00:00:00 2001 From: Jonathan Lange Date: Sun, 13 Feb 2011 14:54:48 +0000 Subject: Consistently use super() rather than upcalling. --- python/subunit/test_results.py | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) (limited to 'python') diff --git a/python/subunit/test_results.py b/python/subunit/test_results.py index e1e59df..e7f9171 100644 --- a/python/subunit/test_results.py +++ b/python/subunit/test_results.py @@ -199,7 +199,7 @@ class TagCollapsingDecorator(TestResultDecorator): """Collapses many 'tags' calls into one where possible.""" def __init__(self, result): - TestResultDecorator.__init__(self, result) + super(TagCollapsingDecorator, self).__init__(result) # The current test (for filtering tags) self._current_test = None # The (new, gone) tags for the current test. @@ -251,7 +251,7 @@ class TimeCollapsingDecorator(HookedTestResultDecorator): """Only pass on the first and last of a consecutive sequence of times.""" def __init__(self, decorated): - HookedTestResultDecorator.__init__(self, decorated) + super(TimeCollapsingDecorator, self).__init__(decorated) self._last_received_time = None self._last_sent_time = None @@ -307,7 +307,7 @@ class TestResultFilter(TestResultDecorator): metadata is available. outcome is the name of the outcome such as 'success' or 'failure'. """ - TestResultDecorator.__init__(self, result) + super(TestResultFilter, self).__init__(result) self.decorated = TimeCollapsingDecorator( TagCollapsingDecorator(self.decorated)) predicates = [] @@ -414,7 +414,7 @@ class TestIdPrintingResult(testtools.TestResult): def __init__(self, stream, show_times=False): """Create a FilterResult object outputting to stream.""" - testtools.TestResult.__init__(self) + super(TestIdPrintingResult, self).__init__() self._stream = stream self.failed_tests = 0 self.__time = 0 -- cgit v1.2.1 From 6301e2918b5f2c880b5084f63201b2ee7de4be93 Mon Sep 17 00:00:00 2001 From: Jonathan Lange Date: Tue, 15 Feb 2011 10:52:45 +0000 Subject: Add different support. --- python/subunit/tests/test_test_results.py | 15 +++++++++++++++ 1 file changed, 15 insertions(+) (limited to 'python') diff --git a/python/subunit/tests/test_test_results.py b/python/subunit/tests/test_test_results.py index bed2bd8..94d2274 100644 --- a/python/subunit/tests/test_test_results.py +++ b/python/subunit/tests/test_test_results.py @@ -214,6 +214,21 @@ class TestTagCollapsingDecorator(TestCase): ('stopTest', test)], result._events) + def test_tags_collapsed_inside_of_tests_different_ordering(self): + result = ExtendedTestResult() + tag_collapser = subunit.test_results.TagCollapsingDecorator(result) + test = subunit.RemotedTestCase('foo') + tag_collapser.startTest(test) + tag_collapser.tags(set(), set(['a'])) + tag_collapser.tags(set(['a', 'b']), set()) + tag_collapser.tags(set(['c']), set()) + tag_collapser.stopTest(test) + self.assertEquals( + [('startTest', test), + ('tags', set(['a', 'b', 'c']), set()), + ('stopTest', test)], + result._events) + class TestTimeCollapsingDecorator(TestCase): -- cgit v1.2.1