/brz/remove-bazaar

To get this branch, use:
bzr branch http://gegoxaren.bato24.eu/bzr/brz/remove-bazaar

« back to all changes in this revision

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

Merge with serialize-transform

Show diffs side-by-side

added added

removed removed

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