diff options
Diffstat (limited to 'lib/testtools/matchers.py')
-rw-r--r-- | lib/testtools/matchers.py | 282 |
1 files changed, 282 insertions, 0 deletions
diff --git a/lib/testtools/matchers.py b/lib/testtools/matchers.py new file mode 100644 index 0000000000..039c84b7c7 --- /dev/null +++ b/lib/testtools/matchers.py @@ -0,0 +1,282 @@ +# 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__ = [ + 'Annotate', + 'DocTestMatches', + 'Equals', + 'MatchesAll', + 'MatchesAny', + 'NotEquals', + 'Not', + ] + +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 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.""" + + 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 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.""" + + 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) + + +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) + + +class Annotate: + """Annotates a matcher with a descriptive string. + + Mismatches are then described as '<mismatch>: <annotation>'. + """ + + def __init__(self, annotation, matcher): + self.annotation = annotation + self.matcher = matcher + + def __str__(self): + return 'Annotate(%r, %s)' % (self.annotation, self.matcher) + + def match(self, other): + mismatch = self.matcher.match(other) + if mismatch is not None: + return AnnotatedMismatch(self.annotation, mismatch) + + +class AnnotatedMismatch: + """A mismatch annotated with a descriptive string.""" + + def __init__(self, annotation, mismatch): + self.annotation = annotation + self.mismatch = mismatch + + def describe(self): + return '%s: %s' % (self.mismatch.describe(), self.annotation) |