Merge lp:~lifeless/testtools/bug-1090582 into lp:~testtools-committers/testtools/trunk

Proposed by Robert Collins
Status: Merged
Merged at revision: 298
Proposed branch: lp:~lifeless/testtools/bug-1090582
Merge into: lp:~testtools-committers/testtools/trunk
Diff against target: 617 lines (+231/-50)
8 files modified
NEWS (+11/-0)
testtools/run.py (+13/-6)
testtools/testresult/doubles.py (+18/-0)
testtools/testresult/real.py (+101/-32)
testtools/tests/helpers.py (+4/-0)
testtools/tests/test_distutilscmd.py (+8/-8)
testtools/tests/test_run.py (+21/-4)
testtools/tests/test_testresult.py (+55/-0)
To merge this branch: bzr merge lp:~lifeless/testtools/bug-1090582
Reviewer Review Type Date Requested Status
Jonathan Lange Needs Fixing
Review via email: mp+140067@code.launchpad.net

Description of the change

Our CLI help says we support -f / --failfast. We don't. This makes us.

To post a comment you must log in.
Revision history for this message
Jonathan Lange (jml) wrote :

Thanks for the patch. All looks pretty straight-forward & well-tested.

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

Oops. Tried to merge this, ran the tests and got errors.

=====================================================================
ERROR: testtools.tests.test_distutilscmd.TestCommandTest.test_test_module
----------------------------------------------------------------------
Traceback (most recent call last):
  File "testtools/tests/test_distutilscmd.py", line 55, in test_test_module
    runner_stdout = self.useFixture(fixtures.DetailStream('stdout')).stream
AttributeError: 'module' object has no attribute 'DetailStream'
======================================================================
ERROR: testtools.tests.test_distutilscmd.TestCommandTest.test_test_suite
----------------------------------------------------------------------
Traceback (most recent call last):
  File "testtools/tests/test_distutilscmd.py", line 75, in test_test_suite
    runner_stdout = self.useFixture(fixtures.DetailStream('stdout')).stream
AttributeError: 'module' object has no attribute 'DetailStream'
======================================================================
ERROR: testtools.tests.test_run.TestRun.test_run_failfast
----------------------------------------------------------------------
Traceback (most recent call last):
  File "testtools/tests/test_run.py", line 82, in test_run_failfast
    runner_stdout = self.useFixture(fixtures.DetailStream('stdout')).stream
AttributeError: 'module' object has no attribute 'DetailStream'

Appears this branch refers to ``fixtures.DetailStream``, but that doesn't seem to be available in any version of python-fixtures, including trunk.

review: Needs Fixing
Revision history for this message
Robert Collins (lifeless) wrote :

Its in fixtures on pypi.

lp:~lifeless/testtools/bug-1090582 updated
296. By Robert Collins

Adjust trunk for TestToolsTestRunner change.

Preview Diff

[H/L] Next/Prev Comment, [J/K] Next/Prev File, [N/P] Next/Prev Hunk
1=== modified file 'NEWS'
2--- NEWS 2012-12-15 14:11:14 +0000
3+++ NEWS 2012-12-16 12:29:26 +0000
4@@ -6,9 +6,20 @@
5 NEXT
6 ~~~~
7
8+Changes
9+-------
10+
11+* ``run.TestToolsTestRunner`` now accepts the verbosity, buffer and failfast
12+ arguments the upstream python TestProgram code wants to give it, making it
13+ possible to support them in a compatible fashion. (Robert Collins)
14+
15 Improvements
16 ------------
17
18+* ``testtools.run`` now supports the ``-f`` or ``--failfast`` parameter.
19+ Previously it was advertised in the help but ignored.
20+ (Robert Collins, #1090582)
21+
22 * ``AnyMatch`` added, a new matcher that matches when any item in a collection
23 matches the given matcher. (Jonathan Lange)
24
25
26=== modified file 'testtools/run.py'
27--- testtools/run.py 2012-11-29 11:40:17 +0000
28+++ testtools/run.py 2012-12-16 12:29:26 +0000
29@@ -35,12 +35,19 @@
30 class TestToolsTestRunner(object):
31 """ A thunk object to support unittest.TestProgram."""
32
33- def __init__(self, stdout):
34- self.stdout = stdout
35+ def __init__(self, verbosity=None, failfast=None, buffer=None):
36+ """Create a TestToolsTestRunner.
37+
38+ :param verbosity: Ignored.
39+ :param failfast: Stop running tests at the first failure.
40+ :param buffer: Ignored.
41+ """
42+ self.failfast = failfast
43
44 def run(self, test):
45 "Run the given test case or test suite."
46- result = TextTestResult(unicode_output_stream(self.stdout))
47+ result = TextTestResult(
48+ unicode_output_stream(sys.stdout), failfast=self.failfast)
49 result.startTestRun()
50 try:
51 return test.run(result)
52@@ -307,7 +314,7 @@
53 and getattr(unittest, 'installHandler', None) is not None):
54 unittest.installHandler()
55 if self.testRunner is None:
56- self.testRunner = TestToolsTestRunner(sys.stdout)
57+ self.testRunner = TestToolsTestRunner
58 if isinstance(self.testRunner, classtypes()):
59 try:
60 testRunner = self.testRunner(verbosity=self.verbosity,
61@@ -325,8 +332,8 @@
62 ################
63
64 def main(argv, stdout):
65- runner = TestToolsTestRunner(stdout)
66- program = TestProgram(argv=argv, testRunner=runner, stdout=stdout)
67+ program = TestProgram(argv=argv, testRunner=TestToolsTestRunner,
68+ stdout=stdout)
69
70 if __name__ == '__main__':
71 main(sys.argv, sys.stdout)
72
73=== modified file 'testtools/testresult/doubles.py'
74--- testtools/testresult/doubles.py 2012-04-12 16:26:02 +0000
75+++ testtools/testresult/doubles.py 2012-12-16 12:29:26 +0000
76@@ -19,6 +19,7 @@
77 self._events = []
78 self.shouldStop = False
79 self._was_successful = True
80+ self.testsRun = 0
81
82
83 class Python26TestResult(LoggingBase):
84@@ -37,6 +38,7 @@
85
86 def startTest(self, test):
87 self._events.append(('startTest', test))
88+ self.testsRun += 1
89
90 def stop(self):
91 self.shouldStop = True
92@@ -51,6 +53,20 @@
93 class Python27TestResult(Python26TestResult):
94 """A precisely python 2.7 like test result, that logs."""
95
96+ def __init__(self):
97+ super(Python27TestResult, self).__init__()
98+ self.failfast = False
99+
100+ def addError(self, test, err):
101+ super(Python27TestResult, self).addError(test, err)
102+ if self.failfast:
103+ self.stop()
104+
105+ def addFailure(self, test, err):
106+ super(Python27TestResult, self).addFailure(test, err)
107+ if self.failfast:
108+ self.stop()
109+
110 def addExpectedFailure(self, test, err):
111 self._events.append(('addExpectedFailure', test, err))
112
113@@ -59,6 +75,8 @@
114
115 def addUnexpectedSuccess(self, test):
116 self._events.append(('addUnexpectedSuccess', test))
117+ if self.failfast:
118+ self.stop()
119
120 def startTestRun(self):
121 self._events.append(('startTestRun',))
122
123=== modified file 'testtools/testresult/real.py'
124--- testtools/testresult/real.py 2012-09-21 15:15:24 +0000
125+++ testtools/testresult/real.py 2012-12-16 12:29:26 +0000
126@@ -21,6 +21,7 @@
127 text_content,
128 TracebackContent,
129 )
130+from testtools.helpers import safe_hasattr
131 from testtools.tags import TagContext
132
133 # From http://docs.python.org/library/datetime.html
134@@ -60,11 +61,12 @@
135 :ivar skip_reasons: A dict of skip-reasons -> list of tests. See addSkip.
136 """
137
138- def __init__(self):
139+ def __init__(self, failfast=False):
140 # startTestRun resets all attributes, and older clients don't know to
141 # call startTestRun, so it is called once here.
142 # Because subclasses may reasonably not expect this, we call the
143 # specific version we want to run.
144+ self.failfast = failfast
145 TestResult.startTestRun(self)
146
147 def addExpectedFailure(self, test, err=None, details=None):
148@@ -89,6 +91,8 @@
149 """
150 self.errors.append((test,
151 self._err_details_to_string(test, err, details)))
152+ if self.failfast:
153+ self.stop()
154
155 def addFailure(self, test, err=None, details=None):
156 """Called when an error has occurred. 'err' is a tuple of values as
157@@ -99,6 +103,8 @@
158 """
159 self.failures.append((test,
160 self._err_details_to_string(test, err, details)))
161+ if self.failfast:
162+ self.stop()
163
164 def addSkip(self, test, reason=None, details=None):
165 """Called when a test has been skipped rather than running.
166@@ -131,6 +137,8 @@
167 def addUnexpectedSuccess(self, test, details=None):
168 """Called when a test was expected to fail, but succeed."""
169 self.unexpectedSuccesses.append(test)
170+ if self.failfast:
171+ self.stop()
172
173 def wasSuccessful(self):
174 """Has this result been successful so far?
175@@ -174,6 +182,8 @@
176 pristine condition ready for use in another test run. Note that this
177 is different from Python 2.7's startTestRun, which does nothing.
178 """
179+ # failfast is reset by the super __init__, so stash it.
180+ failfast = self.failfast
181 super(TestResult, self).__init__()
182 self.skip_reasons = {}
183 self.__now = None
184@@ -181,6 +191,7 @@
185 # -- Start: As per python 2.7 --
186 self.expectedFailures = []
187 self.unexpectedSuccesses = []
188+ self.failfast = failfast
189 # -- End: As per python 2.7 --
190
191 def stopTestRun(self):
192@@ -236,8 +247,9 @@
193 """A test result that dispatches to many test results."""
194
195 def __init__(self, *results):
196+ # Setup _results first, as the base class __init__ assigns to failfast.
197+ self._results = list(map(ExtendedToOriginalDecorator, results))
198 super(MultiTestResult, self).__init__()
199- self._results = list(map(ExtendedToOriginalDecorator, results))
200
201 def __repr__(self):
202 return '<%s (%s)>' % (
203@@ -248,10 +260,26 @@
204 getattr(result, message)(*args, **kwargs)
205 for result in self._results)
206
207+ def _get_failfast(self):
208+ return getattr(self._results[0], 'failfast', False)
209+ def _set_failfast(self, value):
210+ self._dispatch('__setattr__', 'failfast', value)
211+ failfast = property(_get_failfast, _set_failfast)
212+
213+ def _get_shouldStop(self):
214+ return any(self._dispatch('__getattr__', 'shouldStop'))
215+ def _set_shouldStop(self, value):
216+ # Called because we subclass TestResult. Probably should not do that.
217+ pass
218+ shouldStop = property(_get_shouldStop, _set_shouldStop)
219+
220 def startTest(self, test):
221 super(MultiTestResult, self).startTest(test)
222 return self._dispatch('startTest', test)
223
224+ def stop(self):
225+ return self._dispatch('stop')
226+
227 def stopTest(self, test):
228 super(MultiTestResult, self).stopTest(test)
229 return self._dispatch('stopTest', test)
230@@ -303,9 +331,9 @@
231 class TextTestResult(TestResult):
232 """A TestResult which outputs activity to a text stream."""
233
234- def __init__(self, stream):
235+ def __init__(self, stream, failfast=False):
236 """Construct a TextTestResult writing to stream."""
237- super(TextTestResult, self).__init__()
238+ super(TextTestResult, self).__init__(failfast=failfast)
239 self.stream = stream
240 self.sep1 = '=' * 70 + '\n'
241 self.sep2 = '-' * 70 + '\n'
242@@ -451,6 +479,24 @@
243 finally:
244 self.semaphore.release()
245
246+ def _get_shouldStop(self):
247+ self.semaphore.acquire()
248+ try:
249+ return self.result.shouldStop
250+ finally:
251+ self.semaphore.release()
252+ def _set_shouldStop(self, value):
253+ # Another case where we should not subclass TestResult
254+ pass
255+ shouldStop = property(_get_shouldStop, _set_shouldStop)
256+
257+ def stop(self):
258+ self.semaphore.acquire()
259+ try:
260+ self.result.stop()
261+ finally:
262+ self.semaphore.release()
263+
264 def stopTestRun(self):
265 self.semaphore.acquire()
266 try:
267@@ -507,6 +553,8 @@
268 def __init__(self, decorated):
269 self.decorated = decorated
270 self._tags = TagContext()
271+ # Only used for old TestResults that do not have failfast.
272+ self._failfast = False
273
274 def __repr__(self):
275 return '<%s %r>' % (self.__class__.__name__, self.decorated)
276@@ -515,14 +563,18 @@
277 return getattr(self.decorated, name)
278
279 def addError(self, test, err=None, details=None):
280- self._check_args(err, details)
281- if details is not None:
282- try:
283- return self.decorated.addError(test, details=details)
284- except TypeError:
285- # have to convert
286- err = self._details_to_exc_info(details)
287- return self.decorated.addError(test, err)
288+ try:
289+ self._check_args(err, details)
290+ if details is not None:
291+ try:
292+ return self.decorated.addError(test, details=details)
293+ except TypeError:
294+ # have to convert
295+ err = self._details_to_exc_info(details)
296+ return self.decorated.addError(test, err)
297+ finally:
298+ if self.failfast:
299+ self.stop()
300
301 def addExpectedFailure(self, test, err=None, details=None):
302 self._check_args(err, details)
303@@ -539,14 +591,18 @@
304 return addExpectedFailure(test, err)
305
306 def addFailure(self, test, err=None, details=None):
307- self._check_args(err, details)
308- if details is not None:
309- try:
310- return self.decorated.addFailure(test, details=details)
311- except TypeError:
312- # have to convert
313- err = self._details_to_exc_info(details)
314- return self.decorated.addFailure(test, err)
315+ try:
316+ self._check_args(err, details)
317+ if details is not None:
318+ try:
319+ return self.decorated.addFailure(test, details=details)
320+ except TypeError:
321+ # have to convert
322+ err = self._details_to_exc_info(details)
323+ return self.decorated.addFailure(test, err)
324+ finally:
325+ if self.failfast:
326+ self.stop()
327
328 def addSkip(self, test, reason=None, details=None):
329 self._check_args(reason, details)
330@@ -565,18 +621,22 @@
331 return addSkip(test, reason)
332
333 def addUnexpectedSuccess(self, test, details=None):
334- outcome = getattr(self.decorated, 'addUnexpectedSuccess', None)
335- if outcome is None:
336- try:
337- test.fail("")
338- except test.failureException:
339- return self.addFailure(test, sys.exc_info())
340- if details is not None:
341- try:
342- return outcome(test, details=details)
343- except TypeError:
344- pass
345- return outcome(test)
346+ try:
347+ outcome = getattr(self.decorated, 'addUnexpectedSuccess', None)
348+ if outcome is None:
349+ try:
350+ test.fail("")
351+ except test.failureException:
352+ return self.addFailure(test, sys.exc_info())
353+ if details is not None:
354+ try:
355+ return outcome(test, details=details)
356+ except TypeError:
357+ pass
358+ return outcome(test)
359+ finally:
360+ if self.failfast:
361+ self.stop()
362
363 def addSuccess(self, test, details=None):
364 if details is not None:
365@@ -614,6 +674,15 @@
366 except AttributeError:
367 return
368
369+ def _get_failfast(self):
370+ return getattr(self.decorated, 'failfast', self._failfast)
371+ def _set_failfast(self, value):
372+ if safe_hasattr(self.decorated, 'failfast'):
373+ self.decorated.failfast = value
374+ else:
375+ self._failfast = value
376+ failfast = property(_get_failfast, _set_failfast)
377+
378 def progress(self, offset, whence):
379 method = getattr(self.decorated, 'progress', None)
380 if method is None:
381
382=== modified file 'testtools/tests/helpers.py'
383--- testtools/tests/helpers.py 2012-02-04 16:47:09 +0000
384+++ testtools/tests/helpers.py 2012-12-16 12:29:26 +0000
385@@ -38,6 +38,10 @@
386 self._events.append(('startTest', test))
387 super(LoggingResult, self).startTest(test)
388
389+ def stop(self):
390+ self._events.append('stop')
391+ super(LoggingResult, self).stop()
392+
393 def stopTest(self, test):
394 self._events.append(('stopTest', test))
395 super(LoggingResult, self).stopTest(test)
396
397=== modified file 'testtools/tests/test_distutilscmd.py'
398--- testtools/tests/test_distutilscmd.py 2011-07-26 23:56:06 +0000
399+++ testtools/tests/test_distutilscmd.py 2012-12-16 12:29:26 +0000
400@@ -52,7 +52,7 @@
401
402 def test_test_module(self):
403 self.useFixture(SampleTestFixture())
404- stream = BytesIO()
405+ runner_stdout = self.useFixture(fixtures.DetailStream('stdout')).stream
406 dist = Distribution()
407 dist.script_name = 'setup.py'
408 dist.script_args = ['test']
409@@ -60,10 +60,10 @@
410 dist.command_options = {
411 'test': {'test_module': ('command line', 'testtools.runexample')}}
412 cmd = dist.reinitialize_command('test')
413- cmd.runner.stdout = stream
414- dist.run_command('test')
415+ with fixtures.MonkeyPatch('sys.stdout', runner_stdout):
416+ dist.run_command('test')
417 self.assertThat(
418- stream.getvalue(),
419+ runner_stdout.getvalue(),
420 MatchesRegex(_b("""Tests running...
421
422 Ran 2 tests in \\d.\\d\\d\\ds
423@@ -72,7 +72,7 @@
424
425 def test_test_suite(self):
426 self.useFixture(SampleTestFixture())
427- stream = BytesIO()
428+ runner_stdout = self.useFixture(fixtures.DetailStream('stdout')).stream
429 dist = Distribution()
430 dist.script_name = 'setup.py'
431 dist.script_args = ['test']
432@@ -82,10 +82,10 @@
433 'test_suite': (
434 'command line', 'testtools.runexample.test_suite')}}
435 cmd = dist.reinitialize_command('test')
436- cmd.runner.stdout = stream
437- dist.run_command('test')
438+ with fixtures.MonkeyPatch('sys.stdout', runner_stdout):
439+ dist.run_command('test')
440 self.assertThat(
441- stream.getvalue(),
442+ runner_stdout.getvalue(),
443 MatchesRegex(_b("""Tests running...
444
445 Ran 2 tests in \\d.\\d\\d\\ds
446
447=== modified file 'testtools/tests/test_run.py'
448--- testtools/tests/test_run.py 2011-07-26 23:27:18 +0000
449+++ testtools/tests/test_run.py 2012-12-16 12:29:26 +0000
450@@ -2,6 +2,8 @@
451
452 """Tests for the test runner logic."""
453
454+from unittest import TestSuite
455+
456 from testtools.compat import (
457 _b,
458 StringIO,
459@@ -11,6 +13,7 @@
460
461 import testtools
462 from testtools import TestCase, run
463+from testtools.matchers import Contains
464
465
466 if fixtures:
467@@ -41,9 +44,12 @@
468
469 class TestRun(TestCase):
470
471+ def setUp(self):
472+ super(TestRun, self).setUp()
473+ if fixtures is None:
474+ self.skipTest("Need fixtures")
475+
476 def test_run_list(self):
477- if fixtures is None:
478- self.skipTest("Need fixtures")
479 self.useFixture(SampleTestFixture())
480 out = StringIO()
481 run.main(['prog', '-l', 'testtools.runexample.test_suite'], out)
482@@ -52,8 +58,6 @@
483 """, out.getvalue())
484
485 def test_run_load_list(self):
486- if fixtures is None:
487- self.skipTest("Need fixtures")
488 self.useFixture(SampleTestFixture())
489 out = StringIO()
490 # We load two tests - one that exists and one that doesn't, and we
491@@ -74,6 +78,19 @@
492 self.assertEqual("""testtools.runexample.TestFoo.test_bar
493 """, out.getvalue())
494
495+ def test_run_failfast(self):
496+ runner_stdout = self.useFixture(fixtures.DetailStream('stdout')).stream
497+ class Failing(TestCase):
498+ def test_a(self):
499+ self.fail('a')
500+ def test_b(self):
501+ self.fail('b')
502+ runner = run.TestToolsTestRunner(failfast=True)
503+ with fixtures.MonkeyPatch('sys.stdout', runner_stdout):
504+ runner.run(TestSuite([Failing('test_a'), Failing('test_b')]))
505+ self.assertThat(runner_stdout.getvalue(), Contains('Ran 1 test'))
506+
507+
508
509 def test_suite():
510 from unittest import TestLoader
511
512=== modified file 'testtools/tests/test_testresult.py'
513--- testtools/tests/test_testresult.py 2012-10-19 14:29:59 +0000
514+++ testtools/tests/test_testresult.py 2012-12-16 12:29:26 +0000
515@@ -12,6 +12,7 @@
516 import sys
517 import tempfile
518 import threading
519+from unittest import TestSuite
520 import warnings
521
522 from testtools import (
523@@ -43,6 +44,7 @@
524 TracebackContent,
525 )
526 from testtools.content_type import ContentType, UTF8_TEXT
527+from testtools.helpers import safe_hasattr
528 from testtools.matchers import (
529 Contains,
530 DocTestMatches,
531@@ -142,6 +144,11 @@
532 result.stopTest(self)
533 self.assertTrue(result.wasSuccessful())
534
535+ def test_stop_sets_shouldStop(self):
536+ result = self.makeResult()
537+ result.stop()
538+ self.assertTrue(result.shouldStop)
539+
540
541 class Python27Contract(Python26Contract):
542
543@@ -193,6 +200,17 @@
544 result.startTestRun()
545 result.stopTestRun()
546
547+ def test_failfast(self):
548+ result = self.makeResult()
549+ result.failfast = True
550+ class Failing(TestCase):
551+ def test_a(self):
552+ self.fail('a')
553+ def test_b(self):
554+ self.fail('b')
555+ TestSuite([Failing('test_a'), Failing('test_b')]).run(result)
556+ self.assertEqual(1, result.testsRun)
557+
558
559 class TagsContract(Python27Contract):
560 """Tests to ensure correct tagging behaviour.
561@@ -566,12 +584,36 @@
562 # `TestResult`s.
563 self.assertResultLogsEqual([])
564
565+ def test_failfast_get(self):
566+ # Reading reads from the first one - arbitrary choice.
567+ self.assertEqual(False, self.multiResult.failfast)
568+ self.result1.failfast = True
569+ self.assertEqual(True, self.multiResult.failfast)
570+
571+ def test_failfast_set(self):
572+ # Writing writes to all.
573+ self.multiResult.failfast = True
574+ self.assertEqual(True, self.result1.failfast)
575+ self.assertEqual(True, self.result2.failfast)
576+
577+ def test_shouldStop(self):
578+ self.assertFalse(self.multiResult.shouldStop)
579+ self.result2.stop()
580+ # NB: result1 is not stopped: MultiTestResult has to combine the
581+ # values.
582+ self.assertTrue(self.multiResult.shouldStop)
583+
584 def test_startTest(self):
585 # Calling `startTest` on a `MultiTestResult` calls `startTest` on all
586 # its `TestResult`s.
587 self.multiResult.startTest(self)
588 self.assertResultLogsEqual([('startTest', self)])
589
590+ def test_stop(self):
591+ self.assertFalse(self.multiResult.shouldStop)
592+ self.multiResult.stop()
593+ self.assertResultLogsEqual(['stop'])
594+
595 def test_stopTest(self):
596 # Calling `stopTest` on a `MultiTestResult` calls `stopTest` on all
597 # its `TestResult`s.
598@@ -1176,6 +1218,19 @@
599 class TestExtendedToOriginalResultDecorator(
600 TestExtendedToOriginalResultDecoratorBase):
601
602+ def test_failfast_py26(self):
603+ self.make_26_result()
604+ self.assertEqual(False, self.converter.failfast)
605+ self.converter.failfast = True
606+ self.assertFalse(safe_hasattr(self.converter.decorated, 'failfast'))
607+
608+ def test_failfast_py27(self):
609+ self.make_27_result()
610+ self.assertEqual(False, self.converter.failfast)
611+ # setting it should write it to the backing result
612+ self.converter.failfast = True
613+ self.assertEqual(True, self.converter.decorated.failfast)
614+
615 def test_progress_py26(self):
616 self.make_26_result()
617 self.converter.progress(1, 2)

Subscribers

People subscribed via source and target branches