diff options
author | Robert Collins <robertc@robertcollins.net> | 2010-12-07 16:16:41 +1300 |
---|---|---|
committer | Robert Collins <robertc@robertcollins.net> | 2010-12-07 16:16:41 +1300 |
commit | 1f73791771a48b6b947ebf6330251eb13e30491a (patch) | |
tree | 66eeda7334924c9c6ec64a86437a7bcd0e7289f7 /testrepository | |
parent | d8b50f127e8959c2b4020c0b93be3cd1e899a72e (diff) | |
download | testrepository-1f73791771a48b6b947ebf6330251eb13e30491a.tar.gz |
* ``testr load`` and ``testr run`` now have a flag ``--partial``. When set
this will cause existing failures to be preserved. When not set, doing a
load will reset existing failures. The ``testr run`` flag ``--failing``
implicitly sets ``--partial`` (so that an interrupted incremental test run
does not incorrectly discard a failure record). The ``--partial`` flag exists
so that deleted or renamed tests do not persist forever in the database.
(Robert Collins)
Diffstat (limited to 'testrepository')
-rw-r--r-- | testrepository/commands/load.py | 11 | ||||
-rw-r--r-- | testrepository/commands/run.py | 11 | ||||
-rw-r--r-- | testrepository/repository/__init__.py | 6 | ||||
-rw-r--r-- | testrepository/repository/file.py | 22 | ||||
-rw-r--r-- | testrepository/repository/memory.py | 9 | ||||
-rw-r--r-- | testrepository/tests/commands/test_load.py | 11 | ||||
-rw-r--r-- | testrepository/tests/commands/test_run.py | 21 | ||||
-rw-r--r-- | testrepository/tests/test_repository.py | 27 | ||||
-rw-r--r-- | testrepository/tests/ui/__init__.py | 1 | ||||
-rw-r--r-- | testrepository/tests/ui/test_decorator.py | 33 | ||||
-rw-r--r-- | testrepository/ui/decorator.py | 22 |
11 files changed, 146 insertions, 28 deletions
diff --git a/testrepository/commands/load.py b/testrepository/commands/load.py index 653d1cb..ab0f262 100644 --- a/testrepository/commands/load.py +++ b/testrepository/commands/load.py @@ -14,6 +14,8 @@ """Load data into a repository.""" +import optparse + import subunit from testtools import ConcurrentTestSuite, MultiTestResult @@ -26,10 +28,17 @@ class load(Command): Failing tests are shown on the console and a summary of the stream is printed at the end. + + Unless the stream is a partial stream, any existing failures are discarded. """ input_streams = ['subunit+'] + options = [ + optparse.Option("--partial", action="store_true", + default=False, help="The stream being loaded was a partial run."), + ] + def run(self): path = self.ui.here repo = self.repository_factory.open(path) @@ -43,7 +52,7 @@ class load(Command): for stream in streams(): yield subunit.ProtocolTestCase(stream) case = ConcurrentTestSuite(cases, make_tests) - inserter = repo.get_inserter() + inserter = repo.get_inserter(partial=self.ui.options.partial) output_result = self.ui.make_result(lambda: run_id) # XXX: We want to *count* skips, but not show them. filtered = TestResultFilter(output_result, filter_skip=False) diff --git a/testrepository/commands/run.py b/testrepository/commands/run.py index c48d3e7..f6929af 100644 --- a/testrepository/commands/run.py +++ b/testrepository/commands/run.py @@ -33,10 +33,13 @@ class run(Command): __doc__ = """Run the tests for a project and load them into testrepository. """ + testrconf_help - options = [optparse.Option("--failing", action="store_true", + options = [ + optparse.Option("--failing", action="store_true", default=False, help="Run only tests known to be failing."), optparse.Option("--parallel", action="store_true", default=False, help="Run tests in parallel processes."), + optparse.Option("--partial", action="store_true", + default=False, help="Only some tests will be run. Implied by --failing."), ] args = [StringArgument('testargs', 0, None)] # Can be assigned to to inject a custom command factory. @@ -63,7 +66,11 @@ class run(Command): cmd.setUp() try: run_procs = [('subunit', proc.stdout) for proc in cmd.run_tests()] - load_ui = decorator.UI(run_procs, self.ui) + options = {} + if self.ui.options.failing: + options['partial'] = True + load_ui = decorator.UI(input_streams=run_procs, options=options, + decorated=self.ui) load_cmd = load(load_ui) return load_cmd.execute() finally: diff --git a/testrepository/repository/__init__.py b/testrepository/repository/__init__.py index c39c6e1..c62d9dc 100644 --- a/testrepository/repository/__init__.py +++ b/testrepository/repository/__init__.py @@ -67,17 +67,19 @@ class AbstractRepository(object): """ raise NotImplementedError(self.get_failing) - def get_inserter(self): + def get_inserter(self, partial=False): """Get an inserter that will insert a test run into the repository. Repository implementations should implement _get_inserter. + :param partial: If True, the stream being inserted only executed some + tests rather than all the projects tests. :return an inserter: Inserters meet the extended TestResult protocol that testtools 0.9.2 and above offer. The startTestRun and stopTestRun methods in particular must be called. """ return subunit.test_results.AutoTimingTestResultDecorator( - self._get_inserter()) + self._get_inserter(partial)) def _get_inserter(self): """Get an inserter for get_inserter. diff --git a/testrepository/repository/file.py b/testrepository/repository/file.py index 743af1b..dc1ddc4 100644 --- a/testrepository/repository/file.py +++ b/testrepository/repository/file.py @@ -117,8 +117,8 @@ class Repository(AbstractRepository): os.path.join(self.base, str(run_id)), 'rb').read() return _DiskRun(run_subunit_content) - def _get_inserter(self): - return _Inserter(self) + def _get_inserter(self, partial): + return _Inserter(self, partial) def _write_next_stream(self, value): # Note that this is unlocked and not threadsafe : for now, shrug - single @@ -150,11 +150,12 @@ class _DiskRun(AbstractTestRun): class _SafeInserter(TestProtocolClient): - def __init__(self, repository): + def __init__(self, repository, partial=False): self._repository = repository fd, name = tempfile.mkstemp(dir=self._repository.base) self.fname = name stream = os.fdopen(fd, 'wb') + self.partial = partial TestProtocolClient.__init__(self, stream) def startTestRun(self): @@ -195,13 +196,14 @@ class _Inserter(_SafeInserter): # use memory repo to aggregate. a bit awkward on layering ;). import memory repo = memory.Repository() - # Seed with current failing - inserter = repo.get_inserter() - inserter.startTestRun() - failing = self._repository.get_failing() - failing.get_test().run(inserter) - inserter.stopTestRun() - inserter= repo.get_inserter() + if self.partial: + # Seed with current failing + inserter = repo.get_inserter() + inserter.startTestRun() + failing = self._repository.get_failing() + failing.get_test().run(inserter) + inserter.stopTestRun() + inserter= repo.get_inserter(partial=True) inserter.startTestRun() run = self._repository.get_test_run(run_id) run.get_test().run(inserter) diff --git a/testrepository/repository/memory.py b/testrepository/repository/memory.py index 6e9a3c4..aba7e73 100644 --- a/testrepository/repository/memory.py +++ b/testrepository/repository/memory.py @@ -70,8 +70,8 @@ class Repository(AbstractRepository): raise KeyError("No tests in repository") return result - def _get_inserter(self): - return _Inserter(self) + def _get_inserter(self, partial): + return _Inserter(self, partial) # XXX: Too much duplication between this and _Inserter @@ -101,8 +101,9 @@ class _Failures(AbstractTestRun): class _Inserter(AbstractTestRun): """Insert test results into a memory repository, and describe them later.""" - def __init__(self, repository): + def __init__(self, repository, partial): self._repository = repository + self._partial = partial self._outcomes = [] def startTestRun(self): @@ -110,6 +111,8 @@ class _Inserter(AbstractTestRun): def stopTestRun(self): self._repository._runs.append(self) + if not self._partial: + self._repository._failing = {} for record in self._outcomes: test_id = record[1].id() if record[0] in ('Failure', 'Error'): diff --git a/testrepository/tests/commands/test_load.py b/testrepository/tests/commands/test_load.py index a3c2f78..ee7d777 100644 --- a/testrepository/tests/commands/test_load.py +++ b/testrepository/tests/commands/test_load.py @@ -121,3 +121,14 @@ class TestCommandLoad(ResourcedTestCase): cmd.repository_factory.initialise(ui.here) self.assertEqual(0, cmd.execute()) self.assertEqual([], ui.outputs) + + def test_partial_passed_to_repo(self): + ui = UI([('subunit', '')], [('quiet', True), ('partial', True)]) + cmd = load.load(ui) + ui.set_command(cmd) + cmd.repository_factory = memory.RepositoryFactory() + cmd.repository_factory.initialise(ui.here) + self.assertEqual(0, cmd.execute()) + self.assertEqual([], ui.outputs) + self.assertEqual(True, + cmd.repository_factory.repos[ui.here].get_test_run(0)._partial) diff --git a/testrepository/tests/commands/test_run.py b/testrepository/tests/commands/test_run.py index b4fd436..405f2fc 100644 --- a/testrepository/tests/commands/test_run.py +++ b/testrepository/tests/commands/test_run.py @@ -113,6 +113,9 @@ class TestCommand(ResourcedTestCase): ('results', Wildcard), ('values', [('id', 1), ('tests', 0)]) ], ui.outputs) + # Failing causes partial runs to be used. + self.assertEqual(True, + cmd.repository_factory.repos[ui.here].get_test_run(1)._partial) def test_IDLIST_default_is_empty(self): ui, cmd = self.get_test_ui_and_cmd() @@ -192,3 +195,21 @@ class TestCommand(ResourcedTestCase): {'shell': True, 'stdin': PIPE, 'stdout': PIPE}), ], ui.outputs) self.assertEqual(0, result) + + def test_partial_passed_to_repo(self): + ui, cmd = self.get_test_ui_and_cmd( + options=[('quiet', True), ('partial', True)]) + cmd.repository_factory = memory.RepositoryFactory() + self.setup_repo(cmd, ui) + self.set_config( + '[DEFAULT]\ntest_command=foo\n') + result = cmd.execute() + expected_cmd = 'foo' + self.assertEqual([ + ('values', [('running', expected_cmd)]), + ('popen', (expected_cmd,), + {'shell': True, 'stdin': PIPE, 'stdout': PIPE}), + ], ui.outputs) + self.assertEqual(0, result) + self.assertEqual(True, + cmd.repository_factory.repos[ui.here].get_test_run(1)._partial) diff --git a/testrepository/tests/test_repository.py b/testrepository/tests/test_repository.py index 06bf7e7..7dbee1e 100644 --- a/testrepository/tests/test_repository.py +++ b/testrepository/tests/test_repository.py @@ -200,14 +200,14 @@ class TestRepositoryContract(ResourcedTestCase): self.assertEqual(1, len(analyzed.failures)) self.assertEqual('failing', analyzed.failures[0][0].id()) - def test_get_failing_two_runs(self): - # failures from two runs add to existing failures, and successes remove - # from them. + def test_get_failing_complete_runs_delete_missing_failures(self): + # failures from complete runs replace all failures. repo = self.repo_impl.initialise(self.sample_url) result = repo.get_inserter() result.startTestRun() make_test('passing', True).run(result) make_test('failing', False).run(result) + make_test('missing', False).run(result) result.stopTestRun() result = repo.get_inserter() result.startTestRun() @@ -219,6 +219,27 @@ class TestRepositoryContract(ResourcedTestCase): self.assertEqual(1, len(analyzed.failures)) self.assertEqual('passing', analyzed.failures[0][0].id()) + def test_get_failing_partial_runs_preserve_missing_failures(self): + # failures from two runs add to existing failures, and successes remove + # from them. + repo = self.repo_impl.initialise(self.sample_url) + result = repo.get_inserter() + result.startTestRun() + make_test('passing', True).run(result) + make_test('failing', False).run(result) + make_test('missing', False).run(result) + result.stopTestRun() + result = repo.get_inserter(partial=True) + result.startTestRun() + make_test('passing', False).run(result) + make_test('failing', True).run(result) + result.stopTestRun() + analyzed = self.get_failing(repo) + self.assertEqual(2, analyzed.testsRun) + self.assertEqual(2, len(analyzed.failures)) + self.assertEqual(set(['passing', 'missing']), + set([test[0].id() for test in analyzed.failures])) + def test_get_test_run(self): repo = self.repo_impl.initialise(self.sample_url) result = repo.get_inserter() diff --git a/testrepository/tests/ui/__init__.py b/testrepository/tests/ui/__init__.py index 605ce6d..234a736 100644 --- a/testrepository/tests/ui/__init__.py +++ b/testrepository/tests/ui/__init__.py @@ -19,6 +19,7 @@ import unittest def test_suite(): names = [ 'cli', + 'decorator', ] module_names = ['testrepository.tests.ui.test_' + name for name in names] diff --git a/testrepository/tests/ui/test_decorator.py b/testrepository/tests/ui/test_decorator.py new file mode 100644 index 0000000..e6c6592 --- /dev/null +++ b/testrepository/tests/ui/test_decorator.py @@ -0,0 +1,33 @@ +# -*- encoding: utf-8 -*- +# +# 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 UI decorator.""" + +from testrepository import commands +from testrepository.ui import decorator, model +from testrepository.tests import ResourcedTestCase + + +class TestDecoratorUI(ResourcedTestCase): + + def test_options_overridable(self): + base = model.UI(options=[('partial', True), ('other', False)]) + cmd = commands.Command(base) + base.set_command(cmd) + ui = decorator.UI(options={'partial':False}, decorated=base) + internal_cmd = commands.Command(ui) + ui.set_command(internal_cmd) + self.assertEqual(False, ui.options.partial) + self.assertEqual(False, ui.options.other) diff --git a/testrepository/ui/decorator.py b/testrepository/ui/decorator.py index 100674a..5cd3128 100644 --- a/testrepository/ui/decorator.py +++ b/testrepository/ui/decorator.py @@ -15,6 +15,7 @@ """A decorator for UIs to allow use of additional command objects in-process.""" from StringIO import StringIO +import optparse from testrepository import ui @@ -28,11 +29,13 @@ class UI(ui.AbstractUI): does not get passed to the decorated UI unless it has not been initialised. """ - def __init__(self, input_streams=None, decorated=None): + def __init__(self, input_streams=None, options={}, decorated=None): """Create a decorating UI. :param input_streams: The input steams to present from this UI. Should be a list of (stream name, file) tuples. + :param options: Dict of options to replace in the base UI. These are + merged with the underlying ones when set_command is called. :param decorated: The UI to decorate. """ self._decorated = decorated @@ -41,10 +44,7 @@ class UI(ui.AbstractUI): for stream_type, stream_value in input_streams: self.input_streams.setdefault(stream_type, []).append( stream_value) - - @property - def options(self): - return self._decorated.options + self._options = options @property def arguments(self): @@ -85,11 +85,19 @@ class UI(ui.AbstractUI): def set_command(self, cmd): self.cmd = cmd + result = True if getattr(self._decorated, 'cmd', None) is None: - return self._decorated.set_command(cmd) + result = self._decorated.set_command(cmd) # Pickup the repository factory from the decorated UI's command. cmd.repository_factory = self._decorated.cmd.repository_factory - return True + # Merge options + self.options = optparse.Values() + for option in dir(self._decorated.options): + setattr(self.options, option, + getattr(self._decorated.options, option)) + for option, value in self._options.items(): + setattr(self.options, option, value) + return result def subprocess_Popen(self, *args, **kwargs): return self._decorated.subprocess_Popen(*args, **kwargs) |