testtools: Merge in new upstream.
[nivanova/samba-autobuild/.git] / lib / testtools / testtools / matchers.py
1 # Copyright (c) 2009-2010 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     'Is',
19     'LessThan',
20     'MatchesAll',
21     'MatchesAny',
22     'MatchesException',
23     'NotEquals',
24     'Not',
25     'Raises',
26     'raises',
27     'StartsWith',
28     ]
29
30 import doctest
31 import operator
32 from pprint import pformat
33 import sys
34
35 from testtools.compat import classtypes, _error_repr, isbaseexception
36
37
38 class Matcher(object):
39     """A pattern matcher.
40
41     A Matcher must implement match and __str__ to be used by
42     testtools.TestCase.assertThat. Matcher.match(thing) returns None when
43     thing is completely matched, and a Mismatch object otherwise.
44
45     Matchers can be useful outside of test cases, as they are simply a
46     pattern matching language expressed as objects.
47
48     testtools.matchers is inspired by hamcrest, but is pythonic rather than
49     a Java transcription.
50     """
51
52     def match(self, something):
53         """Return None if this matcher matches something, a Mismatch otherwise.
54         """
55         raise NotImplementedError(self.match)
56
57     def __str__(self):
58         """Get a sensible human representation of the matcher.
59
60         This should include the parameters given to the matcher and any
61         state that would affect the matches operation.
62         """
63         raise NotImplementedError(self.__str__)
64
65
66 class Mismatch(object):
67     """An object describing a mismatch detected by a Matcher."""
68
69     def __init__(self, description=None, details=None):
70         """Construct a `Mismatch`.
71
72         :param description: A description to use.  If not provided,
73             `Mismatch.describe` must be implemented.
74         :param details: Extra details about the mismatch.  Defaults
75             to the empty dict.
76         """
77         if description:
78             self._description = description
79         if details is None:
80             details = {}
81         self._details = details
82
83     def describe(self):
84         """Describe the mismatch.
85
86         This should be either a human-readable string or castable to a string.
87         """
88         try:
89             return self._description
90         except AttributeError:
91             raise NotImplementedError(self.describe)
92
93     def get_details(self):
94         """Get extra details about the mismatch.
95
96         This allows the mismatch to provide extra information beyond the basic
97         description, including large text or binary files, or debugging internals
98         without having to force it to fit in the output of 'describe'.
99
100         The testtools assertion assertThat will query get_details and attach
101         all its values to the test, permitting them to be reported in whatever
102         manner the test environment chooses.
103
104         :return: a dict mapping names to Content objects. name is a string to
105             name the detail, and the Content object is the detail to add
106             to the result. For more information see the API to which items from
107             this dict are passed testtools.TestCase.addDetail.
108         """
109         return getattr(self, '_details', {})
110
111     def __repr__(self):
112         return  "<testtools.matchers.Mismatch object at %x attributes=%r>" % (
113             id(self), self.__dict__)
114
115
116 class DocTestMatches(object):
117     """See if a string matches a doctest example."""
118
119     def __init__(self, example, flags=0):
120         """Create a DocTestMatches to match example.
121
122         :param example: The example to match e.g. 'foo bar baz'
123         :param flags: doctest comparison flags to match on. e.g.
124             doctest.ELLIPSIS.
125         """
126         if not example.endswith('\n'):
127             example += '\n'
128         self.want = example # required variable name by doctest.
129         self.flags = flags
130         self._checker = doctest.OutputChecker()
131
132     def __str__(self):
133         if self.flags:
134             flagstr = ", flags=%d" % self.flags
135         else:
136             flagstr = ""
137         return 'DocTestMatches(%r%s)' % (self.want, flagstr)
138
139     def _with_nl(self, actual):
140         result = str(actual)
141         if not result.endswith('\n'):
142             result += '\n'
143         return result
144
145     def match(self, actual):
146         with_nl = self._with_nl(actual)
147         if self._checker.check_output(self.want, with_nl, self.flags):
148             return None
149         return DocTestMismatch(self, with_nl)
150
151     def _describe_difference(self, with_nl):
152         return self._checker.output_difference(self, with_nl, self.flags)
153
154
155 class DocTestMismatch(Mismatch):
156     """Mismatch object for DocTestMatches."""
157
158     def __init__(self, matcher, with_nl):
159         self.matcher = matcher
160         self.with_nl = with_nl
161
162     def describe(self):
163         return self.matcher._describe_difference(self.with_nl)
164
165
166 class DoesNotStartWith(Mismatch):
167
168     def __init__(self, matchee, expected):
169         """Create a DoesNotStartWith Mismatch.
170
171         :param matchee: the string that did not match.
172         :param expected: the string that `matchee` was expected to start
173             with.
174         """
175         self.matchee = matchee
176         self.expected = expected
177
178     def describe(self):
179         return "'%s' does not start with '%s'." % (
180             self.matchee, self.expected)
181
182
183 class DoesNotEndWith(Mismatch):
184
185     def __init__(self, matchee, expected):
186         """Create a DoesNotEndWith Mismatch.
187
188         :param matchee: the string that did not match.
189         :param expected: the string that `matchee` was expected to end with.
190         """
191         self.matchee = matchee
192         self.expected = expected
193
194     def describe(self):
195         return "'%s' does not end with '%s'." % (
196             self.matchee, self.expected)
197
198
199 class _BinaryComparison(object):
200     """Matcher that compares an object to another object."""
201
202     def __init__(self, expected):
203         self.expected = expected
204
205     def __str__(self):
206         return "%s(%r)" % (self.__class__.__name__, self.expected)
207
208     def match(self, other):
209         if self.comparator(other, self.expected):
210             return None
211         return _BinaryMismatch(self.expected, self.mismatch_string, other)
212
213     def comparator(self, expected, other):
214         raise NotImplementedError(self.comparator)
215
216
217 class _BinaryMismatch(Mismatch):
218     """Two things did not match."""
219
220     def __init__(self, expected, mismatch_string, other):
221         self.expected = expected
222         self._mismatch_string = mismatch_string
223         self.other = other
224
225     def describe(self):
226         left = repr(self.expected)
227         right = repr(self.other)
228         if len(left) + len(right) > 70:
229             return "%s:\nreference = %s\nactual = %s\n" % (
230                 self._mismatch_string, pformat(self.expected),
231                 pformat(self.other))
232         else:
233             return "%s %s %s" % (left, self._mismatch_string,right)
234
235
236 class Equals(_BinaryComparison):
237     """Matches if the items are equal."""
238
239     comparator = operator.eq
240     mismatch_string = '!='
241
242
243 class NotEquals(_BinaryComparison):
244     """Matches if the items are not equal.
245
246     In most cases, this is equivalent to `Not(Equals(foo))`. The difference
247     only matters when testing `__ne__` implementations.
248     """
249
250     comparator = operator.ne
251     mismatch_string = '=='
252
253
254 class Is(_BinaryComparison):
255     """Matches if the items are identical."""
256
257     comparator = operator.is_
258     mismatch_string = 'is not'
259
260
261 class LessThan(_BinaryComparison):
262     """Matches if the item is less than the matchers reference object."""
263
264     comparator = operator.__lt__
265     mismatch_string = 'is >='
266
267
268 class MatchesAny(object):
269     """Matches if any of the matchers it is created with match."""
270
271     def __init__(self, *matchers):
272         self.matchers = matchers
273
274     def match(self, matchee):
275         results = []
276         for matcher in self.matchers:
277             mismatch = matcher.match(matchee)
278             if mismatch is None:
279                 return None
280             results.append(mismatch)
281         return MismatchesAll(results)
282
283     def __str__(self):
284         return "MatchesAny(%s)" % ', '.join([
285             str(matcher) for matcher in self.matchers])
286
287
288 class MatchesAll(object):
289     """Matches if all of the matchers it is created with match."""
290
291     def __init__(self, *matchers):
292         self.matchers = matchers
293
294     def __str__(self):
295         return 'MatchesAll(%s)' % ', '.join(map(str, self.matchers))
296
297     def match(self, matchee):
298         results = []
299         for matcher in self.matchers:
300             mismatch = matcher.match(matchee)
301             if mismatch is not None:
302                 results.append(mismatch)
303         if results:
304             return MismatchesAll(results)
305         else:
306             return None
307
308
309 class MismatchesAll(Mismatch):
310     """A mismatch with many child mismatches."""
311
312     def __init__(self, mismatches):
313         self.mismatches = mismatches
314
315     def describe(self):
316         descriptions = ["Differences: ["]
317         for mismatch in self.mismatches:
318             descriptions.append(mismatch.describe())
319         descriptions.append("]")
320         return '\n'.join(descriptions)
321
322
323 class Not(object):
324     """Inverts a matcher."""
325
326     def __init__(self, matcher):
327         self.matcher = matcher
328
329     def __str__(self):
330         return 'Not(%s)' % (self.matcher,)
331
332     def match(self, other):
333         mismatch = self.matcher.match(other)
334         if mismatch is None:
335             return MatchedUnexpectedly(self.matcher, other)
336         else:
337             return None
338
339
340 class MatchedUnexpectedly(Mismatch):
341     """A thing matched when it wasn't supposed to."""
342
343     def __init__(self, matcher, other):
344         self.matcher = matcher
345         self.other = other
346
347     def describe(self):
348         return "%r matches %s" % (self.other, self.matcher)
349
350
351 class MatchesException(Matcher):
352     """Match an exc_info tuple against an exception instance or type."""
353
354     def __init__(self, exception):
355         """Create a MatchesException that will match exc_info's for exception.
356
357         :param exception: Either an exception instance or type.
358             If an instance is given, the type and arguments of the exception
359             are checked. If a type is given only the type of the exception is
360             checked.
361         """
362         Matcher.__init__(self)
363         self.expected = exception
364         self._is_instance = type(self.expected) not in classtypes()
365
366     def match(self, other):
367         if type(other) != tuple:
368             return Mismatch('%r is not an exc_info tuple' % other)
369         expected_class = self.expected
370         if self._is_instance:
371             expected_class = expected_class.__class__
372         if not issubclass(other[0], expected_class):
373             return Mismatch('%r is not a %r' % (other[0], expected_class))
374         if self._is_instance and other[1].args != self.expected.args:
375             return Mismatch('%s has different arguments to %s.' % (
376                 _error_repr(other[1]), _error_repr(self.expected)))
377
378     def __str__(self):
379         if self._is_instance:
380             return "MatchesException(%s)" % _error_repr(self.expected)
381         return "MatchesException(%s)" % repr(self.expected)
382
383
384 class StartsWith(Matcher):
385     """Checks whether one string starts with another."""
386
387     def __init__(self, expected):
388         """Create a StartsWith Matcher.
389
390         :param expected: the string that matchees should start with.
391         """
392         self.expected = expected
393
394     def __str__(self):
395         return "Starts with '%s'." % self.expected
396
397     def match(self, matchee):
398         if not matchee.startswith(self.expected):
399             return DoesNotStartWith(matchee, self.expected)
400         return None
401
402
403 class EndsWith(Matcher):
404     """Checks whether one string starts with another."""
405
406     def __init__(self, expected):
407         """Create a EndsWith Matcher.
408
409         :param expected: the string that matchees should end with.
410         """
411         self.expected = expected
412
413     def __str__(self):
414         return "Ends with '%s'." % self.expected
415
416     def match(self, matchee):
417         if not matchee.endswith(self.expected):
418             return DoesNotEndWith(matchee, self.expected)
419         return None
420
421
422 class KeysEqual(Matcher):
423     """Checks whether a dict has particular keys."""
424
425     def __init__(self, *expected):
426         """Create a `KeysEqual` Matcher.
427
428         :param *expected: The keys the dict is expected to have.  If a dict,
429             then we use the keys of that dict, if a collection, we assume it
430             is a collection of expected keys.
431         """
432         try:
433             self.expected = expected.keys()
434         except AttributeError:
435             self.expected = list(expected)
436
437     def __str__(self):
438         return "KeysEqual(%s)" % ', '.join(map(repr, self.expected))
439
440     def match(self, matchee):
441         expected = sorted(self.expected)
442         matched = Equals(expected).match(sorted(matchee.keys()))
443         if matched:
444             return AnnotatedMismatch(
445                 'Keys not equal',
446                 _BinaryMismatch(expected, 'does not match', matchee))
447         return None
448
449
450 class Annotate(object):
451     """Annotates a matcher with a descriptive string.
452
453     Mismatches are then described as '<mismatch>: <annotation>'.
454     """
455
456     def __init__(self, annotation, matcher):
457         self.annotation = annotation
458         self.matcher = matcher
459
460     def __str__(self):
461         return 'Annotate(%r, %s)' % (self.annotation, self.matcher)
462
463     def match(self, other):
464         mismatch = self.matcher.match(other)
465         if mismatch is not None:
466             return AnnotatedMismatch(self.annotation, mismatch)
467
468
469 class AnnotatedMismatch(Mismatch):
470     """A mismatch annotated with a descriptive string."""
471
472     def __init__(self, annotation, mismatch):
473         self.annotation = annotation
474         self.mismatch = mismatch
475
476     def describe(self):
477         return '%s: %s' % (self.mismatch.describe(), self.annotation)
478
479
480 class Raises(Matcher):
481     """Match if the matchee raises an exception when called.
482
483     Exceptions which are not subclasses of Exception propogate out of the
484     Raises.match call unless they are explicitly matched.
485     """
486
487     def __init__(self, exception_matcher=None):
488         """Create a Raises matcher.
489
490         :param exception_matcher: Optional validator for the exception raised
491             by matchee. If supplied the exc_info tuple for the exception raised
492             is passed into that matcher. If no exception_matcher is supplied
493             then the simple fact of raising an exception is considered enough
494             to match on.
495         """
496         self.exception_matcher = exception_matcher
497
498     def match(self, matchee):
499         try:
500             result = matchee()
501             return Mismatch('%r returned %r' % (matchee, result))
502         # Catch all exceptions: Raises() should be able to match a
503         # KeyboardInterrupt or SystemExit.
504         except:
505             if self.exception_matcher:
506                 mismatch = self.exception_matcher.match(sys.exc_info())
507                 if not mismatch:
508                     return
509             else:
510                 mismatch = None
511             # The exception did not match, or no explicit matching logic was
512             # performed. If the exception is a non-user exception (that is, not
513             # a subclass of Exception on Python 2.5+) then propogate it.
514             if isbaseexception(sys.exc_info()[1]):
515                 raise
516             return mismatch
517
518     def __str__(self):
519         return 'Raises()'
520
521
522 def raises(exception):
523     """Make a matcher that checks that a callable raises an exception.
524
525     This is a convenience function, exactly equivalent to::
526         return Raises(MatchesException(exception))
527
528     See `Raises` and `MatchesException` for more information.
529     """
530     return Raises(MatchesException(exception))