Merge lp:~lifeless/subunit/time-support into lp:~subunit/subunit/trunk
- time-support
- Merge into trunk
Proposed by
Robert Collins
Status: | Superseded |
---|---|
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: |
Reviewer | Review Type | Date Requested | Status |
---|---|---|---|
Subunit Developers | Pending | ||
Review via email: mp+9042@code.launchpad.net |
This proposal has been superseded by a proposal from 2009-07-22.
Commit message
Description of the change
To post a comment you must log in.
Revision history for this message
Robert Collins (lifeless) wrote : | # |
- 75. By Robert Collins
-
Add subunit.
test_results. HookedTestResul tDecorator. - 76. By Robert Collins
-
Add TestProtocolCli
ent.time( ). - 77. By Robert Collins
-
Add AutoTimingTestR
esultDecorator.
Unmerged revisions
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-20 10:08:11 +0000 | |||
4 | @@ -51,6 +51,7 @@ | |||
5 | 51 | 51 | ||
6 | 52 | pkgpython_PYTHON = \ | 52 | pkgpython_PYTHON = \ |
7 | 53 | python/subunit/__init__.py \ | 53 | python/subunit/__init__.py \ |
8 | 54 | python/subunit/iso8601.py \ | ||
9 | 54 | python/subunit/run.py | 55 | python/subunit/run.py |
10 | 55 | 56 | ||
11 | 56 | lib_LTLIBRARIES = libsubunit.la | 57 | lib_LTLIBRARIES = libsubunit.la |
12 | 57 | 58 | ||
13 | === modified file 'NEWS' | |||
14 | --- NEWS 2009-07-20 07:11:54 +0000 | |||
15 | +++ NEWS 2009-07-20 08:13:55 +0000 | |||
16 | @@ -14,6 +14,9 @@ | |||
17 | 14 | 14 | ||
18 | 15 | API CHANGES: | 15 | API CHANGES: |
19 | 16 | 16 | ||
20 | 17 | * When a time: directive is encountered in a subunit stream, the python | ||
21 | 18 | bindings now call the ``time(seconds)`` method on ``TestResult``. | ||
22 | 19 | |||
23 | 17 | INTERNALS: | 20 | INTERNALS: |
24 | 18 | 21 | ||
25 | 19 | * ExecTestCase supports passing arguments to test scripts. | 22 | * ExecTestCase supports passing arguments to test scripts. |
26 | 20 | 23 | ||
27 | === modified file 'README' | |||
28 | --- README 2009-07-18 01:28:43 +0000 | |||
29 | +++ README 2009-07-20 10:08:11 +0000 | |||
30 | @@ -17,6 +17,10 @@ | |||
31 | 17 | Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA | 17 | Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA |
32 | 18 | 18 | ||
33 | 19 | 19 | ||
34 | 20 | subunit reuses iso8601 by Michael Twomey, distributed under an MIT style | ||
35 | 21 | licence - see python/iso8601/LICENSE for details. | ||
36 | 22 | |||
37 | 23 | |||
38 | 20 | Subunit | 24 | Subunit |
39 | 21 | ------- | 25 | ------- |
40 | 22 | 26 | ||
41 | @@ -44,7 +48,7 @@ | |||
42 | 44 | * tap2subunit - convert perl's TestAnythingProtocol to subunit. | 48 | * tap2subunit - convert perl's TestAnythingProtocol to subunit. |
43 | 45 | * subunit2pyunit - convert a subunit stream to pyunit test results. | 49 | * subunit2pyunit - convert a subunit stream to pyunit test results. |
44 | 46 | * subunit-filter - filter out tests from a subunit stream. | 50 | * subunit-filter - filter out tests from a subunit stream. |
46 | 47 | * subunit-ls - list the tests present in a subunit stream. | 51 | * subunit-ls - list info about tests present in a subunit stream. |
47 | 48 | * subunit-stats - generate a summary of a subunit stream. | 52 | * subunit-stats - generate a summary of a subunit stream. |
48 | 49 | * subunit-tags - add or remove tags from a stream. | 53 | * subunit-tags - add or remove tags from a stream. |
49 | 50 | 54 | ||
50 | @@ -123,6 +127,12 @@ | |||
51 | 123 | # needed and report to your result object. | 127 | # needed and report to your result object. |
52 | 124 | suite.run(result) | 128 | suite.run(result) |
53 | 125 | 129 | ||
54 | 130 | subunit includes extensions to the python ``TestResult`` protocol. The | ||
55 | 131 | ``time(a_datetime)`` method is called (if present) when a ``time:`` | ||
56 | 132 | directive is encountered in a subunit stream. This is used to tell a TestResult | ||
57 | 133 | about the time that events in the stream occured at, to allow reconstructing | ||
58 | 134 | test timing from a stream. | ||
59 | 135 | |||
60 | 126 | Finally, subunit.run is a convenience wrapper to run a python test suite via | 136 | Finally, subunit.run is a convenience wrapper to run a python test suite via |
61 | 127 | the command line, reporting via subunit:: | 137 | the command line, reporting via subunit:: |
62 | 128 | 138 | ||
63 | @@ -215,8 +225,8 @@ | |||
64 | 215 | In Python, tags are assigned to the .tags attribute on the RemoteTest objects | 225 | In Python, tags are assigned to the .tags attribute on the RemoteTest objects |
65 | 216 | created by the TestProtocolServer. | 226 | created by the TestProtocolServer. |
66 | 217 | 227 | ||
69 | 218 | The time element acts as a clock event - it sets the time for all future events. | 228 | The time element acts as a clock event - it sets the time for all future |
70 | 219 | Currently this is not exposed at the python API layer. | 229 | events. The value should be a valid ISO8601 time. |
71 | 220 | 230 | ||
72 | 221 | The skip result is used to indicate a test that was found by the runner but not | 231 | The skip result is used to indicate a test that was found by the runner but not |
73 | 222 | fully executed due to some policy or dependency issue. This is represented in | 232 | fully executed due to some policy or dependency issue. This is represented in |
74 | 223 | 233 | ||
75 | === modified file 'filters/subunit-ls' | |||
76 | --- filters/subunit-ls 2009-03-08 20:34:19 +0000 | |||
77 | +++ filters/subunit-ls 2009-07-20 10:12:42 +0000 | |||
78 | @@ -19,6 +19,7 @@ | |||
79 | 19 | 19 | ||
80 | 20 | """List tests in a subunit stream.""" | 20 | """List tests in a subunit stream.""" |
81 | 21 | 21 | ||
82 | 22 | from optparse import OptionParser | ||
83 | 22 | import sys | 23 | import sys |
84 | 23 | import unittest | 24 | import unittest |
85 | 24 | 25 | ||
86 | @@ -26,32 +27,60 @@ | |||
87 | 26 | 27 | ||
88 | 27 | class TestIdPrintingResult(unittest.TestResult): | 28 | class TestIdPrintingResult(unittest.TestResult): |
89 | 28 | 29 | ||
91 | 29 | def __init__(self, stream): | 30 | def __init__(self, stream, show_times=False): |
92 | 30 | """Create a FilterResult object outputting to stream.""" | 31 | """Create a FilterResult object outputting to stream.""" |
93 | 31 | unittest.TestResult.__init__(self) | 32 | unittest.TestResult.__init__(self) |
94 | 32 | self._stream = stream | 33 | self._stream = stream |
95 | 33 | self.failed_tests = 0 | 34 | self.failed_tests = 0 |
96 | 35 | self.__time = 0 | ||
97 | 36 | self.show_times = show_times | ||
98 | 37 | self._test = None | ||
99 | 38 | self._test_duration = 0 | ||
100 | 34 | 39 | ||
101 | 35 | def addError(self, test, err): | 40 | def addError(self, test, err): |
102 | 36 | self.failed_tests += 1 | 41 | self.failed_tests += 1 |
104 | 37 | self.reportTest(test) | 42 | self._test = test |
105 | 38 | 43 | ||
106 | 39 | def addFailure(self, test, err): | 44 | def addFailure(self, test, err): |
107 | 40 | self.failed_tests += 1 | 45 | self.failed_tests += 1 |
109 | 41 | self.reportTest(test) | 46 | self._test = test |
110 | 42 | 47 | ||
111 | 43 | def addSuccess(self, test): | 48 | def addSuccess(self, test): |
116 | 44 | self.reportTest(test) | 49 | self._test = test |
117 | 45 | 50 | ||
118 | 46 | def reportTest(self, test): | 51 | def reportTest(self, test, duration): |
119 | 47 | self._stream.write(test.id() + '\n') | 52 | if self.show_times: |
120 | 53 | seconds = duration.seconds | ||
121 | 54 | seconds += duration.days * 3600 * 24 | ||
122 | 55 | seconds += duration.microseconds / 1000000.0 | ||
123 | 56 | self._stream.write(test.id() + ' %0.3f\n' % seconds) | ||
124 | 57 | else: | ||
125 | 58 | self._stream.write(test.id() + '\n') | ||
126 | 59 | |||
127 | 60 | def startTest(self, test): | ||
128 | 61 | self._start_time = self._time() | ||
129 | 62 | |||
130 | 63 | def stopTest(self, test): | ||
131 | 64 | test_duration = self._time() - self._start_time | ||
132 | 65 | self.reportTest(self._test, test_duration) | ||
133 | 66 | |||
134 | 67 | def time(self, time): | ||
135 | 68 | self.__time = time | ||
136 | 69 | |||
137 | 70 | def _time(self): | ||
138 | 71 | return self.__time | ||
139 | 48 | 72 | ||
140 | 49 | def wasSuccessful(self): | 73 | def wasSuccessful(self): |
141 | 50 | "Tells whether or not this result was a success" | 74 | "Tells whether or not this result was a success" |
142 | 51 | return self.failed_tests == 0 | 75 | return self.failed_tests == 0 |
143 | 52 | 76 | ||
144 | 53 | 77 | ||
146 | 54 | result = TestIdPrintingResult(sys.stdout) | 78 | parser = OptionParser(description=__doc__) |
147 | 79 | parser.add_option("--times", action="store_true", | ||
148 | 80 | help="list the time each test took (requires a timestamped stream)", | ||
149 | 81 | default=False) | ||
150 | 82 | (options, args) = parser.parse_args() | ||
151 | 83 | result = TestIdPrintingResult(sys.stdout, options.times) | ||
152 | 55 | test = ProtocolTestCase(sys.stdin) | 84 | test = ProtocolTestCase(sys.stdin) |
153 | 56 | test.run(result) | 85 | test.run(result) |
154 | 57 | if result.wasSuccessful(): | 86 | if result.wasSuccessful(): |
155 | 58 | 87 | ||
156 | === added directory 'python/iso8601' | |||
157 | === added file 'python/iso8601/LICENSE' | |||
158 | --- python/iso8601/LICENSE 1970-01-01 00:00:00 +0000 | |||
159 | +++ python/iso8601/LICENSE 2009-07-20 10:08:11 +0000 | |||
160 | @@ -0,0 +1,20 @@ | |||
161 | 1 | Copyright (c) 2007 Michael Twomey | ||
162 | 2 | |||
163 | 3 | Permission is hereby granted, free of charge, to any person obtaining a | ||
164 | 4 | copy of this software and associated documentation files (the | ||
165 | 5 | "Software"), to deal in the Software without restriction, including | ||
166 | 6 | without limitation the rights to use, copy, modify, merge, publish, | ||
167 | 7 | distribute, sublicense, and/or sell copies of the Software, and to | ||
168 | 8 | permit persons to whom the Software is furnished to do so, subject to | ||
169 | 9 | the following conditions: | ||
170 | 10 | |||
171 | 11 | The above copyright notice and this permission notice shall be included | ||
172 | 12 | in all copies or substantial portions of the Software. | ||
173 | 13 | |||
174 | 14 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS | ||
175 | 15 | OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF | ||
176 | 16 | MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. | ||
177 | 17 | IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY | ||
178 | 18 | CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, | ||
179 | 19 | TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE | ||
180 | 20 | SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. | ||
181 | 0 | 21 | ||
182 | === added file 'python/iso8601/README' | |||
183 | --- python/iso8601/README 1970-01-01 00:00:00 +0000 | |||
184 | +++ python/iso8601/README 2009-07-20 10:08:11 +0000 | |||
185 | @@ -0,0 +1,26 @@ | |||
186 | 1 | A simple package to deal with ISO 8601 date time formats. | ||
187 | 2 | |||
188 | 3 | ISO 8601 defines a neutral, unambiguous date string format, which also | ||
189 | 4 | has the property of sorting naturally. | ||
190 | 5 | |||
191 | 6 | e.g. YYYY-MM-DDTHH:MM:SSZ or 2007-01-25T12:00:00Z | ||
192 | 7 | |||
193 | 8 | Currently this covers only the most common date formats encountered, not | ||
194 | 9 | all of ISO 8601 is handled. | ||
195 | 10 | |||
196 | 11 | Currently the following formats are handled: | ||
197 | 12 | |||
198 | 13 | * 2006-01-01T00:00:00Z | ||
199 | 14 | * 2006-01-01T00:00:00[+-]00:00 | ||
200 | 15 | |||
201 | 16 | I'll add more as I encounter them in my day to day life. Patches with | ||
202 | 17 | new formats and tests will be gratefully accepted of course :) | ||
203 | 18 | |||
204 | 19 | References: | ||
205 | 20 | |||
206 | 21 | * http://www.cl.cam.ac.uk/~mgk25/iso-time.html - simple overview | ||
207 | 22 | |||
208 | 23 | * http://hydracen.com/dx/iso8601.htm - more detailed enumeration of | ||
209 | 24 | valid formats. | ||
210 | 25 | |||
211 | 26 | See the LICENSE file for the license this package is released under. | ||
212 | 0 | 27 | ||
213 | === added file 'python/iso8601/README.subunit' | |||
214 | --- python/iso8601/README.subunit 1970-01-01 00:00:00 +0000 | |||
215 | +++ python/iso8601/README.subunit 2009-07-20 10:08:11 +0000 | |||
216 | @@ -0,0 +1,5 @@ | |||
217 | 1 | This is a [slightly rearranged] import of http://pypi.python.org/pypi/iso8601/ | ||
218 | 2 | version 0.1.4. The OS X hidden files have been stripped, and the package | ||
219 | 3 | turned into a single module, to simplify installation. The remainder of the | ||
220 | 4 | source distribution is included in the subunit source tree at python/iso8601 | ||
221 | 5 | for reference. | ||
222 | 0 | 6 | ||
223 | === added file 'python/iso8601/setup.py' | |||
224 | --- python/iso8601/setup.py 1970-01-01 00:00:00 +0000 | |||
225 | +++ python/iso8601/setup.py 2009-07-20 10:08:11 +0000 | |||
226 | @@ -0,0 +1,58 @@ | |||
227 | 1 | try: | ||
228 | 2 | from setuptools import setup | ||
229 | 3 | except ImportError: | ||
230 | 4 | from distutils import setup | ||
231 | 5 | |||
232 | 6 | long_description="""Simple module to parse ISO 8601 dates | ||
233 | 7 | |||
234 | 8 | This module parses the most common forms of ISO 8601 date strings (e.g. | ||
235 | 9 | 2007-01-14T20:34:22+00:00) into datetime objects. | ||
236 | 10 | |||
237 | 11 | >>> import iso8601 | ||
238 | 12 | >>> iso8601.parse_date("2007-01-25T12:00:00Z") | ||
239 | 13 | datetime.datetime(2007, 1, 25, 12, 0, tzinfo=<iso8601.iso8601.Utc ...>) | ||
240 | 14 | >>> | ||
241 | 15 | |||
242 | 16 | Changes | ||
243 | 17 | ======= | ||
244 | 18 | |||
245 | 19 | 0.1.4 | ||
246 | 20 | ----- | ||
247 | 21 | |||
248 | 22 | * The default_timezone argument wasn't being passed through correctly, | ||
249 | 23 | UTC was being used in every case. Fixes issue 10. | ||
250 | 24 | |||
251 | 25 | 0.1.3 | ||
252 | 26 | ----- | ||
253 | 27 | |||
254 | 28 | * Fixed the microsecond handling, the generated microsecond values were | ||
255 | 29 | way too small. Fixes issue 9. | ||
256 | 30 | |||
257 | 31 | 0.1.2 | ||
258 | 32 | ----- | ||
259 | 33 | |||
260 | 34 | * Adding ParseError to __all__ in iso8601 module, allows people to import it. | ||
261 | 35 | Addresses issue 7. | ||
262 | 36 | * Be a little more flexible when dealing with dates without leading zeroes. | ||
263 | 37 | This violates the spec a little, but handles more dates as seen in the | ||
264 | 38 | field. Addresses issue 6. | ||
265 | 39 | * Allow date/time separators other than T. | ||
266 | 40 | |||
267 | 41 | 0.1.1 | ||
268 | 42 | ----- | ||
269 | 43 | |||
270 | 44 | * When parsing dates without a timezone the specified default is used. If no | ||
271 | 45 | default is specified then UTC is used. Addresses issue 4. | ||
272 | 46 | """ | ||
273 | 47 | |||
274 | 48 | setup( | ||
275 | 49 | name="iso8601", | ||
276 | 50 | version="0.1.4", | ||
277 | 51 | description=long_description.split("\n")[0], | ||
278 | 52 | long_description=long_description, | ||
279 | 53 | author="Michael Twomey", | ||
280 | 54 | author_email="micktwomey+iso8601@gmail.com", | ||
281 | 55 | url="http://code.google.com/p/pyiso8601/", | ||
282 | 56 | packages=["iso8601"], | ||
283 | 57 | license="MIT", | ||
284 | 58 | ) | ||
285 | 0 | 59 | ||
286 | === added file 'python/iso8601/test_iso8601.py' | |||
287 | --- python/iso8601/test_iso8601.py 1970-01-01 00:00:00 +0000 | |||
288 | +++ python/iso8601/test_iso8601.py 2009-07-20 10:08:11 +0000 | |||
289 | @@ -0,0 +1,111 @@ | |||
290 | 1 | import iso8601 | ||
291 | 2 | |||
292 | 3 | def test_iso8601_regex(): | ||
293 | 4 | assert iso8601.ISO8601_REGEX.match("2006-10-11T00:14:33Z") | ||
294 | 5 | |||
295 | 6 | def test_timezone_regex(): | ||
296 | 7 | assert iso8601.TIMEZONE_REGEX.match("+01:00") | ||
297 | 8 | assert iso8601.TIMEZONE_REGEX.match("+00:00") | ||
298 | 9 | assert iso8601.TIMEZONE_REGEX.match("+01:20") | ||
299 | 10 | assert iso8601.TIMEZONE_REGEX.match("-01:00") | ||
300 | 11 | |||
301 | 12 | def test_parse_date(): | ||
302 | 13 | d = iso8601.parse_date("2006-10-20T15:34:56Z") | ||
303 | 14 | assert d.year == 2006 | ||
304 | 15 | assert d.month == 10 | ||
305 | 16 | assert d.day == 20 | ||
306 | 17 | assert d.hour == 15 | ||
307 | 18 | assert d.minute == 34 | ||
308 | 19 | assert d.second == 56 | ||
309 | 20 | assert d.tzinfo == iso8601.UTC | ||
310 | 21 | |||
311 | 22 | def test_parse_date_fraction(): | ||
312 | 23 | d = iso8601.parse_date("2006-10-20T15:34:56.123Z") | ||
313 | 24 | assert d.year == 2006 | ||
314 | 25 | assert d.month == 10 | ||
315 | 26 | assert d.day == 20 | ||
316 | 27 | assert d.hour == 15 | ||
317 | 28 | assert d.minute == 34 | ||
318 | 29 | assert d.second == 56 | ||
319 | 30 | assert d.microsecond == 123000 | ||
320 | 31 | assert d.tzinfo == iso8601.UTC | ||
321 | 32 | |||
322 | 33 | def test_parse_date_fraction_2(): | ||
323 | 34 | """From bug 6 | ||
324 | 35 | |||
325 | 36 | """ | ||
326 | 37 | d = iso8601.parse_date("2007-5-7T11:43:55.328Z'") | ||
327 | 38 | assert d.year == 2007 | ||
328 | 39 | assert d.month == 5 | ||
329 | 40 | assert d.day == 7 | ||
330 | 41 | assert d.hour == 11 | ||
331 | 42 | assert d.minute == 43 | ||
332 | 43 | assert d.second == 55 | ||
333 | 44 | assert d.microsecond == 328000 | ||
334 | 45 | assert d.tzinfo == iso8601.UTC | ||
335 | 46 | |||
336 | 47 | def test_parse_date_tz(): | ||
337 | 48 | d = iso8601.parse_date("2006-10-20T15:34:56.123+02:30") | ||
338 | 49 | assert d.year == 2006 | ||
339 | 50 | assert d.month == 10 | ||
340 | 51 | assert d.day == 20 | ||
341 | 52 | assert d.hour == 15 | ||
342 | 53 | assert d.minute == 34 | ||
343 | 54 | assert d.second == 56 | ||
344 | 55 | assert d.microsecond == 123000 | ||
345 | 56 | assert d.tzinfo.tzname(None) == "+02:30" | ||
346 | 57 | offset = d.tzinfo.utcoffset(None) | ||
347 | 58 | assert offset.days == 0 | ||
348 | 59 | assert offset.seconds == 60 * 60 * 2.5 | ||
349 | 60 | |||
350 | 61 | def test_parse_invalid_date(): | ||
351 | 62 | try: | ||
352 | 63 | iso8601.parse_date(None) | ||
353 | 64 | except iso8601.ParseError: | ||
354 | 65 | pass | ||
355 | 66 | else: | ||
356 | 67 | assert 1 == 2 | ||
357 | 68 | |||
358 | 69 | def test_parse_invalid_date2(): | ||
359 | 70 | try: | ||
360 | 71 | iso8601.parse_date("23") | ||
361 | 72 | except iso8601.ParseError: | ||
362 | 73 | pass | ||
363 | 74 | else: | ||
364 | 75 | assert 1 == 2 | ||
365 | 76 | |||
366 | 77 | def test_parse_no_timezone(): | ||
367 | 78 | """issue 4 - Handle datetime string without timezone | ||
368 | 79 | |||
369 | 80 | This tests what happens when you parse a date with no timezone. While not | ||
370 | 81 | strictly correct this is quite common. I'll assume UTC for the time zone | ||
371 | 82 | in this case. | ||
372 | 83 | """ | ||
373 | 84 | d = iso8601.parse_date("2007-01-01T08:00:00") | ||
374 | 85 | assert d.year == 2007 | ||
375 | 86 | assert d.month == 1 | ||
376 | 87 | assert d.day == 1 | ||
377 | 88 | assert d.hour == 8 | ||
378 | 89 | assert d.minute == 0 | ||
379 | 90 | assert d.second == 0 | ||
380 | 91 | assert d.microsecond == 0 | ||
381 | 92 | assert d.tzinfo == iso8601.UTC | ||
382 | 93 | |||
383 | 94 | def test_parse_no_timezone_different_default(): | ||
384 | 95 | tz = iso8601.FixedOffset(2, 0, "test offset") | ||
385 | 96 | d = iso8601.parse_date("2007-01-01T08:00:00", default_timezone=tz) | ||
386 | 97 | assert d.tzinfo == tz | ||
387 | 98 | |||
388 | 99 | def test_space_separator(): | ||
389 | 100 | """Handle a separator other than T | ||
390 | 101 | |||
391 | 102 | """ | ||
392 | 103 | d = iso8601.parse_date("2007-06-23 06:40:34.00Z") | ||
393 | 104 | assert d.year == 2007 | ||
394 | 105 | assert d.month == 6 | ||
395 | 106 | assert d.day == 23 | ||
396 | 107 | assert d.hour == 6 | ||
397 | 108 | assert d.minute == 40 | ||
398 | 109 | assert d.second == 34 | ||
399 | 110 | assert d.microsecond == 0 | ||
400 | 111 | assert d.tzinfo == iso8601.UTC | ||
401 | 0 | 112 | ||
402 | === modified file 'python/subunit/__init__.py' | |||
403 | --- python/subunit/__init__.py 2009-07-18 03:43:05 +0000 | |||
404 | +++ python/subunit/__init__.py 2009-07-20 10:08:11 +0000 | |||
405 | @@ -17,13 +17,17 @@ | |||
406 | 17 | # Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA | 17 | # Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA |
407 | 18 | # | 18 | # |
408 | 19 | 19 | ||
409 | 20 | import datetime | ||
410 | 20 | import os | 21 | import os |
411 | 22 | import re | ||
412 | 21 | from StringIO import StringIO | 23 | from StringIO import StringIO |
413 | 22 | import subprocess | 24 | import subprocess |
414 | 23 | import sys | 25 | import sys |
415 | 24 | import re | ||
416 | 25 | import unittest | 26 | import unittest |
417 | 26 | 27 | ||
418 | 28 | import iso8601 | ||
419 | 29 | |||
420 | 30 | |||
421 | 27 | def test_suite(): | 31 | def test_suite(): |
422 | 28 | import subunit.tests | 32 | import subunit.tests |
423 | 29 | return subunit.tests.test_suite() | 33 | return subunit.tests.test_suite() |
424 | @@ -207,6 +211,13 @@ | |||
425 | 207 | update_tags.update(new_tags) | 211 | update_tags.update(new_tags) |
426 | 208 | update_tags.difference_update(gone_tags) | 212 | update_tags.difference_update(gone_tags) |
427 | 209 | 213 | ||
428 | 214 | def _handleTime(self, offset, line): | ||
429 | 215 | # Accept it, but do not do anything with it yet. | ||
430 | 216 | event_time = iso8601.parse_date(line[offset:-1]) | ||
431 | 217 | time_method = getattr(self.client, 'time', None) | ||
432 | 218 | if callable(time_method): | ||
433 | 219 | time_method(event_time) | ||
434 | 220 | |||
435 | 210 | def lineReceived(self, line): | 221 | def lineReceived(self, line): |
436 | 211 | """Call the appropriate local method for the received line.""" | 222 | """Call the appropriate local method for the received line.""" |
437 | 212 | if line == "]\n": | 223 | if line == "]\n": |
438 | @@ -236,8 +247,7 @@ | |||
439 | 236 | elif cmd in ('tags',): | 247 | elif cmd in ('tags',): |
440 | 237 | self._handleTags(offset, line) | 248 | self._handleTags(offset, line) |
441 | 238 | elif cmd in ('time',): | 249 | elif cmd in ('time',): |
444 | 239 | # Accept it, but do not do anything with it yet. | 250 | self._handleTime(offset, line) |
443 | 240 | pass | ||
445 | 241 | elif cmd == 'xfail': | 251 | elif cmd == 'xfail': |
446 | 242 | self._addExpectedFail(offset, line) | 252 | self._addExpectedFail(offset, line) |
447 | 243 | else: | 253 | else: |
448 | 244 | 254 | ||
449 | === added file 'python/subunit/iso8601.py' | |||
450 | --- python/subunit/iso8601.py 1970-01-01 00:00:00 +0000 | |||
451 | +++ python/subunit/iso8601.py 2009-07-20 10:08:11 +0000 | |||
452 | @@ -0,0 +1,123 @@ | |||
453 | 1 | # Copyright (c) 2007 Michael Twomey | ||
454 | 2 | # | ||
455 | 3 | # Permission is hereby granted, free of charge, to any person obtaining a | ||
456 | 4 | # copy of this software and associated documentation files (the | ||
457 | 5 | # "Software"), to deal in the Software without restriction, including | ||
458 | 6 | # without limitation the rights to use, copy, modify, merge, publish, | ||
459 | 7 | # distribute, sublicense, and/or sell copies of the Software, and to | ||
460 | 8 | # permit persons to whom the Software is furnished to do so, subject to | ||
461 | 9 | # the following conditions: | ||
462 | 10 | # | ||
463 | 11 | # The above copyright notice and this permission notice shall be included | ||
464 | 12 | # in all copies or substantial portions of the Software. | ||
465 | 13 | # | ||
466 | 14 | # THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS | ||
467 | 15 | # OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF | ||
468 | 16 | # MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. | ||
469 | 17 | # IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY | ||
470 | 18 | # CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, | ||
471 | 19 | # TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE | ||
472 | 20 | # SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. | ||
473 | 21 | |||
474 | 22 | """ISO 8601 date time string parsing | ||
475 | 23 | |||
476 | 24 | Basic usage: | ||
477 | 25 | >>> import iso8601 | ||
478 | 26 | >>> iso8601.parse_date("2007-01-25T12:00:00Z") | ||
479 | 27 | datetime.datetime(2007, 1, 25, 12, 0, tzinfo=<iso8601.iso8601.Utc ...>) | ||
480 | 28 | >>> | ||
481 | 29 | |||
482 | 30 | """ | ||
483 | 31 | |||
484 | 32 | from datetime import datetime, timedelta, tzinfo | ||
485 | 33 | import re | ||
486 | 34 | |||
487 | 35 | __all__ = ["parse_date", "ParseError"] | ||
488 | 36 | |||
489 | 37 | # Adapted from http://delete.me.uk/2005/03/iso8601.html | ||
490 | 38 | ISO8601_REGEX = re.compile(r"(?P<year>[0-9]{4})(-(?P<month>[0-9]{1,2})(-(?P<day>[0-9]{1,2})" | ||
491 | 39 | r"((?P<separator>.)(?P<hour>[0-9]{2}):(?P<minute>[0-9]{2})(:(?P<second>[0-9]{2})(\.(?P<fraction>[0-9]+))?)?" | ||
492 | 40 | r"(?P<timezone>Z|(([-+])([0-9]{2}):([0-9]{2})))?)?)?)?" | ||
493 | 41 | ) | ||
494 | 42 | TIMEZONE_REGEX = re.compile("(?P<prefix>[+-])(?P<hours>[0-9]{2}).(?P<minutes>[0-9]{2})") | ||
495 | 43 | |||
496 | 44 | class ParseError(Exception): | ||
497 | 45 | """Raised when there is a problem parsing a date string""" | ||
498 | 46 | |||
499 | 47 | # Yoinked from python docs | ||
500 | 48 | ZERO = timedelta(0) | ||
501 | 49 | class Utc(tzinfo): | ||
502 | 50 | """UTC | ||
503 | 51 | |||
504 | 52 | """ | ||
505 | 53 | def utcoffset(self, dt): | ||
506 | 54 | return ZERO | ||
507 | 55 | |||
508 | 56 | def tzname(self, dt): | ||
509 | 57 | return "UTC" | ||
510 | 58 | |||
511 | 59 | def dst(self, dt): | ||
512 | 60 | return ZERO | ||
513 | 61 | UTC = Utc() | ||
514 | 62 | |||
515 | 63 | class FixedOffset(tzinfo): | ||
516 | 64 | """Fixed offset in hours and minutes from UTC | ||
517 | 65 | |||
518 | 66 | """ | ||
519 | 67 | def __init__(self, offset_hours, offset_minutes, name): | ||
520 | 68 | self.__offset = timedelta(hours=offset_hours, minutes=offset_minutes) | ||
521 | 69 | self.__name = name | ||
522 | 70 | |||
523 | 71 | def utcoffset(self, dt): | ||
524 | 72 | return self.__offset | ||
525 | 73 | |||
526 | 74 | def tzname(self, dt): | ||
527 | 75 | return self.__name | ||
528 | 76 | |||
529 | 77 | def dst(self, dt): | ||
530 | 78 | return ZERO | ||
531 | 79 | |||
532 | 80 | def __repr__(self): | ||
533 | 81 | return "<FixedOffset %r>" % self.__name | ||
534 | 82 | |||
535 | 83 | def parse_timezone(tzstring, default_timezone=UTC): | ||
536 | 84 | """Parses ISO 8601 time zone specs into tzinfo offsets | ||
537 | 85 | |||
538 | 86 | """ | ||
539 | 87 | if tzstring == "Z": | ||
540 | 88 | return default_timezone | ||
541 | 89 | # This isn't strictly correct, but it's common to encounter dates without | ||
542 | 90 | # timezones so I'll assume the default (which defaults to UTC). | ||
543 | 91 | # Addresses issue 4. | ||
544 | 92 | if tzstring is None: | ||
545 | 93 | return default_timezone | ||
546 | 94 | m = TIMEZONE_REGEX.match(tzstring) | ||
547 | 95 | prefix, hours, minutes = m.groups() | ||
548 | 96 | hours, minutes = int(hours), int(minutes) | ||
549 | 97 | if prefix == "-": | ||
550 | 98 | hours = -hours | ||
551 | 99 | minutes = -minutes | ||
552 | 100 | return FixedOffset(hours, minutes, tzstring) | ||
553 | 101 | |||
554 | 102 | def parse_date(datestring, default_timezone=UTC): | ||
555 | 103 | """Parses ISO 8601 dates into datetime objects | ||
556 | 104 | |||
557 | 105 | The timezone is parsed from the date string. However it is quite common to | ||
558 | 106 | have dates without a timezone (not strictly correct). In this case the | ||
559 | 107 | default timezone specified in default_timezone is used. This is UTC by | ||
560 | 108 | default. | ||
561 | 109 | """ | ||
562 | 110 | if not isinstance(datestring, basestring): | ||
563 | 111 | raise ParseError("Expecting a string %r" % datestring) | ||
564 | 112 | m = ISO8601_REGEX.match(datestring) | ||
565 | 113 | if not m: | ||
566 | 114 | raise ParseError("Unable to parse date string %r" % datestring) | ||
567 | 115 | groups = m.groupdict() | ||
568 | 116 | tz = parse_timezone(groups["timezone"], default_timezone=default_timezone) | ||
569 | 117 | if groups["fraction"] is None: | ||
570 | 118 | groups["fraction"] = 0 | ||
571 | 119 | else: | ||
572 | 120 | groups["fraction"] = int(float("0.%s" % groups["fraction"]) * 1e6) | ||
573 | 121 | return datetime(int(groups["year"]), int(groups["month"]), int(groups["day"]), | ||
574 | 122 | int(groups["hour"]), int(groups["minute"]), int(groups["second"]), | ||
575 | 123 | int(groups["fraction"]), tz) | ||
576 | 0 | 124 | ||
577 | === modified file 'python/subunit/tests/test_test_protocol.py' | |||
578 | --- python/subunit/tests/test_test_protocol.py 2009-07-18 03:43:05 +0000 | |||
579 | +++ python/subunit/tests/test_test_protocol.py 2009-07-20 10:08:11 +0000 | |||
580 | @@ -17,46 +17,49 @@ | |||
581 | 17 | # Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA | 17 | # Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA |
582 | 18 | # | 18 | # |
583 | 19 | 19 | ||
584 | 20 | import datetime | ||
585 | 20 | import unittest | 21 | import unittest |
586 | 21 | from StringIO import StringIO | 22 | from StringIO import StringIO |
587 | 22 | import os | 23 | import os |
588 | 23 | import subunit | 24 | import subunit |
589 | 24 | import sys | 25 | import sys |
625 | 25 | import time | 26 | |
626 | 26 | 27 | import subunit.iso8601 as iso8601 | |
627 | 27 | try: | 28 | |
628 | 28 | class MockTestProtocolServerClient(object): | 29 | |
629 | 29 | """A mock protocol server client to test callbacks.""" | 30 | class MockTestProtocolServerClient(object): |
630 | 30 | 31 | """A mock protocol server client to test callbacks.""" | |
631 | 31 | def __init__(self): | 32 | |
632 | 32 | self.end_calls = [] | 33 | def __init__(self): |
633 | 33 | self.error_calls = [] | 34 | self.end_calls = [] |
634 | 34 | self.failure_calls = [] | 35 | self.error_calls = [] |
635 | 35 | self.skip_calls = [] | 36 | self.failure_calls = [] |
636 | 36 | self.start_calls = [] | 37 | self.skip_calls = [] |
637 | 37 | self.success_calls = [] | 38 | self.start_calls = [] |
638 | 38 | super(MockTestProtocolServerClient, self).__init__() | 39 | self.success_calls = [] |
639 | 39 | 40 | self._time = None | |
640 | 40 | def addError(self, test, error): | 41 | super(MockTestProtocolServerClient, self).__init__() |
641 | 41 | self.error_calls.append((test, error)) | 42 | |
642 | 42 | 43 | def addError(self, test, error): | |
643 | 43 | def addFailure(self, test, error): | 44 | self.error_calls.append((test, error)) |
644 | 44 | self.failure_calls.append((test, error)) | 45 | |
645 | 45 | 46 | def addFailure(self, test, error): | |
646 | 46 | def addSkip(self, test, reason): | 47 | self.failure_calls.append((test, error)) |
647 | 47 | self.skip_calls.append((test, reason)) | 48 | |
648 | 48 | 49 | def addSkip(self, test, reason): | |
649 | 49 | def addSuccess(self, test): | 50 | self.skip_calls.append((test, reason)) |
650 | 50 | self.success_calls.append(test) | 51 | |
651 | 51 | 52 | def addSuccess(self, test): | |
652 | 52 | def stopTest(self, test): | 53 | self.success_calls.append(test) |
653 | 53 | self.end_calls.append(test) | 54 | |
654 | 54 | 55 | def stopTest(self, test): | |
655 | 55 | def startTest(self, test): | 56 | self.end_calls.append(test) |
656 | 56 | self.start_calls.append(test) | 57 | |
657 | 57 | 58 | def startTest(self, test): | |
658 | 58 | except AttributeError: | 59 | self.start_calls.append(test) |
659 | 59 | MockTestProtocolServer = None | 60 | |
660 | 61 | def time(self, time): | ||
661 | 62 | self._time = time | ||
662 | 60 | 63 | ||
663 | 61 | 64 | ||
664 | 62 | class TestMockTestProtocolServer(unittest.TestCase): | 65 | class TestMockTestProtocolServer(unittest.TestCase): |
665 | @@ -763,15 +766,23 @@ | |||
666 | 763 | class TestTestProtocolServerStreamTime(unittest.TestCase): | 766 | class TestTestProtocolServerStreamTime(unittest.TestCase): |
667 | 764 | """Test managing time information at the protocol level.""" | 767 | """Test managing time information at the protocol level.""" |
668 | 765 | 768 | ||
671 | 766 | def setUp(self): | 769 | def test_time_accepted_stdlib(self): |
672 | 767 | self.client = MockTestProtocolServerClient() | 770 | self.result = unittest.TestResult() |
673 | 768 | self.stream = StringIO() | 771 | self.stream = StringIO() |
675 | 769 | self.protocol = subunit.TestProtocolServer(self.client, | 772 | self.protocol = subunit.TestProtocolServer(self.result, |
676 | 770 | stream=self.stream) | 773 | stream=self.stream) |
677 | 774 | self.protocol.lineReceived("time: 2001-12-12 12:59:59Z\n") | ||
678 | 775 | self.assertEqual("", self.stream.getvalue()) | ||
679 | 771 | 776 | ||
681 | 772 | def test_time_accepted(self): | 777 | def test_time_accepted_extended(self): |
682 | 778 | self.result = MockTestProtocolServerClient() | ||
683 | 779 | self.stream = StringIO() | ||
684 | 780 | self.protocol = subunit.TestProtocolServer(self.result, | ||
685 | 781 | stream=self.stream) | ||
686 | 773 | self.protocol.lineReceived("time: 2001-12-12 12:59:59Z\n") | 782 | self.protocol.lineReceived("time: 2001-12-12 12:59:59Z\n") |
687 | 774 | self.assertEqual("", self.stream.getvalue()) | 783 | self.assertEqual("", self.stream.getvalue()) |
688 | 784 | self.assertEqual(datetime.datetime(2001, 12, 12, 12, 59, 59, 0, | ||
689 | 785 | iso8601.Utc()), self.result._time) | ||
690 | 775 | 786 | ||
691 | 776 | 787 | ||
692 | 777 | class TestRemotedTestCase(unittest.TestCase): | 788 | class TestRemotedTestCase(unittest.TestCase): |
This teaches the python support to export time data to the TestResult.