~bzr-pqm/bzr/bzr.dev

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