/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

  • Committer: Jelmer Vernooij
  • Date: 2020-09-02 16:35:18 UTC
  • mto: (7490.40.109 work)
  • mto: This revision was merged to the branch mainline in revision 7526.
  • Revision ID: jelmer@jelmer.uk-20200902163518-sy9f4unbboljphgu
Handle duplicate directories entries for git.

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