/brz/remove-bazaar

To get this branch, use:
bzr branch http://gegoxaren.bato24.eu/bzr/brz/remove-bazaar

« back to all changes in this revision

Viewing changes to bzrlib/tests/test_bundle.py

  • Committer: Martin Pool
  • Date: 2007-10-03 08:06:44 UTC
  • mto: This revision was merged to the branch mainline in revision 2901.
  • Revision ID: mbp@sourcefrog.net-20071003080644-oivy0gkg98sex0ed
Avoid internal error tracebacks on failure to lock on readonly transport (#129701).

Add new LockFailed, which doesn't imply that we failed to get it because of
contention.  Raise this if we fail to create the pending or lock directories
because of Transport errors.

UnlockableTransport is not an internal error.

ReadOnlyLockError has a message which didn't match its name or usage; it's now
deprecated and callers are updated to use LockFailed which is more appropriate.

Add zero_ninetytwo deprecation symbol.

Unify assertMatchesRe with TestCase.assertContainsRe.

When the constructor is deprecated, just say that the class is deprecated, not
the __init__ method - this works better with applyDeprecated in tests.

Show diffs side-by-side

added added

removed removed

Lines of Context:
 
1
# Copyright (C) 2004, 2005, 2006, 2007 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 (
 
23
    bzrdir,
 
24
    errors,
 
25
    inventory,
 
26
    repository,
 
27
    revision as _mod_revision,
 
28
    treebuilder,
 
29
    )
 
30
from bzrlib.bzrdir import BzrDir
 
31
from bzrlib.bundle.apply_bundle import install_bundle, merge_bundle
 
32
from bzrlib.bundle.bundle_data import BundleTree
 
33
from bzrlib.bundle.serializer import write_bundle, read_bundle, v09, v4
 
34
from bzrlib.bundle.serializer.v08 import BundleSerializerV08
 
35
from bzrlib.bundle.serializer.v09 import BundleSerializerV09
 
36
from bzrlib.bundle.serializer.v4 import BundleSerializerV4
 
37
from bzrlib.branch import Branch
 
38
from bzrlib.diff import internal_diff
 
39
from bzrlib.errors import (BzrError, TestamentMismatch, NotABundle, BadBundle, 
 
40
                           NoSuchFile,)
 
41
from bzrlib.merge import Merge3Merger
 
42
from bzrlib.repofmt import knitrepo
 
43
from bzrlib.osutils import has_symlinks, sha_file
 
44
from bzrlib.tests import (TestCaseInTempDir, TestCaseWithTransport,
 
45
                          TestCase, TestSkipped, test_commit)
 
46
from bzrlib.transform import TreeTransform
 
47
 
 
48
 
 
49
class MockTree(object):
 
50
    def __init__(self):
 
51
        from bzrlib.inventory import InventoryDirectory, ROOT_ID
 
52
        object.__init__(self)
 
53
        self.paths = {ROOT_ID: ""}
 
54
        self.ids = {"": ROOT_ID}
 
55
        self.contents = {}
 
56
        self.root = InventoryDirectory(ROOT_ID, '', None)
 
57
 
 
58
    inventory = property(lambda x:x)
 
59
 
 
60
    def __iter__(self):
 
61
        return self.paths.iterkeys()
 
62
 
 
63
    def __getitem__(self, file_id):
 
64
        if file_id == self.root.file_id:
 
65
            return self.root
 
66
        else:
 
67
            return self.make_entry(file_id, self.paths[file_id])
 
68
 
 
69
    def parent_id(self, file_id):
 
70
        parent_dir = os.path.dirname(self.paths[file_id])
 
71
        if parent_dir == "":
 
72
            return None
 
73
        return self.ids[parent_dir]
 
74
 
 
75
    def iter_entries(self):
 
76
        for path, file_id in self.ids.iteritems():
 
77
            yield path, self[file_id]
 
78
 
 
79
    def get_file_kind(self, file_id):
 
80
        if file_id in self.contents:
 
81
            kind = 'file'
 
82
        else:
 
83
            kind = 'directory'
 
84
        return kind
 
85
 
 
86
    def make_entry(self, file_id, path):
 
87
        from bzrlib.inventory import (InventoryEntry, InventoryFile
 
88
                                    , InventoryDirectory, InventoryLink)
 
89
        name = os.path.basename(path)
 
90
        kind = self.get_file_kind(file_id)
 
91
        parent_id = self.parent_id(file_id)
 
92
        text_sha_1, text_size = self.contents_stats(file_id)
 
93
        if kind == 'directory':
 
94
            ie = InventoryDirectory(file_id, name, parent_id)
 
95
        elif kind == 'file':
 
96
            ie = InventoryFile(file_id, name, parent_id)
 
97
        elif kind == 'symlink':
 
98
            ie = InventoryLink(file_id, name, parent_id)
 
99
        else:
 
100
            raise BzrError('unknown kind %r' % kind)
 
101
        ie.text_sha1 = text_sha_1
 
102
        ie.text_size = text_size
 
103
        return ie
 
104
 
 
105
    def add_dir(self, file_id, path):
 
106
        self.paths[file_id] = path
 
107
        self.ids[path] = file_id
 
108
    
 
109
    def add_file(self, file_id, path, contents):
 
110
        self.add_dir(file_id, path)
 
111
        self.contents[file_id] = contents
 
112
 
 
113
    def path2id(self, path):
 
114
        return self.ids.get(path)
 
115
 
 
116
    def id2path(self, file_id):
 
117
        return self.paths.get(file_id)
 
118
 
 
119
    def has_id(self, file_id):
 
120
        return self.id2path(file_id) is not None
 
121
 
 
122
    def get_file(self, file_id):
 
123
        result = StringIO()
 
124
        result.write(self.contents[file_id])
 
125
        result.seek(0,0)
 
126
        return result
 
127
 
 
128
    def contents_stats(self, file_id):
 
129
        if file_id not in self.contents:
 
130
            return None, None
 
131
        text_sha1 = sha_file(self.get_file(file_id))
 
132
        return text_sha1, len(self.contents[file_id])
 
133
 
 
134
 
 
135
class BTreeTester(TestCase):
 
136
    """A simple unittest tester for the BundleTree class."""
 
137
 
 
138
    def make_tree_1(self):
 
139
        mtree = MockTree()
 
140
        mtree.add_dir("a", "grandparent")
 
141
        mtree.add_dir("b", "grandparent/parent")
 
142
        mtree.add_file("c", "grandparent/parent/file", "Hello\n")
 
143
        mtree.add_dir("d", "grandparent/alt_parent")
 
144
        return BundleTree(mtree, ''), mtree
 
145
        
 
146
    def test_renames(self):
 
147
        """Ensure that file renames have the proper effect on children"""
 
148
        btree = self.make_tree_1()[0]
 
149
        self.assertEqual(btree.old_path("grandparent"), "grandparent")
 
150
        self.assertEqual(btree.old_path("grandparent/parent"), 
 
151
                         "grandparent/parent")
 
152
        self.assertEqual(btree.old_path("grandparent/parent/file"),
 
153
                         "grandparent/parent/file")
 
154
 
 
155
        self.assertEqual(btree.id2path("a"), "grandparent")
 
156
        self.assertEqual(btree.id2path("b"), "grandparent/parent")
 
157
        self.assertEqual(btree.id2path("c"), "grandparent/parent/file")
 
158
 
 
159
        self.assertEqual(btree.path2id("grandparent"), "a")
 
160
        self.assertEqual(btree.path2id("grandparent/parent"), "b")
 
161
        self.assertEqual(btree.path2id("grandparent/parent/file"), "c")
 
162
 
 
163
        assert btree.path2id("grandparent2") is None
 
164
        assert btree.path2id("grandparent2/parent") is None
 
165
        assert btree.path2id("grandparent2/parent/file") is None
 
166
 
 
167
        btree.note_rename("grandparent", "grandparent2")
 
168
        assert btree.old_path("grandparent") is None
 
169
        assert btree.old_path("grandparent/parent") is None
 
170
        assert btree.old_path("grandparent/parent/file") is None
 
171
 
 
172
        self.assertEqual(btree.id2path("a"), "grandparent2")
 
173
        self.assertEqual(btree.id2path("b"), "grandparent2/parent")
 
174
        self.assertEqual(btree.id2path("c"), "grandparent2/parent/file")
 
175
 
 
176
        self.assertEqual(btree.path2id("grandparent2"), "a")
 
177
        self.assertEqual(btree.path2id("grandparent2/parent"), "b")
 
178
        self.assertEqual(btree.path2id("grandparent2/parent/file"), "c")
 
179
 
 
180
        assert btree.path2id("grandparent") is None
 
181
        assert btree.path2id("grandparent/parent") is None
 
182
        assert btree.path2id("grandparent/parent/file") is None
 
183
 
 
184
        btree.note_rename("grandparent/parent", "grandparent2/parent2")
 
185
        self.assertEqual(btree.id2path("a"), "grandparent2")
 
186
        self.assertEqual(btree.id2path("b"), "grandparent2/parent2")
 
187
        self.assertEqual(btree.id2path("c"), "grandparent2/parent2/file")
 
188
 
 
189
        self.assertEqual(btree.path2id("grandparent2"), "a")
 
190
        self.assertEqual(btree.path2id("grandparent2/parent2"), "b")
 
191
        self.assertEqual(btree.path2id("grandparent2/parent2/file"), "c")
 
192
 
 
193
        assert btree.path2id("grandparent2/parent") is None
 
194
        assert btree.path2id("grandparent2/parent/file") is None
 
195
 
 
196
        btree.note_rename("grandparent/parent/file", 
 
197
                          "grandparent2/parent2/file2")
 
198
        self.assertEqual(btree.id2path("a"), "grandparent2")
 
199
        self.assertEqual(btree.id2path("b"), "grandparent2/parent2")
 
200
        self.assertEqual(btree.id2path("c"), "grandparent2/parent2/file2")
 
201
 
 
202
        self.assertEqual(btree.path2id("grandparent2"), "a")
 
203
        self.assertEqual(btree.path2id("grandparent2/parent2"), "b")
 
204
        self.assertEqual(btree.path2id("grandparent2/parent2/file2"), "c")
 
205
 
 
206
        assert btree.path2id("grandparent2/parent2/file") is None
 
207
 
 
208
    def test_moves(self):
 
209
        """Ensure that file moves have the proper effect on children"""
 
210
        btree = self.make_tree_1()[0]
 
211
        btree.note_rename("grandparent/parent/file", 
 
212
                          "grandparent/alt_parent/file")
 
213
        self.assertEqual(btree.id2path("c"), "grandparent/alt_parent/file")
 
214
        self.assertEqual(btree.path2id("grandparent/alt_parent/file"), "c")
 
215
        assert btree.path2id("grandparent/parent/file") is None
 
216
 
 
217
    def unified_diff(self, old, new):
 
218
        out = StringIO()
 
219
        internal_diff("old", old, "new", new, out)
 
220
        out.seek(0,0)
 
221
        return out.read()
 
222
 
 
223
    def make_tree_2(self):
 
224
        btree = self.make_tree_1()[0]
 
225
        btree.note_rename("grandparent/parent/file", 
 
226
                          "grandparent/alt_parent/file")
 
227
        assert btree.id2path("e") is None
 
228
        assert btree.path2id("grandparent/parent/file") is None
 
229
        btree.note_id("e", "grandparent/parent/file")
 
230
        return btree
 
231
 
 
232
    def test_adds(self):
 
233
        """File/inventory adds"""
 
234
        btree = self.make_tree_2()
 
235
        add_patch = self.unified_diff([], ["Extra cheese\n"])
 
236
        btree.note_patch("grandparent/parent/file", add_patch)
 
237
        btree.note_id('f', 'grandparent/parent/symlink', kind='symlink')
 
238
        btree.note_target('grandparent/parent/symlink', 'venus')
 
239
        self.adds_test(btree)
 
240
 
 
241
    def adds_test(self, btree):
 
242
        self.assertEqual(btree.id2path("e"), "grandparent/parent/file")
 
243
        self.assertEqual(btree.path2id("grandparent/parent/file"), "e")
 
244
        self.assertEqual(btree.get_file("e").read(), "Extra cheese\n")
 
245
        self.assertEqual(btree.get_symlink_target('f'), 'venus')
 
246
 
 
247
    def test_adds2(self):
 
248
        """File/inventory adds, with patch-compatibile renames"""
 
249
        btree = self.make_tree_2()
 
250
        btree.contents_by_id = False
 
251
        add_patch = self.unified_diff(["Hello\n"], ["Extra cheese\n"])
 
252
        btree.note_patch("grandparent/parent/file", add_patch)
 
253
        btree.note_id('f', 'grandparent/parent/symlink', kind='symlink')
 
254
        btree.note_target('grandparent/parent/symlink', 'venus')
 
255
        self.adds_test(btree)
 
256
 
 
257
    def make_tree_3(self):
 
258
        btree, mtree = self.make_tree_1()
 
259
        mtree.add_file("e", "grandparent/parent/topping", "Anchovies\n")
 
260
        btree.note_rename("grandparent/parent/file", 
 
261
                          "grandparent/alt_parent/file")
 
262
        btree.note_rename("grandparent/parent/topping", 
 
263
                          "grandparent/alt_parent/stopping")
 
264
        return btree
 
265
 
 
266
    def get_file_test(self, btree):
 
267
        self.assertEqual(btree.get_file("e").read(), "Lemon\n")
 
268
        self.assertEqual(btree.get_file("c").read(), "Hello\n")
 
269
 
 
270
    def test_get_file(self):
 
271
        """Get file contents"""
 
272
        btree = self.make_tree_3()
 
273
        mod_patch = self.unified_diff(["Anchovies\n"], ["Lemon\n"])
 
274
        btree.note_patch("grandparent/alt_parent/stopping", mod_patch)
 
275
        self.get_file_test(btree)
 
276
 
 
277
    def test_get_file2(self):
 
278
        """Get file contents, with patch-compatibile renames"""
 
279
        btree = self.make_tree_3()
 
280
        btree.contents_by_id = False
 
281
        mod_patch = self.unified_diff([], ["Lemon\n"])
 
282
        btree.note_patch("grandparent/alt_parent/stopping", mod_patch)
 
283
        mod_patch = self.unified_diff([], ["Hello\n"])
 
284
        btree.note_patch("grandparent/alt_parent/file", mod_patch)
 
285
        self.get_file_test(btree)
 
286
 
 
287
    def test_delete(self):
 
288
        "Deletion by bundle"
 
289
        btree = self.make_tree_1()[0]
 
290
        self.assertEqual(btree.get_file("c").read(), "Hello\n")
 
291
        btree.note_deletion("grandparent/parent/file")
 
292
        assert btree.id2path("c") is None
 
293
        assert btree.path2id("grandparent/parent/file") is None
 
294
 
 
295
    def sorted_ids(self, tree):
 
296
        ids = list(tree)
 
297
        ids.sort()
 
298
        return ids
 
299
 
 
300
    def test_iteration(self):
 
301
        """Ensure that iteration through ids works properly"""
 
302
        btree = self.make_tree_1()[0]
 
303
        self.assertEqual(self.sorted_ids(btree),
 
304
            [inventory.ROOT_ID, 'a', 'b', 'c', 'd'])
 
305
        btree.note_deletion("grandparent/parent/file")
 
306
        btree.note_id("e", "grandparent/alt_parent/fool", kind="directory")
 
307
        btree.note_last_changed("grandparent/alt_parent/fool", 
 
308
                                "revisionidiguess")
 
309
        self.assertEqual(self.sorted_ids(btree),
 
310
            [inventory.ROOT_ID, 'a', 'b', 'd', 'e'])
 
311
 
 
312
 
 
313
class BundleTester1(TestCaseWithTransport):
 
314
 
 
315
    def test_mismatched_bundle(self):
 
316
        format = bzrdir.BzrDirMetaFormat1()
 
317
        format.repository_format = knitrepo.RepositoryFormatKnit3()
 
318
        serializer = BundleSerializerV08('0.8')
 
319
        b = self.make_branch('.', format=format)
 
320
        self.assertRaises(errors.IncompatibleBundleFormat, serializer.write, 
 
321
                          b.repository, [], {}, StringIO())
 
322
 
 
323
    def test_matched_bundle(self):
 
324
        """Don't raise IncompatibleBundleFormat for knit2 and bundle0.9"""
 
325
        format = bzrdir.BzrDirMetaFormat1()
 
326
        format.repository_format = knitrepo.RepositoryFormatKnit3()
 
327
        serializer = BundleSerializerV09('0.9')
 
328
        b = self.make_branch('.', format=format)
 
329
        serializer.write(b.repository, [], {}, StringIO())
 
330
 
 
331
    def test_mismatched_model(self):
 
332
        """Try copying a bundle from knit2 to knit1"""
 
333
        format = bzrdir.BzrDirMetaFormat1()
 
334
        format.repository_format = knitrepo.RepositoryFormatKnit3()
 
335
        source = self.make_branch_and_tree('source', format=format)
 
336
        source.commit('one', rev_id='one-id')
 
337
        source.commit('two', rev_id='two-id')
 
338
        text = StringIO()
 
339
        write_bundle(source.branch.repository, 'two-id', 'null:', text,
 
340
                     format='0.9')
 
341
        text.seek(0)
 
342
 
 
343
        format = bzrdir.BzrDirMetaFormat1()
 
344
        format.repository_format = knitrepo.RepositoryFormatKnit1()
 
345
        target = self.make_branch('target', format=format)
 
346
        self.assertRaises(errors.IncompatibleRevision, install_bundle, 
 
347
                          target.repository, read_bundle(text))
 
348
 
 
349
 
 
350
class BundleTester(object):
 
351
 
 
352
    def bzrdir_format(self):
 
353
        format = bzrdir.BzrDirMetaFormat1()
 
354
        format.repository_format = knitrepo.RepositoryFormatKnit1()
 
355
        return format
 
356
 
 
357
    def make_branch_and_tree(self, path, format=None):
 
358
        if format is None:
 
359
            format = self.bzrdir_format()
 
360
        return TestCaseWithTransport.make_branch_and_tree(self, path, format)
 
361
 
 
362
    def make_branch(self, path, format=None):
 
363
        if format is None:
 
364
            format = self.bzrdir_format()
 
365
        return TestCaseWithTransport.make_branch(self, path, format)
 
366
 
 
367
    def create_bundle_text(self, base_rev_id, rev_id):
 
368
        bundle_txt = StringIO()
 
369
        rev_ids = write_bundle(self.b1.repository, rev_id, base_rev_id, 
 
370
                               bundle_txt, format=self.format)
 
371
        bundle_txt.seek(0)
 
372
        self.assertEqual(bundle_txt.readline(), 
 
373
                         '# Bazaar revision bundle v%s\n' % self.format)
 
374
        self.assertEqual(bundle_txt.readline(), '#\n')
 
375
 
 
376
        rev = self.b1.repository.get_revision(rev_id)
 
377
        self.assertEqual(bundle_txt.readline().decode('utf-8'),
 
378
                         u'# message:\n')
 
379
        bundle_txt.seek(0)
 
380
        return bundle_txt, rev_ids
 
381
 
 
382
    def get_valid_bundle(self, base_rev_id, rev_id, checkout_dir=None):
 
383
        """Create a bundle from base_rev_id -> rev_id in built-in branch.
 
384
        Make sure that the text generated is valid, and that it
 
385
        can be applied against the base, and generate the same information.
 
386
        
 
387
        :return: The in-memory bundle 
 
388
        """
 
389
        bundle_txt, rev_ids = self.create_bundle_text(base_rev_id, rev_id)
 
390
 
 
391
        # This should also validate the generated bundle 
 
392
        bundle = read_bundle(bundle_txt)
 
393
        repository = self.b1.repository
 
394
        for bundle_rev in bundle.real_revisions:
 
395
            # These really should have already been checked when we read the
 
396
            # bundle, since it computes the sha1 hash for the revision, which
 
397
            # only will match if everything is okay, but lets be explicit about
 
398
            # it
 
399
            branch_rev = repository.get_revision(bundle_rev.revision_id)
 
400
            for a in ('inventory_sha1', 'revision_id', 'parent_ids',
 
401
                      'timestamp', 'timezone', 'message', 'committer', 
 
402
                      'parent_ids', 'properties'):
 
403
                self.assertEqual(getattr(branch_rev, a), 
 
404
                                 getattr(bundle_rev, a))
 
405
            self.assertEqual(len(branch_rev.parent_ids), 
 
406
                             len(bundle_rev.parent_ids))
 
407
        self.assertEqual(rev_ids, 
 
408
                         [r.revision_id for r in bundle.real_revisions])
 
409
        self.valid_apply_bundle(base_rev_id, bundle,
 
410
                                   checkout_dir=checkout_dir)
 
411
 
 
412
        return bundle
 
413
 
 
414
    def get_invalid_bundle(self, base_rev_id, rev_id):
 
415
        """Create a bundle from base_rev_id -> rev_id in built-in branch.
 
416
        Munge the text so that it's invalid.
 
417
        
 
418
        :return: The in-memory bundle
 
419
        """
 
420
        bundle_txt, rev_ids = self.create_bundle_text(base_rev_id, rev_id)
 
421
        new_text = bundle_txt.getvalue().replace('executable:no', 
 
422
                                               'executable:yes')
 
423
        bundle_txt = StringIO(new_text)
 
424
        bundle = read_bundle(bundle_txt)
 
425
        self.valid_apply_bundle(base_rev_id, bundle)
 
426
        return bundle 
 
427
 
 
428
    def test_non_bundle(self):
 
429
        self.assertRaises(NotABundle, read_bundle, StringIO('#!/bin/sh\n'))
 
430
 
 
431
    def test_malformed(self):
 
432
        self.assertRaises(BadBundle, read_bundle, 
 
433
                          StringIO('# Bazaar revision bundle v'))
 
434
 
 
435
    def test_crlf_bundle(self):
 
436
        try:
 
437
            read_bundle(StringIO('# Bazaar revision bundle v0.8\r\n'))
 
438
        except BadBundle:
 
439
            # It is currently permitted for bundles with crlf line endings to
 
440
            # make read_bundle raise a BadBundle, but this should be fixed.
 
441
            # Anything else, especially NotABundle, is an error.
 
442
            pass
 
443
 
 
444
    def get_checkout(self, rev_id, checkout_dir=None):
 
445
        """Get a new tree, with the specified revision in it.
 
446
        """
 
447
 
 
448
        if checkout_dir is None:
 
449
            checkout_dir = tempfile.mkdtemp(prefix='test-branch-', dir='.')
 
450
        else:
 
451
            if not os.path.exists(checkout_dir):
 
452
                os.mkdir(checkout_dir)
 
453
        tree = self.make_branch_and_tree(checkout_dir)
 
454
        s = StringIO()
 
455
        ancestors = write_bundle(self.b1.repository, rev_id, 'null:', s,
 
456
                                 format=self.format)
 
457
        s.seek(0)
 
458
        assert isinstance(s.getvalue(), str), (
 
459
            "Bundle isn't a bytestring:\n %s..." % repr(s.getvalue())[:40])
 
460
        install_bundle(tree.branch.repository, read_bundle(s))
 
461
        for ancestor in ancestors:
 
462
            old = self.b1.repository.revision_tree(ancestor)
 
463
            new = tree.branch.repository.revision_tree(ancestor)
 
464
 
 
465
            # Check that there aren't any inventory level changes
 
466
            delta = new.changes_from(old)
 
467
            self.assertFalse(delta.has_changed(),
 
468
                             'Revision %s not copied correctly.'
 
469
                             % (ancestor,))
 
470
 
 
471
            # Now check that the file contents are all correct
 
472
            for inventory_id in old:
 
473
                try:
 
474
                    old_file = old.get_file(inventory_id)
 
475
                except NoSuchFile:
 
476
                    continue
 
477
                if old_file is None:
 
478
                    continue
 
479
                self.assertEqual(old_file.read(),
 
480
                                 new.get_file(inventory_id).read())
 
481
        if not _mod_revision.is_null(rev_id):
 
482
            rh = self.b1.revision_history()
 
483
            tree.branch.set_revision_history(rh[:rh.index(rev_id)+1])
 
484
            tree.update()
 
485
            delta = tree.changes_from(self.b1.repository.revision_tree(rev_id))
 
486
            self.assertFalse(delta.has_changed(),
 
487
                             'Working tree has modifications: %s' % delta)
 
488
        return tree
 
489
 
 
490
    def valid_apply_bundle(self, base_rev_id, info, checkout_dir=None):
 
491
        """Get the base revision, apply the changes, and make
 
492
        sure everything matches the builtin branch.
 
493
        """
 
494
        to_tree = self.get_checkout(base_rev_id, checkout_dir=checkout_dir)
 
495
        original_parents = to_tree.get_parent_ids()
 
496
        repository = to_tree.branch.repository
 
497
        original_parents = to_tree.get_parent_ids()
 
498
        self.assertIs(repository.has_revision(base_rev_id), True)
 
499
        for rev in info.real_revisions:
 
500
            self.assert_(not repository.has_revision(rev.revision_id),
 
501
                'Revision {%s} present before applying bundle' 
 
502
                % rev.revision_id)
 
503
        merge_bundle(info, to_tree, True, Merge3Merger, False, False)
 
504
 
 
505
        for rev in info.real_revisions:
 
506
            self.assert_(repository.has_revision(rev.revision_id),
 
507
                'Missing revision {%s} after applying bundle' 
 
508
                % rev.revision_id)
 
509
 
 
510
        self.assert_(to_tree.branch.repository.has_revision(info.target))
 
511
        # Do we also want to verify that all the texts have been added?
 
512
 
 
513
        self.assertEqual(original_parents + [info.target],
 
514
            to_tree.get_parent_ids())
 
515
 
 
516
        rev = info.real_revisions[-1]
 
517
        base_tree = self.b1.repository.revision_tree(rev.revision_id)
 
518
        to_tree = to_tree.branch.repository.revision_tree(rev.revision_id)
 
519
        
 
520
        # TODO: make sure the target tree is identical to base tree
 
521
        #       we might also check the working tree.
 
522
 
 
523
        base_files = list(base_tree.list_files())
 
524
        to_files = list(to_tree.list_files())
 
525
        self.assertEqual(len(base_files), len(to_files))
 
526
        for base_file, to_file in zip(base_files, to_files):
 
527
            self.assertEqual(base_file, to_file)
 
528
 
 
529
        for path, status, kind, fileid, entry in base_files:
 
530
            # Check that the meta information is the same
 
531
            self.assertEqual(base_tree.get_file_size(fileid),
 
532
                    to_tree.get_file_size(fileid))
 
533
            self.assertEqual(base_tree.get_file_sha1(fileid),
 
534
                    to_tree.get_file_sha1(fileid))
 
535
            # Check that the contents are the same
 
536
            # This is pretty expensive
 
537
            # self.assertEqual(base_tree.get_file(fileid).read(),
 
538
            #         to_tree.get_file(fileid).read())
 
539
 
 
540
    def test_bundle(self):
 
541
        self.tree1 = self.make_branch_and_tree('b1')
 
542
        self.b1 = self.tree1.branch
 
543
 
 
544
        open('b1/one', 'wb').write('one\n')
 
545
        self.tree1.add('one')
 
546
        self.tree1.commit('add one', rev_id='a@cset-0-1')
 
547
 
 
548
        bundle = self.get_valid_bundle('null:', 'a@cset-0-1')
 
549
 
 
550
        # Make sure we can handle files with spaces, tabs, other
 
551
        # bogus characters
 
552
        self.build_tree([
 
553
                'b1/with space.txt'
 
554
                , 'b1/dir/'
 
555
                , 'b1/dir/filein subdir.c'
 
556
                , 'b1/dir/WithCaps.txt'
 
557
                , 'b1/dir/ pre space'
 
558
                , 'b1/sub/'
 
559
                , 'b1/sub/sub/'
 
560
                , 'b1/sub/sub/nonempty.txt'
 
561
                ])
 
562
        open('b1/sub/sub/emptyfile.txt', 'wb').close()
 
563
        open('b1/dir/nolastnewline.txt', 'wb').write('bloop')
 
564
        tt = TreeTransform(self.tree1)
 
565
        tt.new_file('executable', tt.root, '#!/bin/sh\n', 'exe-1', True)
 
566
        tt.apply()
 
567
        # have to fix length of file-id so that we can predictably rewrite
 
568
        # a (length-prefixed) record containing it later.
 
569
        self.tree1.add('with space.txt', 'withspace-id')
 
570
        self.tree1.add([
 
571
                  'dir'
 
572
                , 'dir/filein subdir.c'
 
573
                , 'dir/WithCaps.txt'
 
574
                , 'dir/ pre space'
 
575
                , 'dir/nolastnewline.txt'
 
576
                , 'sub'
 
577
                , 'sub/sub'
 
578
                , 'sub/sub/nonempty.txt'
 
579
                , 'sub/sub/emptyfile.txt'
 
580
                ])
 
581
        self.tree1.commit('add whitespace', rev_id='a@cset-0-2')
 
582
 
 
583
        bundle = self.get_valid_bundle('a@cset-0-1', 'a@cset-0-2')
 
584
 
 
585
        # Check a rollup bundle 
 
586
        bundle = self.get_valid_bundle('null:', 'a@cset-0-2')
 
587
 
 
588
        # Now delete entries
 
589
        self.tree1.remove(
 
590
                ['sub/sub/nonempty.txt'
 
591
                , 'sub/sub/emptyfile.txt'
 
592
                , 'sub/sub'
 
593
                ])
 
594
        tt = TreeTransform(self.tree1)
 
595
        trans_id = tt.trans_id_tree_file_id('exe-1')
 
596
        tt.set_executability(False, trans_id)
 
597
        tt.apply()
 
598
        self.tree1.commit('removed', rev_id='a@cset-0-3')
 
599
        
 
600
        bundle = self.get_valid_bundle('a@cset-0-2', 'a@cset-0-3')
 
601
        self.assertRaises((TestamentMismatch,
 
602
            errors.VersionedFileInvalidChecksum), self.get_invalid_bundle,
 
603
            'a@cset-0-2', 'a@cset-0-3')
 
604
        # Check a rollup bundle 
 
605
        bundle = self.get_valid_bundle('null:', 'a@cset-0-3')
 
606
 
 
607
        # Now move the directory
 
608
        self.tree1.rename_one('dir', 'sub/dir')
 
609
        self.tree1.commit('rename dir', rev_id='a@cset-0-4')
 
610
 
 
611
        bundle = self.get_valid_bundle('a@cset-0-3', 'a@cset-0-4')
 
612
        # Check a rollup bundle 
 
613
        bundle = self.get_valid_bundle('null:', 'a@cset-0-4')
 
614
 
 
615
        # Modified files
 
616
        open('b1/sub/dir/WithCaps.txt', 'ab').write('\nAdding some text\n')
 
617
        open('b1/sub/dir/ pre space', 'ab').write(
 
618
             '\r\nAdding some\r\nDOS format lines\r\n')
 
619
        open('b1/sub/dir/nolastnewline.txt', 'ab').write('\n')
 
620
        self.tree1.rename_one('sub/dir/ pre space', 
 
621
                              'sub/ start space')
 
622
        self.tree1.commit('Modified files', rev_id='a@cset-0-5')
 
623
        bundle = self.get_valid_bundle('a@cset-0-4', 'a@cset-0-5')
 
624
 
 
625
        self.tree1.rename_one('sub/dir/WithCaps.txt', 'temp')
 
626
        self.tree1.rename_one('with space.txt', 'WithCaps.txt')
 
627
        self.tree1.rename_one('temp', 'with space.txt')
 
628
        self.tree1.commit(u'swap filenames', rev_id='a@cset-0-6',
 
629
                          verbose=False)
 
630
        bundle = self.get_valid_bundle('a@cset-0-5', 'a@cset-0-6')
 
631
        other = self.get_checkout('a@cset-0-5')
 
632
        tree1_inv = self.tree1.branch.repository.get_inventory_xml(
 
633
            'a@cset-0-5')
 
634
        tree2_inv = other.branch.repository.get_inventory_xml('a@cset-0-5')
 
635
        self.assertEqualDiff(tree1_inv, tree2_inv)
 
636
        other.rename_one('sub/dir/nolastnewline.txt', 'sub/nolastnewline.txt')
 
637
        other.commit('rename file', rev_id='a@cset-0-6b')
 
638
        self.tree1.merge_from_branch(other.branch)
 
639
        self.tree1.commit(u'Merge', rev_id='a@cset-0-7',
 
640
                          verbose=False)
 
641
        bundle = self.get_valid_bundle('a@cset-0-6', 'a@cset-0-7')
 
642
 
 
643
    def test_symlink_bundle(self):
 
644
        if not has_symlinks():
 
645
            raise TestSkipped("No symlink support")
 
646
        self.tree1 = self.make_branch_and_tree('b1')
 
647
        self.b1 = self.tree1.branch
 
648
        tt = TreeTransform(self.tree1)
 
649
        tt.new_symlink('link', tt.root, 'bar/foo', 'link-1')
 
650
        tt.apply()
 
651
        self.tree1.commit('add symlink', rev_id='l@cset-0-1')
 
652
        self.get_valid_bundle('null:', 'l@cset-0-1')
 
653
        tt = TreeTransform(self.tree1)
 
654
        trans_id = tt.trans_id_tree_file_id('link-1')
 
655
        tt.adjust_path('link2', tt.root, trans_id)
 
656
        tt.delete_contents(trans_id)
 
657
        tt.create_symlink('mars', trans_id)
 
658
        tt.apply()
 
659
        self.tree1.commit('rename and change symlink', rev_id='l@cset-0-2')
 
660
        self.get_valid_bundle('l@cset-0-1', 'l@cset-0-2')
 
661
        tt = TreeTransform(self.tree1)
 
662
        trans_id = tt.trans_id_tree_file_id('link-1')
 
663
        tt.delete_contents(trans_id)
 
664
        tt.create_symlink('jupiter', trans_id)
 
665
        tt.apply()
 
666
        self.tree1.commit('just change symlink target', rev_id='l@cset-0-3')
 
667
        self.get_valid_bundle('l@cset-0-2', 'l@cset-0-3')
 
668
        tt = TreeTransform(self.tree1)
 
669
        trans_id = tt.trans_id_tree_file_id('link-1')
 
670
        tt.delete_contents(trans_id)
 
671
        tt.apply()
 
672
        self.tree1.commit('Delete symlink', rev_id='l@cset-0-4')
 
673
        self.get_valid_bundle('l@cset-0-3', 'l@cset-0-4')
 
674
 
 
675
    def test_binary_bundle(self):
 
676
        self.tree1 = self.make_branch_and_tree('b1')
 
677
        self.b1 = self.tree1.branch
 
678
        tt = TreeTransform(self.tree1)
 
679
        
 
680
        # Add
 
681
        tt.new_file('file', tt.root, '\x00\n\x00\r\x01\n\x02\r\xff', 'binary-1')
 
682
        tt.new_file('file2', tt.root, '\x01\n\x02\r\x03\n\x04\r\xff',
 
683
            'binary-2')
 
684
        tt.apply()
 
685
        self.tree1.commit('add binary', rev_id='b@cset-0-1')
 
686
        self.get_valid_bundle('null:', 'b@cset-0-1')
 
687
 
 
688
        # Delete
 
689
        tt = TreeTransform(self.tree1)
 
690
        trans_id = tt.trans_id_tree_file_id('binary-1')
 
691
        tt.delete_contents(trans_id)
 
692
        tt.apply()
 
693
        self.tree1.commit('delete binary', rev_id='b@cset-0-2')
 
694
        self.get_valid_bundle('b@cset-0-1', 'b@cset-0-2')
 
695
 
 
696
        # Rename & modify
 
697
        tt = TreeTransform(self.tree1)
 
698
        trans_id = tt.trans_id_tree_file_id('binary-2')
 
699
        tt.adjust_path('file3', tt.root, trans_id)
 
700
        tt.delete_contents(trans_id)
 
701
        tt.create_file('file\rcontents\x00\n\x00', trans_id)
 
702
        tt.apply()
 
703
        self.tree1.commit('rename and modify binary', rev_id='b@cset-0-3')
 
704
        self.get_valid_bundle('b@cset-0-2', 'b@cset-0-3')
 
705
 
 
706
        # Modify
 
707
        tt = TreeTransform(self.tree1)
 
708
        trans_id = tt.trans_id_tree_file_id('binary-2')
 
709
        tt.delete_contents(trans_id)
 
710
        tt.create_file('\x00file\rcontents', trans_id)
 
711
        tt.apply()
 
712
        self.tree1.commit('just modify binary', rev_id='b@cset-0-4')
 
713
        self.get_valid_bundle('b@cset-0-3', 'b@cset-0-4')
 
714
 
 
715
        # Rollup
 
716
        self.get_valid_bundle('null:', 'b@cset-0-4')
 
717
 
 
718
    def test_last_modified(self):
 
719
        self.tree1 = self.make_branch_and_tree('b1')
 
720
        self.b1 = self.tree1.branch
 
721
        tt = TreeTransform(self.tree1)
 
722
        tt.new_file('file', tt.root, 'file', 'file')
 
723
        tt.apply()
 
724
        self.tree1.commit('create file', rev_id='a@lmod-0-1')
 
725
 
 
726
        tt = TreeTransform(self.tree1)
 
727
        trans_id = tt.trans_id_tree_file_id('file')
 
728
        tt.delete_contents(trans_id)
 
729
        tt.create_file('file2', trans_id)
 
730
        tt.apply()
 
731
        self.tree1.commit('modify text', rev_id='a@lmod-0-2a')
 
732
 
 
733
        other = self.get_checkout('a@lmod-0-1')
 
734
        tt = TreeTransform(other)
 
735
        trans_id = tt.trans_id_tree_file_id('file')
 
736
        tt.delete_contents(trans_id)
 
737
        tt.create_file('file2', trans_id)
 
738
        tt.apply()
 
739
        other.commit('modify text in another tree', rev_id='a@lmod-0-2b')
 
740
        self.tree1.merge_from_branch(other.branch)
 
741
        self.tree1.commit(u'Merge', rev_id='a@lmod-0-3',
 
742
                          verbose=False)
 
743
        self.tree1.commit(u'Merge', rev_id='a@lmod-0-4')
 
744
        bundle = self.get_valid_bundle('a@lmod-0-2a', 'a@lmod-0-4')
 
745
 
 
746
    def test_hide_history(self):
 
747
        self.tree1 = self.make_branch_and_tree('b1')
 
748
        self.b1 = self.tree1.branch
 
749
 
 
750
        open('b1/one', 'wb').write('one\n')
 
751
        self.tree1.add('one')
 
752
        self.tree1.commit('add file', rev_id='a@cset-0-1')
 
753
        open('b1/one', 'wb').write('two\n')
 
754
        self.tree1.commit('modify', rev_id='a@cset-0-2')
 
755
        open('b1/one', 'wb').write('three\n')
 
756
        self.tree1.commit('modify', rev_id='a@cset-0-3')
 
757
        bundle_file = StringIO()
 
758
        rev_ids = write_bundle(self.tree1.branch.repository, 'a@cset-0-3',
 
759
                               'a@cset-0-1', bundle_file, format=self.format)
 
760
        self.assertNotContainsRe(bundle_file.getvalue(), '\btwo\b')
 
761
        self.assertContainsRe(self.get_raw(bundle_file), 'one')
 
762
        self.assertContainsRe(self.get_raw(bundle_file), 'three')
 
763
 
 
764
    def test_bundle_same_basis(self):
 
765
        """Ensure using the basis as the target doesn't cause an error"""
 
766
        self.tree1 = self.make_branch_and_tree('b1')
 
767
        self.tree1.commit('add file', rev_id='a@cset-0-1')
 
768
        bundle_file = StringIO()
 
769
        rev_ids = write_bundle(self.tree1.branch.repository, 'a@cset-0-1',
 
770
                               'a@cset-0-1', bundle_file)
 
771
 
 
772
    @staticmethod
 
773
    def get_raw(bundle_file):
 
774
        return bundle_file.getvalue()
 
775
 
 
776
    def test_unicode_bundle(self):
 
777
        # Handle international characters
 
778
        os.mkdir('b1')
 
779
        try:
 
780
            f = open(u'b1/with Dod\xe9', 'wb')
 
781
        except UnicodeEncodeError:
 
782
            raise TestSkipped("Filesystem doesn't support unicode")
 
783
 
 
784
        self.tree1 = self.make_branch_and_tree('b1')
 
785
        self.b1 = self.tree1.branch
 
786
 
 
787
        f.write((u'A file\n'
 
788
            u'With international man of mystery\n'
 
789
            u'William Dod\xe9\n').encode('utf-8'))
 
790
        f.close()
 
791
 
 
792
        self.tree1.add([u'with Dod\xe9'], ['withdod-id'])
 
793
        self.tree1.commit(u'i18n commit from William Dod\xe9',
 
794
                          rev_id='i18n-1', committer=u'William Dod\xe9')
 
795
 
 
796
        if sys.platform == 'darwin':
 
797
            from bzrlib.workingtree import WorkingTree3
 
798
            if type(self.tree1) is WorkingTree3:
 
799
                self.knownFailure("Bug #141438: fails for WorkingTree3 on OSX")
 
800
 
 
801
            # On Mac the '\xe9' gets changed to 'e\u0301'
 
802
            self.assertEqual([u'.bzr', u'with Dode\u0301'],
 
803
                             sorted(os.listdir(u'b1')))
 
804
            delta = self.tree1.changes_from(self.tree1.basis_tree())
 
805
            self.assertEqual([(u'with Dod\xe9', 'withdod-id', 'file')],
 
806
                             delta.removed)
 
807
            self.knownFailure("Mac OSX doesn't preserve unicode"
 
808
                              " combining characters.")
 
809
 
 
810
        # Add
 
811
        bundle = self.get_valid_bundle('null:', 'i18n-1')
 
812
 
 
813
        # Modified
 
814
        f = open(u'b1/with Dod\xe9', 'wb')
 
815
        f.write(u'Modified \xb5\n'.encode('utf8'))
 
816
        f.close()
 
817
        self.tree1.commit(u'modified', rev_id='i18n-2')
 
818
 
 
819
        bundle = self.get_valid_bundle('i18n-1', 'i18n-2')
 
820
        
 
821
        # Renamed
 
822
        self.tree1.rename_one(u'with Dod\xe9', u'B\xe5gfors')
 
823
        self.tree1.commit(u'renamed, the new i18n man', rev_id='i18n-3',
 
824
                          committer=u'Erik B\xe5gfors')
 
825
 
 
826
        bundle = self.get_valid_bundle('i18n-2', 'i18n-3')
 
827
 
 
828
        # Removed
 
829
        self.tree1.remove([u'B\xe5gfors'])
 
830
        self.tree1.commit(u'removed', rev_id='i18n-4')
 
831
 
 
832
        bundle = self.get_valid_bundle('i18n-3', 'i18n-4')
 
833
 
 
834
        # Rollup
 
835
        bundle = self.get_valid_bundle('null:', 'i18n-4')
 
836
 
 
837
 
 
838
    def test_whitespace_bundle(self):
 
839
        if sys.platform in ('win32', 'cygwin'):
 
840
            raise TestSkipped('Windows doesn\'t support filenames'
 
841
                              ' with tabs or trailing spaces')
 
842
        self.tree1 = self.make_branch_and_tree('b1')
 
843
        self.b1 = self.tree1.branch
 
844
 
 
845
        self.build_tree(['b1/trailing space '])
 
846
        self.tree1.add(['trailing space '])
 
847
        # TODO: jam 20060701 Check for handling files with '\t' characters
 
848
        #       once we actually support them
 
849
 
 
850
        # Added
 
851
        self.tree1.commit('funky whitespace', rev_id='white-1')
 
852
 
 
853
        bundle = self.get_valid_bundle('null:', 'white-1')
 
854
 
 
855
        # Modified
 
856
        open('b1/trailing space ', 'ab').write('add some text\n')
 
857
        self.tree1.commit('add text', rev_id='white-2')
 
858
 
 
859
        bundle = self.get_valid_bundle('white-1', 'white-2')
 
860
 
 
861
        # Renamed
 
862
        self.tree1.rename_one('trailing space ', ' start and end space ')
 
863
        self.tree1.commit('rename', rev_id='white-3')
 
864
 
 
865
        bundle = self.get_valid_bundle('white-2', 'white-3')
 
866
 
 
867
        # Removed
 
868
        self.tree1.remove([' start and end space '])
 
869
        self.tree1.commit('removed', rev_id='white-4')
 
870
 
 
871
        bundle = self.get_valid_bundle('white-3', 'white-4')
 
872
        
 
873
        # Now test a complet roll-up
 
874
        bundle = self.get_valid_bundle('null:', 'white-4')
 
875
 
 
876
    def test_alt_timezone_bundle(self):
 
877
        self.tree1 = self.make_branch_and_memory_tree('b1')
 
878
        self.b1 = self.tree1.branch
 
879
        builder = treebuilder.TreeBuilder()
 
880
 
 
881
        self.tree1.lock_write()
 
882
        builder.start_tree(self.tree1)
 
883
        builder.build(['newfile'])
 
884
        builder.finish_tree()
 
885
 
 
886
        # Asia/Colombo offset = 5 hours 30 minutes
 
887
        self.tree1.commit('non-hour offset timezone', rev_id='tz-1',
 
888
                          timezone=19800, timestamp=1152544886.0)
 
889
 
 
890
        bundle = self.get_valid_bundle('null:', 'tz-1')
 
891
        
 
892
        rev = bundle.revisions[0]
 
893
        self.assertEqual('Mon 2006-07-10 20:51:26.000000000 +0530', rev.date)
 
894
        self.assertEqual(19800, rev.timezone)
 
895
        self.assertEqual(1152544886.0, rev.timestamp)
 
896
        self.tree1.unlock()
 
897
 
 
898
    def test_bundle_root_id(self):
 
899
        self.tree1 = self.make_branch_and_tree('b1')
 
900
        self.b1 = self.tree1.branch
 
901
        self.tree1.commit('message', rev_id='revid1')
 
902
        bundle = self.get_valid_bundle('null:', 'revid1')
 
903
        tree = self.get_bundle_tree(bundle, 'revid1')
 
904
        self.assertEqual('revid1', tree.inventory.root.revision)
 
905
 
 
906
    def test_install_revisions(self):
 
907
        self.tree1 = self.make_branch_and_tree('b1')
 
908
        self.b1 = self.tree1.branch
 
909
        self.tree1.commit('message', rev_id='rev2a')
 
910
        bundle = self.get_valid_bundle('null:', 'rev2a')
 
911
        branch2 = self.make_branch('b2')
 
912
        self.assertFalse(branch2.repository.has_revision('rev2a'))
 
913
        target_revision = bundle.install_revisions(branch2.repository)
 
914
        self.assertTrue(branch2.repository.has_revision('rev2a'))
 
915
        self.assertEqual('rev2a', target_revision)
 
916
 
 
917
    def test_bundle_empty_property(self):
 
918
        """Test serializing revision properties with an empty value."""
 
919
        tree = self.make_branch_and_memory_tree('tree')
 
920
        tree.lock_write()
 
921
        self.addCleanup(tree.unlock)
 
922
        tree.add([''], ['TREE_ROOT'])
 
923
        tree.commit('One', revprops={'one':'two', 'empty':''}, rev_id='rev1')
 
924
        self.b1 = tree.branch
 
925
        bundle_sio, revision_ids = self.create_bundle_text('null:', 'rev1')
 
926
        bundle = read_bundle(bundle_sio)
 
927
        revision_info = bundle.revisions[0]
 
928
        self.assertEqual('rev1', revision_info.revision_id)
 
929
        rev = revision_info.as_revision()
 
930
        self.assertEqual({'branch-nick':'tree', 'empty':'', 'one':'two'},
 
931
                         rev.properties)
 
932
 
 
933
    def test_bundle_sorted_properties(self):
 
934
        """For stability the writer should write properties in sorted order."""
 
935
        tree = self.make_branch_and_memory_tree('tree')
 
936
        tree.lock_write()
 
937
        self.addCleanup(tree.unlock)
 
938
 
 
939
        tree.add([''], ['TREE_ROOT'])
 
940
        tree.commit('One', rev_id='rev1',
 
941
                    revprops={'a':'4', 'b':'3', 'c':'2', 'd':'1'})
 
942
        self.b1 = tree.branch
 
943
        bundle_sio, revision_ids = self.create_bundle_text('null:', 'rev1')
 
944
        bundle = read_bundle(bundle_sio)
 
945
        revision_info = bundle.revisions[0]
 
946
        self.assertEqual('rev1', revision_info.revision_id)
 
947
        rev = revision_info.as_revision()
 
948
        self.assertEqual({'branch-nick':'tree', 'a':'4', 'b':'3', 'c':'2',
 
949
                          'd':'1'}, rev.properties)
 
950
 
 
951
    def test_bundle_unicode_properties(self):
 
952
        """We should be able to round trip a non-ascii property."""
 
953
        tree = self.make_branch_and_memory_tree('tree')
 
954
        tree.lock_write()
 
955
        self.addCleanup(tree.unlock)
 
956
 
 
957
        tree.add([''], ['TREE_ROOT'])
 
958
        # Revisions themselves do not require anything about revision property
 
959
        # keys, other than that they are a basestring, and do not contain
 
960
        # whitespace.
 
961
        # However, Testaments assert than they are str(), and thus should not
 
962
        # be Unicode.
 
963
        tree.commit('One', rev_id='rev1',
 
964
                    revprops={'omega':u'\u03a9', 'alpha':u'\u03b1'})
 
965
        self.b1 = tree.branch
 
966
        bundle_sio, revision_ids = self.create_bundle_text('null:', 'rev1')
 
967
        bundle = read_bundle(bundle_sio)
 
968
        revision_info = bundle.revisions[0]
 
969
        self.assertEqual('rev1', revision_info.revision_id)
 
970
        rev = revision_info.as_revision()
 
971
        self.assertEqual({'branch-nick':'tree', 'omega':u'\u03a9',
 
972
                          'alpha':u'\u03b1'}, rev.properties)
 
973
 
 
974
    def test_bundle_with_ghosts(self):
 
975
        tree = self.make_branch_and_tree('tree')
 
976
        self.b1 = tree.branch
 
977
        self.build_tree_contents([('tree/file', 'content1')])
 
978
        tree.add(['file'])
 
979
        tree.commit('rev1')
 
980
        self.build_tree_contents([('tree/file', 'content2')])
 
981
        tree.add_parent_tree_id('ghost')
 
982
        tree.commit('rev2', rev_id='rev2')
 
983
        bundle = self.get_valid_bundle('null:', 'rev2')
 
984
 
 
985
    def make_simple_tree(self, format=None):
 
986
        tree = self.make_branch_and_tree('b1', format=format)
 
987
        self.b1 = tree.branch
 
988
        self.build_tree(['b1/file'])
 
989
        tree.add('file')
 
990
        return tree
 
991
 
 
992
    def test_across_serializers(self):
 
993
        tree = self.make_simple_tree('knit')
 
994
        tree.commit('hello', rev_id='rev1')
 
995
        tree.commit('hello', rev_id='rev2')
 
996
        bundle = read_bundle(self.create_bundle_text('null:', 'rev2')[0])
 
997
        repo = self.make_repository('repo', format='dirstate-with-subtree')
 
998
        bundle.install_revisions(repo)
 
999
        inv_text = repo.get_inventory_xml('rev2')
 
1000
        self.assertNotContainsRe(inv_text, 'format="5"')
 
1001
        self.assertContainsRe(inv_text, 'format="7"')
 
1002
 
 
1003
    def test_across_models(self):
 
1004
        tree = self.make_simple_tree('knit')
 
1005
        tree.commit('hello', rev_id='rev1')
 
1006
        tree.commit('hello', rev_id='rev2')
 
1007
        bundle = read_bundle(self.create_bundle_text('null:', 'rev2')[0])
 
1008
        repo = self.make_repository('repo', format='dirstate-with-subtree')
 
1009
        bundle.install_revisions(repo)
 
1010
        inv = repo.get_inventory('rev2')
 
1011
        self.assertEqual('rev2', inv.root.revision)
 
1012
        root_vf = repo.weave_store.get_weave(inv.root.file_id,
 
1013
                                             repo.get_transaction())
 
1014
        self.assertEqual(root_vf.versions(), ['rev1', 'rev2'])
 
1015
 
 
1016
    def test_across_models_incompatible(self):
 
1017
        tree = self.make_simple_tree('dirstate-with-subtree')
 
1018
        tree.commit('hello', rev_id='rev1')
 
1019
        tree.commit('hello', rev_id='rev2')
 
1020
        try:
 
1021
            bundle = read_bundle(self.create_bundle_text('null:', 'rev1')[0])
 
1022
        except errors.IncompatibleBundleFormat:
 
1023
            raise TestSkipped("Format 0.8 doesn't work with knit3")
 
1024
        repo = self.make_repository('repo', format='knit')
 
1025
        bundle.install_revisions(repo)
 
1026
 
 
1027
        bundle = read_bundle(self.create_bundle_text('null:', 'rev2')[0])
 
1028
        self.assertRaises(errors.IncompatibleRevision,
 
1029
                          bundle.install_revisions, repo)
 
1030
 
 
1031
    def test_get_merge_request(self):
 
1032
        tree = self.make_simple_tree()
 
1033
        tree.commit('hello', rev_id='rev1')
 
1034
        tree.commit('hello', rev_id='rev2')
 
1035
        bundle = read_bundle(self.create_bundle_text('null:', 'rev1')[0])
 
1036
        result = bundle.get_merge_request(tree.branch.repository)
 
1037
        self.assertEqual((None, 'rev1', 'inapplicable'), result)
 
1038
 
 
1039
    def test_with_subtree(self):
 
1040
        tree = self.make_branch_and_tree('tree',
 
1041
                                         format='dirstate-with-subtree')
 
1042
        self.b1 = tree.branch
 
1043
        subtree = self.make_branch_and_tree('tree/subtree',
 
1044
                                            format='dirstate-with-subtree')
 
1045
        tree.add('subtree')
 
1046
        tree.commit('hello', rev_id='rev1')
 
1047
        try:
 
1048
            bundle = read_bundle(self.create_bundle_text('null:', 'rev1')[0])
 
1049
        except errors.IncompatibleBundleFormat:
 
1050
            raise TestSkipped("Format 0.8 doesn't work with knit3")
 
1051
        if isinstance(bundle, v09.BundleInfo09):
 
1052
            raise TestSkipped("Format 0.9 doesn't work with subtrees")
 
1053
        repo = self.make_repository('repo', format='knit')
 
1054
        self.assertRaises(errors.IncompatibleRevision,
 
1055
                          bundle.install_revisions, repo)
 
1056
        repo2 = self.make_repository('repo2', format='dirstate-with-subtree')
 
1057
        bundle.install_revisions(repo2)
 
1058
 
 
1059
    def test_revision_id_with_slash(self):
 
1060
        self.tree1 = self.make_branch_and_tree('tree')
 
1061
        self.b1 = self.tree1.branch
 
1062
        try:
 
1063
            self.tree1.commit('Revision/id/with/slashes', rev_id='rev/id')
 
1064
        except ValueError:
 
1065
            raise TestSkipped("Repository doesn't support revision ids with"
 
1066
                              " slashes")
 
1067
        bundle = self.get_valid_bundle('null:', 'rev/id')
 
1068
 
 
1069
    def test_skip_file(self):
 
1070
        """Make sure we don't accidentally write to the wrong versionedfile"""
 
1071
        self.tree1 = self.make_branch_and_tree('tree')
 
1072
        self.b1 = self.tree1.branch
 
1073
        # rev1 is not present in bundle, done by fetch
 
1074
        self.build_tree_contents([('tree/file2', 'contents1')])
 
1075
        self.tree1.add('file2', 'file2-id')
 
1076
        self.tree1.commit('rev1', rev_id='reva')
 
1077
        self.build_tree_contents([('tree/file3', 'contents2')])
 
1078
        # rev2 is present in bundle, and done by fetch
 
1079
        # having file1 in the bunle causes file1's versionedfile to be opened.
 
1080
        self.tree1.add('file3', 'file3-id')
 
1081
        self.tree1.commit('rev2')
 
1082
        # Updating file2 should not cause an attempt to add to file1's vf
 
1083
        target = self.tree1.bzrdir.sprout('target').open_workingtree()
 
1084
        self.build_tree_contents([('tree/file2', 'contents3')])
 
1085
        self.tree1.commit('rev3', rev_id='rev3')
 
1086
        bundle = self.get_valid_bundle('reva', 'rev3')
 
1087
        if getattr(bundle, 'get_bundle_reader', None) is None:
 
1088
            raise TestSkipped('Bundle format cannot provide reader')
 
1089
        # be sure that file1 comes before file2
 
1090
        for b, m, k, r, f in bundle.get_bundle_reader().iter_records():
 
1091
            if f == 'file3-id':
 
1092
                break
 
1093
            self.assertNotEqual(f, 'file2-id')
 
1094
        bundle.install_revisions(target.branch.repository)
 
1095
 
 
1096
 
 
1097
class V08BundleTester(BundleTester, TestCaseWithTransport):
 
1098
 
 
1099
    format = '0.8'
 
1100
 
 
1101
    def test_bundle_empty_property(self):
 
1102
        """Test serializing revision properties with an empty value."""
 
1103
        tree = self.make_branch_and_memory_tree('tree')
 
1104
        tree.lock_write()
 
1105
        self.addCleanup(tree.unlock)
 
1106
        tree.add([''], ['TREE_ROOT'])
 
1107
        tree.commit('One', revprops={'one':'two', 'empty':''}, rev_id='rev1')
 
1108
        self.b1 = tree.branch
 
1109
        bundle_sio, revision_ids = self.create_bundle_text('null:', 'rev1')
 
1110
        self.assertContainsRe(bundle_sio.getvalue(),
 
1111
                              '# properties:\n'
 
1112
                              '#   branch-nick: tree\n'
 
1113
                              '#   empty: \n'
 
1114
                              '#   one: two\n'
 
1115
                             )
 
1116
        bundle = read_bundle(bundle_sio)
 
1117
        revision_info = bundle.revisions[0]
 
1118
        self.assertEqual('rev1', revision_info.revision_id)
 
1119
        rev = revision_info.as_revision()
 
1120
        self.assertEqual({'branch-nick':'tree', 'empty':'', 'one':'two'},
 
1121
                         rev.properties)
 
1122
 
 
1123
    def get_bundle_tree(self, bundle, revision_id):
 
1124
        repository = self.make_repository('repo')
 
1125
        return bundle.revision_tree(repository, 'revid1')
 
1126
 
 
1127
    def test_bundle_empty_property_alt(self):
 
1128
        """Test serializing revision properties with an empty value.
 
1129
 
 
1130
        Older readers had a bug when reading an empty property.
 
1131
        They assumed that all keys ended in ': \n'. However they would write an
 
1132
        empty value as ':\n'. This tests make sure that all newer bzr versions
 
1133
        can handle th second form.
 
1134
        """
 
1135
        tree = self.make_branch_and_memory_tree('tree')
 
1136
        tree.lock_write()
 
1137
        self.addCleanup(tree.unlock)
 
1138
        tree.add([''], ['TREE_ROOT'])
 
1139
        tree.commit('One', revprops={'one':'two', 'empty':''}, rev_id='rev1')
 
1140
        self.b1 = tree.branch
 
1141
        bundle_sio, revision_ids = self.create_bundle_text('null:', 'rev1')
 
1142
        txt = bundle_sio.getvalue()
 
1143
        loc = txt.find('#   empty: ') + len('#   empty:')
 
1144
        # Create a new bundle, which strips the trailing space after empty
 
1145
        bundle_sio = StringIO(txt[:loc] + txt[loc+1:])
 
1146
 
 
1147
        self.assertContainsRe(bundle_sio.getvalue(),
 
1148
                              '# properties:\n'
 
1149
                              '#   branch-nick: tree\n'
 
1150
                              '#   empty:\n'
 
1151
                              '#   one: two\n'
 
1152
                             )
 
1153
        bundle = read_bundle(bundle_sio)
 
1154
        revision_info = bundle.revisions[0]
 
1155
        self.assertEqual('rev1', revision_info.revision_id)
 
1156
        rev = revision_info.as_revision()
 
1157
        self.assertEqual({'branch-nick':'tree', 'empty':'', 'one':'two'},
 
1158
                         rev.properties)
 
1159
 
 
1160
    def test_bundle_sorted_properties(self):
 
1161
        """For stability the writer should write properties in sorted order."""
 
1162
        tree = self.make_branch_and_memory_tree('tree')
 
1163
        tree.lock_write()
 
1164
        self.addCleanup(tree.unlock)
 
1165
 
 
1166
        tree.add([''], ['TREE_ROOT'])
 
1167
        tree.commit('One', rev_id='rev1',
 
1168
                    revprops={'a':'4', 'b':'3', 'c':'2', 'd':'1'})
 
1169
        self.b1 = tree.branch
 
1170
        bundle_sio, revision_ids = self.create_bundle_text('null:', 'rev1')
 
1171
        self.assertContainsRe(bundle_sio.getvalue(),
 
1172
                              '# properties:\n'
 
1173
                              '#   a: 4\n'
 
1174
                              '#   b: 3\n'
 
1175
                              '#   branch-nick: tree\n'
 
1176
                              '#   c: 2\n'
 
1177
                              '#   d: 1\n'
 
1178
                             )
 
1179
        bundle = read_bundle(bundle_sio)
 
1180
        revision_info = bundle.revisions[0]
 
1181
        self.assertEqual('rev1', revision_info.revision_id)
 
1182
        rev = revision_info.as_revision()
 
1183
        self.assertEqual({'branch-nick':'tree', 'a':'4', 'b':'3', 'c':'2',
 
1184
                          'd':'1'}, rev.properties)
 
1185
 
 
1186
    def test_bundle_unicode_properties(self):
 
1187
        """We should be able to round trip a non-ascii property."""
 
1188
        tree = self.make_branch_and_memory_tree('tree')
 
1189
        tree.lock_write()
 
1190
        self.addCleanup(tree.unlock)
 
1191
 
 
1192
        tree.add([''], ['TREE_ROOT'])
 
1193
        # Revisions themselves do not require anything about revision property
 
1194
        # keys, other than that they are a basestring, and do not contain
 
1195
        # whitespace.
 
1196
        # However, Testaments assert than they are str(), and thus should not
 
1197
        # be Unicode.
 
1198
        tree.commit('One', rev_id='rev1',
 
1199
                    revprops={'omega':u'\u03a9', 'alpha':u'\u03b1'})
 
1200
        self.b1 = tree.branch
 
1201
        bundle_sio, revision_ids = self.create_bundle_text('null:', 'rev1')
 
1202
        self.assertContainsRe(bundle_sio.getvalue(),
 
1203
                              '# properties:\n'
 
1204
                              '#   alpha: \xce\xb1\n'
 
1205
                              '#   branch-nick: tree\n'
 
1206
                              '#   omega: \xce\xa9\n'
 
1207
                             )
 
1208
        bundle = read_bundle(bundle_sio)
 
1209
        revision_info = bundle.revisions[0]
 
1210
        self.assertEqual('rev1', revision_info.revision_id)
 
1211
        rev = revision_info.as_revision()
 
1212
        self.assertEqual({'branch-nick':'tree', 'omega':u'\u03a9',
 
1213
                          'alpha':u'\u03b1'}, rev.properties)
 
1214
 
 
1215
 
 
1216
class V09BundleKnit2Tester(V08BundleTester):
 
1217
 
 
1218
    format = '0.9'
 
1219
 
 
1220
    def bzrdir_format(self):
 
1221
        format = bzrdir.BzrDirMetaFormat1()
 
1222
        format.repository_format = knitrepo.RepositoryFormatKnit3()
 
1223
        return format
 
1224
 
 
1225
 
 
1226
class V09BundleKnit1Tester(V08BundleTester):
 
1227
 
 
1228
    format = '0.9'
 
1229
 
 
1230
    def bzrdir_format(self):
 
1231
        format = bzrdir.BzrDirMetaFormat1()
 
1232
        format.repository_format = knitrepo.RepositoryFormatKnit1()
 
1233
        return format
 
1234
 
 
1235
 
 
1236
class V4BundleTester(BundleTester, TestCaseWithTransport):
 
1237
 
 
1238
    format = '4'
 
1239
 
 
1240
    def get_valid_bundle(self, base_rev_id, rev_id, checkout_dir=None):
 
1241
        """Create a bundle from base_rev_id -> rev_id in built-in branch.
 
1242
        Make sure that the text generated is valid, and that it
 
1243
        can be applied against the base, and generate the same information.
 
1244
        
 
1245
        :return: The in-memory bundle 
 
1246
        """
 
1247
        bundle_txt, rev_ids = self.create_bundle_text(base_rev_id, rev_id)
 
1248
 
 
1249
        # This should also validate the generated bundle 
 
1250
        bundle = read_bundle(bundle_txt)
 
1251
        repository = self.b1.repository
 
1252
        for bundle_rev in bundle.real_revisions:
 
1253
            # These really should have already been checked when we read the
 
1254
            # bundle, since it computes the sha1 hash for the revision, which
 
1255
            # only will match if everything is okay, but lets be explicit about
 
1256
            # it
 
1257
            branch_rev = repository.get_revision(bundle_rev.revision_id)
 
1258
            for a in ('inventory_sha1', 'revision_id', 'parent_ids',
 
1259
                      'timestamp', 'timezone', 'message', 'committer', 
 
1260
                      'parent_ids', 'properties'):
 
1261
                self.assertEqual(getattr(branch_rev, a), 
 
1262
                                 getattr(bundle_rev, a))
 
1263
            self.assertEqual(len(branch_rev.parent_ids), 
 
1264
                             len(bundle_rev.parent_ids))
 
1265
        self.assertEqual(set(rev_ids),
 
1266
                         set([r.revision_id for r in bundle.real_revisions]))
 
1267
        self.valid_apply_bundle(base_rev_id, bundle,
 
1268
                                   checkout_dir=checkout_dir)
 
1269
 
 
1270
        return bundle
 
1271
 
 
1272
    def get_invalid_bundle(self, base_rev_id, rev_id):
 
1273
        """Create a bundle from base_rev_id -> rev_id in built-in branch.
 
1274
        Munge the text so that it's invalid.
 
1275
 
 
1276
        :return: The in-memory bundle
 
1277
        """
 
1278
        from bzrlib.bundle import serializer
 
1279
        bundle_txt, rev_ids = self.create_bundle_text(base_rev_id, rev_id)
 
1280
        new_text = self.get_raw(StringIO(''.join(bundle_txt)))
 
1281
        new_text = new_text.replace('<file file_id="exe-1"',
 
1282
                                    '<file executable="y" file_id="exe-1"')
 
1283
        new_text = new_text.replace('B222', 'B237')
 
1284
        bundle_txt = StringIO()
 
1285
        bundle_txt.write(serializer._get_bundle_header('4'))
 
1286
        bundle_txt.write('\n')
 
1287
        bundle_txt.write(new_text.encode('bz2'))
 
1288
        bundle_txt.seek(0)
 
1289
        bundle = read_bundle(bundle_txt)
 
1290
        self.valid_apply_bundle(base_rev_id, bundle)
 
1291
        return bundle
 
1292
 
 
1293
    def create_bundle_text(self, base_rev_id, rev_id):
 
1294
        bundle_txt = StringIO()
 
1295
        rev_ids = write_bundle(self.b1.repository, rev_id, base_rev_id, 
 
1296
                               bundle_txt, format=self.format)
 
1297
        bundle_txt.seek(0)
 
1298
        self.assertEqual(bundle_txt.readline(), 
 
1299
                         '# Bazaar revision bundle v%s\n' % self.format)
 
1300
        self.assertEqual(bundle_txt.readline(), '#\n')
 
1301
        rev = self.b1.repository.get_revision(rev_id)
 
1302
        bundle_txt.seek(0)
 
1303
        return bundle_txt, rev_ids
 
1304
 
 
1305
    def get_bundle_tree(self, bundle, revision_id):
 
1306
        repository = self.make_repository('repo')
 
1307
        bundle.install_revisions(repository)
 
1308
        return repository.revision_tree(revision_id)
 
1309
 
 
1310
    def test_creation(self):
 
1311
        tree = self.make_branch_and_tree('tree')
 
1312
        self.build_tree_contents([('tree/file', 'contents1\nstatic\n')])
 
1313
        tree.add('file', 'fileid-2')
 
1314
        tree.commit('added file', rev_id='rev1')
 
1315
        self.build_tree_contents([('tree/file', 'contents2\nstatic\n')])
 
1316
        tree.commit('changed file', rev_id='rev2')
 
1317
        s = StringIO()
 
1318
        serializer = BundleSerializerV4('1.0')
 
1319
        serializer.write(tree.branch.repository, ['rev1', 'rev2'], {}, s)
 
1320
        s.seek(0)
 
1321
        tree2 = self.make_branch_and_tree('target')
 
1322
        target_repo = tree2.branch.repository
 
1323
        install_bundle(target_repo, serializer.read(s))
 
1324
        vf = target_repo.weave_store.get_weave('fileid-2',
 
1325
            target_repo.get_transaction())
 
1326
        self.assertEqual('contents1\nstatic\n', vf.get_text('rev1'))
 
1327
        self.assertEqual('contents2\nstatic\n', vf.get_text('rev2'))
 
1328
        rtree = target_repo.revision_tree('rev2')
 
1329
        inventory_vf = target_repo.get_inventory_weave()
 
1330
        self.assertEqual(['rev1'], inventory_vf.get_parents('rev2'))
 
1331
        self.assertEqual('changed file',
 
1332
                         target_repo.get_revision('rev2').message)
 
1333
 
 
1334
    @staticmethod
 
1335
    def get_raw(bundle_file):
 
1336
        bundle_file.seek(0)
 
1337
        line = bundle_file.readline()
 
1338
        line = bundle_file.readline()
 
1339
        lines = bundle_file.readlines()
 
1340
        return ''.join(lines).decode('bz2')
 
1341
 
 
1342
    def test_copy_signatures(self):
 
1343
        tree_a = self.make_branch_and_tree('tree_a')
 
1344
        import bzrlib.gpg
 
1345
        import bzrlib.commit as commit
 
1346
        oldstrategy = bzrlib.gpg.GPGStrategy
 
1347
        branch = tree_a.branch
 
1348
        repo_a = branch.repository
 
1349
        tree_a.commit("base", allow_pointless=True, rev_id='A')
 
1350
        self.failIf(branch.repository.has_signature_for_revision_id('A'))
 
1351
        try:
 
1352
            from bzrlib.testament import Testament
 
1353
            # monkey patch gpg signing mechanism
 
1354
            bzrlib.gpg.GPGStrategy = bzrlib.gpg.LoopbackGPGStrategy
 
1355
            new_config = test_commit.MustSignConfig(branch)
 
1356
            commit.Commit(config=new_config).commit(message="base",
 
1357
                                                    allow_pointless=True,
 
1358
                                                    rev_id='B',
 
1359
                                                    working_tree=tree_a)
 
1360
            def sign(text):
 
1361
                return bzrlib.gpg.LoopbackGPGStrategy(None).sign(text)
 
1362
            self.assertTrue(repo_a.has_signature_for_revision_id('B'))
 
1363
        finally:
 
1364
            bzrlib.gpg.GPGStrategy = oldstrategy
 
1365
        tree_b = self.make_branch_and_tree('tree_b')
 
1366
        repo_b = tree_b.branch.repository
 
1367
        s = StringIO()
 
1368
        serializer = BundleSerializerV4('4')
 
1369
        serializer.write(tree_a.branch.repository, ['A', 'B'], {}, s)
 
1370
        s.seek(0)
 
1371
        install_bundle(repo_b, serializer.read(s))
 
1372
        self.assertTrue(repo_b.has_signature_for_revision_id('B'))
 
1373
        self.assertEqual(repo_b.get_signature_text('B'),
 
1374
                         repo_a.get_signature_text('B'))
 
1375
        s.seek(0)
 
1376
        # ensure repeat installs are harmless
 
1377
        install_bundle(repo_b, serializer.read(s))
 
1378
 
 
1379
 
 
1380
class V4WeaveBundleTester(V4BundleTester):
 
1381
 
 
1382
    def bzrdir_format(self):
 
1383
        return 'metaweave'
 
1384
 
 
1385
 
 
1386
class MungedBundleTester(object):
 
1387
 
 
1388
    def build_test_bundle(self):
 
1389
        wt = self.make_branch_and_tree('b1')
 
1390
 
 
1391
        self.build_tree(['b1/one'])
 
1392
        wt.add('one')
 
1393
        wt.commit('add one', rev_id='a@cset-0-1')
 
1394
        self.build_tree(['b1/two'])
 
1395
        wt.add('two')
 
1396
        wt.commit('add two', rev_id='a@cset-0-2',
 
1397
                  revprops={'branch-nick':'test'})
 
1398
 
 
1399
        bundle_txt = StringIO()
 
1400
        rev_ids = write_bundle(wt.branch.repository, 'a@cset-0-2',
 
1401
                               'a@cset-0-1', bundle_txt, self.format)
 
1402
        self.assertEqual(set(['a@cset-0-2']), set(rev_ids))
 
1403
        bundle_txt.seek(0, 0)
 
1404
        return bundle_txt
 
1405
 
 
1406
    def check_valid(self, bundle):
 
1407
        """Check that after whatever munging, the final object is valid."""
 
1408
        self.assertEqual(['a@cset-0-2'],
 
1409
            [r.revision_id for r in bundle.real_revisions])
 
1410
 
 
1411
    def test_extra_whitespace(self):
 
1412
        bundle_txt = self.build_test_bundle()
 
1413
 
 
1414
        # Seek to the end of the file
 
1415
        # Adding one extra newline used to give us
 
1416
        # TypeError: float() argument must be a string or a number
 
1417
        bundle_txt.seek(0, 2)
 
1418
        bundle_txt.write('\n')
 
1419
        bundle_txt.seek(0)
 
1420
 
 
1421
        bundle = read_bundle(bundle_txt)
 
1422
        self.check_valid(bundle)
 
1423
 
 
1424
    def test_extra_whitespace_2(self):
 
1425
        bundle_txt = self.build_test_bundle()
 
1426
 
 
1427
        # Seek to the end of the file
 
1428
        # Adding two extra newlines used to give us
 
1429
        # MalformedPatches: The first line of all patches should be ...
 
1430
        bundle_txt.seek(0, 2)
 
1431
        bundle_txt.write('\n\n')
 
1432
        bundle_txt.seek(0)
 
1433
 
 
1434
        bundle = read_bundle(bundle_txt)
 
1435
        self.check_valid(bundle)
 
1436
 
 
1437
 
 
1438
class MungedBundleTesterV09(TestCaseWithTransport, MungedBundleTester):
 
1439
 
 
1440
    format = '0.9'
 
1441
 
 
1442
    def test_missing_trailing_whitespace(self):
 
1443
        bundle_txt = self.build_test_bundle()
 
1444
 
 
1445
        # Remove a trailing newline, it shouldn't kill the parser
 
1446
        raw = bundle_txt.getvalue()
 
1447
        # The contents of the bundle don't have to be this, but this
 
1448
        # test is concerned with the exact case where the serializer
 
1449
        # creates a blank line at the end, and fails if that
 
1450
        # line is stripped
 
1451
        self.assertEqual('\n\n', raw[-2:])
 
1452
        bundle_txt = StringIO(raw[:-1])
 
1453
 
 
1454
        bundle = read_bundle(bundle_txt)
 
1455
        self.check_valid(bundle)
 
1456
 
 
1457
    def test_opening_text(self):
 
1458
        bundle_txt = self.build_test_bundle()
 
1459
 
 
1460
        bundle_txt = StringIO("Some random\nemail comments\n"
 
1461
                              + bundle_txt.getvalue())
 
1462
 
 
1463
        bundle = read_bundle(bundle_txt)
 
1464
        self.check_valid(bundle)
 
1465
 
 
1466
    def test_trailing_text(self):
 
1467
        bundle_txt = self.build_test_bundle()
 
1468
 
 
1469
        bundle_txt = StringIO(bundle_txt.getvalue() +
 
1470
                              "Some trailing\nrandom\ntext\n")
 
1471
 
 
1472
        bundle = read_bundle(bundle_txt)
 
1473
        self.check_valid(bundle)
 
1474
 
 
1475
 
 
1476
class MungedBundleTesterV4(TestCaseWithTransport, MungedBundleTester):
 
1477
 
 
1478
    format = '4'
 
1479
 
 
1480
 
 
1481
class TestBundleWriterReader(TestCase):
 
1482
 
 
1483
    def test_roundtrip_record(self):
 
1484
        fileobj = StringIO()
 
1485
        writer = v4.BundleWriter(fileobj)
 
1486
        writer.begin()
 
1487
        writer.add_info_record(foo='bar')
 
1488
        writer._add_record("Record body", {'parents': ['1', '3'],
 
1489
            'storage_kind':'fulltext'}, 'file', 'revid', 'fileid')
 
1490
        writer.end()
 
1491
        fileobj.seek(0)
 
1492
        reader = v4.BundleReader(fileobj, stream_input=True)
 
1493
        record_iter = reader.iter_records()
 
1494
        record = record_iter.next()
 
1495
        self.assertEqual((None, {'foo': 'bar', 'storage_kind': 'header'},
 
1496
            'info', None, None), record)
 
1497
        record = record_iter.next()
 
1498
        self.assertEqual(("Record body", {'storage_kind': 'fulltext',
 
1499
                          'parents': ['1', '3']}, 'file', 'revid', 'fileid'),
 
1500
                          record)
 
1501
 
 
1502
    def test_roundtrip_record_memory_hungry(self):
 
1503
        fileobj = StringIO()
 
1504
        writer = v4.BundleWriter(fileobj)
 
1505
        writer.begin()
 
1506
        writer.add_info_record(foo='bar')
 
1507
        writer._add_record("Record body", {'parents': ['1', '3'],
 
1508
            'storage_kind':'fulltext'}, 'file', 'revid', 'fileid')
 
1509
        writer.end()
 
1510
        fileobj.seek(0)
 
1511
        reader = v4.BundleReader(fileobj, stream_input=False)
 
1512
        record_iter = reader.iter_records()
 
1513
        record = record_iter.next()
 
1514
        self.assertEqual((None, {'foo': 'bar', 'storage_kind': 'header'},
 
1515
            'info', None, None), record)
 
1516
        record = record_iter.next()
 
1517
        self.assertEqual(("Record body", {'storage_kind': 'fulltext',
 
1518
                          'parents': ['1', '3']}, 'file', 'revid', 'fileid'),
 
1519
                          record)
 
1520
 
 
1521
    def test_encode_name(self):
 
1522
        self.assertEqual('revision/rev1',
 
1523
            v4.BundleWriter.encode_name('revision', 'rev1'))
 
1524
        self.assertEqual('file/rev//1/file-id-1',
 
1525
            v4.BundleWriter.encode_name('file', 'rev/1', 'file-id-1'))
 
1526
        self.assertEqual('info',
 
1527
            v4.BundleWriter.encode_name('info', None, None))
 
1528
 
 
1529
    def test_decode_name(self):
 
1530
        self.assertEqual(('revision', 'rev1', None),
 
1531
            v4.BundleReader.decode_name('revision/rev1'))
 
1532
        self.assertEqual(('file', 'rev/1', 'file-id-1'),
 
1533
            v4.BundleReader.decode_name('file/rev//1/file-id-1'))
 
1534
        self.assertEqual(('info', None, None),
 
1535
                         v4.BundleReader.decode_name('info'))
 
1536
 
 
1537
    def test_too_many_names(self):
 
1538
        fileobj = StringIO()
 
1539
        writer = v4.BundleWriter(fileobj)
 
1540
        writer.begin()
 
1541
        writer.add_info_record(foo='bar')
 
1542
        writer._container.add_bytes_record('blah', ['two', 'names'])
 
1543
        writer.end()
 
1544
        fileobj.seek(0)
 
1545
        record_iter = v4.BundleReader(fileobj).iter_records()
 
1546
        record = record_iter.next()
 
1547
        self.assertEqual((None, {'foo': 'bar', 'storage_kind': 'header'},
 
1548
            'info', None, None), record)
 
1549
        self.assertRaises(BadBundle, record_iter.next)