~bzr-pqm/bzr/bzr.dev

« back to all changes in this revision

Viewing changes to bzrlib/tests/test_bundle.py

  • Committer: John Arbash Meinel
  • Date: 2006-08-16 20:43:43 UTC
  • mto: This revision was merged to the branch mainline in revision 1942.
  • Revision ID: john@arbash-meinel.com-20060816204343-2485bd147bd7e253
Write a benchmark for the XML serializer

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