diff options
author | Robert Collins <robertc@robertcollins.net> | 2010-12-06 19:22:46 +1300 |
---|---|---|
committer | Robert Collins <robertc@robertcollins.net> | 2010-12-06 19:22:46 +1300 |
commit | 4045f2c89535934178df895e8b369cd63f82da03 (patch) | |
tree | 3f8f85de6fb27d3176133eb2442a5da9b5657dfd /testrepository | |
parent | 5369521457f1de8307fe4c5d3a3897bf768067be (diff) | |
download | testrepository-4045f2c89535934178df895e8b369cd63f82da03.tar.gz |
* ``testr list-tests`` is a new command that will list the tests for a project
when ``.testr.conf`` has been configured with a ``test_list_option``.
(Robert Collins)
Diffstat (limited to 'testrepository')
-rw-r--r-- | testrepository/commands/__init__.py | 6 | ||||
-rw-r--r-- | testrepository/commands/list_tests.py | 48 | ||||
-rw-r--r-- | testrepository/commands/run.py | 3 | ||||
-rw-r--r-- | testrepository/testcommand.py | 44 | ||||
-rw-r--r-- | testrepository/tests/commands/__init__.py | 1 | ||||
-rw-r--r-- | testrepository/tests/commands/test_list_tests.py | 90 | ||||
-rw-r--r-- | testrepository/tests/test_commands.py | 6 | ||||
-rw-r--r-- | testrepository/tests/test_testcommand.py | 18 | ||||
-rw-r--r-- | testrepository/ui/model.py | 12 |
9 files changed, 212 insertions, 16 deletions
diff --git a/testrepository/commands/__init__.py b/testrepository/commands/__init__.py index 67efee0..16616b5 100644 --- a/testrepository/commands/__init__.py +++ b/testrepository/commands/__init__.py @@ -40,6 +40,7 @@ import subunit from testrepository.repository import file def _find_command(cmd_name): + orig_cmd_name = cmd_name cmd_name = cmd_name.replace('-', '_') classname = "%s" % cmd_name modname = "testrepository.commands.%s" % cmd_name @@ -54,7 +55,7 @@ def _find_command(cmd_name): % (classname, modname)) if getattr(result, 'name', None) is None: # Store the name for the common case of name == lookup path. - result.name = classname + result.name = orig_cmd_name return result @@ -69,8 +70,9 @@ def iter_commands(): if base.startswith('.'): continue name = base.split('.', 1)[0] + name = name.replace('_', '-') names.add(name) - names.discard('__init__') + names.discard('--init--') names = sorted(names) for name in names: yield _find_command(name) diff --git a/testrepository/commands/list_tests.py b/testrepository/commands/list_tests.py new file mode 100644 index 0000000..d11df44 --- /dev/null +++ b/testrepository/commands/list_tests.py @@ -0,0 +1,48 @@ +# +# Copyright (c) 2010 Testrepository 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. + +"""List the tests from a project and show them.""" + +from cStringIO import StringIO + +from testtools import TestResult + +from testrepository.arguments.string import StringArgument +from testrepository.commands import Command +from testrepository.testcommand import testrconf_help, TestCommand + + +class list_tests(Command): + __doc__ = """Lists the tests for a project. + """ + testrconf_help + + args = [StringArgument('testargs', 0, None)] + # Can be assigned to to inject a custom command factory. + command_factory = TestCommand + + def run(self): + testcommand = self.command_factory(self.ui) + ids = None + cmd = testcommand.get_run_command(ids, self.ui.arguments['testargs']) + cmd.setUp() + try: + ids = cmd.list_tests() + stream = StringIO() + for id in ids: + stream.write('%s\n' % id) + stream.seek(0) + self.ui.output_stream(stream) + return 0 + finally: + cmd.cleanUp() diff --git a/testrepository/commands/run.py b/testrepository/commands/run.py index cff0fc8..fb1fdc9 100644 --- a/testrepository/commands/run.py +++ b/testrepository/commands/run.py @@ -28,6 +28,7 @@ from testrepository.commands.load import load from testrepository.ui import decorator from testrepository.testcommand import TestCommand, testrconf_help + class run(Command): __doc__ = """Run the tests for a project and load them into testrepository. """ + testrconf_help @@ -64,5 +65,3 @@ class run(Command): return load_cmd.execute() finally: cmd.cleanUp() - template = string.Template( - ' '.join(elements) + '| testr load %s-d %s' % (quiet, self.ui.here)) diff --git a/testrepository/testcommand.py b/testrepository/testcommand.py index a7a6d41..5236327 100644 --- a/testrepository/testcommand.py +++ b/testrepository/testcommand.py @@ -30,8 +30,9 @@ testrconf_help = dedent(""" test_command=foo $IDOPTION test_id_option=--bar $IDFILE --- - will cause 'testr run' to run 'foo | testr load', and 'testr run --failing' - to run 'foo --bar failing.list | testr load'. + will cause 'testr run' to run 'foo' to execute tests, and + 'testr run --failing' will cause 'foo --bar failing.list ' to be run to + execute tests. The full list of options and variables for .testr.conf: * test_command -- command line to run to execute tests. @@ -39,6 +40,11 @@ testrconf_help = dedent(""" test ids should be run. * test_id_list_default -- the value to use for $IDLIST when no specific test ids are being run. + * test_list_option -- the option to use to cause the test runner to report + on the tests it would run, rather than running them. When supplied the + test_command should output on stdout all the test ids that would have + been run if every other option and argument was honoured, one per line. + This is required for parallel testing, and is substituted into $LISTOPT. * $IDOPTION -- the variable to use to trigger running some specific tests. * $IDFILE -- A file created before the test command is run and deleted afterwards which contains a list of test ids, one per line. This can @@ -52,19 +58,22 @@ testrconf_help = dedent(""" class TestListingFixture(Fixture): """Write a temporary file to disk with test ids in it.""" - def __init__(self, test_ids, cmd_template, ui, listpath=None): + def __init__(self, test_ids, cmd_template, listopt, ui, listpath=None): """Create a TestListingFixture. :param test_ids: The test_ids to use. May be None indicating that no ids are present. :param cmd_template: string to be filled out with IDFILE. + :param listopt: Option to substitute into LISTOPT to cause test listing + to take place. :param ui: The UI in use. :param listpath: The file listing path to use. If None, a unique path is created. """ self.test_ids = test_ids self.template = cmd_template + self.listopt = listopt self.ui = ui self._listpath = listpath @@ -80,7 +89,8 @@ class TestListingFixture(Fixture): cmd = re.sub('\$IDFILE', name, cmd) idlist = ' '.join(self.test_ids) cmd = re.sub('\$IDLIST', idlist, cmd) - self.cmd = cmd + self.cmd = re.sub('\$LISTOPT', '', cmd) + self.list_cmd = re.sub('\$LISTOPT', self.listopt, cmd) def make_listfile(self): name = None @@ -101,6 +111,19 @@ class TestListingFixture(Fixture): self.addCleanup(os.unlink, name) return name + def list_tests(self): + """List the tests returned by list_cmd. + + :return: A list of test ids. + """ + self.ui.output_values([('running', self.list_cmd)]) + run_proc = self.ui.subprocess_Popen(self.list_cmd, shell=True, + stdout=subprocess.PIPE, stdin=subprocess.PIPE) + out, err = run_proc.communicate() + # Should we raise on non-zero exit? + ids = [id for id in out.split('\n') if id] + return ids + def run_tests(self): """Run the tests defined by the command and ui. @@ -167,9 +190,18 @@ class TestCommand(object): # No test ids, no id option. idoption = '' cmd = re.sub('\$IDOPTION', idoption, cmd) + listopt = '' + if '$LISTOPT' in command: + # LISTOPT is used, test_list_option must be configured. + try: + listopt = parser.get('DEFAULT', 'test_list_option') + except ConfigParser.NoOptionError, e: + if e.message != "No option 'test_list_option' in section: 'DEFAULT'": + raise + raise ValueError("No test_list_option option present in .testr.conf") if self.oldschool: listpath = os.path.join(self.ui.here, 'failing.list') - result = self.run_factory(test_ids, cmd, self.ui, listpath) + result = self.run_factory(test_ids, cmd, listopt, self.ui, listpath) else: - result = self.run_factory(test_ids, cmd, self.ui) + result = self.run_factory(test_ids, cmd, listopt, self.ui) return result diff --git a/testrepository/tests/commands/__init__.py b/testrepository/tests/commands/__init__.py index 54b00fb..117221c 100644 --- a/testrepository/tests/commands/__init__.py +++ b/testrepository/tests/commands/__init__.py @@ -23,6 +23,7 @@ def test_suite(): 'help', 'init', 'last', + 'list_tests', 'load', 'quickstart', 'run', diff --git a/testrepository/tests/commands/test_list_tests.py b/testrepository/tests/commands/test_list_tests.py new file mode 100644 index 0000000..8d4e167 --- /dev/null +++ b/testrepository/tests/commands/test_list_tests.py @@ -0,0 +1,90 @@ +# +# Copyright (c) 2010 Testrepository 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. + +"""Tests for the list_tests command.""" + +import os.path +from subprocess import PIPE + +from testtools.matchers import MatchesException + +from testrepository.commands import list_tests +from testrepository.ui.model import UI +from testrepository.repository import memory +from testrepository.tests import ResourcedTestCase, Wildcard +from testrepository.tests.stubpackage import TempDirResource +from testrepository.tests.test_repository import make_test +from testrepository.tests.test_testcommand import FakeTestCommand + + +class TestCommand(ResourcedTestCase): + + resources = [('tempdir', TempDirResource())] + + def get_test_ui_and_cmd(self, options=(), args=()): + self.dirty() + ui = UI(options=options, args=args) + ui.here = self.tempdir + cmd = list_tests.list_tests(ui) + ui.set_command(cmd) + return ui, cmd + + def dirty(self): + # Ugly: TODO - improve testresources to make this go away. + dict(self.resources)['tempdir']._dirty = True + + def config_path(self): + return os.path.join(self.tempdir, '.testr.conf') + + def set_config(self, bytes): + stream = file(self.config_path(), 'wb') + try: + stream.write(bytes) + finally: + stream.close() + + def setup_repo(self, cmd, ui): + repo = cmd.repository_factory.initialise(ui.here) + inserter = repo.get_inserter() + inserter.startTestRun() + make_test('passing', True).run(inserter) + make_test('failing', False).run(inserter) + inserter.stopTestRun() + + def test_no_config_file_errors(self): + ui, cmd = self.get_test_ui_and_cmd() + self.assertEqual(3, cmd.execute()) + self.assertEqual(1, len(ui.outputs)) + self.assertEqual('error', ui.outputs[0][0]) + self.assertThat(ui.outputs[0][1], + MatchesException(ValueError('No .testr.conf config file'))) + + def test_calls_list_tests(self): + ui, cmd = self.get_test_ui_and_cmd(args=('bar', 'quux')) + cmd.repository_factory = memory.RepositoryFactory() + ui.proc_outputs = ['returned\n\nvalues\n'] + self.setup_repo(cmd, ui) + self.set_config( + '[DEFAULT]\ntest_command=foo $LISTOPT $IDOPTION\n' + 'test_id_option=--load-list $IDFILE\n' + 'test_list_option=--list\n') + self.assertEqual(0, cmd.execute()) + expected_cmd = 'foo --list bar quux' + self.assertEqual([ + ('values', [('running', expected_cmd)]), + ('popen', (expected_cmd,), + {'shell': True, 'stdout': PIPE, 'stdin': PIPE}), + ('communicate',), + ('stream', 'returned\nvalues\n'), + ], ui.outputs) diff --git a/testrepository/tests/test_commands.py b/testrepository/tests/test_commands.py index 3472b7f..130493e 100644 --- a/testrepository/tests/test_commands.py +++ b/testrepository/tests/test_commands.py @@ -90,9 +90,9 @@ class TestNameMangling(ResourcedTestCase): def test_sets_name(self): cmd = commands._find_command('foo-bar') - # This is arbitrary in the absence of a reason to do it any particular - # way. - self.assertEqual('foo_bar', cmd.name) + # The name is preserved, so that 'testr commands' shows something + # sensible. + self.assertEqual('foo-bar', cmd.name) class TestIterCommands(ResourcedTestCase): diff --git a/testrepository/tests/test_testcommand.py b/testrepository/tests/test_testcommand.py index a3ad6aa..d275304 100644 --- a/testrepository/tests/test_testcommand.py +++ b/testrepository/tests/test_testcommand.py @@ -161,3 +161,21 @@ class TestTestCommand(ResourcedTestCase): testargs=('bar', 'quux'))) expected_cmd = 'foo bar quux' self.assertEqual(expected_cmd, fixture.cmd) + + def test_list_tests_cmd(self): + ui, command = self.get_test_ui_and_cmd() + self.set_config( + '[DEFAULT]\ntest_command=foo $LISTOPT $IDLIST\ntest_id_list_default=whoo yea\n' + 'test_list_option=--list\n') + fixture = self.useFixture(command.get_run_command()) + expected_cmd = 'foo --list whoo yea' + self.assertEqual(expected_cmd, fixture.list_cmd) + + def test_list_tests_parsing(self): + ui, command = self.get_test_ui_and_cmd() + ui.proc_outputs = ['returned\nids\n'] + self.set_config( + '[DEFAULT]\ntest_command=foo $LISTOPT $IDLIST\ntest_id_list_default=whoo yea\n' + 'test_list_option=--list\n') + fixture = self.useFixture(command.get_run_command()) + self.assertEqual(set(['returned', 'ids']), set(fixture.list_tests())) diff --git a/testrepository/ui/model.py b/testrepository/ui/model.py index 8533b04..de2a0f1 100644 --- a/testrepository/ui/model.py +++ b/testrepository/ui/model.py @@ -31,7 +31,7 @@ class ProcessModel(object): def communicate(self): self.ui.outputs.append(('communicate',)) - return '', '' + return self.stdout.getvalue(), '' class TestSuiteModel(object): @@ -84,7 +84,7 @@ class UI(ui.AbstractUI): """ def __init__(self, input_streams=None, options=(), args={}, - here='memory:'): + here='memory:', proc_outputs=()): """Create a model UI. :param input_streams: A list of stream name, (file or bytes) tuples to @@ -92,6 +92,8 @@ class UI(ui.AbstractUI): :param options: Options to explicitly set values for. :param args: The argument values to give the UI. :param here: Set the here value for the UI. + :param proc_outputs: byte strings to be returned in the stdout from + created processes. """ self.input_streams = {} if input_streams: @@ -103,6 +105,7 @@ class UI(ui.AbstractUI): self.outputs = [] # Could take parsed args, but for now this is easier. self.unparsed_args = args + self.proc_outputs = list(proc_outputs) def _check_cmd(self): options = list(self.unparsed_opts) @@ -161,4 +164,7 @@ class UI(ui.AbstractUI): def subprocess_Popen(self, *args, **kwargs): # Really not an output - outputs should be renamed to events. self.outputs.append(('popen', args, kwargs)) - return ProcessModel(self) + result = ProcessModel(self) + if self.proc_outputs: + result.stdout = StringIO(self.proc_outputs.pop(0)) + return result |