~bzr-pqm/bzr/bzr.dev

« back to all changes in this revision

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