~bzr-pqm/bzr/bzr.dev

« back to all changes in this revision

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

  • Committer: Canonical.com Patch Queue Manager
  • Date: 2011-04-20 02:06:05 UTC
  • mfrom: (5802.2.2 move-reconcile)
  • Revision ID: pqm@pqm.ubuntu.com-20110420020605-0l0fdx2alnsbv5c9
(jelmer) Move more tests that require the full versioned file API from
 bt.per_repository to bt.per_repository_vf. (Jelmer Vernooij)

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