diff options
| author | Robert Collins <robertc@robertcollins.net> | 2009-08-27 19:06:31 +1000 |
|---|---|---|
| committer | Robert Collins <robertc@robertcollins.net> | 2009-08-27 19:06:31 +1000 |
| commit | dc341ccc88b79abd13a0b033c8012dd25329c9ac (patch) | |
| tree | 56ae2427ca53e7e665a7292210fda44d6efdb6b6 | |
| parent | 0b06fddb09128f2e91463657552f92a6661a87c0 (diff) | |
| parent | 37080a3a8b3b3414e8d6ec1eb68fb6000d926a0a (diff) | |
| download | subunit-git-dc341ccc88b79abd13a0b033c8012dd25329c9ac.tar.gz | |
Merge progress model overhaul adding nest progress data.
| -rw-r--r-- | README | 15 | ||||
| -rwxr-xr-x | filters/subunit2gtk | 25 | ||||
| -rw-r--r-- | python/subunit/__init__.py | 36 | ||||
| -rw-r--r-- | python/subunit/progress_model.py | 109 | ||||
| -rw-r--r-- | python/subunit/tests/__init__.py | 2 | ||||
| -rw-r--r-- | python/subunit/tests/test_progress_model.py | 121 | ||||
| -rw-r--r-- | python/subunit/tests/test_test_protocol.py | 25 | ||||
| -rw-r--r-- | python/subunit/tests/test_test_results.py | 7 |
8 files changed, 313 insertions, 27 deletions
@@ -140,8 +140,9 @@ about the time that events in the stream occured at, to allow reconstructing test timing from a stream. The ``progress(offset, whence)`` method controls progress data for a stream. -The offset parameter is an int, and whence is one of subunit.SEEK_CUR, -subunit.SEEK_SET. +The offset parameter is an int, and whence is one of subunit.PROGRESS_CUR, +subunit.PROGRESS_SET, PROGRESS_PUSH, PROGRESS_POP. Push and pop operations +ignore the offset parameter. Finally, subunit.run is a convenience wrapper to run a python test suite via the command line, reporting via subunit:: @@ -224,6 +225,8 @@ xfail[:] test label xfail[:] test label [ ] progress: [+|-]X +progress: push +progress: pop tags: [-]TAG ... time: YYYY-MM-DD HH:MM:SSZ unexpected output on stdout -> stdout. @@ -247,7 +250,13 @@ use - it indicates that two separate streams from different generators have been trivially concatenated together, and there is no knowledge of how many more complete streams are incoming. Smart concatenation could scan each stream for their count and sum them, or alternatively translate absolute counts into -relative counts inline. +relative counts inline. It is recommended that outputters avoid absolute counts +unless necessary. The push and pop directives are used to provide local regions +for progress reporting. This fits with hierarchically operating test +environments - such as those that organise tests into suites - the top-most +runner can report on the number of suites, and each suite surround its output +with a (push, pop) pair. Interpreters should interpret a pop as also advancing +the progress of the restored level by one step. The time directive acts as a clock event - it sets the time for all future events. The value should be a valid ISO8601 time. diff --git a/filters/subunit2gtk b/filters/subunit2gtk index c3b414a..ab33132 100755 --- a/filters/subunit2gtk +++ b/filters/subunit2gtk @@ -56,7 +56,15 @@ import pygtk pygtk.require('2.0') import gtk, gtk.gdk, gobject -from subunit import ProtocolTestCase, TestProtocolServer, SEEK_SET +from subunit import ( + PROGRESS_POP, + PROGRESS_PUSH, + PROGRESS_SET, + ProtocolTestCase, + TestProtocolServer, + ) +from subunit.progress_model import ProgressModel + class GTKTestResult(unittest.TestResult): @@ -91,6 +99,7 @@ class GTKTestResult(unittest.TestResult): align.add(self.pbar) self.pbar.set_text("Running") self.pbar.show() + self.progress = ProgressModel() separator = gtk.HSeparator() vbox.pack_start(separator, False, False, 0) @@ -135,10 +144,12 @@ class GTKTestResult(unittest.TestResult): def stopTest(self, test): super(GTKTestResult, self).stopTest(test) - if not self.total_tests: + self.progress.advance() + if self.progress.width() == 0: self.pbar.pulse() else: - self.pbar.set_fraction(self.testsRun/float(self.total_tests)) + self.pbar.set_fraction( + self.progress.pos() / float(self.progress.width())) def stopTestRun(self): try: @@ -183,10 +194,16 @@ class GTKTestResult(unittest.TestResult): self.update_counts() def progress(self, offset, whence): - if whence == SEEK_SET: + if whence == PROGRESS_PUSH: + self.progress.push() + elif whence == PROGRESS_POP: + self.progress.pop() + elif whence == PROGRESS_SET: self.total_tests = offset + self.progress.set_width(offset) else: self.total_tests += offset + self.progress.adjust_width(offset) def time(self, a_datetime): # We don't try to estimate completion yet. diff --git a/python/subunit/__init__.py b/python/subunit/__init__.py index f474bd9..27d9e53 100644 --- a/python/subunit/__init__.py +++ b/python/subunit/__init__.py @@ -28,8 +28,10 @@ import unittest import iso8601 -SEEK_CUR = os.SEEK_CUR -SEEK_SET = os.SEEK_SET +PROGRESS_SET = 0 +PROGRESS_CUR = 1 +PROGRESS_PUSH = 2 +PROGRESS_POP = 3 def test_suite(): @@ -225,10 +227,17 @@ class TestProtocolServer(object): """Process a progress directive.""" line = line[offset:].strip() if line[0] in '+-': - whence = SEEK_CUR + whence = PROGRESS_CUR + delta = int(line) + elif line == "push": + whence = PROGRESS_PUSH + delta = None + elif line == "pop": + whence = PROGRESS_POP + delta = None else: - whence = SEEK_SET - delta = int(line) + whence = PROGRESS_SET + delta = int(line) progress_method = getattr(self.client, 'progress', None) if callable(progress_method): progress_method(delta, whence) @@ -394,13 +403,20 @@ class TestProtocolClient(unittest.TestResult): """Provide indication about the progress/length of the test run. :param offset: Information about the number of tests remaining. If - whence is SEEK_CUR, then offset increases/decreases the remaining - test count. If whence is SEEK_SET, then offset specifies exactly - the remaining test count. - :param whence: One of SEEK_CUR or SEEK_SET. + whence is PROGRESS_CUR, then offset increases/decreases the + remaining test count. If whence is PROGRESS_SET, then offset + specifies exactly the remaining test count. + :param whence: One of PROGRESS_CUR, PROGRESS_SET, PROGRESS_PUSH, + PROGRESS_POP. """ - if whence == SEEK_CUR and offset > -1: + if whence == PROGRESS_CUR and offset > -1: prefix = "+" + elif whence == PROGRESS_PUSH: + prefix = "" + offset = "push" + elif whence == PROGRESS_POP: + prefix = "" + offset = "pop" else: prefix = "" self._stream.write("progress: %s%s\n" % (prefix, offset)) diff --git a/python/subunit/progress_model.py b/python/subunit/progress_model.py new file mode 100644 index 0000000..f1ed7af --- /dev/null +++ b/python/subunit/progress_model.py @@ -0,0 +1,109 @@ +# +# subunit: extensions to Python unittest to get test results from subprocesses. +# Copyright (C) 2009 Robert Collins <robertc@robertcollins.net> +# +# This program is free software; you can redistribute it and/or modify +# it under the terms of the GNU General Public License as published by +# the Free Software Foundation; either version 2 of the License, or +# (at your option) any later version. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with this program; if not, write to the Free Software +# Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA +# + +"""Support for dealing with progress state.""" + +class ProgressModel(object): + """A model of progress indicators as subunit defines it. + + Instances of this class represent a single logical operation that is + progressing. The operation may have many steps, and some of those steps may + supply their own progress information. ProgressModel uses a nested concept + where the overall state can be pushed, creating new starting state, and + later pushed to return to the prior state. Many user interfaces will want + to display an overall summary though, and accordingly the pos() and width() + methods return overall summary information rather than information on the + current subtask. + + The default state is 0/0 - indicating that the overall progress is unknown. + Anytime the denominator of pos/width is 0, rendering of a ProgressModel + should should take this into consideration. + + :ivar: _tasks. This private attribute stores the subtasks. Each is a tuple: + pos, width, overall_numerator, overall_denominator. The overall fields + store the calculated overall numerator and denominator for the state + that was pushed. + """ + + def __init__(self): + """Create a ProgressModel. + + The new model has no progress data at all - it will claim a summary + width of zero and position of 0. + """ + self._tasks = [] + self.push() + + def adjust_width(self, offset): + """Adjust the with of the current subtask.""" + self._tasks[-1][1] += offset + + def advance(self): + """Advance the current subtask.""" + self._tasks[-1][0] += 1 + + def pop(self): + """Pop a subtask off the ProgressModel. + + See push for a description of how push and pop work. + """ + self._tasks.pop() + + def pos(self): + """Return how far through the operation has progressed.""" + if not self._tasks: + return 0 + task = self._tasks[-1] + if len(self._tasks) > 1: + # scale up the overall pos by the current task or preserve it if + # no current width is known. + offset = task[2] * (task[1] or 1) + else: + offset = 0 + return offset + task[0] + + def push(self): + """Push a new subtask. + + After pushing a new subtask, the overall progress hasn't changed. Calls + to adjust_width, advance, set_width will only after the progress within + the range that calling 'advance' would have before - the subtask + represents progressing one step in the earlier task. + + Call pop() to restore the progress model to the state before push was + called. + """ + self._tasks.append([0, 0, self.pos(), self.width()]) + + def set_width(self, width): + """Set the width of the current subtask.""" + self._tasks[-1][1] = width + + def width(self): + """Return the total width of the operation.""" + if not self._tasks: + return 0 + task = self._tasks[-1] + if len(self._tasks) > 1: + # scale up the overall width by the current task or preserve it if + # no current width is known. + return task[3] * (task[1] or 1) + else: + return task[1] + diff --git a/python/subunit/tests/__init__.py b/python/subunit/tests/__init__.py index 11f3095..bbe12b1 100644 --- a/python/subunit/tests/__init__.py +++ b/python/subunit/tests/__init__.py @@ -19,6 +19,7 @@ from subunit.tests import ( TestUtil, + test_progress_model, test_subunit_filter, test_subunit_stats, test_subunit_tags, @@ -29,6 +30,7 @@ from subunit.tests import ( def test_suite(): result = TestUtil.TestSuite() + result.addTest(test_progress_model.test_suite()) result.addTest(test_test_results.test_suite()) result.addTest(test_test_protocol.test_suite()) result.addTest(test_tap2subunit.test_suite()) diff --git a/python/subunit/tests/test_progress_model.py b/python/subunit/tests/test_progress_model.py new file mode 100644 index 0000000..60beb41 --- /dev/null +++ b/python/subunit/tests/test_progress_model.py @@ -0,0 +1,121 @@ +# +# subunit: extensions to Python unittest to get test results from subprocesses. +# Copyright (C) 2009 Robert Collins <robertc@robertcollins.net> +# +# This program is free software; you can redistribute it and/or modify +# it under the terms of the GNU General Public License as published by +# the Free Software Foundation; either version 2 of the License, or +# (at your option) any later version. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with this program; if not, write to the Free Software +# Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA +# + +import unittest + +import subunit +from subunit.progress_model import ProgressModel + + +class TestProgressModel(unittest.TestCase): + + def assertProgressSummary(self, pos, total, progress): + """Assert that a progress model has reached a particular point.""" + self.assertEqual(pos, progress.pos()) + self.assertEqual(total, progress.width()) + + def test_new_progress_0_0(self): + progress = ProgressModel() + self.assertProgressSummary(0, 0, progress) + + def test_advance_0_0(self): + progress = ProgressModel() + progress.advance() + self.assertProgressSummary(1, 0, progress) + + def test_advance_1_0(self): + progress = ProgressModel() + progress.advance() + self.assertProgressSummary(1, 0, progress) + + def test_set_width_absolute(self): + progress = ProgressModel() + progress.set_width(10) + self.assertProgressSummary(0, 10, progress) + + def test_set_width_absolute_preserves_pos(self): + progress = ProgressModel() + progress.advance() + progress.set_width(2) + self.assertProgressSummary(1, 2, progress) + + def test_adjust_width(self): + progress = ProgressModel() + progress.adjust_width(10) + self.assertProgressSummary(0, 10, progress) + progress.adjust_width(-10) + self.assertProgressSummary(0, 0, progress) + + def test_adjust_width_preserves_pos(self): + progress = ProgressModel() + progress.advance() + progress.adjust_width(10) + self.assertProgressSummary(1, 10, progress) + progress.adjust_width(-10) + self.assertProgressSummary(1, 0, progress) + + def test_push_preserves_progress(self): + progress = ProgressModel() + progress.adjust_width(3) + progress.advance() + progress.push() + self.assertProgressSummary(1, 3, progress) + + def test_advance_advances_substack(self): + progress = ProgressModel() + progress.adjust_width(3) + progress.advance() + progress.push() + progress.adjust_width(1) + progress.advance() + self.assertProgressSummary(2, 3, progress) + + def test_adjust_width_adjusts_substack(self): + progress = ProgressModel() + progress.adjust_width(3) + progress.advance() + progress.push() + progress.adjust_width(2) + progress.advance() + self.assertProgressSummary(3, 6, progress) + + def test_set_width_adjusts_substack(self): + progress = ProgressModel() + progress.adjust_width(3) + progress.advance() + progress.push() + progress.set_width(2) + progress.advance() + self.assertProgressSummary(3, 6, progress) + + def test_pop_restores_progress(self): + progress = ProgressModel() + progress.adjust_width(3) + progress.advance() + progress.push() + progress.adjust_width(1) + progress.advance() + progress.pop() + self.assertProgressSummary(1, 3, progress) + + +def test_suite(): + loader = subunit.tests.TestUtil.TestLoader() + result = loader.loadTestsFromName(__name__) + return result diff --git a/python/subunit/tests/test_test_protocol.py b/python/subunit/tests/test_test_protocol.py index a036192..2b90b9e 100644 --- a/python/subunit/tests/test_test_protocol.py +++ b/python/subunit/tests/test_test_protocol.py @@ -129,8 +129,8 @@ class TestMockTestProtocolServer(unittest.TestCase): def test_progress(self): protocol = MockTestProtocolServerClient() - protocol.progress(-1, subunit.SEEK_CUR) - self.assertEqual(protocol.progress_calls, [(-1, subunit.SEEK_CUR)]) + protocol.progress(-1, subunit.PROGRESS_CUR) + self.assertEqual(protocol.progress_calls, [(-1, subunit.PROGRESS_CUR)]) class TestTestImports(unittest.TestCase): @@ -790,12 +790,15 @@ class TestTestProtocolServerProgress(unittest.TestCase): self.protocol = subunit.TestProtocolServer(self.result, stream=self.stream) self.protocol.lineReceived("progress: 23") + self.protocol.lineReceived("progress: push") self.protocol.lineReceived("progress: -2") + self.protocol.lineReceived("progress: pop") self.protocol.lineReceived("progress: +4") self.assertEqual("", self.stream.getvalue()) self.assertEqual( - [(23, subunit.SEEK_SET), (-2, subunit.SEEK_CUR), - (4, subunit.SEEK_CUR)], + [(23, subunit.PROGRESS_SET), (None, subunit.PROGRESS_PUSH), + (-2, subunit.PROGRESS_CUR), (None, subunit.PROGRESS_POP), + (4, subunit.PROGRESS_CUR)], self.result.progress_calls) @@ -1095,17 +1098,25 @@ class TestTestProtocolClient(unittest.TestCase): 'skip: %s [\nHas it really?\n]\n' % self.test.id()) def test_progress_set(self): - self.protocol.progress(23, subunit.SEEK_SET) + self.protocol.progress(23, subunit.PROGRESS_SET) self.assertEqual(self.io.getvalue(), 'progress: 23\n') def test_progress_neg_cur(self): - self.protocol.progress(-23, subunit.SEEK_CUR) + self.protocol.progress(-23, subunit.PROGRESS_CUR) self.assertEqual(self.io.getvalue(), 'progress: -23\n') def test_progress_pos_cur(self): - self.protocol.progress(23, subunit.SEEK_CUR) + self.protocol.progress(23, subunit.PROGRESS_CUR) self.assertEqual(self.io.getvalue(), 'progress: +23\n') + def test_progress_pop(self): + self.protocol.progress(1234, subunit.PROGRESS_POP) + self.assertEqual(self.io.getvalue(), 'progress: pop\n') + + def test_progress_push(self): + self.protocol.progress(1234, subunit.PROGRESS_PUSH) + self.assertEqual(self.io.getvalue(), 'progress: push\n') + def test_time(self): # Calling time() outputs a time signal immediately. self.protocol.time( diff --git a/python/subunit/tests/test_test_results.py b/python/subunit/tests/test_test_results.py index 4537304..2f76917 100644 --- a/python/subunit/tests/test_test_results.py +++ b/python/subunit/tests/test_test_results.py @@ -21,10 +21,11 @@ import datetime import unittest from StringIO import StringIO import os -import subunit.test_results import sys +import subunit import subunit.iso8601 as iso8601 +import subunit.test_results class LoggingDecorator(subunit.test_results.HookedTestResultDecorator): @@ -109,7 +110,7 @@ class TestHookedTestResultDecorator(unittest.TestCase): self.result.addUnexpectedSuccess(self) def test_progress(self): - self.result.progress(1, os.SEEK_SET) + self.result.progress(1, subunit.PROGRESS_SET) def test_wasSuccessful(self): self.result.wasSuccessful() @@ -139,7 +140,7 @@ class TestAutoTimingTestResultDecorator(unittest.TestCase): self.assertNotEqual(None, self.result.decorated._calls[0]) def test_no_time_from_progress(self): - self.result.progress(1, os.SEEK_CUR) + self.result.progress(1, subunit.PROGRESS_CUR) self.assertEqual(0, len(self.result.decorated._calls)) def test_no_time_from_shouldStop(self): |
