python-subunit
Advanced tools
| #!/usr/bin/env python3 | ||
| # subunit: extensions to python unittest to get test results from subprocesses. | ||
| # Copyright (C) 2026 Jelmer Vernooij <jelmer@samba.org> | ||
| # | ||
| # Licensed under either the Apache License, Version 2.0 or the BSD 3-clause | ||
| # license at the users choice. A copy of both licenses are available in the | ||
| # project source as Apache-2.0 and BSD. You may not use this file except in | ||
| # compliance with one of these two licences. | ||
| # | ||
| # Unless required by applicable law or agreed to in writing, software | ||
| # distributed under these licenses is distributed on an "AS IS" BASIS, WITHOUT | ||
| # WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the | ||
| # license you chose for the specific language governing permissions and | ||
| # limitations under that license. | ||
| # | ||
| """A filter that reads a `go test -json` stream and outputs a subunit stream. | ||
| Pipe Go's structured test output into this script: | ||
| go test -json ./... | gojson2subunit | ||
| The conversion preserves per-test elapsed time (via paired ``inprogress`` / | ||
| terminal subunit packets) and folds captured stdout/stderr lines into a | ||
| single ``text/plain`` attachment on each terminal packet. | ||
| """ | ||
| import sys | ||
| from subunit import GoJSON2SubUnit | ||
| def main(): | ||
| sys.exit(GoJSON2SubUnit(sys.stdin, sys.stdout.buffer)) | ||
| if __name__ == "__main__": | ||
| main() |
| #!/usr/bin/env python3 | ||
| # subunit: extensions to python unittest to get test results from subprocesses. | ||
| # Copyright (C) 2026 Jelmer Vernooij <jelmer@samba.org> | ||
| # | ||
| # Licensed under either the Apache License, Version 2.0 or the BSD 3-clause | ||
| # license at the users choice. A copy of both licenses are available in the | ||
| # project source as Apache-2.0 and BSD. You may not use this file except in | ||
| # compliance with one of these two licences. | ||
| # | ||
| # Unless required by applicable law or agreed to in writing, software | ||
| # distributed under these licenses is distributed on an "AS IS" BASIS, WITHOUT | ||
| # WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the | ||
| # license you chose for the specific language governing permissions and | ||
| # limitations under that license. | ||
| # | ||
| """A filter that reads JUnit XML test reports and emits a subunit v2 stream. | ||
| JUnit XML is the de-facto interchange format for JVM test runners (Maven | ||
| Surefire, Gradle, Ant) and many other ecosystems. Maven and Gradle write | ||
| one XML file per test class into a reports directory, so this script | ||
| accepts directories as well as individual files. | ||
| Typical use with Maven:: | ||
| mvn clean test ; junitxml2subunit -d target/surefire-reports | ||
| Typical use with Gradle:: | ||
| gradle clean test ; junitxml2subunit -d build/test-results/test | ||
| """ | ||
| import argparse | ||
| import os | ||
| import sys | ||
| from subunit import JUnitXML2SubUnit | ||
| def parse_args(argv): | ||
| parser = argparse.ArgumentParser( | ||
| description=( | ||
| "Convert JUnit XML test reports to a subunit v2 stream on stdout. " | ||
| "Pass individual files as positional arguments or use -d/--dir to " | ||
| "walk a reports directory for *.xml files." | ||
| ), | ||
| ) | ||
| parser.add_argument( | ||
| "-d", | ||
| "--dir", | ||
| dest="dirs", | ||
| action="append", | ||
| default=[], | ||
| metavar="DIR", | ||
| help=( | ||
| "Directory to walk for *.xml report files. May be repeated. " | ||
| "Files inside the directory are converted in lexical order so " | ||
| "the output is deterministic across runs." | ||
| ), | ||
| ) | ||
| parser.add_argument( | ||
| "files", | ||
| nargs="*", | ||
| help="Individual JUnit XML report files to convert.", | ||
| ) | ||
| return parser.parse_args(argv) | ||
| def collect_files(dirs, files): | ||
| """Combine `--dir DIR` walks with explicit FILE arguments. | ||
| Within each directory we sort by filename so the resulting subunit | ||
| stream is reproducible. Across directories we preserve the user's | ||
| argv order (some workflows feed multiple module-specific report | ||
| directories and care about the suite ordering). | ||
| """ | ||
| out = [] | ||
| for d in dirs: | ||
| if not os.path.isdir(d): | ||
| sys.stderr.write("junitxml2subunit: not a directory: {}\n".format(d)) | ||
| continue | ||
| for root, _dirs, names in sorted(os.walk(d)): | ||
| for name in sorted(names): | ||
| if name.endswith(".xml"): | ||
| out.append(os.path.join(root, name)) | ||
| out.extend(files) | ||
| return out | ||
| def main(argv=None): | ||
| args = parse_args(argv if argv is not None else sys.argv[1:]) | ||
| inputs = collect_files(args.dirs, args.files) | ||
| if not inputs: | ||
| sys.stderr.write("junitxml2subunit: no input files found (pass FILE arguments or use -d DIR)\n") | ||
| return 2 | ||
| return JUnitXML2SubUnit(inputs, sys.stdout.buffer) | ||
| if __name__ == "__main__": | ||
| sys.exit(main()) |
| #!/usr/bin/env python3 | ||
| # subunit: extensions to python unittest to get test results from subprocesses. | ||
| # Copyright (C) 2026 Jelmer Vernooij <jelmer@jelmer.uk> | ||
| # | ||
| # 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. | ||
| # | ||
| """Run multiple commands producing subunit v2 output and merge their streams. | ||
| The commands to run are described in a YAML configuration file. Each command | ||
| may have a test id ``prefix`` prepended to all test ids it emits, which makes | ||
| it possible to combine output from, for example, a Python and a Rust test | ||
| suite without test id collisions. | ||
| Example configuration:: | ||
| commands: | ||
| - prefix: "python/" | ||
| argv: ["python", "-m", "subunit.run", "discover", ".", "$LISTOPT", "$IDOPTION"] | ||
| list_option: "--list" | ||
| id_option: "--load-list $IDFILE" | ||
| - prefix: "rust/" | ||
| argv: ["cargo", "test", "--", "--format=subunit"] | ||
| cwd: "rust" | ||
| The combined subunit v2 stream is written to stdout. The exit code is 0 if | ||
| all commands exited 0, 1 otherwise. | ||
| testr-style substitutions | ||
| ------------------------- | ||
| Each command's ``argv``, ``list_option`` and ``id_option`` entries may contain | ||
| the following placeholders, which are expanded per invocation: | ||
| * ``$LISTOPT`` -- expands to ``list_option`` when ``subunit-combine --list`` | ||
| is used, empty otherwise. | ||
| * ``$IDLIST`` -- space-separated list of (prefix-stripped) test ids to run | ||
| for this command. Empty if no ids were requested or none match. | ||
| * ``$IDFILE`` -- path to a temporary file containing one test id per line. | ||
| * ``$IDOPTION`` -- expands to ``id_option`` (with ``$IDFILE`` further | ||
| substituted) when ids are being supplied, empty otherwise. | ||
| Test ids can be passed as positional arguments after the config file or via | ||
| ``--load-list FILE``. Ids that start with a command's ``prefix`` are routed | ||
| (with the prefix stripped) to that command; ids that don't match any prefix | ||
| are ignored for that command. | ||
| """ | ||
| import os | ||
| import re | ||
| import subprocess | ||
| import sys | ||
| import tempfile | ||
| from argparse import ArgumentParser | ||
| from typing import Optional | ||
| import yaml | ||
| from subunit import ByteStreamToStreamResult, StreamResultToBytes | ||
| _VARIABLE_RE = re.compile(r"\$(IDOPTION|IDFILE|IDLIST|LISTOPT)") | ||
| class _PrefixingStreamResult: | ||
| """Forward StreamResult events, prepending ``prefix`` to every test_id.""" | ||
| def __init__(self, target, prefix: str): | ||
| self._target = target | ||
| self._prefix = prefix | ||
| def startTestRun(self): | ||
| self._target.startTestRun() | ||
| def stopTestRun(self): | ||
| self._target.stopTestRun() | ||
| def status(self, test_id=None, **kwargs): | ||
| if test_id is not None: | ||
| test_id = self._prefix + test_id | ||
| self._target.status(test_id=test_id, **kwargs) | ||
| def load_config(path: str) -> list[dict]: | ||
| """Load and validate a combine configuration file. | ||
| Returns a list of command dictionaries. | ||
| """ | ||
| with open(path) as f: | ||
| data = yaml.safe_load(f) | ||
| if not isinstance(data, dict): | ||
| raise ValueError(f"{path}: top-level configuration must be a mapping") | ||
| commands = data.get("commands") | ||
| if not isinstance(commands, list) or not commands: | ||
| raise ValueError(f"{path}: 'commands' must be a non-empty list") | ||
| for i, cmd in enumerate(commands): | ||
| if not isinstance(cmd, dict): | ||
| raise ValueError(f"{path}: commands[{i}] must be a mapping") | ||
| argv = cmd.get("argv") | ||
| if not isinstance(argv, list) or not argv or not all(isinstance(a, str) for a in argv): | ||
| raise ValueError(f"{path}: commands[{i}].argv must be a non-empty list of strings") | ||
| prefix = cmd.get("prefix", "") | ||
| if not isinstance(prefix, str): | ||
| raise ValueError(f"{path}: commands[{i}].prefix must be a string") | ||
| for key in ("list_option", "id_option"): | ||
| value = cmd.get(key) | ||
| if value is not None and not isinstance(value, str): | ||
| raise ValueError(f"{path}: commands[{i}].{key} must be a string") | ||
| return commands | ||
| def _substitute(template: str, variables: dict[str, str]) -> list[str]: | ||
| """Substitute $VAR placeholders in ``template`` and return a shell-split list. | ||
| Empty values are expanded to the empty string; the resulting string is then | ||
| split on whitespace so that e.g. an empty ``$LISTOPT`` disappears rather | ||
| than leaving an empty argument behind. | ||
| """ | ||
| def repl(match: re.Match) -> str: | ||
| return variables.get(match.group(1), "") | ||
| return re.sub(_VARIABLE_RE, repl, template).split() | ||
| def _expand_argv( | ||
| cmd: dict, | ||
| *, | ||
| list_mode: bool, | ||
| test_ids: Optional[list[str]], | ||
| idfile_path: Optional[str], | ||
| ) -> list[str]: | ||
| """Expand testr-style placeholders in ``cmd['argv']``.""" | ||
| list_option = cmd.get("list_option", "") if list_mode else "" | ||
| id_option_template = cmd.get("id_option", "") | ||
| if test_ids is None or not id_option_template: | ||
| id_option = "" | ||
| else: | ||
| id_option = re.sub( | ||
| _VARIABLE_RE, | ||
| lambda m: {"IDFILE": idfile_path or "", "IDLIST": " ".join(test_ids)}.get(m.group(1), ""), | ||
| id_option_template, | ||
| ) | ||
| variables = { | ||
| "LISTOPT": list_option, | ||
| "IDOPTION": id_option, | ||
| "IDFILE": idfile_path or "", | ||
| "IDLIST": " ".join(test_ids) if test_ids else "", | ||
| } | ||
| expanded: list[str] = [] | ||
| for piece in cmd["argv"]: | ||
| if _VARIABLE_RE.search(piece): | ||
| expanded.extend(_substitute(piece, variables)) | ||
| else: | ||
| expanded.append(piece) | ||
| return expanded | ||
| def _select_ids_for_command(cmd: dict, test_ids: Optional[list[str]]) -> Optional[list[str]]: | ||
| """Return the ids that belong to ``cmd`` (with the prefix stripped). | ||
| Returns None when no filtering should be applied (no ids were requested | ||
| globally). | ||
| """ | ||
| if test_ids is None: | ||
| return None | ||
| prefix = cmd.get("prefix", "") | ||
| if not prefix: | ||
| return list(test_ids) | ||
| return [tid[len(prefix) :] for tid in test_ids if tid.startswith(prefix)] | ||
| def _write_idfile(test_ids: list[str]) -> str: | ||
| fd, path = tempfile.mkstemp(prefix="subunit-combine-", suffix=".list") | ||
| with os.fdopen(fd, "w") as f: | ||
| for tid in test_ids: | ||
| f.write(tid + "\n") | ||
| return path | ||
| def run_command( | ||
| cmd: dict, | ||
| output, | ||
| *, | ||
| list_mode: bool = False, | ||
| test_ids: Optional[list[str]] = None, | ||
| ) -> int: | ||
| """Run a single command and forward its subunit v2 output. | ||
| The child's stdout is parsed as subunit v2 and re-emitted to ``output`` | ||
| (a :class:`StreamResultToBytes`) with each test_id prefixed with | ||
| ``cmd['prefix']`` (if any). | ||
| :param list_mode: If True, ``$LISTOPT`` is substituted with the command's | ||
| ``list_option`` so the child lists tests rather than running them. | ||
| :param test_ids: If not None, the list of (prefix-stripped) test ids to | ||
| supply to the child via ``$IDLIST`` / ``$IDOPTION`` / ``$IDFILE``. | ||
| :return: The exit code of the child process. | ||
| """ | ||
| prefix = cmd.get("prefix", "") | ||
| cwd = cmd.get("cwd") | ||
| env = os.environ.copy() | ||
| extra_env = cmd.get("env") | ||
| if extra_env: | ||
| env.update(extra_env) | ||
| idfile_path: Optional[str] = None | ||
| if test_ids is not None and test_ids: | ||
| idfile_path = _write_idfile(test_ids) | ||
| try: | ||
| argv = _expand_argv( | ||
| cmd, | ||
| list_mode=list_mode, | ||
| test_ids=test_ids, | ||
| idfile_path=idfile_path, | ||
| ) | ||
| proc = subprocess.Popen(argv, stdout=subprocess.PIPE, cwd=cwd, env=env) | ||
| try: | ||
| assert proc.stdout is not None | ||
| result = _PrefixingStreamResult(output, prefix) if prefix else output | ||
| ByteStreamToStreamResult(proc.stdout, non_subunit_name="stdout").run(result) | ||
| finally: | ||
| returncode = proc.wait() | ||
| finally: | ||
| if idfile_path is not None: | ||
| try: | ||
| os.unlink(idfile_path) | ||
| except OSError: | ||
| pass | ||
| return returncode | ||
| def combine( | ||
| commands: list[dict], | ||
| output_stream, | ||
| *, | ||
| list_mode: bool = False, | ||
| test_ids: Optional[list[str]] = None, | ||
| ) -> int: | ||
| """Run ``commands`` and merge their subunit streams into ``output_stream``. | ||
| :param list_mode: Run each command in listing mode (``$LISTOPT`` expanded). | ||
| :param test_ids: Optional list of test ids to restrict execution to. | ||
| Each command only sees ids whose prefix matches; commands with no | ||
| matching ids are skipped entirely. | ||
| :return: 0 if every command exited 0, 1 otherwise. | ||
| """ | ||
| output = StreamResultToBytes(output_stream) | ||
| output.startTestRun() | ||
| failed = False | ||
| try: | ||
| for cmd in commands: | ||
| cmd_ids = _select_ids_for_command(cmd, test_ids) | ||
| if test_ids is not None and not cmd_ids: | ||
| # Ids were requested, but none match this command. | ||
| continue | ||
| rc = run_command(cmd, output, list_mode=list_mode, test_ids=cmd_ids) | ||
| if rc != 0: | ||
| failed = True | ||
| finally: | ||
| output.stopTestRun() | ||
| return 1 if failed else 0 | ||
| def _read_id_list(path: str) -> list[str]: | ||
| """Read test ids from a file, one per line; blank lines and # comments are skipped.""" | ||
| ids = [] | ||
| with open(path) as f: | ||
| for line in f: | ||
| line = line.split("#", 1)[0].strip() | ||
| if line: | ||
| ids.append(line) | ||
| return ids | ||
| def make_parser() -> ArgumentParser: | ||
| parser = ArgumentParser(description=__doc__.splitlines()[0] if __doc__ else None) | ||
| parser.add_argument( | ||
| "config", | ||
| help="Path to a YAML configuration file describing the commands to run.", | ||
| ) | ||
| parser.add_argument( | ||
| "test_ids", | ||
| nargs="*", | ||
| help="Optional test ids to restrict execution to. Ids whose prefix " | ||
| "matches a command are routed to that command.", | ||
| ) | ||
| parser.add_argument( | ||
| "--list", | ||
| dest="list_mode", | ||
| action="store_true", | ||
| help="List tests that would be run instead of running them. Each " | ||
| "command is invoked with its configured list_option substituted for " | ||
| "$LISTOPT.", | ||
| ) | ||
| parser.add_argument( | ||
| "--load-list", | ||
| dest="load_list", | ||
| metavar="FILE", | ||
| help="Read test ids (one per line) from FILE to restrict execution.", | ||
| ) | ||
| return parser | ||
| def main(argv: Optional[list[str]] = None) -> None: | ||
| parser = make_parser() | ||
| options = parser.parse_args(argv) | ||
| commands = load_config(options.config) | ||
| test_ids: Optional[list[str]] = None | ||
| if options.load_list: | ||
| test_ids = _read_id_list(options.load_list) | ||
| if options.test_ids: | ||
| test_ids = (test_ids or []) + options.test_ids | ||
| sys.exit( | ||
| combine( | ||
| commands, | ||
| sys.stdout, | ||
| list_mode=options.list_mode, | ||
| test_ids=test_ids, | ||
| ) | ||
| ) | ||
| if __name__ == "__main__": | ||
| main() |
| # | ||
| # subunit: extensions to python unittest to get test results from subprocesses. | ||
| # Copyright (C) 2005 Robert Collins <robertc@robertcollins.net> | ||
| # | ||
| # 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 unittest import TestLoader | ||
| from testscenarios import generate_scenarios | ||
| # Before the test module imports to avoid circularity. | ||
| # For testing: different pythons have different str() implementations. | ||
| _remote_exception_repr = "testtools.testresult.real._StringException" | ||
| _remote_exception_repr_chunked = "34\r\n" + _remote_exception_repr + ": boo qux\n0\r\n" | ||
| _remote_exception_str = "Traceback (most recent call last):\ntesttools.testresult.real._StringException" | ||
| _remote_exception_str_chunked = "57\r\n" + _remote_exception_str + ": boo qux\n0\r\n" | ||
| from . import ( # noqa: E402 | ||
| test_chunked, | ||
| test_details, | ||
| test_filter_to_disk, | ||
| test_filters, | ||
| test_output_filter, | ||
| test_progress_model, | ||
| test_run, | ||
| test_subunit_filter, | ||
| test_subunit_stats, | ||
| test_subunit_tags, | ||
| test_tap2subunit, | ||
| test_test_protocol, | ||
| test_test_protocol2, | ||
| test_test_results, | ||
| ) | ||
| def test_suite(): | ||
| loader = TestLoader() | ||
| result = loader.loadTestsFromModule(test_chunked) | ||
| result.addTest(loader.loadTestsFromModule(test_details)) | ||
| result.addTest(loader.loadTestsFromModule(test_filters)) | ||
| result.addTest(loader.loadTestsFromModule(test_progress_model)) | ||
| result.addTest(loader.loadTestsFromModule(test_test_results)) | ||
| result.addTest(loader.loadTestsFromModule(test_test_protocol)) | ||
| result.addTest(loader.loadTestsFromModule(test_test_protocol2)) | ||
| result.addTest(loader.loadTestsFromModule(test_tap2subunit)) | ||
| result.addTest(loader.loadTestsFromModule(test_filter_to_disk)) | ||
| result.addTest(loader.loadTestsFromModule(test_subunit_filter)) | ||
| result.addTest(loader.loadTestsFromModule(test_subunit_tags)) | ||
| result.addTest(loader.loadTestsFromModule(test_subunit_stats)) | ||
| result.addTest(loader.loadTestsFromModule(test_run)) | ||
| result.addTests(generate_scenarios(loader.loadTestsFromModule(test_output_filter))) | ||
| return result |
| #!/usr/bin/env python3 | ||
| import sys | ||
| if sys.platform == "win32": | ||
| import msvcrt | ||
| import os | ||
| msvcrt.setmode(sys.stdout.fileno(), os.O_BINARY) | ||
| if len(sys.argv) == 2: | ||
| # tests.test_test_protocol.TestExecTestCase.test_sample_method_args | ||
| # uses this code path to be sure that the arguments were passed to | ||
| # sample-script.py | ||
| print("test fail") | ||
| print("error fail") | ||
| sys.exit(0) | ||
| print("test old mcdonald") | ||
| print("success old mcdonald") | ||
| print("test bing crosby") | ||
| print("failure bing crosby [") | ||
| print("foo.c:53:ERROR invalid state") | ||
| print("]") | ||
| print("test an error") | ||
| print("error an error") | ||
| sys.exit(0) |
| #!/usr/bin/env python3 | ||
| import sys | ||
| print("test old mcdonald") | ||
| print("success old mcdonald") | ||
| print("test bing crosby") | ||
| print("success bing crosby") | ||
| sys.exit(0) |
| # | ||
| # subunit: extensions to python unittest to get test results from subprocesses. | ||
| # Copyright (C) 2005 Robert Collins <robertc@robertcollins.net> | ||
| # Copyright (C) 2011 Martin Pool <mbp@sourcefrog.net> | ||
| # | ||
| # 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. | ||
| # | ||
| import unittest | ||
| from io import BytesIO | ||
| import subunit.chunked | ||
| class TestDecode(unittest.TestCase): | ||
| def setUp(self): | ||
| unittest.TestCase.setUp(self) | ||
| self.output = BytesIO() | ||
| self.decoder = subunit.chunked.Decoder(self.output) | ||
| def test_close_read_length_short_errors(self): | ||
| self.assertRaises(ValueError, self.decoder.close) | ||
| def test_close_body_short_errors(self): | ||
| self.assertEqual(None, self.decoder.write(b"2\r\na")) | ||
| self.assertRaises(ValueError, self.decoder.close) | ||
| def test_close_body_buffered_data_errors(self): | ||
| self.assertEqual(None, self.decoder.write(b"2\r")) | ||
| self.assertRaises(ValueError, self.decoder.close) | ||
| def test_close_after_finished_stream_safe(self): | ||
| self.assertEqual(None, self.decoder.write(b"2\r\nab")) | ||
| self.assertEqual(b"", self.decoder.write(b"0\r\n")) | ||
| self.decoder.close() | ||
| def test_decode_nothing(self): | ||
| self.assertEqual(b"", self.decoder.write(b"0\r\n")) | ||
| self.assertEqual(b"", self.output.getvalue()) | ||
| def test_decode_serialised_form(self): | ||
| self.assertEqual(None, self.decoder.write(b"F\r\n")) | ||
| self.assertEqual(None, self.decoder.write(b"serialised\n")) | ||
| self.assertEqual(b"", self.decoder.write(b"form0\r\n")) | ||
| def test_decode_short(self): | ||
| self.assertEqual(b"", self.decoder.write(b"3\r\nabc0\r\n")) | ||
| self.assertEqual(b"abc", self.output.getvalue()) | ||
| def test_decode_combines_short(self): | ||
| self.assertEqual(b"", self.decoder.write(b"6\r\nabcdef0\r\n")) | ||
| self.assertEqual(b"abcdef", self.output.getvalue()) | ||
| def test_decode_excess_bytes_from_write(self): | ||
| self.assertEqual(b"1234", self.decoder.write(b"3\r\nabc0\r\n1234")) | ||
| self.assertEqual(b"abc", self.output.getvalue()) | ||
| def test_decode_write_after_finished_errors(self): | ||
| self.assertEqual(b"1234", self.decoder.write(b"3\r\nabc0\r\n1234")) | ||
| self.assertRaises(ValueError, self.decoder.write, b"") | ||
| def test_decode_hex(self): | ||
| self.assertEqual(b"", self.decoder.write(b"A\r\n12345678900\r\n")) | ||
| self.assertEqual(b"1234567890", self.output.getvalue()) | ||
| def test_decode_long_ranges(self): | ||
| self.assertEqual(None, self.decoder.write(b"10000\r\n")) | ||
| self.assertEqual(None, self.decoder.write(b"1" * 65536)) | ||
| self.assertEqual(None, self.decoder.write(b"10000\r\n")) | ||
| self.assertEqual(None, self.decoder.write(b"2" * 65536)) | ||
| self.assertEqual(b"", self.decoder.write(b"0\r\n")) | ||
| self.assertEqual(b"1" * 65536 + b"2" * 65536, self.output.getvalue()) | ||
| def test_decode_newline_nonstrict(self): | ||
| """Tolerate chunk markers with no CR character.""" | ||
| # From <http://pad.lv/505078> | ||
| self.decoder = subunit.chunked.Decoder(self.output, strict=False) | ||
| self.assertEqual(None, self.decoder.write(b"a\n")) | ||
| self.assertEqual(None, self.decoder.write(b"abcdeabcde")) | ||
| self.assertEqual(b"", self.decoder.write(b"0\n")) | ||
| self.assertEqual(b"abcdeabcde", self.output.getvalue()) | ||
| def test_decode_strict_newline_only(self): | ||
| """Reject chunk markers with no CR character in strict mode.""" | ||
| # From <http://pad.lv/505078> | ||
| self.assertRaises(ValueError, self.decoder.write, b"a\n") | ||
| def test_decode_strict_multiple_crs(self): | ||
| self.assertRaises(ValueError, self.decoder.write, b"a\r\r\n") | ||
| def test_decode_short_header(self): | ||
| self.assertRaises(ValueError, self.decoder.write, b"\n") | ||
| class TestEncode(unittest.TestCase): | ||
| def setUp(self): | ||
| unittest.TestCase.setUp(self) | ||
| self.output = BytesIO() | ||
| self.encoder = subunit.chunked.Encoder(self.output) | ||
| def test_encode_nothing(self): | ||
| self.encoder.close() | ||
| self.assertEqual(b"0\r\n", self.output.getvalue()) | ||
| def test_encode_empty(self): | ||
| self.encoder.write(b"") | ||
| self.encoder.close() | ||
| self.assertEqual(b"0\r\n", self.output.getvalue()) | ||
| def test_encode_short(self): | ||
| self.encoder.write(b"abc") | ||
| self.encoder.close() | ||
| self.assertEqual(b"3\r\nabc0\r\n", self.output.getvalue()) | ||
| def test_encode_combines_short(self): | ||
| self.encoder.write(b"abc") | ||
| self.encoder.write(b"def") | ||
| self.encoder.close() | ||
| self.assertEqual(b"6\r\nabcdef0\r\n", self.output.getvalue()) | ||
| def test_encode_over_9_is_in_hex(self): | ||
| self.encoder.write(b"1234567890") | ||
| self.encoder.close() | ||
| self.assertEqual(b"A\r\n12345678900\r\n", self.output.getvalue()) | ||
| def test_encode_long_ranges_not_combined(self): | ||
| self.encoder.write(b"1" * 65536) | ||
| self.encoder.write(b"2" * 65536) | ||
| self.encoder.close() | ||
| self.assertEqual(b"10000\r\n" + b"1" * 65536 + b"10000\r\n" + b"2" * 65536 + b"0\r\n", self.output.getvalue()) |
| # | ||
| # subunit: extensions to python unittest to get test results from subprocesses. | ||
| # Copyright (C) 2005 Robert Collins <robertc@robertcollins.net> | ||
| # | ||
| # 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. | ||
| # | ||
| import unittest | ||
| from subunit import content, content_type, details | ||
| class TestSimpleDetails(unittest.TestCase): | ||
| def test_lineReceived(self): | ||
| parser = details.SimpleDetailsParser(None) | ||
| parser.lineReceived(b"foo\n") | ||
| parser.lineReceived(b"bar\n") | ||
| self.assertEqual(b"foo\nbar\n", parser._message) | ||
| def test_lineReceived_escaped_bracket(self): | ||
| parser = details.SimpleDetailsParser(None) | ||
| parser.lineReceived(b"foo\n") | ||
| parser.lineReceived(b" ]are\n") | ||
| parser.lineReceived(b"bar\n") | ||
| self.assertEqual(b"foo\n]are\nbar\n", parser._message) | ||
| def test_get_message(self): | ||
| parser = details.SimpleDetailsParser(None) | ||
| self.assertEqual(b"", parser.get_message()) | ||
| def test_get_details(self): | ||
| parser = details.SimpleDetailsParser(None) | ||
| expected = {} | ||
| expected["traceback"] = content.Content( | ||
| content_type.ContentType("text", "x-traceback", {"charset": "utf8"}), lambda: [b""] | ||
| ) | ||
| found = parser.get_details() | ||
| self.assertEqual(expected.keys(), found.keys()) | ||
| self.assertEqual(expected["traceback"].content_type, found["traceback"].content_type) | ||
| self.assertEqual(b"".join(expected["traceback"].iter_bytes()), b"".join(found["traceback"].iter_bytes())) | ||
| def test_get_details_skip(self): | ||
| parser = details.SimpleDetailsParser(None) | ||
| expected = {} | ||
| expected["reason"] = content.Content(content_type.ContentType("text", "plain"), lambda: [b""]) | ||
| found = parser.get_details("skip") | ||
| self.assertEqual(expected, found) | ||
| def test_get_details_success(self): | ||
| parser = details.SimpleDetailsParser(None) | ||
| expected = {} | ||
| expected["message"] = content.Content(content_type.ContentType("text", "plain"), lambda: [b""]) | ||
| found = parser.get_details("success") | ||
| self.assertEqual(expected, found) | ||
| class TestMultipartDetails(unittest.TestCase): | ||
| def test_get_message_is_None(self): | ||
| parser = details.MultipartDetailsParser(None) | ||
| self.assertEqual(None, parser.get_message()) | ||
| def test_get_details(self): | ||
| parser = details.MultipartDetailsParser(None) | ||
| self.assertEqual({}, parser.get_details()) | ||
| def test_parts(self): | ||
| parser = details.MultipartDetailsParser(None) | ||
| parser.lineReceived(b"Content-Type: text/plain\n") | ||
| parser.lineReceived(b"something\n") | ||
| parser.lineReceived(b"F\r\n") | ||
| parser.lineReceived(b"serialised\n") | ||
| parser.lineReceived(b"form0\r\n") | ||
| expected = {} | ||
| expected["something"] = content.Content( | ||
| content_type.ContentType("text", "plain"), lambda: [b"serialised\nform"] | ||
| ) | ||
| found = parser.get_details() | ||
| self.assertEqual(expected.keys(), found.keys()) | ||
| self.assertEqual(expected["something"].content_type, found["something"].content_type) | ||
| self.assertEqual(b"".join(expected["something"].iter_bytes()), b"".join(found["something"].iter_bytes())) |
| # | ||
| # subunit: extensions to python unittest to get test results from subprocesses. | ||
| # 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 | ||
| # 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. | ||
| import io | ||
| import os.path | ||
| from fixtures import TempDir | ||
| from testtools import TestCase | ||
| from testtools.matchers import FileContains | ||
| from subunit import _to_disk | ||
| from subunit.v2 import StreamResultToBytes | ||
| class SmokeTest(TestCase): | ||
| def test_smoke(self): | ||
| output = os.path.join(self.useFixture(TempDir()).path, "output") | ||
| stdin = io.BytesIO() | ||
| stdout = io.StringIO() | ||
| writer = StreamResultToBytes(stdin) | ||
| writer.startTestRun() | ||
| writer.status( | ||
| "foo", "success", {"tag"}, file_name="fred", file_bytes=b"abcdefg", eof=True, mime_type="text/plain" | ||
| ) | ||
| writer.stopTestRun() | ||
| stdin.seek(0) | ||
| _to_disk.to_disk(["-d", output], stdin=stdin, stdout=stdout) | ||
| self.expectThat( | ||
| os.path.join(output, "foo/test.json"), | ||
| FileContains( | ||
| '{"details": ["fred"], "id": "foo", "start": null, "status": "success", "stop": null, "tags": ["tag"]}' | ||
| ), | ||
| ) | ||
| self.expectThat(os.path.join(output, "foo/fred"), FileContains("abcdefg")) |
| # | ||
| # subunit: extensions to Python unittest to get test results from subprocesses. | ||
| # Copyright (C) 2013 Robert Collins <robertc@robertcollins.net> | ||
| # | ||
| # 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 tempfile import NamedTemporaryFile | ||
| from testtools import TestCase | ||
| from subunit import read_test_list | ||
| from subunit.filters import find_stream | ||
| class TestReadTestList(TestCase): | ||
| def test_read_list(self): | ||
| with NamedTemporaryFile() as f: | ||
| f.write(b"foo\nbar\n# comment\nother # comment\n") | ||
| f.flush() | ||
| self.assertEqual(read_test_list(f.name), ["foo", "bar", "other"]) | ||
| class TestFindStream(TestCase): | ||
| def test_no_argv(self): | ||
| self.assertEqual("foo", find_stream("foo", [])) | ||
| def test_opens_file(self): | ||
| f = NamedTemporaryFile() | ||
| f.write(b"foo") | ||
| f.flush() | ||
| stream = find_stream("bar", [f.name]) | ||
| self.assertEqual(b"foo", stream.read()) |
| # | ||
| # subunit: extensions to python unittest to get test results from subprocesses. | ||
| # Copyright (C) 2026 Jelmer Vernooij <jelmer@samba.org> | ||
| # | ||
| # 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. | ||
| # | ||
| """Tests for GoJSON2SubUnit.""" | ||
| import json | ||
| from io import BytesIO, StringIO | ||
| from testtools import TestCase | ||
| from testtools.testresult.doubles import StreamResult | ||
| import subunit | ||
| UTF8_TEXT = "text/plain; charset=UTF8" | ||
| def _ndjson(*events): | ||
| """Render an iterable of dicts as one JSON object per line.""" | ||
| return "\n".join(json.dumps(e) for e in events) + "\n" | ||
| class TestGoJSON2SubUnit(TestCase): | ||
| """Behavioural tests for `GoJSON2SubUnit`. | ||
| Each test feeds a synthetic `go test -json` event stream in, decodes | ||
| the resulting subunit bytes back into events via `StreamResult`, and | ||
| asserts on the (status, test_id, test_status, ...) tuples. | ||
| """ | ||
| def setUp(self): | ||
| super().setUp() | ||
| self.gojson = StringIO() | ||
| self.subunit = BytesIO() | ||
| def _events(self): | ||
| self.subunit.seek(0) | ||
| sink = StreamResult() | ||
| subunit.ByteStreamToStreamResult(self.subunit).run(sink) | ||
| return sink._events | ||
| def _statuses(self, events): | ||
| # Strip the verbose tuple down to (test_id, test_status) for clearer | ||
| # assertions when timing/file content isn't what's being tested. | ||
| return [(e[1], e[2]) for e in events if e[0] == "status"] | ||
| def test_pass_emits_inprogress_then_success(self): | ||
| self.gojson.write( | ||
| _ndjson( | ||
| {"Action": "run", "Package": "pkg/a", "Test": "TestFoo"}, | ||
| {"Action": "pass", "Package": "pkg/a", "Test": "TestFoo", "Elapsed": 0.01}, | ||
| ) | ||
| ) | ||
| self.gojson.seek(0) | ||
| rc = subunit.GoJSON2SubUnit(self.gojson, self.subunit) | ||
| self.assertEqual(0, rc) | ||
| self.assertEqual( | ||
| [("pkg/a.TestFoo", "inprogress"), ("pkg/a.TestFoo", "success")], | ||
| self._statuses(self._events()), | ||
| ) | ||
| def test_fail_returns_nonzero(self): | ||
| self.gojson.write( | ||
| _ndjson( | ||
| {"Action": "run", "Package": "pkg/a", "Test": "TestBad"}, | ||
| {"Action": "run", "Package": "pkg/a", "Test": "TestGood"}, | ||
| {"Action": "pass", "Package": "pkg/a", "Test": "TestGood", "Elapsed": 0.01}, | ||
| {"Action": "fail", "Package": "pkg/a", "Test": "TestBad", "Elapsed": 0.02}, | ||
| ) | ||
| ) | ||
| self.gojson.seek(0) | ||
| rc = subunit.GoJSON2SubUnit(self.gojson, self.subunit) | ||
| self.assertEqual(1, rc) | ||
| # Each test produces an inprogress packet and a terminal packet; | ||
| # check the terminal status (last one wins in dict()). | ||
| statuses = dict(self._statuses(self._events())) | ||
| self.assertEqual("fail", statuses["pkg/a.TestBad"]) | ||
| self.assertEqual("success", statuses["pkg/a.TestGood"]) | ||
| def test_skip_status(self): | ||
| self.gojson.write( | ||
| _ndjson( | ||
| {"Action": "run", "Package": "pkg/a", "Test": "TestSkipped"}, | ||
| {"Action": "skip", "Package": "pkg/a", "Test": "TestSkipped", "Elapsed": 0.0}, | ||
| ) | ||
| ) | ||
| self.gojson.seek(0) | ||
| rc = subunit.GoJSON2SubUnit(self.gojson, self.subunit) | ||
| self.assertEqual(0, rc) | ||
| self.assertIn(("pkg/a.TestSkipped", "skip"), self._statuses(self._events())) | ||
| def test_subtest_keeps_slash_separator(self): | ||
| # Go subtests are reported as "Parent/Sub"; the resulting test ID | ||
| # should be "<package>.Parent/Sub" so it round-trips through | ||
| # `go test -run '^Parent$/^Sub$'`. | ||
| self.gojson.write( | ||
| _ndjson( | ||
| {"Action": "run", "Package": "pkg/a", "Test": "TestParent/sub_one"}, | ||
| {"Action": "pass", "Package": "pkg/a", "Test": "TestParent/sub_one", "Elapsed": 0.01}, | ||
| ) | ||
| ) | ||
| self.gojson.seek(0) | ||
| subunit.GoJSON2SubUnit(self.gojson, self.subunit) | ||
| self.assertIn( | ||
| ("pkg/a.TestParent/sub_one", "success"), | ||
| self._statuses(self._events()), | ||
| ) | ||
| def test_output_attached_to_terminal_packet(self): | ||
| self.gojson.write( | ||
| _ndjson( | ||
| {"Action": "run", "Package": "pkg/a", "Test": "TestNoisy"}, | ||
| {"Action": "output", "Package": "pkg/a", "Test": "TestNoisy", "Output": "hello\n"}, | ||
| {"Action": "output", "Package": "pkg/a", "Test": "TestNoisy", "Output": "world\n"}, | ||
| {"Action": "fail", "Package": "pkg/a", "Test": "TestNoisy", "Elapsed": 0.01}, | ||
| ) | ||
| ) | ||
| self.gojson.seek(0) | ||
| subunit.GoJSON2SubUnit(self.gojson, self.subunit) | ||
| events = self._events() | ||
| # Find the terminal packet for TestNoisy and confirm both lines | ||
| # were folded into one attachment. | ||
| terminal = [e for e in events if e[0] == "status" and e[1] == "pkg/a.TestNoisy" and e[2] == "fail"] | ||
| self.assertEqual(1, len(terminal)) | ||
| # Tuple shape from StreamResult: | ||
| # ("status", test_id, test_status, test_tags, runnable, file_name, | ||
| # file_bytes, eof, mime_type, route_code, timestamp) | ||
| ev = terminal[0] | ||
| self.assertEqual("go test output", ev[5]) | ||
| self.assertEqual(b"hello\nworld\n", ev[6]) | ||
| self.assertEqual(UTF8_TEXT, ev[8]) | ||
| def test_package_level_build_failure_synthesises_test(self): | ||
| # When `go test` can't build a package it emits a `fail` event with | ||
| # no `Test` field, preceded by `output` events scoped to the package. | ||
| self.gojson.write( | ||
| _ndjson( | ||
| {"Action": "output", "Package": "pkg/broken", "Output": "./x.go:1:1: syntax error\n"}, | ||
| {"Action": "fail", "Package": "pkg/broken", "Elapsed": 0.0}, | ||
| ) | ||
| ) | ||
| self.gojson.seek(0) | ||
| rc = subunit.GoJSON2SubUnit(self.gojson, self.subunit) | ||
| self.assertEqual(1, rc) | ||
| events = self._events() | ||
| terminal = [e for e in events if e[0] == "status" and e[1] == "pkg/broken [build]" and e[2] == "fail"] | ||
| self.assertEqual(1, len(terminal)) | ||
| self.assertEqual(b"./x.go:1:1: syntax error\n", terminal[0][6]) | ||
| def test_garbage_lines_are_skipped(self): | ||
| # `go test -json` occasionally interleaves a non-JSON banner on | ||
| # certain failure paths; a junk line shouldn't abort the stream. | ||
| self.gojson.write("not json at all\n") | ||
| self.gojson.write( | ||
| _ndjson( | ||
| {"Action": "run", "Package": "pkg/a", "Test": "TestFoo"}, | ||
| {"Action": "pass", "Package": "pkg/a", "Test": "TestFoo", "Elapsed": 0.0}, | ||
| ) | ||
| ) | ||
| self.gojson.seek(0) | ||
| rc = subunit.GoJSON2SubUnit(self.gojson, self.subunit) | ||
| self.assertEqual(0, rc) | ||
| self.assertIn(("pkg/a.TestFoo", "success"), self._statuses(self._events())) | ||
| def test_blank_lines_are_skipped(self): | ||
| self.gojson.write("\n\n") | ||
| self.gojson.write(_ndjson({"Action": "run", "Package": "pkg/a", "Test": "TestFoo"})) | ||
| self.gojson.write("\n") | ||
| self.gojson.write(_ndjson({"Action": "pass", "Package": "pkg/a", "Test": "TestFoo", "Elapsed": 0.0})) | ||
| self.gojson.seek(0) | ||
| rc = subunit.GoJSON2SubUnit(self.gojson, self.subunit) | ||
| self.assertEqual(0, rc) | ||
| self.assertIn(("pkg/a.TestFoo", "success"), self._statuses(self._events())) | ||
| def test_unfinished_test_at_eof_is_failed(self): | ||
| # A test that started but never reached a terminal action — the | ||
| # runner died mid-test. Surface as a failure rather than dropping it. | ||
| self.gojson.write( | ||
| _ndjson( | ||
| {"Action": "run", "Package": "pkg/a", "Test": "TestStuck"}, | ||
| {"Action": "output", "Package": "pkg/a", "Test": "TestStuck", "Output": "panic\n"}, | ||
| ) | ||
| ) | ||
| self.gojson.seek(0) | ||
| rc = subunit.GoJSON2SubUnit(self.gojson, self.subunit) | ||
| self.assertEqual(1, rc) | ||
| events = self._events() | ||
| terminal = [e for e in events if e[0] == "status" and e[1] == "pkg/a.TestStuck" and e[2] == "fail"] | ||
| self.assertEqual(1, len(terminal)) | ||
| self.assertEqual(b"panic\n", terminal[0][6]) | ||
| def test_timestamp_is_propagated(self): | ||
| # The `Time` on each event should land on the matching subunit packet. | ||
| self.gojson.write( | ||
| _ndjson( | ||
| { | ||
| "Time": "2026-01-02T03:04:05.000000Z", | ||
| "Action": "run", | ||
| "Package": "pkg/a", | ||
| "Test": "TestTimed", | ||
| }, | ||
| { | ||
| "Time": "2026-01-02T03:04:06.000000Z", | ||
| "Action": "pass", | ||
| "Package": "pkg/a", | ||
| "Test": "TestTimed", | ||
| "Elapsed": 1.0, | ||
| }, | ||
| ) | ||
| ) | ||
| self.gojson.seek(0) | ||
| subunit.GoJSON2SubUnit(self.gojson, self.subunit) | ||
| # The terminal packet's timestamp (last tuple element) must be set, | ||
| # which gives consumers a basis for computing duration. | ||
| terminal = [e for e in self._events() if e[0] == "status" and e[1] == "pkg/a.TestTimed" and e[2] == "success"] | ||
| self.assertEqual(1, len(terminal)) | ||
| self.assertIsNotNone(terminal[0][-1]) |
| # | ||
| # subunit: extensions to python unittest to get test results from subprocesses. | ||
| # Copyright (C) 2026 Jelmer Vernooij <jelmer@samba.org> | ||
| # | ||
| # 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. | ||
| # | ||
| """Tests for JUnitXML2SubUnit.""" | ||
| import io | ||
| import os | ||
| import tempfile | ||
| from unittest import mock | ||
| from testtools import TestCase | ||
| from testtools.testresult.doubles import StreamResult | ||
| import subunit | ||
| from subunit.filter_scripts import junitxml2subunit | ||
| def _write(tmpdir, name, content): | ||
| path = os.path.join(tmpdir, name) | ||
| with open(path, "w", encoding="utf-8") as fh: | ||
| fh.write(content) | ||
| return path | ||
| class TestJUnitXML2SubUnit(TestCase): | ||
| """Behavioural tests for `JUnitXML2SubUnit`. | ||
| Each test writes a synthetic JUnit XML doc to disk, runs the converter, | ||
| decodes the resulting subunit bytes back into events via | ||
| `StreamResult`, and asserts on the (test_id, test_status) tuples. | ||
| """ | ||
| def setUp(self): | ||
| super().setUp() | ||
| self.tmp = tempfile.mkdtemp(prefix="junitxml2subunit-test-") | ||
| self.addCleanup(self._rmtree, self.tmp) | ||
| self.subunit = io.BytesIO() | ||
| def _rmtree(self, path): | ||
| import shutil | ||
| shutil.rmtree(path, ignore_errors=True) | ||
| def _events(self): | ||
| self.subunit.seek(0) | ||
| sink = StreamResult() | ||
| subunit.ByteStreamToStreamResult(self.subunit).run(sink) | ||
| return sink._events | ||
| def _statuses(self, events): | ||
| return [(e[1], e[2]) for e in events if e[0] == "status"] | ||
| def test_passing_testcase(self): | ||
| path = _write( | ||
| self.tmp, | ||
| "TEST-FooTest.xml", | ||
| """<?xml version="1.0" encoding="UTF-8"?> | ||
| <testsuite name="com.example.FooTest" tests="1" failures="0" errors="0" skipped="0" time="0.123"> | ||
| <testcase classname="com.example.FooTest" name="testBar" time="0.05"/> | ||
| </testsuite> | ||
| """, | ||
| ) | ||
| rc = subunit.JUnitXML2SubUnit([path], self.subunit) | ||
| self.assertEqual(0, rc) | ||
| self.assertEqual( | ||
| [ | ||
| ("com.example.FooTest::testBar", "inprogress"), | ||
| ("com.example.FooTest::testBar", "success"), | ||
| ], | ||
| self._statuses(self._events()), | ||
| ) | ||
| def test_failure_marks_fail_and_returns_nonzero(self): | ||
| path = _write( | ||
| self.tmp, | ||
| "TEST-FooTest.xml", | ||
| """<?xml version="1.0" encoding="UTF-8"?> | ||
| <testsuite name="com.example.FooTest" tests="1" failures="1"> | ||
| <testcase classname="com.example.FooTest" name="testBar" time="0.01"> | ||
| <failure type="java.lang.AssertionError" message="expected x but was y">at line 42</failure> | ||
| </testcase> | ||
| </testsuite> | ||
| """, | ||
| ) | ||
| rc = subunit.JUnitXML2SubUnit([path], self.subunit) | ||
| self.assertEqual(1, rc) | ||
| events = self._events() | ||
| terminal = [e for e in events if e[0] == "status" and e[1] == "com.example.FooTest::testBar" and e[2] == "fail"] | ||
| self.assertEqual(1, len(terminal)) | ||
| # The failure detail (type + message + body) is folded into a | ||
| # single attachment so consumers see everything in one place. | ||
| ev = terminal[0] | ||
| self.assertEqual("junit detail", ev[5]) | ||
| # The decoder returns the attachment as a memoryview; coerce to | ||
| # bytes for substring searching. | ||
| body = bytes(ev[6]) | ||
| self.assertIn(b"java.lang.AssertionError", body) | ||
| self.assertIn(b"expected x but was y", body) | ||
| self.assertIn(b"at line 42", body) | ||
| def test_error_is_treated_as_fail(self): | ||
| # An <error> in JUnit terms is "an unexpected exception". subunit | ||
| # has no separate "error" status that maps cleanly, and from a | ||
| # consumer's perspective both mean "did not pass" — so it lands | ||
| # on `fail` like a failure does. | ||
| path = _write( | ||
| self.tmp, | ||
| "TEST-FooTest.xml", | ||
| """<?xml version="1.0"?> | ||
| <testsuite> | ||
| <testcase classname="com.example.FooTest" name="testBar"> | ||
| <error type="java.lang.NullPointerException">stack trace</error> | ||
| </testcase> | ||
| </testsuite> | ||
| """, | ||
| ) | ||
| rc = subunit.JUnitXML2SubUnit([path], self.subunit) | ||
| self.assertEqual(1, rc) | ||
| statuses = self._statuses(self._events()) | ||
| self.assertIn(("com.example.FooTest::testBar", "fail"), statuses) | ||
| def test_skipped(self): | ||
| path = _write( | ||
| self.tmp, | ||
| "TEST-FooTest.xml", | ||
| """<?xml version="1.0"?> | ||
| <testsuite> | ||
| <testcase classname="com.example.FooTest" name="testBar"> | ||
| <skipped message="ignored for now"/> | ||
| </testcase> | ||
| </testsuite> | ||
| """, | ||
| ) | ||
| rc = subunit.JUnitXML2SubUnit([path], self.subunit) | ||
| self.assertEqual(0, rc) | ||
| statuses = self._statuses(self._events()) | ||
| self.assertIn(("com.example.FooTest::testBar", "skip"), statuses) | ||
| def test_testsuites_wrapper_is_unwrapped(self): | ||
| # Some emitters (Gradle, Ant) wrap multiple <testsuite> in a | ||
| # <testsuites> root; both shapes need to work. | ||
| path = _write( | ||
| self.tmp, | ||
| "report.xml", | ||
| """<?xml version="1.0"?> | ||
| <testsuites> | ||
| <testsuite name="A"> | ||
| <testcase classname="A" name="testOne"/> | ||
| </testsuite> | ||
| <testsuite name="B"> | ||
| <testcase classname="B" name="testTwo"/> | ||
| </testsuite> | ||
| </testsuites> | ||
| """, | ||
| ) | ||
| rc = subunit.JUnitXML2SubUnit([path], self.subunit) | ||
| self.assertEqual(0, rc) | ||
| statuses = self._statuses(self._events()) | ||
| self.assertIn(("A::testOne", "success"), statuses) | ||
| self.assertIn(("B::testTwo", "success"), statuses) | ||
| def test_multiple_files_concatenate(self): | ||
| a = _write( | ||
| self.tmp, | ||
| "TEST-A.xml", | ||
| """<?xml version="1.0"?> | ||
| <testsuite> | ||
| <testcase classname="A" name="testOne"/> | ||
| </testsuite> | ||
| """, | ||
| ) | ||
| b = _write( | ||
| self.tmp, | ||
| "TEST-B.xml", | ||
| """<?xml version="1.0"?> | ||
| <testsuite> | ||
| <testcase classname="B" name="testTwo"/> | ||
| </testsuite> | ||
| """, | ||
| ) | ||
| rc = subunit.JUnitXML2SubUnit([a, b], self.subunit) | ||
| self.assertEqual(0, rc) | ||
| statuses = self._statuses(self._events()) | ||
| self.assertIn(("A::testOne", "success"), statuses) | ||
| self.assertIn(("B::testTwo", "success"), statuses) | ||
| def test_missing_classname_falls_back_to_name(self): | ||
| # `classname` is technically optional; without it the test ID | ||
| # is just the bare method name rather than emitting "::name". | ||
| path = _write( | ||
| self.tmp, | ||
| "report.xml", | ||
| """<?xml version="1.0"?> | ||
| <testsuite> | ||
| <testcase name="bareName"/> | ||
| </testsuite> | ||
| """, | ||
| ) | ||
| rc = subunit.JUnitXML2SubUnit([path], self.subunit) | ||
| self.assertEqual(0, rc) | ||
| statuses = self._statuses(self._events()) | ||
| self.assertIn(("bareName", "success"), statuses) | ||
| def test_unparseable_xml_counts_as_failure(self): | ||
| # A broken file is loud (stderr warning + non-zero exit) rather | ||
| # than silently dropping the suite — broken XML in a CI report | ||
| # almost always means a test runner crash. | ||
| path = _write(self.tmp, "broken.xml", "<<not really xml>>") | ||
| with mock.patch("sys.stderr", new=io.StringIO()) as stderr: | ||
| rc = subunit.JUnitXML2SubUnit([path], self.subunit) | ||
| self.assertEqual(1, rc) | ||
| self.assertIn("failed to parse", stderr.getvalue()) | ||
| def test_time_attribute_advances_synthetic_clock(self): | ||
| # Each testcase's `time` attribute should determine the gap | ||
| # between its inprogress and terminal packets, so the consumer | ||
| # can recover the duration. Use distinct values per test to | ||
| # confirm both make it through. | ||
| path = _write( | ||
| self.tmp, | ||
| "report.xml", | ||
| """<?xml version="1.0"?> | ||
| <testsuite> | ||
| <testcase classname="A" name="testOne" time="0.5"/> | ||
| <testcase classname="A" name="testTwo" time="1.5"/> | ||
| </testsuite> | ||
| """, | ||
| ) | ||
| subunit.JUnitXML2SubUnit([path], self.subunit) | ||
| events = self._events() | ||
| # Pull the per-test (inprogress, terminal) timestamp pairs. The | ||
| # ByteStreamToStreamResult emits a "time" event before each | ||
| # status event when the packet carries a timestamp. | ||
| # Easier: walk the events and pair them up by test_id. | ||
| timestamps = {} | ||
| for e in events: | ||
| if e[0] != "status": | ||
| continue | ||
| test_id = e[1] | ||
| ts = e[-1] | ||
| if ts is None: | ||
| continue | ||
| timestamps.setdefault(test_id, []).append(ts) | ||
| one = timestamps["A::testOne"] | ||
| two = timestamps["A::testTwo"] | ||
| self.assertEqual(2, len(one)) | ||
| self.assertEqual(2, len(two)) | ||
| # testOne spans 0.5s | ||
| self.assertAlmostEqual(0.5, (one[1] - one[0]).total_seconds(), places=3) | ||
| # testTwo spans 1.5s | ||
| self.assertAlmostEqual(1.5, (two[1] - two[0]).total_seconds(), places=3) | ||
| class TestCollectFiles(TestCase): | ||
| """Tests for the `--dir` walking logic in the script entrypoint.""" | ||
| def setUp(self): | ||
| super().setUp() | ||
| self.tmp = tempfile.mkdtemp(prefix="junitxml2subunit-collect-") | ||
| self.addCleanup(self._rmtree, self.tmp) | ||
| def _rmtree(self, path): | ||
| import shutil | ||
| shutil.rmtree(path, ignore_errors=True) | ||
| def test_dir_walks_xml_files_only(self): | ||
| a = _write(self.tmp, "TEST-A.xml", "") | ||
| _write(self.tmp, "ignored.txt", "") | ||
| b = _write(self.tmp, "TEST-B.xml", "") | ||
| out = junitxml2subunit.collect_files([self.tmp], []) | ||
| self.assertEqual({a, b}, set(out)) | ||
| def test_dir_results_are_sorted_for_deterministic_output(self): | ||
| # Lexical sort within a directory so the subunit stream is | ||
| # reproducible across runs (and across filesystems with | ||
| # different readdir order). | ||
| b = _write(self.tmp, "TEST-B.xml", "") | ||
| a = _write(self.tmp, "TEST-A.xml", "") | ||
| out = junitxml2subunit.collect_files([self.tmp], []) | ||
| self.assertEqual([a, b], out) | ||
| def test_explicit_files_appended_after_dir_walks(self): | ||
| a = _write(self.tmp, "TEST-A.xml", "") | ||
| # Build an explicit file outside the walked dir to confirm it's | ||
| # appended after the discovered files rather than re-walked. | ||
| extra_dir = tempfile.mkdtemp(prefix="junitxml2subunit-extra-") | ||
| self.addCleanup(self._rmtree, extra_dir) | ||
| explicit = _write(extra_dir, "extra.xml", "") | ||
| out = junitxml2subunit.collect_files([self.tmp], [explicit]) | ||
| self.assertEqual([a, explicit], out) | ||
| def test_missing_dir_warned_and_skipped(self): | ||
| with mock.patch("sys.stderr", new=io.StringIO()) as stderr: | ||
| out = junitxml2subunit.collect_files(["/nonexistent/junit/dir"], []) | ||
| self.assertEqual([], out) | ||
| self.assertIn("not a directory", stderr.getvalue()) |
| # | ||
| # subunit: extensions to python unittest to get test results from subprocesses. | ||
| # 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 | ||
| # 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. | ||
| # | ||
| import datetime | ||
| import optparse | ||
| from contextlib import contextmanager | ||
| from functools import partial | ||
| from io import BytesIO, TextIOWrapper | ||
| from tempfile import NamedTemporaryFile | ||
| from iso8601 import UTC | ||
| from testtools import TestCase | ||
| from testtools.matchers import Equals, Matcher, MatchesAny, MatchesListwise, Mismatch, raises | ||
| from testtools.testresult.doubles import StreamResult | ||
| import subunit._output as _o | ||
| from subunit._output import _ALL_ACTIONS, _FINAL_ACTIONS, generate_stream_results, parse_arguments | ||
| class SafeOptionParser(optparse.OptionParser): | ||
| """An ArgumentParser class that doesn't call sys.exit.""" | ||
| 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 TestStatusArgParserTests(TestCase): | ||
| scenarios = [(cmd, dict(command=cmd, option="--" + cmd)) for cmd in _ALL_ACTIONS] | ||
| 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(self.command)) | ||
| self.assertThat(args.test_id, Equals(test_id)) | ||
| def test_all_commands_parse_file_attachment(self): | ||
| with NamedTemporaryFile() as tmp_file: | ||
| args = safe_parse_arguments(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.option, "foo", "--attach-file", tmp_file.name, "--mimetype", "text/plain"] | ||
| ) | ||
| 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", "--tag", "foo", "--tag", "bar", "--tag", "baz"]) | ||
| self.assertThat(args.tags, Equals(["foo", "bar", "baz"])) | ||
| def test_attach_file_with_hyphen_opens_stdin(self): | ||
| 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(b"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")) | ||
| def test_requires_test_id(self): | ||
| def fn(): | ||
| return safe_parse_arguments(args=[self.option]) | ||
| self.assertThat(fn, raises(RuntimeError("argument %s: must specify a single TEST_ID." % self.option))) | ||
| class ArgParserTests(TestCase): | ||
| def test_can_parse_attach_file_without_test_id(self): | ||
| with NamedTemporaryFile() as tmp_file: | ||
| args = safe_parse_arguments(args=["--attach-file", tmp_file.name]) | ||
| self.assertThat(args.attach_file.name, Equals(tmp_file.name)) | ||
| def test_can_run_without_args(self): | ||
| safe_parse_arguments([]) | ||
| def test_cannot_specify_more_than_one_status_command(self): | ||
| def fn(): | ||
| return safe_parse_arguments(["--fail", "foo", "--skip", "bar"]) | ||
| self.assertThat(fn, raises(RuntimeError("argument --skip: Only one status may be specified at once."))) | ||
| def test_cannot_specify_mimetype_without_attach_file(self): | ||
| def fn(): | ||
| return safe_parse_arguments(["--mimetype", "foo"]) | ||
| self.assertThat(fn, raises(RuntimeError("Cannot specify --mimetype without --attach-file"))) | ||
| def test_cannot_specify_filename_without_attach_file(self): | ||
| def fn(): | ||
| return safe_parse_arguments(["--file-name", "foo"]) | ||
| self.assertThat(fn, raises(RuntimeError("Cannot specify --file-name without --attach-file"))) | ||
| 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): | ||
| def fn(): | ||
| return safe_parse_arguments(["--fail", "foo", "--tag"]) | ||
| self.assertThat( | ||
| fn, | ||
| 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. | ||
| 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. | ||
| """ | ||
| result = StreamResult() | ||
| args = safe_parse_arguments(commands) | ||
| generate_stream_results(args, result) | ||
| return result | ||
| @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(TestCase): | ||
| scenarios = [(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().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(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[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[1], MatchesStatusCall(test_tags={"hello", "world"})) | ||
| def test_all_commands_generate_timestamp(self): | ||
| result = get_result_for([self.option, self.test_id]) | ||
| self.assertThat(result._events[1], MatchesStatusCall(timestamp=self._dummy_timestamp)) | ||
| def test_all_commands_generate_correct_test_id(self): | ||
| result = get_result_for([self.option, self.test_id]) | ||
| self.assertThat(result._events[1], 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(call="startTestRun"), | ||
| MatchesStatusCall(file_bytes=b"Hello", eof=True), | ||
| MatchesStatusCall(call="stopTestRun"), | ||
| ] | ||
| ), | ||
| ) | ||
| 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(call="startTestRun"), | ||
| MatchesStatusCall(file_bytes=b"\xde\xad\xbe\xef", eof=True), | ||
| MatchesStatusCall(call="stopTestRun"), | ||
| ] | ||
| ), | ||
| ) | ||
| 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(call="startTestRun"), | ||
| MatchesStatusCall(file_bytes=b"", file_name=f.name, eof=True), | ||
| MatchesStatusCall(call="stopTestRun"), | ||
| ] | ||
| ), | ||
| ) | ||
| def test_can_read_stdin(self): | ||
| 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( | ||
| result._events, | ||
| MatchesListwise( | ||
| [ | ||
| MatchesStatusCall(call="startTestRun"), | ||
| MatchesStatusCall(file_bytes=b"\xfe\xed\xfa\xce", file_name="stdin", eof=True), | ||
| MatchesStatusCall(call="stopTestRun"), | ||
| ] | ||
| ), | ||
| ) | ||
| 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]) | ||
| self.assertThat( | ||
| result._events, | ||
| MatchesListwise( | ||
| [ | ||
| MatchesStatusCall(call="startTestRun"), | ||
| MatchesStatusCall(test_id=self.test_id, file_bytes=b"Hello", eof=True), | ||
| MatchesStatusCall(call="stopTestRun"), | ||
| ] | ||
| ), | ||
| ) | ||
| 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(call="startTestRun"), | ||
| MatchesStatusCall(test_status=self.status, file_bytes=b"Hello", eof=True), | ||
| MatchesStatusCall(call="stopTestRun"), | ||
| ] | ||
| ), | ||
| ) | ||
| def test_file_chunk_size_is_honored(self): | ||
| 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(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"), | ||
| ] | ||
| ), | ||
| ) | ||
| 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", | ||
| ] | ||
| ) | ||
| 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"), | ||
| ] | ||
| ), | ||
| ) | ||
| 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, | ||
| "--tag", | ||
| "foo", | ||
| "--tag", | ||
| "bar", | ||
| ] | ||
| ) | ||
| self.assertThat( | ||
| result._events, | ||
| MatchesListwise( | ||
| [ | ||
| MatchesStatusCall(call="startTestRun"), | ||
| MatchesStatusCall(test_id=self.test_id, test_tags={"foo", "bar"}), | ||
| MatchesStatusCall(test_id=self.test_id, test_tags=None), | ||
| MatchesStatusCall(call="stopTestRun"), | ||
| ] | ||
| ), | ||
| ) | ||
| 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, | ||
| ] | ||
| ) | ||
| 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"), | ||
| ] | ||
| ), | ||
| ) | ||
| 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( | ||
| [ | ||
| MatchesStatusCall(call="startTestRun"), | ||
| first_call, | ||
| last_call, | ||
| MatchesStatusCall(call="stopTestRun"), | ||
| ] | ||
| ), | ||
| ) | ||
| 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(call="startTestRun"), | ||
| MatchesStatusCall(file_name=specified_file_name, file_bytes=b"Hello"), | ||
| MatchesStatusCall(call="stopTestRun"), | ||
| ] | ||
| ), | ||
| ) | ||
| 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(call="startTestRun"), | ||
| MatchesStatusCall(file_name=f.name, file_bytes=b"Hello", eof=True), | ||
| MatchesStatusCall(call="stopTestRun"), | ||
| ] | ||
| ), | ||
| ) | ||
| class FileDataTests(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(call="startTestRun"), | ||
| MatchesStatusCall(test_id=None, file_bytes=b"Hello", eof=True), | ||
| MatchesStatusCall(call="stopTestRun"), | ||
| ] | ||
| ), | ||
| ) | ||
| 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(call="startTestRun"), | ||
| MatchesStatusCall(file_name=f.name, file_bytes=b"Hello", eof=True), | ||
| MatchesStatusCall(call="stopTestRun"), | ||
| ] | ||
| ), | ||
| ) | ||
| 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(call="startTestRun"), | ||
| MatchesStatusCall(file_name=specified_file_name, file_bytes=b"Hello"), | ||
| MatchesStatusCall(call="stopTestRun"), | ||
| ] | ||
| ), | ||
| ) | ||
| def test_files_have_timestamp(self): | ||
| _dummy_timestamp = datetime.datetime(2013, 1, 1, 0, 0, 0, 0, UTC) | ||
| self.patch(_o, "create_timestamp", lambda: _dummy_timestamp) | ||
| with temp_file_contents(b"Hello") as f: | ||
| self.getUniqueString() | ||
| result = get_result_for( | ||
| [ | ||
| "--attach-file", | ||
| f.name, | ||
| ] | ||
| ) | ||
| self.assertThat( | ||
| result._events, | ||
| MatchesListwise( | ||
| [ | ||
| MatchesStatusCall(call="startTestRun"), | ||
| MatchesStatusCall(file_bytes=b"Hello", timestamp=_dummy_timestamp), | ||
| MatchesStatusCall(call="stopTestRun"), | ||
| ] | ||
| ), | ||
| ) | ||
| def test_can_specify_tags_without_test_status(self): | ||
| result = get_result_for( | ||
| [ | ||
| "--tag", | ||
| "foo", | ||
| ] | ||
| ) | ||
| self.assertThat( | ||
| result._events, | ||
| MatchesListwise( | ||
| [ | ||
| MatchesStatusCall(call="startTestRun"), | ||
| MatchesStatusCall(test_tags={"foo"}), | ||
| MatchesStatusCall(call="stopTestRun"), | ||
| ] | ||
| ), | ||
| ) | ||
| class MatchesStatusCall(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 = 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 | ||
| def match(self, call_tuple): | ||
| for k, v in self._filters.items(): | ||
| try: | ||
| pos = self._position_lookup[k] | ||
| if call_tuple[pos] != v: | ||
| return Mismatch("Value for key is {!r}, not {!r}".format(call_tuple[pos], v)) | ||
| except IndexError: | ||
| return Mismatch("Key %s is not present." % k) | ||
| def __str__(self): | ||
| return "<MatchesStatusCall %r>" % self._filters |
| # | ||
| # subunit: extensions to Python unittest to get test results from subprocesses. | ||
| # Copyright (C) 2009 Robert Collins <robertc@robertcollins.net> | ||
| # | ||
| # 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. | ||
| # | ||
| import unittest | ||
| from subunit.progress_model import ProgressModel | ||
| class TestProgressModel(unittest.TestCase): | ||
| def assertProgressSummary(self, pos, total, progress): | ||
| """Assert that a progress model has reached a particular point.""" | ||
| self.assertEqual(pos, progress.pos()) | ||
| self.assertEqual(total, progress.width()) | ||
| def test_new_progress_0_0(self): | ||
| progress = ProgressModel() | ||
| self.assertProgressSummary(0, 0, progress) | ||
| def test_advance_0_0(self): | ||
| progress = ProgressModel() | ||
| progress.advance() | ||
| self.assertProgressSummary(1, 0, progress) | ||
| def test_advance_1_0(self): | ||
| progress = ProgressModel() | ||
| progress.advance() | ||
| self.assertProgressSummary(1, 0, progress) | ||
| def test_set_width_absolute(self): | ||
| progress = ProgressModel() | ||
| progress.set_width(10) | ||
| self.assertProgressSummary(0, 10, progress) | ||
| def test_set_width_absolute_preserves_pos(self): | ||
| progress = ProgressModel() | ||
| progress.advance() | ||
| progress.set_width(2) | ||
| self.assertProgressSummary(1, 2, progress) | ||
| def test_adjust_width(self): | ||
| progress = ProgressModel() | ||
| progress.adjust_width(10) | ||
| self.assertProgressSummary(0, 10, progress) | ||
| progress.adjust_width(-10) | ||
| self.assertProgressSummary(0, 0, progress) | ||
| def test_adjust_width_preserves_pos(self): | ||
| progress = ProgressModel() | ||
| progress.advance() | ||
| progress.adjust_width(10) | ||
| self.assertProgressSummary(1, 10, progress) | ||
| progress.adjust_width(-10) | ||
| self.assertProgressSummary(1, 0, progress) | ||
| def test_push_preserves_progress(self): | ||
| progress = ProgressModel() | ||
| progress.adjust_width(3) | ||
| progress.advance() | ||
| progress.push() | ||
| self.assertProgressSummary(1, 3, progress) | ||
| def test_advance_advances_substack(self): | ||
| progress = ProgressModel() | ||
| progress.adjust_width(3) | ||
| progress.advance() | ||
| progress.push() | ||
| progress.adjust_width(1) | ||
| progress.advance() | ||
| self.assertProgressSummary(2, 3, progress) | ||
| def test_adjust_width_adjusts_substack(self): | ||
| progress = ProgressModel() | ||
| progress.adjust_width(3) | ||
| progress.advance() | ||
| progress.push() | ||
| progress.adjust_width(2) | ||
| progress.advance() | ||
| self.assertProgressSummary(3, 6, progress) | ||
| def test_set_width_adjusts_substack(self): | ||
| progress = ProgressModel() | ||
| progress.adjust_width(3) | ||
| progress.advance() | ||
| progress.push() | ||
| progress.set_width(2) | ||
| progress.advance() | ||
| self.assertProgressSummary(3, 6, progress) | ||
| def test_pop_restores_progress(self): | ||
| progress = ProgressModel() | ||
| progress.adjust_width(3) | ||
| progress.advance() | ||
| progress.push() | ||
| progress.adjust_width(1) | ||
| progress.advance() | ||
| progress.pop() | ||
| self.assertProgressSummary(1, 3, progress) |
| # | ||
| # subunit: extensions to python unittest to get test results from subprocesses. | ||
| # Copyright (C) 2011 Robert Collins <robertc@robertcollins.net> | ||
| # | ||
| # 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. | ||
| # | ||
| import io | ||
| import unittest | ||
| from testtools import PlaceHolder, TestCase | ||
| from testtools.matchers import StartsWith | ||
| from testtools.testresult.doubles import StreamResult | ||
| import subunit | ||
| from subunit import run | ||
| from subunit.run import SubunitTestRunner | ||
| class TestSubunitTestRunner(TestCase): | ||
| def test_includes_timing_output(self): | ||
| bytestream = io.BytesIO() | ||
| runner = SubunitTestRunner(stream=bytestream) | ||
| test = PlaceHolder("name") | ||
| runner.run(test) | ||
| bytestream.seek(0) | ||
| eventstream = StreamResult() | ||
| subunit.ByteStreamToStreamResult(bytestream).run(eventstream) | ||
| timestamps = [event[-1] for event in eventstream._events if event is not None] | ||
| self.assertNotEqual([], timestamps) | ||
| def test_enumerates_tests_before_run(self): | ||
| bytestream = io.BytesIO() | ||
| runner = SubunitTestRunner(stream=bytestream) | ||
| test1 = PlaceHolder("name1") | ||
| test2 = PlaceHolder("name2") | ||
| case = unittest.TestSuite([test1, test2]) | ||
| runner.run(case) | ||
| bytestream.seek(0) | ||
| eventstream = StreamResult() | ||
| subunit.ByteStreamToStreamResult(bytestream).run(eventstream) | ||
| self.assertEqual( | ||
| [ | ||
| ("status", "name1", "exists"), | ||
| ("status", "name2", "exists"), | ||
| ], | ||
| [event[:3] for event in eventstream._events[:2]], | ||
| ) | ||
| def test_list_errors_if_errors_from_list_test(self): | ||
| bytestream = io.BytesIO() | ||
| runner = SubunitTestRunner(stream=bytestream) | ||
| def list_test(test): | ||
| return [], ["failed import"] | ||
| self.patch(run, "list_test", list_test) | ||
| exc = self.assertRaises(SystemExit, runner.list, None) | ||
| self.assertEqual((2,), exc.args) | ||
| def test_list_includes_loader_errors(self): | ||
| bytestream = io.BytesIO() | ||
| runner = SubunitTestRunner(stream=bytestream) | ||
| def list_test(test): | ||
| return [], [] | ||
| class Loader: | ||
| errors = ["failed import"] | ||
| loader = Loader() | ||
| self.patch(run, "list_test", list_test) | ||
| exc = self.assertRaises(SystemExit, runner.list, None, loader=loader) | ||
| self.assertEqual((2,), exc.args) | ||
| class FailingTest(TestCase): | ||
| def test_fail(self): | ||
| 1 / 0 | ||
| def test_exits_zero_when_tests_fail(self): | ||
| bytestream = io.BytesIO() | ||
| stream = io.TextIOWrapper(bytestream, encoding="utf8") | ||
| try: | ||
| self.assertEqual( | ||
| None, | ||
| run.main(argv=["progName", "tests.test_run.TestSubunitTestRunner.FailingTest"], stdout=stream), | ||
| ) | ||
| except SystemExit: | ||
| self.fail("SystemExit raised") | ||
| self.assertThat(bytestream.getvalue(), StartsWith(b"\xb3")) | ||
| class ExitingTest(TestCase): | ||
| def test_exit(self): | ||
| raise SystemExit(0) | ||
| def test_exits_nonzero_when_execution_errors(self): | ||
| bytestream = io.BytesIO() | ||
| stream = io.TextIOWrapper(bytestream, encoding="utf8") | ||
| exc = self.assertRaises( | ||
| SystemExit, | ||
| run.main, | ||
| argv=["progName", "tests.test_run.TestSubunitTestRunner.ExitingTest"], | ||
| stdout=stream, | ||
| ) | ||
| self.assertEqual(0, exc.args[0]) |
| # | ||
| # subunit: extensions to Python unittest to get test results from subprocesses. | ||
| # Copyright (C) 2026 Jelmer Vernooij <jelmer@jelmer.uk> | ||
| # | ||
| # 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. | ||
| # | ||
| """Tests for subunit.filter_scripts.subunit_combine.""" | ||
| import os | ||
| import subprocess | ||
| import sys | ||
| import tempfile | ||
| from io import BytesIO | ||
| from testtools import TestCase | ||
| from testtools.testresult.doubles import StreamResult | ||
| from subunit import ByteStreamToStreamResult, StreamResultToBytes | ||
| from subunit.filter_scripts.subunit_combine import ( | ||
| _PrefixingStreamResult, | ||
| _expand_argv, | ||
| _read_id_list, | ||
| _select_ids_for_command, | ||
| combine, | ||
| load_config, | ||
| ) | ||
| def _stream_with_tests(test_ids): | ||
| """Build a subunit v2 byte stream with an inprogress+success pair per id.""" | ||
| buf = BytesIO() | ||
| out = StreamResultToBytes(buf) | ||
| for tid in test_ids: | ||
| out.status(test_id=tid, test_status="inprogress") | ||
| out.status(test_id=tid, test_status="success") | ||
| return buf.getvalue() | ||
| def _parse(data): | ||
| """Parse a subunit v2 byte stream into a list of (test_id, test_status).""" | ||
| events = StreamResult() | ||
| ByteStreamToStreamResult(BytesIO(data)).run(events) | ||
| return [(ev[1], ev[2]) for ev in events._events if ev[0] == "status"] | ||
| class TestPrefixingStreamResult(TestCase): | ||
| def test_prefixes_test_id(self): | ||
| target = StreamResult() | ||
| result = _PrefixingStreamResult(target, "py/") | ||
| result.status(test_id="foo", test_status="success") | ||
| self.assertEqual([("status", "py/foo", "success")], [ev[:3] for ev in target._events]) | ||
| def test_passes_through_none_test_id(self): | ||
| target = StreamResult() | ||
| result = _PrefixingStreamResult(target, "py/") | ||
| result.status(file_name="stdout", file_bytes=b"hi") | ||
| self.assertEqual([("status", None, None)], [ev[:3] for ev in target._events]) | ||
| class TestLoadConfig(TestCase): | ||
| def _write(self, text): | ||
| fd, path = tempfile.mkstemp(suffix=".yaml") | ||
| self.addCleanup(os.unlink, path) | ||
| with os.fdopen(fd, "w") as f: | ||
| f.write(text) | ||
| return path | ||
| def test_basic(self): | ||
| path = self._write("commands:\n - prefix: 'a/'\n argv: ['echo', 'hi']\n - argv: ['true']\n") | ||
| self.assertEqual( | ||
| [{"prefix": "a/", "argv": ["echo", "hi"]}, {"argv": ["true"]}], | ||
| load_config(path), | ||
| ) | ||
| def test_rejects_non_mapping(self): | ||
| path = self._write("- 1\n- 2\n") | ||
| self.assertRaises(ValueError, load_config, path) | ||
| def test_rejects_empty_commands(self): | ||
| path = self._write("commands: []\n") | ||
| self.assertRaises(ValueError, load_config, path) | ||
| def test_rejects_missing_argv(self): | ||
| path = self._write("commands:\n - prefix: 'a/'\n") | ||
| self.assertRaises(ValueError, load_config, path) | ||
| def test_rejects_non_string_prefix(self): | ||
| path = self._write("commands:\n - argv: ['x']\n prefix: 42\n") | ||
| self.assertRaises(ValueError, load_config, path) | ||
| class TestCombine(TestCase): | ||
| def _cat_cmd(self, data): | ||
| """A command that writes the given bytes to stdout and exits 0. | ||
| We use python -c so this works on all platforms and without relying on | ||
| shell quoting. | ||
| """ | ||
| src = "import sys; sys.stdout.buffer.write({!r})".format(data) | ||
| return [sys.executable, "-c", src] | ||
| def test_merges_streams_with_prefixes(self): | ||
| stream_a = _stream_with_tests(["one", "two"]) | ||
| stream_b = _stream_with_tests(["alpha"]) | ||
| commands = [ | ||
| {"prefix": "py/", "argv": self._cat_cmd(stream_a)}, | ||
| {"prefix": "rs/", "argv": self._cat_cmd(stream_b)}, | ||
| ] | ||
| output = BytesIO() | ||
| rc = combine(commands, output) | ||
| self.assertEqual(0, rc) | ||
| self.assertEqual( | ||
| [ | ||
| ("py/one", "inprogress"), | ||
| ("py/one", "success"), | ||
| ("py/two", "inprogress"), | ||
| ("py/two", "success"), | ||
| ("rs/alpha", "inprogress"), | ||
| ("rs/alpha", "success"), | ||
| ], | ||
| _parse(output.getvalue()), | ||
| ) | ||
| def test_no_prefix(self): | ||
| stream = _stream_with_tests(["solo"]) | ||
| commands = [{"argv": self._cat_cmd(stream)}] | ||
| output = BytesIO() | ||
| rc = combine(commands, output) | ||
| self.assertEqual(0, rc) | ||
| self.assertEqual( | ||
| [("solo", "inprogress"), ("solo", "success")], | ||
| _parse(output.getvalue()), | ||
| ) | ||
| def test_nonzero_exit_propagates(self): | ||
| stream = _stream_with_tests(["only"]) | ||
| src = "import sys; sys.stdout.buffer.write({!r}); sys.exit(3)".format(stream) | ||
| commands = [{"argv": [sys.executable, "-c", src]}] | ||
| output = BytesIO() | ||
| rc = combine(commands, output) | ||
| self.assertEqual(1, rc) | ||
| class TestCombineCommand(TestCase): | ||
| def test_end_to_end(self): | ||
| stream_a = _stream_with_tests(["a"]) | ||
| stream_b = _stream_with_tests(["b"]) | ||
| def cmd_for(data): | ||
| src = "import sys; sys.stdout.buffer.write({!r})".format(data) | ||
| return [sys.executable, "-c", src] | ||
| import yaml | ||
| config = { | ||
| "commands": [ | ||
| {"prefix": "py/", "argv": cmd_for(stream_a)}, | ||
| {"prefix": "rs/", "argv": cmd_for(stream_b)}, | ||
| ] | ||
| } | ||
| fd, path = tempfile.mkstemp(suffix=".yaml") | ||
| self.addCleanup(os.unlink, path) | ||
| with os.fdopen(fd, "w") as f: | ||
| yaml.safe_dump(config, f) | ||
| ps = subprocess.Popen( | ||
| [sys.executable, "-m", "subunit.filter_scripts.subunit_combine", path], | ||
| stdout=subprocess.PIPE, | ||
| stderr=subprocess.PIPE, | ||
| ) | ||
| out, err = ps.communicate() | ||
| self.assertEqual(0, ps.returncode, err) | ||
| self.assertEqual( | ||
| [ | ||
| ("py/a", "inprogress"), | ||
| ("py/a", "success"), | ||
| ("rs/b", "inprogress"), | ||
| ("rs/b", "success"), | ||
| ], | ||
| _parse(out), | ||
| ) | ||
| class TestExpandArgv(TestCase): | ||
| def test_listopt_expanded_in_list_mode(self): | ||
| cmd = {"argv": ["runner", "$LISTOPT", "$IDOPTION"], "list_option": "--list"} | ||
| self.assertEqual( | ||
| ["runner", "--list"], | ||
| _expand_argv(cmd, list_mode=True, test_ids=None, idfile_path=None), | ||
| ) | ||
| def test_listopt_empty_when_not_listing(self): | ||
| cmd = {"argv": ["runner", "$LISTOPT", "$IDOPTION"], "list_option": "--list"} | ||
| self.assertEqual( | ||
| ["runner"], | ||
| _expand_argv(cmd, list_mode=False, test_ids=None, idfile_path=None), | ||
| ) | ||
| def test_idoption_expanded_with_idfile(self): | ||
| cmd = { | ||
| "argv": ["runner", "$IDOPTION"], | ||
| "id_option": "--load-list $IDFILE", | ||
| } | ||
| self.assertEqual( | ||
| ["runner", "--load-list", "/tmp/ids"], | ||
| _expand_argv(cmd, list_mode=False, test_ids=["a", "b"], idfile_path="/tmp/ids"), | ||
| ) | ||
| def test_idlist_expanded(self): | ||
| cmd = {"argv": ["runner", "$IDLIST"]} | ||
| self.assertEqual( | ||
| ["runner", "a", "b", "c"], | ||
| _expand_argv(cmd, list_mode=False, test_ids=["a", "b", "c"], idfile_path=None), | ||
| ) | ||
| def test_idoption_empty_without_ids(self): | ||
| cmd = { | ||
| "argv": ["runner", "$IDOPTION"], | ||
| "id_option": "--load-list $IDFILE", | ||
| } | ||
| self.assertEqual( | ||
| ["runner"], | ||
| _expand_argv(cmd, list_mode=False, test_ids=None, idfile_path=None), | ||
| ) | ||
| def test_literal_argv_entries_untouched(self): | ||
| cmd = {"argv": ["runner", "--tag=$literal", "$IDLIST"]} | ||
| self.assertEqual( | ||
| ["runner", "--tag=$literal", "x"], | ||
| _expand_argv(cmd, list_mode=False, test_ids=["x"], idfile_path=None), | ||
| ) | ||
| class TestSelectIdsForCommand(TestCase): | ||
| def test_none_returns_none(self): | ||
| self.assertIsNone(_select_ids_for_command({"prefix": "py/"}, None)) | ||
| def test_filters_by_prefix_and_strips(self): | ||
| ids = ["py/one", "rs/two", "py/three"] | ||
| self.assertEqual(["one", "three"], _select_ids_for_command({"prefix": "py/"}, ids)) | ||
| def test_no_prefix_returns_all(self): | ||
| ids = ["one", "two"] | ||
| self.assertEqual(ids, _select_ids_for_command({}, ids)) | ||
| self.assertIsNot(ids, _select_ids_for_command({}, ids)) # must copy | ||
| def test_no_matches_returns_empty(self): | ||
| self.assertEqual([], _select_ids_for_command({"prefix": "py/"}, ["rs/a"])) | ||
| class TestReadIdList(TestCase): | ||
| def test_skips_blank_and_comments(self): | ||
| fd, path = tempfile.mkstemp() | ||
| self.addCleanup(os.unlink, path) | ||
| with os.fdopen(fd, "w") as f: | ||
| f.write("one\n\n# comment\ntwo # trailing\nthree\n") | ||
| self.assertEqual(["one", "two", "three"], _read_id_list(path)) | ||
| class TestCombineListAndFilter(TestCase): | ||
| def _cmd_for(self, data): | ||
| src = "import sys; sys.stdout.buffer.write({!r})".format(data) | ||
| return [sys.executable, "-c", src] | ||
| def _exists_stream(self, test_ids): | ||
| buf = BytesIO() | ||
| out = StreamResultToBytes(buf) | ||
| for tid in test_ids: | ||
| out.status(test_id=tid, test_status="exists") | ||
| return buf.getvalue() | ||
| def test_list_mode_substitutes_listopt(self): | ||
| # Child emits ids only when --list is present: we simulate this by | ||
| # embedding the argument directly in the command and asserting it's | ||
| # what gets executed. | ||
| list_stream = self._exists_stream(["foo", "bar"]) | ||
| src = ( | ||
| "import sys; args = sys.argv[1:]; assert args == ['--list'], args; sys.stdout.buffer.write({!r})" | ||
| ).format(list_stream) | ||
| commands = [ | ||
| { | ||
| "prefix": "py/", | ||
| "argv": [sys.executable, "-c", src, "$LISTOPT"], | ||
| "list_option": "--list", | ||
| } | ||
| ] | ||
| output = BytesIO() | ||
| rc = combine(commands, output, list_mode=True) | ||
| self.assertEqual(0, rc) | ||
| self.assertEqual( | ||
| [("py/foo", "exists"), ("py/bar", "exists")], | ||
| _parse(output.getvalue()), | ||
| ) | ||
| def test_listopt_absent_when_not_listing(self): | ||
| # When list_mode is False, $LISTOPT should expand to nothing. | ||
| run_stream = _stream_with_tests(["x"]) | ||
| src = ("import sys; args = sys.argv[1:]; assert args == [], args; sys.stdout.buffer.write({!r})").format( | ||
| run_stream | ||
| ) | ||
| commands = [ | ||
| { | ||
| "argv": [sys.executable, "-c", src, "$LISTOPT"], | ||
| "list_option": "--list", | ||
| } | ||
| ] | ||
| output = BytesIO() | ||
| rc = combine(commands, output) | ||
| self.assertEqual(0, rc) | ||
| def test_idoption_and_idfile_substituted(self): | ||
| # The child asserts it received --load-list <file> with two ids in it. | ||
| run_stream = _stream_with_tests(["one", "two"]) | ||
| src = ( | ||
| "import sys, pathlib; " | ||
| "args = sys.argv[1:]; " | ||
| "assert args[0] == '--load-list', args; " | ||
| "contents = pathlib.Path(args[1]).read_text().splitlines(); " | ||
| "assert contents == ['one', 'two'], contents; " | ||
| "sys.stdout.buffer.write({!r})" | ||
| ).format(run_stream) | ||
| commands = [ | ||
| { | ||
| "prefix": "py/", | ||
| "argv": [sys.executable, "-c", src, "$IDOPTION"], | ||
| "id_option": "--load-list $IDFILE", | ||
| } | ||
| ] | ||
| output = BytesIO() | ||
| rc = combine(commands, output, test_ids=["py/one", "py/two"]) | ||
| self.assertEqual(0, rc) | ||
| def test_idlist_substituted(self): | ||
| run_stream = _stream_with_tests(["a"]) | ||
| src = ( | ||
| "import sys; args = sys.argv[1:]; assert args == ['a', 'b'], args; sys.stdout.buffer.write({!r})" | ||
| ).format(run_stream) | ||
| commands = [ | ||
| { | ||
| "prefix": "py/", | ||
| "argv": [sys.executable, "-c", src, "$IDLIST"], | ||
| } | ||
| ] | ||
| output = BytesIO() | ||
| rc = combine(commands, output, test_ids=["py/a", "py/b"]) | ||
| self.assertEqual(0, rc) | ||
| def test_command_with_no_matching_ids_is_skipped(self): | ||
| # 'py/' receives ids; 'rs/' has no matching ids and must not run. | ||
| py_stream = _stream_with_tests(["one"]) | ||
| py_src = "import sys; sys.stdout.buffer.write({!r})".format(py_stream) | ||
| rs_src = "import sys; sys.exit('must not run')" | ||
| commands = [ | ||
| {"prefix": "py/", "argv": [sys.executable, "-c", py_src]}, | ||
| {"prefix": "rs/", "argv": [sys.executable, "-c", rs_src]}, | ||
| ] | ||
| output = BytesIO() | ||
| rc = combine(commands, output, test_ids=["py/one"]) | ||
| self.assertEqual(0, rc) | ||
| self.assertEqual( | ||
| [("py/one", "inprogress"), ("py/one", "success")], | ||
| _parse(output.getvalue()), | ||
| ) | ||
| def test_cli_list_flag(self): | ||
| list_stream = self._exists_stream(["foo"]) | ||
| src = ( | ||
| "import sys; args = sys.argv[1:]; assert args == ['--list'], args; sys.stdout.buffer.write({!r})" | ||
| ).format(list_stream) | ||
| config = { | ||
| "commands": [ | ||
| { | ||
| "prefix": "py/", | ||
| "argv": [sys.executable, "-c", src, "$LISTOPT"], | ||
| "list_option": "--list", | ||
| } | ||
| ] | ||
| } | ||
| fd, path = tempfile.mkstemp(suffix=".yaml") | ||
| self.addCleanup(os.unlink, path) | ||
| with os.fdopen(fd, "w") as f: | ||
| import yaml | ||
| yaml.safe_dump(config, f) | ||
| ps = subprocess.Popen( | ||
| [sys.executable, "-m", "subunit.filter_scripts.subunit_combine", "--list", path], | ||
| stdout=subprocess.PIPE, | ||
| stderr=subprocess.PIPE, | ||
| ) | ||
| out, err = ps.communicate() | ||
| self.assertEqual(0, ps.returncode, err) | ||
| self.assertEqual([("py/foo", "exists")], _parse(out)) | ||
| def test_cli_positional_ids(self): | ||
| run_stream = _stream_with_tests(["a"]) | ||
| src = ("import sys; args = sys.argv[1:]; assert args == ['a'], args; sys.stdout.buffer.write({!r})").format( | ||
| run_stream | ||
| ) | ||
| config = { | ||
| "commands": [ | ||
| { | ||
| "prefix": "py/", | ||
| "argv": [sys.executable, "-c", src, "$IDLIST"], | ||
| } | ||
| ] | ||
| } | ||
| fd, path = tempfile.mkstemp(suffix=".yaml") | ||
| self.addCleanup(os.unlink, path) | ||
| with os.fdopen(fd, "w") as f: | ||
| import yaml | ||
| yaml.safe_dump(config, f) | ||
| ps = subprocess.Popen( | ||
| [sys.executable, "-m", "subunit.filter_scripts.subunit_combine", path, "py/a"], | ||
| stdout=subprocess.PIPE, | ||
| stderr=subprocess.PIPE, | ||
| ) | ||
| out, err = ps.communicate() | ||
| self.assertEqual(0, ps.returncode, err) | ||
| self.assertEqual( | ||
| [("py/a", "inprogress"), ("py/a", "success")], | ||
| _parse(out), | ||
| ) | ||
| def test_cli_load_list(self): | ||
| run_stream = _stream_with_tests(["one"]) | ||
| src = ("import sys; args = sys.argv[1:]; assert args == ['one'], args; sys.stdout.buffer.write({!r})").format( | ||
| run_stream | ||
| ) | ||
| config = { | ||
| "commands": [ | ||
| { | ||
| "prefix": "py/", | ||
| "argv": [sys.executable, "-c", src, "$IDLIST"], | ||
| } | ||
| ] | ||
| } | ||
| cfg_fd, cfg_path = tempfile.mkstemp(suffix=".yaml") | ||
| self.addCleanup(os.unlink, cfg_path) | ||
| with os.fdopen(cfg_fd, "w") as f: | ||
| import yaml | ||
| yaml.safe_dump(config, f) | ||
| list_fd, list_path = tempfile.mkstemp(suffix=".list") | ||
| self.addCleanup(os.unlink, list_path) | ||
| with os.fdopen(list_fd, "w") as f: | ||
| f.write("py/one\n") | ||
| ps = subprocess.Popen( | ||
| [ | ||
| sys.executable, | ||
| "-m", | ||
| "subunit.filter_scripts.subunit_combine", | ||
| "--load-list", | ||
| list_path, | ||
| cfg_path, | ||
| ], | ||
| stdout=subprocess.PIPE, | ||
| stderr=subprocess.PIPE, | ||
| ) | ||
| out, err = ps.communicate() | ||
| self.assertEqual(0, ps.returncode, err) | ||
| self.assertEqual( | ||
| [("py/one", "inprogress"), ("py/one", "success")], | ||
| _parse(out), | ||
| ) |
| # | ||
| # subunit: extensions to python unittest to get test results from subprocesses. | ||
| # Copyright (C) 2005 Robert Collins <robertc@robertcollins.net> | ||
| # | ||
| # 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. | ||
| # | ||
| """Tests for subunit.TestResultFilter.""" | ||
| import subprocess | ||
| import sys | ||
| import unittest | ||
| from datetime import datetime | ||
| from io import BytesIO | ||
| from testtools import TestCase | ||
| from testtools.testresult.doubles import ExtendedTestResult, StreamResult | ||
| import iso8601 | ||
| import subunit | ||
| from subunit.test_results import make_tag_filter, TestResultFilter | ||
| from subunit import ByteStreamToStreamResult, StreamResultToBytes | ||
| class TestTestResultFilter(TestCase): | ||
| """Test for TestResultFilter, a TestResult object which filters tests.""" | ||
| # While TestResultFilter works on python objects, using a subunit stream | ||
| # is an easy pithy way of getting a series of test objects to call into | ||
| # the TestResult, and as TestResultFilter is intended for use with subunit | ||
| # also has the benefit of detecting any interface skew issues. | ||
| example_subunit_stream = b"""\ | ||
| tags: global | ||
| test passed | ||
| success passed | ||
| test failed | ||
| tags: local | ||
| failure failed | ||
| test error | ||
| error error [ | ||
| error details | ||
| ] | ||
| test skipped | ||
| skip skipped | ||
| test todo | ||
| xfail todo | ||
| """ | ||
| def run_tests(self, result_filter, input_stream=None): | ||
| """Run tests through the given filter. | ||
| :param result_filter: A filtering TestResult object. | ||
| :param input_stream: Bytes of subunit stream data. If not provided, | ||
| uses TestTestResultFilter.example_subunit_stream. | ||
| """ | ||
| if input_stream is None: | ||
| input_stream = self.example_subunit_stream | ||
| test = subunit.ProtocolTestCase(BytesIO(input_stream)) | ||
| test.run(result_filter) | ||
| def test_default(self): | ||
| """The default is to exclude success and include everything else.""" | ||
| filtered_result = unittest.TestResult() | ||
| result_filter = TestResultFilter(filtered_result) | ||
| self.run_tests(result_filter) | ||
| # skips are seen as success by default python TestResult. | ||
| self.assertEqual(["error"], [error[0].id() for error in filtered_result.errors]) | ||
| self.assertEqual(["failed"], [failure[0].id() for failure in filtered_result.failures]) | ||
| self.assertEqual(4, filtered_result.testsRun) | ||
| def test_tag_filter(self): | ||
| tag_filter = make_tag_filter(["global"], ["local"]) | ||
| result = ExtendedTestResult() | ||
| result_filter = TestResultFilter(result, filter_success=False, filter_predicate=tag_filter) | ||
| self.run_tests(result_filter) | ||
| tests_included = [event[1] for event in result._events if event[0] == "startTest"] | ||
| tests_expected = list(map(subunit.RemotedTestCase, ["passed", "error", "skipped", "todo"])) | ||
| self.assertEqual(tests_expected, tests_included) | ||
| def test_tags_tracked_correctly(self): | ||
| tag_filter = make_tag_filter(["a"], []) | ||
| result = ExtendedTestResult() | ||
| result_filter = TestResultFilter(result, filter_success=False, filter_predicate=tag_filter) | ||
| input_stream = b"test: foo\ntags: a\nsuccessful: foo\ntest: bar\nsuccessful: bar\n" | ||
| self.run_tests(result_filter, input_stream) | ||
| foo = subunit.RemotedTestCase("foo") | ||
| self.assertEqual( | ||
| [ | ||
| ("startTest", foo), | ||
| ("tags", {"a"}, set()), | ||
| ("addSuccess", foo), | ||
| ("stopTest", foo), | ||
| ], | ||
| result._events, | ||
| ) | ||
| def test_exclude_errors(self): | ||
| filtered_result = unittest.TestResult() | ||
| result_filter = TestResultFilter(filtered_result, filter_error=True) | ||
| self.run_tests(result_filter) | ||
| # skips are seen as errors by default python TestResult. | ||
| self.assertEqual([], filtered_result.errors) | ||
| self.assertEqual(["failed"], [failure[0].id() for failure in filtered_result.failures]) | ||
| self.assertEqual(3, filtered_result.testsRun) | ||
| def test_fixup_expected_failures(self): | ||
| filtered_result = unittest.TestResult() | ||
| result_filter = TestResultFilter(filtered_result, fixup_expected_failures={"failed"}) | ||
| self.run_tests(result_filter) | ||
| self.assertEqual(["failed", "todo"], [failure[0].id() for failure in filtered_result.expectedFailures]) | ||
| self.assertEqual([], filtered_result.failures) | ||
| self.assertEqual(4, filtered_result.testsRun) | ||
| def test_fixup_expected_errors(self): | ||
| filtered_result = unittest.TestResult() | ||
| result_filter = TestResultFilter(filtered_result, fixup_expected_failures={"error"}) | ||
| self.run_tests(result_filter) | ||
| self.assertEqual(["error", "todo"], [failure[0].id() for failure in filtered_result.expectedFailures]) | ||
| self.assertEqual([], filtered_result.errors) | ||
| self.assertEqual(4, filtered_result.testsRun) | ||
| def test_fixup_unexpected_success(self): | ||
| filtered_result = unittest.TestResult() | ||
| result_filter = TestResultFilter(filtered_result, filter_success=False, fixup_expected_failures={"passed"}) | ||
| self.run_tests(result_filter) | ||
| self.assertEqual(["passed"], [passed.id() for passed in filtered_result.unexpectedSuccesses]) | ||
| self.assertEqual(5, filtered_result.testsRun) | ||
| def test_exclude_failure(self): | ||
| filtered_result = unittest.TestResult() | ||
| result_filter = TestResultFilter(filtered_result, filter_failure=True) | ||
| self.run_tests(result_filter) | ||
| self.assertEqual(["error"], [error[0].id() for error in filtered_result.errors]) | ||
| self.assertEqual([], [failure[0].id() for failure in filtered_result.failures]) | ||
| self.assertEqual(3, filtered_result.testsRun) | ||
| def test_exclude_skips(self): | ||
| filtered_result = subunit.TestResultStats(None) | ||
| result_filter = TestResultFilter(filtered_result, filter_skip=True) | ||
| self.run_tests(result_filter) | ||
| self.assertEqual(0, filtered_result.skipped_tests) | ||
| self.assertEqual(2, filtered_result.failed_tests) | ||
| self.assertEqual(3, filtered_result.testsRun) | ||
| def test_include_success(self): | ||
| """Successes can be included if requested.""" | ||
| filtered_result = unittest.TestResult() | ||
| result_filter = TestResultFilter(filtered_result, filter_success=False) | ||
| self.run_tests(result_filter) | ||
| self.assertEqual(["error"], [error[0].id() for error in filtered_result.errors]) | ||
| self.assertEqual(["failed"], [failure[0].id() for failure in filtered_result.failures]) | ||
| self.assertEqual(5, filtered_result.testsRun) | ||
| def test_filter_predicate(self): | ||
| """You can filter by predicate callbacks""" | ||
| # 0.0.7 and earlier did not support the 'tags' parameter, so we need | ||
| # to test that we still support behaviour without it. | ||
| filtered_result = unittest.TestResult() | ||
| def filter_cb(test, outcome, err, details): | ||
| return outcome == "success" | ||
| result_filter = TestResultFilter(filtered_result, filter_predicate=filter_cb, filter_success=False) | ||
| self.run_tests(result_filter) | ||
| # Only success should pass | ||
| self.assertEqual(1, filtered_result.testsRun) | ||
| def test_filter_predicate_with_tags(self): | ||
| """You can filter by predicate callbacks that accept tags""" | ||
| filtered_result = unittest.TestResult() | ||
| def filter_cb(test, outcome, err, details, tags): | ||
| return outcome == "success" | ||
| result_filter = TestResultFilter(filtered_result, filter_predicate=filter_cb, filter_success=False) | ||
| self.run_tests(result_filter) | ||
| # Only success should pass | ||
| self.assertEqual(1, filtered_result.testsRun) | ||
| def test_time_ordering_preserved(self): | ||
| # Passing a subunit stream through TestResultFilter preserves the | ||
| # relative ordering of 'time' directives and any other subunit | ||
| # directives that are still included. | ||
| date_a = datetime(year=2000, month=1, day=1, tzinfo=iso8601.UTC) | ||
| date_b = datetime(year=2000, month=1, day=2, tzinfo=iso8601.UTC) | ||
| date_c = datetime(year=2000, month=1, day=3, tzinfo=iso8601.UTC) | ||
| subunit_stream = ( | ||
| "\n".join(["time: %s", "test: foo", "time: %s", "error: foo", "time: %s", ""]) % (date_a, date_b, date_c) | ||
| ).encode() | ||
| result = ExtendedTestResult() | ||
| result_filter = TestResultFilter(result) | ||
| self.run_tests(result_filter, subunit_stream) | ||
| foo = subunit.RemotedTestCase("foo") | ||
| self.maxDiff = None | ||
| self.assertEqual( | ||
| [ | ||
| ("time", date_a), | ||
| ("time", date_b), | ||
| ("startTest", foo), | ||
| ("addError", foo, {}), | ||
| ("stopTest", foo), | ||
| ("time", date_c), | ||
| ], | ||
| result._events, | ||
| ) | ||
| def test_time_passes_through_filtered_tests(self): | ||
| # Passing a subunit stream through TestResultFilter preserves 'time' | ||
| # directives even if a specific test is filtered out. | ||
| date_a = datetime(year=2000, month=1, day=1, tzinfo=iso8601.UTC) | ||
| date_b = datetime(year=2000, month=1, day=2, tzinfo=iso8601.UTC) | ||
| date_c = datetime(year=2000, month=1, day=3, tzinfo=iso8601.UTC) | ||
| subunit_stream = ( | ||
| "\n".join(["time: %s", "test: foo", "time: %s", "success: foo", "time: %s", ""]) % (date_a, date_b, date_c) | ||
| ).encode() | ||
| result = ExtendedTestResult() | ||
| result_filter = TestResultFilter(result) | ||
| result_filter.startTestRun() | ||
| self.run_tests(result_filter, subunit_stream) | ||
| result_filter.stopTestRun() | ||
| subunit.RemotedTestCase("foo") | ||
| self.maxDiff = None | ||
| self.assertEqual( | ||
| [ | ||
| ("startTestRun",), | ||
| ("time", date_a), | ||
| ("time", date_c), | ||
| ("stopTestRun",), | ||
| ], | ||
| result._events, | ||
| ) | ||
| def test_skip_preserved(self): | ||
| subunit_stream = "\n".join(["test: foo", "skip: foo", ""]).encode() | ||
| result = ExtendedTestResult() | ||
| result_filter = TestResultFilter(result) | ||
| self.run_tests(result_filter, subunit_stream) | ||
| foo = subunit.RemotedTestCase("foo") | ||
| self.assertEqual( | ||
| [ | ||
| ("startTest", foo), | ||
| ("addSkip", foo, {}), | ||
| ("stopTest", foo), | ||
| ], | ||
| result._events, | ||
| ) | ||
| def test_renames(self): | ||
| def rename(name): | ||
| return name + " - renamed" | ||
| result = ExtendedTestResult() | ||
| result_filter = TestResultFilter(result, filter_success=False, rename=rename) | ||
| input_stream = b"test: foo\nsuccessful: foo\n" | ||
| self.run_tests(result_filter, input_stream) | ||
| self.assertEqual( | ||
| [("startTest", "foo - renamed"), ("addSuccess", "foo - renamed"), ("stopTest", "foo - renamed")], | ||
| [(ev[0], ev[1].id()) for ev in result._events], | ||
| ) | ||
| class TestFilterCommand(TestCase): | ||
| def run_command(self, args, stream): | ||
| command = [sys.executable, "-m", "subunit.filter_scripts.subunit_filter"] + list(args) | ||
| ps = subprocess.Popen(command, stdin=subprocess.PIPE, stdout=subprocess.PIPE, stderr=subprocess.PIPE) | ||
| out, err = ps.communicate(stream) | ||
| if ps.returncode != 0: | ||
| raise RuntimeError("{} failed: {}".format(command, err)) | ||
| return out | ||
| def test_default(self): | ||
| byte_stream = BytesIO() | ||
| stream = StreamResultToBytes(byte_stream) | ||
| stream.status(test_id="foo", test_status="inprogress") | ||
| stream.status(test_id="foo", test_status="skip") | ||
| output = self.run_command([], byte_stream.getvalue()) | ||
| events = StreamResult() | ||
| ByteStreamToStreamResult(BytesIO(output)).run(events) | ||
| {event[1] for event in events._events} | ||
| self.assertEqual( | ||
| [ | ||
| ("status", "foo", "inprogress"), | ||
| ("status", "foo", "skip"), | ||
| ], | ||
| [event[:3] for event in events._events], | ||
| ) | ||
| def test_tags(self): | ||
| byte_stream = BytesIO() | ||
| stream = StreamResultToBytes(byte_stream) | ||
| stream.status(test_id="foo", test_status="inprogress", test_tags={"a"}) | ||
| stream.status(test_id="foo", test_status="success", test_tags={"a"}) | ||
| stream.status(test_id="bar", test_status="inprogress") | ||
| stream.status(test_id="bar", test_status="inprogress") | ||
| stream.status(test_id="baz", test_status="inprogress", test_tags={"a"}) | ||
| stream.status(test_id="baz", test_status="success", test_tags={"a"}) | ||
| output = self.run_command(["-s", "--with-tag", "a"], byte_stream.getvalue()) | ||
| events = StreamResult() | ||
| ByteStreamToStreamResult(BytesIO(output)).run(events) | ||
| ids = {event[1] for event in events._events} | ||
| self.assertEqual({"foo", "baz"}, ids) | ||
| def test_no_passthrough(self): | ||
| output = self.run_command(["--no-passthrough"], b"hi thar") | ||
| self.assertEqual(b"", output) | ||
| def test_passthrough(self): | ||
| output = self.run_command([], b"hi thar") | ||
| byte_stream = BytesIO() | ||
| stream = StreamResultToBytes(byte_stream) | ||
| stream.status(file_name="stdout", file_bytes=b"hi thar") | ||
| self.assertEqual(byte_stream.getvalue(), output) |
| # | ||
| # subunit: extensions to python unittest to get test results from subprocesses. | ||
| # Copyright (C) 2005 Robert Collins <robertc@robertcollins.net> | ||
| # | ||
| # 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. | ||
| # | ||
| """Tests for subunit.TestResultStats.""" | ||
| import unittest | ||
| from io import BytesIO, StringIO | ||
| import subunit | ||
| class TestTestResultStats(unittest.TestCase): | ||
| """Test for TestResultStats, a TestResult object that generates stats.""" | ||
| def setUp(self): | ||
| self.output = StringIO() | ||
| self.result = subunit.TestResultStats(self.output) | ||
| self.input_stream = BytesIO() | ||
| self.test = subunit.ProtocolTestCase(self.input_stream) | ||
| def test_stats_empty(self): | ||
| self.test.run(self.result) | ||
| self.assertEqual(0, self.result.total_tests) | ||
| self.assertEqual(0, self.result.passed_tests) | ||
| self.assertEqual(0, self.result.failed_tests) | ||
| self.assertEqual(set(), self.result.seen_tags) | ||
| def setUpUsedStream(self): | ||
| self.input_stream.write( | ||
| b"""tags: global | ||
| test passed | ||
| success passed | ||
| test failed | ||
| tags: local | ||
| failure failed | ||
| test error | ||
| error error | ||
| test skipped | ||
| skip skipped | ||
| test todo | ||
| xfail todo | ||
| """ | ||
| ) | ||
| self.input_stream.seek(0) | ||
| self.test.run(self.result) | ||
| def test_stats_smoke_everything(self): | ||
| # Statistics are calculated usefully. | ||
| self.setUpUsedStream() | ||
| self.assertEqual(5, self.result.total_tests) | ||
| self.assertEqual(2, self.result.passed_tests) | ||
| self.assertEqual(2, self.result.failed_tests) | ||
| self.assertEqual(1, self.result.skipped_tests) | ||
| self.assertEqual({"global", "local"}, self.result.seen_tags) | ||
| def test_stat_formatting(self): | ||
| expected = ( | ||
| """ | ||
| Total tests: 5 | ||
| Passed tests: 2 | ||
| Failed tests: 2 | ||
| Skipped tests: 1 | ||
| Seen tags: global, local | ||
| """ | ||
| )[1:] | ||
| self.setUpUsedStream() | ||
| self.result.formatStats() | ||
| self.assertEqual(expected, self.output.getvalue()) |
| # | ||
| # subunit: extensions to python unittest to get test results from subprocesses. | ||
| # Copyright (C) 2005 Robert Collins <robertc@robertcollins.net> | ||
| # | ||
| # 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. | ||
| # | ||
| """Tests for subunit.tag_stream.""" | ||
| from io import BytesIO | ||
| import testtools | ||
| from testtools.matchers import Contains | ||
| import subunit | ||
| import subunit.test_results | ||
| class TestSubUnitTags(testtools.TestCase): | ||
| def setUp(self): | ||
| super().setUp() | ||
| self.original = BytesIO() | ||
| self.filtered = BytesIO() | ||
| def test_add_tag(self): | ||
| # Literal values to avoid set sort-order dependencies. Python code show | ||
| # derivation. | ||
| # reference = BytesIO() | ||
| # stream = subunit.StreamResultToBytes(reference) | ||
| # stream.status( | ||
| # test_id='test', test_status='inprogress', test_tags=set(['quux', 'foo'])) | ||
| # stream.status( | ||
| # test_id='test', test_status='success', test_tags=set(['bar', 'quux', 'foo'])) | ||
| reference = [ | ||
| b"\xb3)\x82\x17\x04test\x02\x04quux\x03foo\x05\x97n\x86" | ||
| b"\xb3)\x83\x1b\x04test\x03\x03bar\x04quux\x03fooqn\xab)", | ||
| b"\xb3)\x82\x17\x04test\x02\x04quux\x03foo\x05\x97n\x86" | ||
| b"\xb3)\x83\x1b\x04test\x03\x04quux\x03foo\x03bar\xaf\xbd\x9d\xd6", | ||
| b"\xb3)\x82\x17\x04test\x02\x04quux\x03foo\x05\x97n\x86" | ||
| b"\xb3)\x83\x1b\x04test\x03\x04quux\x03bar\x03foo\x03\x04b\r", | ||
| b"\xb3)\x82\x17\x04test\x02\x04quux\x03foo\x05\x97n\x86" | ||
| b"\xb3)\x83\x1b\x04test\x03\x03bar\x03foo\x04quux\xd2\x18\x1bC", | ||
| b"\xb3)\x82\x17\x04test\x02\x04quux\x03foo\x05\x97n\x86" | ||
| b"\xb3)\x83\x1b\x04test\x03\x03foo\x04quux\x03bar\x08\xc2X\x83", | ||
| b"\xb3)\x82\x17\x04test\x02\x03foo\x04quux\xa6\xe1\xde\xec" | ||
| b"\xb3)\x83\x1b\x04test\x03\x03foo\x04quux\x03bar\x08\xc2X\x83", | ||
| b"\xb3)\x82\x17\x04test\x02\x03foo\x04quux\xa6\xe1\xde\xec" | ||
| b"\xb3)\x83\x1b\x04test\x03\x03bar\x03foo\x04quux\xd2\x18\x1bC", | ||
| b"\xb3)\x82\x17\x04test\x02\x03foo\x04quux\xa6\xe1\xde\xec" | ||
| b"\xb3)\x83\x1b\x04test\x03\x03foo\x03bar\x04quux:\x05e\x80", | ||
| b"\xb3)\x82\x17\x04test\x02\x03foo\x04quux\xa6\xe1\xde\xec" | ||
| b"\xb3)\x83\x1b\x04test\x03\x04quux\x03foo\x03bar\xaf\xbd\x9d\xd6", | ||
| ] | ||
| stream = subunit.StreamResultToBytes(self.original) | ||
| stream.status(test_id="test", test_status="inprogress", test_tags={"foo"}) | ||
| stream.status(test_id="test", test_status="success", test_tags={"foo", "bar"}) | ||
| self.original.seek(0) | ||
| self.assertEqual(0, subunit.tag_stream(self.original, self.filtered, ["quux"])) | ||
| self.assertThat(reference, Contains(bytes(self.filtered.getvalue()))) | ||
| def test_remove_tag(self): | ||
| reference = BytesIO() | ||
| stream = subunit.StreamResultToBytes(reference) | ||
| stream.status(test_id="test", test_status="inprogress", test_tags={"foo"}) | ||
| stream.status(test_id="test", test_status="success", test_tags={"foo"}) | ||
| stream = subunit.StreamResultToBytes(self.original) | ||
| stream.status(test_id="test", test_status="inprogress", test_tags={"foo"}) | ||
| stream.status(test_id="test", test_status="success", test_tags={"foo", "bar"}) | ||
| self.original.seek(0) | ||
| self.assertEqual(0, subunit.tag_stream(self.original, self.filtered, ["-bar"])) | ||
| self.assertEqual(reference.getvalue(), self.filtered.getvalue()) |
| # | ||
| # subunit: extensions to python unittest to get test results from subprocesses. | ||
| # Copyright (C) 2005 Robert Collins <robertc@robertcollins.net> | ||
| # | ||
| # 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. | ||
| # | ||
| """Tests for TAP2SubUnit.""" | ||
| from io import BytesIO, StringIO | ||
| from testtools import TestCase | ||
| from testtools.testresult.doubles import StreamResult | ||
| import subunit | ||
| UTF8_TEXT = "text/plain; charset=UTF8" | ||
| class TestTAP2SubUnit(TestCase): | ||
| """Tests for TAP2SubUnit. | ||
| These tests test TAP string data in, and subunit string data out. | ||
| This is ok because the subunit protocol is intended to be stable, | ||
| but it might be easier/pithier to write tests against TAP string in, | ||
| parsed subunit objects out (by hooking the subunit stream to a subunit | ||
| protocol server. | ||
| """ | ||
| def setUp(self): | ||
| super().setUp() | ||
| self.tap = StringIO() | ||
| self.subunit = BytesIO() | ||
| def test_skip_entire_file(self): | ||
| # A file | ||
| # 1..- # Skipped: comment | ||
| # results in a single skipped test. | ||
| self.tap.write("1..0 # Skipped: entire file skipped\n") | ||
| self.tap.seek(0) | ||
| result = subunit.TAP2SubUnit(self.tap, self.subunit) | ||
| self.assertEqual(0, result) | ||
| self.check_events( | ||
| [ | ||
| ( | ||
| "status", | ||
| "file skip", | ||
| "skip", | ||
| None, | ||
| True, | ||
| "tap comment", | ||
| b"Skipped: entire file skipped", | ||
| True, | ||
| None, | ||
| None, | ||
| None, | ||
| ) | ||
| ] | ||
| ) | ||
| def test_ok_test_pass(self): | ||
| # A file | ||
| # ok | ||
| # results in a passed test with name 'test 1' (a synthetic name as tap | ||
| # does not require named fixtures - it is the first test in the tap | ||
| # stream). | ||
| self.tap.write("ok\n") | ||
| self.tap.seek(0) | ||
| result = subunit.TAP2SubUnit(self.tap, self.subunit) | ||
| self.assertEqual(0, result) | ||
| self.check_events([("status", "test 1", "success", None, False, None, None, True, None, None, None)]) | ||
| def test_ok_test_number_pass(self): | ||
| # A file | ||
| # ok 1 | ||
| # results in a passed test with name 'test 1' | ||
| self.tap.write("ok 1\n") | ||
| self.tap.seek(0) | ||
| result = subunit.TAP2SubUnit(self.tap, self.subunit) | ||
| self.assertEqual(0, result) | ||
| self.check_events([("status", "test 1", "success", None, False, None, None, True, None, None, None)]) | ||
| def test_ok_test_number_description_pass(self): | ||
| # A file | ||
| # ok 1 - There is a description | ||
| # results in a passed test with name 'test 1 - There is a description' | ||
| self.tap.write("ok 1 - There is a description\n") | ||
| self.tap.seek(0) | ||
| result = subunit.TAP2SubUnit(self.tap, self.subunit) | ||
| self.assertEqual(0, result) | ||
| self.check_events( | ||
| [("status", "test 1 - There is a description", "success", None, False, None, None, True, None, None, None)] | ||
| ) | ||
| def test_ok_test_description_pass(self): | ||
| # A file | ||
| # ok There is a description | ||
| # results in a passed test with name 'test 1 There is a description' | ||
| self.tap.write("ok There is a description\n") | ||
| self.tap.seek(0) | ||
| result = subunit.TAP2SubUnit(self.tap, self.subunit) | ||
| self.assertEqual(0, result) | ||
| self.check_events( | ||
| [("status", "test 1 There is a description", "success", None, False, None, None, True, None, None, None)] | ||
| ) | ||
| def test_ok_SKIP_skip(self): | ||
| # A file | ||
| # ok # SKIP | ||
| # results in a skkip test with name 'test 1' | ||
| self.tap.write("ok # SKIP\n") | ||
| self.tap.seek(0) | ||
| result = subunit.TAP2SubUnit(self.tap, self.subunit) | ||
| self.assertEqual(0, result) | ||
| self.check_events([("status", "test 1", "skip", None, False, None, None, True, None, None, None)]) | ||
| def test_ok_skip_number_comment_lowercase(self): | ||
| self.tap.write("ok 1 # skip no samba environment available, skipping compilation\n") | ||
| self.tap.seek(0) | ||
| result = subunit.TAP2SubUnit(self.tap, self.subunit) | ||
| self.assertEqual(0, result) | ||
| self.check_events( | ||
| [ | ||
| ( | ||
| "status", | ||
| "test 1", | ||
| "skip", | ||
| None, | ||
| False, | ||
| "tap comment", | ||
| b"no samba environment available, skipping compilation", | ||
| True, | ||
| "text/plain; charset=UTF8", | ||
| None, | ||
| None, | ||
| ) | ||
| ] | ||
| ) | ||
| def test_ok_number_description_SKIP_skip_comment(self): | ||
| # A file | ||
| # ok 1 foo # SKIP Not done yet | ||
| # results in a skip test with name 'test 1 foo' and a log of | ||
| # Not done yet | ||
| self.tap.write("ok 1 foo # SKIP Not done yet\n") | ||
| self.tap.seek(0) | ||
| result = subunit.TAP2SubUnit(self.tap, self.subunit) | ||
| self.assertEqual(0, result) | ||
| self.check_events( | ||
| [ | ||
| ( | ||
| "status", | ||
| "test 1 foo", | ||
| "skip", | ||
| None, | ||
| False, | ||
| "tap comment", | ||
| b"Not done yet", | ||
| True, | ||
| "text/plain; charset=UTF8", | ||
| None, | ||
| None, | ||
| ) | ||
| ] | ||
| ) | ||
| def test_ok_SKIP_skip_comment(self): | ||
| # A file | ||
| # ok # SKIP Not done yet | ||
| # results in a skip test with name 'test 1' and a log of Not done yet | ||
| self.tap.write("ok # SKIP Not done yet\n") | ||
| self.tap.seek(0) | ||
| result = subunit.TAP2SubUnit(self.tap, self.subunit) | ||
| self.assertEqual(0, result) | ||
| self.check_events( | ||
| [ | ||
| ( | ||
| "status", | ||
| "test 1", | ||
| "skip", | ||
| None, | ||
| False, | ||
| "tap comment", | ||
| b"Not done yet", | ||
| True, | ||
| "text/plain; charset=UTF8", | ||
| None, | ||
| None, | ||
| ) | ||
| ] | ||
| ) | ||
| def test_ok_TODO_xfail(self): | ||
| # A file | ||
| # ok # TODO | ||
| # results in a xfail test with name 'test 1' | ||
| self.tap.write("ok # TODO\n") | ||
| self.tap.seek(0) | ||
| result = subunit.TAP2SubUnit(self.tap, self.subunit) | ||
| self.assertEqual(0, result) | ||
| self.check_events([("status", "test 1", "xfail", None, False, None, None, True, None, None, None)]) | ||
| def test_ok_TODO_xfail_comment(self): | ||
| # A file | ||
| # ok # TODO Not done yet | ||
| # results in a xfail test with name 'test 1' and a log of Not done yet | ||
| self.tap.write("ok # TODO Not done yet\n") | ||
| self.tap.seek(0) | ||
| result = subunit.TAP2SubUnit(self.tap, self.subunit) | ||
| self.assertEqual(0, result) | ||
| self.check_events( | ||
| [ | ||
| ( | ||
| "status", | ||
| "test 1", | ||
| "xfail", | ||
| None, | ||
| False, | ||
| "tap comment", | ||
| b"Not done yet", | ||
| True, | ||
| "text/plain; charset=UTF8", | ||
| None, | ||
| None, | ||
| ) | ||
| ] | ||
| ) | ||
| def test_bail_out_errors(self): | ||
| # A file with line in it | ||
| # Bail out! COMMENT | ||
| # is treated as an error | ||
| self.tap.write("ok 1 foo\n") | ||
| self.tap.write("Bail out! Lifejacket engaged\n") | ||
| self.tap.seek(0) | ||
| result = subunit.TAP2SubUnit(self.tap, self.subunit) | ||
| self.assertEqual(0, result) | ||
| self.check_events( | ||
| [ | ||
| ("status", "test 1 foo", "success", None, False, None, None, True, None, None, None), | ||
| ("status", "Bail out! Lifejacket engaged", "fail", None, False, None, None, True, None, None, None), | ||
| ] | ||
| ) | ||
| def test_missing_test_at_end_with_plan_adds_error(self): | ||
| # A file | ||
| # 1..3 | ||
| # ok first test | ||
| # not ok third test | ||
| # results in three tests, with the third being created | ||
| self.tap.write("1..3\n") | ||
| self.tap.write("ok first test\n") | ||
| self.tap.write("not ok second test\n") | ||
| self.tap.seek(0) | ||
| result = subunit.TAP2SubUnit(self.tap, self.subunit) | ||
| self.assertEqual(0, result) | ||
| self.check_events( | ||
| [ | ||
| ("status", "test 1 first test", "success", None, False, None, None, True, None, None, None), | ||
| ("status", "test 2 second test", "fail", None, False, None, None, True, None, None, None), | ||
| ( | ||
| "status", | ||
| "test 3", | ||
| "fail", | ||
| None, | ||
| False, | ||
| "tap meta", | ||
| b"test missing from TAP output", | ||
| True, | ||
| "text/plain; charset=UTF8", | ||
| None, | ||
| None, | ||
| ), | ||
| ] | ||
| ) | ||
| def test_missing_test_with_plan_adds_error(self): | ||
| # A file | ||
| # 1..3 | ||
| # ok first test | ||
| # not ok 3 third test | ||
| # results in three tests, with the second being created | ||
| self.tap.write("1..3\n") | ||
| self.tap.write("ok first test\n") | ||
| self.tap.write("not ok 3 third test\n") | ||
| self.tap.seek(0) | ||
| result = subunit.TAP2SubUnit(self.tap, self.subunit) | ||
| self.assertEqual(0, result) | ||
| self.check_events( | ||
| [ | ||
| ("status", "test 1 first test", "success", None, False, None, None, True, None, None, None), | ||
| ( | ||
| "status", | ||
| "test 2", | ||
| "fail", | ||
| None, | ||
| False, | ||
| "tap meta", | ||
| b"test missing from TAP output", | ||
| True, | ||
| "text/plain; charset=UTF8", | ||
| None, | ||
| None, | ||
| ), | ||
| ("status", "test 3 third test", "fail", None, False, None, None, True, None, None, None), | ||
| ] | ||
| ) | ||
| def test_missing_test_no_plan_adds_error(self): | ||
| # A file | ||
| # ok first test | ||
| # not ok 3 third test | ||
| # results in three tests, with the second being created | ||
| self.tap.write("ok first test\n") | ||
| self.tap.write("not ok 3 third test\n") | ||
| self.tap.seek(0) | ||
| result = subunit.TAP2SubUnit(self.tap, self.subunit) | ||
| self.assertEqual(0, result) | ||
| self.check_events( | ||
| [ | ||
| ("status", "test 1 first test", "success", None, False, None, None, True, None, None, None), | ||
| ( | ||
| "status", | ||
| "test 2", | ||
| "fail", | ||
| None, | ||
| False, | ||
| "tap meta", | ||
| b"test missing from TAP output", | ||
| True, | ||
| "text/plain; charset=UTF8", | ||
| None, | ||
| None, | ||
| ), | ||
| ("status", "test 3 third test", "fail", None, False, None, None, True, None, None, None), | ||
| ] | ||
| ) | ||
| def test_four_tests_in_a_row_trailing_plan(self): | ||
| # A file | ||
| # ok 1 - first test in a script with no plan at all | ||
| # not ok 2 - second | ||
| # ok 3 - third | ||
| # not ok 4 - fourth | ||
| # 1..4 | ||
| # results in four tests numbered and named | ||
| self.tap.write("ok 1 - first test in a script with trailing plan\n") | ||
| self.tap.write("not ok 2 - second\n") | ||
| self.tap.write("ok 3 - third\n") | ||
| self.tap.write("not ok 4 - fourth\n") | ||
| self.tap.write("1..4\n") | ||
| self.tap.seek(0) | ||
| result = subunit.TAP2SubUnit(self.tap, self.subunit) | ||
| self.assertEqual(0, result) | ||
| self.check_events( | ||
| [ | ||
| ( | ||
| "status", | ||
| "test 1 - first test in a script with trailing plan", | ||
| "success", | ||
| None, | ||
| False, | ||
| None, | ||
| None, | ||
| True, | ||
| None, | ||
| None, | ||
| None, | ||
| ), | ||
| ("status", "test 2 - second", "fail", None, False, None, None, True, None, None, None), | ||
| ("status", "test 3 - third", "success", None, False, None, None, True, None, None, None), | ||
| ("status", "test 4 - fourth", "fail", None, False, None, None, True, None, None, None), | ||
| ] | ||
| ) | ||
| def test_four_tests_in_a_row_with_plan(self): | ||
| # A file | ||
| # 1..4 | ||
| # ok 1 - first test in a script with no plan at all | ||
| # not ok 2 - second | ||
| # ok 3 - third | ||
| # not ok 4 - fourth | ||
| # results in four tests numbered and named | ||
| self.tap.write("1..4\n") | ||
| self.tap.write("ok 1 - first test in a script with a plan\n") | ||
| self.tap.write("not ok 2 - second\n") | ||
| self.tap.write("ok 3 - third\n") | ||
| self.tap.write("not ok 4 - fourth\n") | ||
| self.tap.seek(0) | ||
| result = subunit.TAP2SubUnit(self.tap, self.subunit) | ||
| self.assertEqual(0, result) | ||
| self.check_events( | ||
| [ | ||
| ( | ||
| "status", | ||
| "test 1 - first test in a script with a plan", | ||
| "success", | ||
| None, | ||
| False, | ||
| None, | ||
| None, | ||
| True, | ||
| None, | ||
| None, | ||
| None, | ||
| ), | ||
| ("status", "test 2 - second", "fail", None, False, None, None, True, None, None, None), | ||
| ("status", "test 3 - third", "success", None, False, None, None, True, None, None, None), | ||
| ("status", "test 4 - fourth", "fail", None, False, None, None, True, None, None, None), | ||
| ] | ||
| ) | ||
| def test_four_tests_in_a_row_no_plan(self): | ||
| # A file | ||
| # ok 1 - first test in a script with no plan at all | ||
| # not ok 2 - second | ||
| # ok 3 - third | ||
| # not ok 4 - fourth | ||
| # results in four tests numbered and named | ||
| self.tap.write("ok 1 - first test in a script with no plan at all\n") | ||
| self.tap.write("not ok 2 - second\n") | ||
| self.tap.write("ok 3 - third\n") | ||
| self.tap.write("not ok 4 - fourth\n") | ||
| self.tap.seek(0) | ||
| result = subunit.TAP2SubUnit(self.tap, self.subunit) | ||
| self.assertEqual(0, result) | ||
| self.check_events( | ||
| [ | ||
| ( | ||
| "status", | ||
| "test 1 - first test in a script with no plan at all", | ||
| "success", | ||
| None, | ||
| False, | ||
| None, | ||
| None, | ||
| True, | ||
| None, | ||
| None, | ||
| None, | ||
| ), | ||
| ("status", "test 2 - second", "fail", None, False, None, None, True, None, None, None), | ||
| ("status", "test 3 - third", "success", None, False, None, None, True, None, None, None), | ||
| ("status", "test 4 - fourth", "fail", None, False, None, None, True, None, None, None), | ||
| ] | ||
| ) | ||
| def test_todo_and_skip(self): | ||
| # A file | ||
| # not ok 1 - a fail but # TODO but is TODO | ||
| # not ok 2 - another fail # SKIP instead | ||
| # results in two tests, numbered and commented. | ||
| self.tap.write("not ok 1 - a fail but # TODO but is TODO\n") | ||
| self.tap.write("not ok 2 - another fail # SKIP instead\n") | ||
| self.tap.seek(0) | ||
| result = subunit.TAP2SubUnit(self.tap, self.subunit) | ||
| self.assertEqual(0, result) | ||
| self.subunit.seek(0) | ||
| events = StreamResult() | ||
| subunit.ByteStreamToStreamResult(self.subunit).run(events) | ||
| self.check_events( | ||
| [ | ||
| ( | ||
| "status", | ||
| "test 1 - a fail but", | ||
| "xfail", | ||
| None, | ||
| False, | ||
| "tap comment", | ||
| b"but is TODO", | ||
| True, | ||
| "text/plain; charset=UTF8", | ||
| None, | ||
| None, | ||
| ), | ||
| ( | ||
| "status", | ||
| "test 2 - another fail", | ||
| "skip", | ||
| None, | ||
| False, | ||
| "tap comment", | ||
| b"instead", | ||
| True, | ||
| "text/plain; charset=UTF8", | ||
| None, | ||
| None, | ||
| ), | ||
| ] | ||
| ) | ||
| def test_leading_comments_add_to_next_test_log(self): | ||
| # A file | ||
| # # comment | ||
| # ok | ||
| # ok | ||
| # results in a single test with the comment included | ||
| # in the first test and not the second. | ||
| self.tap.write("# comment\n") | ||
| self.tap.write("ok\n") | ||
| self.tap.write("ok\n") | ||
| self.tap.seek(0) | ||
| result = subunit.TAP2SubUnit(self.tap, self.subunit) | ||
| self.assertEqual(0, result) | ||
| self.check_events( | ||
| [ | ||
| ( | ||
| "status", | ||
| "test 1", | ||
| "success", | ||
| None, | ||
| False, | ||
| "tap comment", | ||
| b"# comment", | ||
| True, | ||
| "text/plain; charset=UTF8", | ||
| None, | ||
| None, | ||
| ), | ||
| ("status", "test 2", "success", None, False, None, None, True, None, None, None), | ||
| ] | ||
| ) | ||
| def test_trailing_comments_are_included_in_last_test_log(self): | ||
| # A file | ||
| # ok foo | ||
| # ok foo | ||
| # # comment | ||
| # results in a two tests, with the second having the comment | ||
| # attached to its log. | ||
| self.tap.write("ok\n") | ||
| self.tap.write("ok\n") | ||
| self.tap.write("# comment\n") | ||
| self.tap.seek(0) | ||
| result = subunit.TAP2SubUnit(self.tap, self.subunit) | ||
| self.assertEqual(0, result) | ||
| self.check_events( | ||
| [ | ||
| ("status", "test 1", "success", None, False, None, None, True, None, None, None), | ||
| ( | ||
| "status", | ||
| "test 2", | ||
| "success", | ||
| None, | ||
| False, | ||
| "tap comment", | ||
| b"# comment", | ||
| True, | ||
| "text/plain; charset=UTF8", | ||
| None, | ||
| None, | ||
| ), | ||
| ] | ||
| ) | ||
| def check_events(self, events): | ||
| self.subunit.seek(0) | ||
| eventstream = StreamResult() | ||
| subunit.ByteStreamToStreamResult(self.subunit).run(eventstream) | ||
| self.assertEqual(events, eventstream._events) |
| # | ||
| # subunit: extensions to Python unittest to get test results from subprocesses. | ||
| # Copyright (C) 2005 Robert Collins <robertc@robertcollins.net> | ||
| # | ||
| # 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. | ||
| # | ||
| import datetime | ||
| import io | ||
| import os | ||
| import tempfile | ||
| import unittest | ||
| from io import BytesIO | ||
| from testtools import PlaceHolder, TestCase, TestResult, skipIf | ||
| from testtools.content import Content, TracebackContent, text_content | ||
| from testtools.content_type import ContentType | ||
| from testtools.testresult.doubles import ExtendedTestResult | ||
| from testtools.matchers import Contains, Equals, MatchesAny | ||
| import iso8601 | ||
| import subunit | ||
| from . import ( | ||
| _remote_exception_repr, | ||
| _remote_exception_repr_chunked, | ||
| _remote_exception_str, | ||
| _remote_exception_str_chunked, | ||
| ) | ||
| tb_prelude = "Traceback (most recent call last):\n" | ||
| def details_to_str(details): | ||
| return TestResult()._err_details_to_string(None, details=details) | ||
| class TestHelpers(TestCase): | ||
| def test__unwrap_text_file_read_mode(self): | ||
| fd, file_path = tempfile.mkstemp() | ||
| self.addCleanup(os.remove, file_path) | ||
| fake_file = os.fdopen(fd, "r") | ||
| self.assertEqual(fake_file.buffer, subunit._unwrap_text(fake_file)) | ||
| def test__unwrap_text_file_write_mode(self): | ||
| fd, file_path = tempfile.mkstemp() | ||
| self.addCleanup(os.remove, file_path) | ||
| fake_file = os.fdopen(fd, "w") | ||
| self.assertEqual(fake_file.buffer, subunit._unwrap_text(fake_file)) | ||
| def test__unwrap_text_fileIO_read_mode(self): | ||
| fd, file_path = tempfile.mkstemp() | ||
| self.addCleanup(os.remove, file_path) | ||
| fake_file = io.FileIO(file_path, "r") | ||
| self.assertEqual(fake_file, subunit._unwrap_text(fake_file)) | ||
| def test__unwrap_text_fileIO_write_mode(self): | ||
| fd, file_path = tempfile.mkstemp() | ||
| self.addCleanup(os.remove, file_path) | ||
| fake_file = io.FileIO(file_path, "w") | ||
| self.assertEqual(fake_file, subunit._unwrap_text(fake_file)) | ||
| def test__unwrap_text_BytesIO(self): | ||
| fake_stream = io.BytesIO() | ||
| self.assertEqual(fake_stream, subunit._unwrap_text(fake_stream)) | ||
| class TestTestImports(unittest.TestCase): | ||
| def test_imports(self): | ||
| from subunit import ( # noqa: F401 | ||
| DiscardStream, | ||
| ExecTestCase, | ||
| IsolatedTestCase, | ||
| ProtocolTestCase, | ||
| RemotedTestCase, | ||
| RemoteError, | ||
| TestProtocolClient, | ||
| TestProtocolServer, | ||
| ) # noqa: F401 | ||
| class TestDiscardStream(unittest.TestCase): | ||
| def test_write(self): | ||
| subunit.DiscardStream().write("content") | ||
| class TestProtocolServerForward(unittest.TestCase): | ||
| def test_story(self): | ||
| client = unittest.TestResult() | ||
| out = BytesIO() | ||
| protocol = subunit.TestProtocolServer(client, forward_stream=out) | ||
| pipe = BytesIO(b"test old mcdonald\nsuccess old mcdonald\n") | ||
| protocol.readFrom(pipe) | ||
| self.assertEqual(client.testsRun, 1) | ||
| self.assertEqual(pipe.getvalue(), out.getvalue()) | ||
| def test_not_command(self): | ||
| client = unittest.TestResult() | ||
| out = BytesIO() | ||
| protocol = subunit.TestProtocolServer(client, stream=subunit.DiscardStream(), forward_stream=out) | ||
| pipe = BytesIO(b"success old mcdonald\n") | ||
| protocol.readFrom(pipe) | ||
| self.assertEqual(client.testsRun, 0) | ||
| self.assertEqual(b"", out.getvalue()) | ||
| class TestTestProtocolServerPipe(unittest.TestCase): | ||
| def test_story(self): | ||
| client = unittest.TestResult() | ||
| protocol = subunit.TestProtocolServer(client) | ||
| traceback = "foo.c:53:ERROR invalid state\n" | ||
| pipe = BytesIO( | ||
| b"test old mcdonald\n" | ||
| b"success old mcdonald\n" | ||
| b"test bing crosby\n" | ||
| b"failure bing crosby [\n" + traceback.encode() + b"]\n" | ||
| b"test an error\n" | ||
| b"error an error\n" | ||
| ) | ||
| protocol.readFrom(pipe) | ||
| bing = subunit.RemotedTestCase("bing crosby") | ||
| an_error = subunit.RemotedTestCase("an error") | ||
| self.assertEqual( | ||
| client.errors, | ||
| [(an_error, _remote_exception_repr + "\n")], | ||
| ) | ||
| self.assertEqual( | ||
| client.failures, | ||
| [(bing, _remote_exception_repr + ": " + details_to_str({"traceback": text_content(traceback)}) + "\n")], | ||
| ) | ||
| self.assertEqual(client.testsRun, 3) | ||
| def test_non_test_characters_forwarded_immediately(self): | ||
| pass | ||
| class TestTestProtocolServerStartTest(unittest.TestCase): | ||
| def setUp(self): | ||
| self.client = ExtendedTestResult() | ||
| self.stream = BytesIO() | ||
| self.protocol = subunit.TestProtocolServer(self.client, self.stream) | ||
| def test_start_test(self): | ||
| self.protocol.lineReceived(b"test old mcdonald\n") | ||
| self.assertEqual(self.client._events, [("startTest", subunit.RemotedTestCase("old mcdonald"))]) | ||
| def test_start_testing(self): | ||
| self.protocol.lineReceived(b"testing old mcdonald\n") | ||
| self.assertEqual(self.client._events, [("startTest", subunit.RemotedTestCase("old mcdonald"))]) | ||
| def test_start_test_colon(self): | ||
| self.protocol.lineReceived(b"test: old mcdonald\n") | ||
| self.assertEqual(self.client._events, [("startTest", subunit.RemotedTestCase("old mcdonald"))]) | ||
| def test_indented_test_colon_ignored(self): | ||
| ignored_line = b" test: old mcdonald\n" | ||
| self.protocol.lineReceived(ignored_line) | ||
| self.assertEqual([], self.client._events) | ||
| self.assertEqual(self.stream.getvalue(), ignored_line) | ||
| def test_start_testing_colon(self): | ||
| self.protocol.lineReceived(b"testing: old mcdonald\n") | ||
| self.assertEqual(self.client._events, [("startTest", subunit.RemotedTestCase("old mcdonald"))]) | ||
| class TestTestProtocolServerPassThrough(unittest.TestCase): | ||
| def setUp(self): | ||
| self.stdout = BytesIO() | ||
| self.test = subunit.RemotedTestCase("old mcdonald") | ||
| self.client = ExtendedTestResult() | ||
| self.protocol = subunit.TestProtocolServer(self.client, self.stdout) | ||
| def keywords_before_test(self): | ||
| self.protocol.lineReceived(b"failure a\n") | ||
| self.protocol.lineReceived(b"failure: a\n") | ||
| self.protocol.lineReceived(b"error a\n") | ||
| self.protocol.lineReceived(b"error: a\n") | ||
| self.protocol.lineReceived(b"success a\n") | ||
| self.protocol.lineReceived(b"success: a\n") | ||
| self.protocol.lineReceived(b"successful a\n") | ||
| self.protocol.lineReceived(b"successful: a\n") | ||
| self.protocol.lineReceived(b"]\n") | ||
| self.assertEqual( | ||
| self.stdout.getvalue(), | ||
| b"failure a\nfailure: a\nerror a\nerror: a\nsuccess a\nsuccess: a\nsuccessful a\nsuccessful: a\n]\n", | ||
| ) | ||
| def test_keywords_before_test(self): | ||
| self.keywords_before_test() | ||
| self.assertEqual(self.client._events, []) | ||
| def test_keywords_after_error(self): | ||
| self.protocol.lineReceived(b"test old mcdonald\n") | ||
| self.protocol.lineReceived(b"error old mcdonald\n") | ||
| self.keywords_before_test() | ||
| self.assertEqual( | ||
| [ | ||
| ("startTest", self.test), | ||
| ("addError", self.test, {}), | ||
| ("stopTest", self.test), | ||
| ], | ||
| self.client._events, | ||
| ) | ||
| def test_keywords_after_failure(self): | ||
| self.protocol.lineReceived(b"test old mcdonald\n") | ||
| self.protocol.lineReceived(b"failure old mcdonald\n") | ||
| self.keywords_before_test() | ||
| self.assertEqual( | ||
| self.client._events, | ||
| [ | ||
| ("startTest", self.test), | ||
| ("addFailure", self.test, {}), | ||
| ("stopTest", self.test), | ||
| ], | ||
| ) | ||
| def test_keywords_after_success(self): | ||
| self.protocol.lineReceived(b"test old mcdonald\n") | ||
| self.protocol.lineReceived(b"success old mcdonald\n") | ||
| self.keywords_before_test() | ||
| self.assertEqual( | ||
| [ | ||
| ("startTest", self.test), | ||
| ("addSuccess", self.test), | ||
| ("stopTest", self.test), | ||
| ], | ||
| self.client._events, | ||
| ) | ||
| def test_keywords_after_test(self): | ||
| self.protocol.lineReceived(b"test old mcdonald\n") | ||
| self.protocol.lineReceived(b"test old mcdonald\n") | ||
| self.protocol.lineReceived(b"failure a\n") | ||
| self.protocol.lineReceived(b"failure: a\n") | ||
| self.protocol.lineReceived(b"error a\n") | ||
| self.protocol.lineReceived(b"error: a\n") | ||
| self.protocol.lineReceived(b"success a\n") | ||
| self.protocol.lineReceived(b"success: a\n") | ||
| self.protocol.lineReceived(b"successful a\n") | ||
| self.protocol.lineReceived(b"successful: a\n") | ||
| self.protocol.lineReceived(b"]\n") | ||
| self.protocol.lineReceived(b"failure old mcdonald\n") | ||
| self.assertEqual( | ||
| self.stdout.getvalue(), | ||
| b"test old mcdonald\n" | ||
| b"failure a\n" | ||
| b"failure: a\n" | ||
| b"error a\n" | ||
| b"error: a\n" | ||
| b"success a\n" | ||
| b"success: a\n" | ||
| b"successful a\n" | ||
| b"successful: a\n" | ||
| b"]\n", | ||
| ) | ||
| self.assertEqual( | ||
| self.client._events, | ||
| [ | ||
| ("startTest", self.test), | ||
| ("addFailure", self.test, {}), | ||
| ("stopTest", self.test), | ||
| ], | ||
| ) | ||
| def test_keywords_during_failure(self): | ||
| # A smoke test to make sure that the details parsers have control | ||
| # appropriately. | ||
| self.protocol.lineReceived(b"test old mcdonald\n") | ||
| self.protocol.lineReceived(b"failure: old mcdonald [\n") | ||
| self.protocol.lineReceived(b"test old mcdonald\n") | ||
| self.protocol.lineReceived(b"failure a\n") | ||
| self.protocol.lineReceived(b"failure: a\n") | ||
| self.protocol.lineReceived(b"error a\n") | ||
| self.protocol.lineReceived(b"error: a\n") | ||
| self.protocol.lineReceived(b"success a\n") | ||
| self.protocol.lineReceived(b"success: a\n") | ||
| self.protocol.lineReceived(b"successful a\n") | ||
| self.protocol.lineReceived(b"successful: a\n") | ||
| self.protocol.lineReceived(b" ]\n") | ||
| self.protocol.lineReceived(b"]\n") | ||
| self.assertEqual(self.stdout.getvalue(), b"") | ||
| details = {} | ||
| details["traceback"] = Content( | ||
| ContentType("text", "x-traceback", {"charset": "utf8"}), | ||
| lambda: [ | ||
| b"test old mcdonald\n" | ||
| b"failure a\n" | ||
| b"failure: a\n" | ||
| b"error a\n" | ||
| b"error: a\n" | ||
| b"success a\n" | ||
| b"success: a\n" | ||
| b"successful a\n" | ||
| b"successful: a\n" | ||
| b"]\n" | ||
| ], | ||
| ) | ||
| self.assertEqual( | ||
| self.client._events, | ||
| [ | ||
| ("startTest", self.test), | ||
| ("addFailure", self.test, details), | ||
| ("stopTest", self.test), | ||
| ], | ||
| ) | ||
| def test_stdout_passthrough(self): | ||
| """Lines received which cannot be interpreted as any protocol action | ||
| should be passed through to sys.stdout. | ||
| """ | ||
| bytes = b"randombytes\n" | ||
| self.protocol.lineReceived(bytes) | ||
| self.assertEqual(self.stdout.getvalue(), bytes) | ||
| class TestTestProtocolServerLostConnection(unittest.TestCase): | ||
| def setUp(self): | ||
| self.client = ExtendedTestResult() | ||
| self.protocol = subunit.TestProtocolServer(self.client) | ||
| self.test = subunit.RemotedTestCase("old mcdonald") | ||
| def test_lost_connection_no_input(self): | ||
| self.protocol.lostConnection() | ||
| self.assertEqual([], self.client._events) | ||
| def test_lost_connection_after_start(self): | ||
| self.protocol.lineReceived(b"test old mcdonald\n") | ||
| self.protocol.lostConnection() | ||
| failure = subunit.RemoteError("lost connection during test 'old mcdonald'") | ||
| self.assertEqual( | ||
| [ | ||
| ("startTest", self.test), | ||
| ("addError", self.test, failure), | ||
| ("stopTest", self.test), | ||
| ], | ||
| self.client._events, | ||
| ) | ||
| def test_lost_connected_after_error(self): | ||
| self.protocol.lineReceived(b"test old mcdonald\n") | ||
| self.protocol.lineReceived(b"error old mcdonald\n") | ||
| self.protocol.lostConnection() | ||
| self.assertEqual( | ||
| [ | ||
| ("startTest", self.test), | ||
| ("addError", self.test, {}), | ||
| ("stopTest", self.test), | ||
| ], | ||
| self.client._events, | ||
| ) | ||
| def do_connection_lost(self, outcome, opening): | ||
| self.protocol.lineReceived(b"test old mcdonald\n") | ||
| self.protocol.lineReceived("{} old mcdonald {}".format(outcome, opening).encode()) | ||
| self.protocol.lostConnection() | ||
| failure = subunit.RemoteError("lost connection during %s report of test 'old mcdonald'" % outcome) | ||
| self.assertEqual( | ||
| [ | ||
| ("startTest", self.test), | ||
| ("addError", self.test, failure), | ||
| ("stopTest", self.test), | ||
| ], | ||
| self.client._events, | ||
| ) | ||
| def test_lost_connection_during_error(self): | ||
| self.do_connection_lost("error", "[\n") | ||
| def test_lost_connection_during_error_details(self): | ||
| self.do_connection_lost("error", "[ multipart\n") | ||
| def test_lost_connected_after_failure(self): | ||
| self.protocol.lineReceived(b"test old mcdonald\n") | ||
| self.protocol.lineReceived(b"failure old mcdonald\n") | ||
| self.protocol.lostConnection() | ||
| self.assertEqual( | ||
| [ | ||
| ("startTest", self.test), | ||
| ("addFailure", self.test, {}), | ||
| ("stopTest", self.test), | ||
| ], | ||
| self.client._events, | ||
| ) | ||
| def test_lost_connection_during_failure(self): | ||
| self.do_connection_lost("failure", "[\n") | ||
| def test_lost_connection_during_failure_details(self): | ||
| self.do_connection_lost("failure", "[ multipart\n") | ||
| def test_lost_connection_after_success(self): | ||
| self.protocol.lineReceived(b"test old mcdonald\n") | ||
| self.protocol.lineReceived(b"success old mcdonald\n") | ||
| self.protocol.lostConnection() | ||
| self.assertEqual( | ||
| [ | ||
| ("startTest", self.test), | ||
| ("addSuccess", self.test), | ||
| ("stopTest", self.test), | ||
| ], | ||
| self.client._events, | ||
| ) | ||
| def test_lost_connection_during_success(self): | ||
| self.do_connection_lost("success", "[\n") | ||
| def test_lost_connection_during_success_details(self): | ||
| self.do_connection_lost("success", "[ multipart\n") | ||
| def test_lost_connection_during_skip(self): | ||
| self.do_connection_lost("skip", "[\n") | ||
| def test_lost_connection_during_skip_details(self): | ||
| self.do_connection_lost("skip", "[ multipart\n") | ||
| def test_lost_connection_during_xfail(self): | ||
| self.do_connection_lost("xfail", "[\n") | ||
| def test_lost_connection_during_xfail_details(self): | ||
| self.do_connection_lost("xfail", "[ multipart\n") | ||
| def test_lost_connection_during_uxsuccess(self): | ||
| self.do_connection_lost("uxsuccess", "[\n") | ||
| def test_lost_connection_during_uxsuccess_details(self): | ||
| self.do_connection_lost("uxsuccess", "[ multipart\n") | ||
| class TestInTestMultipart(unittest.TestCase): | ||
| def setUp(self): | ||
| self.client = ExtendedTestResult() | ||
| self.protocol = subunit.TestProtocolServer(self.client) | ||
| self.protocol.lineReceived(b"test mcdonalds farm\n") | ||
| self.test = subunit.RemotedTestCase("mcdonalds farm") | ||
| def test__outcome_sets_details_parser(self): | ||
| self.protocol._reading_success_details.details_parser = None | ||
| self.protocol._state._outcome(0, b"mcdonalds farm [ multipart\n", None, self.protocol._reading_success_details) | ||
| parser = self.protocol._reading_success_details.details_parser | ||
| self.assertNotEqual(None, parser) | ||
| self.assertIsInstance(parser, subunit.details.MultipartDetailsParser) | ||
| class TestTestProtocolServerAddError(unittest.TestCase): | ||
| def setUp(self): | ||
| self.client = ExtendedTestResult() | ||
| self.protocol = subunit.TestProtocolServer(self.client) | ||
| self.protocol.lineReceived(b"test mcdonalds farm\n") | ||
| self.test = subunit.RemotedTestCase("mcdonalds farm") | ||
| def simple_error_keyword(self, keyword): | ||
| self.protocol.lineReceived(("%s mcdonalds farm\n" % keyword).encode()) | ||
| details = {} | ||
| self.assertEqual( | ||
| [ | ||
| ("startTest", self.test), | ||
| ("addError", self.test, details), | ||
| ("stopTest", self.test), | ||
| ], | ||
| self.client._events, | ||
| ) | ||
| def test_simple_error(self): | ||
| self.simple_error_keyword("error") | ||
| def test_simple_error_colon(self): | ||
| self.simple_error_keyword("error:") | ||
| def test_error_empty_message(self): | ||
| self.protocol.lineReceived(b"error mcdonalds farm [\n") | ||
| self.protocol.lineReceived(b"]\n") | ||
| details = {} | ||
| details["traceback"] = Content(ContentType("text", "x-traceback", {"charset": "utf8"}), lambda: [b""]) | ||
| self.assertEqual( | ||
| [ | ||
| ("startTest", self.test), | ||
| ("addError", self.test, details), | ||
| ("stopTest", self.test), | ||
| ], | ||
| self.client._events, | ||
| ) | ||
| def error_quoted_bracket(self, keyword): | ||
| self.protocol.lineReceived(("%s mcdonalds farm [\n" % keyword).encode()) | ||
| self.protocol.lineReceived(b" ]\n") | ||
| self.protocol.lineReceived(b"]\n") | ||
| details = {} | ||
| details["traceback"] = Content(ContentType("text", "x-traceback", {"charset": "utf8"}), lambda: [b"]\n"]) | ||
| self.assertEqual( | ||
| [ | ||
| ("startTest", self.test), | ||
| ("addError", self.test, details), | ||
| ("stopTest", self.test), | ||
| ], | ||
| self.client._events, | ||
| ) | ||
| def test_error_quoted_bracket(self): | ||
| self.error_quoted_bracket("error") | ||
| def test_error_colon_quoted_bracket(self): | ||
| self.error_quoted_bracket("error:") | ||
| class TestTestProtocolServerAddFailure(unittest.TestCase): | ||
| def setUp(self): | ||
| self.client = ExtendedTestResult() | ||
| self.protocol = subunit.TestProtocolServer(self.client) | ||
| self.protocol.lineReceived(b"test mcdonalds farm\n") | ||
| self.test = subunit.RemotedTestCase("mcdonalds farm") | ||
| def assertFailure(self, details): | ||
| self.assertEqual( | ||
| [ | ||
| ("startTest", self.test), | ||
| ("addFailure", self.test, details), | ||
| ("stopTest", self.test), | ||
| ], | ||
| self.client._events, | ||
| ) | ||
| def simple_failure_keyword(self, keyword): | ||
| self.protocol.lineReceived(("%s mcdonalds farm\n" % keyword).encode()) | ||
| details = {} | ||
| self.assertFailure(details) | ||
| def test_simple_failure(self): | ||
| self.simple_failure_keyword("failure") | ||
| def test_simple_failure_colon(self): | ||
| self.simple_failure_keyword("failure:") | ||
| def test_failure_empty_message(self): | ||
| self.protocol.lineReceived(b"failure mcdonalds farm [\n") | ||
| self.protocol.lineReceived(b"]\n") | ||
| details = {} | ||
| details["traceback"] = Content(ContentType("text", "x-traceback", {"charset": "utf8"}), lambda: [b""]) | ||
| self.assertFailure(details) | ||
| def failure_quoted_bracket(self, keyword): | ||
| self.protocol.lineReceived(("%s mcdonalds farm [\n" % keyword).encode()) | ||
| self.protocol.lineReceived(b" ]\n") | ||
| self.protocol.lineReceived(b"]\n") | ||
| details = {} | ||
| details["traceback"] = Content(ContentType("text", "x-traceback", {"charset": "utf8"}), lambda: [b"]\n"]) | ||
| self.assertFailure(details) | ||
| def test_failure_quoted_bracket(self): | ||
| self.failure_quoted_bracket("failure") | ||
| def test_failure_colon_quoted_bracket(self): | ||
| self.failure_quoted_bracket("failure:") | ||
| class TestTestProtocolServerAddxFail(unittest.TestCase): | ||
| """Tests for the xfail keyword.""" | ||
| def setUp(self): | ||
| """Setup a test object ready to be xfailed with details.""" | ||
| self.client = ExtendedTestResult() | ||
| self.protocol = subunit.TestProtocolServer(self.client) | ||
| self.protocol.lineReceived(b"test mcdonalds farm\n") | ||
| self.test = self.client._events[-1][-1] | ||
| def simple_xfail_keyword(self, keyword): | ||
| self.protocol.lineReceived(("%s mcdonalds farm\n" % keyword).encode()) | ||
| self.check_xfail() | ||
| def check_xfail(self, error_message=None): | ||
| details = {} | ||
| if error_message is not None: | ||
| details["traceback"] = Content( | ||
| ContentType("text", "x-traceback", {"charset": "utf8"}), lambda: [error_message.encode()] | ||
| ) | ||
| self.assertEqual( | ||
| [ | ||
| ("startTest", self.test), | ||
| ("addExpectedFailure", self.test, details), | ||
| ("stopTest", self.test), | ||
| ], | ||
| self.client._events, | ||
| ) | ||
| def test_simple_xfail(self): | ||
| self.simple_xfail_keyword("xfail") | ||
| def test_simple_xfail_colon(self): | ||
| self.simple_xfail_keyword("xfail:") | ||
| def test_xfail_empty_message(self): | ||
| self.protocol.lineReceived(b"xfail mcdonalds farm [\n") | ||
| self.protocol.lineReceived(b"]\n") | ||
| self.check_xfail(error_message="") | ||
| def test_xfail_quoted_bracket(self): | ||
| self.protocol.lineReceived(b"xfail mcdonalds farm [\n") | ||
| self.protocol.lineReceived(b" ]\n") | ||
| self.protocol.lineReceived(b"]\n") | ||
| self.check_xfail("]\n") | ||
| def test_xfail_colon_quoted_bracket(self): | ||
| self.protocol.lineReceived(b"xfail: mcdonalds farm [\n") | ||
| self.protocol.lineReceived(b" ]\n") | ||
| self.protocol.lineReceived(b"]\n") | ||
| self.check_xfail("]\n") | ||
| class TestTestProtocolServerAddunexpectedSuccess(TestCase): | ||
| """Tests for the uxsuccess keyword.""" | ||
| def setUp(self): | ||
| """Setup a test object ready for uxsuccess with details.""" | ||
| super().setUp() | ||
| self.client = ExtendedTestResult() | ||
| self.protocol = subunit.TestProtocolServer(self.client) | ||
| self.protocol.lineReceived(b"test mcdonalds farm\n") | ||
| self.test = self.client._events[-1][-1] | ||
| def simple_uxsuccess_keyword(self, keyword): | ||
| self.protocol.lineReceived(("%s mcdonalds farm\n" % keyword).encode()) | ||
| self.check_uxsuccess() | ||
| def check_uxsuccess(self, error_message=None): | ||
| if error_message is not None: | ||
| details = {} | ||
| details["traceback"] = Content( | ||
| ContentType("text", "x-traceback", {"charset": "utf8"}), lambda: [error_message.encode()] | ||
| ) | ||
| expected_events = [ | ||
| ("startTest", self.test), | ||
| ("addUnexpectedSuccess", self.test, details), | ||
| ("stopTest", self.test), | ||
| ] | ||
| else: | ||
| expected_events = [ | ||
| ("startTest", self.test), | ||
| ("addUnexpectedSuccess", self.test), | ||
| ("stopTest", self.test), | ||
| ] | ||
| self.assertEqual(expected_events, self.client._events) | ||
| def test_simple_uxsuccess(self): | ||
| self.simple_uxsuccess_keyword("uxsuccess") | ||
| def test_simple_uxsuccess_colon(self): | ||
| self.simple_uxsuccess_keyword("uxsuccess:") | ||
| def test_uxsuccess_empty_message(self): | ||
| self.protocol.lineReceived(b"uxsuccess mcdonalds farm [\n") | ||
| self.protocol.lineReceived(b"]\n") | ||
| self.check_uxsuccess(error_message="") | ||
| def test_uxsuccess_quoted_bracket(self): | ||
| self.protocol.lineReceived(b"uxsuccess mcdonalds farm [\n") | ||
| self.protocol.lineReceived(b" ]\n") | ||
| self.protocol.lineReceived(b"]\n") | ||
| self.check_uxsuccess("]\n") | ||
| def test_uxsuccess_colon_quoted_bracket(self): | ||
| self.protocol.lineReceived(b"uxsuccess: mcdonalds farm [\n") | ||
| self.protocol.lineReceived(b" ]\n") | ||
| self.protocol.lineReceived(b"]\n") | ||
| self.check_uxsuccess("]\n") | ||
| class TestTestProtocolServerAddSkip(unittest.TestCase): | ||
| """Tests for the skip keyword. | ||
| In Python this meets the testtools extended TestResult contract. | ||
| (See https://launchpad.net/testtools). | ||
| """ | ||
| def setUp(self): | ||
| """Setup a test object ready to be skipped.""" | ||
| self.client = ExtendedTestResult() | ||
| self.protocol = subunit.TestProtocolServer(self.client) | ||
| self.protocol.lineReceived(b"test mcdonalds farm\n") | ||
| self.test = self.client._events[-1][-1] | ||
| def assertSkip(self, reason): | ||
| details = {} | ||
| if reason is not None: | ||
| details["reason"] = Content(ContentType("text", "plain"), lambda: [reason]) | ||
| self.assertEqual( | ||
| [ | ||
| ("startTest", self.test), | ||
| ("addSkip", self.test, details), | ||
| ("stopTest", self.test), | ||
| ], | ||
| self.client._events, | ||
| ) | ||
| def simple_skip_keyword(self, keyword): | ||
| self.protocol.lineReceived(("%s mcdonalds farm\n" % keyword).encode()) | ||
| self.assertSkip(None) | ||
| def test_simple_skip(self): | ||
| self.simple_skip_keyword("skip") | ||
| def test_simple_skip_colon(self): | ||
| self.simple_skip_keyword("skip:") | ||
| def test_skip_empty_message(self): | ||
| self.protocol.lineReceived(b"skip mcdonalds farm [\n") | ||
| self.protocol.lineReceived(b"]\n") | ||
| self.assertSkip(b"") | ||
| def skip_quoted_bracket(self, keyword): | ||
| # This tests it is accepted, but cannot test it is used today, because | ||
| # of not having a way to expose it in Python so far. | ||
| self.protocol.lineReceived(("%s mcdonalds farm [\n" % keyword).encode()) | ||
| self.protocol.lineReceived(b" ]\n") | ||
| self.protocol.lineReceived(b"]\n") | ||
| self.assertSkip(b"]\n") | ||
| def test_skip_quoted_bracket(self): | ||
| self.skip_quoted_bracket("skip") | ||
| def test_skip_colon_quoted_bracket(self): | ||
| self.skip_quoted_bracket("skip:") | ||
| class TestTestProtocolServerAddSuccess(unittest.TestCase): | ||
| def setUp(self): | ||
| self.client = ExtendedTestResult() | ||
| self.protocol = subunit.TestProtocolServer(self.client) | ||
| self.protocol.lineReceived(b"test mcdonalds farm\n") | ||
| self.test = subunit.RemotedTestCase("mcdonalds farm") | ||
| def simple_success_keyword(self, keyword): | ||
| self.protocol.lineReceived(("%s mcdonalds farm\n" % keyword).encode()) | ||
| self.assertEqual( | ||
| [ | ||
| ("startTest", self.test), | ||
| ("addSuccess", self.test), | ||
| ("stopTest", self.test), | ||
| ], | ||
| self.client._events, | ||
| ) | ||
| def test_simple_success(self): | ||
| self.simple_success_keyword("successful") | ||
| def test_simple_success_colon(self): | ||
| self.simple_success_keyword("successful:") | ||
| def assertSuccess(self, details): | ||
| self.assertEqual( | ||
| [ | ||
| ("startTest", self.test), | ||
| ("addSuccess", self.test, details), | ||
| ("stopTest", self.test), | ||
| ], | ||
| self.client._events, | ||
| ) | ||
| def test_success_empty_message(self): | ||
| self.protocol.lineReceived(b"success mcdonalds farm [\n") | ||
| self.protocol.lineReceived(b"]\n") | ||
| details = {} | ||
| details["message"] = Content(ContentType("text", "plain"), lambda: [b""]) | ||
| self.assertSuccess(details) | ||
| def success_quoted_bracket(self, keyword): | ||
| # This tests it is accepted, but cannot test it is used today, because | ||
| # of not having a way to expose it in Python so far. | ||
| self.protocol.lineReceived(("%s mcdonalds farm [\n" % keyword).encode()) | ||
| self.protocol.lineReceived(b" ]\n") | ||
| self.protocol.lineReceived(b"]\n") | ||
| details = {} | ||
| details["message"] = Content(ContentType("text", "plain"), lambda: [b"]\n"]) | ||
| self.assertSuccess(details) | ||
| def test_success_quoted_bracket(self): | ||
| self.success_quoted_bracket("success") | ||
| def test_success_colon_quoted_bracket(self): | ||
| self.success_quoted_bracket("success:") | ||
| class TestTestProtocolServerProgress(unittest.TestCase): | ||
| """Test receipt of progress: directives.""" | ||
| def test_progress_accepted_stdlib(self): | ||
| self.result = ExtendedTestResult() | ||
| self.stream = BytesIO() | ||
| self.protocol = subunit.TestProtocolServer(self.result, stream=self.stream) | ||
| self.protocol.lineReceived(b"progress: 23") | ||
| self.protocol.lineReceived(b"progress: -2") | ||
| self.protocol.lineReceived(b"progress: +4") | ||
| self.assertEqual(b"", self.stream.getvalue()) | ||
| def test_progress_accepted_extended(self): | ||
| # With a progress capable TestResult, progress events are emitted. | ||
| self.result = ExtendedTestResult() | ||
| self.stream = BytesIO() | ||
| self.protocol = subunit.TestProtocolServer(self.result, stream=self.stream) | ||
| self.protocol.lineReceived(b"progress: 23") | ||
| self.protocol.lineReceived(b"progress: push") | ||
| self.protocol.lineReceived(b"progress: -2") | ||
| self.protocol.lineReceived(b"progress: pop") | ||
| self.protocol.lineReceived(b"progress: +4") | ||
| self.assertEqual(b"", self.stream.getvalue()) | ||
| self.assertEqual( | ||
| [ | ||
| ("progress", 23, subunit.PROGRESS_SET), | ||
| ("progress", None, subunit.PROGRESS_PUSH), | ||
| ("progress", -2, subunit.PROGRESS_CUR), | ||
| ("progress", None, subunit.PROGRESS_POP), | ||
| ("progress", 4, subunit.PROGRESS_CUR), | ||
| ], | ||
| self.result._events, | ||
| ) | ||
| class TestTestProtocolServerStreamTags(unittest.TestCase): | ||
| """Test managing tags on the protocol level.""" | ||
| def setUp(self): | ||
| self.client = ExtendedTestResult() | ||
| self.protocol = subunit.TestProtocolServer(self.client) | ||
| def test_initial_tags(self): | ||
| self.protocol.lineReceived(b"tags: foo bar:baz quux\n") | ||
| self.assertEqual( | ||
| [ | ||
| ("tags", {"foo", "bar:baz", "quux"}, set()), | ||
| ], | ||
| self.client._events, | ||
| ) | ||
| def test_minus_removes_tags(self): | ||
| self.protocol.lineReceived(b"tags: -bar quux\n") | ||
| self.assertEqual( | ||
| [ | ||
| ("tags", {"quux"}, {"bar"}), | ||
| ], | ||
| self.client._events, | ||
| ) | ||
| def test_tags_do_not_get_set_on_test(self): | ||
| self.protocol.lineReceived(b"test mcdonalds farm\n") | ||
| test = self.client._events[0][-1] | ||
| self.assertEqual(None, getattr(test, "tags", None)) | ||
| def test_tags_do_not_get_set_on_global_tags(self): | ||
| self.protocol.lineReceived(b"tags: foo bar\n") | ||
| self.protocol.lineReceived(b"test mcdonalds farm\n") | ||
| test = self.client._events[-1][-1] | ||
| self.assertEqual(None, getattr(test, "tags", None)) | ||
| def test_tags_get_set_on_test_tags(self): | ||
| self.protocol.lineReceived(b"test mcdonalds farm\n") | ||
| test = self.client._events[-1][-1] | ||
| self.protocol.lineReceived(b"tags: foo bar\n") | ||
| self.protocol.lineReceived(b"success mcdonalds farm\n") | ||
| self.assertEqual(None, getattr(test, "tags", None)) | ||
| class TestTestProtocolServerStreamTime(unittest.TestCase): | ||
| """Test managing time information at the protocol level.""" | ||
| def test_time_accepted_stdlib(self): | ||
| self.result = ExtendedTestResult() | ||
| self.stream = BytesIO() | ||
| self.protocol = subunit.TestProtocolServer(self.result, stream=self.stream) | ||
| self.protocol.lineReceived(b"time: 2001-12-12 12:59:59Z\n") | ||
| self.assertEqual(b"", self.stream.getvalue()) | ||
| def test_time_accepted_extended(self): | ||
| self.result = ExtendedTestResult() | ||
| self.stream = BytesIO() | ||
| self.protocol = subunit.TestProtocolServer(self.result, stream=self.stream) | ||
| self.protocol.lineReceived(b"time: 2001-12-12 12:59:59Z\n") | ||
| self.assertEqual(b"", self.stream.getvalue()) | ||
| self.assertEqual([("time", datetime.datetime(2001, 12, 12, 12, 59, 59, 0, iso8601.UTC))], self.result._events) | ||
| class TestRemotedTestCase(unittest.TestCase): | ||
| def test_simple(self): | ||
| test = subunit.RemotedTestCase("A test description") | ||
| self.assertRaises(NotImplementedError, test.setUp) | ||
| self.assertRaises(NotImplementedError, test.tearDown) | ||
| self.assertEqual("A test description", test.shortDescription()) | ||
| self.assertEqual("A test description", test.id()) | ||
| self.assertEqual("A test description (subunit.RemotedTestCase)", "%s" % test) | ||
| self.assertEqual("<subunit.RemotedTestCase description='A test description'>", "%r" % test) | ||
| result = unittest.TestResult() | ||
| test.run(result) | ||
| self.assertEqual([(test, _remote_exception_repr + ": " + "Cannot run RemotedTestCases.\n\n")], result.errors) | ||
| self.assertEqual(1, result.testsRun) | ||
| another_test = subunit.RemotedTestCase("A test description") | ||
| self.assertEqual(test, another_test) | ||
| different_test = subunit.RemotedTestCase("ofo") | ||
| self.assertNotEqual(test, different_test) | ||
| self.assertNotEqual(another_test, different_test) | ||
| class TestRemoteError(unittest.TestCase): | ||
| def test_eq(self): | ||
| error = subunit.RemoteError("Something went wrong") | ||
| another_error = subunit.RemoteError("Something went wrong") | ||
| different_error = subunit.RemoteError("boo!") | ||
| self.assertEqual(error, another_error) | ||
| self.assertNotEqual(error, different_error) | ||
| self.assertNotEqual(different_error, another_error) | ||
| def test_empty_constructor(self): | ||
| self.assertEqual(subunit.RemoteError(), subunit.RemoteError("")) | ||
| class TestExecTestCase(unittest.TestCase): | ||
| class SampleExecTestCase(subunit.ExecTestCase): | ||
| def test_sample_method(self): | ||
| """sample-script.py""" | ||
| # the sample script runs three tests, one each | ||
| # that fails, errors and succeeds | ||
| def test_sample_method_args(self): | ||
| """sample-script.py foo""" | ||
| # sample that will run just one test. | ||
| def test_construct(self): | ||
| test = self.SampleExecTestCase("test_sample_method") | ||
| self.assertEqual(test.script, subunit.join_dir(__file__, "sample-script.py")) | ||
| def test_args(self): | ||
| result = unittest.TestResult() | ||
| test = self.SampleExecTestCase("test_sample_method_args") | ||
| test.run(result) | ||
| self.assertEqual(1, result.testsRun) | ||
| def test_run(self): | ||
| result = ExtendedTestResult() | ||
| test = self.SampleExecTestCase("test_sample_method") | ||
| test.run(result) | ||
| mcdonald = subunit.RemotedTestCase("old mcdonald") | ||
| bing = subunit.RemotedTestCase("bing crosby") | ||
| bing_details = {} | ||
| bing_details["traceback"] = Content( | ||
| ContentType("text", "x-traceback", {"charset": "utf8"}), lambda: [b"foo.c:53:ERROR invalid state\n"] | ||
| ) | ||
| an_error = subunit.RemotedTestCase("an error") | ||
| error_details = {} | ||
| self.assertEqual( | ||
| [ | ||
| ("startTest", mcdonald), | ||
| ("addSuccess", mcdonald), | ||
| ("stopTest", mcdonald), | ||
| ("startTest", bing), | ||
| ("addFailure", bing, bing_details), | ||
| ("stopTest", bing), | ||
| ("startTest", an_error), | ||
| ("addError", an_error, error_details), | ||
| ("stopTest", an_error), | ||
| ], | ||
| result._events, | ||
| ) | ||
| def test_debug(self): | ||
| test = self.SampleExecTestCase("test_sample_method") | ||
| test.debug() | ||
| def test_count_test_cases(self): | ||
| """Run the child process and count responses to determine the count.""" | ||
| def test_join_dir(self): | ||
| sibling = subunit.join_dir(__file__, "foo") | ||
| filedir = os.path.abspath(os.path.dirname(__file__)) | ||
| expected = os.path.join(filedir, "foo") | ||
| self.assertEqual(sibling, expected) | ||
| class DoExecTestCase(subunit.ExecTestCase): | ||
| def test_working_script(self): | ||
| """sample-two-script.py""" | ||
| class TestIsolatedTestCase(TestCase): | ||
| class SampleIsolatedTestCase(subunit.IsolatedTestCase): | ||
| SETUP = False | ||
| TEARDOWN = False | ||
| TEST = False | ||
| def setUp(self): | ||
| TestIsolatedTestCase.SampleIsolatedTestCase.SETUP = True | ||
| def tearDown(self): | ||
| TestIsolatedTestCase.SampleIsolatedTestCase.TEARDOWN = True | ||
| def test_sets_global_state(self): | ||
| TestIsolatedTestCase.SampleIsolatedTestCase.TEST = True | ||
| def test_construct(self): | ||
| self.SampleIsolatedTestCase("test_sets_global_state") | ||
| @skipIf(os.name != "posix", "Need a posix system for forking tests") | ||
| def test_run(self): | ||
| result = unittest.TestResult() | ||
| test = self.SampleIsolatedTestCase("test_sets_global_state") | ||
| test.run(result) | ||
| self.assertEqual(result.testsRun, 1) | ||
| self.assertEqual(self.SampleIsolatedTestCase.SETUP, False) | ||
| self.assertEqual(self.SampleIsolatedTestCase.TEARDOWN, False) | ||
| self.assertEqual(self.SampleIsolatedTestCase.TEST, False) | ||
| def test_debug(self): | ||
| pass | ||
| # test = self.SampleExecTestCase("test_sample_method") | ||
| # test.debug() | ||
| class TestIsolatedTestSuite(TestCase): | ||
| class SampleTestToIsolate(unittest.TestCase): | ||
| SETUP = False | ||
| TEARDOWN = False | ||
| TEST = False | ||
| def setUp(self): | ||
| TestIsolatedTestSuite.SampleTestToIsolate.SETUP = True | ||
| def tearDown(self): | ||
| TestIsolatedTestSuite.SampleTestToIsolate.TEARDOWN = True | ||
| def test_sets_global_state(self): | ||
| TestIsolatedTestSuite.SampleTestToIsolate.TEST = True | ||
| def test_construct(self): | ||
| subunit.IsolatedTestSuite() | ||
| @skipIf(os.name != "posix", "Need a posix system for forking tests") | ||
| def test_run(self): | ||
| result = unittest.TestResult() | ||
| suite = subunit.IsolatedTestSuite() | ||
| sub_suite = unittest.TestSuite() | ||
| sub_suite.addTest(self.SampleTestToIsolate("test_sets_global_state")) | ||
| sub_suite.addTest(self.SampleTestToIsolate("test_sets_global_state")) | ||
| suite.addTest(sub_suite) | ||
| suite.addTest(self.SampleTestToIsolate("test_sets_global_state")) | ||
| suite.run(result) | ||
| self.assertEqual(result.testsRun, 3) | ||
| self.assertEqual(self.SampleTestToIsolate.SETUP, False) | ||
| self.assertEqual(self.SampleTestToIsolate.TEARDOWN, False) | ||
| self.assertEqual(self.SampleTestToIsolate.TEST, False) | ||
| # A number of these tests produce different output depending on the | ||
| # testtools version. testtools < 2.5.0 used traceback2, which incorrectly | ||
| # included the traceback header even for an exception with no traceback. | ||
| # testtools 2.5.0 switched to the Python 3 standard library's traceback | ||
| # module, which fixes this bug. See https://bugs.python.org/issue24695. | ||
| class TestTestProtocolClient(TestCase): | ||
| def setUp(self): | ||
| super().setUp() | ||
| self.io = BytesIO() | ||
| self.protocol = subunit.TestProtocolClient(self.io) | ||
| self.unicode_test = PlaceHolder("\u2603") | ||
| self.test = TestTestProtocolClient("test_start_test") | ||
| self.sample_details = {"something": Content(ContentType("text", "plain"), lambda: [b"serialised\nform"])} | ||
| self.sample_tb_details = dict(self.sample_details) | ||
| self.sample_tb_details["traceback"] = TracebackContent(subunit.RemoteError("boo qux"), self.test) | ||
| def test_start_test(self): | ||
| """Test startTest on a TestProtocolClient.""" | ||
| self.protocol.startTest(self.test) | ||
| self.assertEqual(self.io.getvalue(), ("test: %s\n" % self.test.id()).encode()) | ||
| def test_start_test_unicode_id(self): | ||
| """Test startTest on a TestProtocolClient.""" | ||
| self.protocol.startTest(self.unicode_test) | ||
| expected = b"test: " + "\u2603".encode("utf8") + b"\n" | ||
| self.assertEqual(expected, self.io.getvalue()) | ||
| def test_stop_test(self): | ||
| # stopTest doesn't output anything. | ||
| self.protocol.stopTest(self.test) | ||
| self.assertEqual(self.io.getvalue(), b"") | ||
| def test_add_success(self): | ||
| """Test addSuccess on a TestProtocolClient.""" | ||
| self.protocol.addSuccess(self.test) | ||
| self.assertEqual(self.io.getvalue(), ("successful: %s\n" % self.test.id()).encode()) | ||
| def test_add_outcome_unicode_id(self): | ||
| """Test addSuccess on a TestProtocolClient.""" | ||
| self.protocol.addSuccess(self.unicode_test) | ||
| expected = b"successful: " + "\u2603".encode("utf8") + b"\n" | ||
| self.assertEqual(expected, self.io.getvalue()) | ||
| def test_add_success_details(self): | ||
| """Test addSuccess on a TestProtocolClient with details.""" | ||
| self.protocol.addSuccess(self.test, details=self.sample_details) | ||
| self.assertEqual( | ||
| self.io.getvalue(), | ||
| ( | ||
| "successful: %s [ multipart\n" | ||
| "Content-Type: text/plain\n" | ||
| "something\n" | ||
| "F\r\nserialised\nform0\r\n]\n" % self.test.id() | ||
| ).encode(), | ||
| ) | ||
| def test_add_failure(self): | ||
| """Test addFailure on a TestProtocolClient.""" | ||
| self.protocol.addFailure(self.test, subunit.RemoteError("boo qux")) | ||
| self.assertThat( | ||
| self.io.getvalue(), | ||
| MatchesAny( | ||
| # testtools < 2.5.0 | ||
| Equals((("failure: %s [\n" + _remote_exception_str + ": boo qux\n" + "]\n") % self.test.id()).encode()), | ||
| # testtools >= 2.5.0 | ||
| Equals( | ||
| (("failure: %s [\n" + _remote_exception_repr + ": boo qux\n" + "]\n") % self.test.id()).encode() | ||
| ), | ||
| ), | ||
| ) | ||
| def test_add_failure_details(self): | ||
| """Test addFailure on a TestProtocolClient with details.""" | ||
| self.protocol.addFailure(self.test, details=self.sample_tb_details) | ||
| self.assertThat( | ||
| self.io.getvalue(), | ||
| MatchesAny( | ||
| # testtools < 2.5.0 | ||
| Equals( | ||
| ( | ||
| ( | ||
| "failure: %s [ multipart\n" | ||
| "Content-Type: text/plain\n" | ||
| "something\n" | ||
| "F\r\nserialised\nform0\r\n" | ||
| "Content-Type: text/x-traceback;charset=utf8,language=python\n" | ||
| "traceback\n" + _remote_exception_str_chunked + "]\n" | ||
| ) | ||
| % self.test.id() | ||
| ).encode() | ||
| ), | ||
| # testtools >= 2.5.0 | ||
| Equals( | ||
| ( | ||
| ( | ||
| "failure: %s [ multipart\n" | ||
| "Content-Type: text/plain\n" | ||
| "something\n" | ||
| "F\r\nserialised\nform0\r\n" | ||
| "Content-Type: text/x-traceback;charset=utf8,language=python\n" | ||
| "traceback\n" + _remote_exception_repr_chunked + "]\n" | ||
| ) | ||
| % self.test.id() | ||
| ).encode() | ||
| ), | ||
| ), | ||
| ) | ||
| def test_add_error(self): | ||
| """Test stopTest on a TestProtocolClient.""" | ||
| self.protocol.addError(self.test, subunit.RemoteError("phwoar crikey")) | ||
| self.assertThat( | ||
| self.io.getvalue(), | ||
| MatchesAny( | ||
| # testtools < 2.5.0 | ||
| Equals((("error: %s [\n" + _remote_exception_str + ": phwoar crikey\n]\n") % self.test.id()).encode()), | ||
| # testtools >= 2.5.0 | ||
| Equals((("error: %s [\n" + _remote_exception_repr + ": phwoar crikey\n]\n") % self.test.id()).encode()), | ||
| ), | ||
| ) | ||
| def test_add_error_details(self): | ||
| """Test stopTest on a TestProtocolClient with details.""" | ||
| self.protocol.addError(self.test, details=self.sample_tb_details) | ||
| self.assertThat( | ||
| self.io.getvalue(), | ||
| MatchesAny( | ||
| # testtools < 2.5.0 | ||
| Equals( | ||
| ( | ||
| ( | ||
| "error: %s [ multipart\n" | ||
| "Content-Type: text/plain\n" | ||
| "something\n" | ||
| "F\r\nserialised\nform0\r\n" | ||
| "Content-Type: text/x-traceback;charset=utf8,language=python\n" | ||
| "traceback\n" + _remote_exception_str_chunked + "]\n" | ||
| ) | ||
| % self.test.id() | ||
| ).encode() | ||
| ), | ||
| # testtools >= 2.5.0 | ||
| Equals( | ||
| ( | ||
| ( | ||
| "error: %s [ multipart\n" | ||
| "Content-Type: text/plain\n" | ||
| "something\n" | ||
| "F\r\nserialised\nform0\r\n" | ||
| "Content-Type: text/x-traceback;charset=utf8,language=python\n" | ||
| "traceback\n" + _remote_exception_repr_chunked + "]\n" | ||
| ) | ||
| % self.test.id() | ||
| ).encode() | ||
| ), | ||
| ), | ||
| ) | ||
| def test_add_expected_failure(self): | ||
| """Test addExpectedFailure on a TestProtocolClient.""" | ||
| self.protocol.addExpectedFailure(self.test, subunit.RemoteError("phwoar crikey")) | ||
| self.assertThat( | ||
| self.io.getvalue(), | ||
| MatchesAny( | ||
| # testtools < 2.5.0 | ||
| Equals((("xfail: %s [\n" + _remote_exception_str + ": phwoar crikey\n]\n") % self.test.id()).encode()), | ||
| # testtools >= 2.5.0 | ||
| Equals((("xfail: %s [\n" + _remote_exception_repr + ": phwoar crikey\n]\n") % self.test.id()).encode()), | ||
| ), | ||
| ) | ||
| def test_add_expected_failure_details(self): | ||
| """Test addExpectedFailure on a TestProtocolClient with details.""" | ||
| self.protocol.addExpectedFailure(self.test, details=self.sample_tb_details) | ||
| self.assertThat( | ||
| self.io.getvalue(), | ||
| MatchesAny( | ||
| # testtools < 2.5.0 | ||
| Equals( | ||
| ( | ||
| ( | ||
| "xfail: %s [ multipart\n" | ||
| "Content-Type: text/plain\n" | ||
| "something\n" | ||
| "F\r\nserialised\nform0\r\n" | ||
| "Content-Type: text/x-traceback;charset=utf8,language=python\n" | ||
| "traceback\n" + _remote_exception_str_chunked + "]\n" | ||
| ) | ||
| % self.test.id() | ||
| ).encode() | ||
| ), | ||
| # testtools >= 2.5.0 | ||
| Equals( | ||
| ( | ||
| ( | ||
| "xfail: %s [ multipart\n" | ||
| "Content-Type: text/plain\n" | ||
| "something\n" | ||
| "F\r\nserialised\nform0\r\n" | ||
| "Content-Type: text/x-traceback;charset=utf8,language=python\n" | ||
| "traceback\n" + _remote_exception_repr_chunked + "]\n" | ||
| ) | ||
| % self.test.id() | ||
| ).encode() | ||
| ), | ||
| ), | ||
| ) | ||
| def test_add_skip(self): | ||
| """Test addSkip on a TestProtocolClient.""" | ||
| self.protocol.addSkip(self.test, "Has it really?") | ||
| self.assertEqual(self.io.getvalue(), ("skip: %s [\nHas it really?\n]\n" % self.test.id()).encode()) | ||
| def test_add_skip_details(self): | ||
| """Test addSkip on a TestProtocolClient with details.""" | ||
| details = {"reason": Content(ContentType("text", "plain"), lambda: [b"Has it really?"])} | ||
| self.protocol.addSkip(self.test, details=details) | ||
| self.assertEqual( | ||
| self.io.getvalue(), | ||
| ( | ||
| "skip: %s [ multipart\nContent-Type: text/plain\nreason\nE\r\nHas it really?0\r\n]\n" % self.test.id() | ||
| ).encode(), | ||
| ) | ||
| def test_progress_set(self): | ||
| self.protocol.progress(23, subunit.PROGRESS_SET) | ||
| self.assertEqual(self.io.getvalue(), b"progress: 23\n") | ||
| def test_progress_neg_cur(self): | ||
| self.protocol.progress(-23, subunit.PROGRESS_CUR) | ||
| self.assertEqual(self.io.getvalue(), b"progress: -23\n") | ||
| def test_progress_pos_cur(self): | ||
| self.protocol.progress(23, subunit.PROGRESS_CUR) | ||
| self.assertEqual(self.io.getvalue(), b"progress: +23\n") | ||
| def test_progress_pop(self): | ||
| self.protocol.progress(1234, subunit.PROGRESS_POP) | ||
| self.assertEqual(self.io.getvalue(), b"progress: pop\n") | ||
| def test_progress_push(self): | ||
| self.protocol.progress(1234, subunit.PROGRESS_PUSH) | ||
| self.assertEqual(self.io.getvalue(), b"progress: push\n") | ||
| def test_time(self): | ||
| # Calling time() outputs a time signal immediately. | ||
| self.protocol.time(datetime.datetime(2009, 10, 11, 12, 13, 14, 15, iso8601.UTC)) | ||
| self.assertEqual(b"time: 2009-10-11 12:13:14.000015Z\n", self.io.getvalue()) | ||
| def test_add_unexpected_success(self): | ||
| """Test addUnexpectedSuccess on a TestProtocolClient.""" | ||
| self.protocol.addUnexpectedSuccess(self.test) | ||
| self.assertEqual(self.io.getvalue(), ("uxsuccess: %s\n" % self.test.id()).encode()) | ||
| def test_add_unexpected_success_details(self): | ||
| """Test addUnexpectedSuccess on a TestProtocolClient with details.""" | ||
| self.protocol.addUnexpectedSuccess(self.test, details=self.sample_details) | ||
| self.assertEqual( | ||
| self.io.getvalue(), | ||
| ( | ||
| "uxsuccess: %s [ multipart\n" | ||
| "Content-Type: text/plain\n" | ||
| "something\n" | ||
| "F\r\nserialised\nform0\r\n]\n" % self.test.id() | ||
| ).encode(), | ||
| ) | ||
| def test_tags_empty(self): | ||
| self.protocol.tags(set(), set()) | ||
| self.assertEqual(b"", self.io.getvalue()) | ||
| def test_tags_add(self): | ||
| self.protocol.tags({"foo"}, set()) | ||
| self.assertEqual(b"tags: foo\n", self.io.getvalue()) | ||
| def test_tags_both(self): | ||
| self.protocol.tags({"quux"}, {"bar"}) | ||
| self.assertThat([b"tags: quux -bar\n", b"tags: -bar quux\n"], Contains(self.io.getvalue())) | ||
| def test_tags_gone(self): | ||
| self.protocol.tags(set(), {"bar"}) | ||
| self.assertEqual(b"tags: -bar\n", self.io.getvalue()) |
| # | ||
| # subunit: extensions to Python unittest to get test results from subprocesses. | ||
| # Copyright (C) 2013 Robert Collins <robertc@robertcollins.net> | ||
| # | ||
| # 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. | ||
| # | ||
| import datetime | ||
| from io import BytesIO | ||
| from typing import Callable, Optional | ||
| from types import ModuleType | ||
| try: | ||
| from hypothesis import given | ||
| # To debug hypothesis | ||
| # from hypothesis import Settings, Verbosity | ||
| # Settings.default.verbosity = Verbosity.verbose | ||
| import hypothesis.strategies as st | ||
| except ImportError: | ||
| given: Optional[Callable[..., Callable[..., None]]] = None # type: ignore[assignment,no-redef] | ||
| st: Optional[ModuleType] = None # type: ignore[assignment,no-redef] | ||
| from testtools import TestCase | ||
| from testtools.matchers import Contains, HasLength | ||
| from testtools.testresult.doubles import StreamResult | ||
| try: | ||
| from testtools.tests.test_testresult import TestStreamResultContract # type: ignore[import-not-found] | ||
| except ImportError: | ||
| # testtools >= 2.8 no longer includes the tests submodule | ||
| TestStreamResultContract: Optional[type] = None # type: ignore[assignment,misc,no-redef] | ||
| import subunit | ||
| import iso8601 | ||
| CONSTANT_ENUM = b"\xb3)\x01\x0c\x03foo\x08U_\x1b" | ||
| CONSTANT_INPROGRESS = b"\xb3)\x02\x0c\x03foo\x8e\xc1-\xb5" | ||
| CONSTANT_SUCCESS = b"\xb3)\x03\x0c\x03fooE\x9d\xfe\x10" | ||
| CONSTANT_UXSUCCESS = b"\xb3)\x04\x0c\x03fooX\x98\xce\xa8" | ||
| CONSTANT_SKIP = b"\xb3)\x05\x0c\x03foo\x93\xc4\x1d\r" | ||
| CONSTANT_FAIL = b"\xb3)\x06\x0c\x03foo\x15Po\xa3" | ||
| CONSTANT_XFAIL = b"\xb3)\x07\x0c\x03foo\xde\x0c\xbc\x06" | ||
| CONSTANT_EOF = b"\xb3!\x10\x08S\x15\x88\xdc" | ||
| CONSTANT_FILE_CONTENT = b"\xb3!@\x13\x06barney\x03wooA5\xe3\x8c" | ||
| CONSTANT_MIME = b"\xb3! #\x1aapplication/foo; charset=1x3Q\x15" | ||
| CONSTANT_TIMESTAMP = b"\xb3+\x03\x13<\x17T\xcf\x80\xaf\xc8\x03barI\x96>-" | ||
| CONSTANT_ROUTE_CODE = b"\xb3-\x03\x13\x03bar\x06source\x9cY9\x19" | ||
| CONSTANT_RUNNABLE = b"\xb3(\x03\x0c\x03foo\xe3\xea\xf5\xa4" | ||
| CONSTANT_TAGS = [ | ||
| b"\xb3)\x80\x15\x03bar\x02\x03foo\x03barTHn\xb4", | ||
| b"\xb3)\x80\x15\x03bar\x02\x03bar\x03foo\xf8\xf1\x91o", | ||
| ] | ||
| if TestStreamResultContract is not None: | ||
| class TestStreamResultToBytesContract(TestCase, TestStreamResultContract): | ||
| """Check that StreamResult behaves as testtools expects.""" | ||
| def _make_result(self): | ||
| return subunit.StreamResultToBytes(BytesIO()) | ||
| class TestStreamResultToBytes(TestCase): | ||
| def _make_result(self): | ||
| output = BytesIO() | ||
| return subunit.StreamResultToBytes(output), output | ||
| def test_numbers(self): | ||
| result = subunit.StreamResultToBytes(BytesIO()) | ||
| packet = [] | ||
| self.assertRaises(Exception, result._write_number, -1, packet) | ||
| self.assertEqual([], packet) | ||
| result._write_number(0, packet) | ||
| self.assertEqual([b"\x00"], packet) | ||
| del packet[:] | ||
| result._write_number(63, packet) | ||
| self.assertEqual([b"\x3f"], packet) | ||
| del packet[:] | ||
| result._write_number(64, packet) | ||
| self.assertEqual([b"\x40\x40"], packet) | ||
| del packet[:] | ||
| result._write_number(16383, packet) | ||
| self.assertEqual([b"\x7f\xff"], packet) | ||
| del packet[:] | ||
| result._write_number(16384, packet) | ||
| self.assertEqual([b"\x80\x40", b"\x00"], packet) | ||
| del packet[:] | ||
| result._write_number(4194303, packet) | ||
| self.assertEqual([b"\xbf\xff", b"\xff"], packet) | ||
| del packet[:] | ||
| result._write_number(4194304, packet) | ||
| self.assertEqual([b"\xc0\x40\x00\x00"], packet) | ||
| del packet[:] | ||
| result._write_number(1073741823, packet) | ||
| self.assertEqual([b"\xff\xff\xff\xff"], packet) | ||
| del packet[:] | ||
| self.assertRaises(Exception, result._write_number, 1073741824, packet) | ||
| self.assertEqual([], packet) | ||
| def test_volatile_length(self): | ||
| # if the length of the packet data before the length itself is | ||
| # considered is right on the boundary for length's variable length | ||
| # encoding, it is easy to get the length wrong by not accounting for | ||
| # length itself. | ||
| # that is, the encoder has to ensure that length == sum (length_of_rest | ||
| # + length_of_length) | ||
| result, output = self._make_result() | ||
| # 1 byte short: | ||
| result.status(file_name="", file_bytes=b"\xff" * 0) | ||
| self.assertThat(output.getvalue(), HasLength(10)) | ||
| self.assertEqual(b"\x0a", output.getvalue()[3:4]) | ||
| output.seek(0) | ||
| output.truncate() | ||
| # 1 byte long: | ||
| result.status(file_name="", file_bytes=b"\xff" * 53) | ||
| self.assertThat(output.getvalue(), HasLength(63)) | ||
| self.assertEqual(b"\x3f", output.getvalue()[3:4]) | ||
| output.seek(0) | ||
| output.truncate() | ||
| # 2 bytes short | ||
| result.status(file_name="", file_bytes=b"\xff" * 54) | ||
| self.assertThat(output.getvalue(), HasLength(65)) | ||
| self.assertEqual(b"\x40\x41", output.getvalue()[3:5]) | ||
| output.seek(0) | ||
| output.truncate() | ||
| # 2 bytes long | ||
| result.status(file_name="", file_bytes=b"\xff" * 16371) | ||
| self.assertThat(output.getvalue(), HasLength(16383)) | ||
| self.assertEqual(b"\x7f\xff", output.getvalue()[3:5]) | ||
| output.seek(0) | ||
| output.truncate() | ||
| # 3 bytes short | ||
| result.status(file_name="", file_bytes=b"\xff" * 16372) | ||
| self.assertThat(output.getvalue(), HasLength(16385)) | ||
| self.assertEqual(b"\x80\x40\x01", output.getvalue()[3:6]) | ||
| output.seek(0) | ||
| output.truncate() | ||
| # 3 bytes long | ||
| result.status(file_name="", file_bytes=b"\xff" * 4194289) | ||
| self.assertThat(output.getvalue(), HasLength(4194303)) | ||
| self.assertEqual(b"\xbf\xff\xff", output.getvalue()[3:6]) | ||
| output.seek(0) | ||
| output.truncate() | ||
| self.assertRaises(Exception, result.status, file_name="", file_bytes=b"\xff" * 4194290) | ||
| def test_trivial_enumeration(self): | ||
| result, output = self._make_result() | ||
| result.status("foo", "exists") | ||
| self.assertEqual(CONSTANT_ENUM, output.getvalue()) | ||
| def test_inprogress(self): | ||
| result, output = self._make_result() | ||
| result.status("foo", "inprogress") | ||
| self.assertEqual(CONSTANT_INPROGRESS, output.getvalue()) | ||
| def test_success(self): | ||
| result, output = self._make_result() | ||
| result.status("foo", "success") | ||
| self.assertEqual(CONSTANT_SUCCESS, output.getvalue()) | ||
| def test_uxsuccess(self): | ||
| result, output = self._make_result() | ||
| result.status("foo", "uxsuccess") | ||
| self.assertEqual(CONSTANT_UXSUCCESS, output.getvalue()) | ||
| def test_skip(self): | ||
| result, output = self._make_result() | ||
| result.status("foo", "skip") | ||
| self.assertEqual(CONSTANT_SKIP, output.getvalue()) | ||
| def test_fail(self): | ||
| result, output = self._make_result() | ||
| result.status("foo", "fail") | ||
| self.assertEqual(CONSTANT_FAIL, output.getvalue()) | ||
| def test_xfail(self): | ||
| result, output = self._make_result() | ||
| result.status("foo", "xfail") | ||
| self.assertEqual(CONSTANT_XFAIL, output.getvalue()) | ||
| def test_unknown_status(self): | ||
| result, output = self._make_result() | ||
| self.assertRaises(Exception, result.status, "foo", "boo") | ||
| self.assertEqual(b"", output.getvalue()) | ||
| def test_eof(self): | ||
| result, output = self._make_result() | ||
| result.status(eof=True) | ||
| self.assertEqual(CONSTANT_EOF, output.getvalue()) | ||
| def test_file_content(self): | ||
| result, output = self._make_result() | ||
| result.status(file_name="barney", file_bytes=b"woo") | ||
| self.assertEqual(CONSTANT_FILE_CONTENT, output.getvalue()) | ||
| def test_mime(self): | ||
| result, output = self._make_result() | ||
| result.status(mime_type="application/foo; charset=1") | ||
| self.assertEqual(CONSTANT_MIME, output.getvalue()) | ||
| def test_route_code(self): | ||
| result, output = self._make_result() | ||
| result.status(test_id="bar", test_status="success", route_code="source") | ||
| self.assertEqual(CONSTANT_ROUTE_CODE, output.getvalue()) | ||
| def test_runnable(self): | ||
| result, output = self._make_result() | ||
| result.status("foo", "success", runnable=False) | ||
| self.assertEqual(CONSTANT_RUNNABLE, output.getvalue()) | ||
| def test_tags(self): | ||
| result, output = self._make_result() | ||
| result.status(test_id="bar", test_tags={"foo", "bar"}) | ||
| self.assertThat(CONSTANT_TAGS, Contains(output.getvalue())) | ||
| def test_timestamp(self): | ||
| timestamp = datetime.datetime(2001, 12, 12, 12, 59, 59, 45, iso8601.UTC) | ||
| result, output = self._make_result() | ||
| result.status(test_id="bar", test_status="success", timestamp=timestamp) | ||
| self.assertEqual(CONSTANT_TIMESTAMP, output.getvalue()) | ||
| class TestByteStreamToStreamResult(TestCase): | ||
| def test_non_subunit_encapsulated(self): | ||
| source = BytesIO(b"foo\nbar\n") | ||
| result = StreamResult() | ||
| subunit.ByteStreamToStreamResult(source, non_subunit_name="stdout").run(result) | ||
| self.assertEqual( | ||
| [ | ||
| ("status", None, None, None, True, "stdout", b"f", False, None, None, None), | ||
| ("status", None, None, None, True, "stdout", b"o", False, None, None, None), | ||
| ("status", None, None, None, True, "stdout", b"o", False, None, None, None), | ||
| ("status", None, None, None, True, "stdout", b"\n", False, None, None, None), | ||
| ("status", None, None, None, True, "stdout", b"b", False, None, None, None), | ||
| ("status", None, None, None, True, "stdout", b"a", False, None, None, None), | ||
| ("status", None, None, None, True, "stdout", b"r", False, None, None, None), | ||
| ("status", None, None, None, True, "stdout", b"\n", False, None, None, None), | ||
| ], | ||
| result._events, | ||
| ) | ||
| self.assertEqual(b"", source.read()) | ||
| def test_signature_middle_utf8_char(self): | ||
| utf8_bytes = b"\xe3\xb3\x8a" | ||
| source = BytesIO(utf8_bytes) | ||
| # Should be treated as one character (it is u'\u3cca') and wrapped | ||
| result = StreamResult() | ||
| subunit.ByteStreamToStreamResult(source, non_subunit_name="stdout").run(result) | ||
| self.assertEqual( | ||
| [ | ||
| ("status", None, None, None, True, "stdout", b"\xe3", False, None, None, None), | ||
| ("status", None, None, None, True, "stdout", b"\xb3", False, None, None, None), | ||
| ("status", None, None, None, True, "stdout", b"\x8a", False, None, None, None), | ||
| ], | ||
| result._events, | ||
| ) | ||
| def test_non_subunit_disabled_raises(self): | ||
| source = BytesIO(b"foo\nbar\n") | ||
| result = StreamResult() | ||
| case = subunit.ByteStreamToStreamResult(source) | ||
| e = self.assertRaises(Exception, case.run, result) | ||
| self.assertEqual(b"f", e.args[1]) | ||
| self.assertEqual(b"oo\nbar\n", source.read()) | ||
| self.assertEqual([], result._events) | ||
| def test_trivial_enumeration(self): | ||
| source = BytesIO(CONSTANT_ENUM) | ||
| result = StreamResult() | ||
| subunit.ByteStreamToStreamResult(source, non_subunit_name="stdout").run(result) | ||
| self.assertEqual(b"", source.read()) | ||
| self.assertEqual( | ||
| [ | ||
| ("status", "foo", "exists", None, True, None, None, False, None, None, None), | ||
| ], | ||
| result._events, | ||
| ) | ||
| def test_multiple_events(self): | ||
| source = BytesIO(CONSTANT_ENUM + CONSTANT_ENUM) | ||
| result = StreamResult() | ||
| subunit.ByteStreamToStreamResult(source, non_subunit_name="stdout").run(result) | ||
| self.assertEqual(b"", source.read()) | ||
| self.assertEqual( | ||
| [ | ||
| ("status", "foo", "exists", None, True, None, None, False, None, None, None), | ||
| ("status", "foo", "exists", None, True, None, None, False, None, None, None), | ||
| ], | ||
| result._events, | ||
| ) | ||
| def test_inprogress(self): | ||
| self.check_event(CONSTANT_INPROGRESS, "inprogress") | ||
| def test_success(self): | ||
| self.check_event(CONSTANT_SUCCESS, "success") | ||
| def test_uxsuccess(self): | ||
| self.check_event(CONSTANT_UXSUCCESS, "uxsuccess") | ||
| def test_skip(self): | ||
| self.check_event(CONSTANT_SKIP, "skip") | ||
| def test_fail(self): | ||
| self.check_event(CONSTANT_FAIL, "fail") | ||
| def test_xfail(self): | ||
| self.check_event(CONSTANT_XFAIL, "xfail") | ||
| def check_events(self, source_bytes, events): | ||
| source = BytesIO(source_bytes) | ||
| result = StreamResult() | ||
| subunit.ByteStreamToStreamResult(source, non_subunit_name="stdout").run(result) | ||
| self.assertEqual(b"", source.read()) | ||
| self.assertEqual(events, result._events) | ||
| # - any file attachments should be bytes equivalent [as users assume that]. | ||
| for event in result._events: | ||
| if event[5] is not None: | ||
| bytes(event[6]) | ||
| def check_event( | ||
| self, | ||
| source_bytes, | ||
| test_status=None, | ||
| test_id="foo", | ||
| route_code=None, | ||
| timestamp=None, | ||
| tags=None, | ||
| mime_type=None, | ||
| file_name=None, | ||
| file_bytes=None, | ||
| eof=False, | ||
| runnable=True, | ||
| ): | ||
| event = self._event( | ||
| test_id=test_id, | ||
| test_status=test_status, | ||
| tags=tags, | ||
| runnable=runnable, | ||
| file_name=file_name, | ||
| file_bytes=file_bytes, | ||
| eof=eof, | ||
| mime_type=mime_type, | ||
| route_code=route_code, | ||
| timestamp=timestamp, | ||
| ) | ||
| self.check_events(source_bytes, [event]) | ||
| def _event( | ||
| self, | ||
| test_status=None, | ||
| test_id=None, | ||
| route_code=None, | ||
| timestamp=None, | ||
| tags=None, | ||
| mime_type=None, | ||
| file_name=None, | ||
| file_bytes=None, | ||
| eof=False, | ||
| runnable=True, | ||
| ): | ||
| return ( | ||
| "status", | ||
| test_id, | ||
| test_status, | ||
| tags, | ||
| runnable, | ||
| file_name, | ||
| file_bytes, | ||
| eof, | ||
| mime_type, | ||
| route_code, | ||
| timestamp, | ||
| ) | ||
| def test_eof(self): | ||
| self.check_event(CONSTANT_EOF, test_id=None, eof=True) | ||
| def test_file_content(self): | ||
| self.check_event(CONSTANT_FILE_CONTENT, test_id=None, file_name="barney", file_bytes=b"woo") | ||
| def test_file_content_length_into_checksum(self): | ||
| # A bad file content length which creeps into the checksum. | ||
| bad_file_length_content = b"\xb3!@\x13\x06barney\x04woo\xdc\xe2\xdb\x35" | ||
| self.check_events( | ||
| bad_file_length_content, | ||
| [ | ||
| self._event( | ||
| test_id="subunit.parser", | ||
| eof=True, | ||
| file_name="Packet data", | ||
| file_bytes=bad_file_length_content, | ||
| mime_type="application/octet-stream", | ||
| ), | ||
| self._event( | ||
| test_id="subunit.parser", | ||
| test_status="fail", | ||
| eof=True, | ||
| file_name="Parser Error", | ||
| file_bytes=b"File content extends past end of packet: claimed 4 bytes, 3 available", | ||
| mime_type="text/plain;charset=utf8", | ||
| ), | ||
| ], | ||
| ) | ||
| def test_packet_length_4_word_varint(self): | ||
| packet_data = b"\xb3!@\xc0\x00\x11" | ||
| self.check_events( | ||
| packet_data, | ||
| [ | ||
| self._event( | ||
| test_id="subunit.parser", | ||
| eof=True, | ||
| file_name="Packet data", | ||
| file_bytes=packet_data, | ||
| mime_type="application/octet-stream", | ||
| ), | ||
| self._event( | ||
| test_id="subunit.parser", | ||
| test_status="fail", | ||
| eof=True, | ||
| file_name="Parser Error", | ||
| file_bytes=b"3 byte maximum given but 4 byte value found.", | ||
| mime_type="text/plain;charset=utf8", | ||
| ), | ||
| ], | ||
| ) | ||
| def test_mime(self): | ||
| self.check_event(CONSTANT_MIME, test_id=None, mime_type="application/foo; charset=1") | ||
| def test_route_code(self): | ||
| self.check_event(CONSTANT_ROUTE_CODE, "success", route_code="source", test_id="bar") | ||
| def test_runnable(self): | ||
| self.check_event(CONSTANT_RUNNABLE, test_status="success", runnable=False) | ||
| def test_tags(self): | ||
| self.check_event(CONSTANT_TAGS[0], None, tags={"foo", "bar"}, test_id="bar") | ||
| def test_timestamp(self): | ||
| timestamp = datetime.datetime(2001, 12, 12, 12, 59, 59, 45, iso8601.UTC) | ||
| self.check_event(CONSTANT_TIMESTAMP, "success", test_id="bar", timestamp=timestamp) | ||
| def test_bad_crc_errors_via_status(self): | ||
| file_bytes = CONSTANT_MIME[:-1] + b"\x00" | ||
| self.check_events( | ||
| file_bytes, | ||
| [ | ||
| self._event( | ||
| test_id="subunit.parser", | ||
| eof=True, | ||
| file_name="Packet data", | ||
| file_bytes=file_bytes, | ||
| mime_type="application/octet-stream", | ||
| ), | ||
| self._event( | ||
| test_id="subunit.parser", | ||
| test_status="fail", | ||
| eof=True, | ||
| file_name="Parser Error", | ||
| file_bytes=b"Bad checksum - calculated (0x78335115), stored (0x78335100)", | ||
| mime_type="text/plain;charset=utf8", | ||
| ), | ||
| ], | ||
| ) | ||
| def test_not_utf8_in_string(self): | ||
| file_bytes = CONSTANT_ROUTE_CODE[:5] + b"\xb4" + CONSTANT_ROUTE_CODE[6:-4] + b"\xce\x56\xc6\x17" | ||
| self.check_events( | ||
| file_bytes, | ||
| [ | ||
| self._event( | ||
| test_id="subunit.parser", | ||
| eof=True, | ||
| file_name="Packet data", | ||
| file_bytes=file_bytes, | ||
| mime_type="application/octet-stream", | ||
| ), | ||
| self._event( | ||
| test_id="subunit.parser", | ||
| test_status="fail", | ||
| eof=True, | ||
| file_name="Parser Error", | ||
| file_bytes=b"UTF8 string at offset 2 is not UTF8", | ||
| mime_type="text/plain;charset=utf8", | ||
| ), | ||
| ], | ||
| ) | ||
| def test_NULL_in_string(self): | ||
| file_bytes = CONSTANT_ROUTE_CODE[:6] + b"\x00" + CONSTANT_ROUTE_CODE[7:-4] + b"\xd7\x41\xac\xfe" | ||
| self.check_events( | ||
| file_bytes, | ||
| [ | ||
| self._event( | ||
| test_id="subunit.parser", | ||
| eof=True, | ||
| file_name="Packet data", | ||
| file_bytes=file_bytes, | ||
| mime_type="application/octet-stream", | ||
| ), | ||
| self._event( | ||
| test_id="subunit.parser", | ||
| test_status="fail", | ||
| eof=True, | ||
| file_name="Parser Error", | ||
| file_bytes=b"UTF8 string at offset 2 contains NUL byte", | ||
| mime_type="text/plain;charset=utf8", | ||
| ), | ||
| ], | ||
| ) | ||
| def test_bad_utf8_stringlength(self): | ||
| file_bytes = CONSTANT_ROUTE_CODE[:4] + b"\x3f" + CONSTANT_ROUTE_CODE[5:-4] + b"\xbe\x29\xe0\xc2" | ||
| self.check_events( | ||
| file_bytes, | ||
| [ | ||
| self._event( | ||
| test_id="subunit.parser", | ||
| eof=True, | ||
| file_name="Packet data", | ||
| file_bytes=file_bytes, | ||
| mime_type="application/octet-stream", | ||
| ), | ||
| self._event( | ||
| test_id="subunit.parser", | ||
| test_status="fail", | ||
| eof=True, | ||
| file_name="Parser Error", | ||
| file_bytes=b"UTF8 string at offset 2 extends past end of packet: claimed 63 bytes, 10 available", | ||
| mime_type="text/plain;charset=utf8", | ||
| ), | ||
| ], | ||
| ) | ||
| def test_route_code_and_file_content(self): | ||
| content = BytesIO() | ||
| subunit.StreamResultToBytes(content).status( | ||
| route_code="0", mime_type="text/plain", file_name="bar", file_bytes=b"foo" | ||
| ) | ||
| self.check_event( | ||
| content.getvalue(), test_id=None, file_name="bar", route_code="0", mime_type="text/plain", file_bytes=b"foo" | ||
| ) | ||
| if st is not None: | ||
| @given(st.binary()) | ||
| def test_hypothesis_decoding(self, code_bytes): | ||
| source = BytesIO(code_bytes) | ||
| result = StreamResult() | ||
| stream = subunit.ByteStreamToStreamResult(source, non_subunit_name="stdout") | ||
| stream.run(result) | ||
| self.assertEqual(b"", source.read()) |
| # | ||
| # subunit: extensions to Python unittest to get test results from subprocesses. | ||
| # Copyright (C) 2009 Robert Collins <robertc@robertcollins.net> | ||
| # | ||
| # 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. | ||
| # | ||
| import csv | ||
| import datetime | ||
| import sys | ||
| import unittest | ||
| from io import StringIO | ||
| import testtools | ||
| from testtools import TestCase | ||
| from testtools.content import TracebackContent, text_content | ||
| from testtools.testresult.doubles import ExtendedTestResult | ||
| import subunit | ||
| import iso8601 | ||
| import subunit.test_results | ||
| class LoggingDecorator(subunit.test_results.HookedTestResultDecorator): | ||
| def __init__(self, decorated): | ||
| self._calls = 0 | ||
| super().__init__(decorated) | ||
| def _before_event(self): | ||
| self._calls += 1 | ||
| class AssertBeforeTestResult(LoggingDecorator): | ||
| """A TestResult for checking preconditions.""" | ||
| def __init__(self, decorated, test): | ||
| self.test = test | ||
| super().__init__(decorated) | ||
| def _before_event(self): | ||
| self.test.assertEqual(1, self.earlier._calls) | ||
| super()._before_event() | ||
| class TimeCapturingResult(unittest.TestResult): | ||
| def __init__(self): | ||
| super().__init__() | ||
| self._calls = [] | ||
| self.failfast = False | ||
| def time(self, a_datetime): | ||
| self._calls.append(a_datetime) | ||
| def progress(self, offset, whence): | ||
| pass | ||
| def addDuration(self, test, duration): | ||
| """Called to add a test duration. | ||
| :param test: The test that completed. | ||
| :param duration: The duration of the test as a float in seconds. | ||
| """ | ||
| pass | ||
| class TestHookedTestResultDecorator(unittest.TestCase): | ||
| def setUp(self): | ||
| # An end to the chain | ||
| terminal = unittest.TestResult() | ||
| # Asserts that the call was made to self.result before asserter was | ||
| # called. | ||
| asserter = AssertBeforeTestResult(terminal, self) | ||
| # The result object we call, which much increase its call count. | ||
| self.result = LoggingDecorator(asserter) | ||
| asserter.earlier = self.result | ||
| self.decorated = asserter | ||
| def tearDown(self): | ||
| # The hook in self.result must have been called | ||
| self.assertEqual(1, self.result._calls) | ||
| # The hook in asserter must have been called too, otherwise the | ||
| # assertion about ordering won't have completed. | ||
| self.assertEqual(1, self.decorated._calls) | ||
| def test_startTest(self): | ||
| self.result.startTest(self) | ||
| def test_startTestRun(self): | ||
| self.result.startTestRun() | ||
| def test_stopTest(self): | ||
| self.result.stopTest(self) | ||
| def test_stopTestRun(self): | ||
| self.result.stopTestRun() | ||
| def test_addError(self): | ||
| self.result.addError(self, subunit.RemoteError()) | ||
| def test_addError_details(self): | ||
| self.result.addError(self, details={}) | ||
| def test_addFailure(self): | ||
| self.result.addFailure(self, subunit.RemoteError()) | ||
| def test_addFailure_details(self): | ||
| self.result.addFailure(self, details={}) | ||
| def test_addSuccess(self): | ||
| self.result.addSuccess(self) | ||
| def test_addSuccess_details(self): | ||
| self.result.addSuccess(self, details={}) | ||
| def test_addSkip(self): | ||
| self.result.addSkip(self, "foo") | ||
| def test_addSkip_details(self): | ||
| self.result.addSkip(self, details={}) | ||
| def test_addExpectedFailure(self): | ||
| self.result.addExpectedFailure(self, subunit.RemoteError()) | ||
| def test_addExpectedFailure_details(self): | ||
| self.result.addExpectedFailure(self, details={}) | ||
| def test_addUnexpectedSuccess(self): | ||
| self.result.addUnexpectedSuccess(self) | ||
| def test_addUnexpectedSuccess_details(self): | ||
| self.result.addUnexpectedSuccess(self, details={}) | ||
| def test_progress(self): | ||
| self.result.progress(1, subunit.PROGRESS_SET) | ||
| def test_wasSuccessful(self): | ||
| self.result.wasSuccessful() | ||
| def test_shouldStop(self): | ||
| self.result.shouldStop | ||
| def test_stop(self): | ||
| self.result.stop() | ||
| def test_time(self): | ||
| self.result.time(None) | ||
| def test_addDuration(self): | ||
| self.result.addDuration(self, 1.5) | ||
| class TestAutoTimingTestResultDecorator(unittest.TestCase): | ||
| def setUp(self): | ||
| # And end to the chain which captures time events. | ||
| terminal = TimeCapturingResult() | ||
| # The result object under test. | ||
| self.result = subunit.test_results.AutoTimingTestResultDecorator(terminal) | ||
| self.decorated = terminal | ||
| def test_without_time_calls_time_is_called_and_not_None(self): | ||
| self.result.startTest(self) | ||
| self.assertEqual(1, len(self.decorated._calls)) | ||
| self.assertNotEqual(None, self.decorated._calls[0]) | ||
| def test_no_time_from_progress(self): | ||
| self.result.progress(1, subunit.PROGRESS_CUR) | ||
| self.assertEqual(0, len(self.decorated._calls)) | ||
| def test_no_time_from_shouldStop(self): | ||
| self.decorated.stop() | ||
| self.result.shouldStop | ||
| self.assertEqual(0, len(self.decorated._calls)) | ||
| def test_calling_time_inhibits_automatic_time(self): | ||
| # Calling time() outputs a time signal immediately and prevents | ||
| # automatically adding one when other methods are called. | ||
| time = datetime.datetime(2009, 10, 11, 12, 13, 14, 15, iso8601.UTC) | ||
| self.result.time(time) | ||
| self.result.startTest(self) | ||
| self.result.stopTest(self) | ||
| self.assertEqual(1, len(self.decorated._calls)) | ||
| self.assertEqual(time, self.decorated._calls[0]) | ||
| def test_calling_time_None_enables_automatic_time(self): | ||
| time = datetime.datetime(2009, 10, 11, 12, 13, 14, 15, iso8601.UTC) | ||
| self.result.time(time) | ||
| self.assertEqual(1, len(self.decorated._calls)) | ||
| self.assertEqual(time, self.decorated._calls[0]) | ||
| # Calling None passes the None through, in case other results care. | ||
| self.result.time(None) | ||
| self.assertEqual(2, len(self.decorated._calls)) | ||
| self.assertEqual(None, self.decorated._calls[1]) | ||
| # Calling other methods doesn't generate an automatic time event. | ||
| self.result.startTest(self) | ||
| self.assertEqual(3, len(self.decorated._calls)) | ||
| self.assertNotEqual(None, self.decorated._calls[2]) | ||
| def test_set_failfast_True(self): | ||
| self.assertFalse(self.decorated.failfast) | ||
| self.result.failfast = True | ||
| self.assertTrue(self.decorated.failfast) | ||
| class TestTagCollapsingDecorator(TestCase): | ||
| def test_tags_collapsed_outside_of_tests(self): | ||
| result = ExtendedTestResult() | ||
| tag_collapser = subunit.test_results.TagCollapsingDecorator(result) | ||
| tag_collapser.tags({"a"}, set()) | ||
| tag_collapser.tags({"b"}, set()) | ||
| tag_collapser.startTest(self) | ||
| self.assertEqual( | ||
| [ | ||
| ("tags", {"a", "b"}, set()), | ||
| ("startTest", self), | ||
| ], | ||
| result._events, | ||
| ) | ||
| def test_tags_collapsed_outside_of_tests_are_flushed(self): | ||
| result = ExtendedTestResult() | ||
| tag_collapser = subunit.test_results.TagCollapsingDecorator(result) | ||
| tag_collapser.startTestRun() | ||
| tag_collapser.tags({"a"}, set()) | ||
| tag_collapser.tags({"b"}, set()) | ||
| tag_collapser.startTest(self) | ||
| tag_collapser.addSuccess(self) | ||
| tag_collapser.stopTest(self) | ||
| tag_collapser.stopTestRun() | ||
| self.assertEqual( | ||
| [ | ||
| ("startTestRun",), | ||
| ("tags", {"a", "b"}, set()), | ||
| ("startTest", self), | ||
| ("addSuccess", self), | ||
| ("stopTest", self), | ||
| ("stopTestRun",), | ||
| ], | ||
| result._events, | ||
| ) | ||
| def test_tags_forwarded_after_tests(self): | ||
| test = subunit.RemotedTestCase("foo") | ||
| result = ExtendedTestResult() | ||
| tag_collapser = subunit.test_results.TagCollapsingDecorator(result) | ||
| tag_collapser.startTestRun() | ||
| tag_collapser.startTest(test) | ||
| tag_collapser.addSuccess(test) | ||
| tag_collapser.stopTest(test) | ||
| tag_collapser.tags({"a"}, {"b"}) | ||
| tag_collapser.stopTestRun() | ||
| self.assertEqual( | ||
| [ | ||
| ("startTestRun",), | ||
| ("startTest", test), | ||
| ("addSuccess", test), | ||
| ("stopTest", test), | ||
| ("tags", {"a"}, {"b"}), | ||
| ("stopTestRun",), | ||
| ], | ||
| result._events, | ||
| ) | ||
| def test_tags_collapsed_inside_of_tests(self): | ||
| result = ExtendedTestResult() | ||
| tag_collapser = subunit.test_results.TagCollapsingDecorator(result) | ||
| test = subunit.RemotedTestCase("foo") | ||
| tag_collapser.startTest(test) | ||
| tag_collapser.tags({"a"}, set()) | ||
| tag_collapser.tags({"b"}, {"a"}) | ||
| tag_collapser.tags({"c"}, set()) | ||
| tag_collapser.stopTest(test) | ||
| self.assertEqual([("startTest", test), ("tags", {"b", "c"}, {"a"}), ("stopTest", test)], result._events) | ||
| def test_tags_collapsed_inside_of_tests_different_ordering(self): | ||
| result = ExtendedTestResult() | ||
| tag_collapser = subunit.test_results.TagCollapsingDecorator(result) | ||
| test = subunit.RemotedTestCase("foo") | ||
| tag_collapser.startTest(test) | ||
| tag_collapser.tags(set(), {"a"}) | ||
| tag_collapser.tags({"a", "b"}, set()) | ||
| tag_collapser.tags({"c"}, set()) | ||
| tag_collapser.stopTest(test) | ||
| self.assertEqual([("startTest", test), ("tags", {"a", "b", "c"}, set()), ("stopTest", test)], result._events) | ||
| def test_tags_sent_before_result(self): | ||
| # Because addSuccess and friends tend to send subunit output | ||
| # immediately, and because 'tags:' before a result line means | ||
| # something different to 'tags:' after a result line, we need to be | ||
| # sure that tags are emitted before 'addSuccess' (or whatever). | ||
| result = ExtendedTestResult() | ||
| tag_collapser = subunit.test_results.TagCollapsingDecorator(result) | ||
| test = subunit.RemotedTestCase("foo") | ||
| tag_collapser.startTest(test) | ||
| tag_collapser.tags({"a"}, set()) | ||
| tag_collapser.addSuccess(test) | ||
| tag_collapser.stopTest(test) | ||
| self.assertEqual( | ||
| [("startTest", test), ("tags", {"a"}, set()), ("addSuccess", test), ("stopTest", test)], result._events | ||
| ) | ||
| class TestTimeCollapsingDecorator(TestCase): | ||
| def make_time(self): | ||
| # Heh heh. | ||
| return datetime.datetime(2000, 1, self.getUniqueInteger(), tzinfo=iso8601.UTC) | ||
| def test_initial_time_forwarded(self): | ||
| # We always forward the first time event we see. | ||
| result = ExtendedTestResult() | ||
| tag_collapser = subunit.test_results.TimeCollapsingDecorator(result) | ||
| a_time = self.make_time() | ||
| tag_collapser.time(a_time) | ||
| self.assertEqual([("time", a_time)], result._events) | ||
| def test_time_collapsed_to_first_and_last(self): | ||
| # If there are many consecutive time events, only the first and last | ||
| # are sent through. | ||
| result = ExtendedTestResult() | ||
| tag_collapser = subunit.test_results.TimeCollapsingDecorator(result) | ||
| times = [self.make_time() for i in range(5)] | ||
| for a_time in times: | ||
| tag_collapser.time(a_time) | ||
| tag_collapser.startTest(subunit.RemotedTestCase("foo")) | ||
| self.assertEqual([("time", times[0]), ("time", times[-1])], result._events[:-1]) | ||
| def test_only_one_time_sent(self): | ||
| # If we receive a single time event followed by a non-time event, we | ||
| # send exactly one time event. | ||
| result = ExtendedTestResult() | ||
| tag_collapser = subunit.test_results.TimeCollapsingDecorator(result) | ||
| a_time = self.make_time() | ||
| tag_collapser.time(a_time) | ||
| tag_collapser.startTest(subunit.RemotedTestCase("foo")) | ||
| self.assertEqual([("time", a_time)], result._events[:-1]) | ||
| def test_duplicate_times_not_sent(self): | ||
| # Many time events with the exact same time are collapsed into one | ||
| # time event. | ||
| result = ExtendedTestResult() | ||
| tag_collapser = subunit.test_results.TimeCollapsingDecorator(result) | ||
| a_time = self.make_time() | ||
| for i in range(5): | ||
| tag_collapser.time(a_time) | ||
| tag_collapser.startTest(subunit.RemotedTestCase("foo")) | ||
| self.assertEqual([("time", a_time)], result._events[:-1]) | ||
| def test_no_times_inserted(self): | ||
| result = ExtendedTestResult() | ||
| tag_collapser = subunit.test_results.TimeCollapsingDecorator(result) | ||
| a_time = self.make_time() | ||
| tag_collapser.time(a_time) | ||
| foo = subunit.RemotedTestCase("foo") | ||
| tag_collapser.startTest(foo) | ||
| tag_collapser.addSuccess(foo) | ||
| tag_collapser.stopTest(foo) | ||
| self.assertEqual([("time", a_time), ("startTest", foo), ("addSuccess", foo), ("stopTest", foo)], result._events) | ||
| class TestByTestResultTests(testtools.TestCase): | ||
| def setUp(self): | ||
| super().setUp() | ||
| self.log = [] | ||
| self.result = subunit.test_results.TestByTestResult(self.on_test) | ||
| self.result._now = iter(range(5)).__next__ | ||
| def assertCalled(self, **kwargs): | ||
| defaults = { | ||
| "test": self, | ||
| "tags": set(), | ||
| "details": None, | ||
| "start_time": 0, | ||
| "stop_time": 1, | ||
| } | ||
| defaults.update(kwargs) | ||
| self.assertEqual([defaults], self.log) | ||
| def on_test(self, **kwargs): | ||
| self.log.append(kwargs) | ||
| def test_no_tests_nothing_reported(self): | ||
| self.result.startTestRun() | ||
| self.result.stopTestRun() | ||
| self.assertEqual([], self.log) | ||
| def test_add_success(self): | ||
| self.result.startTest(self) | ||
| self.result.addSuccess(self) | ||
| self.result.stopTest(self) | ||
| self.assertCalled(status="success") | ||
| def test_add_success_details(self): | ||
| self.result.startTest(self) | ||
| details = {"foo": "bar"} | ||
| self.result.addSuccess(self, details=details) | ||
| self.result.stopTest(self) | ||
| self.assertCalled(status="success", details=details) | ||
| def test_tags(self): | ||
| if not getattr(self.result, "tags", None): | ||
| self.skipTest("No tags in testtools") | ||
| self.result.tags(["foo"], []) | ||
| self.result.startTest(self) | ||
| self.result.addSuccess(self) | ||
| self.result.stopTest(self) | ||
| self.assertCalled(status="success", tags={"foo"}) | ||
| def test_add_error(self): | ||
| self.result.startTest(self) | ||
| try: | ||
| 1 / 0 | ||
| except ZeroDivisionError: | ||
| error = sys.exc_info() | ||
| self.result.addError(self, error) | ||
| self.result.stopTest(self) | ||
| self.assertCalled(status="error", details={"traceback": TracebackContent(error, self)}) | ||
| def test_add_error_details(self): | ||
| self.result.startTest(self) | ||
| details = {"foo": text_content("bar")} | ||
| self.result.addError(self, details=details) | ||
| self.result.stopTest(self) | ||
| self.assertCalled(status="error", details=details) | ||
| def test_add_failure(self): | ||
| self.result.startTest(self) | ||
| try: | ||
| self.fail("intentional failure") | ||
| except self.failureException: | ||
| failure = sys.exc_info() | ||
| self.result.addFailure(self, failure) | ||
| self.result.stopTest(self) | ||
| self.assertCalled(status="failure", details={"traceback": TracebackContent(failure, self)}) | ||
| def test_add_failure_details(self): | ||
| self.result.startTest(self) | ||
| details = {"foo": text_content("bar")} | ||
| self.result.addFailure(self, details=details) | ||
| self.result.stopTest(self) | ||
| self.assertCalled(status="failure", details=details) | ||
| def test_add_xfail(self): | ||
| self.result.startTest(self) | ||
| try: | ||
| 1 / 0 | ||
| except ZeroDivisionError: | ||
| error = sys.exc_info() | ||
| self.result.addExpectedFailure(self, error) | ||
| self.result.stopTest(self) | ||
| self.assertCalled(status="xfail", details={"traceback": TracebackContent(error, self)}) | ||
| def test_add_xfail_details(self): | ||
| self.result.startTest(self) | ||
| details = {"foo": text_content("bar")} | ||
| self.result.addExpectedFailure(self, details=details) | ||
| self.result.stopTest(self) | ||
| self.assertCalled(status="xfail", details=details) | ||
| def test_add_unexpected_success(self): | ||
| self.result.startTest(self) | ||
| details = {"foo": "bar"} | ||
| self.result.addUnexpectedSuccess(self, details=details) | ||
| self.result.stopTest(self) | ||
| self.assertCalled(status="success", details=details) | ||
| def test_add_skip_reason(self): | ||
| self.result.startTest(self) | ||
| reason = self.getUniqueString() | ||
| self.result.addSkip(self, reason) | ||
| self.result.stopTest(self) | ||
| self.assertCalled(status="skip", details={"reason": text_content(reason)}) | ||
| def test_add_skip_details(self): | ||
| self.result.startTest(self) | ||
| details = {"foo": "bar"} | ||
| self.result.addSkip(self, details=details) | ||
| self.result.stopTest(self) | ||
| self.assertCalled(status="skip", details=details) | ||
| def test_twice(self): | ||
| self.result.startTest(self) | ||
| self.result.addSuccess(self, details={"foo": "bar"}) | ||
| self.result.stopTest(self) | ||
| self.result.startTest(self) | ||
| self.result.addSuccess(self) | ||
| self.result.stopTest(self) | ||
| self.assertEqual( | ||
| [ | ||
| { | ||
| "test": self, | ||
| "status": "success", | ||
| "start_time": 0, | ||
| "stop_time": 1, | ||
| "tags": set(), | ||
| "details": {"foo": "bar"}, | ||
| }, | ||
| {"test": self, "status": "success", "start_time": 2, "stop_time": 3, "tags": set(), "details": None}, | ||
| ], | ||
| self.log, | ||
| ) | ||
| class TestCsvResult(testtools.TestCase): | ||
| def parse_stream(self, stream): | ||
| stream.seek(0) | ||
| reader = csv.reader(stream) | ||
| return list(reader) | ||
| def test_csv_output(self): | ||
| stream = StringIO() | ||
| result = subunit.test_results.CsvResult(stream) | ||
| result._now = iter(range(5)).__next__ | ||
| result.startTestRun() | ||
| result.startTest(self) | ||
| result.addSuccess(self) | ||
| result.stopTest(self) | ||
| result.stopTestRun() | ||
| self.assertEqual( | ||
| [ | ||
| ["test", "status", "start_time", "stop_time"], | ||
| [self.id(), "success", "0", "1"], | ||
| ], | ||
| self.parse_stream(stream), | ||
| ) | ||
| def test_just_header_when_no_tests(self): | ||
| stream = StringIO() | ||
| result = subunit.test_results.CsvResult(stream) | ||
| result.startTestRun() | ||
| result.stopTestRun() | ||
| self.assertEqual([["test", "status", "start_time", "stop_time"]], self.parse_stream(stream)) | ||
| def test_no_output_before_events(self): | ||
| stream = StringIO() | ||
| subunit.test_results.CsvResult(stream) | ||
| self.assertEqual([], self.parse_stream(stream)) | ||
| class TestTestIdPrintingResult(testtools.TestCase): | ||
| def setUp(self): | ||
| super().setUp() | ||
| self.stream = StringIO() | ||
| self.result = subunit.test_results.TestIdPrintingResult(self.stream) | ||
| def test_addDuration(self): | ||
| # addDuration should not raise an exception | ||
| test = subunit.RemotedTestCase("foo") | ||
| self.result.addDuration(test, 2.5) | ||
| # TestIdPrintingResult doesn't output anything for addDuration | ||
| self.assertEqual("", self.stream.getvalue()) |
+1
-1
@@ -32,4 +32,4 @@ Subunit is licensed under two licenses, the Apache License, Version 2.0 or the | ||
| A known list of such code is maintained here: | ||
| * The runtests.py and python/subunit/tests/TestUtil.py module are GPL test | ||
| * The runtests.py and python/tests/TestUtil.py module are GPL test | ||
| support modules. They are not installed by Subunit - they are only ever | ||
| used on the build machine. Copyright 2004 Canonical Limited. |
+1
-0
@@ -21,1 +21,2 @@ exclude .gitignore | ||
| include NEWS | ||
| recursive-include python/tests *.py |
+33
-0
@@ -5,2 +5,35 @@ --------------------- | ||
| 1.4.6 (2026-05-04) | ||
| --------------------- | ||
| BUG FIXES | ||
| ~~~~~~~~~ | ||
| * Fix compatibility with testtools 2.8.2. (Jelmer Vernooij) | ||
| * Fix ``ExecTestCase`` to work without executable permissions on the | ||
| test script. (Jelmer Vernooij) | ||
| * Fix ``ExecTestCase`` to properly handle shell scripts. (Jelmer Vernooij) | ||
| IMPROVEMENTS | ||
| ~~~~~~~~~~~~ | ||
| * Stop installing tests. (Jelmer Vernooij) | ||
| * Add ``subunit-combine`` script that runs multiple subunit-producing | ||
| commands and merges their streams, optionally prefixing each command's | ||
| test ids. (Jelmer Vernooij, #2150097) | ||
| * ``subunit-combine`` supports testr-style ``$LISTOPT``, ``$IDOPTION``, | ||
| ``$IDFILE`` and ``$IDLIST`` placeholders in command argv, along with | ||
| ``--list``, ``--load-list`` and positional test ids for per-command | ||
| filtering. (Jelmer Vernooij) | ||
| * Add ``gojson2subunit`` script for converting ``go test -json`` streams | ||
| to subunit. (Jelmer Vernooij) | ||
| * Add ``junitxml2subunit`` script for converting JUnit XML test reports | ||
| to subunit. (Jelmer Vernooij) | ||
| 1.4.5 (2025-11-10) | ||
@@ -7,0 +40,0 @@ --------------------- |
+13
-8
| Metadata-Version: 2.4 | ||
| Name: python-subunit | ||
| Version: 1.4.5 | ||
| Version: 1.4.6 | ||
| Summary: Python implementation of subunit test streaming protocol | ||
@@ -24,2 +24,3 @@ Author-email: Robert Collins <subunit-dev@lists.launchpad.net> | ||
| Requires-Dist: iso8601 | ||
| Requires-Dist: PyYAML | ||
| Requires-Dist: testtools>=2.7 | ||
@@ -153,2 +154,7 @@ Provides-Extra: docs | ||
| ### perl | ||
| There is a perl module [Test::Subunit](https://metacpan.org/pod/Test::Subunit) | ||
| that can be used to emit and consume subunit v1 streams. | ||
| #### Filter recipes | ||
@@ -510,8 +516,7 @@ | ||
| * Update versions in configure.ac and python/subunit/__init__.py. | ||
| * Update NEWS. | ||
| * Do a make distcheck, which will update Makefile etc. | ||
| * Do a PyPI release: PYTHONPATH=python python setup.py sdist bdist_wheel; twine upload -s dist/* | ||
| * Upload the regular one to LP. | ||
| * Push a tagged commit. | ||
| git push -t origin master:master | ||
| * Update versions in `configure.ac` and `python/subunit/__init__.py`. | ||
| * Update `NEWS`. | ||
| * Do a ``make distcheck``, which will update `Makefile` etc. | ||
| * Push a tagged commit — this triggers the `publish.yml` GitHub Actions workflow, | ||
| which publishes to PyPI automatically via trusted publishing (OIDC), no API token needed: | ||
| ``git push --tags origin master:master`` |
+13
-1
@@ -8,2 +8,11 @@ [build-system] | ||
| [[tool.mypy.overrides]] | ||
| module = [ | ||
| "gi", | ||
| "gi.repository", | ||
| "junitxml", | ||
| "testscenarios", | ||
| ] | ||
| ignore_missing_imports = true | ||
| [project] | ||
@@ -24,3 +33,3 @@ authors = [ | ||
| ] | ||
| dependencies = ["iso8601", "testtools>=2.7"] | ||
| dependencies = ["iso8601", "PyYAML", "testtools>=2.7"] | ||
| description = "Python implementation of subunit test streaming protocol" | ||
@@ -50,2 +59,3 @@ dynamic = ["version"] | ||
| "subunit-2to1" = "subunit.filter_scripts.subunit_2to1:main" | ||
| "subunit-combine" = "subunit.filter_scripts.subunit_combine:main" | ||
| "subunit-filter" = "subunit.filter_scripts.subunit_filter:main" | ||
@@ -62,2 +72,4 @@ "subunit-ls" = "subunit.filter_scripts.subunit_ls:main" | ||
| "subunit2pyunit" = "subunit.filter_scripts.subunit2pyunit:main" | ||
| "gojson2subunit" = "subunit.filter_scripts.gojson2subunit:main" | ||
| "junitxml2subunit" = "subunit.filter_scripts.junitxml2subunit:main" | ||
| "tap2subunit" = "subunit.filter_scripts.tap2subunit:main" | ||
@@ -64,0 +76,0 @@ |
| [console_scripts] | ||
| gojson2subunit = subunit.filter_scripts.gojson2subunit:main | ||
| junitxml2subunit = subunit.filter_scripts.junitxml2subunit:main | ||
| subunit-1to2 = subunit.filter_scripts.subunit_1to2:main | ||
| subunit-2to1 = subunit.filter_scripts.subunit_2to1:main | ||
| subunit-combine = subunit.filter_scripts.subunit_combine:main | ||
| subunit-filter = subunit.filter_scripts.subunit_filter:main | ||
@@ -5,0 +8,0 @@ subunit-ls = subunit.filter_scripts.subunit_ls:main |
| Metadata-Version: 2.4 | ||
| Name: python-subunit | ||
| Version: 1.4.5 | ||
| Version: 1.4.6 | ||
| Summary: Python implementation of subunit test streaming protocol | ||
@@ -24,2 +24,3 @@ Author-email: Robert Collins <subunit-dev@lists.launchpad.net> | ||
| Requires-Dist: iso8601 | ||
| Requires-Dist: PyYAML | ||
| Requires-Dist: testtools>=2.7 | ||
@@ -153,2 +154,7 @@ Provides-Extra: docs | ||
| ### perl | ||
| There is a perl module [Test::Subunit](https://metacpan.org/pod/Test::Subunit) | ||
| that can be used to emit and consume subunit v1 streams. | ||
| #### Filter recipes | ||
@@ -510,8 +516,7 @@ | ||
| * Update versions in configure.ac and python/subunit/__init__.py. | ||
| * Update NEWS. | ||
| * Do a make distcheck, which will update Makefile etc. | ||
| * Do a PyPI release: PYTHONPATH=python python setup.py sdist bdist_wheel; twine upload -s dist/* | ||
| * Upload the regular one to LP. | ||
| * Push a tagged commit. | ||
| git push -t origin master:master | ||
| * Update versions in `configure.ac` and `python/subunit/__init__.py`. | ||
| * Update `NEWS`. | ||
| * Do a ``make distcheck``, which will update `Makefile` etc. | ||
| * Push a tagged commit — this triggers the `publish.yml` GitHub Actions workflow, | ||
| which publishes to PyPI automatically via trusted publishing (OIDC), no API token needed: | ||
| ``git push --tags origin master:master`` |
| iso8601 | ||
| PyYAML | ||
| testtools>=2.7 | ||
@@ -3,0 +4,0 @@ |
@@ -1,5 +0,1 @@ | ||
| .codespellrc | ||
| .git-blame-ignore-revs | ||
| Apache-2.0 | ||
| BSD | ||
| COPYING | ||
@@ -9,8 +5,4 @@ MANIFEST.in | ||
| README.md | ||
| all_tests.py | ||
| disperse.toml | ||
| pyproject.toml | ||
| setup.py | ||
| .github/dependabot.yaml | ||
| .github/workflows/main.yml | ||
| python/python_subunit.egg-info/PKG-INFO | ||
@@ -33,2 +25,4 @@ python/python_subunit.egg-info/SOURCES.txt | ||
| python/subunit/filter_scripts/__init__.py | ||
| python/subunit/filter_scripts/gojson2subunit.py | ||
| python/subunit/filter_scripts/junitxml2subunit.py | ||
| python/subunit/filter_scripts/subunit2csv.py | ||
@@ -41,2 +35,3 @@ python/subunit/filter_scripts/subunit2disk.py | ||
| python/subunit/filter_scripts/subunit_2to1.py | ||
| python/subunit/filter_scripts/subunit_combine.py | ||
| python/subunit/filter_scripts/subunit_filter.py | ||
@@ -49,21 +44,21 @@ python/subunit/filter_scripts/subunit_ls.py | ||
| python/subunit/filter_scripts/tap2subunit.py | ||
| python/subunit/tests/__init__.py | ||
| python/subunit/tests/sample-script.py | ||
| python/subunit/tests/sample-two-script.py | ||
| python/subunit/tests/test_chunked.py | ||
| python/subunit/tests/test_details.py | ||
| python/subunit/tests/test_filter_to_disk.py | ||
| python/subunit/tests/test_filters.py | ||
| python/subunit/tests/test_output_filter.py | ||
| python/subunit/tests/test_progress_model.py | ||
| python/subunit/tests/test_run.py | ||
| python/subunit/tests/test_subunit_filter.py | ||
| python/subunit/tests/test_subunit_stats.py | ||
| python/subunit/tests/test_subunit_tags.py | ||
| python/subunit/tests/test_tap2subunit.py | ||
| python/subunit/tests/test_test_protocol.py | ||
| python/subunit/tests/test_test_protocol2.py | ||
| python/subunit/tests/test_test_results.py | ||
| vim/README.md | ||
| vim/ftdetect/subunit.vim | ||
| vim/syntax/subunit.vim | ||
| python/tests/__init__.py | ||
| python/tests/sample-script.py | ||
| python/tests/sample-two-script.py | ||
| python/tests/test_chunked.py | ||
| python/tests/test_details.py | ||
| python/tests/test_filter_to_disk.py | ||
| python/tests/test_filters.py | ||
| python/tests/test_gojson2subunit.py | ||
| python/tests/test_junitxml2subunit.py | ||
| python/tests/test_output_filter.py | ||
| python/tests/test_progress_model.py | ||
| python/tests/test_run.py | ||
| python/tests/test_subunit_combine.py | ||
| python/tests/test_subunit_filter.py | ||
| python/tests/test_subunit_stats.py | ||
| python/tests/test_subunit_tags.py | ||
| python/tests/test_tap2subunit.py | ||
| python/tests/test_test_protocol.py | ||
| python/tests/test_test_protocol2.py | ||
| python/tests/test_test_results.py |
@@ -147,3 +147,3 @@ # | ||
| __version__ = (1, 4, 5, "final", 0) | ||
| __version__ = (1, 4, 6, "final", 0) | ||
@@ -179,8 +179,2 @@ version_string = ".".join(map(str, __version__[:3])) | ||
| def test_suite(): | ||
| import subunit.tests | ||
| return subunit.tests.test_suite() | ||
| def join_dir(base_path, path): | ||
@@ -1124,2 +1118,303 @@ """ | ||
| def GoJSON2SubUnit(gojson, output_stream): | ||
| """Filter a `go test -json` stream into a subunit v2 byte stream. | ||
| `go test -json` (or `go tool test2json`) emits one JSON object per line. | ||
| Each object carries an `Action` (`run`, `output`, `pass`, `fail`, `skip`, | ||
| `pause`, `cont`, `bench`, `start`), a `Package`, an optional `Test`, and | ||
| on terminal events an `Elapsed` and `Time`. This function maps each | ||
| test's lifecycle to a pair of subunit packets — `inprogress` at the | ||
| `run` event and the final status at the terminal event — so the | ||
| consumer can derive a duration. Captured `output` lines for the test | ||
| are folded into a single `text/plain; charset=UTF8` attachment on the | ||
| terminal packet. | ||
| Test IDs are formed as ``<package>.<TestName>``. Subtests keep Go's | ||
| native ``Parent/Sub`` form, so the resulting ID is | ||
| ``<package>.<Parent>/<Sub>``, which round-trips through | ||
| ``go test -run '^Parent$/^Sub$'``. | ||
| Package-level failures (no `Test` field on a `fail` event — typically | ||
| a build error) are reported as a synthetic ``<package> [build]`` test | ||
| so the failure shows up alongside the test results instead of being | ||
| silently swallowed. | ||
| :param gojson: An iterable of text lines (e.g. ``sys.stdin``) carrying | ||
| the `go test -json` stream. | ||
| :param output_stream: A binary stream to write subunit v2 bytes to. | ||
| :return: 0 if no test failed, 1 otherwise — matching the convention | ||
| used by `TAP2SubUnit`. | ||
| """ | ||
| import json | ||
| output = StreamResultToBytes(output_stream) | ||
| UTF8_TEXT = "text/plain; charset=UTF8" | ||
| # Per-test buffered `output` chunks plus the start timestamp captured | ||
| # on the `run` event, keyed by full test_id. | ||
| buffers = {} | ||
| start_times = {} | ||
| # Per-package buffered output, used to attribute build / setup failures | ||
| # that don't carry a `Test` field. | ||
| pkg_buffers = {} | ||
| any_failed = False | ||
| def parse_time(value): | ||
| if not value: | ||
| return None | ||
| try: | ||
| return iso8601.parse_date(value) | ||
| except (TypeError, ValueError, iso8601.ParseError): | ||
| return None | ||
| def make_test_id(pkg, test): | ||
| # Both package and test are required for an unambiguous ID; the | ||
| # caller checks for `Test` before reaching here. | ||
| return "{}.{}".format(pkg, test) | ||
| for line in gojson: | ||
| line = line.strip() | ||
| if not line: | ||
| continue | ||
| try: | ||
| event = json.loads(line) | ||
| except (TypeError, ValueError): | ||
| # `go test -json` occasionally interleaves a non-JSON banner | ||
| # (e.g. on a panic during package init). Drop it rather than | ||
| # aborting the whole stream. | ||
| continue | ||
| if not isinstance(event, dict): | ||
| continue | ||
| action = event.get("Action") | ||
| pkg = event.get("Package") or "" | ||
| test = event.get("Test") | ||
| timestamp = parse_time(event.get("Time")) | ||
| if action == "output": | ||
| chunk = event.get("Output", "") | ||
| if test: | ||
| buffers.setdefault(make_test_id(pkg, test), []).append(chunk) | ||
| elif pkg: | ||
| pkg_buffers.setdefault(pkg, []).append(chunk) | ||
| continue | ||
| if action == "run" and test: | ||
| test_id = make_test_id(pkg, test) | ||
| start_times[test_id] = timestamp | ||
| output.status( | ||
| test_id=test_id, | ||
| test_status="inprogress", | ||
| timestamp=timestamp, | ||
| ) | ||
| continue | ||
| if action in ("pass", "fail", "skip") and test: | ||
| status = {"pass": "success", "fail": "fail", "skip": "skip"}[action] | ||
| test_id = make_test_id(pkg, test) | ||
| chunks = buffers.pop(test_id, []) | ||
| start_times.pop(test_id, None) | ||
| file_bytes = ("".join(chunks)).encode("utf-8") if chunks else None | ||
| output.status( | ||
| test_id=test_id, | ||
| test_status=status, | ||
| eof=True, | ||
| file_name="go test output" if file_bytes else None, | ||
| file_bytes=file_bytes, | ||
| mime_type=UTF8_TEXT if file_bytes else None, | ||
| timestamp=timestamp, | ||
| ) | ||
| if action == "fail": | ||
| any_failed = True | ||
| continue | ||
| if action == "fail" and not test and pkg: | ||
| # Package-level failure (build error, init panic, etc.). Emit | ||
| # a synthetic test so the failure is visible. | ||
| chunks = pkg_buffers.pop(pkg, []) | ||
| file_bytes = ("".join(chunks)).encode("utf-8") if chunks else None | ||
| output.status( | ||
| test_id="{} [build]".format(pkg), | ||
| test_status="fail", | ||
| eof=True, | ||
| file_name="go test output" if file_bytes else None, | ||
| file_bytes=file_bytes, | ||
| mime_type=UTF8_TEXT if file_bytes else None, | ||
| timestamp=timestamp, | ||
| ) | ||
| any_failed = True | ||
| continue | ||
| # `pass`/`skip` without `Test` is a package-level summary; harmless | ||
| # to drop. `pause`/`cont`/`start`/`bench` aren't terminal — skip. | ||
| # Any tests still in-progress at EOF were aborted (the runner died | ||
| # mid-test). Surface them as failures so they're not silently lost. | ||
| for test_id, chunks in list(buffers.items()): | ||
| file_bytes = ("".join(chunks)).encode("utf-8") if chunks else None | ||
| output.status( | ||
| test_id=test_id, | ||
| test_status="fail", | ||
| eof=True, | ||
| file_name="go test output" if file_bytes else None, | ||
| file_bytes=file_bytes, | ||
| mime_type=UTF8_TEXT if file_bytes else None, | ||
| ) | ||
| any_failed = True | ||
| return 1 if any_failed else 0 | ||
| def JUnitXML2SubUnit(xml_files, output_stream): | ||
| """Convert JUnit XML test reports to a subunit v2 byte stream. | ||
| Reads each path in ``xml_files`` (in the supplied order) and emits | ||
| one subunit packet pair per ``<testcase>`` element. The packet pair | ||
| is ``inprogress`` followed by the terminal status, with synthetic | ||
| timestamps spaced by the testcase's ``time`` attribute so consumers | ||
| can recover the recorded duration. | ||
| Test IDs are formed as ``<classname>::<name>`` from the testcase's | ||
| ``classname`` and ``name`` attributes. Maven Surefire and Gradle | ||
| both populate ``classname`` with the fully-qualified Java class | ||
| (e.g. ``com.example.FooTest``), so the resulting ID is | ||
| ``com.example.FooTest::testBar``. | ||
| Mapping rules: | ||
| * ``<failure>`` child → status ``fail`` (an assertion failure). | ||
| * ``<error>`` child → status ``fail`` (an unexpected exception); | ||
| subunit doesn't distinguish these and the JUnit author intent is | ||
| identical for our purposes (both mean "did not pass"). | ||
| * ``<skipped>`` child → status ``skip``. | ||
| * Otherwise → status ``success``. | ||
| The body text and ``message``/``type`` attributes of the failure | ||
| element are folded into a single ``text/plain`` attachment on the | ||
| terminal packet, mirroring how ``GoJSON2SubUnit`` attaches captured | ||
| stdout to its results. | ||
| Per-class ``<system-out>`` / ``<system-err>`` blocks aren't attributed | ||
| to individual testcases by the JUnit schema (they cover the whole | ||
| suite). They're dropped — preserving them would require attaching | ||
| them to a synthetic suite-level packet, and most consumers don't | ||
| surface that. | ||
| :param xml_files: Iterable of file paths containing JUnit XML. | ||
| :param output_stream: A binary stream to write subunit v2 bytes to. | ||
| :return: 0 if no testcase failed or errored, 1 otherwise. Files that | ||
| fail to parse are reported on stderr and counted as a failure so | ||
| the broken XML doesn't get silently swallowed. | ||
| """ | ||
| import datetime | ||
| import xml.etree.ElementTree as ET | ||
| output = StreamResultToBytes(output_stream) | ||
| UTF8_TEXT = "text/plain; charset=UTF8" | ||
| any_failed = False | ||
| # Synthetic timestamps. We don't know when the JUnit run actually | ||
| # happened, but spacing the inprogress/terminal packets by each | ||
| # testcase's recorded `time` attribute lets consumers compute the | ||
| # right duration without making up wall-clock data. | ||
| clock = datetime.datetime(2000, 1, 1, tzinfo=datetime.timezone.utc) | ||
| def parse_time(value): | ||
| if value is None: | ||
| return 0.0 | ||
| try: | ||
| return float(value) | ||
| except (TypeError, ValueError): | ||
| return 0.0 | ||
| def iter_testsuites(root): | ||
| # JUnit XML files come in two shapes: a single ``<testsuite>`` | ||
| # at the root, or a ``<testsuites>`` wrapper containing many. | ||
| if root.tag == "testsuite": | ||
| yield root | ||
| elif root.tag == "testsuites": | ||
| for ts in root.findall("testsuite"): | ||
| yield ts | ||
| # Anything else is silently ignored — a non-JUnit document. | ||
| for path in xml_files: | ||
| try: | ||
| tree = ET.parse(path) | ||
| except (OSError, ET.ParseError) as exc: | ||
| sys.stderr.write("JUnitXML2SubUnit: failed to parse {}: {}\n".format(path, exc)) | ||
| any_failed = True | ||
| continue | ||
| root = tree.getroot() | ||
| for suite in iter_testsuites(root): | ||
| for case in suite.findall("testcase"): | ||
| classname = case.get("classname") or "" | ||
| name = case.get("name") or "" | ||
| if not name: | ||
| # Without a name there's no usable test_id; skip | ||
| # rather than emit a malformed ID. | ||
| continue | ||
| test_id = "{}::{}".format(classname, name) if classname else name | ||
| duration = parse_time(case.get("time")) | ||
| failure = case.find("failure") | ||
| error = case.find("error") | ||
| skipped = case.find("skipped") | ||
| if failure is not None or error is not None: | ||
| status = "fail" | ||
| detail = failure if failure is not None else error | ||
| file_bytes = _format_junit_detail(detail) | ||
| any_failed = True | ||
| elif skipped is not None: | ||
| status = "skip" | ||
| file_bytes = _format_junit_detail(skipped) | ||
| else: | ||
| status = "success" | ||
| file_bytes = None | ||
| start_ts = clock | ||
| end_ts = clock + datetime.timedelta(seconds=duration) | ||
| clock = end_ts | ||
| output.status( | ||
| test_id=test_id, | ||
| test_status="inprogress", | ||
| timestamp=start_ts, | ||
| ) | ||
| output.status( | ||
| test_id=test_id, | ||
| test_status=status, | ||
| eof=True, | ||
| file_name="junit detail" if file_bytes else None, | ||
| file_bytes=file_bytes, | ||
| mime_type=UTF8_TEXT if file_bytes else None, | ||
| timestamp=end_ts, | ||
| ) | ||
| return 1 if any_failed else 0 | ||
| def _format_junit_detail(element): | ||
| """Serialise a ``<failure>``/``<error>``/``<skipped>`` body to bytes. | ||
| JUnit elements carry the message and exception type as attributes and | ||
| the stack trace as element text. Combine both into a single | ||
| text/plain blob so the consumer sees everything on one packet. Returns | ||
| ``None`` when there's nothing to attach (an empty ``<skipped/>``). | ||
| """ | ||
| parts = [] | ||
| msg = element.get("message") | ||
| typ = element.get("type") | ||
| if typ and msg: | ||
| parts.append("{}: {}".format(typ, msg)) | ||
| elif typ: | ||
| parts.append(typ) | ||
| elif msg: | ||
| parts.append(msg) | ||
| body = (element.text or "").strip() | ||
| if body: | ||
| parts.append(body) | ||
| if not parts: | ||
| return None | ||
| return ("\n".join(parts) + "\n").encode("utf-8") | ||
| def tag_stream(original, filtered, tags): | ||
@@ -1126,0 +1421,0 @@ """Alter tags on a stream. |
@@ -51,3 +51,3 @@ # | ||
| # Contains True for types for which 'nul in thing' falsely returns false. | ||
| _nul_test_broken = {} | ||
| _nul_test_broken: dict[type, bool] = {} | ||
@@ -54,0 +54,0 @@ |
+11
-7
@@ -120,2 +120,7 @@ | ||
| ### perl | ||
| There is a perl module [Test::Subunit](https://metacpan.org/pod/Test::Subunit) | ||
| that can be used to emit and consume subunit v1 streams. | ||
| #### Filter recipes | ||
@@ -477,8 +482,7 @@ | ||
| * Update versions in configure.ac and python/subunit/__init__.py. | ||
| * Update NEWS. | ||
| * Do a make distcheck, which will update Makefile etc. | ||
| * Do a PyPI release: PYTHONPATH=python python setup.py sdist bdist_wheel; twine upload -s dist/* | ||
| * Upload the regular one to LP. | ||
| * Push a tagged commit. | ||
| git push -t origin master:master | ||
| * Update versions in `configure.ac` and `python/subunit/__init__.py`. | ||
| * Update `NEWS`. | ||
| * Do a ``make distcheck``, which will update `Makefile` etc. | ||
| * Push a tagged commit — this triggers the `publish.yml` GitHub Actions workflow, | ||
| which publishes to PyPI automatically via trusted publishing (OIDC), no API token needed: | ||
| ``git push --tags origin master:master`` |
Sorry, the diff of this file is not supported yet
Sorry, the diff of this file is not supported yet
| # Please see the documentation for all configuration options: | ||
| # https://help.github.com/github/administering-a-repository/configuration-options-for-dependency-updates | ||
| version: 2 | ||
| updates: | ||
| - package-ecosystem: "github-actions" | ||
| directory: "/" | ||
| schedule: | ||
| interval: weekly | ||
| - package-ecosystem: "pip" | ||
| directory: "/" | ||
| schedule: | ||
| interval: weekly |
| name: Tests | ||
| on: | ||
| push: | ||
| branches: [master] | ||
| pull_request: | ||
| branches: [master] | ||
| jobs: | ||
| tests: | ||
| name: tests-python${{ matrix.python-version }}-${{ matrix.os }} | ||
| runs-on: ${{ matrix.os }} | ||
| strategy: | ||
| matrix: | ||
| python-version: ["3.10", "3.11", "3.12", "3.13", "3.14", "pypy3.10"] | ||
| os: ["ubuntu-latest"] | ||
| steps: | ||
| - uses: actions/checkout@v5 | ||
| - name: Set up Python ${{ matrix.python-version }} | ||
| uses: actions/setup-python@v6 | ||
| with: | ||
| python-version: ${{ matrix.python-version }} | ||
| - name: Install Deps | ||
| run: sudo apt-get install check libcppunit-dev | ||
| - name: Install package | ||
| run: | | ||
| python -m pip install ruff | ||
| python -m pip install -U '.[test,docs]' | ||
| - name: Build | ||
| run: autoreconf -fi && ./configure && make | ||
| - name: Run ruff check | ||
| run: ruff check python | ||
| - name: Format | ||
| run: ruff format --check . | ||
| - name: Run make check | ||
| run: make check | ||
| - name: Run make distcheck | ||
| run: make distcheck | ||
| # Disabled; needs a tuit or two | ||
| # - name: Docs build | ||
| # run: md2html.py README.md README.html | ||
| # if: "matrix.python-version == '3.8'" | ||
| # - name: Docs build | ||
| # run: md2html README.md README.html | ||
| # if: "matrix.python-version != '3.8'" |
-35
| # | ||
| # subunit: extensions to Python unittest to get test results from subprocesses. | ||
| # Copyright (C) 2013 Robert Collins <robertc@robertcollins.net> | ||
| # | ||
| # 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. | ||
| # | ||
| import unittest | ||
| import subunit | ||
| class ShellTests(subunit.ExecTestCase): | ||
| def test_sourcing(self): | ||
| """./shell/tests/test_source_library.sh""" | ||
| def test_functions(self): | ||
| """./shell/tests/test_function_output.sh""" | ||
| def test_suite(): | ||
| result = unittest.TestSuite() | ||
| result.addTest(subunit.test_suite()) | ||
| result.addTest(ShellTests("test_sourcing")) | ||
| result.addTest(ShellTests("test_functions")) | ||
| return result |
Sorry, the diff of this file is not supported yet
Sorry, the diff of this file is not supported yet
| tag-name = "$VERSION" | ||
| news-file = "NEWS" | ||
| verify-command = "autoreconf -i && ./configure && make distcheck" | ||
| tarball-location = [] | ||
| release-timeout = 5 | ||
| [[update_version]] | ||
| path = "configure.ac" | ||
| new-line = "m4_define([SUBUNIT_MAJOR_VERSION], [$MAJOR_VERSION])" | ||
| [[update_version]] | ||
| path = "configure.ac" | ||
| new-line = "m4_define([SUBUNIT_MINOR_VERSION], [$MINOR_VERSION])" | ||
| [[update_version]] | ||
| path = "configure.ac" | ||
| new-line = "m4_define([SUBUNIT_MICRO_VERSION], [$MICRO_VERSION])" | ||
| [[update_version]] | ||
| path = "python/subunit/__init__.py" | ||
| new-line = "__version__ = $STATUS_TUPLED_VERSION" | ||
| [launchpad] | ||
| project = "subunit" |
| # | ||
| # subunit: extensions to python unittest to get test results from subprocesses. | ||
| # Copyright (C) 2005 Robert Collins <robertc@robertcollins.net> | ||
| # | ||
| # 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 unittest import TestLoader | ||
| from testscenarios import generate_scenarios | ||
| # Before the test module imports to avoid circularity. | ||
| # For testing: different pythons have different str() implementations. | ||
| _remote_exception_repr = "testtools.testresult.real._StringException" | ||
| _remote_exception_repr_chunked = "34\r\n" + _remote_exception_repr + ": boo qux\n0\r\n" | ||
| _remote_exception_str = "Traceback (most recent call last):\ntesttools.testresult.real._StringException" | ||
| _remote_exception_str_chunked = "57\r\n" + _remote_exception_str + ": boo qux\n0\r\n" | ||
| from subunit.tests import ( # noqa: E402 | ||
| test_chunked, | ||
| test_details, | ||
| test_filter_to_disk, | ||
| test_filters, | ||
| test_output_filter, | ||
| test_progress_model, | ||
| test_run, | ||
| test_subunit_filter, | ||
| test_subunit_stats, | ||
| test_subunit_tags, | ||
| test_tap2subunit, | ||
| test_test_protocol, | ||
| test_test_protocol2, | ||
| test_test_results, | ||
| ) | ||
| def test_suite(): | ||
| loader = TestLoader() | ||
| result = loader.loadTestsFromModule(test_chunked) | ||
| result.addTest(loader.loadTestsFromModule(test_details)) | ||
| result.addTest(loader.loadTestsFromModule(test_filters)) | ||
| result.addTest(loader.loadTestsFromModule(test_progress_model)) | ||
| result.addTest(loader.loadTestsFromModule(test_test_results)) | ||
| result.addTest(loader.loadTestsFromModule(test_test_protocol)) | ||
| result.addTest(loader.loadTestsFromModule(test_test_protocol2)) | ||
| result.addTest(loader.loadTestsFromModule(test_tap2subunit)) | ||
| result.addTest(loader.loadTestsFromModule(test_filter_to_disk)) | ||
| result.addTest(loader.loadTestsFromModule(test_subunit_filter)) | ||
| result.addTest(loader.loadTestsFromModule(test_subunit_tags)) | ||
| result.addTest(loader.loadTestsFromModule(test_subunit_stats)) | ||
| result.addTest(loader.loadTestsFromModule(test_run)) | ||
| result.addTests(generate_scenarios(loader.loadTestsFromModule(test_output_filter))) | ||
| return result |
| #!/usr/bin/env python3 | ||
| import sys | ||
| if sys.platform == "win32": | ||
| import msvcrt | ||
| import os | ||
| msvcrt.setmode(sys.stdout.fileno(), os.O_BINARY) | ||
| if len(sys.argv) == 2: | ||
| # subunit.tests.test_test_protocol.TestExecTestCase.test_sample_method_args | ||
| # uses this code path to be sure that the arguments were passed to | ||
| # sample-script.py | ||
| print("test fail") | ||
| print("error fail") | ||
| sys.exit(0) | ||
| print("test old mcdonald") | ||
| print("success old mcdonald") | ||
| print("test bing crosby") | ||
| print("failure bing crosby [") | ||
| print("foo.c:53:ERROR invalid state") | ||
| print("]") | ||
| print("test an error") | ||
| print("error an error") | ||
| sys.exit(0) |
| #!/usr/bin/env python3 | ||
| import sys | ||
| print("test old mcdonald") | ||
| print("success old mcdonald") | ||
| print("test bing crosby") | ||
| print("success bing crosby") | ||
| sys.exit(0) |
| # | ||
| # subunit: extensions to python unittest to get test results from subprocesses. | ||
| # Copyright (C) 2005 Robert Collins <robertc@robertcollins.net> | ||
| # Copyright (C) 2011 Martin Pool <mbp@sourcefrog.net> | ||
| # | ||
| # 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. | ||
| # | ||
| import unittest | ||
| from io import BytesIO | ||
| import subunit.chunked | ||
| class TestDecode(unittest.TestCase): | ||
| def setUp(self): | ||
| unittest.TestCase.setUp(self) | ||
| self.output = BytesIO() | ||
| self.decoder = subunit.chunked.Decoder(self.output) | ||
| def test_close_read_length_short_errors(self): | ||
| self.assertRaises(ValueError, self.decoder.close) | ||
| def test_close_body_short_errors(self): | ||
| self.assertEqual(None, self.decoder.write(b"2\r\na")) | ||
| self.assertRaises(ValueError, self.decoder.close) | ||
| def test_close_body_buffered_data_errors(self): | ||
| self.assertEqual(None, self.decoder.write(b"2\r")) | ||
| self.assertRaises(ValueError, self.decoder.close) | ||
| def test_close_after_finished_stream_safe(self): | ||
| self.assertEqual(None, self.decoder.write(b"2\r\nab")) | ||
| self.assertEqual(b"", self.decoder.write(b"0\r\n")) | ||
| self.decoder.close() | ||
| def test_decode_nothing(self): | ||
| self.assertEqual(b"", self.decoder.write(b"0\r\n")) | ||
| self.assertEqual(b"", self.output.getvalue()) | ||
| def test_decode_serialised_form(self): | ||
| self.assertEqual(None, self.decoder.write(b"F\r\n")) | ||
| self.assertEqual(None, self.decoder.write(b"serialised\n")) | ||
| self.assertEqual(b"", self.decoder.write(b"form0\r\n")) | ||
| def test_decode_short(self): | ||
| self.assertEqual(b"", self.decoder.write(b"3\r\nabc0\r\n")) | ||
| self.assertEqual(b"abc", self.output.getvalue()) | ||
| def test_decode_combines_short(self): | ||
| self.assertEqual(b"", self.decoder.write(b"6\r\nabcdef0\r\n")) | ||
| self.assertEqual(b"abcdef", self.output.getvalue()) | ||
| def test_decode_excess_bytes_from_write(self): | ||
| self.assertEqual(b"1234", self.decoder.write(b"3\r\nabc0\r\n1234")) | ||
| self.assertEqual(b"abc", self.output.getvalue()) | ||
| def test_decode_write_after_finished_errors(self): | ||
| self.assertEqual(b"1234", self.decoder.write(b"3\r\nabc0\r\n1234")) | ||
| self.assertRaises(ValueError, self.decoder.write, b"") | ||
| def test_decode_hex(self): | ||
| self.assertEqual(b"", self.decoder.write(b"A\r\n12345678900\r\n")) | ||
| self.assertEqual(b"1234567890", self.output.getvalue()) | ||
| def test_decode_long_ranges(self): | ||
| self.assertEqual(None, self.decoder.write(b"10000\r\n")) | ||
| self.assertEqual(None, self.decoder.write(b"1" * 65536)) | ||
| self.assertEqual(None, self.decoder.write(b"10000\r\n")) | ||
| self.assertEqual(None, self.decoder.write(b"2" * 65536)) | ||
| self.assertEqual(b"", self.decoder.write(b"0\r\n")) | ||
| self.assertEqual(b"1" * 65536 + b"2" * 65536, self.output.getvalue()) | ||
| def test_decode_newline_nonstrict(self): | ||
| """Tolerate chunk markers with no CR character.""" | ||
| # From <http://pad.lv/505078> | ||
| self.decoder = subunit.chunked.Decoder(self.output, strict=False) | ||
| self.assertEqual(None, self.decoder.write(b"a\n")) | ||
| self.assertEqual(None, self.decoder.write(b"abcdeabcde")) | ||
| self.assertEqual(b"", self.decoder.write(b"0\n")) | ||
| self.assertEqual(b"abcdeabcde", self.output.getvalue()) | ||
| def test_decode_strict_newline_only(self): | ||
| """Reject chunk markers with no CR character in strict mode.""" | ||
| # From <http://pad.lv/505078> | ||
| self.assertRaises(ValueError, self.decoder.write, b"a\n") | ||
| def test_decode_strict_multiple_crs(self): | ||
| self.assertRaises(ValueError, self.decoder.write, b"a\r\r\n") | ||
| def test_decode_short_header(self): | ||
| self.assertRaises(ValueError, self.decoder.write, b"\n") | ||
| class TestEncode(unittest.TestCase): | ||
| def setUp(self): | ||
| unittest.TestCase.setUp(self) | ||
| self.output = BytesIO() | ||
| self.encoder = subunit.chunked.Encoder(self.output) | ||
| def test_encode_nothing(self): | ||
| self.encoder.close() | ||
| self.assertEqual(b"0\r\n", self.output.getvalue()) | ||
| def test_encode_empty(self): | ||
| self.encoder.write(b"") | ||
| self.encoder.close() | ||
| self.assertEqual(b"0\r\n", self.output.getvalue()) | ||
| def test_encode_short(self): | ||
| self.encoder.write(b"abc") | ||
| self.encoder.close() | ||
| self.assertEqual(b"3\r\nabc0\r\n", self.output.getvalue()) | ||
| def test_encode_combines_short(self): | ||
| self.encoder.write(b"abc") | ||
| self.encoder.write(b"def") | ||
| self.encoder.close() | ||
| self.assertEqual(b"6\r\nabcdef0\r\n", self.output.getvalue()) | ||
| def test_encode_over_9_is_in_hex(self): | ||
| self.encoder.write(b"1234567890") | ||
| self.encoder.close() | ||
| self.assertEqual(b"A\r\n12345678900\r\n", self.output.getvalue()) | ||
| def test_encode_long_ranges_not_combined(self): | ||
| self.encoder.write(b"1" * 65536) | ||
| self.encoder.write(b"2" * 65536) | ||
| self.encoder.close() | ||
| self.assertEqual(b"10000\r\n" + b"1" * 65536 + b"10000\r\n" + b"2" * 65536 + b"0\r\n", self.output.getvalue()) |
| # | ||
| # subunit: extensions to python unittest to get test results from subprocesses. | ||
| # Copyright (C) 2005 Robert Collins <robertc@robertcollins.net> | ||
| # | ||
| # 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. | ||
| # | ||
| import unittest | ||
| from subunit import content, content_type, details | ||
| class TestSimpleDetails(unittest.TestCase): | ||
| def test_lineReceived(self): | ||
| parser = details.SimpleDetailsParser(None) | ||
| parser.lineReceived(b"foo\n") | ||
| parser.lineReceived(b"bar\n") | ||
| self.assertEqual(b"foo\nbar\n", parser._message) | ||
| def test_lineReceived_escaped_bracket(self): | ||
| parser = details.SimpleDetailsParser(None) | ||
| parser.lineReceived(b"foo\n") | ||
| parser.lineReceived(b" ]are\n") | ||
| parser.lineReceived(b"bar\n") | ||
| self.assertEqual(b"foo\n]are\nbar\n", parser._message) | ||
| def test_get_message(self): | ||
| parser = details.SimpleDetailsParser(None) | ||
| self.assertEqual(b"", parser.get_message()) | ||
| def test_get_details(self): | ||
| parser = details.SimpleDetailsParser(None) | ||
| expected = {} | ||
| expected["traceback"] = content.Content( | ||
| content_type.ContentType("text", "x-traceback", {"charset": "utf8"}), lambda: [b""] | ||
| ) | ||
| found = parser.get_details() | ||
| self.assertEqual(expected.keys(), found.keys()) | ||
| self.assertEqual(expected["traceback"].content_type, found["traceback"].content_type) | ||
| self.assertEqual(b"".join(expected["traceback"].iter_bytes()), b"".join(found["traceback"].iter_bytes())) | ||
| def test_get_details_skip(self): | ||
| parser = details.SimpleDetailsParser(None) | ||
| expected = {} | ||
| expected["reason"] = content.Content(content_type.ContentType("text", "plain"), lambda: [b""]) | ||
| found = parser.get_details("skip") | ||
| self.assertEqual(expected, found) | ||
| def test_get_details_success(self): | ||
| parser = details.SimpleDetailsParser(None) | ||
| expected = {} | ||
| expected["message"] = content.Content(content_type.ContentType("text", "plain"), lambda: [b""]) | ||
| found = parser.get_details("success") | ||
| self.assertEqual(expected, found) | ||
| class TestMultipartDetails(unittest.TestCase): | ||
| def test_get_message_is_None(self): | ||
| parser = details.MultipartDetailsParser(None) | ||
| self.assertEqual(None, parser.get_message()) | ||
| def test_get_details(self): | ||
| parser = details.MultipartDetailsParser(None) | ||
| self.assertEqual({}, parser.get_details()) | ||
| def test_parts(self): | ||
| parser = details.MultipartDetailsParser(None) | ||
| parser.lineReceived(b"Content-Type: text/plain\n") | ||
| parser.lineReceived(b"something\n") | ||
| parser.lineReceived(b"F\r\n") | ||
| parser.lineReceived(b"serialised\n") | ||
| parser.lineReceived(b"form0\r\n") | ||
| expected = {} | ||
| expected["something"] = content.Content( | ||
| content_type.ContentType("text", "plain"), lambda: [b"serialised\nform"] | ||
| ) | ||
| found = parser.get_details() | ||
| self.assertEqual(expected.keys(), found.keys()) | ||
| self.assertEqual(expected["something"].content_type, found["something"].content_type) | ||
| self.assertEqual(b"".join(expected["something"].iter_bytes()), b"".join(found["something"].iter_bytes())) |
| # | ||
| # subunit: extensions to python unittest to get test results from subprocesses. | ||
| # 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 | ||
| # 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. | ||
| import io | ||
| import os.path | ||
| from fixtures import TempDir | ||
| from testtools import TestCase | ||
| from testtools.matchers import FileContains | ||
| from subunit import _to_disk | ||
| from subunit.v2 import StreamResultToBytes | ||
| class SmokeTest(TestCase): | ||
| def test_smoke(self): | ||
| output = os.path.join(self.useFixture(TempDir()).path, "output") | ||
| stdin = io.BytesIO() | ||
| stdout = io.StringIO() | ||
| writer = StreamResultToBytes(stdin) | ||
| writer.startTestRun() | ||
| writer.status( | ||
| "foo", "success", {"tag"}, file_name="fred", file_bytes=b"abcdefg", eof=True, mime_type="text/plain" | ||
| ) | ||
| writer.stopTestRun() | ||
| stdin.seek(0) | ||
| _to_disk.to_disk(["-d", output], stdin=stdin, stdout=stdout) | ||
| self.expectThat( | ||
| os.path.join(output, "foo/test.json"), | ||
| FileContains( | ||
| '{"details": ["fred"], "id": "foo", "start": null, "status": "success", "stop": null, "tags": ["tag"]}' | ||
| ), | ||
| ) | ||
| self.expectThat(os.path.join(output, "foo/fred"), FileContains("abcdefg")) |
| # | ||
| # subunit: extensions to Python unittest to get test results from subprocesses. | ||
| # Copyright (C) 2013 Robert Collins <robertc@robertcollins.net> | ||
| # | ||
| # 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 tempfile import NamedTemporaryFile | ||
| from testtools import TestCase | ||
| from subunit import read_test_list | ||
| from subunit.filters import find_stream | ||
| class TestReadTestList(TestCase): | ||
| def test_read_list(self): | ||
| with NamedTemporaryFile() as f: | ||
| f.write(b"foo\nbar\n# comment\nother # comment\n") | ||
| f.flush() | ||
| self.assertEqual(read_test_list(f.name), ["foo", "bar", "other"]) | ||
| class TestFindStream(TestCase): | ||
| def test_no_argv(self): | ||
| self.assertEqual("foo", find_stream("foo", [])) | ||
| def test_opens_file(self): | ||
| f = NamedTemporaryFile() | ||
| f.write(b"foo") | ||
| f.flush() | ||
| stream = find_stream("bar", [f.name]) | ||
| self.assertEqual(b"foo", stream.read()) |
| # | ||
| # subunit: extensions to python unittest to get test results from subprocesses. | ||
| # 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 | ||
| # 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. | ||
| # | ||
| import datetime | ||
| import optparse | ||
| from contextlib import contextmanager | ||
| from functools import partial | ||
| from io import BytesIO, TextIOWrapper | ||
| from tempfile import NamedTemporaryFile | ||
| from iso8601 import UTC | ||
| from testtools import TestCase | ||
| from testtools.matchers import Equals, Matcher, MatchesAny, MatchesListwise, Mismatch, raises | ||
| from testtools.testresult.doubles import StreamResult | ||
| import subunit._output as _o | ||
| from subunit._output import _ALL_ACTIONS, _FINAL_ACTIONS, generate_stream_results, parse_arguments | ||
| class SafeOptionParser(optparse.OptionParser): | ||
| """An ArgumentParser class that doesn't call sys.exit.""" | ||
| 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 TestStatusArgParserTests(TestCase): | ||
| scenarios = [(cmd, dict(command=cmd, option="--" + cmd)) for cmd in _ALL_ACTIONS] | ||
| 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(self.command)) | ||
| self.assertThat(args.test_id, Equals(test_id)) | ||
| def test_all_commands_parse_file_attachment(self): | ||
| with NamedTemporaryFile() as tmp_file: | ||
| args = safe_parse_arguments(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.option, "foo", "--attach-file", tmp_file.name, "--mimetype", "text/plain"] | ||
| ) | ||
| 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", "--tag", "foo", "--tag", "bar", "--tag", "baz"]) | ||
| self.assertThat(args.tags, Equals(["foo", "bar", "baz"])) | ||
| def test_attach_file_with_hyphen_opens_stdin(self): | ||
| 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(b"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")) | ||
| def test_requires_test_id(self): | ||
| def fn(): | ||
| return safe_parse_arguments(args=[self.option]) | ||
| self.assertThat(fn, raises(RuntimeError("argument %s: must specify a single TEST_ID." % self.option))) | ||
| class ArgParserTests(TestCase): | ||
| def test_can_parse_attach_file_without_test_id(self): | ||
| with NamedTemporaryFile() as tmp_file: | ||
| args = safe_parse_arguments(args=["--attach-file", tmp_file.name]) | ||
| self.assertThat(args.attach_file.name, Equals(tmp_file.name)) | ||
| def test_can_run_without_args(self): | ||
| safe_parse_arguments([]) | ||
| def test_cannot_specify_more_than_one_status_command(self): | ||
| def fn(): | ||
| return safe_parse_arguments(["--fail", "foo", "--skip", "bar"]) | ||
| self.assertThat(fn, raises(RuntimeError("argument --skip: Only one status may be specified at once."))) | ||
| def test_cannot_specify_mimetype_without_attach_file(self): | ||
| def fn(): | ||
| return safe_parse_arguments(["--mimetype", "foo"]) | ||
| self.assertThat(fn, raises(RuntimeError("Cannot specify --mimetype without --attach-file"))) | ||
| def test_cannot_specify_filename_without_attach_file(self): | ||
| def fn(): | ||
| return safe_parse_arguments(["--file-name", "foo"]) | ||
| self.assertThat(fn, raises(RuntimeError("Cannot specify --file-name without --attach-file"))) | ||
| 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): | ||
| def fn(): | ||
| return safe_parse_arguments(["--fail", "foo", "--tag"]) | ||
| self.assertThat( | ||
| fn, | ||
| 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. | ||
| 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. | ||
| """ | ||
| result = StreamResult() | ||
| args = safe_parse_arguments(commands) | ||
| generate_stream_results(args, result) | ||
| return result | ||
| @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(TestCase): | ||
| scenarios = [(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().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(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[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[1], MatchesStatusCall(test_tags={"hello", "world"})) | ||
| def test_all_commands_generate_timestamp(self): | ||
| result = get_result_for([self.option, self.test_id]) | ||
| self.assertThat(result._events[1], MatchesStatusCall(timestamp=self._dummy_timestamp)) | ||
| def test_all_commands_generate_correct_test_id(self): | ||
| result = get_result_for([self.option, self.test_id]) | ||
| self.assertThat(result._events[1], 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(call="startTestRun"), | ||
| MatchesStatusCall(file_bytes=b"Hello", eof=True), | ||
| MatchesStatusCall(call="stopTestRun"), | ||
| ] | ||
| ), | ||
| ) | ||
| 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(call="startTestRun"), | ||
| MatchesStatusCall(file_bytes=b"\xde\xad\xbe\xef", eof=True), | ||
| MatchesStatusCall(call="stopTestRun"), | ||
| ] | ||
| ), | ||
| ) | ||
| 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(call="startTestRun"), | ||
| MatchesStatusCall(file_bytes=b"", file_name=f.name, eof=True), | ||
| MatchesStatusCall(call="stopTestRun"), | ||
| ] | ||
| ), | ||
| ) | ||
| def test_can_read_stdin(self): | ||
| 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( | ||
| result._events, | ||
| MatchesListwise( | ||
| [ | ||
| MatchesStatusCall(call="startTestRun"), | ||
| MatchesStatusCall(file_bytes=b"\xfe\xed\xfa\xce", file_name="stdin", eof=True), | ||
| MatchesStatusCall(call="stopTestRun"), | ||
| ] | ||
| ), | ||
| ) | ||
| 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]) | ||
| self.assertThat( | ||
| result._events, | ||
| MatchesListwise( | ||
| [ | ||
| MatchesStatusCall(call="startTestRun"), | ||
| MatchesStatusCall(test_id=self.test_id, file_bytes=b"Hello", eof=True), | ||
| MatchesStatusCall(call="stopTestRun"), | ||
| ] | ||
| ), | ||
| ) | ||
| 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(call="startTestRun"), | ||
| MatchesStatusCall(test_status=self.status, file_bytes=b"Hello", eof=True), | ||
| MatchesStatusCall(call="stopTestRun"), | ||
| ] | ||
| ), | ||
| ) | ||
| def test_file_chunk_size_is_honored(self): | ||
| 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(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"), | ||
| ] | ||
| ), | ||
| ) | ||
| 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", | ||
| ] | ||
| ) | ||
| 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"), | ||
| ] | ||
| ), | ||
| ) | ||
| 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, | ||
| "--tag", | ||
| "foo", | ||
| "--tag", | ||
| "bar", | ||
| ] | ||
| ) | ||
| self.assertThat( | ||
| result._events, | ||
| MatchesListwise( | ||
| [ | ||
| MatchesStatusCall(call="startTestRun"), | ||
| MatchesStatusCall(test_id=self.test_id, test_tags={"foo", "bar"}), | ||
| MatchesStatusCall(test_id=self.test_id, test_tags=None), | ||
| MatchesStatusCall(call="stopTestRun"), | ||
| ] | ||
| ), | ||
| ) | ||
| 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, | ||
| ] | ||
| ) | ||
| 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"), | ||
| ] | ||
| ), | ||
| ) | ||
| 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( | ||
| [ | ||
| MatchesStatusCall(call="startTestRun"), | ||
| first_call, | ||
| last_call, | ||
| MatchesStatusCall(call="stopTestRun"), | ||
| ] | ||
| ), | ||
| ) | ||
| 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(call="startTestRun"), | ||
| MatchesStatusCall(file_name=specified_file_name, file_bytes=b"Hello"), | ||
| MatchesStatusCall(call="stopTestRun"), | ||
| ] | ||
| ), | ||
| ) | ||
| 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(call="startTestRun"), | ||
| MatchesStatusCall(file_name=f.name, file_bytes=b"Hello", eof=True), | ||
| MatchesStatusCall(call="stopTestRun"), | ||
| ] | ||
| ), | ||
| ) | ||
| class FileDataTests(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(call="startTestRun"), | ||
| MatchesStatusCall(test_id=None, file_bytes=b"Hello", eof=True), | ||
| MatchesStatusCall(call="stopTestRun"), | ||
| ] | ||
| ), | ||
| ) | ||
| 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(call="startTestRun"), | ||
| MatchesStatusCall(file_name=f.name, file_bytes=b"Hello", eof=True), | ||
| MatchesStatusCall(call="stopTestRun"), | ||
| ] | ||
| ), | ||
| ) | ||
| 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(call="startTestRun"), | ||
| MatchesStatusCall(file_name=specified_file_name, file_bytes=b"Hello"), | ||
| MatchesStatusCall(call="stopTestRun"), | ||
| ] | ||
| ), | ||
| ) | ||
| def test_files_have_timestamp(self): | ||
| _dummy_timestamp = datetime.datetime(2013, 1, 1, 0, 0, 0, 0, UTC) | ||
| self.patch(_o, "create_timestamp", lambda: _dummy_timestamp) | ||
| with temp_file_contents(b"Hello") as f: | ||
| self.getUniqueString() | ||
| result = get_result_for( | ||
| [ | ||
| "--attach-file", | ||
| f.name, | ||
| ] | ||
| ) | ||
| self.assertThat( | ||
| result._events, | ||
| MatchesListwise( | ||
| [ | ||
| MatchesStatusCall(call="startTestRun"), | ||
| MatchesStatusCall(file_bytes=b"Hello", timestamp=_dummy_timestamp), | ||
| MatchesStatusCall(call="stopTestRun"), | ||
| ] | ||
| ), | ||
| ) | ||
| def test_can_specify_tags_without_test_status(self): | ||
| result = get_result_for( | ||
| [ | ||
| "--tag", | ||
| "foo", | ||
| ] | ||
| ) | ||
| self.assertThat( | ||
| result._events, | ||
| MatchesListwise( | ||
| [ | ||
| MatchesStatusCall(call="startTestRun"), | ||
| MatchesStatusCall(test_tags={"foo"}), | ||
| MatchesStatusCall(call="stopTestRun"), | ||
| ] | ||
| ), | ||
| ) | ||
| class MatchesStatusCall(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 = 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 | ||
| def match(self, call_tuple): | ||
| for k, v in self._filters.items(): | ||
| try: | ||
| pos = self._position_lookup[k] | ||
| if call_tuple[pos] != v: | ||
| return Mismatch("Value for key is {!r}, not {!r}".format(call_tuple[pos], v)) | ||
| except IndexError: | ||
| return Mismatch("Key %s is not present." % k) | ||
| def __str__(self): | ||
| return "<MatchesStatusCall %r>" % self._filters |
| # | ||
| # subunit: extensions to Python unittest to get test results from subprocesses. | ||
| # Copyright (C) 2009 Robert Collins <robertc@robertcollins.net> | ||
| # | ||
| # 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. | ||
| # | ||
| import unittest | ||
| from subunit.progress_model import ProgressModel | ||
| class TestProgressModel(unittest.TestCase): | ||
| def assertProgressSummary(self, pos, total, progress): | ||
| """Assert that a progress model has reached a particular point.""" | ||
| self.assertEqual(pos, progress.pos()) | ||
| self.assertEqual(total, progress.width()) | ||
| def test_new_progress_0_0(self): | ||
| progress = ProgressModel() | ||
| self.assertProgressSummary(0, 0, progress) | ||
| def test_advance_0_0(self): | ||
| progress = ProgressModel() | ||
| progress.advance() | ||
| self.assertProgressSummary(1, 0, progress) | ||
| def test_advance_1_0(self): | ||
| progress = ProgressModel() | ||
| progress.advance() | ||
| self.assertProgressSummary(1, 0, progress) | ||
| def test_set_width_absolute(self): | ||
| progress = ProgressModel() | ||
| progress.set_width(10) | ||
| self.assertProgressSummary(0, 10, progress) | ||
| def test_set_width_absolute_preserves_pos(self): | ||
| progress = ProgressModel() | ||
| progress.advance() | ||
| progress.set_width(2) | ||
| self.assertProgressSummary(1, 2, progress) | ||
| def test_adjust_width(self): | ||
| progress = ProgressModel() | ||
| progress.adjust_width(10) | ||
| self.assertProgressSummary(0, 10, progress) | ||
| progress.adjust_width(-10) | ||
| self.assertProgressSummary(0, 0, progress) | ||
| def test_adjust_width_preserves_pos(self): | ||
| progress = ProgressModel() | ||
| progress.advance() | ||
| progress.adjust_width(10) | ||
| self.assertProgressSummary(1, 10, progress) | ||
| progress.adjust_width(-10) | ||
| self.assertProgressSummary(1, 0, progress) | ||
| def test_push_preserves_progress(self): | ||
| progress = ProgressModel() | ||
| progress.adjust_width(3) | ||
| progress.advance() | ||
| progress.push() | ||
| self.assertProgressSummary(1, 3, progress) | ||
| def test_advance_advances_substack(self): | ||
| progress = ProgressModel() | ||
| progress.adjust_width(3) | ||
| progress.advance() | ||
| progress.push() | ||
| progress.adjust_width(1) | ||
| progress.advance() | ||
| self.assertProgressSummary(2, 3, progress) | ||
| def test_adjust_width_adjusts_substack(self): | ||
| progress = ProgressModel() | ||
| progress.adjust_width(3) | ||
| progress.advance() | ||
| progress.push() | ||
| progress.adjust_width(2) | ||
| progress.advance() | ||
| self.assertProgressSummary(3, 6, progress) | ||
| def test_set_width_adjusts_substack(self): | ||
| progress = ProgressModel() | ||
| progress.adjust_width(3) | ||
| progress.advance() | ||
| progress.push() | ||
| progress.set_width(2) | ||
| progress.advance() | ||
| self.assertProgressSummary(3, 6, progress) | ||
| def test_pop_restores_progress(self): | ||
| progress = ProgressModel() | ||
| progress.adjust_width(3) | ||
| progress.advance() | ||
| progress.push() | ||
| progress.adjust_width(1) | ||
| progress.advance() | ||
| progress.pop() | ||
| self.assertProgressSummary(1, 3, progress) |
| # | ||
| # subunit: extensions to python unittest to get test results from subprocesses. | ||
| # Copyright (C) 2011 Robert Collins <robertc@robertcollins.net> | ||
| # | ||
| # 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. | ||
| # | ||
| import io | ||
| import unittest | ||
| from testtools import PlaceHolder, TestCase | ||
| from testtools.matchers import StartsWith | ||
| from testtools.testresult.doubles import StreamResult | ||
| import subunit | ||
| from subunit import run | ||
| from subunit.run import SubunitTestRunner | ||
| class TestSubunitTestRunner(TestCase): | ||
| def test_includes_timing_output(self): | ||
| bytestream = io.BytesIO() | ||
| runner = SubunitTestRunner(stream=bytestream) | ||
| test = PlaceHolder("name") | ||
| runner.run(test) | ||
| bytestream.seek(0) | ||
| eventstream = StreamResult() | ||
| subunit.ByteStreamToStreamResult(bytestream).run(eventstream) | ||
| timestamps = [event[-1] for event in eventstream._events if event is not None] | ||
| self.assertNotEqual([], timestamps) | ||
| def test_enumerates_tests_before_run(self): | ||
| bytestream = io.BytesIO() | ||
| runner = SubunitTestRunner(stream=bytestream) | ||
| test1 = PlaceHolder("name1") | ||
| test2 = PlaceHolder("name2") | ||
| case = unittest.TestSuite([test1, test2]) | ||
| runner.run(case) | ||
| bytestream.seek(0) | ||
| eventstream = StreamResult() | ||
| subunit.ByteStreamToStreamResult(bytestream).run(eventstream) | ||
| self.assertEqual( | ||
| [ | ||
| ("status", "name1", "exists"), | ||
| ("status", "name2", "exists"), | ||
| ], | ||
| [event[:3] for event in eventstream._events[:2]], | ||
| ) | ||
| def test_list_errors_if_errors_from_list_test(self): | ||
| bytestream = io.BytesIO() | ||
| runner = SubunitTestRunner(stream=bytestream) | ||
| def list_test(test): | ||
| return [], ["failed import"] | ||
| self.patch(run, "list_test", list_test) | ||
| exc = self.assertRaises(SystemExit, runner.list, None) | ||
| self.assertEqual((2,), exc.args) | ||
| def test_list_includes_loader_errors(self): | ||
| bytestream = io.BytesIO() | ||
| runner = SubunitTestRunner(stream=bytestream) | ||
| def list_test(test): | ||
| return [], [] | ||
| class Loader: | ||
| errors = ["failed import"] | ||
| loader = Loader() | ||
| self.patch(run, "list_test", list_test) | ||
| exc = self.assertRaises(SystemExit, runner.list, None, loader=loader) | ||
| self.assertEqual((2,), exc.args) | ||
| class FailingTest(TestCase): | ||
| def test_fail(self): | ||
| 1 / 0 | ||
| def test_exits_zero_when_tests_fail(self): | ||
| bytestream = io.BytesIO() | ||
| stream = io.TextIOWrapper(bytestream, encoding="utf8") | ||
| try: | ||
| self.assertEqual( | ||
| None, | ||
| run.main(argv=["progName", "subunit.tests.test_run.TestSubunitTestRunner.FailingTest"], stdout=stream), | ||
| ) | ||
| except SystemExit: | ||
| self.fail("SystemExit raised") | ||
| self.assertThat(bytestream.getvalue(), StartsWith(b"\xb3")) | ||
| class ExitingTest(TestCase): | ||
| def test_exit(self): | ||
| raise SystemExit(0) | ||
| def test_exits_nonzero_when_execution_errors(self): | ||
| bytestream = io.BytesIO() | ||
| stream = io.TextIOWrapper(bytestream, encoding="utf8") | ||
| exc = self.assertRaises( | ||
| SystemExit, | ||
| run.main, | ||
| argv=["progName", "subunit.tests.test_run.TestSubunitTestRunner.ExitingTest"], | ||
| stdout=stream, | ||
| ) | ||
| self.assertEqual(0, exc.args[0]) |
| # | ||
| # subunit: extensions to python unittest to get test results from subprocesses. | ||
| # Copyright (C) 2005 Robert Collins <robertc@robertcollins.net> | ||
| # | ||
| # 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. | ||
| # | ||
| """Tests for subunit.TestResultFilter.""" | ||
| import subprocess | ||
| import sys | ||
| import unittest | ||
| from datetime import datetime | ||
| from io import BytesIO | ||
| from testtools import TestCase | ||
| from testtools.testresult.doubles import ExtendedTestResult, StreamResult | ||
| import iso8601 | ||
| import subunit | ||
| from subunit.test_results import make_tag_filter, TestResultFilter | ||
| from subunit import ByteStreamToStreamResult, StreamResultToBytes | ||
| class TestTestResultFilter(TestCase): | ||
| """Test for TestResultFilter, a TestResult object which filters tests.""" | ||
| # While TestResultFilter works on python objects, using a subunit stream | ||
| # is an easy pithy way of getting a series of test objects to call into | ||
| # the TestResult, and as TestResultFilter is intended for use with subunit | ||
| # also has the benefit of detecting any interface skew issues. | ||
| example_subunit_stream = b"""\ | ||
| tags: global | ||
| test passed | ||
| success passed | ||
| test failed | ||
| tags: local | ||
| failure failed | ||
| test error | ||
| error error [ | ||
| error details | ||
| ] | ||
| test skipped | ||
| skip skipped | ||
| test todo | ||
| xfail todo | ||
| """ | ||
| def run_tests(self, result_filter, input_stream=None): | ||
| """Run tests through the given filter. | ||
| :param result_filter: A filtering TestResult object. | ||
| :param input_stream: Bytes of subunit stream data. If not provided, | ||
| uses TestTestResultFilter.example_subunit_stream. | ||
| """ | ||
| if input_stream is None: | ||
| input_stream = self.example_subunit_stream | ||
| test = subunit.ProtocolTestCase(BytesIO(input_stream)) | ||
| test.run(result_filter) | ||
| def test_default(self): | ||
| """The default is to exclude success and include everything else.""" | ||
| filtered_result = unittest.TestResult() | ||
| result_filter = TestResultFilter(filtered_result) | ||
| self.run_tests(result_filter) | ||
| # skips are seen as success by default python TestResult. | ||
| self.assertEqual(["error"], [error[0].id() for error in filtered_result.errors]) | ||
| self.assertEqual(["failed"], [failure[0].id() for failure in filtered_result.failures]) | ||
| self.assertEqual(4, filtered_result.testsRun) | ||
| def test_tag_filter(self): | ||
| tag_filter = make_tag_filter(["global"], ["local"]) | ||
| result = ExtendedTestResult() | ||
| result_filter = TestResultFilter(result, filter_success=False, filter_predicate=tag_filter) | ||
| self.run_tests(result_filter) | ||
| tests_included = [event[1] for event in result._events if event[0] == "startTest"] | ||
| tests_expected = list(map(subunit.RemotedTestCase, ["passed", "error", "skipped", "todo"])) | ||
| self.assertEqual(tests_expected, tests_included) | ||
| def test_tags_tracked_correctly(self): | ||
| tag_filter = make_tag_filter(["a"], []) | ||
| result = ExtendedTestResult() | ||
| result_filter = TestResultFilter(result, filter_success=False, filter_predicate=tag_filter) | ||
| input_stream = b"test: foo\ntags: a\nsuccessful: foo\ntest: bar\nsuccessful: bar\n" | ||
| self.run_tests(result_filter, input_stream) | ||
| foo = subunit.RemotedTestCase("foo") | ||
| self.assertEqual( | ||
| [ | ||
| ("startTest", foo), | ||
| ("tags", {"a"}, set()), | ||
| ("addSuccess", foo), | ||
| ("stopTest", foo), | ||
| ], | ||
| result._events, | ||
| ) | ||
| def test_exclude_errors(self): | ||
| filtered_result = unittest.TestResult() | ||
| result_filter = TestResultFilter(filtered_result, filter_error=True) | ||
| self.run_tests(result_filter) | ||
| # skips are seen as errors by default python TestResult. | ||
| self.assertEqual([], filtered_result.errors) | ||
| self.assertEqual(["failed"], [failure[0].id() for failure in filtered_result.failures]) | ||
| self.assertEqual(3, filtered_result.testsRun) | ||
| def test_fixup_expected_failures(self): | ||
| filtered_result = unittest.TestResult() | ||
| result_filter = TestResultFilter(filtered_result, fixup_expected_failures={"failed"}) | ||
| self.run_tests(result_filter) | ||
| self.assertEqual(["failed", "todo"], [failure[0].id() for failure in filtered_result.expectedFailures]) | ||
| self.assertEqual([], filtered_result.failures) | ||
| self.assertEqual(4, filtered_result.testsRun) | ||
| def test_fixup_expected_errors(self): | ||
| filtered_result = unittest.TestResult() | ||
| result_filter = TestResultFilter(filtered_result, fixup_expected_failures={"error"}) | ||
| self.run_tests(result_filter) | ||
| self.assertEqual(["error", "todo"], [failure[0].id() for failure in filtered_result.expectedFailures]) | ||
| self.assertEqual([], filtered_result.errors) | ||
| self.assertEqual(4, filtered_result.testsRun) | ||
| def test_fixup_unexpected_success(self): | ||
| filtered_result = unittest.TestResult() | ||
| result_filter = TestResultFilter(filtered_result, filter_success=False, fixup_expected_failures={"passed"}) | ||
| self.run_tests(result_filter) | ||
| self.assertEqual(["passed"], [passed.id() for passed in filtered_result.unexpectedSuccesses]) | ||
| self.assertEqual(5, filtered_result.testsRun) | ||
| def test_exclude_failure(self): | ||
| filtered_result = unittest.TestResult() | ||
| result_filter = TestResultFilter(filtered_result, filter_failure=True) | ||
| self.run_tests(result_filter) | ||
| self.assertEqual(["error"], [error[0].id() for error in filtered_result.errors]) | ||
| self.assertEqual([], [failure[0].id() for failure in filtered_result.failures]) | ||
| self.assertEqual(3, filtered_result.testsRun) | ||
| def test_exclude_skips(self): | ||
| filtered_result = subunit.TestResultStats(None) | ||
| result_filter = TestResultFilter(filtered_result, filter_skip=True) | ||
| self.run_tests(result_filter) | ||
| self.assertEqual(0, filtered_result.skipped_tests) | ||
| self.assertEqual(2, filtered_result.failed_tests) | ||
| self.assertEqual(3, filtered_result.testsRun) | ||
| def test_include_success(self): | ||
| """Successes can be included if requested.""" | ||
| filtered_result = unittest.TestResult() | ||
| result_filter = TestResultFilter(filtered_result, filter_success=False) | ||
| self.run_tests(result_filter) | ||
| self.assertEqual(["error"], [error[0].id() for error in filtered_result.errors]) | ||
| self.assertEqual(["failed"], [failure[0].id() for failure in filtered_result.failures]) | ||
| self.assertEqual(5, filtered_result.testsRun) | ||
| def test_filter_predicate(self): | ||
| """You can filter by predicate callbacks""" | ||
| # 0.0.7 and earlier did not support the 'tags' parameter, so we need | ||
| # to test that we still support behaviour without it. | ||
| filtered_result = unittest.TestResult() | ||
| def filter_cb(test, outcome, err, details): | ||
| return outcome == "success" | ||
| result_filter = TestResultFilter(filtered_result, filter_predicate=filter_cb, filter_success=False) | ||
| self.run_tests(result_filter) | ||
| # Only success should pass | ||
| self.assertEqual(1, filtered_result.testsRun) | ||
| def test_filter_predicate_with_tags(self): | ||
| """You can filter by predicate callbacks that accept tags""" | ||
| filtered_result = unittest.TestResult() | ||
| def filter_cb(test, outcome, err, details, tags): | ||
| return outcome == "success" | ||
| result_filter = TestResultFilter(filtered_result, filter_predicate=filter_cb, filter_success=False) | ||
| self.run_tests(result_filter) | ||
| # Only success should pass | ||
| self.assertEqual(1, filtered_result.testsRun) | ||
| def test_time_ordering_preserved(self): | ||
| # Passing a subunit stream through TestResultFilter preserves the | ||
| # relative ordering of 'time' directives and any other subunit | ||
| # directives that are still included. | ||
| date_a = datetime(year=2000, month=1, day=1, tzinfo=iso8601.UTC) | ||
| date_b = datetime(year=2000, month=1, day=2, tzinfo=iso8601.UTC) | ||
| date_c = datetime(year=2000, month=1, day=3, tzinfo=iso8601.UTC) | ||
| subunit_stream = ( | ||
| "\n".join(["time: %s", "test: foo", "time: %s", "error: foo", "time: %s", ""]) % (date_a, date_b, date_c) | ||
| ).encode() | ||
| result = ExtendedTestResult() | ||
| result_filter = TestResultFilter(result) | ||
| self.run_tests(result_filter, subunit_stream) | ||
| foo = subunit.RemotedTestCase("foo") | ||
| self.maxDiff = None | ||
| self.assertEqual( | ||
| [ | ||
| ("time", date_a), | ||
| ("time", date_b), | ||
| ("startTest", foo), | ||
| ("addError", foo, {}), | ||
| ("stopTest", foo), | ||
| ("time", date_c), | ||
| ], | ||
| result._events, | ||
| ) | ||
| def test_time_passes_through_filtered_tests(self): | ||
| # Passing a subunit stream through TestResultFilter preserves 'time' | ||
| # directives even if a specific test is filtered out. | ||
| date_a = datetime(year=2000, month=1, day=1, tzinfo=iso8601.UTC) | ||
| date_b = datetime(year=2000, month=1, day=2, tzinfo=iso8601.UTC) | ||
| date_c = datetime(year=2000, month=1, day=3, tzinfo=iso8601.UTC) | ||
| subunit_stream = ( | ||
| "\n".join(["time: %s", "test: foo", "time: %s", "success: foo", "time: %s", ""]) % (date_a, date_b, date_c) | ||
| ).encode() | ||
| result = ExtendedTestResult() | ||
| result_filter = TestResultFilter(result) | ||
| result_filter.startTestRun() | ||
| self.run_tests(result_filter, subunit_stream) | ||
| result_filter.stopTestRun() | ||
| subunit.RemotedTestCase("foo") | ||
| self.maxDiff = None | ||
| self.assertEqual( | ||
| [ | ||
| ("startTestRun",), | ||
| ("time", date_a), | ||
| ("time", date_c), | ||
| ("stopTestRun",), | ||
| ], | ||
| result._events, | ||
| ) | ||
| def test_skip_preserved(self): | ||
| subunit_stream = "\n".join(["test: foo", "skip: foo", ""]).encode() | ||
| result = ExtendedTestResult() | ||
| result_filter = TestResultFilter(result) | ||
| self.run_tests(result_filter, subunit_stream) | ||
| foo = subunit.RemotedTestCase("foo") | ||
| self.assertEqual( | ||
| [ | ||
| ("startTest", foo), | ||
| ("addSkip", foo, {}), | ||
| ("stopTest", foo), | ||
| ], | ||
| result._events, | ||
| ) | ||
| def test_renames(self): | ||
| def rename(name): | ||
| return name + " - renamed" | ||
| result = ExtendedTestResult() | ||
| result_filter = TestResultFilter(result, filter_success=False, rename=rename) | ||
| input_stream = b"test: foo\nsuccessful: foo\n" | ||
| self.run_tests(result_filter, input_stream) | ||
| self.assertEqual( | ||
| [("startTest", "foo - renamed"), ("addSuccess", "foo - renamed"), ("stopTest", "foo - renamed")], | ||
| [(ev[0], ev[1].id()) for ev in result._events], | ||
| ) | ||
| class TestFilterCommand(TestCase): | ||
| def run_command(self, args, stream): | ||
| command = [sys.executable, "-m", "subunit.filter_scripts.subunit_filter"] + list(args) | ||
| ps = subprocess.Popen(command, stdin=subprocess.PIPE, stdout=subprocess.PIPE, stderr=subprocess.PIPE) | ||
| out, err = ps.communicate(stream) | ||
| if ps.returncode != 0: | ||
| raise RuntimeError("{} failed: {}".format(command, err)) | ||
| return out | ||
| def test_default(self): | ||
| byte_stream = BytesIO() | ||
| stream = StreamResultToBytes(byte_stream) | ||
| stream.status(test_id="foo", test_status="inprogress") | ||
| stream.status(test_id="foo", test_status="skip") | ||
| output = self.run_command([], byte_stream.getvalue()) | ||
| events = StreamResult() | ||
| ByteStreamToStreamResult(BytesIO(output)).run(events) | ||
| {event[1] for event in events._events} | ||
| self.assertEqual( | ||
| [ | ||
| ("status", "foo", "inprogress"), | ||
| ("status", "foo", "skip"), | ||
| ], | ||
| [event[:3] for event in events._events], | ||
| ) | ||
| def test_tags(self): | ||
| byte_stream = BytesIO() | ||
| stream = StreamResultToBytes(byte_stream) | ||
| stream.status(test_id="foo", test_status="inprogress", test_tags={"a"}) | ||
| stream.status(test_id="foo", test_status="success", test_tags={"a"}) | ||
| stream.status(test_id="bar", test_status="inprogress") | ||
| stream.status(test_id="bar", test_status="inprogress") | ||
| stream.status(test_id="baz", test_status="inprogress", test_tags={"a"}) | ||
| stream.status(test_id="baz", test_status="success", test_tags={"a"}) | ||
| output = self.run_command(["-s", "--with-tag", "a"], byte_stream.getvalue()) | ||
| events = StreamResult() | ||
| ByteStreamToStreamResult(BytesIO(output)).run(events) | ||
| ids = {event[1] for event in events._events} | ||
| self.assertEqual({"foo", "baz"}, ids) | ||
| def test_no_passthrough(self): | ||
| output = self.run_command(["--no-passthrough"], b"hi thar") | ||
| self.assertEqual(b"", output) | ||
| def test_passthrough(self): | ||
| output = self.run_command([], b"hi thar") | ||
| byte_stream = BytesIO() | ||
| stream = StreamResultToBytes(byte_stream) | ||
| stream.status(file_name="stdout", file_bytes=b"hi thar") | ||
| self.assertEqual(byte_stream.getvalue(), output) |
| # | ||
| # subunit: extensions to python unittest to get test results from subprocesses. | ||
| # Copyright (C) 2005 Robert Collins <robertc@robertcollins.net> | ||
| # | ||
| # 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. | ||
| # | ||
| """Tests for subunit.TestResultStats.""" | ||
| import unittest | ||
| from io import BytesIO, StringIO | ||
| import subunit | ||
| class TestTestResultStats(unittest.TestCase): | ||
| """Test for TestResultStats, a TestResult object that generates stats.""" | ||
| def setUp(self): | ||
| self.output = StringIO() | ||
| self.result = subunit.TestResultStats(self.output) | ||
| self.input_stream = BytesIO() | ||
| self.test = subunit.ProtocolTestCase(self.input_stream) | ||
| def test_stats_empty(self): | ||
| self.test.run(self.result) | ||
| self.assertEqual(0, self.result.total_tests) | ||
| self.assertEqual(0, self.result.passed_tests) | ||
| self.assertEqual(0, self.result.failed_tests) | ||
| self.assertEqual(set(), self.result.seen_tags) | ||
| def setUpUsedStream(self): | ||
| self.input_stream.write( | ||
| b"""tags: global | ||
| test passed | ||
| success passed | ||
| test failed | ||
| tags: local | ||
| failure failed | ||
| test error | ||
| error error | ||
| test skipped | ||
| skip skipped | ||
| test todo | ||
| xfail todo | ||
| """ | ||
| ) | ||
| self.input_stream.seek(0) | ||
| self.test.run(self.result) | ||
| def test_stats_smoke_everything(self): | ||
| # Statistics are calculated usefully. | ||
| self.setUpUsedStream() | ||
| self.assertEqual(5, self.result.total_tests) | ||
| self.assertEqual(2, self.result.passed_tests) | ||
| self.assertEqual(2, self.result.failed_tests) | ||
| self.assertEqual(1, self.result.skipped_tests) | ||
| self.assertEqual({"global", "local"}, self.result.seen_tags) | ||
| def test_stat_formatting(self): | ||
| expected = ( | ||
| """ | ||
| Total tests: 5 | ||
| Passed tests: 2 | ||
| Failed tests: 2 | ||
| Skipped tests: 1 | ||
| Seen tags: global, local | ||
| """ | ||
| )[1:] | ||
| self.setUpUsedStream() | ||
| self.result.formatStats() | ||
| self.assertEqual(expected, self.output.getvalue()) |
| # | ||
| # subunit: extensions to python unittest to get test results from subprocesses. | ||
| # Copyright (C) 2005 Robert Collins <robertc@robertcollins.net> | ||
| # | ||
| # 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. | ||
| # | ||
| """Tests for subunit.tag_stream.""" | ||
| from io import BytesIO | ||
| import testtools | ||
| from testtools.matchers import Contains | ||
| import subunit | ||
| import subunit.test_results | ||
| class TestSubUnitTags(testtools.TestCase): | ||
| def setUp(self): | ||
| super().setUp() | ||
| self.original = BytesIO() | ||
| self.filtered = BytesIO() | ||
| def test_add_tag(self): | ||
| # Literal values to avoid set sort-order dependencies. Python code show | ||
| # derivation. | ||
| # reference = BytesIO() | ||
| # stream = subunit.StreamResultToBytes(reference) | ||
| # stream.status( | ||
| # test_id='test', test_status='inprogress', test_tags=set(['quux', 'foo'])) | ||
| # stream.status( | ||
| # test_id='test', test_status='success', test_tags=set(['bar', 'quux', 'foo'])) | ||
| reference = [ | ||
| b"\xb3)\x82\x17\x04test\x02\x04quux\x03foo\x05\x97n\x86" | ||
| b"\xb3)\x83\x1b\x04test\x03\x03bar\x04quux\x03fooqn\xab)", | ||
| b"\xb3)\x82\x17\x04test\x02\x04quux\x03foo\x05\x97n\x86" | ||
| b"\xb3)\x83\x1b\x04test\x03\x04quux\x03foo\x03bar\xaf\xbd\x9d\xd6", | ||
| b"\xb3)\x82\x17\x04test\x02\x04quux\x03foo\x05\x97n\x86" | ||
| b"\xb3)\x83\x1b\x04test\x03\x04quux\x03bar\x03foo\x03\x04b\r", | ||
| b"\xb3)\x82\x17\x04test\x02\x04quux\x03foo\x05\x97n\x86" | ||
| b"\xb3)\x83\x1b\x04test\x03\x03bar\x03foo\x04quux\xd2\x18\x1bC", | ||
| b"\xb3)\x82\x17\x04test\x02\x04quux\x03foo\x05\x97n\x86" | ||
| b"\xb3)\x83\x1b\x04test\x03\x03foo\x04quux\x03bar\x08\xc2X\x83", | ||
| b"\xb3)\x82\x17\x04test\x02\x03foo\x04quux\xa6\xe1\xde\xec" | ||
| b"\xb3)\x83\x1b\x04test\x03\x03foo\x04quux\x03bar\x08\xc2X\x83", | ||
| b"\xb3)\x82\x17\x04test\x02\x03foo\x04quux\xa6\xe1\xde\xec" | ||
| b"\xb3)\x83\x1b\x04test\x03\x03bar\x03foo\x04quux\xd2\x18\x1bC", | ||
| b"\xb3)\x82\x17\x04test\x02\x03foo\x04quux\xa6\xe1\xde\xec" | ||
| b"\xb3)\x83\x1b\x04test\x03\x03foo\x03bar\x04quux:\x05e\x80", | ||
| b"\xb3)\x82\x17\x04test\x02\x03foo\x04quux\xa6\xe1\xde\xec" | ||
| b"\xb3)\x83\x1b\x04test\x03\x04quux\x03foo\x03bar\xaf\xbd\x9d\xd6", | ||
| ] | ||
| stream = subunit.StreamResultToBytes(self.original) | ||
| stream.status(test_id="test", test_status="inprogress", test_tags={"foo"}) | ||
| stream.status(test_id="test", test_status="success", test_tags={"foo", "bar"}) | ||
| self.original.seek(0) | ||
| self.assertEqual(0, subunit.tag_stream(self.original, self.filtered, ["quux"])) | ||
| self.assertThat(reference, Contains(bytes(self.filtered.getvalue()))) | ||
| def test_remove_tag(self): | ||
| reference = BytesIO() | ||
| stream = subunit.StreamResultToBytes(reference) | ||
| stream.status(test_id="test", test_status="inprogress", test_tags={"foo"}) | ||
| stream.status(test_id="test", test_status="success", test_tags={"foo"}) | ||
| stream = subunit.StreamResultToBytes(self.original) | ||
| stream.status(test_id="test", test_status="inprogress", test_tags={"foo"}) | ||
| stream.status(test_id="test", test_status="success", test_tags={"foo", "bar"}) | ||
| self.original.seek(0) | ||
| self.assertEqual(0, subunit.tag_stream(self.original, self.filtered, ["-bar"])) | ||
| self.assertEqual(reference.getvalue(), self.filtered.getvalue()) |
| # | ||
| # subunit: extensions to python unittest to get test results from subprocesses. | ||
| # Copyright (C) 2005 Robert Collins <robertc@robertcollins.net> | ||
| # | ||
| # 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. | ||
| # | ||
| """Tests for TAP2SubUnit.""" | ||
| from io import BytesIO, StringIO | ||
| from testtools import TestCase | ||
| from testtools.testresult.doubles import StreamResult | ||
| import subunit | ||
| UTF8_TEXT = "text/plain; charset=UTF8" | ||
| class TestTAP2SubUnit(TestCase): | ||
| """Tests for TAP2SubUnit. | ||
| These tests test TAP string data in, and subunit string data out. | ||
| This is ok because the subunit protocol is intended to be stable, | ||
| but it might be easier/pithier to write tests against TAP string in, | ||
| parsed subunit objects out (by hooking the subunit stream to a subunit | ||
| protocol server. | ||
| """ | ||
| def setUp(self): | ||
| super().setUp() | ||
| self.tap = StringIO() | ||
| self.subunit = BytesIO() | ||
| def test_skip_entire_file(self): | ||
| # A file | ||
| # 1..- # Skipped: comment | ||
| # results in a single skipped test. | ||
| self.tap.write("1..0 # Skipped: entire file skipped\n") | ||
| self.tap.seek(0) | ||
| result = subunit.TAP2SubUnit(self.tap, self.subunit) | ||
| self.assertEqual(0, result) | ||
| self.check_events( | ||
| [ | ||
| ( | ||
| "status", | ||
| "file skip", | ||
| "skip", | ||
| None, | ||
| True, | ||
| "tap comment", | ||
| b"Skipped: entire file skipped", | ||
| True, | ||
| None, | ||
| None, | ||
| None, | ||
| ) | ||
| ] | ||
| ) | ||
| def test_ok_test_pass(self): | ||
| # A file | ||
| # ok | ||
| # results in a passed test with name 'test 1' (a synthetic name as tap | ||
| # does not require named fixtures - it is the first test in the tap | ||
| # stream). | ||
| self.tap.write("ok\n") | ||
| self.tap.seek(0) | ||
| result = subunit.TAP2SubUnit(self.tap, self.subunit) | ||
| self.assertEqual(0, result) | ||
| self.check_events([("status", "test 1", "success", None, False, None, None, True, None, None, None)]) | ||
| def test_ok_test_number_pass(self): | ||
| # A file | ||
| # ok 1 | ||
| # results in a passed test with name 'test 1' | ||
| self.tap.write("ok 1\n") | ||
| self.tap.seek(0) | ||
| result = subunit.TAP2SubUnit(self.tap, self.subunit) | ||
| self.assertEqual(0, result) | ||
| self.check_events([("status", "test 1", "success", None, False, None, None, True, None, None, None)]) | ||
| def test_ok_test_number_description_pass(self): | ||
| # A file | ||
| # ok 1 - There is a description | ||
| # results in a passed test with name 'test 1 - There is a description' | ||
| self.tap.write("ok 1 - There is a description\n") | ||
| self.tap.seek(0) | ||
| result = subunit.TAP2SubUnit(self.tap, self.subunit) | ||
| self.assertEqual(0, result) | ||
| self.check_events( | ||
| [("status", "test 1 - There is a description", "success", None, False, None, None, True, None, None, None)] | ||
| ) | ||
| def test_ok_test_description_pass(self): | ||
| # A file | ||
| # ok There is a description | ||
| # results in a passed test with name 'test 1 There is a description' | ||
| self.tap.write("ok There is a description\n") | ||
| self.tap.seek(0) | ||
| result = subunit.TAP2SubUnit(self.tap, self.subunit) | ||
| self.assertEqual(0, result) | ||
| self.check_events( | ||
| [("status", "test 1 There is a description", "success", None, False, None, None, True, None, None, None)] | ||
| ) | ||
| def test_ok_SKIP_skip(self): | ||
| # A file | ||
| # ok # SKIP | ||
| # results in a skkip test with name 'test 1' | ||
| self.tap.write("ok # SKIP\n") | ||
| self.tap.seek(0) | ||
| result = subunit.TAP2SubUnit(self.tap, self.subunit) | ||
| self.assertEqual(0, result) | ||
| self.check_events([("status", "test 1", "skip", None, False, None, None, True, None, None, None)]) | ||
| def test_ok_skip_number_comment_lowercase(self): | ||
| self.tap.write("ok 1 # skip no samba environment available, skipping compilation\n") | ||
| self.tap.seek(0) | ||
| result = subunit.TAP2SubUnit(self.tap, self.subunit) | ||
| self.assertEqual(0, result) | ||
| self.check_events( | ||
| [ | ||
| ( | ||
| "status", | ||
| "test 1", | ||
| "skip", | ||
| None, | ||
| False, | ||
| "tap comment", | ||
| b"no samba environment available, skipping compilation", | ||
| True, | ||
| "text/plain; charset=UTF8", | ||
| None, | ||
| None, | ||
| ) | ||
| ] | ||
| ) | ||
| def test_ok_number_description_SKIP_skip_comment(self): | ||
| # A file | ||
| # ok 1 foo # SKIP Not done yet | ||
| # results in a skip test with name 'test 1 foo' and a log of | ||
| # Not done yet | ||
| self.tap.write("ok 1 foo # SKIP Not done yet\n") | ||
| self.tap.seek(0) | ||
| result = subunit.TAP2SubUnit(self.tap, self.subunit) | ||
| self.assertEqual(0, result) | ||
| self.check_events( | ||
| [ | ||
| ( | ||
| "status", | ||
| "test 1 foo", | ||
| "skip", | ||
| None, | ||
| False, | ||
| "tap comment", | ||
| b"Not done yet", | ||
| True, | ||
| "text/plain; charset=UTF8", | ||
| None, | ||
| None, | ||
| ) | ||
| ] | ||
| ) | ||
| def test_ok_SKIP_skip_comment(self): | ||
| # A file | ||
| # ok # SKIP Not done yet | ||
| # results in a skip test with name 'test 1' and a log of Not done yet | ||
| self.tap.write("ok # SKIP Not done yet\n") | ||
| self.tap.seek(0) | ||
| result = subunit.TAP2SubUnit(self.tap, self.subunit) | ||
| self.assertEqual(0, result) | ||
| self.check_events( | ||
| [ | ||
| ( | ||
| "status", | ||
| "test 1", | ||
| "skip", | ||
| None, | ||
| False, | ||
| "tap comment", | ||
| b"Not done yet", | ||
| True, | ||
| "text/plain; charset=UTF8", | ||
| None, | ||
| None, | ||
| ) | ||
| ] | ||
| ) | ||
| def test_ok_TODO_xfail(self): | ||
| # A file | ||
| # ok # TODO | ||
| # results in a xfail test with name 'test 1' | ||
| self.tap.write("ok # TODO\n") | ||
| self.tap.seek(0) | ||
| result = subunit.TAP2SubUnit(self.tap, self.subunit) | ||
| self.assertEqual(0, result) | ||
| self.check_events([("status", "test 1", "xfail", None, False, None, None, True, None, None, None)]) | ||
| def test_ok_TODO_xfail_comment(self): | ||
| # A file | ||
| # ok # TODO Not done yet | ||
| # results in a xfail test with name 'test 1' and a log of Not done yet | ||
| self.tap.write("ok # TODO Not done yet\n") | ||
| self.tap.seek(0) | ||
| result = subunit.TAP2SubUnit(self.tap, self.subunit) | ||
| self.assertEqual(0, result) | ||
| self.check_events( | ||
| [ | ||
| ( | ||
| "status", | ||
| "test 1", | ||
| "xfail", | ||
| None, | ||
| False, | ||
| "tap comment", | ||
| b"Not done yet", | ||
| True, | ||
| "text/plain; charset=UTF8", | ||
| None, | ||
| None, | ||
| ) | ||
| ] | ||
| ) | ||
| def test_bail_out_errors(self): | ||
| # A file with line in it | ||
| # Bail out! COMMENT | ||
| # is treated as an error | ||
| self.tap.write("ok 1 foo\n") | ||
| self.tap.write("Bail out! Lifejacket engaged\n") | ||
| self.tap.seek(0) | ||
| result = subunit.TAP2SubUnit(self.tap, self.subunit) | ||
| self.assertEqual(0, result) | ||
| self.check_events( | ||
| [ | ||
| ("status", "test 1 foo", "success", None, False, None, None, True, None, None, None), | ||
| ("status", "Bail out! Lifejacket engaged", "fail", None, False, None, None, True, None, None, None), | ||
| ] | ||
| ) | ||
| def test_missing_test_at_end_with_plan_adds_error(self): | ||
| # A file | ||
| # 1..3 | ||
| # ok first test | ||
| # not ok third test | ||
| # results in three tests, with the third being created | ||
| self.tap.write("1..3\n") | ||
| self.tap.write("ok first test\n") | ||
| self.tap.write("not ok second test\n") | ||
| self.tap.seek(0) | ||
| result = subunit.TAP2SubUnit(self.tap, self.subunit) | ||
| self.assertEqual(0, result) | ||
| self.check_events( | ||
| [ | ||
| ("status", "test 1 first test", "success", None, False, None, None, True, None, None, None), | ||
| ("status", "test 2 second test", "fail", None, False, None, None, True, None, None, None), | ||
| ( | ||
| "status", | ||
| "test 3", | ||
| "fail", | ||
| None, | ||
| False, | ||
| "tap meta", | ||
| b"test missing from TAP output", | ||
| True, | ||
| "text/plain; charset=UTF8", | ||
| None, | ||
| None, | ||
| ), | ||
| ] | ||
| ) | ||
| def test_missing_test_with_plan_adds_error(self): | ||
| # A file | ||
| # 1..3 | ||
| # ok first test | ||
| # not ok 3 third test | ||
| # results in three tests, with the second being created | ||
| self.tap.write("1..3\n") | ||
| self.tap.write("ok first test\n") | ||
| self.tap.write("not ok 3 third test\n") | ||
| self.tap.seek(0) | ||
| result = subunit.TAP2SubUnit(self.tap, self.subunit) | ||
| self.assertEqual(0, result) | ||
| self.check_events( | ||
| [ | ||
| ("status", "test 1 first test", "success", None, False, None, None, True, None, None, None), | ||
| ( | ||
| "status", | ||
| "test 2", | ||
| "fail", | ||
| None, | ||
| False, | ||
| "tap meta", | ||
| b"test missing from TAP output", | ||
| True, | ||
| "text/plain; charset=UTF8", | ||
| None, | ||
| None, | ||
| ), | ||
| ("status", "test 3 third test", "fail", None, False, None, None, True, None, None, None), | ||
| ] | ||
| ) | ||
| def test_missing_test_no_plan_adds_error(self): | ||
| # A file | ||
| # ok first test | ||
| # not ok 3 third test | ||
| # results in three tests, with the second being created | ||
| self.tap.write("ok first test\n") | ||
| self.tap.write("not ok 3 third test\n") | ||
| self.tap.seek(0) | ||
| result = subunit.TAP2SubUnit(self.tap, self.subunit) | ||
| self.assertEqual(0, result) | ||
| self.check_events( | ||
| [ | ||
| ("status", "test 1 first test", "success", None, False, None, None, True, None, None, None), | ||
| ( | ||
| "status", | ||
| "test 2", | ||
| "fail", | ||
| None, | ||
| False, | ||
| "tap meta", | ||
| b"test missing from TAP output", | ||
| True, | ||
| "text/plain; charset=UTF8", | ||
| None, | ||
| None, | ||
| ), | ||
| ("status", "test 3 third test", "fail", None, False, None, None, True, None, None, None), | ||
| ] | ||
| ) | ||
| def test_four_tests_in_a_row_trailing_plan(self): | ||
| # A file | ||
| # ok 1 - first test in a script with no plan at all | ||
| # not ok 2 - second | ||
| # ok 3 - third | ||
| # not ok 4 - fourth | ||
| # 1..4 | ||
| # results in four tests numbered and named | ||
| self.tap.write("ok 1 - first test in a script with trailing plan\n") | ||
| self.tap.write("not ok 2 - second\n") | ||
| self.tap.write("ok 3 - third\n") | ||
| self.tap.write("not ok 4 - fourth\n") | ||
| self.tap.write("1..4\n") | ||
| self.tap.seek(0) | ||
| result = subunit.TAP2SubUnit(self.tap, self.subunit) | ||
| self.assertEqual(0, result) | ||
| self.check_events( | ||
| [ | ||
| ( | ||
| "status", | ||
| "test 1 - first test in a script with trailing plan", | ||
| "success", | ||
| None, | ||
| False, | ||
| None, | ||
| None, | ||
| True, | ||
| None, | ||
| None, | ||
| None, | ||
| ), | ||
| ("status", "test 2 - second", "fail", None, False, None, None, True, None, None, None), | ||
| ("status", "test 3 - third", "success", None, False, None, None, True, None, None, None), | ||
| ("status", "test 4 - fourth", "fail", None, False, None, None, True, None, None, None), | ||
| ] | ||
| ) | ||
| def test_four_tests_in_a_row_with_plan(self): | ||
| # A file | ||
| # 1..4 | ||
| # ok 1 - first test in a script with no plan at all | ||
| # not ok 2 - second | ||
| # ok 3 - third | ||
| # not ok 4 - fourth | ||
| # results in four tests numbered and named | ||
| self.tap.write("1..4\n") | ||
| self.tap.write("ok 1 - first test in a script with a plan\n") | ||
| self.tap.write("not ok 2 - second\n") | ||
| self.tap.write("ok 3 - third\n") | ||
| self.tap.write("not ok 4 - fourth\n") | ||
| self.tap.seek(0) | ||
| result = subunit.TAP2SubUnit(self.tap, self.subunit) | ||
| self.assertEqual(0, result) | ||
| self.check_events( | ||
| [ | ||
| ( | ||
| "status", | ||
| "test 1 - first test in a script with a plan", | ||
| "success", | ||
| None, | ||
| False, | ||
| None, | ||
| None, | ||
| True, | ||
| None, | ||
| None, | ||
| None, | ||
| ), | ||
| ("status", "test 2 - second", "fail", None, False, None, None, True, None, None, None), | ||
| ("status", "test 3 - third", "success", None, False, None, None, True, None, None, None), | ||
| ("status", "test 4 - fourth", "fail", None, False, None, None, True, None, None, None), | ||
| ] | ||
| ) | ||
| def test_four_tests_in_a_row_no_plan(self): | ||
| # A file | ||
| # ok 1 - first test in a script with no plan at all | ||
| # not ok 2 - second | ||
| # ok 3 - third | ||
| # not ok 4 - fourth | ||
| # results in four tests numbered and named | ||
| self.tap.write("ok 1 - first test in a script with no plan at all\n") | ||
| self.tap.write("not ok 2 - second\n") | ||
| self.tap.write("ok 3 - third\n") | ||
| self.tap.write("not ok 4 - fourth\n") | ||
| self.tap.seek(0) | ||
| result = subunit.TAP2SubUnit(self.tap, self.subunit) | ||
| self.assertEqual(0, result) | ||
| self.check_events( | ||
| [ | ||
| ( | ||
| "status", | ||
| "test 1 - first test in a script with no plan at all", | ||
| "success", | ||
| None, | ||
| False, | ||
| None, | ||
| None, | ||
| True, | ||
| None, | ||
| None, | ||
| None, | ||
| ), | ||
| ("status", "test 2 - second", "fail", None, False, None, None, True, None, None, None), | ||
| ("status", "test 3 - third", "success", None, False, None, None, True, None, None, None), | ||
| ("status", "test 4 - fourth", "fail", None, False, None, None, True, None, None, None), | ||
| ] | ||
| ) | ||
| def test_todo_and_skip(self): | ||
| # A file | ||
| # not ok 1 - a fail but # TODO but is TODO | ||
| # not ok 2 - another fail # SKIP instead | ||
| # results in two tests, numbered and commented. | ||
| self.tap.write("not ok 1 - a fail but # TODO but is TODO\n") | ||
| self.tap.write("not ok 2 - another fail # SKIP instead\n") | ||
| self.tap.seek(0) | ||
| result = subunit.TAP2SubUnit(self.tap, self.subunit) | ||
| self.assertEqual(0, result) | ||
| self.subunit.seek(0) | ||
| events = StreamResult() | ||
| subunit.ByteStreamToStreamResult(self.subunit).run(events) | ||
| self.check_events( | ||
| [ | ||
| ( | ||
| "status", | ||
| "test 1 - a fail but", | ||
| "xfail", | ||
| None, | ||
| False, | ||
| "tap comment", | ||
| b"but is TODO", | ||
| True, | ||
| "text/plain; charset=UTF8", | ||
| None, | ||
| None, | ||
| ), | ||
| ( | ||
| "status", | ||
| "test 2 - another fail", | ||
| "skip", | ||
| None, | ||
| False, | ||
| "tap comment", | ||
| b"instead", | ||
| True, | ||
| "text/plain; charset=UTF8", | ||
| None, | ||
| None, | ||
| ), | ||
| ] | ||
| ) | ||
| def test_leading_comments_add_to_next_test_log(self): | ||
| # A file | ||
| # # comment | ||
| # ok | ||
| # ok | ||
| # results in a single test with the comment included | ||
| # in the first test and not the second. | ||
| self.tap.write("# comment\n") | ||
| self.tap.write("ok\n") | ||
| self.tap.write("ok\n") | ||
| self.tap.seek(0) | ||
| result = subunit.TAP2SubUnit(self.tap, self.subunit) | ||
| self.assertEqual(0, result) | ||
| self.check_events( | ||
| [ | ||
| ( | ||
| "status", | ||
| "test 1", | ||
| "success", | ||
| None, | ||
| False, | ||
| "tap comment", | ||
| b"# comment", | ||
| True, | ||
| "text/plain; charset=UTF8", | ||
| None, | ||
| None, | ||
| ), | ||
| ("status", "test 2", "success", None, False, None, None, True, None, None, None), | ||
| ] | ||
| ) | ||
| def test_trailing_comments_are_included_in_last_test_log(self): | ||
| # A file | ||
| # ok foo | ||
| # ok foo | ||
| # # comment | ||
| # results in a two tests, with the second having the comment | ||
| # attached to its log. | ||
| self.tap.write("ok\n") | ||
| self.tap.write("ok\n") | ||
| self.tap.write("# comment\n") | ||
| self.tap.seek(0) | ||
| result = subunit.TAP2SubUnit(self.tap, self.subunit) | ||
| self.assertEqual(0, result) | ||
| self.check_events( | ||
| [ | ||
| ("status", "test 1", "success", None, False, None, None, True, None, None, None), | ||
| ( | ||
| "status", | ||
| "test 2", | ||
| "success", | ||
| None, | ||
| False, | ||
| "tap comment", | ||
| b"# comment", | ||
| True, | ||
| "text/plain; charset=UTF8", | ||
| None, | ||
| None, | ||
| ), | ||
| ] | ||
| ) | ||
| def check_events(self, events): | ||
| self.subunit.seek(0) | ||
| eventstream = StreamResult() | ||
| subunit.ByteStreamToStreamResult(self.subunit).run(eventstream) | ||
| self.assertEqual(events, eventstream._events) |
| # | ||
| # subunit: extensions to Python unittest to get test results from subprocesses. | ||
| # Copyright (C) 2005 Robert Collins <robertc@robertcollins.net> | ||
| # | ||
| # 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. | ||
| # | ||
| import datetime | ||
| import io | ||
| import os | ||
| import tempfile | ||
| import unittest | ||
| from io import BytesIO | ||
| from testtools import PlaceHolder, TestCase, TestResult, skipIf | ||
| from testtools.content import Content, TracebackContent, text_content | ||
| from testtools.content_type import ContentType | ||
| from testtools.testresult.doubles import ExtendedTestResult | ||
| from testtools.matchers import Contains, Equals, MatchesAny | ||
| import iso8601 | ||
| import subunit | ||
| from subunit.tests import ( | ||
| _remote_exception_repr, | ||
| _remote_exception_repr_chunked, | ||
| _remote_exception_str, | ||
| _remote_exception_str_chunked, | ||
| ) | ||
| tb_prelude = "Traceback (most recent call last):\n" | ||
| def details_to_str(details): | ||
| return TestResult()._err_details_to_string(None, details=details) | ||
| class TestHelpers(TestCase): | ||
| def test__unwrap_text_file_read_mode(self): | ||
| fd, file_path = tempfile.mkstemp() | ||
| self.addCleanup(os.remove, file_path) | ||
| fake_file = os.fdopen(fd, "r") | ||
| self.assertEqual(fake_file.buffer, subunit._unwrap_text(fake_file)) | ||
| def test__unwrap_text_file_write_mode(self): | ||
| fd, file_path = tempfile.mkstemp() | ||
| self.addCleanup(os.remove, file_path) | ||
| fake_file = os.fdopen(fd, "w") | ||
| self.assertEqual(fake_file.buffer, subunit._unwrap_text(fake_file)) | ||
| def test__unwrap_text_fileIO_read_mode(self): | ||
| fd, file_path = tempfile.mkstemp() | ||
| self.addCleanup(os.remove, file_path) | ||
| fake_file = io.FileIO(file_path, "r") | ||
| self.assertEqual(fake_file, subunit._unwrap_text(fake_file)) | ||
| def test__unwrap_text_fileIO_write_mode(self): | ||
| fd, file_path = tempfile.mkstemp() | ||
| self.addCleanup(os.remove, file_path) | ||
| fake_file = io.FileIO(file_path, "w") | ||
| self.assertEqual(fake_file, subunit._unwrap_text(fake_file)) | ||
| def test__unwrap_text_BytesIO(self): | ||
| fake_stream = io.BytesIO() | ||
| self.assertEqual(fake_stream, subunit._unwrap_text(fake_stream)) | ||
| class TestTestImports(unittest.TestCase): | ||
| def test_imports(self): | ||
| from subunit import ( # noqa: F401 | ||
| DiscardStream, | ||
| ExecTestCase, | ||
| IsolatedTestCase, | ||
| ProtocolTestCase, | ||
| RemotedTestCase, | ||
| RemoteError, | ||
| TestProtocolClient, | ||
| TestProtocolServer, | ||
| ) # noqa: F401 | ||
| class TestDiscardStream(unittest.TestCase): | ||
| def test_write(self): | ||
| subunit.DiscardStream().write("content") | ||
| class TestProtocolServerForward(unittest.TestCase): | ||
| def test_story(self): | ||
| client = unittest.TestResult() | ||
| out = BytesIO() | ||
| protocol = subunit.TestProtocolServer(client, forward_stream=out) | ||
| pipe = BytesIO(b"test old mcdonald\nsuccess old mcdonald\n") | ||
| protocol.readFrom(pipe) | ||
| self.assertEqual(client.testsRun, 1) | ||
| self.assertEqual(pipe.getvalue(), out.getvalue()) | ||
| def test_not_command(self): | ||
| client = unittest.TestResult() | ||
| out = BytesIO() | ||
| protocol = subunit.TestProtocolServer(client, stream=subunit.DiscardStream(), forward_stream=out) | ||
| pipe = BytesIO(b"success old mcdonald\n") | ||
| protocol.readFrom(pipe) | ||
| self.assertEqual(client.testsRun, 0) | ||
| self.assertEqual(b"", out.getvalue()) | ||
| class TestTestProtocolServerPipe(unittest.TestCase): | ||
| def test_story(self): | ||
| client = unittest.TestResult() | ||
| protocol = subunit.TestProtocolServer(client) | ||
| traceback = "foo.c:53:ERROR invalid state\n" | ||
| pipe = BytesIO( | ||
| b"test old mcdonald\n" | ||
| b"success old mcdonald\n" | ||
| b"test bing crosby\n" | ||
| b"failure bing crosby [\n" + traceback.encode() + b"]\n" | ||
| b"test an error\n" | ||
| b"error an error\n" | ||
| ) | ||
| protocol.readFrom(pipe) | ||
| bing = subunit.RemotedTestCase("bing crosby") | ||
| an_error = subunit.RemotedTestCase("an error") | ||
| self.assertEqual( | ||
| client.errors, | ||
| [(an_error, _remote_exception_repr + "\n")], | ||
| ) | ||
| self.assertEqual( | ||
| client.failures, | ||
| [(bing, _remote_exception_repr + ": " + details_to_str({"traceback": text_content(traceback)}) + "\n")], | ||
| ) | ||
| self.assertEqual(client.testsRun, 3) | ||
| def test_non_test_characters_forwarded_immediately(self): | ||
| pass | ||
| class TestTestProtocolServerStartTest(unittest.TestCase): | ||
| def setUp(self): | ||
| self.client = ExtendedTestResult() | ||
| self.stream = BytesIO() | ||
| self.protocol = subunit.TestProtocolServer(self.client, self.stream) | ||
| def test_start_test(self): | ||
| self.protocol.lineReceived(b"test old mcdonald\n") | ||
| self.assertEqual(self.client._events, [("startTest", subunit.RemotedTestCase("old mcdonald"))]) | ||
| def test_start_testing(self): | ||
| self.protocol.lineReceived(b"testing old mcdonald\n") | ||
| self.assertEqual(self.client._events, [("startTest", subunit.RemotedTestCase("old mcdonald"))]) | ||
| def test_start_test_colon(self): | ||
| self.protocol.lineReceived(b"test: old mcdonald\n") | ||
| self.assertEqual(self.client._events, [("startTest", subunit.RemotedTestCase("old mcdonald"))]) | ||
| def test_indented_test_colon_ignored(self): | ||
| ignored_line = b" test: old mcdonald\n" | ||
| self.protocol.lineReceived(ignored_line) | ||
| self.assertEqual([], self.client._events) | ||
| self.assertEqual(self.stream.getvalue(), ignored_line) | ||
| def test_start_testing_colon(self): | ||
| self.protocol.lineReceived(b"testing: old mcdonald\n") | ||
| self.assertEqual(self.client._events, [("startTest", subunit.RemotedTestCase("old mcdonald"))]) | ||
| class TestTestProtocolServerPassThrough(unittest.TestCase): | ||
| def setUp(self): | ||
| self.stdout = BytesIO() | ||
| self.test = subunit.RemotedTestCase("old mcdonald") | ||
| self.client = ExtendedTestResult() | ||
| self.protocol = subunit.TestProtocolServer(self.client, self.stdout) | ||
| def keywords_before_test(self): | ||
| self.protocol.lineReceived(b"failure a\n") | ||
| self.protocol.lineReceived(b"failure: a\n") | ||
| self.protocol.lineReceived(b"error a\n") | ||
| self.protocol.lineReceived(b"error: a\n") | ||
| self.protocol.lineReceived(b"success a\n") | ||
| self.protocol.lineReceived(b"success: a\n") | ||
| self.protocol.lineReceived(b"successful a\n") | ||
| self.protocol.lineReceived(b"successful: a\n") | ||
| self.protocol.lineReceived(b"]\n") | ||
| self.assertEqual( | ||
| self.stdout.getvalue(), | ||
| b"failure a\nfailure: a\nerror a\nerror: a\nsuccess a\nsuccess: a\nsuccessful a\nsuccessful: a\n]\n", | ||
| ) | ||
| def test_keywords_before_test(self): | ||
| self.keywords_before_test() | ||
| self.assertEqual(self.client._events, []) | ||
| def test_keywords_after_error(self): | ||
| self.protocol.lineReceived(b"test old mcdonald\n") | ||
| self.protocol.lineReceived(b"error old mcdonald\n") | ||
| self.keywords_before_test() | ||
| self.assertEqual( | ||
| [ | ||
| ("startTest", self.test), | ||
| ("addError", self.test, {}), | ||
| ("stopTest", self.test), | ||
| ], | ||
| self.client._events, | ||
| ) | ||
| def test_keywords_after_failure(self): | ||
| self.protocol.lineReceived(b"test old mcdonald\n") | ||
| self.protocol.lineReceived(b"failure old mcdonald\n") | ||
| self.keywords_before_test() | ||
| self.assertEqual( | ||
| self.client._events, | ||
| [ | ||
| ("startTest", self.test), | ||
| ("addFailure", self.test, {}), | ||
| ("stopTest", self.test), | ||
| ], | ||
| ) | ||
| def test_keywords_after_success(self): | ||
| self.protocol.lineReceived(b"test old mcdonald\n") | ||
| self.protocol.lineReceived(b"success old mcdonald\n") | ||
| self.keywords_before_test() | ||
| self.assertEqual( | ||
| [ | ||
| ("startTest", self.test), | ||
| ("addSuccess", self.test), | ||
| ("stopTest", self.test), | ||
| ], | ||
| self.client._events, | ||
| ) | ||
| def test_keywords_after_test(self): | ||
| self.protocol.lineReceived(b"test old mcdonald\n") | ||
| self.protocol.lineReceived(b"test old mcdonald\n") | ||
| self.protocol.lineReceived(b"failure a\n") | ||
| self.protocol.lineReceived(b"failure: a\n") | ||
| self.protocol.lineReceived(b"error a\n") | ||
| self.protocol.lineReceived(b"error: a\n") | ||
| self.protocol.lineReceived(b"success a\n") | ||
| self.protocol.lineReceived(b"success: a\n") | ||
| self.protocol.lineReceived(b"successful a\n") | ||
| self.protocol.lineReceived(b"successful: a\n") | ||
| self.protocol.lineReceived(b"]\n") | ||
| self.protocol.lineReceived(b"failure old mcdonald\n") | ||
| self.assertEqual( | ||
| self.stdout.getvalue(), | ||
| b"test old mcdonald\n" | ||
| b"failure a\n" | ||
| b"failure: a\n" | ||
| b"error a\n" | ||
| b"error: a\n" | ||
| b"success a\n" | ||
| b"success: a\n" | ||
| b"successful a\n" | ||
| b"successful: a\n" | ||
| b"]\n", | ||
| ) | ||
| self.assertEqual( | ||
| self.client._events, | ||
| [ | ||
| ("startTest", self.test), | ||
| ("addFailure", self.test, {}), | ||
| ("stopTest", self.test), | ||
| ], | ||
| ) | ||
| def test_keywords_during_failure(self): | ||
| # A smoke test to make sure that the details parsers have control | ||
| # appropriately. | ||
| self.protocol.lineReceived(b"test old mcdonald\n") | ||
| self.protocol.lineReceived(b"failure: old mcdonald [\n") | ||
| self.protocol.lineReceived(b"test old mcdonald\n") | ||
| self.protocol.lineReceived(b"failure a\n") | ||
| self.protocol.lineReceived(b"failure: a\n") | ||
| self.protocol.lineReceived(b"error a\n") | ||
| self.protocol.lineReceived(b"error: a\n") | ||
| self.protocol.lineReceived(b"success a\n") | ||
| self.protocol.lineReceived(b"success: a\n") | ||
| self.protocol.lineReceived(b"successful a\n") | ||
| self.protocol.lineReceived(b"successful: a\n") | ||
| self.protocol.lineReceived(b" ]\n") | ||
| self.protocol.lineReceived(b"]\n") | ||
| self.assertEqual(self.stdout.getvalue(), b"") | ||
| details = {} | ||
| details["traceback"] = Content( | ||
| ContentType("text", "x-traceback", {"charset": "utf8"}), | ||
| lambda: [ | ||
| b"test old mcdonald\n" | ||
| b"failure a\n" | ||
| b"failure: a\n" | ||
| b"error a\n" | ||
| b"error: a\n" | ||
| b"success a\n" | ||
| b"success: a\n" | ||
| b"successful a\n" | ||
| b"successful: a\n" | ||
| b"]\n" | ||
| ], | ||
| ) | ||
| self.assertEqual( | ||
| self.client._events, | ||
| [ | ||
| ("startTest", self.test), | ||
| ("addFailure", self.test, details), | ||
| ("stopTest", self.test), | ||
| ], | ||
| ) | ||
| def test_stdout_passthrough(self): | ||
| """Lines received which cannot be interpreted as any protocol action | ||
| should be passed through to sys.stdout. | ||
| """ | ||
| bytes = b"randombytes\n" | ||
| self.protocol.lineReceived(bytes) | ||
| self.assertEqual(self.stdout.getvalue(), bytes) | ||
| class TestTestProtocolServerLostConnection(unittest.TestCase): | ||
| def setUp(self): | ||
| self.client = ExtendedTestResult() | ||
| self.protocol = subunit.TestProtocolServer(self.client) | ||
| self.test = subunit.RemotedTestCase("old mcdonald") | ||
| def test_lost_connection_no_input(self): | ||
| self.protocol.lostConnection() | ||
| self.assertEqual([], self.client._events) | ||
| def test_lost_connection_after_start(self): | ||
| self.protocol.lineReceived(b"test old mcdonald\n") | ||
| self.protocol.lostConnection() | ||
| failure = subunit.RemoteError("lost connection during test 'old mcdonald'") | ||
| self.assertEqual( | ||
| [ | ||
| ("startTest", self.test), | ||
| ("addError", self.test, failure), | ||
| ("stopTest", self.test), | ||
| ], | ||
| self.client._events, | ||
| ) | ||
| def test_lost_connected_after_error(self): | ||
| self.protocol.lineReceived(b"test old mcdonald\n") | ||
| self.protocol.lineReceived(b"error old mcdonald\n") | ||
| self.protocol.lostConnection() | ||
| self.assertEqual( | ||
| [ | ||
| ("startTest", self.test), | ||
| ("addError", self.test, {}), | ||
| ("stopTest", self.test), | ||
| ], | ||
| self.client._events, | ||
| ) | ||
| def do_connection_lost(self, outcome, opening): | ||
| self.protocol.lineReceived(b"test old mcdonald\n") | ||
| self.protocol.lineReceived("{} old mcdonald {}".format(outcome, opening).encode()) | ||
| self.protocol.lostConnection() | ||
| failure = subunit.RemoteError("lost connection during %s report of test 'old mcdonald'" % outcome) | ||
| self.assertEqual( | ||
| [ | ||
| ("startTest", self.test), | ||
| ("addError", self.test, failure), | ||
| ("stopTest", self.test), | ||
| ], | ||
| self.client._events, | ||
| ) | ||
| def test_lost_connection_during_error(self): | ||
| self.do_connection_lost("error", "[\n") | ||
| def test_lost_connection_during_error_details(self): | ||
| self.do_connection_lost("error", "[ multipart\n") | ||
| def test_lost_connected_after_failure(self): | ||
| self.protocol.lineReceived(b"test old mcdonald\n") | ||
| self.protocol.lineReceived(b"failure old mcdonald\n") | ||
| self.protocol.lostConnection() | ||
| self.assertEqual( | ||
| [ | ||
| ("startTest", self.test), | ||
| ("addFailure", self.test, {}), | ||
| ("stopTest", self.test), | ||
| ], | ||
| self.client._events, | ||
| ) | ||
| def test_lost_connection_during_failure(self): | ||
| self.do_connection_lost("failure", "[\n") | ||
| def test_lost_connection_during_failure_details(self): | ||
| self.do_connection_lost("failure", "[ multipart\n") | ||
| def test_lost_connection_after_success(self): | ||
| self.protocol.lineReceived(b"test old mcdonald\n") | ||
| self.protocol.lineReceived(b"success old mcdonald\n") | ||
| self.protocol.lostConnection() | ||
| self.assertEqual( | ||
| [ | ||
| ("startTest", self.test), | ||
| ("addSuccess", self.test), | ||
| ("stopTest", self.test), | ||
| ], | ||
| self.client._events, | ||
| ) | ||
| def test_lost_connection_during_success(self): | ||
| self.do_connection_lost("success", "[\n") | ||
| def test_lost_connection_during_success_details(self): | ||
| self.do_connection_lost("success", "[ multipart\n") | ||
| def test_lost_connection_during_skip(self): | ||
| self.do_connection_lost("skip", "[\n") | ||
| def test_lost_connection_during_skip_details(self): | ||
| self.do_connection_lost("skip", "[ multipart\n") | ||
| def test_lost_connection_during_xfail(self): | ||
| self.do_connection_lost("xfail", "[\n") | ||
| def test_lost_connection_during_xfail_details(self): | ||
| self.do_connection_lost("xfail", "[ multipart\n") | ||
| def test_lost_connection_during_uxsuccess(self): | ||
| self.do_connection_lost("uxsuccess", "[\n") | ||
| def test_lost_connection_during_uxsuccess_details(self): | ||
| self.do_connection_lost("uxsuccess", "[ multipart\n") | ||
| class TestInTestMultipart(unittest.TestCase): | ||
| def setUp(self): | ||
| self.client = ExtendedTestResult() | ||
| self.protocol = subunit.TestProtocolServer(self.client) | ||
| self.protocol.lineReceived(b"test mcdonalds farm\n") | ||
| self.test = subunit.RemotedTestCase("mcdonalds farm") | ||
| def test__outcome_sets_details_parser(self): | ||
| self.protocol._reading_success_details.details_parser = None | ||
| self.protocol._state._outcome(0, b"mcdonalds farm [ multipart\n", None, self.protocol._reading_success_details) | ||
| parser = self.protocol._reading_success_details.details_parser | ||
| self.assertNotEqual(None, parser) | ||
| self.assertIsInstance(parser, subunit.details.MultipartDetailsParser) | ||
| class TestTestProtocolServerAddError(unittest.TestCase): | ||
| def setUp(self): | ||
| self.client = ExtendedTestResult() | ||
| self.protocol = subunit.TestProtocolServer(self.client) | ||
| self.protocol.lineReceived(b"test mcdonalds farm\n") | ||
| self.test = subunit.RemotedTestCase("mcdonalds farm") | ||
| def simple_error_keyword(self, keyword): | ||
| self.protocol.lineReceived(("%s mcdonalds farm\n" % keyword).encode()) | ||
| details = {} | ||
| self.assertEqual( | ||
| [ | ||
| ("startTest", self.test), | ||
| ("addError", self.test, details), | ||
| ("stopTest", self.test), | ||
| ], | ||
| self.client._events, | ||
| ) | ||
| def test_simple_error(self): | ||
| self.simple_error_keyword("error") | ||
| def test_simple_error_colon(self): | ||
| self.simple_error_keyword("error:") | ||
| def test_error_empty_message(self): | ||
| self.protocol.lineReceived(b"error mcdonalds farm [\n") | ||
| self.protocol.lineReceived(b"]\n") | ||
| details = {} | ||
| details["traceback"] = Content(ContentType("text", "x-traceback", {"charset": "utf8"}), lambda: [b""]) | ||
| self.assertEqual( | ||
| [ | ||
| ("startTest", self.test), | ||
| ("addError", self.test, details), | ||
| ("stopTest", self.test), | ||
| ], | ||
| self.client._events, | ||
| ) | ||
| def error_quoted_bracket(self, keyword): | ||
| self.protocol.lineReceived(("%s mcdonalds farm [\n" % keyword).encode()) | ||
| self.protocol.lineReceived(b" ]\n") | ||
| self.protocol.lineReceived(b"]\n") | ||
| details = {} | ||
| details["traceback"] = Content(ContentType("text", "x-traceback", {"charset": "utf8"}), lambda: [b"]\n"]) | ||
| self.assertEqual( | ||
| [ | ||
| ("startTest", self.test), | ||
| ("addError", self.test, details), | ||
| ("stopTest", self.test), | ||
| ], | ||
| self.client._events, | ||
| ) | ||
| def test_error_quoted_bracket(self): | ||
| self.error_quoted_bracket("error") | ||
| def test_error_colon_quoted_bracket(self): | ||
| self.error_quoted_bracket("error:") | ||
| class TestTestProtocolServerAddFailure(unittest.TestCase): | ||
| def setUp(self): | ||
| self.client = ExtendedTestResult() | ||
| self.protocol = subunit.TestProtocolServer(self.client) | ||
| self.protocol.lineReceived(b"test mcdonalds farm\n") | ||
| self.test = subunit.RemotedTestCase("mcdonalds farm") | ||
| def assertFailure(self, details): | ||
| self.assertEqual( | ||
| [ | ||
| ("startTest", self.test), | ||
| ("addFailure", self.test, details), | ||
| ("stopTest", self.test), | ||
| ], | ||
| self.client._events, | ||
| ) | ||
| def simple_failure_keyword(self, keyword): | ||
| self.protocol.lineReceived(("%s mcdonalds farm\n" % keyword).encode()) | ||
| details = {} | ||
| self.assertFailure(details) | ||
| def test_simple_failure(self): | ||
| self.simple_failure_keyword("failure") | ||
| def test_simple_failure_colon(self): | ||
| self.simple_failure_keyword("failure:") | ||
| def test_failure_empty_message(self): | ||
| self.protocol.lineReceived(b"failure mcdonalds farm [\n") | ||
| self.protocol.lineReceived(b"]\n") | ||
| details = {} | ||
| details["traceback"] = Content(ContentType("text", "x-traceback", {"charset": "utf8"}), lambda: [b""]) | ||
| self.assertFailure(details) | ||
| def failure_quoted_bracket(self, keyword): | ||
| self.protocol.lineReceived(("%s mcdonalds farm [\n" % keyword).encode()) | ||
| self.protocol.lineReceived(b" ]\n") | ||
| self.protocol.lineReceived(b"]\n") | ||
| details = {} | ||
| details["traceback"] = Content(ContentType("text", "x-traceback", {"charset": "utf8"}), lambda: [b"]\n"]) | ||
| self.assertFailure(details) | ||
| def test_failure_quoted_bracket(self): | ||
| self.failure_quoted_bracket("failure") | ||
| def test_failure_colon_quoted_bracket(self): | ||
| self.failure_quoted_bracket("failure:") | ||
| class TestTestProtocolServerAddxFail(unittest.TestCase): | ||
| """Tests for the xfail keyword.""" | ||
| def setUp(self): | ||
| """Setup a test object ready to be xfailed with details.""" | ||
| self.client = ExtendedTestResult() | ||
| self.protocol = subunit.TestProtocolServer(self.client) | ||
| self.protocol.lineReceived(b"test mcdonalds farm\n") | ||
| self.test = self.client._events[-1][-1] | ||
| def simple_xfail_keyword(self, keyword): | ||
| self.protocol.lineReceived(("%s mcdonalds farm\n" % keyword).encode()) | ||
| self.check_xfail() | ||
| def check_xfail(self, error_message=None): | ||
| details = {} | ||
| if error_message is not None: | ||
| details["traceback"] = Content( | ||
| ContentType("text", "x-traceback", {"charset": "utf8"}), lambda: [error_message.encode()] | ||
| ) | ||
| self.assertEqual( | ||
| [ | ||
| ("startTest", self.test), | ||
| ("addExpectedFailure", self.test, details), | ||
| ("stopTest", self.test), | ||
| ], | ||
| self.client._events, | ||
| ) | ||
| def test_simple_xfail(self): | ||
| self.simple_xfail_keyword("xfail") | ||
| def test_simple_xfail_colon(self): | ||
| self.simple_xfail_keyword("xfail:") | ||
| def test_xfail_empty_message(self): | ||
| self.protocol.lineReceived(b"xfail mcdonalds farm [\n") | ||
| self.protocol.lineReceived(b"]\n") | ||
| self.check_xfail(error_message="") | ||
| def test_xfail_quoted_bracket(self): | ||
| self.protocol.lineReceived(b"xfail mcdonalds farm [\n") | ||
| self.protocol.lineReceived(b" ]\n") | ||
| self.protocol.lineReceived(b"]\n") | ||
| self.check_xfail("]\n") | ||
| def test_xfail_colon_quoted_bracket(self): | ||
| self.protocol.lineReceived(b"xfail: mcdonalds farm [\n") | ||
| self.protocol.lineReceived(b" ]\n") | ||
| self.protocol.lineReceived(b"]\n") | ||
| self.check_xfail("]\n") | ||
| class TestTestProtocolServerAddunexpectedSuccess(TestCase): | ||
| """Tests for the uxsuccess keyword.""" | ||
| def setUp(self): | ||
| """Setup a test object ready for uxsuccess with details.""" | ||
| super().setUp() | ||
| self.client = ExtendedTestResult() | ||
| self.protocol = subunit.TestProtocolServer(self.client) | ||
| self.protocol.lineReceived(b"test mcdonalds farm\n") | ||
| self.test = self.client._events[-1][-1] | ||
| def simple_uxsuccess_keyword(self, keyword): | ||
| self.protocol.lineReceived(("%s mcdonalds farm\n" % keyword).encode()) | ||
| self.check_uxsuccess() | ||
| def check_uxsuccess(self, error_message=None): | ||
| if error_message is not None: | ||
| details = {} | ||
| details["traceback"] = Content( | ||
| ContentType("text", "x-traceback", {"charset": "utf8"}), lambda: [error_message.encode()] | ||
| ) | ||
| expected_events = [ | ||
| ("startTest", self.test), | ||
| ("addUnexpectedSuccess", self.test, details), | ||
| ("stopTest", self.test), | ||
| ] | ||
| else: | ||
| expected_events = [ | ||
| ("startTest", self.test), | ||
| ("addUnexpectedSuccess", self.test), | ||
| ("stopTest", self.test), | ||
| ] | ||
| self.assertEqual(expected_events, self.client._events) | ||
| def test_simple_uxsuccess(self): | ||
| self.simple_uxsuccess_keyword("uxsuccess") | ||
| def test_simple_uxsuccess_colon(self): | ||
| self.simple_uxsuccess_keyword("uxsuccess:") | ||
| def test_uxsuccess_empty_message(self): | ||
| self.protocol.lineReceived(b"uxsuccess mcdonalds farm [\n") | ||
| self.protocol.lineReceived(b"]\n") | ||
| self.check_uxsuccess(error_message="") | ||
| def test_uxsuccess_quoted_bracket(self): | ||
| self.protocol.lineReceived(b"uxsuccess mcdonalds farm [\n") | ||
| self.protocol.lineReceived(b" ]\n") | ||
| self.protocol.lineReceived(b"]\n") | ||
| self.check_uxsuccess("]\n") | ||
| def test_uxsuccess_colon_quoted_bracket(self): | ||
| self.protocol.lineReceived(b"uxsuccess: mcdonalds farm [\n") | ||
| self.protocol.lineReceived(b" ]\n") | ||
| self.protocol.lineReceived(b"]\n") | ||
| self.check_uxsuccess("]\n") | ||
| class TestTestProtocolServerAddSkip(unittest.TestCase): | ||
| """Tests for the skip keyword. | ||
| In Python this meets the testtools extended TestResult contract. | ||
| (See https://launchpad.net/testtools). | ||
| """ | ||
| def setUp(self): | ||
| """Setup a test object ready to be skipped.""" | ||
| self.client = ExtendedTestResult() | ||
| self.protocol = subunit.TestProtocolServer(self.client) | ||
| self.protocol.lineReceived(b"test mcdonalds farm\n") | ||
| self.test = self.client._events[-1][-1] | ||
| def assertSkip(self, reason): | ||
| details = {} | ||
| if reason is not None: | ||
| details["reason"] = Content(ContentType("text", "plain"), lambda: [reason]) | ||
| self.assertEqual( | ||
| [ | ||
| ("startTest", self.test), | ||
| ("addSkip", self.test, details), | ||
| ("stopTest", self.test), | ||
| ], | ||
| self.client._events, | ||
| ) | ||
| def simple_skip_keyword(self, keyword): | ||
| self.protocol.lineReceived(("%s mcdonalds farm\n" % keyword).encode()) | ||
| self.assertSkip(None) | ||
| def test_simple_skip(self): | ||
| self.simple_skip_keyword("skip") | ||
| def test_simple_skip_colon(self): | ||
| self.simple_skip_keyword("skip:") | ||
| def test_skip_empty_message(self): | ||
| self.protocol.lineReceived(b"skip mcdonalds farm [\n") | ||
| self.protocol.lineReceived(b"]\n") | ||
| self.assertSkip(b"") | ||
| def skip_quoted_bracket(self, keyword): | ||
| # This tests it is accepted, but cannot test it is used today, because | ||
| # of not having a way to expose it in Python so far. | ||
| self.protocol.lineReceived(("%s mcdonalds farm [\n" % keyword).encode()) | ||
| self.protocol.lineReceived(b" ]\n") | ||
| self.protocol.lineReceived(b"]\n") | ||
| self.assertSkip(b"]\n") | ||
| def test_skip_quoted_bracket(self): | ||
| self.skip_quoted_bracket("skip") | ||
| def test_skip_colon_quoted_bracket(self): | ||
| self.skip_quoted_bracket("skip:") | ||
| class TestTestProtocolServerAddSuccess(unittest.TestCase): | ||
| def setUp(self): | ||
| self.client = ExtendedTestResult() | ||
| self.protocol = subunit.TestProtocolServer(self.client) | ||
| self.protocol.lineReceived(b"test mcdonalds farm\n") | ||
| self.test = subunit.RemotedTestCase("mcdonalds farm") | ||
| def simple_success_keyword(self, keyword): | ||
| self.protocol.lineReceived(("%s mcdonalds farm\n" % keyword).encode()) | ||
| self.assertEqual( | ||
| [ | ||
| ("startTest", self.test), | ||
| ("addSuccess", self.test), | ||
| ("stopTest", self.test), | ||
| ], | ||
| self.client._events, | ||
| ) | ||
| def test_simple_success(self): | ||
| self.simple_success_keyword("successful") | ||
| def test_simple_success_colon(self): | ||
| self.simple_success_keyword("successful:") | ||
| def assertSuccess(self, details): | ||
| self.assertEqual( | ||
| [ | ||
| ("startTest", self.test), | ||
| ("addSuccess", self.test, details), | ||
| ("stopTest", self.test), | ||
| ], | ||
| self.client._events, | ||
| ) | ||
| def test_success_empty_message(self): | ||
| self.protocol.lineReceived(b"success mcdonalds farm [\n") | ||
| self.protocol.lineReceived(b"]\n") | ||
| details = {} | ||
| details["message"] = Content(ContentType("text", "plain"), lambda: [b""]) | ||
| self.assertSuccess(details) | ||
| def success_quoted_bracket(self, keyword): | ||
| # This tests it is accepted, but cannot test it is used today, because | ||
| # of not having a way to expose it in Python so far. | ||
| self.protocol.lineReceived(("%s mcdonalds farm [\n" % keyword).encode()) | ||
| self.protocol.lineReceived(b" ]\n") | ||
| self.protocol.lineReceived(b"]\n") | ||
| details = {} | ||
| details["message"] = Content(ContentType("text", "plain"), lambda: [b"]\n"]) | ||
| self.assertSuccess(details) | ||
| def test_success_quoted_bracket(self): | ||
| self.success_quoted_bracket("success") | ||
| def test_success_colon_quoted_bracket(self): | ||
| self.success_quoted_bracket("success:") | ||
| class TestTestProtocolServerProgress(unittest.TestCase): | ||
| """Test receipt of progress: directives.""" | ||
| def test_progress_accepted_stdlib(self): | ||
| self.result = ExtendedTestResult() | ||
| self.stream = BytesIO() | ||
| self.protocol = subunit.TestProtocolServer(self.result, stream=self.stream) | ||
| self.protocol.lineReceived(b"progress: 23") | ||
| self.protocol.lineReceived(b"progress: -2") | ||
| self.protocol.lineReceived(b"progress: +4") | ||
| self.assertEqual(b"", self.stream.getvalue()) | ||
| def test_progress_accepted_extended(self): | ||
| # With a progress capable TestResult, progress events are emitted. | ||
| self.result = ExtendedTestResult() | ||
| self.stream = BytesIO() | ||
| self.protocol = subunit.TestProtocolServer(self.result, stream=self.stream) | ||
| self.protocol.lineReceived(b"progress: 23") | ||
| self.protocol.lineReceived(b"progress: push") | ||
| self.protocol.lineReceived(b"progress: -2") | ||
| self.protocol.lineReceived(b"progress: pop") | ||
| self.protocol.lineReceived(b"progress: +4") | ||
| self.assertEqual(b"", self.stream.getvalue()) | ||
| self.assertEqual( | ||
| [ | ||
| ("progress", 23, subunit.PROGRESS_SET), | ||
| ("progress", None, subunit.PROGRESS_PUSH), | ||
| ("progress", -2, subunit.PROGRESS_CUR), | ||
| ("progress", None, subunit.PROGRESS_POP), | ||
| ("progress", 4, subunit.PROGRESS_CUR), | ||
| ], | ||
| self.result._events, | ||
| ) | ||
| class TestTestProtocolServerStreamTags(unittest.TestCase): | ||
| """Test managing tags on the protocol level.""" | ||
| def setUp(self): | ||
| self.client = ExtendedTestResult() | ||
| self.protocol = subunit.TestProtocolServer(self.client) | ||
| def test_initial_tags(self): | ||
| self.protocol.lineReceived(b"tags: foo bar:baz quux\n") | ||
| self.assertEqual( | ||
| [ | ||
| ("tags", {"foo", "bar:baz", "quux"}, set()), | ||
| ], | ||
| self.client._events, | ||
| ) | ||
| def test_minus_removes_tags(self): | ||
| self.protocol.lineReceived(b"tags: -bar quux\n") | ||
| self.assertEqual( | ||
| [ | ||
| ("tags", {"quux"}, {"bar"}), | ||
| ], | ||
| self.client._events, | ||
| ) | ||
| def test_tags_do_not_get_set_on_test(self): | ||
| self.protocol.lineReceived(b"test mcdonalds farm\n") | ||
| test = self.client._events[0][-1] | ||
| self.assertEqual(None, getattr(test, "tags", None)) | ||
| def test_tags_do_not_get_set_on_global_tags(self): | ||
| self.protocol.lineReceived(b"tags: foo bar\n") | ||
| self.protocol.lineReceived(b"test mcdonalds farm\n") | ||
| test = self.client._events[-1][-1] | ||
| self.assertEqual(None, getattr(test, "tags", None)) | ||
| def test_tags_get_set_on_test_tags(self): | ||
| self.protocol.lineReceived(b"test mcdonalds farm\n") | ||
| test = self.client._events[-1][-1] | ||
| self.protocol.lineReceived(b"tags: foo bar\n") | ||
| self.protocol.lineReceived(b"success mcdonalds farm\n") | ||
| self.assertEqual(None, getattr(test, "tags", None)) | ||
| class TestTestProtocolServerStreamTime(unittest.TestCase): | ||
| """Test managing time information at the protocol level.""" | ||
| def test_time_accepted_stdlib(self): | ||
| self.result = ExtendedTestResult() | ||
| self.stream = BytesIO() | ||
| self.protocol = subunit.TestProtocolServer(self.result, stream=self.stream) | ||
| self.protocol.lineReceived(b"time: 2001-12-12 12:59:59Z\n") | ||
| self.assertEqual(b"", self.stream.getvalue()) | ||
| def test_time_accepted_extended(self): | ||
| self.result = ExtendedTestResult() | ||
| self.stream = BytesIO() | ||
| self.protocol = subunit.TestProtocolServer(self.result, stream=self.stream) | ||
| self.protocol.lineReceived(b"time: 2001-12-12 12:59:59Z\n") | ||
| self.assertEqual(b"", self.stream.getvalue()) | ||
| self.assertEqual([("time", datetime.datetime(2001, 12, 12, 12, 59, 59, 0, iso8601.UTC))], self.result._events) | ||
| class TestRemotedTestCase(unittest.TestCase): | ||
| def test_simple(self): | ||
| test = subunit.RemotedTestCase("A test description") | ||
| self.assertRaises(NotImplementedError, test.setUp) | ||
| self.assertRaises(NotImplementedError, test.tearDown) | ||
| self.assertEqual("A test description", test.shortDescription()) | ||
| self.assertEqual("A test description", test.id()) | ||
| self.assertEqual("A test description (subunit.RemotedTestCase)", "%s" % test) | ||
| self.assertEqual("<subunit.RemotedTestCase description='A test description'>", "%r" % test) | ||
| result = unittest.TestResult() | ||
| test.run(result) | ||
| self.assertEqual([(test, _remote_exception_repr + ": " + "Cannot run RemotedTestCases.\n\n")], result.errors) | ||
| self.assertEqual(1, result.testsRun) | ||
| another_test = subunit.RemotedTestCase("A test description") | ||
| self.assertEqual(test, another_test) | ||
| different_test = subunit.RemotedTestCase("ofo") | ||
| self.assertNotEqual(test, different_test) | ||
| self.assertNotEqual(another_test, different_test) | ||
| class TestRemoteError(unittest.TestCase): | ||
| def test_eq(self): | ||
| error = subunit.RemoteError("Something went wrong") | ||
| another_error = subunit.RemoteError("Something went wrong") | ||
| different_error = subunit.RemoteError("boo!") | ||
| self.assertEqual(error, another_error) | ||
| self.assertNotEqual(error, different_error) | ||
| self.assertNotEqual(different_error, another_error) | ||
| def test_empty_constructor(self): | ||
| self.assertEqual(subunit.RemoteError(), subunit.RemoteError("")) | ||
| class TestExecTestCase(unittest.TestCase): | ||
| class SampleExecTestCase(subunit.ExecTestCase): | ||
| def test_sample_method(self): | ||
| """sample-script.py""" | ||
| # the sample script runs three tests, one each | ||
| # that fails, errors and succeeds | ||
| def test_sample_method_args(self): | ||
| """sample-script.py foo""" | ||
| # sample that will run just one test. | ||
| def test_construct(self): | ||
| test = self.SampleExecTestCase("test_sample_method") | ||
| self.assertEqual(test.script, subunit.join_dir(__file__, "sample-script.py")) | ||
| def test_args(self): | ||
| result = unittest.TestResult() | ||
| test = self.SampleExecTestCase("test_sample_method_args") | ||
| test.run(result) | ||
| self.assertEqual(1, result.testsRun) | ||
| def test_run(self): | ||
| result = ExtendedTestResult() | ||
| test = self.SampleExecTestCase("test_sample_method") | ||
| test.run(result) | ||
| mcdonald = subunit.RemotedTestCase("old mcdonald") | ||
| bing = subunit.RemotedTestCase("bing crosby") | ||
| bing_details = {} | ||
| bing_details["traceback"] = Content( | ||
| ContentType("text", "x-traceback", {"charset": "utf8"}), lambda: [b"foo.c:53:ERROR invalid state\n"] | ||
| ) | ||
| an_error = subunit.RemotedTestCase("an error") | ||
| error_details = {} | ||
| self.assertEqual( | ||
| [ | ||
| ("startTest", mcdonald), | ||
| ("addSuccess", mcdonald), | ||
| ("stopTest", mcdonald), | ||
| ("startTest", bing), | ||
| ("addFailure", bing, bing_details), | ||
| ("stopTest", bing), | ||
| ("startTest", an_error), | ||
| ("addError", an_error, error_details), | ||
| ("stopTest", an_error), | ||
| ], | ||
| result._events, | ||
| ) | ||
| def test_debug(self): | ||
| test = self.SampleExecTestCase("test_sample_method") | ||
| test.debug() | ||
| def test_count_test_cases(self): | ||
| """Run the child process and count responses to determine the count.""" | ||
| def test_join_dir(self): | ||
| sibling = subunit.join_dir(__file__, "foo") | ||
| filedir = os.path.abspath(os.path.dirname(__file__)) | ||
| expected = os.path.join(filedir, "foo") | ||
| self.assertEqual(sibling, expected) | ||
| class DoExecTestCase(subunit.ExecTestCase): | ||
| def test_working_script(self): | ||
| """sample-two-script.py""" | ||
| class TestIsolatedTestCase(TestCase): | ||
| class SampleIsolatedTestCase(subunit.IsolatedTestCase): | ||
| SETUP = False | ||
| TEARDOWN = False | ||
| TEST = False | ||
| def setUp(self): | ||
| TestIsolatedTestCase.SampleIsolatedTestCase.SETUP = True | ||
| def tearDown(self): | ||
| TestIsolatedTestCase.SampleIsolatedTestCase.TEARDOWN = True | ||
| def test_sets_global_state(self): | ||
| TestIsolatedTestCase.SampleIsolatedTestCase.TEST = True | ||
| def test_construct(self): | ||
| self.SampleIsolatedTestCase("test_sets_global_state") | ||
| @skipIf(os.name != "posix", "Need a posix system for forking tests") | ||
| def test_run(self): | ||
| result = unittest.TestResult() | ||
| test = self.SampleIsolatedTestCase("test_sets_global_state") | ||
| test.run(result) | ||
| self.assertEqual(result.testsRun, 1) | ||
| self.assertEqual(self.SampleIsolatedTestCase.SETUP, False) | ||
| self.assertEqual(self.SampleIsolatedTestCase.TEARDOWN, False) | ||
| self.assertEqual(self.SampleIsolatedTestCase.TEST, False) | ||
| def test_debug(self): | ||
| pass | ||
| # test = self.SampleExecTestCase("test_sample_method") | ||
| # test.debug() | ||
| class TestIsolatedTestSuite(TestCase): | ||
| class SampleTestToIsolate(unittest.TestCase): | ||
| SETUP = False | ||
| TEARDOWN = False | ||
| TEST = False | ||
| def setUp(self): | ||
| TestIsolatedTestSuite.SampleTestToIsolate.SETUP = True | ||
| def tearDown(self): | ||
| TestIsolatedTestSuite.SampleTestToIsolate.TEARDOWN = True | ||
| def test_sets_global_state(self): | ||
| TestIsolatedTestSuite.SampleTestToIsolate.TEST = True | ||
| def test_construct(self): | ||
| subunit.IsolatedTestSuite() | ||
| @skipIf(os.name != "posix", "Need a posix system for forking tests") | ||
| def test_run(self): | ||
| result = unittest.TestResult() | ||
| suite = subunit.IsolatedTestSuite() | ||
| sub_suite = unittest.TestSuite() | ||
| sub_suite.addTest(self.SampleTestToIsolate("test_sets_global_state")) | ||
| sub_suite.addTest(self.SampleTestToIsolate("test_sets_global_state")) | ||
| suite.addTest(sub_suite) | ||
| suite.addTest(self.SampleTestToIsolate("test_sets_global_state")) | ||
| suite.run(result) | ||
| self.assertEqual(result.testsRun, 3) | ||
| self.assertEqual(self.SampleTestToIsolate.SETUP, False) | ||
| self.assertEqual(self.SampleTestToIsolate.TEARDOWN, False) | ||
| self.assertEqual(self.SampleTestToIsolate.TEST, False) | ||
| # A number of these tests produce different output depending on the | ||
| # testtools version. testtools < 2.5.0 used traceback2, which incorrectly | ||
| # included the traceback header even for an exception with no traceback. | ||
| # testtools 2.5.0 switched to the Python 3 standard library's traceback | ||
| # module, which fixes this bug. See https://bugs.python.org/issue24695. | ||
| class TestTestProtocolClient(TestCase): | ||
| def setUp(self): | ||
| super().setUp() | ||
| self.io = BytesIO() | ||
| self.protocol = subunit.TestProtocolClient(self.io) | ||
| self.unicode_test = PlaceHolder("\u2603") | ||
| self.test = TestTestProtocolClient("test_start_test") | ||
| self.sample_details = {"something": Content(ContentType("text", "plain"), lambda: [b"serialised\nform"])} | ||
| self.sample_tb_details = dict(self.sample_details) | ||
| self.sample_tb_details["traceback"] = TracebackContent(subunit.RemoteError("boo qux"), self.test) | ||
| def test_start_test(self): | ||
| """Test startTest on a TestProtocolClient.""" | ||
| self.protocol.startTest(self.test) | ||
| self.assertEqual(self.io.getvalue(), ("test: %s\n" % self.test.id()).encode()) | ||
| def test_start_test_unicode_id(self): | ||
| """Test startTest on a TestProtocolClient.""" | ||
| self.protocol.startTest(self.unicode_test) | ||
| expected = b"test: " + "\u2603".encode("utf8") + b"\n" | ||
| self.assertEqual(expected, self.io.getvalue()) | ||
| def test_stop_test(self): | ||
| # stopTest doesn't output anything. | ||
| self.protocol.stopTest(self.test) | ||
| self.assertEqual(self.io.getvalue(), b"") | ||
| def test_add_success(self): | ||
| """Test addSuccess on a TestProtocolClient.""" | ||
| self.protocol.addSuccess(self.test) | ||
| self.assertEqual(self.io.getvalue(), ("successful: %s\n" % self.test.id()).encode()) | ||
| def test_add_outcome_unicode_id(self): | ||
| """Test addSuccess on a TestProtocolClient.""" | ||
| self.protocol.addSuccess(self.unicode_test) | ||
| expected = b"successful: " + "\u2603".encode("utf8") + b"\n" | ||
| self.assertEqual(expected, self.io.getvalue()) | ||
| def test_add_success_details(self): | ||
| """Test addSuccess on a TestProtocolClient with details.""" | ||
| self.protocol.addSuccess(self.test, details=self.sample_details) | ||
| self.assertEqual( | ||
| self.io.getvalue(), | ||
| ( | ||
| "successful: %s [ multipart\n" | ||
| "Content-Type: text/plain\n" | ||
| "something\n" | ||
| "F\r\nserialised\nform0\r\n]\n" % self.test.id() | ||
| ).encode(), | ||
| ) | ||
| def test_add_failure(self): | ||
| """Test addFailure on a TestProtocolClient.""" | ||
| self.protocol.addFailure(self.test, subunit.RemoteError("boo qux")) | ||
| self.assertThat( | ||
| self.io.getvalue(), | ||
| MatchesAny( | ||
| # testtools < 2.5.0 | ||
| Equals((("failure: %s [\n" + _remote_exception_str + ": boo qux\n" + "]\n") % self.test.id()).encode()), | ||
| # testtools >= 2.5.0 | ||
| Equals( | ||
| (("failure: %s [\n" + _remote_exception_repr + ": boo qux\n" + "]\n") % self.test.id()).encode() | ||
| ), | ||
| ), | ||
| ) | ||
| def test_add_failure_details(self): | ||
| """Test addFailure on a TestProtocolClient with details.""" | ||
| self.protocol.addFailure(self.test, details=self.sample_tb_details) | ||
| self.assertThat( | ||
| self.io.getvalue(), | ||
| MatchesAny( | ||
| # testtools < 2.5.0 | ||
| Equals( | ||
| ( | ||
| ( | ||
| "failure: %s [ multipart\n" | ||
| "Content-Type: text/plain\n" | ||
| "something\n" | ||
| "F\r\nserialised\nform0\r\n" | ||
| "Content-Type: text/x-traceback;charset=utf8,language=python\n" | ||
| "traceback\n" + _remote_exception_str_chunked + "]\n" | ||
| ) | ||
| % self.test.id() | ||
| ).encode() | ||
| ), | ||
| # testtools >= 2.5.0 | ||
| Equals( | ||
| ( | ||
| ( | ||
| "failure: %s [ multipart\n" | ||
| "Content-Type: text/plain\n" | ||
| "something\n" | ||
| "F\r\nserialised\nform0\r\n" | ||
| "Content-Type: text/x-traceback;charset=utf8,language=python\n" | ||
| "traceback\n" + _remote_exception_repr_chunked + "]\n" | ||
| ) | ||
| % self.test.id() | ||
| ).encode() | ||
| ), | ||
| ), | ||
| ) | ||
| def test_add_error(self): | ||
| """Test stopTest on a TestProtocolClient.""" | ||
| self.protocol.addError(self.test, subunit.RemoteError("phwoar crikey")) | ||
| self.assertThat( | ||
| self.io.getvalue(), | ||
| MatchesAny( | ||
| # testtools < 2.5.0 | ||
| Equals((("error: %s [\n" + _remote_exception_str + ": phwoar crikey\n]\n") % self.test.id()).encode()), | ||
| # testtools >= 2.5.0 | ||
| Equals((("error: %s [\n" + _remote_exception_repr + ": phwoar crikey\n]\n") % self.test.id()).encode()), | ||
| ), | ||
| ) | ||
| def test_add_error_details(self): | ||
| """Test stopTest on a TestProtocolClient with details.""" | ||
| self.protocol.addError(self.test, details=self.sample_tb_details) | ||
| self.assertThat( | ||
| self.io.getvalue(), | ||
| MatchesAny( | ||
| # testtools < 2.5.0 | ||
| Equals( | ||
| ( | ||
| ( | ||
| "error: %s [ multipart\n" | ||
| "Content-Type: text/plain\n" | ||
| "something\n" | ||
| "F\r\nserialised\nform0\r\n" | ||
| "Content-Type: text/x-traceback;charset=utf8,language=python\n" | ||
| "traceback\n" + _remote_exception_str_chunked + "]\n" | ||
| ) | ||
| % self.test.id() | ||
| ).encode() | ||
| ), | ||
| # testtools >= 2.5.0 | ||
| Equals( | ||
| ( | ||
| ( | ||
| "error: %s [ multipart\n" | ||
| "Content-Type: text/plain\n" | ||
| "something\n" | ||
| "F\r\nserialised\nform0\r\n" | ||
| "Content-Type: text/x-traceback;charset=utf8,language=python\n" | ||
| "traceback\n" + _remote_exception_repr_chunked + "]\n" | ||
| ) | ||
| % self.test.id() | ||
| ).encode() | ||
| ), | ||
| ), | ||
| ) | ||
| def test_add_expected_failure(self): | ||
| """Test addExpectedFailure on a TestProtocolClient.""" | ||
| self.protocol.addExpectedFailure(self.test, subunit.RemoteError("phwoar crikey")) | ||
| self.assertThat( | ||
| self.io.getvalue(), | ||
| MatchesAny( | ||
| # testtools < 2.5.0 | ||
| Equals((("xfail: %s [\n" + _remote_exception_str + ": phwoar crikey\n]\n") % self.test.id()).encode()), | ||
| # testtools >= 2.5.0 | ||
| Equals((("xfail: %s [\n" + _remote_exception_repr + ": phwoar crikey\n]\n") % self.test.id()).encode()), | ||
| ), | ||
| ) | ||
| def test_add_expected_failure_details(self): | ||
| """Test addExpectedFailure on a TestProtocolClient with details.""" | ||
| self.protocol.addExpectedFailure(self.test, details=self.sample_tb_details) | ||
| self.assertThat( | ||
| self.io.getvalue(), | ||
| MatchesAny( | ||
| # testtools < 2.5.0 | ||
| Equals( | ||
| ( | ||
| ( | ||
| "xfail: %s [ multipart\n" | ||
| "Content-Type: text/plain\n" | ||
| "something\n" | ||
| "F\r\nserialised\nform0\r\n" | ||
| "Content-Type: text/x-traceback;charset=utf8,language=python\n" | ||
| "traceback\n" + _remote_exception_str_chunked + "]\n" | ||
| ) | ||
| % self.test.id() | ||
| ).encode() | ||
| ), | ||
| # testtools >= 2.5.0 | ||
| Equals( | ||
| ( | ||
| ( | ||
| "xfail: %s [ multipart\n" | ||
| "Content-Type: text/plain\n" | ||
| "something\n" | ||
| "F\r\nserialised\nform0\r\n" | ||
| "Content-Type: text/x-traceback;charset=utf8,language=python\n" | ||
| "traceback\n" + _remote_exception_repr_chunked + "]\n" | ||
| ) | ||
| % self.test.id() | ||
| ).encode() | ||
| ), | ||
| ), | ||
| ) | ||
| def test_add_skip(self): | ||
| """Test addSkip on a TestProtocolClient.""" | ||
| self.protocol.addSkip(self.test, "Has it really?") | ||
| self.assertEqual(self.io.getvalue(), ("skip: %s [\nHas it really?\n]\n" % self.test.id()).encode()) | ||
| def test_add_skip_details(self): | ||
| """Test addSkip on a TestProtocolClient with details.""" | ||
| details = {"reason": Content(ContentType("text", "plain"), lambda: [b"Has it really?"])} | ||
| self.protocol.addSkip(self.test, details=details) | ||
| self.assertEqual( | ||
| self.io.getvalue(), | ||
| ( | ||
| "skip: %s [ multipart\nContent-Type: text/plain\nreason\nE\r\nHas it really?0\r\n]\n" % self.test.id() | ||
| ).encode(), | ||
| ) | ||
| def test_progress_set(self): | ||
| self.protocol.progress(23, subunit.PROGRESS_SET) | ||
| self.assertEqual(self.io.getvalue(), b"progress: 23\n") | ||
| def test_progress_neg_cur(self): | ||
| self.protocol.progress(-23, subunit.PROGRESS_CUR) | ||
| self.assertEqual(self.io.getvalue(), b"progress: -23\n") | ||
| def test_progress_pos_cur(self): | ||
| self.protocol.progress(23, subunit.PROGRESS_CUR) | ||
| self.assertEqual(self.io.getvalue(), b"progress: +23\n") | ||
| def test_progress_pop(self): | ||
| self.protocol.progress(1234, subunit.PROGRESS_POP) | ||
| self.assertEqual(self.io.getvalue(), b"progress: pop\n") | ||
| def test_progress_push(self): | ||
| self.protocol.progress(1234, subunit.PROGRESS_PUSH) | ||
| self.assertEqual(self.io.getvalue(), b"progress: push\n") | ||
| def test_time(self): | ||
| # Calling time() outputs a time signal immediately. | ||
| self.protocol.time(datetime.datetime(2009, 10, 11, 12, 13, 14, 15, iso8601.UTC)) | ||
| self.assertEqual(b"time: 2009-10-11 12:13:14.000015Z\n", self.io.getvalue()) | ||
| def test_add_unexpected_success(self): | ||
| """Test addUnexpectedSuccess on a TestProtocolClient.""" | ||
| self.protocol.addUnexpectedSuccess(self.test) | ||
| self.assertEqual(self.io.getvalue(), ("uxsuccess: %s\n" % self.test.id()).encode()) | ||
| def test_add_unexpected_success_details(self): | ||
| """Test addUnexpectedSuccess on a TestProtocolClient with details.""" | ||
| self.protocol.addUnexpectedSuccess(self.test, details=self.sample_details) | ||
| self.assertEqual( | ||
| self.io.getvalue(), | ||
| ( | ||
| "uxsuccess: %s [ multipart\n" | ||
| "Content-Type: text/plain\n" | ||
| "something\n" | ||
| "F\r\nserialised\nform0\r\n]\n" % self.test.id() | ||
| ).encode(), | ||
| ) | ||
| def test_tags_empty(self): | ||
| self.protocol.tags(set(), set()) | ||
| self.assertEqual(b"", self.io.getvalue()) | ||
| def test_tags_add(self): | ||
| self.protocol.tags({"foo"}, set()) | ||
| self.assertEqual(b"tags: foo\n", self.io.getvalue()) | ||
| def test_tags_both(self): | ||
| self.protocol.tags({"quux"}, {"bar"}) | ||
| self.assertThat([b"tags: quux -bar\n", b"tags: -bar quux\n"], Contains(self.io.getvalue())) | ||
| def test_tags_gone(self): | ||
| self.protocol.tags(set(), {"bar"}) | ||
| self.assertEqual(b"tags: -bar\n", self.io.getvalue()) |
| # | ||
| # subunit: extensions to Python unittest to get test results from subprocesses. | ||
| # Copyright (C) 2013 Robert Collins <robertc@robertcollins.net> | ||
| # | ||
| # 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. | ||
| # | ||
| import datetime | ||
| from io import BytesIO | ||
| try: | ||
| from hypothesis import given | ||
| # To debug hypothesis | ||
| # from hypothesis import Settings, Verbosity | ||
| # Settings.default.verbosity = Verbosity.verbose | ||
| import hypothesis.strategies as st | ||
| except ImportError: | ||
| given = None | ||
| st = None | ||
| from testtools import TestCase | ||
| from testtools.matchers import Contains, HasLength | ||
| from testtools.testresult.doubles import StreamResult | ||
| from testtools.tests.test_testresult import TestStreamResultContract | ||
| import subunit | ||
| import iso8601 | ||
| CONSTANT_ENUM = b"\xb3)\x01\x0c\x03foo\x08U_\x1b" | ||
| CONSTANT_INPROGRESS = b"\xb3)\x02\x0c\x03foo\x8e\xc1-\xb5" | ||
| CONSTANT_SUCCESS = b"\xb3)\x03\x0c\x03fooE\x9d\xfe\x10" | ||
| CONSTANT_UXSUCCESS = b"\xb3)\x04\x0c\x03fooX\x98\xce\xa8" | ||
| CONSTANT_SKIP = b"\xb3)\x05\x0c\x03foo\x93\xc4\x1d\r" | ||
| CONSTANT_FAIL = b"\xb3)\x06\x0c\x03foo\x15Po\xa3" | ||
| CONSTANT_XFAIL = b"\xb3)\x07\x0c\x03foo\xde\x0c\xbc\x06" | ||
| CONSTANT_EOF = b"\xb3!\x10\x08S\x15\x88\xdc" | ||
| CONSTANT_FILE_CONTENT = b"\xb3!@\x13\x06barney\x03wooA5\xe3\x8c" | ||
| CONSTANT_MIME = b"\xb3! #\x1aapplication/foo; charset=1x3Q\x15" | ||
| CONSTANT_TIMESTAMP = b"\xb3+\x03\x13<\x17T\xcf\x80\xaf\xc8\x03barI\x96>-" | ||
| CONSTANT_ROUTE_CODE = b"\xb3-\x03\x13\x03bar\x06source\x9cY9\x19" | ||
| CONSTANT_RUNNABLE = b"\xb3(\x03\x0c\x03foo\xe3\xea\xf5\xa4" | ||
| CONSTANT_TAGS = [ | ||
| b"\xb3)\x80\x15\x03bar\x02\x03foo\x03barTHn\xb4", | ||
| b"\xb3)\x80\x15\x03bar\x02\x03bar\x03foo\xf8\xf1\x91o", | ||
| ] | ||
| class TestStreamResultToBytesContract(TestCase, TestStreamResultContract): | ||
| """Check that StreamResult behaves as testtools expects.""" | ||
| def _make_result(self): | ||
| return subunit.StreamResultToBytes(BytesIO()) | ||
| class TestStreamResultToBytes(TestCase): | ||
| def _make_result(self): | ||
| output = BytesIO() | ||
| return subunit.StreamResultToBytes(output), output | ||
| def test_numbers(self): | ||
| result = subunit.StreamResultToBytes(BytesIO()) | ||
| packet = [] | ||
| self.assertRaises(Exception, result._write_number, -1, packet) | ||
| self.assertEqual([], packet) | ||
| result._write_number(0, packet) | ||
| self.assertEqual([b"\x00"], packet) | ||
| del packet[:] | ||
| result._write_number(63, packet) | ||
| self.assertEqual([b"\x3f"], packet) | ||
| del packet[:] | ||
| result._write_number(64, packet) | ||
| self.assertEqual([b"\x40\x40"], packet) | ||
| del packet[:] | ||
| result._write_number(16383, packet) | ||
| self.assertEqual([b"\x7f\xff"], packet) | ||
| del packet[:] | ||
| result._write_number(16384, packet) | ||
| self.assertEqual([b"\x80\x40", b"\x00"], packet) | ||
| del packet[:] | ||
| result._write_number(4194303, packet) | ||
| self.assertEqual([b"\xbf\xff", b"\xff"], packet) | ||
| del packet[:] | ||
| result._write_number(4194304, packet) | ||
| self.assertEqual([b"\xc0\x40\x00\x00"], packet) | ||
| del packet[:] | ||
| result._write_number(1073741823, packet) | ||
| self.assertEqual([b"\xff\xff\xff\xff"], packet) | ||
| del packet[:] | ||
| self.assertRaises(Exception, result._write_number, 1073741824, packet) | ||
| self.assertEqual([], packet) | ||
| def test_volatile_length(self): | ||
| # if the length of the packet data before the length itself is | ||
| # considered is right on the boundary for length's variable length | ||
| # encoding, it is easy to get the length wrong by not accounting for | ||
| # length itself. | ||
| # that is, the encoder has to ensure that length == sum (length_of_rest | ||
| # + length_of_length) | ||
| result, output = self._make_result() | ||
| # 1 byte short: | ||
| result.status(file_name="", file_bytes=b"\xff" * 0) | ||
| self.assertThat(output.getvalue(), HasLength(10)) | ||
| self.assertEqual(b"\x0a", output.getvalue()[3:4]) | ||
| output.seek(0) | ||
| output.truncate() | ||
| # 1 byte long: | ||
| result.status(file_name="", file_bytes=b"\xff" * 53) | ||
| self.assertThat(output.getvalue(), HasLength(63)) | ||
| self.assertEqual(b"\x3f", output.getvalue()[3:4]) | ||
| output.seek(0) | ||
| output.truncate() | ||
| # 2 bytes short | ||
| result.status(file_name="", file_bytes=b"\xff" * 54) | ||
| self.assertThat(output.getvalue(), HasLength(65)) | ||
| self.assertEqual(b"\x40\x41", output.getvalue()[3:5]) | ||
| output.seek(0) | ||
| output.truncate() | ||
| # 2 bytes long | ||
| result.status(file_name="", file_bytes=b"\xff" * 16371) | ||
| self.assertThat(output.getvalue(), HasLength(16383)) | ||
| self.assertEqual(b"\x7f\xff", output.getvalue()[3:5]) | ||
| output.seek(0) | ||
| output.truncate() | ||
| # 3 bytes short | ||
| result.status(file_name="", file_bytes=b"\xff" * 16372) | ||
| self.assertThat(output.getvalue(), HasLength(16385)) | ||
| self.assertEqual(b"\x80\x40\x01", output.getvalue()[3:6]) | ||
| output.seek(0) | ||
| output.truncate() | ||
| # 3 bytes long | ||
| result.status(file_name="", file_bytes=b"\xff" * 4194289) | ||
| self.assertThat(output.getvalue(), HasLength(4194303)) | ||
| self.assertEqual(b"\xbf\xff\xff", output.getvalue()[3:6]) | ||
| output.seek(0) | ||
| output.truncate() | ||
| self.assertRaises(Exception, result.status, file_name="", file_bytes=b"\xff" * 4194290) | ||
| def test_trivial_enumeration(self): | ||
| result, output = self._make_result() | ||
| result.status("foo", "exists") | ||
| self.assertEqual(CONSTANT_ENUM, output.getvalue()) | ||
| def test_inprogress(self): | ||
| result, output = self._make_result() | ||
| result.status("foo", "inprogress") | ||
| self.assertEqual(CONSTANT_INPROGRESS, output.getvalue()) | ||
| def test_success(self): | ||
| result, output = self._make_result() | ||
| result.status("foo", "success") | ||
| self.assertEqual(CONSTANT_SUCCESS, output.getvalue()) | ||
| def test_uxsuccess(self): | ||
| result, output = self._make_result() | ||
| result.status("foo", "uxsuccess") | ||
| self.assertEqual(CONSTANT_UXSUCCESS, output.getvalue()) | ||
| def test_skip(self): | ||
| result, output = self._make_result() | ||
| result.status("foo", "skip") | ||
| self.assertEqual(CONSTANT_SKIP, output.getvalue()) | ||
| def test_fail(self): | ||
| result, output = self._make_result() | ||
| result.status("foo", "fail") | ||
| self.assertEqual(CONSTANT_FAIL, output.getvalue()) | ||
| def test_xfail(self): | ||
| result, output = self._make_result() | ||
| result.status("foo", "xfail") | ||
| self.assertEqual(CONSTANT_XFAIL, output.getvalue()) | ||
| def test_unknown_status(self): | ||
| result, output = self._make_result() | ||
| self.assertRaises(Exception, result.status, "foo", "boo") | ||
| self.assertEqual(b"", output.getvalue()) | ||
| def test_eof(self): | ||
| result, output = self._make_result() | ||
| result.status(eof=True) | ||
| self.assertEqual(CONSTANT_EOF, output.getvalue()) | ||
| def test_file_content(self): | ||
| result, output = self._make_result() | ||
| result.status(file_name="barney", file_bytes=b"woo") | ||
| self.assertEqual(CONSTANT_FILE_CONTENT, output.getvalue()) | ||
| def test_mime(self): | ||
| result, output = self._make_result() | ||
| result.status(mime_type="application/foo; charset=1") | ||
| self.assertEqual(CONSTANT_MIME, output.getvalue()) | ||
| def test_route_code(self): | ||
| result, output = self._make_result() | ||
| result.status(test_id="bar", test_status="success", route_code="source") | ||
| self.assertEqual(CONSTANT_ROUTE_CODE, output.getvalue()) | ||
| def test_runnable(self): | ||
| result, output = self._make_result() | ||
| result.status("foo", "success", runnable=False) | ||
| self.assertEqual(CONSTANT_RUNNABLE, output.getvalue()) | ||
| def test_tags(self): | ||
| result, output = self._make_result() | ||
| result.status(test_id="bar", test_tags={"foo", "bar"}) | ||
| self.assertThat(CONSTANT_TAGS, Contains(output.getvalue())) | ||
| def test_timestamp(self): | ||
| timestamp = datetime.datetime(2001, 12, 12, 12, 59, 59, 45, iso8601.UTC) | ||
| result, output = self._make_result() | ||
| result.status(test_id="bar", test_status="success", timestamp=timestamp) | ||
| self.assertEqual(CONSTANT_TIMESTAMP, output.getvalue()) | ||
| class TestByteStreamToStreamResult(TestCase): | ||
| def test_non_subunit_encapsulated(self): | ||
| source = BytesIO(b"foo\nbar\n") | ||
| result = StreamResult() | ||
| subunit.ByteStreamToStreamResult(source, non_subunit_name="stdout").run(result) | ||
| self.assertEqual( | ||
| [ | ||
| ("status", None, None, None, True, "stdout", b"f", False, None, None, None), | ||
| ("status", None, None, None, True, "stdout", b"o", False, None, None, None), | ||
| ("status", None, None, None, True, "stdout", b"o", False, None, None, None), | ||
| ("status", None, None, None, True, "stdout", b"\n", False, None, None, None), | ||
| ("status", None, None, None, True, "stdout", b"b", False, None, None, None), | ||
| ("status", None, None, None, True, "stdout", b"a", False, None, None, None), | ||
| ("status", None, None, None, True, "stdout", b"r", False, None, None, None), | ||
| ("status", None, None, None, True, "stdout", b"\n", False, None, None, None), | ||
| ], | ||
| result._events, | ||
| ) | ||
| self.assertEqual(b"", source.read()) | ||
| def test_signature_middle_utf8_char(self): | ||
| utf8_bytes = b"\xe3\xb3\x8a" | ||
| source = BytesIO(utf8_bytes) | ||
| # Should be treated as one character (it is u'\u3cca') and wrapped | ||
| result = StreamResult() | ||
| subunit.ByteStreamToStreamResult(source, non_subunit_name="stdout").run(result) | ||
| self.assertEqual( | ||
| [ | ||
| ("status", None, None, None, True, "stdout", b"\xe3", False, None, None, None), | ||
| ("status", None, None, None, True, "stdout", b"\xb3", False, None, None, None), | ||
| ("status", None, None, None, True, "stdout", b"\x8a", False, None, None, None), | ||
| ], | ||
| result._events, | ||
| ) | ||
| def test_non_subunit_disabled_raises(self): | ||
| source = BytesIO(b"foo\nbar\n") | ||
| result = StreamResult() | ||
| case = subunit.ByteStreamToStreamResult(source) | ||
| e = self.assertRaises(Exception, case.run, result) | ||
| self.assertEqual(b"f", e.args[1]) | ||
| self.assertEqual(b"oo\nbar\n", source.read()) | ||
| self.assertEqual([], result._events) | ||
| def test_trivial_enumeration(self): | ||
| source = BytesIO(CONSTANT_ENUM) | ||
| result = StreamResult() | ||
| subunit.ByteStreamToStreamResult(source, non_subunit_name="stdout").run(result) | ||
| self.assertEqual(b"", source.read()) | ||
| self.assertEqual( | ||
| [ | ||
| ("status", "foo", "exists", None, True, None, None, False, None, None, None), | ||
| ], | ||
| result._events, | ||
| ) | ||
| def test_multiple_events(self): | ||
| source = BytesIO(CONSTANT_ENUM + CONSTANT_ENUM) | ||
| result = StreamResult() | ||
| subunit.ByteStreamToStreamResult(source, non_subunit_name="stdout").run(result) | ||
| self.assertEqual(b"", source.read()) | ||
| self.assertEqual( | ||
| [ | ||
| ("status", "foo", "exists", None, True, None, None, False, None, None, None), | ||
| ("status", "foo", "exists", None, True, None, None, False, None, None, None), | ||
| ], | ||
| result._events, | ||
| ) | ||
| def test_inprogress(self): | ||
| self.check_event(CONSTANT_INPROGRESS, "inprogress") | ||
| def test_success(self): | ||
| self.check_event(CONSTANT_SUCCESS, "success") | ||
| def test_uxsuccess(self): | ||
| self.check_event(CONSTANT_UXSUCCESS, "uxsuccess") | ||
| def test_skip(self): | ||
| self.check_event(CONSTANT_SKIP, "skip") | ||
| def test_fail(self): | ||
| self.check_event(CONSTANT_FAIL, "fail") | ||
| def test_xfail(self): | ||
| self.check_event(CONSTANT_XFAIL, "xfail") | ||
| def check_events(self, source_bytes, events): | ||
| source = BytesIO(source_bytes) | ||
| result = StreamResult() | ||
| subunit.ByteStreamToStreamResult(source, non_subunit_name="stdout").run(result) | ||
| self.assertEqual(b"", source.read()) | ||
| self.assertEqual(events, result._events) | ||
| # - any file attachments should be bytes equivalent [as users assume that]. | ||
| for event in result._events: | ||
| if event[5] is not None: | ||
| bytes(event[6]) | ||
| def check_event( | ||
| self, | ||
| source_bytes, | ||
| test_status=None, | ||
| test_id="foo", | ||
| route_code=None, | ||
| timestamp=None, | ||
| tags=None, | ||
| mime_type=None, | ||
| file_name=None, | ||
| file_bytes=None, | ||
| eof=False, | ||
| runnable=True, | ||
| ): | ||
| event = self._event( | ||
| test_id=test_id, | ||
| test_status=test_status, | ||
| tags=tags, | ||
| runnable=runnable, | ||
| file_name=file_name, | ||
| file_bytes=file_bytes, | ||
| eof=eof, | ||
| mime_type=mime_type, | ||
| route_code=route_code, | ||
| timestamp=timestamp, | ||
| ) | ||
| self.check_events(source_bytes, [event]) | ||
| def _event( | ||
| self, | ||
| test_status=None, | ||
| test_id=None, | ||
| route_code=None, | ||
| timestamp=None, | ||
| tags=None, | ||
| mime_type=None, | ||
| file_name=None, | ||
| file_bytes=None, | ||
| eof=False, | ||
| runnable=True, | ||
| ): | ||
| return ( | ||
| "status", | ||
| test_id, | ||
| test_status, | ||
| tags, | ||
| runnable, | ||
| file_name, | ||
| file_bytes, | ||
| eof, | ||
| mime_type, | ||
| route_code, | ||
| timestamp, | ||
| ) | ||
| def test_eof(self): | ||
| self.check_event(CONSTANT_EOF, test_id=None, eof=True) | ||
| def test_file_content(self): | ||
| self.check_event(CONSTANT_FILE_CONTENT, test_id=None, file_name="barney", file_bytes=b"woo") | ||
| def test_file_content_length_into_checksum(self): | ||
| # A bad file content length which creeps into the checksum. | ||
| bad_file_length_content = b"\xb3!@\x13\x06barney\x04woo\xdc\xe2\xdb\x35" | ||
| self.check_events( | ||
| bad_file_length_content, | ||
| [ | ||
| self._event( | ||
| test_id="subunit.parser", | ||
| eof=True, | ||
| file_name="Packet data", | ||
| file_bytes=bad_file_length_content, | ||
| mime_type="application/octet-stream", | ||
| ), | ||
| self._event( | ||
| test_id="subunit.parser", | ||
| test_status="fail", | ||
| eof=True, | ||
| file_name="Parser Error", | ||
| file_bytes=b"File content extends past end of packet: claimed 4 bytes, 3 available", | ||
| mime_type="text/plain;charset=utf8", | ||
| ), | ||
| ], | ||
| ) | ||
| def test_packet_length_4_word_varint(self): | ||
| packet_data = b"\xb3!@\xc0\x00\x11" | ||
| self.check_events( | ||
| packet_data, | ||
| [ | ||
| self._event( | ||
| test_id="subunit.parser", | ||
| eof=True, | ||
| file_name="Packet data", | ||
| file_bytes=packet_data, | ||
| mime_type="application/octet-stream", | ||
| ), | ||
| self._event( | ||
| test_id="subunit.parser", | ||
| test_status="fail", | ||
| eof=True, | ||
| file_name="Parser Error", | ||
| file_bytes=b"3 byte maximum given but 4 byte value found.", | ||
| mime_type="text/plain;charset=utf8", | ||
| ), | ||
| ], | ||
| ) | ||
| def test_mime(self): | ||
| self.check_event(CONSTANT_MIME, test_id=None, mime_type="application/foo; charset=1") | ||
| def test_route_code(self): | ||
| self.check_event(CONSTANT_ROUTE_CODE, "success", route_code="source", test_id="bar") | ||
| def test_runnable(self): | ||
| self.check_event(CONSTANT_RUNNABLE, test_status="success", runnable=False) | ||
| def test_tags(self): | ||
| self.check_event(CONSTANT_TAGS[0], None, tags={"foo", "bar"}, test_id="bar") | ||
| def test_timestamp(self): | ||
| timestamp = datetime.datetime(2001, 12, 12, 12, 59, 59, 45, iso8601.UTC) | ||
| self.check_event(CONSTANT_TIMESTAMP, "success", test_id="bar", timestamp=timestamp) | ||
| def test_bad_crc_errors_via_status(self): | ||
| file_bytes = CONSTANT_MIME[:-1] + b"\x00" | ||
| self.check_events( | ||
| file_bytes, | ||
| [ | ||
| self._event( | ||
| test_id="subunit.parser", | ||
| eof=True, | ||
| file_name="Packet data", | ||
| file_bytes=file_bytes, | ||
| mime_type="application/octet-stream", | ||
| ), | ||
| self._event( | ||
| test_id="subunit.parser", | ||
| test_status="fail", | ||
| eof=True, | ||
| file_name="Parser Error", | ||
| file_bytes=b"Bad checksum - calculated (0x78335115), stored (0x78335100)", | ||
| mime_type="text/plain;charset=utf8", | ||
| ), | ||
| ], | ||
| ) | ||
| def test_not_utf8_in_string(self): | ||
| file_bytes = CONSTANT_ROUTE_CODE[:5] + b"\xb4" + CONSTANT_ROUTE_CODE[6:-4] + b"\xce\x56\xc6\x17" | ||
| self.check_events( | ||
| file_bytes, | ||
| [ | ||
| self._event( | ||
| test_id="subunit.parser", | ||
| eof=True, | ||
| file_name="Packet data", | ||
| file_bytes=file_bytes, | ||
| mime_type="application/octet-stream", | ||
| ), | ||
| self._event( | ||
| test_id="subunit.parser", | ||
| test_status="fail", | ||
| eof=True, | ||
| file_name="Parser Error", | ||
| file_bytes=b"UTF8 string at offset 2 is not UTF8", | ||
| mime_type="text/plain;charset=utf8", | ||
| ), | ||
| ], | ||
| ) | ||
| def test_NULL_in_string(self): | ||
| file_bytes = CONSTANT_ROUTE_CODE[:6] + b"\x00" + CONSTANT_ROUTE_CODE[7:-4] + b"\xd7\x41\xac\xfe" | ||
| self.check_events( | ||
| file_bytes, | ||
| [ | ||
| self._event( | ||
| test_id="subunit.parser", | ||
| eof=True, | ||
| file_name="Packet data", | ||
| file_bytes=file_bytes, | ||
| mime_type="application/octet-stream", | ||
| ), | ||
| self._event( | ||
| test_id="subunit.parser", | ||
| test_status="fail", | ||
| eof=True, | ||
| file_name="Parser Error", | ||
| file_bytes=b"UTF8 string at offset 2 contains NUL byte", | ||
| mime_type="text/plain;charset=utf8", | ||
| ), | ||
| ], | ||
| ) | ||
| def test_bad_utf8_stringlength(self): | ||
| file_bytes = CONSTANT_ROUTE_CODE[:4] + b"\x3f" + CONSTANT_ROUTE_CODE[5:-4] + b"\xbe\x29\xe0\xc2" | ||
| self.check_events( | ||
| file_bytes, | ||
| [ | ||
| self._event( | ||
| test_id="subunit.parser", | ||
| eof=True, | ||
| file_name="Packet data", | ||
| file_bytes=file_bytes, | ||
| mime_type="application/octet-stream", | ||
| ), | ||
| self._event( | ||
| test_id="subunit.parser", | ||
| test_status="fail", | ||
| eof=True, | ||
| file_name="Parser Error", | ||
| file_bytes=b"UTF8 string at offset 2 extends past end of packet: claimed 63 bytes, 10 available", | ||
| mime_type="text/plain;charset=utf8", | ||
| ), | ||
| ], | ||
| ) | ||
| def test_route_code_and_file_content(self): | ||
| content = BytesIO() | ||
| subunit.StreamResultToBytes(content).status( | ||
| route_code="0", mime_type="text/plain", file_name="bar", file_bytes=b"foo" | ||
| ) | ||
| self.check_event( | ||
| content.getvalue(), test_id=None, file_name="bar", route_code="0", mime_type="text/plain", file_bytes=b"foo" | ||
| ) | ||
| if st is not None: | ||
| @given(st.binary()) | ||
| def test_hypothesis_decoding(self, code_bytes): | ||
| source = BytesIO(code_bytes) | ||
| result = StreamResult() | ||
| stream = subunit.ByteStreamToStreamResult(source, non_subunit_name="stdout") | ||
| stream.run(result) | ||
| self.assertEqual(b"", source.read()) |
| # | ||
| # subunit: extensions to Python unittest to get test results from subprocesses. | ||
| # Copyright (C) 2009 Robert Collins <robertc@robertcollins.net> | ||
| # | ||
| # 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. | ||
| # | ||
| import csv | ||
| import datetime | ||
| import sys | ||
| import unittest | ||
| from io import StringIO | ||
| import testtools | ||
| from testtools import TestCase | ||
| from testtools.content import TracebackContent, text_content | ||
| from testtools.testresult.doubles import ExtendedTestResult | ||
| import subunit | ||
| import iso8601 | ||
| import subunit.test_results | ||
| class LoggingDecorator(subunit.test_results.HookedTestResultDecorator): | ||
| def __init__(self, decorated): | ||
| self._calls = 0 | ||
| super().__init__(decorated) | ||
| def _before_event(self): | ||
| self._calls += 1 | ||
| class AssertBeforeTestResult(LoggingDecorator): | ||
| """A TestResult for checking preconditions.""" | ||
| def __init__(self, decorated, test): | ||
| self.test = test | ||
| super().__init__(decorated) | ||
| def _before_event(self): | ||
| self.test.assertEqual(1, self.earlier._calls) | ||
| super()._before_event() | ||
| class TimeCapturingResult(unittest.TestResult): | ||
| def __init__(self): | ||
| super().__init__() | ||
| self._calls = [] | ||
| self.failfast = False | ||
| def time(self, a_datetime): | ||
| self._calls.append(a_datetime) | ||
| def progress(self, offset, whence): | ||
| pass | ||
| def addDuration(self, test, duration): | ||
| """Called to add a test duration. | ||
| :param test: The test that completed. | ||
| :param duration: The duration of the test as a float in seconds. | ||
| """ | ||
| pass | ||
| class TestHookedTestResultDecorator(unittest.TestCase): | ||
| def setUp(self): | ||
| # An end to the chain | ||
| terminal = unittest.TestResult() | ||
| # Asserts that the call was made to self.result before asserter was | ||
| # called. | ||
| asserter = AssertBeforeTestResult(terminal, self) | ||
| # The result object we call, which much increase its call count. | ||
| self.result = LoggingDecorator(asserter) | ||
| asserter.earlier = self.result | ||
| self.decorated = asserter | ||
| def tearDown(self): | ||
| # The hook in self.result must have been called | ||
| self.assertEqual(1, self.result._calls) | ||
| # The hook in asserter must have been called too, otherwise the | ||
| # assertion about ordering won't have completed. | ||
| self.assertEqual(1, self.decorated._calls) | ||
| def test_startTest(self): | ||
| self.result.startTest(self) | ||
| def test_startTestRun(self): | ||
| self.result.startTestRun() | ||
| def test_stopTest(self): | ||
| self.result.stopTest(self) | ||
| def test_stopTestRun(self): | ||
| self.result.stopTestRun() | ||
| def test_addError(self): | ||
| self.result.addError(self, subunit.RemoteError()) | ||
| def test_addError_details(self): | ||
| self.result.addError(self, details={}) | ||
| def test_addFailure(self): | ||
| self.result.addFailure(self, subunit.RemoteError()) | ||
| def test_addFailure_details(self): | ||
| self.result.addFailure(self, details={}) | ||
| def test_addSuccess(self): | ||
| self.result.addSuccess(self) | ||
| def test_addSuccess_details(self): | ||
| self.result.addSuccess(self, details={}) | ||
| def test_addSkip(self): | ||
| self.result.addSkip(self, "foo") | ||
| def test_addSkip_details(self): | ||
| self.result.addSkip(self, details={}) | ||
| def test_addExpectedFailure(self): | ||
| self.result.addExpectedFailure(self, subunit.RemoteError()) | ||
| def test_addExpectedFailure_details(self): | ||
| self.result.addExpectedFailure(self, details={}) | ||
| def test_addUnexpectedSuccess(self): | ||
| self.result.addUnexpectedSuccess(self) | ||
| def test_addUnexpectedSuccess_details(self): | ||
| self.result.addUnexpectedSuccess(self, details={}) | ||
| def test_progress(self): | ||
| self.result.progress(1, subunit.PROGRESS_SET) | ||
| def test_wasSuccessful(self): | ||
| self.result.wasSuccessful() | ||
| def test_shouldStop(self): | ||
| self.result.shouldStop | ||
| def test_stop(self): | ||
| self.result.stop() | ||
| def test_time(self): | ||
| self.result.time(None) | ||
| def test_addDuration(self): | ||
| self.result.addDuration(self, 1.5) | ||
| class TestAutoTimingTestResultDecorator(unittest.TestCase): | ||
| def setUp(self): | ||
| # And end to the chain which captures time events. | ||
| terminal = TimeCapturingResult() | ||
| # The result object under test. | ||
| self.result = subunit.test_results.AutoTimingTestResultDecorator(terminal) | ||
| self.decorated = terminal | ||
| def test_without_time_calls_time_is_called_and_not_None(self): | ||
| self.result.startTest(self) | ||
| self.assertEqual(1, len(self.decorated._calls)) | ||
| self.assertNotEqual(None, self.decorated._calls[0]) | ||
| def test_no_time_from_progress(self): | ||
| self.result.progress(1, subunit.PROGRESS_CUR) | ||
| self.assertEqual(0, len(self.decorated._calls)) | ||
| def test_no_time_from_shouldStop(self): | ||
| self.decorated.stop() | ||
| self.result.shouldStop | ||
| self.assertEqual(0, len(self.decorated._calls)) | ||
| def test_calling_time_inhibits_automatic_time(self): | ||
| # Calling time() outputs a time signal immediately and prevents | ||
| # automatically adding one when other methods are called. | ||
| time = datetime.datetime(2009, 10, 11, 12, 13, 14, 15, iso8601.UTC) | ||
| self.result.time(time) | ||
| self.result.startTest(self) | ||
| self.result.stopTest(self) | ||
| self.assertEqual(1, len(self.decorated._calls)) | ||
| self.assertEqual(time, self.decorated._calls[0]) | ||
| def test_calling_time_None_enables_automatic_time(self): | ||
| time = datetime.datetime(2009, 10, 11, 12, 13, 14, 15, iso8601.UTC) | ||
| self.result.time(time) | ||
| self.assertEqual(1, len(self.decorated._calls)) | ||
| self.assertEqual(time, self.decorated._calls[0]) | ||
| # Calling None passes the None through, in case other results care. | ||
| self.result.time(None) | ||
| self.assertEqual(2, len(self.decorated._calls)) | ||
| self.assertEqual(None, self.decorated._calls[1]) | ||
| # Calling other methods doesn't generate an automatic time event. | ||
| self.result.startTest(self) | ||
| self.assertEqual(3, len(self.decorated._calls)) | ||
| self.assertNotEqual(None, self.decorated._calls[2]) | ||
| def test_set_failfast_True(self): | ||
| self.assertFalse(self.decorated.failfast) | ||
| self.result.failfast = True | ||
| self.assertTrue(self.decorated.failfast) | ||
| class TestTagCollapsingDecorator(TestCase): | ||
| def test_tags_collapsed_outside_of_tests(self): | ||
| result = ExtendedTestResult() | ||
| tag_collapser = subunit.test_results.TagCollapsingDecorator(result) | ||
| tag_collapser.tags({"a"}, set()) | ||
| tag_collapser.tags({"b"}, set()) | ||
| tag_collapser.startTest(self) | ||
| self.assertEqual( | ||
| [ | ||
| ("tags", {"a", "b"}, set()), | ||
| ("startTest", self), | ||
| ], | ||
| result._events, | ||
| ) | ||
| def test_tags_collapsed_outside_of_tests_are_flushed(self): | ||
| result = ExtendedTestResult() | ||
| tag_collapser = subunit.test_results.TagCollapsingDecorator(result) | ||
| tag_collapser.startTestRun() | ||
| tag_collapser.tags({"a"}, set()) | ||
| tag_collapser.tags({"b"}, set()) | ||
| tag_collapser.startTest(self) | ||
| tag_collapser.addSuccess(self) | ||
| tag_collapser.stopTest(self) | ||
| tag_collapser.stopTestRun() | ||
| self.assertEqual( | ||
| [ | ||
| ("startTestRun",), | ||
| ("tags", {"a", "b"}, set()), | ||
| ("startTest", self), | ||
| ("addSuccess", self), | ||
| ("stopTest", self), | ||
| ("stopTestRun",), | ||
| ], | ||
| result._events, | ||
| ) | ||
| def test_tags_forwarded_after_tests(self): | ||
| test = subunit.RemotedTestCase("foo") | ||
| result = ExtendedTestResult() | ||
| tag_collapser = subunit.test_results.TagCollapsingDecorator(result) | ||
| tag_collapser.startTestRun() | ||
| tag_collapser.startTest(test) | ||
| tag_collapser.addSuccess(test) | ||
| tag_collapser.stopTest(test) | ||
| tag_collapser.tags({"a"}, {"b"}) | ||
| tag_collapser.stopTestRun() | ||
| self.assertEqual( | ||
| [ | ||
| ("startTestRun",), | ||
| ("startTest", test), | ||
| ("addSuccess", test), | ||
| ("stopTest", test), | ||
| ("tags", {"a"}, {"b"}), | ||
| ("stopTestRun",), | ||
| ], | ||
| result._events, | ||
| ) | ||
| def test_tags_collapsed_inside_of_tests(self): | ||
| result = ExtendedTestResult() | ||
| tag_collapser = subunit.test_results.TagCollapsingDecorator(result) | ||
| test = subunit.RemotedTestCase("foo") | ||
| tag_collapser.startTest(test) | ||
| tag_collapser.tags({"a"}, set()) | ||
| tag_collapser.tags({"b"}, {"a"}) | ||
| tag_collapser.tags({"c"}, set()) | ||
| tag_collapser.stopTest(test) | ||
| self.assertEqual([("startTest", test), ("tags", {"b", "c"}, {"a"}), ("stopTest", test)], result._events) | ||
| def test_tags_collapsed_inside_of_tests_different_ordering(self): | ||
| result = ExtendedTestResult() | ||
| tag_collapser = subunit.test_results.TagCollapsingDecorator(result) | ||
| test = subunit.RemotedTestCase("foo") | ||
| tag_collapser.startTest(test) | ||
| tag_collapser.tags(set(), {"a"}) | ||
| tag_collapser.tags({"a", "b"}, set()) | ||
| tag_collapser.tags({"c"}, set()) | ||
| tag_collapser.stopTest(test) | ||
| self.assertEqual([("startTest", test), ("tags", {"a", "b", "c"}, set()), ("stopTest", test)], result._events) | ||
| def test_tags_sent_before_result(self): | ||
| # Because addSuccess and friends tend to send subunit output | ||
| # immediately, and because 'tags:' before a result line means | ||
| # something different to 'tags:' after a result line, we need to be | ||
| # sure that tags are emitted before 'addSuccess' (or whatever). | ||
| result = ExtendedTestResult() | ||
| tag_collapser = subunit.test_results.TagCollapsingDecorator(result) | ||
| test = subunit.RemotedTestCase("foo") | ||
| tag_collapser.startTest(test) | ||
| tag_collapser.tags({"a"}, set()) | ||
| tag_collapser.addSuccess(test) | ||
| tag_collapser.stopTest(test) | ||
| self.assertEqual( | ||
| [("startTest", test), ("tags", {"a"}, set()), ("addSuccess", test), ("stopTest", test)], result._events | ||
| ) | ||
| class TestTimeCollapsingDecorator(TestCase): | ||
| def make_time(self): | ||
| # Heh heh. | ||
| return datetime.datetime(2000, 1, self.getUniqueInteger(), tzinfo=iso8601.UTC) | ||
| def test_initial_time_forwarded(self): | ||
| # We always forward the first time event we see. | ||
| result = ExtendedTestResult() | ||
| tag_collapser = subunit.test_results.TimeCollapsingDecorator(result) | ||
| a_time = self.make_time() | ||
| tag_collapser.time(a_time) | ||
| self.assertEqual([("time", a_time)], result._events) | ||
| def test_time_collapsed_to_first_and_last(self): | ||
| # If there are many consecutive time events, only the first and last | ||
| # are sent through. | ||
| result = ExtendedTestResult() | ||
| tag_collapser = subunit.test_results.TimeCollapsingDecorator(result) | ||
| times = [self.make_time() for i in range(5)] | ||
| for a_time in times: | ||
| tag_collapser.time(a_time) | ||
| tag_collapser.startTest(subunit.RemotedTestCase("foo")) | ||
| self.assertEqual([("time", times[0]), ("time", times[-1])], result._events[:-1]) | ||
| def test_only_one_time_sent(self): | ||
| # If we receive a single time event followed by a non-time event, we | ||
| # send exactly one time event. | ||
| result = ExtendedTestResult() | ||
| tag_collapser = subunit.test_results.TimeCollapsingDecorator(result) | ||
| a_time = self.make_time() | ||
| tag_collapser.time(a_time) | ||
| tag_collapser.startTest(subunit.RemotedTestCase("foo")) | ||
| self.assertEqual([("time", a_time)], result._events[:-1]) | ||
| def test_duplicate_times_not_sent(self): | ||
| # Many time events with the exact same time are collapsed into one | ||
| # time event. | ||
| result = ExtendedTestResult() | ||
| tag_collapser = subunit.test_results.TimeCollapsingDecorator(result) | ||
| a_time = self.make_time() | ||
| for i in range(5): | ||
| tag_collapser.time(a_time) | ||
| tag_collapser.startTest(subunit.RemotedTestCase("foo")) | ||
| self.assertEqual([("time", a_time)], result._events[:-1]) | ||
| def test_no_times_inserted(self): | ||
| result = ExtendedTestResult() | ||
| tag_collapser = subunit.test_results.TimeCollapsingDecorator(result) | ||
| a_time = self.make_time() | ||
| tag_collapser.time(a_time) | ||
| foo = subunit.RemotedTestCase("foo") | ||
| tag_collapser.startTest(foo) | ||
| tag_collapser.addSuccess(foo) | ||
| tag_collapser.stopTest(foo) | ||
| self.assertEqual([("time", a_time), ("startTest", foo), ("addSuccess", foo), ("stopTest", foo)], result._events) | ||
| class TestByTestResultTests(testtools.TestCase): | ||
| def setUp(self): | ||
| super().setUp() | ||
| self.log = [] | ||
| self.result = subunit.test_results.TestByTestResult(self.on_test) | ||
| self.result._now = iter(range(5)).__next__ | ||
| def assertCalled(self, **kwargs): | ||
| defaults = { | ||
| "test": self, | ||
| "tags": set(), | ||
| "details": None, | ||
| "start_time": 0, | ||
| "stop_time": 1, | ||
| } | ||
| defaults.update(kwargs) | ||
| self.assertEqual([defaults], self.log) | ||
| def on_test(self, **kwargs): | ||
| self.log.append(kwargs) | ||
| def test_no_tests_nothing_reported(self): | ||
| self.result.startTestRun() | ||
| self.result.stopTestRun() | ||
| self.assertEqual([], self.log) | ||
| def test_add_success(self): | ||
| self.result.startTest(self) | ||
| self.result.addSuccess(self) | ||
| self.result.stopTest(self) | ||
| self.assertCalled(status="success") | ||
| def test_add_success_details(self): | ||
| self.result.startTest(self) | ||
| details = {"foo": "bar"} | ||
| self.result.addSuccess(self, details=details) | ||
| self.result.stopTest(self) | ||
| self.assertCalled(status="success", details=details) | ||
| def test_tags(self): | ||
| if not getattr(self.result, "tags", None): | ||
| self.skipTest("No tags in testtools") | ||
| self.result.tags(["foo"], []) | ||
| self.result.startTest(self) | ||
| self.result.addSuccess(self) | ||
| self.result.stopTest(self) | ||
| self.assertCalled(status="success", tags={"foo"}) | ||
| def test_add_error(self): | ||
| self.result.startTest(self) | ||
| try: | ||
| 1 / 0 | ||
| except ZeroDivisionError: | ||
| error = sys.exc_info() | ||
| self.result.addError(self, error) | ||
| self.result.stopTest(self) | ||
| self.assertCalled(status="error", details={"traceback": TracebackContent(error, self)}) | ||
| def test_add_error_details(self): | ||
| self.result.startTest(self) | ||
| details = {"foo": text_content("bar")} | ||
| self.result.addError(self, details=details) | ||
| self.result.stopTest(self) | ||
| self.assertCalled(status="error", details=details) | ||
| def test_add_failure(self): | ||
| self.result.startTest(self) | ||
| try: | ||
| self.fail("intentional failure") | ||
| except self.failureException: | ||
| failure = sys.exc_info() | ||
| self.result.addFailure(self, failure) | ||
| self.result.stopTest(self) | ||
| self.assertCalled(status="failure", details={"traceback": TracebackContent(failure, self)}) | ||
| def test_add_failure_details(self): | ||
| self.result.startTest(self) | ||
| details = {"foo": text_content("bar")} | ||
| self.result.addFailure(self, details=details) | ||
| self.result.stopTest(self) | ||
| self.assertCalled(status="failure", details=details) | ||
| def test_add_xfail(self): | ||
| self.result.startTest(self) | ||
| try: | ||
| 1 / 0 | ||
| except ZeroDivisionError: | ||
| error = sys.exc_info() | ||
| self.result.addExpectedFailure(self, error) | ||
| self.result.stopTest(self) | ||
| self.assertCalled(status="xfail", details={"traceback": TracebackContent(error, self)}) | ||
| def test_add_xfail_details(self): | ||
| self.result.startTest(self) | ||
| details = {"foo": text_content("bar")} | ||
| self.result.addExpectedFailure(self, details=details) | ||
| self.result.stopTest(self) | ||
| self.assertCalled(status="xfail", details=details) | ||
| def test_add_unexpected_success(self): | ||
| self.result.startTest(self) | ||
| details = {"foo": "bar"} | ||
| self.result.addUnexpectedSuccess(self, details=details) | ||
| self.result.stopTest(self) | ||
| self.assertCalled(status="success", details=details) | ||
| def test_add_skip_reason(self): | ||
| self.result.startTest(self) | ||
| reason = self.getUniqueString() | ||
| self.result.addSkip(self, reason) | ||
| self.result.stopTest(self) | ||
| self.assertCalled(status="skip", details={"reason": text_content(reason)}) | ||
| def test_add_skip_details(self): | ||
| self.result.startTest(self) | ||
| details = {"foo": "bar"} | ||
| self.result.addSkip(self, details=details) | ||
| self.result.stopTest(self) | ||
| self.assertCalled(status="skip", details=details) | ||
| def test_twice(self): | ||
| self.result.startTest(self) | ||
| self.result.addSuccess(self, details={"foo": "bar"}) | ||
| self.result.stopTest(self) | ||
| self.result.startTest(self) | ||
| self.result.addSuccess(self) | ||
| self.result.stopTest(self) | ||
| self.assertEqual( | ||
| [ | ||
| { | ||
| "test": self, | ||
| "status": "success", | ||
| "start_time": 0, | ||
| "stop_time": 1, | ||
| "tags": set(), | ||
| "details": {"foo": "bar"}, | ||
| }, | ||
| {"test": self, "status": "success", "start_time": 2, "stop_time": 3, "tags": set(), "details": None}, | ||
| ], | ||
| self.log, | ||
| ) | ||
| class TestCsvResult(testtools.TestCase): | ||
| def parse_stream(self, stream): | ||
| stream.seek(0) | ||
| reader = csv.reader(stream) | ||
| return list(reader) | ||
| def test_csv_output(self): | ||
| stream = StringIO() | ||
| result = subunit.test_results.CsvResult(stream) | ||
| result._now = iter(range(5)).__next__ | ||
| result.startTestRun() | ||
| result.startTest(self) | ||
| result.addSuccess(self) | ||
| result.stopTest(self) | ||
| result.stopTestRun() | ||
| self.assertEqual( | ||
| [ | ||
| ["test", "status", "start_time", "stop_time"], | ||
| [self.id(), "success", "0", "1"], | ||
| ], | ||
| self.parse_stream(stream), | ||
| ) | ||
| def test_just_header_when_no_tests(self): | ||
| stream = StringIO() | ||
| result = subunit.test_results.CsvResult(stream) | ||
| result.startTestRun() | ||
| result.stopTestRun() | ||
| self.assertEqual([["test", "status", "start_time", "stop_time"]], self.parse_stream(stream)) | ||
| def test_no_output_before_events(self): | ||
| stream = StringIO() | ||
| subunit.test_results.CsvResult(stream) | ||
| self.assertEqual([], self.parse_stream(stream)) | ||
| class TestTestIdPrintingResult(testtools.TestCase): | ||
| def setUp(self): | ||
| super().setUp() | ||
| self.stream = StringIO() | ||
| self.result = subunit.test_results.TestIdPrintingResult(self.stream) | ||
| def test_addDuration(self): | ||
| # addDuration should not raise an exception | ||
| test = subunit.RemotedTestCase("foo") | ||
| self.result.addDuration(test, 2.5) | ||
| # TestIdPrintingResult doesn't output anything for addDuration | ||
| self.assertEqual("", self.stream.getvalue()) |
Sorry, the diff of this file is not supported yet
| # Subunit Vim Syntax Highlighting | ||
| This directory contains Vim syntax highlighting for Subunit v1 test protocol streams. | ||
| ## Installation | ||
| ### Option 1: Manual Installation | ||
| Copy the files to your Vim configuration directory: | ||
| ```bash | ||
| cp -r syntax ~/.vim/ | ||
| cp -r ftdetect ~/.vim/ | ||
| ``` | ||
| ### Option 2: Using a Plugin Manager | ||
| If you use a Vim plugin manager like vim-plug, add to your `.vimrc`: | ||
| ```vim | ||
| Plug 'path/to/subunit/vim' | ||
| ``` | ||
| ### Option 3: Pathogen | ||
| If you use Pathogen: | ||
| ```bash | ||
| cd ~/.vim/bundle | ||
| ln -s /path/to/subunit/vim subunit | ||
| ``` |
Sorry, the diff of this file is not supported yet
Alert delta unavailable
Currently unable to show alert delta for PyPI packages.
503693
10.75%9473
19.31%62
-7.46%