~bzr-pqm/bzr/bzr.dev

« back to all changes in this revision

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

  • Committer: Patch Queue Manager
  • Date: 2016-02-01 19:56:05 UTC
  • mfrom: (6615.1.1 trunk)
  • Revision ID: pqm@pqm.ubuntu.com-20160201195605-o7rl92wf6uyum3fk
(vila) Open trunk again as 2.8b1 (Vincent Ladeuil)

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.texts.get_record_stream([('a-file-id', 'rev1a')],
 
465
            "unordered", False).next().get_bytes_as('fulltext')
 
466
        inv = self.make_one_file_inventory(
 
467
            repo, 'rev1b', [], root_revision='rev1b',
 
468
            file_contents=file_contents)
 
469
        self.add_revision(repo, 'rev1b', inv, [])
 
470
 
 
471
        # make rev2, a merge of rev1a and rev1b, with a-file.
 
472
        # a-file is unmodified from rev1a and rev1b, but a new version is
 
473
        # wrongly present anyway.
 
474
        inv = self.make_one_file_inventory(
 
475
            repo, 'rev2', ['rev1a', 'rev1b'], inv_revision='rev1a',
 
476
            file_contents=file_contents)
 
477
        self.add_revision(repo, 'rev2', inv, ['rev1a', 'rev1b'])
 
478
 
 
479
        # rev3: a-file unchanged from rev2, but wrongly referencing rev2 of the
 
480
        # file in its inventory.
 
481
        inv = self.make_one_file_inventory(
 
482
            repo, 'rev3', ['rev2'], inv_revision='rev2',
 
483
            file_contents=file_contents, make_file_version=False)
 
484
        self.add_revision(repo, 'rev3', inv, ['rev2'])
 
485
 
 
486
        # rev4: a modification of a-file on top of rev3.
 
487
        inv = self.make_one_file_inventory(repo, 'rev4', ['rev2'])
 
488
        self.add_revision(repo, 'rev4', inv, ['rev3'])
 
489
        self.versioned_root = repo.supports_rich_root()
 
490
 
 
491
    def repository_text_key_references(self):
 
492
        result = {}
 
493
        if self.versioned_root:
 
494
            result.update({('TREE_ROOT', 'rev1a'): True,
 
495
                           ('TREE_ROOT', 'rev1b'): True,
 
496
                           ('TREE_ROOT', 'rev2'): True,
 
497
                           ('TREE_ROOT', 'rev3'): True,
 
498
                           ('TREE_ROOT', 'rev4'): True})
 
499
        result.update({('a-file-id', 'rev1a'): True,
 
500
                       ('a-file-id', 'rev1b'): True,
 
501
                       ('a-file-id', 'rev2'): False,
 
502
                       ('a-file-id', 'rev4'): True})
 
503
        return result
 
504
 
 
505
    def repository_text_keys(self):
 
506
        return {('a-file-id', 'rev1a'): [NULL_REVISION],
 
507
                ('a-file-id', 'rev1b'): [NULL_REVISION],
 
508
                ('a-file-id', 'rev2'): [NULL_REVISION],
 
509
                ('a-file-id', 'rev4'): [('a-file-id', 'rev2')]}
 
510
 
 
511
    def versioned_repository_text_keys(self):
 
512
        return {('TREE_ROOT', 'rev1a'): [NULL_REVISION],
 
513
                ('TREE_ROOT', 'rev1b'): [NULL_REVISION],
 
514
                ('TREE_ROOT', 'rev2'):
 
515
                    [('TREE_ROOT', 'rev1a'), ('TREE_ROOT', 'rev1b')],
 
516
                ('TREE_ROOT', 'rev3'): [('TREE_ROOT', 'rev2')],
 
517
                ('TREE_ROOT', 'rev4'): [('TREE_ROOT', 'rev3')]}
 
518
 
 
519
 
 
520
class TooManyParentsScenario(BrokenRepoScenario):
 
521
    """A scenario where 'broken-revision' of 'a-file' claims to have parents
 
522
    ['good-parent', 'bad-parent'].  However 'bad-parent' is in the ancestry of
 
523
    'good-parent', so the correct parent list for that file version are is just
 
524
    ['good-parent'].
 
525
    """
 
526
 
 
527
    def all_versions_after_reconcile(self):
 
528
        return ('bad-parent', 'good-parent', 'broken-revision')
 
529
 
 
530
    def populated_parents(self):
 
531
        return (
 
532
            ((), 'bad-parent'),
 
533
            (('bad-parent',), 'good-parent'),
 
534
            (('good-parent', 'bad-parent'), 'broken-revision'))
 
535
 
 
536
    def corrected_parents(self):
 
537
        return (
 
538
            ((), 'bad-parent'),
 
539
            (('bad-parent',), 'good-parent'),
 
540
            (('good-parent',), 'broken-revision'))
 
541
 
 
542
    def check_regexes(self, repo):
 
543
        if repo.supports_rich_root():
 
544
            # TREE_ROOT will be wrong; but we're not testing it. so just adjust
 
545
            # the expected count of errors.
 
546
            count = 3
 
547
        else:
 
548
            count = 1
 
549
        return (
 
550
            '     %d inconsistent parents' % count,
 
551
            (r"      \* a-file-id version broken-revision has parents "
 
552
             r"\('good-parent', 'bad-parent'\) but "
 
553
             r"should have \('good-parent',\)"))
 
554
 
 
555
    def populate_repository(self, repo):
 
556
        inv = self.make_one_file_inventory(
 
557
            repo, 'bad-parent', (), root_revision='bad-parent')
 
558
        self.add_revision(repo, 'bad-parent', inv, ())
 
559
 
 
560
        inv = self.make_one_file_inventory(
 
561
            repo, 'good-parent', ('bad-parent',))
 
562
        self.add_revision(repo, 'good-parent', inv, ('bad-parent',))
 
563
 
 
564
        inv = self.make_one_file_inventory(
 
565
            repo, 'broken-revision', ('good-parent', 'bad-parent'))
 
566
        self.add_revision(repo, 'broken-revision', inv, ('good-parent',))
 
567
        self.versioned_root = repo.supports_rich_root()
 
568
 
 
569
    def repository_text_key_references(self):
 
570
        result = {}
 
571
        if self.versioned_root:
 
572
            result.update({('TREE_ROOT', 'bad-parent'): True,
 
573
                           ('TREE_ROOT', 'broken-revision'): True,
 
574
                           ('TREE_ROOT', 'good-parent'): True})
 
575
        result.update({('a-file-id', 'bad-parent'): True,
 
576
                       ('a-file-id', 'broken-revision'): True,
 
577
                       ('a-file-id', 'good-parent'): True})
 
578
        return result
 
579
 
 
580
    def repository_text_keys(self):
 
581
        return {('a-file-id', 'bad-parent'): [NULL_REVISION],
 
582
                ('a-file-id', 'broken-revision'):
 
583
                    [('a-file-id', 'good-parent')],
 
584
                ('a-file-id', 'good-parent'): [('a-file-id', 'bad-parent')]}
 
585
 
 
586
    def versioned_repository_text_keys(self):
 
587
        return {('TREE_ROOT', 'bad-parent'): [NULL_REVISION],
 
588
                ('TREE_ROOT', 'broken-revision'):
 
589
                    [('TREE_ROOT', 'good-parent')],
 
590
                ('TREE_ROOT', 'good-parent'): [('TREE_ROOT', 'bad-parent')]}
 
591
 
 
592
 
 
593
class ClaimedFileParentDidNotModifyFileScenario(BrokenRepoScenario):
 
594
    """A scenario where the file parent is the same as the revision parent, but
 
595
    should not be because that revision did not modify the file.
 
596
 
 
597
    Specifically, the parent revision of 'current' is
 
598
    'modified-something-else', which does not modify 'a-file', but the
 
599
    'current' version of 'a-file' erroneously claims that
 
600
    'modified-something-else' is the parent file version.
 
601
    """
 
602
 
 
603
    def all_versions_after_reconcile(self):
 
604
        return ('basis', 'current')
 
605
 
 
606
    def populated_parents(self):
 
607
        return (
 
608
            ((), 'basis'),
 
609
            (('basis',), 'modified-something-else'),
 
610
            (('modified-something-else',), 'current'))
 
611
 
 
612
    def corrected_parents(self):
 
613
        return (
 
614
            ((), 'basis'),
 
615
            (None, 'modified-something-else'),
 
616
            (('basis',), 'current'))
 
617
 
 
618
    def check_regexes(self, repo):
 
619
        if repo.supports_rich_root():
 
620
            # TREE_ROOT will be wrong; but we're not testing it. so just adjust
 
621
            # the expected count of errors.
 
622
            count = 3
 
623
        else:
 
624
            count = 1
 
625
        return (
 
626
            "%d inconsistent parents" % count,
 
627
            r"\* a-file-id version current has parents "
 
628
            r"\('modified-something-else',\) but should have \('basis',\)",
 
629
            )
 
630
 
 
631
    def populate_repository(self, repo):
 
632
        inv = self.make_one_file_inventory(repo, 'basis', ())
 
633
        self.add_revision(repo, 'basis', inv, ())
 
634
 
 
635
        # 'modified-something-else' is a correctly recorded revision, but it
 
636
        # does not modify the file we are looking at, so the inventory for that
 
637
        # file in this revision points to 'basis'.
 
638
        inv = self.make_one_file_inventory(
 
639
            repo, 'modified-something-else', ('basis',), inv_revision='basis')
 
640
        self.add_revision(repo, 'modified-something-else', inv, ('basis',))
 
641
 
 
642
        # The 'current' revision has 'modified-something-else' as its parent,
 
643
        # but the 'current' version of 'a-file' should have 'basis' as its
 
644
        # parent.
 
645
        inv = self.make_one_file_inventory(
 
646
            repo, 'current', ('modified-something-else',))
 
647
        self.add_revision(repo, 'current', inv, ('modified-something-else',))
 
648
        self.versioned_root = repo.supports_rich_root()
 
649
 
 
650
    def repository_text_key_references(self):
 
651
        result = {}
 
652
        if self.versioned_root:
 
653
            result.update({('TREE_ROOT', 'basis'): True,
 
654
                           ('TREE_ROOT', 'current'): True,
 
655
                           ('TREE_ROOT', 'modified-something-else'): True})
 
656
        result.update({('a-file-id', 'basis'): True,
 
657
                       ('a-file-id', 'current'): True})
 
658
        return result
 
659
 
 
660
    def repository_text_keys(self):
 
661
        return {('a-file-id', 'basis'): [NULL_REVISION],
 
662
                ('a-file-id', 'current'): [('a-file-id', 'basis')]}
 
663
 
 
664
    def versioned_repository_text_keys(self):
 
665
        return {('TREE_ROOT', 'basis'): ['null:'],
 
666
                ('TREE_ROOT', 'current'):
 
667
                    [('TREE_ROOT', 'modified-something-else')],
 
668
                ('TREE_ROOT', 'modified-something-else'):
 
669
                    [('TREE_ROOT', 'basis')]}
 
670
 
 
671
 
 
672
class IncorrectlyOrderedParentsScenario(BrokenRepoScenario):
 
673
    """A scenario where the set parents of a version of a file are correct, but
 
674
    the order of those parents is incorrect.
 
675
 
 
676
    This defines a 'broken-revision-1-2' and a 'broken-revision-2-1' which both
 
677
    have their file version parents reversed compared to the revision parents,
 
678
    which is invalid.  (We use two revisions with opposite orderings of the
 
679
    same parents to make sure that accidentally relying on dictionary/set
 
680
    ordering cannot make the test pass; the assumption is that while dict/set
 
681
    iteration order is arbitrary, it is also consistent within a single test).
 
682
    """
 
683
 
 
684
    def all_versions_after_reconcile(self):
 
685
        return ['parent-1', 'parent-2', 'broken-revision-1-2',
 
686
                'broken-revision-2-1']
 
687
 
 
688
    def populated_parents(self):
 
689
        return (
 
690
            ((), 'parent-1'),
 
691
            ((), 'parent-2'),
 
692
            (('parent-2', 'parent-1'), 'broken-revision-1-2'),
 
693
            (('parent-1', 'parent-2'), 'broken-revision-2-1'))
 
694
 
 
695
    def corrected_parents(self):
 
696
        return (
 
697
            ((), 'parent-1'),
 
698
            ((), 'parent-2'),
 
699
            (('parent-1', 'parent-2'), 'broken-revision-1-2'),
 
700
            (('parent-2', 'parent-1'), 'broken-revision-2-1'))
 
701
 
 
702
    def check_regexes(self, repo):
 
703
        if repo.supports_rich_root():
 
704
            # TREE_ROOT will be wrong; but we're not testing it. so just adjust
 
705
            # the expected count of errors.
 
706
            count = 4
 
707
        else:
 
708
            count = 2
 
709
        return (
 
710
            "%d inconsistent parents" % count,
 
711
            r"\* a-file-id version broken-revision-1-2 has parents "
 
712
            r"\('parent-2', 'parent-1'\) but should have "
 
713
            r"\('parent-1', 'parent-2'\)",
 
714
            r"\* a-file-id version broken-revision-2-1 has parents "
 
715
            r"\('parent-1', 'parent-2'\) but should have "
 
716
            r"\('parent-2', 'parent-1'\)")
 
717
 
 
718
    def populate_repository(self, repo):
 
719
        inv = self.make_one_file_inventory(repo, 'parent-1', [])
 
720
        self.add_revision(repo, 'parent-1', inv, [])
 
721
 
 
722
        inv = self.make_one_file_inventory(repo, 'parent-2', [])
 
723
        self.add_revision(repo, 'parent-2', inv, [])
 
724
 
 
725
        inv = self.make_one_file_inventory(
 
726
            repo, 'broken-revision-1-2', ['parent-2', 'parent-1'])
 
727
        self.add_revision(
 
728
            repo, 'broken-revision-1-2', inv, ['parent-1', 'parent-2'])
 
729
 
 
730
        inv = self.make_one_file_inventory(
 
731
            repo, 'broken-revision-2-1', ['parent-1', 'parent-2'])
 
732
        self.add_revision(
 
733
            repo, 'broken-revision-2-1', inv, ['parent-2', 'parent-1'])
 
734
        self.versioned_root = repo.supports_rich_root()
 
735
 
 
736
    def repository_text_key_references(self):
 
737
        result = {}
 
738
        if self.versioned_root:
 
739
            result.update({('TREE_ROOT', 'broken-revision-1-2'): True,
 
740
                           ('TREE_ROOT', 'broken-revision-2-1'): True,
 
741
                           ('TREE_ROOT', 'parent-1'): True,
 
742
                           ('TREE_ROOT', 'parent-2'): True})
 
743
        result.update({('a-file-id', 'broken-revision-1-2'): True,
 
744
                       ('a-file-id', 'broken-revision-2-1'): True,
 
745
                       ('a-file-id', 'parent-1'): True,
 
746
                       ('a-file-id', 'parent-2'): True})
 
747
        return result
 
748
 
 
749
    def repository_text_keys(self):
 
750
        return {('a-file-id', 'broken-revision-1-2'):
 
751
                    [('a-file-id', 'parent-1'), ('a-file-id', 'parent-2')],
 
752
                ('a-file-id', 'broken-revision-2-1'):
 
753
                    [('a-file-id', 'parent-2'), ('a-file-id', 'parent-1')],
 
754
                ('a-file-id', 'parent-1'): [NULL_REVISION],
 
755
                ('a-file-id', 'parent-2'): [NULL_REVISION]}
 
756
 
 
757
    def versioned_repository_text_keys(self):
 
758
        return {('TREE_ROOT', 'broken-revision-1-2'):
 
759
                    [('TREE_ROOT', 'parent-1'), ('TREE_ROOT', 'parent-2')],
 
760
                ('TREE_ROOT', 'broken-revision-2-1'):
 
761
                    [('TREE_ROOT', 'parent-2'), ('TREE_ROOT', 'parent-1')],
 
762
                ('TREE_ROOT', 'parent-1'): [NULL_REVISION],
 
763
                ('TREE_ROOT', 'parent-2'): [NULL_REVISION]}
 
764
 
 
765
 
 
766
all_broken_scenario_classes = [
 
767
    UndamagedRepositoryScenario,
 
768
    FileParentIsNotInRevisionAncestryScenario,
 
769
    FileParentHasInaccessibleInventoryScenario,
 
770
    FileParentsNotReferencedByAnyInventoryScenario,
 
771
    TooManyParentsScenario,
 
772
    ClaimedFileParentDidNotModifyFileScenario,
 
773
    IncorrectlyOrderedParentsScenario,
 
774
    UnreferencedFileParentsFromNoOpMergeScenario,
 
775
    ]
 
776
 
 
777
 
 
778
def broken_scenarios_for_all_formats():
 
779
    format_scenarios = all_repository_vf_format_scenarios()
 
780
    # test_check_reconcile needs to be parameterized by format *and* by broken
 
781
    # repository scenario.
 
782
    broken_scenarios = [(s.__name__, {'scenario_class': s})
 
783
                        for s in all_broken_scenario_classes]
 
784
    return multiply_scenarios(format_scenarios, broken_scenarios)
28
785
 
29
786
 
30
787
class TestFileParentReconciliation(TestCaseWithRepository):
31
788
    """Tests for how reconcile corrects errors in parents of file versions."""
32
789
 
 
790
    scenarios = broken_scenarios_for_all_formats()
 
791
 
33
792
    def make_populated_repository(self, factory):
34
793
        """Create a new repository populated by the given factory."""
35
794
        repo = self.make_repository('broken-repo')
64
823
        revision = Revision(revision_id, committer='jrandom@example.com',
65
824
            timestamp=0, inventory_sha1='', timezone=0, message='foo',
66
825
            parent_ids=parent_ids)
67
 
        repo.add_revision(revision_id,revision, inv)
 
826
        repo.add_revision(revision_id, revision, inv)
68
827
 
69
828
    def make_one_file_inventory(self, repo, revision, parents,
70
829
                                inv_revision=None, root_revision=None,
97
856
        entry.text_size = 0
98
857
        if file_contents is None:
99
858
            file_contents = '%sline\n' % entry.revision
100
 
        entry.text_sha1 = osutils.sha(file_contents).hexdigest()
 
859
        entry.text_sha1 = osutils.sha_string(file_contents)
101
860
        inv.add(entry)
102
861
        if make_file_version:
103
862
            repo.texts.add_lines((file_id, revision),