summaryrefslogtreecommitdiff
path: root/testrepository
diff options
context:
space:
mode:
authorRobert Collins <robertc@robertcollins.net>2010-12-07 16:16:41 +1300
committerRobert Collins <robertc@robertcollins.net>2010-12-07 16:16:41 +1300
commit1f73791771a48b6b947ebf6330251eb13e30491a (patch)
tree66eeda7334924c9c6ec64a86437a7bcd0e7289f7 /testrepository
parentd8b50f127e8959c2b4020c0b93be3cd1e899a72e (diff)
downloadtestrepository-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.py11
-rw-r--r--testrepository/commands/run.py11
-rw-r--r--testrepository/repository/__init__.py6
-rw-r--r--testrepository/repository/file.py22
-rw-r--r--testrepository/repository/memory.py9
-rw-r--r--testrepository/tests/commands/test_load.py11
-rw-r--r--testrepository/tests/commands/test_run.py21
-rw-r--r--testrepository/tests/test_repository.py27
-rw-r--r--testrepository/tests/ui/__init__.py1
-rw-r--r--testrepository/tests/ui/test_decorator.py33
-rw-r--r--testrepository/ui/decorator.py22
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)