~bzr-pqm/bzr/bzr.dev

« back to all changes in this revision

Viewing changes to bzrlib/tests/test_bundle.py

  • Committer: Robert Collins
  • Date: 2009-07-07 04:32:13 UTC
  • mto: This revision was merged to the branch mainline in revision 4524.
  • Revision ID: robertc@robertcollins.net-20090707043213-4hjjhgr40iq7gk2d
More informative assertions in xml serialisation.

Show diffs side-by-side

added added

removed removed

Lines of Context:
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
 
# Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA  02111-1307  USA
 
15
# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA
16
16
 
17
17
from cStringIO import StringIO
18
18
import os
 
19
import socket
19
20
import sys
20
 
import tempfile
 
21
import threading
21
22
 
22
23
from bzrlib import (
23
24
    bzrdir,
 
25
    diff,
24
26
    errors,
25
27
    inventory,
 
28
    merge,
 
29
    osutils,
26
30
    repository,
27
31
    revision as _mod_revision,
 
32
    tests,
28
33
    treebuilder,
29
34
    )
30
 
from bzrlib.builtins import _merge_helper
31
 
from bzrlib.bzrdir import BzrDir
 
35
from bzrlib.bundle import read_mergeable_from_url
32
36
from bzrlib.bundle.apply_bundle import install_bundle, merge_bundle
33
37
from bzrlib.bundle.bundle_data import BundleTree
34
 
from bzrlib.bundle.serializer import write_bundle, read_bundle
 
38
from bzrlib.bzrdir import BzrDir
 
39
from bzrlib.directory_service import directories
 
40
from bzrlib.bundle.serializer import write_bundle, read_bundle, v09, v4
35
41
from bzrlib.bundle.serializer.v08 import BundleSerializerV08
36
42
from bzrlib.bundle.serializer.v09 import BundleSerializerV09
 
43
from bzrlib.bundle.serializer.v4 import BundleSerializerV4
37
44
from bzrlib.branch import Branch
38
 
from bzrlib.diff import internal_diff
39
 
from bzrlib.errors import (BzrError, TestamentMismatch, NotABundle, BadBundle, 
40
 
                           NoSuchFile,)
41
 
from bzrlib.merge import Merge3Merger
42
45
from bzrlib.repofmt import knitrepo
43
 
from bzrlib.osutils import has_symlinks, sha_file
44
 
from bzrlib.tests import (TestCaseInTempDir, TestCaseWithTransport,
45
 
                          TestCase, TestSkipped)
 
46
from bzrlib.tests import (
 
47
    test_read_bundle,
 
48
    test_commit,
 
49
    )
46
50
from bzrlib.transform import TreeTransform
47
 
from bzrlib.workingtree import WorkingTree
48
51
 
49
52
 
50
53
class MockTree(object):
98
101
        elif kind == 'symlink':
99
102
            ie = InventoryLink(file_id, name, parent_id)
100
103
        else:
101
 
            raise BzrError('unknown kind %r' % kind)
 
104
            raise errors.BzrError('unknown kind %r' % kind)
102
105
        ie.text_sha1 = text_sha_1
103
106
        ie.text_size = text_size
104
107
        return ie
106
109
    def add_dir(self, file_id, path):
107
110
        self.paths[file_id] = path
108
111
        self.ids[path] = file_id
109
 
    
 
112
 
110
113
    def add_file(self, file_id, path, contents):
111
114
        self.add_dir(file_id, path)
112
115
        self.contents[file_id] = contents
129
132
    def contents_stats(self, file_id):
130
133
        if file_id not in self.contents:
131
134
            return None, None
132
 
        text_sha1 = sha_file(self.get_file(file_id))
 
135
        text_sha1 = osutils.sha_file(self.get_file(file_id))
133
136
        return text_sha1, len(self.contents[file_id])
134
137
 
135
138
 
136
 
class BTreeTester(TestCase):
 
139
class BTreeTester(tests.TestCase):
137
140
    """A simple unittest tester for the BundleTree class."""
138
141
 
139
142
    def make_tree_1(self):
143
146
        mtree.add_file("c", "grandparent/parent/file", "Hello\n")
144
147
        mtree.add_dir("d", "grandparent/alt_parent")
145
148
        return BundleTree(mtree, ''), mtree
146
 
        
 
149
 
147
150
    def test_renames(self):
148
151
        """Ensure that file renames have the proper effect on children"""
149
152
        btree = self.make_tree_1()[0]
150
153
        self.assertEqual(btree.old_path("grandparent"), "grandparent")
151
 
        self.assertEqual(btree.old_path("grandparent/parent"), 
 
154
        self.assertEqual(btree.old_path("grandparent/parent"),
152
155
                         "grandparent/parent")
153
156
        self.assertEqual(btree.old_path("grandparent/parent/file"),
154
157
                         "grandparent/parent/file")
161
164
        self.assertEqual(btree.path2id("grandparent/parent"), "b")
162
165
        self.assertEqual(btree.path2id("grandparent/parent/file"), "c")
163
166
 
164
 
        assert btree.path2id("grandparent2") is None
165
 
        assert btree.path2id("grandparent2/parent") is None
166
 
        assert btree.path2id("grandparent2/parent/file") is None
 
167
        self.assertTrue(btree.path2id("grandparent2") is None)
 
168
        self.assertTrue(btree.path2id("grandparent2/parent") is None)
 
169
        self.assertTrue(btree.path2id("grandparent2/parent/file") is None)
167
170
 
168
171
        btree.note_rename("grandparent", "grandparent2")
169
 
        assert btree.old_path("grandparent") is None
170
 
        assert btree.old_path("grandparent/parent") is None
171
 
        assert btree.old_path("grandparent/parent/file") is None
 
172
        self.assertTrue(btree.old_path("grandparent") is None)
 
173
        self.assertTrue(btree.old_path("grandparent/parent") is None)
 
174
        self.assertTrue(btree.old_path("grandparent/parent/file") is None)
172
175
 
173
176
        self.assertEqual(btree.id2path("a"), "grandparent2")
174
177
        self.assertEqual(btree.id2path("b"), "grandparent2/parent")
178
181
        self.assertEqual(btree.path2id("grandparent2/parent"), "b")
179
182
        self.assertEqual(btree.path2id("grandparent2/parent/file"), "c")
180
183
 
181
 
        assert btree.path2id("grandparent") is None
182
 
        assert btree.path2id("grandparent/parent") is None
183
 
        assert btree.path2id("grandparent/parent/file") is None
 
184
        self.assertTrue(btree.path2id("grandparent") is None)
 
185
        self.assertTrue(btree.path2id("grandparent/parent") is None)
 
186
        self.assertTrue(btree.path2id("grandparent/parent/file") is None)
184
187
 
185
188
        btree.note_rename("grandparent/parent", "grandparent2/parent2")
186
189
        self.assertEqual(btree.id2path("a"), "grandparent2")
191
194
        self.assertEqual(btree.path2id("grandparent2/parent2"), "b")
192
195
        self.assertEqual(btree.path2id("grandparent2/parent2/file"), "c")
193
196
 
194
 
        assert btree.path2id("grandparent2/parent") is None
195
 
        assert btree.path2id("grandparent2/parent/file") is None
 
197
        self.assertTrue(btree.path2id("grandparent2/parent") is None)
 
198
        self.assertTrue(btree.path2id("grandparent2/parent/file") is None)
196
199
 
197
 
        btree.note_rename("grandparent/parent/file", 
 
200
        btree.note_rename("grandparent/parent/file",
198
201
                          "grandparent2/parent2/file2")
199
202
        self.assertEqual(btree.id2path("a"), "grandparent2")
200
203
        self.assertEqual(btree.id2path("b"), "grandparent2/parent2")
204
207
        self.assertEqual(btree.path2id("grandparent2/parent2"), "b")
205
208
        self.assertEqual(btree.path2id("grandparent2/parent2/file2"), "c")
206
209
 
207
 
        assert btree.path2id("grandparent2/parent2/file") is None
 
210
        self.assertTrue(btree.path2id("grandparent2/parent2/file") is None)
208
211
 
209
212
    def test_moves(self):
210
213
        """Ensure that file moves have the proper effect on children"""
211
214
        btree = self.make_tree_1()[0]
212
 
        btree.note_rename("grandparent/parent/file", 
 
215
        btree.note_rename("grandparent/parent/file",
213
216
                          "grandparent/alt_parent/file")
214
217
        self.assertEqual(btree.id2path("c"), "grandparent/alt_parent/file")
215
218
        self.assertEqual(btree.path2id("grandparent/alt_parent/file"), "c")
216
 
        assert btree.path2id("grandparent/parent/file") is None
 
219
        self.assertTrue(btree.path2id("grandparent/parent/file") is None)
217
220
 
218
221
    def unified_diff(self, old, new):
219
222
        out = StringIO()
220
 
        internal_diff("old", old, "new", new, out)
 
223
        diff.internal_diff("old", old, "new", new, out)
221
224
        out.seek(0,0)
222
225
        return out.read()
223
226
 
224
227
    def make_tree_2(self):
225
228
        btree = self.make_tree_1()[0]
226
 
        btree.note_rename("grandparent/parent/file", 
 
229
        btree.note_rename("grandparent/parent/file",
227
230
                          "grandparent/alt_parent/file")
228
 
        assert btree.id2path("e") is None
229
 
        assert btree.path2id("grandparent/parent/file") is None
 
231
        self.assertTrue(btree.id2path("e") is None)
 
232
        self.assertTrue(btree.path2id("grandparent/parent/file") is None)
230
233
        btree.note_id("e", "grandparent/parent/file")
231
234
        return btree
232
235
 
258
261
    def make_tree_3(self):
259
262
        btree, mtree = self.make_tree_1()
260
263
        mtree.add_file("e", "grandparent/parent/topping", "Anchovies\n")
261
 
        btree.note_rename("grandparent/parent/file", 
 
264
        btree.note_rename("grandparent/parent/file",
262
265
                          "grandparent/alt_parent/file")
263
 
        btree.note_rename("grandparent/parent/topping", 
 
266
        btree.note_rename("grandparent/parent/topping",
264
267
                          "grandparent/alt_parent/stopping")
265
268
        return btree
266
269
 
290
293
        btree = self.make_tree_1()[0]
291
294
        self.assertEqual(btree.get_file("c").read(), "Hello\n")
292
295
        btree.note_deletion("grandparent/parent/file")
293
 
        assert btree.id2path("c") is None
294
 
        assert btree.path2id("grandparent/parent/file") is None
 
296
        self.assertTrue(btree.id2path("c") is None)
 
297
        self.assertTrue(btree.path2id("grandparent/parent/file") is None)
295
298
 
296
299
    def sorted_ids(self, tree):
297
300
        ids = list(tree)
305
308
            [inventory.ROOT_ID, 'a', 'b', 'c', 'd'])
306
309
        btree.note_deletion("grandparent/parent/file")
307
310
        btree.note_id("e", "grandparent/alt_parent/fool", kind="directory")
308
 
        btree.note_last_changed("grandparent/alt_parent/fool", 
 
311
        btree.note_last_changed("grandparent/alt_parent/fool",
309
312
                                "revisionidiguess")
310
313
        self.assertEqual(self.sorted_ids(btree),
311
314
            [inventory.ROOT_ID, 'a', 'b', 'd', 'e'])
312
315
 
313
316
 
314
 
class BundleTester1(TestCaseWithTransport):
 
317
class BundleTester1(tests.TestCaseWithTransport):
315
318
 
316
319
    def test_mismatched_bundle(self):
317
320
        format = bzrdir.BzrDirMetaFormat1()
318
321
        format.repository_format = knitrepo.RepositoryFormatKnit3()
319
322
        serializer = BundleSerializerV08('0.8')
320
323
        b = self.make_branch('.', format=format)
321
 
        self.assertRaises(errors.IncompatibleBundleFormat, serializer.write, 
 
324
        self.assertRaises(errors.IncompatibleBundleFormat, serializer.write,
322
325
                          b.repository, [], {}, StringIO())
323
326
 
324
327
    def test_matched_bundle(self):
344
347
        format = bzrdir.BzrDirMetaFormat1()
345
348
        format.repository_format = knitrepo.RepositoryFormatKnit1()
346
349
        target = self.make_branch('target', format=format)
347
 
        self.assertRaises(errors.IncompatibleRevision, install_bundle, 
 
350
        self.assertRaises(errors.IncompatibleRevision, install_bundle,
348
351
                          target.repository, read_bundle(text))
349
352
 
350
353
 
351
 
class V08BundleTester(TestCaseWithTransport):
352
 
 
353
 
    format = '0.8'
 
354
class BundleTester(object):
354
355
 
355
356
    def bzrdir_format(self):
356
357
        format = bzrdir.BzrDirMetaFormat1()
360
361
    def make_branch_and_tree(self, path, format=None):
361
362
        if format is None:
362
363
            format = self.bzrdir_format()
363
 
        return TestCaseWithTransport.make_branch_and_tree(self, path, format)
 
364
        return tests.TestCaseWithTransport.make_branch_and_tree(
 
365
            self, path, format)
364
366
 
365
367
    def make_branch(self, path, format=None):
366
368
        if format is None:
367
369
            format = self.bzrdir_format()
368
 
        return TestCaseWithTransport.make_branch(self, path, format)
 
370
        return tests.TestCaseWithTransport.make_branch(self, path, format)
369
371
 
370
372
    def create_bundle_text(self, base_rev_id, rev_id):
371
373
        bundle_txt = StringIO()
372
 
        rev_ids = write_bundle(self.b1.repository, rev_id, base_rev_id, 
 
374
        rev_ids = write_bundle(self.b1.repository, rev_id, base_rev_id,
373
375
                               bundle_txt, format=self.format)
374
376
        bundle_txt.seek(0)
375
 
        self.assertEqual(bundle_txt.readline(), 
 
377
        self.assertEqual(bundle_txt.readline(),
376
378
                         '# Bazaar revision bundle v%s\n' % self.format)
377
379
        self.assertEqual(bundle_txt.readline(), '#\n')
378
380
 
379
381
        rev = self.b1.repository.get_revision(rev_id)
380
382
        self.assertEqual(bundle_txt.readline().decode('utf-8'),
381
383
                         u'# message:\n')
382
 
 
383
 
        open(',,bundle', 'wb').write(bundle_txt.getvalue())
384
384
        bundle_txt.seek(0)
385
385
        return bundle_txt, rev_ids
386
386
 
388
388
        """Create a bundle from base_rev_id -> rev_id in built-in branch.
389
389
        Make sure that the text generated is valid, and that it
390
390
        can be applied against the base, and generate the same information.
391
 
        
392
 
        :return: The in-memory bundle 
 
391
 
 
392
        :return: The in-memory bundle
393
393
        """
394
394
        bundle_txt, rev_ids = self.create_bundle_text(base_rev_id, rev_id)
395
395
 
396
 
        # This should also validate the generated bundle 
 
396
        # This should also validate the generated bundle
397
397
        bundle = read_bundle(bundle_txt)
398
398
        repository = self.b1.repository
399
399
        for bundle_rev in bundle.real_revisions:
403
403
            # it
404
404
            branch_rev = repository.get_revision(bundle_rev.revision_id)
405
405
            for a in ('inventory_sha1', 'revision_id', 'parent_ids',
406
 
                      'timestamp', 'timezone', 'message', 'committer', 
 
406
                      'timestamp', 'timezone', 'message', 'committer',
407
407
                      'parent_ids', 'properties'):
408
 
                self.assertEqual(getattr(branch_rev, a), 
 
408
                self.assertEqual(getattr(branch_rev, a),
409
409
                                 getattr(bundle_rev, a))
410
 
            self.assertEqual(len(branch_rev.parent_ids), 
 
410
            self.assertEqual(len(branch_rev.parent_ids),
411
411
                             len(bundle_rev.parent_ids))
412
 
        self.assertEqual(rev_ids, 
 
412
        self.assertEqual(rev_ids,
413
413
                         [r.revision_id for r in bundle.real_revisions])
414
414
        self.valid_apply_bundle(base_rev_id, bundle,
415
415
                                   checkout_dir=checkout_dir)
419
419
    def get_invalid_bundle(self, base_rev_id, rev_id):
420
420
        """Create a bundle from base_rev_id -> rev_id in built-in branch.
421
421
        Munge the text so that it's invalid.
422
 
        
 
422
 
423
423
        :return: The in-memory bundle
424
424
        """
425
425
        bundle_txt, rev_ids = self.create_bundle_text(base_rev_id, rev_id)
426
 
        new_text = bundle_txt.getvalue().replace('executable:no', 
 
426
        new_text = bundle_txt.getvalue().replace('executable:no',
427
427
                                               'executable:yes')
428
428
        bundle_txt = StringIO(new_text)
429
429
        bundle = read_bundle(bundle_txt)
430
430
        self.valid_apply_bundle(base_rev_id, bundle)
431
 
        return bundle 
 
431
        return bundle
432
432
 
433
433
    def test_non_bundle(self):
434
 
        self.assertRaises(NotABundle, read_bundle, StringIO('#!/bin/sh\n'))
 
434
        self.assertRaises(errors.NotABundle,
 
435
                          read_bundle, StringIO('#!/bin/sh\n'))
435
436
 
436
437
    def test_malformed(self):
437
 
        self.assertRaises(BadBundle, read_bundle, 
 
438
        self.assertRaises(errors.BadBundle, read_bundle,
438
439
                          StringIO('# Bazaar revision bundle v'))
439
440
 
440
441
    def test_crlf_bundle(self):
441
442
        try:
442
443
            read_bundle(StringIO('# Bazaar revision bundle v0.8\r\n'))
443
 
        except BadBundle:
 
444
        except errors.BadBundle:
444
445
            # It is currently permitted for bundles with crlf line endings to
445
446
            # make read_bundle raise a BadBundle, but this should be fixed.
446
447
            # Anything else, especially NotABundle, is an error.
451
452
        """
452
453
 
453
454
        if checkout_dir is None:
454
 
            checkout_dir = tempfile.mkdtemp(prefix='test-branch-', dir='.')
 
455
            checkout_dir = osutils.mkdtemp(prefix='test-branch-', dir='.')
455
456
        else:
456
457
            if not os.path.exists(checkout_dir):
457
458
                os.mkdir(checkout_dir)
460
461
        ancestors = write_bundle(self.b1.repository, rev_id, 'null:', s,
461
462
                                 format=self.format)
462
463
        s.seek(0)
463
 
        assert isinstance(s.getvalue(), str), (
464
 
            "Bundle isn't a bytestring:\n %s..." % repr(s.getvalue())[:40])
 
464
        self.assertIsInstance(s.getvalue(), str)
465
465
        install_bundle(tree.branch.repository, read_bundle(s))
466
466
        for ancestor in ancestors:
467
467
            old = self.b1.repository.revision_tree(ancestor)
468
468
            new = tree.branch.repository.revision_tree(ancestor)
469
 
 
470
 
            # Check that there aren't any inventory level changes
471
 
            delta = new.changes_from(old)
472
 
            self.assertFalse(delta.has_changed(),
473
 
                             'Revision %s not copied correctly.'
474
 
                             % (ancestor,))
475
 
 
476
 
            # Now check that the file contents are all correct
477
 
            for inventory_id in old:
478
 
                try:
479
 
                    old_file = old.get_file(inventory_id)
480
 
                except NoSuchFile:
481
 
                    continue
482
 
                if old_file is None:
483
 
                    continue
484
 
                self.assertEqual(old_file.read(),
485
 
                                 new.get_file(inventory_id).read())
 
469
            old.lock_read()
 
470
            new.lock_read()
 
471
            try:
 
472
                # Check that there aren't any inventory level changes
 
473
                delta = new.changes_from(old)
 
474
                self.assertFalse(delta.has_changed(),
 
475
                                 'Revision %s not copied correctly.'
 
476
                                 % (ancestor,))
 
477
 
 
478
                # Now check that the file contents are all correct
 
479
                for inventory_id in old:
 
480
                    try:
 
481
                        old_file = old.get_file(inventory_id)
 
482
                    except errors.NoSuchFile:
 
483
                        continue
 
484
                    if old_file is None:
 
485
                        continue
 
486
                    self.assertEqual(old_file.read(),
 
487
                                     new.get_file(inventory_id).read())
 
488
            finally:
 
489
                new.unlock()
 
490
                old.unlock()
486
491
        if not _mod_revision.is_null(rev_id):
487
492
            rh = self.b1.revision_history()
488
493
            tree.branch.set_revision_history(rh[:rh.index(rev_id)+1])
497
502
        sure everything matches the builtin branch.
498
503
        """
499
504
        to_tree = self.get_checkout(base_rev_id, checkout_dir=checkout_dir)
 
505
        to_tree.lock_write()
 
506
        try:
 
507
            self._valid_apply_bundle(base_rev_id, info, to_tree)
 
508
        finally:
 
509
            to_tree.unlock()
 
510
 
 
511
    def _valid_apply_bundle(self, base_rev_id, info, to_tree):
500
512
        original_parents = to_tree.get_parent_ids()
501
513
        repository = to_tree.branch.repository
502
514
        original_parents = to_tree.get_parent_ids()
503
515
        self.assertIs(repository.has_revision(base_rev_id), True)
504
516
        for rev in info.real_revisions:
505
517
            self.assert_(not repository.has_revision(rev.revision_id),
506
 
                'Revision {%s} present before applying bundle' 
 
518
                'Revision {%s} present before applying bundle'
507
519
                % rev.revision_id)
508
 
        merge_bundle(info, to_tree, True, Merge3Merger, False, False)
 
520
        merge_bundle(info, to_tree, True, merge.Merge3Merger, False, False)
509
521
 
510
522
        for rev in info.real_revisions:
511
523
            self.assert_(repository.has_revision(rev.revision_id),
512
 
                'Missing revision {%s} after applying bundle' 
 
524
                'Missing revision {%s} after applying bundle'
513
525
                % rev.revision_id)
514
526
 
515
527
        self.assert_(to_tree.branch.repository.has_revision(info.target))
521
533
        rev = info.real_revisions[-1]
522
534
        base_tree = self.b1.repository.revision_tree(rev.revision_id)
523
535
        to_tree = to_tree.branch.repository.revision_tree(rev.revision_id)
524
 
        
 
536
 
525
537
        # TODO: make sure the target tree is identical to base tree
526
538
        #       we might also check the working tree.
527
539
 
551
563
        self.tree1.commit('add one', rev_id='a@cset-0-1')
552
564
 
553
565
        bundle = self.get_valid_bundle('null:', 'a@cset-0-1')
554
 
        # FIXME: The current write_bundle api no longer supports
555
 
        #        setting a custom summary message
556
 
        #        We should re-introduce the ability, and update
557
 
        #        the tests to make sure it works.
558
 
        # bundle = self.get_valid_bundle(None, 'a@cset-0-1',
559
 
        #         message='With a specialized message')
560
566
 
561
567
        # Make sure we can handle files with spaces, tabs, other
562
568
        # bogus characters
575
581
        tt = TreeTransform(self.tree1)
576
582
        tt.new_file('executable', tt.root, '#!/bin/sh\n', 'exe-1', True)
577
583
        tt.apply()
 
584
        # have to fix length of file-id so that we can predictably rewrite
 
585
        # a (length-prefixed) record containing it later.
 
586
        self.tree1.add('with space.txt', 'withspace-id')
578
587
        self.tree1.add([
579
 
                'with space.txt'
580
 
                , 'dir'
 
588
                  'dir'
581
589
                , 'dir/filein subdir.c'
582
590
                , 'dir/WithCaps.txt'
583
591
                , 'dir/ pre space'
591
599
 
592
600
        bundle = self.get_valid_bundle('a@cset-0-1', 'a@cset-0-2')
593
601
 
594
 
        # Check a rollup bundle 
 
602
        # Check a rollup bundle
595
603
        bundle = self.get_valid_bundle('null:', 'a@cset-0-2')
596
604
 
597
605
        # Now delete entries
605
613
        tt.set_executability(False, trans_id)
606
614
        tt.apply()
607
615
        self.tree1.commit('removed', rev_id='a@cset-0-3')
608
 
        
 
616
 
609
617
        bundle = self.get_valid_bundle('a@cset-0-2', 'a@cset-0-3')
610
 
        self.assertRaises(TestamentMismatch, self.get_invalid_bundle, 
611
 
                          'a@cset-0-2', 'a@cset-0-3')
612
 
        # Check a rollup bundle 
 
618
        self.assertRaises((errors.TestamentMismatch,
 
619
            errors.VersionedFileInvalidChecksum), self.get_invalid_bundle,
 
620
            'a@cset-0-2', 'a@cset-0-3')
 
621
        # Check a rollup bundle
613
622
        bundle = self.get_valid_bundle('null:', 'a@cset-0-3')
614
623
 
615
624
        # Now move the directory
617
626
        self.tree1.commit('rename dir', rev_id='a@cset-0-4')
618
627
 
619
628
        bundle = self.get_valid_bundle('a@cset-0-3', 'a@cset-0-4')
620
 
        # Check a rollup bundle 
 
629
        # Check a rollup bundle
621
630
        bundle = self.get_valid_bundle('null:', 'a@cset-0-4')
622
631
 
623
632
        # Modified files
624
633
        open('b1/sub/dir/WithCaps.txt', 'ab').write('\nAdding some text\n')
625
 
        open('b1/sub/dir/ pre space', 'ab').write('\r\nAdding some\r\nDOS format lines\r\n')
 
634
        open('b1/sub/dir/ pre space', 'ab').write(
 
635
             '\r\nAdding some\r\nDOS format lines\r\n')
626
636
        open('b1/sub/dir/nolastnewline.txt', 'ab').write('\n')
627
 
        self.tree1.rename_one('sub/dir/ pre space', 
 
637
        self.tree1.rename_one('sub/dir/ pre space',
628
638
                              'sub/ start space')
629
639
        self.tree1.commit('Modified files', rev_id='a@cset-0-5')
630
640
        bundle = self.get_valid_bundle('a@cset-0-4', 'a@cset-0-5')
642
652
        self.assertEqualDiff(tree1_inv, tree2_inv)
643
653
        other.rename_one('sub/dir/nolastnewline.txt', 'sub/nolastnewline.txt')
644
654
        other.commit('rename file', rev_id='a@cset-0-6b')
645
 
        _merge_helper([other.basedir, -1], [None, None],
646
 
                      this_dir=self.tree1.basedir)
 
655
        self.tree1.merge_from_branch(other.branch)
647
656
        self.tree1.commit(u'Merge', rev_id='a@cset-0-7',
648
657
                          verbose=False)
649
658
        bundle = self.get_valid_bundle('a@cset-0-6', 'a@cset-0-7')
650
659
 
651
 
    def test_symlink_bundle(self):
652
 
        if not has_symlinks():
653
 
            raise TestSkipped("No symlink support")
 
660
    def _test_symlink_bundle(self, link_name, link_target, new_link_target):
 
661
        link_id = 'link-1'
 
662
 
 
663
        self.requireFeature(tests.SymlinkFeature)
654
664
        self.tree1 = self.make_branch_and_tree('b1')
655
665
        self.b1 = self.tree1.branch
 
666
 
656
667
        tt = TreeTransform(self.tree1)
657
 
        tt.new_symlink('link', tt.root, 'bar/foo', 'link-1')
 
668
        tt.new_symlink(link_name, tt.root, link_target, link_id)
658
669
        tt.apply()
659
670
        self.tree1.commit('add symlink', rev_id='l@cset-0-1')
660
 
        self.get_valid_bundle('null:', 'l@cset-0-1')
 
671
        bundle = self.get_valid_bundle('null:', 'l@cset-0-1')
 
672
        if getattr(bundle ,'revision_tree', None) is not None:
 
673
            # Not all bundle formats supports revision_tree
 
674
            bund_tree = bundle.revision_tree(self.b1.repository, 'l@cset-0-1')
 
675
            self.assertEqual(link_target, bund_tree.get_symlink_target(link_id))
 
676
 
661
677
        tt = TreeTransform(self.tree1)
662
 
        trans_id = tt.trans_id_tree_file_id('link-1')
 
678
        trans_id = tt.trans_id_tree_file_id(link_id)
663
679
        tt.adjust_path('link2', tt.root, trans_id)
664
680
        tt.delete_contents(trans_id)
665
 
        tt.create_symlink('mars', trans_id)
 
681
        tt.create_symlink(new_link_target, trans_id)
666
682
        tt.apply()
667
683
        self.tree1.commit('rename and change symlink', rev_id='l@cset-0-2')
668
 
        self.get_valid_bundle('l@cset-0-1', 'l@cset-0-2')
 
684
        bundle = self.get_valid_bundle('l@cset-0-1', 'l@cset-0-2')
 
685
        if getattr(bundle ,'revision_tree', None) is not None:
 
686
            # Not all bundle formats supports revision_tree
 
687
            bund_tree = bundle.revision_tree(self.b1.repository, 'l@cset-0-2')
 
688
            self.assertEqual(new_link_target,
 
689
                             bund_tree.get_symlink_target(link_id))
 
690
 
669
691
        tt = TreeTransform(self.tree1)
670
 
        trans_id = tt.trans_id_tree_file_id('link-1')
 
692
        trans_id = tt.trans_id_tree_file_id(link_id)
671
693
        tt.delete_contents(trans_id)
672
694
        tt.create_symlink('jupiter', trans_id)
673
695
        tt.apply()
674
696
        self.tree1.commit('just change symlink target', rev_id='l@cset-0-3')
675
 
        self.get_valid_bundle('l@cset-0-2', 'l@cset-0-3')
 
697
        bundle = self.get_valid_bundle('l@cset-0-2', 'l@cset-0-3')
 
698
 
676
699
        tt = TreeTransform(self.tree1)
677
 
        trans_id = tt.trans_id_tree_file_id('link-1')
 
700
        trans_id = tt.trans_id_tree_file_id(link_id)
678
701
        tt.delete_contents(trans_id)
679
702
        tt.apply()
680
703
        self.tree1.commit('Delete symlink', rev_id='l@cset-0-4')
681
 
        self.get_valid_bundle('l@cset-0-3', 'l@cset-0-4')
 
704
        bundle = self.get_valid_bundle('l@cset-0-3', 'l@cset-0-4')
 
705
 
 
706
    def test_symlink_bundle(self):
 
707
        self._test_symlink_bundle('link', 'bar/foo', 'mars')
 
708
 
 
709
    def test_unicode_symlink_bundle(self):
 
710
        self.requireFeature(tests.UnicodeFilenameFeature)
 
711
        self._test_symlink_bundle(u'\N{Euro Sign}link',
 
712
                                  u'bar/\N{Euro Sign}foo',
 
713
                                  u'mars\N{Euro Sign}')
682
714
 
683
715
    def test_binary_bundle(self):
684
716
        self.tree1 = self.make_branch_and_tree('b1')
685
717
        self.b1 = self.tree1.branch
686
718
        tt = TreeTransform(self.tree1)
687
 
        
 
719
 
688
720
        # Add
689
721
        tt.new_file('file', tt.root, '\x00\n\x00\r\x01\n\x02\r\xff', 'binary-1')
690
 
        tt.new_file('file2', tt.root, '\x01\n\x02\r\x03\n\x04\r\xff', 'binary-2')
 
722
        tt.new_file('file2', tt.root, '\x01\n\x02\r\x03\n\x04\r\xff',
 
723
            'binary-2')
691
724
        tt.apply()
692
725
        self.tree1.commit('add binary', rev_id='b@cset-0-1')
693
726
        self.get_valid_bundle('null:', 'b@cset-0-1')
744
777
        tt.create_file('file2', trans_id)
745
778
        tt.apply()
746
779
        other.commit('modify text in another tree', rev_id='a@lmod-0-2b')
747
 
        _merge_helper([other.basedir, -1], [None, None],
748
 
                      this_dir=self.tree1.basedir)
 
780
        self.tree1.merge_from_branch(other.branch)
749
781
        self.tree1.commit(u'Merge', rev_id='a@lmod-0-3',
750
782
                          verbose=False)
751
783
        self.tree1.commit(u'Merge', rev_id='a@lmod-0-4')
766
798
        rev_ids = write_bundle(self.tree1.branch.repository, 'a@cset-0-3',
767
799
                               'a@cset-0-1', bundle_file, format=self.format)
768
800
        self.assertNotContainsRe(bundle_file.getvalue(), '\btwo\b')
769
 
        self.assertContainsRe(bundle_file.getvalue(), 'one')
770
 
        self.assertContainsRe(bundle_file.getvalue(), 'three')
 
801
        self.assertContainsRe(self.get_raw(bundle_file), 'one')
 
802
        self.assertContainsRe(self.get_raw(bundle_file), 'three')
 
803
 
 
804
    def test_bundle_same_basis(self):
 
805
        """Ensure using the basis as the target doesn't cause an error"""
 
806
        self.tree1 = self.make_branch_and_tree('b1')
 
807
        self.tree1.commit('add file', rev_id='a@cset-0-1')
 
808
        bundle_file = StringIO()
 
809
        rev_ids = write_bundle(self.tree1.branch.repository, 'a@cset-0-1',
 
810
                               'a@cset-0-1', bundle_file)
 
811
 
 
812
    @staticmethod
 
813
    def get_raw(bundle_file):
 
814
        return bundle_file.getvalue()
771
815
 
772
816
    def test_unicode_bundle(self):
 
817
        self.requireFeature(tests.UnicodeFilenameFeature)
773
818
        # Handle international characters
774
819
        os.mkdir('b1')
775
 
        try:
776
 
            f = open(u'b1/with Dod\xe9', 'wb')
777
 
        except UnicodeEncodeError:
778
 
            raise TestSkipped("Filesystem doesn't support unicode")
 
820
        f = open(u'b1/with Dod\N{Euro Sign}', 'wb')
779
821
 
780
822
        self.tree1 = self.make_branch_and_tree('b1')
781
823
        self.b1 = self.tree1.branch
785
827
            u'William Dod\xe9\n').encode('utf-8'))
786
828
        f.close()
787
829
 
788
 
        self.tree1.add([u'with Dod\xe9'], ['withdod-id'])
 
830
        self.tree1.add([u'with Dod\N{Euro Sign}'], ['withdod-id'])
789
831
        self.tree1.commit(u'i18n commit from William Dod\xe9',
790
832
                          rev_id='i18n-1', committer=u'William Dod\xe9')
791
833
 
792
 
        if sys.platform == 'darwin':
793
 
            # On Mac the '\xe9' gets changed to 'e\u0301'
794
 
            self.assertEqual([u'.bzr', u'with Dode\u0301'],
795
 
                             sorted(os.listdir(u'b1')))
796
 
            delta = self.tree1.changes_from(self.tree1.basis_tree())
797
 
            self.assertEqual([(u'with Dod\xe9', 'withdod-id', 'file')],
798
 
                             delta.removed)
799
 
            self.knownFailure("Mac OSX doesn't preserve unicode"
800
 
                              " combining characters.")
801
 
 
802
834
        # Add
803
835
        bundle = self.get_valid_bundle('null:', 'i18n-1')
804
836
 
805
837
        # Modified
806
 
        f = open(u'b1/with Dod\xe9', 'wb')
 
838
        f = open(u'b1/with Dod\N{Euro Sign}', 'wb')
807
839
        f.write(u'Modified \xb5\n'.encode('utf8'))
808
840
        f.close()
809
841
        self.tree1.commit(u'modified', rev_id='i18n-2')
810
842
 
811
843
        bundle = self.get_valid_bundle('i18n-1', 'i18n-2')
812
 
        
 
844
 
813
845
        # Renamed
814
 
        self.tree1.rename_one(u'with Dod\xe9', u'B\xe5gfors')
 
846
        self.tree1.rename_one(u'with Dod\N{Euro Sign}', u'B\N{Euro Sign}gfors')
815
847
        self.tree1.commit(u'renamed, the new i18n man', rev_id='i18n-3',
816
848
                          committer=u'Erik B\xe5gfors')
817
849
 
818
850
        bundle = self.get_valid_bundle('i18n-2', 'i18n-3')
819
851
 
820
852
        # Removed
821
 
        self.tree1.remove([u'B\xe5gfors'])
 
853
        self.tree1.remove([u'B\N{Euro Sign}gfors'])
822
854
        self.tree1.commit(u'removed', rev_id='i18n-4')
823
855
 
824
856
        bundle = self.get_valid_bundle('i18n-3', 'i18n-4')
829
861
 
830
862
    def test_whitespace_bundle(self):
831
863
        if sys.platform in ('win32', 'cygwin'):
832
 
            raise TestSkipped('Windows doesn\'t support filenames'
833
 
                              ' with tabs or trailing spaces')
 
864
            raise tests.TestSkipped('Windows doesn\'t support filenames'
 
865
                                    ' with tabs or trailing spaces')
834
866
        self.tree1 = self.make_branch_and_tree('b1')
835
867
        self.b1 = self.tree1.branch
836
868
 
861
893
        self.tree1.commit('removed', rev_id='white-4')
862
894
 
863
895
        bundle = self.get_valid_bundle('white-3', 'white-4')
864
 
        
 
896
 
865
897
        # Now test a complet roll-up
866
898
        bundle = self.get_valid_bundle('null:', 'white-4')
867
899
 
880
912
                          timezone=19800, timestamp=1152544886.0)
881
913
 
882
914
        bundle = self.get_valid_bundle('null:', 'tz-1')
883
 
        
 
915
 
884
916
        rev = bundle.revisions[0]
885
917
        self.assertEqual('Mon 2006-07-10 20:51:26.000000000 +0530', rev.date)
886
918
        self.assertEqual(19800, rev.timezone)
892
924
        self.b1 = self.tree1.branch
893
925
        self.tree1.commit('message', rev_id='revid1')
894
926
        bundle = self.get_valid_bundle('null:', 'revid1')
895
 
        tree = bundle.revision_tree(self.b1.repository, 'revid1')
 
927
        tree = self.get_bundle_tree(bundle, 'revid1')
896
928
        self.assertEqual('revid1', tree.inventory.root.revision)
897
929
 
898
930
    def test_install_revisions(self):
915
947
        tree.commit('One', revprops={'one':'two', 'empty':''}, rev_id='rev1')
916
948
        self.b1 = tree.branch
917
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', 'empty':'', 'one':'two'},
 
955
                         rev.properties)
 
956
 
 
957
    def test_bundle_sorted_properties(self):
 
958
        """For stability the writer should write properties in sorted order."""
 
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
        tree.commit('One', rev_id='rev1',
 
965
                    revprops={'a':'4', 'b':'3', 'c':'2', 'd':'1'})
 
966
        self.b1 = tree.branch
 
967
        bundle_sio, revision_ids = self.create_bundle_text('null:', 'rev1')
 
968
        bundle = read_bundle(bundle_sio)
 
969
        revision_info = bundle.revisions[0]
 
970
        self.assertEqual('rev1', revision_info.revision_id)
 
971
        rev = revision_info.as_revision()
 
972
        self.assertEqual({'branch-nick':'tree', 'a':'4', 'b':'3', 'c':'2',
 
973
                          'd':'1'}, rev.properties)
 
974
 
 
975
    def test_bundle_unicode_properties(self):
 
976
        """We should be able to round trip a non-ascii property."""
 
977
        tree = self.make_branch_and_memory_tree('tree')
 
978
        tree.lock_write()
 
979
        self.addCleanup(tree.unlock)
 
980
 
 
981
        tree.add([''], ['TREE_ROOT'])
 
982
        # Revisions themselves do not require anything about revision property
 
983
        # keys, other than that they are a basestring, and do not contain
 
984
        # whitespace.
 
985
        # However, Testaments assert than they are str(), and thus should not
 
986
        # be Unicode.
 
987
        tree.commit('One', rev_id='rev1',
 
988
                    revprops={'omega':u'\u03a9', 'alpha':u'\u03b1'})
 
989
        self.b1 = tree.branch
 
990
        bundle_sio, revision_ids = self.create_bundle_text('null:', 'rev1')
 
991
        bundle = read_bundle(bundle_sio)
 
992
        revision_info = bundle.revisions[0]
 
993
        self.assertEqual('rev1', revision_info.revision_id)
 
994
        rev = revision_info.as_revision()
 
995
        self.assertEqual({'branch-nick':'tree', 'omega':u'\u03a9',
 
996
                          'alpha':u'\u03b1'}, rev.properties)
 
997
 
 
998
    def test_bundle_with_ghosts(self):
 
999
        tree = self.make_branch_and_tree('tree')
 
1000
        self.b1 = tree.branch
 
1001
        self.build_tree_contents([('tree/file', 'content1')])
 
1002
        tree.add(['file'])
 
1003
        tree.commit('rev1')
 
1004
        self.build_tree_contents([('tree/file', 'content2')])
 
1005
        tree.add_parent_tree_id('ghost')
 
1006
        tree.commit('rev2', rev_id='rev2')
 
1007
        bundle = self.get_valid_bundle('null:', 'rev2')
 
1008
 
 
1009
    def make_simple_tree(self, format=None):
 
1010
        tree = self.make_branch_and_tree('b1', format=format)
 
1011
        self.b1 = tree.branch
 
1012
        self.build_tree(['b1/file'])
 
1013
        tree.add('file')
 
1014
        return tree
 
1015
 
 
1016
    def test_across_serializers(self):
 
1017
        tree = self.make_simple_tree('knit')
 
1018
        tree.commit('hello', rev_id='rev1')
 
1019
        tree.commit('hello', rev_id='rev2')
 
1020
        bundle = read_bundle(self.create_bundle_text('null:', 'rev2')[0])
 
1021
        repo = self.make_repository('repo', format='dirstate-with-subtree')
 
1022
        bundle.install_revisions(repo)
 
1023
        inv_text = repo.get_inventory_xml('rev2')
 
1024
        self.assertNotContainsRe(inv_text, 'format="5"')
 
1025
        self.assertContainsRe(inv_text, 'format="7"')
 
1026
 
 
1027
    def make_repo_with_installed_revisions(self):
 
1028
        tree = self.make_simple_tree('knit')
 
1029
        tree.commit('hello', rev_id='rev1')
 
1030
        tree.commit('hello', rev_id='rev2')
 
1031
        bundle = read_bundle(self.create_bundle_text('null:', 'rev2')[0])
 
1032
        repo = self.make_repository('repo', format='dirstate-with-subtree')
 
1033
        bundle.install_revisions(repo)
 
1034
        return repo
 
1035
 
 
1036
    def test_across_models(self):
 
1037
        repo = self.make_repo_with_installed_revisions()
 
1038
        inv = repo.get_inventory('rev2')
 
1039
        self.assertEqual('rev2', inv.root.revision)
 
1040
        root_id = inv.root.file_id
 
1041
        repo.lock_read()
 
1042
        self.addCleanup(repo.unlock)
 
1043
        self.assertEqual({(root_id, 'rev1'):(),
 
1044
            (root_id, 'rev2'):((root_id, 'rev1'),)},
 
1045
            repo.texts.get_parent_map([(root_id, 'rev1'), (root_id, 'rev2')]))
 
1046
 
 
1047
    def test_inv_hash_across_serializers(self):
 
1048
        repo = self.make_repo_with_installed_revisions()
 
1049
        recorded_inv_sha1 = repo.get_inventory_sha1('rev2')
 
1050
        xml = repo.get_inventory_xml('rev2')
 
1051
        self.assertEqual(osutils.sha_string(xml), recorded_inv_sha1)
 
1052
 
 
1053
    def test_across_models_incompatible(self):
 
1054
        tree = self.make_simple_tree('dirstate-with-subtree')
 
1055
        tree.commit('hello', rev_id='rev1')
 
1056
        tree.commit('hello', rev_id='rev2')
 
1057
        try:
 
1058
            bundle = read_bundle(self.create_bundle_text('null:', 'rev1')[0])
 
1059
        except errors.IncompatibleBundleFormat:
 
1060
            raise tests.TestSkipped("Format 0.8 doesn't work with knit3")
 
1061
        repo = self.make_repository('repo', format='knit')
 
1062
        bundle.install_revisions(repo)
 
1063
 
 
1064
        bundle = read_bundle(self.create_bundle_text('null:', 'rev2')[0])
 
1065
        self.assertRaises(errors.IncompatibleRevision,
 
1066
                          bundle.install_revisions, repo)
 
1067
 
 
1068
    def test_get_merge_request(self):
 
1069
        tree = self.make_simple_tree()
 
1070
        tree.commit('hello', rev_id='rev1')
 
1071
        tree.commit('hello', rev_id='rev2')
 
1072
        bundle = read_bundle(self.create_bundle_text('null:', 'rev1')[0])
 
1073
        result = bundle.get_merge_request(tree.branch.repository)
 
1074
        self.assertEqual((None, 'rev1', 'inapplicable'), result)
 
1075
 
 
1076
    def test_with_subtree(self):
 
1077
        tree = self.make_branch_and_tree('tree',
 
1078
                                         format='dirstate-with-subtree')
 
1079
        self.b1 = tree.branch
 
1080
        subtree = self.make_branch_and_tree('tree/subtree',
 
1081
                                            format='dirstate-with-subtree')
 
1082
        tree.add('subtree')
 
1083
        tree.commit('hello', rev_id='rev1')
 
1084
        try:
 
1085
            bundle = read_bundle(self.create_bundle_text('null:', 'rev1')[0])
 
1086
        except errors.IncompatibleBundleFormat:
 
1087
            raise tests.TestSkipped("Format 0.8 doesn't work with knit3")
 
1088
        if isinstance(bundle, v09.BundleInfo09):
 
1089
            raise tests.TestSkipped("Format 0.9 doesn't work with subtrees")
 
1090
        repo = self.make_repository('repo', format='knit')
 
1091
        self.assertRaises(errors.IncompatibleRevision,
 
1092
                          bundle.install_revisions, repo)
 
1093
        repo2 = self.make_repository('repo2', format='dirstate-with-subtree')
 
1094
        bundle.install_revisions(repo2)
 
1095
 
 
1096
    def test_revision_id_with_slash(self):
 
1097
        self.tree1 = self.make_branch_and_tree('tree')
 
1098
        self.b1 = self.tree1.branch
 
1099
        try:
 
1100
            self.tree1.commit('Revision/id/with/slashes', rev_id='rev/id')
 
1101
        except ValueError:
 
1102
            raise tests.TestSkipped(
 
1103
                "Repository doesn't support revision ids with slashes")
 
1104
        bundle = self.get_valid_bundle('null:', 'rev/id')
 
1105
 
 
1106
    def test_skip_file(self):
 
1107
        """Make sure we don't accidentally write to the wrong versionedfile"""
 
1108
        self.tree1 = self.make_branch_and_tree('tree')
 
1109
        self.b1 = self.tree1.branch
 
1110
        # rev1 is not present in bundle, done by fetch
 
1111
        self.build_tree_contents([('tree/file2', 'contents1')])
 
1112
        self.tree1.add('file2', 'file2-id')
 
1113
        self.tree1.commit('rev1', rev_id='reva')
 
1114
        self.build_tree_contents([('tree/file3', 'contents2')])
 
1115
        # rev2 is present in bundle, and done by fetch
 
1116
        # having file1 in the bunle causes file1's versionedfile to be opened.
 
1117
        self.tree1.add('file3', 'file3-id')
 
1118
        self.tree1.commit('rev2')
 
1119
        # Updating file2 should not cause an attempt to add to file1's vf
 
1120
        target = self.tree1.bzrdir.sprout('target').open_workingtree()
 
1121
        self.build_tree_contents([('tree/file2', 'contents3')])
 
1122
        self.tree1.commit('rev3', rev_id='rev3')
 
1123
        bundle = self.get_valid_bundle('reva', 'rev3')
 
1124
        if getattr(bundle, 'get_bundle_reader', None) is None:
 
1125
            raise tests.TestSkipped('Bundle format cannot provide reader')
 
1126
        # be sure that file1 comes before file2
 
1127
        for b, m, k, r, f in bundle.get_bundle_reader().iter_records():
 
1128
            if f == 'file3-id':
 
1129
                break
 
1130
            self.assertNotEqual(f, 'file2-id')
 
1131
        bundle.install_revisions(target.branch.repository)
 
1132
 
 
1133
 
 
1134
class V08BundleTester(BundleTester, tests.TestCaseWithTransport):
 
1135
 
 
1136
    format = '0.8'
 
1137
 
 
1138
    def test_bundle_empty_property(self):
 
1139
        """Test serializing revision properties with an empty value."""
 
1140
        tree = self.make_branch_and_memory_tree('tree')
 
1141
        tree.lock_write()
 
1142
        self.addCleanup(tree.unlock)
 
1143
        tree.add([''], ['TREE_ROOT'])
 
1144
        tree.commit('One', revprops={'one':'two', 'empty':''}, rev_id='rev1')
 
1145
        self.b1 = tree.branch
 
1146
        bundle_sio, revision_ids = self.create_bundle_text('null:', 'rev1')
918
1147
        self.assertContainsRe(bundle_sio.getvalue(),
919
1148
                              '# properties:\n'
920
1149
                              '#   branch-nick: tree\n'
928
1157
        self.assertEqual({'branch-nick':'tree', 'empty':'', 'one':'two'},
929
1158
                         rev.properties)
930
1159
 
 
1160
    def get_bundle_tree(self, bundle, revision_id):
 
1161
        repository = self.make_repository('repo')
 
1162
        return bundle.revision_tree(repository, 'revid1')
 
1163
 
931
1164
    def test_bundle_empty_property_alt(self):
932
1165
        """Test serializing revision properties with an empty value.
933
1166
 
1037
1270
        return format
1038
1271
 
1039
1272
 
1040
 
class MungedBundleTester(TestCaseWithTransport):
 
1273
class V4BundleTester(BundleTester, tests.TestCaseWithTransport):
 
1274
 
 
1275
    format = '4'
 
1276
 
 
1277
    def get_valid_bundle(self, base_rev_id, rev_id, checkout_dir=None):
 
1278
        """Create a bundle from base_rev_id -> rev_id in built-in branch.
 
1279
        Make sure that the text generated is valid, and that it
 
1280
        can be applied against the base, and generate the same information.
 
1281
 
 
1282
        :return: The in-memory bundle
 
1283
        """
 
1284
        bundle_txt, rev_ids = self.create_bundle_text(base_rev_id, rev_id)
 
1285
 
 
1286
        # This should also validate the generated bundle
 
1287
        bundle = read_bundle(bundle_txt)
 
1288
        repository = self.b1.repository
 
1289
        for bundle_rev in bundle.real_revisions:
 
1290
            # These really should have already been checked when we read the
 
1291
            # bundle, since it computes the sha1 hash for the revision, which
 
1292
            # only will match if everything is okay, but lets be explicit about
 
1293
            # it
 
1294
            branch_rev = repository.get_revision(bundle_rev.revision_id)
 
1295
            for a in ('inventory_sha1', 'revision_id', 'parent_ids',
 
1296
                      'timestamp', 'timezone', 'message', 'committer',
 
1297
                      'parent_ids', 'properties'):
 
1298
                self.assertEqual(getattr(branch_rev, a),
 
1299
                                 getattr(bundle_rev, a))
 
1300
            self.assertEqual(len(branch_rev.parent_ids),
 
1301
                             len(bundle_rev.parent_ids))
 
1302
        self.assertEqual(set(rev_ids),
 
1303
                         set([r.revision_id for r in bundle.real_revisions]))
 
1304
        self.valid_apply_bundle(base_rev_id, bundle,
 
1305
                                   checkout_dir=checkout_dir)
 
1306
 
 
1307
        return bundle
 
1308
 
 
1309
    def get_invalid_bundle(self, base_rev_id, rev_id):
 
1310
        """Create a bundle from base_rev_id -> rev_id in built-in branch.
 
1311
        Munge the text so that it's invalid.
 
1312
 
 
1313
        :return: The in-memory bundle
 
1314
        """
 
1315
        from bzrlib.bundle import serializer
 
1316
        bundle_txt, rev_ids = self.create_bundle_text(base_rev_id, rev_id)
 
1317
        new_text = self.get_raw(StringIO(''.join(bundle_txt)))
 
1318
        new_text = new_text.replace('<file file_id="exe-1"',
 
1319
                                    '<file executable="y" file_id="exe-1"')
 
1320
        new_text = new_text.replace('B222', 'B237')
 
1321
        bundle_txt = StringIO()
 
1322
        bundle_txt.write(serializer._get_bundle_header('4'))
 
1323
        bundle_txt.write('\n')
 
1324
        bundle_txt.write(new_text.encode('bz2'))
 
1325
        bundle_txt.seek(0)
 
1326
        bundle = read_bundle(bundle_txt)
 
1327
        self.valid_apply_bundle(base_rev_id, bundle)
 
1328
        return bundle
 
1329
 
 
1330
    def create_bundle_text(self, base_rev_id, rev_id):
 
1331
        bundle_txt = StringIO()
 
1332
        rev_ids = write_bundle(self.b1.repository, rev_id, base_rev_id,
 
1333
                               bundle_txt, format=self.format)
 
1334
        bundle_txt.seek(0)
 
1335
        self.assertEqual(bundle_txt.readline(),
 
1336
                         '# Bazaar revision bundle v%s\n' % self.format)
 
1337
        self.assertEqual(bundle_txt.readline(), '#\n')
 
1338
        rev = self.b1.repository.get_revision(rev_id)
 
1339
        bundle_txt.seek(0)
 
1340
        return bundle_txt, rev_ids
 
1341
 
 
1342
    def get_bundle_tree(self, bundle, revision_id):
 
1343
        repository = self.make_repository('repo')
 
1344
        bundle.install_revisions(repository)
 
1345
        return repository.revision_tree(revision_id)
 
1346
 
 
1347
    def test_creation(self):
 
1348
        tree = self.make_branch_and_tree('tree')
 
1349
        self.build_tree_contents([('tree/file', 'contents1\nstatic\n')])
 
1350
        tree.add('file', 'fileid-2')
 
1351
        tree.commit('added file', rev_id='rev1')
 
1352
        self.build_tree_contents([('tree/file', 'contents2\nstatic\n')])
 
1353
        tree.commit('changed file', rev_id='rev2')
 
1354
        s = StringIO()
 
1355
        serializer = BundleSerializerV4('1.0')
 
1356
        serializer.write(tree.branch.repository, ['rev1', 'rev2'], {}, s)
 
1357
        s.seek(0)
 
1358
        tree2 = self.make_branch_and_tree('target')
 
1359
        target_repo = tree2.branch.repository
 
1360
        install_bundle(target_repo, serializer.read(s))
 
1361
        target_repo.lock_read()
 
1362
        self.addCleanup(target_repo.unlock)
 
1363
        # Turn the 'iterators_of_bytes' back into simple strings for comparison
 
1364
        repo_texts = dict((i, ''.join(content)) for i, content
 
1365
                          in target_repo.iter_files_bytes(
 
1366
                                [('fileid-2', 'rev1', '1'),
 
1367
                                 ('fileid-2', 'rev2', '2')]))
 
1368
        self.assertEqual({'1':'contents1\nstatic\n',
 
1369
                          '2':'contents2\nstatic\n'},
 
1370
                         repo_texts)
 
1371
        rtree = target_repo.revision_tree('rev2')
 
1372
        inventory_vf = target_repo.inventories
 
1373
        # If the inventory store has a graph, it must match the revision graph.
 
1374
        self.assertSubset(
 
1375
            [inventory_vf.get_parent_map([('rev2',)])[('rev2',)]],
 
1376
            [None, (('rev1',),)])
 
1377
        self.assertEqual('changed file',
 
1378
                         target_repo.get_revision('rev2').message)
 
1379
 
 
1380
    @staticmethod
 
1381
    def get_raw(bundle_file):
 
1382
        bundle_file.seek(0)
 
1383
        line = bundle_file.readline()
 
1384
        line = bundle_file.readline()
 
1385
        lines = bundle_file.readlines()
 
1386
        return ''.join(lines).decode('bz2')
 
1387
 
 
1388
    def test_copy_signatures(self):
 
1389
        tree_a = self.make_branch_and_tree('tree_a')
 
1390
        import bzrlib.gpg
 
1391
        import bzrlib.commit as commit
 
1392
        oldstrategy = bzrlib.gpg.GPGStrategy
 
1393
        branch = tree_a.branch
 
1394
        repo_a = branch.repository
 
1395
        tree_a.commit("base", allow_pointless=True, rev_id='A')
 
1396
        self.failIf(branch.repository.has_signature_for_revision_id('A'))
 
1397
        try:
 
1398
            from bzrlib.testament import Testament
 
1399
            # monkey patch gpg signing mechanism
 
1400
            bzrlib.gpg.GPGStrategy = bzrlib.gpg.LoopbackGPGStrategy
 
1401
            new_config = test_commit.MustSignConfig(branch)
 
1402
            commit.Commit(config=new_config).commit(message="base",
 
1403
                                                    allow_pointless=True,
 
1404
                                                    rev_id='B',
 
1405
                                                    working_tree=tree_a)
 
1406
            def sign(text):
 
1407
                return bzrlib.gpg.LoopbackGPGStrategy(None).sign(text)
 
1408
            self.assertTrue(repo_a.has_signature_for_revision_id('B'))
 
1409
        finally:
 
1410
            bzrlib.gpg.GPGStrategy = oldstrategy
 
1411
        tree_b = self.make_branch_and_tree('tree_b')
 
1412
        repo_b = tree_b.branch.repository
 
1413
        s = StringIO()
 
1414
        serializer = BundleSerializerV4('4')
 
1415
        serializer.write(tree_a.branch.repository, ['A', 'B'], {}, s)
 
1416
        s.seek(0)
 
1417
        install_bundle(repo_b, serializer.read(s))
 
1418
        self.assertTrue(repo_b.has_signature_for_revision_id('B'))
 
1419
        self.assertEqual(repo_b.get_signature_text('B'),
 
1420
                         repo_a.get_signature_text('B'))
 
1421
        s.seek(0)
 
1422
        # ensure repeat installs are harmless
 
1423
        install_bundle(repo_b, serializer.read(s))
 
1424
 
 
1425
 
 
1426
class V4WeaveBundleTester(V4BundleTester):
 
1427
 
 
1428
    def bzrdir_format(self):
 
1429
        return 'metaweave'
 
1430
 
 
1431
 
 
1432
class MungedBundleTester(object):
1041
1433
 
1042
1434
    def build_test_bundle(self):
1043
1435
        wt = self.make_branch_and_tree('b1')
1052
1444
 
1053
1445
        bundle_txt = StringIO()
1054
1446
        rev_ids = write_bundle(wt.branch.repository, 'a@cset-0-2',
1055
 
                               'a@cset-0-1', bundle_txt)
1056
 
        self.assertEqual(['a@cset-0-2'], rev_ids)
 
1447
                               'a@cset-0-1', bundle_txt, self.format)
 
1448
        self.assertEqual(set(['a@cset-0-2']), set(rev_ids))
1057
1449
        bundle_txt.seek(0, 0)
1058
1450
        return bundle_txt
1059
1451
 
1088
1480
        bundle = read_bundle(bundle_txt)
1089
1481
        self.check_valid(bundle)
1090
1482
 
 
1483
 
 
1484
class MungedBundleTesterV09(tests.TestCaseWithTransport, MungedBundleTester):
 
1485
 
 
1486
    format = '0.9'
 
1487
 
1091
1488
    def test_missing_trailing_whitespace(self):
1092
1489
        bundle_txt = self.build_test_bundle()
1093
1490
 
1121
1518
        bundle = read_bundle(bundle_txt)
1122
1519
        self.check_valid(bundle)
1123
1520
 
 
1521
 
 
1522
class MungedBundleTesterV4(tests.TestCaseWithTransport, MungedBundleTester):
 
1523
 
 
1524
    format = '4'
 
1525
 
 
1526
 
 
1527
class TestBundleWriterReader(tests.TestCase):
 
1528
 
 
1529
    def test_roundtrip_record(self):
 
1530
        fileobj = StringIO()
 
1531
        writer = v4.BundleWriter(fileobj)
 
1532
        writer.begin()
 
1533
        writer.add_info_record(foo='bar')
 
1534
        writer._add_record("Record body", {'parents': ['1', '3'],
 
1535
            'storage_kind':'fulltext'}, 'file', 'revid', 'fileid')
 
1536
        writer.end()
 
1537
        fileobj.seek(0)
 
1538
        reader = v4.BundleReader(fileobj, stream_input=True)
 
1539
        record_iter = reader.iter_records()
 
1540
        record = record_iter.next()
 
1541
        self.assertEqual((None, {'foo': 'bar', 'storage_kind': 'header'},
 
1542
            'info', None, None), record)
 
1543
        record = record_iter.next()
 
1544
        self.assertEqual(("Record body", {'storage_kind': 'fulltext',
 
1545
                          'parents': ['1', '3']}, 'file', 'revid', 'fileid'),
 
1546
                          record)
 
1547
 
 
1548
    def test_roundtrip_record_memory_hungry(self):
 
1549
        fileobj = StringIO()
 
1550
        writer = v4.BundleWriter(fileobj)
 
1551
        writer.begin()
 
1552
        writer.add_info_record(foo='bar')
 
1553
        writer._add_record("Record body", {'parents': ['1', '3'],
 
1554
            'storage_kind':'fulltext'}, 'file', 'revid', 'fileid')
 
1555
        writer.end()
 
1556
        fileobj.seek(0)
 
1557
        reader = v4.BundleReader(fileobj, stream_input=False)
 
1558
        record_iter = reader.iter_records()
 
1559
        record = record_iter.next()
 
1560
        self.assertEqual((None, {'foo': 'bar', 'storage_kind': 'header'},
 
1561
            'info', None, None), record)
 
1562
        record = record_iter.next()
 
1563
        self.assertEqual(("Record body", {'storage_kind': 'fulltext',
 
1564
                          'parents': ['1', '3']}, 'file', 'revid', 'fileid'),
 
1565
                          record)
 
1566
 
 
1567
    def test_encode_name(self):
 
1568
        self.assertEqual('revision/rev1',
 
1569
            v4.BundleWriter.encode_name('revision', 'rev1'))
 
1570
        self.assertEqual('file/rev//1/file-id-1',
 
1571
            v4.BundleWriter.encode_name('file', 'rev/1', 'file-id-1'))
 
1572
        self.assertEqual('info',
 
1573
            v4.BundleWriter.encode_name('info', None, None))
 
1574
 
 
1575
    def test_decode_name(self):
 
1576
        self.assertEqual(('revision', 'rev1', None),
 
1577
            v4.BundleReader.decode_name('revision/rev1'))
 
1578
        self.assertEqual(('file', 'rev/1', 'file-id-1'),
 
1579
            v4.BundleReader.decode_name('file/rev//1/file-id-1'))
 
1580
        self.assertEqual(('info', None, None),
 
1581
                         v4.BundleReader.decode_name('info'))
 
1582
 
 
1583
    def test_too_many_names(self):
 
1584
        fileobj = StringIO()
 
1585
        writer = v4.BundleWriter(fileobj)
 
1586
        writer.begin()
 
1587
        writer.add_info_record(foo='bar')
 
1588
        writer._container.add_bytes_record('blah', ['two', 'names'])
 
1589
        writer.end()
 
1590
        fileobj.seek(0)
 
1591
        record_iter = v4.BundleReader(fileobj).iter_records()
 
1592
        record = record_iter.next()
 
1593
        self.assertEqual((None, {'foo': 'bar', 'storage_kind': 'header'},
 
1594
            'info', None, None), record)
 
1595
        self.assertRaises(errors.BadBundle, record_iter.next)
 
1596
 
 
1597
 
 
1598
class TestReadMergeableFromUrl(tests.TestCaseWithTransport):
 
1599
 
 
1600
    def test_read_mergeable_skips_local(self):
 
1601
        """A local bundle named like the URL should not be read.
 
1602
        """
 
1603
        out, wt = test_read_bundle.create_bundle_file(self)
 
1604
        class FooService(object):
 
1605
            """A directory service that always returns source"""
 
1606
 
 
1607
            def look_up(self, name, url):
 
1608
                return 'source'
 
1609
        directories.register('foo:', FooService, 'Testing directory service')
 
1610
        self.addCleanup(lambda: directories.remove('foo:'))
 
1611
        self.build_tree_contents([('./foo:bar', out.getvalue())])
 
1612
        self.assertRaises(errors.NotABundle, read_mergeable_from_url,
 
1613
                          'foo:bar')
 
1614
 
 
1615
    def test_smart_server_connection_reset(self):
 
1616
        """If a smart server connection fails during the attempt to read a
 
1617
        bundle, then the ConnectionReset error should be propagated.
 
1618
        """
 
1619
        # Instantiate a server that will provoke a ConnectionReset
 
1620
        sock_server = _DisconnectingTCPServer()
 
1621
        sock_server.setUp()
 
1622
        self.addCleanup(sock_server.tearDown)
 
1623
        # We don't really care what the url is since the server will close the
 
1624
        # connection without interpreting it
 
1625
        url = sock_server.get_url()
 
1626
        self.assertRaises(errors.ConnectionReset, read_mergeable_from_url, url)
 
1627
 
 
1628
 
 
1629
class _DisconnectingTCPServer(object):
 
1630
    """A TCP server that immediately closes any connection made to it."""
 
1631
 
 
1632
    def setUp(self):
 
1633
        self.sock = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
 
1634
        self.sock.bind(('127.0.0.1', 0))
 
1635
        self.sock.listen(1)
 
1636
        self.port = self.sock.getsockname()[1]
 
1637
        self.thread = threading.Thread(
 
1638
            name='%s (port %d)' % (self.__class__.__name__, self.port),
 
1639
            target=self.accept_and_close)
 
1640
        self.thread.start()
 
1641
 
 
1642
    def accept_and_close(self):
 
1643
        conn, addr = self.sock.accept()
 
1644
        conn.shutdown(socket.SHUT_RDWR)
 
1645
        conn.close()
 
1646
 
 
1647
    def get_url(self):
 
1648
        return 'bzr://127.0.0.1:%d/' % (self.port,)
 
1649
 
 
1650
    def tearDown(self):
 
1651
        try:
 
1652
            # make sure the thread dies by connecting to the listening socket,
 
1653
            # just in case the test failed to do so.
 
1654
            conn = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
 
1655
            conn.connect(self.sock.getsockname())
 
1656
            conn.close()
 
1657
        except socket.error:
 
1658
            pass
 
1659
        self.sock.close()
 
1660
        self.thread.join()
 
1661