Merge lp:~lifeless/subunit/streamresult into lp:~subunit/subunit/trunk

Proposed by Robert Collins
Status: Merged
Merged at revision: 205
Proposed branch: lp:~lifeless/subunit/streamresult
Merge into: lp:~subunit/subunit/trunk
Diff against target: 3262 lines (+1848/-501)
25 files modified
Makefile.am (+2/-0)
NEWS (+35/-0)
README (+252/-14)
filters/subunit-1to2 (+42/-0)
filters/subunit-2to1 (+46/-0)
filters/subunit-filter (+14/-8)
filters/subunit-ls (+21/-10)
filters/subunit-notify (+5/-1)
filters/subunit-stats (+12/-21)
filters/subunit2csv (+4/-1)
filters/subunit2gtk (+29/-48)
filters/subunit2junitxml (+6/-1)
filters/subunit2pyunit (+15/-6)
python/subunit/__init__.py (+57/-47)
python/subunit/filters.py (+75/-15)
python/subunit/run.py (+39/-3)
python/subunit/test_results.py (+58/-7)
python/subunit/tests/__init__.py (+2/-0)
python/subunit/tests/test_run.py (+22/-14)
python/subunit/tests/test_subunit_filter.py (+43/-61)
python/subunit/tests/test_subunit_tags.py (+31/-30)
python/subunit/tests/test_tap2subunit.py (+162/-214)
python/subunit/tests/test_test_protocol2.py (+415/-0)
python/subunit/v2.py (+458/-0)
setup.py (+3/-0)
To merge this branch: bzr merge lp:~lifeless/subunit/streamresult
Reviewer Review Type Date Requested Status
Subunit Developers Pending
Review via email: mp+151442@code.launchpad.net

Description of the change

Woo! Overlapping concurrent tests with built in enumeration and twice as fast parsing.

To post a comment you must log in.
lp:~lifeless/subunit/streamresult updated
203. By Robert Collins

Fixes from getting testrepository running with v2.

204. By Robert Collins

* ``subunit.run`` now replaces sys.stdout to ensure that stdout is unbuffered
  - without this pdb output is not reliably visible when stdout is a pipe
  as it usually is. (Robert Collins)

205. By Robert Collins

Switch to variable length encoded integers.

Preview Diff

[H/L] Next/Prev Comment, [J/K] Next/Prev File, [N/P] Next/Prev Hunk
=== modified file 'Makefile.am'
--- Makefile.am 2012-12-17 07:32:51 +0000
+++ Makefile.am 2013-03-31 05:51:20 +0000
@@ -47,6 +47,8 @@
47include_subunitdir = $(includedir)/subunit47include_subunitdir = $(includedir)/subunit
4848
49dist_bin_SCRIPTS = \49dist_bin_SCRIPTS = \
50 filters/subunit-1to2 \
51 filters/subunit-2to1 \
50 filters/subunit-filter \52 filters/subunit-filter \
51 filters/subunit-ls \53 filters/subunit-ls \
52 filters/subunit-notify \54 filters/subunit-notify \
5355
=== modified file 'NEWS'
--- NEWS 2013-02-07 11:33:11 +0000
+++ NEWS 2013-03-31 05:51:20 +0000
@@ -5,6 +5,41 @@
5NEXT (In development)5NEXT (In development)
6---------------------6---------------------
77
8v2 protocol draft included in this release. The v2 protocol trades off human
9readability for a massive improvement in robustness, the ability to represent
10concurrent tests in a single stream, cheaper parsing, and that provides
11significantly better in-line debugging support and structured forwarding
12of non-test data (such as stdout or stdin data).
13
14This change includes two new filters (subunit-1to2 and subunit-2to1). Use
15these filters to convert old streams to v2 and convert v2 streams to v1.
16
17All the other filters now only parse and emit v2 streams. V2 is still in
18draft format, so if you want to delay and wait for v2 to be finalised, you
19should use subunit-2to1 before any serialisation steps take place.
20With the ability to encapsulate multiple non-test streams, another significant
21cange is that filters which emit subunit now encapsulate any non-subunit they
22encounter, labelling it 'stdout'. This permits multiplexing such streams and
23detangling the stdout streams from each input.
24
25The subunit libraries (Python etc) have not changed their behaviour: they
26still emit v1 from their existing API calls. New API's are being added
27and applications should migrate once their language has those API's available.
28
29IMPROVEMENTS
30~~~~~~~~~~~~
31
32* ``subunit.run`` now replaces sys.stdout to ensure that stdout is unbuffered
33 - without this pdb output is not reliably visible when stdout is a pipe
34 as it usually is. (Robert Collins)
35
36* v2 protocol draft included in this release.
37 (Robert Collins)
38
39* Two new Python classes -- ``StreamResultToBytes`` and
40 ``ByteStreamToStreamResult`` handle v2 generation and parsing.
41 (Robert Collins)
42
80.0.10430.0.10
9------44------
1045
1146
=== modified file 'README'
--- README 2013-02-07 11:33:11 +0000
+++ README 2013-03-31 05:51:20 +0000
@@ -21,9 +21,26 @@
21Subunit21Subunit
22-------22-------
2323
24Subunit is a streaming protocol for test results. The protocol is human24Subunit is a streaming protocol for test results.
25readable and easily generated and parsed. By design all the components of 25
26the protocol conceptually fit into the xUnit TestCase->TestResult interaction.26There are two major revisions of the protocol. Version 1 was trivially human
27readable but had significant defects as far as highly parallel testing was
28concerned - it had no room for doing discovery and execution in parallel,
29required substantial buffering when multiplexing and was fragile - a corrupt
30byte could cause an entire stream to be misparsed. Version 1.1 added
31encapsulation of binary streams which mitigated some of the issues but the
32core remained.
33
34Version 2 shares many of the good characteristics of Version 1 - it can be
35embedded into a regular text stream (e.g. from a build system) and it still
36models xUnit style test execution. It also fixes many of the issues with
37Version 1 - Version 2 can be multiplexed without excessive buffering (in
38time or space), it has a well defined recovery mechanism for dealing with
39corrupted streams (e.g. where two processes write to the same stream
40concurrently, or where the stream generator suffers a bug).
41
42More details on both protocol version s can be found in the 'Protocol' section
43of this document.
2744
28Subunit comes with command line filters to process a subunit stream and45Subunit comes with command line filters to process a subunit stream and
29language bindings for python, C, C++ and shell. Bindings are easy to write46language bindings for python, C, C++ and shell. Bindings are easy to write
@@ -32,11 +49,12 @@
32A number of useful things can be done easily with subunit:49A number of useful things can be done easily with subunit:
33 * Test aggregation: Tests run separately can be combined and then50 * Test aggregation: Tests run separately can be combined and then
34 reported/displayed together. For instance, tests from different languages51 reported/displayed together. For instance, tests from different languages
35 can be shown as a seamless whole.52 can be shown as a seamless whole, and tests running on multiple machines
53 can be aggregated into a single stream through a multiplexer.
36 * Test archiving: A test run may be recorded and replayed later.54 * Test archiving: A test run may be recorded and replayed later.
37 * Test isolation: Tests that may crash or otherwise interact badly with each55 * Test isolation: Tests that may crash or otherwise interact badly with each
38 other can be run seperately and then aggregated, rather than interfering56 other can be run seperately and then aggregated, rather than interfering
39 with each other.57 with each other or requiring an adhoc test->runner reporting protocol.
40 * Grid testing: subunit can act as the necessary serialisation and58 * Grid testing: subunit can act as the necessary serialisation and
41 deserialiation to get test runs on distributed machines to be reported in59 deserialiation to get test runs on distributed machines to be reported in
42 real time.60 real time.
@@ -68,20 +86,20 @@
68in python and there are facilities for using Subunit to increase test isolation86in python and there are facilities for using Subunit to increase test isolation
69seamlessly within a test suite.87seamlessly within a test suite.
7088
71One simple way to run an existing python test suite and have it output subunit89The most common way is to run an existing python test suite and have it output
72is the module ``subunit.run``::90subunit via the ``subunit.run`` module::
7391
74 $ python -m subunit.run mypackage.tests.test_suite92 $ python -m subunit.run mypackage.tests.test_suite
75 93
76For more information on the Python support Subunit offers , please see94For more information on the Python support Subunit offers , please see
77``pydoc subunit``, or the source in ``python/subunit/__init__.py``95``pydoc subunit``, or the source in ``python/subunit/``
7896
79C97C
80=98=
8199
82Subunit has C bindings to emit the protocol, and comes with a patch for 'check'100Subunit has C bindings to emit the protocol. The 'check' C unit testing project
83which has been nominally accepted by the 'check' developers. See 'c/README' for101has included subunit support in their project for some years now. See
84more details.102'c/README' for more details.
85103
86C++104C++
87===105===
@@ -92,9 +110,13 @@
92shell110shell
93=====111=====
94112
95Similar to C, the shell bindings consist of simple functions to output protocol113There are two sets of shell tools. There are filters, which accept a subunit
96elements, and a patch for adding subunit output to the 'ShUnit' shell test114stream on stdin and output processed data (or a transformed stream) on stdout.
97runner. See 'shell/README' for details.115
116Then there are unittest facilities similar to those for C : shell bindings
117consisting of simple functions to output protocol elements, and a patch for
118adding subunit output to the 'ShUnit' shell test runner. See 'shell/README' for
119details.
98120
99Filter recipes121Filter recipes
100--------------122--------------
@@ -104,9 +126,225 @@
104 subunit-filter --without 'AttributeError.*flavor'126 subunit-filter --without 'AttributeError.*flavor'
105127
106128
129The xUnit test model
130--------------------
131
132Subunit implements a slightly modified xUnit test model. The stock standard
133model is that there are tests, which have an id(), can be run, and when run
134start, emit an outcome (like success or failure) and then finish.
135
136Subunit extends this with the idea of test enumeration (find out about tests
137a runner has without running them), tags (allow users to describe tests in
138ways the test framework doesn't apply any semantic value to), file attachments
139(allow arbitrary data to make analysing a failure easy) and timestamps.
140
107The protocol141The protocol
108------------142------------
109143
144Version 2, or v2 is new and still under development, but is intended to
145supercede version 1 in the very near future. Subunit's bundled tools accept
146only version 2 and only emit version 2, but the new filters subunit-1to2 and
147subunit-2to1 can be used to interoperate with older third party libraries.
148
149Version 2
150=========
151
152Version 2 is a binary protocol consisting of independent packets that can be
153embedded in the output from tools like make - as long as each packet has no
154other bytes mixed in with it (which 'make -j N>1' has a tendency of doing).
155Version 2 is currently in draft form, and early adopters should be willing
156to either discard stored results (if protocol changes are made), or bulk
157convert them back to v1 and then to a newer edition of v2.
158
159The protocol synchronises at the start of the stream, after a packet, or
160after any 0x0A byte. That is, a subunit v2 packet starts after a newline or
161directly after the end of the prior packet.
162
163Subunit is intended to be transported over a reliable streaming protocol such
164as TCP. As such it does not concern itself with out of order delivery of
165packets. However, because of the possibility of corruption due to either
166bugs in the sender, or due to mixed up data from concurrent writes to the same
167fd when being embedded, subunit strives to recover reasonably gracefully from
168damaged data.
169
170A key design goal for Subunit version 2 is to allow processing and multiplexing
171without forcing buffering for semantic correctness, as buffering tends to hide
172hung or otherwise misbehaving tests. That said, limited time based buffering
173for network efficiency is a good idea - this is ultimately implementator
174choice. Line buffering is also discouraged for subunit streams, as dropping
175into a debugger or other tool may require interactive traffic even if line
176buffering would not otherwise be a problem.
177
178In version two there are two conceptual events - a test status event and a file
179attachment event. Events may have timestamps, and the path of multiplexers that
180an event is routed through is recorded to permit sending actions back to the
181source (such as new tests to run or stdin for driving debuggers and other
182interactive input). Test status events are used to enumerate tests, to report
183tests and test helpers as they run. Tests may have tags, used to allow
184tunnelling extra meanings through subunit without requiring parsing of
185arbitrary file attachments. Things that are not standalone tests get marked
186as such by setting the 'Runnable' flag to false. (For instance, individual
187assertions in TAP are not runnable tests, only the top level TAP test script
188is runnable).
189
190File attachments are used to provide rich detail about the nature of a failure.
191File attachments can also be used to encapsulate stdout and stderr both during
192and outside tests.
193
194Most numbers are stored in network byte order - Most Significant Byte first
195encoded using a variation of http://www.dlugosz.com/ZIP2/VLI.html. The first
196byte's top 2 high order bits encode the total number of octets in the number.
197This encoding can encode values from 0 to 2**30-1, enough to encode a
198nanosecond. Numbers that are not variable length encoded are still stored in
199MSB order.
200
201 prefix octets max max
202+-------+--------+---------+------------+
203| 00 | 1 | 2**6-1 | 63 |
204| 01 | 2 | 2**14-1 | 16383 |
205| 10 | 3 | 2**22-1 | 4194303 |
206| 11 | 4 | 2**30-1 | 1073741823 |
207+-------+--------+---------+------------+
208
209All variable length elements of the packet are stored with a length prefix
210number allowing them to be skipped over for consumers that don't need to
211interpret them.
212
213UTF-8 strings are with no terminating NUL and should not have any embedded NULs
214(implementations SHOULD validate any such strings that they process and take
215some remedial action (such as discarding the packet as corrupt).
216
217In short the structure of a packet is:
218PACKET := SIGNATURE FLAGS PACKET_LENGTH TIMESTAMP? TESTID? TAGS? MIME?
219 FILECONTENT? ROUTING_CODE? CRC32
220
221In more detail...
222
223Packets are identified by a single byte signature - 0xB3, which is never legal
224in a UTF-8 stream as the first byte of a character. 0xB3 starts with the first
225bit set and the second not, which is the UTF-8 signature for a continuation
226byte. 0xB3 was chosen as 0x73 ('s' in ASCII') with the top two bits replaced by
227the 1 and 0 for a continuation byte.
228
229If subunit packets are being embedded in a non-UTF-8 text stream, where 0x73 is
230a legal character, consider either recoding the text to UTF-8, or using
231subunit's 'file' packets to embed the text stream in subunit, rather than the
232other way around.
233
234Following the signature byte comes a 16-bit flags field, which includes a
2354-bit version field - if the version is not 0x2 then the packet cannot be
236read. It is recommended to signal an error at this point (e.g. by emitting
237a synthetic error packet and returning to the top level loop to look for
238new packets, or exiting with an error). If recovery is desired, treat the
239packet signature as an opaque byte and scan for a new synchronisation point.
240NB: Subunit V1 and V2 packets may legitimately included 0xB3 internally,
241as they are an 8-bit safe container format, so recovery from this situation
242may involve an arbitrary number of false positives until an actual packet
243is encountered : and even then it may still be false, failing after passing
244the version check due to coincidence.
245
246Flags are stored in network byte order too.
247+-------------------------+------------------------+
248| High byte | Low byte |
249| 15 14 13 12 11 10 9 8 | 7 6 5 4 3 2 1 0 |
250| VERSION |feature bits| |
251+------------+------------+------------------------+
252
253Valid version values are:
2540x2 - version 2
255
256Feature bits:
257Bit 11 - mask 0x0800 - Test id present.
258Bit 10 - mask 0x0400 - Routing code present.
259Bit 9 - mask 0x0200 - Timestamp present.
260Bit 8 - mask 0x0100 - Test is 'runnable'.
261Bit 7 - mask 0x0080 - Tags are present.
262Bit 6 - mask 0x0040 - File content is present.
263Bit 5 - mask 0x0020 - File MIME type is present.
264Bit 4 - mask 0x0010 - EOF marker.
265Bit 3 - mask 0x0008 - Must be zero in version 2.
266
267Test status gets three bits:
268Bit 2 | Bit 1 | Bit 0 - mask 0x0007 - A test status enum lookup:
269000 - undefined / no test
270001 - Enumeration / existence
271002 - In progress
272003 - Success
273004 - Unexpected Success
274005 - Skipped
275006 - Failed
276007 - Expected failure
277
278After the flags field is a number field giving the length in bytes for the
279entire packet including the signature and the checksum. This length must
280be less than 4MiB - 4194303 bytes. The encoding can obviously record a larger
281number but one of the goals is to avoid requiring large buffers, or causing
282large latency in the packet forward/processing pipeline. Larger file
283attachments can be communicated in multiple packets, and the overhead in such a
2844MiB packet is approximately 0.2%.
285
286The rest of the packet is a series of optional features as specified by the set
287feature bits in the flags field. When absent they are entirely absent.
288
289Forwarding and multiplexing of packets can be done without interpreting the
290remainder of the packet until the routing code and checksum (which are both at
291the end of the packet). Additionally, routers can often avoid copying or moving
292the bulk of the packet, as long as the routing code size increase doesn't force
293the length encoding to take up a new byte (which will only happen to packets
294less than or equal to 16KiB in length) - large packets are very efficient to
295route.
296
297Timestamp when present is a 32 bit unsigned integer for secnods, and a variable
298length number for nanoseconds, representing UTC time since Unix Epoch in
299seconds and nanoseconds.
300
301Test id when present is a UTF-8 string. The test id should uniquely identify
302runnable tests such that they can be selected individually. For tests and other
303actions which cannot be individually run (such as test
304fixtures/layers/subtests) uniqueness is not required (though being human
305meaningful is highly recommended).
306
307Tags when present is a length prefixed vector of UTF-8 strings, one per tag.
308There are no restrictions on tag content (other than the restrictions on UTF-8
309strings in subunit in general). Tags have no ordering.
310
311When a MIME type is present, it defines the MIME type for the file across all
312packets same file (routing code + testid + name uniquely identifies a file,
313reset when EOF is flagged). If a file never has a MIME type set, it should be
314treated as application/octet-stream.
315
316File content when present is a UTF-8 string for the name followed by the length
317in bytes of the content, and then the content octets.
318
319If present routing code is a UTF-8 string. The routing code is used to
320determine which test backend a test was running on when doing data analysis,
321and to route stdin to the test process if interaction is required.
322
323Multiplexers SHOULD add a routing code if none is present, and prefix any
324existing routing code with a routing code ('/' separated) if one is already
325present. For example, a multiplexer might label each stream it is multiplexing
326with a simple ordinal ('0', '1' etc), and given an incoming packet with route
327code '3' from stream '0' would adjust the route code when forwarding the packet
328to be '0/3'.
329
330Following the end of the packet is a CRC-32 checksum of the contents of the
331packet including the signature.
332
333Example packets
334~~~~~~~~~~~~~~~
335
336Trivial test "foo" enumeration packet, with test id, runnable set,
337status=enumeration. Spaces below are to visually break up signature / flags /
338length / testid / crc32
339
340b3 2901 0c 03666f6f 08555f1b
341
342
343Version 1 (and 1.1)
344===================
345
346Version 1 (and 1.1) are mostly human readable protocols.
347
110Sample subunit wire contents348Sample subunit wire contents
111----------------------------349----------------------------
112350
113351
=== added file 'filters/subunit-1to2'
--- filters/subunit-1to2 1970-01-01 00:00:00 +0000
+++ filters/subunit-1to2 2013-03-31 05:51:20 +0000
@@ -0,0 +1,42 @@
1#!/usr/bin/env python
2# subunit: extensions to python unittest to get test results from subprocesses.
3# Copyright (C) 2013 Robert Collins <robertc@robertcollins.net>
4#
5# Licensed under either the Apache License, Version 2.0 or the BSD 3-clause
6# license at the users choice. A copy of both licenses are available in the
7# project source as Apache-2.0 and BSD. You may not use this file except in
8# compliance with one of these two licences.
9#
10# Unless required by applicable law or agreed to in writing, software
11# distributed under these licenses is distributed on an "AS IS" BASIS, WITHOUT
12# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
13# license you chose for the specific language governing permissions and
14# limitations under that license.
15#
16
17"""Convert a version 1 subunit stream to version 2 stream."""
18
19from optparse import OptionParser
20import sys
21
22from testtools import ExtendedToStreamDecorator
23
24from subunit import StreamResultToBytes
25from subunit.filters import run_tests_from_stream
26
27
28def make_options(description):
29 parser = OptionParser(description=__doc__)
30 return parser
31
32
33def main():
34 parser = make_options(__doc__)
35 (options, args) = parser.parse_args()
36 run_tests_from_stream(sys.stdin,
37 ExtendedToStreamDecorator(StreamResultToBytes(sys.stdout)))
38 sys.exit(0)
39
40
41if __name__ == '__main__':
42 main()
043
=== added file 'filters/subunit-2to1'
--- filters/subunit-2to1 1970-01-01 00:00:00 +0000
+++ filters/subunit-2to1 2013-03-31 05:51:20 +0000
@@ -0,0 +1,46 @@
1#!/usr/bin/env python
2# subunit: extensions to python unittest to get test results from subprocesses.
3# Copyright (C) 2013 Robert Collins <robertc@robertcollins.net>
4#
5# Licensed under either the Apache License, Version 2.0 or the BSD 3-clause
6# license at the users choice. A copy of both licenses are available in the
7# project source as Apache-2.0 and BSD. You may not use this file except in
8# compliance with one of these two licences.
9#
10# Unless required by applicable law or agreed to in writing, software
11# distributed under these licenses is distributed on an "AS IS" BASIS, WITHOUT
12# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
13# license you chose for the specific language governing permissions and
14# limitations under that license.
15#
16
17"""Convert a version 2 subunit stream to a version 1 stream."""
18
19from optparse import OptionParser
20import sys
21
22from testtools import StreamToExtendedDecorator
23
24from subunit import ByteStreamToStreamResult, TestProtocolClient
25from subunit.filters import run_tests_from_stream
26
27
28def make_options(description):
29 parser = OptionParser(description=__doc__)
30 return parser
31
32
33def main():
34 parser = make_options(__doc__)
35 (options, args) = parser.parse_args()
36 case = ByteStreamToStreamResult(sys.stdin, non_subunit_name='stdout')
37 result = StreamToExtendedDecorator(TestProtocolClient(sys.stdout))
38 # What about stdout chunks?
39 result.startTestRun()
40 case.run(result)
41 result.stopTestRun()
42 sys.exit(0)
43
44
45if __name__ == '__main__':
46 main()
047
=== modified file 'filters/subunit-filter'
--- filters/subunit-filter 2012-05-03 08:18:01 +0000
+++ filters/subunit-filter 2013-03-31 05:51:20 +0000
@@ -1,6 +1,6 @@
1#!/usr/bin/env python1#!/usr/bin/env python
2# subunit: extensions to python unittest to get test results from subprocesses.2# subunit: extensions to python unittest to get test results from subprocesses.
3# Copyright (C) 2008 Robert Collins <robertc@robertcollins.net>3# Copyright (C) 200-2013 Robert Collins <robertc@robertcollins.net>
4# (C) 2009 Martin Pool4# (C) 2009 Martin Pool
5#5#
6# Licensed under either the Apache License, Version 2.0 or the BSD 3-clause6# Licensed under either the Apache License, Version 2.0 or the BSD 3-clause
@@ -30,10 +30,12 @@
30import sys30import sys
31import re31import re
3232
33from testtools import ExtendedToStreamDecorator, StreamToExtendedDecorator
34
33from subunit import (35from subunit import (
34 DiscardStream,36 DiscardStream,
35 ProtocolTestCase,37 ProtocolTestCase,
36 TestProtocolClient,38 StreamResultToBytes,
37 read_test_list,39 read_test_list,
38 )40 )
39from subunit.filters import filter_by_result41from subunit.filters import filter_by_result
@@ -55,9 +57,11 @@
55 parser.add_option("-f", "--no-failure", action="store_true",57 parser.add_option("-f", "--no-failure", action="store_true",
56 help="exclude failures", dest="failure")58 help="exclude failures", dest="failure")
57 parser.add_option("--passthrough", action="store_false",59 parser.add_option("--passthrough", action="store_false",
58 help="Show all non subunit input.", default=False, dest="no_passthrough")60 help="Forward non-subunit input as 'stdout'.", default=False,
61 dest="no_passthrough")
59 parser.add_option("--no-passthrough", action="store_true",62 parser.add_option("--no-passthrough", action="store_true",
60 help="Hide all non subunit input.", default=False, dest="no_passthrough")63 help="Discard all non subunit input.", default=False,
64 dest="no_passthrough")
61 parser.add_option("-s", "--success", action="store_false",65 parser.add_option("-s", "--success", action="store_false",
62 help="include successes", dest="success")66 help="include successes", dest="success")
63 parser.add_option("--no-success", action="store_true",67 parser.add_option("--no-success", action="store_true",
@@ -126,15 +130,16 @@
126 fixup_expected_failures = set()130 fixup_expected_failures = set()
127 for path in options.fixup_expected_failures or ():131 for path in options.fixup_expected_failures or ():
128 fixup_expected_failures.update(read_test_list(path))132 fixup_expected_failures.update(read_test_list(path))
129 return TestResultFilter(133 return StreamToExtendedDecorator(TestResultFilter(
130 TestProtocolClient(output),134 ExtendedToStreamDecorator(
135 StreamResultToBytes(output)),
131 filter_error=options.error,136 filter_error=options.error,
132 filter_failure=options.failure,137 filter_failure=options.failure,
133 filter_success=options.success,138 filter_success=options.success,
134 filter_skip=options.skip,139 filter_skip=options.skip,
135 filter_xfail=options.xfail,140 filter_xfail=options.xfail,
136 filter_predicate=predicate,141 filter_predicate=predicate,
137 fixup_expected_failures=fixup_expected_failures)142 fixup_expected_failures=fixup_expected_failures))
138143
139144
140def main():145def main():
@@ -150,7 +155,8 @@
150 lambda output_to: _make_result(sys.stdout, options, filter_predicate),155 lambda output_to: _make_result(sys.stdout, options, filter_predicate),
151 output_path=None,156 output_path=None,
152 passthrough=(not options.no_passthrough),157 passthrough=(not options.no_passthrough),
153 forward=False)158 forward=False,
159 protocol_version=2)
154 sys.exit(0)160 sys.exit(0)
155161
156162
157163
=== modified file 'filters/subunit-ls'
--- filters/subunit-ls 2011-05-23 09:57:58 +0000
+++ filters/subunit-ls 2013-03-31 05:51:20 +0000
@@ -19,9 +19,14 @@
19from optparse import OptionParser19from optparse import OptionParser
20import sys20import sys
2121
22from subunit import DiscardStream, ProtocolTestCase22from testtools import (
23 CopyStreamResult, StreamToExtendedDecorator, StreamResultRouter,
24 StreamSummary)
25
26from subunit import ByteStreamToStreamResult
27from subunit.filters import run_tests_from_stream
23from subunit.test_results import (28from subunit.test_results import (
24 AutoTimingTestResultDecorator,29 CatFiles,
25 TestIdPrintingResult,30 TestIdPrintingResult,
26 )31 )
2732
@@ -30,18 +35,24 @@
30parser.add_option("--times", action="store_true",35parser.add_option("--times", action="store_true",
31 help="list the time each test took (requires a timestamped stream)",36 help="list the time each test took (requires a timestamped stream)",
32 default=False)37 default=False)
38parser.add_option("--exists", action="store_true",
39 help="list tests that are reported as existing (as well as ran)",
40 default=False)
33parser.add_option("--no-passthrough", action="store_true",41parser.add_option("--no-passthrough", action="store_true",
34 help="Hide all non subunit input.", default=False, dest="no_passthrough")42 help="Hide all non subunit input.", default=False, dest="no_passthrough")
35(options, args) = parser.parse_args()43(options, args) = parser.parse_args()
36result = AutoTimingTestResultDecorator(44test = ByteStreamToStreamResult(sys.stdin, non_subunit_name="stdout")
37 TestIdPrintingResult(sys.stdout, options.times))45result = TestIdPrintingResult(sys.stdout, options.times, options.exists)
38if options.no_passthrough:46if not options.no_passthrough:
39 passthrough_stream = DiscardStream()47 result = StreamResultRouter(result)
40else:48 cat = CatFiles(sys.stdout)
41 passthrough_stream = None49 result.map(cat, 'test_id', test_id=None)
42test = ProtocolTestCase(sys.stdin, passthrough=passthrough_stream)50summary = StreamSummary()
51result = CopyStreamResult([result, summary])
52result.startTestRun()
43test.run(result)53test.run(result)
44if result.wasSuccessful():54result.stopTestRun()
55if summary.wasSuccessful():
45 exit_code = 056 exit_code = 0
46else:57else:
47 exit_code = 158 exit_code = 1
4859
=== modified file 'filters/subunit-notify'
--- filters/subunit-notify 2012-03-27 11:17:37 +0000
+++ filters/subunit-notify 2013-03-31 05:51:20 +0000
@@ -19,6 +19,7 @@
19import pygtk19import pygtk
20pygtk.require('2.0')20pygtk.require('2.0')
21import pynotify21import pynotify
22from testtools import StreamToExtendedDecorator
2223
23from subunit import TestResultStats24from subunit import TestResultStats
24from subunit.filters import run_filter_script25from subunit.filters import run_filter_script
@@ -28,6 +29,7 @@
2829
2930
30def notify_of_result(result):31def notify_of_result(result):
32 result = result.decorated
31 if result.failed_tests > 0:33 if result.failed_tests > 0:
32 summary = "Test run failed"34 summary = "Test run failed"
33 else:35 else:
@@ -41,4 +43,6 @@
41 nw.show()43 nw.show()
4244
4345
44run_filter_script(TestResultStats, __doc__, notify_of_result)46run_filter_script(
47 lambda output:StreamToExtendedDecorator(TestResultStats(output)),
48 __doc__, notify_of_result, protocol_version=2)
4549
=== modified file 'filters/subunit-stats'
--- filters/subunit-stats 2009-09-30 12:04:18 +0000
+++ filters/subunit-stats 2013-03-31 05:51:20 +0000
@@ -16,26 +16,17 @@
1616
17"""Filter a subunit stream to get aggregate statistics."""17"""Filter a subunit stream to get aggregate statistics."""
1818
19from optparse import OptionParser
20import sys19import sys
21import unittest20
2221from testtools import StreamToExtendedDecorator
23from subunit import DiscardStream, ProtocolTestCase, TestResultStats22
2423from subunit import TestResultStats
25parser = OptionParser(description=__doc__)24from subunit.filters import run_filter_script
26parser.add_option("--no-passthrough", action="store_true",25
27 help="Hide all non subunit input.", default=False, dest="no_passthrough")26
28(options, args) = parser.parse_args()
29result = TestResultStats(sys.stdout)27result = TestResultStats(sys.stdout)
30if options.no_passthrough:28def show_stats(r):
31 passthrough_stream = DiscardStream()29 r.decorated.formatStats()
32else:30run_filter_script(
33 passthrough_stream = None31 lambda output:StreamToExtendedDecorator(result),
34test = ProtocolTestCase(sys.stdin, passthrough=passthrough_stream)32 __doc__, show_stats, protocol_version=2)
35test.run(result)
36result.formatStats()
37if result.wasSuccessful():
38 exit_code = 0
39else:
40 exit_code = 1
41sys.exit(exit_code)
4233
=== modified file 'filters/subunit2csv'
--- filters/subunit2csv 2012-03-27 10:57:51 +0000
+++ filters/subunit2csv 2013-03-31 05:51:20 +0000
@@ -16,8 +16,11 @@
1616
17"""Turn a subunit stream into a CSV"""17"""Turn a subunit stream into a CSV"""
1818
19from testtools import StreamToExtendedDecorator
20
19from subunit.filters import run_filter_script21from subunit.filters import run_filter_script
20from subunit.test_results import CsvResult22from subunit.test_results import CsvResult
2123
2224
23run_filter_script(CsvResult, __doc__)25run_filter_script(lambda output:StreamToExtendedDecorator(CsvResult(output)),
26 __doc__, protocol_version=2)
2427
=== modified file 'filters/subunit2gtk'
--- filters/subunit2gtk 2010-01-25 15:45:45 +0000
+++ filters/subunit2gtk 2013-03-31 05:51:20 +0000
@@ -46,17 +46,20 @@
46"""Display a subunit stream in a gtk progress window."""46"""Display a subunit stream in a gtk progress window."""
4747
48import sys48import sys
49import threading
49import unittest50import unittest
5051
51import pygtk52import pygtk
52pygtk.require('2.0')53pygtk.require('2.0')
53import gtk, gtk.gdk, gobject54import gtk, gtk.gdk, gobject
5455
56from testtools import StreamToExtendedDecorator
57
55from subunit import (58from subunit import (
56 PROGRESS_POP,59 PROGRESS_POP,
57 PROGRESS_PUSH,60 PROGRESS_PUSH,
58 PROGRESS_SET,61 PROGRESS_SET,
59 TestProtocolServer,62 ByteStreamToStreamResult,
60 )63 )
61from subunit.progress_model import ProgressModel64from subunit.progress_model import ProgressModel
6265
@@ -139,6 +142,9 @@
139142
140 def stopTest(self, test):143 def stopTest(self, test):
141 super(GTKTestResult, self).stopTest(test)144 super(GTKTestResult, self).stopTest(test)
145 gobject.idle_add(self._stopTest)
146
147 def _stopTest(self):
142 self.progress_model.advance()148 self.progress_model.advance()
143 if self.progress_model.width() == 0:149 if self.progress_model.width() == 0:
144 self.pbar.pulse()150 self.pbar.pulse()
@@ -153,26 +159,26 @@
153 super(GTKTestResult, self).stopTestRun()159 super(GTKTestResult, self).stopTestRun()
154 except AttributeError:160 except AttributeError:
155 pass161 pass
156 self.pbar.set_text('Finished')162 gobject.idle_add(self.pbar.set_text, 'Finished')
157163
158 def addError(self, test, err):164 def addError(self, test, err):
159 super(GTKTestResult, self).addError(test, err)165 super(GTKTestResult, self).addError(test, err)
160 self.update_counts()166 gobject.idle_add(self.update_counts)
161167
162 def addFailure(self, test, err):168 def addFailure(self, test, err):
163 super(GTKTestResult, self).addFailure(test, err)169 super(GTKTestResult, self).addFailure(test, err)
164 self.update_counts()170 gobject.idle_add(self.update_counts)
165171
166 def addSuccess(self, test):172 def addSuccess(self, test):
167 super(GTKTestResult, self).addSuccess(test)173 super(GTKTestResult, self).addSuccess(test)
168 self.update_counts()174 gobject.idle_add(self.update_counts)
169175
170 def addSkip(self, test, reason):176 def addSkip(self, test, reason):
171 # addSkip is new in Python 2.7/3.1177 # addSkip is new in Python 2.7/3.1
172 addSkip = getattr(super(GTKTestResult, self), 'addSkip', None)178 addSkip = getattr(super(GTKTestResult, self), 'addSkip', None)
173 if callable(addSkip):179 if callable(addSkip):
174 addSkip(test, reason)180 addSkip(test, reason)
175 self.update_counts()181 gobject.idle_add(self.update_counts)
176182
177 def addExpectedFailure(self, test, err):183 def addExpectedFailure(self, test, err):
178 # addExpectedFailure is new in Python 2.7/3.1184 # addExpectedFailure is new in Python 2.7/3.1
@@ -180,7 +186,7 @@
180 'addExpectedFailure', None)186 'addExpectedFailure', None)
181 if callable(addExpectedFailure):187 if callable(addExpectedFailure):
182 addExpectedFailure(test, err)188 addExpectedFailure(test, err)
183 self.update_counts()189 gobject.idle_add(self.update_counts)
184190
185 def addUnexpectedSuccess(self, test):191 def addUnexpectedSuccess(self, test):
186 # addUnexpectedSuccess is new in Python 2.7/3.1192 # addUnexpectedSuccess is new in Python 2.7/3.1
@@ -188,7 +194,7 @@
188 'addUnexpectedSuccess', None)194 'addUnexpectedSuccess', None)
189 if callable(addUnexpectedSuccess):195 if callable(addUnexpectedSuccess):
190 addUnexpectedSuccess(test)196 addUnexpectedSuccess(test)
191 self.update_counts()197 gobject.idle_add(self.update_counts)
192198
193 def progress(self, offset, whence):199 def progress(self, offset, whence):
194 if whence == PROGRESS_PUSH:200 if whence == PROGRESS_PUSH:
@@ -212,47 +218,22 @@
212 self.ok_label.set_text(str(self.testsRun - bad))218 self.ok_label.set_text(str(self.testsRun - bad))
213 self.not_ok_label.set_text(str(bad))219 self.not_ok_label.set_text(str(bad))
214220
215221gobject.threads_init()
216class GIOProtocolTestCase(object):222result = StreamToExtendedDecorator(GTKTestResult())
217223test = ByteStreamToStreamResult(sys.stdin, non_subunit_name='stdout')
218 def __init__(self, stream, result, on_finish):224# Get setup
219 self.stream = stream225while gtk.events_pending():
220 self.schedule_read()226 gtk.main_iteration()
221 self.hup_id = gobject.io_add_watch(stream, gobject.IO_HUP, self.hup)227# Start IO
222 self.protocol = TestProtocolServer(result)228def run_and_finish():
223 self.on_finish = on_finish229 test.run(result)
224230 result.stopTestRun()
225 def read(self, source, condition, all=False):231t = threading.Thread(target=run_and_finish)
226 #NB: \o/ actually blocks232t.daemon = True
227 line = source.readline()233result.startTestRun()
228 if not line:234t.start()
229 self.protocol.lostConnection()
230 self.on_finish()
231 return False
232 self.protocol.lineReceived(line)
233 # schedule more IO shortly - if we say we're willing to do it
234 # immediately we starve things.
235 if not all:
236 source_id = gobject.timeout_add(1, self.schedule_read)
237 return False
238 else:
239 return True
240
241 def schedule_read(self):
242 self.read_id = gobject.io_add_watch(self.stream, gobject.IO_IN, self.read)
243
244 def hup(self, source, condition):
245 while self.read(source, condition, all=True): pass
246 self.protocol.lostConnection()
247 gobject.source_remove(self.read_id)
248 self.on_finish()
249 return False
250
251
252result = GTKTestResult()
253test = GIOProtocolTestCase(sys.stdin, result, result.stopTestRun)
254gtk.main()235gtk.main()
255if result.wasSuccessful():236if result.decorated.wasSuccessful():
256 exit_code = 0237 exit_code = 0
257else:238else:
258 exit_code = 1239 exit_code = 1
259240
=== modified file 'filters/subunit2junitxml'
--- filters/subunit2junitxml 2012-03-27 10:57:51 +0000
+++ filters/subunit2junitxml 2013-03-31 05:51:20 +0000
@@ -18,6 +18,9 @@
1818
1919
20import sys20import sys
21
22from testtools import StreamToExtendedDecorator
23
21from subunit.filters import run_filter_script24from subunit.filters import run_filter_script
2225
23try:26try:
@@ -28,4 +31,6 @@
28 raise31 raise
2932
3033
31run_filter_script(JUnitXmlResult, __doc__)34run_filter_script(
35 lambda output:StreamToExtendedDecorator(JUnitXmlResult(output)), __doc__,
36 protocol_version=2)
3237
=== modified file 'filters/subunit2pyunit'
--- filters/subunit2pyunit 2009-09-30 12:04:18 +0000
+++ filters/subunit2pyunit 2013-03-31 05:51:20 +0000
@@ -16,11 +16,15 @@
1616
17"""Display a subunit stream through python's unittest test runner."""17"""Display a subunit stream through python's unittest test runner."""
1818
19from operator import methodcaller
19from optparse import OptionParser20from optparse import OptionParser
20import sys21import sys
21import unittest22import unittest
2223
23from subunit import DiscardStream, ProtocolTestCase, TestProtocolServer24from testtools import StreamToExtendedDecorator, DecorateTestCaseResult, StreamResultRouter
25
26from subunit import ByteStreamToStreamResult
27from subunit.test_results import CatFiles
2428
25parser = OptionParser(description=__doc__)29parser = OptionParser(description=__doc__)
26parser.add_option("--no-passthrough", action="store_true",30parser.add_option("--no-passthrough", action="store_true",
@@ -29,11 +33,16 @@
29 help="Use bzrlib's test reporter (requires bzrlib)",33 help="Use bzrlib's test reporter (requires bzrlib)",
30 default=False)34 default=False)
31(options, args) = parser.parse_args()35(options, args) = parser.parse_args()
32if options.no_passthrough:36test = ByteStreamToStreamResult(sys.stdin, non_subunit_name='stdout')
33 passthrough_stream = DiscardStream()37def wrap_result(result):
34else:38 result = StreamToExtendedDecorator(result)
35 passthrough_stream = None39 if not options.no_passthrough:
36test = ProtocolTestCase(sys.stdin, passthrough=passthrough_stream)40 result = StreamResultRouter(result)
41 result.map(CatFiles(sys.stdout), 'test_id', test_id=None)
42 return result
43test = DecorateTestCaseResult(test, wrap_result,
44 before_run=methodcaller('startTestRun'),
45 after_run=methodcaller('stopTestRun'))
37if options.progress:46if options.progress:
38 from bzrlib.tests import TextTestRunner47 from bzrlib.tests import TextTestRunner
39 from bzrlib import ui48 from bzrlib import ui
4049
=== modified file 'python/subunit/__init__.py'
--- python/subunit/__init__.py 2013-02-07 11:33:11 +0000
+++ python/subunit/__init__.py 2013-03-31 05:51:20 +0000
@@ -126,7 +126,7 @@
126except ImportError:126except ImportError:
127 _UnsupportedOperation = AttributeError127 _UnsupportedOperation = AttributeError
128128
129129from extras import safe_hasattr
130from testtools import content, content_type, ExtendedToOriginalDecorator130from testtools import content, content_type, ExtendedToOriginalDecorator
131from testtools.content import TracebackContent131from testtools.content import TracebackContent
132from testtools.compat import _b, _u, BytesIO, StringIO132from testtools.compat import _b, _u, BytesIO, StringIO
@@ -143,9 +143,10 @@
143except ImportError:143except ImportError:
144 raise ImportError ("testtools.testresult.real does not contain "144 raise ImportError ("testtools.testresult.real does not contain "
145 "_StringException, check your version.")145 "_StringException, check your version.")
146from testtools import testresult146from testtools import testresult, CopyStreamResult
147147
148from subunit import chunked, details, iso8601, test_results148from subunit import chunked, details, iso8601, test_results
149from subunit.v2 import ByteStreamToStreamResult, StreamResultToBytes
149150
150# same format as sys.version_info: "A tuple containing the five components of151# same format as sys.version_info: "A tuple containing the five components of
151# the version number: major, minor, micro, releaselevel, and serial. All152# the version number: major, minor, micro, releaselevel, and serial. All
@@ -992,44 +993,51 @@
992 return result993 return result
993994
994995
995def TAP2SubUnit(tap, subunit):996def TAP2SubUnit(tap, output_stream):
996 """Filter a TAP pipe into a subunit pipe.997 """Filter a TAP pipe into a subunit pipe.
997998
998 :param tap: A tap pipe/stream/file object.999 This should be invoked once per TAP script, as TAP scripts get
1000 mapped to a single runnable case with multiple components.
1001
1002 :param tap: A tap pipe/stream/file object - should emit unicode strings.
999 :param subunit: A pipe/stream/file object to write subunit results to.1003 :param subunit: A pipe/stream/file object to write subunit results to.
1000 :return: The exit code to exit with.1004 :return: The exit code to exit with.
1001 """1005 """
1006 output = StreamResultToBytes(output_stream)
1007 UTF8_TEXT = 'text/plain; charset=UTF8'
1002 BEFORE_PLAN = 01008 BEFORE_PLAN = 0
1003 AFTER_PLAN = 11009 AFTER_PLAN = 1
1004 SKIP_STREAM = 21010 SKIP_STREAM = 2
1005 state = BEFORE_PLAN1011 state = BEFORE_PLAN
1006 plan_start = 11012 plan_start = 1
1007 plan_stop = 01013 plan_stop = 0
1008 def _skipped_test(subunit, plan_start):
1009 # Some tests were skipped.
1010 subunit.write('test test %d\n' % plan_start)
1011 subunit.write('error test %d [\n' % plan_start)
1012 subunit.write('test missing from TAP output\n')
1013 subunit.write(']\n')
1014 return plan_start + 1
1015 # Test data for the next test to emit1014 # Test data for the next test to emit
1016 test_name = None1015 test_name = None
1017 log = []1016 log = []
1018 result = None1017 result = None
1018 def missing_test(plan_start):
1019 output.status(test_id='test %d' % plan_start,
1020 test_status='fail', runnable=False,
1021 mime_type=UTF8_TEXT, eof=True, file_name="tap meta",
1022 file_bytes=b"test missing from TAP output")
1019 def _emit_test():1023 def _emit_test():
1020 "write out a test"1024 "write out a test"
1021 if test_name is None:1025 if test_name is None:
1022 return1026 return
1023 subunit.write("test %s\n" % test_name)
1024 if not log:
1025 subunit.write("%s %s\n" % (result, test_name))
1026 else:
1027 subunit.write("%s %s [\n" % (result, test_name))
1028 if log:1027 if log:
1029 for line in log:1028 log_bytes = b'\n'.join(log_line.encode('utf8') for log_line in log)
1030 subunit.write("%s\n" % line)1029 mime_type = UTF8_TEXT
1031 subunit.write("]\n")1030 file_name = 'tap comment'
1031 eof = True
1032 else:
1033 log_bytes = None
1034 mime_type = None
1035 file_name = None
1036 eof = True
1032 del log[:]1037 del log[:]
1038 output.status(test_id=test_name, test_status=result,
1039 file_bytes=log_bytes, mime_type=mime_type, eof=eof,
1040 file_name=file_name, runnable=False)
1033 for line in tap:1041 for line in tap:
1034 if state == BEFORE_PLAN:1042 if state == BEFORE_PLAN:
1035 match = re.match("(\d+)\.\.(\d+)\s*(?:\#\s+(.*))?\n", line)1043 match = re.match("(\d+)\.\.(\d+)\s*(?:\#\s+(.*))?\n", line)
@@ -1040,10 +1048,9 @@
1040 if plan_start > plan_stop and plan_stop == 0:1048 if plan_start > plan_stop and plan_stop == 0:
1041 # skipped file1049 # skipped file
1042 state = SKIP_STREAM1050 state = SKIP_STREAM
1043 subunit.write("test file skip\n")1051 output.status(test_id='file skip', test_status='skip',
1044 subunit.write("skip file skip [\n")1052 file_bytes=comment.encode('utf8'), eof=True,
1045 subunit.write("%s\n" % comment)1053 file_name='tap comment')
1046 subunit.write("]\n")
1047 continue1054 continue
1048 # not a plan line, or have seen one before1055 # not a plan line, or have seen one before
1049 match = re.match("(ok|not ok)(?:\s+(\d+)?)?(?:\s+([^#]*[^#\s]+)\s*)?(?:\s+#\s+(TODO|SKIP|skip|todo)(?:\s+(.*))?)?\n", line)1056 match = re.match("(ok|not ok)(?:\s+(\d+)?)?(?:\s+([^#]*[^#\s]+)\s*)?(?:\s+#\s+(TODO|SKIP|skip|todo)(?:\s+(.*))?)?\n", line)
@@ -1054,7 +1061,7 @@
1054 if status == 'ok':1061 if status == 'ok':
1055 result = 'success'1062 result = 'success'
1056 else:1063 else:
1057 result = "failure"1064 result = "fail"
1058 if description is None:1065 if description is None:
1059 description = ''1066 description = ''
1060 else:1067 else:
@@ -1069,7 +1076,8 @@
1069 if number is not None:1076 if number is not None:
1070 number = int(number)1077 number = int(number)
1071 while plan_start < number:1078 while plan_start < number:
1072 plan_start = _skipped_test(subunit, plan_start)1079 missing_test(plan_start)
1080 plan_start += 1
1073 test_name = "test %d%s" % (plan_start, description)1081 test_name = "test %d%s" % (plan_start, description)
1074 plan_start += 11082 plan_start += 1
1075 continue1083 continue
@@ -1082,18 +1090,21 @@
1082 extra = ' %s' % reason1090 extra = ' %s' % reason
1083 _emit_test()1091 _emit_test()
1084 test_name = "Bail out!%s" % extra1092 test_name = "Bail out!%s" % extra
1085 result = "error"1093 result = "fail"
1086 state = SKIP_STREAM1094 state = SKIP_STREAM
1087 continue1095 continue
1088 match = re.match("\#.*\n", line)1096 match = re.match("\#.*\n", line)
1089 if match:1097 if match:
1090 log.append(line[:-1])1098 log.append(line[:-1])
1091 continue1099 continue
1092 subunit.write(line)1100 # Should look at buffering status and binding this to the prior result.
1101 output.status(file_bytes=line.encode('utf8'), file_name='stdout',
1102 mime_type=UTF8_TEXT)
1093 _emit_test()1103 _emit_test()
1094 while plan_start <= plan_stop:1104 while plan_start <= plan_stop:
1095 # record missed tests1105 # record missed tests
1096 plan_start = _skipped_test(subunit, plan_start)1106 missing_test(plan_start)
1107 plan_start += 1
1097 return 01108 return 0
10981109
10991110
@@ -1121,24 +1132,21 @@
1121 :return: 01132 :return: 0
1122 """1133 """
1123 new_tags, gone_tags = tags_to_new_gone(tags)1134 new_tags, gone_tags = tags_to_new_gone(tags)
1124 def write_tags(new_tags, gone_tags):1135 source = ByteStreamToStreamResult(original, non_subunit_name='stdout')
1125 if new_tags or gone_tags:1136 class Tagger(CopyStreamResult):
1126 filtered.write("tags: " + ' '.join(new_tags))1137 def status(self, **kwargs):
1127 if gone_tags:1138 tags = kwargs.get('test_tags')
1128 for tag in gone_tags:1139 if not tags:
1129 filtered.write("-" + tag)1140 tags = set()
1130 filtered.write("\n")1141 tags.update(new_tags)
1131 write_tags(new_tags, gone_tags)1142 tags.difference_update(gone_tags)
1132 # TODO: use the protocol parser and thus don't mangle test comments.1143 if tags:
1133 for line in original:1144 kwargs['test_tags'] = tags
1134 if line.startswith("tags:"):1145 else:
1135 line_tags = line[5:].split()1146 kwargs['test_tags'] = None
1136 line_new, line_gone = tags_to_new_gone(line_tags)1147 super(Tagger, self).status(**kwargs)
1137 line_new = line_new - gone_tags1148 output = Tagger([StreamResultToBytes(filtered)])
1138 line_gone = line_gone - new_tags1149 source.run(output)
1139 write_tags(line_new, line_gone)
1140 else:
1141 filtered.write(line)
1142 return 01150 return 0
11431151
11441152
@@ -1260,7 +1268,8 @@
1260 else:1268 else:
1261 stream = sys.stdout1269 stream = sys.stdout
1262 if sys.version_info > (3, 0):1270 if sys.version_info > (3, 0):
1263 stream = stream.buffer1271 if safe_hasattr(stream, 'buffer'):
1272 stream = stream.buffer
1264 return stream1273 return stream
12651274
12661275
@@ -1291,6 +1300,7 @@
1291 _make_binary_on_windows(fileno)1300 _make_binary_on_windows(fileno)
1292 return _unwrap_text(stream)1301 return _unwrap_text(stream)
12931302
1303
1294def _make_binary_on_windows(fileno):1304def _make_binary_on_windows(fileno):
1295 """Win32 mangles \r\n to \n and that breaks streams. See bug lp:505078."""1305 """Win32 mangles \r\n to \n and that breaks streams. See bug lp:505078."""
1296 if sys.platform == "win32":1306 if sys.platform == "win32":
12971307
=== modified file 'python/subunit/filters.py'
--- python/subunit/filters.py 2012-03-27 11:17:37 +0000
+++ python/subunit/filters.py 2013-03-31 05:51:20 +0000
@@ -17,7 +17,14 @@
17from optparse import OptionParser17from optparse import OptionParser
18import sys18import sys
1919
20from subunit import DiscardStream, ProtocolTestCase20from extras import safe_hasattr
21from testtools import CopyStreamResult, StreamResult, StreamResultRouter
22
23from subunit import (
24 DiscardStream, ProtocolTestCase, ByteStreamToStreamResult,
25 StreamResultToBytes,
26 )
27from subunit.test_results import CatFiles
2128
2229
23def make_options(description):30def make_options(description):
@@ -31,33 +38,75 @@
31 help="Send the output to this path rather than stdout.")38 help="Send the output to this path rather than stdout.")
32 parser.add_option(39 parser.add_option(
33 "-f", "--forward", action="store_true", default=False,40 "-f", "--forward", action="store_true", default=False,
34 help="Forward subunit stream on stdout.")41 help="Forward subunit stream on stdout. When set, received "
42 "non-subunit output will be encapsulated in subunit.")
35 return parser43 return parser
3644
3745
38def run_tests_from_stream(input_stream, result, passthrough_stream=None,46def run_tests_from_stream(input_stream, result, passthrough_stream=None,
39 forward_stream=None):47 forward_stream=None, protocol_version=1, passthrough_subunit=True):
40 """Run tests from a subunit input stream through 'result'.48 """Run tests from a subunit input stream through 'result'.
4149
50 Non-test events - top level file attachments - are expected to be
51 dropped by v2 StreamResults at the present time (as all the analysis code
52 is in ExtendedTestResult API's), so to implement passthrough_stream they
53 are diverted and copied directly when that is set.
54
42 :param input_stream: A stream containing subunit input.55 :param input_stream: A stream containing subunit input.
43 :param result: A TestResult that will receive the test events.56 :param result: A TestResult that will receive the test events.
57 NB: This should be an ExtendedTestResult for v1 and a StreamResult for
58 v2.
44 :param passthrough_stream: All non-subunit input received will be59 :param passthrough_stream: All non-subunit input received will be
45 sent to this stream. If not provided, uses the ``TestProtocolServer``60 sent to this stream. If not provided, uses the ``TestProtocolServer``
46 default, which is ``sys.stdout``.61 default, which is ``sys.stdout``.
47 :param forward_stream: All subunit input received will be forwarded62 :param forward_stream: All subunit input received will be forwarded
48 to this stream. If not provided, uses the ``TestProtocolServer``63 to this stream. If not provided, uses the ``TestProtocolServer``
49 default, which is to not forward any input.64 default, which is to not forward any input. Do not set this when
65 transforming the stream - items would be double-reported.
66 :param protocol_version: What version of the subunit protocol to expect.
67 :param passthrough_subunit: If True, passthrough should be as subunit
68 otherwise unwrap it. Only has effect when forward_stream is None.
69 (when forwarding as subunit non-subunit input is always turned into
70 subunit)
50 """71 """
51 test = ProtocolTestCase(72 if 1==protocol_version:
52 input_stream, passthrough=passthrough_stream,73 test = ProtocolTestCase(
53 forward=forward_stream)74 input_stream, passthrough=passthrough_stream,
75 forward=forward_stream)
76 elif 2==protocol_version:
77 # In all cases we encapsulate unknown inputs.
78 if forward_stream is not None:
79 # Send events to forward_stream as subunit.
80 forward_result = StreamResultToBytes(forward_stream)
81 # If we're passing non-subunit through, copy:
82 if passthrough_stream is None:
83 # Not passing non-test events - split them off to nothing.
84 router = StreamResultRouter(forward_result)
85 router.map(StreamResult(), 'test_id', test_id=None)
86 result = CopyStreamResult([router, result])
87 else:
88 # otherwise, copy all events to forward_result
89 result = CopyStreamResult([forward_result, result])
90 elif passthrough_stream is not None:
91 if not passthrough_subunit:
92 # Route non-test events to passthrough_stream, unwrapping them for
93 # display.
94 passthrough_result = CatFiles(passthrough_stream)
95 else:
96 passthrough_result = StreamResultToBytes(passthrough_stream)
97 result = StreamResultRouter(result)
98 result.map(passthrough_result, 'test_id', test_id=None)
99 test = ByteStreamToStreamResult(input_stream,
100 non_subunit_name='stdout')
101 else:
102 raise Exception("Unknown protocol version.")
54 result.startTestRun()103 result.startTestRun()
55 test.run(result)104 test.run(result)
56 result.stopTestRun()105 result.stopTestRun()
57106
58107
59def filter_by_result(result_factory, output_path, passthrough, forward,108def filter_by_result(result_factory, output_path, passthrough, forward,
60 input_stream=sys.stdin):109 input_stream=sys.stdin, protocol_version=1):
61 """Filter an input stream using a test result.110 """Filter an input stream using a test result.
62111
63 :param result_factory: A callable that when passed an output stream112 :param result_factory: A callable that when passed an output stream
@@ -71,17 +120,23 @@
71 ``sys.stdout`` as well as to the ``TestResult``.120 ``sys.stdout`` as well as to the ``TestResult``.
72 :param input_stream: The source of subunit input. Defaults to121 :param input_stream: The source of subunit input. Defaults to
73 ``sys.stdin``.122 ``sys.stdin``.
74 :return: A test result with the resultts of the run.123 :param protocol_version: The subunit protocol version to expect.
124 :return: A test result with the results of the run.
75 """125 """
76 if passthrough:126 if passthrough:
77 passthrough_stream = sys.stdout127 passthrough_stream = sys.stdout
78 else:128 else:
79 passthrough_stream = DiscardStream()129 if 1==protocol_version:
130 passthrough_stream = DiscardStream()
131 else:
132 passthrough_stream = None
80133
81 if forward:134 if forward:
82 forward_stream = sys.stdout135 forward_stream = sys.stdout
83 else:136 elif 1==protocol_version:
84 forward_stream = DiscardStream()137 forward_stream = DiscardStream()
138 else:
139 forward_stream = None
85140
86 if output_path is None:141 if output_path is None:
87 output_to = sys.stdout142 output_to = sys.stdout
@@ -91,14 +146,16 @@
91 try:146 try:
92 result = result_factory(output_to)147 result = result_factory(output_to)
93 run_tests_from_stream(148 run_tests_from_stream(
94 input_stream, result, passthrough_stream, forward_stream)149 input_stream, result, passthrough_stream, forward_stream,
150 protocol_version=protocol_version)
95 finally:151 finally:
96 if output_path:152 if output_path:
97 output_to.close()153 output_to.close()
98 return result154 return result
99155
100156
101def run_filter_script(result_factory, description, post_run_hook=None):157def run_filter_script(result_factory, description, post_run_hook=None,
158 protocol_version=1):
102 """Main function for simple subunit filter scripts.159 """Main function for simple subunit filter scripts.
103160
104 Many subunit filter scripts take a stream of subunit input and use a161 Many subunit filter scripts take a stream of subunit input and use a
@@ -111,14 +168,17 @@
111 :param result_factory: A callable that takes an output stream and returns168 :param result_factory: A callable that takes an output stream and returns
112 a test result that outputs to that stream.169 a test result that outputs to that stream.
113 :param description: A description of the filter script.170 :param description: A description of the filter script.
171 :param protocol_version: What protocol version to consume/emit.
114 """172 """
115 parser = make_options(description)173 parser = make_options(description)
116 (options, args) = parser.parse_args()174 (options, args) = parser.parse_args()
117 result = filter_by_result(175 result = filter_by_result(
118 result_factory, options.output_to, not options.no_passthrough,176 result_factory, options.output_to, not options.no_passthrough,
119 options.forward)177 options.forward, protocol_version=protocol_version)
120 if post_run_hook:178 if post_run_hook:
121 post_run_hook(result)179 post_run_hook(result)
180 if not safe_hasattr(result, 'wasSuccessful'):
181 result = result.decorated
122 if result.wasSuccessful():182 if result.wasSuccessful():
123 sys.exit(0)183 sys.exit(0)
124 else:184 else:
125185
=== modified file 'python/subunit/run.py'
--- python/subunit/run.py 2012-12-17 07:24:28 +0000
+++ python/subunit/run.py 2013-03-31 05:51:20 +0000
@@ -20,9 +20,14 @@
20 $ python -m subunit.run mylib.tests.test_suite20 $ python -m subunit.run mylib.tests.test_suite
21"""21"""
2222
23import io
24import os
23import sys25import sys
2426
25from subunit import TestProtocolClient, get_default_formatter27from testtools import ExtendedToStreamDecorator
28from testtools.testsuite import iterate_tests
29
30from subunit import StreamResultToBytes, get_default_formatter
26from subunit.test_results import AutoTimingTestResultDecorator31from subunit.test_results import AutoTimingTestResultDecorator
27from testtools.run import (32from testtools.run import (
28 BUFFEROUTPUT,33 BUFFEROUTPUT,
@@ -46,11 +51,34 @@
4651
47 def run(self, test):52 def run(self, test):
48 "Run the given test case or test suite."53 "Run the given test case or test suite."
49 result = TestProtocolClient(self.stream)54 result = self._list(test)
55 result = ExtendedToStreamDecorator(result)
50 result = AutoTimingTestResultDecorator(result)56 result = AutoTimingTestResultDecorator(result)
51 if self.failfast is not None:57 if self.failfast is not None:
52 result.failfast = self.failfast58 result.failfast = self.failfast
53 test(result)59 result.startTestRun()
60 try:
61 test(result)
62 finally:
63 result.stopTestRun()
64 return result
65
66 def list(self, test):
67 "List the test."
68 self._list(test)
69
70 def _list(self, test):
71 try:
72 fileno = self.stream.fileno()
73 except:
74 fileno = None
75 if fileno is not None:
76 stream = os.fdopen(fileno, 'wb', 0)
77 else:
78 stream = self.stream
79 result = StreamResultToBytes(stream)
80 for case in iterate_tests(test):
81 result.status(test_id=case.id(), test_status='exists')
54 return result82 return result
5583
5684
@@ -78,7 +106,15 @@
78106
79107
80if __name__ == '__main__':108if __name__ == '__main__':
109 # Disable the default buffering, for Python 2.x where pdb doesn't do it
110 # on non-ttys.
81 stream = get_default_formatter()111 stream = get_default_formatter()
82 runner = SubunitTestRunner112 runner = SubunitTestRunner
113 # Patch stdout to be unbuffered, so that pdb works well on 2.6/2.7.
114 binstdout = io.open(sys.stdout.fileno(), 'wb', 0)
115 if sys.version_info[0] > 2:
116 sys.stdout = io.TextIOWrapper(binstdout, encoding=sys.stdout.encoding)
117 else:
118 sys.stdout = binstdout
83 SubunitTestProgram(module=None, argv=sys.argv, testRunner=runner,119 SubunitTestProgram(module=None, argv=sys.argv, testRunner=runner,
84 stdout=sys.stdout)120 stdout=sys.stdout)
85121
=== modified file 'python/subunit/test_results.py'
--- python/subunit/test_results.py 2012-12-17 07:24:28 +0000
+++ python/subunit/test_results.py 2013-03-31 05:51:20 +0000
@@ -25,8 +25,10 @@
25 text_content,25 text_content,
26 TracebackContent,26 TracebackContent,
27 )27 )
28from testtools import StreamResult
2829
29from subunit import iso860130from subunit import iso8601
31import subunit
3032
3133
32# NOT a TestResult, because we are implementing the interface, not inheriting34# NOT a TestResult, because we are implementing the interface, not inheriting
@@ -525,16 +527,24 @@
525527
526528
527class TestIdPrintingResult(testtools.TestResult):529class TestIdPrintingResult(testtools.TestResult):
528530 """Print test ids to a stream.
529 def __init__(self, stream, show_times=False):531
532 Implements both TestResult and StreamResult, for compatibility.
533 """
534
535 def __init__(self, stream, show_times=False, show_exists=False):
530 """Create a FilterResult object outputting to stream."""536 """Create a FilterResult object outputting to stream."""
531 super(TestIdPrintingResult, self).__init__()537 super(TestIdPrintingResult, self).__init__()
532 self._stream = stream538 self._stream = stream
539 self.show_exists = show_exists
540 self.show_times = show_times
541
542 def startTestRun(self):
533 self.failed_tests = 0543 self.failed_tests = 0
534 self.__time = None544 self.__time = None
535 self.show_times = show_times
536 self._test = None545 self._test = None
537 self._test_duration = 0546 self._test_duration = 0
547 self._active_tests = {}
538548
539 def addError(self, test, err):549 def addError(self, test, err):
540 self.failed_tests += 1550 self.failed_tests += 1
@@ -557,21 +567,44 @@
557 def addExpectedFailure(self, test, err=None, details=None):567 def addExpectedFailure(self, test, err=None, details=None):
558 self._test = test568 self._test = test
559569
560 def reportTest(self, test, duration):570 def reportTest(self, test_id, duration):
561 if self.show_times:571 if self.show_times:
562 seconds = duration.seconds572 seconds = duration.seconds
563 seconds += duration.days * 3600 * 24573 seconds += duration.days * 3600 * 24
564 seconds += duration.microseconds / 1000000.0574 seconds += duration.microseconds / 1000000.0
565 self._stream.write(test.id() + ' %0.3f\n' % seconds)575 self._stream.write(test_id + ' %0.3f\n' % seconds)
566 else:576 else:
567 self._stream.write(test.id() + '\n')577 self._stream.write(test_id + '\n')
568578
569 def startTest(self, test):579 def startTest(self, test):
570 self._start_time = self._time()580 self._start_time = self._time()
571581
582 def status(self, test_id=None, test_status=None, test_tags=None,
583 runnable=True, file_name=None, file_bytes=None, eof=False,
584 mime_type=None, route_code=None, timestamp=None):
585 if not test_id:
586 return
587 if timestamp is not None:
588 self.time(timestamp)
589 if test_status=='exists':
590 if self.show_exists:
591 self.reportTest(test_id, 0)
592 elif test_status in ('inprogress', None):
593 self._active_tests[test_id] = self._time()
594 else:
595 self._end_test(test_id)
596
597 def _end_test(self, test_id):
598 test_start = self._active_tests.pop(test_id, None)
599 if not test_start:
600 test_duration = 0
601 else:
602 test_duration = self._time() - test_start
603 self.reportTest(test_id, test_duration)
604
572 def stopTest(self, test):605 def stopTest(self, test):
573 test_duration = self._time() - self._start_time606 test_duration = self._time() - self._start_time
574 self.reportTest(self._test, test_duration)607 self.reportTest(self._test.id(), test_duration)
575608
576 def time(self, time):609 def time(self, time):
577 self.__time = time610 self.__time = time
@@ -583,6 +616,10 @@
583 "Tells whether or not this result was a success"616 "Tells whether or not this result was a success"
584 return self.failed_tests == 0617 return self.failed_tests == 0
585618
619 def stopTestRun(self):
620 for test_id in list(self._active_tests.keys()):
621 self._end_test(test_id)
622
586623
587class TestByTestResult(testtools.TestResult):624class TestByTestResult(testtools.TestResult):
588 """Call something every time a test completes."""625 """Call something every time a test completes."""
@@ -676,3 +713,17 @@
676 def startTestRun(self):713 def startTestRun(self):
677 super(CsvResult, self).startTestRun()714 super(CsvResult, self).startTestRun()
678 self._write_row(['test', 'status', 'start_time', 'stop_time'])715 self._write_row(['test', 'status', 'start_time', 'stop_time'])
716
717
718class CatFiles(StreamResult):
719 """Cat file attachments received to a stream."""
720
721 def __init__(self, byte_stream):
722 self.stream = subunit.make_stream_binary(byte_stream)
723
724 def status(self, test_id=None, test_status=None, test_tags=None,
725 runnable=True, file_name=None, file_bytes=None, eof=False,
726 mime_type=None, route_code=None, timestamp=None):
727 if file_name is not None:
728 self.stream.write(file_bytes)
729 self.stream.flush()
679730
=== modified file 'python/subunit/tests/__init__.py'
--- python/subunit/tests/__init__.py 2011-11-01 15:59:58 +0000
+++ python/subunit/tests/__init__.py 2013-03-31 05:51:20 +0000
@@ -25,6 +25,7 @@
25 test_subunit_tags,25 test_subunit_tags,
26 test_tap2subunit,26 test_tap2subunit,
27 test_test_protocol,27 test_test_protocol,
28 test_test_protocol2,
28 test_test_results,29 test_test_results,
29 )30 )
3031
@@ -35,6 +36,7 @@
35 result.addTest(test_progress_model.test_suite())36 result.addTest(test_progress_model.test_suite())
36 result.addTest(test_test_results.test_suite())37 result.addTest(test_test_results.test_suite())
37 result.addTest(test_test_protocol.test_suite())38 result.addTest(test_test_protocol.test_suite())
39 result.addTest(test_test_protocol2.test_suite())
38 result.addTest(test_tap2subunit.test_suite())40 result.addTest(test_tap2subunit.test_suite())
39 result.addTest(test_subunit_filter.test_suite())41 result.addTest(test_subunit_filter.test_suite())
40 result.addTest(test_subunit_tags.test_suite())42 result.addTest(test_subunit_tags.test_suite())
4143
=== modified file 'python/subunit/tests/test_run.py'
--- python/subunit/tests/test_run.py 2012-05-07 19:36:05 +0000
+++ python/subunit/tests/test_run.py 2013-03-31 05:51:20 +0000
@@ -18,6 +18,7 @@
18import unittest18import unittest
1919
20from testtools import PlaceHolder20from testtools import PlaceHolder
21from testtools.testresult.doubles import StreamResult
2122
22import subunit23import subunit
23from subunit.run import SubunitTestRunner24from subunit.run import SubunitTestRunner
@@ -29,16 +30,6 @@
29 return result30 return result
3031
3132
32class TimeCollectingTestResult(unittest.TestResult):
33
34 def __init__(self, *args, **kwargs):
35 super(TimeCollectingTestResult, self).__init__(*args, **kwargs)
36 self.time_called = []
37
38 def time(self, a_time):
39 self.time_called.append(a_time)
40
41
42class TestSubunitTestRunner(unittest.TestCase):33class TestSubunitTestRunner(unittest.TestCase):
4334
44 def test_includes_timing_output(self):35 def test_includes_timing_output(self):
@@ -46,7 +37,24 @@
46 runner = SubunitTestRunner(stream=io)37 runner = SubunitTestRunner(stream=io)
47 test = PlaceHolder('name')38 test = PlaceHolder('name')
48 runner.run(test)39 runner.run(test)
49 client = TimeCollectingTestResult()40 io.seek(0)
50 io.seek(0)41 eventstream = StreamResult()
51 subunit.TestProtocolServer(client).readFrom(io)42 subunit.ByteStreamToStreamResult(io).run(eventstream)
52 self.assertTrue(len(client.time_called) > 0)43 timestamps = [event[-1] for event in eventstream._events
44 if event is not None]
45 self.assertNotEqual([], timestamps)
46
47 def test_enumerates_tests_before_run(self):
48 io = BytesIO()
49 runner = SubunitTestRunner(stream=io)
50 test1 = PlaceHolder('name1')
51 test2 = PlaceHolder('name2')
52 case = unittest.TestSuite([test1, test2])
53 runner.run(case)
54 io.seek(0)
55 eventstream = StreamResult()
56 subunit.ByteStreamToStreamResult(io).run(eventstream)
57 self.assertEqual([
58 ('status', 'name1', 'exists'),
59 ('status', 'name2', 'exists'),
60 ], [event[:3] for event in eventstream._events[:2]])
5361
=== modified file 'python/subunit/tests/test_subunit_filter.py'
--- python/subunit/tests/test_subunit_filter.py 2012-05-07 22:53:53 +0000
+++ python/subunit/tests/test_subunit_filter.py 2013-03-31 05:51:20 +0000
@@ -25,10 +25,11 @@
2525
26from testtools import TestCase26from testtools import TestCase
27from testtools.compat import _b, BytesIO27from testtools.compat import _b, BytesIO
28from testtools.testresult.doubles import ExtendedTestResult28from testtools.testresult.doubles import ExtendedTestResult, StreamResult
2929
30import subunit30import subunit
31from subunit.test_results import make_tag_filter, TestResultFilter31from subunit.test_results import make_tag_filter, TestResultFilter
32from subunit import ByteStreamToStreamResult, StreamResultToBytes
3233
3334
34class TestTestResultFilter(TestCase):35class TestTestResultFilter(TestCase):
@@ -286,23 +287,6 @@
286287
287class TestFilterCommand(TestCase):288class TestFilterCommand(TestCase):
288289
289 example_subunit_stream = _b("""\
290tags: global
291test passed
292success passed
293test failed
294tags: local
295failure failed
296test error
297error error [
298error details
299]
300test skipped
301skip skipped
302test todo
303xfail todo
304""")
305
306 def run_command(self, args, stream):290 def run_command(self, args, stream):
307 root = os.path.dirname(291 root = os.path.dirname(
308 os.path.dirname(os.path.dirname(os.path.dirname(__file__))))292 os.path.dirname(os.path.dirname(os.path.dirname(__file__))))
@@ -316,52 +300,50 @@
316 raise RuntimeError("%s failed: %s" % (command, err))300 raise RuntimeError("%s failed: %s" % (command, err))
317 return out301 return out
318302
319 def to_events(self, stream):
320 test = subunit.ProtocolTestCase(BytesIO(stream))
321 result = ExtendedTestResult()
322 test.run(result)
323 return result._events
324
325 def test_default(self):303 def test_default(self):
326 output = self.run_command([], _b(304 byte_stream = BytesIO()
327 "test: foo\n"305 stream = StreamResultToBytes(byte_stream)
328 "skip: foo\n"306 stream.status(test_id="foo", test_status="inprogress")
329 ))307 stream.status(test_id="foo", test_status="skip")
330 events = self.to_events(output)308 output = self.run_command([], byte_stream.getvalue())
331 foo = subunit.RemotedTestCase('foo')309 events = StreamResult()
332 self.assertEqual(310 ByteStreamToStreamResult(BytesIO(output)).run(events)
333 [('startTest', foo),311 ids = set(event[1] for event in events._events)
334 ('addSkip', foo, {}),312 self.assertEqual([
335 ('stopTest', foo)],313 ('status', 'foo', 'inprogress'),
336 events)314 ('status', 'foo', 'skip'),
315 ], [event[:3] for event in events._events])
337316
338 def test_tags(self):317 def test_tags(self):
339 output = self.run_command(['-s', '--with-tag', 'a'], _b(318 byte_stream = BytesIO()
340 "tags: a\n"319 stream = StreamResultToBytes(byte_stream)
341 "test: foo\n"320 stream.status(
342 "success: foo\n"321 test_id="foo", test_status="inprogress", test_tags=set(["a"]))
343 "tags: -a\n"322 stream.status(
344 "test: bar\n"323 test_id="foo", test_status="success", test_tags=set(["a"]))
345 "success: bar\n"324 stream.status(test_id="bar", test_status="inprogress")
346 "test: baz\n"325 stream.status(test_id="bar", test_status="inprogress")
347 "tags: a\n"326 stream.status(
348 "success: baz\n"327 test_id="baz", test_status="inprogress", test_tags=set(["a"]))
349 ))328 stream.status(
350 events = self.to_events(output)329 test_id="baz", test_status="success", test_tags=set(["a"]))
351 foo = subunit.RemotedTestCase('foo')330 output = self.run_command(
352 baz = subunit.RemotedTestCase('baz')331 ['-s', '--with-tag', 'a'], byte_stream.getvalue())
353 self.assertEqual(332 events = StreamResult()
354 [('tags', set(['a']), set()),333 ByteStreamToStreamResult(BytesIO(output)).run(events)
355 ('startTest', foo),334 ids = set(event[1] for event in events._events)
356 ('addSuccess', foo),335 self.assertEqual(set(['foo', 'baz']), ids)
357 ('stopTest', foo),336
358 ('tags', set(), set(['a'])),337 def test_no_passthrough(self):
359 ('startTest', baz),338 output = self.run_command(['--no-passthrough'], b'hi thar')
360 ('tags', set(['a']), set()),339 self.assertEqual(b'', output)
361 ('addSuccess', baz),340
362 ('stopTest', baz),341 def test_passthrough(self):
363 ],342 output = self.run_command([], b'hi thar')
364 events)343 byte_stream = BytesIO()
344 stream = StreamResultToBytes(byte_stream)
345 stream.status(file_name="stdout", file_bytes=b'hi thar')
346 self.assertEqual(byte_stream.getvalue(), output)
365347
366348
367def test_suite():349def test_suite():
368350
=== modified file 'python/subunit/tests/test_subunit_tags.py'
--- python/subunit/tests/test_subunit_tags.py 2011-04-24 21:40:52 +0000
+++ python/subunit/tests/test_subunit_tags.py 2013-03-31 05:51:20 +0000
@@ -16,10 +16,9 @@
1616
17"""Tests for subunit.tag_stream."""17"""Tests for subunit.tag_stream."""
1818
19from io import BytesIO
19import unittest20import unittest
2021
21from testtools.compat import StringIO
22
23import subunit22import subunit
24import subunit.test_results23import subunit.test_results
2524
@@ -27,40 +26,42 @@
27class TestSubUnitTags(unittest.TestCase):26class TestSubUnitTags(unittest.TestCase):
2827
29 def setUp(self):28 def setUp(self):
30 self.original = StringIO()29 self.original = BytesIO()
31 self.filtered = StringIO()30 self.filtered = BytesIO()
3231
33 def test_add_tag(self):32 def test_add_tag(self):
34 self.original.write("tags: foo\n")33 reference = BytesIO()
35 self.original.write("test: test\n")34 stream = subunit.StreamResultToBytes(reference)
36 self.original.write("tags: bar -quux\n")35 stream.status(
37 self.original.write("success: test\n")36 test_id='test', test_status='inprogress', test_tags=set(['quux', 'foo']))
37 stream.status(
38 test_id='test', test_status='success', test_tags=set(['bar', 'quux', 'foo']))
39 stream = subunit.StreamResultToBytes(self.original)
40 stream.status(
41 test_id='test', test_status='inprogress', test_tags=set(['foo']))
42 stream.status(
43 test_id='test', test_status='success', test_tags=set(['foo', 'bar']))
38 self.original.seek(0)44 self.original.seek(0)
39 result = subunit.tag_stream(self.original, self.filtered, ["quux"])45 self.assertEqual(
40 self.assertEqual([46 0, subunit.tag_stream(self.original, self.filtered, ["quux"]))
41 "tags: quux",47 self.assertEqual(reference.getvalue(), self.filtered.getvalue())
42 "tags: foo",
43 "test: test",
44 "tags: bar",
45 "success: test",
46 ],
47 self.filtered.getvalue().splitlines())
4848
49 def test_remove_tag(self):49 def test_remove_tag(self):
50 self.original.write("tags: foo\n")50 reference = BytesIO()
51 self.original.write("test: test\n")51 stream = subunit.StreamResultToBytes(reference)
52 self.original.write("tags: bar -quux\n")52 stream.status(
53 self.original.write("success: test\n")53 test_id='test', test_status='inprogress', test_tags=set(['foo']))
54 stream.status(
55 test_id='test', test_status='success', test_tags=set(['foo']))
56 stream = subunit.StreamResultToBytes(self.original)
57 stream.status(
58 test_id='test', test_status='inprogress', test_tags=set(['foo']))
59 stream.status(
60 test_id='test', test_status='success', test_tags=set(['foo', 'bar']))
54 self.original.seek(0)61 self.original.seek(0)
55 result = subunit.tag_stream(self.original, self.filtered, ["-bar"])62 self.assertEqual(
56 self.assertEqual([63 0, subunit.tag_stream(self.original, self.filtered, ["-bar"]))
57 "tags: -bar",64 self.assertEqual(reference.getvalue(), self.filtered.getvalue())
58 "tags: foo",
59 "test: test",
60 "tags: -quux",
61 "success: test",
62 ],
63 self.filtered.getvalue().splitlines())
6465
6566
66def test_suite():67def test_suite():
6768
=== modified file 'python/subunit/tests/test_tap2subunit.py'
--- python/subunit/tests/test_tap2subunit.py 2011-04-24 21:40:52 +0000
+++ python/subunit/tests/test_tap2subunit.py 2013-03-31 05:51:20 +0000
@@ -16,14 +16,19 @@
1616
17"""Tests for TAP2SubUnit."""17"""Tests for TAP2SubUnit."""
1818
19from io import BytesIO, StringIO
19import unittest20import unittest
2021
21from testtools.compat import StringIO22from testtools import TestCase
23from testtools.compat import _u
24from testtools.testresult.doubles import StreamResult
2225
23import subunit26import subunit
2427
2528UTF8_TEXT = 'text/plain; charset=UTF8'
26class TestTAP2SubUnit(unittest.TestCase):29
30
31class TestTAP2SubUnit(TestCase):
27 """Tests for TAP2SubUnit.32 """Tests for TAP2SubUnit.
2833
29 These tests test TAP string data in, and subunit string data out.34 These tests test TAP string data in, and subunit string data out.
@@ -34,24 +39,21 @@
34 """39 """
3540
36 def setUp(self):41 def setUp(self):
42 super(TestTAP2SubUnit, self).setUp()
37 self.tap = StringIO()43 self.tap = StringIO()
38 self.subunit = StringIO()44 self.subunit = BytesIO()
3945
40 def test_skip_entire_file(self):46 def test_skip_entire_file(self):
41 # A file47 # A file
42 # 1..- # Skipped: comment48 # 1..- # Skipped: comment
43 # results in a single skipped test.49 # results in a single skipped test.
44 self.tap.write("1..0 # Skipped: entire file skipped\n")50 self.tap.write(_u("1..0 # Skipped: entire file skipped\n"))
45 self.tap.seek(0)51 self.tap.seek(0)
46 result = subunit.TAP2SubUnit(self.tap, self.subunit)52 result = subunit.TAP2SubUnit(self.tap, self.subunit)
47 self.assertEqual(0, result)53 self.assertEqual(0, result)
48 self.assertEqual([54 self.check_events([('status', 'file skip', 'skip', None, True,
49 "test file skip",55 'tap comment', b'Skipped: entire file skipped', True, None, None,
50 "skip file skip [",56 None)])
51 "Skipped: entire file skipped",
52 "]",
53 ],
54 self.subunit.getvalue().splitlines())
5557
56 def test_ok_test_pass(self):58 def test_ok_test_pass(self):
57 # A file59 # A file
@@ -59,164 +61,128 @@
59 # results in a passed test with name 'test 1' (a synthetic name as tap61 # results in a passed test with name 'test 1' (a synthetic name as tap
60 # does not require named fixtures - it is the first test in the tap62 # does not require named fixtures - it is the first test in the tap
61 # stream).63 # stream).
62 self.tap.write("ok\n")64 self.tap.write(_u("ok\n"))
63 self.tap.seek(0)65 self.tap.seek(0)
64 result = subunit.TAP2SubUnit(self.tap, self.subunit)66 result = subunit.TAP2SubUnit(self.tap, self.subunit)
65 self.assertEqual(0, result)67 self.assertEqual(0, result)
66 self.assertEqual([68 self.check_events([('status', 'test 1', 'success', None, False, None,
67 "test test 1",69 None, True, None, None, None)])
68 "success test 1",
69 ],
70 self.subunit.getvalue().splitlines())
7170
72 def test_ok_test_number_pass(self):71 def test_ok_test_number_pass(self):
73 # A file72 # A file
74 # ok 173 # ok 1
75 # results in a passed test with name 'test 1'74 # results in a passed test with name 'test 1'
76 self.tap.write("ok 1\n")75 self.tap.write(_u("ok 1\n"))
77 self.tap.seek(0)76 self.tap.seek(0)
78 result = subunit.TAP2SubUnit(self.tap, self.subunit)77 result = subunit.TAP2SubUnit(self.tap, self.subunit)
79 self.assertEqual(0, result)78 self.assertEqual(0, result)
80 self.assertEqual([79 self.check_events([('status', 'test 1', 'success', None, False, None,
81 "test test 1",80 None, True, None, None, None)])
82 "success test 1",
83 ],
84 self.subunit.getvalue().splitlines())
8581
86 def test_ok_test_number_description_pass(self):82 def test_ok_test_number_description_pass(self):
87 # A file83 # A file
88 # ok 1 - There is a description84 # ok 1 - There is a description
89 # results in a passed test with name 'test 1 - There is a description'85 # results in a passed test with name 'test 1 - There is a description'
90 self.tap.write("ok 1 - There is a description\n")86 self.tap.write(_u("ok 1 - There is a description\n"))
91 self.tap.seek(0)87 self.tap.seek(0)
92 result = subunit.TAP2SubUnit(self.tap, self.subunit)88 result = subunit.TAP2SubUnit(self.tap, self.subunit)
93 self.assertEqual(0, result)89 self.assertEqual(0, result)
94 self.assertEqual([90 self.check_events([('status', 'test 1 - There is a description',
95 "test test 1 - There is a description",91 'success', None, False, None, None, True, None, None, None)])
96 "success test 1 - There is a description",
97 ],
98 self.subunit.getvalue().splitlines())
9992
100 def test_ok_test_description_pass(self):93 def test_ok_test_description_pass(self):
101 # A file94 # A file
102 # ok There is a description95 # ok There is a description
103 # results in a passed test with name 'test 1 There is a description'96 # results in a passed test with name 'test 1 There is a description'
104 self.tap.write("ok There is a description\n")97 self.tap.write(_u("ok There is a description\n"))
105 self.tap.seek(0)98 self.tap.seek(0)
106 result = subunit.TAP2SubUnit(self.tap, self.subunit)99 result = subunit.TAP2SubUnit(self.tap, self.subunit)
107 self.assertEqual(0, result)100 self.assertEqual(0, result)
108 self.assertEqual([101 self.check_events([('status', 'test 1 There is a description',
109 "test test 1 There is a description",102 'success', None, False, None, None, True, None, None, None)])
110 "success test 1 There is a description",
111 ],
112 self.subunit.getvalue().splitlines())
113103
114 def test_ok_SKIP_skip(self):104 def test_ok_SKIP_skip(self):
115 # A file105 # A file
116 # ok # SKIP106 # ok # SKIP
117 # results in a skkip test with name 'test 1'107 # results in a skkip test with name 'test 1'
118 self.tap.write("ok # SKIP\n")108 self.tap.write(_u("ok # SKIP\n"))
119 self.tap.seek(0)109 self.tap.seek(0)
120 result = subunit.TAP2SubUnit(self.tap, self.subunit)110 result = subunit.TAP2SubUnit(self.tap, self.subunit)
121 self.assertEqual(0, result)111 self.assertEqual(0, result)
122 self.assertEqual([112 self.check_events([('status', 'test 1', 'skip', None, False, None,
123 "test test 1",113 None, True, None, None, None)])
124 "skip test 1",
125 ],
126 self.subunit.getvalue().splitlines())
127114
128 def test_ok_skip_number_comment_lowercase(self):115 def test_ok_skip_number_comment_lowercase(self):
129 self.tap.write("ok 1 # skip no samba environment available, skipping compilation\n")116 self.tap.write(_u("ok 1 # skip no samba environment available, skipping compilation\n"))
130 self.tap.seek(0)117 self.tap.seek(0)
131 result = subunit.TAP2SubUnit(self.tap, self.subunit)118 result = subunit.TAP2SubUnit(self.tap, self.subunit)
132 self.assertEqual(0, result)119 self.assertEqual(0, result)
133 self.assertEqual([120 self.check_events([('status', 'test 1', 'skip', None, False, 'tap comment',
134 "test test 1",121 b'no samba environment available, skipping compilation', True,
135 "skip test 1 [", 122 'text/plain; charset=UTF8', None, None)])
136 "no samba environment available, skipping compilation",
137 "]"
138 ],
139 self.subunit.getvalue().splitlines())
140123
141 def test_ok_number_description_SKIP_skip_comment(self):124 def test_ok_number_description_SKIP_skip_comment(self):
142 # A file125 # A file
143 # ok 1 foo # SKIP Not done yet126 # ok 1 foo # SKIP Not done yet
144 # results in a skip test with name 'test 1 foo' and a log of127 # results in a skip test with name 'test 1 foo' and a log of
145 # Not done yet128 # Not done yet
146 self.tap.write("ok 1 foo # SKIP Not done yet\n")129 self.tap.write(_u("ok 1 foo # SKIP Not done yet\n"))
147 self.tap.seek(0)130 self.tap.seek(0)
148 result = subunit.TAP2SubUnit(self.tap, self.subunit)131 result = subunit.TAP2SubUnit(self.tap, self.subunit)
149 self.assertEqual(0, result)132 self.assertEqual(0, result)
150 self.assertEqual([133 self.check_events([('status', 'test 1 foo', 'skip', None, False,
151 "test test 1 foo",134 'tap comment', b'Not done yet', True, 'text/plain; charset=UTF8',
152 "skip test 1 foo [",135 None, None)])
153 "Not done yet",
154 "]",
155 ],
156 self.subunit.getvalue().splitlines())
157136
158 def test_ok_SKIP_skip_comment(self):137 def test_ok_SKIP_skip_comment(self):
159 # A file138 # A file
160 # ok # SKIP Not done yet139 # ok # SKIP Not done yet
161 # results in a skip test with name 'test 1' and a log of Not done yet140 # results in a skip test with name 'test 1' and a log of Not done yet
162 self.tap.write("ok # SKIP Not done yet\n")141 self.tap.write(_u("ok # SKIP Not done yet\n"))
163 self.tap.seek(0)142 self.tap.seek(0)
164 result = subunit.TAP2SubUnit(self.tap, self.subunit)143 result = subunit.TAP2SubUnit(self.tap, self.subunit)
165 self.assertEqual(0, result)144 self.assertEqual(0, result)
166 self.assertEqual([145 self.check_events([('status', 'test 1', 'skip', None, False,
167 "test test 1",146 'tap comment', b'Not done yet', True, 'text/plain; charset=UTF8',
168 "skip test 1 [",147 None, None)])
169 "Not done yet",
170 "]",
171 ],
172 self.subunit.getvalue().splitlines())
173148
174 def test_ok_TODO_xfail(self):149 def test_ok_TODO_xfail(self):
175 # A file150 # A file
176 # ok # TODO151 # ok # TODO
177 # results in a xfail test with name 'test 1'152 # results in a xfail test with name 'test 1'
178 self.tap.write("ok # TODO\n")153 self.tap.write(_u("ok # TODO\n"))
179 self.tap.seek(0)154 self.tap.seek(0)
180 result = subunit.TAP2SubUnit(self.tap, self.subunit)155 result = subunit.TAP2SubUnit(self.tap, self.subunit)
181 self.assertEqual(0, result)156 self.assertEqual(0, result)
182 self.assertEqual([157 self.check_events([('status', 'test 1', 'xfail', None, False, None,
183 "test test 1",158 None, True, None, None, None)])
184 "xfail test 1",
185 ],
186 self.subunit.getvalue().splitlines())
187159
188 def test_ok_TODO_xfail_comment(self):160 def test_ok_TODO_xfail_comment(self):
189 # A file161 # A file
190 # ok # TODO Not done yet162 # ok # TODO Not done yet
191 # results in a xfail test with name 'test 1' and a log of Not done yet163 # results in a xfail test with name 'test 1' and a log of Not done yet
192 self.tap.write("ok # TODO Not done yet\n")164 self.tap.write(_u("ok # TODO Not done yet\n"))
193 self.tap.seek(0)165 self.tap.seek(0)
194 result = subunit.TAP2SubUnit(self.tap, self.subunit)166 result = subunit.TAP2SubUnit(self.tap, self.subunit)
195 self.assertEqual(0, result)167 self.assertEqual(0, result)
196 self.assertEqual([168 self.check_events([('status', 'test 1', 'xfail', None, False,
197 "test test 1",169 'tap comment', b'Not done yet', True, 'text/plain; charset=UTF8',
198 "xfail test 1 [",170 None, None)])
199 "Not done yet",
200 "]",
201 ],
202 self.subunit.getvalue().splitlines())
203171
204 def test_bail_out_errors(self):172 def test_bail_out_errors(self):
205 # A file with line in it173 # A file with line in it
206 # Bail out! COMMENT174 # Bail out! COMMENT
207 # is treated as an error175 # is treated as an error
208 self.tap.write("ok 1 foo\n")176 self.tap.write(_u("ok 1 foo\n"))
209 self.tap.write("Bail out! Lifejacket engaged\n")177 self.tap.write(_u("Bail out! Lifejacket engaged\n"))
210 self.tap.seek(0)178 self.tap.seek(0)
211 result = subunit.TAP2SubUnit(self.tap, self.subunit)179 result = subunit.TAP2SubUnit(self.tap, self.subunit)
212 self.assertEqual(0, result)180 self.assertEqual(0, result)
213 self.assertEqual([181 self.check_events([
214 "test test 1 foo",182 ('status', 'test 1 foo', 'success', None, False, None, None, True,
215 "success test 1 foo",183 None, None, None),
216 "test Bail out! Lifejacket engaged",184 ('status', 'Bail out! Lifejacket engaged', 'fail', None, False,
217 "error Bail out! Lifejacket engaged",185 None, None, True, None, None, None)])
218 ],
219 self.subunit.getvalue().splitlines())
220186
221 def test_missing_test_at_end_with_plan_adds_error(self):187 def test_missing_test_at_end_with_plan_adds_error(self):
222 # A file188 # A file
@@ -224,23 +190,20 @@
224 # ok first test190 # ok first test
225 # not ok third test191 # not ok third test
226 # results in three tests, with the third being created192 # results in three tests, with the third being created
227 self.tap.write('1..3\n')193 self.tap.write(_u('1..3\n'))
228 self.tap.write('ok first test\n')194 self.tap.write(_u('ok first test\n'))
229 self.tap.write('not ok second test\n')195 self.tap.write(_u('not ok second test\n'))
230 self.tap.seek(0)196 self.tap.seek(0)
231 result = subunit.TAP2SubUnit(self.tap, self.subunit)197 result = subunit.TAP2SubUnit(self.tap, self.subunit)
232 self.assertEqual(0, result)198 self.assertEqual(0, result)
233 self.assertEqual([199 self.check_events([
234 'test test 1 first test',200 ('status', 'test 1 first test', 'success', None, False, None,
235 'success test 1 first test',201 None, True, None, None, None),
236 'test test 2 second test',202 ('status', 'test 2 second test', 'fail', None, False, None, None,
237 'failure test 2 second test',203 True, None, None, None),
238 'test test 3',204 ('status', 'test 3', 'fail', None, False, 'tap meta',
239 'error test 3 [',205 b'test missing from TAP output', True, 'text/plain; charset=UTF8',
240 'test missing from TAP output',206 None, None)])
241 ']',
242 ],
243 self.subunit.getvalue().splitlines())
244207
245 def test_missing_test_with_plan_adds_error(self):208 def test_missing_test_with_plan_adds_error(self):
246 # A file209 # A file
@@ -248,45 +211,39 @@
248 # ok first test211 # ok first test
249 # not ok 3 third test212 # not ok 3 third test
250 # results in three tests, with the second being created213 # results in three tests, with the second being created
251 self.tap.write('1..3\n')214 self.tap.write(_u('1..3\n'))
252 self.tap.write('ok first test\n')215 self.tap.write(_u('ok first test\n'))
253 self.tap.write('not ok 3 third test\n')216 self.tap.write(_u('not ok 3 third test\n'))
254 self.tap.seek(0)217 self.tap.seek(0)
255 result = subunit.TAP2SubUnit(self.tap, self.subunit)218 result = subunit.TAP2SubUnit(self.tap, self.subunit)
256 self.assertEqual(0, result)219 self.assertEqual(0, result)
257 self.assertEqual([220 self.check_events([
258 'test test 1 first test',221 ('status', 'test 1 first test', 'success', None, False, None, None,
259 'success test 1 first test',222 True, None, None, None),
260 'test test 2',223 ('status', 'test 2', 'fail', None, False, 'tap meta',
261 'error test 2 [',224 b'test missing from TAP output', True, 'text/plain; charset=UTF8',
262 'test missing from TAP output',225 None, None),
263 ']',226 ('status', 'test 3 third test', 'fail', None, False, None, None,
264 'test test 3 third test',227 True, None, None, None)])
265 'failure test 3 third test',
266 ],
267 self.subunit.getvalue().splitlines())
268228
269 def test_missing_test_no_plan_adds_error(self):229 def test_missing_test_no_plan_adds_error(self):
270 # A file230 # A file
271 # ok first test231 # ok first test
272 # not ok 3 third test232 # not ok 3 third test
273 # results in three tests, with the second being created233 # results in three tests, with the second being created
274 self.tap.write('ok first test\n')234 self.tap.write(_u('ok first test\n'))
275 self.tap.write('not ok 3 third test\n')235 self.tap.write(_u('not ok 3 third test\n'))
276 self.tap.seek(0)236 self.tap.seek(0)
277 result = subunit.TAP2SubUnit(self.tap, self.subunit)237 result = subunit.TAP2SubUnit(self.tap, self.subunit)
278 self.assertEqual(0, result)238 self.assertEqual(0, result)
279 self.assertEqual([239 self.check_events([
280 'test test 1 first test',240 ('status', 'test 1 first test', 'success', None, False, None, None,
281 'success test 1 first test',241 True, None, None, None),
282 'test test 2',242 ('status', 'test 2', 'fail', None, False, 'tap meta',
283 'error test 2 [',243 b'test missing from TAP output', True, 'text/plain; charset=UTF8',
284 'test missing from TAP output',244 None, None),
285 ']',245 ('status', 'test 3 third test', 'fail', None, False, None, None,
286 'test test 3 third test',246 True, None, None, None)])
287 'failure test 3 third test',
288 ],
289 self.subunit.getvalue().splitlines())
290247
291 def test_four_tests_in_a_row_trailing_plan(self):248 def test_four_tests_in_a_row_trailing_plan(self):
292 # A file249 # A file
@@ -296,25 +253,23 @@
296 # not ok 4 - fourth253 # not ok 4 - fourth
297 # 1..4254 # 1..4
298 # results in four tests numbered and named255 # results in four tests numbered and named
299 self.tap.write('ok 1 - first test in a script with trailing plan\n')256 self.tap.write(_u('ok 1 - first test in a script with trailing plan\n'))
300 self.tap.write('not ok 2 - second\n')257 self.tap.write(_u('not ok 2 - second\n'))
301 self.tap.write('ok 3 - third\n')258 self.tap.write(_u('ok 3 - third\n'))
302 self.tap.write('not ok 4 - fourth\n')259 self.tap.write(_u('not ok 4 - fourth\n'))
303 self.tap.write('1..4\n')260 self.tap.write(_u('1..4\n'))
304 self.tap.seek(0)261 self.tap.seek(0)
305 result = subunit.TAP2SubUnit(self.tap, self.subunit)262 result = subunit.TAP2SubUnit(self.tap, self.subunit)
306 self.assertEqual(0, result)263 self.assertEqual(0, result)
307 self.assertEqual([264 self.check_events([
308 'test test 1 - first test in a script with trailing plan',265 ('status', 'test 1 - first test in a script with trailing plan',
309 'success test 1 - first test in a script with trailing plan',266 'success', None, False, None, None, True, None, None, None),
310 'test test 2 - second',267 ('status', 'test 2 - second', 'fail', None, False, None, None,
311 'failure test 2 - second',268 True, None, None, None),
312 'test test 3 - third',269 ('status', 'test 3 - third', 'success', None, False, None, None,
313 'success test 3 - third',270 True, None, None, None),
314 'test test 4 - fourth',271 ('status', 'test 4 - fourth', 'fail', None, False, None, None,
315 'failure test 4 - fourth'272 True, None, None, None)])
316 ],
317 self.subunit.getvalue().splitlines())
318273
319 def test_four_tests_in_a_row_with_plan(self):274 def test_four_tests_in_a_row_with_plan(self):
320 # A file275 # A file
@@ -324,25 +279,23 @@
324 # ok 3 - third279 # ok 3 - third
325 # not ok 4 - fourth280 # not ok 4 - fourth
326 # results in four tests numbered and named281 # results in four tests numbered and named
327 self.tap.write('1..4\n')282 self.tap.write(_u('1..4\n'))
328 self.tap.write('ok 1 - first test in a script with a plan\n')283 self.tap.write(_u('ok 1 - first test in a script with a plan\n'))
329 self.tap.write('not ok 2 - second\n')284 self.tap.write(_u('not ok 2 - second\n'))
330 self.tap.write('ok 3 - third\n')285 self.tap.write(_u('ok 3 - third\n'))
331 self.tap.write('not ok 4 - fourth\n')286 self.tap.write(_u('not ok 4 - fourth\n'))
332 self.tap.seek(0)287 self.tap.seek(0)
333 result = subunit.TAP2SubUnit(self.tap, self.subunit)288 result = subunit.TAP2SubUnit(self.tap, self.subunit)
334 self.assertEqual(0, result)289 self.assertEqual(0, result)
335 self.assertEqual([290 self.check_events([
336 'test test 1 - first test in a script with a plan',291 ('status', 'test 1 - first test in a script with a plan',
337 'success test 1 - first test in a script with a plan',292 'success', None, False, None, None, True, None, None, None),
338 'test test 2 - second',293 ('status', 'test 2 - second', 'fail', None, False, None, None,
339 'failure test 2 - second',294 True, None, None, None),
340 'test test 3 - third',295 ('status', 'test 3 - third', 'success', None, False, None, None,
341 'success test 3 - third',296 True, None, None, None),
342 'test test 4 - fourth',297 ('status', 'test 4 - fourth', 'fail', None, False, None, None,
343 'failure test 4 - fourth'298 True, None, None, None)])
344 ],
345 self.subunit.getvalue().splitlines())
346299
347 def test_four_tests_in_a_row_no_plan(self):300 def test_four_tests_in_a_row_no_plan(self):
348 # A file301 # A file
@@ -351,46 +304,43 @@
351 # ok 3 - third304 # ok 3 - third
352 # not ok 4 - fourth305 # not ok 4 - fourth
353 # results in four tests numbered and named306 # results in four tests numbered and named
354 self.tap.write('ok 1 - first test in a script with no plan at all\n')307 self.tap.write(_u('ok 1 - first test in a script with no plan at all\n'))
355 self.tap.write('not ok 2 - second\n')308 self.tap.write(_u('not ok 2 - second\n'))
356 self.tap.write('ok 3 - third\n')309 self.tap.write(_u('ok 3 - third\n'))
357 self.tap.write('not ok 4 - fourth\n')310 self.tap.write(_u('not ok 4 - fourth\n'))
358 self.tap.seek(0)311 self.tap.seek(0)
359 result = subunit.TAP2SubUnit(self.tap, self.subunit)312 result = subunit.TAP2SubUnit(self.tap, self.subunit)
360 self.assertEqual(0, result)313 self.assertEqual(0, result)
361 self.assertEqual([314 self.check_events([
362 'test test 1 - first test in a script with no plan at all',315 ('status', 'test 1 - first test in a script with no plan at all',
363 'success test 1 - first test in a script with no plan at all',316 'success', None, False, None, None, True, None, None, None),
364 'test test 2 - second',317 ('status', 'test 2 - second', 'fail', None, False, None, None,
365 'failure test 2 - second',318 True, None, None, None),
366 'test test 3 - third',319 ('status', 'test 3 - third', 'success', None, False, None, None,
367 'success test 3 - third',320 True, None, None, None),
368 'test test 4 - fourth',321 ('status', 'test 4 - fourth', 'fail', None, False, None, None,
369 'failure test 4 - fourth'322 True, None, None, None)])
370 ],
371 self.subunit.getvalue().splitlines())
372323
373 def test_todo_and_skip(self):324 def test_todo_and_skip(self):
374 # A file325 # A file
375 # not ok 1 - a fail but # TODO but is TODO326 # not ok 1 - a fail but # TODO but is TODO
376 # not ok 2 - another fail # SKIP instead327 # not ok 2 - another fail # SKIP instead
377 # results in two tests, numbered and commented.328 # results in two tests, numbered and commented.
378 self.tap.write("not ok 1 - a fail but # TODO but is TODO\n")329 self.tap.write(_u("not ok 1 - a fail but # TODO but is TODO\n"))
379 self.tap.write("not ok 2 - another fail # SKIP instead\n")330 self.tap.write(_u("not ok 2 - another fail # SKIP instead\n"))
380 self.tap.seek(0)331 self.tap.seek(0)
381 result = subunit.TAP2SubUnit(self.tap, self.subunit)332 result = subunit.TAP2SubUnit(self.tap, self.subunit)
382 self.assertEqual(0, result)333 self.assertEqual(0, result)
383 self.assertEqual([334 self.subunit.seek(0)
384 'test test 1 - a fail but',335 events = StreamResult()
385 'xfail test 1 - a fail but [',336 subunit.ByteStreamToStreamResult(self.subunit).run(events)
386 'but is TODO',337 self.check_events([
387 ']',338 ('status', 'test 1 - a fail but', 'xfail', None, False,
388 'test test 2 - another fail',339 'tap comment', b'but is TODO', True, 'text/plain; charset=UTF8',
389 'skip test 2 - another fail [',340 None, None),
390 'instead',341 ('status', 'test 2 - another fail', 'skip', None, False,
391 ']',342 'tap comment', b'instead', True, 'text/plain; charset=UTF8',
392 ],343 None, None)])
393 self.subunit.getvalue().splitlines())
394344
395 def test_leading_comments_add_to_next_test_log(self):345 def test_leading_comments_add_to_next_test_log(self):
396 # A file346 # A file
@@ -399,21 +349,17 @@
399 # ok349 # ok
400 # results in a single test with the comment included350 # results in a single test with the comment included
401 # in the first test and not the second.351 # in the first test and not the second.
402 self.tap.write("# comment\n")352 self.tap.write(_u("# comment\n"))
403 self.tap.write("ok\n")353 self.tap.write(_u("ok\n"))
404 self.tap.write("ok\n")354 self.tap.write(_u("ok\n"))
405 self.tap.seek(0)355 self.tap.seek(0)
406 result = subunit.TAP2SubUnit(self.tap, self.subunit)356 result = subunit.TAP2SubUnit(self.tap, self.subunit)
407 self.assertEqual(0, result)357 self.assertEqual(0, result)
408 self.assertEqual([358 self.check_events([
409 'test test 1',359 ('status', 'test 1', 'success', None, False, 'tap comment',
410 'success test 1 [',360 b'# comment', True, 'text/plain; charset=UTF8', None, None),
411 '# comment',361 ('status', 'test 2', 'success', None, False, None, None, True,
412 ']',362 None, None, None)])
413 'test test 2',
414 'success test 2',
415 ],
416 self.subunit.getvalue().splitlines())
417 363
418 def test_trailing_comments_are_included_in_last_test_log(self):364 def test_trailing_comments_are_included_in_last_test_log(self):
419 # A file365 # A file
@@ -422,21 +368,23 @@
422 # # comment368 # # comment
423 # results in a two tests, with the second having the comment369 # results in a two tests, with the second having the comment
424 # attached to its log.370 # attached to its log.
425 self.tap.write("ok\n")371 self.tap.write(_u("ok\n"))
426 self.tap.write("ok\n")372 self.tap.write(_u("ok\n"))
427 self.tap.write("# comment\n")373 self.tap.write(_u("# comment\n"))
428 self.tap.seek(0)374 self.tap.seek(0)
429 result = subunit.TAP2SubUnit(self.tap, self.subunit)375 result = subunit.TAP2SubUnit(self.tap, self.subunit)
430 self.assertEqual(0, result)376 self.assertEqual(0, result)
431 self.assertEqual([377 self.check_events([
432 'test test 1',378 ('status', 'test 1', 'success', None, False, None, None, True,
433 'success test 1',379 None, None, None),
434 'test test 2',380 ('status', 'test 2', 'success', None, False, 'tap comment',
435 'success test 2 [',381 b'# comment', True, 'text/plain; charset=UTF8', None, None)])
436 '# comment',382
437 ']',383 def check_events(self, events):
438 ],384 self.subunit.seek(0)
439 self.subunit.getvalue().splitlines())385 eventstream = StreamResult()
386 subunit.ByteStreamToStreamResult(self.subunit).run(eventstream)
387 self.assertEqual(events, eventstream._events)
440388
441389
442def test_suite():390def test_suite():
443391
=== added file 'python/subunit/tests/test_test_protocol2.py'
--- python/subunit/tests/test_test_protocol2.py 1970-01-01 00:00:00 +0000
+++ python/subunit/tests/test_test_protocol2.py 2013-03-31 05:51:20 +0000
@@ -0,0 +1,415 @@
1#
2# subunit: extensions to Python unittest to get test results from subprocesses.
3# Copyright (C) 2013 Robert Collins <robertc@robertcollins.net>
4#
5# Licensed under either the Apache License, Version 2.0 or the BSD 3-clause
6# license at the users choice. A copy of both licenses are available in the
7# project source as Apache-2.0 and BSD. You may not use this file except in
8# compliance with one of these two licences.
9#
10# Unless required by applicable law or agreed to in writing, software
11# distributed under these licenses is distributed on an "AS IS" BASIS, WITHOUT
12# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
13# license you chose for the specific language governing permissions and
14# limitations under that license.
15#
16
17from io import BytesIO
18import datetime
19
20from testtools import TestCase
21from testtools.matchers import HasLength
22from testtools.tests.test_testresult import TestStreamResultContract
23from testtools.testresult.doubles import StreamResult
24
25import subunit
26import subunit.iso8601 as iso8601
27
28CONSTANT_ENUM = b'\xb3)\x01\x0c\x03foo\x08U_\x1b'
29CONSTANT_INPROGRESS = b'\xb3)\x02\x0c\x03foo\x8e\xc1-\xb5'
30CONSTANT_SUCCESS = b'\xb3)\x03\x0c\x03fooE\x9d\xfe\x10'
31CONSTANT_UXSUCCESS = b'\xb3)\x04\x0c\x03fooX\x98\xce\xa8'
32CONSTANT_SKIP = b'\xb3)\x05\x0c\x03foo\x93\xc4\x1d\r'
33CONSTANT_FAIL = b'\xb3)\x06\x0c\x03foo\x15Po\xa3'
34CONSTANT_XFAIL = b'\xb3)\x07\x0c\x03foo\xde\x0c\xbc\x06'
35CONSTANT_EOF = b'\xb3!\x10\x08S\x15\x88\xdc'
36CONSTANT_FILE_CONTENT = b'\xb3!@\x13\x06barney\x03wooA5\xe3\x8c'
37CONSTANT_MIME = b'\xb3! #\x1aapplication/foo; charset=1x3Q\x15'
38CONSTANT_TIMESTAMP = b'\xb3+\x03\x13<\x17T\xcf\x80\xaf\xc8\x03barI\x96>-'
39CONSTANT_ROUTE_CODE = b'\xb3-\x03\x13\x03bar\x06source\x9cY9\x19'
40CONSTANT_RUNNABLE = b'\xb3(\x03\x0c\x03foo\xe3\xea\xf5\xa4'
41CONSTANT_TAGS = b'\xb3)\x80\x15\x03bar\x02\x03foo\x03barTHn\xb4'
42
43
44class TestStreamResultToBytesContract(TestCase, TestStreamResultContract):
45 """Check that StreamResult behaves as testtools expects."""
46
47 def _make_result(self):
48 return subunit.StreamResultToBytes(BytesIO())
49
50
51class TestStreamResultToBytes(TestCase):
52
53 def _make_result(self):
54 output = BytesIO()
55 return subunit.StreamResultToBytes(output), output
56
57 def test_numbers(self):
58 result = subunit.StreamResultToBytes(BytesIO())
59 packet = []
60 self.assertRaises(Exception, result._write_number, -1, packet)
61 self.assertEqual([], packet)
62 result._write_number(0, packet)
63 self.assertEqual([b'\x00'], packet)
64 del packet[:]
65 result._write_number(63, packet)
66 self.assertEqual([b'\x3f'], packet)
67 del packet[:]
68 result._write_number(64, packet)
69 self.assertEqual([b'\x40\x40'], packet)
70 del packet[:]
71 result._write_number(16383, packet)
72 self.assertEqual([b'\x7f\xff'], packet)
73 del packet[:]
74 result._write_number(16384, packet)
75 self.assertEqual([b'\x80\x40', b'\x00'], packet)
76 del packet[:]
77 result._write_number(4194303, packet)
78 self.assertEqual([b'\xbf\xff', b'\xff'], packet)
79 del packet[:]
80 result._write_number(4194304, packet)
81 self.assertEqual([b'\xc0\x40\x00\x00'], packet)
82 del packet[:]
83 result._write_number(1073741823, packet)
84 self.assertEqual([b'\xff\xff\xff\xff'], packet)
85 del packet[:]
86 self.assertRaises(Exception, result._write_number, 1073741824, packet)
87 self.assertEqual([], packet)
88
89 def test_volatile_length(self):
90 # if the length of the packet data before the length itself is
91 # considered is right on the boundary for length's variable length
92 # encoding, it is easy to get the length wrong by not accounting for
93 # length itself.
94 # that is, the encoder has to ensure that length == sum (length_of_rest
95 # + length_of_length)
96 result, output = self._make_result()
97 # 1 byte short:
98 result.status(file_name="", file_bytes=b'\xff'*0)
99 self.assertThat(output.getvalue(), HasLength(10))
100 self.assertEqual(b'\x0a', output.getvalue()[3:4])
101 output.seek(0)
102 output.truncate()
103 # 1 byte long:
104 result.status(file_name="", file_bytes=b'\xff'*53)
105 self.assertThat(output.getvalue(), HasLength(63))
106 self.assertEqual(b'\x3f', output.getvalue()[3:4])
107 output.seek(0)
108 output.truncate()
109 # 2 bytes short
110 result.status(file_name="", file_bytes=b'\xff'*54)
111 self.assertThat(output.getvalue(), HasLength(65))
112 self.assertEqual(b'\x40\x41', output.getvalue()[3:5])
113 output.seek(0)
114 output.truncate()
115 # 2 bytes long
116 result.status(file_name="", file_bytes=b'\xff'*16371)
117 self.assertThat(output.getvalue(), HasLength(16383))
118 self.assertEqual(b'\x7f\xff', output.getvalue()[3:5])
119 output.seek(0)
120 output.truncate()
121 # 3 bytes short
122 result.status(file_name="", file_bytes=b'\xff'*16372)
123 self.assertThat(output.getvalue(), HasLength(16385))
124 self.assertEqual(b'\x80\x40\x01', output.getvalue()[3:6])
125 output.seek(0)
126 output.truncate()
127 # 3 bytes long
128 result.status(file_name="", file_bytes=b'\xff'*4194289)
129 self.assertThat(output.getvalue(), HasLength(4194303))
130 self.assertEqual(b'\xbf\xff\xff', output.getvalue()[3:6])
131 output.seek(0)
132 output.truncate()
133 self.assertRaises(Exception, result.status, file_name="",
134 file_bytes=b'\xff'*4194290)
135
136 def test_trivial_enumeration(self):
137 result, output = self._make_result()
138 result.status("foo", 'exists')
139 self.assertEqual(CONSTANT_ENUM, output.getvalue())
140
141 def test_inprogress(self):
142 result, output = self._make_result()
143 result.status("foo", 'inprogress')
144 self.assertEqual(CONSTANT_INPROGRESS, output.getvalue())
145
146 def test_success(self):
147 result, output = self._make_result()
148 result.status("foo", 'success')
149 self.assertEqual(CONSTANT_SUCCESS, output.getvalue())
150
151 def test_uxsuccess(self):
152 result, output = self._make_result()
153 result.status("foo", 'uxsuccess')
154 self.assertEqual(CONSTANT_UXSUCCESS, output.getvalue())
155
156 def test_skip(self):
157 result, output = self._make_result()
158 result.status("foo", 'skip')
159 self.assertEqual(CONSTANT_SKIP, output.getvalue())
160
161 def test_fail(self):
162 result, output = self._make_result()
163 result.status("foo", 'fail')
164 self.assertEqual(CONSTANT_FAIL, output.getvalue())
165
166 def test_xfail(self):
167 result, output = self._make_result()
168 result.status("foo", 'xfail')
169 self.assertEqual(CONSTANT_XFAIL, output.getvalue())
170
171 def test_unknown_status(self):
172 result, output = self._make_result()
173 self.assertRaises(Exception, result.status, "foo", 'boo')
174 self.assertEqual(b'', output.getvalue())
175
176 def test_eof(self):
177 result, output = self._make_result()
178 result.status(eof=True)
179 self.assertEqual(CONSTANT_EOF, output.getvalue())
180
181 def test_file_content(self):
182 result, output = self._make_result()
183 result.status(file_name="barney", file_bytes=b"woo")
184 self.assertEqual(CONSTANT_FILE_CONTENT, output.getvalue())
185
186 def test_mime(self):
187 result, output = self._make_result()
188 result.status(mime_type="application/foo; charset=1")
189 self.assertEqual(CONSTANT_MIME, output.getvalue())
190
191 def test_route_code(self):
192 result, output = self._make_result()
193 result.status(test_id="bar", test_status='success',
194 route_code="source")
195 self.assertEqual(CONSTANT_ROUTE_CODE, output.getvalue())
196
197 def test_runnable(self):
198 result, output = self._make_result()
199 result.status("foo", 'success', runnable=False)
200 self.assertEqual(CONSTANT_RUNNABLE, output.getvalue())
201
202 def test_tags(self):
203 result, output = self._make_result()
204 result.status(test_id="bar", test_tags=set(['foo', 'bar']))
205 self.assertEqual(CONSTANT_TAGS, output.getvalue())
206
207 def test_timestamp(self):
208 timestamp = datetime.datetime(2001, 12, 12, 12, 59, 59, 45,
209 iso8601.Utc())
210 result, output = self._make_result()
211 result.status(test_id="bar", test_status='success', timestamp=timestamp)
212 self.assertEqual(CONSTANT_TIMESTAMP, output.getvalue())
213
214
215class TestByteStreamToStreamResult(TestCase):
216
217 def test_non_subunit_encapsulated(self):
218 source = BytesIO(b"foo\nbar\n")
219 result = StreamResult()
220 subunit.ByteStreamToStreamResult(
221 source, non_subunit_name="stdout").run(result)
222 self.assertEqual([
223 ('status', None, None, None, True, 'stdout', b'f', False, None, None, None),
224 ('status', None, None, None, True, 'stdout', b'o', False, None, None, None),
225 ('status', None, None, None, True, 'stdout', b'o', False, None, None, None),
226 ('status', None, None, None, True, 'stdout', b'\n', False, None, None, None),
227 ('status', None, None, None, True, 'stdout', b'b', False, None, None, None),
228 ('status', None, None, None, True, 'stdout', b'a', False, None, None, None),
229 ('status', None, None, None, True, 'stdout', b'r', False, None, None, None),
230 ('status', None, None, None, True, 'stdout', b'\n', False, None, None, None),
231 ], result._events)
232 self.assertEqual(b'', source.read())
233
234 def test_signature_middle_utf8_char(self):
235 utf8_bytes = b'\xe3\xb3\x8a'
236 source = BytesIO(utf8_bytes)
237 # Should be treated as one character (it is u'\u3cca') and wrapped
238 result = StreamResult()
239 subunit.ByteStreamToStreamResult(
240 source, non_subunit_name="stdout").run(
241 result)
242 self.assertEqual([
243 ('status', None, None, None, True, 'stdout', b'\xe3', False, None, None, None),
244 ('status', None, None, None, True, 'stdout', b'\xb3', False, None, None, None),
245 ('status', None, None, None, True, 'stdout', b'\x8a', False, None, None, None),
246 ], result._events)
247
248 def test_non_subunit_disabled_raises(self):
249 source = BytesIO(b"foo\nbar\n")
250 result = StreamResult()
251 case = subunit.ByteStreamToStreamResult(source)
252 e = self.assertRaises(Exception, case.run, result)
253 self.assertEqual(b'f', e.args[1])
254 self.assertEqual(b'oo\nbar\n', source.read())
255 self.assertEqual([], result._events)
256
257 def test_trivial_enumeration(self):
258 source = BytesIO(CONSTANT_ENUM)
259 result = StreamResult()
260 subunit.ByteStreamToStreamResult(
261 source, non_subunit_name="stdout").run(result)
262 self.assertEqual(b'', source.read())
263 self.assertEqual([
264 ('status', 'foo', 'exists', None, True, None, None, False, None, None, None),
265 ], result._events)
266
267 def test_multiple_events(self):
268 source = BytesIO(CONSTANT_ENUM + CONSTANT_ENUM)
269 result = StreamResult()
270 subunit.ByteStreamToStreamResult(
271 source, non_subunit_name="stdout").run(result)
272 self.assertEqual(b'', source.read())
273 self.assertEqual([
274 ('status', 'foo', 'exists', None, True, None, None, False, None, None, None),
275 ('status', 'foo', 'exists', None, True, None, None, False, None, None, None),
276 ], result._events)
277
278 def test_inprogress(self):
279 self.check_event(CONSTANT_INPROGRESS, 'inprogress')
280
281 def test_success(self):
282 self.check_event(CONSTANT_SUCCESS, 'success')
283
284 def test_uxsuccess(self):
285 self.check_event(CONSTANT_UXSUCCESS, 'uxsuccess')
286
287 def test_skip(self):
288 self.check_event(CONSTANT_SKIP, 'skip')
289
290 def test_fail(self):
291 self.check_event(CONSTANT_FAIL, 'fail')
292
293 def test_xfail(self):
294 self.check_event(CONSTANT_XFAIL, 'xfail')
295
296 def check_events(self, source_bytes, events):
297 source = BytesIO(source_bytes)
298 result = StreamResult()
299 subunit.ByteStreamToStreamResult(
300 source, non_subunit_name="stdout").run(result)
301 self.assertEqual(b'', source.read())
302 self.assertEqual(events, result._events)
303
304 def check_event(self, source_bytes, test_status=None, test_id="foo",
305 route_code=None, timestamp=None, tags=None, mime_type=None,
306 file_name=None, file_bytes=None, eof=False, runnable=True):
307 event = self._event(test_id=test_id, test_status=test_status,
308 tags=tags, runnable=runnable, file_name=file_name,
309 file_bytes=file_bytes, eof=eof, mime_type=mime_type,
310 route_code=route_code, timestamp=timestamp)
311 self.check_events(source_bytes, [event])
312
313 def _event(self, test_status=None, test_id=None, route_code=None,
314 timestamp=None, tags=None, mime_type=None, file_name=None,
315 file_bytes=None, eof=False, runnable=True):
316 return ('status', test_id, test_status, tags, runnable, file_name,
317 file_bytes, eof, mime_type, route_code, timestamp)
318
319 def test_eof(self):
320 self.check_event(CONSTANT_EOF, test_id=None, eof=True)
321
322 def test_file_content(self):
323 self.check_event(CONSTANT_FILE_CONTENT,
324 test_id=None, file_name="barney", file_bytes=b"woo")
325
326 def test_file_content_length_into_checksum(self):
327 # A bad file content length which creeps into the checksum.
328 bad_file_length_content = b'\xb3!@\x13\x06barney\x04woo\xdc\xe2\xdb\x35'
329 self.check_events(bad_file_length_content, [
330 self._event(test_id="subunit.parser", eof=True,
331 file_name="Packet data", file_bytes=bad_file_length_content),
332 self._event(test_id="subunit.parser", test_status="fail", eof=True,
333 file_name="Parser Error",
334 file_bytes=b"File content extends past end of packet: claimed 4 bytes, 3 available"),
335 ])
336
337 def test_packet_length_4_word_varint(self):
338 packet_data = b'\xb3!@\xc0\x00\x11'
339 self.check_events(packet_data, [
340 self._event(test_id="subunit.parser", eof=True,
341 file_name="Packet data", file_bytes=packet_data),
342 self._event(test_id="subunit.parser", test_status="fail", eof=True,
343 file_name="Parser Error",
344 file_bytes=b"3 byte maximum given but 4 byte value found."),
345 ])
346
347 def test_mime(self):
348 self.check_event(CONSTANT_MIME,
349 test_id=None, mime_type='application/foo; charset=1')
350
351 def test_route_code(self):
352 self.check_event(CONSTANT_ROUTE_CODE,
353 'success', route_code="source", test_id="bar")
354
355 def test_runnable(self):
356 self.check_event(CONSTANT_RUNNABLE,
357 test_status='success', runnable=False)
358
359 def test_tags(self):
360 self.check_event(CONSTANT_TAGS,
361 None, tags=set(['foo', 'bar']), test_id="bar")
362
363 def test_timestamp(self):
364 timestamp = datetime.datetime(2001, 12, 12, 12, 59, 59, 45,
365 iso8601.Utc())
366 self.check_event(CONSTANT_TIMESTAMP,
367 'success', test_id='bar', timestamp=timestamp)
368
369 def test_bad_crc_errors_via_status(self):
370 file_bytes = CONSTANT_MIME[:-1] + b'\x00'
371 self.check_events( file_bytes, [
372 self._event(test_id="subunit.parser", eof=True,
373 file_name="Packet data", file_bytes=file_bytes),
374 self._event(test_id="subunit.parser", test_status="fail", eof=True,
375 file_name="Parser Error",
376 file_bytes=b'Bad checksum - calculated (0x78335115), '
377 b'stored (0x78335100)'),
378 ])
379
380 def test_not_utf8_in_string(self):
381 file_bytes = CONSTANT_ROUTE_CODE[:5] + b'\xb4' + CONSTANT_ROUTE_CODE[6:-4] + b'\xce\x56\xc6\x17'
382 self.check_events(file_bytes, [
383 self._event(test_id="subunit.parser", eof=True,
384 file_name="Packet data", file_bytes=file_bytes),
385 self._event(test_id="subunit.parser", test_status="fail", eof=True,
386 file_name="Parser Error",
387 file_bytes=b'UTF8 string at offset 2 is not UTF8'),
388 ])
389
390 def test_NULL_in_string(self):
391 file_bytes = CONSTANT_ROUTE_CODE[:6] + b'\x00' + CONSTANT_ROUTE_CODE[7:-4] + b'\xd7\x41\xac\xfe'
392 self.check_events(file_bytes, [
393 self._event(test_id="subunit.parser", eof=True,
394 file_name="Packet data", file_bytes=file_bytes),
395 self._event(test_id="subunit.parser", test_status="fail", eof=True,
396 file_name="Parser Error",
397 file_bytes=b'UTF8 string at offset 2 contains NUL byte'),
398 ])
399
400 def test_bad_utf8_stringlength(self):
401 file_bytes = CONSTANT_ROUTE_CODE[:4] + b'\x3f' + CONSTANT_ROUTE_CODE[5:-4] + b'\xbe\x29\xe0\xc2'
402 self.check_events(file_bytes, [
403 self._event(test_id="subunit.parser", eof=True,
404 file_name="Packet data", file_bytes=file_bytes),
405 self._event(test_id="subunit.parser", test_status="fail", eof=True,
406 file_name="Parser Error",
407 file_bytes=b'UTF8 string at offset 2 extends past end of '
408 b'packet: claimed 63 bytes, 10 available'),
409 ])
410
411
412def test_suite():
413 loader = subunit.tests.TestUtil.TestLoader()
414 result = loader.loadTestsFromName(__name__)
415 return result
0416
=== added file 'python/subunit/v2.py'
--- python/subunit/v2.py 1970-01-01 00:00:00 +0000
+++ python/subunit/v2.py 2013-03-31 05:51:20 +0000
@@ -0,0 +1,458 @@
1#
2# subunit: extensions to Python unittest to get test results from subprocesses.
3# Copyright (C) 2013 Robert Collins <robertc@robertcollins.net>
4#
5# Licensed under either the Apache License, Version 2.0 or the BSD 3-clause
6# license at the users choice. A copy of both licenses are available in the
7# project source as Apache-2.0 and BSD. You may not use this file except in
8# compliance with one of these two licences.
9#
10# Unless required by applicable law or agreed to in writing, software
11# distributed under these licenses is distributed on an "AS IS" BASIS, WITHOUT
12# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
13# license you chose for the specific language governing permissions and
14# limitations under that license.
15#
16
17import codecs
18import datetime
19from io import UnsupportedOperation
20import os
21import select
22import struct
23import zlib
24
25from extras import safe_hasattr
26
27import subunit
28import subunit.iso8601 as iso8601
29
30__all__ = [
31 'ByteStreamToStreamResult',
32 'StreamResultToBytes',
33 ]
34
35SIGNATURE = b'\xb3'
36FMT_8 = '>B'
37FMT_16 = '>H'
38FMT_24 = '>HB'
39FMT_32 = '>I'
40FMT_TIMESTAMP = '>II'
41FLAG_TEST_ID = 0x0800
42FLAG_ROUTE_CODE = 0x0400
43FLAG_TIMESTAMP = 0x0200
44FLAG_RUNNABLE = 0x0100
45FLAG_TAGS = 0x0080
46FLAG_MIME_TYPE = 0x0020
47FLAG_EOF = 0x0010
48FLAG_FILE_CONTENT = 0x0040
49EPOCH = datetime.datetime.utcfromtimestamp(0).replace(tzinfo=iso8601.Utc())
50NUL_ELEMENT = b'\0'[0]
51
52
53class ParseError(Exception):
54 """Used to pass error messages within the parser."""
55
56
57class StreamResultToBytes(object):
58 """Convert StreamResult API calls to bytes.
59
60 The StreamResult API is defined by testtools.StreamResult.
61 """
62
63 status_mask = {
64 None: 0,
65 'exists': 0x1,
66 'inprogress': 0x2,
67 'success': 0x3,
68 'uxsuccess': 0x4,
69 'skip': 0x5,
70 'fail': 0x6,
71 'xfail': 0x7,
72 }
73
74 zero_b = b'\0'[0]
75
76 def __init__(self, output_stream):
77 """Create a StreamResultToBytes with output written to output_stream.
78
79 :param output_stream: A file-like object. Must support write(bytes)
80 and flush() methods. Flush will be called after each write.
81 The stream will be passed through subunit.make_stream_binary,
82 to handle regular cases such as stdout.
83 """
84 self.output_stream = subunit.make_stream_binary(output_stream)
85
86 def startTestRun(self):
87 pass
88
89 def stopTestRun(self):
90 pass
91
92 def status(self, test_id=None, test_status=None, test_tags=None,
93 runnable=True, file_name=None, file_bytes=None, eof=False,
94 mime_type=None, route_code=None, timestamp=None):
95 self._write_packet(test_id=test_id, test_status=test_status,
96 test_tags=test_tags, runnable=runnable, file_name=file_name,
97 file_bytes=file_bytes, eof=eof, mime_type=mime_type,
98 route_code=route_code, timestamp=timestamp)
99
100 def _write_utf8(self, a_string, packet):
101 utf8 = a_string.encode('utf-8')
102 self._write_number(len(utf8), packet)
103 packet.append(utf8)
104
105 def _write_len16(self, length, packet):
106 assert length < 65536
107 packet.append(struct.pack(FMT_16, length))
108
109 def _write_number(self, value, packet):
110 packet.extend(self._encode_number(value))
111
112 def _encode_number(self, value):
113 assert value >= 0
114 if value < 64:
115 return [struct.pack(FMT_8, value)]
116 elif value < 16384:
117 value = value | 0x4000
118 return [struct.pack(FMT_16, value)]
119 elif value < 4194304:
120 value = value | 0x800000
121 return [struct.pack(FMT_16, value >> 8),
122 struct.pack(FMT_8, value & 0xff)]
123 elif value < 1073741824:
124 value = value | 0xc0000000
125 return [struct.pack(FMT_32, value)]
126 else:
127 raise ValueError('value too large to encode: %r' % (value,))
128
129 def _write_packet(self, test_id=None, test_status=None, test_tags=None,
130 runnable=True, file_name=None, file_bytes=None, eof=False,
131 mime_type=None, route_code=None, timestamp=None):
132 packet = [SIGNATURE]
133 packet.append(b'FF') # placeholder for flags
134 # placeholder for length, but see below as length is variable.
135 packet.append(b'')
136 flags = 0x2000 # Version 0x2
137 if timestamp is not None:
138 flags = flags | FLAG_TIMESTAMP
139 since_epoch = timestamp - EPOCH
140 nanoseconds = since_epoch.microseconds * 1000
141 seconds = (since_epoch.seconds + since_epoch.days * 24 * 3600)
142 packet.append(struct.pack(FMT_32, seconds))
143 self._write_number(nanoseconds, packet)
144 if test_id is not None:
145 flags = flags | FLAG_TEST_ID
146 self._write_utf8(test_id, packet)
147 if test_tags:
148 flags = flags | FLAG_TAGS
149 self._write_number(len(test_tags), packet)
150 for tag in test_tags:
151 self._write_utf8(tag, packet)
152 if runnable:
153 flags = flags | FLAG_RUNNABLE
154 if mime_type:
155 flags = flags | FLAG_MIME_TYPE
156 self._write_utf8(mime_type, packet)
157 if file_name is not None:
158 flags = flags | FLAG_FILE_CONTENT
159 self._write_utf8(file_name, packet)
160 self._write_number(len(file_bytes), packet)
161 packet.append(file_bytes)
162 if eof:
163 flags = flags | FLAG_EOF
164 if route_code is not None:
165 flags = flags | FLAG_ROUTE_CODE
166 self._write_utf8(route_code, packet)
167 # 0x0008 - not used in v2.
168 flags = flags | self.status_mask[test_status]
169 packet[1] = struct.pack(FMT_16, flags)
170 base_length = sum(map(len, packet)) + 4
171 if base_length <= 62:
172 # one byte to encode length, 62+1 = 63
173 length_length = 1
174 elif base_length <= 16381:
175 # two bytes to encode length, 16381+2 = 16383
176 length_length = 2
177 elif base_length <= 4194300:
178 # three bytes to encode length, 419430+3=4194303
179 length_length = 3
180 else:
181 # Longer than policy:
182 # TODO: chunk the packet automatically?
183 # - strip all but file data
184 # - do 4M chunks of that till done
185 # - include original data in final chunk.
186 raise ValueError("Length too long: %r" % base_length)
187 packet[2:3] = self._encode_number(base_length + length_length)
188 # We could either do a partial application of crc32 over each chunk
189 # or a single join to a temp variable then a final join
190 # or two writes (that python might then split).
191 # For now, simplest code: join, crc32, join, output
192 content = b''.join(packet)
193 self.output_stream.write(content + struct.pack(
194 FMT_32, zlib.crc32(content) & 0xffffffff))
195 self.output_stream.flush()
196
197
198class ByteStreamToStreamResult(object):
199 """Parse a subunit byte stream.
200
201 Mixed streams that contain non-subunit content is supported when a
202 non_subunit_name is passed to the contructor. The default is to raise an
203 error containing the non-subunit byte after it has been read from the
204 stream.
205
206 Typical use:
207
208 >>> case = ByteStreamToStreamResult(sys.stdin.buffer)
209 >>> result = StreamResult()
210 >>> result.startTestRun()
211 >>> case.run(result)
212 >>> result.stopTestRun()
213 """
214
215 status_lookup = {
216 0x0: None,
217 0x1: 'exists',
218 0x2: 'inprogress',
219 0x3: 'success',
220 0x4: 'uxsuccess',
221 0x5: 'skip',
222 0x6: 'fail',
223 0x7: 'xfail',
224 }
225
226 def __init__(self, source, non_subunit_name=None):
227 """Create a ByteStreamToStreamResult.
228
229 :param source: A file like object to read bytes from. Must support
230 read(<count>) and return bytes. The file is not closed by
231 ByteStreamToStreamResult. subunit.make_stream_binary() is
232 called on the stream to get it into bytes mode.
233 :param non_subunit_name: If set to non-None, non subunit content
234 encountered in the stream will be converted into file packets
235 labelled with this name.
236 """
237 self.non_subunit_name = non_subunit_name
238 self.source = subunit.make_stream_binary(source)
239 self.codec = codecs.lookup('utf8').incrementaldecoder()
240
241 def run(self, result):
242 """Parse source and emit events to result.
243
244 This is a blocking call: it will run until EOF is detected on source.
245 """
246 self.codec.reset()
247 mid_character = False
248 while True:
249 # We're in blocking mode; read one char
250 content = self.source.read(1)
251 if not content:
252 # EOF
253 return
254 if not mid_character and content[0] == SIGNATURE[0]:
255 self._parse_packet(result)
256 continue
257 if self.non_subunit_name is None:
258 raise Exception("Non subunit content", content)
259 try:
260 if self.codec.decode(content):
261 # End of a character
262 mid_character = False
263 else:
264 mid_character = True
265 except UnicodeDecodeError:
266 # Bad unicode, not our concern.
267 mid_character = False
268 # Aggregate all content that is not subunit until either
269 # 1MiB is accumulated or 50ms has passed with no input.
270 # Both are arbitrary amounts intended to give a simple
271 # balance between efficiency (avoiding death by a thousand
272 # one-byte packets), buffering (avoiding overlarge state
273 # being hidden on intermediary nodes) and interactivity
274 # (when driving a debugger, slow response to typing is
275 # annoying).
276 buffered = [content]
277 while len(buffered[-1]):
278 try:
279 self.source.fileno()
280 except:
281 # Won't be able to select, fallback to
282 # one-byte-at-a-time.
283 break
284 # Note: this has a very low timeout because with stdin, the
285 # BufferedIO layer typically has all the content available
286 # from the stream when e.g. pdb is dropped into, leading to
287 # select always timing out when in fact we could have read
288 # (from the buffer layer) - we typically fail to aggregate
289 # any content on 3.x Pythons.
290 readable = select.select([self.source], [], [], 0.000001)[0]
291 if readable:
292 content = self.source.read(1)
293 if not len(content):
294 # EOF - break and emit buffered.
295 break
296 if not mid_character and content[0] == SIGNATURE[0]:
297 # New packet, break, emit buffered, then parse.
298 break
299 buffered.append(content)
300 # Feed into the codec.
301 try:
302 if self.codec.decode(content):
303 # End of a character
304 mid_character = False
305 else:
306 mid_character = True
307 except UnicodeDecodeError:
308 # Bad unicode, not our concern.
309 mid_character = False
310 if not readable or len(buffered) >= 1048576:
311 # timeout or too much data, emit what we have.
312 break
313 result.status(
314 file_name=self.non_subunit_name,
315 file_bytes=b''.join(buffered))
316 if mid_character or not len(content) or content[0] != SIGNATURE[0]:
317 continue
318 # Otherwise, parse a data packet.
319 self._parse_packet(result)
320
321 def _parse_packet(self, result):
322 try:
323 packet = [SIGNATURE]
324 self._parse(packet, result)
325 except ParseError as error:
326 result.status(test_id="subunit.parser", eof=True,
327 file_name="Packet data", file_bytes=b''.join(packet))
328 result.status(test_id="subunit.parser", test_status='fail',
329 eof=True, file_name="Parser Error",
330 file_bytes=(error.args[0]).encode('utf8'))
331
332 def _parse_varint(self, data, pos, max_3_bytes=False):
333 # because the only incremental IO we do is at the start, and the 32 bit
334 # CRC means we can always safely read enough to cover any varint, we
335 # can be sure that there should be enough data - and if not it is an
336 # error not a normal situation.
337 data_0 = struct.unpack(FMT_8, data[pos:pos+1])[0]
338 typeenum = data_0 & 0xc0
339 value_0 = data_0 & 0x3f
340 if typeenum == 0x00:
341 return value_0, 1
342 elif typeenum == 0x40:
343 data_1 = struct.unpack(FMT_8, data[pos+1:pos+2])[0]
344 return (value_0 << 8) | data_1, 2
345 elif typeenum == 0x80:
346 data_1 = struct.unpack(FMT_16, data[pos+1:pos+3])[0]
347 return (value_0 << 16) | data_1, 3
348 else:
349 if max_3_bytes:
350 raise ParseError('3 byte maximum given but 4 byte value found.')
351 data_1, data_2 = struct.unpack(FMT_24, data[pos+1:pos+4])
352 result = (value_0 << 24) | data_1 << 8 | data_2
353 return result, 4
354
355 def _parse(self, packet, result):
356 # 2 bytes flags, at most 3 bytes length.
357 packet.append(self.source.read(5))
358 flags = struct.unpack(FMT_16, packet[-1][:2])[0]
359 length, consumed = self._parse_varint(
360 packet[-1], 2, max_3_bytes=True)
361 remainder = self.source.read(length - 6)
362 if len(remainder) != length - 6:
363 raise ParseError(
364 'Short read - got %d bytes, wanted %d bytes' % (
365 len(remainder), length - 6))
366 if consumed != 3:
367 # Avoid having to parse torn values
368 packet[-1] += remainder
369 pos = 2 + consumed
370 else:
371 # Avoid copying potentially lots of data.
372 packet.append(remainder)
373 pos = 0
374 crc = zlib.crc32(packet[0])
375 for fragment in packet[1:-1]:
376 crc = zlib.crc32(fragment, crc)
377 crc = zlib.crc32(packet[-1][:-4], crc) & 0xffffffff
378 packet_crc = struct.unpack(FMT_32, packet[-1][-4:])[0]
379 if crc != packet_crc:
380 # Bad CRC, report it and stop parsing the packet.
381 raise ParseError(
382 'Bad checksum - calculated (0x%x), stored (0x%x)'
383 % (crc, packet_crc))
384 if safe_hasattr(__builtins__, 'memoryview'):
385 body = memoryview(packet[-1])
386 else:
387 body = packet[-1]
388 # Discard CRC-32
389 body = body[:-4]
390 # One packet could have both file and status data; the Python API
391 # presents these separately (perhaps it shouldn't?)
392 if flags & FLAG_TIMESTAMP:
393 seconds = struct.unpack(FMT_32, body[pos:pos+4])[0]
394 nanoseconds, consumed = self._parse_varint(body, pos+4)
395 pos = pos + 4 + consumed
396 timestamp = EPOCH + datetime.timedelta(
397 seconds=seconds, microseconds=nanoseconds/1000)
398 else:
399 timestamp = None
400 if flags & FLAG_TEST_ID:
401 test_id, pos = self._read_utf8(body, pos)
402 else:
403 test_id = None
404 if flags & FLAG_TAGS:
405 tag_count, consumed = self._parse_varint(body, pos)
406 pos += consumed
407 test_tags = set()
408 for _ in range(tag_count):
409 tag, pos = self._read_utf8(body, pos)
410 test_tags.add(tag)
411 else:
412 test_tags = None
413 if flags & FLAG_MIME_TYPE:
414 mime_type, pos = self._read_utf8(body, pos)
415 else:
416 mime_type = None
417 if flags & FLAG_FILE_CONTENT:
418 file_name, pos = self._read_utf8(body, pos)
419 content_length, consumed = self._parse_varint(body, pos)
420 pos += consumed
421 file_bytes = body[pos:pos+content_length]
422 if len(file_bytes) != content_length:
423 raise ParseError('File content extends past end of packet: '
424 'claimed %d bytes, %d available' % (
425 content_length, len(file_bytes)))
426 else:
427 file_name = None
428 file_bytes = None
429 if flags & FLAG_ROUTE_CODE:
430 route_code, pos = self._read_utf8(body, pos)
431 else:
432 route_code = None
433 runnable = bool(flags & FLAG_RUNNABLE)
434 eof = bool(flags & FLAG_EOF)
435 test_status = self.status_lookup[flags & 0x0007]
436 result.status(test_id=test_id, test_status=test_status,
437 test_tags=test_tags, runnable=runnable, mime_type=mime_type,
438 eof=eof, file_name=file_name, file_bytes=file_bytes,
439 route_code=route_code, timestamp=timestamp)
440 __call__ = run
441
442 def _read_utf8(self, buf, pos):
443 length, consumed = self._parse_varint(buf, pos)
444 pos += consumed
445 utf8_bytes = buf[pos:pos+length]
446 if length != len(utf8_bytes):
447 raise ParseError(
448 'UTF8 string at offset %d extends past end of packet: '
449 'claimed %d bytes, %d available' % (pos - 2, length,
450 len(utf8_bytes)))
451 if NUL_ELEMENT in utf8_bytes:
452 raise ParseError('UTF8 string at offset %d contains NUL byte' % (
453 pos-2,))
454 try:
455 return utf8_bytes.decode('utf-8'), length+pos
456 except UnicodeDecodeError:
457 raise ParseError('UTF8 string at offset %d is not UTF8' % (pos-2,))
458
0459
=== modified file 'setup.py'
--- setup.py 2012-12-17 08:12:44 +0000
+++ setup.py 2013-03-31 05:51:20 +0000
@@ -9,6 +9,7 @@
9else:9else:
10 extra = {10 extra = {
11 'install_requires': [11 'install_requires': [
12 'extras',
12 'testtools>=0.9.23',13 'testtools>=0.9.23',
13 ]14 ]
14 }15 }
@@ -49,6 +50,8 @@
49 packages=['subunit', 'subunit.tests'],50 packages=['subunit', 'subunit.tests'],
50 package_dir={'subunit': 'python/subunit'},51 package_dir={'subunit': 'python/subunit'},
51 scripts = [52 scripts = [
53 'filters/subunit-1to2',
54 'filters/subunit-2to1',
52 'filters/subunit2gtk',55 'filters/subunit2gtk',
53 'filters/subunit2junitxml',56 'filters/subunit2junitxml',
54 'filters/subunit2pyunit',57 'filters/subunit2pyunit',

Subscribers

People subscribed via source and target branches