From a2e87345869e16ff1d8b97f25f45c18eb828c221 Mon Sep 17 00:00:00 2001 From: Thomi Richards Date: Mon, 18 Nov 2013 11:20:48 +1300 Subject: Added new script file, modified setup.py to install it, and added an empty implementation function. --- filters/subunit-output | 23 +++++++++++++++++++++++ python/subunit/_output.py | 18 ++++++++++++++++++ setup.py | 7 ++++--- 3 files changed, 45 insertions(+), 3 deletions(-) create mode 100644 filters/subunit-output create mode 100644 python/subunit/_output.py diff --git a/filters/subunit-output b/filters/subunit-output new file mode 100644 index 0000000..12c68f5 --- /dev/null +++ b/filters/subunit-output @@ -0,0 +1,23 @@ +#!/usr/bin/env python +# subunit: extensions to python unittest to get test results from subprocesses. +# Copyright (C) 2013 Thomi Richards +# +# Licensed under either the Apache License, Version 2.0 or the BSD 3-clause +# license at the users choice. A copy of both licenses are available in the +# project source as Apache-2.0 and BSD. You may not use this file except in +# compliance with one of these two licences. +# +# Unless required by applicable law or agreed to in writing, software +# distributed under these licenses is distributed on an "AS IS" BASIS, WITHOUT +# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the +# license you chose for the specific language governing permissions and +# limitations under that license. + + +"""A command-line tool to generate a subunit result byte-stream.""" + +from subunit._output import output_main + + +if __name__ == '__main__': + exit(output_main()) diff --git a/python/subunit/_output.py b/python/subunit/_output.py new file mode 100644 index 0000000..66093bb --- /dev/null +++ b/python/subunit/_output.py @@ -0,0 +1,18 @@ +#!/usr/bin/env python +# subunit: extensions to python unittest to get test results from subprocesses. +# Copyright (C) 2013 Thomi Richards +# +# Licensed under either the Apache License, Version 2.0 or the BSD 3-clause +# license at the users choice. A copy of both licenses are available in the +# project source as Apache-2.0 and BSD. You may not use this file except in +# compliance with one of these two licences. +# +# Unless required by applicable law or agreed to in writing, software +# distributed under these licenses is distributed on an "AS IS" BASIS, WITHOUT +# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the +# license you chose for the specific language governing permissions and +# limitations under that license. + + +def output_main(): + return 0 diff --git a/setup.py b/setup.py index 1649b16..d319d9c 100755 --- a/setup.py +++ b/setup.py @@ -52,14 +52,15 @@ setup( scripts = [ 'filters/subunit-1to2', 'filters/subunit-2to1', - 'filters/subunit2gtk', - 'filters/subunit2junitxml', - 'filters/subunit2pyunit', 'filters/subunit-filter', 'filters/subunit-ls', 'filters/subunit-notify', + 'filters/subunit-output', 'filters/subunit-stats', 'filters/subunit-tags', + 'filters/subunit2gtk', + 'filters/subunit2junitxml', + 'filters/subunit2pyunit', 'filters/tap2subunit', ], **extra -- cgit v1.2.1 From d40f2355b98662466d4009d21a1bd1b3b4529a3b Mon Sep 17 00:00:00 2001 From: Thomi Richards Date: Mon, 18 Nov 2013 11:25:19 +1300 Subject: Added empty test module to test suite. --- python/subunit/tests/__init__.py | 4 +++- python/subunit/tests/test_output_filter.py | 22 ++++++++++++++++++++++ 2 files changed, 25 insertions(+), 1 deletion(-) create mode 100644 python/subunit/tests/test_output_filter.py diff --git a/python/subunit/tests/__init__.py b/python/subunit/tests/__init__.py index a3caa38..c9cc7ae 100644 --- a/python/subunit/tests/__init__.py +++ b/python/subunit/tests/__init__.py @@ -6,7 +6,7 @@ # license at the users choice. A copy of both licenses are available in the # project source as Apache-2.0 and BSD. You may not use this file except in # compliance with one of these two licences. -# +# # Unless required by applicable law or agreed to in writing, software # distributed under these licenses is distributed on an "AS IS" BASIS, WITHOUT # WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the @@ -20,6 +20,7 @@ from subunit.tests import ( test_chunked, test_details, test_filters, + test_output_filter, test_progress_model, test_run, test_subunit_filter, @@ -45,4 +46,5 @@ def test_suite(): result.addTest(loader.loadTestsFromModule(test_subunit_tags)) result.addTest(loader.loadTestsFromModule(test_subunit_stats)) result.addTest(loader.loadTestsFromModule(test_run)) + result.addTest(loader.loadTestsFromModule(test_output_filter)) return result diff --git a/python/subunit/tests/test_output_filter.py b/python/subunit/tests/test_output_filter.py new file mode 100644 index 0000000..8317ffe --- /dev/null +++ b/python/subunit/tests/test_output_filter.py @@ -0,0 +1,22 @@ +# +# subunit: extensions to python unittest to get test results from subprocesses. +# Copyright (C) 2005 Thomi Richards +# +# Licensed under either the Apache License, Version 2.0 or the BSD 3-clause +# license at the users choice. A copy of both licenses are available in the +# project source as Apache-2.0 and BSD. You may not use this file except in +# compliance with one of these two licences. +# +# Unless required by applicable law or agreed to in writing, software +# distributed under these licenses is distributed on an "AS IS" BASIS, WITHOUT +# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the +# license you chose for the specific language governing permissions and +# limitations under that license. +# + + +from testtools import TestCase + + +class OutputFilterTests(TestCase): + pass -- cgit v1.2.1 From 432bf7b12404301ae7e884088e8d1b2666842bb7 Mon Sep 17 00:00:00 2001 From: Thomi Richards Date: Mon, 18 Nov 2013 15:27:08 +1300 Subject: First pass, missing some tests. --- python/subunit/_output.py | 57 ++++++++++++++++++++ python/subunit/tests/test_output_filter.py | 87 ++++++++++++++++++++++++++++-- 2 files changed, 141 insertions(+), 3 deletions(-) diff --git a/python/subunit/_output.py b/python/subunit/_output.py index 66093bb..e513639 100644 --- a/python/subunit/_output.py +++ b/python/subunit/_output.py @@ -13,6 +13,63 @@ # license you chose for the specific language governing permissions and # limitations under that license. +from argparse import ArgumentParser +from sys import stdout + +from subunit.v2 import StreamResultToBytes def output_main(): + args = parse_arguments() + output = get_output_stream_writer() + generate_bytestream(args, output) + return 0 + + +def parse_arguments(args=None): + """Parse arguments from the command line. + + If specified, args must be a list of strings, similar to sys.argv[1:]. + + """ + parser = ArgumentParser( + prog='subunit-output', + description="A tool to generate a subunit result byte-stream", + ) + sub_parsers = parser.add_subparsers(dest="action") + + parser_start = sub_parsers.add_parser("start", help="Start a test.") + parser_start.add_argument("test_id", help="The test id you want to start.") + + parser_pass = sub_parsers.add_parser("pass", help="Pass a test.") + parser_pass.add_argument("test_id", help="The test id you want to pass.") + + parser_fail = sub_parsers.add_parser("fail", help="Fail a test.") + parser_fail.add_argument("test_id", help="The test id you want to fail.") + + parser_skip = sub_parsers.add_parser("skip", help="Skip a test.") + parser_skip.add_argument("test_id", help="The test id you want to skip.") + + return parser.parse_args(args) + + +def translate_command_name(command_name): + """Turn the friendly command names we show users on the command line into + something subunit understands. + + """ + return { + 'start': 'inprogress', + 'pass': 'success', + }.get(command_name, command_name) + + +def get_output_stream_writer(): + return StreamResultToBytes(stdout) + + +def generate_bytestream(args, output_writer): + output_writer.status( + test_id=args.test_id, + test_status=translate_command_name(args.action) + ) diff --git a/python/subunit/tests/test_output_filter.py b/python/subunit/tests/test_output_filter.py index 8317ffe..27caa4e 100644 --- a/python/subunit/tests/test_output_filter.py +++ b/python/subunit/tests/test_output_filter.py @@ -15,8 +15,89 @@ # -from testtools import TestCase +from io import BytesIO + +from testtools import TestCase, StreamToExtendedDecorator, TestResult +from testtools.matchers import Equals + +from subunit.v2 import StreamResultToBytes, ByteStreamToStreamResult +from subunit._output import ( + generate_bytestream, + parse_arguments, + translate_command_name, +) + +class OutputFilterArgumentTests(TestCase): + + """Tests for the command line argument parser.""" + + def _test_command(self, command, test_id): + args = parse_arguments(args=[command, test_id]) + + self.assertThat(args.action, Equals(command)) + self.assertThat(args.test_id, Equals(test_id)) + + def test_can_parse_start_test(self): + self._test_command('start', self.getUniqueString()) + + def test_can_parse_pass_test(self): + self._test_command('pass', self.getUniqueString()) + + def test_can_parse_fail_test(self): + self._test_command('fail', self.getUniqueString()) + + def test_can_parse_skip_test(self): + self._test_command('skip', self.getUniqueString()) + + def test_command_translation(self): + self.assertThat(translate_command_name('start'), Equals('inprogress')) + self.assertThat(translate_command_name('pass'), Equals('success')) + for command in ('fail', 'skip'): + self.assertThat(translate_command_name(command), Equals(command)) + + +class ByteStreamCompatibilityTests(TestCase): + + """Tests that ensure that the subunit byetstream we generate contains what + we expect it to. + + """ + + def _get_result_for(self, *commands): + """Get a result object from *args. + + Runs the 'generate_bytestream' function from subunit._output after + parsing *args as if they were specified on the command line. The + resulting bytestream is then converted back into a result object and + returned. + + """ + stream = BytesIO() + + for command_list in commands: + args = parse_arguments(command_list) + output_writer = StreamResultToBytes(output_stream=stream) + generate_bytestream(args, output_writer) + + stream.seek(0) + + case = ByteStreamToStreamResult(source=stream) + result = TestResult() + result = StreamToExtendedDecorator(result) + result.startTestRun() + case.run(result) + result.stopTestRun() + return result + + def test_start(self): + result = self._get_result_for( + ['start', 'foo'], + ['pass', 'foo'], + ) + + self.assertThat(result.decorated.wasSuccessful(), Equals(True)) + # How do I get the id? or details? + self.assertThat(result.decorated.id(), Equals('foo')) + -class OutputFilterTests(TestCase): - pass -- cgit v1.2.1 From 74a11f65b2463d3b531a27dc0eb2f382f1df81ad Mon Sep 17 00:00:00 2001 From: Thomi Richards Date: Mon, 18 Nov 2013 16:47:36 +1300 Subject: A better approach to testing the generate_bytestream function. --- python/subunit/tests/test_output_filter.py | 69 ++++++++++++++++++++++++------ 1 file changed, 55 insertions(+), 14 deletions(-) diff --git a/python/subunit/tests/test_output_filter.py b/python/subunit/tests/test_output_filter.py index 27caa4e..03f4f26 100644 --- a/python/subunit/tests/test_output_filter.py +++ b/python/subunit/tests/test_output_filter.py @@ -17,8 +17,14 @@ from io import BytesIO -from testtools import TestCase, StreamToExtendedDecorator, TestResult -from testtools.matchers import Equals +from collections import namedtuple +from testtools import TestCase +from testtools.matchers import ( + Equals, + Matcher, + MatchesListwise, +) +from testtools.testresult.doubles import StreamResult from subunit.v2 import StreamResultToBytes, ByteStreamToStreamResult from subunit._output import ( @@ -58,16 +64,11 @@ class OutputFilterArgumentTests(TestCase): class ByteStreamCompatibilityTests(TestCase): - """Tests that ensure that the subunit byetstream we generate contains what - we expect it to. - - """ - def _get_result_for(self, *commands): - """Get a result object from *args. + """Get a result object from *commands. Runs the 'generate_bytestream' function from subunit._output after - parsing *args as if they were specified on the command line. The + parsing *commands as if they were specified on the command line. The resulting bytestream is then converted back into a result object and returned. @@ -82,8 +83,7 @@ class ByteStreamCompatibilityTests(TestCase): stream.seek(0) case = ByteStreamToStreamResult(source=stream) - result = TestResult() - result = StreamToExtendedDecorator(result) + result = StreamResult() result.startTestRun() case.run(result) result.stopTestRun() @@ -95,9 +95,50 @@ class ByteStreamCompatibilityTests(TestCase): ['pass', 'foo'], ) - self.assertThat(result.decorated.wasSuccessful(), Equals(True)) - # How do I get the id? or details? - self.assertThat(result.decorated.id(), Equals('foo')) + self.assertThat( + result._events, + MatchesListwise([ + MatchesCall(call='startTestRun'), + MatchesCall(call='status', test_id='foo', test_status='inprogress'), + MatchesCall(call='status', test_id='foo', test_status='success'), + MatchesCall(call='stopTestRun'), + ]) + ) +class MatchesCall(Matcher): + + _position_lookup = { + 'call': 0, + 'test_id': 1, + 'test_status': 2, + 'test_tags': 3, + 'runnable': 4, + 'file_name': 5, + 'file_bytes': 6, + 'eof': 7, + 'mime_type': 8, + 'route_code': 9, + 'timestamp': 10, + } + + def __init__(self, **kwargs): + unknown_kwargs = filter( + lambda k: k not in self._position_lookup, + kwargs + ) + if unknown_kwargs: + raise ValueError("Unknown keywords: %s" % ','.join(unknown_kwargs)) + self._filters = kwargs + + def match(self, call_tuple): + for k,v in self._filters.items(): + try: + if call_tuple[self._position_lookup[k]] != v: + return Mismatch("Value for key is %r, not %r" % (self._position_lookup[k], v)) + except IndexError: + return Mismatch("Key %s is not present." % k) + + def __str__(self): + return "" % self._filters -- cgit v1.2.1 From 40093d25074410170c7798657f5c11ea1816e3ba Mon Sep 17 00:00:00 2001 From: Thomi Richards Date: Tue, 19 Nov 2013 08:22:46 +1300 Subject: Generate a timestamp for all messages, and refactor argument parser to use common arguments. --- python/subunit/_output.py | 46 ++++++++++++++++++++++++------ python/subunit/tests/test_output_filter.py | 43 ++++++++++++++++++++++++---- 2 files changed, 74 insertions(+), 15 deletions(-) diff --git a/python/subunit/_output.py b/python/subunit/_output.py index e513639..9b467c1 100644 --- a/python/subunit/_output.py +++ b/python/subunit/_output.py @@ -14,6 +14,7 @@ # limitations under that license. from argparse import ArgumentParser +import datetime from sys import stdout from subunit.v2 import StreamResultToBytes @@ -36,19 +37,23 @@ def parse_arguments(args=None): prog='subunit-output', description="A tool to generate a subunit result byte-stream", ) + + common_args = ArgumentParser(add_help=False) + common_args.add_argument("test_id", help="""A string that uniquely + identifies this test.""") sub_parsers = parser.add_subparsers(dest="action") - parser_start = sub_parsers.add_parser("start", help="Start a test.") - parser_start.add_argument("test_id", help="The test id you want to start.") + parser_start = sub_parsers.add_parser("start", help="Start a test.", + parents=[common_args]) - parser_pass = sub_parsers.add_parser("pass", help="Pass a test.") - parser_pass.add_argument("test_id", help="The test id you want to pass.") + parser_pass = sub_parsers.add_parser("pass", help="Pass a test.", + parents=[common_args]) - parser_fail = sub_parsers.add_parser("fail", help="Fail a test.") - parser_fail.add_argument("test_id", help="The test id you want to fail.") + parser_fail = sub_parsers.add_parser("fail", help="Fail a test.", + parents=[common_args]) - parser_skip = sub_parsers.add_parser("skip", help="Skip a test.") - parser_skip.add_argument("test_id", help="The test id you want to skip.") + parser_skip = sub_parsers.add_parser("skip", help="Skip a test.", + parents=[common_args]) return parser.parse_args(args) @@ -69,7 +74,30 @@ def get_output_stream_writer(): def generate_bytestream(args, output_writer): + output_writer.startTestRun() output_writer.status( test_id=args.test_id, - test_status=translate_command_name(args.action) + test_status=translate_command_name(args.action), + timestamp=create_timestamp() ) + output_writer.stopTestRun() + + +_ZERO = datetime.timedelta(0) + + +class UTC(datetime.tzinfo): + """UTC""" + def utcoffset(self, dt): + return _ZERO + def tzname(self, dt): + return "UTC" + def dst(self, dt): + return _ZERO + + +utc = UTC() + + +def create_timestamp(): + return datetime.datetime.now(utc) diff --git a/python/subunit/tests/test_output_filter.py b/python/subunit/tests/test_output_filter.py index 03f4f26..05b6267 100644 --- a/python/subunit/tests/test_output_filter.py +++ b/python/subunit/tests/test_output_filter.py @@ -22,6 +22,7 @@ from testtools import TestCase from testtools.matchers import ( Equals, Matcher, + Mismatch, MatchesListwise, ) from testtools.testresult.doubles import StreamResult @@ -84,24 +85,54 @@ class ByteStreamCompatibilityTests(TestCase): case = ByteStreamToStreamResult(source=stream) result = StreamResult() - result.startTestRun() case.run(result) - result.stopTestRun() return result - def test_start(self): + def test_start_generates_inprogress(self): result = self._get_result_for( ['start', 'foo'], - ['pass', 'foo'], ) self.assertThat( result._events, MatchesListwise([ - MatchesCall(call='startTestRun'), MatchesCall(call='status', test_id='foo', test_status='inprogress'), + ]) + ) + + def test_pass_generates_success(self): + result = self._get_result_for( + ['pass', 'foo'], + ) + + self.assertThat( + result._events, + MatchesListwise([ MatchesCall(call='status', test_id='foo', test_status='success'), - MatchesCall(call='stopTestRun'), + ]) + ) + + def test_fail_generates_fail(self): + result = self._get_result_for( + ['fail', 'foo'], + ) + + self.assertThat( + result._events, + MatchesListwise([ + MatchesCall(call='status', test_id='foo', test_status='fail'), + ]) + ) + + def test_skip_generates_skip(self): + result = self._get_result_for( + ['skip', 'foo'], + ) + + self.assertThat( + result._events, + MatchesListwise([ + MatchesCall(call='status', test_id='foo', test_status='skip'), ]) ) -- cgit v1.2.1 From 4573276795b7c62355cc9c1dbda7aea7588d0e20 Mon Sep 17 00:00:00 2001 From: Thomi Richards Date: Tue, 19 Nov 2013 08:24:45 +1300 Subject: Clean up tests: Don't use MatchesListwise for a single-length list. --- python/subunit/tests/test_output_filter.py | 25 ++++++++----------------- 1 file changed, 8 insertions(+), 17 deletions(-) diff --git a/python/subunit/tests/test_output_filter.py b/python/subunit/tests/test_output_filter.py index 05b6267..fb56057 100644 --- a/python/subunit/tests/test_output_filter.py +++ b/python/subunit/tests/test_output_filter.py @@ -23,7 +23,6 @@ from testtools.matchers import ( Equals, Matcher, Mismatch, - MatchesListwise, ) from testtools.testresult.doubles import StreamResult @@ -94,10 +93,8 @@ class ByteStreamCompatibilityTests(TestCase): ) self.assertThat( - result._events, - MatchesListwise([ - MatchesCall(call='status', test_id='foo', test_status='inprogress'), - ]) + result._events[0], + MatchesCall(call='status', test_id='foo', test_status='inprogress') ) def test_pass_generates_success(self): @@ -106,10 +103,8 @@ class ByteStreamCompatibilityTests(TestCase): ) self.assertThat( - result._events, - MatchesListwise([ - MatchesCall(call='status', test_id='foo', test_status='success'), - ]) + result._events[0], + MatchesCall(call='status', test_id='foo', test_status='success') ) def test_fail_generates_fail(self): @@ -118,10 +113,8 @@ class ByteStreamCompatibilityTests(TestCase): ) self.assertThat( - result._events, - MatchesListwise([ - MatchesCall(call='status', test_id='foo', test_status='fail'), - ]) + result._events[0], + MatchesCall(call='status', test_id='foo', test_status='fail') ) def test_skip_generates_skip(self): @@ -130,10 +123,8 @@ class ByteStreamCompatibilityTests(TestCase): ) self.assertThat( - result._events, - MatchesListwise([ - MatchesCall(call='status', test_id='foo', test_status='skip'), - ]) + result._events[0], + MatchesCall(call='status', test_id='foo', test_status='skip') ) -- cgit v1.2.1 From 3277945b614a58aba415001f543b63f84a077e29 Mon Sep 17 00:00:00 2001 From: Thomi Richards Date: Tue, 19 Nov 2013 09:34:26 +1300 Subject: Add tests for timestamps, and add support for 'exists'. --- python/subunit/_output.py | 37 +++++++++++++++----- python/subunit/tests/test_output_filter.py | 55 +++++++++++++++++++++++++++--- 2 files changed, 80 insertions(+), 12 deletions(-) diff --git a/python/subunit/_output.py b/python/subunit/_output.py index 9b467c1..4889e6f 100644 --- a/python/subunit/_output.py +++ b/python/subunit/_output.py @@ -43,17 +43,38 @@ def parse_arguments(args=None): identifies this test.""") sub_parsers = parser.add_subparsers(dest="action") - parser_start = sub_parsers.add_parser("start", help="Start a test.", - parents=[common_args]) + final_state = "This is a final action: No more actions may be generated " \ + "for this test id after this one." - parser_pass = sub_parsers.add_parser("pass", help="Pass a test.", - parents=[common_args]) + parser_start = sub_parsers.add_parser( + "start", + help="Start a test.", + parents=[common_args] + ) + + parser_pass = sub_parsers.add_parser( + "pass", + help="Pass a test. " + final_state, + parents=[common_args] + ) - parser_fail = sub_parsers.add_parser("fail", help="Fail a test.", - parents=[common_args]) + parser_fail = sub_parsers.add_parser( + "fail", + help="Fail a test. " + final_state, + parents=[common_args] + ) - parser_skip = sub_parsers.add_parser("skip", help="Skip a test.", - parents=[common_args]) + parser_skip = sub_parsers.add_parser( + "skip", + help="Skip a test. " + final_state, + parents=[common_args] + ) + + parser_exists = sub_parsers.add_parser( + "exists", + help="Marks a test as existing. " + final_state, + parents=[common_args] + ) return parser.parse_args(args) diff --git a/python/subunit/tests/test_output_filter.py b/python/subunit/tests/test_output_filter.py index fb56057..4031449 100644 --- a/python/subunit/tests/test_output_filter.py +++ b/python/subunit/tests/test_output_filter.py @@ -18,6 +18,7 @@ from io import BytesIO from collections import namedtuple +import datetime from testtools import TestCase from testtools.matchers import ( Equals, @@ -31,7 +32,9 @@ from subunit._output import ( generate_bytestream, parse_arguments, translate_command_name, + utc, ) +import subunit._output as _o class OutputFilterArgumentTests(TestCase): @@ -55,6 +58,9 @@ class OutputFilterArgumentTests(TestCase): def test_can_parse_skip_test(self): self._test_command('skip', self.getUniqueString()) + def test_can_parse_exists(self): + self._test_command('exists', self.getUniqueString()) + def test_command_translation(self): self.assertThat(translate_command_name('start'), Equals('inprogress')) self.assertThat(translate_command_name('pass'), Equals('success')) @@ -64,6 +70,12 @@ class OutputFilterArgumentTests(TestCase): class ByteStreamCompatibilityTests(TestCase): + _dummy_timestamp = datetime.datetime(2013, 1, 1, 0, 0, 0, 0, utc) + + def setUp(self): + super(ByteStreamCompatibilityTests, self).setUp() + self.patch(_o, 'create_timestamp', lambda: self._dummy_timestamp) + def _get_result_for(self, *commands): """Get a result object from *commands. @@ -94,7 +106,12 @@ class ByteStreamCompatibilityTests(TestCase): self.assertThat( result._events[0], - MatchesCall(call='status', test_id='foo', test_status='inprogress') + MatchesCall( + call='status', + test_id='foo', + test_status='inprogress', + timestamp=self._dummy_timestamp, + ) ) def test_pass_generates_success(self): @@ -104,7 +121,12 @@ class ByteStreamCompatibilityTests(TestCase): self.assertThat( result._events[0], - MatchesCall(call='status', test_id='foo', test_status='success') + MatchesCall( + call='status', + test_id='foo', + test_status='success', + timestamp=self._dummy_timestamp, + ) ) def test_fail_generates_fail(self): @@ -114,7 +136,12 @@ class ByteStreamCompatibilityTests(TestCase): self.assertThat( result._events[0], - MatchesCall(call='status', test_id='foo', test_status='fail') + MatchesCall( + call='status', + test_id='foo', + test_status='fail', + timestamp=self._dummy_timestamp, + ) ) def test_skip_generates_skip(self): @@ -124,7 +151,27 @@ class ByteStreamCompatibilityTests(TestCase): self.assertThat( result._events[0], - MatchesCall(call='status', test_id='foo', test_status='skip') + MatchesCall( + call='status', + test_id='foo', + test_status='skip', + timestamp=self._dummy_timestamp, + ) + ) + + def test_exists_generates_exists(self): + result = self._get_result_for( + ['exists', 'foo'], + ) + + self.assertThat( + result._events[0], + MatchesCall( + call='status', + test_id='foo', + test_status='exists', + timestamp=self._dummy_timestamp, + ) ) -- cgit v1.2.1 From 0261bdde749194d51098a97ccb41fa5ddae91c15 Mon Sep 17 00:00:00 2001 From: Thomi Richards Date: Tue, 19 Nov 2013 09:55:44 +1300 Subject: Allow customisation of argument parser class used, so we can write failing tests for command line arguments not yet supported. Have failing test for attaching files. --- python/subunit/_output.py | 15 ++++++---- python/subunit/tests/test_output_filter.py | 48 ++++++++++++++++++------------ 2 files changed, 39 insertions(+), 24 deletions(-) diff --git a/python/subunit/_output.py b/python/subunit/_output.py index 4889e6f..b3a5bba 100644 --- a/python/subunit/_output.py +++ b/python/subunit/_output.py @@ -27,20 +27,25 @@ def output_main(): return 0 -def parse_arguments(args=None): +def parse_arguments(args=None, ParserClass=ArgumentParser): """Parse arguments from the command line. If specified, args must be a list of strings, similar to sys.argv[1:]. + ParserClass can be specified to override the class we use to parse the + command-line arguments. This is useful for testing. + """ - parser = ArgumentParser( + parser = ParserClass( prog='subunit-output', description="A tool to generate a subunit result byte-stream", ) - common_args = ArgumentParser(add_help=False) - common_args.add_argument("test_id", help="""A string that uniquely - identifies this test.""") + common_args = ParserClass(add_help=False) + common_args.add_argument( + "test_id", + help="A string that uniquely identifies this test." + ) sub_parsers = parser.add_subparsers(dest="action") final_state = "This is a final action: No more actions may be generated " \ diff --git a/python/subunit/tests/test_output_filter.py b/python/subunit/tests/test_output_filter.py index 4031449..c9059df 100644 --- a/python/subunit/tests/test_output_filter.py +++ b/python/subunit/tests/test_output_filter.py @@ -15,10 +15,11 @@ # -from io import BytesIO - +import argparse from collections import namedtuple import datetime +from functools import partial +from io import BytesIO from testtools import TestCase from testtools.matchers import ( Equals, @@ -36,37 +37,46 @@ from subunit._output import ( ) import subunit._output as _o + +class SafeArgumentParser(argparse.ArgumentParser): + + def exit(self, status=0, message=""): + raise RuntimeError("ArgumentParser requested to exit with status "\ + " %d and message %r" % (status, message)) + + +safe_parse_arguments = partial(parse_arguments, ParserClass=SafeArgumentParser) + + class OutputFilterArgumentTests(TestCase): """Tests for the command line argument parser.""" + _all_supported_commands = ('start', 'pass', 'fail', 'skip', 'exists') + def _test_command(self, command, test_id): - args = parse_arguments(args=[command, test_id]) + args = safe_parse_arguments(args=[command, test_id]) self.assertThat(args.action, Equals(command)) self.assertThat(args.test_id, Equals(test_id)) - def test_can_parse_start_test(self): - self._test_command('start', self.getUniqueString()) - - def test_can_parse_pass_test(self): - self._test_command('pass', self.getUniqueString()) - - def test_can_parse_fail_test(self): - self._test_command('fail', self.getUniqueString()) - - def test_can_parse_skip_test(self): - self._test_command('skip', self.getUniqueString()) - - def test_can_parse_exists(self): - self._test_command('exists', self.getUniqueString()) + def test_can_parse_all_commands_with_test_id(self): + for command in self._all_supported_commands: + self._test_command(command, self.getUniqueString()) def test_command_translation(self): self.assertThat(translate_command_name('start'), Equals('inprogress')) self.assertThat(translate_command_name('pass'), Equals('success')) - for command in ('fail', 'skip'): + for command in ('fail', 'skip', 'exists'): self.assertThat(translate_command_name(command), Equals(command)) + def test_all_commands_parse_file_attachment(self): + for command in self._all_supported_commands: + args = safe_parse_arguments( + args=[command, 'foo', '--attach-file', '/some/path'] + ) + self.assertThat(args.attach_file, Equals('/some/path')) + class ByteStreamCompatibilityTests(TestCase): @@ -88,7 +98,7 @@ class ByteStreamCompatibilityTests(TestCase): stream = BytesIO() for command_list in commands: - args = parse_arguments(command_list) + args = safe_parse_arguments(command_list) output_writer = StreamResultToBytes(output_stream=stream) generate_bytestream(args, output_writer) -- cgit v1.2.1 From 491727efa8038ef0692f2ec13b72707ff7396a40 Mon Sep 17 00:00:00 2001 From: Thomi Richards Date: Tue, 19 Nov 2013 10:50:18 +1300 Subject: Add support for attaching files. --- python/subunit/_output.py | 25 +++++++++++++++ python/subunit/tests/test_output_filter.py | 50 +++++++++++++++++++++++++++--- 2 files changed, 70 insertions(+), 5 deletions(-) diff --git a/python/subunit/_output.py b/python/subunit/_output.py index b3a5bba..43097e6 100644 --- a/python/subunit/_output.py +++ b/python/subunit/_output.py @@ -15,6 +15,7 @@ from argparse import ArgumentParser import datetime +from functools import partial from sys import stdout from subunit.v2 import StreamResultToBytes @@ -46,6 +47,11 @@ def parse_arguments(args=None, ParserClass=ArgumentParser): "test_id", help="A string that uniquely identifies this test." ) + common_args.add_argument( + "--attach-file", + type=file, + help="Attach a file to the result stream for this test." + ) sub_parsers = parser.add_subparsers(dest="action") final_state = "This is a final action: No more actions may be generated " \ @@ -101,6 +107,8 @@ def get_output_stream_writer(): def generate_bytestream(args, output_writer): output_writer.startTestRun() + if args.attach_file: + write_chunked_file(args.attach_file, args.test_id, output_writer) output_writer.status( test_id=args.test_id, test_status=translate_command_name(args.action), @@ -109,6 +117,23 @@ def generate_bytestream(args, output_writer): output_writer.stopTestRun() +def write_chunked_file(file_obj, test_id, output_writer, chunk_size=1024): + reader = partial(file_obj.read, chunk_size) + for chunk in iter(reader, ''): + output_writer.status( + test_id=test_id, + file_name=file_obj.name, + file_bytes=chunk, + eof=False, + ) + output_writer.status( + test_id=test_id, + file_name=file_obj.name, + file_bytes='', + eof=True, + ) + + _ZERO = datetime.timedelta(0) diff --git a/python/subunit/tests/test_output_filter.py b/python/subunit/tests/test_output_filter.py index c9059df..9d530c5 100644 --- a/python/subunit/tests/test_output_filter.py +++ b/python/subunit/tests/test_output_filter.py @@ -20,10 +20,13 @@ from collections import namedtuple import datetime from functools import partial from io import BytesIO +from tempfile import NamedTemporaryFile from testtools import TestCase from testtools.matchers import ( Equals, + IsInstance, Matcher, + MatchesListwise, Mismatch, ) from testtools.testresult.doubles import StreamResult @@ -34,6 +37,7 @@ from subunit._output import ( parse_arguments, translate_command_name, utc, + write_chunked_file, ) import subunit._output as _o @@ -71,11 +75,13 @@ class OutputFilterArgumentTests(TestCase): self.assertThat(translate_command_name(command), Equals(command)) def test_all_commands_parse_file_attachment(self): - for command in self._all_supported_commands: - args = safe_parse_arguments( - args=[command, 'foo', '--attach-file', '/some/path'] - ) - self.assertThat(args.attach_file, Equals('/some/path')) + with NamedTemporaryFile() as tmp_file: + for command in self._all_supported_commands: + args = safe_parse_arguments( + args=[command, 'foo', '--attach-file', tmp_file.name] + ) + self.assertThat(args.attach_file, IsInstance(file)) + self.assertThat(args.attach_file.name, Equals(tmp_file.name)) class ByteStreamCompatibilityTests(TestCase): @@ -185,6 +191,40 @@ class ByteStreamCompatibilityTests(TestCase): ) +class FileChunkingTests(TestCase): + + def _write_chunk_file(self, file_data, chunk_size): + """Write chunked data to a subunit stream, return a StreamResult object.""" + stream = BytesIO() + output_writer = StreamResultToBytes(output_stream=stream) + + with NamedTemporaryFile() as f: + f.write(file_data) + f.seek(0) + + write_chunked_file(f, 'foo_test', output_writer, chunk_size) + + stream.seek(0) + + case = ByteStreamToStreamResult(source=stream) + result = StreamResult() + case.run(result) + return result + + def test_file_chunk_size_is_honored(self): + result = self._write_chunk_file("Hello", 1) + self.assertThat( + result._events, + MatchesListwise([ + MatchesCall(call='status', file_bytes='H', eof=False), + MatchesCall(call='status', file_bytes='e', eof=False), + MatchesCall(call='status', file_bytes='l', eof=False), + MatchesCall(call='status', file_bytes='l', eof=False), + MatchesCall(call='status', file_bytes='o', eof=False), + MatchesCall(call='status', file_bytes='', eof=True), + ]) + ) + class MatchesCall(Matcher): _position_lookup = { -- cgit v1.2.1 From 56063023c343ee0f25be349edab11eecbbc22e5a Mon Sep 17 00:00:00 2001 From: Thomi Richards Date: Tue, 19 Nov 2013 11:01:15 +1300 Subject: Extend test to make sure that by default no mime-type is specified. --- python/subunit/_output.py | 5 ++++- python/subunit/tests/test_output_filter.py | 12 ++++++------ 2 files changed, 10 insertions(+), 7 deletions(-) diff --git a/python/subunit/_output.py b/python/subunit/_output.py index 43097e6..b4df54c 100644 --- a/python/subunit/_output.py +++ b/python/subunit/_output.py @@ -117,19 +117,22 @@ def generate_bytestream(args, output_writer): output_writer.stopTestRun() -def write_chunked_file(file_obj, test_id, output_writer, chunk_size=1024): +def write_chunked_file(file_obj, test_id, output_writer, chunk_size=1024, + mime_type=None): reader = partial(file_obj.read, chunk_size) for chunk in iter(reader, ''): output_writer.status( test_id=test_id, file_name=file_obj.name, file_bytes=chunk, + mime_type=mime_type, eof=False, ) output_writer.status( test_id=test_id, file_name=file_obj.name, file_bytes='', + mime_type=mime_type, eof=True, ) diff --git a/python/subunit/tests/test_output_filter.py b/python/subunit/tests/test_output_filter.py index 9d530c5..ef6dc9a 100644 --- a/python/subunit/tests/test_output_filter.py +++ b/python/subunit/tests/test_output_filter.py @@ -216,12 +216,12 @@ class FileChunkingTests(TestCase): self.assertThat( result._events, MatchesListwise([ - MatchesCall(call='status', file_bytes='H', eof=False), - MatchesCall(call='status', file_bytes='e', eof=False), - MatchesCall(call='status', file_bytes='l', eof=False), - MatchesCall(call='status', file_bytes='l', eof=False), - MatchesCall(call='status', file_bytes='o', eof=False), - MatchesCall(call='status', file_bytes='', eof=True), + MatchesCall(call='status', file_bytes='H', mime_type=None, eof=False), + MatchesCall(call='status', file_bytes='e', mime_type=None, eof=False), + MatchesCall(call='status', file_bytes='l', mime_type=None, eof=False), + MatchesCall(call='status', file_bytes='l', mime_type=None, eof=False), + MatchesCall(call='status', file_bytes='o', mime_type=None, eof=False), + MatchesCall(call='status', file_bytes='', mime_type=None, eof=True), ]) ) -- cgit v1.2.1 From 66ed68e1d151baf71e63d11a44ce773b6f40b427 Mon Sep 17 00:00:00 2001 From: Thomi Richards Date: Tue, 19 Nov 2013 11:16:41 +1300 Subject: Add support for passing mime-type on the command-line. --- python/subunit/_output.py | 14 +++++++++++++- python/subunit/tests/test_output_filter.py | 27 +++++++++++++++++++++++---- 2 files changed, 36 insertions(+), 5 deletions(-) diff --git a/python/subunit/_output.py b/python/subunit/_output.py index b4df54c..4bd93d1 100644 --- a/python/subunit/_output.py +++ b/python/subunit/_output.py @@ -52,6 +52,13 @@ def parse_arguments(args=None, ParserClass=ArgumentParser): type=file, help="Attach a file to the result stream for this test." ) + common_args.add_argument( + "--mimetype", + help="The mime type to send with this file. This is only used if the "\ + "--attach-file argument is used. This argument is optional. If it is "\ + "not specified, the file will be sent wihtout a mime type.", + default=None + ) sub_parsers = parser.add_subparsers(dest="action") final_state = "This is a final action: No more actions may be generated " \ @@ -108,7 +115,12 @@ def get_output_stream_writer(): def generate_bytestream(args, output_writer): output_writer.startTestRun() if args.attach_file: - write_chunked_file(args.attach_file, args.test_id, output_writer) + write_chunked_file( + args.attach_file, + args.test_id, + output_writer, + args.mimetype, + ) output_writer.status( test_id=args.test_id, test_status=translate_command_name(args.action), diff --git a/python/subunit/tests/test_output_filter.py b/python/subunit/tests/test_output_filter.py index ef6dc9a..72ede6a 100644 --- a/python/subunit/tests/test_output_filter.py +++ b/python/subunit/tests/test_output_filter.py @@ -83,6 +83,13 @@ class OutputFilterArgumentTests(TestCase): self.assertThat(args.attach_file, IsInstance(file)) self.assertThat(args.attach_file.name, Equals(tmp_file.name)) + def test_all_commands_accept_mimetype_argument(self): + for command in self._all_supported_commands: + args = safe_parse_arguments( + args=[command, 'foo', '--mimetype', "text/plain"] + ) + self.assertThat(args.mimetype, Equals("text/plain")) + class ByteStreamCompatibilityTests(TestCase): @@ -193,7 +200,7 @@ class ByteStreamCompatibilityTests(TestCase): class FileChunkingTests(TestCase): - def _write_chunk_file(self, file_data, chunk_size): + def _write_chunk_file(self, file_data, chunk_size, mimetype=None): """Write chunked data to a subunit stream, return a StreamResult object.""" stream = BytesIO() output_writer = StreamResultToBytes(output_stream=stream) @@ -202,7 +209,7 @@ class FileChunkingTests(TestCase): f.write(file_data) f.seek(0) - write_chunked_file(f, 'foo_test', output_writer, chunk_size) + write_chunked_file(f, 'foo_test', output_writer, chunk_size, mimetype) stream.seek(0) @@ -225,6 +232,17 @@ class FileChunkingTests(TestCase): ]) ) + def test_file_mimetype_is_honored(self): + result = self._write_chunk_file("SomeData", 1024, "text/plain") + self.assertThat( + result._events, + MatchesListwise([ + MatchesCall(call='status', file_bytes='SomeData', mime_type="text/plain"), + MatchesCall(call='status', file_bytes='', mime_type="text/plain"), + ]) + ) + + class MatchesCall(Matcher): _position_lookup = { @@ -253,8 +271,9 @@ class MatchesCall(Matcher): def match(self, call_tuple): for k,v in self._filters.items(): try: - if call_tuple[self._position_lookup[k]] != v: - return Mismatch("Value for key is %r, not %r" % (self._position_lookup[k], v)) + pos = self._position_lookup[k] + if call_tuple[pos] != v: + return Mismatch("Value for key is %r, not %r" % (call_tuple[pos], v)) except IndexError: return Mismatch("Key %s is not present." % k) -- cgit v1.2.1 From 4785ad1fa97c723d19af532efb6c0ea91d65dd03 Mon Sep 17 00:00:00 2001 From: Thomi Richards Date: Tue, 19 Nov 2013 11:34:53 +1300 Subject: Aded NEWS item, fixed some test code. --- NEWS | 15 +++++++++------ python/subunit/tests/test_output_filter.py | 5 ++--- 2 files changed, 11 insertions(+), 9 deletions(-) diff --git a/NEWS b/NEWS index 78c251e..6e4cd21 100644 --- a/NEWS +++ b/NEWS @@ -20,6 +20,9 @@ IMPROVEMENTS * Most filters will now accept a file path argument instead of only reading from stdin. (Robert Collins, #409206) +* Add ``subunit-output`` tool that can generate a Subunit v2 bytestream from + arguments passed on the command line. (Thomi Richards, #1252084) + 0.0.15 ------ @@ -334,10 +337,10 @@ BUG FIXES IMPROVEMENTS ~~~~~~~~~~~~ -* New filter `subunit-notify` that will show a notification window with test +* New filter `subunit-notify` that will show a notification window with test statistics when the test run finishes. -* subunit.run will now pipe its output to the command in the +* subunit.run will now pipe its output to the command in the SUBUNIT_FORMATTER environment variable, if set. 0.0.4 @@ -356,7 +359,7 @@ BUG FIXES ----- CHANGES: - + * License change, by unanimous agreement of contributors to BSD/Apache License Version 2.0. This makes Subunit compatible with more testing frameworks. @@ -364,7 +367,7 @@ BUG FIXES IMPROVEMENTS: * CPPUnit is now directly supported: subunit builds a cppunit listener - ``libcppunit-subunit``. + ``libcppunit-subunit``. * In the python API ``addExpectedFailure`` and ``addUnexpectedSuccess`` from python 2.7/3.1 are now supported. ``addExpectedFailure`` is @@ -382,7 +385,7 @@ BUG FIXES * Multipart test outcomes are tentatively supported; the exact protocol for them, both serialiser and object is not yet finalised. Testers and early adopters are sought. As part of this and also in an attempt to - provider a more precise focus on the wire protocol and toolchain, + provider a more precise focus on the wire protocol and toolchain, Subunit now depends on testtools (http://launchpad.net/testtools) release 0.9.0 or newer. @@ -453,7 +456,7 @@ BUG FIXES and like in the protocol, if called while a test is active only applies to that test. (Robert Collins) - * ``TestResultFilter`` takes a new optional constructor parameter + * ``TestResultFilter`` takes a new optional constructor parameter ``filter_predicate``. (Martin Pool) * When a progress: directive is encountered in a subunit stream, the diff --git a/python/subunit/tests/test_output_filter.py b/python/subunit/tests/test_output_filter.py index 72ede6a..bddcc99 100644 --- a/python/subunit/tests/test_output_filter.py +++ b/python/subunit/tests/test_output_filter.py @@ -43,6 +43,7 @@ import subunit._output as _o class SafeArgumentParser(argparse.ArgumentParser): + """An ArgumentParser class that doesn't call sys.exit.""" def exit(self, status=0, message=""): raise RuntimeError("ArgumentParser requested to exit with status "\ @@ -52,9 +53,7 @@ class SafeArgumentParser(argparse.ArgumentParser): safe_parse_arguments = partial(parse_arguments, ParserClass=SafeArgumentParser) -class OutputFilterArgumentTests(TestCase): - - """Tests for the command line argument parser.""" +class OutputFilterArgumentParserTests(TestCase): _all_supported_commands = ('start', 'pass', 'fail', 'skip', 'exists') -- cgit v1.2.1 From e59d18080f8187225745ec4aa87d0a2db731a372 Mon Sep 17 00:00:00 2001 From: Thomi Richards Date: Tue, 19 Nov 2013 12:34:53 +1300 Subject: Add support for tags. --- python/subunit/_output.py | 10 +++++++++- python/subunit/tests/test_output_filter.py | 21 +++++++++++++++++++++ 2 files changed, 30 insertions(+), 1 deletion(-) diff --git a/python/subunit/_output.py b/python/subunit/_output.py index 4bd93d1..788a19f 100644 --- a/python/subunit/_output.py +++ b/python/subunit/_output.py @@ -17,6 +17,7 @@ from argparse import ArgumentParser import datetime from functools import partial from sys import stdout +from string import split from subunit.v2 import StreamResultToBytes @@ -59,6 +60,12 @@ def parse_arguments(args=None, ParserClass=ArgumentParser): "not specified, the file will be sent wihtout a mime type.", default=None ) + common_args.add_argument( + "--tags", + help="A comma-separated list of tags to associate with this test.", + type=partial(split, sep=','), + default=None + ) sub_parsers = parser.add_subparsers(dest="action") final_state = "This is a final action: No more actions may be generated " \ @@ -124,7 +131,8 @@ def generate_bytestream(args, output_writer): output_writer.status( test_id=args.test_id, test_status=translate_command_name(args.action), - timestamp=create_timestamp() + timestamp=create_timestamp(), + test_tags=args.tags, ) output_writer.stopTestRun() diff --git a/python/subunit/tests/test_output_filter.py b/python/subunit/tests/test_output_filter.py index bddcc99..8b2f54b 100644 --- a/python/subunit/tests/test_output_filter.py +++ b/python/subunit/tests/test_output_filter.py @@ -89,6 +89,13 @@ class OutputFilterArgumentParserTests(TestCase): ) self.assertThat(args.mimetype, Equals("text/plain")) + def test_all_commands_accept_tags_argument(self): + for command in self._all_supported_commands: + args = safe_parse_arguments( + args=[command, 'foo', '--tags', "foo,bar,baz"] + ) + self.assertThat(args.tags, Equals(["foo","bar","baz"])) + class ByteStreamCompatibilityTests(TestCase): @@ -196,6 +203,20 @@ class ByteStreamCompatibilityTests(TestCase): ) ) + def test_tags_are_generated(self): + result = self._get_result_for( + ['exists', 'foo', '--tags', 'hello,world'] + ) + self.assertThat( + result._events[0], + MatchesCall( + call='status', + test_id='foo', + test_tags=set(['hello','world']), + timestamp=self._dummy_timestamp, + ) + ) + class FileChunkingTests(TestCase): -- cgit v1.2.1 From 9ef306018ca4057c46a5627379e8671eb1b8db26 Mon Sep 17 00:00:00 2001 From: Thomi Richards Date: Tue, 19 Nov 2013 12:47:22 +1300 Subject: Add support for expected fail and unexpected success test statuses. --- python/subunit/_output.py | 16 ++++++++++++ python/subunit/tests/test_output_filter.py | 42 +++++++++++++++++++++++++++++- 2 files changed, 57 insertions(+), 1 deletion(-) diff --git a/python/subunit/_output.py b/python/subunit/_output.py index 788a19f..ba6d0ce 100644 --- a/python/subunit/_output.py +++ b/python/subunit/_output.py @@ -101,6 +101,20 @@ def parse_arguments(args=None, ParserClass=ArgumentParser): parents=[common_args] ) + parser_expected_fail = sub_parsers.add_parser( + "expected-fail", + help="Marks a test as failing expectedly (this is not counted as a "\ + "failure). " + final_state, + parents=[common_args], + ) + + parser_unexpected_success = sub_parsers.add_parser( + "unexpected-success", + help="Marks a test as succeeding unexpectedly (this is counted as a "\ + "failure). " + final_state, + parents=[common_args], + ) + return parser.parse_args(args) @@ -112,6 +126,8 @@ def translate_command_name(command_name): return { 'start': 'inprogress', 'pass': 'success', + 'expected-fail': 'xfail', + 'unexpected-success': 'uxsuccess', }.get(command_name, command_name) diff --git a/python/subunit/tests/test_output_filter.py b/python/subunit/tests/test_output_filter.py index 8b2f54b..2a70a2c 100644 --- a/python/subunit/tests/test_output_filter.py +++ b/python/subunit/tests/test_output_filter.py @@ -55,7 +55,15 @@ safe_parse_arguments = partial(parse_arguments, ParserClass=SafeArgumentParser) class OutputFilterArgumentParserTests(TestCase): - _all_supported_commands = ('start', 'pass', 'fail', 'skip', 'exists') + _all_supported_commands = ( + 'exists', + 'expected-fail', + 'fail', + 'pass', + 'skip', + 'start', + 'unexpected-success', + ) def _test_command(self, command, test_id): args = safe_parse_arguments(args=[command, test_id]) @@ -70,6 +78,8 @@ class OutputFilterArgumentParserTests(TestCase): def test_command_translation(self): self.assertThat(translate_command_name('start'), Equals('inprogress')) self.assertThat(translate_command_name('pass'), Equals('success')) + self.assertThat(translate_command_name('expected-fail'), Equals('xfail')) + self.assertThat(translate_command_name('unexpected-success'), Equals('uxsuccess')) for command in ('fail', 'skip', 'exists'): self.assertThat(translate_command_name(command), Equals(command)) @@ -203,6 +213,36 @@ class ByteStreamCompatibilityTests(TestCase): ) ) + def test_expected_fail_generates_xfail(self): + result = self._get_result_for( + ['expected-fail', 'foo'], + ) + + self.assertThat( + result._events[0], + MatchesCall( + call='status', + test_id='foo', + test_status='xfail', + timestamp=self._dummy_timestamp, + ) + ) + + def test_unexpected_success_generates_uxsuccess(self): + result = self._get_result_for( + ['unexpected-success', 'foo'], + ) + + self.assertThat( + result._events[0], + MatchesCall( + call='status', + test_id='foo', + test_status='uxsuccess', + timestamp=self._dummy_timestamp, + ) + ) + def test_tags_are_generated(self): result = self._get_result_for( ['exists', 'foo', '--tags', 'hello,world'] -- cgit v1.2.1 From a778ddb7e1d8fb03b58860b273ae3e8f32690ff6 Mon Sep 17 00:00:00 2001 From: Thomi Richards Date: Tue, 19 Nov 2013 12:53:56 +1300 Subject: Reverted whitespace changes to NEWS file. --- NEWS | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/NEWS b/NEWS index 6e4cd21..a799dd4 100644 --- a/NEWS +++ b/NEWS @@ -337,10 +337,10 @@ BUG FIXES IMPROVEMENTS ~~~~~~~~~~~~ -* New filter `subunit-notify` that will show a notification window with test +* New filter `subunit-notify` that will show a notification window with test statistics when the test run finishes. -* subunit.run will now pipe its output to the command in the +* subunit.run will now pipe its output to the command in the SUBUNIT_FORMATTER environment variable, if set. 0.0.4 @@ -359,7 +359,7 @@ BUG FIXES ----- CHANGES: - + * License change, by unanimous agreement of contributors to BSD/Apache License Version 2.0. This makes Subunit compatible with more testing frameworks. @@ -367,7 +367,7 @@ BUG FIXES IMPROVEMENTS: * CPPUnit is now directly supported: subunit builds a cppunit listener - ``libcppunit-subunit``. + ``libcppunit-subunit``. * In the python API ``addExpectedFailure`` and ``addUnexpectedSuccess`` from python 2.7/3.1 are now supported. ``addExpectedFailure`` is @@ -385,7 +385,7 @@ BUG FIXES * Multipart test outcomes are tentatively supported; the exact protocol for them, both serialiser and object is not yet finalised. Testers and early adopters are sought. As part of this and also in an attempt to - provider a more precise focus on the wire protocol and toolchain, + provider a more precise focus on the wire protocol and toolchain, Subunit now depends on testtools (http://launchpad.net/testtools) release 0.9.0 or newer. @@ -456,7 +456,7 @@ BUG FIXES and like in the protocol, if called while a test is active only applies to that test. (Robert Collins) - * ``TestResultFilter`` takes a new optional constructor parameter + * ``TestResultFilter`` takes a new optional constructor parameter ``filter_predicate``. (Martin Pool) * When a progress: directive is encountered in a subunit stream, the -- cgit v1.2.1 From dcb833258d8b4d8dfa3e856cd07e7e932f14e268 Mon Sep 17 00:00:00 2001 From: Thomi Richards Date: Tue, 19 Nov 2013 14:45:03 +1300 Subject: Made help/usage documentation much more useful. --- python/subunit/_output.py | 12 ++++++++++-- 1 file changed, 10 insertions(+), 2 deletions(-) diff --git a/python/subunit/_output.py b/python/subunit/_output.py index ba6d0ce..fe4585f 100644 --- a/python/subunit/_output.py +++ b/python/subunit/_output.py @@ -41,6 +41,10 @@ def parse_arguments(args=None, ParserClass=ArgumentParser): parser = ParserClass( prog='subunit-output', description="A tool to generate a subunit result byte-stream", + usage="""%(prog)s [-h] action [-h] test [--attach-file ATTACH_FILE] + [--mimetype MIMETYPE] [--tags TAGS]""", + epilog="""Additional help can be printed by passing -h to an action + (e.g.- '%(prog)s pass -h' will show help for the 'pass' action).""" ) common_args = ParserClass(add_help=False) @@ -66,7 +70,11 @@ def parse_arguments(args=None, ParserClass=ArgumentParser): type=partial(split, sep=','), default=None ) - sub_parsers = parser.add_subparsers(dest="action") + sub_parsers = parser.add_subparsers( + dest="action", + title="actions", + description="These actions are supported by this tool", + ) final_state = "This is a final action: No more actions may be generated " \ "for this test id after this one." @@ -80,7 +88,7 @@ def parse_arguments(args=None, ParserClass=ArgumentParser): parser_pass = sub_parsers.add_parser( "pass", help="Pass a test. " + final_state, - parents=[common_args] + parents=[common_args], ) parser_fail = sub_parsers.add_parser( -- cgit v1.2.1 From 783e8f2095a4fdc0cfe8a159800df2af0ac5ac88 Mon Sep 17 00:00:00 2001 From: Thomi Richards Date: Tue, 19 Nov 2013 14:54:02 +1300 Subject: PEP8 fixes. --- python/subunit/_output.py | 33 ++++++++++++---------- python/subunit/tests/test_output_filter.py | 44 ++++++++++++++++++++++-------- 2 files changed, 51 insertions(+), 26 deletions(-) diff --git a/python/subunit/_output.py b/python/subunit/_output.py index fe4585f..e3f3bc4 100644 --- a/python/subunit/_output.py +++ b/python/subunit/_output.py @@ -21,6 +21,7 @@ from string import split from subunit.v2 import StreamResultToBytes + def output_main(): args = parse_arguments() output = get_output_stream_writer() @@ -59,8 +60,8 @@ def parse_arguments(args=None, ParserClass=ArgumentParser): ) common_args.add_argument( "--mimetype", - help="The mime type to send with this file. This is only used if the "\ - "--attach-file argument is used. This argument is optional. If it is "\ + help="The mime type to send with this file. This is only used if the " + "--attach-file argument is used. This argument is optional. If it is " "not specified, the file will be sent wihtout a mime type.", default=None ) @@ -76,7 +77,7 @@ def parse_arguments(args=None, ParserClass=ArgumentParser): description="These actions are supported by this tool", ) - final_state = "This is a final action: No more actions may be generated " \ + final_state = "This is a final action: No more actions may be generated "\ "for this test id after this one." parser_start = sub_parsers.add_parser( @@ -111,15 +112,15 @@ def parse_arguments(args=None, ParserClass=ArgumentParser): parser_expected_fail = sub_parsers.add_parser( "expected-fail", - help="Marks a test as failing expectedly (this is not counted as a "\ - "failure). " + final_state, + help="Marks a test as failing expectedly (this is not counted as a " + "failure). " + final_state, parents=[common_args], ) parser_unexpected_success = sub_parsers.add_parser( "unexpected-success", - help="Marks a test as succeeding unexpectedly (this is counted as a "\ - "failure). " + final_state, + help="Marks a test as succeeding unexpectedly (this is counted as a " + "failure). " + final_state, parents=[common_args], ) @@ -162,7 +163,7 @@ def generate_bytestream(args, output_writer): def write_chunked_file(file_obj, test_id, output_writer, chunk_size=1024, - mime_type=None): + mime_type=None): reader = partial(file_obj.read, chunk_size) for chunk in iter(reader, ''): output_writer.status( @@ -173,23 +174,25 @@ def write_chunked_file(file_obj, test_id, output_writer, chunk_size=1024, eof=False, ) output_writer.status( - test_id=test_id, - file_name=file_obj.name, - file_bytes='', - mime_type=mime_type, - eof=True, - ) + test_id=test_id, + file_name=file_obj.name, + file_bytes='', + mime_type=mime_type, + eof=True, + ) _ZERO = datetime.timedelta(0) class UTC(datetime.tzinfo): - """UTC""" + def utcoffset(self, dt): return _ZERO + def tzname(self, dt): return "UTC" + def dst(self, dt): return _ZERO diff --git a/python/subunit/tests/test_output_filter.py b/python/subunit/tests/test_output_filter.py index 2a70a2c..1359c46 100644 --- a/python/subunit/tests/test_output_filter.py +++ b/python/subunit/tests/test_output_filter.py @@ -46,8 +46,10 @@ class SafeArgumentParser(argparse.ArgumentParser): """An ArgumentParser class that doesn't call sys.exit.""" def exit(self, status=0, message=""): - raise RuntimeError("ArgumentParser requested to exit with status "\ - " %d and message %r" % (status, message)) + raise RuntimeError( + "ArgumentParser requested to exit with status %d and message %r" + % (status, message) + ) safe_parse_arguments = partial(parse_arguments, ParserClass=SafeArgumentParser) @@ -76,10 +78,22 @@ class OutputFilterArgumentParserTests(TestCase): self._test_command(command, self.getUniqueString()) def test_command_translation(self): - self.assertThat(translate_command_name('start'), Equals('inprogress')) - self.assertThat(translate_command_name('pass'), Equals('success')) - self.assertThat(translate_command_name('expected-fail'), Equals('xfail')) - self.assertThat(translate_command_name('unexpected-success'), Equals('uxsuccess')) + self.assertThat( + translate_command_name('start'), + Equals('inprogress') + ) + self.assertThat( + translate_command_name('pass'), + Equals('success') + ) + self.assertThat( + translate_command_name('expected-fail'), + Equals('xfail') + ) + self.assertThat( + translate_command_name('unexpected-success'), + Equals('uxsuccess') + ) for command in ('fail', 'skip', 'exists'): self.assertThat(translate_command_name(command), Equals(command)) @@ -104,7 +118,7 @@ class OutputFilterArgumentParserTests(TestCase): args = safe_parse_arguments( args=[command, 'foo', '--tags', "foo,bar,baz"] ) - self.assertThat(args.tags, Equals(["foo","bar","baz"])) + self.assertThat(args.tags, Equals(["foo", "bar", "baz"])) class ByteStreamCompatibilityTests(TestCase): @@ -252,7 +266,7 @@ class ByteStreamCompatibilityTests(TestCase): MatchesCall( call='status', test_id='foo', - test_tags=set(['hello','world']), + test_tags=set(['hello', 'world']), timestamp=self._dummy_timestamp, ) ) @@ -261,7 +275,7 @@ class ByteStreamCompatibilityTests(TestCase): class FileChunkingTests(TestCase): def _write_chunk_file(self, file_data, chunk_size, mimetype=None): - """Write chunked data to a subunit stream, return a StreamResult object.""" + """Write file data to a subunit stream, get a StreamResult object.""" stream = BytesIO() output_writer = StreamResultToBytes(output_stream=stream) @@ -269,7 +283,13 @@ class FileChunkingTests(TestCase): f.write(file_data) f.seek(0) - write_chunked_file(f, 'foo_test', output_writer, chunk_size, mimetype) + write_chunked_file( + f, + 'foo_test', + output_writer, + chunk_size, + mimetype + ) stream.seek(0) @@ -333,7 +353,9 @@ class MatchesCall(Matcher): try: pos = self._position_lookup[k] if call_tuple[pos] != v: - return Mismatch("Value for key is %r, not %r" % (call_tuple[pos], v)) + return Mismatch( + "Value for key is %r, not %r" % (call_tuple[pos], v) + ) except IndexError: return Mismatch("Key %s is not present." % k) -- cgit v1.2.1 From 9e0907fe6e961e0007584d97283d87cb7a720bb7 Mon Sep 17 00:00:00 2001 From: Thomi Richards Date: Tue, 19 Nov 2013 14:56:14 +1300 Subject: Fix things pyflakes complains about. --- python/subunit/_output.py | 14 +++++++------- python/subunit/tests/test_output_filter.py | 1 - 2 files changed, 7 insertions(+), 8 deletions(-) diff --git a/python/subunit/_output.py b/python/subunit/_output.py index e3f3bc4..b3ab675 100644 --- a/python/subunit/_output.py +++ b/python/subunit/_output.py @@ -80,44 +80,44 @@ def parse_arguments(args=None, ParserClass=ArgumentParser): final_state = "This is a final action: No more actions may be generated "\ "for this test id after this one." - parser_start = sub_parsers.add_parser( + sub_parsers.add_parser( "start", help="Start a test.", parents=[common_args] ) - parser_pass = sub_parsers.add_parser( + sub_parsers.add_parser( "pass", help="Pass a test. " + final_state, parents=[common_args], ) - parser_fail = sub_parsers.add_parser( + sub_parsers.add_parser( "fail", help="Fail a test. " + final_state, parents=[common_args] ) - parser_skip = sub_parsers.add_parser( + sub_parsers.add_parser( "skip", help="Skip a test. " + final_state, parents=[common_args] ) - parser_exists = sub_parsers.add_parser( + sub_parsers.add_parser( "exists", help="Marks a test as existing. " + final_state, parents=[common_args] ) - parser_expected_fail = sub_parsers.add_parser( + sub_parsers.add_parser( "expected-fail", help="Marks a test as failing expectedly (this is not counted as a " "failure). " + final_state, parents=[common_args], ) - parser_unexpected_success = sub_parsers.add_parser( + sub_parsers.add_parser( "unexpected-success", help="Marks a test as succeeding unexpectedly (this is counted as a " "failure). " + final_state, diff --git a/python/subunit/tests/test_output_filter.py b/python/subunit/tests/test_output_filter.py index 1359c46..fac47ff 100644 --- a/python/subunit/tests/test_output_filter.py +++ b/python/subunit/tests/test_output_filter.py @@ -16,7 +16,6 @@ import argparse -from collections import namedtuple import datetime from functools import partial from io import BytesIO -- cgit v1.2.1 From 30bca6a29df3a608f9b1b4ad5f0a8d561e478e15 Mon Sep 17 00:00:00 2001 From: Thomi Richards Date: Tue, 19 Nov 2013 15:28:28 +1300 Subject: Python 3 compatibility fixes. --- python/subunit/_output.py | 10 +++++----- python/subunit/tests/test_output_filter.py | 26 +++++++++++++------------- 2 files changed, 18 insertions(+), 18 deletions(-) diff --git a/python/subunit/_output.py b/python/subunit/_output.py index b3ab675..dd81b87 100644 --- a/python/subunit/_output.py +++ b/python/subunit/_output.py @@ -17,7 +17,7 @@ from argparse import ArgumentParser import datetime from functools import partial from sys import stdout -from string import split +from testtools.compat import _b from subunit.v2 import StreamResultToBytes @@ -55,7 +55,7 @@ def parse_arguments(args=None, ParserClass=ArgumentParser): ) common_args.add_argument( "--attach-file", - type=file, + type=open, help="Attach a file to the result stream for this test." ) common_args.add_argument( @@ -68,7 +68,7 @@ def parse_arguments(args=None, ParserClass=ArgumentParser): common_args.add_argument( "--tags", help="A comma-separated list of tags to associate with this test.", - type=partial(split, sep=','), + type=lambda s: s.split(','), default=None ) sub_parsers = parser.add_subparsers( @@ -165,7 +165,7 @@ def generate_bytestream(args, output_writer): def write_chunked_file(file_obj, test_id, output_writer, chunk_size=1024, mime_type=None): reader = partial(file_obj.read, chunk_size) - for chunk in iter(reader, ''): + for chunk in iter(reader, _b('')): output_writer.status( test_id=test_id, file_name=file_obj.name, @@ -176,7 +176,7 @@ def write_chunked_file(file_obj, test_id, output_writer, chunk_size=1024, output_writer.status( test_id=test_id, file_name=file_obj.name, - file_bytes='', + file_bytes=_b(''), mime_type=mime_type, eof=True, ) diff --git a/python/subunit/tests/test_output_filter.py b/python/subunit/tests/test_output_filter.py index fac47ff..be42ea6 100644 --- a/python/subunit/tests/test_output_filter.py +++ b/python/subunit/tests/test_output_filter.py @@ -21,6 +21,7 @@ from functools import partial from io import BytesIO from tempfile import NamedTemporaryFile from testtools import TestCase +from testtools.compat import _b from testtools.matchers import ( Equals, IsInstance, @@ -102,7 +103,6 @@ class OutputFilterArgumentParserTests(TestCase): args = safe_parse_arguments( args=[command, 'foo', '--attach-file', tmp_file.name] ) - self.assertThat(args.attach_file, IsInstance(file)) self.assertThat(args.attach_file.name, Equals(tmp_file.name)) def test_all_commands_accept_mimetype_argument(self): @@ -298,26 +298,26 @@ class FileChunkingTests(TestCase): return result def test_file_chunk_size_is_honored(self): - result = self._write_chunk_file("Hello", 1) + result = self._write_chunk_file(_b("Hello"), 1) self.assertThat( result._events, MatchesListwise([ - MatchesCall(call='status', file_bytes='H', mime_type=None, eof=False), - MatchesCall(call='status', file_bytes='e', mime_type=None, eof=False), - MatchesCall(call='status', file_bytes='l', mime_type=None, eof=False), - MatchesCall(call='status', file_bytes='l', mime_type=None, eof=False), - MatchesCall(call='status', file_bytes='o', mime_type=None, eof=False), - MatchesCall(call='status', file_bytes='', mime_type=None, eof=True), + MatchesCall(call='status', file_bytes=_b('H'), mime_type=None, eof=False), + MatchesCall(call='status', file_bytes=_b('e'), mime_type=None, eof=False), + MatchesCall(call='status', file_bytes=_b('l'), mime_type=None, eof=False), + MatchesCall(call='status', file_bytes=_b('l'), mime_type=None, eof=False), + MatchesCall(call='status', file_bytes=_b('o'), mime_type=None, eof=False), + MatchesCall(call='status', file_bytes=_b(''), mime_type=None, eof=True), ]) ) def test_file_mimetype_is_honored(self): - result = self._write_chunk_file("SomeData", 1024, "text/plain") + result = self._write_chunk_file(_b("SomeData"), 1024, "text/plain") self.assertThat( result._events, MatchesListwise([ - MatchesCall(call='status', file_bytes='SomeData', mime_type="text/plain"), - MatchesCall(call='status', file_bytes='', mime_type="text/plain"), + MatchesCall(call='status', file_bytes=_b('SomeData'), mime_type="text/plain"), + MatchesCall(call='status', file_bytes=_b(''), mime_type="text/plain"), ]) ) @@ -339,10 +339,10 @@ class MatchesCall(Matcher): } def __init__(self, **kwargs): - unknown_kwargs = filter( + unknown_kwargs = list(filter( lambda k: k not in self._position_lookup, kwargs - ) + )) if unknown_kwargs: raise ValueError("Unknown keywords: %s" % ','.join(unknown_kwargs)) self._filters = kwargs -- cgit v1.2.1 From 31de7c17f4f9239be6d8c91302f59633117c2012 Mon Sep 17 00:00:00 2001 From: Thomi Richards Date: Tue, 19 Nov 2013 15:37:28 +1300 Subject: Remove shebang from subunit._output module. --- python/subunit/_output.py | 1 - 1 file changed, 1 deletion(-) diff --git a/python/subunit/_output.py b/python/subunit/_output.py index dd81b87..c8b6a21 100644 --- a/python/subunit/_output.py +++ b/python/subunit/_output.py @@ -1,4 +1,3 @@ -#!/usr/bin/env python # subunit: extensions to python unittest to get test results from subprocesses. # Copyright (C) 2013 Thomi Richards # -- cgit v1.2.1 From 8ac7e16b1c5e83a55ee3cd76086b5c2eb74fe09e Mon Sep 17 00:00:00 2001 From: Thomi Richards Date: Wed, 20 Nov 2013 11:06:07 +1300 Subject: Lots of code cleanup, about to refactor argument parsing. --- filters/subunit-output | 2 +- python/subunit/_output.py | 190 +++++++++++++++-------------- python/subunit/tests/test_output_filter.py | 143 ++++++++++++---------- setup.py | 1 + 4 files changed, 182 insertions(+), 154 deletions(-) diff --git a/filters/subunit-output b/filters/subunit-output index 12c68f5..cb2af2d 100644 --- a/filters/subunit-output +++ b/filters/subunit-output @@ -1,6 +1,6 @@ #!/usr/bin/env python # subunit: extensions to python unittest to get test results from subprocesses. -# Copyright (C) 2013 Thomi Richards +# Copyright (C) 2013 'Subunit Contributors' # # Licensed under either the Apache License, Version 2.0 or the BSD 3-clause # license at the users choice. A copy of both licenses are available in the diff --git a/python/subunit/_output.py b/python/subunit/_output.py index c8b6a21..c65fbe0 100644 --- a/python/subunit/_output.py +++ b/python/subunit/_output.py @@ -1,5 +1,5 @@ # subunit: extensions to python unittest to get test results from subprocesses. -# Copyright (C) 2013 Thomi Richards +# Copyright (C) 2013 'Subunit Contributors' # # Licensed under either the Apache License, Version 2.0 or the BSD 3-clause # license at the users choice. A copy of both licenses are available in the @@ -15,9 +15,11 @@ from argparse import ArgumentParser import datetime from functools import partial -from sys import stdout +from sys import stdin, stdout + from testtools.compat import _b +from subunit.iso8601 import UTC from subunit.v2 import StreamResultToBytes @@ -25,7 +27,6 @@ def output_main(): args = parse_arguments() output = get_output_stream_writer() generate_bytestream(args, output) - return 0 @@ -36,40 +37,53 @@ def parse_arguments(args=None, ParserClass=ArgumentParser): ParserClass can be specified to override the class we use to parse the command-line arguments. This is useful for testing. - """ - parser = ParserClass( - prog='subunit-output', - description="A tool to generate a subunit result byte-stream", - usage="""%(prog)s [-h] action [-h] test [--attach-file ATTACH_FILE] - [--mimetype MIMETYPE] [--tags TAGS]""", - epilog="""Additional help can be printed by passing -h to an action - (e.g.- '%(prog)s pass -h' will show help for the 'pass' action).""" - ) - common_args = ParserClass(add_help=False) - common_args.add_argument( - "test_id", - help="A string that uniquely identifies this test." - ) - common_args.add_argument( + file_args = ParserClass(add_help=False) + file_args.add_argument( "--attach-file", - type=open, - help="Attach a file to the result stream for this test." + help="Attach a file to the result stream for this test. If '-' is " + "specified, stdin will be read instead. In this case, the file " + "name will be set to 'stdin' (but can still be overridden with " + "the --file-name option)." ) - common_args.add_argument( + file_args.add_argument( + "--file-name", + help="The name to give this file attachment. If not specified, the " + "name of the file on disk will be used, or 'stdin' in the case " + "where '-' was passed to the '--attach-file' argument. This option" + " may only be specified when '--attach-file' is specified.", + ) + file_args.add_argument( "--mimetype", help="The mime type to send with this file. This is only used if the " - "--attach-file argument is used. This argument is optional. If it is " - "not specified, the file will be sent wihtout a mime type.", + "--attach-file argument is used. This argument is optional. If it " + "is not specified, the file will be sent wihtout a mime type. This " + "option may only be specified when '--attach-file' is specified.", default=None ) + + common_args = ParserClass(add_help=False) + common_args.add_argument( + "test_id", + help="A string that uniquely identifies this test." + ) common_args.add_argument( "--tags", help="A comma-separated list of tags to associate with this test.", type=lambda s: s.split(','), default=None ) + + parser = ParserClass( + prog='subunit-output', + description="A tool to generate a subunit result byte-stream", + usage="%(prog)s [-h] action [-h] test [--attach-file ATTACH_FILE]" + "[--mimetype MIMETYPE] [--tags TAGS]", + epilog="Additional help can be printed by passing -h to an action" + "(e.g.- '%(prog)s pass -h' will show help for the 'pass' action).", + parents=[file_args] + ) sub_parsers = parser.add_subparsers( dest="action", title="actions", @@ -80,63 +94,65 @@ def parse_arguments(args=None, ParserClass=ArgumentParser): "for this test id after this one." sub_parsers.add_parser( - "start", - help="Start a test.", - parents=[common_args] + "inprogress", + help="Report that a test is in progress.", + parents=[common_args, file_args] ) sub_parsers.add_parser( - "pass", - help="Pass a test. " + final_state, - parents=[common_args], + "success", + help="Report that a test has succeeded. " + final_state, + parents=[common_args, file_args], ) sub_parsers.add_parser( "fail", - help="Fail a test. " + final_state, - parents=[common_args] + help="Report that a test has failed. " + final_state, + parents=[common_args, file_args] ) sub_parsers.add_parser( "skip", - help="Skip a test. " + final_state, - parents=[common_args] + help="Report that a test was skipped. " + final_state, + parents=[common_args, file_args] ) sub_parsers.add_parser( "exists", - help="Marks a test as existing. " + final_state, - parents=[common_args] + help="Report that a test exists. " + final_state, + parents=[common_args, file_args] ) sub_parsers.add_parser( - "expected-fail", - help="Marks a test as failing expectedly (this is not counted as a " - "failure). " + final_state, - parents=[common_args], + "xfail", + help="Report that a test has failed expectedly (this is not counted as " + "a failure). " + final_state, + parents=[common_args, file_args], ) sub_parsers.add_parser( - "unexpected-success", - help="Marks a test as succeeding unexpectedly (this is counted as a " - "failure). " + final_state, - parents=[common_args], + "uxsuccess", + help="Report that a test has succeeded unexpectedly (this is counted " + " as a failure). " + final_state, + parents=[common_args, file_args], ) - return parser.parse_args(args) - - -def translate_command_name(command_name): - """Turn the friendly command names we show users on the command line into - something subunit understands. - - """ - return { - 'start': 'inprogress', - 'pass': 'success', - 'expected-fail': 'xfail', - 'unexpected-success': 'uxsuccess', - }.get(command_name, command_name) + args = parser.parse_args(args) + if args.mimetype and not args.attach_file: + parser.error("Cannot specify --mimetype without --attach_file") + if args.file_name and not args.attach_file: + parser.error("Cannot specify --file-name without --attach_file") + if args.attach_file: + if args.attach_file == '-': + if not args.file_name: + args.file_name = 'stdin' + args.attach_file = stdin + else: + try: + args.attach_file = open(args.attach_file) + except IOError as e: + parser.error("Cannot open %s (%s)" % (args.attach_file, e.strerror)) + return args def get_output_stream_writer(): @@ -147,57 +163,49 @@ def generate_bytestream(args, output_writer): output_writer.startTestRun() if args.attach_file: write_chunked_file( - args.attach_file, - args.test_id, - output_writer, - args.mimetype, + file_obj=args.attach_file, + test_id=args.test_id, + output_writer=output_writer, + mime_type=args.mimetype, ) output_writer.status( test_id=args.test_id, - test_status=translate_command_name(args.action), + test_status=args.action, timestamp=create_timestamp(), test_tags=args.tags, ) output_writer.stopTestRun() -def write_chunked_file(file_obj, test_id, output_writer, chunk_size=1024, - mime_type=None): +def write_chunked_file(file_obj, output_writer, chunk_size=1024, + mime_type=None, test_id=None, file_name=None): reader = partial(file_obj.read, chunk_size) + + write_status = output_writer.status + if mime_type is not None: + write_status = partial( + write_status, + mime_type=mime_type + ) + if test_id is not None: + write_status = partial( + write_status, + test_id=test_id + ) + filename = file_name if file_name else file_obj.name + for chunk in iter(reader, _b('')): - output_writer.status( - test_id=test_id, - file_name=file_obj.name, + write_status( + file_name=filename, file_bytes=chunk, - mime_type=mime_type, eof=False, ) - output_writer.status( - test_id=test_id, - file_name=file_obj.name, + write_status( + file_name=filename, file_bytes=_b(''), - mime_type=mime_type, eof=True, ) -_ZERO = datetime.timedelta(0) - - -class UTC(datetime.tzinfo): - - def utcoffset(self, dt): - return _ZERO - - def tzname(self, dt): - return "UTC" - - def dst(self, dt): - return _ZERO - - -utc = UTC() - - def create_timestamp(): - return datetime.datetime.now(utc) + return datetime.datetime.now(UTC) diff --git a/python/subunit/tests/test_output_filter.py b/python/subunit/tests/test_output_filter.py index be42ea6..8fda9aa 100644 --- a/python/subunit/tests/test_output_filter.py +++ b/python/subunit/tests/test_output_filter.py @@ -1,6 +1,6 @@ # # subunit: extensions to python unittest to get test results from subprocesses. -# Copyright (C) 2005 Thomi Richards +# Copyright (C) 2013 'Subunit Contributors' # # Licensed under either the Apache License, Version 2.0 or the BSD 3-clause # license at the users choice. A copy of both licenses are available in the @@ -18,10 +18,13 @@ import argparse import datetime from functools import partial -from io import BytesIO +from io import BytesIO, StringIO +import sys from tempfile import NamedTemporaryFile + +from testscenarios import WithScenarios from testtools import TestCase -from testtools.compat import _b +from testtools.compat import _b, _u from testtools.matchers import ( Equals, IsInstance, @@ -31,12 +34,11 @@ from testtools.matchers import ( ) from testtools.testresult.doubles import StreamResult +from subunit.iso8601 import UTC from subunit.v2 import StreamResultToBytes, ByteStreamToStreamResult from subunit._output import ( generate_bytestream, parse_arguments, - translate_command_name, - utc, write_chunked_file, ) import subunit._output as _o @@ -55,17 +57,19 @@ class SafeArgumentParser(argparse.ArgumentParser): safe_parse_arguments = partial(parse_arguments, ParserClass=SafeArgumentParser) -class OutputFilterArgumentParserTests(TestCase): +class TestStatusArgParserTests(WithScenarios, TestCase): - _all_supported_commands = ( - 'exists', - 'expected-fail', - 'fail', - 'pass', - 'skip', - 'start', - 'unexpected-success', - ) + scenarios = [ + (cmd, dict(command=cmd)) for cmd in ( + 'exists', + 'xfail', + 'fail', + 'success', + 'skip', + 'inprogress', + 'uxsuccess', + ) + ] def _test_command(self, command, test_id): args = safe_parse_arguments(args=[command, test_id]) @@ -74,55 +78,49 @@ class OutputFilterArgumentParserTests(TestCase): self.assertThat(args.test_id, Equals(test_id)) def test_can_parse_all_commands_with_test_id(self): - for command in self._all_supported_commands: - self._test_command(command, self.getUniqueString()) - - def test_command_translation(self): - self.assertThat( - translate_command_name('start'), - Equals('inprogress') - ) - self.assertThat( - translate_command_name('pass'), - Equals('success') - ) - self.assertThat( - translate_command_name('expected-fail'), - Equals('xfail') - ) - self.assertThat( - translate_command_name('unexpected-success'), - Equals('uxsuccess') - ) - for command in ('fail', 'skip', 'exists'): - self.assertThat(translate_command_name(command), Equals(command)) + self._test_command(self.command, self.getUniqueString()) def test_all_commands_parse_file_attachment(self): with NamedTemporaryFile() as tmp_file: - for command in self._all_supported_commands: - args = safe_parse_arguments( - args=[command, 'foo', '--attach-file', tmp_file.name] - ) - self.assertThat(args.attach_file.name, Equals(tmp_file.name)) + args = safe_parse_arguments( + args=[self.command, 'foo', '--attach-file', tmp_file.name] + ) + self.assertThat(args.attach_file.name, Equals(tmp_file.name)) def test_all_commands_accept_mimetype_argument(self): - for command in self._all_supported_commands: + with NamedTemporaryFile() as tmp_file: args = safe_parse_arguments( - args=[command, 'foo', '--mimetype', "text/plain"] + args=[self.command, 'foo', '--attach-file', tmp_file.name, '--mimetype', "text/plain"] ) self.assertThat(args.mimetype, Equals("text/plain")) def test_all_commands_accept_tags_argument(self): - for command in self._all_supported_commands: + args = safe_parse_arguments( + args=[self.command, 'foo', '--tags', "foo,bar,baz"] + ) + self.assertThat(args.tags, Equals(["foo", "bar", "baz"])) + + def test_attach_file_with_hyphen_opens_stdin(self): + self.patch(_o, 'stdin', StringIO(_u("Hello"))) + args = safe_parse_arguments( + args=[self.command, "foo", "--attach-file", "-"] + ) + + self.assertThat(args.attach_file.read(), Equals("Hello")) + + +class GlobalFileAttachmentTests(TestCase): + + def test_can_parse_attach_file_without_test_id(self): + with NamedTemporaryFile() as tmp_file: args = safe_parse_arguments( - args=[command, 'foo', '--tags', "foo,bar,baz"] + args=["--attach-file", tmp_file.name] ) - self.assertThat(args.tags, Equals(["foo", "bar", "baz"])) - + self.assertThat(args.attach_file.name, Equals(tmp_file.name)) class ByteStreamCompatibilityTests(TestCase): - _dummy_timestamp = datetime.datetime(2013, 1, 1, 0, 0, 0, 0, utc) + _dummy_timestamp = datetime.datetime(2013, 1, 1, 0, 0, 0, 0, UTC) def setUp(self): super(ByteStreamCompatibilityTests, self).setUp() @@ -135,7 +133,6 @@ class ByteStreamCompatibilityTests(TestCase): parsing *commands as if they were specified on the command line. The resulting bytestream is then converted back into a result object and returned. - """ stream = BytesIO() @@ -153,7 +150,7 @@ class ByteStreamCompatibilityTests(TestCase): def test_start_generates_inprogress(self): result = self._get_result_for( - ['start', 'foo'], + ['inprogress', 'foo'], ) self.assertThat( @@ -168,7 +165,7 @@ class ByteStreamCompatibilityTests(TestCase): def test_pass_generates_success(self): result = self._get_result_for( - ['pass', 'foo'], + ['success', 'foo'], ) self.assertThat( @@ -228,7 +225,7 @@ class ByteStreamCompatibilityTests(TestCase): def test_expected_fail_generates_xfail(self): result = self._get_result_for( - ['expected-fail', 'foo'], + ['xfail', 'foo'], ) self.assertThat( @@ -243,7 +240,7 @@ class ByteStreamCompatibilityTests(TestCase): def test_unexpected_success_generates_uxsuccess(self): result = self._get_result_for( - ['unexpected-success', 'foo'], + ['uxsuccess', 'foo'], ) self.assertThat( @@ -273,21 +270,23 @@ class ByteStreamCompatibilityTests(TestCase): class FileChunkingTests(TestCase): - def _write_chunk_file(self, file_data, chunk_size, mimetype=None): + def _write_chunk_file(self, file_data, chunk_size=1024, mimetype=None, filename=None): """Write file data to a subunit stream, get a StreamResult object.""" stream = BytesIO() output_writer = StreamResultToBytes(output_stream=stream) with NamedTemporaryFile() as f: + self._tmp_filename = f.name f.write(file_data) f.seek(0) write_chunked_file( - f, - 'foo_test', - output_writer, - chunk_size, - mimetype + file_obj=f, + output_writer=output_writer, + chunk_size=chunk_size, + mime_type=mimetype, + test_id='foo_test', + file_name=filename, ) stream.seek(0) @@ -298,7 +297,7 @@ class FileChunkingTests(TestCase): return result def test_file_chunk_size_is_honored(self): - result = self._write_chunk_file(_b("Hello"), 1) + result = self._write_chunk_file(file_data=_b("Hello"), chunk_size=1) self.assertThat( result._events, MatchesListwise([ @@ -312,7 +311,7 @@ class FileChunkingTests(TestCase): ) def test_file_mimetype_is_honored(self): - result = self._write_chunk_file(_b("SomeData"), 1024, "text/plain") + result = self._write_chunk_file(file_data=_b("SomeData"), mimetype="text/plain") self.assertThat( result._events, MatchesListwise([ @@ -321,6 +320,26 @@ class FileChunkingTests(TestCase): ]) ) + def test_file_name_is_honored(self): + result = self._write_chunk_file(file_data=_b("data"), filename="/some/name") + self.assertThat( + result._events, + MatchesListwise([ + MatchesCall(call='status', file_name='/some/name'), + MatchesCall(call='status', file_name='/some/name'), + ]) + ) + + def test_default_filename_is_used(self): + result = self._write_chunk_file(file_data=_b("data")) + self.assertThat( + result._events, + MatchesListwise([ + MatchesCall(call='status', file_name=self._tmp_filename), + MatchesCall(call='status', file_name=self._tmp_filename), + ]) + ) + class MatchesCall(Matcher): diff --git a/setup.py b/setup.py index d319d9c..2f22300 100755 --- a/setup.py +++ b/setup.py @@ -11,6 +11,7 @@ else: 'install_requires': [ 'extras', 'testtools>=0.9.30', + 'testscenarios', ] } -- cgit v1.2.1 From 11c444a34f8e64211feb0072e7f9b6bd1902a212 Mon Sep 17 00:00:00 2001 From: Thomi Richards Date: Wed, 20 Nov 2013 14:09:48 +1300 Subject: Switch to using command line options to specify status. Expand help output, and refactor several test cases. --- python/subunit/_output.py | 139 +++++++-------- python/subunit/tests/test_output_filter.py | 261 +++++++++++++---------------- 2 files changed, 179 insertions(+), 221 deletions(-) diff --git a/python/subunit/_output.py b/python/subunit/_output.py index c65fbe0..ae405ef 100644 --- a/python/subunit/_output.py +++ b/python/subunit/_output.py @@ -12,7 +12,7 @@ # license you chose for the specific language governing permissions and # limitations under that license. -from argparse import ArgumentParser +from argparse import ArgumentError, ArgumentParser, Action import datetime from functools import partial from sys import stdin, stdout @@ -37,24 +37,74 @@ def parse_arguments(args=None, ParserClass=ArgumentParser): ParserClass can be specified to override the class we use to parse the command-line arguments. This is useful for testing. + """ - file_args = ParserClass(add_help=False) - file_args.add_argument( + class StatusAction(Action): + """A custom action that stores option name and argument separately. + + This is part of a workaround for the fact that argparse does not + support optional subcommands (http://bugs.python.org/issue9253). + """ + + def __init__(self, status_name, *args, **kwargs): + super(StatusAction, self).__init__(*args, **kwargs) + self._status_name = status_name + + def __call__(self, parser, namespace, values, option_string=None): + if getattr(namespace, self.dest, None) is not None: + raise ArgumentError(self, "Only one status may be specified at once.") + setattr(namespace, self.dest, self._status_name) + setattr(namespace, 'test_id', values[0]) + + + parser = ParserClass( + prog='subunit-output', + description="A tool to generate a subunit result byte-stream", + ) + + status_commands = parser.add_argument_group( + "Status Commands", + "These options report the status of a test. TEST_ID must be a string " + "that uniquely identifies the test." + ) + final_actions = 'success fail skip xfail uxsuccess'.split() + for action in "inprogress success fail skip exists xfail uxsuccess".split(): + final_text = "This is a final state: No more status reports may "\ + "be generated for this test id after this one." + + status_commands.add_argument( + "--%s" % action, + nargs=1, + action=partial(StatusAction, action), + dest="action", + metavar="TEST_ID", + help="Report a test status." + final_text if action in final_actions else "" + ) + + file_commands = parser.add_argument_group( + "File Options", + "These options control attaching data to a result stream. They can " + "either be specified with a status command, in which case the file " + "is attached to the test status, or by themselves, in which case " + "the file is attached to the stream (and not associated with any " + "test id)." + ) + file_commands.add_argument( "--attach-file", help="Attach a file to the result stream for this test. If '-' is " "specified, stdin will be read instead. In this case, the file " "name will be set to 'stdin' (but can still be overridden with " "the --file-name option)." ) - file_args.add_argument( + file_commands.add_argument( "--file-name", help="The name to give this file attachment. If not specified, the " "name of the file on disk will be used, or 'stdin' in the case " "where '-' was passed to the '--attach-file' argument. This option" " may only be specified when '--attach-file' is specified.", ) - file_args.add_argument( + file_commands.add_argument( "--mimetype", help="The mime type to send with this file. This is only used if the " "--attach-file argument is used. This argument is optional. If it " @@ -63,85 +113,19 @@ def parse_arguments(args=None, ParserClass=ArgumentParser): default=None ) - common_args = ParserClass(add_help=False) - common_args.add_argument( - "test_id", - help="A string that uniquely identifies this test." - ) - common_args.add_argument( + parser.add_argument( "--tags", - help="A comma-separated list of tags to associate with this test.", + help="A comma-separated list of tags to associate with a test. This " + "option may only be used with a status command.", type=lambda s: s.split(','), default=None ) - parser = ParserClass( - prog='subunit-output', - description="A tool to generate a subunit result byte-stream", - usage="%(prog)s [-h] action [-h] test [--attach-file ATTACH_FILE]" - "[--mimetype MIMETYPE] [--tags TAGS]", - epilog="Additional help can be printed by passing -h to an action" - "(e.g.- '%(prog)s pass -h' will show help for the 'pass' action).", - parents=[file_args] - ) - sub_parsers = parser.add_subparsers( - dest="action", - title="actions", - description="These actions are supported by this tool", - ) - - final_state = "This is a final action: No more actions may be generated "\ - "for this test id after this one." - - sub_parsers.add_parser( - "inprogress", - help="Report that a test is in progress.", - parents=[common_args, file_args] - ) - - sub_parsers.add_parser( - "success", - help="Report that a test has succeeded. " + final_state, - parents=[common_args, file_args], - ) - - sub_parsers.add_parser( - "fail", - help="Report that a test has failed. " + final_state, - parents=[common_args, file_args] - ) - - sub_parsers.add_parser( - "skip", - help="Report that a test was skipped. " + final_state, - parents=[common_args, file_args] - ) - - sub_parsers.add_parser( - "exists", - help="Report that a test exists. " + final_state, - parents=[common_args, file_args] - ) - - sub_parsers.add_parser( - "xfail", - help="Report that a test has failed expectedly (this is not counted as " - "a failure). " + final_state, - parents=[common_args, file_args], - ) - - sub_parsers.add_parser( - "uxsuccess", - help="Report that a test has succeeded unexpectedly (this is counted " - " as a failure). " + final_state, - parents=[common_args, file_args], - ) - args = parser.parse_args(args) if args.mimetype and not args.attach_file: - parser.error("Cannot specify --mimetype without --attach_file") + parser.error("Cannot specify --mimetype without --attach-file") if args.file_name and not args.attach_file: - parser.error("Cannot specify --file-name without --attach_file") + parser.error("Cannot specify --file-name without --attach-file") if args.attach_file: if args.attach_file == '-': if not args.file_name: @@ -152,6 +136,9 @@ def parse_arguments(args=None, ParserClass=ArgumentParser): args.attach_file = open(args.attach_file) except IOError as e: parser.error("Cannot open %s (%s)" % (args.attach_file, e.strerror)) + if args.tags and not args.action: + parser.error("Cannot specify --tags without a status command") + return args diff --git a/python/subunit/tests/test_output_filter.py b/python/subunit/tests/test_output_filter.py index 8fda9aa..102b970 100644 --- a/python/subunit/tests/test_output_filter.py +++ b/python/subunit/tests/test_output_filter.py @@ -31,6 +31,7 @@ from testtools.matchers import ( Matcher, MatchesListwise, Mismatch, + raises, ) from testtools.testresult.doubles import StreamResult @@ -48,10 +49,7 @@ class SafeArgumentParser(argparse.ArgumentParser): """An ArgumentParser class that doesn't call sys.exit.""" def exit(self, status=0, message=""): - raise RuntimeError( - "ArgumentParser requested to exit with status %d and message %r" - % (status, message) - ) + raise RuntimeError(message) safe_parse_arguments = partial(parse_arguments, ParserClass=SafeArgumentParser) @@ -60,56 +58,60 @@ safe_parse_arguments = partial(parse_arguments, ParserClass=SafeArgumentParser) class TestStatusArgParserTests(WithScenarios, TestCase): scenarios = [ - (cmd, dict(command=cmd)) for cmd in ( + (cmd, dict(command=cmd, option='--' + cmd)) for cmd in ( 'exists', - 'xfail', 'fail', - 'success', - 'skip', 'inprogress', + 'skip', + 'success', 'uxsuccess', + 'xfail', ) ] - def _test_command(self, command, test_id): - args = safe_parse_arguments(args=[command, test_id]) + def test_can_parse_all_commands_with_test_id(self): + test_id = self.getUniqueString() + args = safe_parse_arguments(args=[self.option, test_id]) - self.assertThat(args.action, Equals(command)) + self.assertThat(args.action, Equals(self.command)) self.assertThat(args.test_id, Equals(test_id)) - def test_can_parse_all_commands_with_test_id(self): - self._test_command(self.command, self.getUniqueString()) - def test_all_commands_parse_file_attachment(self): with NamedTemporaryFile() as tmp_file: args = safe_parse_arguments( - args=[self.command, 'foo', '--attach-file', tmp_file.name] + args=[self.option, 'foo', '--attach-file', tmp_file.name] ) self.assertThat(args.attach_file.name, Equals(tmp_file.name)) def test_all_commands_accept_mimetype_argument(self): with NamedTemporaryFile() as tmp_file: args = safe_parse_arguments( - args=[self.command, 'foo', '--attach-file', tmp_file.name, '--mimetype', "text/plain"] + args=[self.option, 'foo', '--attach-file', tmp_file.name, '--mimetype', "text/plain"] ) self.assertThat(args.mimetype, Equals("text/plain")) def test_all_commands_accept_tags_argument(self): args = safe_parse_arguments( - args=[self.command, 'foo', '--tags', "foo,bar,baz"] + args=[self.option, 'foo', '--tags', "foo,bar,baz"] ) self.assertThat(args.tags, Equals(["foo", "bar", "baz"])) def test_attach_file_with_hyphen_opens_stdin(self): self.patch(_o, 'stdin', StringIO(_u("Hello"))) args = safe_parse_arguments( - args=[self.command, "foo", "--attach-file", "-"] + args=[self.option, "foo", "--attach-file", "-"] ) self.assertThat(args.attach_file.read(), Equals("Hello")) -class GlobalFileAttachmentTests(TestCase): +class ArgParserTests(TestCase): + + def setUp(self): + super(ArgParserTests, self).setUp() + # prevent ARgumentParser from printing to stderr: + self._stderr = BytesIO() + self.patch(argparse._sys, 'stderr', self._stderr) def test_can_parse_attach_file_without_test_id(self): with NamedTemporaryFile() as tmp_file: @@ -118,145 +120,97 @@ class GlobalFileAttachmentTests(TestCase): ) self.assertThat(args.attach_file.name, Equals(tmp_file.name)) -class ByteStreamCompatibilityTests(TestCase): - - _dummy_timestamp = datetime.datetime(2013, 1, 1, 0, 0, 0, 0, UTC) - - def setUp(self): - super(ByteStreamCompatibilityTests, self).setUp() - self.patch(_o, 'create_timestamp', lambda: self._dummy_timestamp) - - def _get_result_for(self, *commands): - """Get a result object from *commands. - - Runs the 'generate_bytestream' function from subunit._output after - parsing *commands as if they were specified on the command line. The - resulting bytestream is then converted back into a result object and - returned. - """ - stream = BytesIO() - - for command_list in commands: - args = safe_parse_arguments(command_list) - output_writer = StreamResultToBytes(output_stream=stream) - generate_bytestream(args, output_writer) - - stream.seek(0) - - case = ByteStreamToStreamResult(source=stream) - result = StreamResult() - case.run(result) - return result - - def test_start_generates_inprogress(self): - result = self._get_result_for( - ['inprogress', 'foo'], + def test_cannot_specify_more_than_one_status_command(self): + fn = lambda: safe_parse_arguments(['--fail', 'foo', '--skip', 'bar']) + self.assertThat( + fn, + raises(RuntimeError('subunit-output: error: argument --skip: '\ + 'Only one status may be specified at once.\n')) ) + def test_cannot_specify_mimetype_without_attach_file(self): + fn = lambda: safe_parse_arguments(['--mimetype', 'foo']) self.assertThat( - result._events[0], - MatchesCall( - call='status', - test_id='foo', - test_status='inprogress', - timestamp=self._dummy_timestamp, - ) + fn, + raises(RuntimeError('subunit-output: error: Cannot specify '\ + '--mimetype without --attach-file\n')) ) - def test_pass_generates_success(self): - result = self._get_result_for( - ['success', 'foo'], + def test_cannot_specify_filename_without_attach_file(self): + fn = lambda: safe_parse_arguments(['--file-name', 'foo']) + self.assertThat( + fn, + raises(RuntimeError('subunit-output: error: Cannot specify '\ + '--file-name without --attach-file\n')) ) + def test_cannot_specify_tags_without_status_command(self): + fn = lambda: safe_parse_arguments(['--tags', 'foo']) self.assertThat( - result._events[0], - MatchesCall( - call='status', - test_id='foo', - test_status='success', - timestamp=self._dummy_timestamp, - ) + fn, + raises(RuntimeError('subunit-output: error: Cannot specify '\ + '--tags without a status command\n')) ) - def test_fail_generates_fail(self): - result = self._get_result_for( - ['fail', 'foo'], - ) - self.assertThat( - result._events[0], - MatchesCall( - call='status', - test_id='foo', - test_status='fail', - timestamp=self._dummy_timestamp, - ) - ) +def get_result_for(commands): + """Get a result object from *commands. - def test_skip_generates_skip(self): - result = self._get_result_for( - ['skip', 'foo'], - ) + Runs the 'generate_bytestream' function from subunit._output after + parsing *commands as if they were specified on the command line. The + resulting bytestream is then converted back into a result object and + returned. + """ + stream = BytesIO() - self.assertThat( - result._events[0], - MatchesCall( - call='status', - test_id='foo', - test_status='skip', - timestamp=self._dummy_timestamp, - ) - ) + args = safe_parse_arguments(commands) + output_writer = StreamResultToBytes(output_stream=stream) + generate_bytestream(args, output_writer) - def test_exists_generates_exists(self): - result = self._get_result_for( - ['exists', 'foo'], - ) + stream.seek(0) - self.assertThat( - result._events[0], - MatchesCall( - call='status', - test_id='foo', - test_status='exists', - timestamp=self._dummy_timestamp, - ) - ) + case = ByteStreamToStreamResult(source=stream) + result = StreamResult() + case.run(result) + return result - def test_expected_fail_generates_xfail(self): - result = self._get_result_for( - ['xfail', 'foo'], - ) - self.assertThat( - result._events[0], - MatchesCall( - call='status', - test_id='foo', - test_status='xfail', - timestamp=self._dummy_timestamp, - ) - ) +class ByteStreamCompatibilityTests(WithScenarios, TestCase): - def test_unexpected_success_generates_uxsuccess(self): - result = self._get_result_for( - ['uxsuccess', 'foo'], + scenarios = [ + (s, dict(status=s, option='--' + s)) for s in ( + 'exists', + 'fail', + 'inprogress', + 'skip', + 'success', + 'uxsuccess', + 'xfail', ) + ] + + _dummy_timestamp = datetime.datetime(2013, 1, 1, 0, 0, 0, 0, UTC) + + def setUp(self): + super(ByteStreamCompatibilityTests, self).setUp() + self.patch(_o, 'create_timestamp', lambda: self._dummy_timestamp) + + + def test_correct_status_is_generated(self): + result = get_result_for([self.option, 'foo']) self.assertThat( result._events[0], MatchesCall( call='status', test_id='foo', - test_status='uxsuccess', + test_status=self.status, timestamp=self._dummy_timestamp, ) ) - def test_tags_are_generated(self): - result = self._get_result_for( - ['exists', 'foo', '--tags', 'hello,world'] - ) + def test_all_commands_accept_tags(self): + result = get_result_for([self.option, 'foo', '--tags', 'hello,world']) self.assertThat( result._events[0], MatchesCall( @@ -268,9 +222,14 @@ class ByteStreamCompatibilityTests(TestCase): ) -class FileChunkingTests(TestCase): +class FileChunkingTests(WithScenarios, TestCase): - def _write_chunk_file(self, file_data, chunk_size=1024, mimetype=None, filename=None): + scenarios = [ + ("With test_id", dict(test_id="foo")), + ("Without test_id", dict(test_id=None)), + ] + + def _write_chunk_file(self, file_data, chunk_size=1024, mimetype=None, filename=None, test_id=None): """Write file data to a subunit stream, get a StreamResult object.""" stream = BytesIO() output_writer = StreamResultToBytes(output_stream=stream) @@ -285,7 +244,7 @@ class FileChunkingTests(TestCase): output_writer=output_writer, chunk_size=chunk_size, mime_type=mimetype, - test_id='foo_test', + test_id=test_id, file_name=filename, ) @@ -297,36 +256,48 @@ class FileChunkingTests(TestCase): return result def test_file_chunk_size_is_honored(self): - result = self._write_chunk_file(file_data=_b("Hello"), chunk_size=1) + result = self._write_chunk_file( + file_data=_b("Hello"), + chunk_size=1, + test_id=self.test_id, + ) self.assertThat( result._events, MatchesListwise([ - MatchesCall(call='status', file_bytes=_b('H'), mime_type=None, eof=False), - MatchesCall(call='status', file_bytes=_b('e'), mime_type=None, eof=False), - MatchesCall(call='status', file_bytes=_b('l'), mime_type=None, eof=False), - MatchesCall(call='status', file_bytes=_b('l'), mime_type=None, eof=False), - MatchesCall(call='status', file_bytes=_b('o'), mime_type=None, eof=False), - MatchesCall(call='status', file_bytes=_b(''), mime_type=None, eof=True), + MatchesCall(call='status', test_id=self.test_id, file_bytes=_b('H'), mime_type=None, eof=False), + MatchesCall(call='status', test_id=self.test_id, file_bytes=_b('e'), mime_type=None, eof=False), + MatchesCall(call='status', test_id=self.test_id, file_bytes=_b('l'), mime_type=None, eof=False), + MatchesCall(call='status', test_id=self.test_id, file_bytes=_b('l'), mime_type=None, eof=False), + MatchesCall(call='status', test_id=self.test_id, file_bytes=_b('o'), mime_type=None, eof=False), + MatchesCall(call='status', test_id=self.test_id, file_bytes=_b(''), mime_type=None, eof=True), ]) ) def test_file_mimetype_is_honored(self): - result = self._write_chunk_file(file_data=_b("SomeData"), mimetype="text/plain") + result = self._write_chunk_file( + file_data=_b("SomeData"), + mimetype="text/plain", + test_id=self.test_id, + ) self.assertThat( result._events, MatchesListwise([ - MatchesCall(call='status', file_bytes=_b('SomeData'), mime_type="text/plain"), - MatchesCall(call='status', file_bytes=_b(''), mime_type="text/plain"), + MatchesCall(call='status', test_id=self.test_id, file_bytes=_b('SomeData'), mime_type="text/plain"), + MatchesCall(call='status', test_id=self.test_id, file_bytes=_b(''), mime_type="text/plain"), ]) ) def test_file_name_is_honored(self): - result = self._write_chunk_file(file_data=_b("data"), filename="/some/name") + result = self._write_chunk_file( + file_data=_b("data"), + filename="/some/name", + test_id=self.test_id + ) self.assertThat( result._events, MatchesListwise([ - MatchesCall(call='status', file_name='/some/name'), - MatchesCall(call='status', file_name='/some/name'), + MatchesCall(call='status', test_id=self.test_id, file_name='/some/name'), + MatchesCall(call='status', test_id=self.test_id, file_name='/some/name'), ]) ) -- cgit v1.2.1 From 52e8d163f68bfa42d71fbb281ccc5ee6deefa23e Mon Sep 17 00:00:00 2001 From: Thomi Richards Date: Wed, 20 Nov 2013 14:14:25 +1300 Subject: Remove quotes around 'subunit contributors' in copyright headers. --- filters/subunit-output | 2 +- python/subunit/_output.py | 2 +- python/subunit/tests/test_output_filter.py | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/filters/subunit-output b/filters/subunit-output index cb2af2d..61e5d11 100644 --- a/filters/subunit-output +++ b/filters/subunit-output @@ -1,6 +1,6 @@ #!/usr/bin/env python # subunit: extensions to python unittest to get test results from subprocesses. -# Copyright (C) 2013 'Subunit Contributors' +# Copyright (C) 2013 Subunit Contributors # # Licensed under either the Apache License, Version 2.0 or the BSD 3-clause # license at the users choice. A copy of both licenses are available in the diff --git a/python/subunit/_output.py b/python/subunit/_output.py index ae405ef..ff6004e 100644 --- a/python/subunit/_output.py +++ b/python/subunit/_output.py @@ -1,5 +1,5 @@ # subunit: extensions to python unittest to get test results from subprocesses. -# Copyright (C) 2013 'Subunit Contributors' +# Copyright (C) 2013 Subunit Contributors # # Licensed under either the Apache License, Version 2.0 or the BSD 3-clause # license at the users choice. A copy of both licenses are available in the diff --git a/python/subunit/tests/test_output_filter.py b/python/subunit/tests/test_output_filter.py index 102b970..15dce81 100644 --- a/python/subunit/tests/test_output_filter.py +++ b/python/subunit/tests/test_output_filter.py @@ -1,6 +1,6 @@ # # subunit: extensions to python unittest to get test results from subprocesses. -# Copyright (C) 2013 'Subunit Contributors' +# Copyright (C) 2013 Subunit Contributors # # Licensed under either the Apache License, Version 2.0 or the BSD 3-clause # license at the users choice. A copy of both licenses are available in the -- cgit v1.2.1 From 4c9b32360c7f2c3cc82c438d0206dc484c85ad02 Mon Sep 17 00:00:00 2001 From: Thomi Richards Date: Wed, 20 Nov 2013 14:17:36 +1300 Subject: Fix docstring, code shuffle. --- python/subunit/_output.py | 40 +++++++++++++++++++--------------------- 1 file changed, 19 insertions(+), 21 deletions(-) diff --git a/python/subunit/_output.py b/python/subunit/_output.py index ff6004e..08ed3fc 100644 --- a/python/subunit/_output.py +++ b/python/subunit/_output.py @@ -35,29 +35,9 @@ def parse_arguments(args=None, ParserClass=ArgumentParser): If specified, args must be a list of strings, similar to sys.argv[1:]. - ParserClass can be specified to override the class we use to parse the + ParserClass may be specified to override the class we use to parse the command-line arguments. This is useful for testing. - """ - - class StatusAction(Action): - """A custom action that stores option name and argument separately. - - This is part of a workaround for the fact that argparse does not - support optional subcommands (http://bugs.python.org/issue9253). - """ - - def __init__(self, status_name, *args, **kwargs): - super(StatusAction, self).__init__(*args, **kwargs) - self._status_name = status_name - - def __call__(self, parser, namespace, values, option_string=None): - if getattr(namespace, self.dest, None) is not None: - raise ArgumentError(self, "Only one status may be specified at once.") - setattr(namespace, self.dest, self._status_name) - setattr(namespace, 'test_id', values[0]) - - parser = ParserClass( prog='subunit-output', description="A tool to generate a subunit result byte-stream", @@ -142,6 +122,24 @@ def parse_arguments(args=None, ParserClass=ArgumentParser): return args +class StatusAction(Action): + """A custom action that stores option name and argument separately. + + This is part of a workaround for the fact that argparse does not + support optional subcommands (http://bugs.python.org/issue9253). + """ + + def __init__(self, status_name, *args, **kwargs): + super(StatusAction, self).__init__(*args, **kwargs) + self._status_name = status_name + + def __call__(self, parser, namespace, values, option_string=None): + if getattr(namespace, self.dest, None) is not None: + raise ArgumentError(self, "Only one status may be specified at once.") + setattr(namespace, self.dest, self._status_name) + setattr(namespace, 'test_id', values[0]) + + def get_output_stream_writer(): return StreamResultToBytes(stdout) -- cgit v1.2.1 From a1e4e89846b3c236569c2fb275527584635bcd7f Mon Sep 17 00:00:00 2001 From: Thomi Richards Date: Wed, 20 Nov 2013 14:28:49 +1300 Subject: code cleanup, added a few more tests for the --file-name option. --- python/subunit/_output.py | 9 +++++---- python/subunit/tests/test_output_filter.py | 20 ++++++++++++++++++++ 2 files changed, 25 insertions(+), 4 deletions(-) diff --git a/python/subunit/_output.py b/python/subunit/_output.py index 08ed3fc..12479e8 100644 --- a/python/subunit/_output.py +++ b/python/subunit/_output.py @@ -40,7 +40,7 @@ def parse_arguments(args=None, ParserClass=ArgumentParser): """ parser = ParserClass( prog='subunit-output', - description="A tool to generate a subunit result byte-stream", + description="A tool to generate a subunit v2 result byte-stream", ) status_commands = parser.add_argument_group( @@ -48,9 +48,10 @@ def parse_arguments(args=None, ParserClass=ArgumentParser): "These options report the status of a test. TEST_ID must be a string " "that uniquely identifies the test." ) - final_actions = 'success fail skip xfail uxsuccess'.split() - for action in "inprogress success fail skip exists xfail uxsuccess".split(): - final_text = "This is a final state: No more status reports may "\ + final_actions = 'exists fail skip success xfail uxsuccess'.split() + all_actions = final_actions + ['inprogress'] + for action in all_actions: + final_text = " This is a final state: No more status reports may "\ "be generated for this test id after this one." status_commands.add_argument( diff --git a/python/subunit/tests/test_output_filter.py b/python/subunit/tests/test_output_filter.py index 15dce81..ede32dc 100644 --- a/python/subunit/tests/test_output_filter.py +++ b/python/subunit/tests/test_output_filter.py @@ -90,6 +90,13 @@ class TestStatusArgParserTests(WithScenarios, TestCase): ) self.assertThat(args.mimetype, Equals("text/plain")) + def test_all_commands_accept_file_name_argument(self): + with NamedTemporaryFile() as tmp_file: + args = safe_parse_arguments( + args=[self.option, 'foo', '--attach-file', tmp_file.name, '--file-name', "foo"] + ) + self.assertThat(args.file_name, Equals("foo")) + def test_all_commands_accept_tags_argument(self): args = safe_parse_arguments( args=[self.option, 'foo', '--tags', "foo,bar,baz"] @@ -104,6 +111,19 @@ class TestStatusArgParserTests(WithScenarios, TestCase): self.assertThat(args.attach_file.read(), Equals("Hello")) + def test_attach_file_with_hyphen_sets_filename_to_stdin(self): + args = safe_parse_arguments( + args=[self.option, "foo", "--attach-file", "-"] + ) + + self.assertThat(args.file_name, Equals("stdin")) + + def test_can_override_stdin_filename(self): + args = safe_parse_arguments( + args=[self.option, "foo", "--attach-file", "-", '--file-name', 'foo'] + ) + + self.assertThat(args.file_name, Equals("foo")) class ArgParserTests(TestCase): -- cgit v1.2.1 From b19f752ffe8463788244b7d6c1126c9056067eac Mon Sep 17 00:00:00 2001 From: Thomi Richards Date: Wed, 20 Nov 2013 14:33:03 +1300 Subject: PEP8 fixes. --- python/subunit/tests/test_output_filter.py | 39 ++++++++++++++---------------- 1 file changed, 18 insertions(+), 21 deletions(-) diff --git a/python/subunit/tests/test_output_filter.py b/python/subunit/tests/test_output_filter.py index ede32dc..69e5b2a 100644 --- a/python/subunit/tests/test_output_filter.py +++ b/python/subunit/tests/test_output_filter.py @@ -19,7 +19,6 @@ import argparse import datetime from functools import partial from io import BytesIO, StringIO -import sys from tempfile import NamedTemporaryFile from testscenarios import WithScenarios @@ -27,7 +26,6 @@ from testtools import TestCase from testtools.compat import _b, _u from testtools.matchers import ( Equals, - IsInstance, Matcher, MatchesListwise, Mismatch, @@ -125,6 +123,7 @@ class TestStatusArgParserTests(WithScenarios, TestCase): self.assertThat(args.file_name, Equals("foo")) + class ArgParserTests(TestCase): def setUp(self): @@ -144,7 +143,7 @@ class ArgParserTests(TestCase): fn = lambda: safe_parse_arguments(['--fail', 'foo', '--skip', 'bar']) self.assertThat( fn, - raises(RuntimeError('subunit-output: error: argument --skip: '\ + raises(RuntimeError('subunit-output: error: argument --skip: ' 'Only one status may be specified at once.\n')) ) @@ -152,7 +151,7 @@ class ArgParserTests(TestCase): fn = lambda: safe_parse_arguments(['--mimetype', 'foo']) self.assertThat( fn, - raises(RuntimeError('subunit-output: error: Cannot specify '\ + raises(RuntimeError('subunit-output: error: Cannot specify ' '--mimetype without --attach-file\n')) ) @@ -160,7 +159,7 @@ class ArgParserTests(TestCase): fn = lambda: safe_parse_arguments(['--file-name', 'foo']) self.assertThat( fn, - raises(RuntimeError('subunit-output: error: Cannot specify '\ + raises(RuntimeError('subunit-output: error: Cannot specify ' '--file-name without --attach-file\n')) ) @@ -168,7 +167,7 @@ class ArgParserTests(TestCase): fn = lambda: safe_parse_arguments(['--tags', 'foo']) self.assertThat( fn, - raises(RuntimeError('subunit-output: error: Cannot specify '\ + raises(RuntimeError('subunit-output: error: Cannot specify ' '--tags without a status command\n')) ) @@ -215,7 +214,6 @@ class ByteStreamCompatibilityTests(WithScenarios, TestCase): super(ByteStreamCompatibilityTests, self).setUp() self.patch(_o, 'create_timestamp', lambda: self._dummy_timestamp) - def test_correct_status_is_generated(self): result = get_result_for([self.option, 'foo']) @@ -335,18 +333,18 @@ class FileChunkingTests(WithScenarios, TestCase): class MatchesCall(Matcher): _position_lookup = { - 'call': 0, - 'test_id': 1, - 'test_status': 2, - 'test_tags': 3, - 'runnable': 4, - 'file_name': 5, - 'file_bytes': 6, - 'eof': 7, - 'mime_type': 8, - 'route_code': 9, - 'timestamp': 10, - } + 'call': 0, + 'test_id': 1, + 'test_status': 2, + 'test_tags': 3, + 'runnable': 4, + 'file_name': 5, + 'file_bytes': 6, + 'eof': 7, + 'mime_type': 8, + 'route_code': 9, + 'timestamp': 10, + } def __init__(self, **kwargs): unknown_kwargs = list(filter( @@ -358,7 +356,7 @@ class MatchesCall(Matcher): self._filters = kwargs def match(self, call_tuple): - for k,v in self._filters.items(): + for k, v in self._filters.items(): try: pos = self._position_lookup[k] if call_tuple[pos] != v: @@ -370,4 +368,3 @@ class MatchesCall(Matcher): def __str__(self): return "" % self._filters - -- cgit v1.2.1 From bcbee786daffffce1a07bb24829d408da76c6176 Mon Sep 17 00:00:00 2001 From: Thomi Richards Date: Wed, 20 Nov 2013 14:37:53 +1300 Subject: Fix indentation. --- python/subunit/_output.py | 24 ++++++++++++------------ 1 file changed, 12 insertions(+), 12 deletions(-) diff --git a/python/subunit/_output.py b/python/subunit/_output.py index 12479e8..432fa12 100644 --- a/python/subunit/_output.py +++ b/python/subunit/_output.py @@ -124,21 +124,21 @@ def parse_arguments(args=None, ParserClass=ArgumentParser): class StatusAction(Action): - """A custom action that stores option name and argument separately. + """A custom action that stores option name and argument separately. - This is part of a workaround for the fact that argparse does not - support optional subcommands (http://bugs.python.org/issue9253). - """ + This is part of a workaround for the fact that argparse does not + support optional subcommands (http://bugs.python.org/issue9253). + """ - def __init__(self, status_name, *args, **kwargs): - super(StatusAction, self).__init__(*args, **kwargs) - self._status_name = status_name + def __init__(self, status_name, *args, **kwargs): + super(StatusAction, self).__init__(*args, **kwargs) + self._status_name = status_name - def __call__(self, parser, namespace, values, option_string=None): - if getattr(namespace, self.dest, None) is not None: - raise ArgumentError(self, "Only one status may be specified at once.") - setattr(namespace, self.dest, self._status_name) - setattr(namespace, 'test_id', values[0]) + def __call__(self, parser, namespace, values, option_string=None): + if getattr(namespace, self.dest, None) is not None: + raise ArgumentError(self, "Only one status may be specified at once.") + setattr(namespace, self.dest, self._status_name) + setattr(namespace, 'test_id', values[0]) def get_output_stream_writer(): -- cgit v1.2.1 From 0eb7dbe09fb06f4f14e4a3ff766a776126a0c200 Mon Sep 17 00:00:00 2001 From: Thomi Richards Date: Mon, 25 Nov 2013 10:50:33 +1300 Subject: Port code to use optparse, rather than argparse. --- python/subunit/_output.py | 86 ++++++++++++++++-------------- python/subunit/tests/test_output_filter.py | 12 ++--- 2 files changed, 51 insertions(+), 47 deletions(-) diff --git a/python/subunit/_output.py b/python/subunit/_output.py index 432fa12..6f111cc 100644 --- a/python/subunit/_output.py +++ b/python/subunit/_output.py @@ -12,7 +12,11 @@ # license you chose for the specific language governing permissions and # limitations under that license. -from argparse import ArgumentError, ArgumentParser, Action +from optparse import ( + OptionGroup, + OptionParser, + OptionValueError, +) import datetime from functools import partial from sys import stdin, stdout @@ -30,7 +34,7 @@ def output_main(): return 0 -def parse_arguments(args=None, ParserClass=ArgumentParser): +def parse_arguments(args=None, ParserClass=OptionParser): """Parse arguments from the command line. If specified, args must be a list of strings, similar to sys.argv[1:]. @@ -42,28 +46,34 @@ def parse_arguments(args=None, ParserClass=ArgumentParser): prog='subunit-output', description="A tool to generate a subunit v2 result byte-stream", ) + parser.set_default('tags', None) - status_commands = parser.add_argument_group( + status_commands = OptionGroup( + parser, "Status Commands", "These options report the status of a test. TEST_ID must be a string " "that uniquely identifies the test." ) final_actions = 'exists fail skip success xfail uxsuccess'.split() all_actions = final_actions + ['inprogress'] - for action in all_actions: + for action_name in all_actions: final_text = " This is a final state: No more status reports may "\ "be generated for this test id after this one." - status_commands.add_argument( - "--%s" % action, + status_commands.add_option( + "--%s" % action_name, nargs=1, - action=partial(StatusAction, action), + action="callback", + callback=status_action, + callback_args=(action_name,), dest="action", metavar="TEST_ID", - help="Report a test status." + final_text if action in final_actions else "" + help="Report a test status." + final_text if action_name in final_actions else "" ) + parser.add_option_group(status_commands) - file_commands = parser.add_argument_group( + file_commands = OptionGroup( + parser, "File Options", "These options control attaching data to a result stream. They can " "either be specified with a status command, in which case the file " @@ -71,21 +81,21 @@ def parse_arguments(args=None, ParserClass=ArgumentParser): "the file is attached to the stream (and not associated with any " "test id)." ) - file_commands.add_argument( + file_commands.add_option( "--attach-file", help="Attach a file to the result stream for this test. If '-' is " "specified, stdin will be read instead. In this case, the file " "name will be set to 'stdin' (but can still be overridden with " "the --file-name option)." ) - file_commands.add_argument( + file_commands.add_option( "--file-name", help="The name to give this file attachment. If not specified, the " "name of the file on disk will be used, or 'stdin' in the case " "where '-' was passed to the '--attach-file' argument. This option" " may only be specified when '--attach-file' is specified.", ) - file_commands.add_argument( + file_commands.add_option( "--mimetype", help="The mime type to send with this file. This is only used if the " "--attach-file argument is used. This argument is optional. If it " @@ -93,52 +103,48 @@ def parse_arguments(args=None, ParserClass=ArgumentParser): "option may only be specified when '--attach-file' is specified.", default=None ) + parser.add_option_group(file_commands) - parser.add_argument( + parser.add_option( "--tags", help="A comma-separated list of tags to associate with a test. This " "option may only be used with a status command.", - type=lambda s: s.split(','), - default=None + action="callback", + callback=tags_action, + default=[] ) - args = parser.parse_args(args) - if args.mimetype and not args.attach_file: + (options, args) = parser.parse_args(args) + if options.mimetype and not options.attach_file: parser.error("Cannot specify --mimetype without --attach-file") - if args.file_name and not args.attach_file: + if options.file_name and not options.attach_file: parser.error("Cannot specify --file-name without --attach-file") - if args.attach_file: - if args.attach_file == '-': - if not args.file_name: - args.file_name = 'stdin' - args.attach_file = stdin + if options.attach_file: + if options.attach_file == '-': + if not options.file_name: + options.file_name = 'stdin' + options.attach_file = stdin else: try: - args.attach_file = open(args.attach_file) + options.attach_file = open(options.attach_file) except IOError as e: - parser.error("Cannot open %s (%s)" % (args.attach_file, e.strerror)) - if args.tags and not args.action: + parser.error("Cannot open %s (%s)" % (options.attach_file, e.strerror)) + if options.tags and not options.action: parser.error("Cannot specify --tags without a status command") - return args + return options -class StatusAction(Action): - """A custom action that stores option name and argument separately. +def status_action(option, opt_str, value, parser, status_name): + if getattr(parser.values, "action", None) is not None: + raise OptionValueError("argument %s: Only one status may be specified at once." % option) - This is part of a workaround for the fact that argparse does not - support optional subcommands (http://bugs.python.org/issue9253). - """ + parser.values.action = status_name + parser.values.test_id = parser.rargs.pop(0) - def __init__(self, status_name, *args, **kwargs): - super(StatusAction, self).__init__(*args, **kwargs) - self._status_name = status_name - def __call__(self, parser, namespace, values, option_string=None): - if getattr(namespace, self.dest, None) is not None: - raise ArgumentError(self, "Only one status may be specified at once.") - setattr(namespace, self.dest, self._status_name) - setattr(namespace, 'test_id', values[0]) +def tags_action(option, opt_str, value, parser): + parser.values.tags = parser.rargs.pop(0).split(',') def get_output_stream_writer(): diff --git a/python/subunit/tests/test_output_filter.py b/python/subunit/tests/test_output_filter.py index 69e5b2a..ba96687 100644 --- a/python/subunit/tests/test_output_filter.py +++ b/python/subunit/tests/test_output_filter.py @@ -13,9 +13,7 @@ # license you chose for the specific language governing permissions and # limitations under that license. # - - -import argparse +import optparse import datetime from functools import partial from io import BytesIO, StringIO @@ -43,14 +41,14 @@ from subunit._output import ( import subunit._output as _o -class SafeArgumentParser(argparse.ArgumentParser): +class SafeOptionParser(optparse.OptionParser): """An ArgumentParser class that doesn't call sys.exit.""" def exit(self, status=0, message=""): raise RuntimeError(message) -safe_parse_arguments = partial(parse_arguments, ParserClass=SafeArgumentParser) +safe_parse_arguments = partial(parse_arguments, ParserClass=SafeOptionParser) class TestStatusArgParserTests(WithScenarios, TestCase): @@ -128,9 +126,9 @@ class ArgParserTests(TestCase): def setUp(self): super(ArgParserTests, self).setUp() - # prevent ARgumentParser from printing to stderr: + # prevent OptionParser from printing to stderr: self._stderr = BytesIO() - self.patch(argparse._sys, 'stderr', self._stderr) + self.patch(optparse.sys, 'stderr', self._stderr) def test_can_parse_attach_file_without_test_id(self): with NamedTemporaryFile() as tmp_file: -- cgit v1.2.1 From 4d3beb5ed8f0601770b7c9eaf0bd64ce9b917547 Mon Sep 17 00:00:00 2001 From: Thomi Richards Date: Mon, 25 Nov 2013 11:46:12 +1300 Subject: Python version compatibility fixes. --- python/subunit/tests/test_output_filter.py | 8 ++++++-- 1 file changed, 6 insertions(+), 2 deletions(-) diff --git a/python/subunit/tests/test_output_filter.py b/python/subunit/tests/test_output_filter.py index ba96687..658174c 100644 --- a/python/subunit/tests/test_output_filter.py +++ b/python/subunit/tests/test_output_filter.py @@ -13,10 +13,11 @@ # license you chose for the specific language governing permissions and # limitations under that license. # -import optparse import datetime from functools import partial from io import BytesIO, StringIO +import optparse +import sys from tempfile import NamedTemporaryFile from testscenarios import WithScenarios @@ -127,7 +128,10 @@ class ArgParserTests(TestCase): def setUp(self): super(ArgParserTests, self).setUp() # prevent OptionParser from printing to stderr: - self._stderr = BytesIO() + if sys.version[0] > '2': + self._stderr = StringIO() + else: + self._stderr = BytesIO() self.patch(optparse.sys, 'stderr', self._stderr) def test_can_parse_attach_file_without_test_id(self): -- cgit v1.2.1 From bc9d2e6733469d0482a6fdd56019b862514ba396 Mon Sep 17 00:00:00 2001 From: Thomi Richards Date: Mon, 25 Nov 2013 12:17:11 +1300 Subject: Add a few more tests for error cases in option parser. --- python/subunit/_output.py | 7 +++++- python/subunit/tests/test_output_filter.py | 39 ++++++++++++++++++++++-------- 2 files changed, 35 insertions(+), 11 deletions(-) diff --git a/python/subunit/_output.py b/python/subunit/_output.py index 6f111cc..bdea14f 100644 --- a/python/subunit/_output.py +++ b/python/subunit/_output.py @@ -43,8 +43,9 @@ def parse_arguments(args=None, ParserClass=OptionParser): command-line arguments. This is useful for testing. """ parser = ParserClass( - prog='subunit-output', + prog="subunit-output", description="A tool to generate a subunit v2 result byte-stream", + usage="subunit-output [-h] [status test_id] [options]", ) parser.set_default('tags', None) @@ -131,6 +132,8 @@ def parse_arguments(args=None, ParserClass=OptionParser): parser.error("Cannot open %s (%s)" % (options.attach_file, e.strerror)) if options.tags and not options.action: parser.error("Cannot specify --tags without a status command") + if not (options.attach_file or options.action): + parser.error("Must specify either --attach-file or a status command") return options @@ -139,6 +142,8 @@ def status_action(option, opt_str, value, parser, status_name): if getattr(parser.values, "action", None) is not None: raise OptionValueError("argument %s: Only one status may be specified at once." % option) + if len(parser.rargs) == 0: + raise OptionValueError("argument %s: must specify a single TEST_ID.") parser.values.action = status_name parser.values.test_id = parser.rargs.pop(0) diff --git a/python/subunit/tests/test_output_filter.py b/python/subunit/tests/test_output_filter.py index 658174c..21d8172 100644 --- a/python/subunit/tests/test_output_filter.py +++ b/python/subunit/tests/test_output_filter.py @@ -52,7 +52,19 @@ class SafeOptionParser(optparse.OptionParser): safe_parse_arguments = partial(parse_arguments, ParserClass=SafeOptionParser) -class TestStatusArgParserTests(WithScenarios, TestCase): +class TestCaseWithPatchedStderr(TestCase): + + def setUp(self): + super(TestCaseWithPatchedStderr, self).setUp() + # prevent OptionParser from printing to stderr: + if sys.version[0] > '2': + self._stderr = StringIO() + else: + self._stderr = BytesIO() + self.patch(optparse.sys, 'stderr', self._stderr) + + +class TestStatusArgParserTests(WithScenarios, TestCaseWithPatchedStderr): scenarios = [ (cmd, dict(command=cmd, option='--' + cmd)) for cmd in ( @@ -122,17 +134,16 @@ class TestStatusArgParserTests(WithScenarios, TestCase): self.assertThat(args.file_name, Equals("foo")) + def test_requires_test_id(self): + fn = lambda: safe_parse_arguments(args=[self.option]) + self.assertThat( + fn, + raises(RuntimeError('subunit-output: error: argument %s: must ' + 'specify a single TEST_ID.\n')) + ) -class ArgParserTests(TestCase): - def setUp(self): - super(ArgParserTests, self).setUp() - # prevent OptionParser from printing to stderr: - if sys.version[0] > '2': - self._stderr = StringIO() - else: - self._stderr = BytesIO() - self.patch(optparse.sys, 'stderr', self._stderr) +class ArgParserTests(TestCaseWithPatchedStderr): def test_can_parse_attach_file_without_test_id(self): with NamedTemporaryFile() as tmp_file: @@ -141,6 +152,14 @@ class ArgParserTests(TestCase): ) self.assertThat(args.attach_file.name, Equals(tmp_file.name)) + def test_must_specify_argument(self): + fn = lambda: safe_parse_arguments([]) + self.assertThat( + fn, + raises(RuntimeError('subunit-output: error: Must specify either ' + '--attach-file or a status command\n')) + ) + def test_cannot_specify_more_than_one_status_command(self): fn = lambda: safe_parse_arguments(['--fail', 'foo', '--skip', 'bar']) self.assertThat( -- cgit v1.2.1 From 8d89380814d71a8a896c26519a7da81a71e73bc3 Mon Sep 17 00:00:00 2001 From: Thomi Richards Date: Mon, 25 Nov 2013 14:42:09 +1300 Subject: Make sure filter script is installed. --- Makefile.am | 1 + README | 6 +++--- 2 files changed, 4 insertions(+), 3 deletions(-) diff --git a/Makefile.am b/Makefile.am index ae72035..3d592cc 100644 --- a/Makefile.am +++ b/Makefile.am @@ -52,6 +52,7 @@ dist_bin_SCRIPTS = \ filters/subunit-filter \ filters/subunit-ls \ filters/subunit-notify \ + filters/subunit-output \ filters/subunit-stats \ filters/subunit-tags \ filters/subunit2csv \ diff --git a/README b/README index 5c2526f..4fa9444 100644 --- a/README +++ b/README @@ -6,7 +6,7 @@ license at the users choice. A copy of both licenses are available in the project source as Apache-2.0 and BSD. You may not use this file except in compliance with one of these two licences. - + Unless required by applicable law or agreed to in writing, software distributed under these licenses is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the @@ -90,7 +90,7 @@ The most common way is to run an existing python test suite and have it output subunit via the ``subunit.run`` module:: $ python -m subunit.run mypackage.tests.test_suite - + For more information on the Python support Subunit offers , please see ``pydoc subunit``, or the source in ``python/subunit/`` @@ -343,7 +343,7 @@ b3 2901 0c 03666f6f 08555f1b Version 1 (and 1.1) =================== -Version 1 (and 1.1) are mostly human readable protocols. +Version 1 (and 1.1) are mostly human readable protocols. Sample subunit wire contents ---------------------------- -- cgit v1.2.1 From c1cf894ccff6fe465f4a707acd394236ce898f27 Mon Sep 17 00:00:00 2001 From: Thomi Richards Date: Mon, 25 Nov 2013 17:52:26 +1300 Subject: Lots of fixes from code review. --- python/subunit/_output.py | 138 ++++++------ python/subunit/tests/test_output_filter.py | 343 +++++++++++++++++++---------- setup.py | 3 +- 3 files changed, 303 insertions(+), 181 deletions(-) diff --git a/python/subunit/_output.py b/python/subunit/_output.py index bdea14f..49b5e81 100644 --- a/python/subunit/_output.py +++ b/python/subunit/_output.py @@ -12,25 +12,35 @@ # license you chose for the specific language governing permissions and # limitations under that license. +import datetime +from functools import partial from optparse import ( OptionGroup, OptionParser, OptionValueError, ) -import datetime -from functools import partial -from sys import stdin, stdout - -from testtools.compat import _b +import sys from subunit.iso8601 import UTC from subunit.v2 import StreamResultToBytes +_FINAL_ACTIONS = frozenset([ + 'exists', + 'fail', + 'skip', + 'success', + 'uxsuccess', + 'xfail', +]) +_ALL_ACTIONS = _FINAL_ACTIONS.union(['inprogress']) +_CHUNK_SIZE=3670016 # 3.5 MiB + + def output_main(): args = parse_arguments() - output = get_output_stream_writer() - generate_bytestream(args, output) + output = StreamResultToBytes(sys.stdout) + generate_stream_results(args, output) return 0 @@ -48,6 +58,7 @@ def parse_arguments(args=None, ParserClass=OptionParser): usage="subunit-output [-h] [status test_id] [options]", ) parser.set_default('tags', None) + parser.set_default('test_id', None) status_commands = OptionGroup( parser, @@ -55,21 +66,16 @@ def parse_arguments(args=None, ParserClass=OptionParser): "These options report the status of a test. TEST_ID must be a string " "that uniquely identifies the test." ) - final_actions = 'exists fail skip success xfail uxsuccess'.split() - all_actions = final_actions + ['inprogress'] - for action_name in all_actions: - final_text = " This is a final state: No more status reports may "\ - "be generated for this test id after this one." - + for action_name in _ALL_ACTIONS: status_commands.add_option( "--%s" % action_name, nargs=1, action="callback", - callback=status_action, + callback=set_status_cb, callback_args=(action_name,), dest="action", metavar="TEST_ID", - help="Report a test status." + final_text if action_name in final_actions else "" + help="Report a test status." ) parser.add_option_group(status_commands) @@ -111,7 +117,7 @@ def parse_arguments(args=None, ParserClass=OptionParser): help="A comma-separated list of tags to associate with a test. This " "option may only be used with a status command.", action="callback", - callback=tags_action, + callback=set_tags_cb, default=[] ) @@ -124,7 +130,7 @@ def parse_arguments(args=None, ParserClass=OptionParser): if options.attach_file == '-': if not options.file_name: options.file_name = 'stdin' - options.attach_file = stdin + options.attach_file = sys.stdin else: try: options.attach_file = open(options.attach_file) @@ -138,7 +144,7 @@ def parse_arguments(args=None, ParserClass=OptionParser): return options -def status_action(option, opt_str, value, parser, status_name): +def set_status_cb(option, opt_str, value, parser, status_name): if getattr(parser.values, "action", None) is not None: raise OptionValueError("argument %s: Only one status may be specified at once." % option) @@ -148,60 +154,66 @@ def status_action(option, opt_str, value, parser, status_name): parser.values.test_id = parser.rargs.pop(0) -def tags_action(option, opt_str, value, parser): +def set_tags_cb(option, opt_str, value, parser): parser.values.tags = parser.rargs.pop(0).split(',') -def get_output_stream_writer(): - return StreamResultToBytes(stdout) - - -def generate_bytestream(args, output_writer): +def generate_stream_results(args, output_writer): output_writer.startTestRun() + if args.attach_file: - write_chunked_file( - file_obj=args.attach_file, - test_id=args.test_id, - output_writer=output_writer, - mime_type=args.mimetype, - ) - output_writer.status( - test_id=args.test_id, - test_status=args.action, - timestamp=create_timestamp(), - test_tags=args.tags, - ) - output_writer.stopTestRun() + reader = partial(args.attach_file.read, _CHUNK_SIZE) + this_file_hunk = reader().encode('utf8') + next_file_hunk = reader().encode('utf8') + + is_first_packet = True + is_last_packet = False + while not is_last_packet: + + # XXX + def logme(*args, **kwargs): + print(args, kwargs) + output_writer.status(*args, **kwargs) + write_status = output_writer.status + + if is_first_packet: + if args.attach_file: + # mimetype is specified on the first chunk only: + if args.mimetype: + write_status = partial(write_status, mime_type=args.mimetype) + # tags are only written on the first packet: + if args.tags: + write_status = partial(write_status, test_tags=args.tags) + # timestamp is specified on the first chunk as well: + write_status = partial(write_status, timestamp=create_timestamp()) + if args.action not in _FINAL_ACTIONS: + write_status = partial(write_status, test_status=args.action) + is_first_packet = False + + if args.attach_file: + # filename might be overridden by the user + filename = args.file_name or args.attach_file.name + write_status = partial(write_status, file_name=filename, file_bytes=this_file_hunk) + if next_file_hunk == b'': + write_status = partial(write_status, eof=True) + is_last_packet = True + else: + this_file_hunk = next_file_hunk + next_file_hunk = reader().encode('utf8') + else: + is_last_packet = True + if args.test_id: + write_status = partial(write_status, test_id=args.test_id) -def write_chunked_file(file_obj, output_writer, chunk_size=1024, - mime_type=None, test_id=None, file_name=None): - reader = partial(file_obj.read, chunk_size) + if is_last_packet: + write_status = partial(write_status, eof=True) + if args.action in _FINAL_ACTIONS: + write_status = partial(write_status, test_status=args.action) - write_status = output_writer.status - if mime_type is not None: - write_status = partial( - write_status, - mime_type=mime_type - ) - if test_id is not None: - write_status = partial( - write_status, - test_id=test_id - ) - filename = file_name if file_name else file_obj.name + write_status() - for chunk in iter(reader, _b('')): - write_status( - file_name=filename, - file_bytes=chunk, - eof=False, - ) - write_status( - file_name=filename, - file_bytes=_b(''), - eof=True, - ) + output_writer.stopTestRun() def create_timestamp(): diff --git a/python/subunit/tests/test_output_filter.py b/python/subunit/tests/test_output_filter.py index 21d8172..401ec08 100644 --- a/python/subunit/tests/test_output_filter.py +++ b/python/subunit/tests/test_output_filter.py @@ -13,6 +13,7 @@ # license you chose for the specific language governing permissions and # limitations under that license. # + import datetime from functools import partial from io import BytesIO, StringIO @@ -20,9 +21,10 @@ import optparse import sys from tempfile import NamedTemporaryFile +from contextlib import contextmanager from testscenarios import WithScenarios from testtools import TestCase -from testtools.compat import _b, _u +from testtools.compat import _u from testtools.matchers import ( Equals, Matcher, @@ -35,9 +37,10 @@ from testtools.testresult.doubles import StreamResult from subunit.iso8601 import UTC from subunit.v2 import StreamResultToBytes, ByteStreamToStreamResult from subunit._output import ( - generate_bytestream, + _ALL_ACTIONS, + _FINAL_ACTIONS, + generate_stream_results, parse_arguments, - write_chunked_file, ) import subunit._output as _o @@ -67,15 +70,7 @@ class TestCaseWithPatchedStderr(TestCase): class TestStatusArgParserTests(WithScenarios, TestCaseWithPatchedStderr): scenarios = [ - (cmd, dict(command=cmd, option='--' + cmd)) for cmd in ( - 'exists', - 'fail', - 'inprogress', - 'skip', - 'success', - 'uxsuccess', - 'xfail', - ) + (cmd, dict(command=cmd, option='--' + cmd)) for cmd in _ALL_ACTIONS ] def test_can_parse_all_commands_with_test_id(self): @@ -113,7 +108,7 @@ class TestStatusArgParserTests(WithScenarios, TestCaseWithPatchedStderr): self.assertThat(args.tags, Equals(["foo", "bar", "baz"])) def test_attach_file_with_hyphen_opens_stdin(self): - self.patch(_o, 'stdin', StringIO(_u("Hello"))) + self.patch(_o.sys, 'stdin', StringIO(_u("Hello"))) args = safe_parse_arguments( args=[self.option, "foo", "--attach-file", "-"] ) @@ -196,7 +191,7 @@ class ArgParserTests(TestCaseWithPatchedStderr): def get_result_for(commands): """Get a result object from *commands. - Runs the 'generate_bytestream' function from subunit._output after + Runs the 'generate_stream_results' function from subunit._output after parsing *commands as if they were specified on the command line. The resulting bytestream is then converted back into a result object and returned. @@ -205,7 +200,7 @@ def get_result_for(commands): args = safe_parse_arguments(commands) output_writer = StreamResultToBytes(output_stream=stream) - generate_bytestream(args, output_writer) + generate_stream_results(args, output_writer) stream.seek(0) @@ -215,143 +210,257 @@ def get_result_for(commands): return result -class ByteStreamCompatibilityTests(WithScenarios, TestCase): +@contextmanager +def temp_file_contents(data): + """Create a temporary file on disk containing 'data'.""" + with NamedTemporaryFile() as f: + f.write(data) + f.seek(0) + yield f + + +class StatusStreamResultTests(WithScenarios, TestCase): scenarios = [ - (s, dict(status=s, option='--' + s)) for s in ( - 'exists', - 'fail', - 'inprogress', - 'skip', - 'success', - 'uxsuccess', - 'xfail', - ) + (s, dict(status=s, option='--' + s)) for s in _ALL_ACTIONS ] _dummy_timestamp = datetime.datetime(2013, 1, 1, 0, 0, 0, 0, UTC) def setUp(self): - super(ByteStreamCompatibilityTests, self).setUp() + super(StatusStreamResultTests, self).setUp() self.patch(_o, 'create_timestamp', lambda: self._dummy_timestamp) + self.test_id = self.getUniqueString() + + def test_only_one_packet_is_generated(self): + result = get_result_for([self.option, self.test_id]) + self.assertThat( + len(result._events), + Equals(1) + ) def test_correct_status_is_generated(self): - result = get_result_for([self.option, 'foo']) + result = get_result_for([self.option, self.test_id]) self.assertThat( result._events[0], - MatchesCall( - call='status', - test_id='foo', - test_status=self.status, - timestamp=self._dummy_timestamp, - ) + MatchesStatusCall(test_status=self.status) ) - def test_all_commands_accept_tags(self): - result = get_result_for([self.option, 'foo', '--tags', 'hello,world']) + def test_all_commands_generate_tags(self): + result = get_result_for([self.option, self.test_id, '--tags', 'hello,world']) self.assertThat( result._events[0], - MatchesCall( - call='status', - test_id='foo', - test_tags=set(['hello', 'world']), - timestamp=self._dummy_timestamp, - ) + MatchesStatusCall(test_tags=set(['hello', 'world'])) ) + def test_all_commands_generate_timestamp(self): + result = get_result_for([self.option, self.test_id]) -class FileChunkingTests(WithScenarios, TestCase): + self.assertThat( + result._events[0], + MatchesStatusCall(timestamp=self._dummy_timestamp) + ) - scenarios = [ - ("With test_id", dict(test_id="foo")), - ("Without test_id", dict(test_id=None)), - ] + def test_all_commands_generate_correct_test_id(self): + result = get_result_for([self.option, self.test_id]) - def _write_chunk_file(self, file_data, chunk_size=1024, mimetype=None, filename=None, test_id=None): - """Write file data to a subunit stream, get a StreamResult object.""" - stream = BytesIO() - output_writer = StreamResultToBytes(output_stream=stream) - - with NamedTemporaryFile() as f: - self._tmp_filename = f.name - f.write(file_data) - f.seek(0) - - write_chunked_file( - file_obj=f, - output_writer=output_writer, - chunk_size=chunk_size, - mime_type=mimetype, - test_id=test_id, - file_name=filename, + self.assertThat( + result._events[0], + MatchesStatusCall(test_id=self.test_id) + ) + + def test_file_is_sent_in_single_packet(self): + with temp_file_contents(b"Hello") as f: + result = get_result_for([self.option, self.test_id, '--attach-file', f.name]) + + self.assertThat( + result._events, + MatchesListwise([ + MatchesStatusCall(file_bytes=b'Hello', eof=True), + ]) ) - stream.seek(0) + def test_file_is_sent_with_test_id(self): + with temp_file_contents(b"Hello") as f: + result = get_result_for([self.option, self.test_id, '--attach-file', f.name]) - case = ByteStreamToStreamResult(source=stream) - result = StreamResult() - case.run(result) - return result + self.assertThat( + result._events, + MatchesListwise([ + MatchesStatusCall(test_id=self.test_id, file_bytes=b'Hello', eof=True), + ]) + ) def test_file_chunk_size_is_honored(self): - result = self._write_chunk_file( - file_data=_b("Hello"), - chunk_size=1, - test_id=self.test_id, - ) - self.assertThat( - result._events, - MatchesListwise([ - MatchesCall(call='status', test_id=self.test_id, file_bytes=_b('H'), mime_type=None, eof=False), - MatchesCall(call='status', test_id=self.test_id, file_bytes=_b('e'), mime_type=None, eof=False), - MatchesCall(call='status', test_id=self.test_id, file_bytes=_b('l'), mime_type=None, eof=False), - MatchesCall(call='status', test_id=self.test_id, file_bytes=_b('l'), mime_type=None, eof=False), - MatchesCall(call='status', test_id=self.test_id, file_bytes=_b('o'), mime_type=None, eof=False), - MatchesCall(call='status', test_id=self.test_id, file_bytes=_b(''), mime_type=None, eof=True), + with temp_file_contents(b"Hello") as f: + self.patch(_o, '_CHUNK_SIZE', 1) + result = get_result_for([self.option, self.test_id, '--attach-file', f.name]) + + self.assertThat( + result._events, + MatchesListwise([ + MatchesStatusCall(test_id=self.test_id, file_bytes=b'H', eof=False), + MatchesStatusCall(test_id=self.test_id, file_bytes=b'e', eof=False), + MatchesStatusCall(test_id=self.test_id, file_bytes=b'l', eof=False), + MatchesStatusCall(test_id=self.test_id, file_bytes=b'l', eof=False), + MatchesStatusCall(test_id=self.test_id, file_bytes=b'o', eof=True), + ]) + ) + + def test_file_mimetype_specified_once_only(self): + with temp_file_contents(b"Hi") as f: + self.patch(_o, '_CHUNK_SIZE', 1) + result = get_result_for([ + self.option, + self.test_id, + '--attach-file', + f.name, + '--mimetype', + 'text/plain', ]) - ) - def test_file_mimetype_is_honored(self): - result = self._write_chunk_file( - file_data=_b("SomeData"), - mimetype="text/plain", - test_id=self.test_id, - ) - self.assertThat( - result._events, - MatchesListwise([ - MatchesCall(call='status', test_id=self.test_id, file_bytes=_b('SomeData'), mime_type="text/plain"), - MatchesCall(call='status', test_id=self.test_id, file_bytes=_b(''), mime_type="text/plain"), + self.assertThat( + result._events, + MatchesListwise([ + MatchesStatusCall(test_id=self.test_id, mime_type='text/plain', file_bytes=b'H', eof=False), + MatchesStatusCall(test_id=self.test_id, mime_type=None, file_bytes=b'i', eof=True), + ]) + ) + + def test_tags_specified_once_only(self): + with temp_file_contents(b"Hi") as f: + self.patch(_o, '_CHUNK_SIZE', 1) + result = get_result_for([ + self.option, + self.test_id, + '--attach-file', + f.name, + '--tags', + 'foo,bar', ]) - ) - def test_file_name_is_honored(self): - result = self._write_chunk_file( - file_data=_b("data"), - filename="/some/name", - test_id=self.test_id - ) - self.assertThat( - result._events, - MatchesListwise([ - MatchesCall(call='status', test_id=self.test_id, file_name='/some/name'), - MatchesCall(call='status', test_id=self.test_id, file_name='/some/name'), + self.assertThat( + result._events, + MatchesListwise([ + MatchesStatusCall(test_id=self.test_id, test_tags=set(['foo', 'bar'])), + MatchesStatusCall(test_id=self.test_id, test_tags=None), + ]) + ) + + def test_timestamp_specified_once_only(self): + with temp_file_contents(b"Hi") as f: + self.patch(_o, '_CHUNK_SIZE', 1) + result = get_result_for([ + self.option, + self.test_id, + '--attach-file', + f.name, ]) - ) - def test_default_filename_is_used(self): - result = self._write_chunk_file(file_data=_b("data")) - self.assertThat( - result._events, - MatchesListwise([ - MatchesCall(call='status', file_name=self._tmp_filename), - MatchesCall(call='status', file_name=self._tmp_filename), + self.assertThat( + result._events, + MatchesListwise([ + MatchesStatusCall(test_id=self.test_id, timestamp=self._dummy_timestamp), + MatchesStatusCall(test_id=self.test_id, timestamp=None), + ]) + ) + + def test_test_status_specified_once_only(self): + with temp_file_contents(b"Hi") as f: + self.patch(_o, '_CHUNK_SIZE', 1) + result = get_result_for([ + self.option, + self.test_id, + '--attach-file', + f.name, ]) - ) + + # 'inprogress' status should be on the first packet only, all other + # statuses should be on the last packet. + if self.status in _FINAL_ACTIONS: + first_call = MatchesStatusCall(test_id=self.test_id, test_status=None) + last_call = MatchesStatusCall(test_id=self.test_id, test_status=self.status) + else: + first_call = MatchesStatusCall(test_id=self.test_id, test_status=self.status) + last_call = MatchesStatusCall(test_id=self.test_id, test_status=None) + self.assertThat( + result._events, + MatchesListwise([first_call, last_call]) + ) + + def test_filename_can_be_overridden(self): + with temp_file_contents(b"Hello") as f: + specified_file_name = self.getUniqueString() + result = get_result_for([ + self.option, + self.test_id, + '--attach-file', + f.name, + '--file-name', + specified_file_name]) + + self.assertThat( + result._events, + MatchesListwise([ + MatchesStatusCall(file_name=specified_file_name, file_bytes=b'Hello'), + ]) + ) + + def test_file_name_is_used_by_default(self): + with temp_file_contents(b"Hello") as f: + result = get_result_for([self.option, self.test_id, '--attach-file', f.name]) + + self.assertThat( + result._events, + MatchesListwise([ + MatchesStatusCall(file_name=f.name, file_bytes=b'Hello', eof=True), + ]) + ) + + +class GlobalFileDataTests(TestCase): + + def test_can_attach_file_without_test_id(self): + with temp_file_contents(b"Hello") as f: + result = get_result_for(['--attach-file', f.name]) + + self.assertThat( + result._events, + MatchesListwise([ + MatchesStatusCall(test_id=None, file_bytes=b'Hello', eof=True), + ]) + ) + + def test_file_name_is_used_by_default(self): + with temp_file_contents(b"Hello") as f: + result = get_result_for(['--attach-file', f.name]) + + self.assertThat( + result._events, + MatchesListwise([ + MatchesStatusCall(file_name=f.name, file_bytes=b'Hello', eof=True), + ]) + ) + + def test_filename_can_be_overridden(self): + with temp_file_contents(b"Hello") as f: + specified_file_name = self.getUniqueString() + result = get_result_for([ + '--attach-file', + f.name, + '--file-name', + specified_file_name]) + + self.assertThat( + result._events, + MatchesListwise([ + MatchesStatusCall(file_name=specified_file_name, file_bytes=b'Hello'), + ]) + ) -class MatchesCall(Matcher): +class MatchesStatusCall(Matcher): _position_lookup = { 'call': 0, @@ -388,4 +497,4 @@ class MatchesCall(Matcher): return Mismatch("Key %s is not present." % k) def __str__(self): - return "" % self._filters + return "" % self._filters diff --git a/setup.py b/setup.py index 2f22300..1fe168d 100755 --- a/setup.py +++ b/setup.py @@ -9,9 +9,10 @@ except ImportError: else: extra = { 'install_requires': [ + 'contextlib', 'extras', - 'testtools>=0.9.30', 'testscenarios', + 'testtools>=0.9.30', ] } -- cgit v1.2.1 From 6b500b82b94d335e497ef83982c2db4a4b9b1e72 Mon Sep 17 00:00:00 2001 From: Thomi Richards Date: Mon, 25 Nov 2013 18:01:46 +1300 Subject: Add a few missing tests. --- python/subunit/_output.py | 1 + python/subunit/tests/test_output_filter.py | 34 ++++++++++++++++++++++++++++-- 2 files changed, 33 insertions(+), 2 deletions(-) diff --git a/python/subunit/_output.py b/python/subunit/_output.py index 49b5e81..24d63dc 100644 --- a/python/subunit/_output.py +++ b/python/subunit/_output.py @@ -11,6 +11,7 @@ # WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the # license you chose for the specific language governing permissions and # limitations under that license. +# import datetime from functools import partial diff --git a/python/subunit/tests/test_output_filter.py b/python/subunit/tests/test_output_filter.py index 401ec08..f3000ad 100644 --- a/python/subunit/tests/test_output_filter.py +++ b/python/subunit/tests/test_output_filter.py @@ -292,6 +292,17 @@ class StatusStreamResultTests(WithScenarios, TestCase): ]) ) + def test_file_is_sent_with_test_status(self): + with temp_file_contents(b"Hello") as f: + result = get_result_for([self.option, self.test_id, '--attach-file', f.name]) + + self.assertThat( + result._events, + MatchesListwise([ + MatchesStatusCall(test_status=self.status, file_bytes=b'Hello', eof=True), + ]) + ) + def test_file_chunk_size_is_honored(self): with temp_file_contents(b"Hello") as f: self.patch(_o, '_CHUNK_SIZE', 1) @@ -419,7 +430,7 @@ class StatusStreamResultTests(WithScenarios, TestCase): ) -class GlobalFileDataTests(TestCase): +class FileDataTests(TestCase): def test_can_attach_file_without_test_id(self): with temp_file_contents(b"Hello") as f: @@ -450,7 +461,8 @@ class GlobalFileDataTests(TestCase): '--attach-file', f.name, '--file-name', - specified_file_name]) + specified_file_name + ]) self.assertThat( result._events, @@ -459,6 +471,24 @@ class GlobalFileDataTests(TestCase): ]) ) + def test_files_have_timestamp(self): + _dummy_timestamp = datetime.datetime(2013, 1, 1, 0, 0, 0, 0, UTC) + self.patch(_o, 'create_timestamp', lambda: self._dummy_timestamp) + + with temp_file_contents(b"Hello") as f: + specified_file_name = self.getUniqueString() + result = get_result_for([ + '--attach-file', + f.name, + ]) + + self.assertThat( + result._events, + MatchesListwise([ + MatchesStatusCall(file_bytes=b'Hello', timestamp=self._dummy_timestamp), + ]) + ) + class MatchesStatusCall(Matcher): -- cgit v1.2.1 From 81c8839bd41bc0d25f478025083c8cd2d1a9afa8 Mon Sep 17 00:00:00 2001 From: Thomi Richards Date: Mon, 25 Nov 2013 18:10:07 +1300 Subject: Fix setup.py deps. --- setup.py | 1 - 1 file changed, 1 deletion(-) diff --git a/setup.py b/setup.py index 1fe168d..d7089df 100755 --- a/setup.py +++ b/setup.py @@ -9,7 +9,6 @@ except ImportError: else: extra = { 'install_requires': [ - 'contextlib', 'extras', 'testscenarios', 'testtools>=0.9.30', -- cgit v1.2.1 From 9c24b5dbb4e3a2ff55372ffca620bb1cda3ef875 Mon Sep 17 00:00:00 2001 From: Thomi Richards Date: Mon, 25 Nov 2013 18:14:57 +1300 Subject: Added test for poorly specified tags. --- python/subunit/_output.py | 4 +++- python/subunit/tests/test_output_filter.py | 7 +++++++ 2 files changed, 10 insertions(+), 1 deletion(-) diff --git a/python/subunit/_output.py b/python/subunit/_output.py index 24d63dc..66ff5df 100644 --- a/python/subunit/_output.py +++ b/python/subunit/_output.py @@ -150,12 +150,14 @@ def set_status_cb(option, opt_str, value, parser, status_name): raise OptionValueError("argument %s: Only one status may be specified at once." % option) if len(parser.rargs) == 0: - raise OptionValueError("argument %s: must specify a single TEST_ID.") + raise OptionValueError("argument %s: must specify a single TEST_ID." % option) parser.values.action = status_name parser.values.test_id = parser.rargs.pop(0) def set_tags_cb(option, opt_str, value, parser): + if not parser.rargs: + raise OptionValueError("Must specify at least one tag with --tags") parser.values.tags = parser.rargs.pop(0).split(',') diff --git a/python/subunit/tests/test_output_filter.py b/python/subunit/tests/test_output_filter.py index f3000ad..a31ae2b 100644 --- a/python/subunit/tests/test_output_filter.py +++ b/python/subunit/tests/test_output_filter.py @@ -187,6 +187,13 @@ class ArgParserTests(TestCaseWithPatchedStderr): '--tags without a status command\n')) ) + def test_must_specify_tags_with_tags_options(self): + fn = lambda: safe_parse_arguments(['--fail', 'foo', '--tags']) + self.assertThat( + fn, + raises(RuntimeError('subunit-output: error: Must specify at least one tag with --tags\n')) + ) + def get_result_for(commands): """Get a result object from *commands. -- cgit v1.2.1 From f28cc4ffb9fecc63210ed0bca52077fbcc9612d5 Mon Sep 17 00:00:00 2001 From: Thomi Richards Date: Mon, 25 Nov 2013 18:17:28 +1300 Subject: Make usage line match help text. --- python/subunit/_output.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/python/subunit/_output.py b/python/subunit/_output.py index 66ff5df..32d5110 100644 --- a/python/subunit/_output.py +++ b/python/subunit/_output.py @@ -56,7 +56,7 @@ def parse_arguments(args=None, ParserClass=OptionParser): parser = ParserClass( prog="subunit-output", description="A tool to generate a subunit v2 result byte-stream", - usage="subunit-output [-h] [status test_id] [options]", + usage="subunit-output [-h] [status TEST_ID] [options]", ) parser.set_default('tags', None) parser.set_default('test_id', None) -- cgit v1.2.1 From b1ca3bcbc0ce150d99b94d9c4b84de1b73693cb9 Mon Sep 17 00:00:00 2001 From: Thomi Richards Date: Mon, 25 Nov 2013 18:22:30 +1300 Subject: Open files in binary mode. --- python/subunit/_output.py | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/python/subunit/_output.py b/python/subunit/_output.py index 32d5110..8be2ca8 100644 --- a/python/subunit/_output.py +++ b/python/subunit/_output.py @@ -134,7 +134,7 @@ def parse_arguments(args=None, ParserClass=OptionParser): options.attach_file = sys.stdin else: try: - options.attach_file = open(options.attach_file) + options.attach_file = open(options.attach_file, 'rb') except IOError as e: parser.error("Cannot open %s (%s)" % (options.attach_file, e.strerror)) if options.tags and not options.action: @@ -166,8 +166,8 @@ def generate_stream_results(args, output_writer): if args.attach_file: reader = partial(args.attach_file.read, _CHUNK_SIZE) - this_file_hunk = reader().encode('utf8') - next_file_hunk = reader().encode('utf8') + this_file_hunk = reader() + next_file_hunk = reader() is_first_packet = True is_last_packet = False @@ -202,7 +202,7 @@ def generate_stream_results(args, output_writer): is_last_packet = True else: this_file_hunk = next_file_hunk - next_file_hunk = reader().encode('utf8') + next_file_hunk = reader() else: is_last_packet = True -- cgit v1.2.1 From 8ee031da94894c8249ba79a5f20442def5944699 Mon Sep 17 00:00:00 2001 From: Thomi Richards Date: Mon, 25 Nov 2013 18:30:14 +1300 Subject: Add tests around reading binary files, empty files, and stdin. --- python/subunit/tests/test_output_filter.py | 33 ++++++++++++++++++++++++++++++ 1 file changed, 33 insertions(+) diff --git a/python/subunit/tests/test_output_filter.py b/python/subunit/tests/test_output_filter.py index a31ae2b..758110e 100644 --- a/python/subunit/tests/test_output_filter.py +++ b/python/subunit/tests/test_output_filter.py @@ -288,6 +288,39 @@ class StatusStreamResultTests(WithScenarios, TestCase): ]) ) + def test_can_read_binary_files(self): + with temp_file_contents(b"\xDE\xAD\xBE\xEF") as f: + result = get_result_for([self.option, self.test_id, '--attach-file', f.name]) + + self.assertThat( + result._events, + MatchesListwise([ + MatchesStatusCall(file_bytes=b"\xDE\xAD\xBE\xEF", eof=True), + ]) + ) + + def test_can_read_empty_files(self): + with temp_file_contents(b"") as f: + result = get_result_for([self.option, self.test_id, '--attach-file', f.name]) + + self.assertThat( + result._events, + MatchesListwise([ + MatchesStatusCall(file_bytes=b"", file_name=f.name, eof=True), + ]) + ) + + def test_can_read_stdin(self): + self.patch(_o.sys, 'stdin', BytesIO(b"\xFE\xED\xFA\xCE")) + result = get_result_for([self.option, self.test_id, '--attach-file', '-']) + + self.assertThat( + result._events, + MatchesListwise([ + MatchesStatusCall(file_bytes=b"\xFE\xED\xFA\xCE", file_name='stdin', eof=True), + ]) + ) + def test_file_is_sent_with_test_id(self): with temp_file_contents(b"Hello") as f: result = get_result_for([self.option, self.test_id, '--attach-file', f.name]) -- cgit v1.2.1 From 89acb22a7b22de233d953842949ca22e951a6931 Mon Sep 17 00:00:00 2001 From: Thomi Richards Date: Mon, 25 Nov 2013 18:32:29 +1300 Subject: Read binary from stdin. --- python/subunit/_output.py | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/python/subunit/_output.py b/python/subunit/_output.py index 8be2ca8..0a1ef5d 100644 --- a/python/subunit/_output.py +++ b/python/subunit/_output.py @@ -131,7 +131,10 @@ def parse_arguments(args=None, ParserClass=OptionParser): if options.attach_file == '-': if not options.file_name: options.file_name = 'stdin' - options.attach_file = sys.stdin + if sys.version[0] >= '3': + options.attach_file = sys.stdin.buffer + else: + options.attach_file = sys.stdin else: try: options.attach_file = open(options.attach_file, 'rb') -- cgit v1.2.1 From 095223bebfc6322e6a1421257ce9b9a5d969c5fc Mon Sep 17 00:00:00 2001 From: Thomi Richards Date: Mon, 25 Nov 2013 18:34:37 +1300 Subject: Remove debugging code. --- python/subunit/_output.py | 9 --------- 1 file changed, 9 deletions(-) diff --git a/python/subunit/_output.py b/python/subunit/_output.py index 0a1ef5d..0e9302b 100644 --- a/python/subunit/_output.py +++ b/python/subunit/_output.py @@ -175,29 +175,20 @@ def generate_stream_results(args, output_writer): is_first_packet = True is_last_packet = False while not is_last_packet: - - # XXX - def logme(*args, **kwargs): - print(args, kwargs) - output_writer.status(*args, **kwargs) write_status = output_writer.status if is_first_packet: if args.attach_file: - # mimetype is specified on the first chunk only: if args.mimetype: write_status = partial(write_status, mime_type=args.mimetype) - # tags are only written on the first packet: if args.tags: write_status = partial(write_status, test_tags=args.tags) - # timestamp is specified on the first chunk as well: write_status = partial(write_status, timestamp=create_timestamp()) if args.action not in _FINAL_ACTIONS: write_status = partial(write_status, test_status=args.action) is_first_packet = False if args.attach_file: - # filename might be overridden by the user filename = args.file_name or args.attach_file.name write_status = partial(write_status, file_name=filename, file_bytes=this_file_hunk) if next_file_hunk == b'': -- cgit v1.2.1 From 11f80bb18b66ce0315dbe33856ac35e08b44bfcc Mon Sep 17 00:00:00 2001 From: Thomi Richards Date: Mon, 2 Dec 2013 18:21:57 +1300 Subject: Updated NEWS file. --- NEWS | 9 ++++++--- 1 file changed, 6 insertions(+), 3 deletions(-) diff --git a/NEWS b/NEWS index e3e805e..b9ac16c 100644 --- a/NEWS +++ b/NEWS @@ -5,6 +5,12 @@ subunit release notes NEXT (In development) --------------------- +IMPROVMENTS +~~~~~~~~~~~ + +* Add ``subunit-output`` tool that can generate a Subunit v2 bytestream from + arguments passed on the command line. (Thomi Richards, #1252084) + 0.0.16 ------ @@ -34,9 +40,6 @@ IMPROVEMENTS * Most filters will now accept a file path argument instead of only reading from stdin. (Robert Collins, #409206) -* Add ``subunit-output`` tool that can generate a Subunit v2 bytestream from - arguments passed on the command line. (Thomi Richards, #1252084) - 0.0.15 ------ -- cgit v1.2.1 From 3f6f9f6eaec55bfd055907c218c3e9707a03635c Mon Sep 17 00:00:00 2001 From: Thomi Richards Date: Mon, 2 Dec 2013 18:30:24 +1300 Subject: Add files to listings in Makefile.am --- Makefile.am | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/Makefile.am b/Makefile.am index 2a14b81..e8f018e 100644 --- a/Makefile.am +++ b/Makefile.am @@ -27,9 +27,10 @@ EXTRA_DIST = \ python/subunit/tests/test_chunked.py \ python/subunit/tests/test_details.py \ python/subunit/tests/test_filters.py \ + python/subunit/tests/test_output_filter.py \ python/subunit/tests/test_progress_model.py \ - python/subunit/tests/test_subunit_filter.py \ python/subunit/tests/test_run.py \ + python/subunit/tests/test_subunit_filter.py \ python/subunit/tests/test_subunit_stats.py \ python/subunit/tests/test_subunit_tags.py \ python/subunit/tests/test_tap2subunit.py \ @@ -79,7 +80,8 @@ pkgpython_PYTHON = \ python/subunit/progress_model.py \ python/subunit/run.py \ python/subunit/v2.py \ - python/subunit/test_results.py + python/subunit/test_results.py \ + python/subunit/_output.py lib_LTLIBRARIES = libsubunit.la lib_LTLIBRARIES += libcppunit_subunit.la -- cgit v1.2.1 From 51b0b53c3854f5bb1c2c024cc3176aaad87727df Mon Sep 17 00:00:00 2001 From: Thomi Richards Date: Mon, 2 Dec 2013 18:41:11 +1300 Subject: Fix failing tests. --- python/subunit/tests/test_output_filter.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/python/subunit/tests/test_output_filter.py b/python/subunit/tests/test_output_filter.py index 758110e..c6caada 100644 --- a/python/subunit/tests/test_output_filter.py +++ b/python/subunit/tests/test_output_filter.py @@ -134,7 +134,7 @@ class TestStatusArgParserTests(WithScenarios, TestCaseWithPatchedStderr): self.assertThat( fn, raises(RuntimeError('subunit-output: error: argument %s: must ' - 'specify a single TEST_ID.\n')) + 'specify a single TEST_ID.\n' % self.option)) ) -- cgit v1.2.1 From 59175ec6b495e330b709ea137f6dc7ce4caefb68 Mon Sep 17 00:00:00 2001 From: Thomi Richards Date: Mon, 2 Dec 2013 18:52:10 +1300 Subject: Fix failing tests. --- python/subunit/_output.py | 6 +++--- python/subunit/tests/test_output_filter.py | 4 ++-- 2 files changed, 5 insertions(+), 5 deletions(-) diff --git a/python/subunit/_output.py b/python/subunit/_output.py index 0e9302b..af9dcad 100644 --- a/python/subunit/_output.py +++ b/python/subunit/_output.py @@ -132,7 +132,7 @@ def parse_arguments(args=None, ParserClass=OptionParser): if not options.file_name: options.file_name = 'stdin' if sys.version[0] >= '3': - options.attach_file = sys.stdin.buffer + options.attach_file = getattr(sys.stdin, 'buffer', sys.stdin) else: options.attach_file = sys.stdin else: @@ -150,10 +150,10 @@ def parse_arguments(args=None, ParserClass=OptionParser): def set_status_cb(option, opt_str, value, parser, status_name): if getattr(parser.values, "action", None) is not None: - raise OptionValueError("argument %s: Only one status may be specified at once." % option) + raise OptionValueError("argument %s: Only one status may be specified at once." % opt_str) if len(parser.rargs) == 0: - raise OptionValueError("argument %s: must specify a single TEST_ID." % option) + raise OptionValueError("argument %s: must specify a single TEST_ID." % opt_str) parser.values.action = status_name parser.values.test_id = parser.rargs.pop(0) diff --git a/python/subunit/tests/test_output_filter.py b/python/subunit/tests/test_output_filter.py index c6caada..f53b37a 100644 --- a/python/subunit/tests/test_output_filter.py +++ b/python/subunit/tests/test_output_filter.py @@ -513,7 +513,7 @@ class FileDataTests(TestCase): def test_files_have_timestamp(self): _dummy_timestamp = datetime.datetime(2013, 1, 1, 0, 0, 0, 0, UTC) - self.patch(_o, 'create_timestamp', lambda: self._dummy_timestamp) + self.patch(_o, 'create_timestamp', lambda: _dummy_timestamp) with temp_file_contents(b"Hello") as f: specified_file_name = self.getUniqueString() @@ -525,7 +525,7 @@ class FileDataTests(TestCase): self.assertThat( result._events, MatchesListwise([ - MatchesStatusCall(file_bytes=b'Hello', timestamp=self._dummy_timestamp), + MatchesStatusCall(file_bytes=b'Hello', timestamp=_dummy_timestamp), ]) ) -- cgit v1.2.1 From 4aee3ea6c904afd4c98049552f6ac2fccb6d379d Mon Sep 17 00:00:00 2001 From: Thomi Richards Date: Mon, 2 Dec 2013 19:01:18 +1300 Subject: Use make_stream_binary to turn stdin into a binary stream. --- python/subunit/_output.py | 6 ++---- 1 file changed, 2 insertions(+), 4 deletions(-) diff --git a/python/subunit/_output.py b/python/subunit/_output.py index af9dcad..6df6404 100644 --- a/python/subunit/_output.py +++ b/python/subunit/_output.py @@ -22,6 +22,7 @@ from optparse import ( ) import sys +from subunit import make_stream_binary from subunit.iso8601 import UTC from subunit.v2 import StreamResultToBytes @@ -131,10 +132,7 @@ def parse_arguments(args=None, ParserClass=OptionParser): if options.attach_file == '-': if not options.file_name: options.file_name = 'stdin' - if sys.version[0] >= '3': - options.attach_file = getattr(sys.stdin, 'buffer', sys.stdin) - else: - options.attach_file = sys.stdin + options.attach_file = make_stream_binary(sys.stdin) else: try: options.attach_file = open(options.attach_file, 'rb') -- cgit v1.2.1 From 3763dba8d33291800236973791bc2d4d45b4a018 Mon Sep 17 00:00:00 2001 From: Thomi Richards Date: Mon, 2 Dec 2013 19:12:14 +1300 Subject: Patch sys.stdin correctly for testing. --- python/subunit/tests/test_output_filter.py | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/python/subunit/tests/test_output_filter.py b/python/subunit/tests/test_output_filter.py index f53b37a..9a54a42 100644 --- a/python/subunit/tests/test_output_filter.py +++ b/python/subunit/tests/test_output_filter.py @@ -16,7 +16,7 @@ import datetime from functools import partial -from io import BytesIO, StringIO +from io import BytesIO, StringIO, TextIOWrapper import optparse import sys from tempfile import NamedTemporaryFile @@ -108,12 +108,12 @@ class TestStatusArgParserTests(WithScenarios, TestCaseWithPatchedStderr): self.assertThat(args.tags, Equals(["foo", "bar", "baz"])) def test_attach_file_with_hyphen_opens_stdin(self): - self.patch(_o.sys, 'stdin', StringIO(_u("Hello"))) + self.patch(_o.sys, 'stdin', TextIOWrapper(BytesIO(b"Hello"))) args = safe_parse_arguments( args=[self.option, "foo", "--attach-file", "-"] ) - self.assertThat(args.attach_file.read(), Equals("Hello")) + self.assertThat(args.attach_file.read(), Equals(b"Hello")) def test_attach_file_with_hyphen_sets_filename_to_stdin(self): args = safe_parse_arguments( @@ -311,7 +311,7 @@ class StatusStreamResultTests(WithScenarios, TestCase): ) def test_can_read_stdin(self): - self.patch(_o.sys, 'stdin', BytesIO(b"\xFE\xED\xFA\xCE")) + self.patch(_o.sys, 'stdin', TextIOWrapper(BytesIO(b"\xFE\xED\xFA\xCE"))) result = get_result_for([self.option, self.test_id, '--attach-file', '-']) self.assertThat( -- cgit v1.2.1 From 3f03102b2d8c48914ba448f4e5f2213e9545f907 Mon Sep 17 00:00:00 2001 From: Thomi Richards Date: Mon, 9 Dec 2013 15:36:12 +1300 Subject: Fix typo. --- python/subunit/_output.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/python/subunit/_output.py b/python/subunit/_output.py index 6df6404..8d6f169 100644 --- a/python/subunit/_output.py +++ b/python/subunit/_output.py @@ -108,7 +108,7 @@ def parse_arguments(args=None, ParserClass=OptionParser): "--mimetype", help="The mime type to send with this file. This is only used if the " "--attach-file argument is used. This argument is optional. If it " - "is not specified, the file will be sent wihtout a mime type. This " + "is not specified, the file will be sent without a mime type. This " "option may only be specified when '--attach-file' is specified.", default=None ) -- cgit v1.2.1 From dd942d9a7d5f704ed512cbe5a9fef13223dd08be Mon Sep 17 00:00:00 2001 From: Thomi Richards Date: Mon, 9 Dec 2013 16:22:37 +1300 Subject: Use the 'append' action, instead of specifying tags as a comma-separated list of values. --- python/subunit/_output.py | 18 +++++------------- python/subunit/tests/test_output_filter.py | 18 ++++++++++-------- 2 files changed, 15 insertions(+), 21 deletions(-) diff --git a/python/subunit/_output.py b/python/subunit/_output.py index 8d6f169..51aaa8f 100644 --- a/python/subunit/_output.py +++ b/python/subunit/_output.py @@ -115,11 +115,10 @@ def parse_arguments(args=None, ParserClass=OptionParser): parser.add_option_group(file_commands) parser.add_option( - "--tags", - help="A comma-separated list of tags to associate with a test. This " - "option may only be used with a status command.", - action="callback", - callback=set_tags_cb, + "--tag", + help="Specifies a tag. May be used multiple times", + action="append", + dest="tags", default=[] ) @@ -139,7 +138,7 @@ def parse_arguments(args=None, ParserClass=OptionParser): except IOError as e: parser.error("Cannot open %s (%s)" % (options.attach_file, e.strerror)) if options.tags and not options.action: - parser.error("Cannot specify --tags without a status command") + parser.error("Cannot specify --tag without a status command") if not (options.attach_file or options.action): parser.error("Must specify either --attach-file or a status command") @@ -156,12 +155,6 @@ def set_status_cb(option, opt_str, value, parser, status_name): parser.values.test_id = parser.rargs.pop(0) -def set_tags_cb(option, opt_str, value, parser): - if not parser.rargs: - raise OptionValueError("Must specify at least one tag with --tags") - parser.values.tags = parser.rargs.pop(0).split(',') - - def generate_stream_results(args, output_writer): output_writer.startTestRun() @@ -202,7 +195,6 @@ def generate_stream_results(args, output_writer): write_status = partial(write_status, test_id=args.test_id) if is_last_packet: - write_status = partial(write_status, eof=True) if args.action in _FINAL_ACTIONS: write_status = partial(write_status, test_status=args.action) diff --git a/python/subunit/tests/test_output_filter.py b/python/subunit/tests/test_output_filter.py index 9a54a42..f01d66a 100644 --- a/python/subunit/tests/test_output_filter.py +++ b/python/subunit/tests/test_output_filter.py @@ -103,7 +103,7 @@ class TestStatusArgParserTests(WithScenarios, TestCaseWithPatchedStderr): def test_all_commands_accept_tags_argument(self): args = safe_parse_arguments( - args=[self.option, 'foo', '--tags', "foo,bar,baz"] + args=[self.option, 'foo', '--tag', "foo", "--tag", "bar", "--tag", "baz"] ) self.assertThat(args.tags, Equals(["foo", "bar", "baz"])) @@ -180,18 +180,18 @@ class ArgParserTests(TestCaseWithPatchedStderr): ) def test_cannot_specify_tags_without_status_command(self): - fn = lambda: safe_parse_arguments(['--tags', 'foo']) + fn = lambda: safe_parse_arguments(['--tag', 'foo']) self.assertThat( fn, raises(RuntimeError('subunit-output: error: Cannot specify ' - '--tags without a status command\n')) + '--tag without a status command\n')) ) def test_must_specify_tags_with_tags_options(self): - fn = lambda: safe_parse_arguments(['--fail', 'foo', '--tags']) + fn = lambda: safe_parse_arguments(['--fail', 'foo', '--tag']) self.assertThat( fn, - raises(RuntimeError('subunit-output: error: Must specify at least one tag with --tags\n')) + raises(RuntimeError('subunit-output: error: --tag option requires 1 argument\n')) ) @@ -255,7 +255,7 @@ class StatusStreamResultTests(WithScenarios, TestCase): ) def test_all_commands_generate_tags(self): - result = get_result_for([self.option, self.test_id, '--tags', 'hello,world']) + result = get_result_for([self.option, self.test_id, '--tag', 'hello', '--tag', 'world']) self.assertThat( result._events[0], MatchesStatusCall(test_tags=set(['hello', 'world'])) @@ -387,8 +387,10 @@ class StatusStreamResultTests(WithScenarios, TestCase): self.test_id, '--attach-file', f.name, - '--tags', - 'foo,bar', + '--tag', + 'foo', + '--tag', + 'bar', ]) self.assertThat( -- cgit v1.2.1 From 77e6b99c36d52d13ff68567d07b83307fbe64ed5 Mon Sep 17 00:00:00 2001 From: Thomi Richards Date: Mon, 9 Dec 2013 17:28:26 +1300 Subject: Allow the use of the --tag argument without specifying a test id. --- python/subunit/_output.py | 4 ---- python/subunit/tests/test_output_filter.py | 31 ++++++++++++++++-------------- 2 files changed, 17 insertions(+), 18 deletions(-) diff --git a/python/subunit/_output.py b/python/subunit/_output.py index 51aaa8f..14d7ad5 100644 --- a/python/subunit/_output.py +++ b/python/subunit/_output.py @@ -137,10 +137,6 @@ def parse_arguments(args=None, ParserClass=OptionParser): options.attach_file = open(options.attach_file, 'rb') except IOError as e: parser.error("Cannot open %s (%s)" % (options.attach_file, e.strerror)) - if options.tags and not options.action: - parser.error("Cannot specify --tag without a status command") - if not (options.attach_file or options.action): - parser.error("Must specify either --attach-file or a status command") return options diff --git a/python/subunit/tests/test_output_filter.py b/python/subunit/tests/test_output_filter.py index f01d66a..673f89d 100644 --- a/python/subunit/tests/test_output_filter.py +++ b/python/subunit/tests/test_output_filter.py @@ -147,13 +147,8 @@ class ArgParserTests(TestCaseWithPatchedStderr): ) self.assertThat(args.attach_file.name, Equals(tmp_file.name)) - def test_must_specify_argument(self): - fn = lambda: safe_parse_arguments([]) - self.assertThat( - fn, - raises(RuntimeError('subunit-output: error: Must specify either ' - '--attach-file or a status command\n')) - ) + def test_can_run_without_args(self): + args = safe_parse_arguments([]) def test_cannot_specify_more_than_one_status_command(self): fn = lambda: safe_parse_arguments(['--fail', 'foo', '--skip', 'bar']) @@ -179,13 +174,9 @@ class ArgParserTests(TestCaseWithPatchedStderr): '--file-name without --attach-file\n')) ) - def test_cannot_specify_tags_without_status_command(self): - fn = lambda: safe_parse_arguments(['--tag', 'foo']) - self.assertThat( - fn, - raises(RuntimeError('subunit-output: error: Cannot specify ' - '--tag without a status command\n')) - ) + def test_can_specify_tags_without_status_command(self): + args = safe_parse_arguments(['--tag', 'foo']) + self.assertEqual(['foo'], args.tags) def test_must_specify_tags_with_tags_options(self): fn = lambda: safe_parse_arguments(['--fail', 'foo', '--tag']) @@ -531,6 +522,18 @@ class FileDataTests(TestCase): ]) ) + def test_can_specify_tags_without_test_status(self): + result = get_result_for([ + '--tag', + 'foo', + ]) + + self.assertThat( + result._events, + MatchesListwise([ + MatchesStatusCall(test_tags=set(['foo'])), + ]) + ) class MatchesStatusCall(Matcher): -- cgit v1.2.1 From a0fcc7408a80faaa74e20b3ae64d9f21b9037b42 Mon Sep 17 00:00:00 2001 From: Thomi Richards Date: Mon, 9 Dec 2013 17:39:26 +1300 Subject: Don't need to patch stderr anymore in the tests. --- python/subunit/tests/test_output_filter.py | 33 +++++++++--------------------- 1 file changed, 10 insertions(+), 23 deletions(-) diff --git a/python/subunit/tests/test_output_filter.py b/python/subunit/tests/test_output_filter.py index 673f89d..f03a7b7 100644 --- a/python/subunit/tests/test_output_filter.py +++ b/python/subunit/tests/test_output_filter.py @@ -51,23 +51,14 @@ class SafeOptionParser(optparse.OptionParser): def exit(self, status=0, message=""): raise RuntimeError(message) + def error(self, message): + raise RuntimeError(message) -safe_parse_arguments = partial(parse_arguments, ParserClass=SafeOptionParser) - - -class TestCaseWithPatchedStderr(TestCase): - def setUp(self): - super(TestCaseWithPatchedStderr, self).setUp() - # prevent OptionParser from printing to stderr: - if sys.version[0] > '2': - self._stderr = StringIO() - else: - self._stderr = BytesIO() - self.patch(optparse.sys, 'stderr', self._stderr) +safe_parse_arguments = partial(parse_arguments, ParserClass=SafeOptionParser) -class TestStatusArgParserTests(WithScenarios, TestCaseWithPatchedStderr): +class TestStatusArgParserTests(WithScenarios, TestCase): scenarios = [ (cmd, dict(command=cmd, option='--' + cmd)) for cmd in _ALL_ACTIONS @@ -133,12 +124,11 @@ class TestStatusArgParserTests(WithScenarios, TestCaseWithPatchedStderr): fn = lambda: safe_parse_arguments(args=[self.option]) self.assertThat( fn, - raises(RuntimeError('subunit-output: error: argument %s: must ' - 'specify a single TEST_ID.\n' % self.option)) + raises(RuntimeError('argument %s: must specify a single TEST_ID.' % self.option)) ) -class ArgParserTests(TestCaseWithPatchedStderr): +class ArgParserTests(TestCase): def test_can_parse_attach_file_without_test_id(self): with NamedTemporaryFile() as tmp_file: @@ -154,24 +144,21 @@ class ArgParserTests(TestCaseWithPatchedStderr): fn = lambda: safe_parse_arguments(['--fail', 'foo', '--skip', 'bar']) self.assertThat( fn, - raises(RuntimeError('subunit-output: error: argument --skip: ' - 'Only one status may be specified at once.\n')) + raises(RuntimeError('argument --skip: Only one status may be specified at once.')) ) def test_cannot_specify_mimetype_without_attach_file(self): fn = lambda: safe_parse_arguments(['--mimetype', 'foo']) self.assertThat( fn, - raises(RuntimeError('subunit-output: error: Cannot specify ' - '--mimetype without --attach-file\n')) + raises(RuntimeError('Cannot specify --mimetype without --attach-file')) ) def test_cannot_specify_filename_without_attach_file(self): fn = lambda: safe_parse_arguments(['--file-name', 'foo']) self.assertThat( fn, - raises(RuntimeError('subunit-output: error: Cannot specify ' - '--file-name without --attach-file\n')) + raises(RuntimeError('Cannot specify --file-name without --attach-file')) ) def test_can_specify_tags_without_status_command(self): @@ -182,7 +169,7 @@ class ArgParserTests(TestCaseWithPatchedStderr): fn = lambda: safe_parse_arguments(['--fail', 'foo', '--tag']) self.assertThat( fn, - raises(RuntimeError('subunit-output: error: --tag option requires 1 argument\n')) + raises(RuntimeError('--tag option requires 1 argument')) ) -- cgit v1.2.1 From bd4b81509d6e766ee59328859f649431023d5b2a Mon Sep 17 00:00:00 2001 From: Thomi Richards Date: Mon, 9 Dec 2013 17:52:06 +1300 Subject: Add testscenario dependency to the INSTALL file. --- INSTALL | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/INSTALL b/INSTALL index 51c0e79..29052eb 100644 --- a/INSTALL +++ b/INSTALL @@ -14,9 +14,12 @@ Dependencies * Python for the filters * 'testtools' (On Debian and Ubuntu systems the 'python-testtools' package, the testtools package on pypi, or https://launchpad.net/testtools) for - the extended test API which permits attachments. Version 0.9.30 or newer is + the extended test API which permits attachments. Version 0.9.30 or newer is required. Of particular note, http://testtools.python-hosting.com/ is not the testtools you want. +* 'testscenarios' (On Debian and Ubuntu systems the 'python-testscenarios' + package, the 'testscenarios' package on pypi, or + https://launchpad.net/testscenarios) for running some of the python unit tests. * A C compiler for the C bindings * Perl for the Perl tools (including subunit-diff) * Check to run the subunit test suite. -- cgit v1.2.1 From ce9e871752a6bc22f8cc30d558d45d22f85ddeea Mon Sep 17 00:00:00 2001 From: Thomi Richards Date: Mon, 9 Dec 2013 17:58:22 +1300 Subject: Generate scenarios inside subunit.tests.test_suite, not by subclassing WithScenarios. --- python/subunit/tests/__init__.py | 6 +++++- python/subunit/tests/test_output_filter.py | 5 ++--- 2 files changed, 7 insertions(+), 4 deletions(-) diff --git a/python/subunit/tests/__init__.py b/python/subunit/tests/__init__.py index b5a7fdc..c1c2c64 100644 --- a/python/subunit/tests/__init__.py +++ b/python/subunit/tests/__init__.py @@ -17,6 +17,8 @@ import sys from unittest import TestLoader +from testscenarios import generate_scenarios + # Before the test module imports to avoid circularity. # For testing: different pythons have different str() implementations. @@ -61,5 +63,7 @@ def test_suite(): result.addTest(loader.loadTestsFromModule(test_subunit_tags)) result.addTest(loader.loadTestsFromModule(test_subunit_stats)) result.addTest(loader.loadTestsFromModule(test_run)) - result.addTest(loader.loadTestsFromModule(test_output_filter)) + result.addTests( + generate_scenarios(loader.loadTestsFromModule(test_output_filter)) + ) return result diff --git a/python/subunit/tests/test_output_filter.py b/python/subunit/tests/test_output_filter.py index f03a7b7..3373d48 100644 --- a/python/subunit/tests/test_output_filter.py +++ b/python/subunit/tests/test_output_filter.py @@ -22,7 +22,6 @@ import sys from tempfile import NamedTemporaryFile from contextlib import contextmanager -from testscenarios import WithScenarios from testtools import TestCase from testtools.compat import _u from testtools.matchers import ( @@ -58,7 +57,7 @@ class SafeOptionParser(optparse.OptionParser): safe_parse_arguments = partial(parse_arguments, ParserClass=SafeOptionParser) -class TestStatusArgParserTests(WithScenarios, TestCase): +class TestStatusArgParserTests(TestCase): scenarios = [ (cmd, dict(command=cmd, option='--' + cmd)) for cmd in _ALL_ACTIONS @@ -204,7 +203,7 @@ def temp_file_contents(data): yield f -class StatusStreamResultTests(WithScenarios, TestCase): +class StatusStreamResultTests(TestCase): scenarios = [ (s, dict(status=s, option='--' + s)) for s in _ALL_ACTIONS -- cgit v1.2.1 From 850d95de4b145607fc3e2d39971e39374081e182 Mon Sep 17 00:00:00 2001 From: Thomi Richards Date: Tue, 10 Dec 2013 09:52:07 +1300 Subject: Don't make tests convert to and from bytes. Instead, just use a StreamResult double. Update test code. --- python/subunit/_output.py | 2 +- python/subunit/tests/test_output_filter.py | 64 ++++++++++++++++++++++-------- 2 files changed, 49 insertions(+), 17 deletions(-) diff --git a/python/subunit/_output.py b/python/subunit/_output.py index 14d7ad5..aa92646 100644 --- a/python/subunit/_output.py +++ b/python/subunit/_output.py @@ -169,7 +169,7 @@ def generate_stream_results(args, output_writer): if args.mimetype: write_status = partial(write_status, mime_type=args.mimetype) if args.tags: - write_status = partial(write_status, test_tags=args.tags) + write_status = partial(write_status, test_tags=set(args.tags)) write_status = partial(write_status, timestamp=create_timestamp()) if args.action not in _FINAL_ACTIONS: write_status = partial(write_status, test_status=args.action) diff --git a/python/subunit/tests/test_output_filter.py b/python/subunit/tests/test_output_filter.py index 3373d48..4099023 100644 --- a/python/subunit/tests/test_output_filter.py +++ b/python/subunit/tests/test_output_filter.py @@ -180,17 +180,9 @@ def get_result_for(commands): resulting bytestream is then converted back into a result object and returned. """ - stream = BytesIO() - - args = safe_parse_arguments(commands) - output_writer = StreamResultToBytes(output_stream=stream) - generate_stream_results(args, output_writer) - - stream.seek(0) - - case = ByteStreamToStreamResult(source=stream) result = StreamResult() - case.run(result) + args = safe_parse_arguments(commands) + generate_stream_results(args, result) return result @@ -220,21 +212,21 @@ class StatusStreamResultTests(TestCase): result = get_result_for([self.option, self.test_id]) self.assertThat( len(result._events), - Equals(1) + Equals(3) # startTestRun and stopTestRun are also called, making 3 total. ) def test_correct_status_is_generated(self): result = get_result_for([self.option, self.test_id]) self.assertThat( - result._events[0], + result._events[1], MatchesStatusCall(test_status=self.status) ) def test_all_commands_generate_tags(self): result = get_result_for([self.option, self.test_id, '--tag', 'hello', '--tag', 'world']) self.assertThat( - result._events[0], + result._events[1], MatchesStatusCall(test_tags=set(['hello', 'world'])) ) @@ -242,7 +234,7 @@ class StatusStreamResultTests(TestCase): result = get_result_for([self.option, self.test_id]) self.assertThat( - result._events[0], + result._events[1], MatchesStatusCall(timestamp=self._dummy_timestamp) ) @@ -250,7 +242,7 @@ class StatusStreamResultTests(TestCase): result = get_result_for([self.option, self.test_id]) self.assertThat( - result._events[0], + result._events[1], MatchesStatusCall(test_id=self.test_id) ) @@ -261,7 +253,9 @@ class StatusStreamResultTests(TestCase): self.assertThat( result._events, MatchesListwise([ + MatchesStatusCall(call='startTestRun'), MatchesStatusCall(file_bytes=b'Hello', eof=True), + MatchesStatusCall(call='stopTestRun'), ]) ) @@ -272,7 +266,9 @@ class StatusStreamResultTests(TestCase): self.assertThat( result._events, MatchesListwise([ + MatchesStatusCall(call='startTestRun'), MatchesStatusCall(file_bytes=b"\xDE\xAD\xBE\xEF", eof=True), + MatchesStatusCall(call='stopTestRun'), ]) ) @@ -283,7 +279,9 @@ class StatusStreamResultTests(TestCase): self.assertThat( result._events, MatchesListwise([ + MatchesStatusCall(call='startTestRun'), MatchesStatusCall(file_bytes=b"", file_name=f.name, eof=True), + MatchesStatusCall(call='stopTestRun'), ]) ) @@ -294,7 +292,9 @@ class StatusStreamResultTests(TestCase): self.assertThat( result._events, MatchesListwise([ + MatchesStatusCall(call='startTestRun'), MatchesStatusCall(file_bytes=b"\xFE\xED\xFA\xCE", file_name='stdin', eof=True), + MatchesStatusCall(call='stopTestRun'), ]) ) @@ -305,7 +305,9 @@ class StatusStreamResultTests(TestCase): self.assertThat( result._events, MatchesListwise([ + MatchesStatusCall(call='startTestRun'), MatchesStatusCall(test_id=self.test_id, file_bytes=b'Hello', eof=True), + MatchesStatusCall(call='stopTestRun'), ]) ) @@ -316,7 +318,9 @@ class StatusStreamResultTests(TestCase): self.assertThat( result._events, MatchesListwise([ + MatchesStatusCall(call='startTestRun'), MatchesStatusCall(test_status=self.status, file_bytes=b'Hello', eof=True), + MatchesStatusCall(call='stopTestRun'), ]) ) @@ -328,11 +332,13 @@ class StatusStreamResultTests(TestCase): self.assertThat( result._events, MatchesListwise([ + MatchesStatusCall(call='startTestRun'), MatchesStatusCall(test_id=self.test_id, file_bytes=b'H', eof=False), MatchesStatusCall(test_id=self.test_id, file_bytes=b'e', eof=False), MatchesStatusCall(test_id=self.test_id, file_bytes=b'l', eof=False), MatchesStatusCall(test_id=self.test_id, file_bytes=b'l', eof=False), MatchesStatusCall(test_id=self.test_id, file_bytes=b'o', eof=True), + MatchesStatusCall(call='stopTestRun'), ]) ) @@ -351,8 +357,10 @@ class StatusStreamResultTests(TestCase): self.assertThat( result._events, MatchesListwise([ + MatchesStatusCall(call='startTestRun'), MatchesStatusCall(test_id=self.test_id, mime_type='text/plain', file_bytes=b'H', eof=False), MatchesStatusCall(test_id=self.test_id, mime_type=None, file_bytes=b'i', eof=True), + MatchesStatusCall(call='stopTestRun'), ]) ) @@ -373,8 +381,10 @@ class StatusStreamResultTests(TestCase): self.assertThat( result._events, MatchesListwise([ + MatchesStatusCall(call='startTestRun'), MatchesStatusCall(test_id=self.test_id, test_tags=set(['foo', 'bar'])), MatchesStatusCall(test_id=self.test_id, test_tags=None), + MatchesStatusCall(call='stopTestRun'), ]) ) @@ -391,8 +401,10 @@ class StatusStreamResultTests(TestCase): self.assertThat( result._events, MatchesListwise([ + MatchesStatusCall(call='startTestRun'), MatchesStatusCall(test_id=self.test_id, timestamp=self._dummy_timestamp), MatchesStatusCall(test_id=self.test_id, timestamp=None), + MatchesStatusCall(call='stopTestRun'), ]) ) @@ -416,7 +428,12 @@ class StatusStreamResultTests(TestCase): last_call = MatchesStatusCall(test_id=self.test_id, test_status=None) self.assertThat( result._events, - MatchesListwise([first_call, last_call]) + MatchesListwise([ + MatchesStatusCall(call='startTestRun'), + first_call, + last_call, + MatchesStatusCall(call='stopTestRun'), + ]) ) def test_filename_can_be_overridden(self): @@ -433,7 +450,9 @@ class StatusStreamResultTests(TestCase): self.assertThat( result._events, MatchesListwise([ + MatchesStatusCall(call='startTestRun'), MatchesStatusCall(file_name=specified_file_name, file_bytes=b'Hello'), + MatchesStatusCall(call='stopTestRun'), ]) ) @@ -444,7 +463,9 @@ class StatusStreamResultTests(TestCase): self.assertThat( result._events, MatchesListwise([ + MatchesStatusCall(call='startTestRun'), MatchesStatusCall(file_name=f.name, file_bytes=b'Hello', eof=True), + MatchesStatusCall(call='stopTestRun'), ]) ) @@ -458,7 +479,9 @@ class FileDataTests(TestCase): self.assertThat( result._events, MatchesListwise([ + MatchesStatusCall(call='startTestRun'), MatchesStatusCall(test_id=None, file_bytes=b'Hello', eof=True), + MatchesStatusCall(call='stopTestRun'), ]) ) @@ -469,7 +492,9 @@ class FileDataTests(TestCase): self.assertThat( result._events, MatchesListwise([ + MatchesStatusCall(call='startTestRun'), MatchesStatusCall(file_name=f.name, file_bytes=b'Hello', eof=True), + MatchesStatusCall(call='stopTestRun'), ]) ) @@ -486,7 +511,9 @@ class FileDataTests(TestCase): self.assertThat( result._events, MatchesListwise([ + MatchesStatusCall(call='startTestRun'), MatchesStatusCall(file_name=specified_file_name, file_bytes=b'Hello'), + MatchesStatusCall(call='stopTestRun'), ]) ) @@ -504,7 +531,9 @@ class FileDataTests(TestCase): self.assertThat( result._events, MatchesListwise([ + MatchesStatusCall(call='startTestRun'), MatchesStatusCall(file_bytes=b'Hello', timestamp=_dummy_timestamp), + MatchesStatusCall(call='stopTestRun'), ]) ) @@ -517,10 +546,13 @@ class FileDataTests(TestCase): self.assertThat( result._events, MatchesListwise([ + MatchesStatusCall(call='startTestRun'), MatchesStatusCall(test_tags=set(['foo'])), + MatchesStatusCall(call='stopTestRun'), ]) ) + class MatchesStatusCall(Matcher): _position_lookup = { -- cgit v1.2.1 From 98df8113e8192265fb7d53867444b842082d9da5 Mon Sep 17 00:00:00 2001 From: Thomi Richards Date: Tue, 10 Dec 2013 11:10:57 +1300 Subject: Make tests work in py2 and py3. --- python/subunit/tests/test_output_filter.py | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/python/subunit/tests/test_output_filter.py b/python/subunit/tests/test_output_filter.py index 4099023..40bec89 100644 --- a/python/subunit/tests/test_output_filter.py +++ b/python/subunit/tests/test_output_filter.py @@ -166,9 +166,13 @@ class ArgParserTests(TestCase): def test_must_specify_tags_with_tags_options(self): fn = lambda: safe_parse_arguments(['--fail', 'foo', '--tag']) + if sys.version[0] >= '3': + expected_message = '--tag option requires 1 argument' + else: + expected_message = '--tag option requires an argument' self.assertThat( fn, - raises(RuntimeError('--tag option requires 1 argument')) + raises(RuntimeError(expected_message)) ) -- cgit v1.2.1 From 69b9cfbb37effa0f55097be6342dafab97111555 Mon Sep 17 00:00:00 2001 From: Thomi Richards Date: Mon, 16 Dec 2013 09:02:35 +1300 Subject: Fix failing test in python 3.2. --- python/subunit/tests/test_output_filter.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/python/subunit/tests/test_output_filter.py b/python/subunit/tests/test_output_filter.py index 40bec89..7b11d4d 100644 --- a/python/subunit/tests/test_output_filter.py +++ b/python/subunit/tests/test_output_filter.py @@ -166,7 +166,7 @@ class ArgParserTests(TestCase): def test_must_specify_tags_with_tags_options(self): fn = lambda: safe_parse_arguments(['--fail', 'foo', '--tag']) - if sys.version[0] >= '3': + if sys.version[0] > '3.2': expected_message = '--tag option requires 1 argument' else: expected_message = '--tag option requires an argument' -- cgit v1.2.1 From 19b2995c1171430b421ddbd5d93101f4f427f4c6 Mon Sep 17 00:00:00 2001 From: Thomi Richards Date: Mon, 13 Jan 2014 11:10:11 +1300 Subject: Fix failing test on py33. --- python/subunit/tests/test_output_filter.py | 10 ++++------ 1 file changed, 4 insertions(+), 6 deletions(-) diff --git a/python/subunit/tests/test_output_filter.py b/python/subunit/tests/test_output_filter.py index 7b11d4d..cba9332 100644 --- a/python/subunit/tests/test_output_filter.py +++ b/python/subunit/tests/test_output_filter.py @@ -166,16 +166,14 @@ class ArgParserTests(TestCase): def test_must_specify_tags_with_tags_options(self): fn = lambda: safe_parse_arguments(['--fail', 'foo', '--tag']) - if sys.version[0] > '3.2': - expected_message = '--tag option requires 1 argument' - else: - expected_message = '--tag option requires an argument' self.assertThat( fn, - raises(RuntimeError(expected_message)) + MatchesAny( + raises(RuntimeError('--tag option requires 1 argument')), + raises(RuntimeError('--tag option requires an argument')), + ) ) - def get_result_for(commands): """Get a result object from *commands. -- cgit v1.2.1 From 3c660e191002c852bf579bbee92597a274c8ada8 Mon Sep 17 00:00:00 2001 From: Thomi Richards Date: Mon, 13 Jan 2014 11:14:30 +1300 Subject: Import matcher used. --- python/subunit/tests/test_output_filter.py | 1 + 1 file changed, 1 insertion(+) diff --git a/python/subunit/tests/test_output_filter.py b/python/subunit/tests/test_output_filter.py index cba9332..0f61ac5 100644 --- a/python/subunit/tests/test_output_filter.py +++ b/python/subunit/tests/test_output_filter.py @@ -27,6 +27,7 @@ from testtools.compat import _u from testtools.matchers import ( Equals, Matcher, + MatchesAny, MatchesListwise, Mismatch, raises, -- cgit v1.2.1