Commit c4339f48 authored by SVN-Git Migration's avatar SVN-Git Migration

Imported Upstream version 0.0.4

parent 9189840b
......@@ -5,11 +5,28 @@ testrepository release notes
NEXT (In development)
* ``failing`` now supports ``--list`` to list the failing tests.
(Jonathan Lange)
* Repository not found errors are now clearer. (Jonathan Lange, #530010)
* The summary of a test run is now formatted as foo=NN rather than foo: NN,
which some folk find easier to read.
* The file implementation of now performs ~ expansion.
(Jonathan Lange, #529665)
* ``failing`` now correctly calls ``repository.get_failing`` and will this
track all seen failures rather than just the latest observed failures.
Metadata-Version: 1.0
Name: testrepository
Version: 0.0.3
Version: 0.0.4
Summary: A repository of test results.
Author: Robert Collins
......@@ -18,9 +18,10 @@ Data model/storage
testrepository stores subunit streams as subunit streams in .testrespository
with simple additional metadata - a format marker to allow this to be changed
in the future, and a next-stream counter counting the id to give the next
with simple additional metadata. See the MANUAL for documentation on the
repository layout. The key design elements are that streams are stored
verbatim, and a testr managed stream called 'failing' is used to track the
current failures.
Code layout
......@@ -36,3 +37,15 @@ all types.
The tests for the code in is in Interface tests for is
in testrepository.tests.test_foo.
External integration
Test Repository command, ui, parsing etc objects should all be suitable for
reuse from other programs.
In general using any public interface
......@@ -42,7 +42,7 @@ to run are passed through to your test runner command line. To pass options
through to your test running, use a ``--`` before your options.
For instance, ``testr run foo -- bar --no-plugins`` would run
``foo foo bar --no-plugins | testr load`` using the above config example. The
command help for ``testr run`` describes the availableoptions for .testr.conf.
command help for ``testr run`` describes the available options for .testr.conf.
Having setup a .testr.conf, a common workflow then becomes::
......@@ -52,3 +52,25 @@ Having setup a .testr.conf, a common workflow then becomes::
$ testr run
# And either commit or loop around this again depending on whether errors
# were found.
A testr repository is a very simple disk structure. It contains the following
files (for a format 1 repository - the only current format):
* format: This file identifies the precise layout of the repository, in case
future changes are needed.
* next-stream: This file contains the serial number to be used when adding another
stream to the repository.
* failing: This file is a stream containing just the known failing tests. It
is updated whenever a new stream is added to the repository, so that it only
references known failing tests.
* #N - all the streams inserted in the repository are given a serial number.
* repo.conf: This file contains user configuration settings for the repository.
``testr repo-config`` will dump a repo configration and
``test help repo-config`` has online help for all the repository settings.
......@@ -33,4 +33,4 @@ The tests package contains tests and test specific support code.
# established at this point, and will use a version of next-$(revno).
# If the releaselevel is 'final', then the tarball will be major.minor.micro.
# Otherwise it is major.minor.micro~$(revno).
__version__ = (0, 0, 3, 'final', 0)
__version__ = (0, 0, 4, 'final', 0)
......@@ -125,6 +125,10 @@ class Command(object):
and finally calls run() to perform the command. Most commands should
not need to override this method, and any user wanting to run a
command should call this method.
This is a synchronous method, and basically just a helper. GUI's or
asynchronous programs can choose to not call it and instead should call
lower level API's.
if not self.ui.set_command(self):
return 1
......@@ -32,8 +32,14 @@ class failing(Command):
tests will only be detected on full runs with this approach.
options = [optparse.Option("--subunit", action="store_true",
default=False, help="Show output as a subunit stream.")]
options = [
"--subunit", action="store_true",
default=False, help="Show output as a subunit stream."),
"--list", action="store_true",
default=False, help="Show only a list of failing tests."),
def run(self):
repo =
......@@ -56,6 +62,11 @@ class failing(Command):
result = 1
result = 0
if self.ui.options.list:
failing_tests = [
test for test, _ in evaluator.errors + evaluator.failures]
return result
if self.ui.options.subunit:
# TODO only failing tests.
......@@ -40,7 +40,10 @@ class AbstractRepositoryFactory(object):
raise NotImplementedError(self.initialise)
def open(self, url):
"""Open the repository at url."""
"""Open the repository at url.
Raise RepositoryNotFound if there is no repository at the given url.
raise NotImplementedError(
......@@ -110,3 +113,12 @@ class AbstractTestRun(object):
tests reported to a testtools.TestResult.
raise NotImplementedError(self.get_test)
class RepositoryNotFound(Exception):
"""Raised when we try to open a repository that isn't there."""
def __init__(self, url):
self.url = url
msg = 'No repository found in %s. Create one by running "testr init".'
Exception.__init__(self, msg % url)
......@@ -27,6 +27,7 @@ from testrepository.repository import (
......@@ -34,7 +35,7 @@ class RepositoryFactory(AbstractRepositoryFactory):
def initialise(klass, url):
"""Create a repository at url/path."""
base = os.path.join(url, '.testrepository')
base = os.path.join(os.path.expanduser(url), '.testrepository')
stream = file(os.path.join(base, 'format'), 'wb')
......@@ -46,8 +47,14 @@ class RepositoryFactory(AbstractRepositoryFactory):
return result
def open(self, url):
base = os.path.join(url, '.testrepository')
stream = file(os.path.join(base, 'format'), 'rb')
path = os.path.expanduser(url)
base = os.path.join(path, '.testrepository')
stream = file(os.path.join(base, 'format'), 'rb')
except (IOError, OSError), e:
if e.errno == errno.ENOENT:
raise RepositoryNotFound(url)
if '1\n' !=
raise ValueError(url)
return Repository(base)
......@@ -22,6 +22,7 @@ from testrepository.repository import (
......@@ -40,7 +41,10 @@ class RepositoryFactory(AbstractRepositoryFactory):
return self.repos[url]
def open(self, url):
return self.repos[url]
return self.repos[url]
except KeyError:
raise RepositoryNotFound(url)
class Repository(AbstractRepository):
......@@ -46,7 +46,7 @@ class TestCommand(ResourcedTestCase):
id = inserter.stopTestRun()
self.assertEqual(1, cmd.execute())
self.assertEqual('results', ui.outputs[0][0])
suite = ui.outputs[0][1]
......@@ -78,13 +78,37 @@ class TestCommand(ResourcedTestCase):
id = inserter.stopTestRun()
self.assertEqual(1, cmd.execute())
self.assertEqual(1, len(ui.outputs))
self.assertEqual('stream', ui.outputs[0][0])
self.assertThat(ui.outputs[0][1], DocTestMatches("""...test: ...failing
...failure: ...failing...""", doctest.ELLIPSIS))
def test_with_list_shows_list_of_tests(self):
ui, cmd = self.get_test_ui_and_cmd(options=[('list', True)])
cmd.repository_factory = memory.RepositoryFactory()
repo = cmd.repository_factory.initialise(
inserter = repo.get_inserter()
class Cases(ResourcedTestCase):
def failing1(self):'foo')
def failing2(self):'bar')
def ok(self):
self.assertEqual(1, cmd.execute(), ui.outputs)
self.assertEqual(1, len(ui.outputs))
self.assertEqual('tests', ui.outputs[0][0])
set([Cases('failing1').id(), Cases('failing2').id()]),
set([ for test in ui.outputs[0][1]]))
def test_uses_get_failing(self):
ui, cmd = self.get_test_ui_and_cmd()
cmd.repository_factory = memory.RepositoryFactory()
......@@ -99,6 +123,6 @@ class TestCommand(ResourcedTestCase):
repo.get_failing = get_failing
return repo = decorate_open_with_get_failing
repo = cmd.repository_factory.initialise(
self.assertEqual(0, cmd.execute())
self.assertEqual([True], calls)
......@@ -15,6 +15,8 @@
"""Tests for the file repository implementation."""
import os.path
import shutil
import tempfile
from testrepository.repository import file
from testrepository.tests import ResourcedTestCase
......@@ -26,7 +28,7 @@ class TestFileRepository(ResourcedTestCase):
resources = [('tempdir', TempDirResource())]
def test_initialise(self):
repo = file.RepositoryFactory().initialise(self.tempdir)
base = os.path.join(self.tempdir, '.testrepository')
stream = open(os.path.join(base, 'format'), 'rb')
......@@ -42,6 +44,14 @@ class TestFileRepository(ResourcedTestCase):
self.assertEqual("0\n", contents)
def test_initialise_expands_user_directory(self):
home_dir = os.path.expanduser('~')
temp_dir = tempfile.mkdtemp(dir=home_dir)
self.addCleanup(shutil.rmtree, temp_dir)
short_path = os.path.join('~', os.path.basename(temp_dir))
repo = file.RepositoryFactory().initialise(short_path)
def test_inserter_output_path(self):
repo = file.RepositoryFactory().initialise(self.tempdir)
......@@ -57,3 +67,12 @@ class TestFileRepository(ResourcedTestCase):
result = repo.get_inserter()
self.assertEqual(0, result.stopTestRun())
def test_open_expands_user_directory(self):
home_dir = os.path.expanduser('~')
temp_dir = tempfile.mkdtemp(dir=home_dir)
self.addCleanup(shutil.rmtree, temp_dir)
short_path = os.path.join('~', os.path.basename(temp_dir))
repo1 = file.RepositoryFactory().initialise(short_path)
repo2 = file.RepositoryFactory().open(short_path)
self.assertEqual(repo1.base, repo2.base)
......@@ -132,8 +132,16 @@ class TestRepositoryContract(ResourcedTestCase):
self.assertEqual(1, repo.count())
def test_open(self):
repo1 = self.repo_impl.initialise(self.sample_url)
repo2 =
def test_open_non_existent(self):
url = 'doesntexistatall'
error = self.assertRaises(
repository.RepositoryNotFound,, url)
'No repository found in %s. Create one by running "testr init".'
% url, str(error))
def test_inserting_creates_id(self):
# When inserting a stream, an id is returned from stopTestRun.
......@@ -120,6 +120,11 @@ class TestUIContract(ResourcedTestCase):
ui = self.get_test_ui()
ui.output_table([('col1', 'col2'), ('row1c1','row1c2')])
def test_output_tests(self):
# output_tests can be called, and takes a list of tests to output.
ui = self.get_test_ui()
ui.output_tests([self, self.__class__('test_output_table')])
def test_output_values(self):
# output_values can be called and takes a list of things to output.
ui = self.get_test_ui()
......@@ -116,10 +116,19 @@ AssertionError: quux
self.assertEqual('foo 1\n--- ----\nb quux\n',
def test_outputs_tests_to_stdout(self):
ui, cmd = self.get_test_ui_and_cmd()
ui.output_tests([self, self.__class__('test_construct')])
'...TestCLIUI.test_construct\n', doctest.ELLIPSIS))
def test_outputs_values_to_stdout(self):
ui, cmd = self.get_test_ui_and_cmd()
ui.output_values([('foo', 1), ('bar', 'quux')])
self.assertEqual('foo: 1 bar: quux\n', ui._stdout.getvalue())
self.assertEqual('foo=1, bar=quux\n', ui._stdout.getvalue())
def test_parse_error_goes_to_stderr(self):
stdout = StringIO()
......@@ -124,14 +124,18 @@ class UI(ui.AbstractUI):
def output_tests(self, tests):
for test in tests:
def output_values(self, values):
outputs = []
for label, value in values:
outputs.append('%s: %s' % (label, value))
self._stdout.write('%s\n' % ' '.join(outputs))
outputs.append('%s=%s' % (label, value))
self._stdout.write('%s\n' % ', '.join(outputs))
def _check_cmd(self):
cmd = self.cmd
parser = OptionParser()
parser.add_option("-d", "--here", dest="here",
help="Set the directory or url that a command should run from. "
......@@ -145,7 +149,6 @@ class UI(ui.AbstractUI):
options, args = parser.parse_args(self._argv) =
self.options = options
orig_args = list(args)
parsed_args = {}
failed = False
for arg in self.cmd.args:
......@@ -102,6 +102,10 @@ class UI(ui.AbstractUI):
def output_table(self, table):
self.outputs.append(('table', table))
def output_tests(self, tests):
"""Output a list of tests."""
self.outputs.append(('tests', tests))
def output_values(self, values):
self.outputs.append(('values', values))
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment