218
def mirror_scenarios(base_scenarios):
219
"""Return a list of mirrored scenarios.
221
Each scenario in base_scenarios is duplicated switching the roles of 'this'
225
for common, (lname, ldict), (rname, rdict) in base_scenarios:
226
a = tests.multiply_scenarios([(lname, dict(_this=ldict))],
227
[(rname, dict(_other=rdict))])
228
b = tests.multiply_scenarios([(rname, dict(_this=rdict))],
229
[(lname, dict(_other=ldict))])
230
# Inject the common parameters in all scenarios
231
for name, d in a + b:
233
scenarios.extend(a + b)
218
237
# FIXME: Get rid of parametrized (in the class name) once we delete
219
238
# TestResolveConflicts -- vila 20100308
220
239
class TestParametrizedResolveConflicts(tests.TestCaseWithTransport):
221
240
"""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.
242
Since all conflict objects are created with specific semantics for their
243
attributes, each class should implement the necessary functions and
244
attributes described below.
246
Each class should define the scenarios that create the expected (single)
249
Each scenario describes:
250
* how to create 'base' tree (and revision)
251
* how to create 'left' tree (and revision, parent rev 'base')
252
* how to create 'right' tree (and revision, parent rev 'base')
253
* how to check that changes in 'base'->'left' have been taken
254
* how to check that changes in 'base'->'right' have been taken
256
From each base scenario, we generate two concrete scenarios where:
257
* this=left, other=right
258
* this=right, other=left
260
Then the test case verifies each concrete scenario by:
261
* creating a branch containing the 'base', 'this' and 'other' revisions
262
* creating a working tree for the 'this' revision
263
* performing the merge of 'other' into 'this'
264
* verifying the expected conflict was generated
265
* resolving with --take-this or --take-other, and running the corresponding
266
checks (for either 'base'->'this', or 'base'->'other')
268
:cvar _conflict_type: The expected class of the generated conflict.
270
:cvar _assert_conflict: A method receiving the working tree and the
271
conflict object and checking its attributes.
273
:cvar _base_actions: The branchbuilder actions to create the 'base'
276
:cvar _this: The dict related to 'base' -> 'this'. It contains at least:
277
* 'actions': The branchbuilder actions to create the 'this'
279
* 'check': how to check the changes after resolution with --take-this.
281
:cvar _other: The dict related to 'base' -> 'other'. It contains at least:
282
* 'actions': The branchbuilder actions to create the 'other'
284
* 'check': how to check the changes after resolution with --take-other.
238
287
# Set by daughter classes
242
291
# Set by load_tests
243
292
_base_actions = None
245
_other_actions = 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):
298
"""Return the scenario list for the conflict type defined by the class.
300
Each scenario is of the form:
301
(common, (left_name, left_dict), (right_name, right_dict))
305
* left_name and right_name are the scenario names that will be combined
307
* left_dict and right_dict are the attributes specific to each half of
308
the scenario. They should include at least 'actions' and 'check' and
309
will be available as '_this' and '_other' test instance attributes.
311
Daughters classes are free to add their specific attributes as they see
312
fit in any of the three dicts.
314
This is a class method so that load_tests can find it.
316
'_base_actions' in the common dict, 'actions' and 'check' in the left
317
and right dicts use names that map to methods in the test classes. Some
318
prefixes are added to these names to get the correspong methods (see
319
_get_actions() and _get_check()). The motivation here is to avoid
320
collisions in the class namespace.
290
322
# Only concrete classes return actual scenarios
320
350
def _get_check(self, name):
321
351
return getattr(self, 'check_%s' % name)
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', ''))])
347
def do_modify_file(self):
348
return ('file', 'file-id',
349
[('modify', ('file-id', 'trunk content\nmore content\n'))])
351
def check_file_has_more_content(self):
352
self.assertFileEqual('trunk content\nmore content\n', 'branch/file')
354
def do_delete_file(self):
355
return ('file', 'file-id', [('unversion', 'file-id')])
357
def check_file_doesnt_exist(self):
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')
374
def do_rename_dir(self):
375
return ('new-dir', 'dir-id', [('rename', ('dir', 'new-dir'))])
377
def check_dir_renamed(self):
378
self.failIfExists('branch/dir')
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')
388
def do_delete_dir(self):
389
return ('<deleted>', 'dir-id', [('unversion', 'dir-id')])
391
def check_dir_doesnt_exist(self):
392
self.failIfExists('branch/dir')
394
353
def _merge_other_into_this(self):
395
354
b = self.builder.get_branch()
396
355
wt = b.bzrdir.sprout('branch').open_workingtree()
418
377
wt = self._merge_other_into_this()
419
378
self.assertConflict(wt)
420
379
self.check_resolved(wt, 'take_this')
421
check_this = self._get_check(self._check_this)
380
check_this = self._get_check(self._this['check'])
424
383
def test_resolve_taking_other(self):
425
384
wt = self._merge_other_into_this()
426
385
self.assertConflict(wt)
427
386
self.check_resolved(wt, 'take_other')
428
check_other = self._get_check(self._check_other)
387
check_other = self._get_check(self._other['check'])
432
391
class TestResolveContentsConflict(TestParametrizedResolveConflicts):
434
393
_conflict_type = conflicts.ContentsConflict,
436
def scenarios(klass):
395
# Set by load_tests from scenarios()
396
# path and file-id for the file involved in the conflict
437
402
base_scenarios = [
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')),
403
# File modified/deleted
404
(dict(_base_actions='create_file',
405
_path='file', _file_id='file-id'),
407
dict(actions='modify_file', check='file_has_more_content')),
409
dict(actions='delete_file', check='file_doesnt_exist')),),
444
return klass.mirror_scenarios(base_scenarios)
411
return mirror_scenarios(base_scenarios)
413
def do_create_file(self):
414
return [('add', ('file', 'file-id', 'file', 'trunk content\n'))]
416
def do_modify_file(self):
417
return [('modify', ('file-id', 'trunk content\nmore content\n'))]
419
def check_file_has_more_content(self):
420
self.assertFileEqual('trunk content\nmore content\n', 'branch/file')
422
def do_delete_file(self):
423
return [('unversion', 'file-id')]
425
def check_file_doesnt_exist(self):
426
self.failIfExists('branch/file')
428
def _get_resolve_path_arg(self, wt, action):
446
431
def assertContentsConflict(self, wt, c):
447
self.assertEqual(self._other_id, c.file_id)
448
self.assertEqual(self._other_path, c.path)
432
self.assertEqual(self._file_id, c.file_id)
433
self.assertEqual(self._path, c.path)
449
434
_assert_conflict = assertContentsConflict
453
437
class TestResolvePathConflict(TestParametrizedResolveConflicts):
455
439
_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',)
441
def do_nothing(self):
446
# Each side dict additionally defines:
447
# - path path involved (can be '<deleted>')
463
449
base_scenarios = [
465
dict(actions='rename_file', check='file_renamed')),
450
# File renamed/deleted
451
(dict(_base_actions='create_file'),
453
dict(actions='rename_file', check='file_renamed',
454
path='new-file', file_id='file-id')),
467
dict(actions='delete_file', check='file_doesnt_exist')),
470
dict(actions='rename_file', check='file_renamed')),
456
dict(actions='delete_file', check='file_doesnt_exist',
457
# PathConflicts deletion handling requires a special
459
path='<deleted>', file_id='file-id')),),
460
# File renamed/renamed differently
461
(dict(_base_actions='create_file'),
463
dict(actions='rename_file', check='file_renamed',
464
path='new-file', file_id='file-id')),
471
465
('file_renamed2',
472
dict(actions='rename_file2', check='file_renamed2')),
475
dict(actions='rename_dir', check='dir_renamed')),
466
dict(actions='rename_file2', check='file_renamed2',
467
path='new-file2', file_id='file-id')),),
468
# Dir renamed/deleted
469
(dict(_base_actions='create_dir'),
471
dict(actions='rename_dir', check='dir_renamed',
472
path='new-dir', file_id='dir-id')),
477
dict(actions='delete_dir', check='dir_doesnt_exist')),
480
dict(actions='rename_dir', check='dir_renamed')),
474
dict(actions='delete_dir', check='dir_doesnt_exist',
475
# PathConflicts deletion handling requires a special
477
path='<deleted>', file_id='dir-id')),),
478
# Dir renamed/renamed differently
479
(dict(_base_actions='create_dir'),
481
dict(actions='rename_dir', check='dir_renamed',
482
path='new-dir', file_id='dir-id')),
482
dict(actions='rename_dir2', check='dir_renamed2')),
484
dict(actions='rename_dir2', check='dir_renamed2',
485
path='new-dir2', file_id='dir-id')),),
485
return klass.mirror_scenarios(base_scenarios)
487
return mirror_scenarios(base_scenarios)
489
def do_create_file(self):
490
return [('add', ('file', 'file-id', 'file', 'trunk content\n'))]
492
def do_create_dir(self):
493
return [('add', ('dir', 'dir-id', 'directory', ''))]
495
def do_rename_file(self):
496
return [('rename', ('file', 'new-file'))]
498
def check_file_renamed(self):
499
self.failIfExists('branch/file')
500
self.failUnlessExists('branch/new-file')
502
def do_rename_file2(self):
503
return [('rename', ('file', 'new-file2'))]
505
def check_file_renamed2(self):
506
self.failIfExists('branch/file')
507
self.failUnlessExists('branch/new-file2')
509
def do_rename_dir(self):
510
return [('rename', ('dir', 'new-dir'))]
512
def check_dir_renamed(self):
513
self.failIfExists('branch/dir')
514
self.failUnlessExists('branch/new-dir')
516
def do_rename_dir2(self):
517
return [('rename', ('dir', 'new-dir2'))]
519
def check_dir_renamed2(self):
520
self.failIfExists('branch/dir')
521
self.failUnlessExists('branch/new-dir2')
487
523
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:]
524
return [('unversion', 'file-id')]
526
def check_file_doesnt_exist(self):
527
self.failIfExists('branch/file')
529
def do_delete_dir(self):
530
return [('unversion', 'dir-id')]
532
def check_dir_doesnt_exist(self):
533
self.failIfExists('branch/dir')
535
def _get_resolve_path_arg(self, wt, action):
536
tpath = self._this['path']
537
opath = self._other['path']
538
if tpath == '<deleted>':
493
544
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)
545
tpath = self._this['path']
546
tfile_id = self._this['file_id']
547
opath = self._other['path']
548
ofile_id = self._other['file_id']
549
self.assertEqual(tfile_id, ofile_id) # Sanity check
550
self.assertEqual(tfile_id, c.file_id)
551
self.assertEqual(tpath, c.path)
552
self.assertEqual(opath, c.conflict_path)
497
553
_assert_conflict = assertPathConflict
513
569
class TestResolveDuplicateEntry(TestParametrizedResolveConflicts):
515
571
_conflict_type = conflicts.DuplicateEntry,
517
def scenarios(klass):
575
# Each side dict additionally defines:
518
578
base_scenarios = [
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')),
579
# File created with different file-ids
580
(dict(_base_actions='nothing'),
582
dict(actions='create_file_a', check='file_content_a',
583
path='file', file_id='file-a-id')),
585
dict(actions='create_file_b', check='file_content_b',
586
path='file', file_id='file-b-id')),),
525
return klass.mirror_scenarios(base_scenarios)
588
return mirror_scenarios(base_scenarios)
590
def do_nothing(self):
593
def do_create_file_a(self):
594
return [('add', ('file', 'file-a-id', 'file', 'file a content\n'))]
596
def check_file_content_a(self):
597
self.assertFileEqual('file a content\n', 'branch/file')
599
def do_create_file_b(self):
600
return [('add', ('file', 'file-b-id', 'file', 'file b content\n'))]
602
def check_file_content_b(self):
603
self.assertFileEqual('file b content\n', 'branch/file')
605
def _get_resolve_path_arg(self, wt, action):
606
return self._this['path']
527
608
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)
609
tpath = self._this['path']
610
tfile_id = self._this['file_id']
611
opath = self._other['path']
612
ofile_id = self._other['file_id']
613
self.assertEqual(tpath, opath) # Sanity check
614
self.assertEqual(tfile_id, c.file_id)
615
self.assertEqual(tpath + '.moved', c.path)
616
self.assertEqual(tpath, c.conflict_path)
531
617
_assert_conflict = assertDuplicateEntry
701
787
class TestResolveParentLoop(TestParametrizedResolveConflicts):
703
789
_conflict_type = conflicts.ParentLoop,
705
def scenarios(klass):
796
# Each side dict additionally defines:
797
# - dir_id: the directory being moved
798
# - target_id: The target directory
799
# - xfail: whether the test is expected to fail if the action is
800
# involved as 'other'
706
801
base_scenarios = [
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')),
802
# Dirs moved into each other
803
(dict(_base_actions='create_dir1_dir2'),
805
dict(actions='move_dir1_into_dir2', check='dir1_moved',
806
dir_id='dir1-id', target_id='dir2-id', xfail=False)),
808
dict(actions='move_dir2_into_dir1', check='dir2_moved',
809
dir_id='dir2-id', target_id='dir1-id', xfail=False))),
810
# Subdirs moved into each other
811
(dict(_base_actions='create_dir1_4'),
813
dict(actions='move_dir1_into_dir4', check='dir1_2_moved',
814
dir_id='dir1-id', target_id='dir4-id', xfail=True)),
816
dict(actions='move_dir3_into_dir2', check='dir3_4_moved',
817
dir_id='dir3-id', target_id='dir2-id', xfail=True))),
718
return klass.mirror_scenarios(base_scenarios)
819
return mirror_scenarios(base_scenarios)
720
821
def do_create_dir1_dir2(self):
722
[('add', ('dir1', 'dir1-id', 'directory', '')),
723
('add', ('dir2', 'dir2-id', 'directory', '')),
822
return [('add', ('dir1', 'dir1-id', 'directory', '')),
823
('add', ('dir2', 'dir2-id', 'directory', '')),]
726
825
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'))])
826
return [('rename', ('dir1', 'dir2/dir1'))]
730
828
def check_dir1_moved(self):
731
829
self.failIfExists('branch/dir1')
732
830
self.failUnlessExists('branch/dir2/dir1')
734
832
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'))])
833
return [('rename', ('dir2', 'dir1/dir2'))]
738
835
def check_dir2_moved(self):
739
836
self.failIfExists('branch/dir2')
740
837
self.failUnlessExists('branch/dir1/dir2')
742
839
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', '')),
840
return [('add', ('dir1', 'dir1-id', 'directory', '')),
841
('add', ('dir1/dir2', 'dir2-id', 'directory', '')),
842
('add', ('dir3', 'dir3-id', 'directory', '')),
843
('add', ('dir3/dir4', 'dir4-id', 'directory', '')),]
750
845
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'))])
846
return [('rename', ('dir1', 'dir3/dir4/dir1'))]
755
848
def check_dir1_2_moved(self):
756
849
self.failIfExists('branch/dir1')
768
859
self.failUnlessExists('branch/dir1/dir2/dir3/dir4')
770
861
def _get_resolve_path_arg(self, wt, action):
771
# ParentLoop is unsual as it says:
772
# moving <conflict_path> into <path>. Cancelled move.
862
# ParentLoop says: moving <conflict_path> into <path>. Cancelled move.
773
863
# 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)
864
# <conflict_path> instead, and that, in turn, is given by dir_id. Pfew.
865
return wt.id2path(self._other['dir_id'])
778
867
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)
868
self.assertEqual(self._other['dir_id'], c.file_id)
869
self.assertEqual(self._other['target_id'], c.conflict_file_id)
789
870
# The conflict paths are irrelevant (they are deterministic but not
790
871
# worth checking since they don't provide the needed information
873
if self._other['xfail']:
874
# It's a bit hackish to raise from here relying on being called for
875
# both tests but this avoid overriding test_resolve_taking_other
876
raise tests.KnownFailure(
877
"ParentLoop doesn't carry enough info to resolve --take-other")
792
878
_assert_conflict = assertParentLoop
795
class OldTestResolveParentLoop(TestResolveConflicts):
802
$ bzr commit -m 'Create trunk'
805
$ bzr commit -m 'Moved dir2 into dir1'
807
$ bzr branch . -r 1 ../branch
810
$ bzr commit -m 'Moved dir1 into dir2'
813
2>Conflict moving dir2 into dir2/dir1. Cancelled move.
814
2>1 conflicts encountered.
817
def test_take_this(self):
820
$ bzr commit --strict -m 'No more conflicts nor unknown files'
823
def test_take_other(self):
825
$ bzr mv dir2/dir1 dir1
828
$ bzr commit --strict -m 'No more conflicts nor unknown files'
831
def test_resolve_taking_this(self):
833
$ bzr resolve --take-this dir2
834
$ bzr commit --strict -m 'No more conflicts nor unknown files'
836
self.failUnlessExists('dir2')
838
def test_resolve_taking_other(self):
840
$ bzr resolve --take-other dir2
841
$ bzr commit --strict -m 'No more conflicts nor unknown files'
843
self.failUnlessExists('dir1')
846
881
class TestResolveNonDirectoryParent(TestResolveConflicts):