/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: Robert Collins
  • Date: 2007-04-26 01:48:29 UTC
  • mto: This revision was merged to the branch mainline in revision 2457.
  • Revision ID: robertc@robertcollins.net-20070426014829-znbzqzsk1gq68xqh
Fix the 'Unprintable error' message display to use the repr of the
exception that prevented printing the error because the str value for it
is often not useful in debugging (e.g.  KeyError('foo') has a str() of
'foo' but a repr of 'KeyError('foo')' which is much more useful.
(Robert Collins)

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
    treebuilder,
 
28
    )
 
29
from bzrlib.builtins import _merge_helper
 
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
 
34
from bzrlib.bundle.serializer.v08 import BundleSerializerV08
 
35
from bzrlib.bundle.serializer.v09 import BundleSerializerV09
 
36
from bzrlib.branch import Branch
 
37
from bzrlib.diff import internal_diff
 
38
from bzrlib.errors import (BzrError, TestamentMismatch, NotABundle, BadBundle, 
 
39
                           NoSuchFile,)
 
40
from bzrlib.merge import Merge3Merger
 
41
from bzrlib.repofmt import knitrepo
 
42
from bzrlib.osutils import has_symlinks, sha_file
 
43
from bzrlib.tests import (TestCaseInTempDir, TestCaseWithTransport,
 
44
                          TestCase, TestSkipped)
 
45
from bzrlib.transform import TreeTransform
 
46
from bzrlib.workingtree import WorkingTree
 
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', None, 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 V08BundleTester(TestCaseWithTransport):
 
351
 
 
352
    format = '0.8'
 
353
 
 
354
    def bzrdir_format(self):
 
355
        format = bzrdir.BzrDirMetaFormat1()
 
356
        format.repository_format = knitrepo.RepositoryFormatKnit1()
 
357
        return format
 
358
 
 
359
    def make_branch_and_tree(self, path, format=None):
 
360
        if format is None:
 
361
            format = self.bzrdir_format()
 
362
        return TestCaseWithTransport.make_branch_and_tree(self, path, format)
 
363
 
 
364
    def make_branch(self, path, format=None):
 
365
        if format is None:
 
366
            format = self.bzrdir_format()
 
367
        return TestCaseWithTransport.make_branch(self, path, format)
 
368
 
 
369
    def create_bundle_text(self, base_rev_id, rev_id):
 
370
        bundle_txt = StringIO()
 
371
        rev_ids = write_bundle(self.b1.repository, rev_id, base_rev_id, 
 
372
                               bundle_txt, format=self.format)
 
373
        bundle_txt.seek(0)
 
374
        self.assertEqual(bundle_txt.readline(), 
 
375
                         '# Bazaar revision bundle v%s\n' % self.format)
 
376
        self.assertEqual(bundle_txt.readline(), '#\n')
 
377
 
 
378
        rev = self.b1.repository.get_revision(rev_id)
 
379
        self.assertEqual(bundle_txt.readline().decode('utf-8'),
 
380
                         u'# message:\n')
 
381
 
 
382
        open(',,bundle', 'wb').write(bundle_txt.getvalue())
 
383
        bundle_txt.seek(0)
 
384
        return bundle_txt, rev_ids
 
385
 
 
386
    def get_valid_bundle(self, base_rev_id, rev_id, checkout_dir=None):
 
387
        """Create a bundle from base_rev_id -> rev_id in built-in branch.
 
388
        Make sure that the text generated is valid, and that it
 
389
        can be applied against the base, and generate the same information.
 
390
        
 
391
        :return: The in-memory bundle 
 
392
        """
 
393
        bundle_txt, rev_ids = self.create_bundle_text(base_rev_id, rev_id)
 
394
 
 
395
        # This should also validate the generated bundle 
 
396
        bundle = read_bundle(bundle_txt)
 
397
        repository = self.b1.repository
 
398
        for bundle_rev in bundle.real_revisions:
 
399
            # These really should have already been checked when we read the
 
400
            # bundle, since it computes the sha1 hash for the revision, which
 
401
            # only will match if everything is okay, but lets be explicit about
 
402
            # it
 
403
            branch_rev = repository.get_revision(bundle_rev.revision_id)
 
404
            for a in ('inventory_sha1', 'revision_id', 'parent_ids',
 
405
                      'timestamp', 'timezone', 'message', 'committer', 
 
406
                      'parent_ids', 'properties'):
 
407
                self.assertEqual(getattr(branch_rev, a), 
 
408
                                 getattr(bundle_rev, a))
 
409
            self.assertEqual(len(branch_rev.parent_ids), 
 
410
                             len(bundle_rev.parent_ids))
 
411
        self.assertEqual(rev_ids, 
 
412
                         [r.revision_id for r in bundle.real_revisions])
 
413
        self.valid_apply_bundle(base_rev_id, bundle,
 
414
                                   checkout_dir=checkout_dir)
 
415
 
 
416
        return bundle
 
417
 
 
418
    def get_invalid_bundle(self, base_rev_id, rev_id):
 
419
        """Create a bundle from base_rev_id -> rev_id in built-in branch.
 
420
        Munge the text so that it's invalid.
 
421
        
 
422
        :return: The in-memory bundle
 
423
        """
 
424
        bundle_txt, rev_ids = self.create_bundle_text(base_rev_id, rev_id)
 
425
        new_text = bundle_txt.getvalue().replace('executable:no', 
 
426
                                               'executable:yes')
 
427
        bundle_txt = StringIO(new_text)
 
428
        bundle = read_bundle(bundle_txt)
 
429
        self.valid_apply_bundle(base_rev_id, bundle)
 
430
        return bundle 
 
431
 
 
432
    def test_non_bundle(self):
 
433
        self.assertRaises(NotABundle, read_bundle, StringIO('#!/bin/sh\n'))
 
434
 
 
435
    def test_malformed(self):
 
436
        self.assertRaises(BadBundle, read_bundle, 
 
437
                          StringIO('# Bazaar revision bundle v'))
 
438
 
 
439
    def test_crlf_bundle(self):
 
440
        try:
 
441
            read_bundle(StringIO('# Bazaar revision bundle v0.8\r\n'))
 
442
        except BadBundle:
 
443
            # It is currently permitted for bundles with crlf line endings to
 
444
            # make read_bundle raise a BadBundle, but this should be fixed.
 
445
            # Anything else, especially NotABundle, is an error.
 
446
            pass
 
447
 
 
448
    def get_checkout(self, rev_id, checkout_dir=None):
 
449
        """Get a new tree, with the specified revision in it.
 
450
        """
 
451
 
 
452
        if checkout_dir is None:
 
453
            checkout_dir = tempfile.mkdtemp(prefix='test-branch-', dir='.')
 
454
        else:
 
455
            if not os.path.exists(checkout_dir):
 
456
                os.mkdir(checkout_dir)
 
457
        tree = self.make_branch_and_tree(checkout_dir)
 
458
        s = StringIO()
 
459
        ancestors = write_bundle(self.b1.repository, rev_id, None, s,
 
460
                                 format=self.format)
 
461
        s.seek(0)
 
462
        assert isinstance(s.getvalue(), str), (
 
463
            "Bundle isn't a bytestring:\n %s..." % repr(s.getvalue())[:40])
 
464
        install_bundle(tree.branch.repository, read_bundle(s))
 
465
        for ancestor in ancestors:
 
466
            old = self.b1.repository.revision_tree(ancestor)
 
467
            new = tree.branch.repository.revision_tree(ancestor)
 
468
 
 
469
            # Check that there aren't any inventory level changes
 
470
            delta = new.changes_from(old)
 
471
            self.assertFalse(delta.has_changed(),
 
472
                             'Revision %s not copied correctly.'
 
473
                             % (ancestor,))
 
474
 
 
475
            # Now check that the file contents are all correct
 
476
            for inventory_id in old:
 
477
                try:
 
478
                    old_file = old.get_file(inventory_id)
 
479
                except NoSuchFile:
 
480
                    continue
 
481
                if old_file is None:
 
482
                    continue
 
483
                self.assertEqual(old_file.read(),
 
484
                                 new.get_file(inventory_id).read())
 
485
        if rev_id is not None:
 
486
            rh = self.b1.revision_history()
 
487
            tree.branch.set_revision_history(rh[:rh.index(rev_id)+1])
 
488
            tree.update()
 
489
            delta = tree.changes_from(self.b1.repository.revision_tree(rev_id))
 
490
            self.assertFalse(delta.has_changed(),
 
491
                             'Working tree has modifications: %s' % delta)
 
492
        return tree
 
493
 
 
494
    def valid_apply_bundle(self, base_rev_id, info, checkout_dir=None):
 
495
        """Get the base revision, apply the changes, and make
 
496
        sure everything matches the builtin branch.
 
497
        """
 
498
        to_tree = self.get_checkout(base_rev_id, checkout_dir=checkout_dir)
 
499
        original_parents = to_tree.get_parent_ids()
 
500
        repository = to_tree.branch.repository
 
501
        original_parents = to_tree.get_parent_ids()
 
502
        self.assertIs(repository.has_revision(base_rev_id), True)
 
503
        for rev in info.real_revisions:
 
504
            self.assert_(not repository.has_revision(rev.revision_id),
 
505
                'Revision {%s} present before applying bundle' 
 
506
                % rev.revision_id)
 
507
        merge_bundle(info, to_tree, True, Merge3Merger, False, False)
 
508
 
 
509
        for rev in info.real_revisions:
 
510
            self.assert_(repository.has_revision(rev.revision_id),
 
511
                'Missing revision {%s} after applying bundle' 
 
512
                % rev.revision_id)
 
513
 
 
514
        self.assert_(to_tree.branch.repository.has_revision(info.target))
 
515
        # Do we also want to verify that all the texts have been added?
 
516
 
 
517
        self.assertEqual(original_parents + [info.target],
 
518
            to_tree.get_parent_ids())
 
519
 
 
520
        rev = info.real_revisions[-1]
 
521
        base_tree = self.b1.repository.revision_tree(rev.revision_id)
 
522
        to_tree = to_tree.branch.repository.revision_tree(rev.revision_id)
 
523
        
 
524
        # TODO: make sure the target tree is identical to base tree
 
525
        #       we might also check the working tree.
 
526
 
 
527
        base_files = list(base_tree.list_files())
 
528
        to_files = list(to_tree.list_files())
 
529
        self.assertEqual(len(base_files), len(to_files))
 
530
        for base_file, to_file in zip(base_files, to_files):
 
531
            self.assertEqual(base_file, to_file)
 
532
 
 
533
        for path, status, kind, fileid, entry in base_files:
 
534
            # Check that the meta information is the same
 
535
            self.assertEqual(base_tree.get_file_size(fileid),
 
536
                    to_tree.get_file_size(fileid))
 
537
            self.assertEqual(base_tree.get_file_sha1(fileid),
 
538
                    to_tree.get_file_sha1(fileid))
 
539
            # Check that the contents are the same
 
540
            # This is pretty expensive
 
541
            # self.assertEqual(base_tree.get_file(fileid).read(),
 
542
            #         to_tree.get_file(fileid).read())
 
543
 
 
544
    def test_bundle(self):
 
545
        self.tree1 = self.make_branch_and_tree('b1')
 
546
        self.b1 = self.tree1.branch
 
547
 
 
548
        open('b1/one', 'wb').write('one\n')
 
549
        self.tree1.add('one')
 
550
        self.tree1.commit('add one', rev_id='a@cset-0-1')
 
551
 
 
552
        bundle = self.get_valid_bundle(None, 'a@cset-0-1')
 
553
        # FIXME: The current write_bundle api no longer supports
 
554
        #        setting a custom summary message
 
555
        #        We should re-introduce the ability, and update
 
556
        #        the tests to make sure it works.
 
557
        # bundle = self.get_valid_bundle(None, 'a@cset-0-1',
 
558
        #         message='With a specialized message')
 
559
 
 
560
        # Make sure we can handle files with spaces, tabs, other
 
561
        # bogus characters
 
562
        self.build_tree([
 
563
                'b1/with space.txt'
 
564
                , 'b1/dir/'
 
565
                , 'b1/dir/filein subdir.c'
 
566
                , 'b1/dir/WithCaps.txt'
 
567
                , 'b1/dir/ pre space'
 
568
                , 'b1/sub/'
 
569
                , 'b1/sub/sub/'
 
570
                , 'b1/sub/sub/nonempty.txt'
 
571
                ])
 
572
        open('b1/sub/sub/emptyfile.txt', 'wb').close()
 
573
        open('b1/dir/nolastnewline.txt', 'wb').write('bloop')
 
574
        tt = TreeTransform(self.tree1)
 
575
        tt.new_file('executable', tt.root, '#!/bin/sh\n', 'exe-1', True)
 
576
        tt.apply()
 
577
        self.tree1.add([
 
578
                'with space.txt'
 
579
                , 'dir'
 
580
                , 'dir/filein subdir.c'
 
581
                , 'dir/WithCaps.txt'
 
582
                , 'dir/ pre space'
 
583
                , 'dir/nolastnewline.txt'
 
584
                , 'sub'
 
585
                , 'sub/sub'
 
586
                , 'sub/sub/nonempty.txt'
 
587
                , 'sub/sub/emptyfile.txt'
 
588
                ])
 
589
        self.tree1.commit('add whitespace', rev_id='a@cset-0-2')
 
590
 
 
591
        bundle = self.get_valid_bundle('a@cset-0-1', 'a@cset-0-2')
 
592
 
 
593
        # Check a rollup bundle 
 
594
        bundle = self.get_valid_bundle(None, 'a@cset-0-2')
 
595
 
 
596
        # Now delete entries
 
597
        self.tree1.remove(
 
598
                ['sub/sub/nonempty.txt'
 
599
                , 'sub/sub/emptyfile.txt'
 
600
                , 'sub/sub'
 
601
                ])
 
602
        tt = TreeTransform(self.tree1)
 
603
        trans_id = tt.trans_id_tree_file_id('exe-1')
 
604
        tt.set_executability(False, trans_id)
 
605
        tt.apply()
 
606
        self.tree1.commit('removed', rev_id='a@cset-0-3')
 
607
        
 
608
        bundle = self.get_valid_bundle('a@cset-0-2', 'a@cset-0-3')
 
609
        self.assertRaises(TestamentMismatch, self.get_invalid_bundle, 
 
610
                          'a@cset-0-2', 'a@cset-0-3')
 
611
        # Check a rollup bundle 
 
612
        bundle = self.get_valid_bundle(None, 'a@cset-0-3')
 
613
 
 
614
        # Now move the directory
 
615
        self.tree1.rename_one('dir', 'sub/dir')
 
616
        self.tree1.commit('rename dir', rev_id='a@cset-0-4')
 
617
 
 
618
        bundle = self.get_valid_bundle('a@cset-0-3', 'a@cset-0-4')
 
619
        # Check a rollup bundle 
 
620
        bundle = self.get_valid_bundle(None, 'a@cset-0-4')
 
621
 
 
622
        # Modified files
 
623
        open('b1/sub/dir/WithCaps.txt', 'ab').write('\nAdding some text\n')
 
624
        open('b1/sub/dir/ pre space', 'ab').write('\r\nAdding some\r\nDOS format lines\r\n')
 
625
        open('b1/sub/dir/nolastnewline.txt', 'ab').write('\n')
 
626
        self.tree1.rename_one('sub/dir/ pre space', 
 
627
                              'sub/ start space')
 
628
        self.tree1.commit('Modified files', rev_id='a@cset-0-5')
 
629
        bundle = self.get_valid_bundle('a@cset-0-4', 'a@cset-0-5')
 
630
 
 
631
        self.tree1.rename_one('sub/dir/WithCaps.txt', 'temp')
 
632
        self.tree1.rename_one('with space.txt', 'WithCaps.txt')
 
633
        self.tree1.rename_one('temp', 'with space.txt')
 
634
        self.tree1.commit(u'swap filenames', rev_id='a@cset-0-6',
 
635
                          verbose=False)
 
636
        bundle = self.get_valid_bundle('a@cset-0-5', 'a@cset-0-6')
 
637
        other = self.get_checkout('a@cset-0-5')
 
638
        tree1_inv = self.tree1.branch.repository.get_inventory_xml(
 
639
            'a@cset-0-5')
 
640
        tree2_inv = other.branch.repository.get_inventory_xml('a@cset-0-5')
 
641
        self.assertEqualDiff(tree1_inv, tree2_inv)
 
642
        other.rename_one('sub/dir/nolastnewline.txt', 'sub/nolastnewline.txt')
 
643
        other.commit('rename file', rev_id='a@cset-0-6b')
 
644
        _merge_helper([other.basedir, -1], [None, None],
 
645
                      this_dir=self.tree1.basedir)
 
646
        self.tree1.commit(u'Merge', rev_id='a@cset-0-7',
 
647
                          verbose=False)
 
648
        bundle = self.get_valid_bundle('a@cset-0-6', 'a@cset-0-7')
 
649
 
 
650
    def test_symlink_bundle(self):
 
651
        if not has_symlinks():
 
652
            raise TestSkipped("No symlink support")
 
653
        self.tree1 = self.make_branch_and_tree('b1')
 
654
        self.b1 = self.tree1.branch
 
655
        tt = TreeTransform(self.tree1)
 
656
        tt.new_symlink('link', tt.root, 'bar/foo', 'link-1')
 
657
        tt.apply()
 
658
        self.tree1.commit('add symlink', rev_id='l@cset-0-1')
 
659
        self.get_valid_bundle(None, 'l@cset-0-1')
 
660
        tt = TreeTransform(self.tree1)
 
661
        trans_id = tt.trans_id_tree_file_id('link-1')
 
662
        tt.adjust_path('link2', tt.root, trans_id)
 
663
        tt.delete_contents(trans_id)
 
664
        tt.create_symlink('mars', trans_id)
 
665
        tt.apply()
 
666
        self.tree1.commit('rename and change symlink', rev_id='l@cset-0-2')
 
667
        self.get_valid_bundle('l@cset-0-1', 'l@cset-0-2')
 
668
        tt = TreeTransform(self.tree1)
 
669
        trans_id = tt.trans_id_tree_file_id('link-1')
 
670
        tt.delete_contents(trans_id)
 
671
        tt.create_symlink('jupiter', trans_id)
 
672
        tt.apply()
 
673
        self.tree1.commit('just change symlink target', rev_id='l@cset-0-3')
 
674
        self.get_valid_bundle('l@cset-0-2', 'l@cset-0-3')
 
675
        tt = TreeTransform(self.tree1)
 
676
        trans_id = tt.trans_id_tree_file_id('link-1')
 
677
        tt.delete_contents(trans_id)
 
678
        tt.apply()
 
679
        self.tree1.commit('Delete symlink', rev_id='l@cset-0-4')
 
680
        self.get_valid_bundle('l@cset-0-3', 'l@cset-0-4')
 
681
 
 
682
    def test_binary_bundle(self):
 
683
        self.tree1 = self.make_branch_and_tree('b1')
 
684
        self.b1 = self.tree1.branch
 
685
        tt = TreeTransform(self.tree1)
 
686
        
 
687
        # Add
 
688
        tt.new_file('file', tt.root, '\x00\n\x00\r\x01\n\x02\r\xff', 'binary-1')
 
689
        tt.new_file('file2', tt.root, '\x01\n\x02\r\x03\n\x04\r\xff', 'binary-2')
 
690
        tt.apply()
 
691
        self.tree1.commit('add binary', rev_id='b@cset-0-1')
 
692
        self.get_valid_bundle(None, 'b@cset-0-1')
 
693
 
 
694
        # Delete
 
695
        tt = TreeTransform(self.tree1)
 
696
        trans_id = tt.trans_id_tree_file_id('binary-1')
 
697
        tt.delete_contents(trans_id)
 
698
        tt.apply()
 
699
        self.tree1.commit('delete binary', rev_id='b@cset-0-2')
 
700
        self.get_valid_bundle('b@cset-0-1', 'b@cset-0-2')
 
701
 
 
702
        # Rename & modify
 
703
        tt = TreeTransform(self.tree1)
 
704
        trans_id = tt.trans_id_tree_file_id('binary-2')
 
705
        tt.adjust_path('file3', tt.root, trans_id)
 
706
        tt.delete_contents(trans_id)
 
707
        tt.create_file('file\rcontents\x00\n\x00', trans_id)
 
708
        tt.apply()
 
709
        self.tree1.commit('rename and modify binary', rev_id='b@cset-0-3')
 
710
        self.get_valid_bundle('b@cset-0-2', 'b@cset-0-3')
 
711
 
 
712
        # Modify
 
713
        tt = TreeTransform(self.tree1)
 
714
        trans_id = tt.trans_id_tree_file_id('binary-2')
 
715
        tt.delete_contents(trans_id)
 
716
        tt.create_file('\x00file\rcontents', trans_id)
 
717
        tt.apply()
 
718
        self.tree1.commit('just modify binary', rev_id='b@cset-0-4')
 
719
        self.get_valid_bundle('b@cset-0-3', 'b@cset-0-4')
 
720
 
 
721
        # Rollup
 
722
        self.get_valid_bundle(None, 'b@cset-0-4')
 
723
 
 
724
    def test_last_modified(self):
 
725
        self.tree1 = self.make_branch_and_tree('b1')
 
726
        self.b1 = self.tree1.branch
 
727
        tt = TreeTransform(self.tree1)
 
728
        tt.new_file('file', tt.root, 'file', 'file')
 
729
        tt.apply()
 
730
        self.tree1.commit('create file', rev_id='a@lmod-0-1')
 
731
 
 
732
        tt = TreeTransform(self.tree1)
 
733
        trans_id = tt.trans_id_tree_file_id('file')
 
734
        tt.delete_contents(trans_id)
 
735
        tt.create_file('file2', trans_id)
 
736
        tt.apply()
 
737
        self.tree1.commit('modify text', rev_id='a@lmod-0-2a')
 
738
 
 
739
        other = self.get_checkout('a@lmod-0-1')
 
740
        tt = TreeTransform(other)
 
741
        trans_id = tt.trans_id_tree_file_id('file')
 
742
        tt.delete_contents(trans_id)
 
743
        tt.create_file('file2', trans_id)
 
744
        tt.apply()
 
745
        other.commit('modify text in another tree', rev_id='a@lmod-0-2b')
 
746
        _merge_helper([other.basedir, -1], [None, None],
 
747
                      this_dir=self.tree1.basedir)
 
748
        self.tree1.commit(u'Merge', rev_id='a@lmod-0-3',
 
749
                          verbose=False)
 
750
        self.tree1.commit(u'Merge', rev_id='a@lmod-0-4')
 
751
        bundle = self.get_valid_bundle('a@lmod-0-2a', 'a@lmod-0-4')
 
752
 
 
753
    def test_hide_history(self):
 
754
        self.tree1 = self.make_branch_and_tree('b1')
 
755
        self.b1 = self.tree1.branch
 
756
 
 
757
        open('b1/one', 'wb').write('one\n')
 
758
        self.tree1.add('one')
 
759
        self.tree1.commit('add file', rev_id='a@cset-0-1')
 
760
        open('b1/one', 'wb').write('two\n')
 
761
        self.tree1.commit('modify', rev_id='a@cset-0-2')
 
762
        open('b1/one', 'wb').write('three\n')
 
763
        self.tree1.commit('modify', rev_id='a@cset-0-3')
 
764
        bundle_file = StringIO()
 
765
        rev_ids = write_bundle(self.tree1.branch.repository, 'a@cset-0-3',
 
766
                               'a@cset-0-1', bundle_file, format=self.format)
 
767
        self.assertNotContainsRe(bundle_file.getvalue(), '\btwo\b')
 
768
        self.assertContainsRe(bundle_file.getvalue(), 'one')
 
769
        self.assertContainsRe(bundle_file.getvalue(), 'three')
 
770
 
 
771
    def test_unicode_bundle(self):
 
772
        # Handle international characters
 
773
        os.mkdir('b1')
 
774
        try:
 
775
            f = open(u'b1/with Dod\xe9', 'wb')
 
776
        except UnicodeEncodeError:
 
777
            raise TestSkipped("Filesystem doesn't support unicode")
 
778
 
 
779
        self.tree1 = self.make_branch_and_tree('b1')
 
780
        self.b1 = self.tree1.branch
 
781
 
 
782
        f.write((u'A file\n'
 
783
            u'With international man of mystery\n'
 
784
            u'William Dod\xe9\n').encode('utf-8'))
 
785
        f.close()
 
786
 
 
787
        self.tree1.add([u'with Dod\xe9'], ['withdod-id'])
 
788
        self.tree1.commit(u'i18n commit from William Dod\xe9',
 
789
                          rev_id='i18n-1', committer=u'William Dod\xe9')
 
790
 
 
791
        if sys.platform == 'darwin':
 
792
            # On Mac the '\xe9' gets changed to 'e\u0301'
 
793
            self.assertEqual([u'.bzr', u'with Dode\u0301'],
 
794
                             sorted(os.listdir(u'b1')))
 
795
            delta = self.tree1.changes_from(self.tree1.basis_tree())
 
796
            self.assertEqual([(u'with Dod\xe9', 'withdod-id', 'file')],
 
797
                             delta.removed)
 
798
            self.knownFailure("Mac OSX doesn't preserve unicode"
 
799
                              " combining characters.")
 
800
 
 
801
        # Add
 
802
        bundle = self.get_valid_bundle(None, 'i18n-1')
 
803
 
 
804
        # Modified
 
805
        f = open(u'b1/with Dod\xe9', 'wb')
 
806
        f.write(u'Modified \xb5\n'.encode('utf8'))
 
807
        f.close()
 
808
        self.tree1.commit(u'modified', rev_id='i18n-2')
 
809
 
 
810
        bundle = self.get_valid_bundle('i18n-1', 'i18n-2')
 
811
        
 
812
        # Renamed
 
813
        self.tree1.rename_one(u'with Dod\xe9', u'B\xe5gfors')
 
814
        self.tree1.commit(u'renamed, the new i18n man', rev_id='i18n-3',
 
815
                          committer=u'Erik B\xe5gfors')
 
816
 
 
817
        bundle = self.get_valid_bundle('i18n-2', 'i18n-3')
 
818
 
 
819
        # Removed
 
820
        self.tree1.remove([u'B\xe5gfors'])
 
821
        self.tree1.commit(u'removed', rev_id='i18n-4')
 
822
 
 
823
        bundle = self.get_valid_bundle('i18n-3', 'i18n-4')
 
824
 
 
825
        # Rollup
 
826
        bundle = self.get_valid_bundle(None, 'i18n-4')
 
827
 
 
828
 
 
829
    def test_whitespace_bundle(self):
 
830
        if sys.platform in ('win32', 'cygwin'):
 
831
            raise TestSkipped('Windows doesn\'t support filenames'
 
832
                              ' with tabs or trailing spaces')
 
833
        self.tree1 = self.make_branch_and_tree('b1')
 
834
        self.b1 = self.tree1.branch
 
835
 
 
836
        self.build_tree(['b1/trailing space '])
 
837
        self.tree1.add(['trailing space '])
 
838
        # TODO: jam 20060701 Check for handling files with '\t' characters
 
839
        #       once we actually support them
 
840
 
 
841
        # Added
 
842
        self.tree1.commit('funky whitespace', rev_id='white-1')
 
843
 
 
844
        bundle = self.get_valid_bundle(None, 'white-1')
 
845
 
 
846
        # Modified
 
847
        open('b1/trailing space ', 'ab').write('add some text\n')
 
848
        self.tree1.commit('add text', rev_id='white-2')
 
849
 
 
850
        bundle = self.get_valid_bundle('white-1', 'white-2')
 
851
 
 
852
        # Renamed
 
853
        self.tree1.rename_one('trailing space ', ' start and end space ')
 
854
        self.tree1.commit('rename', rev_id='white-3')
 
855
 
 
856
        bundle = self.get_valid_bundle('white-2', 'white-3')
 
857
 
 
858
        # Removed
 
859
        self.tree1.remove([' start and end space '])
 
860
        self.tree1.commit('removed', rev_id='white-4')
 
861
 
 
862
        bundle = self.get_valid_bundle('white-3', 'white-4')
 
863
        
 
864
        # Now test a complet roll-up
 
865
        bundle = self.get_valid_bundle(None, 'white-4')
 
866
 
 
867
    def test_alt_timezone_bundle(self):
 
868
        self.tree1 = self.make_branch_and_memory_tree('b1')
 
869
        self.b1 = self.tree1.branch
 
870
        builder = treebuilder.TreeBuilder()
 
871
 
 
872
        self.tree1.lock_write()
 
873
        builder.start_tree(self.tree1)
 
874
        builder.build(['newfile'])
 
875
        builder.finish_tree()
 
876
 
 
877
        # Asia/Colombo offset = 5 hours 30 minutes
 
878
        self.tree1.commit('non-hour offset timezone', rev_id='tz-1',
 
879
                          timezone=19800, timestamp=1152544886.0)
 
880
 
 
881
        bundle = self.get_valid_bundle(None, 'tz-1')
 
882
        
 
883
        rev = bundle.revisions[0]
 
884
        self.assertEqual('Mon 2006-07-10 20:51:26.000000000 +0530', rev.date)
 
885
        self.assertEqual(19800, rev.timezone)
 
886
        self.assertEqual(1152544886.0, rev.timestamp)
 
887
        self.tree1.unlock()
 
888
 
 
889
    def test_bundle_root_id(self):
 
890
        self.tree1 = self.make_branch_and_tree('b1')
 
891
        self.b1 = self.tree1.branch
 
892
        self.tree1.commit('message', rev_id='revid1')
 
893
        bundle = self.get_valid_bundle(None, 'revid1')
 
894
        tree = bundle.revision_tree(self.b1.repository, 'revid1')
 
895
        self.assertEqual('revid1', tree.inventory.root.revision)
 
896
 
 
897
    def test_install_revisions(self):
 
898
        self.tree1 = self.make_branch_and_tree('b1')
 
899
        self.b1 = self.tree1.branch
 
900
        self.tree1.commit('message', rev_id='rev2a')
 
901
        bundle = self.get_valid_bundle(None, 'rev2a')
 
902
        branch2 = self.make_branch('b2')
 
903
        self.assertFalse(branch2.repository.has_revision('rev2a'))
 
904
        target_revision = bundle.install_revisions(branch2.repository)
 
905
        self.assertTrue(branch2.repository.has_revision('rev2a'))
 
906
        self.assertEqual('rev2a', target_revision)
 
907
 
 
908
 
 
909
class V09BundleKnit2Tester(V08BundleTester):
 
910
 
 
911
    format = '0.9'
 
912
 
 
913
    def bzrdir_format(self):
 
914
        format = bzrdir.BzrDirMetaFormat1()
 
915
        format.repository_format = knitrepo.RepositoryFormatKnit3()
 
916
        return format
 
917
 
 
918
 
 
919
class V09BundleKnit1Tester(V08BundleTester):
 
920
 
 
921
    format = '0.9'
 
922
 
 
923
    def bzrdir_format(self):
 
924
        format = bzrdir.BzrDirMetaFormat1()
 
925
        format.repository_format = knitrepo.RepositoryFormatKnit1()
 
926
        return format
 
927
 
 
928
 
 
929
class MungedBundleTester(TestCaseWithTransport):
 
930
 
 
931
    def build_test_bundle(self):
 
932
        wt = self.make_branch_and_tree('b1')
 
933
 
 
934
        self.build_tree(['b1/one'])
 
935
        wt.add('one')
 
936
        wt.commit('add one', rev_id='a@cset-0-1')
 
937
        self.build_tree(['b1/two'])
 
938
        wt.add('two')
 
939
        wt.commit('add two', rev_id='a@cset-0-2',
 
940
                  revprops={'branch-nick':'test'})
 
941
 
 
942
        bundle_txt = StringIO()
 
943
        rev_ids = write_bundle(wt.branch.repository, 'a@cset-0-2',
 
944
                               'a@cset-0-1', bundle_txt)
 
945
        self.assertEqual(['a@cset-0-2'], rev_ids)
 
946
        bundle_txt.seek(0, 0)
 
947
        return bundle_txt
 
948
 
 
949
    def check_valid(self, bundle):
 
950
        """Check that after whatever munging, the final object is valid."""
 
951
        self.assertEqual(['a@cset-0-2'],
 
952
            [r.revision_id for r in bundle.real_revisions])
 
953
 
 
954
    def test_extra_whitespace(self):
 
955
        bundle_txt = self.build_test_bundle()
 
956
 
 
957
        # Seek to the end of the file
 
958
        # Adding one extra newline used to give us
 
959
        # TypeError: float() argument must be a string or a number
 
960
        bundle_txt.seek(0, 2)
 
961
        bundle_txt.write('\n')
 
962
        bundle_txt.seek(0)
 
963
 
 
964
        bundle = read_bundle(bundle_txt)
 
965
        self.check_valid(bundle)
 
966
 
 
967
    def test_extra_whitespace_2(self):
 
968
        bundle_txt = self.build_test_bundle()
 
969
 
 
970
        # Seek to the end of the file
 
971
        # Adding two extra newlines used to give us
 
972
        # MalformedPatches: The first line of all patches should be ...
 
973
        bundle_txt.seek(0, 2)
 
974
        bundle_txt.write('\n\n')
 
975
        bundle_txt.seek(0)
 
976
 
 
977
        bundle = read_bundle(bundle_txt)
 
978
        self.check_valid(bundle)
 
979
 
 
980
    def test_missing_trailing_whitespace(self):
 
981
        bundle_txt = self.build_test_bundle()
 
982
 
 
983
        # Remove a trailing newline, it shouldn't kill the parser
 
984
        raw = bundle_txt.getvalue()
 
985
        # The contents of the bundle don't have to be this, but this
 
986
        # test is concerned with the exact case where the serializer
 
987
        # creates a blank line at the end, and fails if that
 
988
        # line is stripped
 
989
        self.assertEqual('\n\n', raw[-2:])
 
990
        bundle_txt = StringIO(raw[:-1])
 
991
 
 
992
        bundle = read_bundle(bundle_txt)
 
993
        self.check_valid(bundle)
 
994
 
 
995
    def test_opening_text(self):
 
996
        bundle_txt = self.build_test_bundle()
 
997
 
 
998
        bundle_txt = StringIO("Some random\nemail comments\n"
 
999
                              + bundle_txt.getvalue())
 
1000
 
 
1001
        bundle = read_bundle(bundle_txt)
 
1002
        self.check_valid(bundle)
 
1003
 
 
1004
    def test_trailing_text(self):
 
1005
        bundle_txt = self.build_test_bundle()
 
1006
 
 
1007
        bundle_txt = StringIO(bundle_txt.getvalue() +
 
1008
                              "Some trailing\nrandom\ntext\n")
 
1009
 
 
1010
        bundle = read_bundle(bundle_txt)
 
1011
        self.check_valid(bundle)
 
1012