Merge lp:~jml/subunit/filter-refactoring into lp:~subunit/subunit/trunk

Proposed by Jonathan Lange
Status: Merged
Merged at revision: 141
Proposed branch: lp:~jml/subunit/filter-refactoring
Merge into: lp:~subunit/subunit/trunk
Diff against target: 350 lines (+215/-41)
2 files modified
python/subunit/test_results.py (+108/-41)
python/subunit/tests/test_test_results.py (+107/-0)
To merge this branch: bzr merge lp:~jml/subunit/filter-refactoring
Reviewer Review Type Date Requested Status
Jelmer Vernooij code Approve
Review via email: mp+49496@code.launchpad.net

Description of the change

This branch separates the concerns of TestResultFilter a little bit. It separates out the tag-collapsing logic into a new decorator which can be used independently of filtering. It also adds a TimeCollapsingDecorator which I *think* fixes bug 567150. Also, TRF now assembles a single predicate function, rather than doing lots of per-method checking.

Bug 681828 gets fixed along the way, by virtue of me doing TDD.

Take a close look at the TagCollapsingDecorator tests, since they verify the current logic, and I'm not sure that it is the desired logic.

To post a comment you must log in.
lp:~jml/subunit/filter-refactoring updated
150. By Jonathan Lange

Add different support.

Revision history for this message
Jonathan Lange (jml) wrote :

Still waiting on a review for this.

Revision history for this message
Jelmer Vernooij (jelmer) wrote :

w00t.

review: Approve (code)

Preview Diff

[H/L] Next/Prev Comment, [J/K] Next/Prev File, [N/P] Next/Prev Hunk
1=== modified file 'python/subunit/test_results.py'
2--- python/subunit/test_results.py 2011-02-11 17:46:54 +0000
3+++ python/subunit/test_results.py 2011-02-15 10:56:46 +0000
4@@ -82,7 +82,7 @@
5 return self.decorated.stop()
6
7 def tags(self, new_tags, gone_tags):
8- return self.decorated.time(new_tags, gone_tags)
9+ return self.decorated.tags(new_tags, gone_tags)
10
11 def time(self, a_datetime):
12 return self.decorated.time(a_datetime)
13@@ -195,6 +195,91 @@
14 return self.decorated.time(a_datetime)
15
16
17+class TagCollapsingDecorator(TestResultDecorator):
18+ """Collapses many 'tags' calls into one where possible."""
19+
20+ def __init__(self, result):
21+ super(TagCollapsingDecorator, self).__init__(result)
22+ # The current test (for filtering tags)
23+ self._current_test = None
24+ # The (new, gone) tags for the current test.
25+ self._current_test_tags = None
26+
27+ def startTest(self, test):
28+ """Start a test.
29+
30+ Not directly passed to the client, but used for handling of tags
31+ correctly.
32+ """
33+ self.decorated.startTest(test)
34+ self._current_test = test
35+ self._current_test_tags = set(), set()
36+
37+ def stopTest(self, test):
38+ """Stop a test.
39+
40+ Not directly passed to the client, but used for handling of tags
41+ correctly.
42+ """
43+ # Tags to output for this test.
44+ if self._current_test_tags[0] or self._current_test_tags[1]:
45+ self.decorated.tags(*self._current_test_tags)
46+ self.decorated.stopTest(test)
47+ self._current_test = None
48+ self._current_test_tags = None
49+
50+ def tags(self, new_tags, gone_tags):
51+ """Handle tag instructions.
52+
53+ Adds and removes tags as appropriate. If a test is currently running,
54+ tags are not affected for subsequent tests.
55+
56+ :param new_tags: Tags to add,
57+ :param gone_tags: Tags to remove.
58+ """
59+ if self._current_test is not None:
60+ # gather the tags until the test stops.
61+ self._current_test_tags[0].update(new_tags)
62+ self._current_test_tags[0].difference_update(gone_tags)
63+ self._current_test_tags[1].update(gone_tags)
64+ self._current_test_tags[1].difference_update(new_tags)
65+ else:
66+ return self.decorated.tags(new_tags, gone_tags)
67+
68+
69+class TimeCollapsingDecorator(HookedTestResultDecorator):
70+ """Only pass on the first and last of a consecutive sequence of times."""
71+
72+ def __init__(self, decorated):
73+ super(TimeCollapsingDecorator, self).__init__(decorated)
74+ self._last_received_time = None
75+ self._last_sent_time = None
76+
77+ def _before_event(self):
78+ if self._last_received_time is None:
79+ return
80+ if self._last_received_time != self._last_sent_time:
81+ self.decorated.time(self._last_received_time)
82+ self._last_sent_time = self._last_received_time
83+ self._last_received_time = None
84+
85+ def time(self, a_time):
86+ # Don't upcall, because we don't want to call _before_event, it's only
87+ # for non-time events.
88+ if self._last_received_time is None:
89+ self.decorated.time(a_time)
90+ self._last_sent_time = a_time
91+ self._last_received_time = a_time
92+
93+
94+def all_true(bools):
95+ """Return True if all of 'bools' are True. False otherwise."""
96+ for b in bools:
97+ if not b:
98+ return False
99+ return True
100+
101+
102 class TestResultFilter(TestResultDecorator):
103 """A pyunit TestResult interface implementation which filters tests.
104
105@@ -222,50 +307,53 @@
106 metadata is available. outcome is the name of the outcome such
107 as 'success' or 'failure'.
108 """
109- TestResultDecorator.__init__(self, result)
110- self._filter_error = filter_error
111- self._filter_failure = filter_failure
112- self._filter_success = filter_success
113- self._filter_skip = filter_skip
114- if filter_predicate is None:
115- filter_predicate = lambda test, outcome, err, details: True
116- self.filter_predicate = filter_predicate
117+ super(TestResultFilter, self).__init__(result)
118+ self.decorated = TimeCollapsingDecorator(
119+ TagCollapsingDecorator(self.decorated))
120+ predicates = []
121+ if filter_error:
122+ predicates.append(lambda t, outcome, e, d: outcome != 'error')
123+ if filter_failure:
124+ predicates.append(lambda t, outcome, e, d: outcome != 'failure')
125+ if filter_success:
126+ predicates.append(lambda t, outcome, e, d: outcome != 'success')
127+ if filter_skip:
128+ predicates.append(lambda t, outcome, e, d: outcome != 'skip')
129+ if filter_predicate is not None:
130+ predicates.append(filter_predicate)
131+ self.filter_predicate = (
132+ lambda test, outcome, err, details:
133+ all_true(p(test, outcome, err, details) for p in predicates))
134 # The current test (for filtering tags)
135 self._current_test = None
136 # Has the current test been filtered (for outputting test tags)
137 self._current_test_filtered = None
138- # The (new, gone) tags for the current test.
139- self._current_test_tags = None
140 # Calls to this result that we don't know whether to forward on yet.
141 self._buffered_calls = []
142
143 def addError(self, test, err=None, details=None):
144- if (not self._filter_error and
145- self.filter_predicate(test, 'error', err, details)):
146+ if (self.filter_predicate(test, 'error', err, details)):
147 self._buffered_calls.append(
148 ('addError', [test, err], {'details': details}))
149 else:
150 self._filtered()
151
152 def addFailure(self, test, err=None, details=None):
153- if (not self._filter_failure and
154- self.filter_predicate(test, 'failure', err, details)):
155+ if (self.filter_predicate(test, 'failure', err, details)):
156 self._buffered_calls.append(
157 ('addFailure', [test, err], {'details': details}))
158 else:
159 self._filtered()
160
161 def addSkip(self, test, reason=None, details=None):
162- if (not self._filter_skip and
163- self.filter_predicate(test, 'skip', reason, details)):
164+ if (self.filter_predicate(test, 'skip', reason, details)):
165 self._buffered_calls.append(
166 ('addSkip', [reason], {'details': details}))
167 else:
168 self._filtered()
169
170 def addSuccess(self, test, details=None):
171- if (not self._filter_success and
172- self.filter_predicate(test, 'success', None, details)):
173+ if (self.filter_predicate(test, 'success', None, details)):
174 self._buffered_calls.append(
175 ('addSuccess', [test], {'details': details}))
176 else:
177@@ -293,7 +381,6 @@
178 """
179 self._current_test = test
180 self._current_test_filtered = False
181- self._current_test_tags = set(), set()
182 self._buffered_calls.append(('startTest', [test], {}))
183
184 def stopTest(self, test):
185@@ -306,31 +393,11 @@
186 # Tags to output for this test.
187 for method, args, kwargs in self._buffered_calls:
188 getattr(self.decorated, method)(*args, **kwargs)
189- if self._current_test_tags[0] or self._current_test_tags[1]:
190- self.decorated.tags(*self._current_test_tags)
191 self.decorated.stopTest(test)
192 self._current_test = None
193 self._current_test_filtered = None
194- self._current_test_tags = None
195 self._buffered_calls = []
196
197- def tags(self, new_tags, gone_tags):
198- """Handle tag instructions.
199-
200- Adds and removes tags as appropriate. If a test is currently running,
201- tags are not affected for subsequent tests.
202-
203- :param new_tags: Tags to add,
204- :param gone_tags: Tags to remove.
205- """
206- if self._current_test is not None:
207- # gather the tags until the test stops.
208- self._current_test_tags[0].update(new_tags)
209- self._current_test_tags[0].difference_update(gone_tags)
210- self._current_test_tags[1].update(gone_tags)
211- self._current_test_tags[1].difference_update(new_tags)
212- return self.decorated.tags(new_tags, gone_tags)
213-
214 def time(self, a_time):
215 if self._current_test is not None:
216 self._buffered_calls.append(('time', [a_time], {}))
217@@ -347,7 +414,7 @@
218
219 def __init__(self, stream, show_times=False):
220 """Create a FilterResult object outputting to stream."""
221- testtools.TestResult.__init__(self)
222+ super(TestIdPrintingResult, self).__init__()
223 self._stream = stream
224 self.failed_tests = 0
225 self.__time = 0
226
227=== modified file 'python/subunit/tests/test_test_results.py'
228--- python/subunit/tests/test_test_results.py 2011-02-10 17:42:15 +0000
229+++ python/subunit/tests/test_test_results.py 2011-02-15 10:56:46 +0000
230@@ -17,6 +17,9 @@
231 import datetime
232 import unittest
233
234+from testtools import TestCase
235+from testtools.testresult.doubles import ExtendedTestResult
236+
237 import subunit
238 import subunit.iso8601 as iso8601
239 import subunit.test_results
240@@ -187,6 +190,110 @@
241 self.assertNotEqual(None, self.decorated._calls[2])
242
243
244+class TestTagCollapsingDecorator(TestCase):
245+
246+ def test_tags_forwarded_outside_of_tests(self):
247+ result = ExtendedTestResult()
248+ tag_collapser = subunit.test_results.TagCollapsingDecorator(result)
249+ tag_collapser.tags(set(['a', 'b']), set())
250+ self.assertEquals(
251+ [('tags', set(['a', 'b']), set([]))], result._events)
252+
253+ def test_tags_collapsed_inside_of_tests(self):
254+ result = ExtendedTestResult()
255+ tag_collapser = subunit.test_results.TagCollapsingDecorator(result)
256+ test = subunit.RemotedTestCase('foo')
257+ tag_collapser.startTest(test)
258+ tag_collapser.tags(set(['a']), set())
259+ tag_collapser.tags(set(['b']), set(['a']))
260+ tag_collapser.tags(set(['c']), set())
261+ tag_collapser.stopTest(test)
262+ self.assertEquals(
263+ [('startTest', test),
264+ ('tags', set(['b', 'c']), set(['a'])),
265+ ('stopTest', test)],
266+ result._events)
267+
268+ def test_tags_collapsed_inside_of_tests_different_ordering(self):
269+ result = ExtendedTestResult()
270+ tag_collapser = subunit.test_results.TagCollapsingDecorator(result)
271+ test = subunit.RemotedTestCase('foo')
272+ tag_collapser.startTest(test)
273+ tag_collapser.tags(set(), set(['a']))
274+ tag_collapser.tags(set(['a', 'b']), set())
275+ tag_collapser.tags(set(['c']), set())
276+ tag_collapser.stopTest(test)
277+ self.assertEquals(
278+ [('startTest', test),
279+ ('tags', set(['a', 'b', 'c']), set()),
280+ ('stopTest', test)],
281+ result._events)
282+
283+
284+class TestTimeCollapsingDecorator(TestCase):
285+
286+ def make_time(self):
287+ # Heh heh.
288+ return datetime.datetime(
289+ 2000, 1, self.getUniqueInteger(), tzinfo=iso8601.UTC)
290+
291+ def test_initial_time_forwarded(self):
292+ # We always forward the first time event we see.
293+ result = ExtendedTestResult()
294+ tag_collapser = subunit.test_results.TimeCollapsingDecorator(result)
295+ a_time = self.make_time()
296+ tag_collapser.time(a_time)
297+ self.assertEquals([('time', a_time)], result._events)
298+
299+ def test_time_collapsed_to_first_and_last(self):
300+ # If there are many consecutive time events, only the first and last
301+ # are sent through.
302+ result = ExtendedTestResult()
303+ tag_collapser = subunit.test_results.TimeCollapsingDecorator(result)
304+ times = [self.make_time() for i in range(5)]
305+ for a_time in times:
306+ tag_collapser.time(a_time)
307+ tag_collapser.startTest(subunit.RemotedTestCase('foo'))
308+ self.assertEquals(
309+ [('time', times[0]), ('time', times[-1])], result._events[:-1])
310+
311+ def test_only_one_time_sent(self):
312+ # If we receive a single time event followed by a non-time event, we
313+ # send exactly one time event.
314+ result = ExtendedTestResult()
315+ tag_collapser = subunit.test_results.TimeCollapsingDecorator(result)
316+ a_time = self.make_time()
317+ tag_collapser.time(a_time)
318+ tag_collapser.startTest(subunit.RemotedTestCase('foo'))
319+ self.assertEquals([('time', a_time)], result._events[:-1])
320+
321+ def test_duplicate_times_not_sent(self):
322+ # Many time events with the exact same time are collapsed into one
323+ # time event.
324+ result = ExtendedTestResult()
325+ tag_collapser = subunit.test_results.TimeCollapsingDecorator(result)
326+ a_time = self.make_time()
327+ for i in range(5):
328+ tag_collapser.time(a_time)
329+ tag_collapser.startTest(subunit.RemotedTestCase('foo'))
330+ self.assertEquals([('time', a_time)], result._events[:-1])
331+
332+ def test_no_times_inserted(self):
333+ result = ExtendedTestResult()
334+ tag_collapser = subunit.test_results.TimeCollapsingDecorator(result)
335+ a_time = self.make_time()
336+ tag_collapser.time(a_time)
337+ foo = subunit.RemotedTestCase('foo')
338+ tag_collapser.startTest(foo)
339+ tag_collapser.addSuccess(foo)
340+ tag_collapser.stopTest(foo)
341+ self.assertEquals(
342+ [('time', a_time),
343+ ('startTest', foo),
344+ ('addSuccess', foo),
345+ ('stopTest', foo)], result._events)
346+
347+
348 def test_suite():
349 loader = subunit.tests.TestUtil.TestLoader()
350 result = loader.loadTestsFromName(__name__)

Subscribers

People subscribed via source and target branches