From 2ec5792a4ba0cefa079a6d7e1b0ec2472151e794 Mon Sep 17 00:00:00 2001 From: Jelmer Vernooij Date: Sat, 16 Jan 2010 19:56:21 +1300 Subject: subunit/testtools: Include newer version. --- lib/subunit/python/subunit/__init__.py | 17 ++++- lib/subunit/python/subunit/run.py | 5 +- .../python/subunit/tests/test_test_protocol.py | 4 ++ lib/subunit/python/testtools/content.py | 2 +- lib/subunit/python/testtools/content_type.py | 2 +- lib/subunit/python/testtools/matchers.py | 82 ++++++++++++++++++++++ lib/subunit/python/testtools/testcase.py | 50 +++++++++---- lib/subunit/python/testtools/tests/__init__.py | 2 + .../python/testtools/tests/test_matchers.py | 45 ++++++++++++ .../python/testtools/tests/test_testtools.py | 16 ++++- lib/subunit/python/testtools/utils.py | 2 +- 11 files changed, 205 insertions(+), 22 deletions(-) (limited to 'lib/subunit') 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: -- cgit