~bzr-pqm/bzr/bzr.dev

« back to all changes in this revision

Viewing changes to bzrlib/tests/test_transform.py

  • Committer: Martin Pool
  • Date: 2007-07-10 10:33:45 UTC
  • mto: This revision was merged to the branch mainline in revision 2599.
  • Revision ID: mbp@sourcefrog.net-20070710103345-hwfpnzxcx1mfv31x
Also check that option help ends in a period, and fix those that don't

Show diffs side-by-side

added added

removed removed

Lines of Context:
 
1
# Copyright (C) 2006 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., 59 Temple Place, Suite 330, Boston, MA  02111-1307  USA
 
16
 
 
17
import os
 
18
import stat
 
19
import sys
 
20
 
 
21
from bzrlib import (
 
22
    errors,
 
23
    generate_ids,
 
24
    symbol_versioning,
 
25
    tests,
 
26
    urlutils,
 
27
    )
 
28
from bzrlib.bzrdir import BzrDir
 
29
from bzrlib.conflicts import (DuplicateEntry, DuplicateID, MissingParent,
 
30
                              UnversionedParent, ParentLoop, DeletingParent,)
 
31
from bzrlib.errors import (DuplicateKey, MalformedTransform, NoSuchFile,
 
32
                           ReusingTransform, CantMoveRoot, 
 
33
                           PathsNotVersionedError, ExistingLimbo,
 
34
                           ImmortalLimbo, LockError)
 
35
from bzrlib.osutils import file_kind, has_symlinks, pathjoin
 
36
from bzrlib.merge import Merge3Merger
 
37
from bzrlib.tests import TestCaseInTempDir, TestSkipped, TestCase
 
38
from bzrlib.transform import (TreeTransform, ROOT_PARENT, FinalPaths, 
 
39
                              resolve_conflicts, cook_conflicts, 
 
40
                              find_interesting, build_tree, get_backup_name)
 
41
 
 
42
 
 
43
class TestTreeTransform(tests.TestCaseWithTransport):
 
44
 
 
45
    def setUp(self):
 
46
        super(TestTreeTransform, self).setUp()
 
47
        self.wt = self.make_branch_and_tree('.', format='dirstate-with-subtree')
 
48
        os.chdir('..')
 
49
 
 
50
    def get_transform(self):
 
51
        transform = TreeTransform(self.wt)
 
52
        #self.addCleanup(transform.finalize)
 
53
        return transform, transform.root
 
54
 
 
55
    def test_existing_limbo(self):
 
56
        limbo_name = urlutils.local_path_from_url(
 
57
            self.wt._control_files.controlfilename('limbo'))
 
58
        transform, root = self.get_transform()
 
59
        os.mkdir(pathjoin(limbo_name, 'hehe'))
 
60
        self.assertRaises(ImmortalLimbo, transform.apply)
 
61
        self.assertRaises(LockError, self.wt.unlock)
 
62
        self.assertRaises(ExistingLimbo, self.get_transform)
 
63
        self.assertRaises(LockError, self.wt.unlock)
 
64
        os.rmdir(pathjoin(limbo_name, 'hehe'))
 
65
        os.rmdir(limbo_name)
 
66
        transform, root = self.get_transform()
 
67
        transform.apply()
 
68
 
 
69
    def test_build(self):
 
70
        transform, root = self.get_transform() 
 
71
        self.assertIs(transform.get_tree_parent(root), ROOT_PARENT)
 
72
        imaginary_id = transform.trans_id_tree_path('imaginary')
 
73
        imaginary_id2 = transform.trans_id_tree_path('imaginary/')
 
74
        self.assertEqual(imaginary_id, imaginary_id2)
 
75
        self.assertEqual(transform.get_tree_parent(imaginary_id), root)
 
76
        self.assertEqual(transform.final_kind(root), 'directory')
 
77
        self.assertEqual(transform.final_file_id(root), self.wt.get_root_id())
 
78
        trans_id = transform.create_path('name', root)
 
79
        self.assertIs(transform.final_file_id(trans_id), None)
 
80
        self.assertRaises(NoSuchFile, transform.final_kind, trans_id)
 
81
        transform.create_file('contents', trans_id)
 
82
        transform.set_executability(True, trans_id)
 
83
        transform.version_file('my_pretties', trans_id)
 
84
        self.assertRaises(DuplicateKey, transform.version_file,
 
85
                          'my_pretties', trans_id)
 
86
        self.assertEqual(transform.final_file_id(trans_id), 'my_pretties')
 
87
        self.assertEqual(transform.final_parent(trans_id), root)
 
88
        self.assertIs(transform.final_parent(root), ROOT_PARENT)
 
89
        self.assertIs(transform.get_tree_parent(root), ROOT_PARENT)
 
90
        oz_id = transform.create_path('oz', root)
 
91
        transform.create_directory(oz_id)
 
92
        transform.version_file('ozzie', oz_id)
 
93
        trans_id2 = transform.create_path('name2', root)
 
94
        transform.create_file('contents', trans_id2)
 
95
        transform.set_executability(False, trans_id2)
 
96
        transform.version_file('my_pretties2', trans_id2)
 
97
        modified_paths = transform.apply().modified_paths
 
98
        self.assertEqual('contents', self.wt.get_file_byname('name').read())
 
99
        self.assertEqual(self.wt.path2id('name'), 'my_pretties')
 
100
        self.assertIs(self.wt.is_executable('my_pretties'), True)
 
101
        self.assertIs(self.wt.is_executable('my_pretties2'), False)
 
102
        self.assertEqual('directory', file_kind(self.wt.abspath('oz')))
 
103
        self.assertEqual(len(modified_paths), 3)
 
104
        tree_mod_paths = [self.wt.id2abspath(f) for f in 
 
105
                          ('ozzie', 'my_pretties', 'my_pretties2')]
 
106
        self.assertSubset(tree_mod_paths, modified_paths)
 
107
        # is it safe to finalize repeatedly?
 
108
        transform.finalize()
 
109
        transform.finalize()
 
110
 
 
111
    def test_convenience(self):
 
112
        transform, root = self.get_transform()
 
113
        trans_id = transform.new_file('name', root, 'contents', 
 
114
                                      'my_pretties', True)
 
115
        oz = transform.new_directory('oz', root, 'oz-id')
 
116
        dorothy = transform.new_directory('dorothy', oz, 'dorothy-id')
 
117
        toto = transform.new_file('toto', dorothy, 'toto-contents', 
 
118
                                  'toto-id', False)
 
119
 
 
120
        self.assertEqual(len(transform.find_conflicts()), 0)
 
121
        transform.apply()
 
122
        self.assertRaises(ReusingTransform, transform.find_conflicts)
 
123
        self.assertEqual('contents', file(self.wt.abspath('name')).read())
 
124
        self.assertEqual(self.wt.path2id('name'), 'my_pretties')
 
125
        self.assertIs(self.wt.is_executable('my_pretties'), True)
 
126
        self.assertEqual(self.wt.path2id('oz'), 'oz-id')
 
127
        self.assertEqual(self.wt.path2id('oz/dorothy'), 'dorothy-id')
 
128
        self.assertEqual(self.wt.path2id('oz/dorothy/toto'), 'toto-id')
 
129
 
 
130
        self.assertEqual('toto-contents',
 
131
                         self.wt.get_file_byname('oz/dorothy/toto').read())
 
132
        self.assertIs(self.wt.is_executable('toto-id'), False)
 
133
 
 
134
    def test_tree_reference(self):
 
135
        transform, root = self.get_transform()
 
136
        tree = transform._tree
 
137
        trans_id = transform.new_directory('reference', root, 'subtree-id')
 
138
        transform.set_tree_reference('subtree-revision', trans_id)
 
139
        transform.apply()
 
140
        tree.lock_read()
 
141
        self.addCleanup(tree.unlock)
 
142
        self.assertEqual('subtree-revision',
 
143
                         tree.inventory['subtree-id'].reference_revision)
 
144
 
 
145
    def test_conflicts(self):
 
146
        transform, root = self.get_transform()
 
147
        trans_id = transform.new_file('name', root, 'contents', 
 
148
                                      'my_pretties')
 
149
        self.assertEqual(len(transform.find_conflicts()), 0)
 
150
        trans_id2 = transform.new_file('name', root, 'Crontents', 'toto')
 
151
        self.assertEqual(transform.find_conflicts(), 
 
152
                         [('duplicate', trans_id, trans_id2, 'name')])
 
153
        self.assertRaises(MalformedTransform, transform.apply)
 
154
        transform.adjust_path('name', trans_id, trans_id2)
 
155
        self.assertEqual(transform.find_conflicts(), 
 
156
                         [('non-directory parent', trans_id)])
 
157
        tinman_id = transform.trans_id_tree_path('tinman')
 
158
        transform.adjust_path('name', tinman_id, trans_id2)
 
159
        self.assertEqual(transform.find_conflicts(), 
 
160
                         [('unversioned parent', tinman_id), 
 
161
                          ('missing parent', tinman_id)])
 
162
        lion_id = transform.create_path('lion', root)
 
163
        self.assertEqual(transform.find_conflicts(), 
 
164
                         [('unversioned parent', tinman_id), 
 
165
                          ('missing parent', tinman_id)])
 
166
        transform.adjust_path('name', lion_id, trans_id2)
 
167
        self.assertEqual(transform.find_conflicts(), 
 
168
                         [('unversioned parent', lion_id),
 
169
                          ('missing parent', lion_id)])
 
170
        transform.version_file("Courage", lion_id)
 
171
        self.assertEqual(transform.find_conflicts(), 
 
172
                         [('missing parent', lion_id), 
 
173
                          ('versioning no contents', lion_id)])
 
174
        transform.adjust_path('name2', root, trans_id2)
 
175
        self.assertEqual(transform.find_conflicts(), 
 
176
                         [('versioning no contents', lion_id)])
 
177
        transform.create_file('Contents, okay?', lion_id)
 
178
        transform.adjust_path('name2', trans_id2, trans_id2)
 
179
        self.assertEqual(transform.find_conflicts(), 
 
180
                         [('parent loop', trans_id2), 
 
181
                          ('non-directory parent', trans_id2)])
 
182
        transform.adjust_path('name2', root, trans_id2)
 
183
        oz_id = transform.new_directory('oz', root)
 
184
        transform.set_executability(True, oz_id)
 
185
        self.assertEqual(transform.find_conflicts(), 
 
186
                         [('unversioned executability', oz_id)])
 
187
        transform.version_file('oz-id', oz_id)
 
188
        self.assertEqual(transform.find_conflicts(), 
 
189
                         [('non-file executability', oz_id)])
 
190
        transform.set_executability(None, oz_id)
 
191
        tip_id = transform.new_file('tip', oz_id, 'ozma', 'tip-id')
 
192
        transform.apply()
 
193
        self.assertEqual(self.wt.path2id('name'), 'my_pretties')
 
194
        self.assertEqual('contents', file(self.wt.abspath('name')).read())
 
195
        transform2, root = self.get_transform()
 
196
        oz_id = transform2.trans_id_tree_file_id('oz-id')
 
197
        newtip = transform2.new_file('tip', oz_id, 'other', 'tip-id')
 
198
        result = transform2.find_conflicts()
 
199
        fp = FinalPaths(transform2)
 
200
        self.assert_('oz/tip' in transform2._tree_path_ids)
 
201
        self.assertEqual(fp.get_path(newtip), pathjoin('oz', 'tip'))
 
202
        self.assertEqual(len(result), 2)
 
203
        self.assertEqual((result[0][0], result[0][1]), 
 
204
                         ('duplicate', newtip))
 
205
        self.assertEqual((result[1][0], result[1][2]), 
 
206
                         ('duplicate id', newtip))
 
207
        transform2.finalize()
 
208
        transform3 = TreeTransform(self.wt)
 
209
        self.addCleanup(transform3.finalize)
 
210
        oz_id = transform3.trans_id_tree_file_id('oz-id')
 
211
        transform3.delete_contents(oz_id)
 
212
        self.assertEqual(transform3.find_conflicts(), 
 
213
                         [('missing parent', oz_id)])
 
214
        root_id = transform3.root
 
215
        tip_id = transform3.trans_id_tree_file_id('tip-id')
 
216
        transform3.adjust_path('tip', root_id, tip_id)
 
217
        transform3.apply()
 
218
 
 
219
    def test_add_del(self):
 
220
        start, root = self.get_transform()
 
221
        start.new_directory('a', root, 'a')
 
222
        start.apply()
 
223
        transform, root = self.get_transform()
 
224
        transform.delete_versioned(transform.trans_id_tree_file_id('a'))
 
225
        transform.new_directory('a', root, 'a')
 
226
        transform.apply()
 
227
 
 
228
    def test_unversioning(self):
 
229
        create_tree, root = self.get_transform()
 
230
        parent_id = create_tree.new_directory('parent', root, 'parent-id')
 
231
        create_tree.new_file('child', parent_id, 'child', 'child-id')
 
232
        create_tree.apply()
 
233
        unversion = TreeTransform(self.wt)
 
234
        self.addCleanup(unversion.finalize)
 
235
        parent = unversion.trans_id_tree_path('parent')
 
236
        unversion.unversion_file(parent)
 
237
        self.assertEqual(unversion.find_conflicts(), 
 
238
                         [('unversioned parent', parent_id)])
 
239
        file_id = unversion.trans_id_tree_file_id('child-id')
 
240
        unversion.unversion_file(file_id)
 
241
        unversion.apply()
 
242
 
 
243
    def test_name_invariants(self):
 
244
        create_tree, root = self.get_transform()
 
245
        # prepare tree
 
246
        root = create_tree.root
 
247
        create_tree.new_file('name1', root, 'hello1', 'name1')
 
248
        create_tree.new_file('name2', root, 'hello2', 'name2')
 
249
        ddir = create_tree.new_directory('dying_directory', root, 'ddir')
 
250
        create_tree.new_file('dying_file', ddir, 'goodbye1', 'dfile')
 
251
        create_tree.new_file('moving_file', ddir, 'later1', 'mfile')
 
252
        create_tree.new_file('moving_file2', root, 'later2', 'mfile2')
 
253
        create_tree.apply()
 
254
 
 
255
        mangle_tree,root = self.get_transform()
 
256
        root = mangle_tree.root
 
257
        #swap names
 
258
        name1 = mangle_tree.trans_id_tree_file_id('name1')
 
259
        name2 = mangle_tree.trans_id_tree_file_id('name2')
 
260
        mangle_tree.adjust_path('name2', root, name1)
 
261
        mangle_tree.adjust_path('name1', root, name2)
 
262
 
 
263
        #tests for deleting parent directories 
 
264
        ddir = mangle_tree.trans_id_tree_file_id('ddir')
 
265
        mangle_tree.delete_contents(ddir)
 
266
        dfile = mangle_tree.trans_id_tree_file_id('dfile')
 
267
        mangle_tree.delete_versioned(dfile)
 
268
        mangle_tree.unversion_file(dfile)
 
269
        mfile = mangle_tree.trans_id_tree_file_id('mfile')
 
270
        mangle_tree.adjust_path('mfile', root, mfile)
 
271
 
 
272
        #tests for adding parent directories
 
273
        newdir = mangle_tree.new_directory('new_directory', root, 'newdir')
 
274
        mfile2 = mangle_tree.trans_id_tree_file_id('mfile2')
 
275
        mangle_tree.adjust_path('mfile2', newdir, mfile2)
 
276
        mangle_tree.new_file('newfile', newdir, 'hello3', 'dfile')
 
277
        self.assertEqual(mangle_tree.final_file_id(mfile2), 'mfile2')
 
278
        self.assertEqual(mangle_tree.final_parent(mfile2), newdir)
 
279
        self.assertEqual(mangle_tree.final_file_id(mfile2), 'mfile2')
 
280
        mangle_tree.apply()
 
281
        self.assertEqual(file(self.wt.abspath('name1')).read(), 'hello2')
 
282
        self.assertEqual(file(self.wt.abspath('name2')).read(), 'hello1')
 
283
        mfile2_path = self.wt.abspath(pathjoin('new_directory','mfile2'))
 
284
        self.assertEqual(mangle_tree.final_parent(mfile2), newdir)
 
285
        self.assertEqual(file(mfile2_path).read(), 'later2')
 
286
        self.assertEqual(self.wt.id2path('mfile2'), 'new_directory/mfile2')
 
287
        self.assertEqual(self.wt.path2id('new_directory/mfile2'), 'mfile2')
 
288
        newfile_path = self.wt.abspath(pathjoin('new_directory','newfile'))
 
289
        self.assertEqual(file(newfile_path).read(), 'hello3')
 
290
        self.assertEqual(self.wt.path2id('dying_directory'), 'ddir')
 
291
        self.assertIs(self.wt.path2id('dying_directory/dying_file'), None)
 
292
        mfile2_path = self.wt.abspath(pathjoin('new_directory','mfile2'))
 
293
 
 
294
    def test_both_rename(self):
 
295
        create_tree,root = self.get_transform()
 
296
        newdir = create_tree.new_directory('selftest', root, 'selftest-id')
 
297
        create_tree.new_file('blackbox.py', newdir, 'hello1', 'blackbox-id')
 
298
        create_tree.apply()        
 
299
        mangle_tree,root = self.get_transform()
 
300
        selftest = mangle_tree.trans_id_tree_file_id('selftest-id')
 
301
        blackbox = mangle_tree.trans_id_tree_file_id('blackbox-id')
 
302
        mangle_tree.adjust_path('test', root, selftest)
 
303
        mangle_tree.adjust_path('test_too_much', root, selftest)
 
304
        mangle_tree.set_executability(True, blackbox)
 
305
        mangle_tree.apply()
 
306
 
 
307
    def test_both_rename2(self):
 
308
        create_tree,root = self.get_transform()
 
309
        bzrlib = create_tree.new_directory('bzrlib', root, 'bzrlib-id')
 
310
        tests = create_tree.new_directory('tests', bzrlib, 'tests-id')
 
311
        blackbox = create_tree.new_directory('blackbox', tests, 'blackbox-id')
 
312
        create_tree.new_file('test_too_much.py', blackbox, 'hello1', 
 
313
                             'test_too_much-id')
 
314
        create_tree.apply()        
 
315
        mangle_tree,root = self.get_transform()
 
316
        bzrlib = mangle_tree.trans_id_tree_file_id('bzrlib-id')
 
317
        tests = mangle_tree.trans_id_tree_file_id('tests-id')
 
318
        test_too_much = mangle_tree.trans_id_tree_file_id('test_too_much-id')
 
319
        mangle_tree.adjust_path('selftest', bzrlib, tests)
 
320
        mangle_tree.adjust_path('blackbox.py', tests, test_too_much) 
 
321
        mangle_tree.set_executability(True, test_too_much)
 
322
        mangle_tree.apply()
 
323
 
 
324
    def test_both_rename3(self):
 
325
        create_tree,root = self.get_transform()
 
326
        tests = create_tree.new_directory('tests', root, 'tests-id')
 
327
        create_tree.new_file('test_too_much.py', tests, 'hello1', 
 
328
                             'test_too_much-id')
 
329
        create_tree.apply()        
 
330
        mangle_tree,root = self.get_transform()
 
331
        tests = mangle_tree.trans_id_tree_file_id('tests-id')
 
332
        test_too_much = mangle_tree.trans_id_tree_file_id('test_too_much-id')
 
333
        mangle_tree.adjust_path('selftest', root, tests)
 
334
        mangle_tree.adjust_path('blackbox.py', tests, test_too_much) 
 
335
        mangle_tree.set_executability(True, test_too_much)
 
336
        mangle_tree.apply()
 
337
 
 
338
    def test_move_dangling_ie(self):
 
339
        create_tree, root = self.get_transform()
 
340
        # prepare tree
 
341
        root = create_tree.root
 
342
        create_tree.new_file('name1', root, 'hello1', 'name1')
 
343
        create_tree.apply()
 
344
        delete_contents, root = self.get_transform()
 
345
        file = delete_contents.trans_id_tree_file_id('name1')
 
346
        delete_contents.delete_contents(file)
 
347
        delete_contents.apply()
 
348
        move_id, root = self.get_transform()
 
349
        name1 = move_id.trans_id_tree_file_id('name1')
 
350
        newdir = move_id.new_directory('dir', root, 'newdir')
 
351
        move_id.adjust_path('name2', newdir, name1)
 
352
        move_id.apply()
 
353
        
 
354
    def test_replace_dangling_ie(self):
 
355
        create_tree, root = self.get_transform()
 
356
        # prepare tree
 
357
        root = create_tree.root
 
358
        create_tree.new_file('name1', root, 'hello1', 'name1')
 
359
        create_tree.apply()
 
360
        delete_contents = TreeTransform(self.wt)
 
361
        self.addCleanup(delete_contents.finalize)
 
362
        file = delete_contents.trans_id_tree_file_id('name1')
 
363
        delete_contents.delete_contents(file)
 
364
        delete_contents.apply()
 
365
        delete_contents.finalize()
 
366
        replace = TreeTransform(self.wt)
 
367
        self.addCleanup(replace.finalize)
 
368
        name2 = replace.new_file('name2', root, 'hello2', 'name1')
 
369
        conflicts = replace.find_conflicts()
 
370
        name1 = replace.trans_id_tree_file_id('name1')
 
371
        self.assertEqual(conflicts, [('duplicate id', name1, name2)])
 
372
        resolve_conflicts(replace)
 
373
        replace.apply()
 
374
 
 
375
    def test_symlinks(self):
 
376
        if not has_symlinks():
 
377
            raise TestSkipped('Symlinks are not supported on this platform')
 
378
        transform,root = self.get_transform()
 
379
        oz_id = transform.new_directory('oz', root, 'oz-id')
 
380
        wizard = transform.new_symlink('wizard', oz_id, 'wizard-target', 
 
381
                                       'wizard-id')
 
382
        wiz_id = transform.create_path('wizard2', oz_id)
 
383
        transform.create_symlink('behind_curtain', wiz_id)
 
384
        transform.version_file('wiz-id2', wiz_id)            
 
385
        transform.set_executability(True, wiz_id)
 
386
        self.assertEqual(transform.find_conflicts(), 
 
387
                         [('non-file executability', wiz_id)])
 
388
        transform.set_executability(None, wiz_id)
 
389
        transform.apply()
 
390
        self.assertEqual(self.wt.path2id('oz/wizard'), 'wizard-id')
 
391
        self.assertEqual(file_kind(self.wt.abspath('oz/wizard')), 'symlink')
 
392
        self.assertEqual(os.readlink(self.wt.abspath('oz/wizard2')), 
 
393
                         'behind_curtain')
 
394
        self.assertEqual(os.readlink(self.wt.abspath('oz/wizard')),
 
395
                         'wizard-target')
 
396
 
 
397
    def get_conflicted(self):
 
398
        create,root = self.get_transform()
 
399
        create.new_file('dorothy', root, 'dorothy', 'dorothy-id')
 
400
        oz = create.new_directory('oz', root, 'oz-id')
 
401
        create.new_directory('emeraldcity', oz, 'emerald-id')
 
402
        create.apply()
 
403
        conflicts,root = self.get_transform()
 
404
        # set up duplicate entry, duplicate id
 
405
        new_dorothy = conflicts.new_file('dorothy', root, 'dorothy', 
 
406
                                         'dorothy-id')
 
407
        old_dorothy = conflicts.trans_id_tree_file_id('dorothy-id')
 
408
        oz = conflicts.trans_id_tree_file_id('oz-id')
 
409
        # set up DeletedParent parent conflict
 
410
        conflicts.delete_versioned(oz)
 
411
        emerald = conflicts.trans_id_tree_file_id('emerald-id')
 
412
        # set up MissingParent conflict
 
413
        munchkincity = conflicts.trans_id_file_id('munchkincity-id')
 
414
        conflicts.adjust_path('munchkincity', root, munchkincity)
 
415
        conflicts.new_directory('auntem', munchkincity, 'auntem-id')
 
416
        # set up parent loop
 
417
        conflicts.adjust_path('emeraldcity', emerald, emerald)
 
418
        return conflicts, emerald, oz, old_dorothy, new_dorothy
 
419
 
 
420
    def test_conflict_resolution(self):
 
421
        conflicts, emerald, oz, old_dorothy, new_dorothy =\
 
422
            self.get_conflicted()
 
423
        resolve_conflicts(conflicts)
 
424
        self.assertEqual(conflicts.final_name(old_dorothy), 'dorothy.moved')
 
425
        self.assertIs(conflicts.final_file_id(old_dorothy), None)
 
426
        self.assertEqual(conflicts.final_name(new_dorothy), 'dorothy')
 
427
        self.assertEqual(conflicts.final_file_id(new_dorothy), 'dorothy-id')
 
428
        self.assertEqual(conflicts.final_parent(emerald), oz)
 
429
        conflicts.apply()
 
430
 
 
431
    def test_cook_conflicts(self):
 
432
        tt, emerald, oz, old_dorothy, new_dorothy = self.get_conflicted()
 
433
        raw_conflicts = resolve_conflicts(tt)
 
434
        cooked_conflicts = cook_conflicts(raw_conflicts, tt)
 
435
        duplicate = DuplicateEntry('Moved existing file to', 'dorothy.moved', 
 
436
                                   'dorothy', None, 'dorothy-id')
 
437
        self.assertEqual(cooked_conflicts[0], duplicate)
 
438
        duplicate_id = DuplicateID('Unversioned existing file', 
 
439
                                   'dorothy.moved', 'dorothy', None,
 
440
                                   'dorothy-id')
 
441
        self.assertEqual(cooked_conflicts[1], duplicate_id)
 
442
        missing_parent = MissingParent('Created directory', 'munchkincity',
 
443
                                       'munchkincity-id')
 
444
        deleted_parent = DeletingParent('Not deleting', 'oz', 'oz-id')
 
445
        self.assertEqual(cooked_conflicts[2], missing_parent)
 
446
        unversioned_parent = UnversionedParent('Versioned directory',
 
447
                                               'munchkincity',
 
448
                                               'munchkincity-id')
 
449
        unversioned_parent2 = UnversionedParent('Versioned directory', 'oz',
 
450
                                               'oz-id')
 
451
        self.assertEqual(cooked_conflicts[3], unversioned_parent)
 
452
        parent_loop = ParentLoop('Cancelled move', 'oz/emeraldcity', 
 
453
                                 'oz/emeraldcity', 'emerald-id', 'emerald-id')
 
454
        self.assertEqual(cooked_conflicts[4], deleted_parent)
 
455
        self.assertEqual(cooked_conflicts[5], unversioned_parent2)
 
456
        self.assertEqual(cooked_conflicts[6], parent_loop)
 
457
        self.assertEqual(len(cooked_conflicts), 7)
 
458
        tt.finalize()
 
459
 
 
460
    def test_string_conflicts(self):
 
461
        tt, emerald, oz, old_dorothy, new_dorothy = self.get_conflicted()
 
462
        raw_conflicts = resolve_conflicts(tt)
 
463
        cooked_conflicts = cook_conflicts(raw_conflicts, tt)
 
464
        tt.finalize()
 
465
        conflicts_s = [str(c) for c in cooked_conflicts]
 
466
        self.assertEqual(len(cooked_conflicts), len(conflicts_s))
 
467
        self.assertEqual(conflicts_s[0], 'Conflict adding file dorothy.  '
 
468
                                         'Moved existing file to '
 
469
                                         'dorothy.moved.')
 
470
        self.assertEqual(conflicts_s[1], 'Conflict adding id to dorothy.  '
 
471
                                         'Unversioned existing file '
 
472
                                         'dorothy.moved.')
 
473
        self.assertEqual(conflicts_s[2], 'Conflict adding files to'
 
474
                                         ' munchkincity.  Created directory.')
 
475
        self.assertEqual(conflicts_s[3], 'Conflict because munchkincity is not'
 
476
                                         ' versioned, but has versioned'
 
477
                                         ' children.  Versioned directory.')
 
478
        self.assertEqualDiff(conflicts_s[4], "Conflict: can't delete oz because it"
 
479
                                         " is not empty.  Not deleting.")
 
480
        self.assertEqual(conflicts_s[5], 'Conflict because oz is not'
 
481
                                         ' versioned, but has versioned'
 
482
                                         ' children.  Versioned directory.')
 
483
        self.assertEqual(conflicts_s[6], 'Conflict moving oz/emeraldcity into'
 
484
                                         ' oz/emeraldcity.  Cancelled move.')
 
485
 
 
486
    def test_moving_versioned_directories(self):
 
487
        create, root = self.get_transform()
 
488
        kansas = create.new_directory('kansas', root, 'kansas-id')
 
489
        create.new_directory('house', kansas, 'house-id')
 
490
        create.new_directory('oz', root, 'oz-id')
 
491
        create.apply()
 
492
        cyclone, root = self.get_transform()
 
493
        oz = cyclone.trans_id_tree_file_id('oz-id')
 
494
        house = cyclone.trans_id_tree_file_id('house-id')
 
495
        cyclone.adjust_path('house', oz, house)
 
496
        cyclone.apply()
 
497
 
 
498
    def test_moving_root(self):
 
499
        create, root = self.get_transform()
 
500
        fun = create.new_directory('fun', root, 'fun-id')
 
501
        create.new_directory('sun', root, 'sun-id')
 
502
        create.new_directory('moon', root, 'moon')
 
503
        create.apply()
 
504
        transform, root = self.get_transform()
 
505
        transform.adjust_root_path('oldroot', fun)
 
506
        new_root=transform.trans_id_tree_path('')
 
507
        transform.version_file('new-root', new_root)
 
508
        transform.apply()
 
509
 
 
510
    def test_renames(self):
 
511
        create, root = self.get_transform()
 
512
        old = create.new_directory('old-parent', root, 'old-id')
 
513
        intermediate = create.new_directory('intermediate', old, 'im-id')
 
514
        myfile = create.new_file('myfile', intermediate, 'myfile-text',
 
515
                                 'myfile-id')
 
516
        create.apply()
 
517
        rename, root = self.get_transform()
 
518
        old = rename.trans_id_file_id('old-id')
 
519
        rename.adjust_path('new', root, old)
 
520
        myfile = rename.trans_id_file_id('myfile-id')
 
521
        rename.set_executability(True, myfile)
 
522
        rename.apply()
 
523
 
 
524
    def test_find_interesting(self):
 
525
        create, root = self.get_transform()
 
526
        wt = create._tree
 
527
        create.new_file('vfile', root, 'myfile-text', 'myfile-id')
 
528
        create.new_file('uvfile', root, 'othertext')
 
529
        create.apply()
 
530
        result = self.applyDeprecated(symbol_versioning.zero_fifteen,
 
531
            find_interesting, wt, wt, ['vfile'])
 
532
        self.assertEqual(result, set(['myfile-id']))
 
533
 
 
534
    def test_set_executability_order(self):
 
535
        """Ensure that executability behaves the same, no matter what order.
 
536
        
 
537
        - create file and set executability simultaneously
 
538
        - create file and set executability afterward
 
539
        - unsetting the executability of a file whose executability has not been
 
540
        declared should throw an exception (this may happen when a
 
541
        merge attempts to create a file with a duplicate ID)
 
542
        """
 
543
        transform, root = self.get_transform()
 
544
        wt = transform._tree
 
545
        transform.new_file('set_on_creation', root, 'Set on creation', 'soc',
 
546
                           True)
 
547
        sac = transform.new_file('set_after_creation', root,
 
548
                                 'Set after creation', 'sac')
 
549
        transform.set_executability(True, sac)
 
550
        uws = transform.new_file('unset_without_set', root, 'Unset badly',
 
551
                                 'uws')
 
552
        self.assertRaises(KeyError, transform.set_executability, None, uws)
 
553
        transform.apply()
 
554
        self.assertTrue(wt.is_executable('soc'))
 
555
        self.assertTrue(wt.is_executable('sac'))
 
556
 
 
557
    def test_preserve_mode(self):
 
558
        """File mode is preserved when replacing content"""
 
559
        if sys.platform == 'win32':
 
560
            raise TestSkipped('chmod has no effect on win32')
 
561
        transform, root = self.get_transform()
 
562
        transform.new_file('file1', root, 'contents', 'file1-id', True)
 
563
        transform.apply()
 
564
        self.assertTrue(self.wt.is_executable('file1-id'))
 
565
        transform, root = self.get_transform()
 
566
        file1_id = transform.trans_id_tree_file_id('file1-id')
 
567
        transform.delete_contents(file1_id)
 
568
        transform.create_file('contents2', file1_id)
 
569
        transform.apply()
 
570
        self.assertTrue(self.wt.is_executable('file1-id'))
 
571
 
 
572
    def test__set_mode_stats_correctly(self):
 
573
        """_set_mode stats to determine file mode."""
 
574
        if sys.platform == 'win32':
 
575
            raise TestSkipped('chmod has no effect on win32')
 
576
 
 
577
        stat_paths = []
 
578
        real_stat = os.stat
 
579
        def instrumented_stat(path):
 
580
            stat_paths.append(path)
 
581
            return real_stat(path)
 
582
 
 
583
        transform, root = self.get_transform()
 
584
 
 
585
        bar1_id = transform.new_file('bar', root, 'bar contents 1\n',
 
586
                                     file_id='bar-id-1', executable=False)
 
587
        transform.apply()
 
588
 
 
589
        transform, root = self.get_transform()
 
590
        bar1_id = transform.trans_id_tree_path('bar')
 
591
        bar2_id = transform.trans_id_tree_path('bar2')
 
592
        try:
 
593
            os.stat = instrumented_stat
 
594
            transform.create_file('bar2 contents\n', bar2_id, mode_id=bar1_id)
 
595
        finally:
 
596
            os.stat = real_stat
 
597
            transform.finalize()
 
598
 
 
599
        bar1_abspath = self.wt.abspath('bar')
 
600
        self.assertEqual([bar1_abspath], stat_paths)
 
601
 
 
602
    def test_iter_changes(self):
 
603
        self.wt.set_root_id('eert_toor')
 
604
        transform, root = self.get_transform()
 
605
        transform.new_file('old', root, 'blah', 'id-1', True)
 
606
        transform.apply()
 
607
        transform, root = self.get_transform()
 
608
        try:
 
609
            self.assertEqual([], list(transform._iter_changes()))
 
610
            old = transform.trans_id_tree_file_id('id-1')
 
611
            transform.unversion_file(old)
 
612
            self.assertEqual([('id-1', ('old', None), False, (True, False),
 
613
                ('eert_toor', 'eert_toor'), ('old', 'old'), ('file', 'file'),
 
614
                (True, True))], list(transform._iter_changes()))
 
615
            transform.new_directory('new', root, 'id-1')
 
616
            self.assertEqual([('id-1', ('old', 'new'), True, (True, True),
 
617
                ('eert_toor', 'eert_toor'), ('old', 'new'),
 
618
                ('file', 'directory'),
 
619
                (True, False))], list(transform._iter_changes()))
 
620
        finally:
 
621
            transform.finalize()
 
622
 
 
623
    def test_iter_changes_new(self):
 
624
        self.wt.set_root_id('eert_toor')
 
625
        transform, root = self.get_transform()
 
626
        transform.new_file('old', root, 'blah')
 
627
        transform.apply()
 
628
        transform, root = self.get_transform()
 
629
        try:
 
630
            old = transform.trans_id_tree_path('old')
 
631
            transform.version_file('id-1', old)
 
632
            self.assertEqual([('id-1', (None, 'old'), False, (False, True),
 
633
                ('eert_toor', 'eert_toor'), ('old', 'old'), ('file', 'file'),
 
634
                (False, False))], list(transform._iter_changes()))
 
635
        finally:
 
636
            transform.finalize()
 
637
 
 
638
    def test_iter_changes_modifications(self):
 
639
        self.wt.set_root_id('eert_toor')
 
640
        transform, root = self.get_transform()
 
641
        transform.new_file('old', root, 'blah', 'id-1')
 
642
        transform.new_file('new', root, 'blah')
 
643
        transform.new_directory('subdir', root, 'subdir-id')
 
644
        transform.apply()
 
645
        transform, root = self.get_transform()
 
646
        try:
 
647
            old = transform.trans_id_tree_path('old')
 
648
            subdir = transform.trans_id_tree_file_id('subdir-id')
 
649
            new = transform.trans_id_tree_path('new')
 
650
            self.assertEqual([], list(transform._iter_changes()))
 
651
 
 
652
            #content deletion
 
653
            transform.delete_contents(old)
 
654
            self.assertEqual([('id-1', ('old', 'old'), True, (True, True),
 
655
                ('eert_toor', 'eert_toor'), ('old', 'old'), ('file', None),
 
656
                (False, False))], list(transform._iter_changes()))
 
657
 
 
658
            #content change
 
659
            transform.create_file('blah', old)
 
660
            self.assertEqual([('id-1', ('old', 'old'), True, (True, True),
 
661
                ('eert_toor', 'eert_toor'), ('old', 'old'), ('file', 'file'),
 
662
                (False, False))], list(transform._iter_changes()))
 
663
            transform.cancel_deletion(old)
 
664
            self.assertEqual([('id-1', ('old', 'old'), True, (True, True),
 
665
                ('eert_toor', 'eert_toor'), ('old', 'old'), ('file', 'file'),
 
666
                (False, False))], list(transform._iter_changes()))
 
667
            transform.cancel_creation(old)
 
668
 
 
669
            # move file_id to a different file
 
670
            self.assertEqual([], list(transform._iter_changes()))
 
671
            transform.unversion_file(old)
 
672
            transform.version_file('id-1', new)
 
673
            transform.adjust_path('old', root, new)
 
674
            self.assertEqual([('id-1', ('old', 'old'), True, (True, True),
 
675
                ('eert_toor', 'eert_toor'), ('old', 'old'), ('file', 'file'),
 
676
                (False, False))], list(transform._iter_changes()))
 
677
            transform.cancel_versioning(new)
 
678
            transform._removed_id = set()
 
679
 
 
680
            #execute bit
 
681
            self.assertEqual([], list(transform._iter_changes()))
 
682
            transform.set_executability(True, old)
 
683
            self.assertEqual([('id-1', ('old', 'old'), False, (True, True),
 
684
                ('eert_toor', 'eert_toor'), ('old', 'old'), ('file', 'file'),
 
685
                (False, True))], list(transform._iter_changes()))
 
686
            transform.set_executability(None, old)
 
687
 
 
688
            # filename
 
689
            self.assertEqual([], list(transform._iter_changes()))
 
690
            transform.adjust_path('new', root, old)
 
691
            transform._new_parent = {}
 
692
            self.assertEqual([('id-1', ('old', 'new'), False, (True, True),
 
693
                ('eert_toor', 'eert_toor'), ('old', 'new'), ('file', 'file'),
 
694
                (False, False))], list(transform._iter_changes()))
 
695
            transform._new_name = {}
 
696
 
 
697
            # parent directory
 
698
            self.assertEqual([], list(transform._iter_changes()))
 
699
            transform.adjust_path('new', subdir, old)
 
700
            transform._new_name = {}
 
701
            self.assertEqual([('id-1', ('old', 'subdir/old'), False,
 
702
                (True, True), ('eert_toor', 'subdir-id'), ('old', 'old'),
 
703
                ('file', 'file'), (False, False))],
 
704
                list(transform._iter_changes()))
 
705
            transform._new_path = {}
 
706
 
 
707
        finally:
 
708
            transform.finalize()
 
709
 
 
710
    def test_iter_changes_modified_bleed(self):
 
711
        self.wt.set_root_id('eert_toor')
 
712
        """Modified flag should not bleed from one change to another"""
 
713
        # unfortunately, we have no guarantee that file1 (which is modified)
 
714
        # will be applied before file2.  And if it's applied after file2, it
 
715
        # obviously can't bleed into file2's change output.  But for now, it
 
716
        # works.
 
717
        transform, root = self.get_transform()
 
718
        transform.new_file('file1', root, 'blah', 'id-1')
 
719
        transform.new_file('file2', root, 'blah', 'id-2')
 
720
        transform.apply()
 
721
        transform, root = self.get_transform()
 
722
        try:
 
723
            transform.delete_contents(transform.trans_id_file_id('id-1'))
 
724
            transform.set_executability(True,
 
725
            transform.trans_id_file_id('id-2'))
 
726
            self.assertEqual([('id-1', (u'file1', u'file1'), True, (True, True),
 
727
                ('eert_toor', 'eert_toor'), ('file1', u'file1'),
 
728
                ('file', None), (False, False)),
 
729
                ('id-2', (u'file2', u'file2'), False, (True, True),
 
730
                ('eert_toor', 'eert_toor'), ('file2', u'file2'),
 
731
                ('file', 'file'), (False, True))],
 
732
                list(transform._iter_changes()))
 
733
        finally:
 
734
            transform.finalize()
 
735
 
 
736
    def test_iter_changes_move_missing(self):
 
737
        """Test moving ids with no files around"""
 
738
        self.wt.set_root_id('toor_eert')
 
739
        # Need two steps because versioning a non-existant file is a conflict.
 
740
        transform, root = self.get_transform()
 
741
        transform.new_directory('floater', root, 'floater-id')
 
742
        transform.apply()
 
743
        transform, root = self.get_transform()
 
744
        transform.delete_contents(transform.trans_id_tree_path('floater'))
 
745
        transform.apply()
 
746
        transform, root = self.get_transform()
 
747
        floater = transform.trans_id_tree_path('floater')
 
748
        try:
 
749
            transform.adjust_path('flitter', root, floater)
 
750
            self.assertEqual([('floater-id', ('floater', 'flitter'), False,
 
751
            (True, True), ('toor_eert', 'toor_eert'), ('floater', 'flitter'),
 
752
            (None, None), (False, False))], list(transform._iter_changes()))
 
753
        finally:
 
754
            transform.finalize()
 
755
 
 
756
    def test_iter_changes_pointless(self):
 
757
        """Ensure that no-ops are not treated as modifications"""
 
758
        self.wt.set_root_id('eert_toor')
 
759
        transform, root = self.get_transform()
 
760
        transform.new_file('old', root, 'blah', 'id-1')
 
761
        transform.new_directory('subdir', root, 'subdir-id')
 
762
        transform.apply()
 
763
        transform, root = self.get_transform()
 
764
        try:
 
765
            old = transform.trans_id_tree_path('old')
 
766
            subdir = transform.trans_id_tree_file_id('subdir-id')
 
767
            self.assertEqual([], list(transform._iter_changes()))
 
768
            transform.delete_contents(subdir)
 
769
            transform.create_directory(subdir)
 
770
            transform.set_executability(False, old)
 
771
            transform.unversion_file(old)
 
772
            transform.version_file('id-1', old)
 
773
            transform.adjust_path('old', root, old)
 
774
            self.assertEqual([], list(transform._iter_changes()))
 
775
        finally:
 
776
            transform.finalize()
 
777
 
 
778
    def test_rename_count(self):
 
779
        transform, root = self.get_transform()
 
780
        transform.new_file('name1', root, 'contents')
 
781
        self.assertEqual(transform.rename_count, 0)
 
782
        transform.apply()
 
783
        self.assertEqual(transform.rename_count, 1)
 
784
        transform2, root = self.get_transform()
 
785
        transform2.adjust_path('name2', root,
 
786
                               transform2.trans_id_tree_path('name1'))
 
787
        self.assertEqual(transform2.rename_count, 0)
 
788
        transform2.apply()
 
789
        self.assertEqual(transform2.rename_count, 2)
 
790
 
 
791
    def test_change_parent(self):
 
792
        """Ensure that after we change a parent, the results are still right.
 
793
 
 
794
        Renames and parent changes on pending transforms can happen as part
 
795
        of conflict resolution, and are explicitly permitted by the
 
796
        TreeTransform API.
 
797
 
 
798
        This test ensures they work correctly with the rename-avoidance
 
799
        optimization.
 
800
        """
 
801
        transform, root = self.get_transform()
 
802
        parent1 = transform.new_directory('parent1', root)
 
803
        child1 = transform.new_file('child1', parent1, 'contents')
 
804
        parent2 = transform.new_directory('parent2', root)
 
805
        transform.adjust_path('child1', parent2, child1)
 
806
        transform.apply()
 
807
        self.failIfExists(self.wt.abspath('parent1/child1'))
 
808
        self.failUnlessExists(self.wt.abspath('parent2/child1'))
 
809
        # rename limbo/new-1 => parent1, rename limbo/new-3 => parent2
 
810
        # no rename for child1 (counting only renames during apply)
 
811
        self.failUnlessEqual(2, transform.rename_count)
 
812
 
 
813
    def test_cancel_parent(self):
 
814
        """Cancelling a parent doesn't cause deletion of a non-empty directory
 
815
 
 
816
        This is like the test_change_parent, except that we cancel the parent
 
817
        before adjusting the path.  The transform must detect that the
 
818
        directory is non-empty, and move children to safe locations.
 
819
        """
 
820
        transform, root = self.get_transform()
 
821
        parent1 = transform.new_directory('parent1', root)
 
822
        child1 = transform.new_file('child1', parent1, 'contents')
 
823
        child2 = transform.new_file('child2', parent1, 'contents')
 
824
        try:
 
825
            transform.cancel_creation(parent1)
 
826
        except OSError:
 
827
            self.fail('Failed to move child1 before deleting parent1')
 
828
        transform.cancel_creation(child2)
 
829
        transform.create_directory(parent1)
 
830
        try:
 
831
            transform.cancel_creation(parent1)
 
832
        # If the transform incorrectly believes that child2 is still in
 
833
        # parent1's limbo directory, it will try to rename it and fail
 
834
        # because was already moved by the first cancel_creation.
 
835
        except OSError:
 
836
            self.fail('Transform still thinks child2 is a child of parent1')
 
837
        parent2 = transform.new_directory('parent2', root)
 
838
        transform.adjust_path('child1', parent2, child1)
 
839
        transform.apply()
 
840
        self.failIfExists(self.wt.abspath('parent1'))
 
841
        self.failUnlessExists(self.wt.abspath('parent2/child1'))
 
842
        # rename limbo/new-3 => parent2, rename limbo/new-2 => child1
 
843
        self.failUnlessEqual(2, transform.rename_count)
 
844
 
 
845
    def test_adjust_and_cancel(self):
 
846
        """Make sure adjust_path keeps track of limbo children properly"""
 
847
        transform, root = self.get_transform()
 
848
        parent1 = transform.new_directory('parent1', root)
 
849
        child1 = transform.new_file('child1', parent1, 'contents')
 
850
        parent2 = transform.new_directory('parent2', root)
 
851
        transform.adjust_path('child1', parent2, child1)
 
852
        transform.cancel_creation(child1)
 
853
        try:
 
854
            transform.cancel_creation(parent1)
 
855
        # if the transform thinks child1 is still in parent1's limbo
 
856
        # directory, it will attempt to move it and fail.
 
857
        except OSError:
 
858
            self.fail('Transform still thinks child1 is a child of parent1')
 
859
        transform.finalize()
 
860
 
 
861
    def test_noname_contents(self):
 
862
        """TreeTransform should permit deferring naming files."""
 
863
        transform, root = self.get_transform()
 
864
        parent = transform.trans_id_file_id('parent-id')
 
865
        try:
 
866
            transform.create_directory(parent)
 
867
        except KeyError:
 
868
            self.fail("Can't handle contents with no name")
 
869
        transform.finalize()
 
870
 
 
871
    def test_noname_contents_nested(self):
 
872
        """TreeTransform should permit deferring naming files."""
 
873
        transform, root = self.get_transform()
 
874
        parent = transform.trans_id_file_id('parent-id')
 
875
        try:
 
876
            transform.create_directory(parent)
 
877
        except KeyError:
 
878
            self.fail("Can't handle contents with no name")
 
879
        child = transform.new_directory('child', parent)
 
880
        transform.adjust_path('parent', root, parent)
 
881
        transform.apply()
 
882
        self.failUnlessExists(self.wt.abspath('parent/child'))
 
883
        self.assertEqual(1, transform.rename_count)
 
884
 
 
885
    def test_reuse_name(self):
 
886
        """Avoid reusing the same limbo name for different files"""
 
887
        transform, root = self.get_transform()
 
888
        parent = transform.new_directory('parent', root)
 
889
        child1 = transform.new_directory('child', parent)
 
890
        try:
 
891
            child2 = transform.new_directory('child', parent)
 
892
        except OSError:
 
893
            self.fail('Tranform tried to use the same limbo name twice')
 
894
        transform.adjust_path('child2', parent, child2)
 
895
        transform.apply()
 
896
        # limbo/new-1 => parent, limbo/new-3 => parent/child2
 
897
        # child2 is put into top-level limbo because child1 has already
 
898
        # claimed the direct limbo path when child2 is created.  There is no
 
899
        # advantage in renaming files once they're in top-level limbo, except
 
900
        # as part of apply.
 
901
        self.assertEqual(2, transform.rename_count)
 
902
 
 
903
    def test_reuse_when_first_moved(self):
 
904
        """Don't avoid direct paths when it is safe to use them"""
 
905
        transform, root = self.get_transform()
 
906
        parent = transform.new_directory('parent', root)
 
907
        child1 = transform.new_directory('child', parent)
 
908
        transform.adjust_path('child1', parent, child1)
 
909
        child2 = transform.new_directory('child', parent)
 
910
        transform.apply()
 
911
        # limbo/new-1 => parent
 
912
        self.assertEqual(1, transform.rename_count)
 
913
 
 
914
    def test_reuse_after_cancel(self):
 
915
        """Don't avoid direct paths when it is safe to use them"""
 
916
        transform, root = self.get_transform()
 
917
        parent2 = transform.new_directory('parent2', root)
 
918
        child1 = transform.new_directory('child1', parent2)
 
919
        transform.cancel_creation(parent2)
 
920
        transform.create_directory(parent2)
 
921
        child2 = transform.new_directory('child1', parent2)
 
922
        transform.adjust_path('child2', parent2, child1)
 
923
        transform.apply()
 
924
        # limbo/new-1 => parent2, limbo/new-2 => parent2/child1
 
925
        self.assertEqual(2, transform.rename_count)
 
926
 
 
927
    def test_finalize_order(self):
 
928
        """Finalize must be done in child-to-parent order"""
 
929
        transform, root = self.get_transform()
 
930
        parent = transform.new_directory('parent', root)
 
931
        child = transform.new_directory('child', parent)
 
932
        try:
 
933
            transform.finalize()
 
934
        except OSError:
 
935
            self.fail('Tried to remove parent before child1')
 
936
 
 
937
    def test_cancel_with_cancelled_child_should_succeed(self):
 
938
        transform, root = self.get_transform()
 
939
        parent = transform.new_directory('parent', root)
 
940
        child = transform.new_directory('child', parent)
 
941
        transform.cancel_creation(child)
 
942
        transform.cancel_creation(parent)
 
943
        transform.finalize()
 
944
 
 
945
 
 
946
class TransformGroup(object):
 
947
    def __init__(self, dirname, root_id):
 
948
        self.name = dirname
 
949
        os.mkdir(dirname)
 
950
        self.wt = BzrDir.create_standalone_workingtree(dirname)
 
951
        self.wt.set_root_id(root_id)
 
952
        self.b = self.wt.branch
 
953
        self.tt = TreeTransform(self.wt)
 
954
        self.root = self.tt.trans_id_tree_file_id(self.wt.get_root_id())
 
955
 
 
956
 
 
957
def conflict_text(tree, merge):
 
958
    template = '%s TREE\n%s%s\n%s%s MERGE-SOURCE\n'
 
959
    return template % ('<' * 7, tree, '=' * 7, merge, '>' * 7)
 
960
 
 
961
 
 
962
class TestTransformMerge(TestCaseInTempDir):
 
963
    def test_text_merge(self):
 
964
        root_id = generate_ids.gen_root_id()
 
965
        base = TransformGroup("base", root_id)
 
966
        base.tt.new_file('a', base.root, 'a\nb\nc\nd\be\n', 'a')
 
967
        base.tt.new_file('b', base.root, 'b1', 'b')
 
968
        base.tt.new_file('c', base.root, 'c', 'c')
 
969
        base.tt.new_file('d', base.root, 'd', 'd')
 
970
        base.tt.new_file('e', base.root, 'e', 'e')
 
971
        base.tt.new_file('f', base.root, 'f', 'f')
 
972
        base.tt.new_directory('g', base.root, 'g')
 
973
        base.tt.new_directory('h', base.root, 'h')
 
974
        base.tt.apply()
 
975
        other = TransformGroup("other", root_id)
 
976
        other.tt.new_file('a', other.root, 'y\nb\nc\nd\be\n', 'a')
 
977
        other.tt.new_file('b', other.root, 'b2', 'b')
 
978
        other.tt.new_file('c', other.root, 'c2', 'c')
 
979
        other.tt.new_file('d', other.root, 'd', 'd')
 
980
        other.tt.new_file('e', other.root, 'e2', 'e')
 
981
        other.tt.new_file('f', other.root, 'f', 'f')
 
982
        other.tt.new_file('g', other.root, 'g', 'g')
 
983
        other.tt.new_file('h', other.root, 'h\ni\nj\nk\n', 'h')
 
984
        other.tt.new_file('i', other.root, 'h\ni\nj\nk\n', 'i')
 
985
        other.tt.apply()
 
986
        this = TransformGroup("this", root_id)
 
987
        this.tt.new_file('a', this.root, 'a\nb\nc\nd\bz\n', 'a')
 
988
        this.tt.new_file('b', this.root, 'b', 'b')
 
989
        this.tt.new_file('c', this.root, 'c', 'c')
 
990
        this.tt.new_file('d', this.root, 'd2', 'd')
 
991
        this.tt.new_file('e', this.root, 'e2', 'e')
 
992
        this.tt.new_file('f', this.root, 'f', 'f')
 
993
        this.tt.new_file('g', this.root, 'g', 'g')
 
994
        this.tt.new_file('h', this.root, '1\n2\n3\n4\n', 'h')
 
995
        this.tt.new_file('i', this.root, '1\n2\n3\n4\n', 'i')
 
996
        this.tt.apply()
 
997
        Merge3Merger(this.wt, this.wt, base.wt, other.wt)
 
998
        # textual merge
 
999
        self.assertEqual(this.wt.get_file('a').read(), 'y\nb\nc\nd\bz\n')
 
1000
        # three-way text conflict
 
1001
        self.assertEqual(this.wt.get_file('b').read(), 
 
1002
                         conflict_text('b', 'b2'))
 
1003
        # OTHER wins
 
1004
        self.assertEqual(this.wt.get_file('c').read(), 'c2')
 
1005
        # THIS wins
 
1006
        self.assertEqual(this.wt.get_file('d').read(), 'd2')
 
1007
        # Ambigious clean merge
 
1008
        self.assertEqual(this.wt.get_file('e').read(), 'e2')
 
1009
        # No change
 
1010
        self.assertEqual(this.wt.get_file('f').read(), 'f')
 
1011
        # Correct correct results when THIS == OTHER 
 
1012
        self.assertEqual(this.wt.get_file('g').read(), 'g')
 
1013
        # Text conflict when THIS & OTHER are text and BASE is dir
 
1014
        self.assertEqual(this.wt.get_file('h').read(), 
 
1015
                         conflict_text('1\n2\n3\n4\n', 'h\ni\nj\nk\n'))
 
1016
        self.assertEqual(this.wt.get_file_byname('h.THIS').read(),
 
1017
                         '1\n2\n3\n4\n')
 
1018
        self.assertEqual(this.wt.get_file_byname('h.OTHER').read(),
 
1019
                         'h\ni\nj\nk\n')
 
1020
        self.assertEqual(file_kind(this.wt.abspath('h.BASE')), 'directory')
 
1021
        self.assertEqual(this.wt.get_file('i').read(), 
 
1022
                         conflict_text('1\n2\n3\n4\n', 'h\ni\nj\nk\n'))
 
1023
        self.assertEqual(this.wt.get_file_byname('i.THIS').read(),
 
1024
                         '1\n2\n3\n4\n')
 
1025
        self.assertEqual(this.wt.get_file_byname('i.OTHER').read(),
 
1026
                         'h\ni\nj\nk\n')
 
1027
        self.assertEqual(os.path.exists(this.wt.abspath('i.BASE')), False)
 
1028
        modified = ['a', 'b', 'c', 'h', 'i']
 
1029
        merge_modified = this.wt.merge_modified()
 
1030
        self.assertSubset(merge_modified, modified)
 
1031
        self.assertEqual(len(merge_modified), len(modified))
 
1032
        file(this.wt.id2abspath('a'), 'wb').write('booga')
 
1033
        modified.pop(0)
 
1034
        merge_modified = this.wt.merge_modified()
 
1035
        self.assertSubset(merge_modified, modified)
 
1036
        self.assertEqual(len(merge_modified), len(modified))
 
1037
        this.wt.remove('b')
 
1038
        this.wt.revert([])
 
1039
 
 
1040
    def test_file_merge(self):
 
1041
        if not has_symlinks():
 
1042
            raise TestSkipped('Symlinks are not supported on this platform')
 
1043
        root_id = generate_ids.gen_root_id()
 
1044
        base = TransformGroup("BASE", root_id)
 
1045
        this = TransformGroup("THIS", root_id)
 
1046
        other = TransformGroup("OTHER", root_id)
 
1047
        for tg in this, base, other:
 
1048
            tg.tt.new_directory('a', tg.root, 'a')
 
1049
            tg.tt.new_symlink('b', tg.root, 'b', 'b')
 
1050
            tg.tt.new_file('c', tg.root, 'c', 'c')
 
1051
            tg.tt.new_symlink('d', tg.root, tg.name, 'd')
 
1052
        targets = ((base, 'base-e', 'base-f', None, None), 
 
1053
                   (this, 'other-e', 'this-f', 'other-g', 'this-h'), 
 
1054
                   (other, 'other-e', None, 'other-g', 'other-h'))
 
1055
        for tg, e_target, f_target, g_target, h_target in targets:
 
1056
            for link, target in (('e', e_target), ('f', f_target), 
 
1057
                                 ('g', g_target), ('h', h_target)):
 
1058
                if target is not None:
 
1059
                    tg.tt.new_symlink(link, tg.root, target, link)
 
1060
 
 
1061
        for tg in this, base, other:
 
1062
            tg.tt.apply()
 
1063
        Merge3Merger(this.wt, this.wt, base.wt, other.wt)
 
1064
        self.assertIs(os.path.isdir(this.wt.abspath('a')), True)
 
1065
        self.assertIs(os.path.islink(this.wt.abspath('b')), True)
 
1066
        self.assertIs(os.path.isfile(this.wt.abspath('c')), True)
 
1067
        for suffix in ('THIS', 'BASE', 'OTHER'):
 
1068
            self.assertEqual(os.readlink(this.wt.abspath('d.'+suffix)), suffix)
 
1069
        self.assertIs(os.path.lexists(this.wt.abspath('d')), False)
 
1070
        self.assertEqual(this.wt.id2path('d'), 'd.OTHER')
 
1071
        self.assertEqual(this.wt.id2path('f'), 'f.THIS')
 
1072
        self.assertEqual(os.readlink(this.wt.abspath('e')), 'other-e')
 
1073
        self.assertIs(os.path.lexists(this.wt.abspath('e.THIS')), False)
 
1074
        self.assertIs(os.path.lexists(this.wt.abspath('e.OTHER')), False)
 
1075
        self.assertIs(os.path.lexists(this.wt.abspath('e.BASE')), False)
 
1076
        self.assertIs(os.path.lexists(this.wt.abspath('g')), True)
 
1077
        self.assertIs(os.path.lexists(this.wt.abspath('g.BASE')), False)
 
1078
        self.assertIs(os.path.lexists(this.wt.abspath('h')), False)
 
1079
        self.assertIs(os.path.lexists(this.wt.abspath('h.BASE')), False)
 
1080
        self.assertIs(os.path.lexists(this.wt.abspath('h.THIS')), True)
 
1081
        self.assertIs(os.path.lexists(this.wt.abspath('h.OTHER')), True)
 
1082
 
 
1083
    def test_filename_merge(self):
 
1084
        root_id = generate_ids.gen_root_id()
 
1085
        base = TransformGroup("BASE", root_id)
 
1086
        this = TransformGroup("THIS", root_id)
 
1087
        other = TransformGroup("OTHER", root_id)
 
1088
        base_a, this_a, other_a = [t.tt.new_directory('a', t.root, 'a') 
 
1089
                                   for t in [base, this, other]]
 
1090
        base_b, this_b, other_b = [t.tt.new_directory('b', t.root, 'b') 
 
1091
                                   for t in [base, this, other]]
 
1092
        base.tt.new_directory('c', base_a, 'c')
 
1093
        this.tt.new_directory('c1', this_a, 'c')
 
1094
        other.tt.new_directory('c', other_b, 'c')
 
1095
 
 
1096
        base.tt.new_directory('d', base_a, 'd')
 
1097
        this.tt.new_directory('d1', this_b, 'd')
 
1098
        other.tt.new_directory('d', other_a, 'd')
 
1099
 
 
1100
        base.tt.new_directory('e', base_a, 'e')
 
1101
        this.tt.new_directory('e', this_a, 'e')
 
1102
        other.tt.new_directory('e1', other_b, 'e')
 
1103
 
 
1104
        base.tt.new_directory('f', base_a, 'f')
 
1105
        this.tt.new_directory('f1', this_b, 'f')
 
1106
        other.tt.new_directory('f1', other_b, 'f')
 
1107
 
 
1108
        for tg in [this, base, other]:
 
1109
            tg.tt.apply()
 
1110
        Merge3Merger(this.wt, this.wt, base.wt, other.wt)
 
1111
        self.assertEqual(this.wt.id2path('c'), pathjoin('b/c1'))
 
1112
        self.assertEqual(this.wt.id2path('d'), pathjoin('b/d1'))
 
1113
        self.assertEqual(this.wt.id2path('e'), pathjoin('b/e1'))
 
1114
        self.assertEqual(this.wt.id2path('f'), pathjoin('b/f1'))
 
1115
 
 
1116
    def test_filename_merge_conflicts(self):
 
1117
        root_id = generate_ids.gen_root_id()
 
1118
        base = TransformGroup("BASE", root_id)
 
1119
        this = TransformGroup("THIS", root_id)
 
1120
        other = TransformGroup("OTHER", root_id)
 
1121
        base_a, this_a, other_a = [t.tt.new_directory('a', t.root, 'a') 
 
1122
                                   for t in [base, this, other]]
 
1123
        base_b, this_b, other_b = [t.tt.new_directory('b', t.root, 'b') 
 
1124
                                   for t in [base, this, other]]
 
1125
 
 
1126
        base.tt.new_file('g', base_a, 'g', 'g')
 
1127
        other.tt.new_file('g1', other_b, 'g1', 'g')
 
1128
 
 
1129
        base.tt.new_file('h', base_a, 'h', 'h')
 
1130
        this.tt.new_file('h1', this_b, 'h1', 'h')
 
1131
 
 
1132
        base.tt.new_file('i', base.root, 'i', 'i')
 
1133
        other.tt.new_directory('i1', this_b, 'i')
 
1134
 
 
1135
        for tg in [this, base, other]:
 
1136
            tg.tt.apply()
 
1137
        Merge3Merger(this.wt, this.wt, base.wt, other.wt)
 
1138
 
 
1139
        self.assertEqual(this.wt.id2path('g'), pathjoin('b/g1.OTHER'))
 
1140
        self.assertIs(os.path.lexists(this.wt.abspath('b/g1.BASE')), True)
 
1141
        self.assertIs(os.path.lexists(this.wt.abspath('b/g1.THIS')), False)
 
1142
        self.assertEqual(this.wt.id2path('h'), pathjoin('b/h1.THIS'))
 
1143
        self.assertIs(os.path.lexists(this.wt.abspath('b/h1.BASE')), True)
 
1144
        self.assertIs(os.path.lexists(this.wt.abspath('b/h1.OTHER')), False)
 
1145
        self.assertEqual(this.wt.id2path('i'), pathjoin('b/i1.OTHER'))
 
1146
 
 
1147
 
 
1148
class TestBuildTree(tests.TestCaseWithTransport):
 
1149
 
 
1150
    def test_build_tree(self):
 
1151
        if not has_symlinks():
 
1152
            raise TestSkipped('Test requires symlink support')
 
1153
        os.mkdir('a')
 
1154
        a = BzrDir.create_standalone_workingtree('a')
 
1155
        os.mkdir('a/foo')
 
1156
        file('a/foo/bar', 'wb').write('contents')
 
1157
        os.symlink('a/foo/bar', 'a/foo/baz')
 
1158
        a.add(['foo', 'foo/bar', 'foo/baz'])
 
1159
        a.commit('initial commit')
 
1160
        b = BzrDir.create_standalone_workingtree('b')
 
1161
        basis = a.basis_tree()
 
1162
        basis.lock_read()
 
1163
        self.addCleanup(basis.unlock)
 
1164
        build_tree(basis, b)
 
1165
        self.assertIs(os.path.isdir('b/foo'), True)
 
1166
        self.assertEqual(file('b/foo/bar', 'rb').read(), "contents")
 
1167
        self.assertEqual(os.readlink('b/foo/baz'), 'a/foo/bar')
 
1168
 
 
1169
    def test_build_with_references(self):
 
1170
        tree = self.make_branch_and_tree('source',
 
1171
            format='dirstate-with-subtree')
 
1172
        subtree = self.make_branch_and_tree('source/subtree',
 
1173
            format='dirstate-with-subtree')
 
1174
        tree.add_reference(subtree)
 
1175
        tree.commit('a revision')
 
1176
        tree.branch.create_checkout('target')
 
1177
        self.failUnlessExists('target')
 
1178
        self.failUnlessExists('target/subtree')
 
1179
 
 
1180
    def test_file_conflict_handling(self):
 
1181
        """Ensure that when building trees, conflict handling is done"""
 
1182
        source = self.make_branch_and_tree('source')
 
1183
        target = self.make_branch_and_tree('target')
 
1184
        self.build_tree(['source/file', 'target/file'])
 
1185
        source.add('file', 'new-file')
 
1186
        source.commit('added file')
 
1187
        build_tree(source.basis_tree(), target)
 
1188
        self.assertEqual([DuplicateEntry('Moved existing file to',
 
1189
                          'file.moved', 'file', None, 'new-file')],
 
1190
                         target.conflicts())
 
1191
        target2 = self.make_branch_and_tree('target2')
 
1192
        target_file = file('target2/file', 'wb')
 
1193
        try:
 
1194
            source_file = file('source/file', 'rb')
 
1195
            try:
 
1196
                target_file.write(source_file.read())
 
1197
            finally:
 
1198
                source_file.close()
 
1199
        finally:
 
1200
            target_file.close()
 
1201
        build_tree(source.basis_tree(), target2)
 
1202
        self.assertEqual([], target2.conflicts())
 
1203
 
 
1204
    def test_symlink_conflict_handling(self):
 
1205
        """Ensure that when building trees, conflict handling is done"""
 
1206
        if not has_symlinks():
 
1207
            raise TestSkipped('Test requires symlink support')
 
1208
        source = self.make_branch_and_tree('source')
 
1209
        os.symlink('foo', 'source/symlink')
 
1210
        source.add('symlink', 'new-symlink')
 
1211
        source.commit('added file')
 
1212
        target = self.make_branch_and_tree('target')
 
1213
        os.symlink('bar', 'target/symlink')
 
1214
        build_tree(source.basis_tree(), target)
 
1215
        self.assertEqual([DuplicateEntry('Moved existing file to',
 
1216
            'symlink.moved', 'symlink', None, 'new-symlink')],
 
1217
            target.conflicts())
 
1218
        target = self.make_branch_and_tree('target2')
 
1219
        os.symlink('foo', 'target2/symlink')
 
1220
        build_tree(source.basis_tree(), target)
 
1221
        self.assertEqual([], target.conflicts())
 
1222
        
 
1223
    def test_directory_conflict_handling(self):
 
1224
        """Ensure that when building trees, conflict handling is done"""
 
1225
        source = self.make_branch_and_tree('source')
 
1226
        target = self.make_branch_and_tree('target')
 
1227
        self.build_tree(['source/dir1/', 'source/dir1/file', 'target/dir1/'])
 
1228
        source.add(['dir1', 'dir1/file'], ['new-dir1', 'new-file'])
 
1229
        source.commit('added file')
 
1230
        build_tree(source.basis_tree(), target)
 
1231
        self.assertEqual([], target.conflicts())
 
1232
        self.failUnlessExists('target/dir1/file')
 
1233
 
 
1234
        # Ensure contents are merged
 
1235
        target = self.make_branch_and_tree('target2')
 
1236
        self.build_tree(['target2/dir1/', 'target2/dir1/file2'])
 
1237
        build_tree(source.basis_tree(), target)
 
1238
        self.assertEqual([], target.conflicts())
 
1239
        self.failUnlessExists('target2/dir1/file2')
 
1240
        self.failUnlessExists('target2/dir1/file')
 
1241
 
 
1242
        # Ensure new contents are suppressed for existing branches
 
1243
        target = self.make_branch_and_tree('target3')
 
1244
        self.make_branch('target3/dir1')
 
1245
        self.build_tree(['target3/dir1/file2'])
 
1246
        build_tree(source.basis_tree(), target)
 
1247
        self.failIfExists('target3/dir1/file')
 
1248
        self.failUnlessExists('target3/dir1/file2')
 
1249
        self.failUnlessExists('target3/dir1.diverted/file')
 
1250
        self.assertEqual([DuplicateEntry('Diverted to',
 
1251
            'dir1.diverted', 'dir1', 'new-dir1', None)],
 
1252
            target.conflicts())
 
1253
 
 
1254
        target = self.make_branch_and_tree('target4')
 
1255
        self.build_tree(['target4/dir1/'])
 
1256
        self.make_branch('target4/dir1/file')
 
1257
        build_tree(source.basis_tree(), target)
 
1258
        self.failUnlessExists('target4/dir1/file')
 
1259
        self.assertEqual('directory', file_kind('target4/dir1/file'))
 
1260
        self.failUnlessExists('target4/dir1/file.diverted')
 
1261
        self.assertEqual([DuplicateEntry('Diverted to',
 
1262
            'dir1/file.diverted', 'dir1/file', 'new-file', None)],
 
1263
            target.conflicts())
 
1264
 
 
1265
    def test_mixed_conflict_handling(self):
 
1266
        """Ensure that when building trees, conflict handling is done"""
 
1267
        source = self.make_branch_and_tree('source')
 
1268
        target = self.make_branch_and_tree('target')
 
1269
        self.build_tree(['source/name', 'target/name/'])
 
1270
        source.add('name', 'new-name')
 
1271
        source.commit('added file')
 
1272
        build_tree(source.basis_tree(), target)
 
1273
        self.assertEqual([DuplicateEntry('Moved existing file to',
 
1274
            'name.moved', 'name', None, 'new-name')], target.conflicts())
 
1275
 
 
1276
    def test_raises_in_populated(self):
 
1277
        source = self.make_branch_and_tree('source')
 
1278
        self.build_tree(['source/name'])
 
1279
        source.add('name')
 
1280
        source.commit('added name')
 
1281
        target = self.make_branch_and_tree('target')
 
1282
        self.build_tree(['target/name'])
 
1283
        target.add('name')
 
1284
        self.assertRaises(errors.WorkingTreeAlreadyPopulated, 
 
1285
            build_tree, source.basis_tree(), target)
 
1286
 
 
1287
    def test_build_tree_rename_count(self):
 
1288
        source = self.make_branch_and_tree('source')
 
1289
        self.build_tree(['source/file1', 'source/dir1/'])
 
1290
        source.add(['file1', 'dir1'])
 
1291
        source.commit('add1')
 
1292
        target1 = self.make_branch_and_tree('target1')
 
1293
        transform_result = build_tree(source.basis_tree(), target1)
 
1294
        self.assertEqual(2, transform_result.rename_count)
 
1295
 
 
1296
        self.build_tree(['source/dir1/file2'])
 
1297
        source.add(['dir1/file2'])
 
1298
        source.commit('add3')
 
1299
        target2 = self.make_branch_and_tree('target2')
 
1300
        transform_result = build_tree(source.basis_tree(), target2)
 
1301
        # children of non-root directories should not be renamed
 
1302
        self.assertEqual(2, transform_result.rename_count)
 
1303
 
 
1304
 
 
1305
class MockTransform(object):
 
1306
 
 
1307
    def has_named_child(self, by_parent, parent_id, name):
 
1308
        for child_id in by_parent[parent_id]:
 
1309
            if child_id == '0':
 
1310
                if name == "name~":
 
1311
                    return True
 
1312
            elif name == "name.~%s~" % child_id:
 
1313
                return True
 
1314
        return False
 
1315
 
 
1316
 
 
1317
class MockEntry(object):
 
1318
    def __init__(self):
 
1319
        object.__init__(self)
 
1320
        self.name = "name"
 
1321
 
 
1322
class TestGetBackupName(TestCase):
 
1323
    def test_get_backup_name(self):
 
1324
        tt = MockTransform()
 
1325
        name = get_backup_name(MockEntry(), {'a':[]}, 'a', tt)
 
1326
        self.assertEqual(name, 'name.~1~')
 
1327
        name = get_backup_name(MockEntry(), {'a':['1']}, 'a', tt)
 
1328
        self.assertEqual(name, 'name.~2~')
 
1329
        name = get_backup_name(MockEntry(), {'a':['2']}, 'a', tt)
 
1330
        self.assertEqual(name, 'name.~1~')
 
1331
        name = get_backup_name(MockEntry(), {'a':['2'], 'b':[]}, 'b', tt)
 
1332
        self.assertEqual(name, 'name.~1~')
 
1333
        name = get_backup_name(MockEntry(), {'a':['1', '2', '3']}, 'a', tt)
 
1334
        self.assertEqual(name, 'name.~4~')