subunit: Update to newer upstream version.
[samba.git] / lib / subunit / python / testtools / matchers.py
1 # Copyright (c) 2009 Jonathan M. Lange. See LICENSE for details.
2
3 """Matchers, a way to express complex assertions outside the testcase.
4
5 Inspired by 'hamcrest'.
6
7 Matcher provides the abstract API that all matchers need to implement.
8
9 Bundled matchers are listed in __all__: a list can be obtained by running
10 $ python -c 'import testtools.matchers; print testtools.matchers.__all__'
11 """
12
13 __metaclass__ = type
14 __all__ = [
15     'Annotate',
16     'DocTestMatches',
17     'Equals',
18     'MatchesAll',
19     'MatchesAny',
20     'NotEquals',
21     'Not',
22     ]
23
24 import doctest
25
26
27 class Matcher:
28     """A pattern matcher.
29
30     A Matcher must implement match and __str__ to be used by
31     testtools.TestCase.assertThat. Matcher.match(thing) returns None when
32     thing is completely matched, and a Mismatch object otherwise.
33
34     Matchers can be useful outside of test cases, as they are simply a
35     pattern matching language expressed as objects.
36
37     testtools.matchers is inspired by hamcrest, but is pythonic rather than
38     a Java transcription.
39     """
40
41     def match(self, something):
42         """Return None if this matcher matches something, a Mismatch otherwise.
43         """
44         raise NotImplementedError(self.match)
45
46     def __str__(self):
47         """Get a sensible human representation of the matcher.
48
49         This should include the parameters given to the matcher and any
50         state that would affect the matches operation.
51         """
52         raise NotImplementedError(self.__str__)
53
54
55 class Mismatch:
56     """An object describing a mismatch detected by a Matcher."""
57
58     def describe(self):
59         """Describe the mismatch.
60
61         This should be either a human-readable string or castable to a string.
62         """
63         raise NotImplementedError(self.describe_difference)
64
65
66 class DocTestMatches:
67     """See if a string matches a doctest example."""
68
69     def __init__(self, example, flags=0):
70         """Create a DocTestMatches to match example.
71
72         :param example: The example to match e.g. 'foo bar baz'
73         :param flags: doctest comparison flags to match on. e.g.
74             doctest.ELLIPSIS.
75         """
76         if not example.endswith('\n'):
77             example += '\n'
78         self.want = example # required variable name by doctest.
79         self.flags = flags
80         self._checker = doctest.OutputChecker()
81
82     def __str__(self):
83         if self.flags:
84             flagstr = ", flags=%d" % self.flags
85         else:
86             flagstr = ""
87         return 'DocTestMatches(%r%s)' % (self.want, flagstr)
88
89     def _with_nl(self, actual):
90         result = str(actual)
91         if not result.endswith('\n'):
92             result += '\n'
93         return result
94
95     def match(self, actual):
96         with_nl = self._with_nl(actual)
97         if self._checker.check_output(self.want, with_nl, self.flags):
98             return None
99         return DocTestMismatch(self, with_nl)
100
101     def _describe_difference(self, with_nl):
102         return self._checker.output_difference(self, with_nl, self.flags)
103
104
105 class DocTestMismatch:
106     """Mismatch object for DocTestMatches."""
107
108     def __init__(self, matcher, with_nl):
109         self.matcher = matcher
110         self.with_nl = with_nl
111
112     def describe(self):
113         return self.matcher._describe_difference(self.with_nl)
114
115
116 class Equals:
117     """Matches if the items are equal."""
118
119     def __init__(self, expected):
120         self.expected = expected
121
122     def match(self, other):
123         if self.expected == other:
124             return None
125         return EqualsMismatch(self.expected, other)
126
127     def __str__(self):
128         return "Equals(%r)" % self.expected
129
130
131 class EqualsMismatch:
132     """Two things differed."""
133
134     def __init__(self, expected, other):
135         self.expected = expected
136         self.other = other
137
138     def describe(self):
139         return "%r != %r" % (self.expected, self.other)
140
141
142 class NotEquals:
143     """Matches if the items are not equal.
144
145     In most cases, this is equivalent to `Not(Equals(foo))`. The difference
146     only matters when testing `__ne__` implementations.
147     """
148
149     def __init__(self, expected):
150         self.expected = expected
151
152     def __str__(self):
153         return 'NotEquals(%r)' % (self.expected,)
154
155     def match(self, other):
156         if self.expected != other:
157             return None
158         return NotEqualsMismatch(self.expected, other)
159
160
161 class NotEqualsMismatch:
162     """Two things are the same."""
163
164     def __init__(self, expected, other):
165         self.expected = expected
166         self.other = other
167
168     def describe(self):
169         return '%r == %r' % (self.expected, self.other)
170
171
172 class MatchesAny:
173     """Matches if any of the matchers it is created with match."""
174
175     def __init__(self, *matchers):
176         self.matchers = matchers
177
178     def match(self, matchee):
179         results = []
180         for matcher in self.matchers:
181             mismatch = matcher.match(matchee)
182             if mismatch is None:
183                 return None
184             results.append(mismatch)
185         return MismatchesAll(results)
186
187     def __str__(self):
188         return "MatchesAny(%s)" % ', '.join([
189             str(matcher) for matcher in self.matchers])
190
191
192 class MatchesAll:
193     """Matches if all of the matchers it is created with match."""
194
195     def __init__(self, *matchers):
196         self.matchers = matchers
197
198     def __str__(self):
199         return 'MatchesAll(%s)' % ', '.join(map(str, self.matchers))
200
201     def match(self, matchee):
202         results = []
203         for matcher in self.matchers:
204             mismatch = matcher.match(matchee)
205             if mismatch is not None:
206                 results.append(mismatch)
207         if results:
208             return MismatchesAll(results)
209         else:
210             return None
211
212
213 class MismatchesAll:
214     """A mismatch with many child mismatches."""
215
216     def __init__(self, mismatches):
217         self.mismatches = mismatches
218
219     def describe(self):
220         descriptions = ["Differences: ["]
221         for mismatch in self.mismatches:
222             descriptions.append(mismatch.describe())
223         descriptions.append("]\n")
224         return '\n'.join(descriptions)
225
226
227 class Not:
228     """Inverts a matcher."""
229
230     def __init__(self, matcher):
231         self.matcher = matcher
232
233     def __str__(self):
234         return 'Not(%s)' % (self.matcher,)
235
236     def match(self, other):
237         mismatch = self.matcher.match(other)
238         if mismatch is None:
239             return MatchedUnexpectedly(self.matcher, other)
240         else:
241             return None
242
243
244 class MatchedUnexpectedly:
245     """A thing matched when it wasn't supposed to."""
246
247     def __init__(self, matcher, other):
248         self.matcher = matcher
249         self.other = other
250
251     def describe(self):
252         return "%r matches %s" % (self.other, self.matcher)
253
254
255 class Annotate:
256     """Annotates a matcher with a descriptive string.
257
258     Mismatches are then described as '<mismatch>: <annotation>'.
259     """
260
261     def __init__(self, annotation, matcher):
262         self.annotation = annotation
263         self.matcher = matcher
264
265     def __str__(self):
266         return 'Annotate(%r, %s)' % (self.annotation, self.matcher)
267
268     def match(self, other):
269         mismatch = self.matcher.match(other)
270         if mismatch is not None:
271             return AnnotatedMismatch(self.annotation, mismatch)
272
273
274 class AnnotatedMismatch:
275     """A mismatch annotated with a descriptive string."""
276
277     def __init__(self, annotation, mismatch):
278         self.annotation = annotation
279         self.mismatch = mismatch
280
281     def describe(self):
282         return '%s: %s' % (self.mismatch.describe(), self.annotation)