~bzr-pqm/bzr/bzr.dev

« back to all changes in this revision

Viewing changes to bzrlib/tests/per_repository/__init__.py

  • Committer: Andrew Bennetts
  • Date: 2010-10-08 08:15:14 UTC
  • mto: This revision was merged to the branch mainline in revision 5498.
  • Revision ID: andrew.bennetts@canonical.com-20101008081514-dviqzrdfwyzsqbz2
Split NEWS into per-release doc/en/release-notes/bzr-*.txt

Show diffs side-by-side

added added

removed removed

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