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