/brz/remove-bazaar

To get this branch, use:
bzr branch http://gegoxaren.bato24.eu/bzr/brz/remove-bazaar

« back to all changes in this revision

Viewing changes to bzrlib/tests/per_repository/test_write_group.py

  • Committer: Martin von Gagern
  • Date: 2010-04-20 08:47:38 UTC
  • mfrom: (5167 +trunk)
  • mto: This revision was merged to the branch mainline in revision 5195.
  • Revision ID: martin.vgagern@gmx.net-20100420084738-ygymnqmdllzrhpfn
merge trunk

Show diffs side-by-side

added added

removed removed

Lines of Context:
1
 
# Copyright (C) 2007 Canonical Ltd
 
1
# Copyright (C) 2007-2010 Canonical Ltd
2
2
#
3
3
# This program is free software; you can redistribute it and/or modify
4
4
# it under the terms of the GNU General Public License as published by
12
12
#
13
13
# You should have received a copy of the GNU General Public License
14
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
 
15
# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA
16
16
 
17
17
"""Tests for repository write groups."""
18
18
 
19
 
from bzrlib import errors
20
 
from bzrlib.tests.per_repository import TestCaseWithRepository
21
 
 
22
 
 
23
 
class TestWriteGroup(TestCaseWithRepository):
 
19
import sys
 
20
 
 
21
from bzrlib import (
 
22
    branch,
 
23
    bzrdir,
 
24
    errors,
 
25
    graph,
 
26
    memorytree,
 
27
    osutils,
 
28
    remote,
 
29
    tests,
 
30
    versionedfile,
 
31
    )
 
32
from bzrlib.tests import (
 
33
    per_repository,
 
34
    test_server,
 
35
    )
 
36
from bzrlib.transport import memory
 
37
 
 
38
 
 
39
class TestWriteGroup(per_repository.TestCaseWithRepository):
24
40
 
25
41
    def test_start_write_group_unlocked_needs_write_lock(self):
26
42
        repo = self.make_repository('.')
46
62
        repo.lock_write()
47
63
        repo.start_write_group()
48
64
        try:
49
 
            # don't need a specific exception for now - this is 
 
65
            # don't need a specific exception for now - this is
50
66
            # really to be sure it's used right, not for signalling
51
67
            # semantic information.
52
68
            self.assertRaises(errors.BzrError, repo.start_write_group)
54
70
            repo.commit_write_group()
55
71
            repo.unlock()
56
72
 
57
 
    def test_commit_write_group_gets_None(self):
 
73
    def test_commit_write_group_does_not_error(self):
58
74
        repo = self.make_repository('.')
59
75
        repo.lock_write()
60
76
        repo.start_write_group()
61
 
        self.assertEqual(None, repo.commit_write_group())
 
77
        # commit_write_group can either return None (for repositories without
 
78
        # isolated transactions) or a hint for pack(). So we only check it
 
79
        # works in this interface test, because all repositories are exercised.
 
80
        repo.commit_write_group()
62
81
        repo.unlock()
63
82
 
64
83
    def test_unlock_in_write_group(self):
65
84
        repo = self.make_repository('.')
66
85
        repo.lock_write()
67
86
        repo.start_write_group()
68
 
        # don't need a specific exception for now - this is 
 
87
        # don't need a specific exception for now - this is
69
88
        # really to be sure it's used right, not for signalling
70
89
        # semantic information.
71
 
        self.assertRaises(errors.BzrError, repo.unlock)
 
90
        self.assertLogsError(errors.BzrError, repo.unlock)
72
91
        # after this error occurs, the repository is unlocked, and the write
73
92
        # group is gone.  you've had your chance, and you blew it. ;-)
74
93
        self.assertFalse(repo.is_locked())
96
115
        repo.start_write_group()
97
116
        self.assertEqual(None, repo.abort_write_group())
98
117
        repo.unlock()
 
118
 
 
119
    def test_abort_write_group_does_not_raise_when_suppressed(self):
 
120
        if self.transport_server is test_server.LocalURLServer:
 
121
            self.transport_server = None
 
122
        self.vfs_transport_factory = memory.MemoryServer
 
123
        repo = self.make_repository('repo')
 
124
        token = repo.lock_write()
 
125
        self.addCleanup(repo.unlock)
 
126
        repo.start_write_group()
 
127
        # Damage the repository on the filesystem
 
128
        t = self.get_transport('')
 
129
        t.rename('repo', 'foo')
 
130
        self.addCleanup(t.rename, 'foo', 'repo')
 
131
        # abort_write_group will not raise an error, because either an
 
132
        # exception was not generated, or the exception was caught and
 
133
        # suppressed.  See also test_pack_repository's test of the same name.
 
134
        self.assertEqual(None, repo.abort_write_group(suppress_errors=True))
 
135
 
 
136
 
 
137
class TestGetMissingParentInventories(per_repository.TestCaseWithRepository):
 
138
 
 
139
    def test_empty_get_missing_parent_inventories(self):
 
140
        """A new write group has no missing parent inventories."""
 
141
        repo = self.make_repository('.')
 
142
        repo.lock_write()
 
143
        repo.start_write_group()
 
144
        try:
 
145
            self.assertEqual(set(), set(repo.get_missing_parent_inventories()))
 
146
        finally:
 
147
            repo.commit_write_group()
 
148
            repo.unlock()
 
149
 
 
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'])
 
156
        return tree 
 
157
 
 
158
    def make_first_commit(self, repo):
 
159
        trunk = repo.bzrdir.create_branch()
 
160
        tree = memorytree.MemoryTree.create_on_branch(trunk)
 
161
        tree.lock_write()
 
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')
 
168
        tree.unlock()
 
169
 
 
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)
 
177
        return branch_repo
 
178
 
 
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()
 
186
        else:
 
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())
 
191
        return repo
 
192
 
 
193
    def reopen_repo_and_resume_write_group(self, repo):
 
194
        try:
 
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.
 
200
            repo.unlock()
 
201
            return
 
202
        repo.unlock()
 
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)
 
207
        return reopened_repo
 
208
 
 
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
 
212
        parent inventory.
 
213
        """
 
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')
 
228
        repo.lock_write()
 
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
 
233
        all_texts = [
 
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
 
244
        # ghost.
 
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()
 
251
 
 
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.
 
257
 
 
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
 
260
        """
 
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')
 
273
        repo.lock_write()
 
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())
 
284
        self.assertEqual(
 
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)
 
290
        self.assertEqual(
 
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))
 
297
        self.assertEqual(
 
298
            set(), reopened_repo.get_missing_parent_inventories())
 
299
        reopened_repo.abort_write_group()
 
300
 
 
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()
 
308
        b.lock_read()
 
309
        self.addCleanup(b.unlock)
 
310
        repo = self.make_repository('test-repo')
 
311
        repo.lock_write()
 
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())
 
330
        else:
 
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())
 
335
 
 
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
 
344
        call_log = []
 
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
 
350
        repo.lock_write()
 
351
        self.addCleanup(repo.unlock)
 
352
        sink = repo._get_sink()
 
353
        sink.insert_stream((), repo._format, [])
 
354
        self.assertEqual([False], call_log)
 
355
        del call_log[:]
 
356
        repo.start_write_group()
 
357
        # We need to insert something, or suspend_write_group won't actually
 
358
        # create a token
 
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)
 
365
 
 
366
 
 
367
class TestResumeableWriteGroup(per_repository.TestCaseWithRepository):
 
368
 
 
369
    def make_write_locked_repo(self, relpath='repo'):
 
370
        repo = self.make_repository(relpath)
 
371
        repo.lock_write()
 
372
        self.addCleanup(repo.unlock)
 
373
        return repo
 
374
 
 
375
    def reopen_repo(self, repo):
 
376
        same_repo = repo.bzrdir.open_repository()
 
377
        same_repo.lock_write()
 
378
        self.addCleanup(same_repo.unlock)
 
379
        return same_repo
 
380
 
 
381
    def require_suspendable_write_groups(self, reason):
 
382
        repo = self.make_repository('__suspend_test')
 
383
        repo.lock_write()
 
384
        self.addCleanup(repo.unlock)
 
385
        repo.start_write_group()
 
386
        try:
 
387
            wg_tokens = repo.suspend_write_group()
 
388
        except errors.UnsuspendableWriteGroup:
 
389
            repo.abort_write_group()
 
390
            raise tests.TestNotApplicable(reason)
 
391
 
 
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
 
396
        # 0 tokens)
 
397
        repo.texts.add_lines(('file-id', 'revid'), (), ['lines'])
 
398
        try:
 
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
 
404
            # as usual.
 
405
            self.assertTrue(repo.is_in_write_group())
 
406
            repo.abort_write_group()
 
407
        else:
 
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.
 
415
 
 
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
 
420
        # 0 tokens)
 
421
        text_key = ('file-id', 'revid')
 
422
        repo.texts.add_lines(text_key, (), ['lines'])
 
423
        try:
 
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()
 
429
            self.assertRaises(
 
430
                errors.UnsuspendableWriteGroup, repo.resume_write_group, [])
 
431
        else:
 
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.
 
440
 
 
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
 
447
        # 0 tokens)
 
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'])
 
456
        try:
 
457
            new_wg_tokens = same_repo.suspend_write_group()
 
458
        except:
 
459
            e = sys.exc_info()
 
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()
 
469
 
 
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
 
476
        # 0 tokens)
 
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()
 
488
 
 
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
 
495
        # 0 tokens)
 
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()))
 
500
 
 
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
 
507
        # 0 tokens)
 
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()))
 
515
 
 
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
 
522
        # 0 tokens)
 
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()))
 
530
 
 
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
 
537
        # 0 tokens)
 
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)
 
545
        self.assertRaises(
 
546
            errors.UnresumableWriteGroup, same_repo.resume_write_group,
 
547
            wg_tokens)
 
548
 
 
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
 
555
        # 0 tokens)
 
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()))
 
563
        self.assertEqual(
 
564
            'lines', same_repo.texts.get_record_stream([text_key],
 
565
                'unordered', True).next().get_bytes_as('fulltext'))
 
566
        self.assertRaises(
 
567
            errors.UnresumableWriteGroup, same_repo.resume_write_group,
 
568
            wg_tokens)
 
569
 
 
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
 
576
        # 0 tokens)
 
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()
 
585
        self.assertEqual(
 
586
            set([first_key, second_key]), set(same_repo.texts.keys()))
 
587
        self.assertEqual(
 
588
            'lines', same_repo.texts.get_record_stream([first_key],
 
589
                'unordered', True).next().get_bytes_as('fulltext'))
 
590
        self.assertEqual(
 
591
            'more lines', same_repo.texts.get_record_stream([second_key],
 
592
                'unordered', True).next().get_bytes_as('fulltext'))
 
593
 
 
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')
 
600
        def text_stream():
 
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()
 
607
        return source_repo
 
608
 
 
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.
 
623
        try:
 
624
            repo.commit_write_group()
 
625
        except errors.BzrCheckError:
 
626
            # It refused to commit because we have a missing parent
 
627
            pass
 
628
        else:
 
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'))
 
634
            return
 
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)
 
639
        self.assertRaises(
 
640
            errors.BzrCheckError, same_repo.commit_write_group)
 
641
        same_repo.abort_write_group()
 
642
 
 
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
 
653
        # 0 tokens)
 
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.
 
666
        try:
 
667
            same_repo.commit_write_group()
 
668
        except errors.BzrCheckError:
 
669
            pass
 
670
        else:
 
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'))
 
678
            return
 
679
        same_repo.abort_write_group()
 
680
 
 
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()
 
702
 
 
703
    def test_suspend_empty_initial_write_group(self):
 
704
        """Suspending a write group with no writes returns an empty token
 
705
        list.
 
706
        """
 
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)
 
713
 
 
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()
 
721
 
 
722