Merge lp:~lifeless/subunit/time-support into lp:~subunit/subunit/trunk

Proposed by Robert Collins
Status: Merged
Merged at revision: not available
Proposed branch: lp:~lifeless/subunit/time-support
Merge into: lp:~subunit/subunit/trunk
Diff against target: None lines
To merge this branch: bzr merge lp:~lifeless/subunit/time-support

This proposal supersedes a proposal from 2009-07-20.

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

This teaches the python support to export time data to the TestResult.

Preview Diff

[H/L] Next/Prev Comment, [J/K] Next/Prev File, [N/P] Next/Prev Hunk
1=== modified file 'Makefile.am'
2--- Makefile.am 2009-06-12 01:45:53 +0000
3+++ Makefile.am 2009-07-22 08:11:41 +0000
4@@ -2,6 +2,7 @@
5 INSTALL \
6 Makefile.am \
7 README \
8+ NEWS \
9 c/README \
10 c/check-subunit-0.9.3.patch \
11 c/check-subunit-0.9.5.patch \
12@@ -19,6 +20,7 @@
13 python/subunit/tests/test_subunit_tags.py \
14 python/subunit/tests/test_tap2subunit.py \
15 python/subunit/tests/test_test_protocol.py \
16+ python/subunit/tests/test_test_results.py \
17 runtests.py \
18 shell/README \
19 shell/share/subunit.sh \
20@@ -51,7 +53,9 @@
21
22 pkgpython_PYTHON = \
23 python/subunit/__init__.py \
24- python/subunit/run.py
25+ python/subunit/iso8601.py \
26+ python/subunit/run.py \
27+ python/subunit/test_results.py
28
29 lib_LTLIBRARIES = libsubunit.la
30
31
32=== modified file 'NEWS'
33--- NEWS 2009-07-20 07:11:54 +0000
34+++ NEWS 2009-07-22 09:39:00 +0000
35@@ -14,7 +14,20 @@
36
37 API CHANGES:
38
39+ * When a time: directive is encountered in a subunit stream, the python
40+ bindings now call the ``time(seconds)`` method on ``TestResult``.
41+
42 INTERNALS:
43
44- * ExecTestCase supports passing arguments to test scripts.
45-
46+ * (python) Added ``subunit.test_results.AutoTimingTestResultDecorator``. Most
47+ users of subunit will want to wrap their ``TestProtocolClient`` objects
48+ in this decorator to get test timing data for performance analysis.
49+
50+ * (python) ExecTestCase supports passing arguments to test scripts.
51+
52+ * (python) New helper ``subunit.test_results.HookedTestResultDecorator``
53+ which can be used to call some code on every event, without having to
54+ implement all the event methods.
55+
56+ * (python) ``TestProtocolClient.time(a_datetime)`` has been added which
57+ causes a timestamp to be output to the stream.
58
59=== modified file 'README'
60--- README 2009-07-18 01:28:43 +0000
61+++ README 2009-07-22 09:39:00 +0000
62@@ -17,6 +17,10 @@
63 Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA
64
65
66+ subunit reuses iso8601 by Michael Twomey, distributed under an MIT style
67+ licence - see python/iso8601/LICENSE for details.
68+
69+
70 Subunit
71 -------
72
73@@ -44,7 +48,7 @@
74 * tap2subunit - convert perl's TestAnythingProtocol to subunit.
75 * subunit2pyunit - convert a subunit stream to pyunit test results.
76 * subunit-filter - filter out tests from a subunit stream.
77- * subunit-ls - list the tests present in a subunit stream.
78+ * subunit-ls - list info about tests present in a subunit stream.
79 * subunit-stats - generate a summary of a subunit stream.
80 * subunit-tags - add or remove tags from a stream.
81
82@@ -68,6 +72,9 @@
83 stream = file('tests.log', 'wb')
84 # Create a subunit result object which will output to the stream
85 result = subunit.TestProtocolClient(stream)
86+ # Optionally, to get timing data for performance analysis, wrap the
87+ # serialiser with a timing decorator
88+ result = subunit.test_results.AutoTimingTestResultDecorator(result)
89 # Run the test suite reporting to the subunit result object
90 suite.run(result)
91 # Close the stream.
92@@ -123,6 +130,12 @@
93 # needed and report to your result object.
94 suite.run(result)
95
96+subunit includes extensions to the python ``TestResult`` protocol. The
97+``time(a_datetime)`` method is called (if present) when a ``time:``
98+directive is encountered in a subunit stream. This is used to tell a TestResult
99+about the time that events in the stream occured at, to allow reconstructing
100+test timing from a stream.
101+
102 Finally, subunit.run is a convenience wrapper to run a python test suite via
103 the command line, reporting via subunit::
104
105@@ -215,8 +228,8 @@
106 In Python, tags are assigned to the .tags attribute on the RemoteTest objects
107 created by the TestProtocolServer.
108
109-The time element acts as a clock event - it sets the time for all future events.
110-Currently this is not exposed at the python API layer.
111+The time element acts as a clock event - it sets the time for all future
112+events. The value should be a valid ISO8601 time.
113
114 The skip result is used to indicate a test that was found by the runner but not
115 fully executed due to some policy or dependency issue. This is represented in
116
117=== modified file 'filters/subunit-ls'
118--- filters/subunit-ls 2009-03-08 20:34:19 +0000
119+++ filters/subunit-ls 2009-07-20 10:12:42 +0000
120@@ -19,6 +19,7 @@
121
122 """List tests in a subunit stream."""
123
124+from optparse import OptionParser
125 import sys
126 import unittest
127
128@@ -26,32 +27,60 @@
129
130 class TestIdPrintingResult(unittest.TestResult):
131
132- def __init__(self, stream):
133+ def __init__(self, stream, show_times=False):
134 """Create a FilterResult object outputting to stream."""
135 unittest.TestResult.__init__(self)
136 self._stream = stream
137 self.failed_tests = 0
138+ self.__time = 0
139+ self.show_times = show_times
140+ self._test = None
141+ self._test_duration = 0
142
143 def addError(self, test, err):
144 self.failed_tests += 1
145- self.reportTest(test)
146+ self._test = test
147
148 def addFailure(self, test, err):
149 self.failed_tests += 1
150- self.reportTest(test)
151+ self._test = test
152
153 def addSuccess(self, test):
154- self.reportTest(test)
155-
156- def reportTest(self, test):
157- self._stream.write(test.id() + '\n')
158+ self._test = test
159+
160+ def reportTest(self, test, duration):
161+ if self.show_times:
162+ seconds = duration.seconds
163+ seconds += duration.days * 3600 * 24
164+ seconds += duration.microseconds / 1000000.0
165+ self._stream.write(test.id() + ' %0.3f\n' % seconds)
166+ else:
167+ self._stream.write(test.id() + '\n')
168+
169+ def startTest(self, test):
170+ self._start_time = self._time()
171+
172+ def stopTest(self, test):
173+ test_duration = self._time() - self._start_time
174+ self.reportTest(self._test, test_duration)
175+
176+ def time(self, time):
177+ self.__time = time
178+
179+ def _time(self):
180+ return self.__time
181
182 def wasSuccessful(self):
183 "Tells whether or not this result was a success"
184 return self.failed_tests == 0
185
186
187-result = TestIdPrintingResult(sys.stdout)
188+parser = OptionParser(description=__doc__)
189+parser.add_option("--times", action="store_true",
190+ help="list the time each test took (requires a timestamped stream)",
191+ default=False)
192+(options, args) = parser.parse_args()
193+result = TestIdPrintingResult(sys.stdout, options.times)
194 test = ProtocolTestCase(sys.stdin)
195 test.run(result)
196 if result.wasSuccessful():
197
198=== added directory 'python/iso8601'
199=== added file 'python/iso8601/LICENSE'
200--- python/iso8601/LICENSE 1970-01-01 00:00:00 +0000
201+++ python/iso8601/LICENSE 2009-07-20 10:08:11 +0000
202@@ -0,0 +1,20 @@
203+Copyright (c) 2007 Michael Twomey
204+
205+Permission is hereby granted, free of charge, to any person obtaining a
206+copy of this software and associated documentation files (the
207+"Software"), to deal in the Software without restriction, including
208+without limitation the rights to use, copy, modify, merge, publish,
209+distribute, sublicense, and/or sell copies of the Software, and to
210+permit persons to whom the Software is furnished to do so, subject to
211+the following conditions:
212+
213+The above copyright notice and this permission notice shall be included
214+in all copies or substantial portions of the Software.
215+
216+THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS
217+OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
218+MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT.
219+IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY
220+CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT,
221+TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE
222+SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
223
224=== added file 'python/iso8601/README'
225--- python/iso8601/README 1970-01-01 00:00:00 +0000
226+++ python/iso8601/README 2009-07-20 10:08:11 +0000
227@@ -0,0 +1,26 @@
228+A simple package to deal with ISO 8601 date time formats.
229+
230+ISO 8601 defines a neutral, unambiguous date string format, which also
231+has the property of sorting naturally.
232+
233+e.g. YYYY-MM-DDTHH:MM:SSZ or 2007-01-25T12:00:00Z
234+
235+Currently this covers only the most common date formats encountered, not
236+all of ISO 8601 is handled.
237+
238+Currently the following formats are handled:
239+
240+* 2006-01-01T00:00:00Z
241+* 2006-01-01T00:00:00[+-]00:00
242+
243+I'll add more as I encounter them in my day to day life. Patches with
244+new formats and tests will be gratefully accepted of course :)
245+
246+References:
247+
248+* http://www.cl.cam.ac.uk/~mgk25/iso-time.html - simple overview
249+
250+* http://hydracen.com/dx/iso8601.htm - more detailed enumeration of
251+ valid formats.
252+
253+See the LICENSE file for the license this package is released under.
254
255=== added file 'python/iso8601/README.subunit'
256--- python/iso8601/README.subunit 1970-01-01 00:00:00 +0000
257+++ python/iso8601/README.subunit 2009-07-20 10:08:11 +0000
258@@ -0,0 +1,5 @@
259+This is a [slightly rearranged] import of http://pypi.python.org/pypi/iso8601/
260+version 0.1.4. The OS X hidden files have been stripped, and the package
261+turned into a single module, to simplify installation. The remainder of the
262+source distribution is included in the subunit source tree at python/iso8601
263+for reference.
264
265=== added file 'python/iso8601/setup.py'
266--- python/iso8601/setup.py 1970-01-01 00:00:00 +0000
267+++ python/iso8601/setup.py 2009-07-20 10:08:11 +0000
268@@ -0,0 +1,58 @@
269+try:
270+ from setuptools import setup
271+except ImportError:
272+ from distutils import setup
273+
274+long_description="""Simple module to parse ISO 8601 dates
275+
276+This module parses the most common forms of ISO 8601 date strings (e.g.
277+2007-01-14T20:34:22+00:00) into datetime objects.
278+
279+>>> import iso8601
280+>>> iso8601.parse_date("2007-01-25T12:00:00Z")
281+datetime.datetime(2007, 1, 25, 12, 0, tzinfo=<iso8601.iso8601.Utc ...>)
282+>>>
283+
284+Changes
285+=======
286+
287+0.1.4
288+-----
289+
290+* The default_timezone argument wasn't being passed through correctly,
291+ UTC was being used in every case. Fixes issue 10.
292+
293+0.1.3
294+-----
295+
296+* Fixed the microsecond handling, the generated microsecond values were
297+ way too small. Fixes issue 9.
298+
299+0.1.2
300+-----
301+
302+* Adding ParseError to __all__ in iso8601 module, allows people to import it.
303+ Addresses issue 7.
304+* Be a little more flexible when dealing with dates without leading zeroes.
305+ This violates the spec a little, but handles more dates as seen in the
306+ field. Addresses issue 6.
307+* Allow date/time separators other than T.
308+
309+0.1.1
310+-----
311+
312+* When parsing dates without a timezone the specified default is used. If no
313+ default is specified then UTC is used. Addresses issue 4.
314+"""
315+
316+setup(
317+ name="iso8601",
318+ version="0.1.4",
319+ description=long_description.split("\n")[0],
320+ long_description=long_description,
321+ author="Michael Twomey",
322+ author_email="micktwomey+iso8601@gmail.com",
323+ url="http://code.google.com/p/pyiso8601/",
324+ packages=["iso8601"],
325+ license="MIT",
326+)
327
328=== added file 'python/iso8601/test_iso8601.py'
329--- python/iso8601/test_iso8601.py 1970-01-01 00:00:00 +0000
330+++ python/iso8601/test_iso8601.py 2009-07-20 10:08:11 +0000
331@@ -0,0 +1,111 @@
332+import iso8601
333+
334+def test_iso8601_regex():
335+ assert iso8601.ISO8601_REGEX.match("2006-10-11T00:14:33Z")
336+
337+def test_timezone_regex():
338+ assert iso8601.TIMEZONE_REGEX.match("+01:00")
339+ assert iso8601.TIMEZONE_REGEX.match("+00:00")
340+ assert iso8601.TIMEZONE_REGEX.match("+01:20")
341+ assert iso8601.TIMEZONE_REGEX.match("-01:00")
342+
343+def test_parse_date():
344+ d = iso8601.parse_date("2006-10-20T15:34:56Z")
345+ assert d.year == 2006
346+ assert d.month == 10
347+ assert d.day == 20
348+ assert d.hour == 15
349+ assert d.minute == 34
350+ assert d.second == 56
351+ assert d.tzinfo == iso8601.UTC
352+
353+def test_parse_date_fraction():
354+ d = iso8601.parse_date("2006-10-20T15:34:56.123Z")
355+ assert d.year == 2006
356+ assert d.month == 10
357+ assert d.day == 20
358+ assert d.hour == 15
359+ assert d.minute == 34
360+ assert d.second == 56
361+ assert d.microsecond == 123000
362+ assert d.tzinfo == iso8601.UTC
363+
364+def test_parse_date_fraction_2():
365+ """From bug 6
366+
367+ """
368+ d = iso8601.parse_date("2007-5-7T11:43:55.328Z'")
369+ assert d.year == 2007
370+ assert d.month == 5
371+ assert d.day == 7
372+ assert d.hour == 11
373+ assert d.minute == 43
374+ assert d.second == 55
375+ assert d.microsecond == 328000
376+ assert d.tzinfo == iso8601.UTC
377+
378+def test_parse_date_tz():
379+ d = iso8601.parse_date("2006-10-20T15:34:56.123+02:30")
380+ assert d.year == 2006
381+ assert d.month == 10
382+ assert d.day == 20
383+ assert d.hour == 15
384+ assert d.minute == 34
385+ assert d.second == 56
386+ assert d.microsecond == 123000
387+ assert d.tzinfo.tzname(None) == "+02:30"
388+ offset = d.tzinfo.utcoffset(None)
389+ assert offset.days == 0
390+ assert offset.seconds == 60 * 60 * 2.5
391+
392+def test_parse_invalid_date():
393+ try:
394+ iso8601.parse_date(None)
395+ except iso8601.ParseError:
396+ pass
397+ else:
398+ assert 1 == 2
399+
400+def test_parse_invalid_date2():
401+ try:
402+ iso8601.parse_date("23")
403+ except iso8601.ParseError:
404+ pass
405+ else:
406+ assert 1 == 2
407+
408+def test_parse_no_timezone():
409+ """issue 4 - Handle datetime string without timezone
410+
411+ This tests what happens when you parse a date with no timezone. While not
412+ strictly correct this is quite common. I'll assume UTC for the time zone
413+ in this case.
414+ """
415+ d = iso8601.parse_date("2007-01-01T08:00:00")
416+ assert d.year == 2007
417+ assert d.month == 1
418+ assert d.day == 1
419+ assert d.hour == 8
420+ assert d.minute == 0
421+ assert d.second == 0
422+ assert d.microsecond == 0
423+ assert d.tzinfo == iso8601.UTC
424+
425+def test_parse_no_timezone_different_default():
426+ tz = iso8601.FixedOffset(2, 0, "test offset")
427+ d = iso8601.parse_date("2007-01-01T08:00:00", default_timezone=tz)
428+ assert d.tzinfo == tz
429+
430+def test_space_separator():
431+ """Handle a separator other than T
432+
433+ """
434+ d = iso8601.parse_date("2007-06-23 06:40:34.00Z")
435+ assert d.year == 2007
436+ assert d.month == 6
437+ assert d.day == 23
438+ assert d.hour == 6
439+ assert d.minute == 40
440+ assert d.second == 34
441+ assert d.microsecond == 0
442+ assert d.tzinfo == iso8601.UTC
443
444=== modified file 'python/subunit/__init__.py'
445--- python/subunit/__init__.py 2009-07-18 03:43:05 +0000
446+++ python/subunit/__init__.py 2009-07-22 08:46:04 +0000
447@@ -17,13 +17,17 @@
448 # Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA
449 #
450
451+import datetime
452 import os
453+import re
454 from StringIO import StringIO
455 import subprocess
456 import sys
457-import re
458 import unittest
459
460+import iso8601
461+
462+
463 def test_suite():
464 import subunit.tests
465 return subunit.tests.test_suite()
466@@ -207,6 +211,16 @@
467 update_tags.update(new_tags)
468 update_tags.difference_update(gone_tags)
469
470+ def _handleTime(self, offset, line):
471+ # Accept it, but do not do anything with it yet.
472+ try:
473+ event_time = iso8601.parse_date(line[offset:-1])
474+ except TypeError, e:
475+ raise TypeError("Failed to parse %r, got %r" % (line, e))
476+ time_method = getattr(self.client, 'time', None)
477+ if callable(time_method):
478+ time_method(event_time)
479+
480 def lineReceived(self, line):
481 """Call the appropriate local method for the received line."""
482 if line == "]\n":
483@@ -236,8 +250,7 @@
484 elif cmd in ('tags',):
485 self._handleTags(offset, line)
486 elif cmd in ('time',):
487- # Accept it, but do not do anything with it yet.
488- pass
489+ self._handleTime(offset, line)
490 elif cmd == 'xfail':
491 self._addExpectedFail(offset, line)
492 else:
493@@ -342,6 +355,16 @@
494 """Mark a test as starting its test run."""
495 self._stream.write("test: %s\n" % test.id())
496
497+ def time(self, a_datetime):
498+ """Inform the client of the time.
499+
500+ ":param datetime: A datetime.datetime object.
501+ """
502+ time = a_datetime.astimezone(iso8601.Utc())
503+ self._stream.write("time: %04d-%02d-%02d %02d:%02d:%02d.%06dZ\n" % (
504+ time.year, time.month, time.day, time.hour, time.minute,
505+ time.second, time.microsecond))
506+
507 def done(self):
508 """Obey the testtools result.done() interface."""
509
510
511=== added file 'python/subunit/iso8601.py'
512--- python/subunit/iso8601.py 1970-01-01 00:00:00 +0000
513+++ python/subunit/iso8601.py 2009-07-20 10:08:11 +0000
514@@ -0,0 +1,123 @@
515+# Copyright (c) 2007 Michael Twomey
516+#
517+# Permission is hereby granted, free of charge, to any person obtaining a
518+# copy of this software and associated documentation files (the
519+# "Software"), to deal in the Software without restriction, including
520+# without limitation the rights to use, copy, modify, merge, publish,
521+# distribute, sublicense, and/or sell copies of the Software, and to
522+# permit persons to whom the Software is furnished to do so, subject to
523+# the following conditions:
524+#
525+# The above copyright notice and this permission notice shall be included
526+# in all copies or substantial portions of the Software.
527+#
528+# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS
529+# OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
530+# MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT.
531+# IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY
532+# CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT,
533+# TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE
534+# SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
535+
536+"""ISO 8601 date time string parsing
537+
538+Basic usage:
539+>>> import iso8601
540+>>> iso8601.parse_date("2007-01-25T12:00:00Z")
541+datetime.datetime(2007, 1, 25, 12, 0, tzinfo=<iso8601.iso8601.Utc ...>)
542+>>>
543+
544+"""
545+
546+from datetime import datetime, timedelta, tzinfo
547+import re
548+
549+__all__ = ["parse_date", "ParseError"]
550+
551+# Adapted from http://delete.me.uk/2005/03/iso8601.html
552+ISO8601_REGEX = re.compile(r"(?P<year>[0-9]{4})(-(?P<month>[0-9]{1,2})(-(?P<day>[0-9]{1,2})"
553+ r"((?P<separator>.)(?P<hour>[0-9]{2}):(?P<minute>[0-9]{2})(:(?P<second>[0-9]{2})(\.(?P<fraction>[0-9]+))?)?"
554+ r"(?P<timezone>Z|(([-+])([0-9]{2}):([0-9]{2})))?)?)?)?"
555+)
556+TIMEZONE_REGEX = re.compile("(?P<prefix>[+-])(?P<hours>[0-9]{2}).(?P<minutes>[0-9]{2})")
557+
558+class ParseError(Exception):
559+ """Raised when there is a problem parsing a date string"""
560+
561+# Yoinked from python docs
562+ZERO = timedelta(0)
563+class Utc(tzinfo):
564+ """UTC
565+
566+ """
567+ def utcoffset(self, dt):
568+ return ZERO
569+
570+ def tzname(self, dt):
571+ return "UTC"
572+
573+ def dst(self, dt):
574+ return ZERO
575+UTC = Utc()
576+
577+class FixedOffset(tzinfo):
578+ """Fixed offset in hours and minutes from UTC
579+
580+ """
581+ def __init__(self, offset_hours, offset_minutes, name):
582+ self.__offset = timedelta(hours=offset_hours, minutes=offset_minutes)
583+ self.__name = name
584+
585+ def utcoffset(self, dt):
586+ return self.__offset
587+
588+ def tzname(self, dt):
589+ return self.__name
590+
591+ def dst(self, dt):
592+ return ZERO
593+
594+ def __repr__(self):
595+ return "<FixedOffset %r>" % self.__name
596+
597+def parse_timezone(tzstring, default_timezone=UTC):
598+ """Parses ISO 8601 time zone specs into tzinfo offsets
599+
600+ """
601+ if tzstring == "Z":
602+ return default_timezone
603+ # This isn't strictly correct, but it's common to encounter dates without
604+ # timezones so I'll assume the default (which defaults to UTC).
605+ # Addresses issue 4.
606+ if tzstring is None:
607+ return default_timezone
608+ m = TIMEZONE_REGEX.match(tzstring)
609+ prefix, hours, minutes = m.groups()
610+ hours, minutes = int(hours), int(minutes)
611+ if prefix == "-":
612+ hours = -hours
613+ minutes = -minutes
614+ return FixedOffset(hours, minutes, tzstring)
615+
616+def parse_date(datestring, default_timezone=UTC):
617+ """Parses ISO 8601 dates into datetime objects
618+
619+ The timezone is parsed from the date string. However it is quite common to
620+ have dates without a timezone (not strictly correct). In this case the
621+ default timezone specified in default_timezone is used. This is UTC by
622+ default.
623+ """
624+ if not isinstance(datestring, basestring):
625+ raise ParseError("Expecting a string %r" % datestring)
626+ m = ISO8601_REGEX.match(datestring)
627+ if not m:
628+ raise ParseError("Unable to parse date string %r" % datestring)
629+ groups = m.groupdict()
630+ tz = parse_timezone(groups["timezone"], default_timezone=default_timezone)
631+ if groups["fraction"] is None:
632+ groups["fraction"] = 0
633+ else:
634+ groups["fraction"] = int(float("0.%s" % groups["fraction"]) * 1e6)
635+ return datetime(int(groups["year"]), int(groups["month"]), int(groups["day"]),
636+ int(groups["hour"]), int(groups["minute"]), int(groups["second"]),
637+ int(groups["fraction"]), tz)
638
639=== added file 'python/subunit/test_results.py'
640--- python/subunit/test_results.py 1970-01-01 00:00:00 +0000
641+++ python/subunit/test_results.py 2009-07-22 09:39:00 +0000
642@@ -0,0 +1,131 @@
643+#
644+# subunit: extensions to Python unittest to get test results from subprocesses.
645+# Copyright (C) 2009 Robert Collins <robertc@robertcollins.net>
646+#
647+# This program is free software; you can redistribute it and/or modify
648+# it under the terms of the GNU General Public License as published by
649+# the Free Software Foundation; either version 2 of the License, or
650+# (at your option) any later version.
651+#
652+# This program is distributed in the hope that it will be useful,
653+# but WITHOUT ANY WARRANTY; without even the implied warranty of
654+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
655+# GNU General Public License for more details.
656+#
657+# You should have received a copy of the GNU General Public License
658+# along with this program; if not, write to the Free Software
659+# Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA
660+#
661+
662+"""TestResult helper classes used to by subunit."""
663+
664+import datetime
665+
666+import iso8601
667+
668+class HookedTestResultDecorator(object):
669+ """A TestResult which calls a hook on every event."""
670+
671+ def __init__(self, decorated):
672+ self.decorated = decorated
673+
674+ def _call_maybe(self, method_name, *params):
675+ """Call method_name on self.decorated, if present.
676+
677+ This is used to guard newer methods which older pythons do not
678+ support. While newer clients won't call these methods if they don't
679+ exist, they do exist on the decorator, and thus the decorator has to be
680+ the one to filter them out.
681+
682+ :param method_name: The name of the method to call.
683+ :param *params: Parameters to pass to method_name.
684+ :return: The result of self.decorated.method_name(*params), if it
685+ exists, and None otherwise.
686+ """
687+ method = getattr(self.decorated, method_name, None)
688+ if method is None:
689+ return
690+ return method(*params)
691+
692+ def startTest(self, test):
693+ self._before_event()
694+ return self.decorated.startTest(test)
695+
696+ def startTestRun(self):
697+ self._before_event()
698+ return self._call_maybe("startTestRun")
699+
700+ def stopTest(self, test):
701+ self._before_event()
702+ return self.decorated.stopTest(test)
703+
704+ def stopTestRun(self):
705+ self._before_event()
706+ return self._call_maybe("stopTestRun")
707+
708+ def addError(self, test, err):
709+ self._before_event()
710+ return self.decorated.addError(test, err)
711+
712+ def addFailure(self, test, err):
713+ self._before_event()
714+ return self.decorated.addFailure(test, err)
715+
716+ def addSuccess(self, test):
717+ self._before_event()
718+ return self.decorated.addSuccess(test)
719+
720+ def addSkip(self, test, reason):
721+ self._before_event()
722+ return self._call_maybe("addSkip", test, reason)
723+
724+ def addExpectedFailure(self, test, err):
725+ self._before_event()
726+ return self._call_maybe("addExpectedFailure", test, err)
727+
728+ def addUnexpectedSuccess(self, test):
729+ self._before_event()
730+ return self._call_maybe("addUnexpectedSuccess", test)
731+
732+ def wasSuccessful(self):
733+ self._before_event()
734+ return self.decorated.wasSuccessful()
735+
736+ def stop(self):
737+ self._before_event()
738+ return self.decorated.stop()
739+
740+ def time(self, a_datetime):
741+ self._before_event()
742+ return self._call_maybe("time", a_datetime)
743+
744+
745+class AutoTimingTestResultDecorator(HookedTestResultDecorator):
746+ """Decorate a TestResult to add time events to a test run.
747+
748+ By default this will cause a time event before every test event,
749+ but if explicit time data is being provided by the test run, then
750+ this decorator will turn itself off to prevent causing confusion.
751+ """
752+
753+ def __init__(self, decorated):
754+ self._time = None
755+ super(AutoTimingTestResultDecorator, self).__init__(decorated)
756+
757+ def _before_event(self):
758+ time = self._time
759+ if time is not None:
760+ return
761+ time = datetime.datetime.utcnow().replace(tzinfo=iso8601.Utc())
762+ self._call_maybe("time", time)
763+
764+ def time(self, a_datetime):
765+ """Provide a timestamp for the current test activity.
766+
767+ :param a_datetime: If None, automatically add timestamps before every
768+ event (this is the default behaviour if time() is not called at
769+ all). If not None, pass the provided time onto the decorated
770+ result object and disable automatic timestamps.
771+ """
772+ self._time = a_datetime
773+ return self._call_maybe("time", a_datetime)
774
775=== modified file 'python/subunit/tests/__init__.py'
776--- python/subunit/tests/__init__.py 2009-02-22 06:28:08 +0000
777+++ python/subunit/tests/__init__.py 2009-07-22 08:11:41 +0000
778@@ -24,10 +24,12 @@
779 test_subunit_tags,
780 test_tap2subunit,
781 test_test_protocol,
782+ test_test_results,
783 )
784
785 def test_suite():
786 result = TestUtil.TestSuite()
787+ result.addTest(test_test_results.test_suite())
788 result.addTest(test_test_protocol.test_suite())
789 result.addTest(test_tap2subunit.test_suite())
790 result.addTest(test_subunit_filter.test_suite())
791
792=== modified file 'python/subunit/tests/test_test_protocol.py'
793--- python/subunit/tests/test_test_protocol.py 2009-07-18 03:43:05 +0000
794+++ python/subunit/tests/test_test_protocol.py 2009-07-22 08:46:04 +0000
795@@ -17,46 +17,49 @@
796 # Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA
797 #
798
799+import datetime
800 import unittest
801 from StringIO import StringIO
802 import os
803 import subunit
804 import sys
805-import time
806-
807-try:
808- class MockTestProtocolServerClient(object):
809- """A mock protocol server client to test callbacks."""
810-
811- def __init__(self):
812- self.end_calls = []
813- self.error_calls = []
814- self.failure_calls = []
815- self.skip_calls = []
816- self.start_calls = []
817- self.success_calls = []
818- super(MockTestProtocolServerClient, self).__init__()
819-
820- def addError(self, test, error):
821- self.error_calls.append((test, error))
822-
823- def addFailure(self, test, error):
824- self.failure_calls.append((test, error))
825-
826- def addSkip(self, test, reason):
827- self.skip_calls.append((test, reason))
828-
829- def addSuccess(self, test):
830- self.success_calls.append(test)
831-
832- def stopTest(self, test):
833- self.end_calls.append(test)
834-
835- def startTest(self, test):
836- self.start_calls.append(test)
837-
838-except AttributeError:
839- MockTestProtocolServer = None
840+
841+import subunit.iso8601 as iso8601
842+
843+
844+class MockTestProtocolServerClient(object):
845+ """A mock protocol server client to test callbacks."""
846+
847+ def __init__(self):
848+ self.end_calls = []
849+ self.error_calls = []
850+ self.failure_calls = []
851+ self.skip_calls = []
852+ self.start_calls = []
853+ self.success_calls = []
854+ self._time = None
855+ super(MockTestProtocolServerClient, self).__init__()
856+
857+ def addError(self, test, error):
858+ self.error_calls.append((test, error))
859+
860+ def addFailure(self, test, error):
861+ self.failure_calls.append((test, error))
862+
863+ def addSkip(self, test, reason):
864+ self.skip_calls.append((test, reason))
865+
866+ def addSuccess(self, test):
867+ self.success_calls.append(test)
868+
869+ def stopTest(self, test):
870+ self.end_calls.append(test)
871+
872+ def startTest(self, test):
873+ self.start_calls.append(test)
874+
875+ def time(self, time):
876+ self._time = time
877
878
879 class TestMockTestProtocolServer(unittest.TestCase):
880@@ -763,15 +766,23 @@
881 class TestTestProtocolServerStreamTime(unittest.TestCase):
882 """Test managing time information at the protocol level."""
883
884- def setUp(self):
885- self.client = MockTestProtocolServerClient()
886+ def test_time_accepted_stdlib(self):
887+ self.result = unittest.TestResult()
888 self.stream = StringIO()
889- self.protocol = subunit.TestProtocolServer(self.client,
890+ self.protocol = subunit.TestProtocolServer(self.result,
891 stream=self.stream)
892+ self.protocol.lineReceived("time: 2001-12-12 12:59:59Z\n")
893+ self.assertEqual("", self.stream.getvalue())
894
895- def test_time_accepted(self):
896+ def test_time_accepted_extended(self):
897+ self.result = MockTestProtocolServerClient()
898+ self.stream = StringIO()
899+ self.protocol = subunit.TestProtocolServer(self.result,
900+ stream=self.stream)
901 self.protocol.lineReceived("time: 2001-12-12 12:59:59Z\n")
902 self.assertEqual("", self.stream.getvalue())
903+ self.assertEqual(datetime.datetime(2001, 12, 12, 12, 59, 59, 0,
904+ iso8601.Utc()), self.result._time)
905
906
907 class TestRemotedTestCase(unittest.TestCase):
908@@ -958,7 +969,7 @@
909 self.assertEqual(self.io.getvalue(), "test: %s\n" % self.test.id())
910
911 def test_stop_test(self):
912- """Test stopTest on a TestProtocolClient."""
913+ # stopTest doesn't output anything.
914 self.protocol.stopTest(self.test)
915 self.assertEqual(self.io.getvalue(), "")
916
917@@ -994,6 +1005,14 @@
918 self.io.getvalue(),
919 'skip: %s [\nHas it really?\n]\n' % self.test.id())
920
921+ def test_time(self):
922+ # Calling time() outputs a time signal immediately.
923+ self.protocol.time(
924+ datetime.datetime(2009,10,11,12,13,14,15, iso8601.Utc()))
925+ self.assertEqual(
926+ "time: 2009-10-11 12:13:14.000015Z\n",
927+ self.io.getvalue())
928+
929
930 def test_suite():
931 loader = subunit.tests.TestUtil.TestLoader()
932
933=== added file 'python/subunit/tests/test_test_results.py'
934--- python/subunit/tests/test_test_results.py 1970-01-01 00:00:00 +0000
935+++ python/subunit/tests/test_test_results.py 2009-07-22 09:39:00 +0000
936@@ -0,0 +1,163 @@
937+#
938+# subunit: extensions to Python unittest to get test results from subprocesses.
939+# Copyright (C) 2009 Robert Collins <robertc@robertcollins.net>
940+#
941+# This program is free software; you can redistribute it and/or modify
942+# it under the terms of the GNU General Public License as published by
943+# the Free Software Foundation; either version 2 of the License, or
944+# (at your option) any later version.
945+#
946+# This program is distributed in the hope that it will be useful,
947+# but WITHOUT ANY WARRANTY; without even the implied warranty of
948+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
949+# GNU General Public License for more details.
950+#
951+# You should have received a copy of the GNU General Public License
952+# along with this program; if not, write to the Free Software
953+# Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA
954+#
955+
956+import datetime
957+import unittest
958+from StringIO import StringIO
959+import os
960+import subunit.test_results
961+import sys
962+
963+import subunit.iso8601 as iso8601
964+
965+
966+class LoggingDecorator(subunit.test_results.HookedTestResultDecorator):
967+
968+ def __init__(self, decorated):
969+ self._calls = 0
970+ super(LoggingDecorator, self).__init__(decorated)
971+
972+ def _before_event(self):
973+ self._calls += 1
974+
975+
976+class AssertBeforeTestResult(LoggingDecorator):
977+ """A TestResult for checking preconditions."""
978+
979+ def __init__(self, decorated, test):
980+ self.test = test
981+ super(AssertBeforeTestResult, self).__init__(decorated)
982+
983+ def _before_event(self):
984+ self.test.assertEqual(1, self.earlier._calls)
985+ super(AssertBeforeTestResult, self)._before_event()
986+
987+
988+class TimeCapturingResult(unittest.TestResult):
989+
990+ def __init__(self):
991+ super(TimeCapturingResult, self).__init__()
992+ self._calls = []
993+
994+ def time(self, a_datetime):
995+ self._calls.append(a_datetime)
996+
997+
998+class TestHookedTestResultDecorator(unittest.TestCase):
999+
1000+ def setUp(self):
1001+ # And end to the chain
1002+ terminal = unittest.TestResult()
1003+ # Asserts that the call was made to self.result before asserter was
1004+ # called.
1005+ asserter = AssertBeforeTestResult(terminal, self)
1006+ # The result object we call, which much increase its call count.
1007+ self.result = LoggingDecorator(asserter)
1008+ asserter.earlier = self.result
1009+
1010+ def tearDown(self):
1011+ # The hook in self.result must have been called
1012+ self.assertEqual(1, self.result._calls)
1013+ # The hook in asserter must have been called too, otherwise the
1014+ # assertion about ordering won't have completed.
1015+ self.assertEqual(1, self.result.decorated._calls)
1016+
1017+ def test_startTest(self):
1018+ self.result.startTest(self)
1019+
1020+ def test_startTestRun(self):
1021+ self.result.startTestRun()
1022+
1023+ def test_stopTest(self):
1024+ self.result.stopTest(self)
1025+
1026+ def test_stopTestRun(self):
1027+ self.result.stopTestRun()
1028+
1029+ def test_addError(self):
1030+ self.result.addError(self, subunit.RemoteError())
1031+
1032+ def test_addFailure(self):
1033+ self.result.addFailure(self, subunit.RemoteError())
1034+
1035+ def test_addSuccess(self):
1036+ self.result.addSuccess(self)
1037+
1038+ def test_addSkip(self):
1039+ self.result.addSkip(self, "foo")
1040+
1041+ def test_addExpectedFailure(self):
1042+ self.result.addExpectedFailure(self, subunit.RemoteError())
1043+
1044+ def test_addUnexpectedSuccess(self):
1045+ self.result.addUnexpectedSuccess(self)
1046+
1047+ def test_wasSuccessful(self):
1048+ self.result.wasSuccessful()
1049+
1050+ def test_stop(self):
1051+ self.result.stop()
1052+
1053+ def test_time(self):
1054+ self.result.time(None)
1055+
1056+
1057+class TestAutoTimingTestResultDecorator(unittest.TestCase):
1058+
1059+ def setUp(self):
1060+ # And end to the chain which captures time events.
1061+ terminal = TimeCapturingResult()
1062+ # The result object under test.
1063+ self.result = subunit.test_results.AutoTimingTestResultDecorator(
1064+ terminal)
1065+
1066+ def test_without_time_calls_time_is_called_and_not_None(self):
1067+ self.result.startTest(self)
1068+ self.assertEqual(1, len(self.result.decorated._calls))
1069+ self.assertNotEqual(None, self.result.decorated._calls[0])
1070+
1071+ def test_calling_time_inhibits_automatic_time(self):
1072+ # Calling time() outputs a time signal immediately and prevents
1073+ # automatically adding one when other methods are called.
1074+ time = datetime.datetime(2009,10,11,12,13,14,15, iso8601.Utc())
1075+ self.result.time(time)
1076+ self.result.startTest(self)
1077+ self.result.stopTest(self)
1078+ self.assertEqual(1, len(self.result.decorated._calls))
1079+ self.assertEqual(time, self.result.decorated._calls[0])
1080+
1081+ def test_calling_time_None_enables_automatic_time(self):
1082+ time = datetime.datetime(2009,10,11,12,13,14,15, iso8601.Utc())
1083+ self.result.time(time)
1084+ self.assertEqual(1, len(self.result.decorated._calls))
1085+ self.assertEqual(time, self.result.decorated._calls[0])
1086+ # Calling None passes the None through, in case other results care.
1087+ self.result.time(None)
1088+ self.assertEqual(2, len(self.result.decorated._calls))
1089+ self.assertEqual(None, self.result.decorated._calls[1])
1090+ # Calling other methods doesn't generate an automatic time event.
1091+ self.result.startTest(self)
1092+ self.assertEqual(3, len(self.result.decorated._calls))
1093+ self.assertNotEqual(None, self.result.decorated._calls[2])
1094+
1095+
1096+def test_suite():
1097+ loader = subunit.tests.TestUtil.TestLoader()
1098+ result = loader.loadTestsFromName(__name__)
1099+ return result

Subscribers

People subscribed via source and target branches