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 testtools import TestCase
26 from testtools.compat import _u
27 from testtools.matchers import (
34 from testtools.testresult.doubles import StreamResult
36 from subunit.iso8601 import UTC
37 from subunit.v2 import StreamResultToBytes, ByteStreamToStreamResult
38 from subunit._output import (
41 generate_stream_results,
44 import subunit._output as _o
47 class SafeOptionParser(optparse.OptionParser):
48 """An ArgumentParser class that doesn't call sys.exit."""
50 def exit(self, status=0, message=""):
51 raise RuntimeError(message)
53 def error(self, message):
54 raise RuntimeError(message)
57 safe_parse_arguments = partial(parse_arguments, ParserClass=SafeOptionParser)
60 class TestStatusArgParserTests(TestCase):
63 (cmd, dict(command=cmd, option='--' + cmd)) for cmd in _ALL_ACTIONS
66 def test_can_parse_all_commands_with_test_id(self):
67 test_id = self.getUniqueString()
68 args = safe_parse_arguments(args=[self.option, test_id])
70 self.assertThat(args.action, Equals(self.command))
71 self.assertThat(args.test_id, Equals(test_id))
73 def test_all_commands_parse_file_attachment(self):
74 with NamedTemporaryFile() as tmp_file:
75 args = safe_parse_arguments(
76 args=[self.option, 'foo', '--attach-file', tmp_file.name]
78 self.assertThat(args.attach_file.name, Equals(tmp_file.name))
80 def test_all_commands_accept_mimetype_argument(self):
81 with NamedTemporaryFile() as tmp_file:
82 args = safe_parse_arguments(
83 args=[self.option, 'foo', '--attach-file', tmp_file.name, '--mimetype', "text/plain"]
85 self.assertThat(args.mimetype, Equals("text/plain"))
87 def test_all_commands_accept_file_name_argument(self):
88 with NamedTemporaryFile() as tmp_file:
89 args = safe_parse_arguments(
90 args=[self.option, 'foo', '--attach-file', tmp_file.name, '--file-name', "foo"]
92 self.assertThat(args.file_name, Equals("foo"))
94 def test_all_commands_accept_tags_argument(self):
95 args = safe_parse_arguments(
96 args=[self.option, 'foo', '--tag', "foo", "--tag", "bar", "--tag", "baz"]
98 self.assertThat(args.tags, Equals(["foo", "bar", "baz"]))
100 def test_attach_file_with_hyphen_opens_stdin(self):
101 self.patch(_o.sys, 'stdin', TextIOWrapper(BytesIO(b"Hello")))
102 args = safe_parse_arguments(
103 args=[self.option, "foo", "--attach-file", "-"]
106 self.assertThat(args.attach_file.read(), Equals(b"Hello"))
108 def test_attach_file_with_hyphen_sets_filename_to_stdin(self):
109 args = safe_parse_arguments(
110 args=[self.option, "foo", "--attach-file", "-"]
113 self.assertThat(args.file_name, Equals("stdin"))
115 def test_can_override_stdin_filename(self):
116 args = safe_parse_arguments(
117 args=[self.option, "foo", "--attach-file", "-", '--file-name', 'foo']
120 self.assertThat(args.file_name, Equals("foo"))
122 def test_requires_test_id(self):
123 fn = lambda: safe_parse_arguments(args=[self.option])
126 raises(RuntimeError('argument %s: must specify a single TEST_ID.' % self.option))
130 class ArgParserTests(TestCase):
132 def test_can_parse_attach_file_without_test_id(self):
133 with NamedTemporaryFile() as tmp_file:
134 args = safe_parse_arguments(
135 args=["--attach-file", tmp_file.name]
137 self.assertThat(args.attach_file.name, Equals(tmp_file.name))
139 def test_can_run_without_args(self):
140 args = safe_parse_arguments([])
142 def test_cannot_specify_more_than_one_status_command(self):
143 fn = lambda: safe_parse_arguments(['--fail', 'foo', '--skip', 'bar'])
146 raises(RuntimeError('argument --skip: Only one status may be specified at once.'))
149 def test_cannot_specify_mimetype_without_attach_file(self):
150 fn = lambda: safe_parse_arguments(['--mimetype', 'foo'])
153 raises(RuntimeError('Cannot specify --mimetype without --attach-file'))
156 def test_cannot_specify_filename_without_attach_file(self):
157 fn = lambda: safe_parse_arguments(['--file-name', 'foo'])
160 raises(RuntimeError('Cannot specify --file-name without --attach-file'))
163 def test_can_specify_tags_without_status_command(self):
164 args = safe_parse_arguments(['--tag', 'foo'])
165 self.assertEqual(['foo'], args.tags)
167 def test_must_specify_tags_with_tags_options(self):
168 fn = lambda: safe_parse_arguments(['--fail', 'foo', '--tag'])
171 raises(RuntimeError('--tag option requires 1 argument'))
175 def get_result_for(commands):
176 """Get a result object from *commands.
178 Runs the 'generate_stream_results' function from subunit._output after
179 parsing *commands as if they were specified on the command line. The
180 resulting bytestream is then converted back into a result object and
183 result = StreamResult()
184 args = safe_parse_arguments(commands)
185 generate_stream_results(args, result)
190 def temp_file_contents(data):
191 """Create a temporary file on disk containing 'data'."""
192 with NamedTemporaryFile() as f:
198 class StatusStreamResultTests(TestCase):
201 (s, dict(status=s, option='--' + s)) for s in _ALL_ACTIONS
204 _dummy_timestamp = datetime.datetime(2013, 1, 1, 0, 0, 0, 0, UTC)
207 super(StatusStreamResultTests, self).setUp()
208 self.patch(_o, 'create_timestamp', lambda: self._dummy_timestamp)
209 self.test_id = self.getUniqueString()
211 def test_only_one_packet_is_generated(self):
212 result = get_result_for([self.option, self.test_id])
215 Equals(3) # startTestRun and stopTestRun are also called, making 3 total.
218 def test_correct_status_is_generated(self):
219 result = get_result_for([self.option, self.test_id])
223 MatchesStatusCall(test_status=self.status)
226 def test_all_commands_generate_tags(self):
227 result = get_result_for([self.option, self.test_id, '--tag', 'hello', '--tag', 'world'])
230 MatchesStatusCall(test_tags=set(['hello', 'world']))
233 def test_all_commands_generate_timestamp(self):
234 result = get_result_for([self.option, self.test_id])
238 MatchesStatusCall(timestamp=self._dummy_timestamp)
241 def test_all_commands_generate_correct_test_id(self):
242 result = get_result_for([self.option, self.test_id])
246 MatchesStatusCall(test_id=self.test_id)
249 def test_file_is_sent_in_single_packet(self):
250 with temp_file_contents(b"Hello") as f:
251 result = get_result_for([self.option, self.test_id, '--attach-file', f.name])
256 MatchesStatusCall(call='startTestRun'),
257 MatchesStatusCall(file_bytes=b'Hello', eof=True),
258 MatchesStatusCall(call='stopTestRun'),
262 def test_can_read_binary_files(self):
263 with temp_file_contents(b"\xDE\xAD\xBE\xEF") as f:
264 result = get_result_for([self.option, self.test_id, '--attach-file', f.name])
269 MatchesStatusCall(call='startTestRun'),
270 MatchesStatusCall(file_bytes=b"\xDE\xAD\xBE\xEF", eof=True),
271 MatchesStatusCall(call='stopTestRun'),
275 def test_can_read_empty_files(self):
276 with temp_file_contents(b"") as f:
277 result = get_result_for([self.option, self.test_id, '--attach-file', f.name])
282 MatchesStatusCall(call='startTestRun'),
283 MatchesStatusCall(file_bytes=b"", file_name=f.name, eof=True),
284 MatchesStatusCall(call='stopTestRun'),
288 def test_can_read_stdin(self):
289 self.patch(_o.sys, 'stdin', TextIOWrapper(BytesIO(b"\xFE\xED\xFA\xCE")))
290 result = get_result_for([self.option, self.test_id, '--attach-file', '-'])
295 MatchesStatusCall(call='startTestRun'),
296 MatchesStatusCall(file_bytes=b"\xFE\xED\xFA\xCE", file_name='stdin', eof=True),
297 MatchesStatusCall(call='stopTestRun'),
301 def test_file_is_sent_with_test_id(self):
302 with temp_file_contents(b"Hello") as f:
303 result = get_result_for([self.option, self.test_id, '--attach-file', f.name])
308 MatchesStatusCall(call='startTestRun'),
309 MatchesStatusCall(test_id=self.test_id, file_bytes=b'Hello', eof=True),
310 MatchesStatusCall(call='stopTestRun'),
314 def test_file_is_sent_with_test_status(self):
315 with temp_file_contents(b"Hello") as f:
316 result = get_result_for([self.option, self.test_id, '--attach-file', f.name])
321 MatchesStatusCall(call='startTestRun'),
322 MatchesStatusCall(test_status=self.status, file_bytes=b'Hello', eof=True),
323 MatchesStatusCall(call='stopTestRun'),
327 def test_file_chunk_size_is_honored(self):
328 with temp_file_contents(b"Hello") as f:
329 self.patch(_o, '_CHUNK_SIZE', 1)
330 result = get_result_for([self.option, self.test_id, '--attach-file', f.name])
335 MatchesStatusCall(call='startTestRun'),
336 MatchesStatusCall(test_id=self.test_id, file_bytes=b'H', eof=False),
337 MatchesStatusCall(test_id=self.test_id, file_bytes=b'e', eof=False),
338 MatchesStatusCall(test_id=self.test_id, file_bytes=b'l', eof=False),
339 MatchesStatusCall(test_id=self.test_id, file_bytes=b'l', eof=False),
340 MatchesStatusCall(test_id=self.test_id, file_bytes=b'o', eof=True),
341 MatchesStatusCall(call='stopTestRun'),
345 def test_file_mimetype_specified_once_only(self):
346 with temp_file_contents(b"Hi") as f:
347 self.patch(_o, '_CHUNK_SIZE', 1)
348 result = get_result_for([
360 MatchesStatusCall(call='startTestRun'),
361 MatchesStatusCall(test_id=self.test_id, mime_type='text/plain', file_bytes=b'H', eof=False),
362 MatchesStatusCall(test_id=self.test_id, mime_type=None, file_bytes=b'i', eof=True),
363 MatchesStatusCall(call='stopTestRun'),
367 def test_tags_specified_once_only(self):
368 with temp_file_contents(b"Hi") as f:
369 self.patch(_o, '_CHUNK_SIZE', 1)
370 result = get_result_for([
384 MatchesStatusCall(call='startTestRun'),
385 MatchesStatusCall(test_id=self.test_id, test_tags=set(['foo', 'bar'])),
386 MatchesStatusCall(test_id=self.test_id, test_tags=None),
387 MatchesStatusCall(call='stopTestRun'),
391 def test_timestamp_specified_once_only(self):
392 with temp_file_contents(b"Hi") as f:
393 self.patch(_o, '_CHUNK_SIZE', 1)
394 result = get_result_for([
404 MatchesStatusCall(call='startTestRun'),
405 MatchesStatusCall(test_id=self.test_id, timestamp=self._dummy_timestamp),
406 MatchesStatusCall(test_id=self.test_id, timestamp=None),
407 MatchesStatusCall(call='stopTestRun'),
411 def test_test_status_specified_once_only(self):
412 with temp_file_contents(b"Hi") as f:
413 self.patch(_o, '_CHUNK_SIZE', 1)
414 result = get_result_for([
421 # 'inprogress' status should be on the first packet only, all other
422 # statuses should be on the last packet.
423 if self.status in _FINAL_ACTIONS:
424 first_call = MatchesStatusCall(test_id=self.test_id, test_status=None)
425 last_call = MatchesStatusCall(test_id=self.test_id, test_status=self.status)
427 first_call = MatchesStatusCall(test_id=self.test_id, test_status=self.status)
428 last_call = MatchesStatusCall(test_id=self.test_id, test_status=None)
432 MatchesStatusCall(call='startTestRun'),
435 MatchesStatusCall(call='stopTestRun'),
439 def test_filename_can_be_overridden(self):
440 with temp_file_contents(b"Hello") as f:
441 specified_file_name = self.getUniqueString()
442 result = get_result_for([
448 specified_file_name])
453 MatchesStatusCall(call='startTestRun'),
454 MatchesStatusCall(file_name=specified_file_name, file_bytes=b'Hello'),
455 MatchesStatusCall(call='stopTestRun'),
459 def test_file_name_is_used_by_default(self):
460 with temp_file_contents(b"Hello") as f:
461 result = get_result_for([self.option, self.test_id, '--attach-file', f.name])
466 MatchesStatusCall(call='startTestRun'),
467 MatchesStatusCall(file_name=f.name, file_bytes=b'Hello', eof=True),
468 MatchesStatusCall(call='stopTestRun'),
473 class FileDataTests(TestCase):
475 def test_can_attach_file_without_test_id(self):
476 with temp_file_contents(b"Hello") as f:
477 result = get_result_for(['--attach-file', f.name])
482 MatchesStatusCall(call='startTestRun'),
483 MatchesStatusCall(test_id=None, file_bytes=b'Hello', eof=True),
484 MatchesStatusCall(call='stopTestRun'),
488 def test_file_name_is_used_by_default(self):
489 with temp_file_contents(b"Hello") as f:
490 result = get_result_for(['--attach-file', f.name])
495 MatchesStatusCall(call='startTestRun'),
496 MatchesStatusCall(file_name=f.name, file_bytes=b'Hello', eof=True),
497 MatchesStatusCall(call='stopTestRun'),
501 def test_filename_can_be_overridden(self):
502 with temp_file_contents(b"Hello") as f:
503 specified_file_name = self.getUniqueString()
504 result = get_result_for([
514 MatchesStatusCall(call='startTestRun'),
515 MatchesStatusCall(file_name=specified_file_name, file_bytes=b'Hello'),
516 MatchesStatusCall(call='stopTestRun'),
520 def test_files_have_timestamp(self):
521 _dummy_timestamp = datetime.datetime(2013, 1, 1, 0, 0, 0, 0, UTC)
522 self.patch(_o, 'create_timestamp', lambda: _dummy_timestamp)
524 with temp_file_contents(b"Hello") as f:
525 specified_file_name = self.getUniqueString()
526 result = get_result_for([
534 MatchesStatusCall(call='startTestRun'),
535 MatchesStatusCall(file_bytes=b'Hello', timestamp=_dummy_timestamp),
536 MatchesStatusCall(call='stopTestRun'),
540 def test_can_specify_tags_without_test_status(self):
541 result = get_result_for([
549 MatchesStatusCall(call='startTestRun'),
550 MatchesStatusCall(test_tags=set(['foo'])),
551 MatchesStatusCall(call='stopTestRun'),
556 class MatchesStatusCall(Matcher):
572 def __init__(self, **kwargs):
573 unknown_kwargs = list(filter(
574 lambda k: k not in self._position_lookup,
578 raise ValueError("Unknown keywords: %s" % ','.join(unknown_kwargs))
579 self._filters = kwargs
581 def match(self, call_tuple):
582 for k, v in self._filters.items():
584 pos = self._position_lookup[k]
585 if call_tuple[pos] != v:
587 "Value for key is %r, not %r" % (call_tuple[pos], v)
590 return Mismatch("Key %s is not present." % k)
593 return "<MatchesStatusCall %r>" % self._filters