14
14
# along with this program; if not, write to the Free Software
15
15
# Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA
17
"""Tests for reconiliation of repositories."""
17
"""Tests for reconciliation of repositories."""
21
21
import bzrlib.errors as errors
22
22
from bzrlib.inventory import Inventory
23
23
from bzrlib.reconcile import reconcile, Reconciler
24
from bzrlib.repofmt.knitrepo import RepositoryFormatKnit
24
25
from bzrlib.revision import Revision
25
from bzrlib.tests import TestSkipped
26
from bzrlib.tests.repository_implementations.test_repository import TestCaseWithRepository
26
from bzrlib.tests import TestSkipped, TestNotApplicable
27
from bzrlib.tests.repository_implementations.helpers import (
28
TestCaseWithBrokenRevisionIndex,
30
from bzrlib.tests.repository_implementations.test_repository import (
31
TestCaseWithRepository,
27
33
from bzrlib.transport import get_transport
28
34
from bzrlib.uncommit import uncommit
29
from bzrlib.workingtree import WorkingTree
32
37
class TestReconcile(TestCaseWithRepository):
42
47
def checkNoBackupInventory(self, aBzrDir):
43
48
"""Check that there is no backup inventory in aBzrDir."""
44
49
repo = aBzrDir.open_repository()
45
self.assertRaises(errors.NoSuchFile,
46
repo.control_weaves.get_weave,
48
repo.get_transaction())
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)
51
57
class TestsNeedingReweave(TestReconcile):
56
62
t = get_transport(self.get_url())
57
63
# an empty inventory with no revision for testing with.
58
64
repo = self.make_repository('inventory_without_revision')
66
repo.start_write_group()
59
67
inv = Inventory(revision_id='missing')
60
68
inv.root.revision = 'missing'
61
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()
63
91
# an empty inventory with no revision for testing with.
64
92
# this is referenced by 'references_missing' to let us test
65
93
# that all the cached data is correctly converted into ghost links
66
94
# and the referenced inventory still cleaned.
67
95
repo = self.make_repository('inventory_without_revision_and_ghost')
97
repo.start_write_group()
68
98
repo.add_inventory('missing', inv, [])
69
inv = Inventory(revision_id='references_missing')
70
inv.root.revision = 'references_missing'
71
sha1 = repo.add_inventory('references_missing', inv, ['missing'])
72
rev = Revision(timestamp=0,
74
committer="Foo Bar <foo@example.com>",
77
revision_id='references_missing')
78
rev.parent_ids = ['missing']
79
repo.add_revision('references_missing', rev)
99
repo.commit_write_group()
101
add_commit(repo, 'references_missing', ['missing'])
81
103
# a inventory with no parents and the revision has parents..
83
105
repo = self.make_repository('inventory_one_ghost')
84
inv = Inventory(revision_id='ghost')
85
inv.root.revision = 'ghost'
86
sha1 = repo.add_inventory('ghost', inv, [])
87
rev = Revision(timestamp=0,
89
committer="Foo Bar <foo@example.com>",
93
rev.parent_ids = ['the_ghost']
94
repo.add_revision('ghost', rev)
106
add_commit(repo, 'ghost', ['the_ghost'])
96
108
# a inventory with a ghost that can be corrected now.
97
109
t.copy_tree('inventory_one_ghost', 'inventory_ghost_present')
98
repo = bzrlib.repository.Repository.open('inventory_ghost_present')
99
inv = Inventory(revision_id='the_ghost')
100
inv.root.revision = 'the_ghost'
101
sha1 = repo.add_inventory('the_ghost', inv, [])
102
rev = Revision(timestamp=0,
104
committer="Foo Bar <foo@example.com>",
107
revision_id='the_ghost')
109
repo.add_revision('the_ghost', rev)
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', [])
111
115
def checkEmptyReconcile(self, **kwargs):
112
116
"""Check a reconcile on an empty repository."""
113
117
self.make_repository('empty')
114
d = bzrlib.bzrdir.BzrDir.open('empty')
118
d = bzrlib.bzrdir.BzrDir.open(self.get_url('empty'))
115
119
# calling on a empty repository should do nothing
116
120
reconciler = d.find_repository().reconcile(**kwargs)
117
121
# no inconsistent parents should have been found
121
125
# and no backup weave should have been needed/made.
122
126
self.checkNoBackupInventory(d)
124
def test_reconile_empty(self):
128
def test_reconcile_empty(self):
125
129
# in an empty repo, theres nothing to do.
126
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)
128
136
def test_reconcile_empty_thorough(self):
129
137
# reconcile should accept thorough=True
130
138
self.checkEmptyReconcile(thorough=True)
132
140
def test_convenience_reconcile_inventory_without_revision_reconcile(self):
133
141
# smoke test for the all in one ui tool
134
d = bzrlib.bzrdir.BzrDir.open('inventory_without_revision')
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')
136
148
# now the backup should have it but not the current inventory
137
repo = d.open_repository()
149
repo = bzrdir.open_repository()
138
150
self.check_missing_was_removed(repo)
140
152
def test_reweave_inventory_without_revision(self):
141
153
# an excess inventory on its own is only reconciled by using thorough
142
d = bzrlib.bzrdir.BzrDir.open('inventory_without_revision')
154
d_url = self.get_url('inventory_without_revision')
155
d = bzrlib.bzrdir.BzrDir.open(d_url)
143
156
repo = d.open_repository()
157
if not repo._reconcile_does_inventory_gc:
158
raise TestSkipped('Irrelevant test')
144
159
self.checkUnreconciled(d, repo.reconcile())
145
160
reconciler = repo.reconcile(thorough=True)
147
162
self.assertEqual(0, reconciler.inconsistent_parents)
148
# and one garbage inventoriy
163
# and one garbage inventory
149
164
self.assertEqual(1, reconciler.garbage_inventories)
150
165
self.check_missing_was_removed(repo)
154
169
# actual low level test.
155
170
repo = aBzrDir.open_repository()
156
if ([None, 'missing', 'references_missing']
171
if ([None, 'missing', 'references_missing']
157
172
!= repo.get_ancestry('references_missing')):
158
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.
160
178
expected_inconsistent_parents = 0
162
180
expected_inconsistent_parents = 1
175
193
repo.get_ancestry('references_missing'))
177
195
def check_missing_was_removed(self, repo):
178
backup = repo.control_weaves.get_weave('inventory.backup',
179
repo.get_transaction())
180
self.assertTrue('missing' in backup.versions())
181
self.assertRaises(errors.RevisionNotPresent,
182
repo.get_inventory, 'missing')
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')
184
206
def test_reweave_inventory_without_revision_reconciler(self):
185
207
# smoke test for the all in one Reconciler class,
186
208
# other tests use the lower level repo.reconcile()
187
d = bzrlib.bzrdir.BzrDir.open('inventory_without_revision_and_ghost')
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')
189
214
reconciler = Reconciler(d)
190
215
reconciler.reconcile()
194
219
def test_reweave_inventory_without_revision_and_ghost(self):
195
220
# actual low level test.
196
d = bzrlib.bzrdir.BzrDir.open('inventory_without_revision_and_ghost')
221
d_url = self.get_url('inventory_without_revision_and_ghost')
222
d = bzrlib.bzrdir.BzrDir.open(d_url)
197
223
repo = d.open_repository()
224
if not repo._reconcile_does_inventory_gc:
225
raise TestSkipped('Irrelevant test')
198
226
# nothing should have been altered yet : inventories without
199
227
# revisions are not data loss incurring for current format
200
228
self.check_thorough_reweave_missing_revision(d, repo.reconcile,
203
231
def test_reweave_inventory_preserves_a_revision_with_ghosts(self):
204
d = bzrlib.bzrdir.BzrDir.open('inventory_one_ghost')
232
d = bzrlib.bzrdir.BzrDir.open(self.get_url('inventory_one_ghost'))
205
233
reconciler = d.open_repository().reconcile(thorough=True)
206
234
# no inconsistent parents should have been found:
207
235
# the lack of a parent for ghost is normal
261
290
# first off the common logic:
262
291
tree = self.make_branch_and_tree('wrong-first-parent')
263
tree.commit('1', rev_id='1')
264
uncommit(tree.branch, tree=tree)
265
tree.commit('2', rev_id='2')
266
uncommit(tree.branch, tree=tree)
267
tree.commit('3', rev_id='3')
268
uncommit(tree.branch, tree=tree)
269
repo_secondary = tree.bzrdir.clone(
270
'reversed-secondary-parents').open_repository()
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
272
304
# now setup the wrong-first parent case
273
305
repo = tree.branch.repository
307
repo.start_write_group()
274
308
inv = Inventory(revision_id='wrong-first-parent')
275
309
inv.root.revision = 'wrong-first-parent'
276
310
sha1 = repo.add_inventory('wrong-first-parent', inv, ['2', '1'])
282
316
revision_id='wrong-first-parent')
283
317
rev.parent_ids = ['1', '2']
284
318
repo.add_revision('wrong-first-parent', rev)
319
repo.commit_write_group()
286
322
# now setup the wrong-secondary parent case
287
323
repo = repo_secondary
325
repo.start_write_group()
288
326
inv = Inventory(revision_id='wrong-secondary-parent')
289
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'), [], [])
290
331
sha1 = repo.add_inventory('wrong-secondary-parent', inv, ['1', '3', '2'])
291
332
rev = Revision(timestamp=0,
296
337
revision_id='wrong-secondary-parent')
297
338
rev.parent_ids = ['1', '2', '3']
298
339
repo.add_revision('wrong-secondary-parent', rev)
340
repo.commit_write_group()
300
343
def test_reconcile_wrong_order(self):
301
344
# a wrong order in primary parents is optionally correctable
302
d = bzrlib.bzrdir.BzrDir.open('wrong-first-parent')
345
t = get_transport(self.get_url()).clone('wrong-first-parent')
346
d = bzrlib.bzrdir.BzrDir.open_from_transport(t)
303
347
repo = d.open_repository()
304
g = repo.get_revision_graph()
305
if g['wrong-first-parent'] == ['1', '2']:
306
raise TestSkipped('wrong-first-parent is not setup for testing')
351
if g.get_parent_map(['wrong-first-parent'])['wrong-first-parent'] \
353
raise TestSkipped('wrong-first-parent is not setup for testing')
307
356
self.checkUnreconciled(d, repo.reconcile())
308
357
# nothing should have been altered yet : inventories without
309
358
# revisions are not data loss incurring for current format
313
362
# and no garbage inventories
314
363
self.assertEqual(0, reconciler.garbage_inventories)
315
364
# and should have been fixed:
316
g = repo.get_revision_graph()
317
self.assertEqual(['1', '2'], g['wrong-first-parent'])
366
self.addCleanup(repo.unlock)
369
{'wrong-first-parent':('1', '2')},
370
g.get_parent_map(['wrong-first-parent']))
319
def test_reconcile_wrong_order_secondary(self):
320
# a wrong order in secondary parents is ignored.
321
d = bzrlib.bzrdir.BzrDir.open('reversed-secondary-parents')
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)
322
376
repo = d.open_repository()
323
377
self.checkUnreconciled(d, repo.reconcile())
324
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)