~landscape/zope3/trunk

« back to all changes in this revision

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