~bzr-pqm/bzr/bzr.dev

« back to all changes in this revision

Viewing changes to bzrlib/tests/per_repository_vf/test_check_reconcile.py

merge trunk

Show diffs side-by-side

added added

removed removed

Lines of Context:
21
21
 
22
22
from bzrlib import osutils
23
23
 
24
 
from bzrlib.inventory import Inventory, InventoryFile
25
 
from bzrlib.revision import Revision
26
 
from bzrlib.tests import TestNotApplicable
27
 
from bzrlib.tests.per_repository import TestCaseWithRepository
 
24
from bzrlib.inventory import (
 
25
    Inventory,
 
26
    InventoryFile,
 
27
    )
 
28
from bzrlib.revision import (
 
29
    NULL_REVISION,
 
30
    Revision,
 
31
    )
 
32
from bzrlib.tests import (
 
33
    TestNotApplicable,
 
34
    multiply_scenarios,
 
35
    )
 
36
from bzrlib.tests.per_repository_vf import (
 
37
    TestCaseWithRepository,
 
38
    all_repository_vf_format_scenarios,
 
39
    )
 
40
from bzrlib.tests.scenarios import load_tests_apply_scenarios
 
41
 
 
42
 
 
43
load_tests = load_tests_apply_scenarios
 
44
 
 
45
 
 
46
class BrokenRepoScenario(object):
 
47
    """Base class for defining scenarios for testing check and reconcile.
 
48
 
 
49
    A subclass needs to define the following methods:
 
50
        :populate_repository: a method to use to populate a repository with
 
51
            sample revisions, inventories and file versions.
 
52
        :all_versions_after_reconcile: all the versions in repository after
 
53
            reconcile.  run_test verifies that the text of each of these
 
54
            versions of the file is unchanged by the reconcile.
 
55
        :populated_parents: a list of (parents list, revision).  Each version
 
56
            of the file is verified to have the given parents before running
 
57
            the reconcile.  i.e. this is used to assert that the repo from the
 
58
            factory is what we expect.
 
59
        :corrected_parents: a list of (parents list, revision).  Each version
 
60
            of the file is verified to have the given parents after the
 
61
            reconcile.  i.e. this is used to assert that reconcile made the
 
62
            changes we expect it to make.
 
63
 
 
64
    A subclass may define the following optional method as well:
 
65
        :corrected_fulltexts: a list of file versions that should be stored as
 
66
            fulltexts (not deltas) after reconcile.  run_test will verify that
 
67
            this occurs.
 
68
    """
 
69
 
 
70
    def __init__(self, test_case):
 
71
        self.test_case = test_case
 
72
 
 
73
    def make_one_file_inventory(self, repo, revision, parents,
 
74
                                inv_revision=None, root_revision=None,
 
75
                                file_contents=None, make_file_version=True):
 
76
        return self.test_case.make_one_file_inventory(
 
77
            repo, revision, parents, inv_revision=inv_revision,
 
78
            root_revision=root_revision, file_contents=file_contents,
 
79
            make_file_version=make_file_version)
 
80
 
 
81
    def add_revision(self, repo, revision_id, inv, parent_ids):
 
82
        return self.test_case.add_revision(repo, revision_id, inv, parent_ids)
 
83
 
 
84
    def corrected_fulltexts(self):
 
85
        return []
 
86
 
 
87
    def repository_text_key_index(self):
 
88
        result = {}
 
89
        if self.versioned_root:
 
90
            result.update(self.versioned_repository_text_keys())
 
91
        result.update(self.repository_text_keys())
 
92
        return result
 
93
 
 
94
 
 
95
class UndamagedRepositoryScenario(BrokenRepoScenario):
 
96
    """A scenario where the repository has no damage.
 
97
 
 
98
    It has a single revision, 'rev1a', with a single file.
 
99
    """
 
100
 
 
101
    def all_versions_after_reconcile(self):
 
102
        return ('rev1a', )
 
103
 
 
104
    def populated_parents(self):
 
105
        return (((), 'rev1a'), )
 
106
 
 
107
    def corrected_parents(self):
 
108
        # Same as the populated parents, because there was nothing wrong.
 
109
        return self.populated_parents()
 
110
 
 
111
    def check_regexes(self, repo):
 
112
        return ["0 unreferenced text versions"]
 
113
 
 
114
    def populate_repository(self, repo):
 
115
        # make rev1a: A well-formed revision, containing 'a-file'
 
116
        inv = self.make_one_file_inventory(
 
117
            repo, 'rev1a', [], root_revision='rev1a')
 
118
        self.add_revision(repo, 'rev1a', inv, [])
 
119
        self.versioned_root = repo.supports_rich_root()
 
120
 
 
121
    def repository_text_key_references(self):
 
122
        result = {}
 
123
        if self.versioned_root:
 
124
            result.update({('TREE_ROOT', 'rev1a'): True})
 
125
        result.update({('a-file-id', 'rev1a'): True})
 
126
        return result
 
127
 
 
128
    def repository_text_keys(self):
 
129
        return {('a-file-id', 'rev1a'):[NULL_REVISION]}
 
130
 
 
131
    def versioned_repository_text_keys(self):
 
132
        return {('TREE_ROOT', 'rev1a'):[NULL_REVISION]}
 
133
 
 
134
 
 
135
class FileParentIsNotInRevisionAncestryScenario(BrokenRepoScenario):
 
136
    """A scenario where a revision 'rev2' has 'a-file' with a
 
137
    parent 'rev1b' that is not in the revision ancestry.
 
138
 
 
139
    Reconcile should remove 'rev1b' from the parents list of 'a-file' in
 
140
    'rev2', preserving 'rev1a' as a parent.
 
141
    """
 
142
 
 
143
    def all_versions_after_reconcile(self):
 
144
        return ('rev1a', 'rev2')
 
145
 
 
146
    def populated_parents(self):
 
147
        return (
 
148
            ((), 'rev1a'),
 
149
            ((), 'rev1b'), # Will be gc'd
 
150
            (('rev1a', 'rev1b'), 'rev2')) # Will have parents trimmed
 
151
 
 
152
    def corrected_parents(self):
 
153
        return (
 
154
            ((), 'rev1a'),
 
155
            (None, 'rev1b'),
 
156
            (('rev1a',), 'rev2'))
 
157
 
 
158
    def check_regexes(self, repo):
 
159
        return [r"\* a-file-id version rev2 has parents \('rev1a', 'rev1b'\) "
 
160
                r"but should have \('rev1a',\)",
 
161
                "1 unreferenced text versions",
 
162
                ]
 
163
 
 
164
    def populate_repository(self, repo):
 
165
        # make rev1a: A well-formed revision, containing 'a-file'
 
166
        inv = self.make_one_file_inventory(
 
167
            repo, 'rev1a', [], root_revision='rev1a')
 
168
        self.add_revision(repo, 'rev1a', inv, [])
 
169
 
 
170
        # make rev1b, which has no Revision, but has an Inventory, and
 
171
        # a-file
 
172
        inv = self.make_one_file_inventory(
 
173
            repo, 'rev1b', [], root_revision='rev1b')
 
174
        repo.add_inventory('rev1b', inv, [])
 
175
 
 
176
        # make rev2, with a-file.
 
177
        # a-file has 'rev1b' as an ancestor, even though this is not
 
178
        # mentioned by 'rev1a', making it an unreferenced ancestor
 
179
        inv = self.make_one_file_inventory(
 
180
            repo, 'rev2', ['rev1a', 'rev1b'])
 
181
        self.add_revision(repo, 'rev2', inv, ['rev1a'])
 
182
        self.versioned_root = repo.supports_rich_root()
 
183
 
 
184
    def repository_text_key_references(self):
 
185
        result = {}
 
186
        if self.versioned_root:
 
187
            result.update({('TREE_ROOT', 'rev1a'): True,
 
188
                           ('TREE_ROOT', 'rev2'): True})
 
189
        result.update({('a-file-id', 'rev1a'): True,
 
190
                       ('a-file-id', 'rev2'): True})
 
191
        return result
 
192
 
 
193
    def repository_text_keys(self):
 
194
        return {('a-file-id', 'rev1a'):[NULL_REVISION],
 
195
                ('a-file-id', 'rev2'):[('a-file-id', 'rev1a')]}
 
196
 
 
197
    def versioned_repository_text_keys(self):
 
198
        return {('TREE_ROOT', 'rev1a'):[NULL_REVISION],
 
199
                ('TREE_ROOT', 'rev2'):[('TREE_ROOT', 'rev1a')]}
 
200
 
 
201
 
 
202
class FileParentHasInaccessibleInventoryScenario(BrokenRepoScenario):
 
203
    """A scenario where a revision 'rev3' containing 'a-file' modified in
 
204
    'rev3', and with a parent which is in the revision ancestory, but whose
 
205
    inventory cannot be accessed at all.
 
206
 
 
207
    Reconcile should remove the file version parent whose inventory is
 
208
    inaccessbile (i.e. remove 'rev1c' from the parents of a-file's rev3).
 
209
    """
 
210
 
 
211
    def all_versions_after_reconcile(self):
 
212
        return ('rev2', 'rev3')
 
213
 
 
214
    def populated_parents(self):
 
215
        return (
 
216
            ((), 'rev2'),
 
217
            (('rev1c',), 'rev3'))
 
218
 
 
219
    def corrected_parents(self):
 
220
        return (
 
221
            ((), 'rev2'),
 
222
            ((), 'rev3'))
 
223
 
 
224
    def check_regexes(self, repo):
 
225
        return [r"\* a-file-id version rev3 has parents "
 
226
                r"\('rev1c',\) but should have \(\)",
 
227
                ]
 
228
 
 
229
    def populate_repository(self, repo):
 
230
        # make rev2, with a-file
 
231
        # a-file is sane
 
232
        inv = self.make_one_file_inventory(repo, 'rev2', [])
 
233
        self.add_revision(repo, 'rev2', inv, [])
 
234
 
 
235
        # make ghost revision rev1c, with a version of a-file present so
 
236
        # that we generate a knit delta against this version.  In real life
 
237
        # the ghost might never have been present or rev3 might have been
 
238
        # generated against a revision that was present at the time.  So
 
239
        # currently we have the full history of a-file present even though
 
240
        # the inventory and revision objects are not.
 
241
        self.make_one_file_inventory(repo, 'rev1c', [])
 
242
 
 
243
        # make rev3 with a-file
 
244
        # a-file refers to 'rev1c', which is a ghost in this repository, so
 
245
        # a-file cannot have rev1c as its ancestor.
 
246
        inv = self.make_one_file_inventory(repo, 'rev3', ['rev1c'])
 
247
        self.add_revision(repo, 'rev3', inv, ['rev1c', 'rev1a'])
 
248
        self.versioned_root = repo.supports_rich_root()
 
249
 
 
250
    def repository_text_key_references(self):
 
251
        result = {}
 
252
        if self.versioned_root:
 
253
            result.update({('TREE_ROOT', 'rev2'): True,
 
254
                           ('TREE_ROOT', 'rev3'): True})
 
255
        result.update({('a-file-id', 'rev2'): True,
 
256
                       ('a-file-id', 'rev3'): True})
 
257
        return result
 
258
 
 
259
    def repository_text_keys(self):
 
260
        return {('a-file-id', 'rev2'):[NULL_REVISION],
 
261
                ('a-file-id', 'rev3'):[NULL_REVISION]}
 
262
 
 
263
    def versioned_repository_text_keys(self):
 
264
        return {('TREE_ROOT', 'rev2'):[NULL_REVISION],
 
265
                ('TREE_ROOT', 'rev3'):[NULL_REVISION]}
 
266
 
 
267
 
 
268
class FileParentsNotReferencedByAnyInventoryScenario(BrokenRepoScenario):
 
269
    """A scenario where a repository with file 'a-file' which has extra
 
270
    per-file versions that are not referenced by any inventory (even though
 
271
    they have the same ID as actual revisions).  The inventory of 'rev2'
 
272
    references 'rev1a' of 'a-file', but there is a 'rev2' of 'some-file' stored
 
273
    and erroneously referenced by later per-file versions (revisions 'rev4' and
 
274
    'rev5').
 
275
 
 
276
    Reconcile should remove the file parents that are not referenced by any
 
277
    inventory.
 
278
    """
 
279
 
 
280
    def all_versions_after_reconcile(self):
 
281
        return ('rev1a', 'rev2c', 'rev4', 'rev5')
 
282
 
 
283
    def populated_parents(self):
 
284
        return [
 
285
            (('rev1a',), 'rev2'),
 
286
            (('rev1a',), 'rev2b'),
 
287
            (('rev2',), 'rev3'),
 
288
            (('rev2',), 'rev4'),
 
289
            (('rev2', 'rev2c'), 'rev5')]
 
290
 
 
291
    def corrected_parents(self):
 
292
        return (
 
293
            # rev2 and rev2b have been removed.
 
294
            (None, 'rev2'),
 
295
            (None, 'rev2b'),
 
296
            # rev3's accessible parent inventories all have rev1a as the last
 
297
            # modifier.
 
298
            (('rev1a',), 'rev3'),
 
299
            # rev1a features in both rev4's parents but should only appear once
 
300
            # in the result
 
301
            (('rev1a',), 'rev4'),
 
302
            # rev2c is the head of rev1a and rev2c, the inventory provided
 
303
            # per-file last-modified revisions.
 
304
            (('rev2c',), 'rev5'))
 
305
 
 
306
    def check_regexes(self, repo):
 
307
        if repo.supports_rich_root():
 
308
            # TREE_ROOT will be wrong; but we're not testing it. so just adjust
 
309
            # the expected count of errors.
 
310
            count = 9
 
311
        else:
 
312
            count = 3
 
313
        return [
 
314
            # will be gc'd
 
315
            r"unreferenced version: {rev2} in a-file-id",
 
316
            r"unreferenced version: {rev2b} in a-file-id",
 
317
            # will be corrected
 
318
            r"a-file-id version rev3 has parents \('rev2',\) "
 
319
            r"but should have \('rev1a',\)",
 
320
            r"a-file-id version rev5 has parents \('rev2', 'rev2c'\) "
 
321
            r"but should have \('rev2c',\)",
 
322
            r"a-file-id version rev4 has parents \('rev2',\) "
 
323
            r"but should have \('rev1a',\)",
 
324
            "%d inconsistent parents" % count,
 
325
            ]
 
326
 
 
327
    def populate_repository(self, repo):
 
328
        # make rev1a: A well-formed revision, containing 'a-file'
 
329
        inv = self.make_one_file_inventory(
 
330
            repo, 'rev1a', [], root_revision='rev1a')
 
331
        self.add_revision(repo, 'rev1a', inv, [])
 
332
 
 
333
        # make rev2, with a-file.
 
334
        # a-file is unmodified from rev1a, and an unreferenced rev2 file
 
335
        # version is present in the repository.
 
336
        self.make_one_file_inventory(
 
337
            repo, 'rev2', ['rev1a'], inv_revision='rev1a')
 
338
        self.add_revision(repo, 'rev2', inv, ['rev1a'])
 
339
 
 
340
        # make rev3 with a-file
 
341
        # a-file has 'rev2' as its ancestor, but the revision in 'rev2' was
 
342
        # rev1a so this is inconsistent with rev2's inventory - it should
 
343
        # be rev1a, and at the revision level 1c is not present - it is a
 
344
        # ghost, so only the details from rev1a are available for
 
345
        # determining whether a delta is acceptable, or a full is needed,
 
346
        # and what the correct parents are.
 
347
        inv = self.make_one_file_inventory(repo, 'rev3', ['rev2'])
 
348
        self.add_revision(repo, 'rev3', inv, ['rev1c', 'rev1a'])
 
349
 
 
350
        # In rev2b, the true last-modifying-revision of a-file is rev1a,
 
351
        # inherited from rev2, but there is a version rev2b of the file, which
 
352
        # reconcile could remove, leaving no rev2b.  Most importantly,
 
353
        # revisions descending from rev2b should not have per-file parents of
 
354
        # a-file-rev2b.
 
355
        # ??? This is to test deduplication in fixing rev4
 
356
        inv = self.make_one_file_inventory(
 
357
            repo, 'rev2b', ['rev1a'], inv_revision='rev1a')
 
358
        self.add_revision(repo, 'rev2b', inv, ['rev1a'])
 
359
 
 
360
        # rev4 is for testing that when the last modified of a file in
 
361
        # multiple parent revisions is the same, that it only appears once
 
362
        # in the generated per file parents list: rev2 and rev2b both
 
363
        # descend from 1a and do not change the file a-file, so there should
 
364
        # be no version of a-file 'rev2' or 'rev2b', but rev4 does change
 
365
        # a-file, and is a merge of rev2 and rev2b, so it should end up with
 
366
        # a parent of just rev1a - the starting file parents list is simply
 
367
        # completely wrong.
 
368
        inv = self.make_one_file_inventory(repo, 'rev4', ['rev2'])
 
369
        self.add_revision(repo, 'rev4', inv, ['rev2', 'rev2b'])
 
370
 
 
371
        # rev2c changes a-file from rev1a, so the version it of a-file it
 
372
        # introduces is a head revision when rev5 is checked.
 
373
        inv = self.make_one_file_inventory(repo, 'rev2c', ['rev1a'])
 
374
        self.add_revision(repo, 'rev2c', inv, ['rev1a'])
 
375
 
 
376
        # rev5 descends from rev2 and rev2c; as rev2 does not alter a-file,
 
377
        # but rev2c does, this should use rev2c as the parent for the per
 
378
        # file history, even though more than one per-file parent is
 
379
        # available, because we use the heads of the revision parents for
 
380
        # the inventory modification revisions of the file to determine the
 
381
        # parents for the per file graph.
 
382
        inv = self.make_one_file_inventory(repo, 'rev5', ['rev2', 'rev2c'])
 
383
        self.add_revision(repo, 'rev5', inv, ['rev2', 'rev2c'])
 
384
        self.versioned_root = repo.supports_rich_root()
 
385
 
 
386
    def repository_text_key_references(self):
 
387
        result = {}
 
388
        if self.versioned_root:
 
389
            result.update({('TREE_ROOT', 'rev1a'): True,
 
390
                           ('TREE_ROOT', 'rev2'): True,
 
391
                           ('TREE_ROOT', 'rev2b'): True,
 
392
                           ('TREE_ROOT', 'rev2c'): True,
 
393
                           ('TREE_ROOT', 'rev3'): True,
 
394
                           ('TREE_ROOT', 'rev4'): True,
 
395
                           ('TREE_ROOT', 'rev5'): True})
 
396
        result.update({('a-file-id', 'rev1a'): True,
 
397
                       ('a-file-id', 'rev2c'): True,
 
398
                       ('a-file-id', 'rev3'): True,
 
399
                       ('a-file-id', 'rev4'): True,
 
400
                       ('a-file-id', 'rev5'): True})
 
401
        return result
 
402
 
 
403
    def repository_text_keys(self):
 
404
        return {('a-file-id', 'rev1a'): [NULL_REVISION],
 
405
                 ('a-file-id', 'rev2c'): [('a-file-id', 'rev1a')],
 
406
                 ('a-file-id', 'rev3'): [('a-file-id', 'rev1a')],
 
407
                 ('a-file-id', 'rev4'): [('a-file-id', 'rev1a')],
 
408
                 ('a-file-id', 'rev5'): [('a-file-id', 'rev2c')]}
 
409
 
 
410
    def versioned_repository_text_keys(self):
 
411
        return {('TREE_ROOT', 'rev1a'): [NULL_REVISION],
 
412
                ('TREE_ROOT', 'rev2'): [('TREE_ROOT', 'rev1a')],
 
413
                ('TREE_ROOT', 'rev2b'): [('TREE_ROOT', 'rev1a')],
 
414
                ('TREE_ROOT', 'rev2c'): [('TREE_ROOT', 'rev1a')],
 
415
                ('TREE_ROOT', 'rev3'): [('TREE_ROOT', 'rev1a')],
 
416
                ('TREE_ROOT', 'rev4'):
 
417
                    [('TREE_ROOT', 'rev2'), ('TREE_ROOT', 'rev2b')],
 
418
                ('TREE_ROOT', 'rev5'):
 
419
                    [('TREE_ROOT', 'rev2'), ('TREE_ROOT', 'rev2c')]}
 
420
 
 
421
 
 
422
class UnreferencedFileParentsFromNoOpMergeScenario(BrokenRepoScenario):
 
423
    """
 
424
    rev1a and rev1b with identical contents
 
425
    rev2 revision has parents of [rev1a, rev1b]
 
426
    There is a a-file:rev2 file version, not referenced by the inventory.
 
427
    """
 
428
 
 
429
    def all_versions_after_reconcile(self):
 
430
        return ('rev1a', 'rev1b', 'rev2', 'rev4')
 
431
 
 
432
    def populated_parents(self):
 
433
        return (
 
434
            ((), 'rev1a'),
 
435
            ((), 'rev1b'),
 
436
            (('rev1a', 'rev1b'), 'rev2'),
 
437
            (None, 'rev3'),
 
438
            (('rev2',), 'rev4'),
 
439
            )
 
440
 
 
441
    def corrected_parents(self):
 
442
        return (
 
443
            ((), 'rev1a'),
 
444
            ((), 'rev1b'),
 
445
            ((), 'rev2'),
 
446
            (None, 'rev3'),
 
447
            (('rev2',), 'rev4'),
 
448
            )
 
449
 
 
450
    def corrected_fulltexts(self):
 
451
        return ['rev2']
 
452
 
 
453
    def check_regexes(self, repo):
 
454
        return []
 
455
 
 
456
    def populate_repository(self, repo):
 
457
        # make rev1a: A well-formed revision, containing 'a-file'
 
458
        inv1a = self.make_one_file_inventory(
 
459
            repo, 'rev1a', [], root_revision='rev1a')
 
460
        self.add_revision(repo, 'rev1a', inv1a, [])
 
461
 
 
462
        # make rev1b: A well-formed revision, containing 'a-file'
 
463
        # rev1b of a-file has the exact same contents as rev1a.
 
464
        file_contents = repo.revision_tree('rev1a').get_file_text('a-file-id')
 
465
        inv = self.make_one_file_inventory(
 
466
            repo, 'rev1b', [], root_revision='rev1b',
 
467
            file_contents=file_contents)
 
468
        self.add_revision(repo, 'rev1b', inv, [])
 
469
 
 
470
        # make rev2, a merge of rev1a and rev1b, with a-file.
 
471
        # a-file is unmodified from rev1a and rev1b, but a new version is
 
472
        # wrongly present anyway.
 
473
        inv = self.make_one_file_inventory(
 
474
            repo, 'rev2', ['rev1a', 'rev1b'], inv_revision='rev1a',
 
475
            file_contents=file_contents)
 
476
        self.add_revision(repo, 'rev2', inv, ['rev1a', 'rev1b'])
 
477
 
 
478
        # rev3: a-file unchanged from rev2, but wrongly referencing rev2 of the
 
479
        # file in its inventory.
 
480
        inv = self.make_one_file_inventory(
 
481
            repo, 'rev3', ['rev2'], inv_revision='rev2',
 
482
            file_contents=file_contents, make_file_version=False)
 
483
        self.add_revision(repo, 'rev3', inv, ['rev2'])
 
484
 
 
485
        # rev4: a modification of a-file on top of rev3.
 
486
        inv = self.make_one_file_inventory(repo, 'rev4', ['rev2'])
 
487
        self.add_revision(repo, 'rev4', inv, ['rev3'])
 
488
        self.versioned_root = repo.supports_rich_root()
 
489
 
 
490
    def repository_text_key_references(self):
 
491
        result = {}
 
492
        if self.versioned_root:
 
493
            result.update({('TREE_ROOT', 'rev1a'): True,
 
494
                           ('TREE_ROOT', 'rev1b'): True,
 
495
                           ('TREE_ROOT', 'rev2'): True,
 
496
                           ('TREE_ROOT', 'rev3'): True,
 
497
                           ('TREE_ROOT', 'rev4'): True})
 
498
        result.update({('a-file-id', 'rev1a'): True,
 
499
                       ('a-file-id', 'rev1b'): True,
 
500
                       ('a-file-id', 'rev2'): False,
 
501
                       ('a-file-id', 'rev4'): True})
 
502
        return result
 
503
 
 
504
    def repository_text_keys(self):
 
505
        return {('a-file-id', 'rev1a'): [NULL_REVISION],
 
506
                ('a-file-id', 'rev1b'): [NULL_REVISION],
 
507
                ('a-file-id', 'rev2'): [NULL_REVISION],
 
508
                ('a-file-id', 'rev4'): [('a-file-id', 'rev2')]}
 
509
 
 
510
    def versioned_repository_text_keys(self):
 
511
        return {('TREE_ROOT', 'rev1a'): [NULL_REVISION],
 
512
                ('TREE_ROOT', 'rev1b'): [NULL_REVISION],
 
513
                ('TREE_ROOT', 'rev2'):
 
514
                    [('TREE_ROOT', 'rev1a'), ('TREE_ROOT', 'rev1b')],
 
515
                ('TREE_ROOT', 'rev3'): [('TREE_ROOT', 'rev2')],
 
516
                ('TREE_ROOT', 'rev4'): [('TREE_ROOT', 'rev3')]}
 
517
 
 
518
 
 
519
class TooManyParentsScenario(BrokenRepoScenario):
 
520
    """A scenario where 'broken-revision' of 'a-file' claims to have parents
 
521
    ['good-parent', 'bad-parent'].  However 'bad-parent' is in the ancestry of
 
522
    'good-parent', so the correct parent list for that file version are is just
 
523
    ['good-parent'].
 
524
    """
 
525
 
 
526
    def all_versions_after_reconcile(self):
 
527
        return ('bad-parent', 'good-parent', 'broken-revision')
 
528
 
 
529
    def populated_parents(self):
 
530
        return (
 
531
            ((), 'bad-parent'),
 
532
            (('bad-parent',), 'good-parent'),
 
533
            (('good-parent', 'bad-parent'), 'broken-revision'))
 
534
 
 
535
    def corrected_parents(self):
 
536
        return (
 
537
            ((), 'bad-parent'),
 
538
            (('bad-parent',), 'good-parent'),
 
539
            (('good-parent',), 'broken-revision'))
 
540
 
 
541
    def check_regexes(self, repo):
 
542
        if repo.supports_rich_root():
 
543
            # TREE_ROOT will be wrong; but we're not testing it. so just adjust
 
544
            # the expected count of errors.
 
545
            count = 3
 
546
        else:
 
547
            count = 1
 
548
        return (
 
549
            '     %d inconsistent parents' % count,
 
550
            (r"      \* a-file-id version broken-revision has parents "
 
551
             r"\('good-parent', 'bad-parent'\) but "
 
552
             r"should have \('good-parent',\)"))
 
553
 
 
554
    def populate_repository(self, repo):
 
555
        inv = self.make_one_file_inventory(
 
556
            repo, 'bad-parent', (), root_revision='bad-parent')
 
557
        self.add_revision(repo, 'bad-parent', inv, ())
 
558
 
 
559
        inv = self.make_one_file_inventory(
 
560
            repo, 'good-parent', ('bad-parent',))
 
561
        self.add_revision(repo, 'good-parent', inv, ('bad-parent',))
 
562
 
 
563
        inv = self.make_one_file_inventory(
 
564
            repo, 'broken-revision', ('good-parent', 'bad-parent'))
 
565
        self.add_revision(repo, 'broken-revision', inv, ('good-parent',))
 
566
        self.versioned_root = repo.supports_rich_root()
 
567
 
 
568
    def repository_text_key_references(self):
 
569
        result = {}
 
570
        if self.versioned_root:
 
571
            result.update({('TREE_ROOT', 'bad-parent'): True,
 
572
                           ('TREE_ROOT', 'broken-revision'): True,
 
573
                           ('TREE_ROOT', 'good-parent'): True})
 
574
        result.update({('a-file-id', 'bad-parent'): True,
 
575
                       ('a-file-id', 'broken-revision'): True,
 
576
                       ('a-file-id', 'good-parent'): True})
 
577
        return result
 
578
 
 
579
    def repository_text_keys(self):
 
580
        return {('a-file-id', 'bad-parent'): [NULL_REVISION],
 
581
                ('a-file-id', 'broken-revision'):
 
582
                    [('a-file-id', 'good-parent')],
 
583
                ('a-file-id', 'good-parent'): [('a-file-id', 'bad-parent')]}
 
584
 
 
585
    def versioned_repository_text_keys(self):
 
586
        return {('TREE_ROOT', 'bad-parent'): [NULL_REVISION],
 
587
                ('TREE_ROOT', 'broken-revision'):
 
588
                    [('TREE_ROOT', 'good-parent')],
 
589
                ('TREE_ROOT', 'good-parent'): [('TREE_ROOT', 'bad-parent')]}
 
590
 
 
591
 
 
592
class ClaimedFileParentDidNotModifyFileScenario(BrokenRepoScenario):
 
593
    """A scenario where the file parent is the same as the revision parent, but
 
594
    should not be because that revision did not modify the file.
 
595
 
 
596
    Specifically, the parent revision of 'current' is
 
597
    'modified-something-else', which does not modify 'a-file', but the
 
598
    'current' version of 'a-file' erroneously claims that
 
599
    'modified-something-else' is the parent file version.
 
600
    """
 
601
 
 
602
    def all_versions_after_reconcile(self):
 
603
        return ('basis', 'current')
 
604
 
 
605
    def populated_parents(self):
 
606
        return (
 
607
            ((), 'basis'),
 
608
            (('basis',), 'modified-something-else'),
 
609
            (('modified-something-else',), 'current'))
 
610
 
 
611
    def corrected_parents(self):
 
612
        return (
 
613
            ((), 'basis'),
 
614
            (None, 'modified-something-else'),
 
615
            (('basis',), 'current'))
 
616
 
 
617
    def check_regexes(self, repo):
 
618
        if repo.supports_rich_root():
 
619
            # TREE_ROOT will be wrong; but we're not testing it. so just adjust
 
620
            # the expected count of errors.
 
621
            count = 3
 
622
        else:
 
623
            count = 1
 
624
        return (
 
625
            "%d inconsistent parents" % count,
 
626
            r"\* a-file-id version current has parents "
 
627
            r"\('modified-something-else',\) but should have \('basis',\)",
 
628
            )
 
629
 
 
630
    def populate_repository(self, repo):
 
631
        inv = self.make_one_file_inventory(repo, 'basis', ())
 
632
        self.add_revision(repo, 'basis', inv, ())
 
633
 
 
634
        # 'modified-something-else' is a correctly recorded revision, but it
 
635
        # does not modify the file we are looking at, so the inventory for that
 
636
        # file in this revision points to 'basis'.
 
637
        inv = self.make_one_file_inventory(
 
638
            repo, 'modified-something-else', ('basis',), inv_revision='basis')
 
639
        self.add_revision(repo, 'modified-something-else', inv, ('basis',))
 
640
 
 
641
        # The 'current' revision has 'modified-something-else' as its parent,
 
642
        # but the 'current' version of 'a-file' should have 'basis' as its
 
643
        # parent.
 
644
        inv = self.make_one_file_inventory(
 
645
            repo, 'current', ('modified-something-else',))
 
646
        self.add_revision(repo, 'current', inv, ('modified-something-else',))
 
647
        self.versioned_root = repo.supports_rich_root()
 
648
 
 
649
    def repository_text_key_references(self):
 
650
        result = {}
 
651
        if self.versioned_root:
 
652
            result.update({('TREE_ROOT', 'basis'): True,
 
653
                           ('TREE_ROOT', 'current'): True,
 
654
                           ('TREE_ROOT', 'modified-something-else'): True})
 
655
        result.update({('a-file-id', 'basis'): True,
 
656
                       ('a-file-id', 'current'): True})
 
657
        return result
 
658
 
 
659
    def repository_text_keys(self):
 
660
        return {('a-file-id', 'basis'): [NULL_REVISION],
 
661
                ('a-file-id', 'current'): [('a-file-id', 'basis')]}
 
662
 
 
663
    def versioned_repository_text_keys(self):
 
664
        return {('TREE_ROOT', 'basis'): ['null:'],
 
665
                ('TREE_ROOT', 'current'):
 
666
                    [('TREE_ROOT', 'modified-something-else')],
 
667
                ('TREE_ROOT', 'modified-something-else'):
 
668
                    [('TREE_ROOT', 'basis')]}
 
669
 
 
670
 
 
671
class IncorrectlyOrderedParentsScenario(BrokenRepoScenario):
 
672
    """A scenario where the set parents of a version of a file are correct, but
 
673
    the order of those parents is incorrect.
 
674
 
 
675
    This defines a 'broken-revision-1-2' and a 'broken-revision-2-1' which both
 
676
    have their file version parents reversed compared to the revision parents,
 
677
    which is invalid.  (We use two revisions with opposite orderings of the
 
678
    same parents to make sure that accidentally relying on dictionary/set
 
679
    ordering cannot make the test pass; the assumption is that while dict/set
 
680
    iteration order is arbitrary, it is also consistent within a single test).
 
681
    """
 
682
 
 
683
    def all_versions_after_reconcile(self):
 
684
        return ['parent-1', 'parent-2', 'broken-revision-1-2',
 
685
                'broken-revision-2-1']
 
686
 
 
687
    def populated_parents(self):
 
688
        return (
 
689
            ((), 'parent-1'),
 
690
            ((), 'parent-2'),
 
691
            (('parent-2', 'parent-1'), 'broken-revision-1-2'),
 
692
            (('parent-1', 'parent-2'), 'broken-revision-2-1'))
 
693
 
 
694
    def corrected_parents(self):
 
695
        return (
 
696
            ((), 'parent-1'),
 
697
            ((), 'parent-2'),
 
698
            (('parent-1', 'parent-2'), 'broken-revision-1-2'),
 
699
            (('parent-2', 'parent-1'), 'broken-revision-2-1'))
 
700
 
 
701
    def check_regexes(self, repo):
 
702
        if repo.supports_rich_root():
 
703
            # TREE_ROOT will be wrong; but we're not testing it. so just adjust
 
704
            # the expected count of errors.
 
705
            count = 4
 
706
        else:
 
707
            count = 2
 
708
        return (
 
709
            "%d inconsistent parents" % count,
 
710
            r"\* a-file-id version broken-revision-1-2 has parents "
 
711
            r"\('parent-2', 'parent-1'\) but should have "
 
712
            r"\('parent-1', 'parent-2'\)",
 
713
            r"\* a-file-id version broken-revision-2-1 has parents "
 
714
            r"\('parent-1', 'parent-2'\) but should have "
 
715
            r"\('parent-2', 'parent-1'\)")
 
716
 
 
717
    def populate_repository(self, repo):
 
718
        inv = self.make_one_file_inventory(repo, 'parent-1', [])
 
719
        self.add_revision(repo, 'parent-1', inv, [])
 
720
 
 
721
        inv = self.make_one_file_inventory(repo, 'parent-2', [])
 
722
        self.add_revision(repo, 'parent-2', inv, [])
 
723
 
 
724
        inv = self.make_one_file_inventory(
 
725
            repo, 'broken-revision-1-2', ['parent-2', 'parent-1'])
 
726
        self.add_revision(
 
727
            repo, 'broken-revision-1-2', inv, ['parent-1', 'parent-2'])
 
728
 
 
729
        inv = self.make_one_file_inventory(
 
730
            repo, 'broken-revision-2-1', ['parent-1', 'parent-2'])
 
731
        self.add_revision(
 
732
            repo, 'broken-revision-2-1', inv, ['parent-2', 'parent-1'])
 
733
        self.versioned_root = repo.supports_rich_root()
 
734
 
 
735
    def repository_text_key_references(self):
 
736
        result = {}
 
737
        if self.versioned_root:
 
738
            result.update({('TREE_ROOT', 'broken-revision-1-2'): True,
 
739
                           ('TREE_ROOT', 'broken-revision-2-1'): True,
 
740
                           ('TREE_ROOT', 'parent-1'): True,
 
741
                           ('TREE_ROOT', 'parent-2'): True})
 
742
        result.update({('a-file-id', 'broken-revision-1-2'): True,
 
743
                       ('a-file-id', 'broken-revision-2-1'): True,
 
744
                       ('a-file-id', 'parent-1'): True,
 
745
                       ('a-file-id', 'parent-2'): True})
 
746
        return result
 
747
 
 
748
    def repository_text_keys(self):
 
749
        return {('a-file-id', 'broken-revision-1-2'):
 
750
                    [('a-file-id', 'parent-1'), ('a-file-id', 'parent-2')],
 
751
                ('a-file-id', 'broken-revision-2-1'):
 
752
                    [('a-file-id', 'parent-2'), ('a-file-id', 'parent-1')],
 
753
                ('a-file-id', 'parent-1'): [NULL_REVISION],
 
754
                ('a-file-id', 'parent-2'): [NULL_REVISION]}
 
755
 
 
756
    def versioned_repository_text_keys(self):
 
757
        return {('TREE_ROOT', 'broken-revision-1-2'):
 
758
                    [('TREE_ROOT', 'parent-1'), ('TREE_ROOT', 'parent-2')],
 
759
                ('TREE_ROOT', 'broken-revision-2-1'):
 
760
                    [('TREE_ROOT', 'parent-2'), ('TREE_ROOT', 'parent-1')],
 
761
                ('TREE_ROOT', 'parent-1'): [NULL_REVISION],
 
762
                ('TREE_ROOT', 'parent-2'): [NULL_REVISION]}
 
763
 
 
764
 
 
765
all_broken_scenario_classes = [
 
766
    UndamagedRepositoryScenario,
 
767
    FileParentIsNotInRevisionAncestryScenario,
 
768
    FileParentHasInaccessibleInventoryScenario,
 
769
    FileParentsNotReferencedByAnyInventoryScenario,
 
770
    TooManyParentsScenario,
 
771
    ClaimedFileParentDidNotModifyFileScenario,
 
772
    IncorrectlyOrderedParentsScenario,
 
773
    UnreferencedFileParentsFromNoOpMergeScenario,
 
774
    ]
 
775
 
 
776
 
 
777
def broken_scenarios_for_all_formats():
 
778
    format_scenarios = all_repository_vf_format_scenarios()
 
779
    # test_check_reconcile needs to be parameterized by format *and* by broken
 
780
    # repository scenario.
 
781
    broken_scenarios = [(s.__name__, {'scenario_class': s})
 
782
                        for s in all_broken_scenario_classes]
 
783
    return multiply_scenarios(format_scenarios, broken_scenarios)
28
784
 
29
785
 
30
786
class TestFileParentReconciliation(TestCaseWithRepository):
31
787
    """Tests for how reconcile corrects errors in parents of file versions."""
32
788
 
 
789
    scenarios = broken_scenarios_for_all_formats()
 
790
 
33
791
    def make_populated_repository(self, factory):
34
792
        """Create a new repository populated by the given factory."""
35
793
        repo = self.make_repository('broken-repo')