/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 bzrlib/tests/test_graph.py

  • Committer: Canonical.com Patch Queue Manager
  • Date: 2008-03-28 06:42:20 UTC
  • mfrom: (3287.6.9 integration)
  • Revision ID: pqm@pqm.ubuntu.com-20080328064220-ongijg78bfqhvbay
Deprecate a number of VersionedFile method calls,
        and Repository.get_revision_graph. (Robert Collins)

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
# A graph that contains a ghost
 
242
#  NULL_REVISION
 
243
#       |
 
244
#       f
 
245
#       |
 
246
#       e   g
 
247
#      / \ /
 
248
#     b   d
 
249
#     | \ |
 
250
#     a   c
 
251
 
 
252
with_ghost = {'a': ['b'], 'c': ['b', 'd'], 'b':['e'], 'd':['e', 'g'],
 
253
              'e': ['f'], 'f':[NULL_REVISION], NULL_REVISION:()}
 
254
 
 
255
 
 
256
 
 
257
class InstrumentedParentsProvider(object):
 
258
 
 
259
    def __init__(self, parents_provider):
 
260
        self.calls = []
 
261
        self._real_parents_provider = parents_provider
 
262
 
 
263
    def get_parent_map(self, nodes):
 
264
        self.calls.extend(nodes)
 
265
        return self._real_parents_provider.get_parent_map(nodes)
 
266
 
 
267
 
 
268
class TestGraph(TestCaseWithMemoryTransport):
 
269
 
 
270
    def make_graph(self, ancestors):
 
271
        return _mod_graph.Graph(_mod_graph.DictParentsProvider(ancestors))
 
272
 
 
273
    def prepare_memory_tree(self, location):
 
274
        tree = self.make_branch_and_memory_tree(location)
 
275
        tree.lock_write()
 
276
        tree.add('.')
 
277
        return tree
 
278
 
 
279
    def build_ancestry(self, tree, ancestors):
 
280
        """Create an ancestry as specified by a graph dict
 
281
 
 
282
        :param tree: A tree to use
 
283
        :param ancestors: a dict of {node: [node_parent, ...]}
 
284
        """
 
285
        pending = [NULL_REVISION]
 
286
        descendants = {}
 
287
        for descendant, parents in ancestors.iteritems():
 
288
            for parent in parents:
 
289
                descendants.setdefault(parent, []).append(descendant)
 
290
        while len(pending) > 0:
 
291
            cur_node = pending.pop()
 
292
            for descendant in descendants.get(cur_node, []):
 
293
                if tree.branch.repository.has_revision(descendant):
 
294
                    continue
 
295
                parents = [p for p in ancestors[descendant] if p is not
 
296
                           NULL_REVISION]
 
297
                if len([p for p in parents if not
 
298
                    tree.branch.repository.has_revision(p)]) > 0:
 
299
                    continue
 
300
                tree.set_parent_ids(parents)
 
301
                if len(parents) > 0:
 
302
                    left_parent = parents[0]
 
303
                else:
 
304
                    left_parent = NULL_REVISION
 
305
                tree.branch.set_last_revision_info(
 
306
                    len(tree.branch._lefthand_history(left_parent)),
 
307
                    left_parent)
 
308
                tree.commit(descendant, rev_id=descendant)
 
309
                pending.append(descendant)
 
310
 
 
311
    def test_lca(self):
 
312
        """Test finding least common ancestor.
 
313
 
 
314
        ancestry_1 should always have a single common ancestor
 
315
        """
 
316
        graph = self.make_graph(ancestry_1)
 
317
        self.assertRaises(errors.InvalidRevisionId, graph.find_lca, None)
 
318
        self.assertEqual(set([NULL_REVISION]),
 
319
                         graph.find_lca(NULL_REVISION, NULL_REVISION))
 
320
        self.assertEqual(set([NULL_REVISION]),
 
321
                         graph.find_lca(NULL_REVISION, 'rev1'))
 
322
        self.assertEqual(set(['rev1']), graph.find_lca('rev1', 'rev1'))
 
323
        self.assertEqual(set(['rev1']), graph.find_lca('rev2a', 'rev2b'))
 
324
 
 
325
    def test_no_unique_lca(self):
 
326
        """Test error when one revision is not in the graph"""
 
327
        graph = self.make_graph(ancestry_1)
 
328
        self.assertRaises(errors.NoCommonAncestor, graph.find_unique_lca,
 
329
                          'rev1', '1rev')
 
330
 
 
331
    def test_lca_criss_cross(self):
 
332
        """Test least-common-ancestor after a criss-cross merge."""
 
333
        graph = self.make_graph(criss_cross)
 
334
        self.assertEqual(set(['rev2a', 'rev2b']),
 
335
                         graph.find_lca('rev3a', 'rev3b'))
 
336
        self.assertEqual(set(['rev2b']),
 
337
                         graph.find_lca('rev3a', 'rev3b', 'rev2b'))
 
338
 
 
339
    def test_lca_shortcut(self):
 
340
        """Test least-common ancestor on this history shortcut"""
 
341
        graph = self.make_graph(history_shortcut)
 
342
        self.assertEqual(set(['rev2b']), graph.find_lca('rev3a', 'rev3b'))
 
343
 
 
344
    def test_recursive_unique_lca(self):
 
345
        """Test finding a unique least common ancestor.
 
346
 
 
347
        ancestry_1 should always have a single common ancestor
 
348
        """
 
349
        graph = self.make_graph(ancestry_1)
 
350
        self.assertEqual(NULL_REVISION,
 
351
                         graph.find_unique_lca(NULL_REVISION, NULL_REVISION))
 
352
        self.assertEqual(NULL_REVISION,
 
353
                         graph.find_unique_lca(NULL_REVISION, 'rev1'))
 
354
        self.assertEqual('rev1', graph.find_unique_lca('rev1', 'rev1'))
 
355
        self.assertEqual('rev1', graph.find_unique_lca('rev2a', 'rev2b'))
 
356
        self.assertEqual(('rev1', 1,),
 
357
                         graph.find_unique_lca('rev2a', 'rev2b',
 
358
                         count_steps=True))
 
359
 
 
360
    def test_unique_lca_criss_cross(self):
 
361
        """Ensure we don't pick non-unique lcas in a criss-cross"""
 
362
        graph = self.make_graph(criss_cross)
 
363
        self.assertEqual('rev1', graph.find_unique_lca('rev3a', 'rev3b'))
 
364
        lca, steps = graph.find_unique_lca('rev3a', 'rev3b', count_steps=True)
 
365
        self.assertEqual('rev1', lca)
 
366
        self.assertEqual(2, steps)
 
367
 
 
368
    def test_unique_lca_null_revision(self):
 
369
        """Ensure we pick NULL_REVISION when necessary"""
 
370
        graph = self.make_graph(criss_cross2)
 
371
        self.assertEqual('rev1b', graph.find_unique_lca('rev2a', 'rev1b'))
 
372
        self.assertEqual(NULL_REVISION,
 
373
                         graph.find_unique_lca('rev2a', 'rev2b'))
 
374
 
 
375
    def test_unique_lca_null_revision2(self):
 
376
        """Ensure we pick NULL_REVISION when necessary"""
 
377
        graph = self.make_graph(ancestry_2)
 
378
        self.assertEqual(NULL_REVISION,
 
379
                         graph.find_unique_lca('rev4a', 'rev1b'))
 
380
 
 
381
    def test_lca_double_shortcut(self):
 
382
        graph = self.make_graph(double_shortcut)
 
383
        self.assertEqual('c', graph.find_unique_lca('f', 'g'))
 
384
 
 
385
    def test_common_ancestor_two_repos(self):
 
386
        """Ensure we do unique_lca using data from two repos"""
 
387
        mainline_tree = self.prepare_memory_tree('mainline')
 
388
        self.build_ancestry(mainline_tree, mainline)
 
389
        self.addCleanup(mainline_tree.unlock)
 
390
 
 
391
        # This is cheating, because the revisions in the graph are actually
 
392
        # different revisions, despite having the same revision-id.
 
393
        feature_tree = self.prepare_memory_tree('feature')
 
394
        self.build_ancestry(feature_tree, feature_branch)
 
395
        self.addCleanup(feature_tree.unlock)
 
396
 
 
397
        graph = mainline_tree.branch.repository.get_graph(
 
398
            feature_tree.branch.repository)
 
399
        self.assertEqual('rev2b', graph.find_unique_lca('rev2a', 'rev3b'))
 
400
 
 
401
    def test_graph_difference(self):
 
402
        graph = self.make_graph(ancestry_1)
 
403
        self.assertEqual((set(), set()), graph.find_difference('rev1', 'rev1'))
 
404
        self.assertEqual((set(), set(['rev1'])),
 
405
                         graph.find_difference(NULL_REVISION, 'rev1'))
 
406
        self.assertEqual((set(['rev1']), set()),
 
407
                         graph.find_difference('rev1', NULL_REVISION))
 
408
        self.assertEqual((set(['rev2a', 'rev3']), set(['rev2b'])),
 
409
                         graph.find_difference('rev3', 'rev2b'))
 
410
        self.assertEqual((set(['rev4', 'rev3', 'rev2a']), set()),
 
411
                         graph.find_difference('rev4', 'rev2b'))
 
412
 
 
413
    def test_graph_difference_separate_ancestry(self):
 
414
        graph = self.make_graph(ancestry_2)
 
415
        self.assertEqual((set(['rev1a']), set(['rev1b'])),
 
416
                         graph.find_difference('rev1a', 'rev1b'))
 
417
        self.assertEqual((set(['rev1a', 'rev2a', 'rev3a', 'rev4a']),
 
418
                          set(['rev1b'])),
 
419
                         graph.find_difference('rev4a', 'rev1b'))
 
420
 
 
421
    def test_graph_difference_criss_cross(self):
 
422
        graph = self.make_graph(criss_cross)
 
423
        self.assertEqual((set(['rev3a']), set(['rev3b'])),
 
424
                         graph.find_difference('rev3a', 'rev3b'))
 
425
        self.assertEqual((set([]), set(['rev3b', 'rev2b'])),
 
426
                         graph.find_difference('rev2a', 'rev3b'))
 
427
 
 
428
    def test_graph_difference_extended_history(self):
 
429
        graph = self.make_graph(extended_history_shortcut)
 
430
        self.expectFailure('find_difference cannot handle shortcuts',
 
431
            self.assertEqual, (set(['e']), set(['f'])),
 
432
                graph.find_difference('e', 'f'))
 
433
        self.assertEqual((set(['e']), set(['f'])),
 
434
                         graph.find_difference('e', 'f'))
 
435
        self.assertEqual((set(['f']), set(['e'])),
 
436
                         graph.find_difference('f', 'e'))
 
437
 
 
438
    def test_graph_difference_double_shortcut(self):
 
439
        graph = self.make_graph(double_shortcut)
 
440
        self.assertEqual((set(['d', 'f']), set(['e', 'g'])),
 
441
                         graph.find_difference('f', 'g'))
 
442
 
 
443
    def test_graph_difference_complex_shortcut(self):
 
444
        graph = self.make_graph(complex_shortcut)
 
445
        self.expectFailure('find_difference cannot handle shortcuts',
 
446
            self.assertEqual, (set(['m']), set(['h', 'n'])),
 
447
                graph.find_difference('m', 'n'))
 
448
        self.assertEqual((set(['m']), set(['h', 'n'])),
 
449
                         graph.find_difference('m', 'n'))
 
450
 
 
451
    def test_graph_difference_shortcut_extra_root(self):
 
452
        graph = self.make_graph(shortcut_extra_root)
 
453
        self.expectFailure('find_difference cannot handle shortcuts',
 
454
            self.assertEqual, (set(['e']), set(['f', 'g'])),
 
455
                graph.find_difference('e', 'f'))
 
456
        self.assertEqual((set(['e']), set(['f', 'g'])),
 
457
                         graph.find_difference('e', 'f'))
 
458
 
 
459
    def test_stacked_parents_provider(self):
 
460
        parents1 = _mod_graph.DictParentsProvider({'rev2': ['rev3']})
 
461
        parents2 = _mod_graph.DictParentsProvider({'rev1': ['rev4']})
 
462
        stacked = _mod_graph._StackedParentsProvider([parents1, parents2])
 
463
        self.assertEqual({'rev1':['rev4'], 'rev2':['rev3']},
 
464
                         stacked.get_parent_map(['rev1', 'rev2']))
 
465
        self.assertEqual({'rev2':['rev3'], 'rev1':['rev4']},
 
466
                         stacked.get_parent_map(['rev2', 'rev1']))
 
467
        self.assertEqual({'rev2':['rev3']},
 
468
                         stacked.get_parent_map(['rev2', 'rev2']))
 
469
        self.assertEqual({'rev1':['rev4']},
 
470
                         stacked.get_parent_map(['rev1', 'rev1']))
 
471
 
 
472
    def test_iter_topo_order(self):
 
473
        graph = self.make_graph(ancestry_1)
 
474
        args = ['rev2a', 'rev3', 'rev1']
 
475
        topo_args = list(graph.iter_topo_order(args))
 
476
        self.assertEqual(set(args), set(topo_args))
 
477
        self.assertTrue(topo_args.index('rev2a') > topo_args.index('rev1'))
 
478
        self.assertTrue(topo_args.index('rev2a') < topo_args.index('rev3'))
 
479
 
 
480
    def test_is_ancestor(self):
 
481
        graph = self.make_graph(ancestry_1)
 
482
        self.assertEqual(True, graph.is_ancestor('null:', 'null:'))
 
483
        self.assertEqual(True, graph.is_ancestor('null:', 'rev1'))
 
484
        self.assertEqual(False, graph.is_ancestor('rev1', 'null:'))
 
485
        self.assertEqual(True, graph.is_ancestor('null:', 'rev4'))
 
486
        self.assertEqual(False, graph.is_ancestor('rev4', 'null:'))
 
487
        self.assertEqual(False, graph.is_ancestor('rev4', 'rev2b'))
 
488
        self.assertEqual(True, graph.is_ancestor('rev2b', 'rev4'))
 
489
        self.assertEqual(False, graph.is_ancestor('rev2b', 'rev3'))
 
490
        self.assertEqual(False, graph.is_ancestor('rev3', 'rev2b'))
 
491
        instrumented_provider = InstrumentedParentsProvider(graph)
 
492
        instrumented_graph = _mod_graph.Graph(instrumented_provider)
 
493
        instrumented_graph.is_ancestor('rev2a', 'rev2b')
 
494
        self.assertTrue('null:' not in instrumented_provider.calls)
 
495
 
 
496
    def test_is_ancestor_boundary(self):
 
497
        """Ensure that we avoid searching the whole graph.
 
498
        
 
499
        This requires searching through b as a common ancestor, so we
 
500
        can identify that e is common.
 
501
        """
 
502
        graph = self.make_graph(boundary)
 
503
        instrumented_provider = InstrumentedParentsProvider(graph)
 
504
        graph = _mod_graph.Graph(instrumented_provider)
 
505
        self.assertFalse(graph.is_ancestor('a', 'c'))
 
506
        self.assertTrue('null:' not in instrumented_provider.calls)
 
507
 
 
508
    def test_iter_ancestry(self):
 
509
        nodes = boundary.copy()
 
510
        nodes[NULL_REVISION] = ()
 
511
        graph = self.make_graph(nodes)
 
512
        expected = nodes.copy()
 
513
        expected.pop('a') # 'a' is not in the ancestry of 'c', all the
 
514
                          # other nodes are
 
515
        self.assertEqual(expected, dict(graph.iter_ancestry(['c'])))
 
516
        self.assertEqual(nodes, dict(graph.iter_ancestry(['a', 'c'])))
 
517
 
 
518
    def test_iter_ancestry_with_ghost(self):
 
519
        graph = self.make_graph(with_ghost)
 
520
        expected = with_ghost.copy()
 
521
        # 'a' is not in the ancestry of 'c', and 'g' is a ghost
 
522
        expected['g'] = None
 
523
        self.assertEqual(expected, dict(graph.iter_ancestry(['a', 'c'])))
 
524
        expected.pop('a') 
 
525
        self.assertEqual(expected, dict(graph.iter_ancestry(['c'])))
 
526
 
 
527
    def test_filter_candidate_lca(self):
 
528
        """Test filter_candidate_lca for a corner case
 
529
 
 
530
        This tests the case where we encounter the end of iteration for 'e'
 
531
        in the same pass as we discover that 'd' is an ancestor of 'e', and
 
532
        therefore 'e' can't be an lca.
 
533
 
 
534
        To compensate for different dict orderings on other Python
 
535
        implementations, we mirror 'd' and 'e' with 'b' and 'a'.
 
536
        """
 
537
        # This test is sensitive to the iteration order of dicts.  It will
 
538
        # pass incorrectly if 'e' and 'a' sort before 'c'
 
539
        #
 
540
        # NULL_REVISION
 
541
        #     / \
 
542
        #    a   e
 
543
        #    |   |
 
544
        #    b   d
 
545
        #     \ /
 
546
        #      c
 
547
        graph = self.make_graph({'c': ['b', 'd'], 'd': ['e'], 'b': ['a'],
 
548
                                 'a': [NULL_REVISION], 'e': [NULL_REVISION]})
 
549
        self.assertEqual(set(['c']), graph.heads(['a', 'c', 'e']))
 
550
 
 
551
    def test_heads_null(self):
 
552
        graph = self.make_graph(ancestry_1)
 
553
        self.assertEqual(set(['null:']), graph.heads(['null:']))
 
554
        self.assertEqual(set(['rev1']), graph.heads(['null:', 'rev1']))
 
555
        self.assertEqual(set(['rev1']), graph.heads(['rev1', 'null:']))
 
556
        self.assertEqual(set(['rev1']), graph.heads(set(['rev1', 'null:'])))
 
557
        self.assertEqual(set(['rev1']), graph.heads(('rev1', 'null:')))
 
558
 
 
559
    def test_heads_one(self):
 
560
        # A single node will always be a head
 
561
        graph = self.make_graph(ancestry_1)
 
562
        self.assertEqual(set(['null:']), graph.heads(['null:']))
 
563
        self.assertEqual(set(['rev1']), graph.heads(['rev1']))
 
564
        self.assertEqual(set(['rev2a']), graph.heads(['rev2a']))
 
565
        self.assertEqual(set(['rev2b']), graph.heads(['rev2b']))
 
566
        self.assertEqual(set(['rev3']), graph.heads(['rev3']))
 
567
        self.assertEqual(set(['rev4']), graph.heads(['rev4']))
 
568
 
 
569
    def test_heads_single(self):
 
570
        graph = self.make_graph(ancestry_1)
 
571
        self.assertEqual(set(['rev4']), graph.heads(['null:', 'rev4']))
 
572
        self.assertEqual(set(['rev2a']), graph.heads(['rev1', 'rev2a']))
 
573
        self.assertEqual(set(['rev2b']), graph.heads(['rev1', 'rev2b']))
 
574
        self.assertEqual(set(['rev3']), graph.heads(['rev1', 'rev3']))
 
575
        self.assertEqual(set(['rev4']), graph.heads(['rev1', 'rev4']))
 
576
        self.assertEqual(set(['rev4']), graph.heads(['rev2a', 'rev4']))
 
577
        self.assertEqual(set(['rev4']), graph.heads(['rev2b', 'rev4']))
 
578
        self.assertEqual(set(['rev4']), graph.heads(['rev3', 'rev4']))
 
579
 
 
580
    def test_heads_two_heads(self):
 
581
        graph = self.make_graph(ancestry_1)
 
582
        self.assertEqual(set(['rev2a', 'rev2b']),
 
583
                         graph.heads(['rev2a', 'rev2b']))
 
584
        self.assertEqual(set(['rev3', 'rev2b']),
 
585
                         graph.heads(['rev3', 'rev2b']))
 
586
 
 
587
    def test_heads_criss_cross(self):
 
588
        graph = self.make_graph(criss_cross)
 
589
        self.assertEqual(set(['rev2a']),
 
590
                         graph.heads(['rev2a', 'rev1']))
 
591
        self.assertEqual(set(['rev2b']),
 
592
                         graph.heads(['rev2b', 'rev1']))
 
593
        self.assertEqual(set(['rev3a']),
 
594
                         graph.heads(['rev3a', 'rev1']))
 
595
        self.assertEqual(set(['rev3b']),
 
596
                         graph.heads(['rev3b', 'rev1']))
 
597
        self.assertEqual(set(['rev2a', 'rev2b']),
 
598
                         graph.heads(['rev2a', 'rev2b']))
 
599
        self.assertEqual(set(['rev3a']),
 
600
                         graph.heads(['rev3a', 'rev2a']))
 
601
        self.assertEqual(set(['rev3a']),
 
602
                         graph.heads(['rev3a', 'rev2b']))
 
603
        self.assertEqual(set(['rev3a']),
 
604
                         graph.heads(['rev3a', 'rev2a', 'rev2b']))
 
605
        self.assertEqual(set(['rev3b']),
 
606
                         graph.heads(['rev3b', 'rev2a']))
 
607
        self.assertEqual(set(['rev3b']),
 
608
                         graph.heads(['rev3b', 'rev2b']))
 
609
        self.assertEqual(set(['rev3b']),
 
610
                         graph.heads(['rev3b', 'rev2a', 'rev2b']))
 
611
        self.assertEqual(set(['rev3a', 'rev3b']),
 
612
                         graph.heads(['rev3a', 'rev3b']))
 
613
        self.assertEqual(set(['rev3a', 'rev3b']),
 
614
                         graph.heads(['rev3a', 'rev3b', 'rev2a', 'rev2b']))
 
615
 
 
616
    def test_heads_shortcut(self):
 
617
        graph = self.make_graph(history_shortcut)
 
618
 
 
619
        self.assertEqual(set(['rev2a', 'rev2b', 'rev2c']),
 
620
                         graph.heads(['rev2a', 'rev2b', 'rev2c']))
 
621
        self.assertEqual(set(['rev3a', 'rev3b']),
 
622
                         graph.heads(['rev3a', 'rev3b']))
 
623
        self.assertEqual(set(['rev3a', 'rev3b']),
 
624
                         graph.heads(['rev2a', 'rev3a', 'rev3b']))
 
625
        self.assertEqual(set(['rev2a', 'rev3b']),
 
626
                         graph.heads(['rev2a', 'rev3b']))
 
627
        self.assertEqual(set(['rev2c', 'rev3a']),
 
628
                         graph.heads(['rev2c', 'rev3a']))
 
629
 
 
630
    def _run_heads_break_deeper(self, graph_dict, search):
 
631
        """Run heads on a graph-as-a-dict.
 
632
        
 
633
        If the search asks for the parents of 'deeper' the test will fail.
 
634
        """
 
635
        class stub(object):
 
636
            pass
 
637
        def get_parent_map(keys):
 
638
            result = {}
 
639
            for key in keys:
 
640
                if key == 'deeper':
 
641
                    self.fail('key deeper was accessed')
 
642
                result[key] = graph_dict[key]
 
643
            return result
 
644
        an_obj = stub()
 
645
        an_obj.get_parent_map = get_parent_map
 
646
        graph = _mod_graph.Graph(an_obj)
 
647
        return graph.heads(search)
 
648
 
 
649
    def test_heads_limits_search(self):
 
650
        # test that a heads query does not search all of history
 
651
        graph_dict = {
 
652
            'left':['common'],
 
653
            'right':['common'],
 
654
            'common':['deeper'],
 
655
        }
 
656
        self.assertEqual(set(['left', 'right']),
 
657
            self._run_heads_break_deeper(graph_dict, ['left', 'right']))
 
658
 
 
659
    def test_heads_limits_search_assymetric(self):
 
660
        # test that a heads query does not search all of history
 
661
        graph_dict = {
 
662
            'left':['midleft'],
 
663
            'midleft':['common'],
 
664
            'right':['common'],
 
665
            'common':['aftercommon'],
 
666
            'aftercommon':['deeper'],
 
667
        }
 
668
        self.assertEqual(set(['left', 'right']),
 
669
            self._run_heads_break_deeper(graph_dict, ['left', 'right']))
 
670
 
 
671
    def test_heads_limits_search_common_search_must_continue(self):
 
672
        # test that common nodes are still queried, preventing
 
673
        # all-the-way-to-origin behaviour in the following graph:
 
674
        graph_dict = {
 
675
            'h1':['shortcut', 'common1'],
 
676
            'h2':['common1'],
 
677
            'shortcut':['common2'],
 
678
            'common1':['common2'],
 
679
            'common2':['deeper'],
 
680
        }
 
681
        self.assertEqual(set(['h1', 'h2']),
 
682
            self._run_heads_break_deeper(graph_dict, ['h1', 'h2']))
 
683
 
 
684
    def test_breadth_first_search_start_ghosts(self):
 
685
        graph = self.make_graph({})
 
686
        # with_ghosts reports the ghosts
 
687
        search = graph._make_breadth_first_searcher(['a-ghost'])
 
688
        self.assertEqual((set(), set(['a-ghost'])), search.next_with_ghosts())
 
689
        self.assertRaises(StopIteration, search.next_with_ghosts)
 
690
        # next includes them
 
691
        search = graph._make_breadth_first_searcher(['a-ghost'])
 
692
        self.assertEqual(set(['a-ghost']), search.next())
 
693
        self.assertRaises(StopIteration, search.next)
 
694
 
 
695
    def test_breadth_first_search_deep_ghosts(self):
 
696
        graph = self.make_graph({
 
697
            'head':['present'],
 
698
            'present':['child', 'ghost'],
 
699
            'child':[],
 
700
            })
 
701
        # with_ghosts reports the ghosts
 
702
        search = graph._make_breadth_first_searcher(['head'])
 
703
        self.assertEqual((set(['head']), set()), search.next_with_ghosts())
 
704
        self.assertEqual((set(['present']), set()), search.next_with_ghosts())
 
705
        self.assertEqual((set(['child']), set(['ghost'])),
 
706
            search.next_with_ghosts())
 
707
        self.assertRaises(StopIteration, search.next_with_ghosts)
 
708
        # next includes them
 
709
        search = graph._make_breadth_first_searcher(['head'])
 
710
        self.assertEqual(set(['head']), search.next())
 
711
        self.assertEqual(set(['present']), search.next())
 
712
        self.assertEqual(set(['child', 'ghost']),
 
713
            search.next())
 
714
        self.assertRaises(StopIteration, search.next)
 
715
 
 
716
    def test_breadth_first_search_change_next_to_next_with_ghosts(self):
 
717
        # To make the API robust, we allow calling both next() and
 
718
        # next_with_ghosts() on the same searcher.
 
719
        graph = self.make_graph({
 
720
            'head':['present'],
 
721
            'present':['child', 'ghost'],
 
722
            'child':[],
 
723
            })
 
724
        # start with next_with_ghosts
 
725
        search = graph._make_breadth_first_searcher(['head'])
 
726
        self.assertEqual((set(['head']), set()), search.next_with_ghosts())
 
727
        self.assertEqual(set(['present']), search.next())
 
728
        self.assertEqual((set(['child']), set(['ghost'])),
 
729
            search.next_with_ghosts())
 
730
        self.assertRaises(StopIteration, search.next)
 
731
        # start with next
 
732
        search = graph._make_breadth_first_searcher(['head'])
 
733
        self.assertEqual(set(['head']), search.next())
 
734
        self.assertEqual((set(['present']), set()), search.next_with_ghosts())
 
735
        self.assertEqual(set(['child', 'ghost']),
 
736
            search.next())
 
737
        self.assertRaises(StopIteration, search.next_with_ghosts)
 
738
 
 
739
    def test_breadth_first_change_search(self):
 
740
        # Changing the search should work with both next and next_with_ghosts.
 
741
        graph = self.make_graph({
 
742
            'head':['present'],
 
743
            'present':['stopped'],
 
744
            'other':['other_2'],
 
745
            'other_2':[],
 
746
            })
 
747
        search = graph._make_breadth_first_searcher(['head'])
 
748
        self.assertEqual((set(['head']), set()), search.next_with_ghosts())
 
749
        self.assertEqual((set(['present']), set()), search.next_with_ghosts())
 
750
        self.assertEqual(set(['present']),
 
751
            search.stop_searching_any(['present']))
 
752
        self.assertEqual((set(['other']), set(['other_ghost'])),
 
753
            search.start_searching(['other', 'other_ghost']))
 
754
        self.assertEqual((set(['other_2']), set()), search.next_with_ghosts())
 
755
        self.assertRaises(StopIteration, search.next_with_ghosts)
 
756
        # next includes them
 
757
        search = graph._make_breadth_first_searcher(['head'])
 
758
        self.assertEqual(set(['head']), search.next())
 
759
        self.assertEqual(set(['present']), search.next())
 
760
        self.assertEqual(set(['present']),
 
761
            search.stop_searching_any(['present']))
 
762
        search.start_searching(['other', 'other_ghost'])
 
763
        self.assertEqual(set(['other_2']), search.next())
 
764
        self.assertRaises(StopIteration, search.next)
 
765
 
 
766
    def assertSeenAndResult(self, instructions, search, next):
 
767
        """Check the results of .seen and get_result() for a seach.
 
768
 
 
769
        :param instructions: A list of tuples:
 
770
            (seen, recipe, included_keys, starts, stops).
 
771
            seen, recipe and included_keys are results to check on the search
 
772
            and the searches get_result(). starts and stops are parameters to
 
773
            pass to start_searching and stop_searching_any during each
 
774
            iteration, if they are not None.
 
775
        :param search: The search to use.
 
776
        :param next: A callable to advance the search.
 
777
        """
 
778
        for seen, recipe, included_keys, starts, stops in instructions:
 
779
            next()
 
780
            if starts is not None:
 
781
                search.start_searching(starts)
 
782
            if stops is not None:
 
783
                search.stop_searching_any(stops)
 
784
            result = search.get_result()
 
785
            self.assertEqual(recipe, result.get_recipe())
 
786
            self.assertEqual(set(included_keys), result.get_keys())
 
787
            self.assertEqual(seen, search.seen)
 
788
 
 
789
    def test_breadth_first_get_result_excludes_current_pending(self):
 
790
        graph = self.make_graph({
 
791
            'head':['child'],
 
792
            'child':[NULL_REVISION],
 
793
            NULL_REVISION:[],
 
794
            })
 
795
        search = graph._make_breadth_first_searcher(['head'])
 
796
        # At the start, nothing has been seen, to its all excluded:
 
797
        result = search.get_result()
 
798
        self.assertEqual((set(['head']), set(['head']), 0),
 
799
            result.get_recipe())
 
800
        self.assertEqual(set(), result.get_keys())
 
801
        self.assertEqual(set(), search.seen)
 
802
        # using next:
 
803
        expected = [
 
804
            (set(['head']), (set(['head']), set(['child']), 1),
 
805
             ['head'], None, None),
 
806
            (set(['head', 'child']), (set(['head']), set([NULL_REVISION]), 2),
 
807
             ['head', 'child'], None, None),
 
808
            (set(['head', 'child', NULL_REVISION]), (set(['head']), set(), 3),
 
809
             ['head', 'child', NULL_REVISION], None, None),
 
810
            ]
 
811
        self.assertSeenAndResult(expected, search, search.next)
 
812
        # using next_with_ghosts:
 
813
        search = graph._make_breadth_first_searcher(['head'])
 
814
        self.assertSeenAndResult(expected, search, search.next_with_ghosts)
 
815
 
 
816
    def test_breadth_first_get_result_starts_stops(self):
 
817
        graph = self.make_graph({
 
818
            'head':['child'],
 
819
            'child':[NULL_REVISION],
 
820
            'otherhead':['otherchild'],
 
821
            'otherchild':['excluded'],
 
822
            'excluded':[NULL_REVISION],
 
823
            NULL_REVISION:[]
 
824
            })
 
825
        search = graph._make_breadth_first_searcher([])
 
826
        # Starting with nothing and adding a search works:
 
827
        search.start_searching(['head'])
 
828
        # head has been seen:
 
829
        result = search.get_result()
 
830
        self.assertEqual((set(['head']), set(['child']), 1),
 
831
            result.get_recipe())
 
832
        self.assertEqual(set(['head']), result.get_keys())
 
833
        self.assertEqual(set(['head']), search.seen)
 
834
        # using next:
 
835
        expected = [
 
836
            # stop at child, and start a new search at otherhead:
 
837
            # - otherhead counts as seen immediately when start_searching is
 
838
            # called.
 
839
            (set(['head', 'child', 'otherhead']),
 
840
             (set(['head', 'otherhead']), set(['child', 'otherchild']), 2),
 
841
             ['head', 'otherhead'], ['otherhead'], ['child']),
 
842
            (set(['head', 'child', 'otherhead', 'otherchild']),
 
843
             (set(['head', 'otherhead']), set(['child', 'excluded']), 3),
 
844
             ['head', 'otherhead', 'otherchild'], None, None),
 
845
            # stop searching excluded now
 
846
            (set(['head', 'child', 'otherhead', 'otherchild', 'excluded']),
 
847
             (set(['head', 'otherhead']), set(['child', 'excluded']), 3),
 
848
             ['head', 'otherhead', 'otherchild'], None, ['excluded']),
 
849
            ]
 
850
        self.assertSeenAndResult(expected, search, search.next)
 
851
        # using next_with_ghosts:
 
852
        search = graph._make_breadth_first_searcher([])
 
853
        search.start_searching(['head'])
 
854
        self.assertSeenAndResult(expected, search, search.next_with_ghosts)
 
855
 
 
856
    def test_breadth_first_stop_searching_not_queried(self):
 
857
        # A client should be able to say 'stop node X' even if X has not been
 
858
        # returned to the client.
 
859
        graph = self.make_graph({
 
860
            'head':['child', 'ghost1'],
 
861
            'child':[NULL_REVISION],
 
862
            NULL_REVISION:[],
 
863
            })
 
864
        search = graph._make_breadth_first_searcher(['head'])
 
865
        expected = [
 
866
            # NULL_REVISION and ghost1 have not been returned
 
867
            (set(['head']), (set(['head']), set(['child', 'ghost1']), 1),
 
868
             ['head'], None, [NULL_REVISION, 'ghost1']),
 
869
            # ghost1 has been returned, NULL_REVISION is to be returned in the
 
870
            # next iteration.
 
871
            (set(['head', 'child', 'ghost1']),
 
872
             (set(['head']), set(['ghost1', NULL_REVISION]), 2),
 
873
             ['head', 'child'], None, [NULL_REVISION, 'ghost1']),
 
874
            ]
 
875
        self.assertSeenAndResult(expected, search, search.next)
 
876
        # using next_with_ghosts:
 
877
        search = graph._make_breadth_first_searcher(['head'])
 
878
        self.assertSeenAndResult(expected, search, search.next_with_ghosts)
 
879
 
 
880
    def test_breadth_first_get_result_ghosts_are_excluded(self):
 
881
        graph = self.make_graph({
 
882
            'head':['child', 'ghost'],
 
883
            'child':[NULL_REVISION],
 
884
            NULL_REVISION:[],
 
885
            })
 
886
        search = graph._make_breadth_first_searcher(['head'])
 
887
        # using next:
 
888
        expected = [
 
889
            (set(['head']),
 
890
             (set(['head']), set(['ghost', 'child']), 1),
 
891
             ['head'], None, None),
 
892
            (set(['head', 'child', 'ghost']),
 
893
             (set(['head']), set([NULL_REVISION, 'ghost']), 2),
 
894
             ['head', 'child'], None, None),
 
895
            ]
 
896
        self.assertSeenAndResult(expected, search, search.next)
 
897
        # using next_with_ghosts:
 
898
        search = graph._make_breadth_first_searcher(['head'])
 
899
        self.assertSeenAndResult(expected, search, search.next_with_ghosts)
 
900
 
 
901
    def test_breadth_first_get_result_starting_a_ghost_ghost_is_excluded(self):
 
902
        graph = self.make_graph({
 
903
            'head':['child'],
 
904
            'child':[NULL_REVISION],
 
905
            NULL_REVISION:[],
 
906
            })
 
907
        search = graph._make_breadth_first_searcher(['head'])
 
908
        # using next:
 
909
        expected = [
 
910
            (set(['head', 'ghost']),
 
911
             (set(['head', 'ghost']), set(['child', 'ghost']), 1),
 
912
             ['head'], ['ghost'], None),
 
913
            (set(['head', 'child', 'ghost']),
 
914
             (set(['head', 'ghost']), set([NULL_REVISION, 'ghost']), 2),
 
915
             ['head', 'child'], None, None),
 
916
            ]
 
917
        self.assertSeenAndResult(expected, search, search.next)
 
918
        # using next_with_ghosts:
 
919
        search = graph._make_breadth_first_searcher(['head'])
 
920
        self.assertSeenAndResult(expected, search, search.next_with_ghosts)
 
921
 
 
922
    def test_breadth_first_revision_count_includes_NULL_REVISION(self):
 
923
        graph = self.make_graph({
 
924
            'head':[NULL_REVISION],
 
925
            NULL_REVISION:[],
 
926
            })
 
927
        search = graph._make_breadth_first_searcher(['head'])
 
928
        # using next:
 
929
        expected = [
 
930
            (set(['head']),
 
931
             (set(['head']), set([NULL_REVISION]), 1),
 
932
             ['head'], None, None),
 
933
            (set(['head', NULL_REVISION]),
 
934
             (set(['head']), set([]), 2),
 
935
             ['head', NULL_REVISION], None, None),
 
936
            ]
 
937
        self.assertSeenAndResult(expected, search, search.next)
 
938
        # using next_with_ghosts:
 
939
        search = graph._make_breadth_first_searcher(['head'])
 
940
        self.assertSeenAndResult(expected, search, search.next_with_ghosts)
 
941
 
 
942
    def test_breadth_first_search_get_result_after_StopIteration(self):
 
943
        # StopIteration should not invalid anything..
 
944
        graph = self.make_graph({
 
945
            'head':[NULL_REVISION],
 
946
            NULL_REVISION:[],
 
947
            })
 
948
        search = graph._make_breadth_first_searcher(['head'])
 
949
        # using next:
 
950
        expected = [
 
951
            (set(['head']),
 
952
             (set(['head']), set([NULL_REVISION]), 1),
 
953
             ['head'], None, None),
 
954
            (set(['head', 'ghost', NULL_REVISION]),
 
955
             (set(['head', 'ghost']), set(['ghost']), 2),
 
956
             ['head', NULL_REVISION], ['ghost'], None),
 
957
            ]
 
958
        self.assertSeenAndResult(expected, search, search.next)
 
959
        self.assertRaises(StopIteration, search.next)
 
960
        self.assertEqual(set(['head', 'ghost', NULL_REVISION]), search.seen)
 
961
        result = search.get_result()
 
962
        self.assertEqual((set(['ghost', 'head']), set(['ghost']), 2),
 
963
            result.get_recipe())
 
964
        self.assertEqual(set(['head', NULL_REVISION]), result.get_keys())
 
965
        # using next_with_ghosts:
 
966
        search = graph._make_breadth_first_searcher(['head'])
 
967
        self.assertSeenAndResult(expected, search, search.next_with_ghosts)
 
968
        self.assertRaises(StopIteration, search.next)
 
969
        self.assertEqual(set(['head', 'ghost', NULL_REVISION]), search.seen)
 
970
        result = search.get_result()
 
971
        self.assertEqual((set(['ghost', 'head']), set(['ghost']), 2),
 
972
            result.get_recipe())
 
973
        self.assertEqual(set(['head', NULL_REVISION]), result.get_keys())
 
974
 
 
975
 
 
976
class TestCachingParentsProvider(tests.TestCase):
 
977
 
 
978
    def setUp(self):
 
979
        super(TestCachingParentsProvider, self).setUp()
 
980
        dict_pp = _mod_graph.DictParentsProvider({'a':('b',)})
 
981
        self.inst_pp = InstrumentedParentsProvider(dict_pp)
 
982
        self.caching_pp = _mod_graph.CachingParentsProvider(self.inst_pp)
 
983
 
 
984
    def test_get_parent_map(self):
 
985
        """Requesting the same revision should be returned from cache"""
 
986
        self.assertEqual({}, self.caching_pp._cache)
 
987
        self.assertEqual({'a':('b',)}, self.caching_pp.get_parent_map(['a']))
 
988
        self.assertEqual(['a'], self.inst_pp.calls)
 
989
        self.assertEqual({'a':('b',)}, self.caching_pp.get_parent_map(['a']))
 
990
        # No new call, as it should have been returned from the cache
 
991
        self.assertEqual(['a'], self.inst_pp.calls)
 
992
        self.assertEqual({'a':('b',)}, self.caching_pp._cache)
 
993
 
 
994
    def test_get_parent_map_not_present(self):
 
995
        """The cache should also track when a revision doesn't exist"""
 
996
        self.assertEqual({}, self.caching_pp.get_parent_map(['b']))
 
997
        self.assertEqual(['b'], self.inst_pp.calls)
 
998
        self.assertEqual({}, self.caching_pp.get_parent_map(['b']))
 
999
        # No new calls
 
1000
        self.assertEqual(['b'], self.inst_pp.calls)
 
1001
        self.assertEqual({'b':None}, self.caching_pp._cache)
 
1002
 
 
1003
    def test_get_parent_map_mixed(self):
 
1004
        """Anything that can be returned from cache, should be"""
 
1005
        self.assertEqual({}, self.caching_pp.get_parent_map(['b']))
 
1006
        self.assertEqual(['b'], self.inst_pp.calls)
 
1007
        self.assertEqual({'a':('b',)},
 
1008
                         self.caching_pp.get_parent_map(['a', 'b']))
 
1009
        self.assertEqual(['b', 'a'], self.inst_pp.calls)
 
1010
 
 
1011
    def test_get_parent_map_repeated(self):
 
1012
        """Asking for the same parent 2x will only forward 1 request."""
 
1013
        self.assertEqual({'a':('b',)},
 
1014
                         self.caching_pp.get_parent_map(['b', 'a', 'b']))
 
1015
        # Use sorted because we don't care about the order, just that each is
 
1016
        # only present 1 time.
 
1017
        self.assertEqual(['a', 'b'], sorted(self.inst_pp.calls))