~bzr-pqm/bzr/bzr.dev

« back to all changes in this revision

Viewing changes to bzrlib/tests/per_repository/test_reconcile.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:
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
    )
20
33
from bzrlib.tests.per_repository import (
21
34
    TestCaseWithRepository,
22
35
    )
23
 
 
24
 
 
25
 
class TestRepeatedReconcile(TestCaseWithRepository):
 
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):
26
444
 
27
445
    def test_trivial_two_reconciles_no_error(self):
28
446
        tree = self.make_branch_and_tree('.')