diff options
author | Robert Collins <robertc@robertcollins.net> | 2015-07-13 23:02:48 +1200 |
---|---|---|
committer | Robert Collins <robertc@robertcollins.net> | 2015-07-13 23:02:48 +1200 |
commit | c90fc5d0a74f7bc8a7e0d9c846c2d0f46b97f8d2 (patch) | |
tree | c287802daae58d571c1b5a61123b4bf682bb7fad | |
parent | 2bd3a0ef34b3fee9b6608799be31844cc0510164 (diff) | |
download | subunit-c90fc5d0a74f7bc8a7e0d9c846c2d0f46b97f8d2.tar.gz |
Add subunit2disk which exports a stream to the fs.
-rw-r--r-- | Makefile.am | 5 | ||||
-rw-r--r-- | NEWS | 9 | ||||
-rw-r--r-- | README.rst | 1 | ||||
-rwxr-xr-x | filters/subunit2disk | 23 | ||||
-rw-r--r-- | python/subunit/_to_disk.py | 131 | ||||
-rw-r--r-- | python/subunit/tests/__init__.py | 2 | ||||
-rw-r--r-- | python/subunit/tests/test_filter_to_disk.py | 49 | ||||
-rwxr-xr-x | setup.py | 2 |
8 files changed, 220 insertions, 2 deletions
diff --git a/Makefile.am b/Makefile.am index 82cd61e..b1c0473 100644 --- a/Makefile.am +++ b/Makefile.am @@ -27,6 +27,7 @@ EXTRA_DIST = \ python/subunit/tests/test_chunked.py \ python/subunit/tests/test_details.py \ python/subunit/tests/test_filters.py \ + python/subunit/tests/test_filter_to_disk.py \ python/subunit/tests/test_output_filter.py \ python/subunit/tests/test_progress_model.py \ python/subunit/tests/test_run.py \ @@ -58,6 +59,7 @@ dist_bin_SCRIPTS = \ filters/subunit-stats \ filters/subunit-tags \ filters/subunit2csv \ + filters/subunit2disk \ filters/subunit2gtk \ filters/subunit2junitxml \ filters/subunit2pyunit \ @@ -81,7 +83,8 @@ pkgpython_PYTHON = \ python/subunit/run.py \ python/subunit/v2.py \ python/subunit/test_results.py \ - python/subunit/_output.py + python/subunit/_output.py \ + python/subunit/_to_disk.py lib_LTLIBRARIES = libsubunit.la lib_LTLIBRARIES += libcppunit_subunit.la @@ -5,6 +5,15 @@ subunit release notes NEXT (In development) --------------------- +IMPROVEMENTS +~~~~~~~~~~~~ + +* Added subunit2disk, which explodes a stream out to files on disk. + (Robert Collins) + +1.1.0 +----- + BUGFIXES ~~~~~~~~ @@ -62,6 +62,7 @@ A number of useful things can be done easily with subunit: Subunit supplies the following filters: * tap2subunit - convert perl's TestAnythingProtocol to subunit. * subunit2csv - convert a subunit stream to csv. + * subunit2disk - export a subunit stream to files on disk. * subunit2pyunit - convert a subunit stream to pyunit test results. * subunit2gtk - show a subunit stream in GTK. * subunit2junitxml - convert a subunit stream to JUnit's XML format. diff --git a/filters/subunit2disk b/filters/subunit2disk new file mode 100755 index 0000000..c1d2e1a --- /dev/null +++ b/filters/subunit2disk @@ -0,0 +1,23 @@ +#!/usr/bin/env python +# 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. + + +"""Export a stream to files and directories on disk.""" + +from subunit._to_disk import to_disk + + +if __name__ == '__main__': + exit(to_disk()) diff --git a/python/subunit/_to_disk.py b/python/subunit/_to_disk.py new file mode 100644 index 0000000..45f4fb5 --- /dev/null +++ b/python/subunit/_to_disk.py @@ -0,0 +1,131 @@ +# 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. + +from errno import EEXIST +import io +import json +import optparse +import os.path +import sys +from textwrap import dedent + +from testtools import StreamToDict + +from subunit.filters import run_tests_from_stream + + +def _allocate_path(root, sub): + """Figoure a path for sub under root. + + If sub tries to escape root, squash it with prejuidice. + + If the path already exists, a numeric suffix is appended. + E.g. foo, foo-1, foo-2, etc. + + :return: the full path to sub. + """ + # subpathss are allowed, but not parents. + candidate = os.path.realpath(os.path.join(root, sub)) + realroot = os.path.realpath(root) + if not candidate.startswith(realroot): + sub = sub.replace('/', '_').replace('\\', '_') + return _allocate_path(root, sub) + + attempt = 0 + probe = candidate + while os.path.exists(probe): + attempt += 1 + probe = '%s-%s' % (candidate, attempt) + return probe + + +def _open_path(root, subpath): + name = _allocate_path(root, subpath) + try: + os.makedirs(os.path.dirname(name)) + except (OSError, IOError) as e: + if e.errno != EEXIST: + raise + return io.open(name, 'wb') + + +def _json_time(a_time): + if a_time is None: + return a_time + return str(a_time) + + +class DiskExporter: + """Exports tests to disk.""" + + def __init__(self, directory): + self._directory = os.path.realpath(directory) + + def export(self, test_dict): + id = test_dict['id'] + tags = sorted(test_dict['tags']) + details = test_dict['details'] + status = test_dict['status'] + start, stop = test_dict['timestamps'] + test_summary = {} + test_summary['id'] = id + test_summary['tags'] = tags + test_summary['status'] = status + test_summary['details'] = sorted(details.keys()) + test_summary['start'] = _json_time(start) + test_summary['stop'] = _json_time(stop) + root = _allocate_path(self._directory, id) + with _open_path(root, 'test.json') as f: + maybe_str = json.dumps( + test_summary, sort_keys=True, ensure_ascii=False) + if not isinstance(maybe_str, bytes): + maybe_str = maybe_str.encode('utf-8') + f.write(maybe_str) + for name, detail in details.items(): + with _open_path(root, name) as f: + for chunk in detail.iter_bytes(): + f.write(chunk) + + +def to_disk(argv=None, stdin=None, stdout=None): + if stdout is None: + stdout = sys.stdout + if stdin is None: + stdin = sys.stdin + parser = optparse.OptionParser( + description="Export a subunit stream to files on disk.", + epilog=dedent("""\ + Creates a directory per test id, a JSON file with test + metadata within that directory, and each attachment + is written to their name relative to that directory. + + Global packages (no test id) are discarded. + + Exits 0 if the export was completed, or non-zero otherwise. + """)) + parser.add_option( + "-d", "--directory", help="Root directory to export to.", + default=".") + options, args = parser.parse_args(argv) + if len(args) > 1: + raise Exception("Unexpected arguments.") + if len(args): + source = io.open(args[0], 'rb') + else: + source = stdin + exporter = DiskExporter(options.directory) + result = StreamToDict(exporter.export) + run_tests_from_stream(source, result, protocol_version=2) + return 0 + diff --git a/python/subunit/tests/__init__.py b/python/subunit/tests/__init__.py index a62a006..c6599f7 100644 --- a/python/subunit/tests/__init__.py +++ b/python/subunit/tests/__init__.py @@ -31,6 +31,7 @@ from subunit.tests import ( test_chunked, test_details, test_filters, + test_filter_to_disk, test_output_filter, test_progress_model, test_run, @@ -54,6 +55,7 @@ def test_suite(): 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)) diff --git a/python/subunit/tests/test_filter_to_disk.py b/python/subunit/tests/test_filter_to_disk.py new file mode 100644 index 0000000..c2b0996 --- /dev/null +++ b/python/subunit/tests/test_filter_to_disk.py @@ -0,0 +1,49 @@ +# +# 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', set(['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')) @@ -18,7 +18,7 @@ else: ], 'extras_require': { 'docs': ['docutils'], - 'test': ['testscenarios'], + 'test': ['fixtures', 'testscenarios'], }, } |