Merge lp:~lifeless/subunit/addSkip into lp:~subunit/subunit/trunk

Proposed by Robert Collins
Status: Merged
Approved by: Jonathan Lange
Approved revision: 58
Merged at revision: not available
Proposed branch: lp:~lifeless/subunit/addSkip
Merge into: lp:~subunit/subunit/trunk
Diff against target: None lines
To merge this branch: bzr merge lp:~lifeless/subunit/addSkip
Reviewer Review Type Date Requested Status
Jonathan Lange Approve
Review via email: mp+4033@code.launchpad.net
To post a comment you must log in.
Revision history for this message
Robert Collins (lifeless) wrote :

This includes filters; best to review after that is reviewed. Also when is that getting reviewed ? :)

lp:~lifeless/subunit/addSkip updated
58. By Robert Collins

subunit-filter can now filter skips too.

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

Fine to land, along with the tweaks suggested for lp:~lifeless/subunit/filter.

review: Approve

Preview Diff

[H/L] Next/Prev Comment, [J/K] Next/Prev File, [N/P] Next/Prev Hunk
1=== modified file 'README'
2--- README 2009-02-15 11:55:00 +0000
3+++ README 2009-02-28 09:27:25 +0000
4@@ -40,6 +40,8 @@
5 stream on-the-fly. Currently subunit provides:
6 * tap2subunit - convert perl's TestAnythingProtocol to subunit.
7 * subunit2pyunit - convert a subunit stream to pyunit test results.
8+ * subunit-filter - filter out tests from a subunit stream.
9+ * subunit-ls - list the tests present in a subunit stream.
10 * subunit-stats - generate a summary of a subunit stream.
11 * subunit-tags - add or remove tags from a stream.
12
13@@ -200,8 +202,10 @@
14 Currently this is not exposed at the python API layer.
15
16 The skip result is used to indicate a test that was found by the runner but not
17-fully executed due to some policy or dependency issue. Currently this is
18-represented in Python as a successful test.
19+fully executed due to some policy or dependency issue. This is represented in
20+python using the addSkip interface that testtools
21+(https://edge.launchpad.net/testtools) defines. When communicating with a non
22+skip aware test result, the test is reported as an error.
23 The xfail result is used to indicate a test that was expected to fail failing
24 in the expected manner. As this is a normal condition for such tests it is
25 represented as a successful test in Python.
26
27=== added file 'filters/subunit-filter'
28--- filters/subunit-filter 1970-01-01 00:00:00 +0000
29+++ filters/subunit-filter 2009-02-22 07:51:04 +0000
30@@ -0,0 +1,50 @@
31+#!/usr/bin/env python
32+# subunit: extensions to python unittest to get test results from subprocesses.
33+# Copyright (C) 2008 Robert Collins <robertc@robertcollins.net>
34+#
35+# This program is free software; you can redistribute it and/or modify
36+# it under the terms of the GNU General Public License as published by
37+# the Free Software Foundation; either version 2 of the License, or
38+# (at your option) any later version.
39+#
40+# This program is distributed in the hope that it will be useful,
41+# but WITHOUT ANY WARRANTY; without even the implied warranty of
42+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
43+# GNU General Public License for more details.
44+#
45+# You should have received a copy of the GNU General Public License
46+# along with this program; if not, write to the Free Software
47+# Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA
48+#
49+
50+"""Filter a subunit stream to include/exclude tests.
51+
52+The default is to strip successful tests.
53+"""
54+
55+from optparse import OptionParser
56+import sys
57+import unittest
58+
59+from subunit import ProtocolTestCase, TestResultFilter, TestProtocolClient
60+
61+parser = OptionParser(description=__doc__)
62+parser.add_option("--error", action="store_false",
63+ help="include errors", default=False, dest="error")
64+parser.add_option("-e", "--no-error", action="store_true",
65+ help="exclude errors", dest="error")
66+parser.add_option("--failure", action="store_false",
67+ help="include failures", default=False, dest="failure")
68+parser.add_option("-f", "--no-failure", action="store_true",
69+ help="include failures", dest="failure")
70+parser.add_option("-s", "--success", action="store_false",
71+ help="include successes", dest="success")
72+parser.add_option("--no-success", action="store_true",
73+ help="exclude successes", default=True, dest="success")
74+(options, args) = parser.parse_args()
75+result = TestProtocolClient(sys.stdout)
76+result = TestResultFilter(result, filter_error=options.error, filter_failure=options.failure,
77+ filter_success=options.success)
78+test = ProtocolTestCase(sys.stdin)
79+test.run(result)
80+sys.exit(0)
81
82=== added file 'filters/subunit-ls'
83--- filters/subunit-ls 1970-01-01 00:00:00 +0000
84+++ filters/subunit-ls 2009-02-23 10:54:28 +0000
85@@ -0,0 +1,62 @@
86+#!/usr/bin/env python
87+# subunit: extensions to python unittest to get test results from subprocesses.
88+# Copyright (C) 2008 Robert Collins <robertc@robertcollins.net>
89+#
90+# This program is free software; you can redistribute it and/or modify
91+# it under the terms of the GNU General Public License as published by
92+# the Free Software Foundation; either version 2 of the License, or
93+# (at your option) any later version.
94+#
95+# This program is distributed in the hope that it will be useful,
96+# but WITHOUT ANY WARRANTY; without even the implied warranty of
97+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
98+# GNU General Public License for more details.
99+#
100+# You should have received a copy of the GNU General Public License
101+# along with this program; if not, write to the Free Software
102+# Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA
103+#
104+
105+"""List tests in a subunit stream."""
106+
107+import sys
108+import unittest
109+
110+from subunit import ProtocolTestCase
111+
112+class FilterResult(unittest.TestResult):
113+ """Filter test objects for display."""
114+
115+ def __init__(self, stream):
116+ """Create a FilterResult object outputting to stream."""
117+ unittest.TestResult.__init__(self)
118+ self._stream = stream
119+ self.failed_tests = 0
120+
121+ def addError(self, test, err):
122+ self.failed_tests += 1
123+ self.reportTest(test)
124+
125+ def addFailure(self, test, err):
126+ self.failed_tests += 1
127+ self.reportTest(test)
128+
129+ def addSuccess(self, test):
130+ self.reportTest(test)
131+
132+ def reportTest(self, test):
133+ self._stream.write(test.id() + '\n')
134+
135+ def wasSuccessful(self):
136+ "Tells whether or not this result was a success"
137+ return self.failed_tests == 0
138+
139+
140+result = FilterResult(sys.stdout)
141+test = ProtocolTestCase(sys.stdin)
142+test.run(result)
143+if result.wasSuccessful():
144+ exit_code = 0
145+else:
146+ exit_code = 1
147+sys.exit(exit_code)
148
149=== modified file 'python/subunit/__init__.py'
150--- python/subunit/__init__.py 2009-02-15 11:55:00 +0000
151+++ python/subunit/__init__.py 2009-02-28 09:27:25 +0000
152@@ -133,7 +133,7 @@
153 self.current_test_description == line[offset:-1]):
154 self.state = TestProtocolServer.OUTSIDE_TEST
155 self.current_test_description = None
156- self.client.addSuccess(self._current_test)
157+ self._skip_or_error()
158 self.client.stopTest(self._current_test)
159 elif (self.state == TestProtocolServer.TEST_STARTED and
160 self.current_test_description + " [" == line[offset:-1]):
161@@ -142,6 +142,16 @@
162 else:
163 self.stdOutLineReceived(line)
164
165+ def _skip_or_error(self, message=None):
166+ """Report the current test as a skip if possible, or else an error."""
167+ addSkip = getattr(self.client, 'addSkip', None)
168+ if not callable(addSkip):
169+ self.client.addError(self._current_test, RemoteError(message))
170+ else:
171+ if not message:
172+ message = "No reason given"
173+ addSkip(self._current_test, message)
174+
175 def _addSuccess(self, offset, line):
176 if (self.state == TestProtocolServer.TEST_STARTED and
177 self.current_test_description == line[offset:-1]):
178@@ -173,8 +183,12 @@
179 self.client.addError(self._current_test,
180 RemoteError(self._message))
181 self.client.stopTest(self._current_test)
182+ elif self.state == TestProtocolServer.READING_SKIP:
183+ self.state = TestProtocolServer.OUTSIDE_TEST
184+ self.current_test_description = None
185+ self._skip_or_error(self._message)
186+ self.client.stopTest(self._current_test)
187 elif self.state in (
188- TestProtocolServer.READING_SKIP,
189 TestProtocolServer.READING_SUCCESS,
190 TestProtocolServer.READING_XFAIL,
191 ):
192@@ -314,6 +328,12 @@
193 self._stream.write("%s\n" % line)
194 self._stream.write("]\n")
195
196+ def addSkip(self, test, reason):
197+ """Report a skipped test."""
198+ self._stream.write("skip: %s [\n" % test.id())
199+ self._stream.write("%s\n" % reason)
200+ self._stream.write("]\n")
201+
202 def addSuccess(self, test):
203 """Report a success in a test."""
204 self._stream.write("successful: %s\n" % test.id())
205@@ -363,7 +383,7 @@
206 return self.__description
207
208 def id(self):
209- return "%s.%s" % (self._strclass(), self.__description)
210+ return "%s" % (self.__description,)
211
212 def __str__(self):
213 return "%s (%s)" % (self.__description, self._strclass())
214@@ -651,6 +671,7 @@
215 unittest.TestResult.__init__(self)
216 self._stream = stream
217 self.failed_tests = 0
218+ self.skipped_tests = 0
219 self.tags = set()
220
221 @property
222@@ -663,16 +684,20 @@
223 def addFailure(self, test, err):
224 self.failed_tests += 1
225
226+ def addSkip(self, test, reason):
227+ self.skipped_tests += 1
228+
229 def formatStats(self):
230- self._stream.write("Total tests: %5d\n" % self.total_tests)
231- self._stream.write("Passed tests: %5d\n" % self.passed_tests)
232- self._stream.write("Failed tests: %5d\n" % self.failed_tests)
233+ self._stream.write("Total tests: %5d\n" % self.total_tests)
234+ self._stream.write("Passed tests: %5d\n" % self.passed_tests)
235+ self._stream.write("Failed tests: %5d\n" % self.failed_tests)
236+ self._stream.write("Skipped tests: %5d\n" % self.skipped_tests)
237 tags = sorted(self.tags)
238 self._stream.write("Tags: %s\n" % (", ".join(tags)))
239
240 @property
241 def passed_tests(self):
242- return self.total_tests - self.failed_tests
243+ return self.total_tests - self.failed_tests - self.skipped_tests
244
245 def stopTest(self, test):
246 unittest.TestResult.stopTest(self, test)
247@@ -681,3 +706,66 @@
248 def wasSuccessful(self):
249 """Tells whether or not this result was a success"""
250 return self.failed_tests == 0
251+
252+
253+class TestResultFilter(unittest.TestResult):
254+ """A pyunit TestResult interface implementation which filters tests.
255+
256+ Tests that pass the filter are handed on to another TestResult instance
257+ for further processing/reporting. To obtain the filtered results,
258+ the other instance must be interrogated.
259+
260+ :ivar result: The result that tests are passed to after filtering.
261+ """
262+
263+ def __init__(self, result, filter_error=False, filter_failure=False,
264+ filter_success=True, filter_skip=False):
265+ """Create a FilterResult object filtering to result.
266+
267+ :param filter_error: Filter out errors.
268+ :param filter_failure: Filter out failures.
269+ :param filter_success: Filter out successful tests.
270+ :param filter_skip: Filter out skipped tests.
271+ """
272+ unittest.TestResult.__init__(self)
273+ self.result = result
274+ self._filter_error = filter_error
275+ self._filter_failure = filter_failure
276+ self._filter_success = filter_success
277+ self._filter_skip = filter_skip
278+
279+ def addError(self, test, err):
280+ if not self._filter_error:
281+ self.result.startTest(test)
282+ self.result.addError(test, err)
283+ self.result.stopTest(test)
284+
285+ def addFailure(self, test, err):
286+ if not self._filter_failure:
287+ self.result.startTest(test)
288+ self.result.addFailure(test, err)
289+ self.result.stopTest(test)
290+
291+ def addSkip(self, test, reason):
292+ if not self._filter_skip:
293+ self.result.startTest(test)
294+ # This is duplicated, it would be nice to have on a 'calls
295+ # TestResults' mixin perhaps.
296+ addSkip = getattr(self.result, 'addSkip', None)
297+ if not callable(addSkip):
298+ self.result.addError(test, RemoteError(reason))
299+ else:
300+ self.result.addSkip(test, reason)
301+ self.result.stopTest(test)
302+
303+ def addSuccess(self, test):
304+ if not self._filter_success:
305+ self.result.startTest(test)
306+ self.result.addSuccess(test)
307+ self.result.stopTest(test)
308+
309+ def id_to_orig_id(self, id):
310+ if id.startswith("subunit.RemotedTestCase."):
311+ return id[len("subunit.RemotedTestCase."):]
312+ return id
313+
314
315=== modified file 'python/subunit/tests/__init__.py'
316--- python/subunit/tests/__init__.py 2008-12-09 01:00:03 +0000
317+++ python/subunit/tests/__init__.py 2009-02-22 06:28:08 +0000
318@@ -19,6 +19,7 @@
319
320 from subunit.tests import (
321 TestUtil,
322+ test_subunit_filter,
323 test_subunit_stats,
324 test_subunit_tags,
325 test_tap2subunit,
326@@ -29,6 +30,7 @@
327 result = TestUtil.TestSuite()
328 result.addTest(test_test_protocol.test_suite())
329 result.addTest(test_tap2subunit.test_suite())
330+ result.addTest(test_subunit_filter.test_suite())
331 result.addTest(test_subunit_tags.test_suite())
332 result.addTest(test_subunit_stats.test_suite())
333 return result
334
335=== added file 'python/subunit/tests/test_subunit_filter.py'
336--- python/subunit/tests/test_subunit_filter.py 1970-01-01 00:00:00 +0000
337+++ python/subunit/tests/test_subunit_filter.py 2009-02-28 09:27:25 +0000
338@@ -0,0 +1,125 @@
339+#
340+# subunit: extensions to python unittest to get test results from subprocesses.
341+# Copyright (C) 2005 Robert Collins <robertc@robertcollins.net>
342+#
343+# This program is free software; you can redistribute it and/or modify
344+# it under the terms of the GNU General Public License as published by
345+# the Free Software Foundation; either version 2 of the License, or
346+# (at your option) any later version.
347+#
348+# This program is distributed in the hope that it will be useful,
349+# but WITHOUT ANY WARRANTY; without even the implied warranty of
350+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
351+# GNU General Public License for more details.
352+#
353+# You should have received a copy of the GNU General Public License
354+# along with this program; if not, write to the Free Software
355+# Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA
356+#
357+
358+"""Tests for subunit.TestResultFilter."""
359+
360+import unittest
361+from StringIO import StringIO
362+
363+import subunit
364+
365+
366+class TestTestResultFilter(unittest.TestCase):
367+ """Test for TestResultFilter, a TestResult object which filters tests."""
368+
369+ def _setUp(self):
370+ self.output = StringIO()
371+
372+ def test_default(self):
373+ """The default is to exclude success and include everything else."""
374+ self.filtered_result = unittest.TestResult()
375+ self.filter = subunit.TestResultFilter(self.filtered_result)
376+ self.run_tests()
377+ # skips are seen as errors by default python TestResult.
378+ self.assertEqual(['error', 'skipped'],
379+ [error[0].id() for error in self.filtered_result.errors])
380+ self.assertEqual(['failed'],
381+ [failure[0].id() for failure in
382+ self.filtered_result.failures])
383+ self.assertEqual(3, self.filtered_result.testsRun)
384+
385+ def test_exclude_errors(self):
386+ self.filtered_result = unittest.TestResult()
387+ self.filter = subunit.TestResultFilter(self.filtered_result,
388+ filter_error=True)
389+ self.run_tests()
390+ # skips are seen as errors by default python TestResult.
391+ self.assertEqual(['skipped'],
392+ [error[0].id() for error in self.filtered_result.errors])
393+ self.assertEqual(['failed'],
394+ [failure[0].id() for failure in
395+ self.filtered_result.failures])
396+ self.assertEqual(2, self.filtered_result.testsRun)
397+
398+ def test_exclude_failure(self):
399+ self.filtered_result = unittest.TestResult()
400+ self.filter = subunit.TestResultFilter(self.filtered_result,
401+ filter_failure=True)
402+ self.run_tests()
403+ self.assertEqual(['error', 'skipped'],
404+ [error[0].id() for error in self.filtered_result.errors])
405+ self.assertEqual([],
406+ [failure[0].id() for failure in
407+ self.filtered_result.failures])
408+ self.assertEqual(2, self.filtered_result.testsRun)
409+
410+ def test_exclude_skips(self):
411+ self.filtered_result = subunit.TestResultStats(None)
412+ self.filter = subunit.TestResultFilter(self.filtered_result,
413+ filter_skip=True)
414+ self.run_tests()
415+ self.assertEqual(0, self.filtered_result.skipped_tests)
416+ self.assertEqual(2, self.filtered_result.failed_tests)
417+ self.assertEqual(2, self.filtered_result.testsRun)
418+
419+ def test_include_success(self):
420+ """Success's can be included if requested."""
421+ self.filtered_result = unittest.TestResult()
422+ self.filter = subunit.TestResultFilter(self.filtered_result,
423+ filter_success=False)
424+ self.run_tests()
425+ self.assertEqual(['error', 'skipped'],
426+ [error[0].id() for error in self.filtered_result.errors])
427+ self.assertEqual(['failed'],
428+ [failure[0].id() for failure in
429+ self.filtered_result.failures])
430+ self.assertEqual(5, self.filtered_result.testsRun)
431+
432+ def run_tests(self):
433+ self.setUpTestStream()
434+ self.test = subunit.ProtocolTestCase(self.input_stream)
435+ self.test.run(self.filter)
436+
437+ def setUpTestStream(self):
438+ # While TestResultFilter works on python objects, using a subunit
439+ # stream is an easy pithy way of getting a series of test objects to
440+ # call into the TestResult, and as TestResultFilter is intended for use
441+ # with subunit also has the benefit of detecting any interface skew issues.
442+ self.input_stream = StringIO()
443+ self.input_stream.write("""tags: global
444+test passed
445+success passed
446+test failed
447+tags: local
448+failure failed
449+test error
450+error error
451+test skipped
452+skip skipped
453+test todo
454+xfail todo
455+""")
456+ self.input_stream.seek(0)
457+
458+
459+
460+def test_suite():
461+ loader = subunit.tests.TestUtil.TestLoader()
462+ result = loader.loadTestsFromName(__name__)
463+ return result
464
465=== modified file 'python/subunit/tests/test_subunit_stats.py'
466--- python/subunit/tests/test_subunit_stats.py 2008-12-09 01:00:03 +0000
467+++ python/subunit/tests/test_subunit_stats.py 2009-02-28 09:27:25 +0000
468@@ -62,15 +62,17 @@
469 # Statistics are calculated usefully.
470 self.setUpUsedStream()
471 self.assertEqual(5, self.result.total_tests)
472- self.assertEqual(3, self.result.passed_tests)
473+ self.assertEqual(2, self.result.passed_tests)
474 self.assertEqual(2, self.result.failed_tests)
475+ self.assertEqual(1, self.result.skipped_tests)
476 self.assertEqual(set(["global", "local"]), self.result.tags)
477
478 def test_stat_formatting(self):
479 expected = ("""
480-Total tests: 5
481-Passed tests: 3
482-Failed tests: 2
483+Total tests: 5
484+Passed tests: 2
485+Failed tests: 2
486+Skipped tests: 1
487 Tags: global, local
488 """)[1:]
489 self.setUpUsedStream()
490
491=== modified file 'python/subunit/tests/test_test_protocol.py'
492--- python/subunit/tests/test_test_protocol.py 2008-12-14 18:37:08 +0000
493+++ python/subunit/tests/test_test_protocol.py 2009-02-28 09:27:25 +0000
494@@ -32,6 +32,7 @@
495 self.end_calls = []
496 self.error_calls = []
497 self.failure_calls = []
498+ self.skip_calls = []
499 self.start_calls = []
500 self.success_calls = []
501 super(MockTestProtocolServerClient, self).__init__()
502@@ -42,6 +43,9 @@
503 def addFailure(self, test, error):
504 self.failure_calls.append((test, error))
505
506+ def addSkip(self, test, reason):
507+ self.skip_calls.append((test, reason))
508+
509 def addSuccess(self, test):
510 self.success_calls.append(test)
511
512@@ -589,8 +593,8 @@
513 class TestTestProtocolServerAddSkip(unittest.TestCase):
514 """Tests for the skip keyword.
515
516- In Python this thunks through to Success due to stdlib limitations. (See
517- README).
518+ In python this meets the testtools extended TestResult contract.
519+ (See https://launchpad.net/testtools).
520 """
521
522 def setUp(self):
523@@ -606,7 +610,9 @@
524 self.assertEqual(self.client.end_calls, [self.test])
525 self.assertEqual(self.client.error_calls, [])
526 self.assertEqual(self.client.failure_calls, [])
527- self.assertEqual(self.client.success_calls, [self.test])
528+ self.assertEqual(self.client.success_calls, [])
529+ self.assertEqual(self.client.skip_calls,
530+ [(self.test, 'No reason given')])
531
532 def test_simple_skip(self):
533 self.simple_skip_keyword("skip")
534@@ -621,7 +627,9 @@
535 self.assertEqual(self.client.end_calls, [self.test])
536 self.assertEqual(self.client.error_calls, [])
537 self.assertEqual(self.client.failure_calls, [])
538- self.assertEqual(self.client.success_calls, [self.test])
539+ self.assertEqual(self.client.success_calls, [])
540+ self.assertEqual(self.client.skip_calls,
541+ [(self.test, "No reason given")])
542
543 def skip_quoted_bracket(self, keyword):
544 # This tests it is accepted, but cannot test it is used today, because
545@@ -633,7 +641,9 @@
546 self.assertEqual(self.client.end_calls, [self.test])
547 self.assertEqual(self.client.error_calls, [])
548 self.assertEqual(self.client.failure_calls, [])
549- self.assertEqual(self.client.success_calls, [self.test])
550+ self.assertEqual(self.client.success_calls, [])
551+ self.assertEqual(self.client.skip_calls,
552+ [(self.test, "]\n")])
553
554 def test_skip_quoted_bracket(self):
555 self.skip_quoted_bracket("skip")
556@@ -772,7 +782,7 @@
557 self.assertRaises(NotImplementedError, test.tearDown)
558 self.assertEqual("A test description",
559 test.shortDescription())
560- self.assertEqual("subunit.RemotedTestCase.A test description",
561+ self.assertEqual("A test description",
562 test.id())
563 self.assertEqual("A test description (subunit.RemotedTestCase)", "%s" % test)
564 self.assertEqual("<subunit.RemotedTestCase description="
565@@ -933,7 +943,6 @@
566 self.protocol = subunit.TestProtocolClient(self.io)
567 self.test = TestTestProtocolClient("test_start_test")
568
569-
570 def test_start_test(self):
571 """Test startTest on a TestProtocolClient."""
572 self.protocol.startTest(self.test)
573@@ -968,6 +977,14 @@
574 "RemoteException: phwoar crikey\n"
575 "]\n" % self.test.id())
576
577+ def test_add_skip(self):
578+ """Test addSkip on a TestProtocolClient."""
579+ self.protocol.addSkip(
580+ self.test, "Has it really?")
581+ self.assertEqual(
582+ self.io.getvalue(),
583+ 'skip: %s [\nHas it really?\n]\n' % self.test.id())
584+
585
586 def test_suite():
587 loader = subunit.tests.TestUtil.TestLoader()

Subscribers

People subscribed via source and target branches