1
# Copyright (C) 2006 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., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA
17
"""Tests for reconciliation of repositories."""
21
import bzrlib.errors as errors
22
from bzrlib.inventory import Inventory
23
from bzrlib.reconcile import reconcile, Reconciler
24
from bzrlib.repofmt.knitrepo import RepositoryFormatKnit
25
from bzrlib.revision import Revision
26
from bzrlib.tests import TestSkipped, TestNotApplicable
27
from bzrlib.tests.per_repository.helpers import (
28
TestCaseWithBrokenRevisionIndex,
30
from bzrlib.tests.per_repository import (
31
TestCaseWithRepository,
33
from bzrlib.transport import get_transport
34
from bzrlib.uncommit import uncommit
37
class TestReconcile(TestCaseWithRepository):
39
def checkUnreconciled(self, d, reconciler):
40
"""Check that d did not get reconciled."""
41
# nothing should have been fixed yet:
42
self.assertEqual(0, reconciler.inconsistent_parents)
43
# and no garbage inventories
44
self.assertEqual(0, reconciler.garbage_inventories)
45
self.checkNoBackupInventory(d)
47
def checkNoBackupInventory(self, aBzrDir):
48
"""Check that there is no backup inventory in aBzrDir."""
49
repo = aBzrDir.open_repository()
50
# Remote repository, and possibly others, do not have
52
if getattr(repo, '_transport', None) is not None:
53
for path in repo._transport.list_dir('.'):
54
self.assertFalse('inventory.backup' in path)
57
class TestsNeedingReweave(TestReconcile):
60
super(TestsNeedingReweave, self).setUp()
62
t = get_transport(self.get_url())
63
# an empty inventory with no revision for testing with.
64
repo = self.make_repository('inventory_without_revision')
66
repo.start_write_group()
67
inv = Inventory(revision_id='missing')
68
inv.root.revision = 'missing'
69
repo.add_inventory('missing', inv, [])
70
repo.commit_write_group()
73
def add_commit(repo, revision_id, parent_ids):
75
repo.start_write_group()
76
inv = Inventory(revision_id=revision_id)
77
inv.root.revision = revision_id
78
root_id = inv.root.file_id
79
sha1 = repo.add_inventory(revision_id, inv, parent_ids)
80
repo.texts.add_lines((root_id, revision_id), [], [])
81
rev = bzrlib.revision.Revision(timestamp=0,
83
committer="Foo Bar <foo@example.com>",
86
revision_id=revision_id)
87
rev.parent_ids = parent_ids
88
repo.add_revision(revision_id, rev)
89
repo.commit_write_group()
91
# an empty inventory with no revision for testing with.
92
# this is referenced by 'references_missing' to let us test
93
# that all the cached data is correctly converted into ghost links
94
# and the referenced inventory still cleaned.
95
repo = self.make_repository('inventory_without_revision_and_ghost')
97
repo.start_write_group()
98
repo.add_inventory('missing', inv, [])
99
repo.commit_write_group()
101
add_commit(repo, 'references_missing', ['missing'])
103
# a inventory with no parents and the revision has parents..
105
repo = self.make_repository('inventory_one_ghost')
106
add_commit(repo, 'ghost', ['the_ghost'])
108
# a inventory with a ghost that can be corrected now.
109
t.copy_tree('inventory_one_ghost', 'inventory_ghost_present')
110
bzrdir_url = self.get_url('inventory_ghost_present')
111
bzrdir = bzrlib.bzrdir.BzrDir.open(bzrdir_url)
112
repo = bzrdir.open_repository()
113
add_commit(repo, 'the_ghost', [])
115
def checkEmptyReconcile(self, **kwargs):
116
"""Check a reconcile on an empty repository."""
117
self.make_repository('empty')
118
d = bzrlib.bzrdir.BzrDir.open(self.get_url('empty'))
119
# calling on a empty repository should do nothing
120
reconciler = d.find_repository().reconcile(**kwargs)
121
# no inconsistent parents should have been found
122
self.assertEqual(0, reconciler.inconsistent_parents)
123
# and no garbage inventories
124
self.assertEqual(0, reconciler.garbage_inventories)
125
# and no backup weave should have been needed/made.
126
self.checkNoBackupInventory(d)
128
def test_reconcile_empty(self):
129
# in an empty repo, theres nothing to do.
130
self.checkEmptyReconcile()
132
def test_repo_has_reconcile_does_inventory_gc_attribute(self):
133
repo = self.make_repository('repo')
134
self.assertNotEqual(None, repo._reconcile_does_inventory_gc)
136
def test_reconcile_empty_thorough(self):
137
# reconcile should accept thorough=True
138
self.checkEmptyReconcile(thorough=True)
140
def test_convenience_reconcile_inventory_without_revision_reconcile(self):
141
# smoke test for the all in one ui tool
142
bzrdir_url = self.get_url('inventory_without_revision')
143
bzrdir = bzrlib.bzrdir.BzrDir.open(bzrdir_url)
144
repo = bzrdir.open_repository()
145
if not repo._reconcile_does_inventory_gc:
146
raise TestSkipped('Irrelevant test')
148
# now the backup should have it but not the current inventory
149
repo = bzrdir.open_repository()
150
self.check_missing_was_removed(repo)
152
def test_reweave_inventory_without_revision(self):
153
# an excess inventory on its own is only reconciled by using thorough
154
d_url = self.get_url('inventory_without_revision')
155
d = bzrlib.bzrdir.BzrDir.open(d_url)
156
repo = d.open_repository()
157
if not repo._reconcile_does_inventory_gc:
158
raise TestSkipped('Irrelevant test')
159
self.checkUnreconciled(d, repo.reconcile())
160
reconciler = repo.reconcile(thorough=True)
162
self.assertEqual(0, reconciler.inconsistent_parents)
163
# and one garbage inventory
164
self.assertEqual(1, reconciler.garbage_inventories)
165
self.check_missing_was_removed(repo)
167
def check_thorough_reweave_missing_revision(self, aBzrDir, reconcile,
169
# actual low level test.
170
repo = aBzrDir.open_repository()
171
if ([None, 'missing', 'references_missing']
172
!= repo.get_ancestry('references_missing')):
173
# the repo handles ghosts without corruption, so reconcile has
174
# nothing to do here. Specifically, this test has the inventory
175
# 'missing' present and the revision 'missing' missing, so clearly
176
# 'missing' cannot be reported in the present ancestry -> missing
177
# is something that can be filled as a ghost.
178
expected_inconsistent_parents = 0
180
expected_inconsistent_parents = 1
181
reconciler = reconcile(**kwargs)
182
# some number of inconsistent parents should have been found
183
self.assertEqual(expected_inconsistent_parents,
184
reconciler.inconsistent_parents)
185
# and one garbage inventories
186
self.assertEqual(1, reconciler.garbage_inventories)
187
# now the backup should have it but not the current inventory
188
repo = aBzrDir.open_repository()
189
self.check_missing_was_removed(repo)
190
# and the parent list for 'references_missing' should have that
191
# revision a ghost now.
192
self.assertEqual([None, 'references_missing'],
193
repo.get_ancestry('references_missing'))
195
def check_missing_was_removed(self, repo):
196
if repo._reconcile_backsup_inventory:
198
for path in repo._transport.list_dir('.'):
199
if 'inventory.backup' in path:
201
self.assertTrue(backed_up)
202
# Not clear how to do this at an interface level:
203
# self.assertTrue('missing' in backup.versions())
204
self.assertRaises(errors.NoSuchRevision, repo.get_inventory, 'missing')
206
def test_reweave_inventory_without_revision_reconciler(self):
207
# smoke test for the all in one Reconciler class,
208
# other tests use the lower level repo.reconcile()
209
d_url = self.get_url('inventory_without_revision_and_ghost')
210
d = bzrlib.bzrdir.BzrDir.open(d_url)
211
if not d.open_repository()._reconcile_does_inventory_gc:
212
raise TestSkipped('Irrelevant test')
214
reconciler = Reconciler(d)
215
reconciler.reconcile()
217
self.check_thorough_reweave_missing_revision(d, reconcile)
219
def test_reweave_inventory_without_revision_and_ghost(self):
220
# actual low level test.
221
d_url = self.get_url('inventory_without_revision_and_ghost')
222
d = bzrlib.bzrdir.BzrDir.open(d_url)
223
repo = d.open_repository()
224
if not repo._reconcile_does_inventory_gc:
225
raise TestSkipped('Irrelevant test')
226
# nothing should have been altered yet : inventories without
227
# revisions are not data loss incurring for current format
228
self.check_thorough_reweave_missing_revision(d, repo.reconcile,
231
def test_reweave_inventory_preserves_a_revision_with_ghosts(self):
232
d = bzrlib.bzrdir.BzrDir.open(self.get_url('inventory_one_ghost'))
233
reconciler = d.open_repository().reconcile(thorough=True)
234
# no inconsistent parents should have been found:
235
# the lack of a parent for ghost is normal
236
self.assertEqual(0, reconciler.inconsistent_parents)
237
# and one garbage inventories
238
self.assertEqual(0, reconciler.garbage_inventories)
239
# now the current inventory should still have 'ghost'
240
repo = d.open_repository()
241
repo.get_inventory('ghost')
242
self.assertEqual([None, 'ghost'], repo.get_ancestry('ghost'))
244
def test_reweave_inventory_fixes_ancestryfor_a_present_ghost(self):
245
d = bzrlib.bzrdir.BzrDir.open(self.get_url('inventory_ghost_present'))
246
repo = d.open_repository()
247
ghost_ancestry = repo.get_ancestry('ghost')
248
if ghost_ancestry == [None, 'the_ghost', 'ghost']:
249
# the repo handles ghosts without corruption, so reconcile has
252
self.assertEqual([None, 'ghost'], ghost_ancestry)
253
reconciler = repo.reconcile()
254
# this is a data corrupting error, so a normal reconcile should fix it.
255
# one inconsistent parents should have been found : the
256
# available but not reference parent for ghost.
257
self.assertEqual(1, reconciler.inconsistent_parents)
258
# and no garbage inventories
259
self.assertEqual(0, reconciler.garbage_inventories)
260
# now the current inventory should still have 'ghost'
261
repo = d.open_repository()
262
repo.get_inventory('ghost')
263
repo.get_inventory('the_ghost')
264
self.assertEqual([None, 'the_ghost', 'ghost'], repo.get_ancestry('ghost'))
265
self.assertEqual([None, 'the_ghost'], repo.get_ancestry('the_ghost'))
268
class TestReconcileWithIncorrectRevisionCache(TestReconcile):
269
"""Ancestry data gets cached in knits and weaves should be reconcilable.
271
This class tests that reconcile can correct invalid caches (such as after
276
self.reduceLockdirTimeout()
277
super(TestReconcileWithIncorrectRevisionCache, self).setUp()
279
t = get_transport(self.get_url())
280
# we need a revision with two parents in the wrong order
281
# which should trigger reinsertion.
282
# and another with the first one correct but the other two not
283
# which should not trigger reinsertion.
284
# these need to be in different repositories so that we don't
285
# trigger a reconcile based on the other case.
286
# there is no api to construct a broken knit repository at
287
# this point. if we ever encounter a bad graph in a knit repo
288
# we should add a lower level api to allow constructing such cases.
290
# first off the common logic:
291
tree = self.make_branch_and_tree('wrong-first-parent')
292
second_tree = self.make_branch_and_tree('reversed-secondary-parents')
293
for t in [tree, second_tree]:
294
t.commit('1', rev_id='1')
295
uncommit(t.branch, tree=t)
296
t.commit('2', rev_id='2')
297
uncommit(t.branch, tree=t)
298
t.commit('3', rev_id='3')
299
uncommit(t.branch, tree=t)
300
#second_tree = self.make_branch_and_tree('reversed-secondary-parents')
301
#second_tree.pull(tree) # XXX won't copy the repo?
302
repo_secondary = second_tree.branch.repository
304
# now setup the wrong-first parent case
305
repo = tree.branch.repository
307
repo.start_write_group()
308
inv = Inventory(revision_id='wrong-first-parent')
309
inv.root.revision = 'wrong-first-parent'
310
sha1 = repo.add_inventory('wrong-first-parent', inv, ['2', '1'])
311
rev = Revision(timestamp=0,
313
committer="Foo Bar <foo@example.com>",
316
revision_id='wrong-first-parent')
317
rev.parent_ids = ['1', '2']
318
repo.add_revision('wrong-first-parent', rev)
319
repo.commit_write_group()
322
# now setup the wrong-secondary parent case
323
repo = repo_secondary
325
repo.start_write_group()
326
inv = Inventory(revision_id='wrong-secondary-parent')
327
inv.root.revision = 'wrong-secondary-parent'
328
if repo.supports_rich_root():
329
root_id = inv.root.file_id
330
repo.texts.add_lines((root_id, 'wrong-secondary-parent'), [], [])
331
sha1 = repo.add_inventory('wrong-secondary-parent', inv, ['1', '3', '2'])
332
rev = Revision(timestamp=0,
334
committer="Foo Bar <foo@example.com>",
337
revision_id='wrong-secondary-parent')
338
rev.parent_ids = ['1', '2', '3']
339
repo.add_revision('wrong-secondary-parent', rev)
340
repo.commit_write_group()
343
def test_reconcile_wrong_order(self):
344
# a wrong order in primary parents is optionally correctable
345
t = get_transport(self.get_url()).clone('wrong-first-parent')
346
d = bzrlib.bzrdir.BzrDir.open_from_transport(t)
347
repo = d.open_repository()
351
if g.get_parent_map(['wrong-first-parent'])['wrong-first-parent'] \
353
raise TestSkipped('wrong-first-parent is not setup for testing')
356
self.checkUnreconciled(d, repo.reconcile())
357
# nothing should have been altered yet : inventories without
358
# revisions are not data loss incurring for current format
359
reconciler = repo.reconcile(thorough=True)
360
# these show up as inconsistent parents
361
self.assertEqual(1, reconciler.inconsistent_parents)
362
# and no garbage inventories
363
self.assertEqual(0, reconciler.garbage_inventories)
364
# and should have been fixed:
366
self.addCleanup(repo.unlock)
369
{'wrong-first-parent':('1', '2')},
370
g.get_parent_map(['wrong-first-parent']))
372
def test_reconcile_wrong_order_secondary_inventory(self):
373
# a wrong order in the parents for inventories is ignored.
374
t = get_transport(self.get_url()).clone('reversed-secondary-parents')
375
d = bzrlib.bzrdir.BzrDir.open_from_transport(t)
376
repo = d.open_repository()
377
self.checkUnreconciled(d, repo.reconcile())
378
self.checkUnreconciled(d, repo.reconcile(thorough=True))
381
class TestBadRevisionParents(TestCaseWithBrokenRevisionIndex):
383
def test_aborts_if_bad_parents_in_index(self):
384
"""Reconcile refuses to proceed if the revision index is wrong when
385
checked against the revision texts, so that it does not generate broken
388
Ideally reconcile would fix this, but until we implement that we just
389
make sure we safely detect this problem.
391
repo = self.make_repo_with_extra_ghost_index()
392
reconciler = repo.reconcile(thorough=True)
393
self.assertTrue(reconciler.aborted,
394
"reconcile should have aborted due to bad parents.")
396
def test_does_not_abort_on_clean_repo(self):
397
repo = self.make_repository('.')
398
reconciler = repo.reconcile(thorough=True)
399
self.assertFalse(reconciler.aborted,
400
"reconcile should not have aborted on an unbroken repository.")
403
class TestRepeatedReconcile(TestReconcile):
405
def test_trivial_two_reconciles_no_error(self):
406
tree = self.make_branch_and_tree('.')
407
tree.commit('first post')
408
tree.branch.repository.reconcile(thorough=True)
409
tree.branch.repository.reconcile(thorough=True)