~debian-bazaar/debian/sid/bzr/unstable

« back to all changes in this revision

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

  • Committer: Jelmer Vernooij
  • Date: 2019-02-23 22:23:54 UTC
  • Revision ID: jelmer@jelmer.uk-20190223222354-np2qpunfi10llrex
Add transitional packages to brz.

Show diffs side-by-side

added added

removed removed

Lines of Context:
1
 
# Copyright (C) 2007-2010 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., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA
16
 
 
17
 
"""Tests that use BrokenRepoScenario objects.
18
 
 
19
 
That is, tests for reconcile and check.
20
 
"""
21
 
 
22
 
from bzrlib import osutils
23
 
 
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)
785
 
 
786
 
 
787
 
class TestFileParentReconciliation(TestCaseWithRepository):
788
 
    """Tests for how reconcile corrects errors in parents of file versions."""
789
 
 
790
 
    scenarios = broken_scenarios_for_all_formats()
791
 
 
792
 
    def make_populated_repository(self, factory):
793
 
        """Create a new repository populated by the given factory."""
794
 
        repo = self.make_repository('broken-repo')
795
 
        repo.lock_write()
796
 
        try:
797
 
            repo.start_write_group()
798
 
            try:
799
 
                factory(repo)
800
 
                repo.commit_write_group()
801
 
                return repo
802
 
            except:
803
 
                repo.abort_write_group()
804
 
                raise
805
 
        finally:
806
 
            repo.unlock()
807
 
 
808
 
    def add_revision(self, repo, revision_id, inv, parent_ids):
809
 
        """Add a revision with a given inventory and parents to a repository.
810
 
 
811
 
        :param repo: a repository.
812
 
        :param revision_id: the revision ID for the new revision.
813
 
        :param inv: an inventory (such as created by
814
 
            `make_one_file_inventory`).
815
 
        :param parent_ids: the parents for the new revision.
816
 
        """
817
 
        inv.revision_id = revision_id
818
 
        inv.root.revision = revision_id
819
 
        if repo.supports_rich_root():
820
 
            root_id = inv.root.file_id
821
 
            repo.texts.add_lines((root_id, revision_id), [], [])
822
 
        repo.add_inventory(revision_id, inv, parent_ids)
823
 
        revision = Revision(revision_id, committer='jrandom@example.com',
824
 
            timestamp=0, inventory_sha1='', timezone=0, message='foo',
825
 
            parent_ids=parent_ids)
826
 
        repo.add_revision(revision_id, revision, inv)
827
 
 
828
 
    def make_one_file_inventory(self, repo, revision, parents,
829
 
                                inv_revision=None, root_revision=None,
830
 
                                file_contents=None, make_file_version=True):
831
 
        """Make an inventory containing a version of a file with ID 'a-file'.
832
 
 
833
 
        The file's ID will be 'a-file', and its filename will be 'a file name',
834
 
        stored at the tree root.
835
 
 
836
 
        :param repo: a repository to add the new file version to.
837
 
        :param revision: the revision ID of the new inventory.
838
 
        :param parents: the parents for this revision of 'a-file'.
839
 
        :param inv_revision: if not None, the revision ID to store in the
840
 
            inventory entry.  Otherwise, this defaults to revision.
841
 
        :param root_revision: if not None, the inventory's root.revision will
842
 
            be set to this.
843
 
        :param file_contents: if not None, the contents of this file version.
844
 
            Otherwise a unique default (based on revision ID) will be
845
 
            generated.
846
 
        """
847
 
        inv = Inventory(revision_id=revision)
848
 
        if root_revision is not None:
849
 
            inv.root.revision = root_revision
850
 
        file_id = 'a-file-id'
851
 
        entry = InventoryFile(file_id, 'a file name', 'TREE_ROOT')
852
 
        if inv_revision is not None:
853
 
            entry.revision = inv_revision
854
 
        else:
855
 
            entry.revision = revision
856
 
        entry.text_size = 0
857
 
        if file_contents is None:
858
 
            file_contents = '%sline\n' % entry.revision
859
 
        entry.text_sha1 = osutils.sha_string(file_contents)
860
 
        inv.add(entry)
861
 
        if make_file_version:
862
 
            repo.texts.add_lines((file_id, revision),
863
 
                [(file_id, parent) for parent in parents], [file_contents])
864
 
        return inv
865
 
 
866
 
    def require_repo_suffers_text_parent_corruption(self, repo):
867
 
        if not repo._reconcile_fixes_text_parents:
868
 
            raise TestNotApplicable(
869
 
                    "Format does not support text parent reconciliation")
870
 
 
871
 
    def file_parents(self, repo, revision_id):
872
 
        key = ('a-file-id', revision_id)
873
 
        parent_map = repo.texts.get_parent_map([key])
874
 
        return tuple(parent[-1] for parent in parent_map[key])
875
 
 
876
 
    def assertFileVersionAbsent(self, repo, revision_id):
877
 
        self.assertEqual({},
878
 
            repo.texts.get_parent_map([('a-file-id', revision_id)]))
879
 
 
880
 
    def assertParentsMatch(self, expected_parents_for_versions, repo,
881
 
                           when_description):
882
 
        for expected_parents, version in expected_parents_for_versions:
883
 
            if expected_parents is None:
884
 
                self.assertFileVersionAbsent(repo, version)
885
 
            else:
886
 
                found_parents = self.file_parents(repo, version)
887
 
                self.assertEqual(expected_parents, found_parents,
888
 
                    "%s reconcile %s has parents %s, should have %s."
889
 
                    % (when_description, version, found_parents,
890
 
                       expected_parents))
891
 
 
892
 
    def prepare_test_repository(self):
893
 
        """Prepare a repository to test with from the test scenario.
894
 
 
895
 
        :return: A repository, and the scenario instance.
896
 
        """
897
 
        scenario = self.scenario_class(self)
898
 
        repo = self.make_populated_repository(scenario.populate_repository)
899
 
        self.require_repo_suffers_text_parent_corruption(repo)
900
 
        return repo, scenario
901
 
 
902
 
    def shas_for_versions_of_file(self, repo, versions):
903
 
        """Get the SHA-1 hashes of the versions of 'a-file' in the repository.
904
 
 
905
 
        :param repo: the repository to get the hashes from.
906
 
        :param versions: a list of versions to get hashes for.
907
 
 
908
 
        :returns: A dict of `{version: hash}`.
909
 
        """
910
 
        keys = [('a-file-id', version) for version in versions]
911
 
        return repo.texts.get_sha1s(keys)
912
 
 
913
 
    def test_reconcile_behaviour(self):
914
 
        """Populate a repository and reconcile it, verifying the state before
915
 
        and after.
916
 
        """
917
 
        repo, scenario = self.prepare_test_repository()
918
 
        repo.lock_read()
919
 
        try:
920
 
            self.assertParentsMatch(scenario.populated_parents(), repo,
921
 
                'before')
922
 
            vf_shas = self.shas_for_versions_of_file(
923
 
                repo, scenario.all_versions_after_reconcile())
924
 
        finally:
925
 
            repo.unlock()
926
 
        result = repo.reconcile(thorough=True)
927
 
        repo.lock_read()
928
 
        try:
929
 
            self.assertParentsMatch(scenario.corrected_parents(), repo,
930
 
                'after')
931
 
            # The contents of the versions in the versionedfile should be the
932
 
            # same after the reconcile.
933
 
            self.assertEqual(
934
 
                vf_shas,
935
 
                self.shas_for_versions_of_file(
936
 
                    repo, scenario.all_versions_after_reconcile()))
937
 
 
938
 
            # Scenario.corrected_fulltexts contains texts which the test wants
939
 
            # to assert are now fulltexts. However this is an abstraction
940
 
            # violation; really we care that:
941
 
            # - the text is reconstructable
942
 
            # - it has an empty parents list
943
 
            # (we specify it this way because a store can use arbitrary
944
 
            # compression pointers in principle.
945
 
            for file_version in scenario.corrected_fulltexts():
946
 
                key = ('a-file-id', file_version)
947
 
                self.assertEqual({key:()}, repo.texts.get_parent_map([key]))
948
 
                self.assertIsInstance(
949
 
                    repo.texts.get_record_stream([key], 'unordered',
950
 
                        True).next().get_bytes_as('fulltext'),
951
 
                    str)
952
 
        finally:
953
 
            repo.unlock()
954
 
 
955
 
    def test_check_behaviour(self):
956
 
        """Populate a repository and check it, and verify the output."""
957
 
        repo, scenario = self.prepare_test_repository()
958
 
        check_result = repo.check()
959
 
        check_result.report_results(verbose=True)
960
 
        log = self.get_log()
961
 
        for pattern in scenario.check_regexes(repo):
962
 
            self.assertContainsRe(log, pattern)
963
 
 
964
 
    def test_find_text_key_references(self):
965
 
        """Test that find_text_key_references finds erroneous references."""
966
 
        repo, scenario = self.prepare_test_repository()
967
 
        repo.lock_read()
968
 
        self.addCleanup(repo.unlock)
969
 
        self.assertEqual(scenario.repository_text_key_references(),
970
 
            repo.find_text_key_references())
971
 
 
972
 
    def test__generate_text_key_index(self):
973
 
        """Test that the generated text key index has all entries."""
974
 
        repo, scenario = self.prepare_test_repository()
975
 
        repo.lock_read()
976
 
        self.addCleanup(repo.unlock)
977
 
        self.assertEqual(scenario.repository_text_key_index(),
978
 
            repo._generate_text_key_index())