~bzr-pqm/bzr/bzr.dev

« back to all changes in this revision

Viewing changes to bzrlib/tests/test_bundle.py

  • Committer: Ian Clatworthy
  • Date: 2009-01-19 02:24:15 UTC
  • mto: This revision was merged to the branch mainline in revision 3944.
  • Revision ID: ian.clatworthy@canonical.com-20090119022415-mo0mcfeiexfktgwt
apply jam's log --short fix (Ian Clatworthy)

Show diffs side-by-side

added added

removed removed

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