~bzr-pqm/bzr/bzr.dev

« back to all changes in this revision

Viewing changes to bzrlib/tests/test_graph.py

  • Committer: Andrew Bennetts
  • Date: 2008-02-07 07:05:13 UTC
  • mto: This revision was merged to the branch mainline in revision 3398.
  • Revision ID: andrew.bennetts@canonical.com-20080207070513-u7tvul100g1yn6n7
Add a comment to the new CSS.

Show diffs side-by-side

added added

removed removed

Lines of Context:
 
1
# Copyright (C) 2007 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., 59 Temple Place, Suite 330, Boston, MA  02111-1307  USA
 
16
 
 
17
from bzrlib import (
 
18
    errors,
 
19
    graph as _mod_graph,
 
20
    symbol_versioning,
 
21
    tests,
 
22
    )
 
23
from bzrlib.revision import NULL_REVISION
 
24
from bzrlib.tests import TestCaseWithMemoryTransport
 
25
 
 
26
 
 
27
# Ancestry 1:
 
28
#
 
29
#  NULL_REVISION
 
30
#       |
 
31
#     rev1
 
32
#      /\
 
33
#  rev2a rev2b
 
34
#     |    |
 
35
#   rev3  /
 
36
#     |  /
 
37
#   rev4
 
38
ancestry_1 = {'rev1': [NULL_REVISION], 'rev2a': ['rev1'], 'rev2b': ['rev1'],
 
39
              'rev3': ['rev2a'], 'rev4': ['rev3', 'rev2b']}
 
40
 
 
41
 
 
42
# Ancestry 2:
 
43
#
 
44
#  NULL_REVISION
 
45
#    /    \
 
46
# rev1a  rev1b
 
47
#   |
 
48
# rev2a
 
49
#   |
 
50
# rev3a
 
51
#   |
 
52
# rev4a
 
53
ancestry_2 = {'rev1a': [NULL_REVISION], 'rev2a': ['rev1a'],
 
54
              'rev1b': [NULL_REVISION], 'rev3a': ['rev2a'], 'rev4a': ['rev3a']}
 
55
 
 
56
 
 
57
# Criss cross ancestry
 
58
#
 
59
#     NULL_REVISION
 
60
#         |
 
61
#        rev1
 
62
#        /  \
 
63
#    rev2a  rev2b
 
64
#       |\  /|
 
65
#       |  X |
 
66
#       |/  \|
 
67
#    rev3a  rev3b
 
68
criss_cross = {'rev1': [NULL_REVISION], 'rev2a': ['rev1'], 'rev2b': ['rev1'],
 
69
               'rev3a': ['rev2a', 'rev2b'], 'rev3b': ['rev2b', 'rev2a']}
 
70
 
 
71
 
 
72
# Criss-cross 2
 
73
#
 
74
#  NULL_REVISION
 
75
#    /   \
 
76
# rev1a  rev1b
 
77
#   |\   /|
 
78
#   | \ / |
 
79
#   |  X  |
 
80
#   | / \ |
 
81
#   |/   \|
 
82
# rev2a  rev2b
 
83
criss_cross2 = {'rev1a': [NULL_REVISION], 'rev1b': [NULL_REVISION],
 
84
                'rev2a': ['rev1a', 'rev1b'], 'rev2b': ['rev1b', 'rev1a']}
 
85
 
 
86
 
 
87
# Mainline:
 
88
#
 
89
#  NULL_REVISION
 
90
#       |
 
91
#      rev1
 
92
#      /  \
 
93
#      | rev2b
 
94
#      |  /
 
95
#     rev2a
 
96
mainline = {'rev1': [NULL_REVISION], 'rev2a': ['rev1', 'rev2b'],
 
97
            'rev2b': ['rev1']}
 
98
 
 
99
 
 
100
# feature branch:
 
101
#
 
102
#  NULL_REVISION
 
103
#       |
 
104
#      rev1
 
105
#       |
 
106
#     rev2b
 
107
#       |
 
108
#     rev3b
 
109
feature_branch = {'rev1': [NULL_REVISION],
 
110
                  'rev2b': ['rev1'], 'rev3b': ['rev2b']}
 
111
 
 
112
 
 
113
# History shortcut
 
114
#  NULL_REVISION
 
115
#       |
 
116
#     rev1------
 
117
#     /  \      \
 
118
#  rev2a rev2b rev2c
 
119
#    |  /   \   /
 
120
#  rev3a    rev3b
 
121
history_shortcut = {'rev1': [NULL_REVISION], 'rev2a': ['rev1'],
 
122
                    'rev2b': ['rev1'], 'rev2c': ['rev1'],
 
123
                    'rev3a': ['rev2a', 'rev2b'], 'rev3b': ['rev2b', 'rev2c']}
 
124
 
 
125
# Extended history shortcut
 
126
#  NULL_REVISION
 
127
#       |
 
128
#       a
 
129
#       |\
 
130
#       b |
 
131
#       | |
 
132
#       c |
 
133
#       | |
 
134
#       d |
 
135
#       |\|
 
136
#       e f
 
137
extended_history_shortcut = {'a': [NULL_REVISION],
 
138
                             'b': ['a'],
 
139
                             'c': ['b'],
 
140
                             'd': ['c'],
 
141
                             'e': ['d'],
 
142
                             'f': ['a', 'd'],
 
143
                            }
 
144
 
 
145
# Double shortcut
 
146
# Both sides will see 'A' first, even though it is actually a decendent of a
 
147
# different common revision.
 
148
#
 
149
#  NULL_REVISION
 
150
#       |
 
151
#       a
 
152
#      /|\
 
153
#     / b \
 
154
#    /  |  \
 
155
#   |   c   |
 
156
#   |  / \  |
 
157
#   | d   e |
 
158
#   |/     \|
 
159
#   f       g
 
160
 
 
161
double_shortcut = {'a':[NULL_REVISION], 'b':['a'], 'c':['b'],
 
162
                   'd':['c'], 'e':['c'], 'f':['a', 'd'],
 
163
                   'g':['a', 'e']}
 
164
 
 
165
# Complex shortcut
 
166
# This has a failure mode in that a shortcut will find some nodes in common,
 
167
# but the common searcher won't have time to find that one branch is actually
 
168
# in common. The extra nodes at the top are because we want to avoid
 
169
# walking off the graph. Specifically, node G should be considered common, but
 
170
# is likely to be seen by M long before the common searcher finds it.
 
171
#
 
172
# NULL_REVISION
 
173
#     |
 
174
#     a
 
175
#     |
 
176
#     b
 
177
#     |
 
178
#     c
 
179
#     |
 
180
#     d
 
181
#     |\
 
182
#     e f
 
183
#     | |\
 
184
#     i | h
 
185
#     |\| |
 
186
#     | g |
 
187
#     | | |
 
188
#     | j |
 
189
#     | | |
 
190
#     | k |
 
191
#     | | |
 
192
#     | l |
 
193
#     |/|/
 
194
#     m n
 
195
complex_shortcut = {'d':[NULL_REVISION],
 
196
                    'x':['d'], 'y':['x'],
 
197
                    'e':['y'], 'f':['d'], 'g':['f', 'i'], 'h':['f'],
 
198
                    'i':['e'], 'j':['g'], 'k':['j'],
 
199
                    'l':['k'], 'm':['i', 's'], 'n':['s', 'h'],
 
200
                    'o':['l'], 'p':['o'], 'q':['p'],
 
201
                    'r':['q'], 's':['r'],
 
202
                    }
 
203
 
 
204
# Shortcut with extra root
 
205
# We have a long history shortcut, and an extra root, which is why we can't
 
206
# stop searchers based on seeing NULL_REVISION
 
207
#  NULL_REVISION
 
208
#       |   |
 
209
#       a   |
 
210
#       |\  |
 
211
#       b | |
 
212
#       | | |
 
213
#       c | |
 
214
#       | | |
 
215
#       d | g
 
216
#       |\|/
 
217
#       e f
 
218
shortcut_extra_root = {'a': [NULL_REVISION],
 
219
                       'b': ['a'],
 
220
                       'c': ['b'],
 
221
                       'd': ['c'],
 
222
                       'e': ['d'],
 
223
                       'f': ['a', 'd', 'g'],
 
224
                       'g': [NULL_REVISION],
 
225
                      }
 
226
 
 
227
#  NULL_REVISION
 
228
#       |
 
229
#       f
 
230
#       |
 
231
#       e
 
232
#      / \
 
233
#     b   d
 
234
#     | \ |
 
235
#     a   c
 
236
 
 
237
boundary = {'a': ['b'], 'c': ['b', 'd'], 'b':['e'], 'd':['e'], 'e': ['f'],
 
238
            'f':[NULL_REVISION]}
 
239
 
 
240
 
 
241
class InstrumentedParentsProvider(object):
 
242
 
 
243
    def __init__(self, parents_provider):
 
244
        self.calls = []
 
245
        self._real_parents_provider = parents_provider
 
246
 
 
247
    def get_parent_map(self, nodes):
 
248
        self.calls.extend(nodes)
 
249
        return self._real_parents_provider.get_parent_map(nodes)
 
250
 
 
251
 
 
252
class TestGraph(TestCaseWithMemoryTransport):
 
253
 
 
254
    def make_graph(self, ancestors):
 
255
        return _mod_graph.Graph(_mod_graph.DictParentsProvider(ancestors))
 
256
 
 
257
    def prepare_memory_tree(self, location):
 
258
        tree = self.make_branch_and_memory_tree(location)
 
259
        tree.lock_write()
 
260
        tree.add('.')
 
261
        return tree
 
262
 
 
263
    def build_ancestry(self, tree, ancestors):
 
264
        """Create an ancestry as specified by a graph dict
 
265
 
 
266
        :param tree: A tree to use
 
267
        :param ancestors: a dict of {node: [node_parent, ...]}
 
268
        """
 
269
        pending = [NULL_REVISION]
 
270
        descendants = {}
 
271
        for descendant, parents in ancestors.iteritems():
 
272
            for parent in parents:
 
273
                descendants.setdefault(parent, []).append(descendant)
 
274
        while len(pending) > 0:
 
275
            cur_node = pending.pop()
 
276
            for descendant in descendants.get(cur_node, []):
 
277
                if tree.branch.repository.has_revision(descendant):
 
278
                    continue
 
279
                parents = [p for p in ancestors[descendant] if p is not
 
280
                           NULL_REVISION]
 
281
                if len([p for p in parents if not
 
282
                    tree.branch.repository.has_revision(p)]) > 0:
 
283
                    continue
 
284
                tree.set_parent_ids(parents)
 
285
                if len(parents) > 0:
 
286
                    left_parent = parents[0]
 
287
                else:
 
288
                    left_parent = NULL_REVISION
 
289
                tree.branch.set_last_revision_info(
 
290
                    len(tree.branch._lefthand_history(left_parent)),
 
291
                    left_parent)
 
292
                tree.commit(descendant, rev_id=descendant)
 
293
                pending.append(descendant)
 
294
 
 
295
    def test_lca(self):
 
296
        """Test finding least common ancestor.
 
297
 
 
298
        ancestry_1 should always have a single common ancestor
 
299
        """
 
300
        graph = self.make_graph(ancestry_1)
 
301
        self.assertRaises(errors.InvalidRevisionId, graph.find_lca, None)
 
302
        self.assertEqual(set([NULL_REVISION]),
 
303
                         graph.find_lca(NULL_REVISION, NULL_REVISION))
 
304
        self.assertEqual(set([NULL_REVISION]),
 
305
                         graph.find_lca(NULL_REVISION, 'rev1'))
 
306
        self.assertEqual(set(['rev1']), graph.find_lca('rev1', 'rev1'))
 
307
        self.assertEqual(set(['rev1']), graph.find_lca('rev2a', 'rev2b'))
 
308
 
 
309
    def test_no_unique_lca(self):
 
310
        """Test error when one revision is not in the graph"""
 
311
        graph = self.make_graph(ancestry_1)
 
312
        self.assertRaises(errors.NoCommonAncestor, graph.find_unique_lca,
 
313
                          'rev1', '1rev')
 
314
 
 
315
    def test_lca_criss_cross(self):
 
316
        """Test least-common-ancestor after a criss-cross merge."""
 
317
        graph = self.make_graph(criss_cross)
 
318
        self.assertEqual(set(['rev2a', 'rev2b']),
 
319
                         graph.find_lca('rev3a', 'rev3b'))
 
320
        self.assertEqual(set(['rev2b']),
 
321
                         graph.find_lca('rev3a', 'rev3b', 'rev2b'))
 
322
 
 
323
    def test_lca_shortcut(self):
 
324
        """Test least-common ancestor on this history shortcut"""
 
325
        graph = self.make_graph(history_shortcut)
 
326
        self.assertEqual(set(['rev2b']), graph.find_lca('rev3a', 'rev3b'))
 
327
 
 
328
    def test_recursive_unique_lca(self):
 
329
        """Test finding a unique least common ancestor.
 
330
 
 
331
        ancestry_1 should always have a single common ancestor
 
332
        """
 
333
        graph = self.make_graph(ancestry_1)
 
334
        self.assertEqual(NULL_REVISION,
 
335
                         graph.find_unique_lca(NULL_REVISION, NULL_REVISION))
 
336
        self.assertEqual(NULL_REVISION,
 
337
                         graph.find_unique_lca(NULL_REVISION, 'rev1'))
 
338
        self.assertEqual('rev1', graph.find_unique_lca('rev1', 'rev1'))
 
339
        self.assertEqual('rev1', graph.find_unique_lca('rev2a', 'rev2b'))
 
340
        self.assertEqual(('rev1', 1,),
 
341
                         graph.find_unique_lca('rev2a', 'rev2b',
 
342
                         count_steps=True))
 
343
 
 
344
    def test_unique_lca_criss_cross(self):
 
345
        """Ensure we don't pick non-unique lcas in a criss-cross"""
 
346
        graph = self.make_graph(criss_cross)
 
347
        self.assertEqual('rev1', graph.find_unique_lca('rev3a', 'rev3b'))
 
348
        lca, steps = graph.find_unique_lca('rev3a', 'rev3b', count_steps=True)
 
349
        self.assertEqual('rev1', lca)
 
350
        self.assertEqual(2, steps)
 
351
 
 
352
    def test_unique_lca_null_revision(self):
 
353
        """Ensure we pick NULL_REVISION when necessary"""
 
354
        graph = self.make_graph(criss_cross2)
 
355
        self.assertEqual('rev1b', graph.find_unique_lca('rev2a', 'rev1b'))
 
356
        self.assertEqual(NULL_REVISION,
 
357
                         graph.find_unique_lca('rev2a', 'rev2b'))
 
358
 
 
359
    def test_unique_lca_null_revision2(self):
 
360
        """Ensure we pick NULL_REVISION when necessary"""
 
361
        graph = self.make_graph(ancestry_2)
 
362
        self.assertEqual(NULL_REVISION,
 
363
                         graph.find_unique_lca('rev4a', 'rev1b'))
 
364
 
 
365
    def test_lca_double_shortcut(self):
 
366
        graph = self.make_graph(double_shortcut)
 
367
        self.assertEqual('c', graph.find_unique_lca('f', 'g'))
 
368
 
 
369
    def test_common_ancestor_two_repos(self):
 
370
        """Ensure we do unique_lca using data from two repos"""
 
371
        mainline_tree = self.prepare_memory_tree('mainline')
 
372
        self.build_ancestry(mainline_tree, mainline)
 
373
        self.addCleanup(mainline_tree.unlock)
 
374
 
 
375
        # This is cheating, because the revisions in the graph are actually
 
376
        # different revisions, despite having the same revision-id.
 
377
        feature_tree = self.prepare_memory_tree('feature')
 
378
        self.build_ancestry(feature_tree, feature_branch)
 
379
        self.addCleanup(feature_tree.unlock)
 
380
 
 
381
        graph = mainline_tree.branch.repository.get_graph(
 
382
            feature_tree.branch.repository)
 
383
        self.assertEqual('rev2b', graph.find_unique_lca('rev2a', 'rev3b'))
 
384
 
 
385
    def test_graph_difference(self):
 
386
        graph = self.make_graph(ancestry_1)
 
387
        self.assertEqual((set(), set()), graph.find_difference('rev1', 'rev1'))
 
388
        self.assertEqual((set(), set(['rev1'])),
 
389
                         graph.find_difference(NULL_REVISION, 'rev1'))
 
390
        self.assertEqual((set(['rev1']), set()),
 
391
                         graph.find_difference('rev1', NULL_REVISION))
 
392
        self.assertEqual((set(['rev2a', 'rev3']), set(['rev2b'])),
 
393
                         graph.find_difference('rev3', 'rev2b'))
 
394
        self.assertEqual((set(['rev4', 'rev3', 'rev2a']), set()),
 
395
                         graph.find_difference('rev4', 'rev2b'))
 
396
 
 
397
    def test_graph_difference_separate_ancestry(self):
 
398
        graph = self.make_graph(ancestry_2)
 
399
        self.assertEqual((set(['rev1a']), set(['rev1b'])),
 
400
                         graph.find_difference('rev1a', 'rev1b'))
 
401
        self.assertEqual((set(['rev1a', 'rev2a', 'rev3a', 'rev4a']),
 
402
                          set(['rev1b'])),
 
403
                         graph.find_difference('rev4a', 'rev1b'))
 
404
 
 
405
    def test_graph_difference_criss_cross(self):
 
406
        graph = self.make_graph(criss_cross)
 
407
        self.assertEqual((set(['rev3a']), set(['rev3b'])),
 
408
                         graph.find_difference('rev3a', 'rev3b'))
 
409
        self.assertEqual((set([]), set(['rev3b', 'rev2b'])),
 
410
                         graph.find_difference('rev2a', 'rev3b'))
 
411
 
 
412
    def test_graph_difference_extended_history(self):
 
413
        graph = self.make_graph(extended_history_shortcut)
 
414
        self.expectFailure('find_difference cannot handle shortcuts',
 
415
            self.assertEqual, (set(['e']), set(['f'])),
 
416
                graph.find_difference('e', 'f'))
 
417
        self.assertEqual((set(['e']), set(['f'])),
 
418
                         graph.find_difference('e', 'f'))
 
419
        self.assertEqual((set(['f']), set(['e'])),
 
420
                         graph.find_difference('f', 'e'))
 
421
 
 
422
    def test_graph_difference_double_shortcut(self):
 
423
        graph = self.make_graph(double_shortcut)
 
424
        self.assertEqual((set(['d', 'f']), set(['e', 'g'])),
 
425
                         graph.find_difference('f', 'g'))
 
426
 
 
427
    def test_graph_difference_complex_shortcut(self):
 
428
        graph = self.make_graph(complex_shortcut)
 
429
        self.expectFailure('find_difference cannot handle shortcuts',
 
430
            self.assertEqual, (set(['m']), set(['h', 'n'])),
 
431
                graph.find_difference('m', 'n'))
 
432
        self.assertEqual((set(['m']), set(['h', 'n'])),
 
433
                         graph.find_difference('m', 'n'))
 
434
 
 
435
    def test_graph_difference_shortcut_extra_root(self):
 
436
        graph = self.make_graph(shortcut_extra_root)
 
437
        self.expectFailure('find_difference cannot handle shortcuts',
 
438
            self.assertEqual, (set(['e']), set(['f', 'g'])),
 
439
                graph.find_difference('e', 'f'))
 
440
        self.assertEqual((set(['e']), set(['f', 'g'])),
 
441
                         graph.find_difference('e', 'f'))
 
442
 
 
443
    def test_stacked_parents_provider(self):
 
444
        parents1 = _mod_graph.DictParentsProvider({'rev2': ['rev3']})
 
445
        parents2 = _mod_graph.DictParentsProvider({'rev1': ['rev4']})
 
446
        stacked = _mod_graph._StackedParentsProvider([parents1, parents2])
 
447
        self.assertEqual({'rev1':['rev4'], 'rev2':['rev3']},
 
448
                         stacked.get_parent_map(['rev1', 'rev2']))
 
449
        self.assertEqual({'rev2':['rev3'], 'rev1':['rev4']},
 
450
                         stacked.get_parent_map(['rev2', 'rev1']))
 
451
        self.assertEqual({'rev2':['rev3']},
 
452
                         stacked.get_parent_map(['rev2', 'rev2']))
 
453
        self.assertEqual({'rev1':['rev4']},
 
454
                         stacked.get_parent_map(['rev1', 'rev1']))
 
455
 
 
456
    def test_iter_topo_order(self):
 
457
        graph = self.make_graph(ancestry_1)
 
458
        args = ['rev2a', 'rev3', 'rev1']
 
459
        topo_args = list(graph.iter_topo_order(args))
 
460
        self.assertEqual(set(args), set(topo_args))
 
461
        self.assertTrue(topo_args.index('rev2a') > topo_args.index('rev1'))
 
462
        self.assertTrue(topo_args.index('rev2a') < topo_args.index('rev3'))
 
463
 
 
464
    def test_is_ancestor(self):
 
465
        graph = self.make_graph(ancestry_1)
 
466
        self.assertEqual(True, graph.is_ancestor('null:', 'null:'))
 
467
        self.assertEqual(True, graph.is_ancestor('null:', 'rev1'))
 
468
        self.assertEqual(False, graph.is_ancestor('rev1', 'null:'))
 
469
        self.assertEqual(True, graph.is_ancestor('null:', 'rev4'))
 
470
        self.assertEqual(False, graph.is_ancestor('rev4', 'null:'))
 
471
        self.assertEqual(False, graph.is_ancestor('rev4', 'rev2b'))
 
472
        self.assertEqual(True, graph.is_ancestor('rev2b', 'rev4'))
 
473
        self.assertEqual(False, graph.is_ancestor('rev2b', 'rev3'))
 
474
        self.assertEqual(False, graph.is_ancestor('rev3', 'rev2b'))
 
475
        instrumented_provider = InstrumentedParentsProvider(graph)
 
476
        instrumented_graph = _mod_graph.Graph(instrumented_provider)
 
477
        instrumented_graph.is_ancestor('rev2a', 'rev2b')
 
478
        self.assertTrue('null:' not in instrumented_provider.calls)
 
479
 
 
480
    def test_is_ancestor_boundary(self):
 
481
        """Ensure that we avoid searching the whole graph.
 
482
        
 
483
        This requires searching through b as a common ancestor, so we
 
484
        can identify that e is common.
 
485
        """
 
486
        graph = self.make_graph(boundary)
 
487
        instrumented_provider = InstrumentedParentsProvider(graph)
 
488
        graph = _mod_graph.Graph(instrumented_provider)
 
489
        self.assertFalse(graph.is_ancestor('a', 'c'))
 
490
        self.assertTrue('null:' not in instrumented_provider.calls)
 
491
 
 
492
    def test_filter_candidate_lca(self):
 
493
        """Test filter_candidate_lca for a corner case
 
494
 
 
495
        This tests the case where we encounter the end of iteration for 'e'
 
496
        in the same pass as we discover that 'd' is an ancestor of 'e', and
 
497
        therefore 'e' can't be an lca.
 
498
 
 
499
        To compensate for different dict orderings on other Python
 
500
        implementations, we mirror 'd' and 'e' with 'b' and 'a'.
 
501
        """
 
502
        # This test is sensitive to the iteration order of dicts.  It will
 
503
        # pass incorrectly if 'e' and 'a' sort before 'c'
 
504
        #
 
505
        # NULL_REVISION
 
506
        #     / \
 
507
        #    a   e
 
508
        #    |   |
 
509
        #    b   d
 
510
        #     \ /
 
511
        #      c
 
512
        graph = self.make_graph({'c': ['b', 'd'], 'd': ['e'], 'b': ['a'],
 
513
                                 'a': [NULL_REVISION], 'e': [NULL_REVISION]})
 
514
        self.assertEqual(set(['c']), graph.heads(['a', 'c', 'e']))
 
515
 
 
516
    def test_heads_null(self):
 
517
        graph = self.make_graph(ancestry_1)
 
518
        self.assertEqual(set(['null:']), graph.heads(['null:']))
 
519
        self.assertEqual(set(['rev1']), graph.heads(['null:', 'rev1']))
 
520
        self.assertEqual(set(['rev1']), graph.heads(['rev1', 'null:']))
 
521
        self.assertEqual(set(['rev1']), graph.heads(set(['rev1', 'null:'])))
 
522
        self.assertEqual(set(['rev1']), graph.heads(('rev1', 'null:')))
 
523
 
 
524
    def test_heads_one(self):
 
525
        # A single node will always be a head
 
526
        graph = self.make_graph(ancestry_1)
 
527
        self.assertEqual(set(['null:']), graph.heads(['null:']))
 
528
        self.assertEqual(set(['rev1']), graph.heads(['rev1']))
 
529
        self.assertEqual(set(['rev2a']), graph.heads(['rev2a']))
 
530
        self.assertEqual(set(['rev2b']), graph.heads(['rev2b']))
 
531
        self.assertEqual(set(['rev3']), graph.heads(['rev3']))
 
532
        self.assertEqual(set(['rev4']), graph.heads(['rev4']))
 
533
 
 
534
    def test_heads_single(self):
 
535
        graph = self.make_graph(ancestry_1)
 
536
        self.assertEqual(set(['rev4']), graph.heads(['null:', 'rev4']))
 
537
        self.assertEqual(set(['rev2a']), graph.heads(['rev1', 'rev2a']))
 
538
        self.assertEqual(set(['rev2b']), graph.heads(['rev1', 'rev2b']))
 
539
        self.assertEqual(set(['rev3']), graph.heads(['rev1', 'rev3']))
 
540
        self.assertEqual(set(['rev4']), graph.heads(['rev1', 'rev4']))
 
541
        self.assertEqual(set(['rev4']), graph.heads(['rev2a', 'rev4']))
 
542
        self.assertEqual(set(['rev4']), graph.heads(['rev2b', 'rev4']))
 
543
        self.assertEqual(set(['rev4']), graph.heads(['rev3', 'rev4']))
 
544
 
 
545
    def test_heads_two_heads(self):
 
546
        graph = self.make_graph(ancestry_1)
 
547
        self.assertEqual(set(['rev2a', 'rev2b']),
 
548
                         graph.heads(['rev2a', 'rev2b']))
 
549
        self.assertEqual(set(['rev3', 'rev2b']),
 
550
                         graph.heads(['rev3', 'rev2b']))
 
551
 
 
552
    def test_heads_criss_cross(self):
 
553
        graph = self.make_graph(criss_cross)
 
554
        self.assertEqual(set(['rev2a']),
 
555
                         graph.heads(['rev2a', 'rev1']))
 
556
        self.assertEqual(set(['rev2b']),
 
557
                         graph.heads(['rev2b', 'rev1']))
 
558
        self.assertEqual(set(['rev3a']),
 
559
                         graph.heads(['rev3a', 'rev1']))
 
560
        self.assertEqual(set(['rev3b']),
 
561
                         graph.heads(['rev3b', 'rev1']))
 
562
        self.assertEqual(set(['rev2a', 'rev2b']),
 
563
                         graph.heads(['rev2a', 'rev2b']))
 
564
        self.assertEqual(set(['rev3a']),
 
565
                         graph.heads(['rev3a', 'rev2a']))
 
566
        self.assertEqual(set(['rev3a']),
 
567
                         graph.heads(['rev3a', 'rev2b']))
 
568
        self.assertEqual(set(['rev3a']),
 
569
                         graph.heads(['rev3a', 'rev2a', 'rev2b']))
 
570
        self.assertEqual(set(['rev3b']),
 
571
                         graph.heads(['rev3b', 'rev2a']))
 
572
        self.assertEqual(set(['rev3b']),
 
573
                         graph.heads(['rev3b', 'rev2b']))
 
574
        self.assertEqual(set(['rev3b']),
 
575
                         graph.heads(['rev3b', 'rev2a', 'rev2b']))
 
576
        self.assertEqual(set(['rev3a', 'rev3b']),
 
577
                         graph.heads(['rev3a', 'rev3b']))
 
578
        self.assertEqual(set(['rev3a', 'rev3b']),
 
579
                         graph.heads(['rev3a', 'rev3b', 'rev2a', 'rev2b']))
 
580
 
 
581
    def test_heads_shortcut(self):
 
582
        graph = self.make_graph(history_shortcut)
 
583
 
 
584
        self.assertEqual(set(['rev2a', 'rev2b', 'rev2c']),
 
585
                         graph.heads(['rev2a', 'rev2b', 'rev2c']))
 
586
        self.assertEqual(set(['rev3a', 'rev3b']),
 
587
                         graph.heads(['rev3a', 'rev3b']))
 
588
        self.assertEqual(set(['rev3a', 'rev3b']),
 
589
                         graph.heads(['rev2a', 'rev3a', 'rev3b']))
 
590
        self.assertEqual(set(['rev2a', 'rev3b']),
 
591
                         graph.heads(['rev2a', 'rev3b']))
 
592
        self.assertEqual(set(['rev2c', 'rev3a']),
 
593
                         graph.heads(['rev2c', 'rev3a']))
 
594
 
 
595
    def _run_heads_break_deeper(self, graph_dict, search):
 
596
        """Run heads on a graph-as-a-dict.
 
597
        
 
598
        If the search asks for the parents of 'deeper' the test will fail.
 
599
        """
 
600
        class stub(object):
 
601
            pass
 
602
        def get_parent_map(keys):
 
603
            result = {}
 
604
            for key in keys:
 
605
                if key == 'deeper':
 
606
                    self.fail('key deeper was accessed')
 
607
                result[key] = graph_dict[key]
 
608
            return result
 
609
        an_obj = stub()
 
610
        an_obj.get_parent_map = get_parent_map
 
611
        graph = _mod_graph.Graph(an_obj)
 
612
        return graph.heads(search)
 
613
 
 
614
    def test_heads_limits_search(self):
 
615
        # test that a heads query does not search all of history
 
616
        graph_dict = {
 
617
            'left':['common'],
 
618
            'right':['common'],
 
619
            'common':['deeper'],
 
620
        }
 
621
        self.assertEqual(set(['left', 'right']),
 
622
            self._run_heads_break_deeper(graph_dict, ['left', 'right']))
 
623
 
 
624
    def test_heads_limits_search_assymetric(self):
 
625
        # test that a heads query does not search all of history
 
626
        graph_dict = {
 
627
            'left':['midleft'],
 
628
            'midleft':['common'],
 
629
            'right':['common'],
 
630
            'common':['aftercommon'],
 
631
            'aftercommon':['deeper'],
 
632
        }
 
633
        self.assertEqual(set(['left', 'right']),
 
634
            self._run_heads_break_deeper(graph_dict, ['left', 'right']))
 
635
 
 
636
    def test_heads_limits_search_common_search_must_continue(self):
 
637
        # test that common nodes are still queried, preventing
 
638
        # all-the-way-to-origin behaviour in the following graph:
 
639
        graph_dict = {
 
640
            'h1':['shortcut', 'common1'],
 
641
            'h2':['common1'],
 
642
            'shortcut':['common2'],
 
643
            'common1':['common2'],
 
644
            'common2':['deeper'],
 
645
        }
 
646
        self.assertEqual(set(['h1', 'h2']),
 
647
            self._run_heads_break_deeper(graph_dict, ['h1', 'h2']))
 
648
 
 
649
    def test_breadth_first_search_start_ghosts(self):
 
650
        graph = self.make_graph({})
 
651
        # with_ghosts reports the ghosts
 
652
        search = graph._make_breadth_first_searcher(['a-ghost'])
 
653
        self.assertEqual((set(), set(['a-ghost'])), search.next_with_ghosts())
 
654
        self.assertRaises(StopIteration, search.next_with_ghosts)
 
655
        # next includes them
 
656
        search = graph._make_breadth_first_searcher(['a-ghost'])
 
657
        self.assertEqual(set(['a-ghost']), search.next())
 
658
        self.assertRaises(StopIteration, search.next)
 
659
 
 
660
    def test_breadth_first_search_deep_ghosts(self):
 
661
        graph = self.make_graph({
 
662
            'head':['present'],
 
663
            'present':['child', 'ghost'],
 
664
            'child':[],
 
665
            })
 
666
        # with_ghosts reports the ghosts
 
667
        search = graph._make_breadth_first_searcher(['head'])
 
668
        self.assertEqual((set(['head']), set()), search.next_with_ghosts())
 
669
        self.assertEqual((set(['present']), set()), search.next_with_ghosts())
 
670
        self.assertEqual((set(['child']), set(['ghost'])),
 
671
            search.next_with_ghosts())
 
672
        self.assertRaises(StopIteration, search.next_with_ghosts)
 
673
        # next includes them
 
674
        search = graph._make_breadth_first_searcher(['head'])
 
675
        self.assertEqual(set(['head']), search.next())
 
676
        self.assertEqual(set(['present']), search.next())
 
677
        self.assertEqual(set(['child', 'ghost']),
 
678
            search.next())
 
679
        self.assertRaises(StopIteration, search.next)
 
680
 
 
681
    def test_breadth_first_search_change_next_to_next_with_ghosts(self):
 
682
        # To make the API robust, we allow calling both next() and
 
683
        # next_with_ghosts() on the same searcher.
 
684
        graph = self.make_graph({
 
685
            'head':['present'],
 
686
            'present':['child', 'ghost'],
 
687
            'child':[],
 
688
            })
 
689
        # start with next_with_ghosts
 
690
        search = graph._make_breadth_first_searcher(['head'])
 
691
        self.assertEqual((set(['head']), set()), search.next_with_ghosts())
 
692
        self.assertEqual(set(['present']), search.next())
 
693
        self.assertEqual((set(['child']), set(['ghost'])),
 
694
            search.next_with_ghosts())
 
695
        self.assertRaises(StopIteration, search.next)
 
696
        # start with next
 
697
        search = graph._make_breadth_first_searcher(['head'])
 
698
        self.assertEqual(set(['head']), search.next())
 
699
        self.assertEqual((set(['present']), set()), search.next_with_ghosts())
 
700
        self.assertEqual(set(['child', 'ghost']),
 
701
            search.next())
 
702
        self.assertRaises(StopIteration, search.next_with_ghosts)
 
703
 
 
704
    def test_breadth_first_change_search(self):
 
705
        # Changing the search should work with both next and next_with_ghosts.
 
706
        graph = self.make_graph({
 
707
            'head':['present'],
 
708
            'present':['stopped'],
 
709
            'other':['other_2'],
 
710
            'other_2':[],
 
711
            })
 
712
        search = graph._make_breadth_first_searcher(['head'])
 
713
        self.assertEqual((set(['head']), set()), search.next_with_ghosts())
 
714
        self.assertEqual((set(['present']), set()), search.next_with_ghosts())
 
715
        self.assertEqual(set(['present']),
 
716
            search.stop_searching_any(['present']))
 
717
        self.assertEqual((set(['other']), set(['other_ghost'])),
 
718
            search.start_searching(['other', 'other_ghost']))
 
719
        self.assertEqual((set(['other_2']), set()), search.next_with_ghosts())
 
720
        self.assertRaises(StopIteration, search.next_with_ghosts)
 
721
        # next includes them
 
722
        search = graph._make_breadth_first_searcher(['head'])
 
723
        self.assertEqual(set(['head']), search.next())
 
724
        self.assertEqual(set(['present']), search.next())
 
725
        self.assertEqual(set(['present']),
 
726
            search.stop_searching_any(['present']))
 
727
        search.start_searching(['other', 'other_ghost'])
 
728
        self.assertEqual(set(['other_2']), search.next())
 
729
        self.assertRaises(StopIteration, search.next)
 
730
 
 
731
    def assertSeenAndResult(self, instructions, search, next):
 
732
        """Check the results of .seen and get_result() for a seach.
 
733
 
 
734
        :param instructions: A list of tuples:
 
735
            (seen, recipe, included_keys, starts, stops).
 
736
            seen, recipe and included_keys are results to check on the search
 
737
            and the searches get_result(). starts and stops are parameters to
 
738
            pass to start_searching and stop_searching_any during each
 
739
            iteration, if they are not None.
 
740
        :param search: The search to use.
 
741
        :param next: A callable to advance the search.
 
742
        """
 
743
        for seen, recipe, included_keys, starts, stops in instructions:
 
744
            next()
 
745
            if starts is not None:
 
746
                search.start_searching(starts)
 
747
            if stops is not None:
 
748
                search.stop_searching_any(stops)
 
749
            result = search.get_result()
 
750
            self.assertEqual(recipe, result.get_recipe())
 
751
            self.assertEqual(set(included_keys), result.get_keys())
 
752
            self.assertEqual(seen, search.seen)
 
753
 
 
754
    def test_breadth_first_get_result_excludes_current_pending(self):
 
755
        graph = self.make_graph({
 
756
            'head':['child'],
 
757
            'child':[NULL_REVISION],
 
758
            NULL_REVISION:[],
 
759
            })
 
760
        search = graph._make_breadth_first_searcher(['head'])
 
761
        # At the start, nothing has been seen, to its all excluded:
 
762
        result = search.get_result()
 
763
        self.assertEqual((set(['head']), set(['head']), 0),
 
764
            result.get_recipe())
 
765
        self.assertEqual(set(), result.get_keys())
 
766
        self.assertEqual(set(), search.seen)
 
767
        # using next:
 
768
        expected = [
 
769
            (set(['head']), (set(['head']), set(['child']), 1),
 
770
             ['head'], None, None),
 
771
            (set(['head', 'child']), (set(['head']), set([NULL_REVISION]), 2),
 
772
             ['head', 'child'], None, None),
 
773
            (set(['head', 'child', NULL_REVISION]), (set(['head']), set(), 3),
 
774
             ['head', 'child', NULL_REVISION], None, None),
 
775
            ]
 
776
        self.assertSeenAndResult(expected, search, search.next)
 
777
        # using next_with_ghosts:
 
778
        search = graph._make_breadth_first_searcher(['head'])
 
779
        self.assertSeenAndResult(expected, search, search.next_with_ghosts)
 
780
 
 
781
    def test_breadth_first_get_result_starts_stops(self):
 
782
        graph = self.make_graph({
 
783
            'head':['child'],
 
784
            'child':[NULL_REVISION],
 
785
            'otherhead':['otherchild'],
 
786
            'otherchild':['excluded'],
 
787
            'excluded':[NULL_REVISION],
 
788
            NULL_REVISION:[]
 
789
            })
 
790
        search = graph._make_breadth_first_searcher([])
 
791
        # Starting with nothing and adding a search works:
 
792
        search.start_searching(['head'])
 
793
        # head has been seen:
 
794
        result = search.get_result()
 
795
        self.assertEqual((set(['head']), set(['child']), 1),
 
796
            result.get_recipe())
 
797
        self.assertEqual(set(['head']), result.get_keys())
 
798
        self.assertEqual(set(['head']), search.seen)
 
799
        # using next:
 
800
        expected = [
 
801
            # stop at child, and start a new search at otherhead:
 
802
            # - otherhead counts as seen immediately when start_searching is
 
803
            # called.
 
804
            (set(['head', 'child', 'otherhead']),
 
805
             (set(['head', 'otherhead']), set(['child', 'otherchild']), 2),
 
806
             ['head', 'otherhead'], ['otherhead'], ['child']),
 
807
            (set(['head', 'child', 'otherhead', 'otherchild']),
 
808
             (set(['head', 'otherhead']), set(['child', 'excluded']), 3),
 
809
             ['head', 'otherhead', 'otherchild'], None, None),
 
810
            # stop searching excluded now
 
811
            (set(['head', 'child', 'otherhead', 'otherchild', 'excluded']),
 
812
             (set(['head', 'otherhead']), set(['child', 'excluded']), 3),
 
813
             ['head', 'otherhead', 'otherchild'], None, ['excluded']),
 
814
            ]
 
815
        self.assertSeenAndResult(expected, search, search.next)
 
816
        # using next_with_ghosts:
 
817
        search = graph._make_breadth_first_searcher([])
 
818
        search.start_searching(['head'])
 
819
        self.assertSeenAndResult(expected, search, search.next_with_ghosts)
 
820
 
 
821
    def test_breadth_first_stop_searching_not_queried(self):
 
822
        # A client should be able to say 'stop node X' even if X has not been
 
823
        # returned to the client.
 
824
        graph = self.make_graph({
 
825
            'head':['child', 'ghost1'],
 
826
            'child':[NULL_REVISION],
 
827
            NULL_REVISION:[],
 
828
            })
 
829
        search = graph._make_breadth_first_searcher(['head'])
 
830
        expected = [
 
831
            # NULL_REVISION and ghost1 have not been returned
 
832
            (set(['head']), (set(['head']), set(['child', 'ghost1']), 1),
 
833
             ['head'], None, [NULL_REVISION, 'ghost1']),
 
834
            # ghost1 has been returned, NULL_REVISION is to be returned in the
 
835
            # next iteration.
 
836
            (set(['head', 'child', 'ghost1']),
 
837
             (set(['head']), set(['ghost1', NULL_REVISION]), 2),
 
838
             ['head', 'child'], None, [NULL_REVISION, 'ghost1']),
 
839
            ]
 
840
        self.assertSeenAndResult(expected, search, search.next)
 
841
        # using next_with_ghosts:
 
842
        search = graph._make_breadth_first_searcher(['head'])
 
843
        self.assertSeenAndResult(expected, search, search.next_with_ghosts)
 
844
 
 
845
    def test_breadth_first_get_result_ghosts_are_excluded(self):
 
846
        graph = self.make_graph({
 
847
            'head':['child', 'ghost'],
 
848
            'child':[NULL_REVISION],
 
849
            NULL_REVISION:[],
 
850
            })
 
851
        search = graph._make_breadth_first_searcher(['head'])
 
852
        # using next:
 
853
        expected = [
 
854
            (set(['head']),
 
855
             (set(['head']), set(['ghost', 'child']), 1),
 
856
             ['head'], None, None),
 
857
            (set(['head', 'child', 'ghost']),
 
858
             (set(['head']), set([NULL_REVISION, 'ghost']), 2),
 
859
             ['head', 'child'], None, None),
 
860
            ]
 
861
        self.assertSeenAndResult(expected, search, search.next)
 
862
        # using next_with_ghosts:
 
863
        search = graph._make_breadth_first_searcher(['head'])
 
864
        self.assertSeenAndResult(expected, search, search.next_with_ghosts)
 
865
 
 
866
    def test_breadth_first_get_result_starting_a_ghost_ghost_is_excluded(self):
 
867
        graph = self.make_graph({
 
868
            'head':['child'],
 
869
            'child':[NULL_REVISION],
 
870
            NULL_REVISION:[],
 
871
            })
 
872
        search = graph._make_breadth_first_searcher(['head'])
 
873
        # using next:
 
874
        expected = [
 
875
            (set(['head', 'ghost']),
 
876
             (set(['head', 'ghost']), set(['child', 'ghost']), 1),
 
877
             ['head'], ['ghost'], None),
 
878
            (set(['head', 'child', 'ghost']),
 
879
             (set(['head', 'ghost']), set([NULL_REVISION, 'ghost']), 2),
 
880
             ['head', 'child'], None, None),
 
881
            ]
 
882
        self.assertSeenAndResult(expected, search, search.next)
 
883
        # using next_with_ghosts:
 
884
        search = graph._make_breadth_first_searcher(['head'])
 
885
        self.assertSeenAndResult(expected, search, search.next_with_ghosts)
 
886
 
 
887
    def test_breadth_first_revision_count_includes_NULL_REVISION(self):
 
888
        graph = self.make_graph({
 
889
            'head':[NULL_REVISION],
 
890
            NULL_REVISION:[],
 
891
            })
 
892
        search = graph._make_breadth_first_searcher(['head'])
 
893
        # using next:
 
894
        expected = [
 
895
            (set(['head']),
 
896
             (set(['head']), set([NULL_REVISION]), 1),
 
897
             ['head'], None, None),
 
898
            (set(['head', NULL_REVISION]),
 
899
             (set(['head']), set([]), 2),
 
900
             ['head', NULL_REVISION], None, None),
 
901
            ]
 
902
        self.assertSeenAndResult(expected, search, search.next)
 
903
        # using next_with_ghosts:
 
904
        search = graph._make_breadth_first_searcher(['head'])
 
905
        self.assertSeenAndResult(expected, search, search.next_with_ghosts)
 
906
 
 
907
    def test_breadth_first_search_get_result_after_StopIteration(self):
 
908
        # StopIteration should not invalid anything..
 
909
        graph = self.make_graph({
 
910
            'head':[NULL_REVISION],
 
911
            NULL_REVISION:[],
 
912
            })
 
913
        search = graph._make_breadth_first_searcher(['head'])
 
914
        # using next:
 
915
        expected = [
 
916
            (set(['head']),
 
917
             (set(['head']), set([NULL_REVISION]), 1),
 
918
             ['head'], None, None),
 
919
            (set(['head', 'ghost', NULL_REVISION]),
 
920
             (set(['head', 'ghost']), set(['ghost']), 2),
 
921
             ['head', NULL_REVISION], ['ghost'], None),
 
922
            ]
 
923
        self.assertSeenAndResult(expected, search, search.next)
 
924
        self.assertRaises(StopIteration, search.next)
 
925
        self.assertEqual(set(['head', 'ghost', NULL_REVISION]), search.seen)
 
926
        result = search.get_result()
 
927
        self.assertEqual((set(['ghost', 'head']), set(['ghost']), 2),
 
928
            result.get_recipe())
 
929
        self.assertEqual(set(['head', NULL_REVISION]), result.get_keys())
 
930
        # using next_with_ghosts:
 
931
        search = graph._make_breadth_first_searcher(['head'])
 
932
        self.assertSeenAndResult(expected, search, search.next_with_ghosts)
 
933
        self.assertRaises(StopIteration, search.next)
 
934
        self.assertEqual(set(['head', 'ghost', NULL_REVISION]), search.seen)
 
935
        result = search.get_result()
 
936
        self.assertEqual((set(['ghost', 'head']), set(['ghost']), 2),
 
937
            result.get_recipe())
 
938
        self.assertEqual(set(['head', NULL_REVISION]), result.get_keys())
 
939
 
 
940
 
 
941
class TestCachingParentsProvider(tests.TestCase):
 
942
 
 
943
    def setUp(self):
 
944
        super(TestCachingParentsProvider, self).setUp()
 
945
        dict_pp = _mod_graph.DictParentsProvider({'a':('b',)})
 
946
        self.inst_pp = InstrumentedParentsProvider(dict_pp)
 
947
        self.caching_pp = _mod_graph.CachingParentsProvider(self.inst_pp)
 
948
 
 
949
    def test_get_parent_map(self):
 
950
        """Requesting the same revision should be returned from cache"""
 
951
        self.assertEqual({}, self.caching_pp._cache)
 
952
        self.assertEqual({'a':('b',)}, self.caching_pp.get_parent_map(['a']))
 
953
        self.assertEqual(['a'], self.inst_pp.calls)
 
954
        self.assertEqual({'a':('b',)}, self.caching_pp.get_parent_map(['a']))
 
955
        # No new call, as it should have been returned from the cache
 
956
        self.assertEqual(['a'], self.inst_pp.calls)
 
957
        self.assertEqual({'a':('b',)}, self.caching_pp._cache)
 
958
 
 
959
    def test_get_parent_map_not_present(self):
 
960
        """The cache should also track when a revision doesn't exist"""
 
961
        self.assertEqual({}, self.caching_pp.get_parent_map(['b']))
 
962
        self.assertEqual(['b'], self.inst_pp.calls)
 
963
        self.assertEqual({}, self.caching_pp.get_parent_map(['b']))
 
964
        # No new calls
 
965
        self.assertEqual(['b'], self.inst_pp.calls)
 
966
        self.assertEqual({'b':None}, self.caching_pp._cache)
 
967
 
 
968
    def test_get_parent_map_mixed(self):
 
969
        """Anything that can be returned from cache, should be"""
 
970
        self.assertEqual({}, self.caching_pp.get_parent_map(['b']))
 
971
        self.assertEqual(['b'], self.inst_pp.calls)
 
972
        self.assertEqual({'a':('b',)},
 
973
                         self.caching_pp.get_parent_map(['a', 'b']))
 
974
        self.assertEqual(['b', 'a'], self.inst_pp.calls)
 
975
 
 
976
    def test_get_parent_map_repeated(self):
 
977
        """Asking for the same parent 2x will only forward 1 request."""
 
978
        self.assertEqual({'a':('b',)},
 
979
                         self.caching_pp.get_parent_map(['b', 'a', 'b']))
 
980
        # Use sorted because we don't care about the order, just that each is
 
981
        # only present 1 time.
 
982
        self.assertEqual(['a', 'b'], sorted(self.inst_pp.calls))