~launchpad-pqm/launchpad/devel

« back to all changes in this revision

Viewing changes to lib/lp/testing/matchers.py

  • Committer: Canonical.com Patch Queue Manager
  • Date: 2004-08-03 09:17:25 UTC
  • mfrom: (unknown (missing))
  • Revision ID: Arch-1:rocketfuel@canonical.com%launchpad--devel--0--patch-19
Removed defaultSkin directive to make launchpad work with the latest zope.
Patches applied:

 * steve.alexander@canonical.com/launchpad--devel--0--patch-16
   merge from rocketfuel

 * steve.alexander@canonical.com/launchpad--devel--0--patch-17
   removed use of defaultSkin directive, which has been removed from zope3

Show diffs side-by-side

added added

removed removed

Lines of Context:
1
 
# Copyright 2010 Canonical Ltd.  This software is licensed under the
2
 
# GNU Affero General Public License version 3 (see the file LICENSE).
3
 
 
4
 
__metaclass__ = type
5
 
__all__ = [
6
 
    'BrowsesWithQueryLimit',
7
 
    'Contains',
8
 
    'DocTestMatches',
9
 
    'DoesNotCorrectlyProvide',
10
 
    'DoesNotProvide',
11
 
    'EqualsIgnoringWhitespace',
12
 
    'HasQueryCount',
13
 
    'IsNotProxied',
14
 
    'IsProxied',
15
 
    'MatchesPickerText',
16
 
    'MatchesTagText',
17
 
    'MissingElement',
18
 
    'MultipleElements',
19
 
    'Provides',
20
 
    'ProvidesAndIsProxied',
21
 
    ]
22
 
 
23
 
from lazr.lifecycle.snapshot import Snapshot
24
 
from testtools import matchers
25
 
from testtools.content import Content
26
 
from testtools.content_type import UTF8_TEXT
27
 
from testtools.matchers import (
28
 
    DocTestMatches as OriginalDocTestMatches,
29
 
    Equals,
30
 
    LessThan,
31
 
    Matcher,
32
 
    Mismatch,
33
 
    MismatchesAll,
34
 
    )
35
 
from zope.interface.exceptions import (
36
 
    BrokenImplementation,
37
 
    BrokenMethodImplementation,
38
 
    DoesNotImplement,
39
 
    )
40
 
from zope.interface.verify import verifyObject
41
 
from zope.security.proxy import (
42
 
    builtin_isinstance,
43
 
    Proxy,
44
 
    )
45
 
 
46
 
from canonical.launchpad.webapp import canonical_url
47
 
from canonical.launchpad.webapp.batching import BatchNavigator
48
 
from lp.testing import normalize_whitespace
49
 
from lp.testing._login import person_logged_in
50
 
from lp.testing._webservice import QueryCollector
51
 
 
52
 
 
53
 
class BrowsesWithQueryLimit(Matcher):
54
 
    """Matches the rendering of an objects default view with a query limit.
55
 
 
56
 
    This is a wrapper for HasQueryCount which does the heavy lifting on the
57
 
    query comparison - BrowsesWithQueryLimit simply provides convenient
58
 
    glue to use a userbrowser and view an object.
59
 
    """
60
 
 
61
 
    def __init__(self, query_limit, user, view_name="+index", **options):
62
 
        """Create a BrowsesWithQueryLimit checking for limit query_limit.
63
 
 
64
 
        :param query_limit: The number of queries permited for the page.
65
 
        :param user: The user to use to render the page.
66
 
        :param view_name: The name of the view to use to render tha page.
67
 
        :param options: Additional options for view generation eg rootsite.
68
 
        """
69
 
        self.query_limit = query_limit
70
 
        self.user = user
71
 
        self.view_name = view_name
72
 
        self.options = options
73
 
 
74
 
    def match(self, context):
75
 
        # circular dependencies.
76
 
        from canonical.launchpad.testing.pages import setupBrowserForUser
77
 
        with person_logged_in(self.user):
78
 
            context_url = canonical_url(
79
 
                context, view_name=self.view_name, **self.options)
80
 
        browser = setupBrowserForUser(self.user)
81
 
        collector = QueryCollector()
82
 
        collector.register()
83
 
        try:
84
 
            browser.open(context_url)
85
 
            counter = HasQueryCount(LessThan(self.query_limit))
86
 
            # When bug 724691 is fixed, this can become an AnnotateMismatch to
87
 
            # describe the object being rendered.
88
 
            return counter.match(collector)
89
 
        finally:
90
 
            # Unregister now in case this method is called multiple
91
 
            # times in a single test.
92
 
            collector.unregister()
93
 
 
94
 
    def __str__(self):
95
 
        return "BrowsesWithQueryLimit(%s, %s)" % (self.query_limit, self.user)
96
 
 
97
 
 
98
 
class DoesNotProvide(Mismatch):
99
 
    """An object does not provide an interface."""
100
 
 
101
 
    def __init__(self, obj, interface):
102
 
        """Create a DoesNotProvide Mismatch.
103
 
 
104
 
        :param obj: the object that does not match.
105
 
        :param interface: the Interface that the object was supposed to match.
106
 
        """
107
 
        self.obj = obj
108
 
        self.interface = interface
109
 
 
110
 
    def describe(self):
111
 
        return "%r does not provide %r." % (self.obj, self.interface)
112
 
 
113
 
 
114
 
class DoesNotCorrectlyProvide(DoesNotProvide):
115
 
    """An object does not correctly provide an interface."""
116
 
 
117
 
    def __init__(self, obj, interface, extra=None):
118
 
        """Create a DoesNotCorrectlyProvide Mismatch.
119
 
 
120
 
        :param obj: the object that does not match.
121
 
        :param interface: the Interface that the object was supposed to match.
122
 
        :param extra: any extra information about the mismatch as a string,
123
 
            or None
124
 
        """
125
 
        super(DoesNotCorrectlyProvide, self).__init__(obj, interface)
126
 
        self.extra = extra
127
 
 
128
 
    def describe(self):
129
 
        if self.extra is not None:
130
 
            extra = ": %s" % self.extra
131
 
        else:
132
 
            extra = "."
133
 
        return ("%r claims to provide %r, but does not do so correctly%s"
134
 
                % (self.obj, self.interface, extra))
135
 
 
136
 
 
137
 
class Provides(Matcher):
138
 
    """Test that an object provides a certain interface."""
139
 
 
140
 
    def __init__(self, interface):
141
 
        """Create a Provides Matcher.
142
 
 
143
 
        :param interface: the Interface that the object should provide.
144
 
        """
145
 
        self.interface = interface
146
 
 
147
 
    def __str__(self):
148
 
        return "provides %r." % self.interface
149
 
 
150
 
    def match(self, matchee):
151
 
        if not self.interface.providedBy(matchee):
152
 
            return DoesNotProvide(matchee, self.interface)
153
 
        passed = True
154
 
        extra = None
155
 
        try:
156
 
            if not verifyObject(self.interface, matchee):
157
 
                passed = False
158
 
        except (BrokenImplementation, BrokenMethodImplementation,
159
 
                DoesNotImplement), e:
160
 
            passed = False
161
 
            extra = str(e)
162
 
        if not passed:
163
 
            return DoesNotCorrectlyProvide(
164
 
                matchee, self.interface, extra=extra)
165
 
        return None
166
 
 
167
 
 
168
 
class HasQueryCount(Matcher):
169
 
    """Adapt a Binary Matcher to the query count on a QueryCollector.
170
 
 
171
 
    If there is a mismatch, the queries from the collector are provided as a
172
 
    test attachment.
173
 
    """
174
 
 
175
 
    def __init__(self, count_matcher):
176
 
        """Create a HasQueryCount that will match using count_matcher."""
177
 
        self.count_matcher = count_matcher
178
 
 
179
 
    def __str__(self):
180
 
        return "HasQueryCount(%s)" % self.count_matcher
181
 
 
182
 
    def match(self, something):
183
 
        mismatch = self.count_matcher.match(something.count)
184
 
        if mismatch is None:
185
 
            return None
186
 
        return _MismatchedQueryCount(mismatch, something)
187
 
 
188
 
 
189
 
class _MismatchedQueryCount(Mismatch):
190
 
    """The Mismatch for a HasQueryCount matcher."""
191
 
 
192
 
    def __init__(self, mismatch, query_collector):
193
 
        self.count_mismatch = mismatch
194
 
        self.query_collector = query_collector
195
 
 
196
 
    def describe(self):
197
 
        return "queries do not match: %s" % (self.count_mismatch.describe(),)
198
 
 
199
 
    def get_details(self):
200
 
        result = []
201
 
        for query in self.query_collector.queries:
202
 
            result.append(unicode(query).encode('utf8'))
203
 
        return {'queries': Content(UTF8_TEXT, lambda: ['\n'.join(result)])}
204
 
 
205
 
 
206
 
class IsNotProxied(Mismatch):
207
 
    """An object is not proxied."""
208
 
 
209
 
    def __init__(self, obj):
210
 
        """Create an IsNotProxied Mismatch.
211
 
 
212
 
        :param obj: the object that is not proxied.
213
 
        """
214
 
        self.obj = obj
215
 
 
216
 
    def describe(self):
217
 
        return "%r is not proxied." % self.obj
218
 
 
219
 
 
220
 
class IsProxied(Matcher):
221
 
    """Check that an object is proxied."""
222
 
 
223
 
    def __str__(self):
224
 
        return "Is proxied."
225
 
 
226
 
    def match(self, matchee):
227
 
        if not builtin_isinstance(matchee, Proxy):
228
 
            return IsNotProxied(matchee)
229
 
        return None
230
 
 
231
 
 
232
 
class ProvidesAndIsProxied(Matcher):
233
 
    """Test that an object implements an interface, and is proxied."""
234
 
 
235
 
    def __init__(self, interface):
236
 
        """Create a ProvidesAndIsProxied matcher.
237
 
 
238
 
        :param interface: the Interface the object must provide.
239
 
        """
240
 
        self.interface = interface
241
 
 
242
 
    def __str__(self):
243
 
        return "Provides %r and is proxied." % self.interface
244
 
 
245
 
    def match(self, matchee):
246
 
        mismatch = Provides(self.interface).match(matchee)
247
 
        if mismatch is not None:
248
 
            return mismatch
249
 
        return IsProxied().match(matchee)
250
 
 
251
 
 
252
 
class DoesNotContain(Mismatch):
253
 
 
254
 
    def __init__(self, matchee, expected):
255
 
        """Create a DoesNotContain Mismatch.
256
 
 
257
 
        :param matchee: the string that did not match.
258
 
        :param expected: the string that `matchee` was expected to contain.
259
 
        """
260
 
        self.matchee = matchee
261
 
        self.expected = expected
262
 
 
263
 
    def describe(self):
264
 
        return "'%s' does not contain '%s'." % (
265
 
            self.matchee, self.expected)
266
 
 
267
 
 
268
 
class Contains(Matcher):
269
 
    """Checks whether one string contains another."""
270
 
 
271
 
    def __init__(self, expected):
272
 
        """Create a Contains Matcher.
273
 
 
274
 
        :param expected: the string that matchees should contain.
275
 
        """
276
 
        self.expected = expected
277
 
 
278
 
    def __str__(self):
279
 
        return "Contains '%s'." % self.expected
280
 
 
281
 
    def match(self, matchee):
282
 
        if self.expected not in matchee:
283
 
            return DoesNotContain(matchee, self.expected)
284
 
        return None
285
 
 
286
 
 
287
 
class IsConfiguredBatchNavigator(Matcher):
288
 
    """Check that an object is a batch navigator."""
289
 
 
290
 
    def __init__(self, singular, plural, batch_size=None):
291
 
        """Create a ConfiguredBatchNavigator.
292
 
 
293
 
        :param singular: The singular header the batch should be using.
294
 
        :param plural: The plural header the batch should be using.
295
 
        :param batch_size: The batch size that should be configured by
296
 
            default.
297
 
        """
298
 
        self._single = Equals(singular)
299
 
        self._plural = Equals(plural)
300
 
        self._batch = None
301
 
        if batch_size:
302
 
            self._batch = Equals(batch_size)
303
 
        self.matchers = dict(
304
 
            _singular_heading=self._single, _plural_heading=self._plural)
305
 
        if self._batch:
306
 
            self.matchers['default_size'] = self._batch
307
 
 
308
 
    def __str__(self):
309
 
        if self._batch:
310
 
            batch = ", %r" % self._batch.expected
311
 
        else:
312
 
            batch = ''
313
 
        return "ConfiguredBatchNavigator(%r, %r%s)" % (
314
 
            self._single.expected, self._plural.expected, batch)
315
 
 
316
 
    def match(self, matchee):
317
 
        if not isinstance(matchee, BatchNavigator):
318
 
            # Testtools doesn't have an IsInstanceMismatch yet.
319
 
            return matchers._BinaryMismatch(
320
 
                BatchNavigator, 'isinstance', matchee)
321
 
        mismatches = []
322
 
        for attrname, matcher in self.matchers.items():
323
 
            mismatch = matcher.match(getattr(matchee, attrname))
324
 
            if mismatch is not None:
325
 
                mismatches.append(mismatch)
326
 
        if mismatches:
327
 
            return MismatchesAll(mismatches)
328
 
 
329
 
 
330
 
class WasSnapshotted(Mismatch):
331
 
 
332
 
    def __init__(self, matchee, attribute):
333
 
        self.matchee = matchee
334
 
        self.attribute = attribute
335
 
 
336
 
    def describe(self):
337
 
        return "Snapshot of %s should not include %s" % (
338
 
            self.matchee, self.attribute)
339
 
 
340
 
 
341
 
class DoesNotSnapshot(Matcher):
342
 
    """Checks that certain fields are skipped on Snapshots."""
343
 
 
344
 
    def __init__(self, attr_list, interface, error_msg=None):
345
 
        self.attr_list = attr_list
346
 
        self.interface = interface
347
 
        self.error_msg = error_msg
348
 
 
349
 
    def __str__(self):
350
 
        return "Does not include %s when Snapshot is provided %s." % (
351
 
            ', '.join(self.attr_list), self.interface)
352
 
 
353
 
    def match(self, matchee):
354
 
        snapshot = Snapshot(matchee, providing=self.interface)
355
 
        mismatches = []
356
 
        for attribute in self.attr_list:
357
 
            if hasattr(snapshot, attribute):
358
 
                mismatches.append(WasSnapshotted(matchee, attribute))
359
 
 
360
 
        if len(mismatches) == 0:
361
 
            return None
362
 
        else:
363
 
            return MismatchesAll(mismatches)
364
 
 
365
 
 
366
 
def DocTestMatches(example):
367
 
    """See if a string matches a doctest example.
368
 
 
369
 
    Uses the default doctest flags used across Launchpad.
370
 
    """
371
 
    from canonical.launchpad.testing.systemdocs import default_optionflags
372
 
    return OriginalDocTestMatches(example, default_optionflags)
373
 
 
374
 
 
375
 
class SoupMismatch(Mismatch):
376
 
 
377
 
    def __init__(self, widget_id, soup_content):
378
 
        self.widget_id = widget_id
379
 
        self.soup_content = soup_content
380
 
 
381
 
    def get_details(self):
382
 
        return {'content': self.soup_content}
383
 
 
384
 
 
385
 
class MissingElement(SoupMismatch):
386
 
 
387
 
    def describe(self):
388
 
        return 'No HTML element found with id %r' % self.widget_id
389
 
 
390
 
 
391
 
class MultipleElements(SoupMismatch):
392
 
 
393
 
    def describe(self):
394
 
        return 'HTML id %r found multiple times in document' % self.widget_id
395
 
 
396
 
 
397
 
class MatchesTagText(Matcher):
398
 
    """Match against the extracted text of the tag."""
399
 
 
400
 
    def __init__(self, soup_content, tag_id):
401
 
        """Construct the matcher with the soup content."""
402
 
        self.soup_content = soup_content
403
 
        self.tag_id = tag_id
404
 
 
405
 
    def __str__(self):
406
 
        return "matches widget %r text" % self.tag_id
407
 
 
408
 
    def match(self, matchee):
409
 
        # Here to avoid circular dependancies.
410
 
        from canonical.launchpad.testing.pages import extract_text
411
 
        widgets = self.soup_content.findAll(id=self.tag_id)
412
 
        if len(widgets) == 0:
413
 
            return MissingElement(self.tag_id, self.soup_content)
414
 
        elif len(widgets) > 1:
415
 
            return MultipleElements(self.tag_id, self.soup_content)
416
 
        widget = widgets[0]
417
 
        text_matcher = DocTestMatches(extract_text(widget))
418
 
        return text_matcher.match(matchee)
419
 
 
420
 
 
421
 
class MatchesPickerText(Matcher):
422
 
    """Match against the text in a widget."""
423
 
 
424
 
    def __init__(self, soup_content, widget_id):
425
 
        """Construct the matcher with the soup content."""
426
 
        self.soup_content = soup_content
427
 
        self.widget_id = widget_id
428
 
 
429
 
    def __str__(self):
430
 
        return "matches widget %r text" % self.widget_id
431
 
 
432
 
    def match(self, matchee):
433
 
        # Here to avoid circular dependancies.
434
 
        from canonical.launchpad.testing.pages import extract_text
435
 
        widgets = self.soup_content.findAll(id=self.widget_id)
436
 
        if len(widgets) == 0:
437
 
            return MissingElement(self.widget_id, self.soup_content)
438
 
        elif len(widgets) > 1:
439
 
            return MultipleElements(self.widget_id, self.soup_content)
440
 
        widget = widgets[0]
441
 
        text = widget.findAll(attrs={'class': 'yui3-activator-data-box'})[0]
442
 
        text_matcher = DocTestMatches(extract_text(text))
443
 
        return text_matcher.match(matchee)
444
 
 
445
 
 
446
 
class EqualsIgnoringWhitespace(Equals):
447
 
    """Compare equality, ignoring whitespace in strings.
448
 
 
449
 
    Whitespace in strings is normalized before comparison. All other objects
450
 
    are compared as they come.
451
 
    """
452
 
 
453
 
    def __init__(self, expected):
454
 
        if isinstance(expected, (str, unicode)):
455
 
            expected = normalize_whitespace(expected)
456
 
        super(EqualsIgnoringWhitespace, self).__init__(expected)
457
 
 
458
 
    def match(self, observed):
459
 
        if isinstance(observed, (str, unicode)):
460
 
            observed = normalize_whitespace(observed)
461
 
        return super(EqualsIgnoringWhitespace, self).match(observed)