summaryrefslogtreecommitdiff
path: root/testrepository
diff options
context:
space:
mode:
authorRobert Collins <robertc@robertcollins.net>2010-12-06 19:22:46 +1300
committerRobert Collins <robertc@robertcollins.net>2010-12-06 19:22:46 +1300
commit4045f2c89535934178df895e8b369cd63f82da03 (patch)
tree3f8f85de6fb27d3176133eb2442a5da9b5657dfd /testrepository
parent5369521457f1de8307fe4c5d3a3897bf768067be (diff)
downloadtestrepository-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__.py6
-rw-r--r--testrepository/commands/list_tests.py48
-rw-r--r--testrepository/commands/run.py3
-rw-r--r--testrepository/testcommand.py44
-rw-r--r--testrepository/tests/commands/__init__.py1
-rw-r--r--testrepository/tests/commands/test_list_tests.py90
-rw-r--r--testrepository/tests/test_commands.py6
-rw-r--r--testrepository/tests/test_testcommand.py18
-rw-r--r--testrepository/ui/model.py12
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