/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_merger.py

  • Committer: Vincent Ladeuil
  • Date: 2012-01-18 14:09:19 UTC
  • mto: This revision was merged to the branch mainline in revision 6468.
  • Revision ID: v.ladeuil+lp@free.fr-20120118140919-rlvdrhpc0nq1lbwi
Change set/remove to require a lock for the branch config files.

This means that tests (or any plugin for that matter) do not requires an
explicit lock on the branch anymore to change a single option. This also
means the optimisation becomes "opt-in" and as such won't be as
spectacular as it may be and/or harder to get right (nothing fails
anymore).

This reduces the diff by ~300 lines.

Code/tests that were updating more than one config option is still taking
a lock to at least avoid some IOs and demonstrate the benefits through
the decreased number of hpss calls.

The duplication between BranchStack and BranchOnlyStack will be removed
once the same sharing is in place for local config files, at which point
the Stack class itself may be able to host the changes.

Show diffs side-by-side

added added

removed removed

Lines of Context:
 
1
# Copyright (C) 2009, 2010, 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
"""Implementation tests for bzrlib.merge.Merger."""
 
18
 
 
19
import os
 
20
 
 
21
from bzrlib.conflicts import TextConflict
 
22
from bzrlib import (
 
23
    errors,
 
24
    merge as _mod_merge,
 
25
    )
 
26
from bzrlib.tests import (
 
27
    multiply_tests,
 
28
    TestCaseWithTransport,
 
29
    )
 
30
from bzrlib.tests.test_merge_core import MergeBuilder
 
31
from bzrlib.transform import TreeTransform
 
32
 
 
33
 
 
34
 
 
35
def load_tests(standard_tests, module, loader):
 
36
    """Multiply tests for tranport implementations."""
 
37
    result = loader.suiteClass()
 
38
    scenarios = [
 
39
        (name, {'merge_type': merger})
 
40
        for name, merger in _mod_merge.merge_type_registry.items()]
 
41
    return multiply_tests(standard_tests, scenarios, result)
 
42
 
 
43
 
 
44
class TestMergeImplementation(TestCaseWithTransport):
 
45
 
 
46
    def do_merge(self, target_tree, source_tree, **kwargs):
 
47
        merger = _mod_merge.Merger.from_revision_ids(None,
 
48
            target_tree, source_tree.last_revision(),
 
49
            other_branch=source_tree.branch)
 
50
        merger.merge_type=self.merge_type
 
51
        for name, value in kwargs.items():
 
52
            setattr(merger, name, value)
 
53
        merger.do_merge()
 
54
 
 
55
    def test_merge_specific_file(self):
 
56
        this_tree = self.make_branch_and_tree('this')
 
57
        this_tree.lock_write()
 
58
        self.addCleanup(this_tree.unlock)
 
59
        self.build_tree_contents([
 
60
            ('this/file1', 'a\nb\n'),
 
61
            ('this/file2', 'a\nb\n')
 
62
        ])
 
63
        this_tree.add(['file1', 'file2'])
 
64
        this_tree.commit('Added files')
 
65
        other_tree = this_tree.bzrdir.sprout('other').open_workingtree()
 
66
        self.build_tree_contents([
 
67
            ('other/file1', 'a\nb\nc\n'),
 
68
            ('other/file2', 'a\nb\nc\n')
 
69
        ])
 
70
        other_tree.commit('modified both')
 
71
        self.build_tree_contents([
 
72
            ('this/file1', 'd\na\nb\n'),
 
73
            ('this/file2', 'd\na\nb\n')
 
74
        ])
 
75
        this_tree.commit('modified both')
 
76
        self.do_merge(this_tree, other_tree, interesting_files=['file1'])
 
77
        self.assertFileEqual('d\na\nb\nc\n', 'this/file1')
 
78
        self.assertFileEqual('d\na\nb\n', 'this/file2')
 
79
 
 
80
    def test_merge_move_and_change(self):
 
81
        this_tree = self.make_branch_and_tree('this')
 
82
        this_tree.lock_write()
 
83
        self.addCleanup(this_tree.unlock)
 
84
        self.build_tree_contents([
 
85
            ('this/file1', 'line 1\nline 2\nline 3\nline 4\n'),
 
86
        ])
 
87
        this_tree.add('file1',)
 
88
        this_tree.commit('Added file')
 
89
        other_tree = this_tree.bzrdir.sprout('other').open_workingtree()
 
90
        self.build_tree_contents([
 
91
            ('other/file1', 'line 1\nline 2 to 2.1\nline 3\nline 4\n'),
 
92
        ])
 
93
        other_tree.commit('Changed 2 to 2.1')
 
94
        self.build_tree_contents([
 
95
            ('this/file1', 'line 1\nline 3\nline 2\nline 4\n'),
 
96
        ])
 
97
        this_tree.commit('Swapped 2 & 3')
 
98
        self.do_merge(this_tree, other_tree)
 
99
        if self.merge_type is _mod_merge.LCAMerger:
 
100
            self.expectFailure(
 
101
                "lca merge doesn't conflict for move and change",
 
102
                self.assertFileEqual,
 
103
                'line 1\n'
 
104
                '<<<<<<< TREE\n'
 
105
                'line 3\n'
 
106
                'line 2\n'
 
107
                '=======\n'
 
108
                'line 2 to 2.1\n'
 
109
                'line 3\n'
 
110
                '>>>>>>> MERGE-SOURCE\n'
 
111
                'line 4\n', 'this/file1')
 
112
        else:
 
113
            self.assertFileEqual('line 1\n'
 
114
                '<<<<<<< TREE\n'
 
115
                'line 3\n'
 
116
                'line 2\n'
 
117
                '=======\n'
 
118
                'line 2 to 2.1\n'
 
119
                'line 3\n'
 
120
                '>>>>>>> MERGE-SOURCE\n'
 
121
                'line 4\n', 'this/file1')
 
122
 
 
123
    def test_modify_conflicts_with_delete(self):
 
124
        # If one side deletes a line, and the other modifies that line, then
 
125
        # the modification should be considered a conflict
 
126
        builder = self.make_branch_builder('test')
 
127
        builder.start_series()
 
128
        builder.build_snapshot('BASE-id', None,
 
129
            [('add', ('', None, 'directory', None)),
 
130
             ('add', ('foo', 'foo-id', 'file', 'a\nb\nc\nd\ne\n')),
 
131
            ])
 
132
        # Delete 'b\n'
 
133
        builder.build_snapshot('OTHER-id', ['BASE-id'],
 
134
            [('modify', ('foo-id', 'a\nc\nd\ne\n'))])
 
135
        # Modify 'b\n', add 'X\n'
 
136
        builder.build_snapshot('THIS-id', ['BASE-id'],
 
137
            [('modify', ('foo-id', 'a\nb2\nc\nd\nX\ne\n'))])
 
138
        builder.finish_series()
 
139
        branch = builder.get_branch()
 
140
        this_tree = branch.bzrdir.create_workingtree()
 
141
        this_tree.lock_write()
 
142
        self.addCleanup(this_tree.unlock)
 
143
        other_tree = this_tree.bzrdir.sprout('other',
 
144
                                             'OTHER-id').open_workingtree()
 
145
        self.do_merge(this_tree, other_tree)
 
146
        if self.merge_type is _mod_merge.LCAMerger:
 
147
            self.expectFailure("lca merge doesn't track deleted lines",
 
148
                self.assertFileEqual,
 
149
                    'a\n'
 
150
                    '<<<<<<< TREE\n'
 
151
                    'b2\n'
 
152
                    '=======\n'
 
153
                    '>>>>>>> MERGE-SOURCE\n'
 
154
                    'c\n'
 
155
                    'd\n'
 
156
                    'X\n'
 
157
                    'e\n', 'test/foo')
 
158
        else:
 
159
            self.assertFileEqual(
 
160
                'a\n'
 
161
                '<<<<<<< TREE\n'
 
162
                'b2\n'
 
163
                '=======\n'
 
164
                '>>>>>>> MERGE-SOURCE\n'
 
165
                'c\n'
 
166
                'd\n'
 
167
                'X\n'
 
168
                'e\n', 'test/foo')
 
169
 
 
170
    def get_limbodir_deletiondir(self, wt):
 
171
        transform = TreeTransform(wt)
 
172
        limbodir = transform._limbodir
 
173
        deletiondir = transform._deletiondir
 
174
        transform.finalize()
 
175
        return (limbodir, deletiondir)
 
176
 
 
177
    def test_merge_with_existing_limbo_empty(self):
 
178
        """Empty limbo dir is just cleaned up - see bug 427773"""
 
179
        wt = self.make_branch_and_tree('this')
 
180
        (limbodir, deletiondir) =  self.get_limbodir_deletiondir(wt)
 
181
        os.mkdir(limbodir)
 
182
        self.do_merge(wt, wt)
 
183
 
 
184
    def test_merge_with_existing_limbo_non_empty(self):
 
185
        wt = self.make_branch_and_tree('this')
 
186
        (limbodir, deletiondir) =  self.get_limbodir_deletiondir(wt)
 
187
        os.mkdir(limbodir)
 
188
        os.mkdir(os.path.join(limbodir, 'something'))
 
189
        self.assertRaises(errors.ExistingLimbo, self.do_merge, wt, wt)
 
190
        self.assertRaises(errors.LockError, wt.unlock)
 
191
 
 
192
    def test_merge_with_pending_deletion_empty(self):
 
193
        wt = self.make_branch_and_tree('this')
 
194
        (limbodir, deletiondir) =  self.get_limbodir_deletiondir(wt)
 
195
        os.mkdir(deletiondir)
 
196
        self.do_merge(wt, wt)
 
197
 
 
198
    def test_merge_with_pending_deletion_non_empty(self):
 
199
        """Also see bug 427773"""
 
200
        wt = self.make_branch_and_tree('this')
 
201
        (limbodir, deletiondir) =  self.get_limbodir_deletiondir(wt)
 
202
        os.mkdir(deletiondir)
 
203
        os.mkdir(os.path.join(deletiondir, 'something'))
 
204
        self.assertRaises(errors.ExistingPendingDeletion, self.do_merge, wt, wt)
 
205
        self.assertRaises(errors.LockError, wt.unlock)
 
206
 
 
207
 
 
208
class TestHookMergeFileContent(TestCaseWithTransport):
 
209
    """Tests that the 'merge_file_content' hook is invoked."""
 
210
 
 
211
    def setUp(self):
 
212
        TestCaseWithTransport.setUp(self)
 
213
        self.hook_log = []
 
214
 
 
215
    def install_hook_inactive(self):
 
216
        def inactive_factory(merger):
 
217
            # This hook is never active
 
218
            self.hook_log.append(('inactive',))
 
219
            return None
 
220
        _mod_merge.Merger.hooks.install_named_hook(
 
221
            'merge_file_content', inactive_factory, 'test hook (inactive)')
 
222
 
 
223
    def install_hook_noop(self):
 
224
        test = self
 
225
        class HookNA(_mod_merge.AbstractPerFileMerger):
 
226
            def merge_contents(self, merge_params):
 
227
                # This hook unconditionally does nothing.
 
228
                test.hook_log.append(('no-op',))
 
229
                return 'not_applicable', None
 
230
        def hook_na_factory(merger):
 
231
            return HookNA(merger)
 
232
        _mod_merge.Merger.hooks.install_named_hook(
 
233
            'merge_file_content', hook_na_factory, 'test hook (no-op)')
 
234
 
 
235
    def install_hook_success(self):
 
236
        test = self
 
237
        class HookSuccess(_mod_merge.AbstractPerFileMerger):
 
238
            def merge_contents(self, merge_params):
 
239
                test.hook_log.append(('success',))
 
240
                if merge_params.file_id == '1':
 
241
                    return 'success', ['text-merged-by-hook']
 
242
                return 'not_applicable', None
 
243
        def hook_success_factory(merger):
 
244
            return HookSuccess(merger)
 
245
        _mod_merge.Merger.hooks.install_named_hook(
 
246
            'merge_file_content', hook_success_factory, 'test hook (success)')
 
247
 
 
248
    def install_hook_conflict(self):
 
249
        test = self
 
250
        class HookConflict(_mod_merge.AbstractPerFileMerger):
 
251
            def merge_contents(self, merge_params):
 
252
                test.hook_log.append(('conflict',))
 
253
                if merge_params.file_id == '1':
 
254
                    return ('conflicted',
 
255
                        ['text-with-conflict-markers-from-hook'])
 
256
                return 'not_applicable', None
 
257
        def hook_conflict_factory(merger):
 
258
            return HookConflict(merger)
 
259
        _mod_merge.Merger.hooks.install_named_hook(
 
260
            'merge_file_content', hook_conflict_factory, 'test hook (delete)')
 
261
 
 
262
    def install_hook_delete(self):
 
263
        test = self
 
264
        class HookDelete(_mod_merge.AbstractPerFileMerger):
 
265
            def merge_contents(self, merge_params):
 
266
                test.hook_log.append(('delete',))
 
267
                if merge_params.file_id == '1':
 
268
                    return 'delete', None
 
269
                return 'not_applicable', None
 
270
        def hook_delete_factory(merger):
 
271
            return HookDelete(merger)
 
272
        _mod_merge.Merger.hooks.install_named_hook(
 
273
            'merge_file_content', hook_delete_factory, 'test hook (delete)')
 
274
 
 
275
    def install_hook_log_lines(self):
 
276
        """Install a hook that saves the get_lines for the this, base and other
 
277
        versions of the file.
 
278
        """
 
279
        test = self
 
280
        class HookLogLines(_mod_merge.AbstractPerFileMerger):
 
281
            def merge_contents(self, merge_params):
 
282
                test.hook_log.append((
 
283
                    'log_lines',
 
284
                    merge_params.this_lines,
 
285
                    merge_params.other_lines,
 
286
                    merge_params.base_lines,
 
287
                    ))
 
288
                return 'not_applicable', None
 
289
        def hook_log_lines_factory(merger):
 
290
            return HookLogLines(merger)
 
291
        _mod_merge.Merger.hooks.install_named_hook(
 
292
            'merge_file_content', hook_log_lines_factory,
 
293
            'test hook (log_lines)')
 
294
 
 
295
    def make_merge_builder(self):
 
296
        builder = MergeBuilder(self.test_base_dir)
 
297
        self.addCleanup(builder.cleanup)
 
298
        return builder
 
299
 
 
300
    def create_file_needing_contents_merge(self, builder, file_id):
 
301
        builder.add_file(file_id, builder.tree_root, "name1", "text1", True)
 
302
        builder.change_contents(file_id, other="text4", this="text3")
 
303
 
 
304
    def test_change_vs_change(self):
 
305
        """Hook is used for (changed, changed)"""
 
306
        self.install_hook_success()
 
307
        builder = self.make_merge_builder()
 
308
        builder.add_file("1", builder.tree_root, "name1", "text1", True)
 
309
        builder.change_contents("1", other="text4", this="text3")
 
310
        conflicts = builder.merge(self.merge_type)
 
311
        self.assertEqual(conflicts, [])
 
312
        self.assertEqual(
 
313
            builder.this.get_file('1').read(), 'text-merged-by-hook')
 
314
 
 
315
    def test_change_vs_deleted(self):
 
316
        """Hook is used for (changed, deleted)"""
 
317
        self.install_hook_success()
 
318
        builder = self.make_merge_builder()
 
319
        builder.add_file("1", builder.tree_root, "name1", "text1", True)
 
320
        builder.change_contents("1", this="text2")
 
321
        builder.remove_file("1", other=True)
 
322
        conflicts = builder.merge(self.merge_type)
 
323
        self.assertEqual(conflicts, [])
 
324
        self.assertEqual(
 
325
            builder.this.get_file('1').read(), 'text-merged-by-hook')
 
326
 
 
327
    def test_result_can_be_delete(self):
 
328
        """A hook's result can be the deletion of a file."""
 
329
        self.install_hook_delete()
 
330
        builder = self.make_merge_builder()
 
331
        self.create_file_needing_contents_merge(builder, "1")
 
332
        conflicts = builder.merge(self.merge_type)
 
333
        self.assertEqual(conflicts, [])
 
334
        self.assertRaises(errors.NoSuchId, builder.this.id2path, '1')
 
335
        self.assertEqual([], list(builder.this.list_files()))
 
336
 
 
337
    def test_result_can_be_conflict(self):
 
338
        """A hook's result can be a conflict."""
 
339
        self.install_hook_conflict()
 
340
        builder = self.make_merge_builder()
 
341
        self.create_file_needing_contents_merge(builder, "1")
 
342
        conflicts = builder.merge(self.merge_type)
 
343
        self.assertEqual(conflicts, [TextConflict('name1', file_id='1')])
 
344
        # The hook still gets to set the file contents in this case, so that it
 
345
        # can insert custom conflict markers.
 
346
        self.assertEqual(
 
347
            builder.this.get_file('1').read(),
 
348
            'text-with-conflict-markers-from-hook')
 
349
 
 
350
    def test_can_access_this_other_and_base_versions(self):
 
351
        """The hook function can call params.merger.get_lines to access the
 
352
        THIS/OTHER/BASE versions of the file.
 
353
        """
 
354
        self.install_hook_log_lines()
 
355
        builder = self.make_merge_builder()
 
356
        builder.add_file("1", builder.tree_root, "name1", "text1", True)
 
357
        builder.change_contents("1", this="text2", other="text3")
 
358
        conflicts = builder.merge(self.merge_type)
 
359
        self.assertEqual(
 
360
            [('log_lines', ['text2'], ['text3'], ['text1'])], self.hook_log)
 
361
 
 
362
    def test_chain_when_not_active(self):
 
363
        """When a hook function returns None, merging still works."""
 
364
        self.install_hook_inactive()
 
365
        self.install_hook_success()
 
366
        builder = self.make_merge_builder()
 
367
        self.create_file_needing_contents_merge(builder, "1")
 
368
        conflicts = builder.merge(self.merge_type)
 
369
        self.assertEqual(conflicts, [])
 
370
        self.assertEqual(
 
371
            builder.this.get_file('1').read(), 'text-merged-by-hook')
 
372
        self.assertEqual([('inactive',), ('success',)], self.hook_log)
 
373
 
 
374
    def test_chain_when_not_applicable(self):
 
375
        """When a hook function returns not_applicable, the next function is
 
376
        tried (when one exists).
 
377
        """
 
378
        self.install_hook_noop()
 
379
        self.install_hook_success()
 
380
        builder = self.make_merge_builder()
 
381
        self.create_file_needing_contents_merge(builder, "1")
 
382
        conflicts = builder.merge(self.merge_type)
 
383
        self.assertEqual(conflicts, [])
 
384
        self.assertEqual(
 
385
            builder.this.get_file('1').read(), 'text-merged-by-hook')
 
386
        self.assertEqual([('no-op',), ('success',)], self.hook_log)
 
387
 
 
388
    def test_chain_stops_after_success(self):
 
389
        """When a hook function returns success, no later functions are tried.
 
390
        """
 
391
        self.install_hook_success()
 
392
        self.install_hook_noop()
 
393
        builder = self.make_merge_builder()
 
394
        self.create_file_needing_contents_merge(builder, "1")
 
395
        conflicts = builder.merge(self.merge_type)
 
396
        self.assertEqual([('success',)], self.hook_log)
 
397
 
 
398
    def test_chain_stops_after_conflict(self):
 
399
        """When a hook function returns conflict, no later functions are tried.
 
400
        """
 
401
        self.install_hook_conflict()
 
402
        self.install_hook_noop()
 
403
        builder = self.make_merge_builder()
 
404
        self.create_file_needing_contents_merge(builder, "1")
 
405
        conflicts = builder.merge(self.merge_type)
 
406
        self.assertEqual([('conflict',)], self.hook_log)
 
407
 
 
408
    def test_chain_stops_after_delete(self):
 
409
        """When a hook function returns delete, no later functions are tried.
 
410
        """
 
411
        self.install_hook_delete()
 
412
        self.install_hook_noop()
 
413
        builder = self.make_merge_builder()
 
414
        self.create_file_needing_contents_merge(builder, "1")
 
415
        conflicts = builder.merge(self.merge_type)
 
416
        self.assertEqual([('delete',)], self.hook_log)
 
417