~bzr-pqm/bzr/bzr.dev

5200.3.1 by Robert Collins
Added ``bzrlib.tests.matchers`` as a place to put matchers, along with
1
# Copyright (C) 2010 Canonical Ltd
2
#
3
# This program is free software; you can redistribute it and/or modify
4
# it under the terms of the GNU General Public License as published by
5
# the Free Software Foundation; either version 2 of the License, or
6
# (at your option) any later version.
7
#
8
# This program is distributed in the hope that it will be useful,
9
# but WITHOUT ANY WARRANTY; without even the implied warranty of
10
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
11
# GNU General Public License for more details.
12
#
13
# You should have received a copy of the GNU General Public License
14
# along with this program; if not, write to the Free Software
15
# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA
16
17
"""Matchers for bzrlib.
18
19
Primarily test support, Matchers are used by self.assertThat in the bzrlib
20
test suite. A matcher is a stateful test helper which can be used to determine
21
if a passed object 'matches', much like a regex. If the object does not match
22
the mismatch can be described in a human readable fashion. assertThat then
23
raises if a mismatch occurs, showing the description as the assertion error.
24
25
Matchers are designed to be more reusable and composable than layered
26
assertions in Test Case objects, so they are recommended for new testing work.
27
"""
28
29
__all__ = [
6072.2.4 by Jelmer Vernooij
tests for matcher
30
    'HasLayout',
5972.3.13 by Jelmer Vernooij
Add matcher for ancestry.
31
    'MatchesAncestry',
6352.2.3 by Jelmer Vernooij
s/NoVfsCalls/ContainsNoVfsCalls/.
32
    'ContainsNoVfsCalls',
5200.3.2 by Robert Collins
Cleaner matcher matching revised unlocking protocol.
33
    'ReturnsUnlockable',
6228.3.1 by Jelmer Vernooij
Add RevisionHistoryMatches.
34
    'RevisionHistoryMatches',
5200.3.1 by Robert Collins
Added ``bzrlib.tests.matchers`` as a place to put matchers, along with
35
    ]
36
5972.3.13 by Jelmer Vernooij
Add matcher for ancestry.
37
from bzrlib import (
6110.6.2 by Jelmer Vernooij
In HasLayout, take into consideration Tree.has_versioned_directories.
38
    osutils,
5972.3.13 by Jelmer Vernooij
Add matcher for ancestry.
39
    revision as _mod_revision,
40
    )
6352.2.1 by Jelmer Vernooij
Add matcher for NoVfsCalls.
41
from bzrlib import lazy_import
42
lazy_import.lazy_import(globals(),
43
"""
44
from bzrlib.smart.request import request_handlers as smart_request_handlers
45
from bzrlib.smart import vfs
46
""")
5972.3.13 by Jelmer Vernooij
Add matcher for ancestry.
47
6072.2.1 by Jelmer Vernooij
Add HasLayout matcher.
48
from testtools.matchers import Equals, Mismatch, Matcher
5200.3.1 by Robert Collins
Added ``bzrlib.tests.matchers`` as a place to put matchers, along with
49
50
5200.3.2 by Robert Collins
Cleaner matcher matching revised unlocking protocol.
51
class ReturnsUnlockable(Matcher):
5200.3.1 by Robert Collins
Added ``bzrlib.tests.matchers`` as a place to put matchers, along with
52
    """A matcher that checks for the pattern we want lock* methods to have:
53
5200.3.2 by Robert Collins
Cleaner matcher matching revised unlocking protocol.
54
    They should return an object with an unlock() method.
55
    Calling that method should unlock the original object.
5200.3.1 by Robert Collins
Added ``bzrlib.tests.matchers`` as a place to put matchers, along with
56
57
    :ivar lockable_thing: The object which can be locked that will be
58
        inspected.
59
    """
60
61
    def __init__(self, lockable_thing):
62
        Matcher.__init__(self)
63
        self.lockable_thing = lockable_thing
64
65
    def __str__(self):
6110.6.2 by Jelmer Vernooij
In HasLayout, take into consideration Tree.has_versioned_directories.
66
        return ('ReturnsUnlockable(lockable_thing=%s)' %
5200.3.1 by Robert Collins
Added ``bzrlib.tests.matchers`` as a place to put matchers, along with
67
            self.lockable_thing)
68
69
    def match(self, lock_method):
5200.3.2 by Robert Collins
Cleaner matcher matching revised unlocking protocol.
70
        lock_method().unlock()
5200.3.1 by Robert Collins
Added ``bzrlib.tests.matchers`` as a place to put matchers, along with
71
        if self.lockable_thing.is_locked():
72
            return _IsLocked(self.lockable_thing)
73
        return None
74
75
76
class _IsLocked(Mismatch):
77
    """Something is locked."""
78
79
    def __init__(self, lockable_thing):
80
        self.lockable_thing = lockable_thing
81
82
    def describe(self):
83
        return "%s is locked" % self.lockable_thing
5972.3.13 by Jelmer Vernooij
Add matcher for ancestry.
84
85
86
class _AncestryMismatch(Mismatch):
87
    """Ancestry matching mismatch."""
88
89
    def __init__(self, tip_revision, got, expected):
90
        self.tip_revision = tip_revision
91
        self.got = got
92
        self.expected = expected
93
94
    def describe(self):
95
        return "mismatched ancestry for revision %r was %r, expected %r" % (
96
            self.tip_revision, self.got, self.expected)
97
98
99
class MatchesAncestry(Matcher):
100
    """A matcher that checks the ancestry of a particular revision.
101
102
    :ivar graph: Graph in which to check the ancestry
103
    :ivar revision_id: Revision id of the revision
104
    """
105
106
    def __init__(self, repository, revision_id):
107
        Matcher.__init__(self)
108
        self.repository = repository
109
        self.revision_id = revision_id
110
111
    def __str__(self):
112
        return ('MatchesAncestry(repository=%r, revision_id=%r)' % (
113
            self.repository, self.revision_id))
114
115
    def match(self, expected):
116
        self.repository.lock_read()
117
        try:
118
            graph = self.repository.get_graph()
119
            got = [r for r, p in graph.iter_ancestry([self.revision_id])]
5972.3.20 by Jelmer Vernooij
fix test.
120
            if _mod_revision.NULL_REVISION in got:
121
                got.remove(_mod_revision.NULL_REVISION)
5972.3.13 by Jelmer Vernooij
Add matcher for ancestry.
122
        finally:
123
            self.repository.unlock()
124
        if sorted(got) != sorted(expected):
6072.2.1 by Jelmer Vernooij
Add HasLayout matcher.
125
            return _AncestryMismatch(self.revision_id, sorted(got),
126
                sorted(expected))
127
128
129
class HasLayout(Matcher):
130
    """A matcher that checks if a tree has a specific layout.
131
132
    :ivar entries: List of expected entries, as (path, file_id) pairs.
133
    """
134
135
    def __init__(self, entries):
136
        Matcher.__init__(self)
137
        self.entries = entries
138
139
    def get_tree_layout(self, tree):
140
        """Get the (path, file_id) pairs for the current tree."""
141
        tree.lock_read()
142
        try:
6110.6.2 by Jelmer Vernooij
In HasLayout, take into consideration Tree.has_versioned_directories.
143
            for path, ie in tree.iter_entries_by_dir():
144
                if ie.parent_id is None:
145
                    yield (u"", ie.file_id)
146
                else:
147
                    yield (path+ie.kind_character(), ie.file_id)
6072.2.1 by Jelmer Vernooij
Add HasLayout matcher.
148
        finally:
149
            tree.unlock()
150
6110.6.2 by Jelmer Vernooij
In HasLayout, take into consideration Tree.has_versioned_directories.
151
    @staticmethod
152
    def _strip_unreferenced_directories(entries):
6110.6.3 by Jelmer Vernooij
review feedback from mgz
153
        """Strip all directories that don't (in)directly contain any files.
154
155
        :param entries: List of path strings or (path, ie) tuples to process
156
        """
6110.6.2 by Jelmer Vernooij
In HasLayout, take into consideration Tree.has_versioned_directories.
157
        directories = []
158
        for entry in entries:
159
            if isinstance(entry, basestring):
160
                path = entry
161
            else:
162
                path = entry[0]
6110.6.3 by Jelmer Vernooij
review feedback from mgz
163
            if not path or path[-1] == "/":
6110.6.2 by Jelmer Vernooij
In HasLayout, take into consideration Tree.has_versioned_directories.
164
                # directory
165
                directories.append((path, entry))
166
            else:
167
                # Yield the referenced parent directories
168
                for dirpath, direntry in directories:
169
                    if osutils.is_inside(dirpath, path):
170
                        yield direntry
171
                directories = []
172
                yield entry
173
6072.2.1 by Jelmer Vernooij
Add HasLayout matcher.
174
    def __str__(self):
6110.6.2 by Jelmer Vernooij
In HasLayout, take into consideration Tree.has_versioned_directories.
175
        return 'HasLayout(%r)' % self.entries
6072.2.1 by Jelmer Vernooij
Add HasLayout matcher.
176
177
    def match(self, tree):
6110.6.2 by Jelmer Vernooij
In HasLayout, take into consideration Tree.has_versioned_directories.
178
        actual = list(self.get_tree_layout(tree))
6072.2.3 by Jelmer Vernooij
Use the matcher in test_merge_from_branch.
179
        if self.entries and isinstance(self.entries[0], basestring):
180
            actual = [path for (path, fileid) in actual]
6110.6.2 by Jelmer Vernooij
In HasLayout, take into consideration Tree.has_versioned_directories.
181
        if not tree.has_versioned_directories():
182
            entries = list(self._strip_unreferenced_directories(self.entries))
183
        else:
184
            entries = self.entries
6110.6.3 by Jelmer Vernooij
review feedback from mgz
185
        return Equals(entries).match(actual)
6228.3.1 by Jelmer Vernooij
Add RevisionHistoryMatches.
186
187
188
class RevisionHistoryMatches(Matcher):
189
    """A matcher that checks if a branch has a specific revision history.
190
191
    :ivar history: Revision history, as list of revisions. Oldest first.
192
    """
193
194
    def __init__(self, history):
195
        Matcher.__init__(self)
196
        self.expected = history
197
198
    def __str__(self):
199
        return 'RevisionHistoryMatches(%r)' % self.expected
200
201
    def match(self, branch):
202
        branch.lock_read()
203
        try:
204
            graph = branch.repository.get_graph()
205
            history = list(graph.iter_lefthand_ancestry(
206
                branch.last_revision(), [_mod_revision.NULL_REVISION]))
207
            history.reverse()
208
        finally:
209
            branch.unlock()
210
        return Equals(self.expected).match(history)
6228.3.4 by Jelmer Vernooij
Merge bzr.dev.
211
212
6352.2.1 by Jelmer Vernooij
Add matcher for NoVfsCalls.
213
class _NoVfsCallsMismatch(Mismatch):
214
    """Mismatch describing a list of HPSS calls which includes VFS requests."""
215
216
    def __init__(self, vfs_calls):
217
        self.vfs_calls = vfs_calls
218
219
    def describe(self):
220
        return "no VFS calls expected, got: %s" % ",".join([
221
            "%s(%s)" % (c.method,
222
                ", ".join([repr(a) for a in c.args])) for c in self.vfs_calls])
223
224
6352.2.3 by Jelmer Vernooij
s/NoVfsCalls/ContainsNoVfsCalls/.
225
class ContainsNoVfsCalls(Matcher):
6352.2.1 by Jelmer Vernooij
Add matcher for NoVfsCalls.
226
    """Ensure that none of the specified calls are HPSS calls."""
227
228
    def __str__(self):
6352.2.3 by Jelmer Vernooij
s/NoVfsCalls/ContainsNoVfsCalls/.
229
        return 'ContainsNoVfsCalls()'
6352.2.1 by Jelmer Vernooij
Add matcher for NoVfsCalls.
230
231
    @classmethod
232
    def match(cls, hpss_calls):
233
        vfs_calls = []
234
        for call in hpss_calls:
235
            try:
236
                request_method = smart_request_handlers.get(call.call.method)
237
            except KeyError:
238
                # A method we don't know about doesn't count as a VFS method.
239
                continue
240
            if issubclass(request_method, vfs.VfsRequest):
241
                vfs_calls.append(call.call)
242
        if len(vfs_calls) == 0:
243
            return None
244
        return _NoVfsCallsMismatch(vfs_calls)