2 # subunit: extensions to python unittest to get test results from subprocesses.
3 # Copyright (C) 2013 Subunit Contributors
5 # Licensed under either the Apache License, Version 2.0 or the BSD 3-clause
6 # license at the users choice. A copy of both licenses are available in the
7 # project source as Apache-2.0 and BSD. You may not use this file except in
8 # compliance with one of these two licences.
10 # Unless required by applicable law or agreed to in writing, software
11 # distributed under these licenses is distributed on an "AS IS" BASIS, WITHOUT
12 # WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
13 # license you chose for the specific language governing permissions and
14 # limitations under that license.
18 from functools import partial
19 from io import BytesIO, StringIO, TextIOWrapper
22 from tempfile import NamedTemporaryFile
24 from contextlib import contextmanager
25 from testscenarios import WithScenarios
26 from testtools import TestCase
27 from testtools.compat import _u
28 from testtools.matchers import (
35 from testtools.testresult.doubles import StreamResult
37 from subunit.iso8601 import UTC
38 from subunit.v2 import StreamResultToBytes, ByteStreamToStreamResult
39 from subunit._output import (
42 generate_stream_results,
45 import subunit._output as _o
48 class SafeOptionParser(optparse.OptionParser):
49 """An ArgumentParser class that doesn't call sys.exit."""
51 def exit(self, status=0, message=""):
52 raise RuntimeError(message)
55 safe_parse_arguments = partial(parse_arguments, ParserClass=SafeOptionParser)
58 class TestCaseWithPatchedStderr(TestCase):
61 super(TestCaseWithPatchedStderr, self).setUp()
62 # prevent OptionParser from printing to stderr:
63 if sys.version[0] > '2':
64 self._stderr = StringIO()
66 self._stderr = BytesIO()
67 self.patch(optparse.sys, 'stderr', self._stderr)
70 class TestStatusArgParserTests(WithScenarios, TestCaseWithPatchedStderr):
73 (cmd, dict(command=cmd, option='--' + cmd)) for cmd in _ALL_ACTIONS
76 def test_can_parse_all_commands_with_test_id(self):
77 test_id = self.getUniqueString()
78 args = safe_parse_arguments(args=[self.option, test_id])
80 self.assertThat(args.action, Equals(self.command))
81 self.assertThat(args.test_id, Equals(test_id))
83 def test_all_commands_parse_file_attachment(self):
84 with NamedTemporaryFile() as tmp_file:
85 args = safe_parse_arguments(
86 args=[self.option, 'foo', '--attach-file', tmp_file.name]
88 self.assertThat(args.attach_file.name, Equals(tmp_file.name))
90 def test_all_commands_accept_mimetype_argument(self):
91 with NamedTemporaryFile() as tmp_file:
92 args = safe_parse_arguments(
93 args=[self.option, 'foo', '--attach-file', tmp_file.name, '--mimetype', "text/plain"]
95 self.assertThat(args.mimetype, Equals("text/plain"))
97 def test_all_commands_accept_file_name_argument(self):
98 with NamedTemporaryFile() as tmp_file:
99 args = safe_parse_arguments(
100 args=[self.option, 'foo', '--attach-file', tmp_file.name, '--file-name', "foo"]
102 self.assertThat(args.file_name, Equals("foo"))
104 def test_all_commands_accept_tags_argument(self):
105 args = safe_parse_arguments(
106 args=[self.option, 'foo', '--tag', "foo", "--tag", "bar", "--tag", "baz"]
108 self.assertThat(args.tags, Equals(["foo", "bar", "baz"]))
110 def test_attach_file_with_hyphen_opens_stdin(self):
111 self.patch(_o.sys, 'stdin', TextIOWrapper(BytesIO(b"Hello")))
112 args = safe_parse_arguments(
113 args=[self.option, "foo", "--attach-file", "-"]
116 self.assertThat(args.attach_file.read(), Equals(b"Hello"))
118 def test_attach_file_with_hyphen_sets_filename_to_stdin(self):
119 args = safe_parse_arguments(
120 args=[self.option, "foo", "--attach-file", "-"]
123 self.assertThat(args.file_name, Equals("stdin"))
125 def test_can_override_stdin_filename(self):
126 args = safe_parse_arguments(
127 args=[self.option, "foo", "--attach-file", "-", '--file-name', 'foo']
130 self.assertThat(args.file_name, Equals("foo"))
132 def test_requires_test_id(self):
133 fn = lambda: safe_parse_arguments(args=[self.option])
136 raises(RuntimeError('subunit-output: error: argument %s: must '
137 'specify a single TEST_ID.\n' % self.option))
141 class ArgParserTests(TestCaseWithPatchedStderr):
143 def test_can_parse_attach_file_without_test_id(self):
144 with NamedTemporaryFile() as tmp_file:
145 args = safe_parse_arguments(
146 args=["--attach-file", tmp_file.name]
148 self.assertThat(args.attach_file.name, Equals(tmp_file.name))
150 def test_can_run_without_args(self):
151 args = safe_parse_arguments([])
153 def test_cannot_specify_more_than_one_status_command(self):
154 fn = lambda: safe_parse_arguments(['--fail', 'foo', '--skip', 'bar'])
157 raises(RuntimeError('subunit-output: error: argument --skip: '
158 'Only one status may be specified at once.\n'))
161 def test_cannot_specify_mimetype_without_attach_file(self):
162 fn = lambda: safe_parse_arguments(['--mimetype', 'foo'])
165 raises(RuntimeError('subunit-output: error: Cannot specify '
166 '--mimetype without --attach-file\n'))
169 def test_cannot_specify_filename_without_attach_file(self):
170 fn = lambda: safe_parse_arguments(['--file-name', 'foo'])
173 raises(RuntimeError('subunit-output: error: Cannot specify '
174 '--file-name without --attach-file\n'))
177 def test_can_specify_tags_without_status_command(self):
178 args = safe_parse_arguments(['--tag', 'foo'])
179 self.assertEqual(['foo'], args.tags)
181 def test_must_specify_tags_with_tags_options(self):
182 fn = lambda: safe_parse_arguments(['--fail', 'foo', '--tag'])
185 raises(RuntimeError('subunit-output: error: --tag option requires 1 argument\n'))
189 def get_result_for(commands):
190 """Get a result object from *commands.
192 Runs the 'generate_stream_results' function from subunit._output after
193 parsing *commands as if they were specified on the command line. The
194 resulting bytestream is then converted back into a result object and
199 args = safe_parse_arguments(commands)
200 output_writer = StreamResultToBytes(output_stream=stream)
201 generate_stream_results(args, output_writer)
205 case = ByteStreamToStreamResult(source=stream)
206 result = StreamResult()
212 def temp_file_contents(data):
213 """Create a temporary file on disk containing 'data'."""
214 with NamedTemporaryFile() as f:
220 class StatusStreamResultTests(WithScenarios, TestCase):
223 (s, dict(status=s, option='--' + s)) for s in _ALL_ACTIONS
226 _dummy_timestamp = datetime.datetime(2013, 1, 1, 0, 0, 0, 0, UTC)
229 super(StatusStreamResultTests, self).setUp()
230 self.patch(_o, 'create_timestamp', lambda: self._dummy_timestamp)
231 self.test_id = self.getUniqueString()
233 def test_only_one_packet_is_generated(self):
234 result = get_result_for([self.option, self.test_id])
240 def test_correct_status_is_generated(self):
241 result = get_result_for([self.option, self.test_id])
245 MatchesStatusCall(test_status=self.status)
248 def test_all_commands_generate_tags(self):
249 result = get_result_for([self.option, self.test_id, '--tag', 'hello', '--tag', 'world'])
252 MatchesStatusCall(test_tags=set(['hello', 'world']))
255 def test_all_commands_generate_timestamp(self):
256 result = get_result_for([self.option, self.test_id])
260 MatchesStatusCall(timestamp=self._dummy_timestamp)
263 def test_all_commands_generate_correct_test_id(self):
264 result = get_result_for([self.option, self.test_id])
268 MatchesStatusCall(test_id=self.test_id)
271 def test_file_is_sent_in_single_packet(self):
272 with temp_file_contents(b"Hello") as f:
273 result = get_result_for([self.option, self.test_id, '--attach-file', f.name])
278 MatchesStatusCall(file_bytes=b'Hello', eof=True),
282 def test_can_read_binary_files(self):
283 with temp_file_contents(b"\xDE\xAD\xBE\xEF") as f:
284 result = get_result_for([self.option, self.test_id, '--attach-file', f.name])
289 MatchesStatusCall(file_bytes=b"\xDE\xAD\xBE\xEF", eof=True),
293 def test_can_read_empty_files(self):
294 with temp_file_contents(b"") as f:
295 result = get_result_for([self.option, self.test_id, '--attach-file', f.name])
300 MatchesStatusCall(file_bytes=b"", file_name=f.name, eof=True),
304 def test_can_read_stdin(self):
305 self.patch(_o.sys, 'stdin', TextIOWrapper(BytesIO(b"\xFE\xED\xFA\xCE")))
306 result = get_result_for([self.option, self.test_id, '--attach-file', '-'])
311 MatchesStatusCall(file_bytes=b"\xFE\xED\xFA\xCE", file_name='stdin', eof=True),
315 def test_file_is_sent_with_test_id(self):
316 with temp_file_contents(b"Hello") as f:
317 result = get_result_for([self.option, self.test_id, '--attach-file', f.name])
322 MatchesStatusCall(test_id=self.test_id, file_bytes=b'Hello', eof=True),
326 def test_file_is_sent_with_test_status(self):
327 with temp_file_contents(b"Hello") as f:
328 result = get_result_for([self.option, self.test_id, '--attach-file', f.name])
333 MatchesStatusCall(test_status=self.status, file_bytes=b'Hello', eof=True),
337 def test_file_chunk_size_is_honored(self):
338 with temp_file_contents(b"Hello") as f:
339 self.patch(_o, '_CHUNK_SIZE', 1)
340 result = get_result_for([self.option, self.test_id, '--attach-file', f.name])
345 MatchesStatusCall(test_id=self.test_id, file_bytes=b'H', eof=False),
346 MatchesStatusCall(test_id=self.test_id, file_bytes=b'e', eof=False),
347 MatchesStatusCall(test_id=self.test_id, file_bytes=b'l', eof=False),
348 MatchesStatusCall(test_id=self.test_id, file_bytes=b'l', eof=False),
349 MatchesStatusCall(test_id=self.test_id, file_bytes=b'o', eof=True),
353 def test_file_mimetype_specified_once_only(self):
354 with temp_file_contents(b"Hi") as f:
355 self.patch(_o, '_CHUNK_SIZE', 1)
356 result = get_result_for([
368 MatchesStatusCall(test_id=self.test_id, mime_type='text/plain', file_bytes=b'H', eof=False),
369 MatchesStatusCall(test_id=self.test_id, mime_type=None, file_bytes=b'i', eof=True),
373 def test_tags_specified_once_only(self):
374 with temp_file_contents(b"Hi") as f:
375 self.patch(_o, '_CHUNK_SIZE', 1)
376 result = get_result_for([
390 MatchesStatusCall(test_id=self.test_id, test_tags=set(['foo', 'bar'])),
391 MatchesStatusCall(test_id=self.test_id, test_tags=None),
395 def test_timestamp_specified_once_only(self):
396 with temp_file_contents(b"Hi") as f:
397 self.patch(_o, '_CHUNK_SIZE', 1)
398 result = get_result_for([
408 MatchesStatusCall(test_id=self.test_id, timestamp=self._dummy_timestamp),
409 MatchesStatusCall(test_id=self.test_id, timestamp=None),
413 def test_test_status_specified_once_only(self):
414 with temp_file_contents(b"Hi") as f:
415 self.patch(_o, '_CHUNK_SIZE', 1)
416 result = get_result_for([
423 # 'inprogress' status should be on the first packet only, all other
424 # statuses should be on the last packet.
425 if self.status in _FINAL_ACTIONS:
426 first_call = MatchesStatusCall(test_id=self.test_id, test_status=None)
427 last_call = MatchesStatusCall(test_id=self.test_id, test_status=self.status)
429 first_call = MatchesStatusCall(test_id=self.test_id, test_status=self.status)
430 last_call = MatchesStatusCall(test_id=self.test_id, test_status=None)
433 MatchesListwise([first_call, last_call])
436 def test_filename_can_be_overridden(self):
437 with temp_file_contents(b"Hello") as f:
438 specified_file_name = self.getUniqueString()
439 result = get_result_for([
445 specified_file_name])
450 MatchesStatusCall(file_name=specified_file_name, file_bytes=b'Hello'),
454 def test_file_name_is_used_by_default(self):
455 with temp_file_contents(b"Hello") as f:
456 result = get_result_for([self.option, self.test_id, '--attach-file', f.name])
461 MatchesStatusCall(file_name=f.name, file_bytes=b'Hello', eof=True),
466 class FileDataTests(TestCase):
468 def test_can_attach_file_without_test_id(self):
469 with temp_file_contents(b"Hello") as f:
470 result = get_result_for(['--attach-file', f.name])
475 MatchesStatusCall(test_id=None, file_bytes=b'Hello', eof=True),
479 def test_file_name_is_used_by_default(self):
480 with temp_file_contents(b"Hello") as f:
481 result = get_result_for(['--attach-file', f.name])
486 MatchesStatusCall(file_name=f.name, file_bytes=b'Hello', eof=True),
490 def test_filename_can_be_overridden(self):
491 with temp_file_contents(b"Hello") as f:
492 specified_file_name = self.getUniqueString()
493 result = get_result_for([
503 MatchesStatusCall(file_name=specified_file_name, file_bytes=b'Hello'),
507 def test_files_have_timestamp(self):
508 _dummy_timestamp = datetime.datetime(2013, 1, 1, 0, 0, 0, 0, UTC)
509 self.patch(_o, 'create_timestamp', lambda: _dummy_timestamp)
511 with temp_file_contents(b"Hello") as f:
512 specified_file_name = self.getUniqueString()
513 result = get_result_for([
521 MatchesStatusCall(file_bytes=b'Hello', timestamp=_dummy_timestamp),
525 def test_can_specify_tags_without_test_status(self):
526 result = get_result_for([
534 MatchesStatusCall(test_tags=set(['foo'])),
538 class MatchesStatusCall(Matcher):
554 def __init__(self, **kwargs):
555 unknown_kwargs = list(filter(
556 lambda k: k not in self._position_lookup,
560 raise ValueError("Unknown keywords: %s" % ','.join(unknown_kwargs))
561 self._filters = kwargs
563 def match(self, call_tuple):
564 for k, v in self._filters.items():
566 pos = self._position_lookup[k]
567 if call_tuple[pos] != v:
569 "Value for key is %r, not %r" % (call_tuple[pos], v)
572 return Mismatch("Key %s is not present." % k)
575 return "<MatchesStatusCall %r>" % self._filters