summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorRobert Collins <robertc@robertcollins.net>2015-07-13 23:02:48 +1200
committerRobert Collins <robertc@robertcollins.net>2015-07-13 23:02:48 +1200
commitc90fc5d0a74f7bc8a7e0d9c846c2d0f46b97f8d2 (patch)
treec287802daae58d571c1b5a61123b4bf682bb7fad
parent2bd3a0ef34b3fee9b6608799be31844cc0510164 (diff)
downloadsubunit-c90fc5d0a74f7bc8a7e0d9c846c2d0f46b97f8d2.tar.gz
Add subunit2disk which exports a stream to the fs.
-rw-r--r--Makefile.am5
-rw-r--r--NEWS9
-rw-r--r--README.rst1
-rwxr-xr-xfilters/subunit2disk23
-rw-r--r--python/subunit/_to_disk.py131
-rw-r--r--python/subunit/tests/__init__.py2
-rw-r--r--python/subunit/tests/test_filter_to_disk.py49
-rwxr-xr-xsetup.py2
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
diff --git a/NEWS b/NEWS
index 4aa845b..f7acbbc 100644
--- a/NEWS
+++ b/NEWS
@@ -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
~~~~~~~~
diff --git a/README.rst b/README.rst
index bd393ae..1096a49 100644
--- a/README.rst
+++ b/README.rst
@@ -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'))
diff --git a/setup.py b/setup.py
index 803cbb9..0e8c4e7 100755
--- a/setup.py
+++ b/setup.py
@@ -18,7 +18,7 @@ else:
],
'extras_require': {
'docs': ['docutils'],
- 'test': ['testscenarios'],
+ 'test': ['fixtures', 'testscenarios'],
},
}