~bzr-pqm/bzr/bzr.dev

« back to all changes in this revision

Viewing changes to bzrlib/tests/test_bundle.py

  • Committer: Robert Collins
  • Date: 2006-07-20 13:00:31 UTC
  • mto: (1852.9.1 Tree.compare().)
  • mto: This revision was merged to the branch mainline in revision 1890.
  • Revision ID: robertc@robertcollins.net-20060720130031-d26103a427ea10f3
StartĀ treeĀ implementationĀ tests.

Show diffs side-by-side

added added

removed removed

Lines of Context:
 
1
# Copyright (C) 2004-2006 by 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
from cStringIO import StringIO
 
18
import os
 
19
import sys
 
20
import tempfile
 
21
 
 
22
from bzrlib.builtins import merge
 
23
from bzrlib.bzrdir import BzrDir
 
24
from bzrlib.bundle.apply_bundle import install_bundle, merge_bundle
 
25
from bzrlib.bundle.bundle_data import BundleTree
 
26
from bzrlib.bundle.serializer import write_bundle, read_bundle
 
27
from bzrlib.branch import Branch
 
28
from bzrlib.diff import internal_diff
 
29
from bzrlib.delta import compare_trees
 
30
from bzrlib.errors import BzrError, TestamentMismatch, NotABundle, BadBundle
 
31
from bzrlib.merge import Merge3Merger
 
32
from bzrlib.osutils import has_symlinks, sha_file
 
33
from bzrlib.tests import (TestCaseInTempDir, TestCaseWithTransport,
 
34
                          TestCase, TestSkipped)
 
35
from bzrlib.transform import TreeTransform
 
36
from bzrlib.workingtree import WorkingTree
 
37
 
 
38
 
 
39
class MockTree(object):
 
40
    def __init__(self):
 
41
        from bzrlib.inventory import RootEntry, ROOT_ID
 
42
        object.__init__(self)
 
43
        self.paths = {ROOT_ID: ""}
 
44
        self.ids = {"": ROOT_ID}
 
45
        self.contents = {}
 
46
        self.root = RootEntry(ROOT_ID)
 
47
 
 
48
    inventory = property(lambda x:x)
 
49
 
 
50
    def __iter__(self):
 
51
        return self.paths.iterkeys()
 
52
 
 
53
    def __getitem__(self, file_id):
 
54
        if file_id == self.root.file_id:
 
55
            return self.root
 
56
        else:
 
57
            return self.make_entry(file_id, self.paths[file_id])
 
58
 
 
59
    def parent_id(self, file_id):
 
60
        parent_dir = os.path.dirname(self.paths[file_id])
 
61
        if parent_dir == "":
 
62
            return None
 
63
        return self.ids[parent_dir]
 
64
 
 
65
    def iter_entries(self):
 
66
        for path, file_id in self.ids.iteritems():
 
67
            yield path, self[file_id]
 
68
 
 
69
    def get_file_kind(self, file_id):
 
70
        if file_id in self.contents:
 
71
            kind = 'file'
 
72
        else:
 
73
            kind = 'directory'
 
74
        return kind
 
75
 
 
76
    def make_entry(self, file_id, path):
 
77
        from bzrlib.inventory import (InventoryEntry, InventoryFile
 
78
                                    , InventoryDirectory, InventoryLink)
 
79
        name = os.path.basename(path)
 
80
        kind = self.get_file_kind(file_id)
 
81
        parent_id = self.parent_id(file_id)
 
82
        text_sha_1, text_size = self.contents_stats(file_id)
 
83
        if kind == 'directory':
 
84
            ie = InventoryDirectory(file_id, name, parent_id)
 
85
        elif kind == 'file':
 
86
            ie = InventoryFile(file_id, name, parent_id)
 
87
        elif kind == 'symlink':
 
88
            ie = InventoryLink(file_id, name, parent_id)
 
89
        else:
 
90
            raise BzrError('unknown kind %r' % kind)
 
91
        ie.text_sha1 = text_sha_1
 
92
        ie.text_size = text_size
 
93
        return ie
 
94
 
 
95
    def add_dir(self, file_id, path):
 
96
        self.paths[file_id] = path
 
97
        self.ids[path] = file_id
 
98
    
 
99
    def add_file(self, file_id, path, contents):
 
100
        self.add_dir(file_id, path)
 
101
        self.contents[file_id] = contents
 
102
 
 
103
    def path2id(self, path):
 
104
        return self.ids.get(path)
 
105
 
 
106
    def id2path(self, file_id):
 
107
        return self.paths.get(file_id)
 
108
 
 
109
    def has_id(self, file_id):
 
110
        return self.id2path(file_id) is not None
 
111
 
 
112
    def get_file(self, file_id):
 
113
        result = StringIO()
 
114
        result.write(self.contents[file_id])
 
115
        result.seek(0,0)
 
116
        return result
 
117
 
 
118
    def contents_stats(self, file_id):
 
119
        if file_id not in self.contents:
 
120
            return None, None
 
121
        text_sha1 = sha_file(self.get_file(file_id))
 
122
        return text_sha1, len(self.contents[file_id])
 
123
 
 
124
 
 
125
class BTreeTester(TestCase):
 
126
    """A simple unittest tester for the BundleTree class."""
 
127
 
 
128
    def make_tree_1(self):
 
129
        mtree = MockTree()
 
130
        mtree.add_dir("a", "grandparent")
 
131
        mtree.add_dir("b", "grandparent/parent")
 
132
        mtree.add_file("c", "grandparent/parent/file", "Hello\n")
 
133
        mtree.add_dir("d", "grandparent/alt_parent")
 
134
        return BundleTree(mtree, ''), mtree
 
135
        
 
136
    def test_renames(self):
 
137
        """Ensure that file renames have the proper effect on children"""
 
138
        btree = self.make_tree_1()[0]
 
139
        self.assertEqual(btree.old_path("grandparent"), "grandparent")
 
140
        self.assertEqual(btree.old_path("grandparent/parent"), 
 
141
                         "grandparent/parent")
 
142
        self.assertEqual(btree.old_path("grandparent/parent/file"),
 
143
                         "grandparent/parent/file")
 
144
 
 
145
        self.assertEqual(btree.id2path("a"), "grandparent")
 
146
        self.assertEqual(btree.id2path("b"), "grandparent/parent")
 
147
        self.assertEqual(btree.id2path("c"), "grandparent/parent/file")
 
148
 
 
149
        self.assertEqual(btree.path2id("grandparent"), "a")
 
150
        self.assertEqual(btree.path2id("grandparent/parent"), "b")
 
151
        self.assertEqual(btree.path2id("grandparent/parent/file"), "c")
 
152
 
 
153
        assert btree.path2id("grandparent2") is None
 
154
        assert btree.path2id("grandparent2/parent") is None
 
155
        assert btree.path2id("grandparent2/parent/file") is None
 
156
 
 
157
        btree.note_rename("grandparent", "grandparent2")
 
158
        assert btree.old_path("grandparent") is None
 
159
        assert btree.old_path("grandparent/parent") is None
 
160
        assert btree.old_path("grandparent/parent/file") is None
 
161
 
 
162
        self.assertEqual(btree.id2path("a"), "grandparent2")
 
163
        self.assertEqual(btree.id2path("b"), "grandparent2/parent")
 
164
        self.assertEqual(btree.id2path("c"), "grandparent2/parent/file")
 
165
 
 
166
        self.assertEqual(btree.path2id("grandparent2"), "a")
 
167
        self.assertEqual(btree.path2id("grandparent2/parent"), "b")
 
168
        self.assertEqual(btree.path2id("grandparent2/parent/file"), "c")
 
169
 
 
170
        assert btree.path2id("grandparent") is None
 
171
        assert btree.path2id("grandparent/parent") is None
 
172
        assert btree.path2id("grandparent/parent/file") is None
 
173
 
 
174
        btree.note_rename("grandparent/parent", "grandparent2/parent2")
 
175
        self.assertEqual(btree.id2path("a"), "grandparent2")
 
176
        self.assertEqual(btree.id2path("b"), "grandparent2/parent2")
 
177
        self.assertEqual(btree.id2path("c"), "grandparent2/parent2/file")
 
178
 
 
179
        self.assertEqual(btree.path2id("grandparent2"), "a")
 
180
        self.assertEqual(btree.path2id("grandparent2/parent2"), "b")
 
181
        self.assertEqual(btree.path2id("grandparent2/parent2/file"), "c")
 
182
 
 
183
        assert btree.path2id("grandparent2/parent") is None
 
184
        assert btree.path2id("grandparent2/parent/file") is None
 
185
 
 
186
        btree.note_rename("grandparent/parent/file", 
 
187
                          "grandparent2/parent2/file2")
 
188
        self.assertEqual(btree.id2path("a"), "grandparent2")
 
189
        self.assertEqual(btree.id2path("b"), "grandparent2/parent2")
 
190
        self.assertEqual(btree.id2path("c"), "grandparent2/parent2/file2")
 
191
 
 
192
        self.assertEqual(btree.path2id("grandparent2"), "a")
 
193
        self.assertEqual(btree.path2id("grandparent2/parent2"), "b")
 
194
        self.assertEqual(btree.path2id("grandparent2/parent2/file2"), "c")
 
195
 
 
196
        assert btree.path2id("grandparent2/parent2/file") is None
 
197
 
 
198
    def test_moves(self):
 
199
        """Ensure that file moves have the proper effect on children"""
 
200
        btree = self.make_tree_1()[0]
 
201
        btree.note_rename("grandparent/parent/file", 
 
202
                          "grandparent/alt_parent/file")
 
203
        self.assertEqual(btree.id2path("c"), "grandparent/alt_parent/file")
 
204
        self.assertEqual(btree.path2id("grandparent/alt_parent/file"), "c")
 
205
        assert btree.path2id("grandparent/parent/file") is None
 
206
 
 
207
    def unified_diff(self, old, new):
 
208
        out = StringIO()
 
209
        internal_diff("old", old, "new", new, out)
 
210
        out.seek(0,0)
 
211
        return out.read()
 
212
 
 
213
    def make_tree_2(self):
 
214
        btree = self.make_tree_1()[0]
 
215
        btree.note_rename("grandparent/parent/file", 
 
216
                          "grandparent/alt_parent/file")
 
217
        assert btree.id2path("e") is None
 
218
        assert btree.path2id("grandparent/parent/file") is None
 
219
        btree.note_id("e", "grandparent/parent/file")
 
220
        return btree
 
221
 
 
222
    def test_adds(self):
 
223
        """File/inventory adds"""
 
224
        btree = self.make_tree_2()
 
225
        add_patch = self.unified_diff([], ["Extra cheese\n"])
 
226
        btree.note_patch("grandparent/parent/file", add_patch)
 
227
        btree.note_id('f', 'grandparent/parent/symlink', kind='symlink')
 
228
        btree.note_target('grandparent/parent/symlink', 'venus')
 
229
        self.adds_test(btree)
 
230
 
 
231
    def adds_test(self, btree):
 
232
        self.assertEqual(btree.id2path("e"), "grandparent/parent/file")
 
233
        self.assertEqual(btree.path2id("grandparent/parent/file"), "e")
 
234
        self.assertEqual(btree.get_file("e").read(), "Extra cheese\n")
 
235
        self.assertEqual(btree.get_symlink_target('f'), 'venus')
 
236
 
 
237
    def test_adds2(self):
 
238
        """File/inventory adds, with patch-compatibile renames"""
 
239
        btree = self.make_tree_2()
 
240
        btree.contents_by_id = False
 
241
        add_patch = self.unified_diff(["Hello\n"], ["Extra cheese\n"])
 
242
        btree.note_patch("grandparent/parent/file", add_patch)
 
243
        btree.note_id('f', 'grandparent/parent/symlink', kind='symlink')
 
244
        btree.note_target('grandparent/parent/symlink', 'venus')
 
245
        self.adds_test(btree)
 
246
 
 
247
    def make_tree_3(self):
 
248
        btree, mtree = self.make_tree_1()
 
249
        mtree.add_file("e", "grandparent/parent/topping", "Anchovies\n")
 
250
        btree.note_rename("grandparent/parent/file", 
 
251
                          "grandparent/alt_parent/file")
 
252
        btree.note_rename("grandparent/parent/topping", 
 
253
                          "grandparent/alt_parent/stopping")
 
254
        return btree
 
255
 
 
256
    def get_file_test(self, btree):
 
257
        self.assertEqual(btree.get_file("e").read(), "Lemon\n")
 
258
        self.assertEqual(btree.get_file("c").read(), "Hello\n")
 
259
 
 
260
    def test_get_file(self):
 
261
        """Get file contents"""
 
262
        btree = self.make_tree_3()
 
263
        mod_patch = self.unified_diff(["Anchovies\n"], ["Lemon\n"])
 
264
        btree.note_patch("grandparent/alt_parent/stopping", mod_patch)
 
265
        self.get_file_test(btree)
 
266
 
 
267
    def test_get_file2(self):
 
268
        """Get file contents, with patch-compatibile renames"""
 
269
        btree = self.make_tree_3()
 
270
        btree.contents_by_id = False
 
271
        mod_patch = self.unified_diff([], ["Lemon\n"])
 
272
        btree.note_patch("grandparent/alt_parent/stopping", mod_patch)
 
273
        mod_patch = self.unified_diff([], ["Hello\n"])
 
274
        btree.note_patch("grandparent/alt_parent/file", mod_patch)
 
275
        self.get_file_test(btree)
 
276
 
 
277
    def test_delete(self):
 
278
        "Deletion by bundle"
 
279
        btree = self.make_tree_1()[0]
 
280
        self.assertEqual(btree.get_file("c").read(), "Hello\n")
 
281
        btree.note_deletion("grandparent/parent/file")
 
282
        assert btree.id2path("c") is None
 
283
        assert btree.path2id("grandparent/parent/file") is None
 
284
 
 
285
    def sorted_ids(self, tree):
 
286
        ids = list(tree)
 
287
        ids.sort()
 
288
        return ids
 
289
 
 
290
    def test_iteration(self):
 
291
        """Ensure that iteration through ids works properly"""
 
292
        btree = self.make_tree_1()[0]
 
293
        self.assertEqual(self.sorted_ids(btree), ['a', 'b', 'c', 'd'])
 
294
        btree.note_deletion("grandparent/parent/file")
 
295
        btree.note_id("e", "grandparent/alt_parent/fool", kind="directory")
 
296
        btree.note_last_changed("grandparent/alt_parent/fool", 
 
297
                                "revisionidiguess")
 
298
        self.assertEqual(self.sorted_ids(btree), ['a', 'b', 'd', 'e'])
 
299
 
 
300
 
 
301
class BundleTester(TestCaseWithTransport):
 
302
 
 
303
    def create_bundle_text(self, base_rev_id, rev_id):
 
304
        bundle_txt = StringIO()
 
305
        rev_ids = write_bundle(self.b1.repository, rev_id, base_rev_id, 
 
306
                               bundle_txt)
 
307
        bundle_txt.seek(0)
 
308
        self.assertEqual(bundle_txt.readline(), 
 
309
                         '# Bazaar revision bundle v0.8\n')
 
310
        self.assertEqual(bundle_txt.readline(), '#\n')
 
311
 
 
312
        rev = self.b1.repository.get_revision(rev_id)
 
313
        self.assertEqual(bundle_txt.readline().decode('utf-8'),
 
314
                         u'# message:\n')
 
315
 
 
316
        open(',,bundle', 'wb').write(bundle_txt.getvalue())
 
317
        bundle_txt.seek(0)
 
318
        return bundle_txt, rev_ids
 
319
 
 
320
    def get_valid_bundle(self, base_rev_id, rev_id, checkout_dir=None):
 
321
        """Create a bundle from base_rev_id -> rev_id in built-in branch.
 
322
        Make sure that the text generated is valid, and that it
 
323
        can be applied against the base, and generate the same information.
 
324
        
 
325
        :return: The in-memory bundle 
 
326
        """
 
327
        bundle_txt, rev_ids = self.create_bundle_text(base_rev_id, rev_id)
 
328
 
 
329
        # This should also validate the generated bundle 
 
330
        bundle = read_bundle(bundle_txt)
 
331
        repository = self.b1.repository
 
332
        for bundle_rev in bundle.real_revisions:
 
333
            # These really should have already been checked when we read the
 
334
            # bundle, since it computes the sha1 hash for the revision, which
 
335
            # only will match if everything is okay, but lets be explicit about
 
336
            # it
 
337
            branch_rev = repository.get_revision(bundle_rev.revision_id)
 
338
            for a in ('inventory_sha1', 'revision_id', 'parent_ids',
 
339
                      'timestamp', 'timezone', 'message', 'committer', 
 
340
                      'parent_ids', 'properties'):
 
341
                self.assertEqual(getattr(branch_rev, a), 
 
342
                                 getattr(bundle_rev, a))
 
343
            self.assertEqual(len(branch_rev.parent_ids), 
 
344
                             len(bundle_rev.parent_ids))
 
345
        self.assertEqual(rev_ids, 
 
346
                         [r.revision_id for r in bundle.real_revisions])
 
347
        self.valid_apply_bundle(base_rev_id, bundle,
 
348
                                   checkout_dir=checkout_dir)
 
349
 
 
350
        return bundle
 
351
 
 
352
    def get_invalid_bundle(self, base_rev_id, rev_id):
 
353
        """Create a bundle from base_rev_id -> rev_id in built-in branch.
 
354
        Munge the text so that it's invalid.
 
355
        
 
356
        :return: The in-memory bundle
 
357
        """
 
358
        bundle_txt, rev_ids = self.create_bundle_text(base_rev_id, rev_id)
 
359
        new_text = bundle_txt.getvalue().replace('executable:no', 
 
360
                                               'executable:yes')
 
361
        bundle_txt = StringIO(new_text)
 
362
        bundle = read_bundle(bundle_txt)
 
363
        self.valid_apply_bundle(base_rev_id, bundle)
 
364
        return bundle 
 
365
 
 
366
    def test_non_bundle(self):
 
367
        self.assertRaises(NotABundle, read_bundle, StringIO('#!/bin/sh\n'))
 
368
 
 
369
    def test_malformed(self):
 
370
        self.assertRaises(BadBundle, read_bundle, 
 
371
                          StringIO('# Bazaar revision bundle v'))
 
372
 
 
373
    def test_crlf_bundle(self):
 
374
        try:
 
375
            read_bundle(StringIO('# Bazaar revision bundle v0.8\r\n'))
 
376
        except BadBundle:
 
377
            # It is currently permitted for bundles with crlf line endings to
 
378
            # make read_bundle raise a BadBundle, but this should be fixed.
 
379
            # Anything else, especially NotABundle, is an error.
 
380
            pass
 
381
 
 
382
    def get_checkout(self, rev_id, checkout_dir=None):
 
383
        """Get a new tree, with the specified revision in it.
 
384
        """
 
385
 
 
386
        if checkout_dir is None:
 
387
            checkout_dir = tempfile.mkdtemp(prefix='test-branch-', dir='.')
 
388
        else:
 
389
            if not os.path.exists(checkout_dir):
 
390
                os.mkdir(checkout_dir)
 
391
        tree = BzrDir.create_standalone_workingtree(checkout_dir)
 
392
        s = StringIO()
 
393
        ancestors = write_bundle(self.b1.repository, rev_id, None, s)
 
394
        s.seek(0)
 
395
        assert isinstance(s.getvalue(), str), (
 
396
            "Bundle isn't a bytestring:\n %s..." % repr(s.getvalue())[:40])
 
397
        install_bundle(tree.branch.repository, read_bundle(s))
 
398
        for ancestor in ancestors:
 
399
            old = self.b1.repository.revision_tree(ancestor)
 
400
            new = tree.branch.repository.revision_tree(ancestor)
 
401
 
 
402
            # Check that there aren't any inventory level changes
 
403
            delta = compare_trees(old, new)
 
404
            self.assertFalse(delta.has_changed(),
 
405
                             'Revision %s not copied correctly.'
 
406
                             % (ancestor,))
 
407
 
 
408
            # Now check that the file contents are all correct
 
409
            for inventory_id in old:
 
410
                try:
 
411
                    old_file = old.get_file(inventory_id)
 
412
                except:
 
413
                    continue
 
414
                if old_file is None:
 
415
                    continue
 
416
                self.assertEqual(old_file.read(),
 
417
                                 new.get_file(inventory_id).read())
 
418
        if rev_id is not None:
 
419
            rh = self.b1.revision_history()
 
420
            tree.branch.set_revision_history(rh[:rh.index(rev_id)+1])
 
421
            tree.update()
 
422
            delta = compare_trees(self.b1.repository.revision_tree(rev_id),
 
423
                                  tree)
 
424
            self.assertFalse(delta.has_changed(),
 
425
                             'Working tree has modifications')
 
426
        return tree
 
427
 
 
428
    def valid_apply_bundle(self, base_rev_id, info, checkout_dir=None):
 
429
        """Get the base revision, apply the changes, and make
 
430
        sure everything matches the builtin branch.
 
431
        """
 
432
        to_tree = self.get_checkout(base_rev_id, checkout_dir=checkout_dir)
 
433
        repository = to_tree.branch.repository
 
434
        self.assertIs(repository.has_revision(base_rev_id), True)
 
435
        for rev in info.real_revisions:
 
436
            self.assert_(not repository.has_revision(rev.revision_id),
 
437
                'Revision {%s} present before applying bundle' 
 
438
                % rev.revision_id)
 
439
        merge_bundle(info, to_tree, True, Merge3Merger, False, False)
 
440
 
 
441
        for rev in info.real_revisions:
 
442
            self.assert_(repository.has_revision(rev.revision_id),
 
443
                'Missing revision {%s} after applying bundle' 
 
444
                % rev.revision_id)
 
445
 
 
446
        self.assert_(to_tree.branch.repository.has_revision(info.target))
 
447
        # Do we also want to verify that all the texts have been added?
 
448
 
 
449
        self.assert_(info.target in to_tree.pending_merges())
 
450
 
 
451
 
 
452
        rev = info.real_revisions[-1]
 
453
        base_tree = self.b1.repository.revision_tree(rev.revision_id)
 
454
        to_tree = to_tree.branch.repository.revision_tree(rev.revision_id)
 
455
        
 
456
        # TODO: make sure the target tree is identical to base tree
 
457
        #       we might also check the working tree.
 
458
 
 
459
        base_files = list(base_tree.list_files())
 
460
        to_files = list(to_tree.list_files())
 
461
        self.assertEqual(len(base_files), len(to_files))
 
462
        for base_file, to_file in zip(base_files, to_files):
 
463
            self.assertEqual(base_file, to_file)
 
464
 
 
465
        for path, status, kind, fileid, entry in base_files:
 
466
            # Check that the meta information is the same
 
467
            self.assertEqual(base_tree.get_file_size(fileid),
 
468
                    to_tree.get_file_size(fileid))
 
469
            self.assertEqual(base_tree.get_file_sha1(fileid),
 
470
                    to_tree.get_file_sha1(fileid))
 
471
            # Check that the contents are the same
 
472
            # This is pretty expensive
 
473
            # self.assertEqual(base_tree.get_file(fileid).read(),
 
474
            #         to_tree.get_file(fileid).read())
 
475
 
 
476
    def test_bundle(self):
 
477
        self.tree1 = self.make_branch_and_tree('b1')
 
478
        self.b1 = self.tree1.branch
 
479
 
 
480
        open('b1/one', 'wb').write('one\n')
 
481
        self.tree1.add('one')
 
482
        self.tree1.commit('add one', rev_id='a@cset-0-1')
 
483
 
 
484
        bundle = self.get_valid_bundle(None, 'a@cset-0-1')
 
485
        # FIXME: The current write_bundle api no longer supports
 
486
        #        setting a custom summary message
 
487
        #        We should re-introduce the ability, and update
 
488
        #        the tests to make sure it works.
 
489
        # bundle = self.get_valid_bundle(None, 'a@cset-0-1',
 
490
        #         message='With a specialized message')
 
491
 
 
492
        # Make sure we can handle files with spaces, tabs, other
 
493
        # bogus characters
 
494
        self.build_tree([
 
495
                'b1/with space.txt'
 
496
                , 'b1/dir/'
 
497
                , 'b1/dir/filein subdir.c'
 
498
                , 'b1/dir/WithCaps.txt'
 
499
                , 'b1/dir/ pre space'
 
500
                , 'b1/sub/'
 
501
                , 'b1/sub/sub/'
 
502
                , 'b1/sub/sub/nonempty.txt'
 
503
                ])
 
504
        open('b1/sub/sub/emptyfile.txt', 'wb').close()
 
505
        open('b1/dir/nolastnewline.txt', 'wb').write('bloop')
 
506
        tt = TreeTransform(self.tree1)
 
507
        tt.new_file('executable', tt.root, '#!/bin/sh\n', 'exe-1', True)
 
508
        tt.apply()
 
509
        self.tree1.add([
 
510
                'with space.txt'
 
511
                , 'dir'
 
512
                , 'dir/filein subdir.c'
 
513
                , 'dir/WithCaps.txt'
 
514
                , 'dir/ pre space'
 
515
                , 'dir/nolastnewline.txt'
 
516
                , 'sub'
 
517
                , 'sub/sub'
 
518
                , 'sub/sub/nonempty.txt'
 
519
                , 'sub/sub/emptyfile.txt'
 
520
                ])
 
521
        self.tree1.commit('add whitespace', rev_id='a@cset-0-2')
 
522
 
 
523
        bundle = self.get_valid_bundle('a@cset-0-1', 'a@cset-0-2')
 
524
 
 
525
        # Check a rollup bundle 
 
526
        bundle = self.get_valid_bundle(None, 'a@cset-0-2')
 
527
 
 
528
        # Now delete entries
 
529
        self.tree1.remove(
 
530
                ['sub/sub/nonempty.txt'
 
531
                , 'sub/sub/emptyfile.txt'
 
532
                , 'sub/sub'
 
533
                ])
 
534
        tt = TreeTransform(self.tree1)
 
535
        trans_id = tt.trans_id_tree_file_id('exe-1')
 
536
        tt.set_executability(False, trans_id)
 
537
        tt.apply()
 
538
        self.tree1.commit('removed', rev_id='a@cset-0-3')
 
539
        
 
540
        bundle = self.get_valid_bundle('a@cset-0-2', 'a@cset-0-3')
 
541
        self.assertRaises(TestamentMismatch, self.get_invalid_bundle, 
 
542
                          'a@cset-0-2', 'a@cset-0-3')
 
543
        # Check a rollup bundle 
 
544
        bundle = self.get_valid_bundle(None, 'a@cset-0-3')
 
545
 
 
546
        # Now move the directory
 
547
        self.tree1.rename_one('dir', 'sub/dir')
 
548
        self.tree1.commit('rename dir', rev_id='a@cset-0-4')
 
549
 
 
550
        bundle = self.get_valid_bundle('a@cset-0-3', 'a@cset-0-4')
 
551
        # Check a rollup bundle 
 
552
        bundle = self.get_valid_bundle(None, 'a@cset-0-4')
 
553
 
 
554
        # Modified files
 
555
        open('b1/sub/dir/WithCaps.txt', 'ab').write('\nAdding some text\n')
 
556
        open('b1/sub/dir/ pre space', 'ab').write('\r\nAdding some\r\nDOS format lines\r\n')
 
557
        open('b1/sub/dir/nolastnewline.txt', 'ab').write('\n')
 
558
        self.tree1.rename_one('sub/dir/ pre space', 
 
559
                              'sub/ start space')
 
560
        self.tree1.commit('Modified files', rev_id='a@cset-0-5')
 
561
        bundle = self.get_valid_bundle('a@cset-0-4', 'a@cset-0-5')
 
562
 
 
563
        self.tree1.rename_one('sub/dir/WithCaps.txt', 'temp')
 
564
        self.tree1.rename_one('with space.txt', 'WithCaps.txt')
 
565
        self.tree1.rename_one('temp', 'with space.txt')
 
566
        self.tree1.commit(u'swap filenames', rev_id='a@cset-0-6',
 
567
                          verbose=False)
 
568
        bundle = self.get_valid_bundle('a@cset-0-5', 'a@cset-0-6')
 
569
        other = self.get_checkout('a@cset-0-5')
 
570
        other.rename_one('sub/dir/nolastnewline.txt', 'sub/nolastnewline.txt')
 
571
        other.commit('rename file', rev_id='a@cset-0-6b')
 
572
        merge([other.basedir, -1], [None, None], this_dir=self.tree1.basedir)
 
573
        self.tree1.commit(u'Merge', rev_id='a@cset-0-7',
 
574
                          verbose=False)
 
575
        bundle = self.get_valid_bundle('a@cset-0-6', 'a@cset-0-7')
 
576
 
 
577
    def test_symlink_bundle(self):
 
578
        if not has_symlinks():
 
579
            raise TestSkipped("No symlink support")
 
580
        self.tree1 = BzrDir.create_standalone_workingtree('b1')
 
581
        self.b1 = self.tree1.branch
 
582
        tt = TreeTransform(self.tree1)
 
583
        tt.new_symlink('link', tt.root, 'bar/foo', 'link-1')
 
584
        tt.apply()
 
585
        self.tree1.commit('add symlink', rev_id='l@cset-0-1')
 
586
        self.get_valid_bundle(None, 'l@cset-0-1')
 
587
        tt = TreeTransform(self.tree1)
 
588
        trans_id = tt.trans_id_tree_file_id('link-1')
 
589
        tt.adjust_path('link2', tt.root, trans_id)
 
590
        tt.delete_contents(trans_id)
 
591
        tt.create_symlink('mars', trans_id)
 
592
        tt.apply()
 
593
        self.tree1.commit('rename and change symlink', rev_id='l@cset-0-2')
 
594
        self.get_valid_bundle('l@cset-0-1', 'l@cset-0-2')
 
595
        tt = TreeTransform(self.tree1)
 
596
        trans_id = tt.trans_id_tree_file_id('link-1')
 
597
        tt.delete_contents(trans_id)
 
598
        tt.create_symlink('jupiter', trans_id)
 
599
        tt.apply()
 
600
        self.tree1.commit('just change symlink target', rev_id='l@cset-0-3')
 
601
        self.get_valid_bundle('l@cset-0-2', 'l@cset-0-3')
 
602
        tt = TreeTransform(self.tree1)
 
603
        trans_id = tt.trans_id_tree_file_id('link-1')
 
604
        tt.delete_contents(trans_id)
 
605
        tt.apply()
 
606
        self.tree1.commit('Delete symlink', rev_id='l@cset-0-4')
 
607
        self.get_valid_bundle('l@cset-0-3', 'l@cset-0-4')
 
608
 
 
609
    def test_binary_bundle(self):
 
610
        self.tree1 = BzrDir.create_standalone_workingtree('b1')
 
611
        self.b1 = self.tree1.branch
 
612
        tt = TreeTransform(self.tree1)
 
613
        
 
614
        # Add
 
615
        tt.new_file('file', tt.root, '\x00\n\x00\r\x01\n\x02\r\xff', 'binary-1')
 
616
        tt.new_file('file2', tt.root, '\x01\n\x02\r\x03\n\x04\r\xff', 'binary-2')
 
617
        tt.apply()
 
618
        self.tree1.commit('add binary', rev_id='b@cset-0-1')
 
619
        self.get_valid_bundle(None, 'b@cset-0-1')
 
620
 
 
621
        # Delete
 
622
        tt = TreeTransform(self.tree1)
 
623
        trans_id = tt.trans_id_tree_file_id('binary-1')
 
624
        tt.delete_contents(trans_id)
 
625
        tt.apply()
 
626
        self.tree1.commit('delete binary', rev_id='b@cset-0-2')
 
627
        self.get_valid_bundle('b@cset-0-1', 'b@cset-0-2')
 
628
 
 
629
        # Rename & modify
 
630
        tt = TreeTransform(self.tree1)
 
631
        trans_id = tt.trans_id_tree_file_id('binary-2')
 
632
        tt.adjust_path('file3', tt.root, trans_id)
 
633
        tt.delete_contents(trans_id)
 
634
        tt.create_file('file\rcontents\x00\n\x00', trans_id)
 
635
        tt.apply()
 
636
        self.tree1.commit('rename and modify binary', rev_id='b@cset-0-3')
 
637
        self.get_valid_bundle('b@cset-0-2', 'b@cset-0-3')
 
638
 
 
639
        # Modify
 
640
        tt = TreeTransform(self.tree1)
 
641
        trans_id = tt.trans_id_tree_file_id('binary-2')
 
642
        tt.delete_contents(trans_id)
 
643
        tt.create_file('\x00file\rcontents', trans_id)
 
644
        tt.apply()
 
645
        self.tree1.commit('just modify binary', rev_id='b@cset-0-4')
 
646
        self.get_valid_bundle('b@cset-0-3', 'b@cset-0-4')
 
647
 
 
648
        # Rollup
 
649
        self.get_valid_bundle(None, 'b@cset-0-4')
 
650
 
 
651
    def test_last_modified(self):
 
652
        self.tree1 = BzrDir.create_standalone_workingtree('b1')
 
653
        self.b1 = self.tree1.branch
 
654
        tt = TreeTransform(self.tree1)
 
655
        tt.new_file('file', tt.root, 'file', 'file')
 
656
        tt.apply()
 
657
        self.tree1.commit('create file', rev_id='a@lmod-0-1')
 
658
 
 
659
        tt = TreeTransform(self.tree1)
 
660
        trans_id = tt.trans_id_tree_file_id('file')
 
661
        tt.delete_contents(trans_id)
 
662
        tt.create_file('file2', trans_id)
 
663
        tt.apply()
 
664
        self.tree1.commit('modify text', rev_id='a@lmod-0-2a')
 
665
 
 
666
        other = self.get_checkout('a@lmod-0-1')
 
667
        tt = TreeTransform(other)
 
668
        trans_id = tt.trans_id_tree_file_id('file')
 
669
        tt.delete_contents(trans_id)
 
670
        tt.create_file('file2', trans_id)
 
671
        tt.apply()
 
672
        other.commit('modify text in another tree', rev_id='a@lmod-0-2b')
 
673
        merge([other.basedir, -1], [None, None], this_dir=self.tree1.basedir)
 
674
        self.tree1.commit(u'Merge', rev_id='a@lmod-0-3',
 
675
                          verbose=False)
 
676
        self.tree1.commit(u'Merge', rev_id='a@lmod-0-4')
 
677
        bundle = self.get_valid_bundle('a@lmod-0-2a', 'a@lmod-0-4')
 
678
 
 
679
    def test_hide_history(self):
 
680
        self.tree1 = BzrDir.create_standalone_workingtree('b1')
 
681
        self.b1 = self.tree1.branch
 
682
 
 
683
        open('b1/one', 'wb').write('one\n')
 
684
        self.tree1.add('one')
 
685
        self.tree1.commit('add file', rev_id='a@cset-0-1')
 
686
        open('b1/one', 'wb').write('two\n')
 
687
        self.tree1.commit('modify', rev_id='a@cset-0-2')
 
688
        open('b1/one', 'wb').write('three\n')
 
689
        self.tree1.commit('modify', rev_id='a@cset-0-3')
 
690
        bundle_file = StringIO()
 
691
        rev_ids = write_bundle(self.tree1.branch.repository, 'a@cset-0-3',
 
692
                               'a@cset-0-1', bundle_file)
 
693
        self.assertNotContainsRe(bundle_file.getvalue(), 'two')
 
694
        self.assertContainsRe(bundle_file.getvalue(), 'one')
 
695
        self.assertContainsRe(bundle_file.getvalue(), 'three')
 
696
 
 
697
    def test_unicode_bundle(self):
 
698
        # Handle international characters
 
699
        os.mkdir('b1')
 
700
        try:
 
701
            f = open(u'b1/with Dod\xe9', 'wb')
 
702
        except UnicodeEncodeError:
 
703
            raise TestSkipped("Filesystem doesn't support unicode")
 
704
 
 
705
        self.tree1 = self.make_branch_and_tree('b1')
 
706
        self.b1 = self.tree1.branch
 
707
 
 
708
        f.write((u'A file\n'
 
709
            u'With international man of mystery\n'
 
710
            u'William Dod\xe9\n').encode('utf-8'))
 
711
        f.close()
 
712
 
 
713
        self.tree1.add([u'with Dod\xe9'])
 
714
        self.tree1.commit(u'i18n commit from William Dod\xe9', 
 
715
                          rev_id='i18n-1', committer=u'William Dod\xe9')
 
716
 
 
717
        # Add
 
718
        bundle = self.get_valid_bundle(None, 'i18n-1')
 
719
 
 
720
        # Modified
 
721
        f = open(u'b1/with Dod\xe9', 'wb')
 
722
        f.write(u'Modified \xb5\n'.encode('utf8'))
 
723
        f.close()
 
724
        self.tree1.commit(u'modified', rev_id='i18n-2')
 
725
 
 
726
        bundle = self.get_valid_bundle('i18n-1', 'i18n-2')
 
727
        
 
728
        # Renamed
 
729
        self.tree1.rename_one(u'with Dod\xe9', u'B\xe5gfors')
 
730
        self.tree1.commit(u'renamed, the new i18n man', rev_id='i18n-3',
 
731
                          committer=u'Erik B\xe5gfors')
 
732
 
 
733
        bundle = self.get_valid_bundle('i18n-2', 'i18n-3')
 
734
 
 
735
        # Removed
 
736
        self.tree1.remove([u'B\xe5gfors'])
 
737
        self.tree1.commit(u'removed', rev_id='i18n-4')
 
738
 
 
739
        bundle = self.get_valid_bundle('i18n-3', 'i18n-4')
 
740
 
 
741
        # Rollup
 
742
        bundle = self.get_valid_bundle(None, 'i18n-4')
 
743
 
 
744
 
 
745
    def test_whitespace_bundle(self):
 
746
        if sys.platform in ('win32', 'cygwin'):
 
747
            raise TestSkipped('Windows doesn\'t support filenames'
 
748
                              ' with tabs or trailing spaces')
 
749
        self.tree1 = self.make_branch_and_tree('b1')
 
750
        self.b1 = self.tree1.branch
 
751
 
 
752
        self.build_tree(['b1/trailing space '])
 
753
        self.tree1.add(['trailing space '])
 
754
        # TODO: jam 20060701 Check for handling files with '\t' characters
 
755
        #       once we actually support them
 
756
 
 
757
        # Added
 
758
        self.tree1.commit('funky whitespace', rev_id='white-1')
 
759
 
 
760
        bundle = self.get_valid_bundle(None, 'white-1')
 
761
 
 
762
        # Modified
 
763
        open('b1/trailing space ', 'ab').write('add some text\n')
 
764
        self.tree1.commit('add text', rev_id='white-2')
 
765
 
 
766
        bundle = self.get_valid_bundle('white-1', 'white-2')
 
767
 
 
768
        # Renamed
 
769
        self.tree1.rename_one('trailing space ', ' start and end space ')
 
770
        self.tree1.commit('rename', rev_id='white-3')
 
771
 
 
772
        bundle = self.get_valid_bundle('white-2', 'white-3')
 
773
 
 
774
        # Removed
 
775
        self.tree1.remove([' start and end space '])
 
776
        self.tree1.commit('removed', rev_id='white-4')
 
777
 
 
778
        bundle = self.get_valid_bundle('white-3', 'white-4')
 
779
        
 
780
        # Now test a complet roll-up
 
781
        bundle = self.get_valid_bundle(None, 'white-4')
 
782
 
 
783
    def test_alt_timezone_bundle(self):
 
784
        self.tree1 = self.make_branch_and_tree('b1')
 
785
        self.b1 = self.tree1.branch
 
786
 
 
787
        self.build_tree(['b1/newfile'])
 
788
        self.tree1.add(['newfile'])
 
789
 
 
790
        # Asia/Colombo offset = 5 hours 30 minutes
 
791
        self.tree1.commit('non-hour offset timezone', rev_id='tz-1',
 
792
                          timezone=19800, timestamp=1152544886.0)
 
793
 
 
794
        bundle = self.get_valid_bundle(None, 'tz-1')
 
795
        
 
796
        rev = bundle.revisions[0]
 
797
        self.assertEqual('Mon 2006-07-10 20:51:26.000000000 +0530', rev.date)
 
798
        self.assertEqual(19800, rev.timezone)
 
799
        self.assertEqual(1152544886.0, rev.timestamp)
 
800
 
 
801
 
 
802
class MungedBundleTester(TestCaseWithTransport):
 
803
 
 
804
    def build_test_bundle(self):
 
805
        wt = self.make_branch_and_tree('b1')
 
806
 
 
807
        self.build_tree(['b1/one'])
 
808
        wt.add('one')
 
809
        wt.commit('add one', rev_id='a@cset-0-1')
 
810
        self.build_tree(['b1/two'])
 
811
        wt.add('two')
 
812
        wt.commit('add two', rev_id='a@cset-0-2',
 
813
                  revprops={'branch-nick':'test'})
 
814
 
 
815
        bundle_txt = StringIO()
 
816
        rev_ids = write_bundle(wt.branch.repository, 'a@cset-0-2',
 
817
                               'a@cset-0-1', bundle_txt)
 
818
        self.assertEqual(['a@cset-0-2'], rev_ids)
 
819
        bundle_txt.seek(0, 0)
 
820
        return bundle_txt
 
821
 
 
822
    def check_valid(self, bundle):
 
823
        """Check that after whatever munging, the final object is valid."""
 
824
        self.assertEqual(['a@cset-0-2'],
 
825
            [r.revision_id for r in bundle.real_revisions])
 
826
 
 
827
    def test_extra_whitespace(self):
 
828
        bundle_txt = self.build_test_bundle()
 
829
 
 
830
        # Seek to the end of the file
 
831
        # Adding one extra newline used to give us
 
832
        # TypeError: float() argument must be a string or a number
 
833
        bundle_txt.seek(0, 2)
 
834
        bundle_txt.write('\n')
 
835
        bundle_txt.seek(0)
 
836
 
 
837
        bundle = read_bundle(bundle_txt)
 
838
        self.check_valid(bundle)
 
839
 
 
840
    def test_extra_whitespace_2(self):
 
841
        bundle_txt = self.build_test_bundle()
 
842
 
 
843
        # Seek to the end of the file
 
844
        # Adding two extra newlines used to give us
 
845
        # MalformedPatches: The first line of all patches should be ...
 
846
        bundle_txt.seek(0, 2)
 
847
        bundle_txt.write('\n\n')
 
848
        bundle_txt.seek(0)
 
849
 
 
850
        bundle = read_bundle(bundle_txt)
 
851
        self.check_valid(bundle)
 
852
 
 
853
    def test_missing_trailing_whitespace(self):
 
854
        bundle_txt = self.build_test_bundle()
 
855
 
 
856
        # Remove a trailing newline, it shouldn't kill the parser
 
857
        raw = bundle_txt.getvalue()
 
858
        # The contents of the bundle don't have to be this, but this
 
859
        # test is concerned with the exact case where the serializer
 
860
        # creates a blank line at the end, and fails if that
 
861
        # line is stripped
 
862
        self.assertEqual('\n\n', raw[-2:])
 
863
        bundle_txt = StringIO(raw[:-1])
 
864
 
 
865
        bundle = read_bundle(bundle_txt)
 
866
        self.check_valid(bundle)
 
867
 
 
868
    def test_opening_text(self):
 
869
        bundle_txt = self.build_test_bundle()
 
870
 
 
871
        bundle_txt = StringIO("Some random\nemail comments\n"
 
872
                              + bundle_txt.getvalue())
 
873
 
 
874
        bundle = read_bundle(bundle_txt)
 
875
        self.check_valid(bundle)
 
876
 
 
877
    def test_trailing_text(self):
 
878
        bundle_txt = self.build_test_bundle()
 
879
 
 
880
        bundle_txt = StringIO(bundle_txt.getvalue() +
 
881
                              "Some trailing\nrandom\ntext\n")
 
882
 
 
883
        bundle = read_bundle(bundle_txt)
 
884
        self.check_valid(bundle)
 
885