~bzr-pqm/bzr/bzr.dev

« back to all changes in this revision

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

  • Committer: Richard Wilbur
  • Date: 2016-02-04 19:07:28 UTC
  • mto: This revision was merged to the branch mainline in revision 6618.
  • Revision ID: richard.wilbur@gmail.com-20160204190728-p0zvfii6zase0fw7
Update COPYING.txt from the original http://www.gnu.org/licenses/gpl-2.0.txt  (Only differences were in whitespace.)  Thanks to Petr Stodulka for pointing out the discrepancy.

Show diffs side-by-side

added added

removed removed

Lines of Context:
17
17
"""Tests for reconciliation of repositories."""
18
18
 
19
19
 
20
 
import bzrlib
21
 
from bzrlib import (
22
 
    errors,
23
 
    transport,
24
 
    )
25
 
from bzrlib.inventory import Inventory
26
 
from bzrlib.reconcile import reconcile, Reconciler
27
 
from bzrlib.repofmt.knitrepo import RepositoryFormatKnit
28
 
from bzrlib.revision import Revision
29
 
from bzrlib.tests import TestSkipped, TestNotApplicable
30
 
from bzrlib.tests.per_repository.helpers import (
31
 
    TestCaseWithBrokenRevisionIndex,
32
 
    )
33
20
from bzrlib.tests.per_repository import (
34
21
    TestCaseWithRepository,
35
22
    )
36
 
from bzrlib.uncommit import uncommit
37
 
 
38
 
 
39
 
class TestReconcile(TestCaseWithRepository):
40
 
 
41
 
    def checkUnreconciled(self, d, reconciler):
42
 
        """Check that d did not get reconciled."""
43
 
        # nothing should have been fixed yet:
44
 
        self.assertEqual(0, reconciler.inconsistent_parents)
45
 
        # and no garbage inventories
46
 
        self.assertEqual(0, reconciler.garbage_inventories)
47
 
        self.checkNoBackupInventory(d)
48
 
 
49
 
    def checkNoBackupInventory(self, aBzrDir):
50
 
        """Check that there is no backup inventory in aBzrDir."""
51
 
        repo = aBzrDir.open_repository()
52
 
        # Remote repository, and possibly others, do not have
53
 
        # _transport.
54
 
        if getattr(repo, '_transport', None) is not None:
55
 
            for path in repo._transport.list_dir('.'):
56
 
                self.assertFalse('inventory.backup' in path)
57
 
 
58
 
 
59
 
class TestsNeedingReweave(TestReconcile):
60
 
 
61
 
    def setUp(self):
62
 
        super(TestsNeedingReweave, self).setUp()
63
 
 
64
 
        t = transport.get_transport(self.get_url())
65
 
        # an empty inventory with no revision for testing with.
66
 
        repo = self.make_repository('inventory_without_revision')
67
 
        repo.lock_write()
68
 
        repo.start_write_group()
69
 
        inv = Inventory(revision_id='missing')
70
 
        inv.root.revision = 'missing'
71
 
        repo.add_inventory('missing', inv, [])
72
 
        repo.commit_write_group()
73
 
        repo.unlock()
74
 
 
75
 
        def add_commit(repo, revision_id, parent_ids):
76
 
            repo.lock_write()
77
 
            repo.start_write_group()
78
 
            inv = Inventory(revision_id=revision_id)
79
 
            inv.root.revision = revision_id
80
 
            root_id = inv.root.file_id
81
 
            sha1 = repo.add_inventory(revision_id, inv, parent_ids)
82
 
            repo.texts.add_lines((root_id, revision_id), [], [])
83
 
            rev = bzrlib.revision.Revision(timestamp=0,
84
 
                                           timezone=None,
85
 
                                           committer="Foo Bar <foo@example.com>",
86
 
                                           message="Message",
87
 
                                           inventory_sha1=sha1,
88
 
                                           revision_id=revision_id)
89
 
            rev.parent_ids = parent_ids
90
 
            repo.add_revision(revision_id, rev)
91
 
            repo.commit_write_group()
92
 
            repo.unlock()
93
 
        # an empty inventory with no revision for testing with.
94
 
        # this is referenced by 'references_missing' to let us test
95
 
        # that all the cached data is correctly converted into ghost links
96
 
        # and the referenced inventory still cleaned.
97
 
        repo = self.make_repository('inventory_without_revision_and_ghost')
98
 
        repo.lock_write()
99
 
        repo.start_write_group()
100
 
        repo.add_inventory('missing', inv, [])
101
 
        repo.commit_write_group()
102
 
        repo.unlock()
103
 
        add_commit(repo, 'references_missing', ['missing'])
104
 
 
105
 
        # a inventory with no parents and the revision has parents..
106
 
        # i.e. a ghost.
107
 
        repo = self.make_repository('inventory_one_ghost')
108
 
        add_commit(repo, 'ghost', ['the_ghost'])
109
 
 
110
 
        # a inventory with a ghost that can be corrected now.
111
 
        t.copy_tree('inventory_one_ghost', 'inventory_ghost_present')
112
 
        bzrdir_url = self.get_url('inventory_ghost_present')
113
 
        bzrdir = bzrlib.bzrdir.BzrDir.open(bzrdir_url)
114
 
        repo = bzrdir.open_repository()
115
 
        add_commit(repo, 'the_ghost', [])
116
 
 
117
 
    def checkEmptyReconcile(self, **kwargs):
118
 
        """Check a reconcile on an empty repository."""
119
 
        self.make_repository('empty')
120
 
        d = bzrlib.bzrdir.BzrDir.open(self.get_url('empty'))
121
 
        # calling on a empty repository should do nothing
122
 
        reconciler = d.find_repository().reconcile(**kwargs)
123
 
        # no inconsistent parents should have been found
124
 
        self.assertEqual(0, reconciler.inconsistent_parents)
125
 
        # and no garbage inventories
126
 
        self.assertEqual(0, reconciler.garbage_inventories)
127
 
        # and no backup weave should have been needed/made.
128
 
        self.checkNoBackupInventory(d)
129
 
 
130
 
    def test_reconcile_empty(self):
131
 
        # in an empty repo, theres nothing to do.
132
 
        self.checkEmptyReconcile()
133
 
 
134
 
    def test_repo_has_reconcile_does_inventory_gc_attribute(self):
135
 
        repo = self.make_repository('repo')
136
 
        self.assertNotEqual(None, repo._reconcile_does_inventory_gc)
137
 
 
138
 
    def test_reconcile_empty_thorough(self):
139
 
        # reconcile should accept thorough=True
140
 
        self.checkEmptyReconcile(thorough=True)
141
 
 
142
 
    def test_convenience_reconcile_inventory_without_revision_reconcile(self):
143
 
        # smoke test for the all in one ui tool
144
 
        bzrdir_url = self.get_url('inventory_without_revision')
145
 
        bzrdir = bzrlib.bzrdir.BzrDir.open(bzrdir_url)
146
 
        repo = bzrdir.open_repository()
147
 
        if not repo._reconcile_does_inventory_gc:
148
 
            raise TestSkipped('Irrelevant test')
149
 
        reconcile(bzrdir)
150
 
        # now the backup should have it but not the current inventory
151
 
        repo = bzrdir.open_repository()
152
 
        self.check_missing_was_removed(repo)
153
 
 
154
 
    def test_reweave_inventory_without_revision(self):
155
 
        # an excess inventory on its own is only reconciled by using thorough
156
 
        d_url = self.get_url('inventory_without_revision')
157
 
        d = bzrlib.bzrdir.BzrDir.open(d_url)
158
 
        repo = d.open_repository()
159
 
        if not repo._reconcile_does_inventory_gc:
160
 
            raise TestSkipped('Irrelevant test')
161
 
        self.checkUnreconciled(d, repo.reconcile())
162
 
        reconciler = repo.reconcile(thorough=True)
163
 
        # no bad parents
164
 
        self.assertEqual(0, reconciler.inconsistent_parents)
165
 
        # and one garbage inventory
166
 
        self.assertEqual(1, reconciler.garbage_inventories)
167
 
        self.check_missing_was_removed(repo)
168
 
 
169
 
    def check_thorough_reweave_missing_revision(self, aBzrDir, reconcile,
170
 
            **kwargs):
171
 
        # actual low level test.
172
 
        repo = aBzrDir.open_repository()
173
 
        if ([None, 'missing', 'references_missing']
174
 
            != repo.get_ancestry('references_missing')):
175
 
            # the repo handles ghosts without corruption, so reconcile has
176
 
            # nothing to do here. Specifically, this test has the inventory
177
 
            # 'missing' present and the revision 'missing' missing, so clearly
178
 
            # 'missing' cannot be reported in the present ancestry -> missing
179
 
            # is something that can be filled as a ghost.
180
 
            expected_inconsistent_parents = 0
181
 
        else:
182
 
            expected_inconsistent_parents = 1
183
 
        reconciler = reconcile(**kwargs)
184
 
        # some number of inconsistent parents should have been found
185
 
        self.assertEqual(expected_inconsistent_parents,
186
 
                         reconciler.inconsistent_parents)
187
 
        # and one garbage inventories
188
 
        self.assertEqual(1, reconciler.garbage_inventories)
189
 
        # now the backup should have it but not the current inventory
190
 
        repo = aBzrDir.open_repository()
191
 
        self.check_missing_was_removed(repo)
192
 
        # and the parent list for 'references_missing' should have that
193
 
        # revision a ghost now.
194
 
        self.assertEqual([None, 'references_missing'],
195
 
                         repo.get_ancestry('references_missing'))
196
 
 
197
 
    def check_missing_was_removed(self, repo):
198
 
        if repo._reconcile_backsup_inventory:
199
 
            backed_up = False
200
 
            for path in repo._transport.list_dir('.'):
201
 
                if 'inventory.backup' in path:
202
 
                    backed_up = True
203
 
            self.assertTrue(backed_up)
204
 
            # Not clear how to do this at an interface level:
205
 
            # self.assertTrue('missing' in backup.versions())
206
 
        self.assertRaises(errors.NoSuchRevision, repo.get_inventory, 'missing')
207
 
 
208
 
    def test_reweave_inventory_without_revision_reconciler(self):
209
 
        # smoke test for the all in one Reconciler class,
210
 
        # other tests use the lower level repo.reconcile()
211
 
        d_url = self.get_url('inventory_without_revision_and_ghost')
212
 
        d = bzrlib.bzrdir.BzrDir.open(d_url)
213
 
        if not d.open_repository()._reconcile_does_inventory_gc:
214
 
            raise TestSkipped('Irrelevant test')
215
 
        def reconcile():
216
 
            reconciler = Reconciler(d)
217
 
            reconciler.reconcile()
218
 
            return reconciler
219
 
        self.check_thorough_reweave_missing_revision(d, reconcile)
220
 
 
221
 
    def test_reweave_inventory_without_revision_and_ghost(self):
222
 
        # actual low level test.
223
 
        d_url = self.get_url('inventory_without_revision_and_ghost')
224
 
        d = bzrlib.bzrdir.BzrDir.open(d_url)
225
 
        repo = d.open_repository()
226
 
        if not repo._reconcile_does_inventory_gc:
227
 
            raise TestSkipped('Irrelevant test')
228
 
        # nothing should have been altered yet : inventories without
229
 
        # revisions are not data loss incurring for current format
230
 
        self.check_thorough_reweave_missing_revision(d, repo.reconcile,
231
 
            thorough=True)
232
 
 
233
 
    def test_reweave_inventory_preserves_a_revision_with_ghosts(self):
234
 
        d = bzrlib.bzrdir.BzrDir.open(self.get_url('inventory_one_ghost'))
235
 
        reconciler = d.open_repository().reconcile(thorough=True)
236
 
        # no inconsistent parents should have been found:
237
 
        # the lack of a parent for ghost is normal
238
 
        self.assertEqual(0, reconciler.inconsistent_parents)
239
 
        # and one garbage inventories
240
 
        self.assertEqual(0, reconciler.garbage_inventories)
241
 
        # now the current inventory should still have 'ghost'
242
 
        repo = d.open_repository()
243
 
        repo.get_inventory('ghost')
244
 
        self.assertEqual([None, 'ghost'], repo.get_ancestry('ghost'))
245
 
 
246
 
    def test_reweave_inventory_fixes_ancestryfor_a_present_ghost(self):
247
 
        d = bzrlib.bzrdir.BzrDir.open(self.get_url('inventory_ghost_present'))
248
 
        repo = d.open_repository()
249
 
        ghost_ancestry = repo.get_ancestry('ghost')
250
 
        if ghost_ancestry == [None, 'the_ghost', 'ghost']:
251
 
            # the repo handles ghosts without corruption, so reconcile has
252
 
            # nothing to do
253
 
            return
254
 
        self.assertEqual([None, 'ghost'], ghost_ancestry)
255
 
        reconciler = repo.reconcile()
256
 
        # this is a data corrupting error, so a normal reconcile should fix it.
257
 
        # one inconsistent parents should have been found : the
258
 
        # available but not reference parent for ghost.
259
 
        self.assertEqual(1, reconciler.inconsistent_parents)
260
 
        # and no garbage inventories
261
 
        self.assertEqual(0, reconciler.garbage_inventories)
262
 
        # now the current inventory should still have 'ghost'
263
 
        repo = d.open_repository()
264
 
        repo.get_inventory('ghost')
265
 
        repo.get_inventory('the_ghost')
266
 
        self.assertEqual([None, 'the_ghost', 'ghost'], repo.get_ancestry('ghost'))
267
 
        self.assertEqual([None, 'the_ghost'], repo.get_ancestry('the_ghost'))
268
 
 
269
 
    def test_text_from_ghost_revision(self):
270
 
        repo = self.make_repository('text-from-ghost')
271
 
        inv = Inventory(revision_id='final-revid')
272
 
        inv.root.revision = 'root-revid'
273
 
        ie = inv.add_path('bla', 'file', 'myfileid')
274
 
        ie.revision = 'ghostrevid'
275
 
        ie.text_size = 42
276
 
        ie.text_sha1 = "bee68c8acd989f5f1765b4660695275948bf5c00"
277
 
        rev = bzrlib.revision.Revision(timestamp=0,
278
 
                                       timezone=None,
279
 
                                       committer="Foo Bar <foo@example.com>",
280
 
                                       message="Message",
281
 
                                       revision_id='final-revid')
282
 
        repo.lock_write()
283
 
        try:
284
 
            repo.start_write_group()
285
 
            try:
286
 
                repo.add_revision('final-revid', rev, inv)
287
 
                try:
288
 
                    repo.texts.add_lines(('myfileid', 'ghostrevid'),
289
 
                        (('myfileid', 'ghost-text-parent'),),
290
 
                        ["line1\n", "line2\n"])
291
 
                except errors.RevisionNotPresent:
292
 
                    raise TestSkipped("text ghost parents not supported")
293
 
                if repo.supports_rich_root():
294
 
                    root_id = inv.root.file_id
295
 
                    repo.texts.add_lines((inv.root.file_id, inv.root.revision),
296
 
                        [], [])
297
 
            finally:
298
 
                repo.commit_write_group()
299
 
        finally:
300
 
            repo.unlock()
301
 
        repo.reconcile(thorough=True)
302
 
 
303
 
 
304
 
class TestReconcileWithIncorrectRevisionCache(TestReconcile):
305
 
    """Ancestry data gets cached in knits and weaves should be reconcilable.
306
 
 
307
 
    This class tests that reconcile can correct invalid caches (such as after
308
 
    a reconcile).
309
 
    """
310
 
 
311
 
    def setUp(self):
312
 
        self.reduceLockdirTimeout()
313
 
        super(TestReconcileWithIncorrectRevisionCache, self).setUp()
314
 
 
315
 
        t = transport.get_transport(self.get_url())
316
 
        # we need a revision with two parents in the wrong order
317
 
        # which should trigger reinsertion.
318
 
        # and another with the first one correct but the other two not
319
 
        # which should not trigger reinsertion.
320
 
        # these need to be in different repositories so that we don't
321
 
        # trigger a reconcile based on the other case.
322
 
        # there is no api to construct a broken knit repository at
323
 
        # this point. if we ever encounter a bad graph in a knit repo
324
 
        # we should add a lower level api to allow constructing such cases.
325
 
 
326
 
        # first off the common logic:
327
 
        tree = self.make_branch_and_tree('wrong-first-parent')
328
 
        second_tree = self.make_branch_and_tree('reversed-secondary-parents')
329
 
        for t in [tree, second_tree]:
330
 
            t.commit('1', rev_id='1')
331
 
            uncommit(t.branch, tree=t)
332
 
            t.commit('2', rev_id='2')
333
 
            uncommit(t.branch, tree=t)
334
 
            t.commit('3', rev_id='3')
335
 
            uncommit(t.branch, tree=t)
336
 
        #second_tree = self.make_branch_and_tree('reversed-secondary-parents')
337
 
        #second_tree.pull(tree) # XXX won't copy the repo?
338
 
        repo_secondary = second_tree.branch.repository
339
 
 
340
 
        # now setup the wrong-first parent case
341
 
        repo = tree.branch.repository
342
 
        repo.lock_write()
343
 
        repo.start_write_group()
344
 
        inv = Inventory(revision_id='wrong-first-parent')
345
 
        inv.root.revision = 'wrong-first-parent'
346
 
        if repo.supports_rich_root():
347
 
            root_id = inv.root.file_id
348
 
            repo.texts.add_lines((root_id, 'wrong-first-parent'), [], [])
349
 
        sha1 = repo.add_inventory('wrong-first-parent', inv, ['2', '1'])
350
 
        rev = Revision(timestamp=0,
351
 
                       timezone=None,
352
 
                       committer="Foo Bar <foo@example.com>",
353
 
                       message="Message",
354
 
                       inventory_sha1=sha1,
355
 
                       revision_id='wrong-first-parent')
356
 
        rev.parent_ids = ['1', '2']
357
 
        repo.add_revision('wrong-first-parent', rev)
358
 
        repo.commit_write_group()
359
 
        repo.unlock()
360
 
 
361
 
        # now setup the wrong-secondary parent case
362
 
        repo = repo_secondary
363
 
        repo.lock_write()
364
 
        repo.start_write_group()
365
 
        inv = Inventory(revision_id='wrong-secondary-parent')
366
 
        inv.root.revision = 'wrong-secondary-parent'
367
 
        if repo.supports_rich_root():
368
 
            root_id = inv.root.file_id
369
 
            repo.texts.add_lines((root_id, 'wrong-secondary-parent'), [], [])
370
 
        sha1 = repo.add_inventory('wrong-secondary-parent', inv, ['1', '3', '2'])
371
 
        rev = Revision(timestamp=0,
372
 
                       timezone=None,
373
 
                       committer="Foo Bar <foo@example.com>",
374
 
                       message="Message",
375
 
                       inventory_sha1=sha1,
376
 
                       revision_id='wrong-secondary-parent')
377
 
        rev.parent_ids = ['1', '2', '3']
378
 
        repo.add_revision('wrong-secondary-parent', rev)
379
 
        repo.commit_write_group()
380
 
        repo.unlock()
381
 
 
382
 
    def test_reconcile_wrong_order(self):
383
 
        # a wrong order in primary parents is optionally correctable
384
 
        t = transport.get_transport(self.get_url()).clone('wrong-first-parent')
385
 
        d = bzrlib.bzrdir.BzrDir.open_from_transport(t)
386
 
        repo = d.open_repository()
387
 
        repo.lock_read()
388
 
        try:
389
 
            g = repo.get_graph()
390
 
            if g.get_parent_map(['wrong-first-parent'])['wrong-first-parent'] \
391
 
                == ('1', '2'):
392
 
                raise TestSkipped('wrong-first-parent is not setup for testing')
393
 
        finally:
394
 
            repo.unlock()
395
 
        self.checkUnreconciled(d, repo.reconcile())
396
 
        # nothing should have been altered yet : inventories without
397
 
        # revisions are not data loss incurring for current format
398
 
        reconciler = repo.reconcile(thorough=True)
399
 
        # these show up as inconsistent parents
400
 
        self.assertEqual(1, reconciler.inconsistent_parents)
401
 
        # and no garbage inventories
402
 
        self.assertEqual(0, reconciler.garbage_inventories)
403
 
        # and should have been fixed:
404
 
        repo.lock_read()
405
 
        self.addCleanup(repo.unlock)
406
 
        g = repo.get_graph()
407
 
        self.assertEqual(
408
 
            {'wrong-first-parent':('1', '2')},
409
 
            g.get_parent_map(['wrong-first-parent']))
410
 
 
411
 
    def test_reconcile_wrong_order_secondary_inventory(self):
412
 
        # a wrong order in the parents for inventories is ignored.
413
 
        t = transport.get_transport(self.get_url()
414
 
                                    ).clone('reversed-secondary-parents')
415
 
        d = bzrlib.bzrdir.BzrDir.open_from_transport(t)
416
 
        repo = d.open_repository()
417
 
        self.checkUnreconciled(d, repo.reconcile())
418
 
        self.checkUnreconciled(d, repo.reconcile(thorough=True))
419
 
 
420
 
 
421
 
class TestBadRevisionParents(TestCaseWithBrokenRevisionIndex):
422
 
 
423
 
    def test_aborts_if_bad_parents_in_index(self):
424
 
        """Reconcile refuses to proceed if the revision index is wrong when
425
 
        checked against the revision texts, so that it does not generate broken
426
 
        data.
427
 
 
428
 
        Ideally reconcile would fix this, but until we implement that we just
429
 
        make sure we safely detect this problem.
430
 
        """
431
 
        repo = self.make_repo_with_extra_ghost_index()
432
 
        reconciler = repo.reconcile(thorough=True)
433
 
        self.assertTrue(reconciler.aborted,
434
 
            "reconcile should have aborted due to bad parents.")
435
 
 
436
 
    def test_does_not_abort_on_clean_repo(self):
437
 
        repo = self.make_repository('.')
438
 
        reconciler = repo.reconcile(thorough=True)
439
 
        self.assertFalse(reconciler.aborted,
440
 
            "reconcile should not have aborted on an unbroken repository.")
441
 
 
442
 
 
443
 
class TestRepeatedReconcile(TestReconcile):
 
23
 
 
24
 
 
25
class TestRepeatedReconcile(TestCaseWithRepository):
444
26
 
445
27
    def test_trivial_two_reconciles_no_error(self):
446
28
        tree = self.make_branch_and_tree('.')