1
# Copyright (C) 2007 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., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA
21
from bzrlib.revision import NULL_REVISION
22
from bzrlib.tests import TestCaseWithMemoryTransport
36
ancestry_1 = {'rev1': [NULL_REVISION], 'rev2a': ['rev1'], 'rev2b': ['rev1'],
37
'rev3': ['rev2a'], 'rev4': ['rev3', 'rev2b']}
51
ancestry_2 = {'rev1a': [NULL_REVISION], 'rev2a': ['rev1a'],
52
'rev1b': [NULL_REVISION], 'rev3a': ['rev2a'], 'rev4a': ['rev3a']}
55
# Criss cross ancestry
66
criss_cross = {'rev1': [NULL_REVISION], 'rev2a': ['rev1'], 'rev2b': ['rev1'],
67
'rev3a': ['rev2a', 'rev2b'], 'rev3b': ['rev2b', 'rev2a']}
81
criss_cross2 = {'rev1a': [NULL_REVISION], 'rev1b': [NULL_REVISION],
82
'rev2a': ['rev1a', 'rev1b'], 'rev2b': ['rev1b', 'rev1a']}
94
mainline = {'rev1': [NULL_REVISION], 'rev2a': ['rev1', 'rev2b'],
107
feature_branch = {'rev1': [NULL_REVISION],
108
'rev2b': ['rev1'], 'rev3b': ['rev2b']}
119
history_shortcut = {'rev1': [NULL_REVISION], 'rev2a': ['rev1'],
120
'rev2b': ['rev1'], 'rev2c': ['rev1'],
121
'rev3a': ['rev2a', 'rev2b'], 'rev3b': ['rev2b', 'rev2c']}
133
boundary = {'a': ['b'], 'c': ['b', 'd'], 'b':['e'], 'd':['e'], 'e': ['f'],
137
class InstrumentedParentsProvider(object):
139
def __init__(self, parents_provider):
141
self._real_parents_provider = parents_provider
143
def get_parents(self, nodes):
144
self.calls.extend(nodes)
145
return self._real_parents_provider.get_parents(nodes)
148
class TestGraph(TestCaseWithMemoryTransport):
150
def make_graph(self, ancestors):
151
tree = self.prepare_memory_tree('.')
152
self.build_ancestry(tree, ancestors)
154
return tree.branch.repository.get_graph()
156
def prepare_memory_tree(self, location):
157
tree = self.make_branch_and_memory_tree(location)
162
def build_ancestry(self, tree, ancestors):
163
"""Create an ancestry as specified by a graph dict
165
:param tree: A tree to use
166
:param ancestors: a dict of {node: [node_parent, ...]}
168
pending = [NULL_REVISION]
170
for descendant, parents in ancestors.iteritems():
171
for parent in parents:
172
descendants.setdefault(parent, []).append(descendant)
173
while len(pending) > 0:
174
cur_node = pending.pop()
175
for descendant in descendants.get(cur_node, []):
176
if tree.branch.repository.has_revision(descendant):
178
parents = [p for p in ancestors[descendant] if p is not
180
if len([p for p in parents if not
181
tree.branch.repository.has_revision(p)]) > 0:
183
tree.set_parent_ids(parents)
185
left_parent = parents[0]
187
left_parent = NULL_REVISION
188
tree.branch.set_last_revision_info(
189
len(tree.branch._lefthand_history(left_parent)),
191
tree.commit(descendant, rev_id=descendant)
192
pending.append(descendant)
195
"""Test finding least common ancestor.
197
ancestry_1 should always have a single common ancestor
199
graph = self.make_graph(ancestry_1)
200
self.assertRaises(errors.InvalidRevisionId, graph.find_lca, None)
201
self.assertEqual(set([NULL_REVISION]),
202
graph.find_lca(NULL_REVISION, NULL_REVISION))
203
self.assertEqual(set([NULL_REVISION]),
204
graph.find_lca(NULL_REVISION, 'rev1'))
205
self.assertEqual(set(['rev1']), graph.find_lca('rev1', 'rev1'))
206
self.assertEqual(set(['rev1']), graph.find_lca('rev2a', 'rev2b'))
208
def test_no_unique_lca(self):
209
"""Test error when one revision is not in the graph"""
210
graph = self.make_graph(ancestry_1)
211
self.assertRaises(errors.NoCommonAncestor, graph.find_unique_lca,
214
def test_lca_criss_cross(self):
215
"""Test least-common-ancestor after a criss-cross merge."""
216
graph = self.make_graph(criss_cross)
217
self.assertEqual(set(['rev2a', 'rev2b']),
218
graph.find_lca('rev3a', 'rev3b'))
219
self.assertEqual(set(['rev2b']),
220
graph.find_lca('rev3a', 'rev3b', 'rev2b'))
222
def test_lca_shortcut(self):
223
"""Test least-common ancestor on this history shortcut"""
224
graph = self.make_graph(history_shortcut)
225
self.assertEqual(set(['rev2b']), graph.find_lca('rev3a', 'rev3b'))
227
def test_recursive_unique_lca(self):
228
"""Test finding a unique least common ancestor.
230
ancestry_1 should always have a single common ancestor
232
graph = self.make_graph(ancestry_1)
233
self.assertEqual(NULL_REVISION,
234
graph.find_unique_lca(NULL_REVISION, NULL_REVISION))
235
self.assertEqual(NULL_REVISION,
236
graph.find_unique_lca(NULL_REVISION, 'rev1'))
237
self.assertEqual('rev1', graph.find_unique_lca('rev1', 'rev1'))
238
self.assertEqual('rev1', graph.find_unique_lca('rev2a', 'rev2b'))
240
def test_unique_lca_criss_cross(self):
241
"""Ensure we don't pick non-unique lcas in a criss-cross"""
242
graph = self.make_graph(criss_cross)
243
self.assertEqual('rev1', graph.find_unique_lca('rev3a', 'rev3b'))
245
def test_unique_lca_null_revision(self):
246
"""Ensure we pick NULL_REVISION when necessary"""
247
graph = self.make_graph(criss_cross2)
248
self.assertEqual('rev1b', graph.find_unique_lca('rev2a', 'rev1b'))
249
self.assertEqual(NULL_REVISION,
250
graph.find_unique_lca('rev2a', 'rev2b'))
252
def test_unique_lca_null_revision2(self):
253
"""Ensure we pick NULL_REVISION when necessary"""
254
graph = self.make_graph(ancestry_2)
255
self.assertEqual(NULL_REVISION,
256
graph.find_unique_lca('rev4a', 'rev1b'))
258
def test_common_ancestor_two_repos(self):
259
"""Ensure we do unique_lca using data from two repos"""
260
mainline_tree = self.prepare_memory_tree('mainline')
261
self.build_ancestry(mainline_tree, mainline)
262
mainline_tree.unlock()
264
# This is cheating, because the revisions in the graph are actually
265
# different revisions, despite having the same revision-id.
266
feature_tree = self.prepare_memory_tree('feature')
267
self.build_ancestry(feature_tree, feature_branch)
268
feature_tree.unlock()
269
graph = mainline_tree.branch.repository.get_graph(
270
feature_tree.branch.repository)
271
self.assertEqual('rev2b', graph.find_unique_lca('rev2a', 'rev3b'))
273
def test_graph_difference(self):
274
graph = self.make_graph(ancestry_1)
275
self.assertEqual((set(), set()), graph.find_difference('rev1', 'rev1'))
276
self.assertEqual((set(), set(['rev1'])),
277
graph.find_difference(NULL_REVISION, 'rev1'))
278
self.assertEqual((set(['rev1']), set()),
279
graph.find_difference('rev1', NULL_REVISION))
280
self.assertEqual((set(['rev2a', 'rev3']), set(['rev2b'])),
281
graph.find_difference('rev3', 'rev2b'))
282
self.assertEqual((set(['rev4', 'rev3', 'rev2a']), set()),
283
graph.find_difference('rev4', 'rev2b'))
285
def test_graph_difference_criss_cross(self):
286
graph = self.make_graph(criss_cross)
287
self.assertEqual((set(['rev3a']), set(['rev3b'])),
288
graph.find_difference('rev3a', 'rev3b'))
289
self.assertEqual((set([]), set(['rev3b', 'rev2b'])),
290
graph.find_difference('rev2a', 'rev3b'))
292
def test_stacked_parents_provider(self):
294
class ParentsProvider(object):
296
def __init__(self, ancestry):
297
self.ancestry = ancestry
299
def get_parents(self, revisions):
300
return [self.ancestry.get(r, None) for r in revisions]
302
parents1 = ParentsProvider({'rev2': ['rev3']})
303
parents2 = ParentsProvider({'rev1': ['rev4']})
304
stacked = _mod_graph._StackedParentsProvider([parents1, parents2])
305
self.assertEqual([['rev4',], ['rev3']],
306
stacked.get_parents(['rev1', 'rev2']))
307
self.assertEqual([['rev3',], ['rev4']],
308
stacked.get_parents(['rev2', 'rev1']))
309
self.assertEqual([['rev3',], ['rev3']],
310
stacked.get_parents(['rev2', 'rev2']))
311
self.assertEqual([['rev4',], ['rev4']],
312
stacked.get_parents(['rev1', 'rev1']))
314
def test_iter_topo_order(self):
315
graph = self.make_graph(ancestry_1)
316
args = ['rev2a', 'rev3', 'rev1']
317
topo_args = list(graph.iter_topo_order(args))
318
self.assertEqual(set(args), set(topo_args))
319
self.assertTrue(topo_args.index('rev2a') > topo_args.index('rev1'))
320
self.assertTrue(topo_args.index('rev2a') < topo_args.index('rev3'))
322
def test_is_ancestor(self):
323
graph = self.make_graph(ancestry_1)
324
self.assertEqual(True, graph.is_ancestor('null:', 'null:'))
325
self.assertEqual(True, graph.is_ancestor('null:', 'rev1'))
326
self.assertEqual(False, graph.is_ancestor('rev1', 'null:'))
327
self.assertEqual(True, graph.is_ancestor('null:', 'rev4'))
328
self.assertEqual(False, graph.is_ancestor('rev4', 'null:'))
329
self.assertEqual(False, graph.is_ancestor('rev4', 'rev2b'))
330
self.assertEqual(True, graph.is_ancestor('rev2b', 'rev4'))
331
self.assertEqual(False, graph.is_ancestor('rev2b', 'rev3'))
332
self.assertEqual(False, graph.is_ancestor('rev3', 'rev2b'))
333
instrumented_provider = InstrumentedParentsProvider(graph)
334
instrumented_graph = _mod_graph.Graph(instrumented_provider)
335
instrumented_graph.is_ancestor('rev2a', 'rev2b')
336
self.assertTrue('null:' not in instrumented_provider.calls)
338
def test_is_ancestor_boundary(self):
339
"""Ensure that we avoid searching the whole graph.
341
This requires searching through b as a common ancestor, so we
342
can identify that e is common.
344
graph = self.make_graph(boundary)
345
instrumented_provider = InstrumentedParentsProvider(graph)
346
graph = _mod_graph.Graph(instrumented_provider)
347
self.assertFalse(graph.is_ancestor('a', 'c'))
348
self.assertTrue('null:' not in instrumented_provider.calls)