1
# Copyright (C) 2010 Canonical Ltd
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.
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.
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
17
"""Matchers for breezy.
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.
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.
34
'RevisionHistoryMatches',
39
revision as _mod_revision,
41
from ..sixish import (
44
from ..tree import InterTree
46
from testtools.matchers import Equals, Mismatch, Matcher
49
class ReturnsUnlockable(Matcher):
50
"""A matcher that checks for the pattern we want lock* methods to have:
52
They should return an object with an unlock() method.
53
Calling that method should unlock the original object.
55
:ivar lockable_thing: The object which can be locked that will be
59
def __init__(self, lockable_thing):
60
Matcher.__init__(self)
61
self.lockable_thing = lockable_thing
64
return ('ReturnsUnlockable(lockable_thing=%s)' %
67
def match(self, lock_method):
68
lock_method().unlock()
69
if self.lockable_thing.is_locked():
70
return _IsLocked(self.lockable_thing)
74
class _IsLocked(Mismatch):
75
"""Something is locked."""
77
def __init__(self, lockable_thing):
78
self.lockable_thing = lockable_thing
81
return "%s is locked" % self.lockable_thing
84
class _AncestryMismatch(Mismatch):
85
"""Ancestry matching mismatch."""
87
def __init__(self, tip_revision, got, expected):
88
self.tip_revision = tip_revision
90
self.expected = expected
93
return "mismatched ancestry for revision %r was %r, expected %r" % (
94
self.tip_revision, self.got, self.expected)
97
class MatchesAncestry(Matcher):
98
"""A matcher that checks the ancestry of a particular revision.
100
:ivar graph: Graph in which to check the ancestry
101
:ivar revision_id: Revision id of the revision
104
def __init__(self, repository, revision_id):
105
Matcher.__init__(self)
106
self.repository = repository
107
self.revision_id = revision_id
110
return ('MatchesAncestry(repository=%r, revision_id=%r)' % (
111
self.repository, self.revision_id))
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),
124
class HasLayout(Matcher):
125
"""A matcher that checks if a tree has a specific layout.
127
:ivar entries: List of expected entries, as (path, file_id) pairs.
130
def __init__(self, entries):
131
Matcher.__init__(self)
132
self.entries = entries
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():
139
path += ie.kind_character()
141
yield (path, ie.file_id)
146
def _strip_unreferenced_directories(entries):
147
"""Strip all directories that don't (in)directly contain any files.
149
:param entries: List of path strings or (path, ie) tuples to process
152
for entry in entries:
153
if isinstance(entry, (str, text_type)):
157
if not path or path[-1] == "/":
159
directories.append((path, entry))
161
# Yield the referenced parent directories
162
for dirpath, direntry in directories:
163
if osutils.is_inside(dirpath, path):
169
return 'HasLayout(%r)' % self.entries
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))
179
entries = self.entries
180
return Equals(entries).match(actual)
183
class HasPathRelations(Matcher):
184
"""Matcher verifies that paths have a relation to those in another tree.
186
:ivar previous_tree: tree to compare to
187
:ivar previous_entries: List of expected entries, as (path, previous_path) pairs.
190
def __init__(self, previous_tree, previous_entries):
191
Matcher.__init__(self)
192
self.previous_tree = previous_tree
193
self.previous_entries = previous_entries
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)
203
if self.previous_tree.is_versioned(path):
208
kind = self.previous_tree.kind(previous_path)
209
if kind == 'directory':
212
yield (u"", previous_path)
214
yield (path + ie.kind_character(), previous_path)
217
def _strip_unreferenced_directories(entries):
218
"""Strip all directories that don't (in)directly contain any files.
220
:param entries: List of path strings or (path, previous_path) tuples to process
222
directory_used = set()
224
for (path, previous_path) in entries:
225
if not path or path[-1] == "/":
227
directories.append((path, previous_path))
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)
238
return 'HasPathRelations(%r, %r)' % (self.previous_tree, self.previous_entries)
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))
246
entries = self.previous_entries
247
if not tree.supports_rename_tracking():
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)
254
class RevisionHistoryMatches(Matcher):
255
"""A matcher that checks if a branch has a specific revision history.
257
:ivar history: Revision history, as list of revisions. Oldest first.
260
def __init__(self, history):
261
Matcher.__init__(self)
262
self.expected = history
265
return 'RevisionHistoryMatches(%r)' % self.expected
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]))
273
return Equals(self.expected).match(history)