~bzr-pqm/bzr/bzr.dev

« back to all changes in this revision

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

  • Committer: Patch Queue Manager
  • Date: 2016-02-01 19:56:05 UTC
  • mfrom: (6615.1.1 trunk)
  • Revision ID: pqm@pqm.ubuntu.com-20160201195605-o7rl92wf6uyum3fk
(vila) Open trunk again as 2.8b1 (Vincent Ladeuil)

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))