~bzr-pqm/bzr/bzr.dev

« back to all changes in this revision

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

  • Committer: Tarmac
  • Author(s): Vincent Ladeuil
  • Date: 2017-01-30 14:42:05 UTC
  • mfrom: (6620.1.1 trunk)
  • Revision ID: tarmac-20170130144205-r8fh2xpmiuxyozpv
Merge  2.7 into trunk including fix for bug #1657238 [r=vila]

Show diffs side-by-side

added added

removed removed

Lines of Context:
 
1
# Copyright (C) 2007-2011 Canonical Ltd
 
2
#
 
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.
 
7
#
 
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.
 
12
#
 
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
 
16
 
 
17
"""Tests for repository write groups."""
 
18
 
 
19
import sys
 
20
 
 
21
from bzrlib import (
 
22
    branch,
 
23
    controldir,
 
24
    errors,
 
25
    memorytree,
 
26
    remote,
 
27
    tests,
 
28
    versionedfile,
 
29
    )
 
30
from bzrlib.tests.per_repository_vf import (
 
31
    TestCaseWithRepository,
 
32
    all_repository_vf_format_scenarios,
 
33
    )
 
34
from bzrlib.tests.scenarios import load_tests_apply_scenarios
 
35
 
 
36
 
 
37
load_tests = load_tests_apply_scenarios
 
38
 
 
39
 
 
40
class TestGetMissingParentInventories(TestCaseWithRepository):
 
41
 
 
42
    scenarios = all_repository_vf_format_scenarios()
 
43
 
 
44
    def test_empty_get_missing_parent_inventories(self):
 
45
        """A new write group has no missing parent inventories."""
 
46
        repo = self.make_repository('.')
 
47
        repo.lock_write()
 
48
        repo.start_write_group()
 
49
        try:
 
50
            self.assertEqual(set(), set(repo.get_missing_parent_inventories()))
 
51
        finally:
 
52
            repo.commit_write_group()
 
53
            repo.unlock()
 
54
 
 
55
    def branch_trunk_and_make_tree(self, trunk_repo, relpath):
 
56
        tree = self.make_branch_and_memory_tree('branch')
 
57
        trunk_repo.lock_read()
 
58
        self.addCleanup(trunk_repo.unlock)
 
59
        tree.branch.repository.fetch(trunk_repo, revision_id='rev-1')
 
60
        tree.set_parent_ids(['rev-1'])
 
61
        return tree 
 
62
 
 
63
    def make_first_commit(self, repo):
 
64
        trunk = repo.bzrdir.create_branch()
 
65
        tree = memorytree.MemoryTree.create_on_branch(trunk)
 
66
        tree.lock_write()
 
67
        tree.add([''], ['TREE_ROOT'], ['directory'])
 
68
        tree.add(['dir'], ['dir-id'], ['directory'])
 
69
        tree.add(['filename'], ['file-id'], ['file'])
 
70
        tree.put_file_bytes_non_atomic('file-id', 'content\n')
 
71
        tree.commit('Trunk commit', rev_id='rev-0')
 
72
        tree.commit('Trunk commit', rev_id='rev-1')
 
73
        tree.unlock()
 
74
 
 
75
    def make_new_commit_in_new_repo(self, trunk_repo, parents=None):
 
76
        tree = self.branch_trunk_and_make_tree(trunk_repo, 'branch')
 
77
        tree.set_parent_ids(parents)
 
78
        tree.commit('Branch commit', rev_id='rev-2')
 
79
        branch_repo = tree.branch.repository
 
80
        branch_repo.lock_read()
 
81
        self.addCleanup(branch_repo.unlock)
 
82
        return branch_repo
 
83
 
 
84
    def make_stackable_repo(self, relpath='trunk'):
 
85
        if isinstance(self.repository_format, remote.RemoteRepositoryFormat):
 
86
            # RemoteRepository by default builds a default format real
 
87
            # repository, but the default format is unstackble.  So explicitly
 
88
            # make a stackable real repository and use that.
 
89
            repo = self.make_repository(relpath, format='1.9')
 
90
            dir = controldir.ControlDir.open(self.get_url(relpath))
 
91
            repo = dir.open_repository()
 
92
        else:
 
93
            repo = self.make_repository(relpath)
 
94
        if not repo._format.supports_external_lookups:
 
95
            raise tests.TestNotApplicable('format not stackable')
 
96
        repo.bzrdir._format.set_branch_format(branch.BzrBranchFormat7())
 
97
        return repo
 
98
 
 
99
    def reopen_repo_and_resume_write_group(self, repo):
 
100
        try:
 
101
            resume_tokens = repo.suspend_write_group()
 
102
        except errors.UnsuspendableWriteGroup:
 
103
            # If we got this far, and this repo does not support resuming write
 
104
            # groups, then get_missing_parent_inventories works in all
 
105
            # cases this repo supports.
 
106
            repo.unlock()
 
107
            return
 
108
        repo.unlock()
 
109
        reopened_repo = repo.bzrdir.open_repository()
 
110
        reopened_repo.lock_write()
 
111
        self.addCleanup(reopened_repo.unlock)
 
112
        reopened_repo.resume_write_group(resume_tokens)
 
113
        return reopened_repo
 
114
 
 
115
    def test_ghost_revision(self):
 
116
        """A parent inventory may be absent if all the needed texts are present.
 
117
        i.e., a ghost revision isn't (necessarily) considered to be a missing
 
118
        parent inventory.
 
119
        """
 
120
        # Make a trunk with one commit.
 
121
        trunk_repo = self.make_stackable_repo()
 
122
        self.make_first_commit(trunk_repo)
 
123
        trunk_repo.lock_read()
 
124
        self.addCleanup(trunk_repo.unlock)
 
125
        # Branch the trunk, add a new commit.
 
126
        branch_repo = self.make_new_commit_in_new_repo(
 
127
            trunk_repo, parents=['rev-1', 'ghost-rev'])
 
128
        inv = branch_repo.get_inventory('rev-2')
 
129
        # Make a new repo stacked on trunk, and then copy into it:
 
130
        #  - all texts in rev-2
 
131
        #  - the new inventory (rev-2)
 
132
        #  - the new revision (rev-2)
 
133
        repo = self.make_stackable_repo('stacked')
 
134
        repo.lock_write()
 
135
        repo.start_write_group()
 
136
        # Add all texts from in rev-2 inventory.  Note that this has to exclude
 
137
        # the root if the repo format does not support rich roots.
 
138
        rich_root = branch_repo._format.rich_root_data
 
139
        all_texts = [
 
140
            (ie.file_id, ie.revision) for ie in inv.iter_just_entries()
 
141
             if rich_root or inv.id2path(ie.file_id) != '']
 
142
        repo.texts.insert_record_stream(
 
143
            branch_repo.texts.get_record_stream(all_texts, 'unordered', False))
 
144
        # Add inventory and revision for rev-2.
 
145
        repo.add_inventory('rev-2', inv, ['rev-1', 'ghost-rev'])
 
146
        repo.revisions.insert_record_stream(
 
147
            branch_repo.revisions.get_record_stream(
 
148
                [('rev-2',)], 'unordered', False))
 
149
        # Now, no inventories are reported as missing, even though there is a
 
150
        # ghost.
 
151
        self.assertEqual(set(), repo.get_missing_parent_inventories())
 
152
        # Resuming the write group does not affect
 
153
        # get_missing_parent_inventories.
 
154
        reopened_repo = self.reopen_repo_and_resume_write_group(repo)
 
155
        self.assertEqual(set(), reopened_repo.get_missing_parent_inventories())
 
156
        reopened_repo.abort_write_group()
 
157
 
 
158
    def test_get_missing_parent_inventories(self):
 
159
        """A stacked repo with a single revision and inventory (no parent
 
160
        inventory) in it must have all the texts in its inventory (even if not
 
161
        changed w.r.t. to the absent parent), otherwise it will report missing
 
162
        texts/parent inventory.
 
163
 
 
164
        The core of this test is that a file was changed in rev-1, but in a
 
165
        stacked repo that only has rev-2
 
166
        """
 
167
        # Make a trunk with one commit.
 
168
        trunk_repo = self.make_stackable_repo()
 
169
        self.make_first_commit(trunk_repo)
 
170
        trunk_repo.lock_read()
 
171
        self.addCleanup(trunk_repo.unlock)
 
172
        # Branch the trunk, add a new commit.
 
173
        branch_repo = self.make_new_commit_in_new_repo(
 
174
            trunk_repo, parents=['rev-1'])
 
175
        inv = branch_repo.get_inventory('rev-2')
 
176
        # Make a new repo stacked on trunk, and copy the new commit's revision
 
177
        # and inventory records to it.
 
178
        repo = self.make_stackable_repo('stacked')
 
179
        repo.lock_write()
 
180
        repo.start_write_group()
 
181
        # Insert a single fulltext inv (using add_inventory because it's
 
182
        # simpler than insert_record_stream)
 
183
        repo.add_inventory('rev-2', inv, ['rev-1'])
 
184
        repo.revisions.insert_record_stream(
 
185
            branch_repo.revisions.get_record_stream(
 
186
                [('rev-2',)], 'unordered', False))
 
187
        # There should be no missing compression parents
 
188
        self.assertEqual(set(),
 
189
                repo.inventories.get_missing_compression_parent_keys())
 
190
        self.assertEqual(
 
191
            set([('inventories', 'rev-1')]),
 
192
            repo.get_missing_parent_inventories())
 
193
        # Resuming the write group does not affect
 
194
        # get_missing_parent_inventories.
 
195
        reopened_repo = self.reopen_repo_and_resume_write_group(repo)
 
196
        self.assertEqual(
 
197
            set([('inventories', 'rev-1')]),
 
198
            reopened_repo.get_missing_parent_inventories())
 
199
        # Adding the parent inventory satisfies get_missing_parent_inventories.
 
200
        reopened_repo.inventories.insert_record_stream(
 
201
            branch_repo.inventories.get_record_stream(
 
202
                [('rev-1',)], 'unordered', False))
 
203
        self.assertEqual(
 
204
            set(), reopened_repo.get_missing_parent_inventories())
 
205
        reopened_repo.abort_write_group()
 
206
 
 
207
    def test_get_missing_parent_inventories_check(self):
 
208
        builder = self.make_branch_builder('test')
 
209
        builder.build_snapshot('A-id', ['ghost-parent-id'], [
 
210
            ('add', ('', 'root-id', 'directory', None)),
 
211
            ('add', ('file', 'file-id', 'file', 'content\n'))],
 
212
            allow_leftmost_as_ghost=True)
 
213
        b = builder.get_branch()
 
214
        b.lock_read()
 
215
        self.addCleanup(b.unlock)
 
216
        repo = self.make_repository('test-repo')
 
217
        repo.lock_write()
 
218
        self.addCleanup(repo.unlock)
 
219
        repo.start_write_group()
 
220
        self.addCleanup(repo.abort_write_group)
 
221
        # Now, add the objects manually
 
222
        text_keys = [('file-id', 'A-id')]
 
223
        if repo.supports_rich_root():
 
224
            text_keys.append(('root-id', 'A-id'))
 
225
        # Directly add the texts, inventory, and revision object for 'A-id'
 
226
        repo.texts.insert_record_stream(b.repository.texts.get_record_stream(
 
227
            text_keys, 'unordered', True))
 
228
        repo.add_revision('A-id', b.repository.get_revision('A-id'),
 
229
                          b.repository.get_inventory('A-id'))
 
230
        get_missing = repo.get_missing_parent_inventories
 
231
        if repo._format.supports_external_lookups:
 
232
            self.assertEqual(set([('inventories', 'ghost-parent-id')]),
 
233
                get_missing(check_for_missing_texts=False))
 
234
            self.assertEqual(set(), get_missing(check_for_missing_texts=True))
 
235
            self.assertEqual(set(), get_missing())
 
236
        else:
 
237
            # If we don't support external lookups, we always return empty
 
238
            self.assertEqual(set(), get_missing(check_for_missing_texts=False))
 
239
            self.assertEqual(set(), get_missing(check_for_missing_texts=True))
 
240
            self.assertEqual(set(), get_missing())
 
241
 
 
242
    def test_insert_stream_passes_resume_info(self):
 
243
        repo = self.make_repository('test-repo')
 
244
        if (not repo._format.supports_external_lookups or
 
245
            isinstance(repo, remote.RemoteRepository)):
 
246
            raise tests.TestNotApplicable(
 
247
                'only valid for direct connections to resumable repos')
 
248
        # log calls to get_missing_parent_inventories, so that we can assert it
 
249
        # is called with the correct parameters
 
250
        call_log = []
 
251
        orig = repo.get_missing_parent_inventories
 
252
        def get_missing(check_for_missing_texts=True):
 
253
            call_log.append(check_for_missing_texts)
 
254
            return orig(check_for_missing_texts=check_for_missing_texts)
 
255
        repo.get_missing_parent_inventories = get_missing
 
256
        repo.lock_write()
 
257
        self.addCleanup(repo.unlock)
 
258
        sink = repo._get_sink()
 
259
        sink.insert_stream((), repo._format, [])
 
260
        self.assertEqual([False], call_log)
 
261
        del call_log[:]
 
262
        repo.start_write_group()
 
263
        # We need to insert something, or suspend_write_group won't actually
 
264
        # create a token
 
265
        repo.texts.insert_record_stream([versionedfile.FulltextContentFactory(
 
266
            ('file-id', 'rev-id'), (), None, 'lines\n')])
 
267
        tokens = repo.suspend_write_group()
 
268
        self.assertNotEqual([], tokens)
 
269
        sink.insert_stream((), repo._format, tokens)
 
270
        self.assertEqual([True], call_log)
 
271
 
 
272
    def test_insert_stream_without_locking_fails_without_lock(self):
 
273
        repo = self.make_repository('test-repo')
 
274
        sink = repo._get_sink()
 
275
        stream = [('texts', [versionedfile.FulltextContentFactory(
 
276
            ('file-id', 'rev-id'), (), None, 'lines\n')])]
 
277
        self.assertRaises(errors.ObjectNotLocked,
 
278
            sink.insert_stream_without_locking, stream, repo._format)
 
279
 
 
280
    def test_insert_stream_without_locking_fails_without_write_group(self):
 
281
        repo = self.make_repository('test-repo')
 
282
        self.addCleanup(repo.lock_write().unlock)
 
283
        sink = repo._get_sink()
 
284
        stream = [('texts', [versionedfile.FulltextContentFactory(
 
285
            ('file-id', 'rev-id'), (), None, 'lines\n')])]
 
286
        self.assertRaises(errors.BzrError,
 
287
            sink.insert_stream_without_locking, stream, repo._format)
 
288
 
 
289
    def test_insert_stream_without_locking(self):
 
290
        repo = self.make_repository('test-repo')
 
291
        self.addCleanup(repo.lock_write().unlock)
 
292
        repo.start_write_group()
 
293
        sink = repo._get_sink()
 
294
        stream = [('texts', [versionedfile.FulltextContentFactory(
 
295
            ('file-id', 'rev-id'), (), None, 'lines\n')])]
 
296
        missing_keys = sink.insert_stream_without_locking(stream, repo._format)
 
297
        repo.commit_write_group()
 
298
        self.assertEqual(set(), missing_keys)
 
299
 
 
300
 
 
301
class TestResumeableWriteGroup(TestCaseWithRepository):
 
302
 
 
303
    scenarios = all_repository_vf_format_scenarios()
 
304
 
 
305
    def make_write_locked_repo(self, relpath='repo'):
 
306
        repo = self.make_repository(relpath)
 
307
        repo.lock_write()
 
308
        self.addCleanup(repo.unlock)
 
309
        return repo
 
310
 
 
311
    def reopen_repo(self, repo):
 
312
        same_repo = repo.bzrdir.open_repository()
 
313
        same_repo.lock_write()
 
314
        self.addCleanup(same_repo.unlock)
 
315
        return same_repo
 
316
 
 
317
    def require_suspendable_write_groups(self, reason):
 
318
        repo = self.make_repository('__suspend_test')
 
319
        repo.lock_write()
 
320
        self.addCleanup(repo.unlock)
 
321
        repo.start_write_group()
 
322
        try:
 
323
            wg_tokens = repo.suspend_write_group()
 
324
        except errors.UnsuspendableWriteGroup:
 
325
            repo.abort_write_group()
 
326
            raise tests.TestNotApplicable(reason)
 
327
 
 
328
    def test_suspend_write_group(self):
 
329
        repo = self.make_write_locked_repo()
 
330
        repo.start_write_group()
 
331
        # Add some content so this isn't an empty write group (which may return
 
332
        # 0 tokens)
 
333
        repo.texts.add_lines(('file-id', 'revid'), (), ['lines'])
 
334
        try:
 
335
            wg_tokens = repo.suspend_write_group()
 
336
        except errors.UnsuspendableWriteGroup:
 
337
            # The contract for repos that don't support suspending write groups
 
338
            # is that suspend_write_group raises UnsuspendableWriteGroup, but
 
339
            # is otherwise a no-op.  So we can still e.g. abort the write group
 
340
            # as usual.
 
341
            self.assertTrue(repo.is_in_write_group())
 
342
            repo.abort_write_group()
 
343
        else:
 
344
            # After suspending a write group we are no longer in a write group
 
345
            self.assertFalse(repo.is_in_write_group())
 
346
            # suspend_write_group returns a list of tokens, which are strs.  If
 
347
            # no other write groups were resumed, there will only be one token.
 
348
            self.assertEqual(1, len(wg_tokens))
 
349
            self.assertIsInstance(wg_tokens[0], str)
 
350
            # See also test_pack_repository's test of the same name.
 
351
 
 
352
    def test_resume_write_group_then_abort(self):
 
353
        repo = self.make_write_locked_repo()
 
354
        repo.start_write_group()
 
355
        # Add some content so this isn't an empty write group (which may return
 
356
        # 0 tokens)
 
357
        text_key = ('file-id', 'revid')
 
358
        repo.texts.add_lines(text_key, (), ['lines'])
 
359
        try:
 
360
            wg_tokens = repo.suspend_write_group()
 
361
        except errors.UnsuspendableWriteGroup:
 
362
            # If the repo does not support suspending write groups, it doesn't
 
363
            # support resuming them either.
 
364
            repo.abort_write_group()
 
365
            self.assertRaises(
 
366
                errors.UnsuspendableWriteGroup, repo.resume_write_group, [])
 
367
        else:
 
368
            #self.assertEqual([], list(repo.texts.keys()))
 
369
            same_repo = self.reopen_repo(repo)
 
370
            same_repo.resume_write_group(wg_tokens)
 
371
            self.assertEqual([text_key], list(same_repo.texts.keys()))
 
372
            self.assertTrue(same_repo.is_in_write_group())
 
373
            same_repo.abort_write_group()
 
374
            self.assertEqual([], list(repo.texts.keys()))
 
375
            # See also test_pack_repository's test of the same name.
 
376
 
 
377
    def test_multiple_resume_write_group(self):
 
378
        self.require_suspendable_write_groups(
 
379
            'Cannot test resume on repo that does not support suspending')
 
380
        repo = self.make_write_locked_repo()
 
381
        repo.start_write_group()
 
382
        # Add some content so this isn't an empty write group (which may return
 
383
        # 0 tokens)
 
384
        first_key = ('file-id', 'revid')
 
385
        repo.texts.add_lines(first_key, (), ['lines'])
 
386
        wg_tokens = repo.suspend_write_group()
 
387
        same_repo = self.reopen_repo(repo)
 
388
        same_repo.resume_write_group(wg_tokens)
 
389
        self.assertTrue(same_repo.is_in_write_group())
 
390
        second_key = ('file-id', 'second-revid')
 
391
        same_repo.texts.add_lines(second_key, (first_key,), ['more lines'])
 
392
        try:
 
393
            new_wg_tokens = same_repo.suspend_write_group()
 
394
        except:
 
395
            e = sys.exc_info()
 
396
            same_repo.abort_write_group(suppress_errors=True)
 
397
            raise e[0], e[1], e[2]
 
398
        self.assertEqual(2, len(new_wg_tokens))
 
399
        self.assertSubset(wg_tokens, new_wg_tokens)
 
400
        same_repo = self.reopen_repo(repo)
 
401
        same_repo.resume_write_group(new_wg_tokens)
 
402
        both_keys = set([first_key, second_key])
 
403
        self.assertEqual(both_keys, same_repo.texts.keys())
 
404
        same_repo.abort_write_group()
 
405
 
 
406
    def test_no_op_suspend_resume(self):
 
407
        self.require_suspendable_write_groups(
 
408
            'Cannot test resume on repo that does not support suspending')
 
409
        repo = self.make_write_locked_repo()
 
410
        repo.start_write_group()
 
411
        # Add some content so this isn't an empty write group (which may return
 
412
        # 0 tokens)
 
413
        text_key = ('file-id', 'revid')
 
414
        repo.texts.add_lines(text_key, (), ['lines'])
 
415
        wg_tokens = repo.suspend_write_group()
 
416
        same_repo = self.reopen_repo(repo)
 
417
        same_repo.resume_write_group(wg_tokens)
 
418
        new_wg_tokens = same_repo.suspend_write_group()
 
419
        self.assertEqual(wg_tokens, new_wg_tokens)
 
420
        same_repo = self.reopen_repo(repo)
 
421
        same_repo.resume_write_group(wg_tokens)
 
422
        self.assertEqual([text_key], list(same_repo.texts.keys()))
 
423
        same_repo.abort_write_group()
 
424
 
 
425
    def test_read_after_suspend_fails(self):
 
426
        self.require_suspendable_write_groups(
 
427
            'Cannot test suspend on repo that does not support suspending')
 
428
        repo = self.make_write_locked_repo()
 
429
        repo.start_write_group()
 
430
        # Add some content so this isn't an empty write group (which may return
 
431
        # 0 tokens)
 
432
        text_key = ('file-id', 'revid')
 
433
        repo.texts.add_lines(text_key, (), ['lines'])
 
434
        wg_tokens = repo.suspend_write_group()
 
435
        self.assertEqual([], list(repo.texts.keys()))
 
436
 
 
437
    def test_read_after_second_suspend_fails(self):
 
438
        self.require_suspendable_write_groups(
 
439
            'Cannot test suspend on repo that does not support suspending')
 
440
        repo = self.make_write_locked_repo()
 
441
        repo.start_write_group()
 
442
        # Add some content so this isn't an empty write group (which may return
 
443
        # 0 tokens)
 
444
        text_key = ('file-id', 'revid')
 
445
        repo.texts.add_lines(text_key, (), ['lines'])
 
446
        wg_tokens = repo.suspend_write_group()
 
447
        same_repo = self.reopen_repo(repo)
 
448
        same_repo.resume_write_group(wg_tokens)
 
449
        same_repo.suspend_write_group()
 
450
        self.assertEqual([], list(same_repo.texts.keys()))
 
451
 
 
452
    def test_read_after_resume_abort_fails(self):
 
453
        self.require_suspendable_write_groups(
 
454
            'Cannot test suspend on repo that does not support suspending')
 
455
        repo = self.make_write_locked_repo()
 
456
        repo.start_write_group()
 
457
        # Add some content so this isn't an empty write group (which may return
 
458
        # 0 tokens)
 
459
        text_key = ('file-id', 'revid')
 
460
        repo.texts.add_lines(text_key, (), ['lines'])
 
461
        wg_tokens = repo.suspend_write_group()
 
462
        same_repo = self.reopen_repo(repo)
 
463
        same_repo.resume_write_group(wg_tokens)
 
464
        same_repo.abort_write_group()
 
465
        self.assertEqual([], list(same_repo.texts.keys()))
 
466
 
 
467
    def test_cannot_resume_aborted_write_group(self):
 
468
        self.require_suspendable_write_groups(
 
469
            'Cannot test resume on repo that does not support suspending')
 
470
        repo = self.make_write_locked_repo()
 
471
        repo.start_write_group()
 
472
        # Add some content so this isn't an empty write group (which may return
 
473
        # 0 tokens)
 
474
        text_key = ('file-id', 'revid')
 
475
        repo.texts.add_lines(text_key, (), ['lines'])
 
476
        wg_tokens = repo.suspend_write_group()
 
477
        same_repo = self.reopen_repo(repo)
 
478
        same_repo.resume_write_group(wg_tokens)
 
479
        same_repo.abort_write_group()
 
480
        same_repo = self.reopen_repo(repo)
 
481
        self.assertRaises(
 
482
            errors.UnresumableWriteGroup, same_repo.resume_write_group,
 
483
            wg_tokens)
 
484
 
 
485
    def test_commit_resumed_write_group_no_new_data(self):
 
486
        self.require_suspendable_write_groups(
 
487
            'Cannot test resume on repo that does not support suspending')
 
488
        repo = self.make_write_locked_repo()
 
489
        repo.start_write_group()
 
490
        # Add some content so this isn't an empty write group (which may return
 
491
        # 0 tokens)
 
492
        text_key = ('file-id', 'revid')
 
493
        repo.texts.add_lines(text_key, (), ['lines'])
 
494
        wg_tokens = repo.suspend_write_group()
 
495
        same_repo = self.reopen_repo(repo)
 
496
        same_repo.resume_write_group(wg_tokens)
 
497
        same_repo.commit_write_group()
 
498
        self.assertEqual([text_key], list(same_repo.texts.keys()))
 
499
        self.assertEqual(
 
500
            'lines', same_repo.texts.get_record_stream([text_key],
 
501
                'unordered', True).next().get_bytes_as('fulltext'))
 
502
        self.assertRaises(
 
503
            errors.UnresumableWriteGroup, same_repo.resume_write_group,
 
504
            wg_tokens)
 
505
 
 
506
    def test_commit_resumed_write_group_plus_new_data(self):
 
507
        self.require_suspendable_write_groups(
 
508
            'Cannot test resume on repo that does not support suspending')
 
509
        repo = self.make_write_locked_repo()
 
510
        repo.start_write_group()
 
511
        # Add some content so this isn't an empty write group (which may return
 
512
        # 0 tokens)
 
513
        first_key = ('file-id', 'revid')
 
514
        repo.texts.add_lines(first_key, (), ['lines'])
 
515
        wg_tokens = repo.suspend_write_group()
 
516
        same_repo = self.reopen_repo(repo)
 
517
        same_repo.resume_write_group(wg_tokens)
 
518
        second_key = ('file-id', 'second-revid')
 
519
        same_repo.texts.add_lines(second_key, (first_key,), ['more lines'])
 
520
        same_repo.commit_write_group()
 
521
        self.assertEqual(
 
522
            set([first_key, second_key]), set(same_repo.texts.keys()))
 
523
        self.assertEqual(
 
524
            'lines', same_repo.texts.get_record_stream([first_key],
 
525
                'unordered', True).next().get_bytes_as('fulltext'))
 
526
        self.assertEqual(
 
527
            'more lines', same_repo.texts.get_record_stream([second_key],
 
528
                'unordered', True).next().get_bytes_as('fulltext'))
 
529
 
 
530
    def make_source_with_delta_record(self):
 
531
        # Make a source repository with a delta record in it.
 
532
        source_repo = self.make_write_locked_repo('source')
 
533
        source_repo.start_write_group()
 
534
        key_base = ('file-id', 'base')
 
535
        key_delta = ('file-id', 'delta')
 
536
        def text_stream():
 
537
            yield versionedfile.FulltextContentFactory(
 
538
                key_base, (), None, 'lines\n')
 
539
            yield versionedfile.FulltextContentFactory(
 
540
                key_delta, (key_base,), None, 'more\nlines\n')
 
541
        source_repo.texts.insert_record_stream(text_stream())
 
542
        source_repo.commit_write_group()
 
543
        return source_repo
 
544
 
 
545
    def test_commit_resumed_write_group_with_missing_parents(self):
 
546
        self.require_suspendable_write_groups(
 
547
            'Cannot test resume on repo that does not support suspending')
 
548
        source_repo = self.make_source_with_delta_record()
 
549
        key_base = ('file-id', 'base')
 
550
        key_delta = ('file-id', 'delta')
 
551
        # Start a write group, insert just a delta.
 
552
        repo = self.make_write_locked_repo()
 
553
        repo.start_write_group()
 
554
        stream = source_repo.texts.get_record_stream(
 
555
            [key_delta], 'unordered', False)
 
556
        repo.texts.insert_record_stream(stream)
 
557
        # It's either not commitable due to the missing compression parent, or
 
558
        # the stacked location has already filled in the fulltext.
 
559
        try:
 
560
            repo.commit_write_group()
 
561
        except errors.BzrCheckError:
 
562
            # It refused to commit because we have a missing parent
 
563
            pass
 
564
        else:
 
565
            same_repo = self.reopen_repo(repo)
 
566
            same_repo.lock_read()
 
567
            record = same_repo.texts.get_record_stream([key_delta],
 
568
                                                       'unordered', True).next()
 
569
            self.assertEqual('more\nlines\n', record.get_bytes_as('fulltext'))
 
570
            return
 
571
        # Merely suspending and resuming doesn't make it commitable either.
 
572
        wg_tokens = repo.suspend_write_group()
 
573
        same_repo = self.reopen_repo(repo)
 
574
        same_repo.resume_write_group(wg_tokens)
 
575
        self.assertRaises(
 
576
            errors.BzrCheckError, same_repo.commit_write_group)
 
577
        same_repo.abort_write_group()
 
578
 
 
579
    def test_commit_resumed_write_group_adding_missing_parents(self):
 
580
        self.require_suspendable_write_groups(
 
581
            'Cannot test resume on repo that does not support suspending')
 
582
        source_repo = self.make_source_with_delta_record()
 
583
        key_base = ('file-id', 'base')
 
584
        key_delta = ('file-id', 'delta')
 
585
        # Start a write group.
 
586
        repo = self.make_write_locked_repo()
 
587
        repo.start_write_group()
 
588
        # Add some content so this isn't an empty write group (which may return
 
589
        # 0 tokens)
 
590
        text_key = ('file-id', 'revid')
 
591
        repo.texts.add_lines(text_key, (), ['lines'])
 
592
        # Suspend it, then resume it.
 
593
        wg_tokens = repo.suspend_write_group()
 
594
        same_repo = self.reopen_repo(repo)
 
595
        same_repo.resume_write_group(wg_tokens)
 
596
        # Add a record with a missing compression parent
 
597
        stream = source_repo.texts.get_record_stream(
 
598
            [key_delta], 'unordered', False)
 
599
        same_repo.texts.insert_record_stream(stream)
 
600
        # Just like if we'd added that record without a suspend/resume cycle,
 
601
        # commit_write_group fails.
 
602
        try:
 
603
            same_repo.commit_write_group()
 
604
        except errors.BzrCheckError:
 
605
            pass
 
606
        else:
 
607
            # If the commit_write_group didn't fail, that is because the
 
608
            # insert_record_stream already gave it a fulltext.
 
609
            same_repo = self.reopen_repo(repo)
 
610
            same_repo.lock_read()
 
611
            record = same_repo.texts.get_record_stream([key_delta],
 
612
                                                       'unordered', True).next()
 
613
            self.assertEqual('more\nlines\n', record.get_bytes_as('fulltext'))
 
614
            return
 
615
        same_repo.abort_write_group()
 
616
 
 
617
    def test_add_missing_parent_after_resume(self):
 
618
        self.require_suspendable_write_groups(
 
619
            'Cannot test resume on repo that does not support suspending')
 
620
        source_repo = self.make_source_with_delta_record()
 
621
        key_base = ('file-id', 'base')
 
622
        key_delta = ('file-id', 'delta')
 
623
        # Start a write group, insert just a delta.
 
624
        repo = self.make_write_locked_repo()
 
625
        repo.start_write_group()
 
626
        stream = source_repo.texts.get_record_stream(
 
627
            [key_delta], 'unordered', False)
 
628
        repo.texts.insert_record_stream(stream)
 
629
        # Suspend it, then resume it.
 
630
        wg_tokens = repo.suspend_write_group()
 
631
        same_repo = self.reopen_repo(repo)
 
632
        same_repo.resume_write_group(wg_tokens)
 
633
        # Fill in the missing compression parent.
 
634
        stream = source_repo.texts.get_record_stream(
 
635
            [key_base], 'unordered', False)
 
636
        same_repo.texts.insert_record_stream(stream)
 
637
        same_repo.commit_write_group()
 
638
 
 
639
    def test_suspend_empty_initial_write_group(self):
 
640
        """Suspending a write group with no writes returns an empty token
 
641
        list.
 
642
        """
 
643
        self.require_suspendable_write_groups(
 
644
            'Cannot test suspend on repo that does not support suspending')
 
645
        repo = self.make_write_locked_repo()
 
646
        repo.start_write_group()
 
647
        wg_tokens = repo.suspend_write_group()
 
648
        self.assertEqual([], wg_tokens)
 
649
 
 
650
    def test_resume_empty_initial_write_group(self):
 
651
        """Resuming an empty token list is equivalent to start_write_group."""
 
652
        self.require_suspendable_write_groups(
 
653
            'Cannot test resume on repo that does not support suspending')
 
654
        repo = self.make_write_locked_repo()
 
655
        repo.resume_write_group([])
 
656
        repo.abort_write_group()