/brz/remove-bazaar

To get this branch, use:
bzr branch http://gegoxaren.bato24.eu/bzr/brz/remove-bazaar

« back to all changes in this revision

Viewing changes to breezy/tests/matchers.py

[merge] robertc's integration, updated tests to check for retcode=3

Show diffs side-by-side

added added

removed removed

Lines of Context:
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 breezy.
18
 
 
19
 
Primarily test support, Matchers are used by self.assertThat in the breezy
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__ = [
30
 
    'HasLayout',
31
 
    'HasPathRelations',
32
 
    'MatchesAncestry',
33
 
    'ContainsNoVfsCalls',
34
 
    'ReturnsUnlockable',
35
 
    'RevisionHistoryMatches',
36
 
    ]
37
 
 
38
 
from .. import (
39
 
    osutils,
40
 
    revision as _mod_revision,
41
 
    )
42
 
from .. import lazy_import
43
 
lazy_import.lazy_import(globals(),
44
 
                        """
45
 
from breezy.bzr.smart.request import request_handlers as smart_request_handlers
46
 
from breezy.bzr.smart import vfs
47
 
""")
48
 
from ..sixish import (
49
 
    text_type,
50
 
    )
51
 
from ..tree import InterTree
52
 
 
53
 
from testtools.matchers import Equals, Mismatch, Matcher
54
 
 
55
 
 
56
 
class ReturnsUnlockable(Matcher):
57
 
    """A matcher that checks for the pattern we want lock* methods to have:
58
 
 
59
 
    They should return an object with an unlock() method.
60
 
    Calling that method should unlock the original object.
61
 
 
62
 
    :ivar lockable_thing: The object which can be locked that will be
63
 
        inspected.
64
 
    """
65
 
 
66
 
    def __init__(self, lockable_thing):
67
 
        Matcher.__init__(self)
68
 
        self.lockable_thing = lockable_thing
69
 
 
70
 
    def __str__(self):
71
 
        return ('ReturnsUnlockable(lockable_thing=%s)' %
72
 
                self.lockable_thing)
73
 
 
74
 
    def match(self, lock_method):
75
 
        lock_method().unlock()
76
 
        if self.lockable_thing.is_locked():
77
 
            return _IsLocked(self.lockable_thing)
78
 
        return None
79
 
 
80
 
 
81
 
class _IsLocked(Mismatch):
82
 
    """Something is locked."""
83
 
 
84
 
    def __init__(self, lockable_thing):
85
 
        self.lockable_thing = lockable_thing
86
 
 
87
 
    def describe(self):
88
 
        return "%s is locked" % self.lockable_thing
89
 
 
90
 
 
91
 
class _AncestryMismatch(Mismatch):
92
 
    """Ancestry matching mismatch."""
93
 
 
94
 
    def __init__(self, tip_revision, got, expected):
95
 
        self.tip_revision = tip_revision
96
 
        self.got = got
97
 
        self.expected = expected
98
 
 
99
 
    def describe(self):
100
 
        return "mismatched ancestry for revision %r was %r, expected %r" % (
101
 
            self.tip_revision, self.got, self.expected)
102
 
 
103
 
 
104
 
class MatchesAncestry(Matcher):
105
 
    """A matcher that checks the ancestry of a particular revision.
106
 
 
107
 
    :ivar graph: Graph in which to check the ancestry
108
 
    :ivar revision_id: Revision id of the revision
109
 
    """
110
 
 
111
 
    def __init__(self, repository, revision_id):
112
 
        Matcher.__init__(self)
113
 
        self.repository = repository
114
 
        self.revision_id = revision_id
115
 
 
116
 
    def __str__(self):
117
 
        return ('MatchesAncestry(repository=%r, revision_id=%r)' % (
118
 
            self.repository, self.revision_id))
119
 
 
120
 
    def match(self, expected):
121
 
        with self.repository.lock_read():
122
 
            graph = self.repository.get_graph()
123
 
            got = [r for r, p in graph.iter_ancestry([self.revision_id])]
124
 
            if _mod_revision.NULL_REVISION in got:
125
 
                got.remove(_mod_revision.NULL_REVISION)
126
 
        if sorted(got) != sorted(expected):
127
 
            return _AncestryMismatch(self.revision_id, sorted(got),
128
 
                                     sorted(expected))
129
 
 
130
 
 
131
 
class HasLayout(Matcher):
132
 
    """A matcher that checks if a tree has a specific layout.
133
 
 
134
 
    :ivar entries: List of expected entries, as (path, file_id) pairs.
135
 
    """
136
 
 
137
 
    def __init__(self, entries):
138
 
        Matcher.__init__(self)
139
 
        self.entries = entries
140
 
 
141
 
    def get_tree_layout(self, tree, include_file_ids):
142
 
        """Get the (path, file_id) pairs for the current tree."""
143
 
        with tree.lock_read():
144
 
            for path, ie in tree.iter_entries_by_dir():
145
 
                if path != u'':
146
 
                    path += ie.kind_character()
147
 
                if include_file_ids:
148
 
                    yield (path, ie.file_id)
149
 
                else:
150
 
                    yield path
151
 
 
152
 
    @staticmethod
153
 
    def _strip_unreferenced_directories(entries):
154
 
        """Strip all directories that don't (in)directly contain any files.
155
 
 
156
 
        :param entries: List of path strings or (path, ie) tuples to process
157
 
        """
158
 
        directories = []
159
 
        for entry in entries:
160
 
            if isinstance(entry, (str, text_type)):
161
 
                path = entry
162
 
            else:
163
 
                path = entry[0]
164
 
            if not path or path[-1] == "/":
165
 
                # directory
166
 
                directories.append((path, entry))
167
 
            else:
168
 
                # Yield the referenced parent directories
169
 
                for dirpath, direntry in directories:
170
 
                    if osutils.is_inside(dirpath, path):
171
 
                        yield direntry
172
 
                directories = []
173
 
                yield entry
174
 
 
175
 
    def __str__(self):
176
 
        return 'HasLayout(%r)' % self.entries
177
 
 
178
 
    def match(self, tree):
179
 
        include_file_ids = self.entries and not isinstance(
180
 
            self.entries[0], (str, text_type))
181
 
        actual = list(self.get_tree_layout(
182
 
            tree, include_file_ids=include_file_ids))
183
 
        if not tree.has_versioned_directories():
184
 
            entries = list(self._strip_unreferenced_directories(self.entries))
185
 
        else:
186
 
            entries = self.entries
187
 
        return Equals(entries).match(actual)
188
 
 
189
 
 
190
 
class HasPathRelations(Matcher):
191
 
    """Matcher verifies that paths have a relation to those in another tree.
192
 
 
193
 
    :ivar previous_tree: tree to compare to
194
 
    :ivar previous_entries: List of expected entries, as (path, previous_path) pairs.
195
 
    """
196
 
 
197
 
    def __init__(self, previous_tree, previous_entries):
198
 
        Matcher.__init__(self)
199
 
        self.previous_tree = previous_tree
200
 
        self.previous_entries = previous_entries
201
 
 
202
 
    def get_path_map(self, tree):
203
 
        """Get the (path, previous_path) pairs for the current tree."""
204
 
        previous_intertree = InterTree.get(self.previous_tree, tree)
205
 
        with tree.lock_read(), self.previous_tree.lock_read():
206
 
            for path, ie in tree.iter_entries_by_dir():
207
 
                if tree.supports_rename_tracking():
208
 
                    previous_path = previous_intertree.find_source_path(path)
209
 
                else:
210
 
                    if self.previous_tree.is_versioned(path):
211
 
                        previous_path = path
212
 
                    else:
213
 
                        previous_path = None
214
 
                if previous_path:
215
 
                    kind = self.previous_tree.kind(previous_path)
216
 
                    if kind == 'directory':
217
 
                        previous_path += '/'
218
 
                if path == u'':
219
 
                    yield (u"", previous_path)
220
 
                else:
221
 
                    yield (path + ie.kind_character(), previous_path)
222
 
 
223
 
    @staticmethod
224
 
    def _strip_unreferenced_directories(entries):
225
 
        """Strip all directories that don't (in)directly contain any files.
226
 
 
227
 
        :param entries: List of path strings or (path, previous_path) tuples to process
228
 
        """
229
 
        directory_used = set()
230
 
        directories = []
231
 
        for (path, previous_path) in entries:
232
 
            if not path or path[-1] == "/":
233
 
                # directory
234
 
                directories.append((path, previous_path))
235
 
            else:
236
 
                # Yield the referenced parent directories
237
 
                for direntry in directories:
238
 
                    if osutils.is_inside(direntry[0], path):
239
 
                        directory_used.add(direntry[0])
240
 
        for (path, previous_path) in entries:
241
 
            if (not path.endswith("/")) or path in directory_used:
242
 
                yield (path, previous_path)
243
 
 
244
 
    def __str__(self):
245
 
        return 'HasPathRelations(%r, %r)' % (self.previous_tree, self.previous_entries)
246
 
 
247
 
    def match(self, tree):
248
 
        actual = list(self.get_path_map(tree))
249
 
        if not tree.has_versioned_directories():
250
 
            entries = list(self._strip_unreferenced_directories(
251
 
                self.previous_entries))
252
 
        else:
253
 
            entries = self.previous_entries
254
 
        if not tree.supports_rename_tracking():
255
 
            entries = [
256
 
                (path, path if self.previous_tree.is_versioned(path) else None)
257
 
                for (path, previous_path) in entries]
258
 
        return Equals(entries).match(actual)
259
 
 
260
 
 
261
 
class RevisionHistoryMatches(Matcher):
262
 
    """A matcher that checks if a branch has a specific revision history.
263
 
 
264
 
    :ivar history: Revision history, as list of revisions. Oldest first.
265
 
    """
266
 
 
267
 
    def __init__(self, history):
268
 
        Matcher.__init__(self)
269
 
        self.expected = history
270
 
 
271
 
    def __str__(self):
272
 
        return 'RevisionHistoryMatches(%r)' % self.expected
273
 
 
274
 
    def match(self, branch):
275
 
        with branch.lock_read():
276
 
            graph = branch.repository.get_graph()
277
 
            history = list(graph.iter_lefthand_ancestry(
278
 
                branch.last_revision(), [_mod_revision.NULL_REVISION]))
279
 
            history.reverse()
280
 
        return Equals(self.expected).match(history)
281
 
 
282
 
 
283
 
class _NoVfsCallsMismatch(Mismatch):
284
 
    """Mismatch describing a list of HPSS calls which includes VFS requests."""
285
 
 
286
 
    def __init__(self, vfs_calls):
287
 
        self.vfs_calls = vfs_calls
288
 
 
289
 
    def describe(self):
290
 
        return "no VFS calls expected, got: %s" % ",".join([
291
 
            "%s(%s)" % (c.method,
292
 
                        ", ".join([repr(a) for a in c.args])) for c in self.vfs_calls])
293
 
 
294
 
 
295
 
class ContainsNoVfsCalls(Matcher):
296
 
    """Ensure that none of the specified calls are HPSS calls."""
297
 
 
298
 
    def __str__(self):
299
 
        return 'ContainsNoVfsCalls()'
300
 
 
301
 
    @classmethod
302
 
    def match(cls, hpss_calls):
303
 
        vfs_calls = []
304
 
        for call in hpss_calls:
305
 
            try:
306
 
                request_method = smart_request_handlers.get(call.call.method)
307
 
            except KeyError:
308
 
                # A method we don't know about doesn't count as a VFS method.
309
 
                continue
310
 
            if issubclass(request_method, vfs.VfsRequest):
311
 
                vfs_calls.append(call.call)
312
 
        if len(vfs_calls) == 0:
313
 
            return None
314
 
        return _NoVfsCallsMismatch(vfs_calls)