~bzr-pqm/bzr/bzr.dev

« back to all changes in this revision

Viewing changes to bzrlib/tests/test_conflicts.py

  • Committer: Canonical.com Patch Queue Manager
  • Date: 2010-11-11 08:45:19 UTC
  • mfrom: (4597.9.22 reports-conflict-resolved)
  • Revision ID: pqm@pqm.ubuntu.com-20101111084519-bmk1zmblp7kex41a
(vila) More feedback about the conflicts just resolved and the remaining
 ones. (Vincent Ladeuil)

Show diffs side-by-side

added added

removed removed

Lines of Context:
 
1
# Copyright (C) 2005-2010 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
 
 
18
import os
 
19
 
 
20
from bzrlib import (
 
21
    branchbuilder,
 
22
    bzrdir,
 
23
    conflicts,
 
24
    errors,
 
25
    option,
 
26
    tests,
 
27
    workingtree,
 
28
    )
 
29
from bzrlib.tests import (
 
30
    script,
 
31
    scenarios,
 
32
    )
 
33
 
 
34
 
 
35
load_tests = scenarios.load_tests_apply_scenarios
 
36
 
 
37
 
 
38
# TODO: Test commit with some added, and added-but-missing files
 
39
# RBC 20060124 is that not tested in test_commit.py ?
 
40
 
 
41
# The order of 'path' here is important - do not let it
 
42
# be a sorted list.
 
43
# u'\xe5' == a with circle
 
44
# '\xc3\xae' == u'\xee' == i with hat
 
45
# So these are u'path' and 'id' only with a circle and a hat. (shappo?)
 
46
example_conflicts = conflicts.ConflictList(
 
47
    [conflicts.MissingParent('Not deleting', u'p\xe5thg', '\xc3\xaedg'),
 
48
     conflicts.ContentsConflict(u'p\xe5tha', None, '\xc3\xaeda'),
 
49
     conflicts.TextConflict(u'p\xe5tha'),
 
50
     conflicts.PathConflict(u'p\xe5thb', u'p\xe5thc', '\xc3\xaedb'),
 
51
     conflicts.DuplicateID('Unversioned existing file',
 
52
                           u'p\xe5thc', u'p\xe5thc2',
 
53
                           '\xc3\xaedc', '\xc3\xaedc'),
 
54
    conflicts.DuplicateEntry('Moved existing file to',
 
55
                             u'p\xe5thdd.moved', u'p\xe5thd',
 
56
                             '\xc3\xaedd', None),
 
57
    conflicts.ParentLoop('Cancelled move', u'p\xe5the', u'p\xe5th2e',
 
58
                         None, '\xc3\xaed2e'),
 
59
    conflicts.UnversionedParent('Versioned directory',
 
60
                                u'p\xe5thf', '\xc3\xaedf'),
 
61
    conflicts.NonDirectoryParent('Created directory',
 
62
                                 u'p\xe5thg', '\xc3\xaedg'),
 
63
])
 
64
 
 
65
 
 
66
class TestConflicts(tests.TestCaseWithTransport):
 
67
 
 
68
    def test_conflicts(self):
 
69
        """Conflicts are detected properly"""
 
70
        # Use BzrDirFormat6 so we can fake conflicts
 
71
        tree = self.make_branch_and_tree('.', format=bzrdir.BzrDirFormat6())
 
72
        self.build_tree_contents([('hello', 'hello world4'),
 
73
                                  ('hello.THIS', 'hello world2'),
 
74
                                  ('hello.BASE', 'hello world1'),
 
75
                                  ('hello.OTHER', 'hello world3'),
 
76
                                  ('hello.sploo.BASE', 'yellowworld'),
 
77
                                  ('hello.sploo.OTHER', 'yellowworld2'),
 
78
                                  ])
 
79
        tree.lock_read()
 
80
        self.assertLength(6, list(tree.list_files()))
 
81
        tree.unlock()
 
82
        tree_conflicts = tree.conflicts()
 
83
        self.assertLength(2, tree_conflicts)
 
84
        self.assertTrue('hello' in tree_conflicts[0].path)
 
85
        self.assertTrue('hello.sploo' in tree_conflicts[1].path)
 
86
        conflicts.restore('hello')
 
87
        conflicts.restore('hello.sploo')
 
88
        self.assertLength(0, tree.conflicts())
 
89
        self.assertFileEqual('hello world2', 'hello')
 
90
        self.assertFalse(os.path.lexists('hello.sploo'))
 
91
        self.assertRaises(errors.NotConflicted, conflicts.restore, 'hello')
 
92
        self.assertRaises(errors.NotConflicted,
 
93
                          conflicts.restore, 'hello.sploo')
 
94
 
 
95
    def test_resolve_conflict_dir(self):
 
96
        tree = self.make_branch_and_tree('.')
 
97
        self.build_tree_contents([('hello', 'hello world4'),
 
98
                                  ('hello.THIS', 'hello world2'),
 
99
                                  ('hello.BASE', 'hello world1'),
 
100
                                  ])
 
101
        os.mkdir('hello.OTHER')
 
102
        tree.add('hello', 'q')
 
103
        l = conflicts.ConflictList([conflicts.TextConflict('hello')])
 
104
        l.remove_files(tree)
 
105
 
 
106
    def test_select_conflicts(self):
 
107
        tree = self.make_branch_and_tree('.')
 
108
        clist = conflicts.ConflictList
 
109
 
 
110
        def check_select(not_selected, selected, paths, **kwargs):
 
111
            self.assertEqual(
 
112
                (not_selected, selected),
 
113
                tree_conflicts.select_conflicts(tree, paths, **kwargs))
 
114
 
 
115
        foo = conflicts.ContentsConflict('foo')
 
116
        bar = conflicts.ContentsConflict('bar')
 
117
        tree_conflicts = clist([foo, bar])
 
118
 
 
119
        check_select(clist([bar]), clist([foo]), ['foo'])
 
120
        check_select(clist(), tree_conflicts,
 
121
                     [''], ignore_misses=True, recurse=True)
 
122
 
 
123
        foobaz  = conflicts.ContentsConflict('foo/baz')
 
124
        tree_conflicts = clist([foobaz, bar])
 
125
 
 
126
        check_select(clist([bar]), clist([foobaz]),
 
127
                     ['foo'], ignore_misses=True, recurse=True)
 
128
 
 
129
        qux = conflicts.PathConflict('qux', 'foo/baz')
 
130
        tree_conflicts = clist([qux])
 
131
 
 
132
        check_select(clist(), tree_conflicts,
 
133
                     ['foo'], ignore_misses=True, recurse=True)
 
134
        check_select (tree_conflicts, clist(), ['foo'], ignore_misses=True)
 
135
 
 
136
    def test_resolve_conflicts_recursive(self):
 
137
        tree = self.make_branch_and_tree('.')
 
138
        self.build_tree(['dir/', 'dir/hello'])
 
139
        tree.add(['dir', 'dir/hello'])
 
140
 
 
141
        dirhello = conflicts.ConflictList([conflicts.TextConflict('dir/hello')])
 
142
        tree.set_conflicts(dirhello)
 
143
 
 
144
        conflicts.resolve(tree, ['dir'], recursive=False, ignore_misses=True)
 
145
        self.assertEqual(dirhello, tree.conflicts())
 
146
 
 
147
        conflicts.resolve(tree, ['dir'], recursive=True, ignore_misses=True)
 
148
        self.assertEqual(conflicts.ConflictList([]), tree.conflicts())
 
149
 
 
150
 
 
151
class TestConflictStanzas(tests.TestCase):
 
152
 
 
153
    def test_stanza_roundtrip(self):
 
154
        # write and read our example stanza.
 
155
        stanza_iter = example_conflicts.to_stanzas()
 
156
        processed = conflicts.ConflictList.from_stanzas(stanza_iter)
 
157
        for o, p in zip(processed, example_conflicts):
 
158
            self.assertEqual(o, p)
 
159
 
 
160
            self.assertIsInstance(o.path, unicode)
 
161
 
 
162
            if o.file_id is not None:
 
163
                self.assertIsInstance(o.file_id, str)
 
164
 
 
165
            conflict_path = getattr(o, 'conflict_path', None)
 
166
            if conflict_path is not None:
 
167
                self.assertIsInstance(conflict_path, unicode)
 
168
 
 
169
            conflict_file_id = getattr(o, 'conflict_file_id', None)
 
170
            if conflict_file_id is not None:
 
171
                self.assertIsInstance(conflict_file_id, str)
 
172
 
 
173
    def test_stanzification(self):
 
174
        for stanza in example_conflicts.to_stanzas():
 
175
            if 'file_id' in stanza:
 
176
                # In Stanza form, the file_id has to be unicode.
 
177
                self.assertStartsWith(stanza['file_id'], u'\xeed')
 
178
            self.assertStartsWith(stanza['path'], u'p\xe5th')
 
179
            if 'conflict_path' in stanza:
 
180
                self.assertStartsWith(stanza['conflict_path'], u'p\xe5th')
 
181
            if 'conflict_file_id' in stanza:
 
182
                self.assertStartsWith(stanza['conflict_file_id'], u'\xeed')
 
183
 
 
184
 
 
185
# FIXME: The shell-like tests should be converted to real whitebox tests... or
 
186
# moved to a blackbox module -- vila 20100205
 
187
 
 
188
# FIXME: test missing for multiple conflicts
 
189
 
 
190
# FIXME: Tests missing for DuplicateID conflict type
 
191
class TestResolveConflicts(script.TestCaseWithTransportAndScript):
 
192
 
 
193
    preamble = None # The setup script set by daughter classes
 
194
 
 
195
    def setUp(self):
 
196
        super(TestResolveConflicts, self).setUp()
 
197
        self.run_script(self.preamble)
 
198
 
 
199
 
 
200
class TestResolveTextConflicts(TestResolveConflicts):
 
201
    # TBC
 
202
    pass
 
203
 
 
204
 
 
205
def mirror_scenarios(base_scenarios):
 
206
    """Return a list of mirrored scenarios.
 
207
 
 
208
    Each scenario in base_scenarios is duplicated switching the roles of 'this'
 
209
    and 'other'
 
210
    """
 
211
    scenarios = []
 
212
    for common, (lname, ldict), (rname, rdict) in base_scenarios:
 
213
        a = tests.multiply_scenarios([(lname, dict(_this=ldict))],
 
214
                                     [(rname, dict(_other=rdict))])
 
215
        b = tests.multiply_scenarios([(rname, dict(_this=rdict))],
 
216
                                     [(lname, dict(_other=ldict))])
 
217
        # Inject the common parameters in all scenarios
 
218
        for name, d in a + b:
 
219
            d.update(common)
 
220
        scenarios.extend(a + b)
 
221
    return scenarios
 
222
 
 
223
 
 
224
# FIXME: Get rid of parametrized (in the class name) once we delete
 
225
# TestResolveConflicts -- vila 20100308
 
226
class TestParametrizedResolveConflicts(tests.TestCaseWithTransport):
 
227
    """This class provides a base to test single conflict resolution.
 
228
 
 
229
    Since all conflict objects are created with specific semantics for their
 
230
    attributes, each class should implement the necessary functions and
 
231
    attributes described below.
 
232
 
 
233
    Each class should define the scenarios that create the expected (single)
 
234
    conflict.
 
235
 
 
236
    Each scenario describes:
 
237
    * how to create 'base' tree (and revision)
 
238
    * how to create 'left' tree (and revision, parent rev 'base')
 
239
    * how to create 'right' tree (and revision, parent rev 'base')
 
240
    * how to check that changes in 'base'->'left' have been taken
 
241
    * how to check that changes in 'base'->'right' have been taken
 
242
 
 
243
    From each base scenario, we generate two concrete scenarios where:
 
244
    * this=left, other=right
 
245
    * this=right, other=left
 
246
 
 
247
    Then the test case verifies each concrete scenario by:
 
248
    * creating a branch containing the 'base', 'this' and 'other' revisions
 
249
    * creating a working tree for the 'this' revision
 
250
    * performing the merge of 'other' into 'this'
 
251
    * verifying the expected conflict was generated
 
252
    * resolving with --take-this or --take-other, and running the corresponding
 
253
      checks (for either 'base'->'this', or 'base'->'other')
 
254
 
 
255
    :cvar _conflict_type: The expected class of the generated conflict.
 
256
 
 
257
    :cvar _assert_conflict: A method receiving the working tree and the
 
258
        conflict object and checking its attributes.
 
259
 
 
260
    :cvar _base_actions: The branchbuilder actions to create the 'base'
 
261
        revision.
 
262
 
 
263
    :cvar _this: The dict related to 'base' -> 'this'. It contains at least:
 
264
      * 'actions': The branchbuilder actions to create the 'this'
 
265
          revision.
 
266
      * 'check': how to check the changes after resolution with --take-this.
 
267
 
 
268
    :cvar _other: The dict related to 'base' -> 'other'. It contains at least:
 
269
      * 'actions': The branchbuilder actions to create the 'other'
 
270
          revision.
 
271
      * 'check': how to check the changes after resolution with --take-other.
 
272
    """
 
273
 
 
274
    # Set by daughter classes
 
275
    _conflict_type = None
 
276
    _assert_conflict = None
 
277
 
 
278
    # Set by load_tests
 
279
    _base_actions = None
 
280
    _this = None
 
281
    _other = None
 
282
 
 
283
    scenarios = []
 
284
    """The scenario list for the conflict type defined by the class.
 
285
 
 
286
    Each scenario is of the form:
 
287
    (common, (left_name, left_dict), (right_name, right_dict))
 
288
 
 
289
    * common is a dict
 
290
 
 
291
    * left_name and right_name are the scenario names that will be combined
 
292
 
 
293
    * left_dict and right_dict are the attributes specific to each half of
 
294
      the scenario. They should include at least 'actions' and 'check' and
 
295
      will be available as '_this' and '_other' test instance attributes.
 
296
 
 
297
    Daughters classes are free to add their specific attributes as they see
 
298
    fit in any of the three dicts.
 
299
 
 
300
    This is a class method so that load_tests can find it.
 
301
 
 
302
    '_base_actions' in the common dict, 'actions' and 'check' in the left
 
303
    and right dicts use names that map to methods in the test classes. Some
 
304
    prefixes are added to these names to get the correspong methods (see
 
305
    _get_actions() and _get_check()). The motivation here is to avoid
 
306
    collisions in the class namespace.
 
307
    """
 
308
 
 
309
    def setUp(self):
 
310
        super(TestParametrizedResolveConflicts, self).setUp()
 
311
        builder = self.make_branch_builder('trunk')
 
312
        builder.start_series()
 
313
 
 
314
        # Create an empty trunk
 
315
        builder.build_snapshot('start', None, [
 
316
                ('add', ('', 'root-id', 'directory', ''))])
 
317
        # Add a minimal base content
 
318
        base_actions = self._get_actions(self._base_actions)()
 
319
        builder.build_snapshot('base', ['start'], base_actions)
 
320
        # Modify the base content in branch
 
321
        actions_other = self._get_actions(self._other['actions'])()
 
322
        builder.build_snapshot('other', ['base'], actions_other)
 
323
        # Modify the base content in trunk
 
324
        actions_this = self._get_actions(self._this['actions'])()
 
325
        builder.build_snapshot('this', ['base'], actions_this)
 
326
        # builder.get_branch() tip is now 'this'
 
327
 
 
328
        builder.finish_series()
 
329
        self.builder = builder
 
330
 
 
331
    def _get_actions(self, name):
 
332
        return getattr(self, 'do_%s' % name)
 
333
 
 
334
    def _get_check(self, name):
 
335
        return getattr(self, 'check_%s' % name)
 
336
 
 
337
    def _merge_other_into_this(self):
 
338
        b = self.builder.get_branch()
 
339
        wt = b.bzrdir.sprout('branch').open_workingtree()
 
340
        wt.merge_from_branch(b, 'other')
 
341
        return wt
 
342
 
 
343
    def assertConflict(self, wt):
 
344
        confs = wt.conflicts()
 
345
        self.assertLength(1, confs)
 
346
        c = confs[0]
 
347
        self.assertIsInstance(c, self._conflict_type)
 
348
        self._assert_conflict(wt, c)
 
349
 
 
350
    def _get_resolve_path_arg(self, wt, action):
 
351
        raise NotImplementedError(self._get_resolve_path_arg)
 
352
 
 
353
    def check_resolved(self, wt, action):
 
354
        path = self._get_resolve_path_arg(wt, action)
 
355
        conflicts.resolve(wt, [path], action=action)
 
356
        # Check that we don't have any conflicts nor unknown left
 
357
        self.assertLength(0, wt.conflicts())
 
358
        self.assertLength(0, list(wt.unknowns()))
 
359
 
 
360
    def test_resolve_taking_this(self):
 
361
        wt = self._merge_other_into_this()
 
362
        self.assertConflict(wt)
 
363
        self.check_resolved(wt, 'take_this')
 
364
        check_this = self._get_check(self._this['check'])
 
365
        check_this()
 
366
 
 
367
    def test_resolve_taking_other(self):
 
368
        wt = self._merge_other_into_this()
 
369
        self.assertConflict(wt)
 
370
        self.check_resolved(wt, 'take_other')
 
371
        check_other = self._get_check(self._other['check'])
 
372
        check_other()
 
373
 
 
374
 
 
375
class TestResolveContentsConflict(TestParametrizedResolveConflicts):
 
376
 
 
377
    _conflict_type = conflicts.ContentsConflict
 
378
 
 
379
    # Set by the scenarios
 
380
    # path and file-id for the file involved in the conflict
 
381
    _path = None
 
382
    _file_id = None
 
383
 
 
384
    scenarios = mirror_scenarios(
 
385
        [
 
386
            # File modified/deleted
 
387
            (dict(_base_actions='create_file',
 
388
                  _path='file', _file_id='file-id'),
 
389
             ('file_modified',
 
390
              dict(actions='modify_file', check='file_has_more_content')),
 
391
             ('file_deleted',
 
392
              dict(actions='delete_file', check='file_doesnt_exist')),),
 
393
            ])
 
394
 
 
395
    def do_create_file(self):
 
396
        return [('add', ('file', 'file-id', 'file', 'trunk content\n'))]
 
397
 
 
398
    def do_modify_file(self):
 
399
        return [('modify', ('file-id', 'trunk content\nmore content\n'))]
 
400
 
 
401
    def check_file_has_more_content(self):
 
402
        self.assertFileEqual('trunk content\nmore content\n', 'branch/file')
 
403
 
 
404
    def do_delete_file(self):
 
405
        return [('unversion', 'file-id')]
 
406
 
 
407
    def check_file_doesnt_exist(self):
 
408
        self.failIfExists('branch/file')
 
409
 
 
410
    def _get_resolve_path_arg(self, wt, action):
 
411
        return self._path
 
412
 
 
413
    def assertContentsConflict(self, wt, c):
 
414
        self.assertEqual(self._file_id, c.file_id)
 
415
        self.assertEqual(self._path, c.path)
 
416
    _assert_conflict = assertContentsConflict
 
417
 
 
418
 
 
419
class TestResolvePathConflict(TestParametrizedResolveConflicts):
 
420
 
 
421
    _conflict_type = conflicts.PathConflict
 
422
 
 
423
    def do_nothing(self):
 
424
        return []
 
425
 
 
426
    # Each side dict additionally defines:
 
427
    # - path path involved (can be '<deleted>')
 
428
    # - file-id involved
 
429
    scenarios = mirror_scenarios(
 
430
        [
 
431
            # File renamed/deleted
 
432
            (dict(_base_actions='create_file'),
 
433
             ('file_renamed',
 
434
              dict(actions='rename_file', check='file_renamed',
 
435
                   path='new-file', file_id='file-id')),
 
436
             ('file_deleted',
 
437
              dict(actions='delete_file', check='file_doesnt_exist',
 
438
                   # PathConflicts deletion handling requires a special
 
439
                   # hard-coded value
 
440
                   path='<deleted>', file_id='file-id')),),
 
441
            # File renamed/renamed differently
 
442
            (dict(_base_actions='create_file'),
 
443
             ('file_renamed',
 
444
              dict(actions='rename_file', check='file_renamed',
 
445
                   path='new-file', file_id='file-id')),
 
446
             ('file_renamed2',
 
447
              dict(actions='rename_file2', check='file_renamed2',
 
448
                   path='new-file2', file_id='file-id')),),
 
449
            # Dir renamed/deleted
 
450
            (dict(_base_actions='create_dir'),
 
451
             ('dir_renamed',
 
452
              dict(actions='rename_dir', check='dir_renamed',
 
453
                   path='new-dir', file_id='dir-id')),
 
454
             ('dir_deleted',
 
455
              dict(actions='delete_dir', check='dir_doesnt_exist',
 
456
                   # PathConflicts deletion handling requires a special
 
457
                   # hard-coded value
 
458
                   path='<deleted>', file_id='dir-id')),),
 
459
            # Dir renamed/renamed differently
 
460
            (dict(_base_actions='create_dir'),
 
461
             ('dir_renamed',
 
462
              dict(actions='rename_dir', check='dir_renamed',
 
463
                   path='new-dir', file_id='dir-id')),
 
464
             ('dir_renamed2',
 
465
              dict(actions='rename_dir2', check='dir_renamed2',
 
466
                   path='new-dir2', file_id='dir-id')),),
 
467
            ])
 
468
 
 
469
    def do_create_file(self):
 
470
        return [('add', ('file', 'file-id', 'file', 'trunk content\n'))]
 
471
 
 
472
    def do_create_dir(self):
 
473
        return [('add', ('dir', 'dir-id', 'directory', ''))]
 
474
 
 
475
    def do_rename_file(self):
 
476
        return [('rename', ('file', 'new-file'))]
 
477
 
 
478
    def check_file_renamed(self):
 
479
        self.failIfExists('branch/file')
 
480
        self.failUnlessExists('branch/new-file')
 
481
 
 
482
    def do_rename_file2(self):
 
483
        return [('rename', ('file', 'new-file2'))]
 
484
 
 
485
    def check_file_renamed2(self):
 
486
        self.failIfExists('branch/file')
 
487
        self.failUnlessExists('branch/new-file2')
 
488
 
 
489
    def do_rename_dir(self):
 
490
        return [('rename', ('dir', 'new-dir'))]
 
491
 
 
492
    def check_dir_renamed(self):
 
493
        self.failIfExists('branch/dir')
 
494
        self.failUnlessExists('branch/new-dir')
 
495
 
 
496
    def do_rename_dir2(self):
 
497
        return [('rename', ('dir', 'new-dir2'))]
 
498
 
 
499
    def check_dir_renamed2(self):
 
500
        self.failIfExists('branch/dir')
 
501
        self.failUnlessExists('branch/new-dir2')
 
502
 
 
503
    def do_delete_file(self):
 
504
        return [('unversion', 'file-id')]
 
505
 
 
506
    def check_file_doesnt_exist(self):
 
507
        self.failIfExists('branch/file')
 
508
 
 
509
    def do_delete_dir(self):
 
510
        return [('unversion', 'dir-id')]
 
511
 
 
512
    def check_dir_doesnt_exist(self):
 
513
        self.failIfExists('branch/dir')
 
514
 
 
515
    def _get_resolve_path_arg(self, wt, action):
 
516
        tpath = self._this['path']
 
517
        opath = self._other['path']
 
518
        if tpath == '<deleted>':
 
519
            path = opath
 
520
        else:
 
521
            path = tpath
 
522
        return path
 
523
 
 
524
    def assertPathConflict(self, wt, c):
 
525
        tpath = self._this['path']
 
526
        tfile_id = self._this['file_id']
 
527
        opath = self._other['path']
 
528
        ofile_id = self._other['file_id']
 
529
        self.assertEqual(tfile_id, ofile_id) # Sanity check
 
530
        self.assertEqual(tfile_id, c.file_id)
 
531
        self.assertEqual(tpath, c.path)
 
532
        self.assertEqual(opath, c.conflict_path)
 
533
    _assert_conflict = assertPathConflict
 
534
 
 
535
 
 
536
class TestResolvePathConflictBefore531967(TestResolvePathConflict):
 
537
    """Same as TestResolvePathConflict but a specific conflict object.
 
538
    """
 
539
 
 
540
    def assertPathConflict(self, c):
 
541
        # We create a conflict object as it was created before the fix and
 
542
        # inject it into the working tree, the test will exercise the
 
543
        # compatibility code.
 
544
        old_c = conflicts.PathConflict('<deleted>', self._item_path,
 
545
                                       file_id=None)
 
546
        wt.set_conflicts(conflicts.ConflictList([old_c]))
 
547
 
 
548
 
 
549
class TestResolveDuplicateEntry(TestParametrizedResolveConflicts):
 
550
 
 
551
    _conflict_type = conflicts.DuplicateEntry
 
552
 
 
553
    scenarios = mirror_scenarios(
 
554
        [
 
555
            # File created with different file-ids
 
556
            (dict(_base_actions='nothing'),
 
557
             ('filea_created',
 
558
              dict(actions='create_file_a', check='file_content_a',
 
559
                   path='file', file_id='file-a-id')),
 
560
             ('fileb_created',
 
561
              dict(actions='create_file_b', check='file_content_b',
 
562
                   path='file', file_id='file-b-id')),),
 
563
            ])
 
564
 
 
565
    def do_nothing(self):
 
566
        return []
 
567
 
 
568
    def do_create_file_a(self):
 
569
        return [('add', ('file', 'file-a-id', 'file', 'file a content\n'))]
 
570
 
 
571
    def check_file_content_a(self):
 
572
        self.assertFileEqual('file a content\n', 'branch/file')
 
573
 
 
574
    def do_create_file_b(self):
 
575
        return [('add', ('file', 'file-b-id', 'file', 'file b content\n'))]
 
576
 
 
577
    def check_file_content_b(self):
 
578
        self.assertFileEqual('file b content\n', 'branch/file')
 
579
 
 
580
    def _get_resolve_path_arg(self, wt, action):
 
581
        return self._this['path']
 
582
 
 
583
    def assertDuplicateEntry(self, wt, c):
 
584
        tpath = self._this['path']
 
585
        tfile_id = self._this['file_id']
 
586
        opath = self._other['path']
 
587
        ofile_id = self._other['file_id']
 
588
        self.assertEqual(tpath, opath) # Sanity check
 
589
        self.assertEqual(tfile_id, c.file_id)
 
590
        self.assertEqual(tpath + '.moved', c.path)
 
591
        self.assertEqual(tpath, c.conflict_path)
 
592
    _assert_conflict = assertDuplicateEntry
 
593
 
 
594
 
 
595
class TestResolveUnversionedParent(TestResolveConflicts):
 
596
 
 
597
    # FIXME: Add the reverse tests: dir deleted in trunk, file added in branch
 
598
 
 
599
    # FIXME: While this *creates* UnversionedParent conflicts, this really only
 
600
    # tests MissingParent resolution :-/
 
601
    preamble = """
 
602
$ bzr init trunk
 
603
...
 
604
$ cd trunk
 
605
$ mkdir dir
 
606
$ bzr add -q dir
 
607
$ bzr commit -m 'Create trunk' -q
 
608
$ echo 'trunk content' >dir/file
 
609
$ bzr add -q dir/file
 
610
$ bzr commit -q -m 'Add dir/file in trunk'
 
611
$ bzr branch -q . -r 1 ../branch
 
612
$ cd ../branch
 
613
$ bzr rm dir -q
 
614
$ bzr commit -q -m 'Remove dir in branch'
 
615
$ bzr merge ../trunk
 
616
2>+N  dir/
 
617
2>+N  dir/file
 
618
2>Conflict adding files to dir.  Created directory.
 
619
2>Conflict because dir is not versioned, but has versioned children.  Versioned directory.
 
620
2>2 conflicts encountered.
 
621
"""
 
622
 
 
623
    def test_take_this(self):
 
624
        self.run_script("""
 
625
$ bzr rm -q dir  --force
 
626
$ bzr resolve dir
 
627
2>2 conflict(s) resolved, 0 remaining
 
628
$ bzr commit -q --strict -m 'No more conflicts nor unknown files'
 
629
""")
 
630
 
 
631
    def test_take_other(self):
 
632
        self.run_script("""
 
633
$ bzr resolve dir
 
634
2>2 conflict(s) resolved, 0 remaining
 
635
$ bzr commit -q --strict -m 'No more conflicts nor unknown files'
 
636
""")
 
637
 
 
638
 
 
639
class TestResolveMissingParent(TestResolveConflicts):
 
640
 
 
641
    preamble = """
 
642
$ bzr init trunk
 
643
...
 
644
$ cd trunk
 
645
$ mkdir dir
 
646
$ echo 'trunk content' >dir/file
 
647
$ bzr add -q
 
648
$ bzr commit -m 'Create trunk' -q
 
649
$ echo 'trunk content' >dir/file2
 
650
$ bzr add -q dir/file2
 
651
$ bzr commit -q -m 'Add dir/file2 in branch'
 
652
$ bzr branch -q . -r 1 ../branch
 
653
$ cd ../branch
 
654
$ bzr rm -q dir/file --force
 
655
$ bzr rm -q dir
 
656
$ bzr commit -q -m 'Remove dir/file'
 
657
$ bzr merge ../trunk
 
658
2>+N  dir/
 
659
2>+N  dir/file2
 
660
2>Conflict adding files to dir.  Created directory.
 
661
2>Conflict because dir is not versioned, but has versioned children.  Versioned directory.
 
662
2>2 conflicts encountered.
 
663
"""
 
664
 
 
665
    def test_keep_them_all(self):
 
666
        self.run_script("""
 
667
$ bzr resolve dir
 
668
2>2 conflict(s) resolved, 0 remaining
 
669
$ bzr commit -q --strict -m 'No more conflicts nor unknown files'
 
670
""")
 
671
 
 
672
    def test_adopt_child(self):
 
673
        self.run_script("""
 
674
$ bzr mv -q dir/file2 file2
 
675
$ bzr rm -q dir --force
 
676
$ bzr resolve dir
 
677
2>2 conflict(s) resolved, 0 remaining
 
678
$ bzr commit -q --strict -m 'No more conflicts nor unknown files'
 
679
""")
 
680
 
 
681
    def test_kill_them_all(self):
 
682
        self.run_script("""
 
683
$ bzr rm -q dir --force
 
684
$ bzr resolve dir
 
685
2>2 conflict(s) resolved, 0 remaining
 
686
$ bzr commit -q --strict -m 'No more conflicts nor unknown files'
 
687
""")
 
688
 
 
689
    def test_resolve_taking_this(self):
 
690
        self.run_script("""
 
691
$ bzr resolve --take-this dir
 
692
2>...
 
693
$ bzr commit -q --strict -m 'No more conflicts nor unknown files'
 
694
""")
 
695
 
 
696
    def test_resolve_taking_other(self):
 
697
        self.run_script("""
 
698
$ bzr resolve --take-other dir
 
699
2>...
 
700
$ bzr commit -q --strict -m 'No more conflicts nor unknown files'
 
701
""")
 
702
 
 
703
 
 
704
class TestResolveDeletingParent(TestResolveConflicts):
 
705
 
 
706
    preamble = """
 
707
$ bzr init trunk
 
708
...
 
709
$ cd trunk
 
710
$ mkdir dir
 
711
$ echo 'trunk content' >dir/file
 
712
$ bzr add -q
 
713
$ bzr commit -m 'Create trunk' -q
 
714
$ bzr rm -q dir/file --force
 
715
$ bzr rm -q dir --force
 
716
$ bzr commit -q -m 'Remove dir/file'
 
717
$ bzr branch -q . -r 1 ../branch
 
718
$ cd ../branch
 
719
$ echo 'branch content' >dir/file2
 
720
$ bzr add -q dir/file2
 
721
$ bzr commit -q -m 'Add dir/file2 in branch'
 
722
$ bzr merge ../trunk
 
723
2>-D  dir/file
 
724
2>Conflict: can't delete dir because it is not empty.  Not deleting.
 
725
2>Conflict because dir is not versioned, but has versioned children.  Versioned directory.
 
726
2>2 conflicts encountered.
 
727
"""
 
728
 
 
729
    def test_keep_them_all(self):
 
730
        self.run_script("""
 
731
$ bzr resolve dir
 
732
2>2 conflict(s) resolved, 0 remaining
 
733
$ bzr commit -q --strict -m 'No more conflicts nor unknown files'
 
734
""")
 
735
 
 
736
    def test_adopt_child(self):
 
737
        self.run_script("""
 
738
$ bzr mv -q dir/file2 file2
 
739
$ bzr rm -q dir --force
 
740
$ bzr resolve dir
 
741
2>2 conflict(s) resolved, 0 remaining
 
742
$ bzr commit -q --strict -m 'No more conflicts nor unknown files'
 
743
""")
 
744
 
 
745
    def test_kill_them_all(self):
 
746
        self.run_script("""
 
747
$ bzr rm -q dir --force
 
748
$ bzr resolve dir
 
749
2>2 conflict(s) resolved, 0 remaining
 
750
$ bzr commit -q --strict -m 'No more conflicts nor unknown files'
 
751
""")
 
752
 
 
753
    def test_resolve_taking_this(self):
 
754
        self.run_script("""
 
755
$ bzr resolve --take-this dir
 
756
2>2 conflict(s) resolved, 0 remaining
 
757
$ bzr commit -q --strict -m 'No more conflicts nor unknown files'
 
758
""")
 
759
 
 
760
    def test_resolve_taking_other(self):
 
761
        self.run_script("""
 
762
$ bzr resolve --take-other dir
 
763
2>deleted dir/file2
 
764
2>deleted dir
 
765
2>2 conflict(s) resolved, 0 remaining
 
766
$ bzr commit -q --strict -m 'No more conflicts nor unknown files'
 
767
""")
 
768
 
 
769
 
 
770
class TestResolveParentLoop(TestParametrizedResolveConflicts):
 
771
 
 
772
    _conflict_type = conflicts.ParentLoop
 
773
 
 
774
    _this_args = None
 
775
    _other_args = None
 
776
 
 
777
    # Each side dict additionally defines:
 
778
    # - dir_id: the directory being moved
 
779
    # - target_id: The target directory
 
780
    # - xfail: whether the test is expected to fail if the action is
 
781
    #   involved as 'other'
 
782
    scenarios = mirror_scenarios(
 
783
        [
 
784
            # Dirs moved into each other
 
785
            (dict(_base_actions='create_dir1_dir2'),
 
786
             ('dir1_into_dir2',
 
787
              dict(actions='move_dir1_into_dir2', check='dir1_moved',
 
788
                   dir_id='dir1-id', target_id='dir2-id', xfail=False)),
 
789
             ('dir2_into_dir1',
 
790
              dict(actions='move_dir2_into_dir1', check='dir2_moved',
 
791
                   dir_id='dir2-id', target_id='dir1-id', xfail=False))),
 
792
            # Subdirs moved into each other
 
793
            (dict(_base_actions='create_dir1_4'),
 
794
             ('dir1_into_dir4',
 
795
              dict(actions='move_dir1_into_dir4', check='dir1_2_moved',
 
796
                   dir_id='dir1-id', target_id='dir4-id', xfail=True)),
 
797
             ('dir3_into_dir2',
 
798
              dict(actions='move_dir3_into_dir2', check='dir3_4_moved',
 
799
                   dir_id='dir3-id', target_id='dir2-id', xfail=True))),
 
800
            ])
 
801
 
 
802
    def do_create_dir1_dir2(self):
 
803
        return [('add', ('dir1', 'dir1-id', 'directory', '')),
 
804
                ('add', ('dir2', 'dir2-id', 'directory', '')),]
 
805
 
 
806
    def do_move_dir1_into_dir2(self):
 
807
        return [('rename', ('dir1', 'dir2/dir1'))]
 
808
 
 
809
    def check_dir1_moved(self):
 
810
        self.failIfExists('branch/dir1')
 
811
        self.failUnlessExists('branch/dir2/dir1')
 
812
 
 
813
    def do_move_dir2_into_dir1(self):
 
814
        return [('rename', ('dir2', 'dir1/dir2'))]
 
815
 
 
816
    def check_dir2_moved(self):
 
817
        self.failIfExists('branch/dir2')
 
818
        self.failUnlessExists('branch/dir1/dir2')
 
819
 
 
820
    def do_create_dir1_4(self):
 
821
        return [('add', ('dir1', 'dir1-id', 'directory', '')),
 
822
                ('add', ('dir1/dir2', 'dir2-id', 'directory', '')),
 
823
                ('add', ('dir3', 'dir3-id', 'directory', '')),
 
824
                ('add', ('dir3/dir4', 'dir4-id', 'directory', '')),]
 
825
 
 
826
    def do_move_dir1_into_dir4(self):
 
827
        return [('rename', ('dir1', 'dir3/dir4/dir1'))]
 
828
 
 
829
    def check_dir1_2_moved(self):
 
830
        self.failIfExists('branch/dir1')
 
831
        self.failUnlessExists('branch/dir3/dir4/dir1')
 
832
        self.failUnlessExists('branch/dir3/dir4/dir1/dir2')
 
833
 
 
834
    def do_move_dir3_into_dir2(self):
 
835
        return [('rename', ('dir3', 'dir1/dir2/dir3'))]
 
836
 
 
837
    def check_dir3_4_moved(self):
 
838
        self.failIfExists('branch/dir3')
 
839
        self.failUnlessExists('branch/dir1/dir2/dir3')
 
840
        self.failUnlessExists('branch/dir1/dir2/dir3/dir4')
 
841
 
 
842
    def _get_resolve_path_arg(self, wt, action):
 
843
        # ParentLoop says: moving <conflict_path> into <path>. Cancelled move.
 
844
        # But since <path> doesn't exist in the working tree, we need to use
 
845
        # <conflict_path> instead, and that, in turn, is given by dir_id. Pfew.
 
846
        return wt.id2path(self._other['dir_id'])
 
847
 
 
848
    def assertParentLoop(self, wt, c):
 
849
        self.assertEqual(self._other['dir_id'], c.file_id)
 
850
        self.assertEqual(self._other['target_id'], c.conflict_file_id)
 
851
        # The conflict paths are irrelevant (they are deterministic but not
 
852
        # worth checking since they don't provide the needed information
 
853
        # anyway)
 
854
        if self._other['xfail']:
 
855
            # It's a bit hackish to raise from here relying on being called for
 
856
            # both tests but this avoid overriding test_resolve_taking_other
 
857
            raise tests.KnownFailure(
 
858
                "ParentLoop doesn't carry enough info to resolve --take-other")
 
859
    _assert_conflict = assertParentLoop
 
860
 
 
861
 
 
862
class TestResolveNonDirectoryParent(TestResolveConflicts):
 
863
 
 
864
    preamble = """
 
865
$ bzr init trunk
 
866
...
 
867
$ cd trunk
 
868
$ bzr mkdir foo
 
869
...
 
870
$ bzr commit -m 'Create trunk' -q
 
871
$ echo "Boing" >foo/bar
 
872
$ bzr add -q foo/bar
 
873
$ bzr commit -q -m 'Add foo/bar'
 
874
$ bzr branch -q . -r 1 ../branch
 
875
$ cd ../branch
 
876
$ rm -r foo
 
877
$ echo "Boo!" >foo
 
878
$ bzr commit -q -m 'foo is now a file'
 
879
$ bzr merge ../trunk
 
880
2>+N  foo.new/bar
 
881
2>RK  foo => foo.new/
 
882
# FIXME: The message is misleading, foo.new *is* a directory when the message
 
883
# is displayed -- vila 090916
 
884
2>Conflict: foo.new is not a directory, but has files in it.  Created directory.
 
885
2>1 conflicts encountered.
 
886
"""
 
887
 
 
888
    def test_take_this(self):
 
889
        self.run_script("""
 
890
$ bzr rm -q foo.new --force
 
891
# FIXME: Isn't it weird that foo is now unkown even if foo.new has been put
 
892
# aside ? -- vila 090916
 
893
$ bzr add -q foo
 
894
$ bzr resolve foo.new
 
895
2>1 conflict(s) resolved, 0 remaining
 
896
$ bzr commit -q --strict -m 'No more conflicts nor unknown files'
 
897
""")
 
898
 
 
899
    def test_take_other(self):
 
900
        self.run_script("""
 
901
$ bzr rm -q foo --force
 
902
$ bzr mv -q foo.new foo
 
903
$ bzr resolve foo
 
904
2>1 conflict(s) resolved, 0 remaining
 
905
$ bzr commit -q --strict -m 'No more conflicts nor unknown files'
 
906
""")
 
907
 
 
908
    def test_resolve_taking_this(self):
 
909
        self.run_script("""
 
910
$ bzr resolve --take-this foo.new
 
911
2>...
 
912
$ bzr commit -q --strict -m 'No more conflicts nor unknown files'
 
913
""")
 
914
 
 
915
    def test_resolve_taking_other(self):
 
916
        self.run_script("""
 
917
$ bzr resolve --take-other foo.new
 
918
2>...
 
919
$ bzr commit -q --strict -m 'No more conflicts nor unknown files'
 
920
""")
 
921
 
 
922
 
 
923
class TestMalformedTransform(script.TestCaseWithTransportAndScript):
 
924
 
 
925
    def test_bug_430129(self):
 
926
        # This is nearly like TestResolveNonDirectoryParent but with branch and
 
927
        # trunk switched. As such it should certainly produce the same
 
928
        # conflict.
 
929
        self.run_script("""
 
930
$ bzr init trunk
 
931
...
 
932
$ cd trunk
 
933
$ bzr mkdir foo
 
934
...
 
935
$ bzr commit -m 'Create trunk' -q
 
936
$ rm -r foo
 
937
$ echo "Boo!" >foo
 
938
$ bzr commit -m 'foo is now a file' -q
 
939
$ bzr branch -q . -r 1 ../branch -q
 
940
$ cd ../branch
 
941
$ echo "Boing" >foo/bar
 
942
$ bzr add -q foo/bar -q
 
943
$ bzr commit -m 'Add foo/bar' -q
 
944
$ bzr merge ../trunk
 
945
2>bzr: ERROR: Tree transform is malformed [('unversioned executability', 'new-1')]
 
946
""")
 
947
 
 
948
 
 
949
class TestResolveActionOption(tests.TestCase):
 
950
 
 
951
    def setUp(self):
 
952
        super(TestResolveActionOption, self).setUp()
 
953
        self.options = [conflicts.ResolveActionOption()]
 
954
        self.parser = option.get_optparser(dict((o.name, o)
 
955
                                                for o in self.options))
 
956
 
 
957
    def parse(self, args):
 
958
        return self.parser.parse_args(args)
 
959
 
 
960
    def test_unknown_action(self):
 
961
        self.assertRaises(errors.BadOptionValue,
 
962
                          self.parse, ['--action', 'take-me-to-the-moon'])
 
963
 
 
964
    def test_done(self):
 
965
        opts, args = self.parse(['--action', 'done'])
 
966
        self.assertEqual({'action':'done'}, opts)
 
967
 
 
968
    def test_take_this(self):
 
969
        opts, args = self.parse(['--action', 'take-this'])
 
970
        self.assertEqual({'action': 'take_this'}, opts)
 
971
        opts, args = self.parse(['--take-this'])
 
972
        self.assertEqual({'action': 'take_this'}, opts)
 
973
 
 
974
    def test_take_other(self):
 
975
        opts, args = self.parse(['--action', 'take-other'])
 
976
        self.assertEqual({'action': 'take_other'}, opts)
 
977
        opts, args = self.parse(['--take-other'])
 
978
        self.assertEqual({'action': 'take_other'}, opts)