~bzr-pqm/bzr/bzr.dev

4763.2.4 by John Arbash Meinel
merge bzr.2.1 in preparation for NEWS entry.
1
# Copyright (C) 2007-2010 Canonical Ltd
2745.6.42 by Andrew Bennetts
Use TestScenarioApplier to more cleanly parameterise check and reconcile tests.
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
4183.7.1 by Sabin Iacob
update FSF mailing address
15
# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA
2745.6.42 by Andrew Bennetts
Use TestScenarioApplier to more cleanly parameterise check and reconcile tests.
16
2745.6.44 by Andrew Bennetts
More docstring and formatting tweaks.
17
"""Tests that use BrokenRepoScenario objects.
18
19
That is, tests for reconcile and check.
20
"""
21
3734.2.4 by Vincent Ladeuil
Fix python2.6 deprecation warnings related to hashlib.
22
from bzrlib import osutils
2745.6.46 by Andrew Bennetts
Improvements to check/reconcile tests suggested by review.
23
5684.3.2 by Jelmer Vernooij
Merge per-repository-vf, move scenarios to test_check_reconcile.
24
from bzrlib.inventory import (
25
    Inventory,
26
    InventoryFile,
27
    )
28
from bzrlib.revision import (
29
    NULL_REVISION,
30
    Revision,
31
    )
32
from bzrlib.tests import (
33
    TestNotApplicable,
34
    multiply_scenarios,
35
    )
36
from bzrlib.tests.per_repository_vf import (
37
    TestCaseWithRepository,
38
    all_repository_vf_format_scenarios,
39
    )
40
from bzrlib.tests.scenarios import load_tests_apply_scenarios
41
42
43
load_tests = load_tests_apply_scenarios
44
45
46
class BrokenRepoScenario(object):
47
    """Base class for defining scenarios for testing check and reconcile.
48
49
    A subclass needs to define the following methods:
50
        :populate_repository: a method to use to populate a repository with
51
            sample revisions, inventories and file versions.
52
        :all_versions_after_reconcile: all the versions in repository after
53
            reconcile.  run_test verifies that the text of each of these
54
            versions of the file is unchanged by the reconcile.
55
        :populated_parents: a list of (parents list, revision).  Each version
56
            of the file is verified to have the given parents before running
57
            the reconcile.  i.e. this is used to assert that the repo from the
58
            factory is what we expect.
59
        :corrected_parents: a list of (parents list, revision).  Each version
60
            of the file is verified to have the given parents after the
61
            reconcile.  i.e. this is used to assert that reconcile made the
62
            changes we expect it to make.
63
64
    A subclass may define the following optional method as well:
65
        :corrected_fulltexts: a list of file versions that should be stored as
66
            fulltexts (not deltas) after reconcile.  run_test will verify that
67
            this occurs.
68
    """
69
70
    def __init__(self, test_case):
71
        self.test_case = test_case
72
73
    def make_one_file_inventory(self, repo, revision, parents,
74
                                inv_revision=None, root_revision=None,
75
                                file_contents=None, make_file_version=True):
76
        return self.test_case.make_one_file_inventory(
77
            repo, revision, parents, inv_revision=inv_revision,
78
            root_revision=root_revision, file_contents=file_contents,
79
            make_file_version=make_file_version)
80
81
    def add_revision(self, repo, revision_id, inv, parent_ids):
82
        return self.test_case.add_revision(repo, revision_id, inv, parent_ids)
83
84
    def corrected_fulltexts(self):
85
        return []
86
87
    def repository_text_key_index(self):
88
        result = {}
89
        if self.versioned_root:
90
            result.update(self.versioned_repository_text_keys())
91
        result.update(self.repository_text_keys())
92
        return result
93
94
95
class UndamagedRepositoryScenario(BrokenRepoScenario):
96
    """A scenario where the repository has no damage.
97
98
    It has a single revision, 'rev1a', with a single file.
99
    """
100
101
    def all_versions_after_reconcile(self):
102
        return ('rev1a', )
103
104
    def populated_parents(self):
105
        return (((), 'rev1a'), )
106
107
    def corrected_parents(self):
108
        # Same as the populated parents, because there was nothing wrong.
109
        return self.populated_parents()
110
111
    def check_regexes(self, repo):
112
        return ["0 unreferenced text versions"]
113
114
    def populate_repository(self, repo):
115
        # make rev1a: A well-formed revision, containing 'a-file'
116
        inv = self.make_one_file_inventory(
117
            repo, 'rev1a', [], root_revision='rev1a')
118
        self.add_revision(repo, 'rev1a', inv, [])
119
        self.versioned_root = repo.supports_rich_root()
120
121
    def repository_text_key_references(self):
122
        result = {}
123
        if self.versioned_root:
124
            result.update({('TREE_ROOT', 'rev1a'): True})
125
        result.update({('a-file-id', 'rev1a'): True})
126
        return result
127
128
    def repository_text_keys(self):
129
        return {('a-file-id', 'rev1a'):[NULL_REVISION]}
130
131
    def versioned_repository_text_keys(self):
132
        return {('TREE_ROOT', 'rev1a'):[NULL_REVISION]}
133
134
135
class FileParentIsNotInRevisionAncestryScenario(BrokenRepoScenario):
136
    """A scenario where a revision 'rev2' has 'a-file' with a
137
    parent 'rev1b' that is not in the revision ancestry.
138
139
    Reconcile should remove 'rev1b' from the parents list of 'a-file' in
140
    'rev2', preserving 'rev1a' as a parent.
141
    """
142
143
    def all_versions_after_reconcile(self):
144
        return ('rev1a', 'rev2')
145
146
    def populated_parents(self):
147
        return (
148
            ((), 'rev1a'),
149
            ((), 'rev1b'), # Will be gc'd
150
            (('rev1a', 'rev1b'), 'rev2')) # Will have parents trimmed
151
152
    def corrected_parents(self):
153
        return (
154
            ((), 'rev1a'),
155
            (None, 'rev1b'),
156
            (('rev1a',), 'rev2'))
157
158
    def check_regexes(self, repo):
159
        return [r"\* a-file-id version rev2 has parents \('rev1a', 'rev1b'\) "
160
                r"but should have \('rev1a',\)",
161
                "1 unreferenced text versions",
162
                ]
163
164
    def populate_repository(self, repo):
165
        # make rev1a: A well-formed revision, containing 'a-file'
166
        inv = self.make_one_file_inventory(
167
            repo, 'rev1a', [], root_revision='rev1a')
168
        self.add_revision(repo, 'rev1a', inv, [])
169
170
        # make rev1b, which has no Revision, but has an Inventory, and
171
        # a-file
172
        inv = self.make_one_file_inventory(
173
            repo, 'rev1b', [], root_revision='rev1b')
174
        repo.add_inventory('rev1b', inv, [])
175
176
        # make rev2, with a-file.
177
        # a-file has 'rev1b' as an ancestor, even though this is not
178
        # mentioned by 'rev1a', making it an unreferenced ancestor
179
        inv = self.make_one_file_inventory(
180
            repo, 'rev2', ['rev1a', 'rev1b'])
181
        self.add_revision(repo, 'rev2', inv, ['rev1a'])
182
        self.versioned_root = repo.supports_rich_root()
183
184
    def repository_text_key_references(self):
185
        result = {}
186
        if self.versioned_root:
187
            result.update({('TREE_ROOT', 'rev1a'): True,
188
                           ('TREE_ROOT', 'rev2'): True})
189
        result.update({('a-file-id', 'rev1a'): True,
190
                       ('a-file-id', 'rev2'): True})
191
        return result
192
193
    def repository_text_keys(self):
194
        return {('a-file-id', 'rev1a'):[NULL_REVISION],
195
                ('a-file-id', 'rev2'):[('a-file-id', 'rev1a')]}
196
197
    def versioned_repository_text_keys(self):
198
        return {('TREE_ROOT', 'rev1a'):[NULL_REVISION],
199
                ('TREE_ROOT', 'rev2'):[('TREE_ROOT', 'rev1a')]}
200
201
202
class FileParentHasInaccessibleInventoryScenario(BrokenRepoScenario):
203
    """A scenario where a revision 'rev3' containing 'a-file' modified in
204
    'rev3', and with a parent which is in the revision ancestory, but whose
205
    inventory cannot be accessed at all.
206
207
    Reconcile should remove the file version parent whose inventory is
208
    inaccessbile (i.e. remove 'rev1c' from the parents of a-file's rev3).
209
    """
210
211
    def all_versions_after_reconcile(self):
212
        return ('rev2', 'rev3')
213
214
    def populated_parents(self):
215
        return (
216
            ((), 'rev2'),
217
            (('rev1c',), 'rev3'))
218
219
    def corrected_parents(self):
220
        return (
221
            ((), 'rev2'),
222
            ((), 'rev3'))
223
224
    def check_regexes(self, repo):
225
        return [r"\* a-file-id version rev3 has parents "
226
                r"\('rev1c',\) but should have \(\)",
227
                ]
228
229
    def populate_repository(self, repo):
230
        # make rev2, with a-file
231
        # a-file is sane
232
        inv = self.make_one_file_inventory(repo, 'rev2', [])
233
        self.add_revision(repo, 'rev2', inv, [])
234
235
        # make ghost revision rev1c, with a version of a-file present so
236
        # that we generate a knit delta against this version.  In real life
237
        # the ghost might never have been present or rev3 might have been
238
        # generated against a revision that was present at the time.  So
239
        # currently we have the full history of a-file present even though
240
        # the inventory and revision objects are not.
241
        self.make_one_file_inventory(repo, 'rev1c', [])
242
243
        # make rev3 with a-file
244
        # a-file refers to 'rev1c', which is a ghost in this repository, so
245
        # a-file cannot have rev1c as its ancestor.
246
        inv = self.make_one_file_inventory(repo, 'rev3', ['rev1c'])
247
        self.add_revision(repo, 'rev3', inv, ['rev1c', 'rev1a'])
248
        self.versioned_root = repo.supports_rich_root()
249
250
    def repository_text_key_references(self):
251
        result = {}
252
        if self.versioned_root:
253
            result.update({('TREE_ROOT', 'rev2'): True,
254
                           ('TREE_ROOT', 'rev3'): True})
255
        result.update({('a-file-id', 'rev2'): True,
256
                       ('a-file-id', 'rev3'): True})
257
        return result
258
259
    def repository_text_keys(self):
260
        return {('a-file-id', 'rev2'):[NULL_REVISION],
261
                ('a-file-id', 'rev3'):[NULL_REVISION]}
262
263
    def versioned_repository_text_keys(self):
264
        return {('TREE_ROOT', 'rev2'):[NULL_REVISION],
265
                ('TREE_ROOT', 'rev3'):[NULL_REVISION]}
266
267
268
class FileParentsNotReferencedByAnyInventoryScenario(BrokenRepoScenario):
269
    """A scenario where a repository with file 'a-file' which has extra
270
    per-file versions that are not referenced by any inventory (even though
271
    they have the same ID as actual revisions).  The inventory of 'rev2'
272
    references 'rev1a' of 'a-file', but there is a 'rev2' of 'some-file' stored
273
    and erroneously referenced by later per-file versions (revisions 'rev4' and
274
    'rev5').
275
276
    Reconcile should remove the file parents that are not referenced by any
277
    inventory.
278
    """
279
280
    def all_versions_after_reconcile(self):
281
        return ('rev1a', 'rev2c', 'rev4', 'rev5')
282
283
    def populated_parents(self):
284
        return [
285
            (('rev1a',), 'rev2'),
286
            (('rev1a',), 'rev2b'),
287
            (('rev2',), 'rev3'),
288
            (('rev2',), 'rev4'),
289
            (('rev2', 'rev2c'), 'rev5')]
290
291
    def corrected_parents(self):
292
        return (
293
            # rev2 and rev2b have been removed.
294
            (None, 'rev2'),
295
            (None, 'rev2b'),
296
            # rev3's accessible parent inventories all have rev1a as the last
297
            # modifier.
298
            (('rev1a',), 'rev3'),
299
            # rev1a features in both rev4's parents but should only appear once
300
            # in the result
301
            (('rev1a',), 'rev4'),
302
            # rev2c is the head of rev1a and rev2c, the inventory provided
303
            # per-file last-modified revisions.
304
            (('rev2c',), 'rev5'))
305
306
    def check_regexes(self, repo):
307
        if repo.supports_rich_root():
308
            # TREE_ROOT will be wrong; but we're not testing it. so just adjust
309
            # the expected count of errors.
310
            count = 9
311
        else:
312
            count = 3
313
        return [
314
            # will be gc'd
315
            r"unreferenced version: {rev2} in a-file-id",
316
            r"unreferenced version: {rev2b} in a-file-id",
317
            # will be corrected
318
            r"a-file-id version rev3 has parents \('rev2',\) "
319
            r"but should have \('rev1a',\)",
320
            r"a-file-id version rev5 has parents \('rev2', 'rev2c'\) "
321
            r"but should have \('rev2c',\)",
322
            r"a-file-id version rev4 has parents \('rev2',\) "
323
            r"but should have \('rev1a',\)",
324
            "%d inconsistent parents" % count,
325
            ]
326
327
    def populate_repository(self, repo):
328
        # make rev1a: A well-formed revision, containing 'a-file'
329
        inv = self.make_one_file_inventory(
330
            repo, 'rev1a', [], root_revision='rev1a')
331
        self.add_revision(repo, 'rev1a', inv, [])
332
333
        # make rev2, with a-file.
334
        # a-file is unmodified from rev1a, and an unreferenced rev2 file
335
        # version is present in the repository.
336
        self.make_one_file_inventory(
337
            repo, 'rev2', ['rev1a'], inv_revision='rev1a')
338
        self.add_revision(repo, 'rev2', inv, ['rev1a'])
339
340
        # make rev3 with a-file
341
        # a-file has 'rev2' as its ancestor, but the revision in 'rev2' was
342
        # rev1a so this is inconsistent with rev2's inventory - it should
343
        # be rev1a, and at the revision level 1c is not present - it is a
344
        # ghost, so only the details from rev1a are available for
345
        # determining whether a delta is acceptable, or a full is needed,
346
        # and what the correct parents are.
347
        inv = self.make_one_file_inventory(repo, 'rev3', ['rev2'])
348
        self.add_revision(repo, 'rev3', inv, ['rev1c', 'rev1a'])
349
350
        # In rev2b, the true last-modifying-revision of a-file is rev1a,
351
        # inherited from rev2, but there is a version rev2b of the file, which
352
        # reconcile could remove, leaving no rev2b.  Most importantly,
353
        # revisions descending from rev2b should not have per-file parents of
354
        # a-file-rev2b.
355
        # ??? This is to test deduplication in fixing rev4
356
        inv = self.make_one_file_inventory(
357
            repo, 'rev2b', ['rev1a'], inv_revision='rev1a')
358
        self.add_revision(repo, 'rev2b', inv, ['rev1a'])
359
360
        # rev4 is for testing that when the last modified of a file in
361
        # multiple parent revisions is the same, that it only appears once
362
        # in the generated per file parents list: rev2 and rev2b both
363
        # descend from 1a and do not change the file a-file, so there should
364
        # be no version of a-file 'rev2' or 'rev2b', but rev4 does change
365
        # a-file, and is a merge of rev2 and rev2b, so it should end up with
366
        # a parent of just rev1a - the starting file parents list is simply
367
        # completely wrong.
368
        inv = self.make_one_file_inventory(repo, 'rev4', ['rev2'])
369
        self.add_revision(repo, 'rev4', inv, ['rev2', 'rev2b'])
370
371
        # rev2c changes a-file from rev1a, so the version it of a-file it
372
        # introduces is a head revision when rev5 is checked.
373
        inv = self.make_one_file_inventory(repo, 'rev2c', ['rev1a'])
374
        self.add_revision(repo, 'rev2c', inv, ['rev1a'])
375
376
        # rev5 descends from rev2 and rev2c; as rev2 does not alter a-file,
377
        # but rev2c does, this should use rev2c as the parent for the per
378
        # file history, even though more than one per-file parent is
379
        # available, because we use the heads of the revision parents for
380
        # the inventory modification revisions of the file to determine the
381
        # parents for the per file graph.
382
        inv = self.make_one_file_inventory(repo, 'rev5', ['rev2', 'rev2c'])
383
        self.add_revision(repo, 'rev5', inv, ['rev2', 'rev2c'])
384
        self.versioned_root = repo.supports_rich_root()
385
386
    def repository_text_key_references(self):
387
        result = {}
388
        if self.versioned_root:
389
            result.update({('TREE_ROOT', 'rev1a'): True,
390
                           ('TREE_ROOT', 'rev2'): True,
391
                           ('TREE_ROOT', 'rev2b'): True,
392
                           ('TREE_ROOT', 'rev2c'): True,
393
                           ('TREE_ROOT', 'rev3'): True,
394
                           ('TREE_ROOT', 'rev4'): True,
395
                           ('TREE_ROOT', 'rev5'): True})
396
        result.update({('a-file-id', 'rev1a'): True,
397
                       ('a-file-id', 'rev2c'): True,
398
                       ('a-file-id', 'rev3'): True,
399
                       ('a-file-id', 'rev4'): True,
400
                       ('a-file-id', 'rev5'): True})
401
        return result
402
403
    def repository_text_keys(self):
404
        return {('a-file-id', 'rev1a'): [NULL_REVISION],
405
                 ('a-file-id', 'rev2c'): [('a-file-id', 'rev1a')],
406
                 ('a-file-id', 'rev3'): [('a-file-id', 'rev1a')],
407
                 ('a-file-id', 'rev4'): [('a-file-id', 'rev1a')],
408
                 ('a-file-id', 'rev5'): [('a-file-id', 'rev2c')]}
409
410
    def versioned_repository_text_keys(self):
411
        return {('TREE_ROOT', 'rev1a'): [NULL_REVISION],
412
                ('TREE_ROOT', 'rev2'): [('TREE_ROOT', 'rev1a')],
413
                ('TREE_ROOT', 'rev2b'): [('TREE_ROOT', 'rev1a')],
414
                ('TREE_ROOT', 'rev2c'): [('TREE_ROOT', 'rev1a')],
415
                ('TREE_ROOT', 'rev3'): [('TREE_ROOT', 'rev1a')],
416
                ('TREE_ROOT', 'rev4'):
417
                    [('TREE_ROOT', 'rev2'), ('TREE_ROOT', 'rev2b')],
418
                ('TREE_ROOT', 'rev5'):
419
                    [('TREE_ROOT', 'rev2'), ('TREE_ROOT', 'rev2c')]}
420
421
422
class UnreferencedFileParentsFromNoOpMergeScenario(BrokenRepoScenario):
423
    """
424
    rev1a and rev1b with identical contents
425
    rev2 revision has parents of [rev1a, rev1b]
426
    There is a a-file:rev2 file version, not referenced by the inventory.
427
    """
428
429
    def all_versions_after_reconcile(self):
430
        return ('rev1a', 'rev1b', 'rev2', 'rev4')
431
432
    def populated_parents(self):
433
        return (
434
            ((), 'rev1a'),
435
            ((), 'rev1b'),
436
            (('rev1a', 'rev1b'), 'rev2'),
437
            (None, 'rev3'),
438
            (('rev2',), 'rev4'),
439
            )
440
441
    def corrected_parents(self):
442
        return (
443
            ((), 'rev1a'),
444
            ((), 'rev1b'),
445
            ((), 'rev2'),
446
            (None, 'rev3'),
447
            (('rev2',), 'rev4'),
448
            )
449
450
    def corrected_fulltexts(self):
451
        return ['rev2']
452
453
    def check_regexes(self, repo):
454
        return []
455
456
    def populate_repository(self, repo):
457
        # make rev1a: A well-formed revision, containing 'a-file'
458
        inv1a = self.make_one_file_inventory(
459
            repo, 'rev1a', [], root_revision='rev1a')
460
        self.add_revision(repo, 'rev1a', inv1a, [])
461
462
        # make rev1b: A well-formed revision, containing 'a-file'
463
        # rev1b of a-file has the exact same contents as rev1a.
464
        file_contents = repo.revision_tree('rev1a').get_file_text('a-file-id')
465
        inv = self.make_one_file_inventory(
466
            repo, 'rev1b', [], root_revision='rev1b',
467
            file_contents=file_contents)
468
        self.add_revision(repo, 'rev1b', inv, [])
469
470
        # make rev2, a merge of rev1a and rev1b, with a-file.
471
        # a-file is unmodified from rev1a and rev1b, but a new version is
472
        # wrongly present anyway.
473
        inv = self.make_one_file_inventory(
474
            repo, 'rev2', ['rev1a', 'rev1b'], inv_revision='rev1a',
475
            file_contents=file_contents)
476
        self.add_revision(repo, 'rev2', inv, ['rev1a', 'rev1b'])
477
478
        # rev3: a-file unchanged from rev2, but wrongly referencing rev2 of the
479
        # file in its inventory.
480
        inv = self.make_one_file_inventory(
481
            repo, 'rev3', ['rev2'], inv_revision='rev2',
482
            file_contents=file_contents, make_file_version=False)
483
        self.add_revision(repo, 'rev3', inv, ['rev2'])
484
485
        # rev4: a modification of a-file on top of rev3.
486
        inv = self.make_one_file_inventory(repo, 'rev4', ['rev2'])
487
        self.add_revision(repo, 'rev4', inv, ['rev3'])
488
        self.versioned_root = repo.supports_rich_root()
489
490
    def repository_text_key_references(self):
491
        result = {}
492
        if self.versioned_root:
493
            result.update({('TREE_ROOT', 'rev1a'): True,
494
                           ('TREE_ROOT', 'rev1b'): True,
495
                           ('TREE_ROOT', 'rev2'): True,
496
                           ('TREE_ROOT', 'rev3'): True,
497
                           ('TREE_ROOT', 'rev4'): True})
498
        result.update({('a-file-id', 'rev1a'): True,
499
                       ('a-file-id', 'rev1b'): True,
500
                       ('a-file-id', 'rev2'): False,
501
                       ('a-file-id', 'rev4'): True})
502
        return result
503
504
    def repository_text_keys(self):
505
        return {('a-file-id', 'rev1a'): [NULL_REVISION],
506
                ('a-file-id', 'rev1b'): [NULL_REVISION],
507
                ('a-file-id', 'rev2'): [NULL_REVISION],
508
                ('a-file-id', 'rev4'): [('a-file-id', 'rev2')]}
509
510
    def versioned_repository_text_keys(self):
511
        return {('TREE_ROOT', 'rev1a'): [NULL_REVISION],
512
                ('TREE_ROOT', 'rev1b'): [NULL_REVISION],
513
                ('TREE_ROOT', 'rev2'):
514
                    [('TREE_ROOT', 'rev1a'), ('TREE_ROOT', 'rev1b')],
515
                ('TREE_ROOT', 'rev3'): [('TREE_ROOT', 'rev2')],
516
                ('TREE_ROOT', 'rev4'): [('TREE_ROOT', 'rev3')]}
517
518
519
class TooManyParentsScenario(BrokenRepoScenario):
520
    """A scenario where 'broken-revision' of 'a-file' claims to have parents
521
    ['good-parent', 'bad-parent'].  However 'bad-parent' is in the ancestry of
522
    'good-parent', so the correct parent list for that file version are is just
523
    ['good-parent'].
524
    """
525
526
    def all_versions_after_reconcile(self):
527
        return ('bad-parent', 'good-parent', 'broken-revision')
528
529
    def populated_parents(self):
530
        return (
531
            ((), 'bad-parent'),
532
            (('bad-parent',), 'good-parent'),
533
            (('good-parent', 'bad-parent'), 'broken-revision'))
534
535
    def corrected_parents(self):
536
        return (
537
            ((), 'bad-parent'),
538
            (('bad-parent',), 'good-parent'),
539
            (('good-parent',), 'broken-revision'))
540
541
    def check_regexes(self, repo):
542
        if repo.supports_rich_root():
543
            # TREE_ROOT will be wrong; but we're not testing it. so just adjust
544
            # the expected count of errors.
545
            count = 3
546
        else:
547
            count = 1
548
        return (
549
            '     %d inconsistent parents' % count,
550
            (r"      \* a-file-id version broken-revision has parents "
551
             r"\('good-parent', 'bad-parent'\) but "
552
             r"should have \('good-parent',\)"))
553
554
    def populate_repository(self, repo):
555
        inv = self.make_one_file_inventory(
556
            repo, 'bad-parent', (), root_revision='bad-parent')
557
        self.add_revision(repo, 'bad-parent', inv, ())
558
559
        inv = self.make_one_file_inventory(
560
            repo, 'good-parent', ('bad-parent',))
561
        self.add_revision(repo, 'good-parent', inv, ('bad-parent',))
562
563
        inv = self.make_one_file_inventory(
564
            repo, 'broken-revision', ('good-parent', 'bad-parent'))
565
        self.add_revision(repo, 'broken-revision', inv, ('good-parent',))
566
        self.versioned_root = repo.supports_rich_root()
567
568
    def repository_text_key_references(self):
569
        result = {}
570
        if self.versioned_root:
571
            result.update({('TREE_ROOT', 'bad-parent'): True,
572
                           ('TREE_ROOT', 'broken-revision'): True,
573
                           ('TREE_ROOT', 'good-parent'): True})
574
        result.update({('a-file-id', 'bad-parent'): True,
575
                       ('a-file-id', 'broken-revision'): True,
576
                       ('a-file-id', 'good-parent'): True})
577
        return result
578
579
    def repository_text_keys(self):
580
        return {('a-file-id', 'bad-parent'): [NULL_REVISION],
581
                ('a-file-id', 'broken-revision'):
582
                    [('a-file-id', 'good-parent')],
583
                ('a-file-id', 'good-parent'): [('a-file-id', 'bad-parent')]}
584
585
    def versioned_repository_text_keys(self):
586
        return {('TREE_ROOT', 'bad-parent'): [NULL_REVISION],
587
                ('TREE_ROOT', 'broken-revision'):
588
                    [('TREE_ROOT', 'good-parent')],
589
                ('TREE_ROOT', 'good-parent'): [('TREE_ROOT', 'bad-parent')]}
590
591
592
class ClaimedFileParentDidNotModifyFileScenario(BrokenRepoScenario):
593
    """A scenario where the file parent is the same as the revision parent, but
594
    should not be because that revision did not modify the file.
595
596
    Specifically, the parent revision of 'current' is
597
    'modified-something-else', which does not modify 'a-file', but the
598
    'current' version of 'a-file' erroneously claims that
599
    'modified-something-else' is the parent file version.
600
    """
601
602
    def all_versions_after_reconcile(self):
603
        return ('basis', 'current')
604
605
    def populated_parents(self):
606
        return (
607
            ((), 'basis'),
608
            (('basis',), 'modified-something-else'),
609
            (('modified-something-else',), 'current'))
610
611
    def corrected_parents(self):
612
        return (
613
            ((), 'basis'),
614
            (None, 'modified-something-else'),
615
            (('basis',), 'current'))
616
617
    def check_regexes(self, repo):
618
        if repo.supports_rich_root():
619
            # TREE_ROOT will be wrong; but we're not testing it. so just adjust
620
            # the expected count of errors.
621
            count = 3
622
        else:
623
            count = 1
624
        return (
625
            "%d inconsistent parents" % count,
626
            r"\* a-file-id version current has parents "
627
            r"\('modified-something-else',\) but should have \('basis',\)",
628
            )
629
630
    def populate_repository(self, repo):
631
        inv = self.make_one_file_inventory(repo, 'basis', ())
632
        self.add_revision(repo, 'basis', inv, ())
633
634
        # 'modified-something-else' is a correctly recorded revision, but it
635
        # does not modify the file we are looking at, so the inventory for that
636
        # file in this revision points to 'basis'.
637
        inv = self.make_one_file_inventory(
638
            repo, 'modified-something-else', ('basis',), inv_revision='basis')
639
        self.add_revision(repo, 'modified-something-else', inv, ('basis',))
640
641
        # The 'current' revision has 'modified-something-else' as its parent,
642
        # but the 'current' version of 'a-file' should have 'basis' as its
643
        # parent.
644
        inv = self.make_one_file_inventory(
645
            repo, 'current', ('modified-something-else',))
646
        self.add_revision(repo, 'current', inv, ('modified-something-else',))
647
        self.versioned_root = repo.supports_rich_root()
648
649
    def repository_text_key_references(self):
650
        result = {}
651
        if self.versioned_root:
652
            result.update({('TREE_ROOT', 'basis'): True,
653
                           ('TREE_ROOT', 'current'): True,
654
                           ('TREE_ROOT', 'modified-something-else'): True})
655
        result.update({('a-file-id', 'basis'): True,
656
                       ('a-file-id', 'current'): True})
657
        return result
658
659
    def repository_text_keys(self):
660
        return {('a-file-id', 'basis'): [NULL_REVISION],
661
                ('a-file-id', 'current'): [('a-file-id', 'basis')]}
662
663
    def versioned_repository_text_keys(self):
664
        return {('TREE_ROOT', 'basis'): ['null:'],
665
                ('TREE_ROOT', 'current'):
666
                    [('TREE_ROOT', 'modified-something-else')],
667
                ('TREE_ROOT', 'modified-something-else'):
668
                    [('TREE_ROOT', 'basis')]}
669
670
671
class IncorrectlyOrderedParentsScenario(BrokenRepoScenario):
672
    """A scenario where the set parents of a version of a file are correct, but
673
    the order of those parents is incorrect.
674
675
    This defines a 'broken-revision-1-2' and a 'broken-revision-2-1' which both
676
    have their file version parents reversed compared to the revision parents,
677
    which is invalid.  (We use two revisions with opposite orderings of the
678
    same parents to make sure that accidentally relying on dictionary/set
679
    ordering cannot make the test pass; the assumption is that while dict/set
680
    iteration order is arbitrary, it is also consistent within a single test).
681
    """
682
683
    def all_versions_after_reconcile(self):
684
        return ['parent-1', 'parent-2', 'broken-revision-1-2',
685
                'broken-revision-2-1']
686
687
    def populated_parents(self):
688
        return (
689
            ((), 'parent-1'),
690
            ((), 'parent-2'),
691
            (('parent-2', 'parent-1'), 'broken-revision-1-2'),
692
            (('parent-1', 'parent-2'), 'broken-revision-2-1'))
693
694
    def corrected_parents(self):
695
        return (
696
            ((), 'parent-1'),
697
            ((), 'parent-2'),
698
            (('parent-1', 'parent-2'), 'broken-revision-1-2'),
699
            (('parent-2', 'parent-1'), 'broken-revision-2-1'))
700
701
    def check_regexes(self, repo):
702
        if repo.supports_rich_root():
703
            # TREE_ROOT will be wrong; but we're not testing it. so just adjust
704
            # the expected count of errors.
705
            count = 4
706
        else:
707
            count = 2
708
        return (
709
            "%d inconsistent parents" % count,
710
            r"\* a-file-id version broken-revision-1-2 has parents "
711
            r"\('parent-2', 'parent-1'\) but should have "
712
            r"\('parent-1', 'parent-2'\)",
713
            r"\* a-file-id version broken-revision-2-1 has parents "
714
            r"\('parent-1', 'parent-2'\) but should have "
715
            r"\('parent-2', 'parent-1'\)")
716
717
    def populate_repository(self, repo):
718
        inv = self.make_one_file_inventory(repo, 'parent-1', [])
719
        self.add_revision(repo, 'parent-1', inv, [])
720
721
        inv = self.make_one_file_inventory(repo, 'parent-2', [])
722
        self.add_revision(repo, 'parent-2', inv, [])
723
724
        inv = self.make_one_file_inventory(
725
            repo, 'broken-revision-1-2', ['parent-2', 'parent-1'])
726
        self.add_revision(
727
            repo, 'broken-revision-1-2', inv, ['parent-1', 'parent-2'])
728
729
        inv = self.make_one_file_inventory(
730
            repo, 'broken-revision-2-1', ['parent-1', 'parent-2'])
731
        self.add_revision(
732
            repo, 'broken-revision-2-1', inv, ['parent-2', 'parent-1'])
733
        self.versioned_root = repo.supports_rich_root()
734
735
    def repository_text_key_references(self):
736
        result = {}
737
        if self.versioned_root:
738
            result.update({('TREE_ROOT', 'broken-revision-1-2'): True,
739
                           ('TREE_ROOT', 'broken-revision-2-1'): True,
740
                           ('TREE_ROOT', 'parent-1'): True,
741
                           ('TREE_ROOT', 'parent-2'): True})
742
        result.update({('a-file-id', 'broken-revision-1-2'): True,
743
                       ('a-file-id', 'broken-revision-2-1'): True,
744
                       ('a-file-id', 'parent-1'): True,
745
                       ('a-file-id', 'parent-2'): True})
746
        return result
747
748
    def repository_text_keys(self):
749
        return {('a-file-id', 'broken-revision-1-2'):
750
                    [('a-file-id', 'parent-1'), ('a-file-id', 'parent-2')],
751
                ('a-file-id', 'broken-revision-2-1'):
752
                    [('a-file-id', 'parent-2'), ('a-file-id', 'parent-1')],
753
                ('a-file-id', 'parent-1'): [NULL_REVISION],
754
                ('a-file-id', 'parent-2'): [NULL_REVISION]}
755
756
    def versioned_repository_text_keys(self):
757
        return {('TREE_ROOT', 'broken-revision-1-2'):
758
                    [('TREE_ROOT', 'parent-1'), ('TREE_ROOT', 'parent-2')],
759
                ('TREE_ROOT', 'broken-revision-2-1'):
760
                    [('TREE_ROOT', 'parent-2'), ('TREE_ROOT', 'parent-1')],
761
                ('TREE_ROOT', 'parent-1'): [NULL_REVISION],
762
                ('TREE_ROOT', 'parent-2'): [NULL_REVISION]}
763
764
765
all_broken_scenario_classes = [
766
    UndamagedRepositoryScenario,
767
    FileParentIsNotInRevisionAncestryScenario,
768
    FileParentHasInaccessibleInventoryScenario,
769
    FileParentsNotReferencedByAnyInventoryScenario,
770
    TooManyParentsScenario,
771
    ClaimedFileParentDidNotModifyFileScenario,
772
    IncorrectlyOrderedParentsScenario,
773
    UnreferencedFileParentsFromNoOpMergeScenario,
774
    ]
775
776
777
def broken_scenarios_for_all_formats():
778
    format_scenarios = all_repository_vf_format_scenarios()
779
    # test_check_reconcile needs to be parameterized by format *and* by broken
780
    # repository scenario.
781
    broken_scenarios = [(s.__name__, {'scenario_class': s})
782
                        for s in all_broken_scenario_classes]
783
    return multiply_scenarios(format_scenarios, broken_scenarios)
2745.6.42 by Andrew Bennetts
Use TestScenarioApplier to more cleanly parameterise check and reconcile tests.
784
2745.6.46 by Andrew Bennetts
Improvements to check/reconcile tests suggested by review.
785
786
class TestFileParentReconciliation(TestCaseWithRepository):
2745.6.42 by Andrew Bennetts
Use TestScenarioApplier to more cleanly parameterise check and reconcile tests.
787
    """Tests for how reconcile corrects errors in parents of file versions."""
788
5684.3.2 by Jelmer Vernooij
Merge per-repository-vf, move scenarios to test_check_reconcile.
789
    scenarios = broken_scenarios_for_all_formats()
790
2745.6.46 by Andrew Bennetts
Improvements to check/reconcile tests suggested by review.
791
    def make_populated_repository(self, factory):
2745.6.42 by Andrew Bennetts
Use TestScenarioApplier to more cleanly parameterise check and reconcile tests.
792
        """Create a new repository populated by the given factory."""
793
        repo = self.make_repository('broken-repo')
794
        repo.lock_write()
795
        try:
796
            repo.start_write_group()
797
            try:
798
                factory(repo)
799
                repo.commit_write_group()
800
                return repo
801
            except:
802
                repo.abort_write_group()
803
                raise
804
        finally:
805
            repo.unlock()
806
807
    def add_revision(self, repo, revision_id, inv, parent_ids):
2745.6.46 by Andrew Bennetts
Improvements to check/reconcile tests suggested by review.
808
        """Add a revision with a given inventory and parents to a repository.
3943.8.1 by Marius Kruger
remove all trailing whitespace from bzr source
809
2745.6.46 by Andrew Bennetts
Improvements to check/reconcile tests suggested by review.
810
        :param repo: a repository.
811
        :param revision_id: the revision ID for the new revision.
812
        :param inv: an inventory (such as created by
813
            `make_one_file_inventory`).
814
        :param parent_ids: the parents for the new revision.
815
        """
2745.6.42 by Andrew Bennetts
Use TestScenarioApplier to more cleanly parameterise check and reconcile tests.
816
        inv.revision_id = revision_id
817
        inv.root.revision = revision_id
2951.1.5 by Robert Collins
Some work towards including the correct changes for TREE_ROOT in check parameterised tests.
818
        if repo.supports_rich_root():
819
            root_id = inv.root.file_id
3350.6.4 by Robert Collins
First cut at pluralised VersionedFiles. Some rather massive API incompatabilities, primarily because of the difficulty of coherence among competing stores.
820
            repo.texts.add_lines((root_id, revision_id), [], [])
2745.6.42 by Andrew Bennetts
Use TestScenarioApplier to more cleanly parameterise check and reconcile tests.
821
        repo.add_inventory(revision_id, inv, parent_ids)
822
        revision = Revision(revision_id, committer='jrandom@example.com',
823
            timestamp=0, inventory_sha1='', timezone=0, message='foo',
824
            parent_ids=parent_ids)
825
        repo.add_revision(revision_id,revision, inv)
826
827
    def make_one_file_inventory(self, repo, revision, parents,
2927.2.3 by Andrew Bennetts
Add fulltexts to avoid bug 155730.
828
                                inv_revision=None, root_revision=None,
2927.2.4 by Andrew Bennetts
Don't create a 'rev3' file version in the test.
829
                                file_contents=None, make_file_version=True):
2745.6.42 by Andrew Bennetts
Use TestScenarioApplier to more cleanly parameterise check and reconcile tests.
830
        """Make an inventory containing a version of a file with ID 'a-file'.
831
832
        The file's ID will be 'a-file', and its filename will be 'a file name',
833
        stored at the tree root.
834
835
        :param repo: a repository to add the new file version to.
836
        :param revision: the revision ID of the new inventory.
837
        :param parents: the parents for this revision of 'a-file'.
838
        :param inv_revision: if not None, the revision ID to store in the
839
            inventory entry.  Otherwise, this defaults to revision.
840
        :param root_revision: if not None, the inventory's root.revision will
841
            be set to this.
2927.2.3 by Andrew Bennetts
Add fulltexts to avoid bug 155730.
842
        :param file_contents: if not None, the contents of this file version.
843
            Otherwise a unique default (based on revision ID) will be
844
            generated.
2745.6.42 by Andrew Bennetts
Use TestScenarioApplier to more cleanly parameterise check and reconcile tests.
845
        """
846
        inv = Inventory(revision_id=revision)
847
        if root_revision is not None:
848
            inv.root.revision = root_revision
849
        file_id = 'a-file-id'
850
        entry = InventoryFile(file_id, 'a file name', 'TREE_ROOT')
851
        if inv_revision is not None:
852
            entry.revision = inv_revision
853
        else:
854
            entry.revision = revision
855
        entry.text_size = 0
2927.2.3 by Andrew Bennetts
Add fulltexts to avoid bug 155730.
856
        if file_contents is None:
857
            file_contents = '%sline\n' % entry.revision
5849.1.1 by Jelmer Vernooij
Use osutils.sha_string() when possible.
858
        entry.text_sha1 = osutils.sha_string(file_contents)
2745.6.42 by Andrew Bennetts
Use TestScenarioApplier to more cleanly parameterise check and reconcile tests.
859
        inv.add(entry)
2927.2.4 by Andrew Bennetts
Don't create a 'rev3' file version in the test.
860
        if make_file_version:
3350.6.4 by Robert Collins
First cut at pluralised VersionedFiles. Some rather massive API incompatabilities, primarily because of the difficulty of coherence among competing stores.
861
            repo.texts.add_lines((file_id, revision),
862
                [(file_id, parent) for parent in parents], [file_contents])
2745.6.42 by Andrew Bennetts
Use TestScenarioApplier to more cleanly parameterise check and reconcile tests.
863
        return inv
864
2745.6.46 by Andrew Bennetts
Improvements to check/reconcile tests suggested by review.
865
    def require_repo_suffers_text_parent_corruption(self, repo):
2745.6.42 by Andrew Bennetts
Use TestScenarioApplier to more cleanly parameterise check and reconcile tests.
866
        if not repo._reconcile_fixes_text_parents:
867
            raise TestNotApplicable(
868
                    "Format does not support text parent reconciliation")
869
870
    def file_parents(self, repo, revision_id):
3350.6.4 by Robert Collins
First cut at pluralised VersionedFiles. Some rather massive API incompatabilities, primarily because of the difficulty of coherence among competing stores.
871
        key = ('a-file-id', revision_id)
872
        parent_map = repo.texts.get_parent_map([key])
873
        return tuple(parent[-1] for parent in parent_map[key])
2745.6.42 by Andrew Bennetts
Use TestScenarioApplier to more cleanly parameterise check and reconcile tests.
874
2927.2.3 by Andrew Bennetts
Add fulltexts to avoid bug 155730.
875
    def assertFileVersionAbsent(self, repo, revision_id):
3350.6.4 by Robert Collins
First cut at pluralised VersionedFiles. Some rather massive API incompatabilities, primarily because of the difficulty of coherence among competing stores.
876
        self.assertEqual({},
877
            repo.texts.get_parent_map([('a-file-id', revision_id)]))
2927.2.3 by Andrew Bennetts
Add fulltexts to avoid bug 155730.
878
2745.6.46 by Andrew Bennetts
Improvements to check/reconcile tests suggested by review.
879
    def assertParentsMatch(self, expected_parents_for_versions, repo,
880
                           when_description):
881
        for expected_parents, version in expected_parents_for_versions:
2927.2.3 by Andrew Bennetts
Add fulltexts to avoid bug 155730.
882
            if expected_parents is None:
883
                self.assertFileVersionAbsent(repo, version)
884
            else:
885
                found_parents = self.file_parents(repo, version)
886
                self.assertEqual(expected_parents, found_parents,
2927.2.7 by Andrew Bennetts
Condense assertion message.
887
                    "%s reconcile %s has parents %s, should have %s."
888
                    % (when_description, version, found_parents,
889
                       expected_parents))
2745.6.46 by Andrew Bennetts
Improvements to check/reconcile tests suggested by review.
890
2988.1.2 by Robert Collins
New Repository API find_text_key_references for use by reconcile and check.
891
    def prepare_test_repository(self):
892
        """Prepare a repository to test with from the test scenario.
893
894
        :return: A repository, and the scenario instance.
895
        """
896
        scenario = self.scenario_class(self)
897
        repo = self.make_populated_repository(scenario.populate_repository)
898
        self.require_repo_suffers_text_parent_corruption(repo)
899
        return repo, scenario
900
2745.6.46 by Andrew Bennetts
Improvements to check/reconcile tests suggested by review.
901
    def shas_for_versions_of_file(self, repo, versions):
902
        """Get the SHA-1 hashes of the versions of 'a-file' in the repository.
3943.8.1 by Marius Kruger
remove all trailing whitespace from bzr source
903
2745.6.46 by Andrew Bennetts
Improvements to check/reconcile tests suggested by review.
904
        :param repo: the repository to get the hashes from.
905
        :param versions: a list of versions to get hashes for.
906
907
        :returns: A dict of `{version: hash}`.
908
        """
3350.6.4 by Robert Collins
First cut at pluralised VersionedFiles. Some rather massive API incompatabilities, primarily because of the difficulty of coherence among competing stores.
909
        keys = [('a-file-id', version) for version in versions]
3350.8.3 by Robert Collins
VF.get_sha1s needed changing to be stackable.
910
        return repo.texts.get_sha1s(keys)
2745.6.46 by Andrew Bennetts
Improvements to check/reconcile tests suggested by review.
911
912
    def test_reconcile_behaviour(self):
913
        """Populate a repository and reconcile it, verifying the state before
2745.6.42 by Andrew Bennetts
Use TestScenarioApplier to more cleanly parameterise check and reconcile tests.
914
        and after.
915
        """
2988.1.2 by Robert Collins
New Repository API find_text_key_references for use by reconcile and check.
916
        repo, scenario = self.prepare_test_repository()
2951.1.4 by Robert Collins
Lock correctness for check/reconcile tests.
917
        repo.lock_read()
918
        try:
919
            self.assertParentsMatch(scenario.populated_parents(), repo,
920
                'before')
921
            vf_shas = self.shas_for_versions_of_file(
922
                repo, scenario.all_versions_after_reconcile())
923
        finally:
924
            repo.unlock()
2745.6.42 by Andrew Bennetts
Use TestScenarioApplier to more cleanly parameterise check and reconcile tests.
925
        result = repo.reconcile(thorough=True)
2951.1.4 by Robert Collins
Lock correctness for check/reconcile tests.
926
        repo.lock_read()
927
        try:
928
            self.assertParentsMatch(scenario.corrected_parents(), repo,
929
                'after')
930
            # The contents of the versions in the versionedfile should be the
931
            # same after the reconcile.
932
            self.assertEqual(
933
                vf_shas,
934
                self.shas_for_versions_of_file(
935
                    repo, scenario.all_versions_after_reconcile()))
2745.6.46 by Andrew Bennetts
Improvements to check/reconcile tests suggested by review.
936
3350.6.4 by Robert Collins
First cut at pluralised VersionedFiles. Some rather massive API incompatabilities, primarily because of the difficulty of coherence among competing stores.
937
            # Scenario.corrected_fulltexts contains texts which the test wants
938
            # to assert are now fulltexts. However this is an abstraction
939
            # violation; really we care that:
940
            # - the text is reconstructable
941
            # - it has an empty parents list
942
            # (we specify it this way because a store can use arbitrary
943
            # compression pointers in principle.
2951.1.4 by Robert Collins
Lock correctness for check/reconcile tests.
944
            for file_version in scenario.corrected_fulltexts():
3350.6.4 by Robert Collins
First cut at pluralised VersionedFiles. Some rather massive API incompatabilities, primarily because of the difficulty of coherence among competing stores.
945
                key = ('a-file-id', file_version)
946
                self.assertEqual({key:()}, repo.texts.get_parent_map([key]))
947
                self.assertIsInstance(
948
                    repo.texts.get_record_stream([key], 'unordered',
949
                        True).next().get_bytes_as('fulltext'),
950
                    str)
2951.1.4 by Robert Collins
Lock correctness for check/reconcile tests.
951
        finally:
952
            repo.unlock()
2927.2.3 by Andrew Bennetts
Add fulltexts to avoid bug 155730.
953
2745.6.46 by Andrew Bennetts
Improvements to check/reconcile tests suggested by review.
954
    def test_check_behaviour(self):
955
        """Populate a repository and check it, and verify the output."""
2988.1.2 by Robert Collins
New Repository API find_text_key_references for use by reconcile and check.
956
        repo, scenario = self.prepare_test_repository()
2745.6.46 by Andrew Bennetts
Improvements to check/reconcile tests suggested by review.
957
        check_result = repo.check()
958
        check_result.report_results(verbose=True)
4794.1.15 by Robert Collins
Review feedback.
959
        log = self.get_log()
2951.1.6 by Robert Collins
All check/reconcile tests passing now.
960
        for pattern in scenario.check_regexes(repo):
4794.1.8 by Robert Collins
Move the passing of test logs to the result to be via the getDetails API and remove all public use of TestCase._get_log.
961
            self.assertContainsRe(log, pattern)
2745.6.46 by Andrew Bennetts
Improvements to check/reconcile tests suggested by review.
962
2988.1.2 by Robert Collins
New Repository API find_text_key_references for use by reconcile and check.
963
    def test_find_text_key_references(self):
964
        """Test that find_text_key_references finds erroneous references."""
965
        repo, scenario = self.prepare_test_repository()
966
        repo.lock_read()
967
        self.addCleanup(repo.unlock)
968
        self.assertEqual(scenario.repository_text_key_references(),
969
            repo.find_text_key_references())
2988.1.3 by Robert Collins
Add a new repositoy method _generate_text_key_index for use by reconcile/check.
970
971
    def test__generate_text_key_index(self):
972
        """Test that the generated text key index has all entries."""
973
        repo, scenario = self.prepare_test_repository()
974
        repo.lock_read()
975
        self.addCleanup(repo.unlock)
976
        self.assertEqual(scenario.repository_text_key_index(),
977
            repo._generate_text_key_index())