~bzr-pqm/bzr/bzr.dev

« back to all changes in this revision

Viewing changes to bzrlib/tests/test_bundle.py

  • Committer: Martin Pool
  • Date: 2005-06-21 06:10:18 UTC
  • Revision ID: mbp@sourcefrog.net-20050621061017-12e8f0ff45228338
- move whitebox/blackbox modules into bzrlib.selftest subdirectory

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.7\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
 
 
814
 
        bundle_txt = StringIO()
815
 
        rev_ids = write_bundle(wt.branch.repository, 'a@cset-0-2',
816
 
                               'a@cset-0-1', bundle_txt)
817
 
        self.assertEqual(['a@cset-0-2'], rev_ids)
818
 
        bundle_txt.seek(0, 0)
819
 
        return bundle_txt
820
 
 
821
 
    def check_valid(self, bundle):
822
 
        """Check that after whatever munging, the final object is valid."""
823
 
        self.assertEqual(['a@cset-0-2'], 
824
 
            [r.revision_id for r in bundle.real_revisions])
825
 
 
826
 
    def test_extra_whitespace(self):
827
 
        bundle_txt = self.build_test_bundle()
828
 
 
829
 
        # Seek to the end of the file
830
 
        # Adding one extra newline used to give us
831
 
        # TypeError: float() argument must be a string or a number
832
 
        bundle_txt.seek(0, 2)
833
 
        bundle_txt.write('\n')
834
 
        bundle_txt.seek(0)
835
 
 
836
 
        bundle = read_bundle(bundle_txt)
837
 
        self.check_valid(bundle)
838
 
 
839
 
    def test_extra_whitespace_2(self):
840
 
        bundle_txt = self.build_test_bundle()
841
 
 
842
 
        # Seek to the end of the file
843
 
        # Adding two extra newlines used to give us
844
 
        # MalformedPatches: The first line of all patches should be ...
845
 
        bundle_txt.seek(0, 2)
846
 
        bundle_txt.write('\n\n')
847
 
        bundle_txt.seek(0)
848
 
 
849
 
        bundle = read_bundle(bundle_txt)
850
 
        self.check_valid(bundle)
851
 
 
852
 
    def test_missing_trailing_whitespace(self):
853
 
        bundle_txt = self.build_test_bundle()
854
 
 
855
 
        # Remove a trailing newline, it shouldn't kill the parser
856
 
        raw = bundle_txt.getvalue()
857
 
        # The contents of the bundle don't have to be this, but this
858
 
        # test is concerned with the exact case where the serializer
859
 
        # creates a blank line at the end, and fails if that
860
 
        # line is stripped
861
 
        self.assertEqual('\n\n', raw[-2:])
862
 
        bundle_text = StringIO(raw[:-1])
863
 
 
864
 
        bundle = read_bundle(bundle_txt)
865
 
        self.check_valid(bundle)