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)
237
# FIXME: Get rid of parametrized (in the class name) once we delete
238
# TestResolveConflicts -- vila 20100308
239
class TestParametrizedResolveConflicts(tests.TestCaseWithTransport):
240
"""This class provides a base to test single conflict resolution.
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.
287
# Set by daughter classes
288
_conflict_type = None
289
_assert_conflict = None
212
def content_conflict_scenarios():
213
return [('file,None', dict(_this_actions='modify_file',
214
_check_this='file_has_more_content',
215
_other_actions='delete_file',
216
_check_other='file_doesnt_exist',
218
('None,file', dict(_this_actions='delete_file',
219
_check_this='file_doesnt_exist',
220
_other_actions='modify_file',
221
_check_other='file_has_more_content',
226
class TestResolveContentConflicts(tests.TestCaseWithTransport):
291
228
# Set by load_tests
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.
322
# Only concrete classes return actual scenarios
326
super(TestParametrizedResolveConflicts, self).setUp()
233
super(TestResolveContentConflicts, self).setUp()
327
234
builder = self.make_branch_builder('trunk')
328
235
builder.start_series()
330
236
# Create an empty trunk
331
237
builder.build_snapshot('start', None, [
332
238
('add', ('', 'root-id', 'directory', ''))])
333
239
# Add a minimal base content
334
base_actions = self._get_actions(self._base_actions)()
335
builder.build_snapshot('base', ['start'], base_actions)
240
builder.build_snapshot('base', ['start'], [
241
('add', ('file', 'file-id', 'file', 'trunk content\n'))])
336
242
# Modify the base content in branch
337
actions_other = self._get_actions(self._other['actions'])()
338
builder.build_snapshot('other', ['base'], actions_other)
243
other_actions = self._get_actions(self._other_actions)
244
builder.build_snapshot('other', ['base'], other_actions())
339
245
# Modify the base content in trunk
340
actions_this = self._get_actions(self._this['actions'])()
341
builder.build_snapshot('this', ['base'], actions_this)
342
# builder.get_branch() tip is now 'this'
246
this_actions = self._get_actions(self._this_actions)
247
builder.build_snapshot('this', ['base'], this_actions())
344
248
builder.finish_series()
345
249
self.builder = builder
350
254
def _get_check(self, name):
351
255
return getattr(self, 'check_%s' % name)
257
def do_modify_file(self):
258
return [('modify', ('file-id', 'trunk content\nmore content\n'))]
260
def check_file_has_more_content(self):
261
self.assertFileEqual('trunk content\nmore content\n', 'branch/file')
263
def do_delete_file(self):
264
return [('unversion', 'file-id')]
266
def check_file_doesnt_exist(self):
267
self.failIfExists('branch/file')
353
269
def _merge_other_into_this(self):
354
270
b = self.builder.get_branch()
355
271
wt = b.bzrdir.sprout('branch').open_workingtree()
356
272
wt.merge_from_branch(b, 'other')
359
def assertConflict(self, wt):
275
def assertConflict(self, wt, ctype, **kwargs):
360
276
confs = wt.conflicts()
361
277
self.assertLength(1, confs)
363
self.assertIsInstance(c, self._conflict_type)
364
self._assert_conflict(wt, c)
366
def _get_resolve_path_arg(self, wt, action):
367
raise NotImplementedError(self._get_resolve_path_arg)
369
def check_resolved(self, wt, action):
370
path = self._get_resolve_path_arg(wt, action)
371
conflicts.resolve(wt, [path], action=action)
279
self.assertIsInstance(c, ctype)
280
sentinel = object() # An impossible value
281
for k, v in kwargs.iteritems():
282
self.assertEqual(v, getattr(c, k, sentinel))
284
def check_resolved(self, wt, item, action):
285
conflicts.resolve(wt, [item], action=action)
372
286
# Check that we don't have any conflicts nor unknown left
373
287
self.assertLength(0, wt.conflicts())
374
288
self.assertLength(0, list(wt.unknowns()))
376
290
def test_resolve_taking_this(self):
377
291
wt = self._merge_other_into_this()
378
self.assertConflict(wt)
379
self.check_resolved(wt, 'take_this')
380
check_this = self._get_check(self._this['check'])
292
self.assertConflict(wt, conflicts.ContentsConflict,
293
path='file', file_id='file-id',)
294
self.check_resolved(wt, 'file', 'take_this')
295
check_this = self._get_check(self._check_this)
383
298
def test_resolve_taking_other(self):
384
299
wt = self._merge_other_into_this()
385
self.assertConflict(wt)
386
self.check_resolved(wt, 'take_other')
387
check_other = self._get_check(self._other['check'])
300
self.assertConflict(wt, conflicts.ContentsConflict,
301
path='file', file_id='file-id',)
302
self.check_resolved(wt, 'file', 'take_other')
303
check_other = self._get_check(self._check_other)
391
class TestResolveContentsConflict(TestParametrizedResolveConflicts):
393
_conflict_type = conflicts.ContentsConflict,
395
# Set by load_tests from scenarios()
396
# path and file-id for the file involved in the conflict
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')),),
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):
431
def assertContentsConflict(self, wt, c):
432
self.assertEqual(self._file_id, c.file_id)
433
self.assertEqual(self._path, c.path)
434
_assert_conflict = assertContentsConflict
437
class TestResolvePathConflict(TestParametrizedResolveConflicts):
439
_conflict_type = conflicts.PathConflict,
441
def do_nothing(self):
446
# Each side dict additionally defines:
447
# - path path involved (can be '<deleted>')
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')),
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')),
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')),
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')),
484
dict(actions='rename_dir2', check='dir_renamed2',
485
path='new-dir2', file_id='dir-id')),),
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')
523
def do_delete_file(self):
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>':
544
def assertPathConflict(self, wt, c):
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)
553
_assert_conflict = assertPathConflict
556
class TestResolvePathConflictBefore531967(TestResolvePathConflict):
557
"""Same as TestResolvePathConflict but a specific conflict object.
560
def assertPathConflict(self, c):
561
# We create a conflict object as it was created before the fix and
562
# inject it into the working tree, the test will exercise the
563
# compatibility code.
564
old_c = conflicts.PathConflict('<deleted>', self._item_path,
566
wt.set_conflicts(conflicts.ConflictList([old_c]))
569
class TestResolveDuplicateEntry(TestParametrizedResolveConflicts):
571
_conflict_type = conflicts.DuplicateEntry,
575
# Each side dict additionally defines:
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')),),
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']
608
def assertDuplicateEntry(self, wt, c):
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)
617
_assert_conflict = assertDuplicateEntry
307
class TestResolveDuplicateEntry(TestResolveConflicts):
312
$ echo 'trunk content' >file
314
$ bzr commit -m 'Create trunk'
316
$ echo 'trunk content too' >file2
318
$ bzr commit -m 'Add file2 in trunk'
320
$ bzr branch . -r 1 ../branch
322
$ echo 'branch content' >file2
324
$ bzr commit -m 'Add file2 in branch'
328
2>R file2 => file2.moved
329
2>Conflict adding file file2. Moved existing file to file2.moved.
330
2>1 conflicts encountered.
333
def test_keep_this(self):
335
$ bzr rm file2 --force
336
$ bzr mv file2.moved file2
338
$ bzr commit --strict -m 'No more conflicts nor unknown files'
341
def test_keep_other(self):
342
self.failIfExists('branch/file2.moved')
344
$ bzr rm file2.moved --force
346
$ bzr commit --strict -m 'No more conflicts nor unknown files'
348
self.failIfExists('branch/file2.moved')
350
def test_resolve_taking_this(self):
352
$ bzr resolve --take-this file2
353
$ bzr commit --strict -m 'No more conflicts nor unknown files'
356
def test_resolve_taking_other(self):
358
$ bzr resolve --take-other file2
359
$ bzr commit --strict -m 'No more conflicts nor unknown files'
620
363
class TestResolveUnversionedParent(TestResolveConflicts):
787
class TestResolveParentLoop(TestParametrizedResolveConflicts):
789
_conflict_type = conflicts.ParentLoop,
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'
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))),
819
return mirror_scenarios(base_scenarios)
821
def do_create_dir1_dir2(self):
822
return [('add', ('dir1', 'dir1-id', 'directory', '')),
823
('add', ('dir2', 'dir2-id', 'directory', '')),]
825
def do_move_dir1_into_dir2(self):
826
return [('rename', ('dir1', 'dir2/dir1'))]
828
def check_dir1_moved(self):
829
self.failIfExists('branch/dir1')
830
self.failUnlessExists('branch/dir2/dir1')
832
def do_move_dir2_into_dir1(self):
833
return [('rename', ('dir2', 'dir1/dir2'))]
835
def check_dir2_moved(self):
836
self.failIfExists('branch/dir2')
837
self.failUnlessExists('branch/dir1/dir2')
839
def do_create_dir1_4(self):
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', '')),]
845
def do_move_dir1_into_dir4(self):
846
return [('rename', ('dir1', 'dir3/dir4/dir1'))]
848
def check_dir1_2_moved(self):
849
self.failIfExists('branch/dir1')
850
self.failUnlessExists('branch/dir3/dir4/dir1')
851
self.failUnlessExists('branch/dir3/dir4/dir1/dir2')
853
def do_move_dir3_into_dir2(self):
854
return [('rename', ('dir3', 'dir1/dir2/dir3'))]
856
def check_dir3_4_moved(self):
857
self.failIfExists('branch/dir3')
858
self.failUnlessExists('branch/dir1/dir2/dir3')
859
self.failUnlessExists('branch/dir1/dir2/dir3/dir4')
861
def _get_resolve_path_arg(self, wt, action):
862
# ParentLoop says: moving <conflict_path> into <path>. Cancelled move.
863
# But since <path> doesn't exist in the working tree, we need to use
864
# <conflict_path> instead, and that, in turn, is given by dir_id. Pfew.
865
return wt.id2path(self._other['dir_id'])
867
def assertParentLoop(self, wt, c):
868
self.assertEqual(self._other['dir_id'], c.file_id)
869
self.assertEqual(self._other['target_id'], c.conflict_file_id)
870
# The conflict paths are irrelevant (they are deterministic but not
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")
878
_assert_conflict = assertParentLoop
530
class TestResolvePathConflict(TestResolveConflicts):
537
$ bzr commit -m 'Create trunk'
539
$ bzr mv file file-in-trunk
540
$ bzr commit -m 'Renamed to file-in-trunk'
542
$ bzr branch . -r 1 ../branch
544
$ bzr mv file file-in-branch
545
$ bzr commit -m 'Renamed to file-in-branch'
548
2>R file-in-branch => file-in-trunk
549
2>Path conflict: file-in-branch / file-in-trunk
550
2>1 conflicts encountered.
553
def test_keep_source(self):
555
$ bzr resolve file-in-trunk
556
$ bzr commit --strict -m 'No more conflicts nor unknown files'
559
def test_keep_target(self):
561
$ bzr mv file-in-trunk file-in-branch
562
$ bzr resolve file-in-branch
563
$ bzr commit --strict -m 'No more conflicts nor unknown files'
566
def test_resolve_taking_this(self):
568
$ bzr resolve --take-this file-in-branch
569
$ bzr commit --strict -m 'No more conflicts nor unknown files'
572
def test_resolve_taking_other(self):
574
$ bzr resolve --take-other file-in-branch
575
$ bzr commit --strict -m 'No more conflicts nor unknown files'
579
class TestResolveParentLoop(TestResolveConflicts):
586
$ bzr commit -m 'Create trunk'
589
$ bzr commit -m 'Moved dir2 into dir1'
591
$ bzr branch . -r 1 ../branch
594
$ bzr commit -m 'Moved dir1 into dir2'
597
2>Conflict moving dir2/dir1 into dir2. Cancelled move.
598
2>1 conflicts encountered.
601
def test_take_this(self):
604
$ bzr commit --strict -m 'No more conflicts nor unknown files'
607
def test_take_other(self):
609
$ bzr mv dir2/dir1 dir1
612
$ bzr commit --strict -m 'No more conflicts nor unknown files'
615
def test_resolve_taking_this(self):
617
$ bzr resolve --take-this dir2
618
$ bzr commit --strict -m 'No more conflicts nor unknown files'
620
self.failUnlessExists('dir2')
622
def test_resolve_taking_other(self):
624
$ bzr resolve --take-other dir2
625
$ bzr commit --strict -m 'No more conflicts nor unknown files'
627
self.failUnlessExists('dir1')
881
630
class TestResolveNonDirectoryParent(TestResolveConflicts):