132
122
# exception was not generated, or the exception was caught and
133
123
# suppressed. See also test_pack_repository's test of the same name.
134
124
self.assertEqual(None, repo.abort_write_group(suppress_errors=True))
137
class TestGetMissingParentInventories(per_repository.TestCaseWithRepository):
139
def test_empty_get_missing_parent_inventories(self):
140
"""A new write group has no missing parent inventories."""
141
repo = self.make_repository('.')
143
repo.start_write_group()
145
self.assertEqual(set(), set(repo.get_missing_parent_inventories()))
147
repo.commit_write_group()
150
def branch_trunk_and_make_tree(self, trunk_repo, relpath):
151
tree = self.make_branch_and_memory_tree('branch')
152
trunk_repo.lock_read()
153
self.addCleanup(trunk_repo.unlock)
154
tree.branch.repository.fetch(trunk_repo, revision_id='rev-1')
155
tree.set_parent_ids(['rev-1'])
158
def make_first_commit(self, repo):
159
trunk = repo.bzrdir.create_branch()
160
tree = memorytree.MemoryTree.create_on_branch(trunk)
162
tree.add([''], ['TREE_ROOT'], ['directory'])
163
tree.add(['dir'], ['dir-id'], ['directory'])
164
tree.add(['filename'], ['file-id'], ['file'])
165
tree.put_file_bytes_non_atomic('file-id', 'content\n')
166
tree.commit('Trunk commit', rev_id='rev-0')
167
tree.commit('Trunk commit', rev_id='rev-1')
170
def make_new_commit_in_new_repo(self, trunk_repo, parents=None):
171
tree = self.branch_trunk_and_make_tree(trunk_repo, 'branch')
172
tree.set_parent_ids(parents)
173
tree.commit('Branch commit', rev_id='rev-2')
174
branch_repo = tree.branch.repository
175
branch_repo.lock_read()
176
self.addCleanup(branch_repo.unlock)
179
def make_stackable_repo(self, relpath='trunk'):
180
if isinstance(self.repository_format, remote.RemoteRepositoryFormat):
181
# RemoteRepository by default builds a default format real
182
# repository, but the default format is unstackble. So explicitly
183
# make a stackable real repository and use that.
184
repo = self.make_repository(relpath, format='1.9')
185
repo = bzrdir.BzrDir.open(self.get_url(relpath)).open_repository()
187
repo = self.make_repository(relpath)
188
if not repo._format.supports_external_lookups:
189
raise tests.TestNotApplicable('format not stackable')
190
repo.bzrdir._format.set_branch_format(branch.BzrBranchFormat7())
193
def reopen_repo_and_resume_write_group(self, repo):
195
resume_tokens = repo.suspend_write_group()
196
except errors.UnsuspendableWriteGroup:
197
# If we got this far, and this repo does not support resuming write
198
# groups, then get_missing_parent_inventories works in all
199
# cases this repo supports.
203
reopened_repo = repo.bzrdir.open_repository()
204
reopened_repo.lock_write()
205
self.addCleanup(reopened_repo.unlock)
206
reopened_repo.resume_write_group(resume_tokens)
209
def test_ghost_revision(self):
210
"""A parent inventory may be absent if all the needed texts are present.
211
i.e., a ghost revision isn't (necessarily) considered to be a missing
214
# Make a trunk with one commit.
215
trunk_repo = self.make_stackable_repo()
216
self.make_first_commit(trunk_repo)
217
trunk_repo.lock_read()
218
self.addCleanup(trunk_repo.unlock)
219
# Branch the trunk, add a new commit.
220
branch_repo = self.make_new_commit_in_new_repo(
221
trunk_repo, parents=['rev-1', 'ghost-rev'])
222
inv = branch_repo.get_inventory('rev-2')
223
# Make a new repo stacked on trunk, and then copy into it:
224
# - all texts in rev-2
225
# - the new inventory (rev-2)
226
# - the new revision (rev-2)
227
repo = self.make_stackable_repo('stacked')
229
repo.start_write_group()
230
# Add all texts from in rev-2 inventory. Note that this has to exclude
231
# the root if the repo format does not support rich roots.
232
rich_root = branch_repo._format.rich_root_data
234
(ie.file_id, ie.revision) for ie in inv.iter_just_entries()
235
if rich_root or inv.id2path(ie.file_id) != '']
236
repo.texts.insert_record_stream(
237
branch_repo.texts.get_record_stream(all_texts, 'unordered', False))
238
# Add inventory and revision for rev-2.
239
repo.add_inventory('rev-2', inv, ['rev-1', 'ghost-rev'])
240
repo.revisions.insert_record_stream(
241
branch_repo.revisions.get_record_stream(
242
[('rev-2',)], 'unordered', False))
243
# Now, no inventories are reported as missing, even though there is a
245
self.assertEqual(set(), repo.get_missing_parent_inventories())
246
# Resuming the write group does not affect
247
# get_missing_parent_inventories.
248
reopened_repo = self.reopen_repo_and_resume_write_group(repo)
249
self.assertEqual(set(), reopened_repo.get_missing_parent_inventories())
250
reopened_repo.abort_write_group()
252
def test_get_missing_parent_inventories(self):
253
"""A stacked repo with a single revision and inventory (no parent
254
inventory) in it must have all the texts in its inventory (even if not
255
changed w.r.t. to the absent parent), otherwise it will report missing
256
texts/parent inventory.
258
The core of this test is that a file was changed in rev-1, but in a
259
stacked repo that only has rev-2
261
# Make a trunk with one commit.
262
trunk_repo = self.make_stackable_repo()
263
self.make_first_commit(trunk_repo)
264
trunk_repo.lock_read()
265
self.addCleanup(trunk_repo.unlock)
266
# Branch the trunk, add a new commit.
267
branch_repo = self.make_new_commit_in_new_repo(
268
trunk_repo, parents=['rev-1'])
269
inv = branch_repo.get_inventory('rev-2')
270
# Make a new repo stacked on trunk, and copy the new commit's revision
271
# and inventory records to it.
272
repo = self.make_stackable_repo('stacked')
274
repo.start_write_group()
275
# Insert a single fulltext inv (using add_inventory because it's
276
# simpler than insert_record_stream)
277
repo.add_inventory('rev-2', inv, ['rev-1'])
278
repo.revisions.insert_record_stream(
279
branch_repo.revisions.get_record_stream(
280
[('rev-2',)], 'unordered', False))
281
# There should be no missing compression parents
282
self.assertEqual(set(),
283
repo.inventories.get_missing_compression_parent_keys())
285
set([('inventories', 'rev-1')]),
286
repo.get_missing_parent_inventories())
287
# Resuming the write group does not affect
288
# get_missing_parent_inventories.
289
reopened_repo = self.reopen_repo_and_resume_write_group(repo)
291
set([('inventories', 'rev-1')]),
292
reopened_repo.get_missing_parent_inventories())
293
# Adding the parent inventory satisfies get_missing_parent_inventories.
294
reopened_repo.inventories.insert_record_stream(
295
branch_repo.inventories.get_record_stream(
296
[('rev-1',)], 'unordered', False))
298
set(), reopened_repo.get_missing_parent_inventories())
299
reopened_repo.abort_write_group()
301
def test_get_missing_parent_inventories_check(self):
302
builder = self.make_branch_builder('test')
303
builder.build_snapshot('A-id', ['ghost-parent-id'], [
304
('add', ('', 'root-id', 'directory', None)),
305
('add', ('file', 'file-id', 'file', 'content\n'))],
306
allow_leftmost_as_ghost=True)
307
b = builder.get_branch()
309
self.addCleanup(b.unlock)
310
repo = self.make_repository('test-repo')
312
self.addCleanup(repo.unlock)
313
repo.start_write_group()
314
self.addCleanup(repo.abort_write_group)
315
# Now, add the objects manually
316
text_keys = [('file-id', 'A-id')]
317
if repo.supports_rich_root():
318
text_keys.append(('root-id', 'A-id'))
319
# Directly add the texts, inventory, and revision object for 'A-id'
320
repo.texts.insert_record_stream(b.repository.texts.get_record_stream(
321
text_keys, 'unordered', True))
322
repo.add_revision('A-id', b.repository.get_revision('A-id'),
323
b.repository.get_inventory('A-id'))
324
get_missing = repo.get_missing_parent_inventories
325
if repo._format.supports_external_lookups:
326
self.assertEqual(set([('inventories', 'ghost-parent-id')]),
327
get_missing(check_for_missing_texts=False))
328
self.assertEqual(set(), get_missing(check_for_missing_texts=True))
329
self.assertEqual(set(), get_missing())
331
# If we don't support external lookups, we always return empty
332
self.assertEqual(set(), get_missing(check_for_missing_texts=False))
333
self.assertEqual(set(), get_missing(check_for_missing_texts=True))
334
self.assertEqual(set(), get_missing())
336
def test_insert_stream_passes_resume_info(self):
337
repo = self.make_repository('test-repo')
338
if (not repo._format.supports_external_lookups or
339
isinstance(repo, remote.RemoteRepository)):
340
raise tests.TestNotApplicable(
341
'only valid for direct connections to resumable repos')
342
# log calls to get_missing_parent_inventories, so that we can assert it
343
# is called with the correct parameters
345
orig = repo.get_missing_parent_inventories
346
def get_missing(check_for_missing_texts=True):
347
call_log.append(check_for_missing_texts)
348
return orig(check_for_missing_texts=check_for_missing_texts)
349
repo.get_missing_parent_inventories = get_missing
351
self.addCleanup(repo.unlock)
352
sink = repo._get_sink()
353
sink.insert_stream((), repo._format, [])
354
self.assertEqual([False], call_log)
356
repo.start_write_group()
357
# We need to insert something, or suspend_write_group won't actually
359
repo.texts.insert_record_stream([versionedfile.FulltextContentFactory(
360
('file-id', 'rev-id'), (), None, 'lines\n')])
361
tokens = repo.suspend_write_group()
362
self.assertNotEqual([], tokens)
363
sink.insert_stream((), repo._format, tokens)
364
self.assertEqual([True], call_log)
367
class TestResumeableWriteGroup(per_repository.TestCaseWithRepository):
369
def make_write_locked_repo(self, relpath='repo'):
370
repo = self.make_repository(relpath)
372
self.addCleanup(repo.unlock)
375
def reopen_repo(self, repo):
376
same_repo = repo.bzrdir.open_repository()
377
same_repo.lock_write()
378
self.addCleanup(same_repo.unlock)
381
def require_suspendable_write_groups(self, reason):
382
repo = self.make_repository('__suspend_test')
384
self.addCleanup(repo.unlock)
385
repo.start_write_group()
387
wg_tokens = repo.suspend_write_group()
388
except errors.UnsuspendableWriteGroup:
389
repo.abort_write_group()
390
raise tests.TestNotApplicable(reason)
392
def test_suspend_write_group(self):
393
repo = self.make_write_locked_repo()
394
repo.start_write_group()
395
# Add some content so this isn't an empty write group (which may return
397
repo.texts.add_lines(('file-id', 'revid'), (), ['lines'])
399
wg_tokens = repo.suspend_write_group()
400
except errors.UnsuspendableWriteGroup:
401
# The contract for repos that don't support suspending write groups
402
# is that suspend_write_group raises UnsuspendableWriteGroup, but
403
# is otherwise a no-op. So we can still e.g. abort the write group
405
self.assertTrue(repo.is_in_write_group())
406
repo.abort_write_group()
408
# After suspending a write group we are no longer in a write group
409
self.assertFalse(repo.is_in_write_group())
410
# suspend_write_group returns a list of tokens, which are strs. If
411
# no other write groups were resumed, there will only be one token.
412
self.assertEqual(1, len(wg_tokens))
413
self.assertIsInstance(wg_tokens[0], str)
414
# See also test_pack_repository's test of the same name.
416
def test_resume_write_group_then_abort(self):
417
repo = self.make_write_locked_repo()
418
repo.start_write_group()
419
# Add some content so this isn't an empty write group (which may return
421
text_key = ('file-id', 'revid')
422
repo.texts.add_lines(text_key, (), ['lines'])
424
wg_tokens = repo.suspend_write_group()
425
except errors.UnsuspendableWriteGroup:
426
# If the repo does not support suspending write groups, it doesn't
427
# support resuming them either.
428
repo.abort_write_group()
430
errors.UnsuspendableWriteGroup, repo.resume_write_group, [])
432
#self.assertEqual([], list(repo.texts.keys()))
433
same_repo = self.reopen_repo(repo)
434
same_repo.resume_write_group(wg_tokens)
435
self.assertEqual([text_key], list(same_repo.texts.keys()))
436
self.assertTrue(same_repo.is_in_write_group())
437
same_repo.abort_write_group()
438
self.assertEqual([], list(repo.texts.keys()))
439
# See also test_pack_repository's test of the same name.
441
def test_multiple_resume_write_group(self):
442
self.require_suspendable_write_groups(
443
'Cannot test resume on repo that does not support suspending')
444
repo = self.make_write_locked_repo()
445
repo.start_write_group()
446
# Add some content so this isn't an empty write group (which may return
448
first_key = ('file-id', 'revid')
449
repo.texts.add_lines(first_key, (), ['lines'])
450
wg_tokens = repo.suspend_write_group()
451
same_repo = self.reopen_repo(repo)
452
same_repo.resume_write_group(wg_tokens)
453
self.assertTrue(same_repo.is_in_write_group())
454
second_key = ('file-id', 'second-revid')
455
same_repo.texts.add_lines(second_key, (first_key,), ['more lines'])
457
new_wg_tokens = same_repo.suspend_write_group()
460
same_repo.abort_write_group(suppress_errors=True)
461
raise e[0], e[1], e[2]
462
self.assertEqual(2, len(new_wg_tokens))
463
self.assertSubset(wg_tokens, new_wg_tokens)
464
same_repo = self.reopen_repo(repo)
465
same_repo.resume_write_group(new_wg_tokens)
466
both_keys = set([first_key, second_key])
467
self.assertEqual(both_keys, same_repo.texts.keys())
468
same_repo.abort_write_group()
470
def test_no_op_suspend_resume(self):
471
self.require_suspendable_write_groups(
472
'Cannot test resume on repo that does not support suspending')
473
repo = self.make_write_locked_repo()
474
repo.start_write_group()
475
# Add some content so this isn't an empty write group (which may return
477
text_key = ('file-id', 'revid')
478
repo.texts.add_lines(text_key, (), ['lines'])
479
wg_tokens = repo.suspend_write_group()
480
same_repo = self.reopen_repo(repo)
481
same_repo.resume_write_group(wg_tokens)
482
new_wg_tokens = same_repo.suspend_write_group()
483
self.assertEqual(wg_tokens, new_wg_tokens)
484
same_repo = self.reopen_repo(repo)
485
same_repo.resume_write_group(wg_tokens)
486
self.assertEqual([text_key], list(same_repo.texts.keys()))
487
same_repo.abort_write_group()
489
def test_read_after_suspend_fails(self):
490
self.require_suspendable_write_groups(
491
'Cannot test suspend on repo that does not support suspending')
492
repo = self.make_write_locked_repo()
493
repo.start_write_group()
494
# Add some content so this isn't an empty write group (which may return
496
text_key = ('file-id', 'revid')
497
repo.texts.add_lines(text_key, (), ['lines'])
498
wg_tokens = repo.suspend_write_group()
499
self.assertEqual([], list(repo.texts.keys()))
501
def test_read_after_second_suspend_fails(self):
502
self.require_suspendable_write_groups(
503
'Cannot test suspend on repo that does not support suspending')
504
repo = self.make_write_locked_repo()
505
repo.start_write_group()
506
# Add some content so this isn't an empty write group (which may return
508
text_key = ('file-id', 'revid')
509
repo.texts.add_lines(text_key, (), ['lines'])
510
wg_tokens = repo.suspend_write_group()
511
same_repo = self.reopen_repo(repo)
512
same_repo.resume_write_group(wg_tokens)
513
same_repo.suspend_write_group()
514
self.assertEqual([], list(same_repo.texts.keys()))
516
def test_read_after_resume_abort_fails(self):
517
self.require_suspendable_write_groups(
518
'Cannot test suspend on repo that does not support suspending')
519
repo = self.make_write_locked_repo()
520
repo.start_write_group()
521
# Add some content so this isn't an empty write group (which may return
523
text_key = ('file-id', 'revid')
524
repo.texts.add_lines(text_key, (), ['lines'])
525
wg_tokens = repo.suspend_write_group()
526
same_repo = self.reopen_repo(repo)
527
same_repo.resume_write_group(wg_tokens)
528
same_repo.abort_write_group()
529
self.assertEqual([], list(same_repo.texts.keys()))
531
def test_cannot_resume_aborted_write_group(self):
532
self.require_suspendable_write_groups(
533
'Cannot test resume on repo that does not support suspending')
534
repo = self.make_write_locked_repo()
535
repo.start_write_group()
536
# Add some content so this isn't an empty write group (which may return
538
text_key = ('file-id', 'revid')
539
repo.texts.add_lines(text_key, (), ['lines'])
540
wg_tokens = repo.suspend_write_group()
541
same_repo = self.reopen_repo(repo)
542
same_repo.resume_write_group(wg_tokens)
543
same_repo.abort_write_group()
544
same_repo = self.reopen_repo(repo)
546
errors.UnresumableWriteGroup, same_repo.resume_write_group,
549
def test_commit_resumed_write_group_no_new_data(self):
550
self.require_suspendable_write_groups(
551
'Cannot test resume on repo that does not support suspending')
552
repo = self.make_write_locked_repo()
553
repo.start_write_group()
554
# Add some content so this isn't an empty write group (which may return
556
text_key = ('file-id', 'revid')
557
repo.texts.add_lines(text_key, (), ['lines'])
558
wg_tokens = repo.suspend_write_group()
559
same_repo = self.reopen_repo(repo)
560
same_repo.resume_write_group(wg_tokens)
561
same_repo.commit_write_group()
562
self.assertEqual([text_key], list(same_repo.texts.keys()))
564
'lines', same_repo.texts.get_record_stream([text_key],
565
'unordered', True).next().get_bytes_as('fulltext'))
567
errors.UnresumableWriteGroup, same_repo.resume_write_group,
570
def test_commit_resumed_write_group_plus_new_data(self):
571
self.require_suspendable_write_groups(
572
'Cannot test resume on repo that does not support suspending')
573
repo = self.make_write_locked_repo()
574
repo.start_write_group()
575
# Add some content so this isn't an empty write group (which may return
577
first_key = ('file-id', 'revid')
578
repo.texts.add_lines(first_key, (), ['lines'])
579
wg_tokens = repo.suspend_write_group()
580
same_repo = self.reopen_repo(repo)
581
same_repo.resume_write_group(wg_tokens)
582
second_key = ('file-id', 'second-revid')
583
same_repo.texts.add_lines(second_key, (first_key,), ['more lines'])
584
same_repo.commit_write_group()
586
set([first_key, second_key]), set(same_repo.texts.keys()))
588
'lines', same_repo.texts.get_record_stream([first_key],
589
'unordered', True).next().get_bytes_as('fulltext'))
591
'more lines', same_repo.texts.get_record_stream([second_key],
592
'unordered', True).next().get_bytes_as('fulltext'))
594
def make_source_with_delta_record(self):
595
# Make a source repository with a delta record in it.
596
source_repo = self.make_write_locked_repo('source')
597
source_repo.start_write_group()
598
key_base = ('file-id', 'base')
599
key_delta = ('file-id', 'delta')
601
yield versionedfile.FulltextContentFactory(
602
key_base, (), None, 'lines\n')
603
yield versionedfile.FulltextContentFactory(
604
key_delta, (key_base,), None, 'more\nlines\n')
605
source_repo.texts.insert_record_stream(text_stream())
606
source_repo.commit_write_group()
609
def test_commit_resumed_write_group_with_missing_parents(self):
610
self.require_suspendable_write_groups(
611
'Cannot test resume on repo that does not support suspending')
612
source_repo = self.make_source_with_delta_record()
613
key_base = ('file-id', 'base')
614
key_delta = ('file-id', 'delta')
615
# Start a write group, insert just a delta.
616
repo = self.make_write_locked_repo()
617
repo.start_write_group()
618
stream = source_repo.texts.get_record_stream(
619
[key_delta], 'unordered', False)
620
repo.texts.insert_record_stream(stream)
621
# It's either not commitable due to the missing compression parent, or
622
# the stacked location has already filled in the fulltext.
624
repo.commit_write_group()
625
except errors.BzrCheckError:
626
# It refused to commit because we have a missing parent
629
same_repo = self.reopen_repo(repo)
630
same_repo.lock_read()
631
record = same_repo.texts.get_record_stream([key_delta],
632
'unordered', True).next()
633
self.assertEqual('more\nlines\n', record.get_bytes_as('fulltext'))
635
# Merely suspending and resuming doesn't make it commitable either.
636
wg_tokens = repo.suspend_write_group()
637
same_repo = self.reopen_repo(repo)
638
same_repo.resume_write_group(wg_tokens)
640
errors.BzrCheckError, same_repo.commit_write_group)
641
same_repo.abort_write_group()
643
def test_commit_resumed_write_group_adding_missing_parents(self):
644
self.require_suspendable_write_groups(
645
'Cannot test resume on repo that does not support suspending')
646
source_repo = self.make_source_with_delta_record()
647
key_base = ('file-id', 'base')
648
key_delta = ('file-id', 'delta')
649
# Start a write group.
650
repo = self.make_write_locked_repo()
651
repo.start_write_group()
652
# Add some content so this isn't an empty write group (which may return
654
text_key = ('file-id', 'revid')
655
repo.texts.add_lines(text_key, (), ['lines'])
656
# Suspend it, then resume it.
657
wg_tokens = repo.suspend_write_group()
658
same_repo = self.reopen_repo(repo)
659
same_repo.resume_write_group(wg_tokens)
660
# Add a record with a missing compression parent
661
stream = source_repo.texts.get_record_stream(
662
[key_delta], 'unordered', False)
663
same_repo.texts.insert_record_stream(stream)
664
# Just like if we'd added that record without a suspend/resume cycle,
665
# commit_write_group fails.
667
same_repo.commit_write_group()
668
except errors.BzrCheckError:
671
# If the commit_write_group didn't fail, that is because the
672
# insert_record_stream already gave it a fulltext.
673
same_repo = self.reopen_repo(repo)
674
same_repo.lock_read()
675
record = same_repo.texts.get_record_stream([key_delta],
676
'unordered', True).next()
677
self.assertEqual('more\nlines\n', record.get_bytes_as('fulltext'))
679
same_repo.abort_write_group()
681
def test_add_missing_parent_after_resume(self):
682
self.require_suspendable_write_groups(
683
'Cannot test resume on repo that does not support suspending')
684
source_repo = self.make_source_with_delta_record()
685
key_base = ('file-id', 'base')
686
key_delta = ('file-id', 'delta')
687
# Start a write group, insert just a delta.
688
repo = self.make_write_locked_repo()
689
repo.start_write_group()
690
stream = source_repo.texts.get_record_stream(
691
[key_delta], 'unordered', False)
692
repo.texts.insert_record_stream(stream)
693
# Suspend it, then resume it.
694
wg_tokens = repo.suspend_write_group()
695
same_repo = self.reopen_repo(repo)
696
same_repo.resume_write_group(wg_tokens)
697
# Fill in the missing compression parent.
698
stream = source_repo.texts.get_record_stream(
699
[key_base], 'unordered', False)
700
same_repo.texts.insert_record_stream(stream)
701
same_repo.commit_write_group()
703
def test_suspend_empty_initial_write_group(self):
704
"""Suspending a write group with no writes returns an empty token
707
self.require_suspendable_write_groups(
708
'Cannot test suspend on repo that does not support suspending')
709
repo = self.make_write_locked_repo()
710
repo.start_write_group()
711
wg_tokens = repo.suspend_write_group()
712
self.assertEqual([], wg_tokens)
714
def test_suspend_empty_initial_write_group(self):
715
"""Resuming an empty token list is equivalent to start_write_group."""
716
self.require_suspendable_write_groups(
717
'Cannot test resume on repo that does not support suspending')
718
repo = self.make_write_locked_repo()
719
repo.resume_write_group([])
720
repo.abort_write_group()