diff --git a/doc/source/target-extensions.rst b/doc/source/target-extensions.rst new file mode 100644 index 0000000..b4e59aa --- /dev/null +++ b/doc/source/target-extensions.rst @@ -0,0 +1,18 @@ +================= +Target Extensions +================= + +Subunit2SQL is meant to be generic. But while processing subunit streams, +you may have some site-specific behavior you'd like to enable, such as +loading attachments into a storage location that can be extracted later. + +To do this, you must create a plugin in the `subunit2sql.target` entry +point namespace, and it will be invoked along with the other targets +that subunit2sql invokes normally to store tests in a SQL database. + +The plugin's entry point should be a class that extends +`testtools.StreamResult`. It should also add a class method, `enabled`, +which returns False if the plugin has no chance of functioning. The +`enabled` method can also do any configuration file preparation if you +are using oslo.config. The constructor will not be executed until after +all config options are loaded. diff --git a/subunit2sql/read_subunit.py b/subunit2sql/read_subunit.py index aed6b75..d994d78 100644 --- a/subunit2sql/read_subunit.py +++ b/subunit2sql/read_subunit.py @@ -33,14 +33,18 @@ def get_duration(start, end): class ReadSubunit(object): - def __init__(self, stream_file, attachments=False, attr_regex=None): + def __init__(self, stream_file, attachments=False, attr_regex=None, + targets=None): + if targets is None: + targets = [] self.stream_file = stream_file self.stream = subunit.ByteStreamToStreamResult(stream_file) starts = testtools.StreamResult() summary = testtools.StreamSummary() outcomes = testtools.StreamToDict(functools.partial( self.parse_outcome)) - self.result = testtools.CopyStreamResult([starts, outcomes, summary]) + targets.extend([starts, outcomes, summary]) + self.result = testtools.CopyStreamResult(targets) self.results = {} self.attachments = attachments if attr_regex: diff --git a/subunit2sql/shell.py b/subunit2sql/shell.py index 4306629..de7db06 100644 --- a/subunit2sql/shell.py +++ b/subunit2sql/shell.py @@ -19,6 +19,7 @@ import sys from oslo_config import cfg from oslo_db import options from pbr import version +from stevedore import enabled from subunit2sql.db import api from subunit2sql import exceptions @@ -182,19 +183,30 @@ def process_results(results): def main(): cli_opts() + + def check_enabled(ext): + return ext.plugin.enabled() + extensions = enabled.EnabledExtensionManager('subunit2sql.target', + check_func=check_enabled) parse_args(sys.argv) + try: + targets = list(extensions.map(lambda ext: ext.plugin())) + except RuntimeError: + targets = [] if CONF.subunit_files: if len(CONF.subunit_files) > 1 and CONF.run_id: print("You can not specify a run id for adding more than 1 stream") return 3 streams = [subunit.ReadSubunit(open(s, 'r'), attachments=CONF.store_attachments, - attr_regex=CONF.attr_regex) + attr_regex=CONF.attr_regex, + targets=targets) for s in CONF.subunit_files] else: streams = [subunit.ReadSubunit(sys.stdin, attachments=CONF.store_attachments, - attr_regex=CONF.attr_regex)] + attr_regex=CONF.attr_regex, + targets=targets)] for stream in streams: process_results(stream.get_results()) diff --git a/subunit2sql/tests/test_read_subunit.py b/subunit2sql/tests/test_read_subunit.py index 60800a3..450264a 100644 --- a/subunit2sql/tests/test_read_subunit.py +++ b/subunit2sql/tests/test_read_subunit.py @@ -170,3 +170,8 @@ class TestReadSubunit(base.TestCase): test_name = read.cleanup_test_name(fake_id, strip_tags=True, strip_scenarios=False) self.assertEqual(fake_id, test_name) + + @mock.patch('testtools.CopyStreamResult') + def test_targets_added_to_result(self, ttc_mock): + subunit.ReadSubunit(mock.MagicMock(), targets=['foo']) + self.assertIn('foo', ttc_mock.call_args[0][0]) diff --git a/subunit2sql/tests/test_shell.py b/subunit2sql/tests/test_shell.py index 8956006..eaed32c 100644 --- a/subunit2sql/tests/test_shell.py +++ b/subunit2sql/tests/test_shell.py @@ -156,7 +156,8 @@ class TestMain(base.TestCase): shell.main() read_subunit_mock.assert_called_once_with(sys.stdin, attachments=False, - attr_regex='\[(.*)\]') + attr_regex='\[(.*)\]', + targets=[]) process_results_mock.assert_called_once_with(fake_get_results) @mock.patch('subunit2sql.read_subunit.ReadSubunit') @@ -165,8 +166,8 @@ class TestMain(base.TestCase): read_subunit_mock): tfile1 = tempfile.NamedTemporaryFile() tfile2 = tempfile.NamedTemporaryFile() - tfile1.write('test me later 1') - tfile2.write('test me later 2') + tfile1.write(b'test me later 1') + tfile2.write(b'test me later 2') tfile1.flush() tfile2.flush() self.fake_args.extend([tfile1.name, tfile2.name]) @@ -180,17 +181,36 @@ class TestMain(base.TestCase): shell.main() read_subunit_mock.assert_called_with(mock.ANY, attachments=False, - attr_regex='\[(.*)\]') + attr_regex='\[(.*)\]', + targets=[]) self.assertEqual(2, len(read_subunit_mock.call_args_list)) file_1 = read_subunit_mock.call_args_list[0][0][0] - self.assertIsInstance(file_1, file) file_1.seek(0) self.assertEqual('test me later 1', file_1.read()) file_2 = read_subunit_mock.call_args_list[1][0][0] - self.assertIsInstance(file_2, file) file_2.seek(0) self.assertEqual('test me later 2', file_2.read()) self.assertEqual(fake_get_results_1, process_results_mock.call_args_list[0][0][0]) self.assertEqual(fake_get_results_2, process_results_mock.call_args_list[1][0][0]) + + @mock.patch('stevedore.enabled.EnabledExtensionManager') + @mock.patch('subunit2sql.read_subunit.ReadSubunit') + @mock.patch('subunit2sql.shell.process_results') + def test_main_with_targets(self, process_results_mock, read_subunit_mock, + ext_mock): + exts = mock.MagicMock('EnabledExtensionManager()') + ext_mock.return_value = exts + exts.map = mock.MagicMock('extensions.map') + exts.map.return_value = [mock.sentinel.extension] + fake_read_subunit = mock.MagicMock('ReadSubunit') + fake_get_results = 'fake results' + fake_read_subunit.get_results = mock.MagicMock('get_results') + fake_read_subunit.get_results.return_value = fake_get_results + read_subunit_mock.return_value = fake_read_subunit + shell.main() + read_subunit_mock.assert_called_once_with( + sys.stdin, attachments=False, attr_regex='\[(.*)\]', + targets=[mock.sentinel.extension]) + process_results_mock.assert_called_once_with(fake_get_results)