1
# Copyright (C) 2006-2010 Canonical Ltd
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.
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.
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
17
"""Tests for reconciliation of repositories."""
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
29
from bzrlib.tests.per_repository_vf.helpers import (
30
TestCaseWithBrokenRevisionIndex,
32
from bzrlib.tests.per_repository_vf import (
33
TestCaseWithRepository,
34
all_repository_vf_format_scenarios,
36
from bzrlib.tests.matchers import MatchesAncestry
37
from bzrlib.tests.scenarios import load_tests_apply_scenarios
38
from bzrlib.uncommit import uncommit
41
load_tests = load_tests_apply_scenarios
44
class TestReconcile(TestCaseWithRepository):
46
scenarios = all_repository_vf_format_scenarios()
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)
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)
63
class TestBadRevisionParents(TestCaseWithBrokenRevisionIndex):
65
scenarios = all_repository_vf_format_scenarios()
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
72
Ideally reconcile would fix this, but until we implement that we just
73
make sure we safely detect this problem.
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.")
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.")
87
class TestsNeedingReweave(TestReconcile):
90
super(TestsNeedingReweave, self).setUp()
92
t = self.get_transport()
93
# an empty inventory with no revision for testing with.
94
repo = self.make_repository('inventory_without_revision')
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()
103
def add_commit(repo, revision_id, parent_ids):
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,
113
committer="Foo Bar <foo@example.com>",
116
revision_id=revision_id)
117
rev.parent_ids = parent_ids
118
repo.add_revision(revision_id, rev)
119
repo.commit_write_group()
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')
127
repo.start_write_group()
128
repo.add_inventory('missing', inv, [])
129
repo.commit_write_group()
131
add_commit(repo, 'references_missing', ['missing'])
133
# a inventory with no parents and the revision has parents..
135
repo = self.make_repository('inventory_one_ghost')
136
add_commit(repo, 'ghost', ['the_ghost'])
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', [])
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)
158
def test_reconcile_empty(self):
159
# in an empty repo, theres nothing to do.
160
self.checkEmptyReconcile()
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)
166
def test_reconcile_empty_thorough(self):
167
# reconcile should accept thorough=True
168
self.checkEmptyReconcile(thorough=True)
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')
178
# now the backup should have it but not the current inventory
179
repo = bzrdir.open_repository()
180
self.check_missing_was_removed(repo)
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)
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)
197
def check_thorough_reweave_missing_revision(self, aBzrDir, reconcile,
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
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'))
223
def check_missing_was_removed(self, repo):
224
if repo._reconcile_backsup_inventory:
226
for path in repo.control_transport.list_dir('.'):
227
if 'inventory.backup' in path:
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')
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')
242
reconciler = Reconciler(d)
243
reconciler.reconcile()
245
self.check_thorough_reweave_missing_revision(d, reconcile)
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,
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'))
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
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'))
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'
304
ie.text_sha1 = "bee68c8acd989f5f1765b4660695275948bf5c00"
305
rev = bzrlib.revision.Revision(timestamp=0,
307
committer="Foo Bar <foo@example.com>",
309
revision_id='final-revid')
312
repo.start_write_group()
314
repo.add_revision('final-revid', rev, inv)
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),
326
repo.commit_write_group()
329
repo.reconcile(thorough=True)
332
class TestReconcileWithIncorrectRevisionCache(TestReconcile):
333
"""Ancestry data gets cached in knits and weaves should be reconcilable.
335
This class tests that reconcile can correct invalid caches (such as after
340
self.reduceLockdirTimeout()
341
super(TestReconcileWithIncorrectRevisionCache, self).setUp()
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.
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
369
# now setup the wrong-first parent case
370
repo = self.first_tree.branch.repository
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,
381
committer="Foo Bar <foo@example.com>",
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()
390
# now setup the wrong-secondary parent case
391
repo = repo_secondary
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,
402
committer="Foo Bar <foo@example.com>",
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()
411
def test_reconcile_wrong_order(self):
412
# a wrong order in primary parents is optionally correctable
413
repo = self.first_tree.branch.repository
417
if g.get_parent_map(['wrong-first-parent'])['wrong-first-parent'] \
419
raise TestSkipped('wrong-first-parent is not setup for testing')
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:
432
self.addCleanup(repo.unlock)
435
{'wrong-first-parent':('1', '2')},
436
g.get_parent_map(['wrong-first-parent']))
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))