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