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