~bzr-pqm/bzr/bzr.dev

« back to all changes in this revision

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

Merge bzr.dev.

Show diffs side-by-side

added added

removed removed

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