diff options
Diffstat (limited to 'lib/subunit/python/testtools/matchers.py')
-rw-r--r-- | lib/subunit/python/testtools/matchers.py | 169 |
1 files changed, 169 insertions, 0 deletions
diff --git a/lib/subunit/python/testtools/matchers.py b/lib/subunit/python/testtools/matchers.py new file mode 100644 index 0000000000..947ef601b3 --- /dev/null +++ b/lib/subunit/python/testtools/matchers.py @@ -0,0 +1,169 @@ +# Copyright (c) 2009 Jonathan M. Lange. See LICENSE for details. + +"""Matchers, a way to express complex assertions outside the testcase. + +Inspired by 'hamcrest'. + +Matcher provides the abstract API that all matchers need to implement. + +Bundled matchers are listed in __all__: a list can be obtained by running +$ python -c 'import testtools.matchers; print testtools.matchers.__all__' +""" + +__metaclass__ = type +__all__ = [ + 'DocTestMatches', + 'Equals', + 'MatchesAny', + ] + +import doctest + + +class Matcher: + """A pattern matcher. + + A Matcher must implement match and __str__ to be used by + testtools.TestCase.assertThat. Matcher.match(thing) returns None when + thing is completely matched, and a Mismatch object otherwise. + + Matchers can be useful outside of test cases, as they are simply a + pattern matching language expressed as objects. + + testtools.matchers is inspired by hamcrest, but is pythonic rather than + a Java transcription. + """ + + def match(self, something): + """Return None if this matcher matches something, a Mismatch otherwise. + """ + raise NotImplementedError(self.match) + + def __str__(self): + """Get a sensible human representation of the matcher. + + This should include the parameters given to the matcher and any + state that would affect the matches operation. + """ + raise NotImplementedError(self.__str__) + + +class Mismatch: + """An object describing a mismatch detected by a Matcher.""" + + def describe(self): + """Describe the mismatch. + + This should be either a human-readable string or castable to a string. + """ + raise NotImplementedError(self.describe_difference) + + +class DocTestMatches: + """See if a string matches a doctest example.""" + + def __init__(self, example, flags=0): + """Create a DocTestMatches to match example. + + :param example: The example to match e.g. 'foo bar baz' + :param flags: doctest comparison flags to match on. e.g. + doctest.ELLIPSIS. + """ + if not example.endswith('\n'): + example += '\n' + self.want = example # required variable name by doctest. + self.flags = flags + self._checker = doctest.OutputChecker() + + def __str__(self): + if self.flags: + flagstr = ", flags=%d" % self.flags + else: + flagstr = "" + return 'DocTestMatches(%r%s)' % (self.want, flagstr) + + def _with_nl(self, actual): + result = str(actual) + if not result.endswith('\n'): + result += '\n' + return result + + def match(self, actual): + with_nl = self._with_nl(actual) + if self._checker.check_output(self.want, with_nl, self.flags): + return None + return DocTestMismatch(self, with_nl) + + def _describe_difference(self, with_nl): + return self._checker.output_difference(self, with_nl, self.flags) + + +class DocTestMismatch: + """Mismatch object for DocTestMatches.""" + + def __init__(self, matcher, with_nl): + self.matcher = matcher + self.with_nl = with_nl + + def describe(self): + return self.matcher._describe_difference(self.with_nl) + + +class Equals: + """Matches if the items are equal.""" + + def __init__(self, expected): + self.expected = expected + + def match(self, other): + if self.expected == other: + return None + return EqualsMismatch(self.expected, other) + + def __str__(self): + return "Equals(%r)" % self.expected + + +class EqualsMismatch: + """Two things differed.""" + + 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.""" + + def __init__(self, *matchers): + self.matchers = matchers + + def match(self, matchee): + results = [] + for matcher in self.matchers: + mismatch = matcher.match(matchee) + if mismatch is None: + return None + results.append(mismatch) + return MismatchesAll(results) + + def __str__(self): + return "MatchesAny(%s)" % ', '.join([ + str(matcher) for matcher in self.matchers]) + + +class MismatchesAll: + """A mismatch with many child mismatches.""" + + def __init__(self, mismatches): + self.mismatches = mismatches + + def describe(self): + descriptions = ["Differences: ["] + for mismatch in self.mismatches: + descriptions.append(mismatch.describe()) + descriptions.append("]\n") + return '\n'.join(descriptions) |