Merge lp:~vila/bzr/537956-parent-loop-incomplete into lp:bzr

Proposed by Vincent Ladeuil on 2010-03-12
Status: Merged
Approved by: Martin Pool on 2010-03-24
Approved revision: 4639
Merged at revision: not available
Proposed branch: lp:~vila/bzr/537956-parent-loop-incomplete
Merge into: lp:bzr
Prerequisite: lp:~vila/bzr/531967-unify-name-conflicts
Diff against target: 469 lines (+180/-134) (has conflicts)
6 files modified
NEWS (+4/-0)
bzrlib/atomicfile.py (+1/-1)
bzrlib/conflicts.py (+2/-2)
bzrlib/tests/test_conflicts.py (+171/-129)
bzrlib/tests/test_transform.py (+1/-1)
bzrlib/trace.py (+1/-1)
Text conflict in NEWS
To merge this branch: bzr merge lp:~vila/bzr/537956-parent-loop-incomplete
Reviewer Review Type Date Requested Status
Andrew Bennetts Approve on 2010-03-25
Martin Pool 2010-03-12 Approve on 2010-03-24
Review via email: mp+21224@code.launchpad.net

Description of the Change

As explained in bug #537956, ParentLoop objects doesn't carry enough information to
implement bzr resolve --take-other.

This patch fixes ParentLoop.format and add tests expected to fail.

This was built on top of https://code.edge.launchpad.net/~vila/bzr/531967-unify-name-conflicts/+merge/21039 pending review.

To post a comment you must log in.
Vincent Ladeuil (vila) wrote :

Despite my usage of the pre-requisite branch, the patch is a bit more verbose than I wished.
Apologies to reviewers.

The relevant stuff is in conflicts.py and class TestResolveParentLoop in test_conflicts.py.

The other parts aren't wrong in themselves either, and worth merging anyway.

4639. By Vincent Ladeuil on 2010-03-12

The ParentLoop.format has been updated, fix fallouts.

Martin Pool (mbp) :
review: Approve
Vincent Ladeuil (vila) wrote :

Waiting for the pre-requisite branch to land.

Andrew Bennetts (spiv) wrote :
Download full text (3.7 KiB)

(following on from the discussion in the pre-requisite branch, as this branch
has an updated version of that test code)

Vincent Ladeuil wrote:
[...]
> + def do_nothing(self):
> + return (None, None, [])
>
> def do_create_file(self):
> return ('file', 'file-id',
> [('add', ('file', 'file-id', 'file', 'trunk content\n'))])
>
> + def do_create_file_a(self):
> + return ('file', 'file-a-id',
> + [('add', ('file', 'file-a-id', 'file', 'file a content\n'))])
> +
> + def check_file_content_a(self):
> + self.assertFileEqual('file a content\n', 'branch/file')
> +
> + def do_create_file_b(self):
> + return ('file', 'file-b-id',
> + [('add', ('file', 'file-b-id', 'file', 'file b content\n'))])
> +
> + def check_file_content_b(self):
> + self.assertFileEqual('file b content\n', 'branch/file')

The repetition here is pretty ugly. I think what would be better is a more
data-driven description of the scenarios.

So, instead of specifying e.g.:

    _actions_base='create_file'

Why not specify the build_snapshot recipe directly in the scenario, and have a
module-global set of canned recipe items to keep it short:

    base_snapshot=[canned['trunk']]

(Note how more complex snapshots with multiple elements are still easy to
assemble, because you specify the whole snapshot list in the scenario.)

I think this would be easier and clearer than your zoo of do_* methods.

I think your check_* methods could be similarly transformed, e.g. to:

   check=[('exists', 'branch/dir1'), ...]

Let's look at the bigger picture. I think that all conflict scenarios have this
shape:

-----
Define scenario template "S" that describes:
  * how to create 'base' tree (and revision)
  * how to create 'left' tree (and revision, parent rev 'base')
  * how to create 'right' tree (and revision, parent rev 'base')
  * the conflict(s) expected
  * how to check that changes in 'base'->'left' have been taken
  * how to check that changes in 'base'->'right' have been taken

From that template S, we generate two concrete scenarios from S, "Sl" and "Sr":
  Sl: define this=left, other=right
  Sr: define this=right, other=left

Then the test case verifies each scenario by:
  * creating the 'base', 'this' and 'other' trees
  * performing the merge of 'other' into 'this'
  * verifying the expected conflicts were generated
  * resolving with --take-this or --take-other, and running the corresponding
    checks (for either 'base'->'this', or 'base'->'other')
-----

Have I missed anything?

If I got that right, please add that high-level description to this test module,
it will make the implementation details less of a concern for me because at
least the intent will be clear, which helps understand the implementation. But
I do think that structure could be expressed more clearly and succintly than
your current code.

(Also, if I'm right, that suggests to me that the tests for --take-this vs.
--take-other could just as easily be another scenario multiplication rather two
test methods, so leaving a TestCase with one test method but many scenarios!)

I don't want to sound too critic...

Read more...

Andrew Bennetts (spiv) wrote :

One more thing:

I wrote:
>
> Let's look at the bigger picture. I think that all conflict scenarios have this
> shape:

I'll add that it took me *many* rereadings of the code to get this summary as
correct as it is. This is part of my fundamental concern with the existing
tests, it's been very hard for me to figure out exactly what the tests are
doing. I'd love for the code to be so simple that this is instantly obvious
(which may be an unattainable goal?) but I'll happily settle for explanatory
comments.

-Andrew.

Martin Pool (mbp) wrote :

On 25 March 2010 17:09, Andrew Bennetts <email address hidden> wrote:
> One more thing:
>
> I wrote:
>>
>> Let's look at the bigger picture.  I think that all conflict scenarios have this
>> shape:
>
> I'll add that it took me *many* rereadings of the code to get this summary as
> correct as it is.  This is part of my fundamental concern with the existing
> tests, it's been very hard for me to figure out exactly what the tests are
> doing.  I'd love for the code to be so simple that this is instantly obvious
> (which may be an unattainable goal?) but I'll happily settle for explanatory
> comments.

I agree, I partially rubberstamped it because it was not obviously
wrong more than being obviously correct. I don't want to block
changes to this on getting all the existing tests to be very clear,
but we should move in that direction.

Thanks for reviewing it so thoroughly, Andrew.

--
Martin <http://launchpad.net/~mbp/>

Andrew Bennetts (spiv) wrote :

Martin Pool wrote:
> I agree, I partially rubberstamped it because it was not obviously
> wrong more than being obviously correct. I don't want to block
> changes to this on getting all the existing tests to be very clear,
> but we should move in that direction.

Right, and with a large comment like the one I suggested I'd be very happy to
see this land, so I should have said:

 review approve

But I'd really like to see this testing improvement polished soon after it lands
:)

review: Approve
Vincent Ladeuil (vila) wrote :

>>>>> Andrew Bennetts <email address hidden> writes:

    > Review: Approve
    > Martin Pool wrote:
    >> I agree, I partially rubberstamped it because it was not obviously
    >> wrong more than being obviously correct. I don't want to block
    >> changes to this on getting all the existing tests to be very clear,
    >> but we should move in that direction.

    > Right, and with a large comment like the one I suggested I'd be very happy to
    > see this land, so I should have said:

    > review approve

Thanks.

    > But I'd really like to see this testing improvement
    > polished soon after it lands :)

Given that these two branches are sons of 'please put your
blackbox tests into the proper file' review comment from John and
that from there I decided to rewrite them as whitebox ones and
entered a mine field from where these two bugs (and one in
dirstate) showed up, rest assured that your remarks will be
promptly taken into account :)

         Vincent

Vincent Ladeuil (vila) wrote :
Download full text (8.5 KiB)

>>>>> Andrew Bennetts <email address hidden> writes:

    > (following on from the discussion in the pre-requisite branch, as this branch
    > has an updated version of that test code)

    > Vincent Ladeuil wrote:
    > [...]
    >> + def do_nothing(self):
    >> + return (None, None, [])
    >>
    >> def do_create_file(self):
    >> return ('file', 'file-id',
    >> [('add', ('file', 'file-id', 'file', 'trunk content\n'))])
    >>
    >> + def do_create_file_a(self):
    >> + return ('file', 'file-a-id',
    >> + [('add', ('file', 'file-a-id', 'file', 'file a content\n'))])
    >> +
    >> + def check_file_content_a(self):
    >> + self.assertFileEqual('file a content\n', 'branch/file')
    >> +
    >> + def do_create_file_b(self):
    >> + return ('file', 'file-b-id',
    >> + [('add', ('file', 'file-b-id', 'file', 'file b content\n'))])
    >> +
    >> + def check_file_content_b(self):
    >> + self.assertFileEqual('file b content\n', 'branch/file')

    > The repetition here is pretty ugly.

Yes.

    > I think what would be better is a more data-driven
    > description of the scenarios.

    > So, instead of specifying e.g.:

    > _actions_base='create_file'

    > Why not specify the build_snapshot recipe directly in the scenario, and have a
    > module-global set of canned recipe items to keep it short:

    > base_snapshot=[canned['trunk']]

I went away from centralizing all recipes in the base class in a
later iteration as it broke the locality and there was almost no
reuse cases (but again I'm referring to later work, so now that I
can push my loom again, take a look at
lp:~vila/bzr/conflict-manager). Revno 4639 in particular goes
into the direction you're asking for (but still miss some
points).

One point worth noting there too is that conflating the actions
modifying the working tree with the info needed to characterized
the conflict object needs to be class specific. But in light of
your remarks, I now think that I should just have separate dicts
for each side and let the parametrization assign these dicts to
the right attribute (instead of trying to flatten them which make
mirror_scenarios more complex than it could be).

    > (Note how more complex snapshots with multiple elements are
    > still easy to assemble, because you specify the whole
    > snapshot list in the scenario.)

That's not the problem and as explained below I think it's even
YAGNI.

The problem is more around defining which information is needed
to check the conflict object (path, file-id) in which branch.

That's also why I refrained from trying to address multiple
conflicts tests at that point.

    > I think this would be easier and clearer than your zoo of
    > do_* methods.

Hehe, hello bialix's zoo :)

While working with these tests having the actions defined in the
right class made it easier to get them right (it's amazing how
hard it was to predict the correct conflict type when defining a
scenario, even with blackbox ones already available...). Well,
the actions themselves wasn't that hard, but what info was used
to create the conflict object ...

Read more...

4640. By Vincent Ladeuil on 2010-03-25

Merge fixes from review from the pre-requisite branch

Preview Diff

[H/L] Next/Prev Comment, [J/K] Next/Prev File, [N/P] Next/Prev Hunk
1=== modified file 'NEWS'
2--- NEWS 2010-03-25 09:11:32 +0000
3+++ NEWS 2010-03-25 09:11:32 +0000
4@@ -246,6 +246,10 @@
5 * Documentation about testing with ``subunit`` has been tweaked.
6 (Robert Collins)
7
8+* Known failures has been added for resolve --take-other on ParentLoop
9+ conflicts. This reflects bug #537956 without fixing it.
10+ (Vincent Ladeuil)
11+
12 * New ``bzrlib.tests.test_import_tariff`` can make assertions about what
13 Python modules are loaded, to guard against startup time or library
14 dependency regressions.
15
16=== modified file 'bzrlib/atomicfile.py'
17--- bzrlib/atomicfile.py 2010-02-22 08:28:41 +0000
18+++ bzrlib/atomicfile.py 2010-03-25 09:11:32 +0000
19@@ -1,4 +1,4 @@
20-# Copyright (C) 2004, 2005 Canonical Ltd
21+# Copyright (C) 2005, 2006, 2008, 2009, 2010 Canonical Ltd
22 #
23 # This program is free software; you can redistribute it and/or modify
24 # it under the terms of the GNU General Public License as published by
25
26=== modified file 'bzrlib/conflicts.py'
27--- bzrlib/conflicts.py 2010-03-25 09:11:32 +0000
28+++ bzrlib/conflicts.py 2010-03-25 09:11:32 +0000
29@@ -555,7 +555,7 @@
30
31
32 class ContentsConflict(PathConflict):
33- """The files are of different types, or not present"""
34+ """The files are of different types (or both binary), or not present"""
35
36 has_files = True
37
38@@ -707,7 +707,7 @@
39
40 typestring = 'parent loop'
41
42- format = 'Conflict moving %(conflict_path)s into %(path)s. %(action)s.'
43+ format = 'Conflict moving %(path)s into %(conflict_path)s. %(action)s.'
44
45 def action_take_this(self, tree):
46 # just acccept bzr proposal
47
48=== modified file 'bzrlib/tests/test_conflicts.py'
49--- bzrlib/tests/test_conflicts.py 2010-03-25 09:11:32 +0000
50+++ bzrlib/tests/test_conflicts.py 2010-03-25 09:11:32 +0000
51@@ -247,6 +247,8 @@
52 _item_id = None
53
54 # Set by _this_actions and other_actions
55+ # FIXME: rename them this_args and other_args so the tests can use them
56+ # more freely
57 _this_path = None
58 _this_id = None
59 _other_path = None
60@@ -318,23 +320,27 @@
61 def _get_check(self, name):
62 return getattr(self, 'check_%s' % name)
63
64- def assertConflict(self, wt):
65- confs = wt.conflicts()
66- self.assertLength(1, confs)
67- c = confs[0]
68- self.assertIsInstance(c, self._conflict_type)
69- self._assert_conflict(wt, c)
70-
71- def check_resolved(self, wt, path, action):
72- conflicts.resolve(wt, [path], action=action)
73- # Check that we don't have any conflicts nor unknown left
74- self.assertLength(0, wt.conflicts())
75- self.assertLength(0, list(wt.unknowns()))
76+ def do_nothing(self):
77+ return (None, None, [])
78
79 def do_create_file(self):
80 return ('file', 'file-id',
81 [('add', ('file', 'file-id', 'file', 'trunk content\n'))])
82
83+ def do_create_file_a(self):
84+ return ('file', 'file-a-id',
85+ [('add', ('file', 'file-a-id', 'file', 'file a content\n'))])
86+
87+ def check_file_content_a(self):
88+ self.assertFileEqual('file a content\n', 'branch/file')
89+
90+ def do_create_file_b(self):
91+ return ('file', 'file-b-id',
92+ [('add', ('file', 'file-b-id', 'file', 'file b content\n'))])
93+
94+ def check_file_content_b(self):
95+ self.assertFileEqual('file b content\n', 'branch/file')
96+
97 def do_create_dir(self):
98 return ('dir', 'dir-id', [('add', ('dir', 'dir-id', 'directory', ''))])
99
100@@ -358,6 +364,13 @@
101 self.failIfExists('branch/file')
102 self.failUnlessExists('branch/new-file')
103
104+ def do_rename_file2(self):
105+ return ('new-file2', 'file-id', [('rename', ('file', 'new-file2'))])
106+
107+ def check_file_renamed2(self):
108+ self.failIfExists('branch/file')
109+ self.failUnlessExists('branch/new-file2')
110+
111 def do_rename_dir(self):
112 return ('new-dir', 'dir-id', [('rename', ('dir', 'new-dir'))])
113
114@@ -384,17 +397,34 @@
115 wt.merge_from_branch(b, 'other')
116 return wt
117
118+ def assertConflict(self, wt):
119+ confs = wt.conflicts()
120+ self.assertLength(1, confs)
121+ c = confs[0]
122+ self.assertIsInstance(c, self._conflict_type)
123+ self._assert_conflict(wt, c)
124+
125+ def _get_resolve_path_arg(self, wt, action):
126+ return self._item_path
127+
128+ def check_resolved(self, wt, action):
129+ path = self._get_resolve_path_arg(wt, action)
130+ conflicts.resolve(wt, [path], action=action)
131+ # Check that we don't have any conflicts nor unknown left
132+ self.assertLength(0, wt.conflicts())
133+ self.assertLength(0, list(wt.unknowns()))
134+
135 def test_resolve_taking_this(self):
136 wt = self._merge_other_into_this()
137 self.assertConflict(wt)
138- self.check_resolved(wt, self._item_path, 'take_this')
139+ self.check_resolved(wt, 'take_this')
140 check_this = self._get_check(self._check_this)
141 check_this()
142
143 def test_resolve_taking_other(self):
144 wt = self._merge_other_into_this()
145 self.assertConflict(wt)
146- self.check_resolved(wt, self._item_path, 'take_other')
147+ self.check_resolved(wt, 'take_other')
148 check_other = self._get_check(self._check_other)
149 check_other()
150
151@@ -404,16 +434,12 @@
152 _conflict_type = conflicts.ContentsConflict,
153 @classmethod
154 def scenarios(klass):
155- common = dict(_actions_base='create_file',
156- _item_path='file', item_id='file-id',
157- )
158 base_scenarios = [
159 (('file_modified', dict(actions='modify_file',
160 check='file_has_more_content')),
161 ('file_deleted', dict(actions='delete_file',
162 check='file_doesnt_exist')),
163- dict(_actions_base='create_file',
164- _item_path='file', item_id='file-id',)),
165+ dict(_actions_base='create_file', _item_path='file')),
166 ]
167 return klass.mirror_scenarios(base_scenarios)
168
169@@ -430,25 +456,31 @@
170
171 @classmethod
172 def scenarios(klass):
173- for_dirs = dict(_actions_base='create_dir',
174+ for_file = dict(_actions_base='create_file',
175+ _item_path='new-file', _item_id='file-id',)
176+ for_dir = dict(_actions_base='create_dir',
177 _item_path='new-dir', _item_id='dir-id',)
178 base_scenarios = [
179 (('file_renamed',
180 dict(actions='rename_file', check='file_renamed')),
181 ('file_deleted',
182 dict(actions='delete_file', check='file_doesnt_exist')),
183- dict(_actions_base='create_file',
184- _item_path='new-file', _item_id='file-id',)),
185+ for_file),
186+ (('file_renamed',
187+ dict(actions='rename_file', check='file_renamed')),
188+ ('file_renamed2',
189+ dict(actions='rename_file2', check='file_renamed2')),
190+ for_file),
191 (('dir_renamed',
192 dict(actions='rename_dir', check='dir_renamed')),
193 ('dir_deleted',
194 dict(actions='delete_dir', check='dir_doesnt_exist')),
195- for_dirs),
196+ for_dir),
197 (('dir_renamed',
198 dict(actions='rename_dir', check='dir_renamed')),
199 ('dir_renamed2',
200 dict(actions='rename_dir2', check='dir_renamed2')),
201- for_dirs),
202+ for_dir),
203 ]
204 return klass.mirror_scenarios(base_scenarios)
205
206@@ -478,60 +510,25 @@
207 wt.set_conflicts(conflicts.ConflictList([old_c]))
208
209
210-class TestResolveDuplicateEntry(TestResolveConflicts):
211-
212- preamble = """
213-$ bzr init trunk
214-$ cd trunk
215-$ echo 'trunk content' >file
216-$ bzr add file
217-$ bzr commit -m 'Create trunk'
218-
219-$ echo 'trunk content too' >file2
220-$ bzr add file2
221-$ bzr commit -m 'Add file2 in trunk'
222-
223-$ bzr branch . -r 1 ../branch
224-$ cd ../branch
225-$ echo 'branch content' >file2
226-$ bzr add file2
227-$ bzr commit -m 'Add file2 in branch'
228-
229-$ bzr merge ../trunk
230-2>+N file2
231-2>R file2 => file2.moved
232-2>Conflict adding file file2. Moved existing file to file2.moved.
233-2>1 conflicts encountered.
234-"""
235-
236- def test_keep_this(self):
237- self.run_script("""
238-$ bzr rm file2 --force
239-$ bzr mv file2.moved file2
240-$ bzr resolve file2
241-$ bzr commit --strict -m 'No more conflicts nor unknown files'
242-""")
243-
244- def test_keep_other(self):
245- self.failIfExists('branch/file2.moved')
246- self.run_script("""
247-$ bzr rm file2.moved --force
248-$ bzr resolve file2
249-$ bzr commit --strict -m 'No more conflicts nor unknown files'
250-""")
251- self.failIfExists('branch/file2.moved')
252-
253- def test_resolve_taking_this(self):
254- self.run_script("""
255-$ bzr resolve --take-this file2
256-$ bzr commit --strict -m 'No more conflicts nor unknown files'
257-""")
258-
259- def test_resolve_taking_other(self):
260- self.run_script("""
261-$ bzr resolve --take-other file2
262-$ bzr commit --strict -m 'No more conflicts nor unknown files'
263-""")
264+class TestResolveDuplicateEntry(TestParametrizedResolveConflicts):
265+
266+ _conflict_type = conflicts.DuplicateEntry,
267+ @classmethod
268+ def scenarios(klass):
269+ base_scenarios = [
270+ (('filea_created', dict(actions='create_file_a',
271+ check='file_content_a')),
272+ ('fileb_created', dict(actions='create_file_b',
273+ check='file_content_b')),
274+ dict(_actions_base='nothing', _item_path='file')),
275+ ]
276+ return klass.mirror_scenarios(base_scenarios)
277+
278+ def assertDuplicateEntry(self, wt, c):
279+ self.assertEqual(self._this_id, c.file_id)
280+ self.assertEqual(self._item_path + '.moved', c.path)
281+ self.assertEqual(self._item_path, c.conflict_path)
282+ _assert_conflict = assertDuplicateEntry
283
284
285 class TestResolveUnversionedParent(TestResolveConflicts):
286@@ -701,56 +698,101 @@
287 """)
288
289
290-class OldTestResolvePathConflict(TestResolveConflicts):
291-
292- preamble = """
293-$ bzr init trunk
294-$ cd trunk
295-$ echo 'Boo!' >file
296-$ bzr add
297-$ bzr commit -m 'Create trunk'
298-
299-$ bzr mv file file-in-trunk
300-$ bzr commit -m 'Renamed to file-in-trunk'
301-
302-$ bzr branch . -r 1 ../branch
303-$ cd ../branch
304-$ bzr mv file file-in-branch
305-$ bzr commit -m 'Renamed to file-in-branch'
306-
307-$ bzr merge ../trunk
308-2>R file-in-branch => file-in-trunk
309-2>Path conflict: file-in-branch / file-in-trunk
310-2>1 conflicts encountered.
311-"""
312-
313- def test_keep_source(self):
314- self.run_script("""
315-$ bzr resolve file-in-trunk
316-$ bzr commit --strict -m 'No more conflicts nor unknown files'
317-""")
318-
319- def test_keep_target(self):
320- self.run_script("""
321-$ bzr mv file-in-trunk file-in-branch
322-$ bzr resolve file-in-branch
323-$ bzr commit --strict -m 'No more conflicts nor unknown files'
324-""")
325-
326- def test_resolve_taking_this(self):
327- self.run_script("""
328-$ bzr resolve --take-this file-in-branch
329-$ bzr commit --strict -m 'No more conflicts nor unknown files'
330-""")
331-
332- def test_resolve_taking_other(self):
333- self.run_script("""
334-$ bzr resolve --take-other file-in-branch
335-$ bzr commit --strict -m 'No more conflicts nor unknown files'
336-""")
337-
338-
339-class TestResolveParentLoop(TestResolveConflicts):
340+class TestResolveParentLoop(TestParametrizedResolveConflicts):
341+
342+ _conflict_type = conflicts.ParentLoop,
343+ @classmethod
344+ def scenarios(klass):
345+ base_scenarios = [
346+ (('dir1_into_dir2', dict(actions='move_dir1_into_dir2',
347+ check='dir1_moved')),
348+ ('dir2_into_dir1', dict(actions='move_dir2_into_dir1',
349+ check='dir2_moved')),
350+ dict(_actions_base='create_dir1_dir2')),
351+ (('dir1_into_dir4', dict(actions='move_dir1_into_dir4',
352+ check='dir1_2_moved')),
353+ ('dir3_into_dir2', dict(actions='move_dir3_into_dir2',
354+ check='dir3_4_moved')),
355+ dict(_actions_base='create_dir1_4')),
356+ ]
357+ return klass.mirror_scenarios(base_scenarios)
358+
359+ def do_create_dir1_dir2(self):
360+ return (None, None,
361+ [('add', ('dir1', 'dir1-id', 'directory', '')),
362+ ('add', ('dir2', 'dir2-id', 'directory', '')),
363+ ])
364+
365+ def do_move_dir1_into_dir2(self):
366+ # The arguments are the file-id to move and the targeted file-id dir.
367+ return ('dir1-id', 'dir2-id', [('rename', ('dir1', 'dir2/dir1'))])
368+
369+ def check_dir1_moved(self):
370+ self.failIfExists('branch/dir1')
371+ self.failUnlessExists('branch/dir2/dir1')
372+
373+ def do_move_dir2_into_dir1(self):
374+ # The arguments are the file-id to move and the targeted file-id dir.
375+ return ('dir2-id', 'dir1-id', [('rename', ('dir2', 'dir1/dir2'))])
376+
377+ def check_dir2_moved(self):
378+ self.failIfExists('branch/dir2')
379+ self.failUnlessExists('branch/dir1/dir2')
380+
381+ def do_create_dir1_4(self):
382+ return (None, None,
383+ [('add', ('dir1', 'dir1-id', 'directory', '')),
384+ ('add', ('dir1/dir2', 'dir2-id', 'directory', '')),
385+ ('add', ('dir3', 'dir3-id', 'directory', '')),
386+ ('add', ('dir3/dir4', 'dir4-id', 'directory', '')),
387+ ])
388+
389+ def do_move_dir1_into_dir4(self):
390+ # The arguments are the file-id to move and the targeted file-id dir.
391+ return ('dir1-id', 'dir4-id',
392+ [('rename', ('dir1', 'dir3/dir4/dir1'))])
393+
394+ def check_dir1_2_moved(self):
395+ self.failIfExists('branch/dir1')
396+ self.failUnlessExists('branch/dir3/dir4/dir1')
397+ self.failUnlessExists('branch/dir3/dir4/dir1/dir2')
398+
399+ def do_move_dir3_into_dir2(self):
400+ # The arguments are the file-id to move and the targeted file-id dir.
401+ return ('dir3-id', 'dir2-id',
402+ [('rename', ('dir3', 'dir1/dir2/dir3'))])
403+
404+ def check_dir3_4_moved(self):
405+ self.failIfExists('branch/dir3')
406+ self.failUnlessExists('branch/dir1/dir2/dir3')
407+ self.failUnlessExists('branch/dir1/dir2/dir3/dir4')
408+
409+ def _get_resolve_path_arg(self, wt, action):
410+ # ParentLoop is unsual as it says:
411+ # moving <conflict_path> into <path>. Cancelled move.
412+ # But since <path> doesn't exist in the working tree, we need to use
413+ # <conflict_path> instead
414+ path = wt.id2path(self._other_id)
415+ return path
416+
417+ def assertParentLoop(self, wt, c):
418+ if 'taking_other(' in self.id() and 'dir4' in self.id():
419+ raise tests.KnownFailure(
420+ "ParentLoop doesn't carry enough info to resolve")
421+ # The relevant file-ids are other_args swapped (which is the main
422+ # reason why they should be renamed other_args instead of Other_path
423+ # and other_id). In the conflict object, they represent:
424+ # c.file_id: the directory being moved
425+ # c.conflict_id_id: The target directory
426+ self.assertEqual(self._other_path, c.file_id)
427+ self.assertEqual(self._other_id, c.conflict_file_id)
428+ # The conflict paths are irrelevant (they are deterministic but not
429+ # worth checking since they don't provide the needed information
430+ # anyway)
431+ _assert_conflict = assertParentLoop
432+
433+
434+class OldTestResolveParentLoop(TestResolveConflicts):
435
436 preamble = """
437 $ bzr init trunk
438@@ -768,7 +810,7 @@
439 $ bzr commit -m 'Moved dir1 into dir2'
440
441 $ bzr merge ../trunk
442-2>Conflict moving dir2/dir1 into dir2. Cancelled move.
443+2>Conflict moving dir2 into dir2/dir1. Cancelled move.
444 2>1 conflicts encountered.
445 """
446
447
448=== modified file 'bzrlib/tests/test_transform.py'
449--- bzrlib/tests/test_transform.py 2010-02-09 19:04:02 +0000
450+++ bzrlib/tests/test_transform.py 2010-03-25 09:11:32 +0000
451@@ -735,7 +735,7 @@
452 ' versioned, but has versioned'
453 ' children. Versioned directory.')
454 self.assertEqual(conflicts_s[6], 'Conflict moving oz/emeraldcity into'
455- ' oz/emeraldcity. Cancelled move.')
456+ ' oz/emeraldcity. Cancelled move.')
457
458 def prepare_wrong_parent_kind(self):
459 tt, root = self.get_transform()
460
461=== modified file 'bzrlib/trace.py'
462--- bzrlib/trace.py 2010-03-06 05:28:17 +0000
463+++ bzrlib/trace.py 2010-03-25 09:11:32 +0000
464@@ -1,4 +1,4 @@
465-# Copyright (C) 2005, 2006, 2007, 2008, 2009, 2010 Canonical Ltd
466+# Copyright (C) 2005-2010 Canonical Ltd
467 #
468 # This program is free software; you can redistribute it and/or modify
469 # it under the terms of the GNU General Public License as published by