Merge lp:~jml/testtools/test-by-test-result into lp:~testtools-committers/testtools/trunk

Proposed by Jonathan Lange
Status: Merged
Merged at revision: 249
Proposed branch: lp:~jml/testtools/test-by-test-result
Merge into: lp:~testtools-committers/testtools/trunk
Diff against target: 351 lines (+254/-4)
5 files modified
NEWS (+7/-0)
testtools/__init__.py (+3/-1)
testtools/testresult/__init__.py (+3/-1)
testtools/testresult/real.py (+81/-1)
testtools/tests/test_testresult.py (+160/-1)
To merge this branch: bzr merge lp:~jml/testtools/test-by-test-result
Reviewer Review Type Date Requested Status
testtools committers Pending
Review via email: mp+101622@code.launchpad.net

Commit message

Add TestByTestResult

Description of the change

Adds TestByTestResult, which is already in subunit, to testtools.

Main problem with this one is that it adds yet another mapping of outcome to string. Starting to think that there should be some properly defined constants, or perhaps even objects.

To post a comment you must log in.
249. By Jonathan Lange

Better tag handling.

Preview Diff

[H/L] Next/Prev Comment, [J/K] Next/Prev File, [N/P] Next/Prev Hunk
1=== modified file 'NEWS'
2--- NEWS 2012-04-03 03:28:29 +0000
3+++ NEWS 2012-04-11 18:32:20 +0000
4@@ -16,6 +16,13 @@
5 * ``ErrorHolder`` is now just a function - all the logic is in ``PlaceHolder``.
6 (Robert Collins)
7
8+Improvements
9+------------
10+
11+* ``TestByTestResult``, a ``TestResult`` that calls a method once per test,
12+ added. (Jonathan Lange)
13+
14+
15 0.9.14
16 ~~~~~~
17
18
19=== modified file 'testtools/__init__.py'
20--- testtools/__init__.py 2012-02-16 10:52:15 +0000
21+++ testtools/__init__.py 2012-04-11 18:32:20 +0000
22@@ -1,4 +1,4 @@
23-# Copyright (c) 2008-2011 testtools developers. See LICENSE for details.
24+# Copyright (c) 2008-2012 testtools developers. See LICENSE for details.
25
26 """Extensions to the standard Python unittest library."""
27
28@@ -16,6 +16,7 @@
29 'run_test_with',
30 'TestCase',
31 'TestCommand',
32+ 'TestByTestResult',
33 'TestResult',
34 'TextTestResult',
35 'RunTest',
36@@ -55,6 +56,7 @@
37 from testtools.testresult import (
38 ExtendedToOriginalDecorator,
39 MultiTestResult,
40+ TestByTestResult,
41 TestResult,
42 TextTestResult,
43 ThreadsafeForwardingResult,
44
45=== modified file 'testtools/testresult/__init__.py'
46--- testtools/testresult/__init__.py 2011-01-22 17:56:00 +0000
47+++ testtools/testresult/__init__.py 2012-04-11 18:32:20 +0000
48@@ -1,10 +1,11 @@
49-# Copyright (c) 2009 testtools developers. See LICENSE for details.
50+# Copyright (c) 2009-2012 testtools developers. See LICENSE for details.
51
52 """Test result objects."""
53
54 __all__ = [
55 'ExtendedToOriginalDecorator',
56 'MultiTestResult',
57+ 'TestByTestResult',
58 'TestResult',
59 'TextTestResult',
60 'ThreadsafeForwardingResult',
61@@ -13,6 +14,7 @@
62 from testtools.testresult.real import (
63 ExtendedToOriginalDecorator,
64 MultiTestResult,
65+ TestByTestResult,
66 TestResult,
67 TextTestResult,
68 ThreadsafeForwardingResult,
69
70=== modified file 'testtools/testresult/real.py'
71--- testtools/testresult/real.py 2012-02-09 17:52:15 +0000
72+++ testtools/testresult/real.py 2012-04-11 18:32:20 +0000
73@@ -15,7 +15,10 @@
74 import unittest
75
76 from testtools.compat import all, str_is_unicode, _u
77-from testtools.content import TracebackContent
78+from testtools.content import (
79+ text_content,
80+ TracebackContent,
81+ )
82
83 # From http://docs.python.org/library/datetime.html
84 _ZERO = datetime.timedelta(0)
85@@ -624,6 +627,83 @@
86 return self.decorated.wasSuccessful()
87
88
89+class TestByTestResult(TestResult):
90+ """Call something every time a test completes."""
91+
92+ def __init__(self, on_test):
93+ """Construct a ``TestByTestResult``.
94+
95+ :param on_test: A callable that take a test case, a status (one of
96+ "success", "failure", "error", "skip", or "xfail"), a start time
97+ (a ``datetime`` with timezone), a stop time, an iterable of tags,
98+ and a details dict. Is called at the end of each test (i.e. on
99+ ``stopTest``) with the accumulated values for that test.
100+ """
101+ super(TestByTestResult, self).__init__()
102+ self._on_test = on_test
103+
104+ def startTest(self, test):
105+ super(TestByTestResult, self).startTest(test)
106+ self._start_time = self._now()
107+ # There's no supported (i.e. tested) behaviour that relies on these
108+ # being set, but it makes me more comfortable all the same. -- jml
109+ self._status = None
110+ self._details = None
111+ self._stop_time = None
112+
113+ def stopTest(self, test):
114+ self._stop_time = self._now()
115+ tags = set(self.current_tags)
116+ super(TestByTestResult, self).stopTest(test)
117+ self._on_test(
118+ test=test,
119+ status=self._status,
120+ start_time=self._start_time,
121+ stop_time=self._stop_time,
122+ tags=tags,
123+ details=self._details)
124+
125+ def _err_to_details(self, test, err, details):
126+ if details:
127+ return details
128+ return {'traceback': TracebackContent(err, test)}
129+
130+ def addSuccess(self, test, details=None):
131+ super(TestByTestResult, self).addSuccess(test)
132+ self._status = 'success'
133+ self._details = details
134+
135+ def addFailure(self, test, err=None, details=None):
136+ super(TestByTestResult, self).addFailure(test, err, details)
137+ self._status = 'failure'
138+ self._details = self._err_to_details(test, err, details)
139+
140+ def addError(self, test, err=None, details=None):
141+ super(TestByTestResult, self).addError(test, err, details)
142+ self._status = 'error'
143+ self._details = self._err_to_details(test, err, details)
144+
145+ def addSkip(self, test, reason=None, details=None):
146+ super(TestByTestResult, self).addSkip(test, reason, details)
147+ self._status = 'skip'
148+ if details is None:
149+ details = {'reason': text_content(reason)}
150+ elif reason:
151+ # XXX: What if details already has 'reason' key?
152+ details['reason'] = text_content(reason)
153+ self._details = details
154+
155+ def addExpectedFailure(self, test, err=None, details=None):
156+ super(TestByTestResult, self).addExpectedFailure(test, err, details)
157+ self._status = 'xfail'
158+ self._details = self._err_to_details(test, err, details)
159+
160+ def addUnexpectedSuccess(self, test, details=None):
161+ super(TestByTestResult, self).addUnexpectedSuccess(test, details)
162+ self._status = 'success'
163+ self._details = details
164+
165+
166 class _StringException(Exception):
167 """An exception made from an arbitrary string."""
168
169
170=== modified file 'testtools/tests/test_testresult.py'
171--- testtools/tests/test_testresult.py 2012-02-09 17:52:15 +0000
172+++ testtools/tests/test_testresult.py 2012-04-11 18:32:20 +0000
173@@ -1,4 +1,4 @@
174-# Copyright (c) 2008-2011 testtools developers. See LICENSE for details.
175+# Copyright (c) 2008-2012 testtools developers. See LICENSE for details.
176
177 """Test TestResults and related things."""
178
179@@ -19,6 +19,7 @@
180 MultiTestResult,
181 TestCase,
182 TestResult,
183+ TestByTestResult,
184 TextTestResult,
185 ThreadsafeForwardingResult,
186 testresult,
187@@ -1579,6 +1580,164 @@
188 """))
189
190
191+class TestByTestResultTests(TestCase):
192+
193+ def setUp(self):
194+ super(TestByTestResultTests, self).setUp()
195+ self.log = []
196+ self.result = TestByTestResult(self.on_test)
197+ self.result._now = iter(range(5)).next
198+
199+ def assertCalled(self, **kwargs):
200+ defaults = {
201+ 'test': self,
202+ 'tags': set(),
203+ 'details': None,
204+ 'start_time': 0,
205+ 'stop_time': 1,
206+ }
207+ defaults.update(kwargs)
208+ self.assertEqual([defaults], self.log)
209+
210+ def on_test(self, **kwargs):
211+ self.log.append(kwargs)
212+
213+ def test_no_tests_nothing_reported(self):
214+ self.result.startTestRun()
215+ self.result.stopTestRun()
216+ self.assertEqual([], self.log)
217+
218+ def test_add_success(self):
219+ self.result.startTest(self)
220+ self.result.addSuccess(self)
221+ self.result.stopTest(self)
222+ self.assertCalled(status='success')
223+
224+ def test_add_success_details(self):
225+ self.result.startTest(self)
226+ details = {'foo': 'bar'}
227+ self.result.addSuccess(self, details=details)
228+ self.result.stopTest(self)
229+ self.assertCalled(status='success', details=details)
230+
231+ def test_global_tags(self):
232+ self.result.tags(['foo'], [])
233+ self.result.startTest(self)
234+ self.result.addSuccess(self)
235+ self.result.stopTest(self)
236+ self.assertCalled(status='success', tags=set(['foo']))
237+
238+ def test_local_tags(self):
239+ self.result.tags(['foo'], [])
240+ self.result.startTest(self)
241+ self.result.tags(['bar'], [])
242+ self.result.addSuccess(self)
243+ self.result.stopTest(self)
244+ self.assertCalled(status='success', tags=set(['foo', 'bar']))
245+
246+ def test_add_error(self):
247+ self.result.startTest(self)
248+ try:
249+ 1/0
250+ except ZeroDivisionError:
251+ error = sys.exc_info()
252+ self.result.addError(self, error)
253+ self.result.stopTest(self)
254+ self.assertCalled(
255+ status='error',
256+ details={'traceback': TracebackContent(error, self)})
257+
258+ def test_add_error_details(self):
259+ self.result.startTest(self)
260+ details = {"foo": text_content("bar")}
261+ self.result.addError(self, details=details)
262+ self.result.stopTest(self)
263+ self.assertCalled(status='error', details=details)
264+
265+ def test_add_failure(self):
266+ self.result.startTest(self)
267+ try:
268+ self.fail("intentional failure")
269+ except self.failureException:
270+ failure = sys.exc_info()
271+ self.result.addFailure(self, failure)
272+ self.result.stopTest(self)
273+ self.assertCalled(
274+ status='failure',
275+ details={'traceback': TracebackContent(failure, self)})
276+
277+ def test_add_failure_details(self):
278+ self.result.startTest(self)
279+ details = {"foo": text_content("bar")}
280+ self.result.addFailure(self, details=details)
281+ self.result.stopTest(self)
282+ self.assertCalled(status='failure', details=details)
283+
284+ def test_add_xfail(self):
285+ self.result.startTest(self)
286+ try:
287+ 1/0
288+ except ZeroDivisionError:
289+ error = sys.exc_info()
290+ self.result.addExpectedFailure(self, error)
291+ self.result.stopTest(self)
292+ self.assertCalled(
293+ status='xfail',
294+ details={'traceback': TracebackContent(error, self)})
295+
296+ def test_add_xfail_details(self):
297+ self.result.startTest(self)
298+ details = {"foo": text_content("bar")}
299+ self.result.addExpectedFailure(self, details=details)
300+ self.result.stopTest(self)
301+ self.assertCalled(status='xfail', details=details)
302+
303+ def test_add_unexpected_success(self):
304+ self.result.startTest(self)
305+ details = {'foo': 'bar'}
306+ self.result.addUnexpectedSuccess(self, details=details)
307+ self.result.stopTest(self)
308+ self.assertCalled(status='success', details=details)
309+
310+ def test_add_skip_reason(self):
311+ self.result.startTest(self)
312+ reason = self.getUniqueString()
313+ self.result.addSkip(self, reason)
314+ self.result.stopTest(self)
315+ self.assertCalled(
316+ status='skip', details={'reason': text_content(reason)})
317+
318+ def test_add_skip_details(self):
319+ self.result.startTest(self)
320+ details = {'foo': 'bar'}
321+ self.result.addSkip(self, details=details)
322+ self.result.stopTest(self)
323+ self.assertCalled(status='skip', details=details)
324+
325+ def test_twice(self):
326+ self.result.startTest(self)
327+ self.result.addSuccess(self, details={'foo': 'bar'})
328+ self.result.stopTest(self)
329+ self.result.startTest(self)
330+ self.result.addSuccess(self)
331+ self.result.stopTest(self)
332+ self.assertEqual(
333+ [{'test': self,
334+ 'status': 'success',
335+ 'start_time': 0,
336+ 'stop_time': 1,
337+ 'tags': set(),
338+ 'details': {'foo': 'bar'}},
339+ {'test': self,
340+ 'status': 'success',
341+ 'start_time': 2,
342+ 'stop_time': 3,
343+ 'tags': set(),
344+ 'details': None},
345+ ],
346+ self.log)
347+
348+
349 def test_suite():
350 from unittest import TestLoader
351 return TestLoader().loadTestsFromName(__name__)

Subscribers

People subscribed via source and target branches