~bzr-pqm/bzr/bzr.dev

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