Merge lp:~allenap/maas/text-equals-matcher into lp:~maas-committers/maas/trunk

Proposed by Gavin Panella
Status: Merged
Approved by: Gavin Panella
Approved revision: no longer in the source branch.
Merged at revision: 5551
Proposed branch: lp:~allenap/maas/text-equals-matcher
Merge into: lp:~maas-committers/maas/trunk
Diff against target: 154 lines (+106/-0)
2 files modified
src/maastesting/matchers.py (+38/-0)
src/maastesting/tests/test_matchers.py (+68/-0)
To merge this branch: bzr merge lp:~allenap/maas/text-equals-matcher
Reviewer Review Type Date Requested Status
Lee Trager (community) Approve
Review via email: mp+310545@code.launchpad.net

Commit message

TextEquals matcher which displays a diff of differences when there's a mismatch.

This is especially useful when dealing with larger blocks of text.

Description of the change

I wrote this while working on another branch. I didn't need it there in the end, but it seems useful.

To post a comment you must log in.
Revision history for this message
Lee Trager (ltrager) wrote :

Thanks for writing this, should make debug of tests much easier!

review: Approve

Preview Diff

[H/L] Next/Prev Comment, [J/K] Next/Prev File, [N/P] Next/Prev Hunk
1=== modified file 'src/maastesting/matchers.py'
2--- src/maastesting/matchers.py 2016-03-28 13:54:47 +0000
3+++ src/maastesting/matchers.py 2016-11-10 15:36:36 +0000
4@@ -21,12 +21,18 @@
5 'MockCallsMatch',
6 'MockNotCalled',
7 'Provides',
8+ 'TextEquals',
9 ]
10
11+from difflib import ndiff
12 import doctest
13 from functools import partial
14
15 from testtools import matchers
16+from testtools.content import (
17+ Content,
18+ UTF8_TEXT,
19+)
20 from testtools.matchers import (
21 AfterPreprocessing,
22 Annotate,
23@@ -427,3 +433,35 @@
24 "File at path exists and its contents (encoded as %s) "
25 "match %s" % (self.encoding, self.matcher)
26 )
27+
28+
29+class TextEquals(Matcher):
30+ """Compares two blocks of text for equality.
31+
32+ This differs from `Equals` in that is calculates an `ndiff` between the
33+ two which will be included in the test results, making this especially
34+ appropriate for longer pieces of text.
35+ """
36+
37+ def __init__(self, expected):
38+ super(TextEquals, self).__init__()
39+ self.expected = expected
40+
41+ def match(self, observed):
42+ if observed != self.expected:
43+ diff = self._diff(self.expected, observed)
44+ return Mismatch(
45+ "Observed text does not match expectations; see diff.",
46+ {"diff": Content(UTF8_TEXT, lambda: map(str.encode, diff))})
47+
48+ @staticmethod
49+ def _diff(expected, observed):
50+ # ndiff works better when lines consistently end with newlines.
51+ a = str(expected).splitlines(keepends=False)
52+ a = list(line + "\n" for line in a)
53+ b = str(observed).splitlines(keepends=False)
54+ b = list(line + "\n" for line in b)
55+
56+ yield "--- expected\n"
57+ yield "+++ observed\n"
58+ yield from ndiff(a, b)
59
60=== modified file 'src/maastesting/tests/test_matchers.py'
61--- src/maastesting/tests/test_matchers.py 2016-06-22 17:03:02 +0000
62+++ src/maastesting/tests/test_matchers.py 2016-11-10 15:36:36 +0000
63@@ -5,6 +5,7 @@
64
65 __all__ = []
66
67+from textwrap import dedent
68 from unittest.mock import (
69 call,
70 create_autospec,
71@@ -31,12 +32,16 @@
72 MockCalledWith,
73 MockCallsMatch,
74 MockNotCalled,
75+ TextEquals,
76 )
77 from maastesting.testcase import MAASTestCase
78+from testtools.content import Content
79 from testtools.matchers import (
80 AfterPreprocessing,
81 Contains,
82+ ContainsDict,
83 Equals,
84+ IsInstance,
85 MatchesStructure,
86 Mismatch,
87 )
88@@ -499,3 +504,66 @@
89 "File at path exists and its contents (encoded as %s) "
90 "match %s" % (encoding, contents_matcher),
91 FileContains(matcher=contents_matcher, encoding=encoding))
92+
93+
94+class TestTextEquals(MAASTestCase, MockTestMixin):
95+ """Tests for the `TextEquals` matcher."""
96+
97+ def test_matches_equal_strings(self):
98+ contents = factory.make_string()
99+ self.assertThat(contents, TextEquals(contents))
100+
101+ def test_matches_equal_things(self):
102+ contents = object()
103+ self.assertThat(contents, TextEquals(contents))
104+
105+ def test_describes_mismatch(self):
106+ self.assertMismatch(
107+ TextEquals("foo").match("bar"),
108+ "Observed text does not match expectations; see diff.")
109+
110+ def test_includes_diff_of_mismatch(self):
111+ expected = "A line of text that differs at the end."
112+ observed = "A line of text that differs at THE end."
113+ mismatch = TextEquals(expected).match(observed)
114+ details = mismatch.get_details()
115+ self.assertThat(details, ContainsDict({"diff": IsInstance(Content)}))
116+ self.assertThat(details["diff"].as_text(), Equals(dedent("""\
117+ --- expected
118+ +++ observed
119+ - A line of text that differs at the end.
120+ ? ^^^
121+ + A line of text that differs at THE end.
122+ ? ^^^
123+ """)))
124+
125+ def test_includes_diff_of_mismatch_multiple_lines(self):
126+ expected = "A line of text that differs\nat the end of the 2nd line."
127+ observed = "A line of text that differs\nat the end of the 2ND line."
128+ mismatch = TextEquals(expected).match(observed)
129+ details = mismatch.get_details()
130+ self.assertThat(details, ContainsDict({"diff": IsInstance(Content)}))
131+ self.assertThat(details["diff"].as_text(), Equals(dedent("""\
132+ --- expected
133+ +++ observed
134+ A line of text that differs
135+ - at the end of the 2nd line.
136+ ? ^^
137+ + at the end of the 2ND line.
138+ ? ^^
139+ """)))
140+
141+ def test_includes_diff_of_coerced_arguments(self):
142+ expected = "A tuple", "that differs", "here."
143+ observed = "A tuple", "that differs", "HERE."
144+ mismatch = TextEquals(expected).match(observed)
145+ details = mismatch.get_details()
146+ self.assertThat(details, ContainsDict({"diff": IsInstance(Content)}))
147+ self.assertThat(details["diff"].as_text(), Equals(dedent("""\
148+ --- expected
149+ +++ observed
150+ - ('A tuple', 'that differs', 'here.')
151+ ? ^^^^
152+ + ('A tuple', 'that differs', 'HERE.')
153+ ? ^^^^
154+ """)))