Merge lp:~lifeless/subunit/progress-gtk into lp:~subunit/subunit/trunk

Proposed by Robert Collins
Status: Superseded
Proposed branch: lp:~lifeless/subunit/progress-gtk
Merge into: lp:~subunit/subunit/trunk
Diff against target: None lines
To merge this branch: bzr merge lp:~lifeless/subunit/progress-gtk
Reviewer Review Type Date Requested Status
Subunit Developers Pending
Review via email: mp+9418@code.launchpad.net

This proposal has been superseded by a proposal from 2009-07-30.

To post a comment you must log in.
Revision history for this message
Robert Collins (lifeless) wrote :

This builds on progress-core to do a GTK progress bar. Woo. Shiny.

lp:~lifeless/subunit/progress-gtk updated
76. By Robert Collins

Handle python 2.5 a bit better in subunit2gtk.

77. By Robert Collins

Fix gtk support more.

78. By Robert Collins

Fix returning None from gobject IO callbacks.

Unmerged revisions

78. By Robert Collins

Fix returning None from gobject IO callbacks.

Preview Diff

[H/L] Next/Prev Comment, [J/K] Next/Prev File, [N/P] Next/Prev Hunk
1=== modified file 'NEWS'
2--- NEWS 2009-07-22 09:39:00 +0000
3+++ NEWS 2009-07-29 09:59:36 +0000
4@@ -10,10 +10,26 @@
5
6 IMPROVEMENTS:
7
8+ * Subunit streams can now include optional, incremental lookahead
9+ information about progress. This allows reporters to make estimates
10+ about completion, when such information is available. See the README
11+ under ``progress`` for more details.
12+
13+ * ``subunit2gtk`` has been added, a filter that shows a GTK summary of a
14+ test stream.
15+
16+ * ``subunit2pyunit`` has a --progress flag which will cause the bzrlib
17+ test reporter to be used, which has a textual progress bar. This requires
18+ a recent bzrlib as a minor bugfix was required in bzrlib to support this.
19+
20 BUG FIXES:
21
22 API CHANGES:
23
24+ * When a progress: directive is encountered in a subunit stream, the
25+ python bindings now call the ``progress(offset, whence)`` methd on
26+ ``TestResult``.
27+
28 * When a time: directive is encountered in a subunit stream, the python
29 bindings now call the ``time(seconds)`` method on ``TestResult``.
30
31
32=== modified file 'README'
33--- README 2009-07-22 09:39:00 +0000
34+++ README 2009-07-29 09:59:36 +0000
35@@ -47,6 +47,7 @@
36 Subunit supplies the following filters:
37 * tap2subunit - convert perl's TestAnythingProtocol to subunit.
38 * subunit2pyunit - convert a subunit stream to pyunit test results.
39+ * subunit2gtk - show a subunit stream in GTK.
40 * subunit-filter - filter out tests from a subunit stream.
41 * subunit-ls - list info about tests present in a subunit stream.
42 * subunit-stats - generate a summary of a subunit stream.
43@@ -130,12 +131,17 @@
44 # needed and report to your result object.
45 suite.run(result)
46
47-subunit includes extensions to the python ``TestResult`` protocol. The
48-``time(a_datetime)`` method is called (if present) when a ``time:``
49+subunit includes extensions to the python ``TestResult`` protocol.
50+
51+The ``time(a_datetime)`` method is called (if present) when a ``time:``
52 directive is encountered in a subunit stream. This is used to tell a TestResult
53 about the time that events in the stream occured at, to allow reconstructing
54 test timing from a stream.
55
56+The ``progress(offset, whence)`` method controls progress data for a stream.
57+The offset parameter is an int, and whence is one of subunit.SEEK_CUR,
58+subunit.SEEK_SET.
59+
60 Finally, subunit.run is a convenience wrapper to run a python test suite via
61 the command line, reporting via subunit::
62
63@@ -216,10 +222,12 @@
64 xfail[:] test label
65 xfail[:] test label [
66 ]
67+progress: [+|-]X
68 tags: [-]TAG ...
69 time: YYYY-MM-DD HH:MM:SSZ
70 unexpected output on stdout -> stdout.
71-exit w/0 or last test -> error
72+exit w/0 or last test completing -> error
73+
74 Tags given outside a test are applied to all following tests
75 Tags given after a test: line and before the result line for the same test
76 apply only to that test, and inheric the current global tags.
77@@ -228,7 +236,19 @@
78 In Python, tags are assigned to the .tags attribute on the RemoteTest objects
79 created by the TestProtocolServer.
80
81-The time element acts as a clock event - it sets the time for all future
82+The progress directive is used to provide progress information about a stream
83+so that stream consumer can provide completion estimates, progress bars and so
84+on. Stream generators that know how many tests will be present in the stream
85+should output "progress: COUNT". Stream filters that add tests should output
86+"progress: +COUNT", and those that remove tests should output
87+"progress: -COUNT". An absolute count should reset the progress indicators in
88+use - it indicates that two separate streams from different generators have
89+been trivially concatenated together, and there is no knowledge of how many
90+more complete streams are incoming. Smart concatenation could scan each stream
91+for their count and sum them, or alternatively translate absolute counts into
92+relative counts inline.
93+
94+The time directive acts as a clock event - it sets the time for all future
95 events. The value should be a valid ISO8601 time.
96
97 The skip result is used to indicate a test that was found by the runner but not
98
99=== added file 'filters/subunit2gtk'
100--- filters/subunit2gtk 1970-01-01 00:00:00 +0000
101+++ filters/subunit2gtk 2009-07-29 09:59:36 +0000
102@@ -0,0 +1,229 @@
103+#!/usr/bin/env python
104+# subunit: extensions to python unittest to get test results from subprocesses.
105+# Copyright (C) 2009 Robert Collins <robertc@robertcollins.net>
106+#
107+# This program is free software; you can redistribute it and/or modify
108+# it under the terms of the GNU General Public License as published by
109+# the Free Software Foundation; either version 2 of the License, or
110+# (at your option) any later version.
111+#
112+# This program is distributed in the hope that it will be useful,
113+# but WITHOUT ANY WARRANTY; without even the implied warranty of
114+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
115+# GNU General Public License for more details.
116+#
117+# You should have received a copy of the GNU General Public License
118+# along with this program; if not, write to the Free Software
119+# Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA
120+#
121+
122+### The GTK progress bar __init__ function is derived from the pygtk tutorial:
123+# The PyGTK Tutorial is Copyright (C) 2001-2005 John Finlay.
124+#
125+# The GTK Tutorial is Copyright (C) 1997 Ian Main.
126+#
127+# Copyright (C) 1998-1999 Tony Gale.
128+#
129+# Permission is granted to make and distribute verbatim copies of this manual
130+# provided the copyright notice and this permission notice are preserved on all
131+# copies.
132+#
133+# Permission is granted to copy and distribute modified versions of this
134+# document under the conditions for verbatim copying, provided that this
135+# copyright notice is included exactly as in the original, and that the entire
136+# resulting derived work is distributed under the terms of a permission notice
137+# identical to this one.
138+#
139+# Permission is granted to copy and distribute translations of this document
140+# into another language, under the above conditions for modified versions.
141+#
142+# If you are intending to incorporate this document into a published work,
143+# please contact the maintainer, and we will make an effort to ensure that you
144+# have the most up to date information available.
145+#
146+# There is no guarantee that this document lives up to its intended purpose.
147+# This is simply provided as a free resource. As such, the authors and
148+# maintainers of the information provided within can not make any guarantee
149+# that the information is even accurate.
150+
151+"""Display a subunit stream in a gtk progress window."""
152+
153+import os
154+import sys
155+import unittest
156+
157+import pygtk
158+pygtk.require('2.0')
159+import gtk, gtk.gdk, gobject
160+
161+from subunit import ProtocolTestCase, TestProtocolServer
162+
163+class GTKTestResult(unittest.TestResult):
164+
165+ def __init__(self):
166+ super(GTKTestResult, self).__init__()
167+ # Instance variables (in addition to TestResult)
168+ self.window = None
169+ self.run_label = None
170+ self.ok_label = None
171+ self.not_ok_label = None
172+ self.total_tests = None
173+
174+ self.window = gtk.Window(gtk.WINDOW_TOPLEVEL)
175+ self.window.set_resizable(True)
176+
177+ self.window.connect("destroy", gtk.main_quit)
178+ self.window.set_title("Tests...")
179+ self.window.set_border_width(0)
180+
181+ vbox = gtk.VBox(False, 5)
182+ vbox.set_border_width(10)
183+ self.window.add(vbox)
184+ vbox.show()
185+
186+ # Create a centering alignment object
187+ align = gtk.Alignment(0.5, 0.5, 0, 0)
188+ vbox.pack_start(align, False, False, 5)
189+ align.show()
190+
191+ # Create the ProgressBar
192+ self.pbar = gtk.ProgressBar()
193+ align.add(self.pbar)
194+ self.pbar.set_text("Running")
195+ self.pbar.show()
196+
197+ separator = gtk.HSeparator()
198+ vbox.pack_start(separator, False, False, 0)
199+ separator.show()
200+
201+ # rows, columns, homogeneous
202+ table = gtk.Table(2, 3, False)
203+ vbox.pack_start(table, False, True, 0)
204+ table.show()
205+ # Show summary details about the run. Could use an expander.
206+ label = gtk.Label("Run:")
207+ table.attach(label, 0, 1, 1, 2, gtk.EXPAND | gtk.FILL,
208+ gtk.EXPAND | gtk.FILL, 5, 5)
209+ label.show()
210+ self.run_label = gtk.Label("N/A")
211+ table.attach(self.run_label, 1, 2, 1, 2, gtk.EXPAND | gtk.FILL,
212+ gtk.EXPAND | gtk.FILL, 5, 5)
213+ self.run_label.show()
214+
215+ label = gtk.Label("OK:")
216+ table.attach(label, 0, 1, 2, 3, gtk.EXPAND | gtk.FILL,
217+ gtk.EXPAND | gtk.FILL, 5, 5)
218+ label.show()
219+ self.ok_label = gtk.Label("N/A")
220+ table.attach(self.ok_label, 1, 2, 2, 3, gtk.EXPAND | gtk.FILL,
221+ gtk.EXPAND | gtk.FILL, 5, 5)
222+ self.ok_label.show()
223+
224+ label = gtk.Label("Not OK:")
225+ table.attach(label, 0, 1, 3, 4, gtk.EXPAND | gtk.FILL,
226+ gtk.EXPAND | gtk.FILL, 5, 5)
227+ label.show()
228+ self.not_ok_label = gtk.Label("N/A")
229+ table.attach(self.not_ok_label, 1, 2, 3, 4, gtk.EXPAND | gtk.FILL,
230+ gtk.EXPAND | gtk.FILL, 5, 5)
231+ self.not_ok_label.show()
232+
233+ self.window.show()
234+ # For the demo.
235+ self.window.set_keep_above(True)
236+ self.window.present()
237+
238+ def stopTest(self, test):
239+ super(GTKTestResult, self).stopTest(test)
240+ if not self.total_tests:
241+ self.pbar.pulse()
242+ else:
243+ self.pbar.set_fraction(self.testsRun/float(self.total_tests))
244+
245+ def stopTestRun(self):
246+ try:
247+ super(GTKTestResult, self).stopTestRun()
248+ except AttributeError:
249+ pass
250+ self.pbar.set_text('Finished')
251+
252+ def addError(self, test, err):
253+ super(GTKTestResult, self).addError(test, err)
254+ self.update_counts()
255+
256+ def addFailure(self, test, err):
257+ super(GTKTestResult, self).addFailure(test, err)
258+ self.update_counts()
259+
260+ def addSuccess(self, test):
261+ super(GTKTestResult, self).addSuccess(test)
262+ self.update_counts()
263+
264+ def addSkip(self, test, reason):
265+ super(GTKTestResult, self).addSkipSuccess(test, reason)
266+ self.update_counts()
267+
268+ def addExpectedFailure(self, test, err):
269+ super(GTKTestResult, self).addExpectedFailure(test, err)
270+ self.update_counts()
271+
272+ def addUnexpectedSuccess(self, test):
273+ super(GTKTestResult, self).addUnexpectedSuccess(test)
274+ self.update_counts()
275+
276+ def progress(self, offset, whence):
277+ if whence == os.SEEK_SET:
278+ self.total_tests = offset
279+ else:
280+ self.total_tests += offset
281+
282+ def time(self, a_datetime):
283+ # We don't try to estimate completion yet.
284+ pass
285+
286+ def update_counts(self):
287+ self.run_label.set_text(str(self.testsRun))
288+ bad = len(self.failures + self.errors)
289+ self.ok_label.set_text(str(self.testsRun - bad))
290+ self.not_ok_label.set_text(str(bad))
291+
292+
293+class GIOProtocolTestCase(object):
294+
295+ def __init__(self, stream, result, on_finish):
296+ self.stream = stream
297+ self.schedule_read()
298+ self.hup_id = gobject.io_add_watch(stream, gobject.IO_HUP, self.hup)
299+ self.protocol = TestProtocolServer(result)
300+ self.on_finish = on_finish
301+
302+ def read(self, source, condition):
303+ #NB: \o/ actually blocks
304+ line = source.readline()
305+ if not line:
306+ self.protocol.lostConnection()
307+ self.on_finish()
308+ return False
309+ self.protocol.lineReceived(line)
310+ # schedule more IO shortly - if we say we're willing to do it
311+ # immediately we starve things.
312+ source_id = gobject.timeout_add(1, self.schedule_read)
313+ return False
314+
315+ def schedule_read(self):
316+ self.read_id = gobject.io_add_watch(self.stream, gobject.IO_IN, self.read)
317+
318+ def hup(self, source, condition):
319+ self.protocol.lostConnection()
320+ gobject.remove(self.read_id)
321+ self.on_finish()
322+
323+
324+result = GTKTestResult()
325+test = GIOProtocolTestCase(sys.stdin, result, result.stopTestRun)
326+gtk.main()
327+if result.wasSuccessful():
328+ exit_code = 0
329+else:
330+ exit_code = 1
331+sys.exit(exit_code)
332
333=== modified file 'filters/subunit2pyunit'
334--- filters/subunit2pyunit 2009-02-15 11:55:00 +0000
335+++ filters/subunit2pyunit 2009-07-28 21:44:28 +0000
336@@ -17,15 +17,27 @@
337 # Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA
338 #
339
340-"""Filter a subunit stream through python's default unittest test runner."""
341+"""Display a subunit stream through python's unittest test runner."""
342
343+from optparse import OptionParser
344 import sys
345 import unittest
346
347 from subunit import ProtocolTestCase, TestProtocolServer
348
349-runner = unittest.TextTestRunner(verbosity=2)
350+parser = OptionParser(description=__doc__)
351+parser.add_option("--progress", action="store_true",
352+ help="Use bzrlib's test reporter (requires bzrlib)",
353+ default=False)
354+(options, args) = parser.parse_args()
355 test = ProtocolTestCase(sys.stdin)
356+if options.progress:
357+ from bzrlib.tests import TextTestRunner
358+ from bzrlib import ui
359+ ui.ui_factory = ui.make_ui_for_terminal(None, sys.stdout, sys.stderr)
360+ runner = TextTestRunner()
361+else:
362+ runner = unittest.TextTestRunner(verbosity=2)
363 if runner.run(test).wasSuccessful():
364 exit_code = 0
365 else:
366
367=== modified file 'python/subunit/__init__.py'
368--- python/subunit/__init__.py 2009-07-22 08:46:04 +0000
369+++ python/subunit/__init__.py 2009-07-28 13:32:10 +0000
370@@ -28,6 +28,10 @@
371 import iso8601
372
373
374+SEEK_CUR = os.SEEK_CUR
375+SEEK_SET = os.SEEK_SET
376+
377+
378 def test_suite():
379 import subunit.tests
380 return subunit.tests.test_suite()
381@@ -200,6 +204,18 @@
382 else:
383 self.stdOutLineReceived(line)
384
385+ def _handleProgress(self, offset, line):
386+ """Process a progress directive."""
387+ line = line[offset:].strip()
388+ if line[0] in '+-':
389+ whence = SEEK_CUR
390+ else:
391+ whence = SEEK_SET
392+ delta = int(line)
393+ progress_method = getattr(self.client, 'progress', None)
394+ if callable(progress_method):
395+ progress_method(delta, whence)
396+
397 def _handleTags(self, offset, line):
398 """Process a tags command."""
399 tags = line[offset:].split()
400@@ -243,6 +259,8 @@
401 self._addError(offset, line)
402 elif cmd == 'failure':
403 self._addFailure(offset, line)
404+ elif cmd == 'progress':
405+ self._handleProgress(offset, line)
406 elif cmd == 'skip':
407 self._addSkip(offset, line)
408 elif cmd in ('success', 'successful'):
409@@ -355,6 +373,21 @@
410 """Mark a test as starting its test run."""
411 self._stream.write("test: %s\n" % test.id())
412
413+ def progress(self, offset, whence):
414+ """Provide indication about the progress/length of the test run.
415+
416+ :param offset: Information about the number of tests remaining. If
417+ whence is SEEK_CUR, then offset increases/decreases the remaining
418+ test count. If whence is SEEK_SET, then offset specifies exactly
419+ the remaining test count.
420+ :param whence: One of SEEK_CUR or SEEK_SET.
421+ """
422+ if whence == SEEK_CUR and offset > -1:
423+ prefix = "+"
424+ else:
425+ prefix = ""
426+ self._stream.write("progress: %s%s\n" % (prefix, offset))
427+
428 def time(self, a_datetime):
429 """Inform the client of the time.
430
431
432=== modified file 'python/subunit/test_results.py'
433--- python/subunit/test_results.py 2009-07-22 23:17:18 +0000
434+++ python/subunit/test_results.py 2009-07-28 13:32:10 +0000
435@@ -87,6 +87,10 @@
436 self._before_event()
437 return self._call_maybe("addUnexpectedSuccess", test)
438
439+ def progress(self, offset, whence):
440+ self._before_event()
441+ return self._call_maybe("progress", offset, whence)
442+
443 def wasSuccessful(self):
444 self._before_event()
445 return self.decorated.wasSuccessful()
446@@ -124,6 +128,9 @@
447 time = datetime.datetime.utcnow().replace(tzinfo=iso8601.Utc())
448 self._call_maybe("time", time)
449
450+ def progress(self, offset, whence):
451+ return self._call_maybe("progress", offset, whence)
452+
453 @property
454 def shouldStop(self):
455 return self.decorated.shouldStop
456
457=== modified file 'python/subunit/tests/test_test_protocol.py'
458--- python/subunit/tests/test_test_protocol.py 2009-07-22 08:46:04 +0000
459+++ python/subunit/tests/test_test_protocol.py 2009-07-28 13:32:10 +0000
460@@ -37,6 +37,7 @@
461 self.skip_calls = []
462 self.start_calls = []
463 self.success_calls = []
464+ self.progress_calls = []
465 self._time = None
466 super(MockTestProtocolServerClient, self).__init__()
467
468@@ -58,6 +59,9 @@
469 def startTest(self, test):
470 self.start_calls.append(test)
471
472+ def progress(self, offset, whence):
473+ self.progress_calls.append((offset, whence))
474+
475 def time(self, time):
476 self._time = time
477
478@@ -118,6 +122,11 @@
479 self.assertEqual(protocol.success_calls, [])
480 self.assertEqual(protocol.start_calls, [])
481
482+ def test_progress(self):
483+ protocol = MockTestProtocolServerClient()
484+ protocol.progress(-1, subunit.SEEK_CUR)
485+ self.assertEqual(protocol.progress_calls, [(-1, subunit.SEEK_CUR)])
486+
487
488 class TestTestImports(unittest.TestCase):
489
490@@ -710,6 +719,36 @@
491 self.success_quoted_bracket("success:")
492
493
494+class TestTestProtocolServerProgress(unittest.TestCase):
495+ """Test receipt of progress: directives."""
496+
497+ def test_progress_accepted_stdlib(self):
498+ # With a stdlib TestResult, progress events are swallowed.
499+ self.result = unittest.TestResult()
500+ self.stream = StringIO()
501+ self.protocol = subunit.TestProtocolServer(self.result,
502+ stream=self.stream)
503+ self.protocol.lineReceived("progress: 23")
504+ self.protocol.lineReceived("progress: -2")
505+ self.protocol.lineReceived("progress: +4")
506+ self.assertEqual("", self.stream.getvalue())
507+
508+ def test_progress_accepted_extended(self):
509+ # With a progress capable TestResult, progress events are emitted.
510+ self.result = MockTestProtocolServerClient()
511+ self.stream = StringIO()
512+ self.protocol = subunit.TestProtocolServer(self.result,
513+ stream=self.stream)
514+ self.protocol.lineReceived("progress: 23")
515+ self.protocol.lineReceived("progress: -2")
516+ self.protocol.lineReceived("progress: +4")
517+ self.assertEqual("", self.stream.getvalue())
518+ self.assertEqual(
519+ [(23, subunit.SEEK_SET), (-2, subunit.SEEK_CUR),
520+ (4, subunit.SEEK_CUR)],
521+ self.result.progress_calls)
522+
523+
524 class TestTestProtocolServerStreamTags(unittest.TestCase):
525 """Test managing tags on the protocol level."""
526
527@@ -1005,6 +1044,18 @@
528 self.io.getvalue(),
529 'skip: %s [\nHas it really?\n]\n' % self.test.id())
530
531+ def test_progress_set(self):
532+ self.protocol.progress(23, subunit.SEEK_SET)
533+ self.assertEqual(self.io.getvalue(), 'progress: 23\n')
534+
535+ def test_progress_neg_cur(self):
536+ self.protocol.progress(-23, subunit.SEEK_CUR)
537+ self.assertEqual(self.io.getvalue(), 'progress: -23\n')
538+
539+ def test_progress_pos_cur(self):
540+ self.protocol.progress(23, subunit.SEEK_CUR)
541+ self.assertEqual(self.io.getvalue(), 'progress: +23\n')
542+
543 def test_time(self):
544 # Calling time() outputs a time signal immediately.
545 self.protocol.time(
546
547=== modified file 'python/subunit/tests/test_test_results.py'
548--- python/subunit/tests/test_test_results.py 2009-07-22 23:17:18 +0000
549+++ python/subunit/tests/test_test_results.py 2009-07-28 13:32:10 +0000
550@@ -108,6 +108,9 @@
551 def test_addUnexpectedSuccess(self):
552 self.result.addUnexpectedSuccess(self)
553
554+ def test_progress(self):
555+ self.result.progress(1, os.SEEK_SET)
556+
557 def test_wasSuccessful(self):
558 self.result.wasSuccessful()
559
560@@ -135,6 +138,10 @@
561 self.assertEqual(1, len(self.result.decorated._calls))
562 self.assertNotEqual(None, self.result.decorated._calls[0])
563
564+ def test_no_time_from_progress(self):
565+ self.result.progress(1, os.SEEK_CUR)
566+ self.assertEqual(0, len(self.result.decorated._calls))
567+
568 def test_no_time_from_shouldStop(self):
569 self.result.decorated.stop()
570 self.result.shouldStop

Subscribers

People subscribed via source and target branches