From b62ae3a9da848e6b8d0e0109a77cca712eb46a71 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. --- python/subunit/_output.py | 18 ++++++++++++++++++ 1 file changed, 18 insertions(+) create mode 100644 python/subunit/_output.py (limited to 'python') 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 -- cgit v1.2.1 From 980ddea6ac3ca2f2d5cef72a739a95ae6d753bc2 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 (limited to 'python') 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 17743eb0a1dfa1f2bb0b3eafa2431419dd19ded4 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(-) (limited to 'python') 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 c3dc5dd05d7676a32fc7027dd1cd9d73116bd6ff 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(-) (limited to 'python') 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 b705438d10498be7b4aad77cd0650f7a0c4614e0 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(-) (limited to 'python') 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 78bffde2922f10f68aef9db48cea3a00e57ff262 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(-) (limited to 'python') 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 f9b9c8ccebc2f7a9a42caabbfb11a81db02cfc99 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(-) (limited to 'python') 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 2b4a6de5804fb0b4cc207d384f8d6aac9f0c2a67 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(-) (limited to 'python') 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 d54483571e0cb8da9da9bfa5c407a6c152027a28 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(-) (limited to 'python') 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 8782262d7d0750beb6111b8197865343fe0a8637 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(-) (limited to 'python') 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 03b5ef473ebf841b6187f2423eaa5c97f36885fb 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(-) (limited to 'python') 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 e30001daaf6ce2ee233ce856a87df4d16b389062 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. --- python/subunit/tests/test_output_filter.py | 5 ++--- 1 file changed, 2 insertions(+), 3 deletions(-) (limited to 'python') 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 78a8d097a53203d717a653fe3184874bc988660f 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(-) (limited to 'python') 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 3feea3cf5f15d40808b0a787bd2bee15e600f614 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(-) (limited to 'python') 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 0111095cf3107071aeff699c460e248f87baf296 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(-) (limited to 'python') 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 f039432e1d69845d713df84f284d631dfa996190 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(-) (limited to 'python') 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 4d3f98360afb4c5f1677bd50a9f7eefb2e3b570b 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(-) (limited to 'python') 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 8eb109578e86a1e432b4a29081c610cfdbac4b82 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(-) (limited to 'python') 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 e700d2386957e28e10c0cf7477d3edec988462bb 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(-) (limited to 'python') 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 146bb5421645e810e9c17fc77b1e9e34d0c676d5 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. --- python/subunit/_output.py | 190 +++++++++++++++-------------- python/subunit/tests/test_output_filter.py | 143 ++++++++++++---------- 2 files changed, 180 insertions(+), 153 deletions(-) (limited to 'python') 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): -- cgit v1.2.1 From 0d60d811cbcabdf5e969dbc1e4cecf4ba8511413 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(-) (limited to 'python') 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 8ef01cb2e3cce3b95358daeac6db4fda27cc06d8 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. --- python/subunit/_output.py | 2 +- python/subunit/tests/test_output_filter.py | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) (limited to 'python') 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 b236b6c3c93e9949da86d0579c99ebd742f41726 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(-) (limited to 'python') 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 55af015d80cb2924ea2d59561ca3843dcc844345 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(-) (limited to 'python') 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 ede545424ac87391ea6abcd709359c6f53558991 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(-) (limited to 'python') 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 06531d31fe2428caaaf0509d39328787a43f0f44 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(-) (limited to 'python') 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 c4f8ec3a830b49316ae6edc3704f2a932e608625 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(-) (limited to 'python') 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 dd89c81df541ba7a43303e225b0c7c3fd4ccb756 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(-) (limited to 'python') 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 5c0a52bd25e0adc8a1cbba21452b57c355d47090 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(-) (limited to 'python') 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 3413fbd1d00240b74084d9ac001bdcb647a9d515 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 +++++++++++++++++++---------- 2 files changed, 301 insertions(+), 180 deletions(-) (limited to 'python') 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 -- cgit v1.2.1 From 2625bc70ecd451408830f98866f97f66e668dec3 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(-) (limited to 'python') 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 5ff12037a964e1ccf4a179fcafb6e91cf0dd4d0f 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(-) (limited to 'python') 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 1f597852ccc82d60591b17bf076225e54a8e84c2 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(-) (limited to 'python') 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 f9ab16ac7c0627069285e6046ce6f885f347238b 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(-) (limited to 'python') 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 62518e653d519f09d6d47c33ef66ee61590e8856 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(+) (limited to 'python') 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 075c9e5d7e7a1fb96e19da7295238c08504ddb93 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(-) (limited to 'python') 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 1ee230bbe58895d769ba5793eae2272b32de10c9 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(-) (limited to 'python') 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 988c887744f0047938586efe8db84c39de1c3315 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(-) (limited to 'python') 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 d89e45d46a28021669adf7f65ab6c985ca859189 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(-) (limited to 'python') 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 098addd40f6e8176ef3b371916bf89272d0a61e4 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(-) (limited to 'python') 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 0c3f2df1e2ed7c179e410c83908c76519206b8f1 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(-) (limited to 'python') 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 a0530ff5fc5b009c08a3fd48c52898198edd9971 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(-) (limited to 'python') 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 27fd8d82b07159d03c7b306808427b703615841b 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(-) (limited to 'python') 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 e34c19254653e38a8544df6ac149bc6709a2ea19 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(-) (limited to 'python') 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 48738830abf93a60488c74ba58252d01fd44a712 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(-) (limited to 'python') 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 db6c2795d80729fcc6e197b7c533e91b6dcdcbcb 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(-) (limited to 'python') 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 4a4f7ffda0a72a2735008029022cb92f737407b3 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(-) (limited to 'python') 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 da6c0076527849d6e720a5cfc5ed7544650f58f6 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(-) (limited to 'python') 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 727c9f25fac48381d7631a21eb4f6128f7ecb304 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(-) (limited to 'python') 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 745e34df06e236b0e114a55162e4833df9f99b56 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(-) (limited to 'python') 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 153303b8e9d1f76724feaeb1b62ef20cd6f393cf 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(+) (limited to 'python') 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