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