212
def resolve_conflict_scenarios():
214
(dict(_conflict_type=conflicts.ContentsConflict,
215
_item_path='file', _item_id='file-id',),
216
('file_modified', dict(actions='modify_file',
217
check='file_has_more_content')),
218
('file_deleted', dict(actions='delete_file',
219
check='file_doesnt_exist'))),
220
(dict(_conflict_type=conflicts.PathConflict,
221
_item_path='new-dir', _item_id='dir-id',),
222
('dir_renamed', dict(actions='rename_dir', check='dir_renamed')),
223
('dir_deleted', dict(actions='delete_dir', check='dir_doesnt_exist'))),
225
# Each base scenario is duplicated switching the roles of this and other
227
for common, (tname, tdict), (oname, odict) in base_scenarios:
229
d.update(_this_actions=tdict['actions'], _check_this=tdict['check'],
230
_other_actions=odict['actions'], _check_other=odict['check'])
231
scenarios.append(('%s,%s' % (tname, oname), d))
233
d.update(_this_actions=odict['actions'], _check_this=odict['check'],
234
_other_actions=tdict['actions'], _check_other=tdict['check'])
235
scenarios.append(('%s,%s' % (oname, tname), d))
238
# FIXME: Get rid of parametrized once we delete TestResolveConflicts
218
# FIXME: Get rid of parametrized (in the class name) once we delete
219
# TestResolveConflicts -- vila 20100308
239
220
class TestParametrizedResolveConflicts(tests.TestCaseWithTransport):
221
"""This class provides a base to test single conflict resolution.
223
The aim is to define scenarios in daughter classes (one for each conflict
224
type) that create a single conflict object when one branch is merged in
225
another (and vice versa). Each class can define as many scenarios as
226
needed. Each scenario should define a couple of actions that will be
227
swapped to define the sibling scenarios.
229
From there, both resolutions are tested (--take-this and --take-other).
231
Each conflict type use its attributes in a specific way, so each class
232
should define a specific _assert_conflict method.
234
Since the resolution change the working tree state, each action should
235
define an associated check.
238
# Set by daughter classes
239
_conflict_type = None
240
_assert_conflict = None
241
242
# Set by load_tests
242
244
_this_actions = None
243
245
_other_actions = None
244
_conflict_type = None
245
246
_item_path = None
249
# Set by _this_actions and other_actions
250
# FIXME: rename them this_args and other_args so the tests can use them
258
def mirror_scenarios(klass, base_scenarios):
261
"""Modify dict to apply to the given side.
263
'actions' key is turned into '_actions_this' if side is 'this' for
267
# Turn each key into _side_key
268
for k,v in d.iteritems():
269
t['_%s_%s' % (k, side)] = v
271
# Each base scenario is duplicated switching the roles of 'this' and
273
left = [l for l, r, c in base_scenarios]
274
right = [r for l, r, c in base_scenarios]
275
common = [c for l, r, c in base_scenarios]
276
for (lname, ldict), (rname, rdict), common in zip(left, right, common):
277
a = tests.multiply_scenarios([(lname, adapt(ldict, 'this'))],
278
[(rname, adapt(rdict, 'other'))])
279
b = tests.multiply_scenarios(
280
[(rname, adapt(rdict, 'this'))],
281
[(lname, adapt(ldict, 'other'))])
282
# Inject the common parameters in all scenarios
283
for name, d in a + b:
285
scenarios.extend(a + b)
289
def scenarios(klass):
290
# Only concrete classes return actual scenarios
249
294
super(TestParametrizedResolveConflicts, self).setUp()
250
295
builder = self.make_branch_builder('trunk')
251
296
builder.start_series()
252
298
# Create an empty trunk
253
299
builder.build_snapshot('start', None, [
254
300
('add', ('', 'root-id', 'directory', ''))])
255
301
# Add a minimal base content
256
builder.build_snapshot(
258
('add', ('file', 'file-id', 'file', 'trunk content\n')),
259
('add', ('dir', 'dir-id', 'directory', '')),
302
_, _, actions_base = self._get_actions(self._actions_base)()
303
builder.build_snapshot('base', ['start'], actions_base)
261
304
# Modify the base content in branch
262
other_actions = self._get_actions(self._other_actions)
263
builder.build_snapshot('other', ['base'], other_actions())
305
(self._other_path, self._other_id,
306
actions_other) = self._get_actions(self._actions_other)()
307
builder.build_snapshot('other', ['base'], actions_other)
264
308
# Modify the base content in trunk
265
this_actions = self._get_actions(self._this_actions)
266
builder.build_snapshot('this', ['base'], this_actions())
309
(self._this_path, self._this_id,
310
actions_this) = self._get_actions(self._actions_this)()
311
builder.build_snapshot('this', ['base'], actions_this)
312
# builder.get_branch() tip is now 'this'
267
314
builder.finish_series()
268
315
self.builder = builder
273
320
def _get_check(self, name):
274
321
return getattr(self, 'check_%s' % name)
276
def assertConflict(self, wt, **kwargs):
277
confs = wt.conflicts()
278
self.assertLength(1, confs)
280
self.assertIsInstance(c, self._conflict_type)
281
sentinel = object() # An impossible value
282
for k, v in kwargs.iteritems():
283
self.assertEqual(v, getattr(c, k, sentinel), "for key '%s'" % k)
285
def check_resolved(self, wt, item, action):
286
conflicts.resolve(wt, [item], action=action)
287
# Check that we don't have any conflicts nor unknown left
288
self.assertLength(0, wt.conflicts())
289
self.assertLength(0, list(wt.unknowns()))
323
def do_nothing(self):
324
return (None, None, [])
326
def do_create_file(self):
327
return ('file', 'file-id',
328
[('add', ('file', 'file-id', 'file', 'trunk content\n'))])
330
def do_create_file_a(self):
331
return ('file', 'file-a-id',
332
[('add', ('file', 'file-a-id', 'file', 'file a content\n'))])
334
def check_file_content_a(self):
335
self.assertFileEqual('file a content\n', 'branch/file')
337
def do_create_file_b(self):
338
return ('file', 'file-b-id',
339
[('add', ('file', 'file-b-id', 'file', 'file b content\n'))])
341
def check_file_content_b(self):
342
self.assertFileEqual('file b content\n', 'branch/file')
344
def do_create_dir(self):
345
return ('dir', 'dir-id', [('add', ('dir', 'dir-id', 'directory', ''))])
291
347
def do_modify_file(self):
292
return [('modify', ('file-id', 'trunk content\nmore content\n'))]
348
return ('file', 'file-id',
349
[('modify', ('file-id', 'trunk content\nmore content\n'))])
294
351
def check_file_has_more_content(self):
295
352
self.assertFileEqual('trunk content\nmore content\n', 'branch/file')
297
354
def do_delete_file(self):
298
return [('unversion', 'file-id')]
355
return ('file', 'file-id', [('unversion', 'file-id')])
300
357
def check_file_doesnt_exist(self):
301
358
self.failIfExists('branch/file')
360
def do_rename_file(self):
361
return ('new-file', 'file-id', [('rename', ('file', 'new-file'))])
363
def check_file_renamed(self):
364
self.failIfExists('branch/file')
365
self.failUnlessExists('branch/new-file')
367
def do_rename_file2(self):
368
return ('new-file2', 'file-id', [('rename', ('file', 'new-file2'))])
370
def check_file_renamed2(self):
371
self.failIfExists('branch/file')
372
self.failUnlessExists('branch/new-file2')
303
374
def do_rename_dir(self):
304
return [('rename', ('dir', 'new-dir'))]
375
return ('new-dir', 'dir-id', [('rename', ('dir', 'new-dir'))])
306
377
def check_dir_renamed(self):
307
378
self.failIfExists('branch/dir')
308
379
self.failUnlessExists('branch/new-dir')
381
def do_rename_dir2(self):
382
return ('new-dir2', 'dir-id', [('rename', ('dir', 'new-dir2'))])
384
def check_dir_renamed2(self):
385
self.failIfExists('branch/dir')
386
self.failUnlessExists('branch/new-dir2')
310
388
def do_delete_dir(self):
311
return [('unversion', 'dir-id')]
389
return ('<deleted>', 'dir-id', [('unversion', 'dir-id')])
313
391
def check_dir_doesnt_exist(self):
314
392
self.failIfExists('branch/dir')
319
397
wt.merge_from_branch(b, 'other')
400
def assertConflict(self, wt):
401
confs = wt.conflicts()
402
self.assertLength(1, confs)
404
self.assertIsInstance(c, self._conflict_type)
405
self._assert_conflict(wt, c)
407
def _get_resolve_path_arg(self, wt, action):
408
return self._item_path
410
def check_resolved(self, wt, action):
411
path = self._get_resolve_path_arg(wt, action)
412
conflicts.resolve(wt, [path], action=action)
413
# Check that we don't have any conflicts nor unknown left
414
self.assertLength(0, wt.conflicts())
415
self.assertLength(0, list(wt.unknowns()))
322
417
def test_resolve_taking_this(self):
323
418
wt = self._merge_other_into_this()
324
self.assertConflict(wt, path=self._item_path, file_id=self._item_id)
325
self.check_resolved(wt, self._item_path, 'take_this')
419
self.assertConflict(wt)
420
self.check_resolved(wt, 'take_this')
326
421
check_this = self._get_check(self._check_this)
329
424
def test_resolve_taking_other(self):
330
425
wt = self._merge_other_into_this()
331
self.assertConflict(wt, path=self._item_path, file_id=self._item_id)
332
self.check_resolved(wt, self._item_path, 'take_other')
426
self.assertConflict(wt)
427
self.check_resolved(wt, 'take_other')
333
428
check_other = self._get_check(self._check_other)
337
class TestResolveDuplicateEntry(TestResolveConflicts):
342
$ echo 'trunk content' >file
344
$ bzr commit -m 'Create trunk'
346
$ echo 'trunk content too' >file2
348
$ bzr commit -m 'Add file2 in trunk'
350
$ bzr branch . -r 1 ../branch
352
$ echo 'branch content' >file2
354
$ bzr commit -m 'Add file2 in branch'
358
2>R file2 => file2.moved
359
2>Conflict adding file file2. Moved existing file to file2.moved.
360
2>1 conflicts encountered.
363
def test_keep_this(self):
365
$ bzr rm file2 --force
366
$ bzr mv file2.moved file2
368
$ bzr commit --strict -m 'No more conflicts nor unknown files'
371
def test_keep_other(self):
372
self.failIfExists('branch/file2.moved')
374
$ bzr rm file2.moved --force
376
$ bzr commit --strict -m 'No more conflicts nor unknown files'
378
self.failIfExists('branch/file2.moved')
380
def test_resolve_taking_this(self):
382
$ bzr resolve --take-this file2
383
$ bzr commit --strict -m 'No more conflicts nor unknown files'
386
def test_resolve_taking_other(self):
388
$ bzr resolve --take-other file2
389
$ bzr commit --strict -m 'No more conflicts nor unknown files'
432
class TestResolveContentsConflict(TestParametrizedResolveConflicts):
434
_conflict_type = conflicts.ContentsConflict,
436
def scenarios(klass):
438
(('file_modified', dict(actions='modify_file',
439
check='file_has_more_content')),
440
('file_deleted', dict(actions='delete_file',
441
check='file_doesnt_exist')),
442
dict(_actions_base='create_file', _item_path='file')),
444
return klass.mirror_scenarios(base_scenarios)
446
def assertContentsConflict(self, wt, c):
447
self.assertEqual(self._other_id, c.file_id)
448
self.assertEqual(self._other_path, c.path)
449
_assert_conflict = assertContentsConflict
453
class TestResolvePathConflict(TestParametrizedResolveConflicts):
455
_conflict_type = conflicts.PathConflict,
458
def scenarios(klass):
459
for_file = dict(_actions_base='create_file',
460
_item_path='new-file', _item_id='file-id',)
461
for_dir = dict(_actions_base='create_dir',
462
_item_path='new-dir', _item_id='dir-id',)
465
dict(actions='rename_file', check='file_renamed')),
467
dict(actions='delete_file', check='file_doesnt_exist')),
470
dict(actions='rename_file', check='file_renamed')),
472
dict(actions='rename_file2', check='file_renamed2')),
475
dict(actions='rename_dir', check='dir_renamed')),
477
dict(actions='delete_dir', check='dir_doesnt_exist')),
480
dict(actions='rename_dir', check='dir_renamed')),
482
dict(actions='rename_dir2', check='dir_renamed2')),
485
return klass.mirror_scenarios(base_scenarios)
487
def do_delete_file(self):
488
sup = super(TestResolvePathConflict, self).do_delete_file()
489
# PathConflicts handle deletion differently and requires a special
491
return ('<deleted>',) + sup[1:]
493
def assertPathConflict(self, wt, c):
494
self.assertEqual(self._item_id, c.file_id)
495
self.assertEqual(self._this_path, c.path)
496
self.assertEqual(self._other_path, c.conflict_path)
497
_assert_conflict = assertPathConflict
500
class TestResolvePathConflictBefore531967(TestResolvePathConflict):
501
"""Same as TestResolvePathConflict but a specific conflict object.
504
def assertPathConflict(self, c):
505
# We create a conflict object as it was created before the fix and
506
# inject it into the working tree, the test will exercise the
507
# compatibility code.
508
old_c = conflicts.PathConflict('<deleted>', self._item_path,
510
wt.set_conflicts(conflicts.ConflictList([old_c]))
513
class TestResolveDuplicateEntry(TestParametrizedResolveConflicts):
515
_conflict_type = conflicts.DuplicateEntry,
517
def scenarios(klass):
519
(('filea_created', dict(actions='create_file_a',
520
check='file_content_a')),
521
('fileb_created', dict(actions='create_file_b',
522
check='file_content_b')),
523
dict(_actions_base='nothing', _item_path='file')),
525
return klass.mirror_scenarios(base_scenarios)
527
def assertDuplicateEntry(self, wt, c):
528
self.assertEqual(self._this_id, c.file_id)
529
self.assertEqual(self._item_path + '.moved', c.path)
530
self.assertEqual(self._item_path, c.conflict_path)
531
_assert_conflict = assertDuplicateEntry
393
534
class TestResolveUnversionedParent(TestResolveConflicts):
560
class TestResolvePathConflict(TestResolveConflicts):
567
$ bzr commit -m 'Create trunk'
569
$ bzr mv file file-in-trunk
570
$ bzr commit -m 'Renamed to file-in-trunk'
572
$ bzr branch . -r 1 ../branch
574
$ bzr mv file file-in-branch
575
$ bzr commit -m 'Renamed to file-in-branch'
578
2>R file-in-branch => file-in-trunk
579
2>Path conflict: file-in-branch / file-in-trunk
580
2>1 conflicts encountered.
583
def test_keep_source(self):
585
$ bzr resolve file-in-trunk
586
$ bzr commit --strict -m 'No more conflicts nor unknown files'
589
def test_keep_target(self):
591
$ bzr mv file-in-trunk file-in-branch
592
$ bzr resolve file-in-branch
593
$ bzr commit --strict -m 'No more conflicts nor unknown files'
596
def test_resolve_taking_this(self):
598
$ bzr resolve --take-this file-in-branch
599
$ bzr commit --strict -m 'No more conflicts nor unknown files'
602
def test_resolve_taking_other(self):
604
$ bzr resolve --take-other file-in-branch
605
$ bzr commit --strict -m 'No more conflicts nor unknown files'
609
class TestResolveParentLoop(TestResolveConflicts):
701
class TestResolveParentLoop(TestParametrizedResolveConflicts):
703
_conflict_type = conflicts.ParentLoop,
705
def scenarios(klass):
707
(('dir1_into_dir2', dict(actions='move_dir1_into_dir2',
708
check='dir1_moved')),
709
('dir2_into_dir1', dict(actions='move_dir2_into_dir1',
710
check='dir2_moved')),
711
dict(_actions_base='create_dir1_dir2')),
712
(('dir1_into_dir4', dict(actions='move_dir1_into_dir4',
713
check='dir1_2_moved')),
714
('dir3_into_dir2', dict(actions='move_dir3_into_dir2',
715
check='dir3_4_moved')),
716
dict(_actions_base='create_dir1_4')),
718
return klass.mirror_scenarios(base_scenarios)
720
def do_create_dir1_dir2(self):
722
[('add', ('dir1', 'dir1-id', 'directory', '')),
723
('add', ('dir2', 'dir2-id', 'directory', '')),
726
def do_move_dir1_into_dir2(self):
727
# The arguments are the file-id to move and the targeted file-id dir.
728
return ('dir1-id', 'dir2-id', [('rename', ('dir1', 'dir2/dir1'))])
730
def check_dir1_moved(self):
731
self.failIfExists('branch/dir1')
732
self.failUnlessExists('branch/dir2/dir1')
734
def do_move_dir2_into_dir1(self):
735
# The arguments are the file-id to move and the targeted file-id dir.
736
return ('dir2-id', 'dir1-id', [('rename', ('dir2', 'dir1/dir2'))])
738
def check_dir2_moved(self):
739
self.failIfExists('branch/dir2')
740
self.failUnlessExists('branch/dir1/dir2')
742
def do_create_dir1_4(self):
744
[('add', ('dir1', 'dir1-id', 'directory', '')),
745
('add', ('dir1/dir2', 'dir2-id', 'directory', '')),
746
('add', ('dir3', 'dir3-id', 'directory', '')),
747
('add', ('dir3/dir4', 'dir4-id', 'directory', '')),
750
def do_move_dir1_into_dir4(self):
751
# The arguments are the file-id to move and the targeted file-id dir.
752
return ('dir1-id', 'dir4-id',
753
[('rename', ('dir1', 'dir3/dir4/dir1'))])
755
def check_dir1_2_moved(self):
756
self.failIfExists('branch/dir1')
757
self.failUnlessExists('branch/dir3/dir4/dir1')
758
self.failUnlessExists('branch/dir3/dir4/dir1/dir2')
760
def do_move_dir3_into_dir2(self):
761
# The arguments are the file-id to move and the targeted file-id dir.
762
return ('dir3-id', 'dir2-id',
763
[('rename', ('dir3', 'dir1/dir2/dir3'))])
765
def check_dir3_4_moved(self):
766
self.failIfExists('branch/dir3')
767
self.failUnlessExists('branch/dir1/dir2/dir3')
768
self.failUnlessExists('branch/dir1/dir2/dir3/dir4')
770
def _get_resolve_path_arg(self, wt, action):
771
# ParentLoop is unsual as it says:
772
# moving <conflict_path> into <path>. Cancelled move.
773
# But since <path> doesn't exist in the working tree, we need to use
774
# <conflict_path> instead
775
path = wt.id2path(self._other_id)
778
def assertParentLoop(self, wt, c):
779
if 'taking_other(' in self.id() and 'dir4' in self.id():
780
raise tests.KnownFailure(
781
"ParentLoop doesn't carry enough info to resolve")
782
# The relevant file-ids are other_args swapped (which is the main
783
# reason why they should be renamed other_args instead of Other_path
784
# and other_id). In the conflict object, they represent:
785
# c.file_id: the directory being moved
786
# c.conflict_id_id: The target directory
787
self.assertEqual(self._other_path, c.file_id)
788
self.assertEqual(self._other_id, c.conflict_file_id)
789
# The conflict paths are irrelevant (they are deterministic but not
790
# worth checking since they don't provide the needed information
792
_assert_conflict = assertParentLoop
795
class OldTestResolveParentLoop(TestResolveConflicts):