~landscape/zope3/trunk

« back to all changes in this revision

Viewing changes to src/zope/testing/renormalizing.py

  • Committer: Sidnei da Silva
  • Date: 2010-06-10 17:42:38 UTC
  • Revision ID: sidnei.da.silva@canonical.com-20100610174238-m3qguu042koqiutl
- Update to latest zope.testing

Show diffs side-by-side

added added

removed removed

Lines of Context:
1
 
##############################################################################
2
 
#
3
 
# Copyright (c) 2004 Zope Corporation and Contributors.
4
 
# All Rights Reserved.
5
 
#
6
 
# This software is subject to the provisions of the Zope Public License,
7
 
# Version 2.0 (ZPL).  A copy of the ZPL should accompany this distribution.
8
 
# THIS SOFTWARE IS PROVIDED "AS IS" AND ANY AND ALL EXPRESS OR IMPLIED
9
 
# WARRANTIES ARE DISCLAIMED, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
10
 
# WARRANTIES OF TITLE, MERCHANTABILITY, AGAINST INFRINGEMENT, AND FITNESS
11
 
# FOR A PARTICULAR PURPOSE.
12
 
#
13
 
##############################################################################
14
 
r"""Regular expression pattern normalizing output checker
15
 
 
16
 
The pattern-normalizing output checker extends the default output checker with
17
 
an option to normalize expected and actual output.
18
 
 
19
 
You specify a sequence of patterns and replacements.  The replacements are
20
 
applied to the expected and actual outputs before calling the default outputs
21
 
checker.  Let's look at an example.  In this example, we have some times and
22
 
addresses:
23
 
 
24
 
    >>> want = '''\
25
 
    ... <object object at 0xb7f14438>
26
 
    ... completed in 1.234 seconds.
27
 
    ... <BLANKLINE>
28
 
    ... <object object at 0xb7f14440>
29
 
    ... completed in 123.234 seconds.
30
 
    ... <BLANKLINE>
31
 
    ... <object object at 0xb7f14448>
32
 
    ... completed in .234 seconds.
33
 
    ... <BLANKLINE>
34
 
    ... <object object at 0xb7f14450>
35
 
    ... completed in 1.234 seconds.
36
 
    ... <BLANKLINE>
37
 
    ... '''
38
 
 
39
 
    >>> got = '''\
40
 
    ... <object object at 0xb7f14458>
41
 
    ... completed in 1.235 seconds.
42
 
    ...
43
 
    ... <object object at 0xb7f14460>
44
 
    ... completed in 123.233 seconds.
45
 
    ...
46
 
    ... <object object at 0xb7f14468>
47
 
    ... completed in .231 seconds.
48
 
    ...
49
 
    ... <object object at 0xb7f14470>
50
 
    ... completed in 1.23 seconds.
51
 
    ...
52
 
    ... '''
53
 
 
54
 
We may wish to consider these two strings to match, even though they differ in
55
 
actual addresses and times.  The default output checker will consider them
56
 
different:
57
 
 
58
 
    >>> doctest.OutputChecker().check_output(want, got, 0)
59
 
    False
60
 
 
61
 
We'll use the RENormalizing to normalize both the wanted and gotten strings to
62
 
ignore differences in times and addresses:
63
 
 
64
 
    >>> import re
65
 
    >>> checker = RENormalizing([
66
 
    ...    (re.compile('[0-9]*[.][0-9]* seconds'), '<SOME NUMBER OF> seconds'),
67
 
    ...    (re.compile('at 0x[0-9a-f]+'), 'at <SOME ADDRESS>'),
68
 
    ...    ])
69
 
 
70
 
    >>> checker.check_output(want, got, 0)
71
 
    True
72
 
 
73
 
Usual OutputChecker options work as expected:
74
 
 
75
 
    >>> want_ellided = '''\
76
 
    ... <object object at 0xb7f14438>
77
 
    ... completed in 1.234 seconds.
78
 
    ... ...
79
 
    ... <object object at 0xb7f14450>
80
 
    ... completed in 1.234 seconds.
81
 
    ... <BLANKLINE>
82
 
    ... '''
83
 
 
84
 
    >>> checker.check_output(want_ellided, got, 0)
85
 
    False
86
 
 
87
 
    >>> checker.check_output(want_ellided, got, doctest.ELLIPSIS)
88
 
    True
89
 
 
90
 
When we get differencs, we output them with normalized text:
91
 
 
92
 
    >>> source = '''\
93
 
    ... >>> do_something()
94
 
    ... <object object at 0xb7f14438>
95
 
    ... completed in 1.234 seconds.
96
 
    ... ...
97
 
    ... <object object at 0xb7f14450>
98
 
    ... completed in 1.234 seconds.
99
 
    ... <BLANKLINE>
100
 
    ... '''
101
 
 
102
 
    >>> example = doctest.Example(source, want_ellided)
103
 
 
104
 
    >>> print checker.output_difference(example, got, 0)
105
 
    Expected:
106
 
        <object object at <SOME ADDRESS>>
107
 
        completed in <SOME NUMBER OF> seconds.
108
 
        ...
109
 
        <object object at <SOME ADDRESS>>
110
 
        completed in <SOME NUMBER OF> seconds.
111
 
        <BLANKLINE>
112
 
    Got:
113
 
        <object object at <SOME ADDRESS>>
114
 
        completed in <SOME NUMBER OF> seconds.
115
 
        <BLANKLINE>
116
 
        <object object at <SOME ADDRESS>>
117
 
        completed in <SOME NUMBER OF> seconds.
118
 
        <BLANKLINE>
119
 
        <object object at <SOME ADDRESS>>
120
 
        completed in <SOME NUMBER OF> seconds.
121
 
        <BLANKLINE>
122
 
        <object object at <SOME ADDRESS>>
123
 
        completed in <SOME NUMBER OF> seconds.
124
 
        <BLANKLINE>
125
 
    <BLANKLINE>
126
 
 
127
 
    >>> print checker.output_difference(example, got,
128
 
    ...                                 doctest.REPORT_NDIFF)
129
 
    Differences (ndiff with -expected +actual):
130
 
        - <object object at <SOME ADDRESS>>
131
 
        - completed in <SOME NUMBER OF> seconds.
132
 
        - ...
133
 
          <object object at <SOME ADDRESS>>
134
 
          completed in <SOME NUMBER OF> seconds.
135
 
          <BLANKLINE>
136
 
        + <object object at <SOME ADDRESS>>
137
 
        + completed in <SOME NUMBER OF> seconds.
138
 
        + <BLANKLINE>
139
 
        + <object object at <SOME ADDRESS>>
140
 
        + completed in <SOME NUMBER OF> seconds.
141
 
        + <BLANKLINE>
142
 
        + <object object at <SOME ADDRESS>>
143
 
        + completed in <SOME NUMBER OF> seconds.
144
 
        + <BLANKLINE>
145
 
    <BLANKLINE>
146
 
 
147
 
    If the wanted text is empty, however, we don't transform the actual output.
148
 
    This is usful when writing tests.  We leave the expected output empty, run
149
 
    the test, and use the actual output as expected, after reviewing it.
150
 
 
151
 
    >>> source = '''\
152
 
    ... >>> do_something()
153
 
    ... '''
154
 
 
155
 
    >>> example = doctest.Example(source, '\n')
156
 
    >>> print checker.output_difference(example, got, 0)
157
 
    Expected:
158
 
    <BLANKLINE>
159
 
    Got:
160
 
        <object object at 0xb7f14458>
161
 
        completed in 1.235 seconds.
162
 
        <BLANKLINE>
163
 
        <object object at 0xb7f14460>
164
 
        completed in 123.233 seconds.
165
 
        <BLANKLINE>
166
 
        <object object at 0xb7f14468>
167
 
        completed in .231 seconds.
168
 
        <BLANKLINE>
169
 
        <object object at 0xb7f14470>
170
 
        completed in 1.23 seconds.
171
 
        <BLANKLINE>
172
 
    <BLANKLINE>
173
 
 
174
 
If regular expressions aren't expressive enough, you can use arbitrary Python
175
 
callables to transform the text.  For example, suppose you want to ignore
176
 
case during comparison:
177
 
 
178
 
    >>> checker = RENormalizing([
179
 
    ...    lambda s: s.lower(),
180
 
    ...    lambda s: s.replace('<blankline>', '<BLANKLINE>'),
181
 
    ...    ])
182
 
 
183
 
    >>> want = '''\
184
 
    ... Usage: thundermonkey [options] [url]
185
 
    ... <BLANKLINE>
186
 
    ... Options:
187
 
    ...     -h    display this help message
188
 
    ... '''
189
 
 
190
 
    >>> got = '''\
191
 
    ... usage: thundermonkey [options] [URL]
192
 
    ...
193
 
    ... options:
194
 
    ...     -h    Display this help message
195
 
    ... '''
196
 
 
197
 
    >>> checker.check_output(want, got, 0)
198
 
    True
199
 
 
200
 
Suppose we forgot that <BLANKLINE> must be in upper case:
201
 
 
202
 
    >>> checker = RENormalizing([
203
 
    ...    lambda s: s.lower(),
204
 
    ...    ])
205
 
 
206
 
    >>> checker.check_output(want, got, 0)
207
 
    False
208
 
 
209
 
The difference would show us that:
210
 
 
211
 
    >>> source = '''\
212
 
    ... >>> print_help_message()
213
 
    ... ''' + want
214
 
    >>> example = doctest.Example(source, want)
215
 
    >>> print checker.output_difference(example, got,
216
 
    ...                                 doctest.REPORT_NDIFF),
217
 
    Differences (ndiff with -expected +actual):
218
 
          usage: thundermonkey [options] [url]
219
 
        - <blankline>
220
 
        + <BLANKLINE>
221
 
          options:
222
 
              -h    display this help message
223
 
 
224
 
 
225
 
It is possible to combine RENormalizing checkers for easy reuse:
226
 
 
227
 
    >>> address_and_time_checker = RENormalizing([
228
 
    ...    (re.compile('[0-9]*[.][0-9]* seconds'), '<SOME NUMBER OF> seconds'),
229
 
    ...    (re.compile('at 0x[0-9a-f]+'), 'at <SOME ADDRESS>'),
230
 
    ...    ])
231
 
    >>> lowercase_checker = RENormalizing([
232
 
    ...    lambda s: s.lower(),
233
 
    ...    ])
234
 
    >>> combined_checker = address_and_time_checker + lowercase_checker
235
 
    >>> len(combined_checker.transformers)
236
 
    3
237
 
 
238
 
Combining a checker with something else does not work:
239
 
 
240
 
    >>> lowercase_checker + 5
241
 
    Traceback (most recent call last):
242
 
        ...
243
 
    TypeError: unsupported operand type(s) for +: 'instance' and 'int'
244
 
 
245
 
"""
246
 
 
247
 
import doctest
248
 
 
249
 
class RENormalizing(doctest.OutputChecker):
250
 
    """Pattern-normalizing outout checker
251
 
    """
252
 
 
253
 
    def __init__(self, patterns):
254
 
        self.transformers = map(self._cook, patterns)
255
 
 
256
 
    def __add__(self, other):
257
 
        if not isinstance(other, RENormalizing):
258
 
            return NotImplemented
259
 
        return RENormalizing(self.transformers + other.transformers)
260
 
 
261
 
    def _cook(self, pattern):
262
 
        if callable(pattern):
263
 
            return pattern
264
 
        regexp, replacement = pattern
265
 
        return lambda text: regexp.sub(replacement, text)
266
 
 
267
 
    def check_output(self, want, got, optionflags):
268
 
        if got == want:
269
 
            return True
270
 
 
271
 
        for transformer in self.transformers:
272
 
            want = transformer(want)
273
 
            got = transformer(got)
274
 
 
275
 
        return doctest.OutputChecker.check_output(self, want, got, optionflags)
276
 
 
277
 
    def output_difference(self, example, got, optionflags):
278
 
 
279
 
        want = example.want
280
 
 
281
 
        # If want is empty, use original outputter. This is useful
282
 
        # when setting up tests for the first time.  In that case, we
283
 
        # generally use the differencer to display output, which we evaluate
284
 
        # by hand.
285
 
        if not want.strip():
286
 
            return doctest.OutputChecker.output_difference(
287
 
                self, example, got, optionflags)
288
 
 
289
 
        # Dang, this isn't as easy to override as we might wish
290
 
        original = want
291
 
 
292
 
        for transformer in self.transformers:
293
 
            want = transformer(want)
294
 
            got = transformer(got)
295
 
 
296
 
        # temporarily hack example with normalized want:
297
 
        example.want = want
298
 
        result = doctest.OutputChecker.output_difference(
299
 
            self, example, got, optionflags)
300
 
        example.want = original
301
 
 
302
 
        return result