~bzr-pqm/bzr/bzr.dev

« back to all changes in this revision

Viewing changes to bzrlib/tests/test_bundle.py

  • Committer: Robert Collins
  • Date: 2010-05-06 23:54:05 UTC
  • mto: This revision was merged to the branch mainline in revision 5223.
  • Revision ID: robertc@robertcollins.net-20100506235405-wii4elupfhzl3jvy
Add __str__ to the new helper classes.

Show diffs side-by-side

added added

removed removed

Lines of Context:
1
 
# Copyright (C) 2004-2006 by Canonical Ltd
 
1
# Copyright (C) 2005-2010 Canonical Ltd
2
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
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
 
from bzrlib import inventory
23
 
from bzrlib.builtins import _merge_helper
24
 
from bzrlib.bzrdir import BzrDir
 
23
from bzrlib import (
 
24
    bzrdir,
 
25
    diff,
 
26
    errors,
 
27
    inventory,
 
28
    merge,
 
29
    osutils,
 
30
    repository,
 
31
    revision as _mod_revision,
 
32
    tests,
 
33
    treebuilder,
 
34
    )
 
35
from bzrlib.bundle import read_mergeable_from_url
25
36
from bzrlib.bundle.apply_bundle import install_bundle, merge_bundle
26
37
from bzrlib.bundle.bundle_data import BundleTree
27
 
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
 
41
from bzrlib.bundle.serializer.v08 import BundleSerializerV08
 
42
from bzrlib.bundle.serializer.v09 import BundleSerializerV09
 
43
from bzrlib.bundle.serializer.v4 import BundleSerializerV4
28
44
from bzrlib.branch import Branch
29
 
from bzrlib.diff import internal_diff
30
 
from bzrlib.errors import (BzrError, TestamentMismatch, NotABundle, BadBundle, 
31
 
                           NoSuchFile,)
32
 
from bzrlib.merge import Merge3Merger
33
 
from bzrlib.osutils import has_symlinks, sha_file
34
 
from bzrlib.tests import (TestCaseInTempDir, TestCaseWithTransport,
35
 
                          TestCase, TestSkipped)
 
45
from bzrlib.repofmt import knitrepo
 
46
from bzrlib.tests import (
 
47
    test_read_bundle,
 
48
    test_commit,
 
49
    )
36
50
from bzrlib.transform import TreeTransform
37
 
from bzrlib.workingtree import WorkingTree
 
51
 
 
52
 
 
53
def get_text(vf, key):
 
54
    """Get the fulltext for a given revision id that is present in the vf"""
 
55
    stream = vf.get_record_stream([key], 'unordered', True)
 
56
    record = stream.next()
 
57
    return record.get_bytes_as('fulltext')
 
58
 
 
59
 
 
60
def get_inventory_text(repo, revision_id):
 
61
    """Get the fulltext for the inventory at revision id"""
 
62
    repo.lock_read()
 
63
    try:
 
64
        return get_text(repo.inventories, (revision_id,))
 
65
    finally:
 
66
        repo.unlock()
38
67
 
39
68
 
40
69
class MockTree(object):
88
117
        elif kind == 'symlink':
89
118
            ie = InventoryLink(file_id, name, parent_id)
90
119
        else:
91
 
            raise BzrError('unknown kind %r' % kind)
 
120
            raise errors.BzrError('unknown kind %r' % kind)
92
121
        ie.text_sha1 = text_sha_1
93
122
        ie.text_size = text_size
94
123
        return ie
96
125
    def add_dir(self, file_id, path):
97
126
        self.paths[file_id] = path
98
127
        self.ids[path] = file_id
99
 
    
 
128
 
100
129
    def add_file(self, file_id, path, contents):
101
130
        self.add_dir(file_id, path)
102
131
        self.contents[file_id] = contents
119
148
    def contents_stats(self, file_id):
120
149
        if file_id not in self.contents:
121
150
            return None, None
122
 
        text_sha1 = sha_file(self.get_file(file_id))
 
151
        text_sha1 = osutils.sha_file(self.get_file(file_id))
123
152
        return text_sha1, len(self.contents[file_id])
124
153
 
125
154
 
126
 
class BTreeTester(TestCase):
 
155
class BTreeTester(tests.TestCase):
127
156
    """A simple unittest tester for the BundleTree class."""
128
157
 
129
158
    def make_tree_1(self):
133
162
        mtree.add_file("c", "grandparent/parent/file", "Hello\n")
134
163
        mtree.add_dir("d", "grandparent/alt_parent")
135
164
        return BundleTree(mtree, ''), mtree
136
 
        
 
165
 
137
166
    def test_renames(self):
138
167
        """Ensure that file renames have the proper effect on children"""
139
168
        btree = self.make_tree_1()[0]
140
169
        self.assertEqual(btree.old_path("grandparent"), "grandparent")
141
 
        self.assertEqual(btree.old_path("grandparent/parent"), 
 
170
        self.assertEqual(btree.old_path("grandparent/parent"),
142
171
                         "grandparent/parent")
143
172
        self.assertEqual(btree.old_path("grandparent/parent/file"),
144
173
                         "grandparent/parent/file")
151
180
        self.assertEqual(btree.path2id("grandparent/parent"), "b")
152
181
        self.assertEqual(btree.path2id("grandparent/parent/file"), "c")
153
182
 
154
 
        assert btree.path2id("grandparent2") is None
155
 
        assert btree.path2id("grandparent2/parent") is None
156
 
        assert btree.path2id("grandparent2/parent/file") is None
 
183
        self.assertTrue(btree.path2id("grandparent2") is None)
 
184
        self.assertTrue(btree.path2id("grandparent2/parent") is None)
 
185
        self.assertTrue(btree.path2id("grandparent2/parent/file") is None)
157
186
 
158
187
        btree.note_rename("grandparent", "grandparent2")
159
 
        assert btree.old_path("grandparent") is None
160
 
        assert btree.old_path("grandparent/parent") is None
161
 
        assert btree.old_path("grandparent/parent/file") is None
 
188
        self.assertTrue(btree.old_path("grandparent") is None)
 
189
        self.assertTrue(btree.old_path("grandparent/parent") is None)
 
190
        self.assertTrue(btree.old_path("grandparent/parent/file") is None)
162
191
 
163
192
        self.assertEqual(btree.id2path("a"), "grandparent2")
164
193
        self.assertEqual(btree.id2path("b"), "grandparent2/parent")
168
197
        self.assertEqual(btree.path2id("grandparent2/parent"), "b")
169
198
        self.assertEqual(btree.path2id("grandparent2/parent/file"), "c")
170
199
 
171
 
        assert btree.path2id("grandparent") is None
172
 
        assert btree.path2id("grandparent/parent") is None
173
 
        assert btree.path2id("grandparent/parent/file") is None
 
200
        self.assertTrue(btree.path2id("grandparent") is None)
 
201
        self.assertTrue(btree.path2id("grandparent/parent") is None)
 
202
        self.assertTrue(btree.path2id("grandparent/parent/file") is None)
174
203
 
175
204
        btree.note_rename("grandparent/parent", "grandparent2/parent2")
176
205
        self.assertEqual(btree.id2path("a"), "grandparent2")
181
210
        self.assertEqual(btree.path2id("grandparent2/parent2"), "b")
182
211
        self.assertEqual(btree.path2id("grandparent2/parent2/file"), "c")
183
212
 
184
 
        assert btree.path2id("grandparent2/parent") is None
185
 
        assert btree.path2id("grandparent2/parent/file") is None
 
213
        self.assertTrue(btree.path2id("grandparent2/parent") is None)
 
214
        self.assertTrue(btree.path2id("grandparent2/parent/file") is None)
186
215
 
187
 
        btree.note_rename("grandparent/parent/file", 
 
216
        btree.note_rename("grandparent/parent/file",
188
217
                          "grandparent2/parent2/file2")
189
218
        self.assertEqual(btree.id2path("a"), "grandparent2")
190
219
        self.assertEqual(btree.id2path("b"), "grandparent2/parent2")
194
223
        self.assertEqual(btree.path2id("grandparent2/parent2"), "b")
195
224
        self.assertEqual(btree.path2id("grandparent2/parent2/file2"), "c")
196
225
 
197
 
        assert btree.path2id("grandparent2/parent2/file") is None
 
226
        self.assertTrue(btree.path2id("grandparent2/parent2/file") is None)
198
227
 
199
228
    def test_moves(self):
200
229
        """Ensure that file moves have the proper effect on children"""
201
230
        btree = self.make_tree_1()[0]
202
 
        btree.note_rename("grandparent/parent/file", 
 
231
        btree.note_rename("grandparent/parent/file",
203
232
                          "grandparent/alt_parent/file")
204
233
        self.assertEqual(btree.id2path("c"), "grandparent/alt_parent/file")
205
234
        self.assertEqual(btree.path2id("grandparent/alt_parent/file"), "c")
206
 
        assert btree.path2id("grandparent/parent/file") is None
 
235
        self.assertTrue(btree.path2id("grandparent/parent/file") is None)
207
236
 
208
237
    def unified_diff(self, old, new):
209
238
        out = StringIO()
210
 
        internal_diff("old", old, "new", new, out)
 
239
        diff.internal_diff("old", old, "new", new, out)
211
240
        out.seek(0,0)
212
241
        return out.read()
213
242
 
214
243
    def make_tree_2(self):
215
244
        btree = self.make_tree_1()[0]
216
 
        btree.note_rename("grandparent/parent/file", 
 
245
        btree.note_rename("grandparent/parent/file",
217
246
                          "grandparent/alt_parent/file")
218
 
        assert btree.id2path("e") is None
219
 
        assert btree.path2id("grandparent/parent/file") is None
 
247
        self.assertTrue(btree.id2path("e") is None)
 
248
        self.assertTrue(btree.path2id("grandparent/parent/file") is None)
220
249
        btree.note_id("e", "grandparent/parent/file")
221
250
        return btree
222
251
 
248
277
    def make_tree_3(self):
249
278
        btree, mtree = self.make_tree_1()
250
279
        mtree.add_file("e", "grandparent/parent/topping", "Anchovies\n")
251
 
        btree.note_rename("grandparent/parent/file", 
 
280
        btree.note_rename("grandparent/parent/file",
252
281
                          "grandparent/alt_parent/file")
253
 
        btree.note_rename("grandparent/parent/topping", 
 
282
        btree.note_rename("grandparent/parent/topping",
254
283
                          "grandparent/alt_parent/stopping")
255
284
        return btree
256
285
 
280
309
        btree = self.make_tree_1()[0]
281
310
        self.assertEqual(btree.get_file("c").read(), "Hello\n")
282
311
        btree.note_deletion("grandparent/parent/file")
283
 
        assert btree.id2path("c") is None
284
 
        assert btree.path2id("grandparent/parent/file") is None
 
312
        self.assertTrue(btree.id2path("c") is None)
 
313
        self.assertTrue(btree.path2id("grandparent/parent/file") is None)
285
314
 
286
315
    def sorted_ids(self, tree):
287
316
        ids = list(tree)
295
324
            [inventory.ROOT_ID, 'a', 'b', 'c', 'd'])
296
325
        btree.note_deletion("grandparent/parent/file")
297
326
        btree.note_id("e", "grandparent/alt_parent/fool", kind="directory")
298
 
        btree.note_last_changed("grandparent/alt_parent/fool", 
 
327
        btree.note_last_changed("grandparent/alt_parent/fool",
299
328
                                "revisionidiguess")
300
329
        self.assertEqual(self.sorted_ids(btree),
301
330
            [inventory.ROOT_ID, 'a', 'b', 'd', 'e'])
302
331
 
303
332
 
304
 
class BundleTester(TestCaseWithTransport):
 
333
class BundleTester1(tests.TestCaseWithTransport):
 
334
 
 
335
    def test_mismatched_bundle(self):
 
336
        format = bzrdir.BzrDirMetaFormat1()
 
337
        format.repository_format = knitrepo.RepositoryFormatKnit3()
 
338
        serializer = BundleSerializerV08('0.8')
 
339
        b = self.make_branch('.', format=format)
 
340
        self.assertRaises(errors.IncompatibleBundleFormat, serializer.write,
 
341
                          b.repository, [], {}, StringIO())
 
342
 
 
343
    def test_matched_bundle(self):
 
344
        """Don't raise IncompatibleBundleFormat for knit2 and bundle0.9"""
 
345
        format = bzrdir.BzrDirMetaFormat1()
 
346
        format.repository_format = knitrepo.RepositoryFormatKnit3()
 
347
        serializer = BundleSerializerV09('0.9')
 
348
        b = self.make_branch('.', format=format)
 
349
        serializer.write(b.repository, [], {}, StringIO())
 
350
 
 
351
    def test_mismatched_model(self):
 
352
        """Try copying a bundle from knit2 to knit1"""
 
353
        format = bzrdir.BzrDirMetaFormat1()
 
354
        format.repository_format = knitrepo.RepositoryFormatKnit3()
 
355
        source = self.make_branch_and_tree('source', format=format)
 
356
        source.commit('one', rev_id='one-id')
 
357
        source.commit('two', rev_id='two-id')
 
358
        text = StringIO()
 
359
        write_bundle(source.branch.repository, 'two-id', 'null:', text,
 
360
                     format='0.9')
 
361
        text.seek(0)
 
362
 
 
363
        format = bzrdir.BzrDirMetaFormat1()
 
364
        format.repository_format = knitrepo.RepositoryFormatKnit1()
 
365
        target = self.make_branch('target', format=format)
 
366
        self.assertRaises(errors.IncompatibleRevision, install_bundle,
 
367
                          target.repository, read_bundle(text))
 
368
 
 
369
 
 
370
class BundleTester(object):
 
371
 
 
372
    def bzrdir_format(self):
 
373
        format = bzrdir.BzrDirMetaFormat1()
 
374
        format.repository_format = knitrepo.RepositoryFormatKnit1()
 
375
        return format
 
376
 
 
377
    def make_branch_and_tree(self, path, format=None):
 
378
        if format is None:
 
379
            format = self.bzrdir_format()
 
380
        return tests.TestCaseWithTransport.make_branch_and_tree(
 
381
            self, path, format)
 
382
 
 
383
    def make_branch(self, path, format=None):
 
384
        if format is None:
 
385
            format = self.bzrdir_format()
 
386
        return tests.TestCaseWithTransport.make_branch(self, path, format)
305
387
 
306
388
    def create_bundle_text(self, base_rev_id, rev_id):
307
389
        bundle_txt = StringIO()
308
 
        rev_ids = write_bundle(self.b1.repository, rev_id, base_rev_id, 
309
 
                               bundle_txt)
 
390
        rev_ids = write_bundle(self.b1.repository, rev_id, base_rev_id,
 
391
                               bundle_txt, format=self.format)
310
392
        bundle_txt.seek(0)
311
 
        self.assertEqual(bundle_txt.readline(), 
312
 
                         '# Bazaar revision bundle v0.8\n')
 
393
        self.assertEqual(bundle_txt.readline(),
 
394
                         '# Bazaar revision bundle v%s\n' % self.format)
313
395
        self.assertEqual(bundle_txt.readline(), '#\n')
314
396
 
315
397
        rev = self.b1.repository.get_revision(rev_id)
316
398
        self.assertEqual(bundle_txt.readline().decode('utf-8'),
317
399
                         u'# message:\n')
318
 
 
319
 
        open(',,bundle', 'wb').write(bundle_txt.getvalue())
320
400
        bundle_txt.seek(0)
321
401
        return bundle_txt, rev_ids
322
402
 
324
404
        """Create a bundle from base_rev_id -> rev_id in built-in branch.
325
405
        Make sure that the text generated is valid, and that it
326
406
        can be applied against the base, and generate the same information.
327
 
        
328
 
        :return: The in-memory bundle 
 
407
 
 
408
        :return: The in-memory bundle
329
409
        """
330
410
        bundle_txt, rev_ids = self.create_bundle_text(base_rev_id, rev_id)
331
411
 
332
 
        # This should also validate the generated bundle 
 
412
        # This should also validate the generated bundle
333
413
        bundle = read_bundle(bundle_txt)
334
414
        repository = self.b1.repository
335
415
        for bundle_rev in bundle.real_revisions:
339
419
            # it
340
420
            branch_rev = repository.get_revision(bundle_rev.revision_id)
341
421
            for a in ('inventory_sha1', 'revision_id', 'parent_ids',
342
 
                      'timestamp', 'timezone', 'message', 'committer', 
 
422
                      'timestamp', 'timezone', 'message', 'committer',
343
423
                      'parent_ids', 'properties'):
344
 
                self.assertEqual(getattr(branch_rev, a), 
 
424
                self.assertEqual(getattr(branch_rev, a),
345
425
                                 getattr(bundle_rev, a))
346
 
            self.assertEqual(len(branch_rev.parent_ids), 
 
426
            self.assertEqual(len(branch_rev.parent_ids),
347
427
                             len(bundle_rev.parent_ids))
348
 
        self.assertEqual(rev_ids, 
 
428
        self.assertEqual(rev_ids,
349
429
                         [r.revision_id for r in bundle.real_revisions])
350
430
        self.valid_apply_bundle(base_rev_id, bundle,
351
431
                                   checkout_dir=checkout_dir)
355
435
    def get_invalid_bundle(self, base_rev_id, rev_id):
356
436
        """Create a bundle from base_rev_id -> rev_id in built-in branch.
357
437
        Munge the text so that it's invalid.
358
 
        
 
438
 
359
439
        :return: The in-memory bundle
360
440
        """
361
441
        bundle_txt, rev_ids = self.create_bundle_text(base_rev_id, rev_id)
362
 
        new_text = bundle_txt.getvalue().replace('executable:no', 
 
442
        new_text = bundle_txt.getvalue().replace('executable:no',
363
443
                                               'executable:yes')
364
444
        bundle_txt = StringIO(new_text)
365
445
        bundle = read_bundle(bundle_txt)
366
446
        self.valid_apply_bundle(base_rev_id, bundle)
367
 
        return bundle 
 
447
        return bundle
368
448
 
369
449
    def test_non_bundle(self):
370
 
        self.assertRaises(NotABundle, read_bundle, StringIO('#!/bin/sh\n'))
 
450
        self.assertRaises(errors.NotABundle,
 
451
                          read_bundle, StringIO('#!/bin/sh\n'))
371
452
 
372
453
    def test_malformed(self):
373
 
        self.assertRaises(BadBundle, read_bundle, 
 
454
        self.assertRaises(errors.BadBundle, read_bundle,
374
455
                          StringIO('# Bazaar revision bundle v'))
375
456
 
376
457
    def test_crlf_bundle(self):
377
458
        try:
378
459
            read_bundle(StringIO('# Bazaar revision bundle v0.8\r\n'))
379
 
        except BadBundle:
 
460
        except errors.BadBundle:
380
461
            # It is currently permitted for bundles with crlf line endings to
381
462
            # make read_bundle raise a BadBundle, but this should be fixed.
382
463
            # Anything else, especially NotABundle, is an error.
387
468
        """
388
469
 
389
470
        if checkout_dir is None:
390
 
            checkout_dir = tempfile.mkdtemp(prefix='test-branch-', dir='.')
 
471
            checkout_dir = osutils.mkdtemp(prefix='test-branch-', dir='.')
391
472
        else:
392
473
            if not os.path.exists(checkout_dir):
393
474
                os.mkdir(checkout_dir)
394
 
        tree = BzrDir.create_standalone_workingtree(checkout_dir)
 
475
        tree = self.make_branch_and_tree(checkout_dir)
395
476
        s = StringIO()
396
 
        ancestors = write_bundle(self.b1.repository, rev_id, None, s)
 
477
        ancestors = write_bundle(self.b1.repository, rev_id, 'null:', s,
 
478
                                 format=self.format)
397
479
        s.seek(0)
398
 
        assert isinstance(s.getvalue(), str), (
399
 
            "Bundle isn't a bytestring:\n %s..." % repr(s.getvalue())[:40])
 
480
        self.assertIsInstance(s.getvalue(), str)
400
481
        install_bundle(tree.branch.repository, read_bundle(s))
401
482
        for ancestor in ancestors:
402
483
            old = self.b1.repository.revision_tree(ancestor)
403
484
            new = tree.branch.repository.revision_tree(ancestor)
404
 
 
405
 
            # Check that there aren't any inventory level changes
406
 
            delta = new.changes_from(old)
407
 
            self.assertFalse(delta.has_changed(),
408
 
                             'Revision %s not copied correctly.'
409
 
                             % (ancestor,))
410
 
 
411
 
            # Now check that the file contents are all correct
412
 
            for inventory_id in old:
413
 
                try:
414
 
                    old_file = old.get_file(inventory_id)
415
 
                except NoSuchFile:
416
 
                    continue
417
 
                if old_file is None:
418
 
                    continue
419
 
                self.assertEqual(old_file.read(),
420
 
                                 new.get_file(inventory_id).read())
421
 
        if rev_id is not None:
 
485
            old.lock_read()
 
486
            new.lock_read()
 
487
            try:
 
488
                # Check that there aren't any inventory level changes
 
489
                delta = new.changes_from(old)
 
490
                self.assertFalse(delta.has_changed(),
 
491
                                 'Revision %s not copied correctly.'
 
492
                                 % (ancestor,))
 
493
 
 
494
                # Now check that the file contents are all correct
 
495
                for inventory_id in old:
 
496
                    try:
 
497
                        old_file = old.get_file(inventory_id)
 
498
                    except errors.NoSuchFile:
 
499
                        continue
 
500
                    if old_file is None:
 
501
                        continue
 
502
                    self.assertEqual(old_file.read(),
 
503
                                     new.get_file(inventory_id).read())
 
504
            finally:
 
505
                new.unlock()
 
506
                old.unlock()
 
507
        if not _mod_revision.is_null(rev_id):
422
508
            rh = self.b1.revision_history()
423
509
            tree.branch.set_revision_history(rh[:rh.index(rev_id)+1])
424
510
            tree.update()
425
511
            delta = tree.changes_from(self.b1.repository.revision_tree(rev_id))
426
512
            self.assertFalse(delta.has_changed(),
427
 
                             'Working tree has modifications')
 
513
                             'Working tree has modifications: %s' % delta)
428
514
        return tree
429
515
 
430
516
    def valid_apply_bundle(self, base_rev_id, info, checkout_dir=None):
432
518
        sure everything matches the builtin branch.
433
519
        """
434
520
        to_tree = self.get_checkout(base_rev_id, checkout_dir=checkout_dir)
 
521
        to_tree.lock_write()
 
522
        try:
 
523
            self._valid_apply_bundle(base_rev_id, info, to_tree)
 
524
        finally:
 
525
            to_tree.unlock()
 
526
 
 
527
    def _valid_apply_bundle(self, base_rev_id, info, to_tree):
435
528
        original_parents = to_tree.get_parent_ids()
436
529
        repository = to_tree.branch.repository
437
530
        original_parents = to_tree.get_parent_ids()
438
531
        self.assertIs(repository.has_revision(base_rev_id), True)
439
532
        for rev in info.real_revisions:
440
533
            self.assert_(not repository.has_revision(rev.revision_id),
441
 
                'Revision {%s} present before applying bundle' 
 
534
                'Revision {%s} present before applying bundle'
442
535
                % rev.revision_id)
443
 
        merge_bundle(info, to_tree, True, Merge3Merger, False, False)
 
536
        merge_bundle(info, to_tree, True, merge.Merge3Merger, False, False)
444
537
 
445
538
        for rev in info.real_revisions:
446
539
            self.assert_(repository.has_revision(rev.revision_id),
447
 
                'Missing revision {%s} after applying bundle' 
 
540
                'Missing revision {%s} after applying bundle'
448
541
                % rev.revision_id)
449
542
 
450
543
        self.assert_(to_tree.branch.repository.has_revision(info.target))
456
549
        rev = info.real_revisions[-1]
457
550
        base_tree = self.b1.repository.revision_tree(rev.revision_id)
458
551
        to_tree = to_tree.branch.repository.revision_tree(rev.revision_id)
459
 
        
 
552
 
460
553
        # TODO: make sure the target tree is identical to base tree
461
554
        #       we might also check the working tree.
462
555
 
481
574
        self.tree1 = self.make_branch_and_tree('b1')
482
575
        self.b1 = self.tree1.branch
483
576
 
484
 
        open('b1/one', 'wb').write('one\n')
485
 
        self.tree1.add('one')
 
577
        self.build_tree_contents([('b1/one', 'one\n')])
 
578
        self.tree1.add('one', 'one-id')
 
579
        self.tree1.set_root_id('root-id')
486
580
        self.tree1.commit('add one', rev_id='a@cset-0-1')
487
581
 
488
 
        bundle = self.get_valid_bundle(None, 'a@cset-0-1')
489
 
        # FIXME: The current write_bundle api no longer supports
490
 
        #        setting a custom summary message
491
 
        #        We should re-introduce the ability, and update
492
 
        #        the tests to make sure it works.
493
 
        # bundle = self.get_valid_bundle(None, 'a@cset-0-1',
494
 
        #         message='With a specialized message')
 
582
        bundle = self.get_valid_bundle('null:', 'a@cset-0-1')
495
583
 
496
584
        # Make sure we can handle files with spaces, tabs, other
497
585
        # bogus characters
505
593
                , 'b1/sub/sub/'
506
594
                , 'b1/sub/sub/nonempty.txt'
507
595
                ])
508
 
        open('b1/sub/sub/emptyfile.txt', 'wb').close()
509
 
        open('b1/dir/nolastnewline.txt', 'wb').write('bloop')
 
596
        self.build_tree_contents([('b1/sub/sub/emptyfile.txt', ''),
 
597
                                  ('b1/dir/nolastnewline.txt', 'bloop')])
510
598
        tt = TreeTransform(self.tree1)
511
599
        tt.new_file('executable', tt.root, '#!/bin/sh\n', 'exe-1', True)
512
600
        tt.apply()
 
601
        # have to fix length of file-id so that we can predictably rewrite
 
602
        # a (length-prefixed) record containing it later.
 
603
        self.tree1.add('with space.txt', 'withspace-id')
513
604
        self.tree1.add([
514
 
                'with space.txt'
515
 
                , 'dir'
 
605
                  'dir'
516
606
                , 'dir/filein subdir.c'
517
607
                , 'dir/WithCaps.txt'
518
608
                , 'dir/ pre space'
526
616
 
527
617
        bundle = self.get_valid_bundle('a@cset-0-1', 'a@cset-0-2')
528
618
 
529
 
        # Check a rollup bundle 
530
 
        bundle = self.get_valid_bundle(None, 'a@cset-0-2')
 
619
        # Check a rollup bundle
 
620
        bundle = self.get_valid_bundle('null:', 'a@cset-0-2')
531
621
 
532
622
        # Now delete entries
533
623
        self.tree1.remove(
540
630
        tt.set_executability(False, trans_id)
541
631
        tt.apply()
542
632
        self.tree1.commit('removed', rev_id='a@cset-0-3')
543
 
        
 
633
 
544
634
        bundle = self.get_valid_bundle('a@cset-0-2', 'a@cset-0-3')
545
 
        self.assertRaises(TestamentMismatch, self.get_invalid_bundle, 
546
 
                          'a@cset-0-2', 'a@cset-0-3')
547
 
        # Check a rollup bundle 
548
 
        bundle = self.get_valid_bundle(None, 'a@cset-0-3')
 
635
        self.assertRaises((errors.TestamentMismatch,
 
636
            errors.VersionedFileInvalidChecksum,
 
637
            errors.BadBundle), self.get_invalid_bundle,
 
638
            'a@cset-0-2', 'a@cset-0-3')
 
639
        # Check a rollup bundle
 
640
        bundle = self.get_valid_bundle('null:', 'a@cset-0-3')
549
641
 
550
642
        # Now move the directory
551
643
        self.tree1.rename_one('dir', 'sub/dir')
552
644
        self.tree1.commit('rename dir', rev_id='a@cset-0-4')
553
645
 
554
646
        bundle = self.get_valid_bundle('a@cset-0-3', 'a@cset-0-4')
555
 
        # Check a rollup bundle 
556
 
        bundle = self.get_valid_bundle(None, 'a@cset-0-4')
 
647
        # Check a rollup bundle
 
648
        bundle = self.get_valid_bundle('null:', 'a@cset-0-4')
557
649
 
558
650
        # Modified files
559
651
        open('b1/sub/dir/WithCaps.txt', 'ab').write('\nAdding some text\n')
560
 
        open('b1/sub/dir/ pre space', 'ab').write('\r\nAdding some\r\nDOS format lines\r\n')
 
652
        open('b1/sub/dir/ pre space', 'ab').write(
 
653
             '\r\nAdding some\r\nDOS format lines\r\n')
561
654
        open('b1/sub/dir/nolastnewline.txt', 'ab').write('\n')
562
 
        self.tree1.rename_one('sub/dir/ pre space', 
 
655
        self.tree1.rename_one('sub/dir/ pre space',
563
656
                              'sub/ start space')
564
657
        self.tree1.commit('Modified files', rev_id='a@cset-0-5')
565
658
        bundle = self.get_valid_bundle('a@cset-0-4', 'a@cset-0-5')
571
664
                          verbose=False)
572
665
        bundle = self.get_valid_bundle('a@cset-0-5', 'a@cset-0-6')
573
666
        other = self.get_checkout('a@cset-0-5')
 
667
        tree1_inv = get_inventory_text(self.tree1.branch.repository,
 
668
                                       'a@cset-0-5')
 
669
        tree2_inv = get_inventory_text(other.branch.repository,
 
670
                                       'a@cset-0-5')
 
671
        self.assertEqualDiff(tree1_inv, tree2_inv)
574
672
        other.rename_one('sub/dir/nolastnewline.txt', 'sub/nolastnewline.txt')
575
673
        other.commit('rename file', rev_id='a@cset-0-6b')
576
 
        _merge_helper([other.basedir, -1], [None, None],
577
 
                      this_dir=self.tree1.basedir)
 
674
        self.tree1.merge_from_branch(other.branch)
578
675
        self.tree1.commit(u'Merge', rev_id='a@cset-0-7',
579
676
                          verbose=False)
580
677
        bundle = self.get_valid_bundle('a@cset-0-6', 'a@cset-0-7')
581
678
 
582
 
    def test_symlink_bundle(self):
583
 
        if not has_symlinks():
584
 
            raise TestSkipped("No symlink support")
585
 
        self.tree1 = BzrDir.create_standalone_workingtree('b1')
 
679
    def _test_symlink_bundle(self, link_name, link_target, new_link_target):
 
680
        link_id = 'link-1'
 
681
 
 
682
        self.requireFeature(tests.SymlinkFeature)
 
683
        self.tree1 = self.make_branch_and_tree('b1')
586
684
        self.b1 = self.tree1.branch
 
685
 
587
686
        tt = TreeTransform(self.tree1)
588
 
        tt.new_symlink('link', tt.root, 'bar/foo', 'link-1')
 
687
        tt.new_symlink(link_name, tt.root, link_target, link_id)
589
688
        tt.apply()
590
689
        self.tree1.commit('add symlink', rev_id='l@cset-0-1')
591
 
        self.get_valid_bundle(None, 'l@cset-0-1')
 
690
        bundle = self.get_valid_bundle('null:', 'l@cset-0-1')
 
691
        if getattr(bundle ,'revision_tree', None) is not None:
 
692
            # Not all bundle formats supports revision_tree
 
693
            bund_tree = bundle.revision_tree(self.b1.repository, 'l@cset-0-1')
 
694
            self.assertEqual(link_target, bund_tree.get_symlink_target(link_id))
 
695
 
592
696
        tt = TreeTransform(self.tree1)
593
 
        trans_id = tt.trans_id_tree_file_id('link-1')
 
697
        trans_id = tt.trans_id_tree_file_id(link_id)
594
698
        tt.adjust_path('link2', tt.root, trans_id)
595
699
        tt.delete_contents(trans_id)
596
 
        tt.create_symlink('mars', trans_id)
 
700
        tt.create_symlink(new_link_target, trans_id)
597
701
        tt.apply()
598
702
        self.tree1.commit('rename and change symlink', rev_id='l@cset-0-2')
599
 
        self.get_valid_bundle('l@cset-0-1', 'l@cset-0-2')
 
703
        bundle = self.get_valid_bundle('l@cset-0-1', 'l@cset-0-2')
 
704
        if getattr(bundle ,'revision_tree', None) is not None:
 
705
            # Not all bundle formats supports revision_tree
 
706
            bund_tree = bundle.revision_tree(self.b1.repository, 'l@cset-0-2')
 
707
            self.assertEqual(new_link_target,
 
708
                             bund_tree.get_symlink_target(link_id))
 
709
 
600
710
        tt = TreeTransform(self.tree1)
601
 
        trans_id = tt.trans_id_tree_file_id('link-1')
 
711
        trans_id = tt.trans_id_tree_file_id(link_id)
602
712
        tt.delete_contents(trans_id)
603
713
        tt.create_symlink('jupiter', trans_id)
604
714
        tt.apply()
605
715
        self.tree1.commit('just change symlink target', rev_id='l@cset-0-3')
606
 
        self.get_valid_bundle('l@cset-0-2', 'l@cset-0-3')
 
716
        bundle = self.get_valid_bundle('l@cset-0-2', 'l@cset-0-3')
 
717
 
607
718
        tt = TreeTransform(self.tree1)
608
 
        trans_id = tt.trans_id_tree_file_id('link-1')
 
719
        trans_id = tt.trans_id_tree_file_id(link_id)
609
720
        tt.delete_contents(trans_id)
610
721
        tt.apply()
611
722
        self.tree1.commit('Delete symlink', rev_id='l@cset-0-4')
612
 
        self.get_valid_bundle('l@cset-0-3', 'l@cset-0-4')
 
723
        bundle = self.get_valid_bundle('l@cset-0-3', 'l@cset-0-4')
 
724
 
 
725
    def test_symlink_bundle(self):
 
726
        self._test_symlink_bundle('link', 'bar/foo', 'mars')
 
727
 
 
728
    def test_unicode_symlink_bundle(self):
 
729
        self.requireFeature(tests.UnicodeFilenameFeature)
 
730
        self._test_symlink_bundle(u'\N{Euro Sign}link',
 
731
                                  u'bar/\N{Euro Sign}foo',
 
732
                                  u'mars\N{Euro Sign}')
613
733
 
614
734
    def test_binary_bundle(self):
615
 
        self.tree1 = BzrDir.create_standalone_workingtree('b1')
 
735
        self.tree1 = self.make_branch_and_tree('b1')
616
736
        self.b1 = self.tree1.branch
617
737
        tt = TreeTransform(self.tree1)
618
 
        
 
738
 
619
739
        # Add
620
740
        tt.new_file('file', tt.root, '\x00\n\x00\r\x01\n\x02\r\xff', 'binary-1')
621
 
        tt.new_file('file2', tt.root, '\x01\n\x02\r\x03\n\x04\r\xff', 'binary-2')
 
741
        tt.new_file('file2', tt.root, '\x01\n\x02\r\x03\n\x04\r\xff',
 
742
            'binary-2')
622
743
        tt.apply()
623
744
        self.tree1.commit('add binary', rev_id='b@cset-0-1')
624
 
        self.get_valid_bundle(None, 'b@cset-0-1')
 
745
        self.get_valid_bundle('null:', 'b@cset-0-1')
625
746
 
626
747
        # Delete
627
748
        tt = TreeTransform(self.tree1)
651
772
        self.get_valid_bundle('b@cset-0-3', 'b@cset-0-4')
652
773
 
653
774
        # Rollup
654
 
        self.get_valid_bundle(None, 'b@cset-0-4')
 
775
        self.get_valid_bundle('null:', 'b@cset-0-4')
655
776
 
656
777
    def test_last_modified(self):
657
 
        self.tree1 = BzrDir.create_standalone_workingtree('b1')
 
778
        self.tree1 = self.make_branch_and_tree('b1')
658
779
        self.b1 = self.tree1.branch
659
780
        tt = TreeTransform(self.tree1)
660
781
        tt.new_file('file', tt.root, 'file', 'file')
675
796
        tt.create_file('file2', trans_id)
676
797
        tt.apply()
677
798
        other.commit('modify text in another tree', rev_id='a@lmod-0-2b')
678
 
        _merge_helper([other.basedir, -1], [None, None],
679
 
                      this_dir=self.tree1.basedir)
 
799
        self.tree1.merge_from_branch(other.branch)
680
800
        self.tree1.commit(u'Merge', rev_id='a@lmod-0-3',
681
801
                          verbose=False)
682
802
        self.tree1.commit(u'Merge', rev_id='a@lmod-0-4')
683
803
        bundle = self.get_valid_bundle('a@lmod-0-2a', 'a@lmod-0-4')
684
804
 
685
805
    def test_hide_history(self):
686
 
        self.tree1 = BzrDir.create_standalone_workingtree('b1')
 
806
        self.tree1 = self.make_branch_and_tree('b1')
687
807
        self.b1 = self.tree1.branch
688
808
 
689
809
        open('b1/one', 'wb').write('one\n')
695
815
        self.tree1.commit('modify', rev_id='a@cset-0-3')
696
816
        bundle_file = StringIO()
697
817
        rev_ids = write_bundle(self.tree1.branch.repository, 'a@cset-0-3',
 
818
                               'a@cset-0-1', bundle_file, format=self.format)
 
819
        self.assertNotContainsRe(bundle_file.getvalue(), '\btwo\b')
 
820
        self.assertContainsRe(self.get_raw(bundle_file), 'one')
 
821
        self.assertContainsRe(self.get_raw(bundle_file), 'three')
 
822
 
 
823
    def test_bundle_same_basis(self):
 
824
        """Ensure using the basis as the target doesn't cause an error"""
 
825
        self.tree1 = self.make_branch_and_tree('b1')
 
826
        self.tree1.commit('add file', rev_id='a@cset-0-1')
 
827
        bundle_file = StringIO()
 
828
        rev_ids = write_bundle(self.tree1.branch.repository, 'a@cset-0-1',
698
829
                               'a@cset-0-1', bundle_file)
699
 
        self.assertNotContainsRe(bundle_file.getvalue(), 'two')
700
 
        self.assertContainsRe(bundle_file.getvalue(), 'one')
701
 
        self.assertContainsRe(bundle_file.getvalue(), 'three')
 
830
 
 
831
    @staticmethod
 
832
    def get_raw(bundle_file):
 
833
        return bundle_file.getvalue()
702
834
 
703
835
    def test_unicode_bundle(self):
 
836
        self.requireFeature(tests.UnicodeFilenameFeature)
704
837
        # Handle international characters
705
838
        os.mkdir('b1')
706
 
        try:
707
 
            f = open(u'b1/with Dod\xe9', 'wb')
708
 
        except UnicodeEncodeError:
709
 
            raise TestSkipped("Filesystem doesn't support unicode")
 
839
        f = open(u'b1/with Dod\N{Euro Sign}', 'wb')
710
840
 
711
841
        self.tree1 = self.make_branch_and_tree('b1')
712
842
        self.b1 = self.tree1.branch
716
846
            u'William Dod\xe9\n').encode('utf-8'))
717
847
        f.close()
718
848
 
719
 
        self.tree1.add([u'with Dod\xe9'])
720
 
        self.tree1.commit(u'i18n commit from William Dod\xe9', 
 
849
        self.tree1.add([u'with Dod\N{Euro Sign}'], ['withdod-id'])
 
850
        self.tree1.commit(u'i18n commit from William Dod\xe9',
721
851
                          rev_id='i18n-1', committer=u'William Dod\xe9')
722
852
 
723
853
        # Add
724
 
        bundle = self.get_valid_bundle(None, 'i18n-1')
 
854
        bundle = self.get_valid_bundle('null:', 'i18n-1')
725
855
 
726
856
        # Modified
727
 
        f = open(u'b1/with Dod\xe9', 'wb')
 
857
        f = open(u'b1/with Dod\N{Euro Sign}', 'wb')
728
858
        f.write(u'Modified \xb5\n'.encode('utf8'))
729
859
        f.close()
730
860
        self.tree1.commit(u'modified', rev_id='i18n-2')
731
861
 
732
862
        bundle = self.get_valid_bundle('i18n-1', 'i18n-2')
733
 
        
 
863
 
734
864
        # Renamed
735
 
        self.tree1.rename_one(u'with Dod\xe9', u'B\xe5gfors')
 
865
        self.tree1.rename_one(u'with Dod\N{Euro Sign}', u'B\N{Euro Sign}gfors')
736
866
        self.tree1.commit(u'renamed, the new i18n man', rev_id='i18n-3',
737
867
                          committer=u'Erik B\xe5gfors')
738
868
 
739
869
        bundle = self.get_valid_bundle('i18n-2', 'i18n-3')
740
870
 
741
871
        # Removed
742
 
        self.tree1.remove([u'B\xe5gfors'])
 
872
        self.tree1.remove([u'B\N{Euro Sign}gfors'])
743
873
        self.tree1.commit(u'removed', rev_id='i18n-4')
744
874
 
745
875
        bundle = self.get_valid_bundle('i18n-3', 'i18n-4')
746
876
 
747
877
        # Rollup
748
 
        bundle = self.get_valid_bundle(None, 'i18n-4')
 
878
        bundle = self.get_valid_bundle('null:', 'i18n-4')
749
879
 
750
880
 
751
881
    def test_whitespace_bundle(self):
752
882
        if sys.platform in ('win32', 'cygwin'):
753
 
            raise TestSkipped('Windows doesn\'t support filenames'
754
 
                              ' with tabs or trailing spaces')
 
883
            raise tests.TestSkipped('Windows doesn\'t support filenames'
 
884
                                    ' with tabs or trailing spaces')
755
885
        self.tree1 = self.make_branch_and_tree('b1')
756
886
        self.b1 = self.tree1.branch
757
887
 
763
893
        # Added
764
894
        self.tree1.commit('funky whitespace', rev_id='white-1')
765
895
 
766
 
        bundle = self.get_valid_bundle(None, 'white-1')
 
896
        bundle = self.get_valid_bundle('null:', 'white-1')
767
897
 
768
898
        # Modified
769
899
        open('b1/trailing space ', 'ab').write('add some text\n')
782
912
        self.tree1.commit('removed', rev_id='white-4')
783
913
 
784
914
        bundle = self.get_valid_bundle('white-3', 'white-4')
785
 
        
 
915
 
786
916
        # Now test a complet roll-up
787
 
        bundle = self.get_valid_bundle(None, 'white-4')
 
917
        bundle = self.get_valid_bundle('null:', 'white-4')
788
918
 
789
919
    def test_alt_timezone_bundle(self):
790
 
        self.tree1 = self.make_branch_and_tree('b1')
 
920
        self.tree1 = self.make_branch_and_memory_tree('b1')
791
921
        self.b1 = self.tree1.branch
 
922
        builder = treebuilder.TreeBuilder()
792
923
 
793
 
        self.build_tree(['b1/newfile'])
794
 
        self.tree1.add(['newfile'])
 
924
        self.tree1.lock_write()
 
925
        builder.start_tree(self.tree1)
 
926
        builder.build(['newfile'])
 
927
        builder.finish_tree()
795
928
 
796
929
        # Asia/Colombo offset = 5 hours 30 minutes
797
930
        self.tree1.commit('non-hour offset timezone', rev_id='tz-1',
798
931
                          timezone=19800, timestamp=1152544886.0)
799
932
 
800
 
        bundle = self.get_valid_bundle(None, 'tz-1')
801
 
        
 
933
        bundle = self.get_valid_bundle('null:', 'tz-1')
 
934
 
802
935
        rev = bundle.revisions[0]
803
936
        self.assertEqual('Mon 2006-07-10 20:51:26.000000000 +0530', rev.date)
804
937
        self.assertEqual(19800, rev.timezone)
805
938
        self.assertEqual(1152544886.0, rev.timestamp)
 
939
        self.tree1.unlock()
806
940
 
807
941
    def test_bundle_root_id(self):
808
942
        self.tree1 = self.make_branch_and_tree('b1')
809
943
        self.b1 = self.tree1.branch
810
944
        self.tree1.commit('message', rev_id='revid1')
811
 
        bundle = self.get_valid_bundle(None, 'revid1')
812
 
        tree = bundle.revision_tree(self.b1.repository, 'revid1')
 
945
        bundle = self.get_valid_bundle('null:', 'revid1')
 
946
        tree = self.get_bundle_tree(bundle, 'revid1')
813
947
        self.assertEqual('revid1', tree.inventory.root.revision)
814
948
 
815
 
 
816
 
class MungedBundleTester(TestCaseWithTransport):
 
949
    def test_install_revisions(self):
 
950
        self.tree1 = self.make_branch_and_tree('b1')
 
951
        self.b1 = self.tree1.branch
 
952
        self.tree1.commit('message', rev_id='rev2a')
 
953
        bundle = self.get_valid_bundle('null:', 'rev2a')
 
954
        branch2 = self.make_branch('b2')
 
955
        self.assertFalse(branch2.repository.has_revision('rev2a'))
 
956
        target_revision = bundle.install_revisions(branch2.repository)
 
957
        self.assertTrue(branch2.repository.has_revision('rev2a'))
 
958
        self.assertEqual('rev2a', target_revision)
 
959
 
 
960
    def test_bundle_empty_property(self):
 
961
        """Test serializing revision properties with an empty value."""
 
962
        tree = self.make_branch_and_memory_tree('tree')
 
963
        tree.lock_write()
 
964
        self.addCleanup(tree.unlock)
 
965
        tree.add([''], ['TREE_ROOT'])
 
966
        tree.commit('One', revprops={'one':'two', 'empty':''}, rev_id='rev1')
 
967
        self.b1 = tree.branch
 
968
        bundle_sio, revision_ids = self.create_bundle_text('null:', 'rev1')
 
969
        bundle = read_bundle(bundle_sio)
 
970
        revision_info = bundle.revisions[0]
 
971
        self.assertEqual('rev1', revision_info.revision_id)
 
972
        rev = revision_info.as_revision()
 
973
        self.assertEqual({'branch-nick':'tree', 'empty':'', 'one':'two'},
 
974
                         rev.properties)
 
975
 
 
976
    def test_bundle_sorted_properties(self):
 
977
        """For stability the writer should write properties in sorted order."""
 
978
        tree = self.make_branch_and_memory_tree('tree')
 
979
        tree.lock_write()
 
980
        self.addCleanup(tree.unlock)
 
981
 
 
982
        tree.add([''], ['TREE_ROOT'])
 
983
        tree.commit('One', rev_id='rev1',
 
984
                    revprops={'a':'4', 'b':'3', 'c':'2', 'd':'1'})
 
985
        self.b1 = tree.branch
 
986
        bundle_sio, revision_ids = self.create_bundle_text('null:', 'rev1')
 
987
        bundle = read_bundle(bundle_sio)
 
988
        revision_info = bundle.revisions[0]
 
989
        self.assertEqual('rev1', revision_info.revision_id)
 
990
        rev = revision_info.as_revision()
 
991
        self.assertEqual({'branch-nick':'tree', 'a':'4', 'b':'3', 'c':'2',
 
992
                          'd':'1'}, rev.properties)
 
993
 
 
994
    def test_bundle_unicode_properties(self):
 
995
        """We should be able to round trip a non-ascii property."""
 
996
        tree = self.make_branch_and_memory_tree('tree')
 
997
        tree.lock_write()
 
998
        self.addCleanup(tree.unlock)
 
999
 
 
1000
        tree.add([''], ['TREE_ROOT'])
 
1001
        # Revisions themselves do not require anything about revision property
 
1002
        # keys, other than that they are a basestring, and do not contain
 
1003
        # whitespace.
 
1004
        # However, Testaments assert than they are str(), and thus should not
 
1005
        # be Unicode.
 
1006
        tree.commit('One', rev_id='rev1',
 
1007
                    revprops={'omega':u'\u03a9', 'alpha':u'\u03b1'})
 
1008
        self.b1 = tree.branch
 
1009
        bundle_sio, revision_ids = self.create_bundle_text('null:', 'rev1')
 
1010
        bundle = read_bundle(bundle_sio)
 
1011
        revision_info = bundle.revisions[0]
 
1012
        self.assertEqual('rev1', revision_info.revision_id)
 
1013
        rev = revision_info.as_revision()
 
1014
        self.assertEqual({'branch-nick':'tree', 'omega':u'\u03a9',
 
1015
                          'alpha':u'\u03b1'}, rev.properties)
 
1016
 
 
1017
    def test_bundle_with_ghosts(self):
 
1018
        tree = self.make_branch_and_tree('tree')
 
1019
        self.b1 = tree.branch
 
1020
        self.build_tree_contents([('tree/file', 'content1')])
 
1021
        tree.add(['file'])
 
1022
        tree.commit('rev1')
 
1023
        self.build_tree_contents([('tree/file', 'content2')])
 
1024
        tree.add_parent_tree_id('ghost')
 
1025
        tree.commit('rev2', rev_id='rev2')
 
1026
        bundle = self.get_valid_bundle('null:', 'rev2')
 
1027
 
 
1028
    def make_simple_tree(self, format=None):
 
1029
        tree = self.make_branch_and_tree('b1', format=format)
 
1030
        self.b1 = tree.branch
 
1031
        self.build_tree(['b1/file'])
 
1032
        tree.add('file')
 
1033
        return tree
 
1034
 
 
1035
    def test_across_serializers(self):
 
1036
        tree = self.make_simple_tree('knit')
 
1037
        tree.commit('hello', rev_id='rev1')
 
1038
        tree.commit('hello', rev_id='rev2')
 
1039
        bundle = read_bundle(self.create_bundle_text('null:', 'rev2')[0])
 
1040
        repo = self.make_repository('repo', format='dirstate-with-subtree')
 
1041
        bundle.install_revisions(repo)
 
1042
        inv_text = repo._get_inventory_xml('rev2')
 
1043
        self.assertNotContainsRe(inv_text, 'format="5"')
 
1044
        self.assertContainsRe(inv_text, 'format="7"')
 
1045
 
 
1046
    def make_repo_with_installed_revisions(self):
 
1047
        tree = self.make_simple_tree('knit')
 
1048
        tree.commit('hello', rev_id='rev1')
 
1049
        tree.commit('hello', rev_id='rev2')
 
1050
        bundle = read_bundle(self.create_bundle_text('null:', 'rev2')[0])
 
1051
        repo = self.make_repository('repo', format='dirstate-with-subtree')
 
1052
        bundle.install_revisions(repo)
 
1053
        return repo
 
1054
 
 
1055
    def test_across_models(self):
 
1056
        repo = self.make_repo_with_installed_revisions()
 
1057
        inv = repo.get_inventory('rev2')
 
1058
        self.assertEqual('rev2', inv.root.revision)
 
1059
        root_id = inv.root.file_id
 
1060
        repo.lock_read()
 
1061
        self.addCleanup(repo.unlock)
 
1062
        self.assertEqual({(root_id, 'rev1'):(),
 
1063
            (root_id, 'rev2'):((root_id, 'rev1'),)},
 
1064
            repo.texts.get_parent_map([(root_id, 'rev1'), (root_id, 'rev2')]))
 
1065
 
 
1066
    def test_inv_hash_across_serializers(self):
 
1067
        repo = self.make_repo_with_installed_revisions()
 
1068
        recorded_inv_sha1 = repo.get_revision('rev2').inventory_sha1
 
1069
        xml = repo._get_inventory_xml('rev2')
 
1070
        self.assertEqual(osutils.sha_string(xml), recorded_inv_sha1)
 
1071
 
 
1072
    def test_across_models_incompatible(self):
 
1073
        tree = self.make_simple_tree('dirstate-with-subtree')
 
1074
        tree.commit('hello', rev_id='rev1')
 
1075
        tree.commit('hello', rev_id='rev2')
 
1076
        try:
 
1077
            bundle = read_bundle(self.create_bundle_text('null:', 'rev1')[0])
 
1078
        except errors.IncompatibleBundleFormat:
 
1079
            raise tests.TestSkipped("Format 0.8 doesn't work with knit3")
 
1080
        repo = self.make_repository('repo', format='knit')
 
1081
        bundle.install_revisions(repo)
 
1082
 
 
1083
        bundle = read_bundle(self.create_bundle_text('null:', 'rev2')[0])
 
1084
        self.assertRaises(errors.IncompatibleRevision,
 
1085
                          bundle.install_revisions, repo)
 
1086
 
 
1087
    def test_get_merge_request(self):
 
1088
        tree = self.make_simple_tree()
 
1089
        tree.commit('hello', rev_id='rev1')
 
1090
        tree.commit('hello', rev_id='rev2')
 
1091
        bundle = read_bundle(self.create_bundle_text('null:', 'rev1')[0])
 
1092
        result = bundle.get_merge_request(tree.branch.repository)
 
1093
        self.assertEqual((None, 'rev1', 'inapplicable'), result)
 
1094
 
 
1095
    def test_with_subtree(self):
 
1096
        tree = self.make_branch_and_tree('tree',
 
1097
                                         format='dirstate-with-subtree')
 
1098
        self.b1 = tree.branch
 
1099
        subtree = self.make_branch_and_tree('tree/subtree',
 
1100
                                            format='dirstate-with-subtree')
 
1101
        tree.add('subtree')
 
1102
        tree.commit('hello', rev_id='rev1')
 
1103
        try:
 
1104
            bundle = read_bundle(self.create_bundle_text('null:', 'rev1')[0])
 
1105
        except errors.IncompatibleBundleFormat:
 
1106
            raise tests.TestSkipped("Format 0.8 doesn't work with knit3")
 
1107
        if isinstance(bundle, v09.BundleInfo09):
 
1108
            raise tests.TestSkipped("Format 0.9 doesn't work with subtrees")
 
1109
        repo = self.make_repository('repo', format='knit')
 
1110
        self.assertRaises(errors.IncompatibleRevision,
 
1111
                          bundle.install_revisions, repo)
 
1112
        repo2 = self.make_repository('repo2', format='dirstate-with-subtree')
 
1113
        bundle.install_revisions(repo2)
 
1114
 
 
1115
    def test_revision_id_with_slash(self):
 
1116
        self.tree1 = self.make_branch_and_tree('tree')
 
1117
        self.b1 = self.tree1.branch
 
1118
        try:
 
1119
            self.tree1.commit('Revision/id/with/slashes', rev_id='rev/id')
 
1120
        except ValueError:
 
1121
            raise tests.TestSkipped(
 
1122
                "Repository doesn't support revision ids with slashes")
 
1123
        bundle = self.get_valid_bundle('null:', 'rev/id')
 
1124
 
 
1125
    def test_skip_file(self):
 
1126
        """Make sure we don't accidentally write to the wrong versionedfile"""
 
1127
        self.tree1 = self.make_branch_and_tree('tree')
 
1128
        self.b1 = self.tree1.branch
 
1129
        # rev1 is not present in bundle, done by fetch
 
1130
        self.build_tree_contents([('tree/file2', 'contents1')])
 
1131
        self.tree1.add('file2', 'file2-id')
 
1132
        self.tree1.commit('rev1', rev_id='reva')
 
1133
        self.build_tree_contents([('tree/file3', 'contents2')])
 
1134
        # rev2 is present in bundle, and done by fetch
 
1135
        # having file1 in the bunle causes file1's versionedfile to be opened.
 
1136
        self.tree1.add('file3', 'file3-id')
 
1137
        self.tree1.commit('rev2')
 
1138
        # Updating file2 should not cause an attempt to add to file1's vf
 
1139
        target = self.tree1.bzrdir.sprout('target').open_workingtree()
 
1140
        self.build_tree_contents([('tree/file2', 'contents3')])
 
1141
        self.tree1.commit('rev3', rev_id='rev3')
 
1142
        bundle = self.get_valid_bundle('reva', 'rev3')
 
1143
        if getattr(bundle, 'get_bundle_reader', None) is None:
 
1144
            raise tests.TestSkipped('Bundle format cannot provide reader')
 
1145
        # be sure that file1 comes before file2
 
1146
        for b, m, k, r, f in bundle.get_bundle_reader().iter_records():
 
1147
            if f == 'file3-id':
 
1148
                break
 
1149
            self.assertNotEqual(f, 'file2-id')
 
1150
        bundle.install_revisions(target.branch.repository)
 
1151
 
 
1152
 
 
1153
class V08BundleTester(BundleTester, tests.TestCaseWithTransport):
 
1154
 
 
1155
    format = '0.8'
 
1156
 
 
1157
    def test_bundle_empty_property(self):
 
1158
        """Test serializing revision properties with an empty value."""
 
1159
        tree = self.make_branch_and_memory_tree('tree')
 
1160
        tree.lock_write()
 
1161
        self.addCleanup(tree.unlock)
 
1162
        tree.add([''], ['TREE_ROOT'])
 
1163
        tree.commit('One', revprops={'one':'two', 'empty':''}, rev_id='rev1')
 
1164
        self.b1 = tree.branch
 
1165
        bundle_sio, revision_ids = self.create_bundle_text('null:', 'rev1')
 
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 get_bundle_tree(self, bundle, revision_id):
 
1180
        repository = self.make_repository('repo')
 
1181
        return bundle.revision_tree(repository, 'revid1')
 
1182
 
 
1183
    def test_bundle_empty_property_alt(self):
 
1184
        """Test serializing revision properties with an empty value.
 
1185
 
 
1186
        Older readers had a bug when reading an empty property.
 
1187
        They assumed that all keys ended in ': \n'. However they would write an
 
1188
        empty value as ':\n'. This tests make sure that all newer bzr versions
 
1189
        can handle th second form.
 
1190
        """
 
1191
        tree = self.make_branch_and_memory_tree('tree')
 
1192
        tree.lock_write()
 
1193
        self.addCleanup(tree.unlock)
 
1194
        tree.add([''], ['TREE_ROOT'])
 
1195
        tree.commit('One', revprops={'one':'two', 'empty':''}, rev_id='rev1')
 
1196
        self.b1 = tree.branch
 
1197
        bundle_sio, revision_ids = self.create_bundle_text('null:', 'rev1')
 
1198
        txt = bundle_sio.getvalue()
 
1199
        loc = txt.find('#   empty: ') + len('#   empty:')
 
1200
        # Create a new bundle, which strips the trailing space after empty
 
1201
        bundle_sio = StringIO(txt[:loc] + txt[loc+1:])
 
1202
 
 
1203
        self.assertContainsRe(bundle_sio.getvalue(),
 
1204
                              '# properties:\n'
 
1205
                              '#   branch-nick: tree\n'
 
1206
                              '#   empty:\n'
 
1207
                              '#   one: two\n'
 
1208
                             )
 
1209
        bundle = read_bundle(bundle_sio)
 
1210
        revision_info = bundle.revisions[0]
 
1211
        self.assertEqual('rev1', revision_info.revision_id)
 
1212
        rev = revision_info.as_revision()
 
1213
        self.assertEqual({'branch-nick':'tree', 'empty':'', 'one':'two'},
 
1214
                         rev.properties)
 
1215
 
 
1216
    def test_bundle_sorted_properties(self):
 
1217
        """For stability the writer should write properties in sorted order."""
 
1218
        tree = self.make_branch_and_memory_tree('tree')
 
1219
        tree.lock_write()
 
1220
        self.addCleanup(tree.unlock)
 
1221
 
 
1222
        tree.add([''], ['TREE_ROOT'])
 
1223
        tree.commit('One', rev_id='rev1',
 
1224
                    revprops={'a':'4', 'b':'3', 'c':'2', 'd':'1'})
 
1225
        self.b1 = tree.branch
 
1226
        bundle_sio, revision_ids = self.create_bundle_text('null:', 'rev1')
 
1227
        self.assertContainsRe(bundle_sio.getvalue(),
 
1228
                              '# properties:\n'
 
1229
                              '#   a: 4\n'
 
1230
                              '#   b: 3\n'
 
1231
                              '#   branch-nick: tree\n'
 
1232
                              '#   c: 2\n'
 
1233
                              '#   d: 1\n'
 
1234
                             )
 
1235
        bundle = read_bundle(bundle_sio)
 
1236
        revision_info = bundle.revisions[0]
 
1237
        self.assertEqual('rev1', revision_info.revision_id)
 
1238
        rev = revision_info.as_revision()
 
1239
        self.assertEqual({'branch-nick':'tree', 'a':'4', 'b':'3', 'c':'2',
 
1240
                          'd':'1'}, rev.properties)
 
1241
 
 
1242
    def test_bundle_unicode_properties(self):
 
1243
        """We should be able to round trip a non-ascii property."""
 
1244
        tree = self.make_branch_and_memory_tree('tree')
 
1245
        tree.lock_write()
 
1246
        self.addCleanup(tree.unlock)
 
1247
 
 
1248
        tree.add([''], ['TREE_ROOT'])
 
1249
        # Revisions themselves do not require anything about revision property
 
1250
        # keys, other than that they are a basestring, and do not contain
 
1251
        # whitespace.
 
1252
        # However, Testaments assert than they are str(), and thus should not
 
1253
        # be Unicode.
 
1254
        tree.commit('One', rev_id='rev1',
 
1255
                    revprops={'omega':u'\u03a9', 'alpha':u'\u03b1'})
 
1256
        self.b1 = tree.branch
 
1257
        bundle_sio, revision_ids = self.create_bundle_text('null:', 'rev1')
 
1258
        self.assertContainsRe(bundle_sio.getvalue(),
 
1259
                              '# properties:\n'
 
1260
                              '#   alpha: \xce\xb1\n'
 
1261
                              '#   branch-nick: tree\n'
 
1262
                              '#   omega: \xce\xa9\n'
 
1263
                             )
 
1264
        bundle = read_bundle(bundle_sio)
 
1265
        revision_info = bundle.revisions[0]
 
1266
        self.assertEqual('rev1', revision_info.revision_id)
 
1267
        rev = revision_info.as_revision()
 
1268
        self.assertEqual({'branch-nick':'tree', 'omega':u'\u03a9',
 
1269
                          'alpha':u'\u03b1'}, rev.properties)
 
1270
 
 
1271
 
 
1272
class V09BundleKnit2Tester(V08BundleTester):
 
1273
 
 
1274
    format = '0.9'
 
1275
 
 
1276
    def bzrdir_format(self):
 
1277
        format = bzrdir.BzrDirMetaFormat1()
 
1278
        format.repository_format = knitrepo.RepositoryFormatKnit3()
 
1279
        return format
 
1280
 
 
1281
 
 
1282
class V09BundleKnit1Tester(V08BundleTester):
 
1283
 
 
1284
    format = '0.9'
 
1285
 
 
1286
    def bzrdir_format(self):
 
1287
        format = bzrdir.BzrDirMetaFormat1()
 
1288
        format.repository_format = knitrepo.RepositoryFormatKnit1()
 
1289
        return format
 
1290
 
 
1291
 
 
1292
class V4BundleTester(BundleTester, tests.TestCaseWithTransport):
 
1293
 
 
1294
    format = '4'
 
1295
 
 
1296
    def get_valid_bundle(self, base_rev_id, rev_id, checkout_dir=None):
 
1297
        """Create a bundle from base_rev_id -> rev_id in built-in branch.
 
1298
        Make sure that the text generated is valid, and that it
 
1299
        can be applied against the base, and generate the same information.
 
1300
 
 
1301
        :return: The in-memory bundle
 
1302
        """
 
1303
        bundle_txt, rev_ids = self.create_bundle_text(base_rev_id, rev_id)
 
1304
 
 
1305
        # This should also validate the generated bundle
 
1306
        bundle = read_bundle(bundle_txt)
 
1307
        repository = self.b1.repository
 
1308
        for bundle_rev in bundle.real_revisions:
 
1309
            # These really should have already been checked when we read the
 
1310
            # bundle, since it computes the sha1 hash for the revision, which
 
1311
            # only will match if everything is okay, but lets be explicit about
 
1312
            # it
 
1313
            branch_rev = repository.get_revision(bundle_rev.revision_id)
 
1314
            for a in ('inventory_sha1', 'revision_id', 'parent_ids',
 
1315
                      'timestamp', 'timezone', 'message', 'committer',
 
1316
                      'parent_ids', 'properties'):
 
1317
                self.assertEqual(getattr(branch_rev, a),
 
1318
                                 getattr(bundle_rev, a))
 
1319
            self.assertEqual(len(branch_rev.parent_ids),
 
1320
                             len(bundle_rev.parent_ids))
 
1321
        self.assertEqual(set(rev_ids),
 
1322
                         set([r.revision_id for r in bundle.real_revisions]))
 
1323
        self.valid_apply_bundle(base_rev_id, bundle,
 
1324
                                   checkout_dir=checkout_dir)
 
1325
 
 
1326
        return bundle
 
1327
 
 
1328
    def get_invalid_bundle(self, base_rev_id, rev_id):
 
1329
        """Create a bundle from base_rev_id -> rev_id in built-in branch.
 
1330
        Munge the text so that it's invalid.
 
1331
 
 
1332
        :return: The in-memory bundle
 
1333
        """
 
1334
        from bzrlib.bundle import serializer
 
1335
        bundle_txt, rev_ids = self.create_bundle_text(base_rev_id, rev_id)
 
1336
        new_text = self.get_raw(StringIO(''.join(bundle_txt)))
 
1337
        new_text = new_text.replace('<file file_id="exe-1"',
 
1338
                                    '<file executable="y" file_id="exe-1"')
 
1339
        new_text = new_text.replace('B260', 'B275')
 
1340
        bundle_txt = StringIO()
 
1341
        bundle_txt.write(serializer._get_bundle_header('4'))
 
1342
        bundle_txt.write('\n')
 
1343
        bundle_txt.write(new_text.encode('bz2'))
 
1344
        bundle_txt.seek(0)
 
1345
        bundle = read_bundle(bundle_txt)
 
1346
        self.valid_apply_bundle(base_rev_id, bundle)
 
1347
        return bundle
 
1348
 
 
1349
    def create_bundle_text(self, base_rev_id, rev_id):
 
1350
        bundle_txt = StringIO()
 
1351
        rev_ids = write_bundle(self.b1.repository, rev_id, base_rev_id,
 
1352
                               bundle_txt, format=self.format)
 
1353
        bundle_txt.seek(0)
 
1354
        self.assertEqual(bundle_txt.readline(),
 
1355
                         '# Bazaar revision bundle v%s\n' % self.format)
 
1356
        self.assertEqual(bundle_txt.readline(), '#\n')
 
1357
        rev = self.b1.repository.get_revision(rev_id)
 
1358
        bundle_txt.seek(0)
 
1359
        return bundle_txt, rev_ids
 
1360
 
 
1361
    def get_bundle_tree(self, bundle, revision_id):
 
1362
        repository = self.make_repository('repo')
 
1363
        bundle.install_revisions(repository)
 
1364
        return repository.revision_tree(revision_id)
 
1365
 
 
1366
    def test_creation(self):
 
1367
        tree = self.make_branch_and_tree('tree')
 
1368
        self.build_tree_contents([('tree/file', 'contents1\nstatic\n')])
 
1369
        tree.add('file', 'fileid-2')
 
1370
        tree.commit('added file', rev_id='rev1')
 
1371
        self.build_tree_contents([('tree/file', 'contents2\nstatic\n')])
 
1372
        tree.commit('changed file', rev_id='rev2')
 
1373
        s = StringIO()
 
1374
        serializer = BundleSerializerV4('1.0')
 
1375
        serializer.write(tree.branch.repository, ['rev1', 'rev2'], {}, s)
 
1376
        s.seek(0)
 
1377
        tree2 = self.make_branch_and_tree('target')
 
1378
        target_repo = tree2.branch.repository
 
1379
        install_bundle(target_repo, serializer.read(s))
 
1380
        target_repo.lock_read()
 
1381
        self.addCleanup(target_repo.unlock)
 
1382
        # Turn the 'iterators_of_bytes' back into simple strings for comparison
 
1383
        repo_texts = dict((i, ''.join(content)) for i, content
 
1384
                          in target_repo.iter_files_bytes(
 
1385
                                [('fileid-2', 'rev1', '1'),
 
1386
                                 ('fileid-2', 'rev2', '2')]))
 
1387
        self.assertEqual({'1':'contents1\nstatic\n',
 
1388
                          '2':'contents2\nstatic\n'},
 
1389
                         repo_texts)
 
1390
        rtree = target_repo.revision_tree('rev2')
 
1391
        inventory_vf = target_repo.inventories
 
1392
        # If the inventory store has a graph, it must match the revision graph.
 
1393
        self.assertSubset(
 
1394
            [inventory_vf.get_parent_map([('rev2',)])[('rev2',)]],
 
1395
            [None, (('rev1',),)])
 
1396
        self.assertEqual('changed file',
 
1397
                         target_repo.get_revision('rev2').message)
 
1398
 
 
1399
    @staticmethod
 
1400
    def get_raw(bundle_file):
 
1401
        bundle_file.seek(0)
 
1402
        line = bundle_file.readline()
 
1403
        line = bundle_file.readline()
 
1404
        lines = bundle_file.readlines()
 
1405
        return ''.join(lines).decode('bz2')
 
1406
 
 
1407
    def test_copy_signatures(self):
 
1408
        tree_a = self.make_branch_and_tree('tree_a')
 
1409
        import bzrlib.gpg
 
1410
        import bzrlib.commit as commit
 
1411
        oldstrategy = bzrlib.gpg.GPGStrategy
 
1412
        branch = tree_a.branch
 
1413
        repo_a = branch.repository
 
1414
        tree_a.commit("base", allow_pointless=True, rev_id='A')
 
1415
        self.failIf(branch.repository.has_signature_for_revision_id('A'))
 
1416
        try:
 
1417
            from bzrlib.testament import Testament
 
1418
            # monkey patch gpg signing mechanism
 
1419
            bzrlib.gpg.GPGStrategy = bzrlib.gpg.LoopbackGPGStrategy
 
1420
            new_config = test_commit.MustSignConfig(branch)
 
1421
            commit.Commit(config=new_config).commit(message="base",
 
1422
                                                    allow_pointless=True,
 
1423
                                                    rev_id='B',
 
1424
                                                    working_tree=tree_a)
 
1425
            def sign(text):
 
1426
                return bzrlib.gpg.LoopbackGPGStrategy(None).sign(text)
 
1427
            self.assertTrue(repo_a.has_signature_for_revision_id('B'))
 
1428
        finally:
 
1429
            bzrlib.gpg.GPGStrategy = oldstrategy
 
1430
        tree_b = self.make_branch_and_tree('tree_b')
 
1431
        repo_b = tree_b.branch.repository
 
1432
        s = StringIO()
 
1433
        serializer = BundleSerializerV4('4')
 
1434
        serializer.write(tree_a.branch.repository, ['A', 'B'], {}, s)
 
1435
        s.seek(0)
 
1436
        install_bundle(repo_b, serializer.read(s))
 
1437
        self.assertTrue(repo_b.has_signature_for_revision_id('B'))
 
1438
        self.assertEqual(repo_b.get_signature_text('B'),
 
1439
                         repo_a.get_signature_text('B'))
 
1440
        s.seek(0)
 
1441
        # ensure repeat installs are harmless
 
1442
        install_bundle(repo_b, serializer.read(s))
 
1443
 
 
1444
 
 
1445
class V4WeaveBundleTester(V4BundleTester):
 
1446
 
 
1447
    def bzrdir_format(self):
 
1448
        return 'metaweave'
 
1449
 
 
1450
 
 
1451
class V4_2aBundleTester(V4BundleTester):
 
1452
 
 
1453
    def bzrdir_format(self):
 
1454
        return '2a'
 
1455
 
 
1456
    def get_invalid_bundle(self, base_rev_id, rev_id):
 
1457
        """Create a bundle from base_rev_id -> rev_id in built-in branch.
 
1458
        Munge the text so that it's invalid.
 
1459
 
 
1460
        :return: The in-memory bundle
 
1461
        """
 
1462
        from bzrlib.bundle import serializer
 
1463
        bundle_txt, rev_ids = self.create_bundle_text(base_rev_id, rev_id)
 
1464
        new_text = self.get_raw(StringIO(''.join(bundle_txt)))
 
1465
        # We are going to be replacing some text to set the executable bit on a
 
1466
        # file. Make sure the text replacement actually works correctly.
 
1467
        self.assertContainsRe(new_text, '(?m)B244\n\ni 1\n<inventory')
 
1468
        new_text = new_text.replace('<file file_id="exe-1"',
 
1469
                                    '<file executable="y" file_id="exe-1"')
 
1470
        new_text = new_text.replace('B244', 'B259')
 
1471
        bundle_txt = StringIO()
 
1472
        bundle_txt.write(serializer._get_bundle_header('4'))
 
1473
        bundle_txt.write('\n')
 
1474
        bundle_txt.write(new_text.encode('bz2'))
 
1475
        bundle_txt.seek(0)
 
1476
        bundle = read_bundle(bundle_txt)
 
1477
        self.valid_apply_bundle(base_rev_id, bundle)
 
1478
        return bundle
 
1479
 
 
1480
    def make_merged_branch(self):
 
1481
        builder = self.make_branch_builder('source')
 
1482
        builder.start_series()
 
1483
        builder.build_snapshot('a@cset-0-1', None, [
 
1484
            ('add', ('', 'root-id', 'directory', None)),
 
1485
            ('add', ('file', 'file-id', 'file', 'original content\n')),
 
1486
            ])
 
1487
        builder.build_snapshot('a@cset-0-2a', ['a@cset-0-1'], [
 
1488
            ('modify', ('file-id', 'new-content\n')),
 
1489
            ])
 
1490
        builder.build_snapshot('a@cset-0-2b', ['a@cset-0-1'], [
 
1491
            ('add', ('other-file', 'file2-id', 'file', 'file2-content\n')),
 
1492
            ])
 
1493
        builder.build_snapshot('a@cset-0-3', ['a@cset-0-2a', 'a@cset-0-2b'], [
 
1494
            ('add', ('other-file', 'file2-id', 'file', 'file2-content\n')),
 
1495
            ])
 
1496
        builder.finish_series()
 
1497
        self.b1 = builder.get_branch()
 
1498
        self.b1.lock_read()
 
1499
        self.addCleanup(self.b1.unlock)
 
1500
 
 
1501
    def make_bundle_just_inventories(self, base_revision_id,
 
1502
                                     target_revision_id,
 
1503
                                     revision_ids):
 
1504
        sio = StringIO()
 
1505
        writer = v4.BundleWriteOperation(base_revision_id, target_revision_id,
 
1506
                                         self.b1.repository, sio)
 
1507
        writer.bundle.begin()
 
1508
        writer._add_inventory_mpdiffs_from_serializer(revision_ids)
 
1509
        writer.bundle.end()
 
1510
        sio.seek(0)
 
1511
        return sio
 
1512
 
 
1513
    def test_single_inventory_multiple_parents_as_xml(self):
 
1514
        self.make_merged_branch()
 
1515
        sio = self.make_bundle_just_inventories('a@cset-0-1', 'a@cset-0-3',
 
1516
                                                ['a@cset-0-3'])
 
1517
        reader = v4.BundleReader(sio, stream_input=False)
 
1518
        records = list(reader.iter_records())
 
1519
        self.assertEqual(1, len(records))
 
1520
        (bytes, metadata, repo_kind, revision_id,
 
1521
         file_id) = records[0]
 
1522
        self.assertIs(None, file_id)
 
1523
        self.assertEqual('a@cset-0-3', revision_id)
 
1524
        self.assertEqual('inventory', repo_kind)
 
1525
        self.assertEqual({'parents': ['a@cset-0-2a', 'a@cset-0-2b'],
 
1526
                          'sha1': '09c53b0c4de0895e11a2aacc34fef60a6e70865c',
 
1527
                          'storage_kind': 'mpdiff',
 
1528
                         }, metadata)
 
1529
        # We should have an mpdiff that takes some lines from both parents.
 
1530
        self.assertEqualDiff(
 
1531
            'i 1\n'
 
1532
            '<inventory format="10" revision_id="a@cset-0-3">\n'
 
1533
            '\n'
 
1534
            'c 0 1 1 2\n'
 
1535
            'c 1 3 3 2\n', bytes)
 
1536
 
 
1537
    def test_single_inv_no_parents_as_xml(self):
 
1538
        self.make_merged_branch()
 
1539
        sio = self.make_bundle_just_inventories('null:', 'a@cset-0-1',
 
1540
                                                ['a@cset-0-1'])
 
1541
        reader = v4.BundleReader(sio, stream_input=False)
 
1542
        records = list(reader.iter_records())
 
1543
        self.assertEqual(1, len(records))
 
1544
        (bytes, metadata, repo_kind, revision_id,
 
1545
         file_id) = records[0]
 
1546
        self.assertIs(None, file_id)
 
1547
        self.assertEqual('a@cset-0-1', revision_id)
 
1548
        self.assertEqual('inventory', repo_kind)
 
1549
        self.assertEqual({'parents': [],
 
1550
                          'sha1': 'a13f42b142d544aac9b085c42595d304150e31a2',
 
1551
                          'storage_kind': 'mpdiff',
 
1552
                         }, metadata)
 
1553
        # We should have an mpdiff that takes some lines from both parents.
 
1554
        self.assertEqualDiff(
 
1555
            'i 4\n'
 
1556
            '<inventory format="10" revision_id="a@cset-0-1">\n'
 
1557
            '<directory file_id="root-id" name=""'
 
1558
                ' revision="a@cset-0-1" />\n'
 
1559
            '<file file_id="file-id" name="file" parent_id="root-id"'
 
1560
                ' revision="a@cset-0-1"'
 
1561
                ' text_sha1="09c2f8647e14e49e922b955c194102070597c2d1"'
 
1562
                ' text_size="17" />\n'
 
1563
            '</inventory>\n'
 
1564
            '\n', bytes)
 
1565
 
 
1566
    def test_multiple_inventories_as_xml(self):
 
1567
        self.make_merged_branch()
 
1568
        sio = self.make_bundle_just_inventories('a@cset-0-1', 'a@cset-0-3',
 
1569
            ['a@cset-0-2a', 'a@cset-0-2b', 'a@cset-0-3'])
 
1570
        reader = v4.BundleReader(sio, stream_input=False)
 
1571
        records = list(reader.iter_records())
 
1572
        self.assertEqual(3, len(records))
 
1573
        revision_ids = [rev_id for b, m, k, rev_id, f in records]
 
1574
        self.assertEqual(['a@cset-0-2a', 'a@cset-0-2b', 'a@cset-0-3'],
 
1575
                         revision_ids)
 
1576
        metadata_2a = records[0][1]
 
1577
        self.assertEqual({'parents': ['a@cset-0-1'],
 
1578
                          'sha1': '1e105886d62d510763e22885eec733b66f5f09bf',
 
1579
                          'storage_kind': 'mpdiff',
 
1580
                         }, metadata_2a)
 
1581
        metadata_2b = records[1][1]
 
1582
        self.assertEqual({'parents': ['a@cset-0-1'],
 
1583
                          'sha1': 'f03f12574bdb5ed2204c28636c98a8547544ccd8',
 
1584
                          'storage_kind': 'mpdiff',
 
1585
                         }, metadata_2b)
 
1586
        metadata_3 = records[2][1]
 
1587
        self.assertEqual({'parents': ['a@cset-0-2a', 'a@cset-0-2b'],
 
1588
                          'sha1': '09c53b0c4de0895e11a2aacc34fef60a6e70865c',
 
1589
                          'storage_kind': 'mpdiff',
 
1590
                         }, metadata_3)
 
1591
        bytes_2a = records[0][0]
 
1592
        self.assertEqualDiff(
 
1593
            'i 1\n'
 
1594
            '<inventory format="10" revision_id="a@cset-0-2a">\n'
 
1595
            '\n'
 
1596
            'c 0 1 1 1\n'
 
1597
            'i 1\n'
 
1598
            '<file file_id="file-id" name="file" parent_id="root-id"'
 
1599
                ' revision="a@cset-0-2a"'
 
1600
                ' text_sha1="50f545ff40e57b6924b1f3174b267ffc4576e9a9"'
 
1601
                ' text_size="12" />\n'
 
1602
            '\n'
 
1603
            'c 0 3 3 1\n', bytes_2a)
 
1604
        bytes_2b = records[1][0]
 
1605
        self.assertEqualDiff(
 
1606
            'i 1\n'
 
1607
            '<inventory format="10" revision_id="a@cset-0-2b">\n'
 
1608
            '\n'
 
1609
            'c 0 1 1 2\n'
 
1610
            'i 1\n'
 
1611
            '<file file_id="file2-id" name="other-file" parent_id="root-id"'
 
1612
                ' revision="a@cset-0-2b"'
 
1613
                ' text_sha1="b46c0c8ea1e5ef8e46fc8894bfd4752a88ec939e"'
 
1614
                ' text_size="14" />\n'
 
1615
            '\n'
 
1616
            'c 0 3 4 1\n', bytes_2b)
 
1617
        bytes_3 = records[2][0]
 
1618
        self.assertEqualDiff(
 
1619
            'i 1\n'
 
1620
            '<inventory format="10" revision_id="a@cset-0-3">\n'
 
1621
            '\n'
 
1622
            'c 0 1 1 2\n'
 
1623
            'c 1 3 3 2\n', bytes_3)
 
1624
 
 
1625
    def test_creating_bundle_preserves_chk_pages(self):
 
1626
        self.make_merged_branch()
 
1627
        target = self.b1.bzrdir.sprout('target',
 
1628
                                       revision_id='a@cset-0-2a').open_branch()
 
1629
        bundle_txt, rev_ids = self.create_bundle_text('a@cset-0-2a',
 
1630
                                                      'a@cset-0-3')
 
1631
        self.assertEqual(['a@cset-0-2b', 'a@cset-0-3'], rev_ids)
 
1632
        bundle = read_bundle(bundle_txt)
 
1633
        target.lock_write()
 
1634
        self.addCleanup(target.unlock)
 
1635
        install_bundle(target.repository, bundle)
 
1636
        inv1 = self.b1.repository.inventories.get_record_stream([
 
1637
            ('a@cset-0-3',)], 'unordered',
 
1638
            True).next().get_bytes_as('fulltext')
 
1639
        inv2 = target.repository.inventories.get_record_stream([
 
1640
            ('a@cset-0-3',)], 'unordered',
 
1641
            True).next().get_bytes_as('fulltext')
 
1642
        self.assertEqualDiff(inv1, inv2)
 
1643
 
 
1644
 
 
1645
class MungedBundleTester(object):
817
1646
 
818
1647
    def build_test_bundle(self):
819
1648
        wt = self.make_branch_and_tree('b1')
828
1657
 
829
1658
        bundle_txt = StringIO()
830
1659
        rev_ids = write_bundle(wt.branch.repository, 'a@cset-0-2',
831
 
                               'a@cset-0-1', bundle_txt)
832
 
        self.assertEqual(['a@cset-0-2'], rev_ids)
 
1660
                               'a@cset-0-1', bundle_txt, self.format)
 
1661
        self.assertEqual(set(['a@cset-0-2']), set(rev_ids))
833
1662
        bundle_txt.seek(0, 0)
834
1663
        return bundle_txt
835
1664
 
864
1693
        bundle = read_bundle(bundle_txt)
865
1694
        self.check_valid(bundle)
866
1695
 
 
1696
 
 
1697
class MungedBundleTesterV09(tests.TestCaseWithTransport, MungedBundleTester):
 
1698
 
 
1699
    format = '0.9'
 
1700
 
867
1701
    def test_missing_trailing_whitespace(self):
868
1702
        bundle_txt = self.build_test_bundle()
869
1703
 
897
1731
        bundle = read_bundle(bundle_txt)
898
1732
        self.check_valid(bundle)
899
1733
 
 
1734
 
 
1735
class MungedBundleTesterV4(tests.TestCaseWithTransport, MungedBundleTester):
 
1736
 
 
1737
    format = '4'
 
1738
 
 
1739
 
 
1740
class TestBundleWriterReader(tests.TestCase):
 
1741
 
 
1742
    def test_roundtrip_record(self):
 
1743
        fileobj = StringIO()
 
1744
        writer = v4.BundleWriter(fileobj)
 
1745
        writer.begin()
 
1746
        writer.add_info_record(foo='bar')
 
1747
        writer._add_record("Record body", {'parents': ['1', '3'],
 
1748
            'storage_kind':'fulltext'}, 'file', 'revid', 'fileid')
 
1749
        writer.end()
 
1750
        fileobj.seek(0)
 
1751
        reader = v4.BundleReader(fileobj, stream_input=True)
 
1752
        record_iter = reader.iter_records()
 
1753
        record = record_iter.next()
 
1754
        self.assertEqual((None, {'foo': 'bar', 'storage_kind': 'header'},
 
1755
            'info', None, None), record)
 
1756
        record = record_iter.next()
 
1757
        self.assertEqual(("Record body", {'storage_kind': 'fulltext',
 
1758
                          'parents': ['1', '3']}, 'file', 'revid', 'fileid'),
 
1759
                          record)
 
1760
 
 
1761
    def test_roundtrip_record_memory_hungry(self):
 
1762
        fileobj = StringIO()
 
1763
        writer = v4.BundleWriter(fileobj)
 
1764
        writer.begin()
 
1765
        writer.add_info_record(foo='bar')
 
1766
        writer._add_record("Record body", {'parents': ['1', '3'],
 
1767
            'storage_kind':'fulltext'}, 'file', 'revid', 'fileid')
 
1768
        writer.end()
 
1769
        fileobj.seek(0)
 
1770
        reader = v4.BundleReader(fileobj, stream_input=False)
 
1771
        record_iter = reader.iter_records()
 
1772
        record = record_iter.next()
 
1773
        self.assertEqual((None, {'foo': 'bar', 'storage_kind': 'header'},
 
1774
            'info', None, None), record)
 
1775
        record = record_iter.next()
 
1776
        self.assertEqual(("Record body", {'storage_kind': 'fulltext',
 
1777
                          'parents': ['1', '3']}, 'file', 'revid', 'fileid'),
 
1778
                          record)
 
1779
 
 
1780
    def test_encode_name(self):
 
1781
        self.assertEqual('revision/rev1',
 
1782
            v4.BundleWriter.encode_name('revision', 'rev1'))
 
1783
        self.assertEqual('file/rev//1/file-id-1',
 
1784
            v4.BundleWriter.encode_name('file', 'rev/1', 'file-id-1'))
 
1785
        self.assertEqual('info',
 
1786
            v4.BundleWriter.encode_name('info', None, None))
 
1787
 
 
1788
    def test_decode_name(self):
 
1789
        self.assertEqual(('revision', 'rev1', None),
 
1790
            v4.BundleReader.decode_name('revision/rev1'))
 
1791
        self.assertEqual(('file', 'rev/1', 'file-id-1'),
 
1792
            v4.BundleReader.decode_name('file/rev//1/file-id-1'))
 
1793
        self.assertEqual(('info', None, None),
 
1794
                         v4.BundleReader.decode_name('info'))
 
1795
 
 
1796
    def test_too_many_names(self):
 
1797
        fileobj = StringIO()
 
1798
        writer = v4.BundleWriter(fileobj)
 
1799
        writer.begin()
 
1800
        writer.add_info_record(foo='bar')
 
1801
        writer._container.add_bytes_record('blah', ['two', 'names'])
 
1802
        writer.end()
 
1803
        fileobj.seek(0)
 
1804
        record_iter = v4.BundleReader(fileobj).iter_records()
 
1805
        record = record_iter.next()
 
1806
        self.assertEqual((None, {'foo': 'bar', 'storage_kind': 'header'},
 
1807
            'info', None, None), record)
 
1808
        self.assertRaises(errors.BadBundle, record_iter.next)
 
1809
 
 
1810
 
 
1811
class TestReadMergeableFromUrl(tests.TestCaseWithTransport):
 
1812
 
 
1813
    def test_read_mergeable_skips_local(self):
 
1814
        """A local bundle named like the URL should not be read.
 
1815
        """
 
1816
        out, wt = test_read_bundle.create_bundle_file(self)
 
1817
        class FooService(object):
 
1818
            """A directory service that always returns source"""
 
1819
 
 
1820
            def look_up(self, name, url):
 
1821
                return 'source'
 
1822
        directories.register('foo:', FooService, 'Testing directory service')
 
1823
        self.addCleanup(directories.remove, 'foo:')
 
1824
        self.build_tree_contents([('./foo:bar', out.getvalue())])
 
1825
        self.assertRaises(errors.NotABundle, read_mergeable_from_url,
 
1826
                          'foo:bar')
 
1827
 
 
1828
    def test_infinite_redirects_are_not_a_bundle(self):
 
1829
        """If a URL causes TooManyRedirections then NotABundle is raised.
 
1830
        """
 
1831
        from bzrlib.tests.blackbox.test_push import RedirectingMemoryServer
 
1832
        server = RedirectingMemoryServer()
 
1833
        self.start_server(server)
 
1834
        url = server.get_url() + 'infinite-loop'
 
1835
        self.assertRaises(errors.NotABundle, read_mergeable_from_url, url)
 
1836
 
 
1837
    def test_smart_server_connection_reset(self):
 
1838
        """If a smart server connection fails during the attempt to read a
 
1839
        bundle, then the ConnectionReset error should be propagated.
 
1840
        """
 
1841
        # Instantiate a server that will provoke a ConnectionReset
 
1842
        sock_server = _DisconnectingTCPServer()
 
1843
        self.start_server(sock_server)
 
1844
        # We don't really care what the url is since the server will close the
 
1845
        # connection without interpreting it
 
1846
        url = sock_server.get_url()
 
1847
        self.assertRaises(errors.ConnectionReset, read_mergeable_from_url, url)
 
1848
 
 
1849
 
 
1850
class _DisconnectingTCPServer(object):
 
1851
    """A TCP server that immediately closes any connection made to it."""
 
1852
 
 
1853
    def start_server(self):
 
1854
        self.sock = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
 
1855
        self.sock.bind(('127.0.0.1', 0))
 
1856
        self.sock.listen(1)
 
1857
        self.port = self.sock.getsockname()[1]
 
1858
        self.thread = threading.Thread(
 
1859
            name='%s (port %d)' % (self.__class__.__name__, self.port),
 
1860
            target=self.accept_and_close)
 
1861
        self.thread.start()
 
1862
 
 
1863
    def accept_and_close(self):
 
1864
        conn, addr = self.sock.accept()
 
1865
        conn.shutdown(socket.SHUT_RDWR)
 
1866
        conn.close()
 
1867
 
 
1868
    def get_url(self):
 
1869
        return 'bzr://127.0.0.1:%d/' % (self.port,)
 
1870
 
 
1871
    def stop_server(self):
 
1872
        try:
 
1873
            # make sure the thread dies by connecting to the listening socket,
 
1874
            # just in case the test failed to do so.
 
1875
            conn = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
 
1876
            conn.connect(self.sock.getsockname())
 
1877
            conn.close()
 
1878
        except socket.error:
 
1879
            pass
 
1880
        self.sock.close()
 
1881
        self.thread.join()