summaryrefslogtreecommitdiff
path: root/lib
diff options
context:
space:
mode:
Diffstat (limited to 'lib')
-rw-r--r--lib/subunit/python/subunit/__init__.py17
-rwxr-xr-xlib/subunit/python/subunit/run.py5
-rw-r--r--lib/subunit/python/subunit/tests/test_test_protocol.py4
-rw-r--r--lib/subunit/python/testtools/content.py2
-rw-r--r--lib/subunit/python/testtools/content_type.py2
-rw-r--r--lib/subunit/python/testtools/matchers.py82
-rw-r--r--lib/subunit/python/testtools/testcase.py50
-rw-r--r--lib/subunit/python/testtools/tests/__init__.py2
-rw-r--r--lib/subunit/python/testtools/tests/test_matchers.py45
-rw-r--r--lib/subunit/python/testtools/tests/test_testtools.py16
-rw-r--r--lib/subunit/python/testtools/utils.py2
11 files changed, 205 insertions, 22 deletions
diff --git a/lib/subunit/python/subunit/__init__.py b/lib/subunit/python/subunit/__init__.py
index 6e8df90317..6b65ae42dc 100644
--- a/lib/subunit/python/subunit/__init__.py
+++ b/lib/subunit/python/subunit/__init__.py
@@ -213,10 +213,10 @@ class _ParserState(object):
def lineReceived(self, line):
"""a line has been received."""
parts = line.split(None, 1)
- if len(parts) == 2:
+ if len(parts) == 2 and line.startswith(parts[0]):
cmd, rest = parts
offset = len(cmd) + 1
- cmd = cmd.strip(':')
+ cmd = cmd.rstrip(':')
if cmd in ('test', 'testing'):
self.startTest(offset, line)
elif cmd == 'error':
@@ -1111,3 +1111,16 @@ class TestResultStats(unittest.TestResult):
def wasSuccessful(self):
"""Tells whether or not this result was a success"""
return self.failed_tests == 0
+
+
+def get_default_formatter():
+ """Obtain the default formatter to write to.
+
+ :return: A file-like object.
+ """
+ formatter = os.getenv("SUBUNIT_FORMATTER")
+ if formatter:
+ return os.popen(formatter, "w")
+ else:
+ return sys.stdout
+
diff --git a/lib/subunit/python/subunit/run.py b/lib/subunit/python/subunit/run.py
index 2b90791d69..01c0b0e9e6 100755
--- a/lib/subunit/python/subunit/run.py
+++ b/lib/subunit/python/subunit/run.py
@@ -22,7 +22,7 @@
import sys
-from subunit import TestProtocolClient
+from subunit import TestProtocolClient, get_default_formatter
class SubunitTestRunner(object):
@@ -41,6 +41,7 @@ if __name__ == '__main__':
from unittest import TestProgram
parser = optparse.OptionParser(__doc__)
args = parser.parse_args()[1]
- runner = SubunitTestRunner()
+ stream = get_default_formatter()
+ runner = SubunitTestRunner(stream)
program = TestProgram(module=None, argv=[sys.argv[0]] + args,
testRunner=runner)
diff --git a/lib/subunit/python/subunit/tests/test_test_protocol.py b/lib/subunit/python/subunit/tests/test_test_protocol.py
index 9e9db18163..f10380b09b 100644
--- a/lib/subunit/python/subunit/tests/test_test_protocol.py
+++ b/lib/subunit/python/subunit/tests/test_test_protocol.py
@@ -124,6 +124,10 @@ class TestTestProtocolServerStartTest(unittest.TestCase):
self.assertEqual(self.client._events,
[('startTest', subunit.RemotedTestCase("old mcdonald"))])
+ def test_indented_test_colon_ignored(self):
+ self.protocol.lineReceived(" test: old mcdonald\n")
+ self.assertEqual([], self.client._events)
+
def test_start_testing_colon(self):
self.protocol.lineReceived("testing: old mcdonald\n")
self.assertEqual(self.client._events,
diff --git a/lib/subunit/python/testtools/content.py b/lib/subunit/python/testtools/content.py
index 00c782347b..353e3f0f46 100644
--- a/lib/subunit/python/testtools/content.py
+++ b/lib/subunit/python/testtools/content.py
@@ -44,7 +44,7 @@ class Content(object):
no charset parameter is present in the MIME type. (This is somewhat
arbitrary, but consistent with RFC2617 3.7.1).
- :raises: ValueError If the content type is not text/*.
+ :raises ValueError: If the content type is not text/\*.
"""
if self.content_type.type != "text":
raise ValueError("Not a text type %r" % self.content_type)
diff --git a/lib/subunit/python/testtools/content_type.py b/lib/subunit/python/testtools/content_type.py
index e70fa76ec8..aded81b732 100644
--- a/lib/subunit/python/testtools/content_type.py
+++ b/lib/subunit/python/testtools/content_type.py
@@ -9,7 +9,7 @@ class ContentType(object):
:ivar type: The primary type, e.g. "text" or "application"
:ivar subtype: The subtype, e.g. "plain" or "octet-stream"
:ivar parameters: A dict of additional parameters specific to the
- content type.
+ content type.
"""
def __init__(self, primary_type, sub_type, parameters=None):
diff --git a/lib/subunit/python/testtools/matchers.py b/lib/subunit/python/testtools/matchers.py
index 947ef601b3..244daceb7f 100644
--- a/lib/subunit/python/testtools/matchers.py
+++ b/lib/subunit/python/testtools/matchers.py
@@ -14,7 +14,10 @@ __metaclass__ = type
__all__ = [
'DocTestMatches',
'Equals',
+ 'MatchesAll',
'MatchesAny',
+ 'NotEquals',
+ 'Not',
]
import doctest
@@ -135,6 +138,36 @@ class EqualsMismatch:
return "%r != %r" % (self.expected, self.other)
+class NotEquals:
+ """Matches if the items are not equal.
+
+ In most cases, this is equivalent to `Not(Equals(foo))`. The difference
+ only matters when testing `__ne__` implementations.
+ """
+
+ def __init__(self, expected):
+ self.expected = expected
+
+ def __str__(self):
+ return 'NotEquals(%r)' % (self.expected,)
+
+ def match(self, other):
+ if self.expected != other:
+ return None
+ return NotEqualsMismatch(self.expected, other)
+
+
+class NotEqualsMismatch:
+ """Two things are the same."""
+
+ def __init__(self, expected, other):
+ self.expected = expected
+ self.other = other
+
+ def describe(self):
+ return '%r == %r' % (self.expected, self.other)
+
+
class MatchesAny:
"""Matches if any of the matchers it is created with match."""
@@ -155,6 +188,27 @@ class MatchesAny:
str(matcher) for matcher in self.matchers])
+class MatchesAll:
+ """Matches if all of the matchers it is created with match."""
+
+ def __init__(self, *matchers):
+ self.matchers = matchers
+
+ def __str__(self):
+ return 'MatchesAll(%s)' % ', '.join(map(str, self.matchers))
+
+ def match(self, matchee):
+ results = []
+ for matcher in self.matchers:
+ mismatch = matcher.match(matchee)
+ if mismatch is not None:
+ results.append(mismatch)
+ if results:
+ return MismatchesAll(results)
+ else:
+ return None
+
+
class MismatchesAll:
"""A mismatch with many child mismatches."""
@@ -167,3 +221,31 @@ class MismatchesAll:
descriptions.append(mismatch.describe())
descriptions.append("]\n")
return '\n'.join(descriptions)
+
+
+class Not:
+ """Inverts a matcher."""
+
+ def __init__(self, matcher):
+ self.matcher = matcher
+
+ def __str__(self):
+ return 'Not(%s)' % (self.matcher,)
+
+ def match(self, other):
+ mismatch = self.matcher.match(other)
+ if mismatch is None:
+ return MatchedUnexpectedly(self.matcher, other)
+ else:
+ return None
+
+
+class MatchedUnexpectedly:
+ """A thing matched when it wasn't supposed to."""
+
+ def __init__(self, matcher, other):
+ self.matcher = matcher
+ self.other = other
+
+ def describe(self):
+ return "%r matches %s" % (self.other, self.matcher)
diff --git a/lib/subunit/python/testtools/testcase.py b/lib/subunit/python/testtools/testcase.py
index a1d822ed47..fd70141e6d 100644
--- a/lib/subunit/python/testtools/testcase.py
+++ b/lib/subunit/python/testtools/testcase.py
@@ -203,15 +203,26 @@ class TestCase(unittest.TestCase):
self.assertTrue(
needle in haystack, '%r not in %r' % (needle, haystack))
- def assertIs(self, expected, observed):
- """Assert that `expected` is `observed`."""
+ def assertIs(self, expected, observed, message=''):
+ """Assert that 'expected' is 'observed'.
+
+ :param expected: The expected value.
+ :param observed: The observed value.
+ :param message: An optional message describing the error.
+ """
+ if message:
+ message = ': ' + message
self.assertTrue(
- expected is observed, '%r is not %r' % (expected, observed))
+ expected is observed,
+ '%r is not %r%s' % (expected, observed, message))
- def assertIsNot(self, expected, observed):
- """Assert that `expected` is not `observed`."""
+ def assertIsNot(self, expected, observed, message=''):
+ """Assert that 'expected' is not 'observed'."""
+ if message:
+ message = ': ' + message
self.assertTrue(
- expected is not observed, '%r is %r' % (expected, observed))
+ expected is not observed,
+ '%r is %r%s' % (expected, observed, message))
def assertNotIn(self, needle, haystack):
"""Assert that needle is not in haystack."""
@@ -358,7 +369,11 @@ class TestCase(unittest.TestCase):
"""
self.setUp()
if not self.__setup_called:
- raise ValueError("setUp was not called")
+ raise ValueError(
+ "TestCase.setUp was not called. Have you upcalled all the "
+ "way up the hierarchy from your setUp? e.g. Call "
+ "super(%s, self).setUp() from your setUp()."
+ % self.__class__.__name__)
def _run_teardown(self, result):
"""Run the tearDown function for this test.
@@ -369,7 +384,11 @@ class TestCase(unittest.TestCase):
"""
self.tearDown()
if not self.__teardown_called:
- raise ValueError("teardown was not called")
+ raise ValueError(
+ "TestCase.tearDown was not called. Have you upcalled all the "
+ "way up the hierarchy from your tearDown? e.g. Call "
+ "super(%s, self).tearDown() from your tearDown()."
+ % self.__class__.__name__)
def _run_test_method(self, result):
"""Run the test method for this test.
@@ -395,14 +414,19 @@ class TestCase(unittest.TestCase):
self.__teardown_called = True
-# Python 2.4 did not know how to deep copy functions.
-if types.FunctionType not in copy._deepcopy_dispatch:
- copy._deepcopy_dispatch[types.FunctionType] = copy._deepcopy_atomic
+# Python 2.4 did not know how to copy functions.
+if types.FunctionType not in copy._copy_dispatch:
+ copy._copy_dispatch[types.FunctionType] = copy._copy_immutable
+
def clone_test_with_new_id(test, new_id):
- """Copy a TestCase, and give the copied test a new id."""
- newTest = copy.deepcopy(test)
+ """Copy a TestCase, and give the copied test a new id.
+
+ This is only expected to be used on tests that have been constructed but
+ not executed.
+ """
+ newTest = copy.copy(test)
newTest.id = lambda: new_id
return newTest
diff --git a/lib/subunit/python/testtools/tests/__init__.py b/lib/subunit/python/testtools/tests/__init__.py
index e1d1148d5c..2cceba91e2 100644
--- a/lib/subunit/python/testtools/tests/__init__.py
+++ b/lib/subunit/python/testtools/tests/__init__.py
@@ -1,3 +1,5 @@
+"""Tests for testtools itself."""
+
# See README for copyright and licensing details.
import unittest
diff --git a/lib/subunit/python/testtools/tests/test_matchers.py b/lib/subunit/python/testtools/tests/test_matchers.py
index a9f4b245eb..d5fd8bab3b 100644
--- a/lib/subunit/python/testtools/tests/test_matchers.py
+++ b/lib/subunit/python/testtools/tests/test_matchers.py
@@ -12,6 +12,9 @@ from testtools.matchers import (
Equals,
DocTestMatches,
MatchesAny,
+ MatchesAll,
+ Not,
+ NotEquals,
)
@@ -81,6 +84,31 @@ class TestEqualsInterface(TestCase, TestMatchersInterface):
describe_examples = [("1 != 2", 2, Equals(1))]
+class TestNotEqualsInterface(TestCase, TestMatchersInterface):
+
+ matches_matcher = NotEquals(1)
+ matches_matches = [2]
+ matches_mismatches = [1]
+
+ str_examples = [
+ ("NotEquals(1)", NotEquals(1)), ("NotEquals('1')", NotEquals('1'))]
+
+ describe_examples = [("1 == 1", 1, NotEquals(1))]
+
+
+class TestNotInterface(TestCase, TestMatchersInterface):
+
+ matches_matcher = Not(Equals(1))
+ matches_matches = [2]
+ matches_mismatches = [1]
+
+ str_examples = [
+ ("Not(Equals(1))", Not(Equals(1))),
+ ("Not(Equals('1'))", Not(Equals('1')))]
+
+ describe_examples = [('1 matches Equals(1)', 1, Not(Equals(1)))]
+
+
class TestMatchersAnyInterface(TestCase, TestMatchersInterface):
matches_matcher = MatchesAny(DocTestMatches("1"), DocTestMatches("2"))
@@ -108,6 +136,23 @@ Got:
"3", MatchesAny(DocTestMatches("1"), DocTestMatches("2")))]
+class TestMatchesAllInterface(TestCase, TestMatchersInterface):
+
+ matches_matcher = MatchesAll(NotEquals(1), NotEquals(2))
+ matches_matches = [3, 4]
+ matches_mismatches = [1, 2]
+
+ str_examples = [
+ ("MatchesAll(NotEquals(1), NotEquals(2))",
+ MatchesAll(NotEquals(1), NotEquals(2)))]
+
+ describe_examples = [("""Differences: [
+1 == 1
+]
+""",
+ 1, MatchesAll(NotEquals(1), NotEquals(2)))]
+
+
def test_suite():
from unittest import TestLoader
return TestLoader().loadTestsFromName(__name__)
diff --git a/lib/subunit/python/testtools/tests/test_testtools.py b/lib/subunit/python/testtools/tests/test_testtools.py
index 8cd90de6fe..af1fd794c3 100644
--- a/lib/subunit/python/testtools/tests/test_testtools.py
+++ b/lib/subunit/python/testtools/tests/test_testtools.py
@@ -223,6 +223,12 @@ class TestAssertions(TestCase):
self.assertFails('None is not 42', self.assertIs, None, 42)
self.assertFails('[42] is not [42]', self.assertIs, [42], [42])
+ def test_assertIs_fails_with_message(self):
+ # assertIs raises assertion errors if one object is not identical to
+ # another, and includes a user-supplied message, if it's provided.
+ self.assertFails(
+ 'None is not 42: foo bar', self.assertIs, None, 42, 'foo bar')
+
def test_assertIsNot(self):
# assertIsNot asserts that an object is not identical to another
# object.
@@ -238,6 +244,12 @@ class TestAssertions(TestCase):
self.assertFails(
'[42] is [42]', self.assertIsNot, some_list, some_list)
+ def test_assertIsNot_fails_with_message(self):
+ # assertIsNot raises assertion errors if one object is identical to
+ # another, and includes a user-supplied message if it's provided.
+ self.assertFails(
+ 'None is None: foo bar', self.assertIsNot, None, None, "foo bar")
+
def test_assertThat_matches_clean(self):
class Matcher:
def match(self, foo):
@@ -303,12 +315,12 @@ class TestAddCleanup(TestCase):
self.assertEqual(messages, [call[0] for call in self._result_calls])
def assertTestLogEqual(self, messages):
- """Assert that the call log equals `messages`."""
+ """Assert that the call log equals 'messages'."""
case = self._result_calls[0][1]
self.assertEqual(messages, case._calls)
def logAppender(self, message):
- """A cleanup that appends `message` to the tests log.
+ """A cleanup that appends 'message' to the tests log.
Cleanups are callables that are added to a test by addCleanup. To
verify that our cleanups run in the right order, we add strings to a
diff --git a/lib/subunit/python/testtools/utils.py b/lib/subunit/python/testtools/utils.py
index 325572297b..c0845b610c 100644
--- a/lib/subunit/python/testtools/utils.py
+++ b/lib/subunit/python/testtools/utils.py
@@ -28,7 +28,7 @@ else:
def iterate_tests(test_suite_or_case):
- """Iterate through all of the test cases in `test_suite_or_case`."""
+ """Iterate through all of the test cases in 'test_suite_or_case'."""
try:
suite = iter(test_suite_or_case)
except TypeError: