Merge lp:~lifeless/subunit/time-support into lp:~subunit/subunit/trunk
- time-support
- Merge into 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 |
Related bugs: |
This proposal supersedes a proposal from 2009-07-20.
Commit message
Description of the change
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 | # |
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 |
This teaches the python support to export time data to the TestResult.