/brz/remove-bazaar

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

« back to all changes in this revision

Viewing changes to bzrlib/tests/test_bundle.py

  • Committer: John Arbash Meinel
  • Date: 2009-06-18 18:18:36 UTC
  • mto: This revision was merged to the branch mainline in revision 4461.
  • Revision ID: john@arbash-meinel.com-20090618181836-biodfkat9a8eyzjz
The new add_inventory_by_delta is returning a CHKInventory when mapping from NULL
Which is completely valid, but 'broke' one of the tests.
So to fix it, changed the test to use CHKInventories on both sides, and add an __eq__
member. The nice thing is that CHKInventory.__eq__ is fairly cheap, since it only
has to check the root keys.

Show diffs side-by-side

added added

removed removed

Lines of Context:
 
1
# Copyright (C) 2004, 2005, 2006, 2007 Canonical Ltd
 
2
#
 
3
# This program is free software; you can redistribute it and/or modify
 
4
# it under the terms of the GNU General Public License as published by
 
5
# the Free Software Foundation; either version 2 of the License, or
 
6
# (at your option) any later version.
 
7
#
 
8
# This program is distributed in the hope that it will be useful,
 
9
# but WITHOUT ANY WARRANTY; without even the implied warranty of
 
10
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
 
11
# GNU General Public License for more details.
 
12
#
 
13
# You should have received a copy of the GNU General Public License
 
14
# along with this program; if not, write to the Free Software
 
15
# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA
 
16
 
 
17
from cStringIO import StringIO
 
18
import os
 
19
import socket
 
20
import sys
 
21
import threading
 
22
 
 
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
 
36
from bzrlib.bundle.apply_bundle import install_bundle, merge_bundle
 
37
from bzrlib.bundle.bundle_data import BundleTree
 
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
 
44
from bzrlib.branch import Branch
 
45
from bzrlib.repofmt import knitrepo
 
46
from bzrlib.tests import (
 
47
    test_read_bundle,
 
48
    test_commit,
 
49
    )
 
50
from bzrlib.transform import TreeTransform
 
51
 
 
52
 
 
53
class MockTree(object):
 
54
    def __init__(self):
 
55
        from bzrlib.inventory import InventoryDirectory, ROOT_ID
 
56
        object.__init__(self)
 
57
        self.paths = {ROOT_ID: ""}
 
58
        self.ids = {"": ROOT_ID}
 
59
        self.contents = {}
 
60
        self.root = InventoryDirectory(ROOT_ID, '', None)
 
61
 
 
62
    inventory = property(lambda x:x)
 
63
 
 
64
    def __iter__(self):
 
65
        return self.paths.iterkeys()
 
66
 
 
67
    def __getitem__(self, file_id):
 
68
        if file_id == self.root.file_id:
 
69
            return self.root
 
70
        else:
 
71
            return self.make_entry(file_id, self.paths[file_id])
 
72
 
 
73
    def parent_id(self, file_id):
 
74
        parent_dir = os.path.dirname(self.paths[file_id])
 
75
        if parent_dir == "":
 
76
            return None
 
77
        return self.ids[parent_dir]
 
78
 
 
79
    def iter_entries(self):
 
80
        for path, file_id in self.ids.iteritems():
 
81
            yield path, self[file_id]
 
82
 
 
83
    def get_file_kind(self, file_id):
 
84
        if file_id in self.contents:
 
85
            kind = 'file'
 
86
        else:
 
87
            kind = 'directory'
 
88
        return kind
 
89
 
 
90
    def make_entry(self, file_id, path):
 
91
        from bzrlib.inventory import (InventoryEntry, InventoryFile
 
92
                                    , InventoryDirectory, InventoryLink)
 
93
        name = os.path.basename(path)
 
94
        kind = self.get_file_kind(file_id)
 
95
        parent_id = self.parent_id(file_id)
 
96
        text_sha_1, text_size = self.contents_stats(file_id)
 
97
        if kind == 'directory':
 
98
            ie = InventoryDirectory(file_id, name, parent_id)
 
99
        elif kind == 'file':
 
100
            ie = InventoryFile(file_id, name, parent_id)
 
101
        elif kind == 'symlink':
 
102
            ie = InventoryLink(file_id, name, parent_id)
 
103
        else:
 
104
            raise errors.BzrError('unknown kind %r' % kind)
 
105
        ie.text_sha1 = text_sha_1
 
106
        ie.text_size = text_size
 
107
        return ie
 
108
 
 
109
    def add_dir(self, file_id, path):
 
110
        self.paths[file_id] = path
 
111
        self.ids[path] = file_id
 
112
 
 
113
    def add_file(self, file_id, path, contents):
 
114
        self.add_dir(file_id, path)
 
115
        self.contents[file_id] = contents
 
116
 
 
117
    def path2id(self, path):
 
118
        return self.ids.get(path)
 
119
 
 
120
    def id2path(self, file_id):
 
121
        return self.paths.get(file_id)
 
122
 
 
123
    def has_id(self, file_id):
 
124
        return self.id2path(file_id) is not None
 
125
 
 
126
    def get_file(self, file_id):
 
127
        result = StringIO()
 
128
        result.write(self.contents[file_id])
 
129
        result.seek(0,0)
 
130
        return result
 
131
 
 
132
    def contents_stats(self, file_id):
 
133
        if file_id not in self.contents:
 
134
            return None, None
 
135
        text_sha1 = osutils.sha_file(self.get_file(file_id))
 
136
        return text_sha1, len(self.contents[file_id])
 
137
 
 
138
 
 
139
class BTreeTester(tests.TestCase):
 
140
    """A simple unittest tester for the BundleTree class."""
 
141
 
 
142
    def make_tree_1(self):
 
143
        mtree = MockTree()
 
144
        mtree.add_dir("a", "grandparent")
 
145
        mtree.add_dir("b", "grandparent/parent")
 
146
        mtree.add_file("c", "grandparent/parent/file", "Hello\n")
 
147
        mtree.add_dir("d", "grandparent/alt_parent")
 
148
        return BundleTree(mtree, ''), mtree
 
149
 
 
150
    def test_renames(self):
 
151
        """Ensure that file renames have the proper effect on children"""
 
152
        btree = self.make_tree_1()[0]
 
153
        self.assertEqual(btree.old_path("grandparent"), "grandparent")
 
154
        self.assertEqual(btree.old_path("grandparent/parent"),
 
155
                         "grandparent/parent")
 
156
        self.assertEqual(btree.old_path("grandparent/parent/file"),
 
157
                         "grandparent/parent/file")
 
158
 
 
159
        self.assertEqual(btree.id2path("a"), "grandparent")
 
160
        self.assertEqual(btree.id2path("b"), "grandparent/parent")
 
161
        self.assertEqual(btree.id2path("c"), "grandparent/parent/file")
 
162
 
 
163
        self.assertEqual(btree.path2id("grandparent"), "a")
 
164
        self.assertEqual(btree.path2id("grandparent/parent"), "b")
 
165
        self.assertEqual(btree.path2id("grandparent/parent/file"), "c")
 
166
 
 
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)
 
170
 
 
171
        btree.note_rename("grandparent", "grandparent2")
 
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)
 
175
 
 
176
        self.assertEqual(btree.id2path("a"), "grandparent2")
 
177
        self.assertEqual(btree.id2path("b"), "grandparent2/parent")
 
178
        self.assertEqual(btree.id2path("c"), "grandparent2/parent/file")
 
179
 
 
180
        self.assertEqual(btree.path2id("grandparent2"), "a")
 
181
        self.assertEqual(btree.path2id("grandparent2/parent"), "b")
 
182
        self.assertEqual(btree.path2id("grandparent2/parent/file"), "c")
 
183
 
 
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)
 
187
 
 
188
        btree.note_rename("grandparent/parent", "grandparent2/parent2")
 
189
        self.assertEqual(btree.id2path("a"), "grandparent2")
 
190
        self.assertEqual(btree.id2path("b"), "grandparent2/parent2")
 
191
        self.assertEqual(btree.id2path("c"), "grandparent2/parent2/file")
 
192
 
 
193
        self.assertEqual(btree.path2id("grandparent2"), "a")
 
194
        self.assertEqual(btree.path2id("grandparent2/parent2"), "b")
 
195
        self.assertEqual(btree.path2id("grandparent2/parent2/file"), "c")
 
196
 
 
197
        self.assertTrue(btree.path2id("grandparent2/parent") is None)
 
198
        self.assertTrue(btree.path2id("grandparent2/parent/file") is None)
 
199
 
 
200
        btree.note_rename("grandparent/parent/file",
 
201
                          "grandparent2/parent2/file2")
 
202
        self.assertEqual(btree.id2path("a"), "grandparent2")
 
203
        self.assertEqual(btree.id2path("b"), "grandparent2/parent2")
 
204
        self.assertEqual(btree.id2path("c"), "grandparent2/parent2/file2")
 
205
 
 
206
        self.assertEqual(btree.path2id("grandparent2"), "a")
 
207
        self.assertEqual(btree.path2id("grandparent2/parent2"), "b")
 
208
        self.assertEqual(btree.path2id("grandparent2/parent2/file2"), "c")
 
209
 
 
210
        self.assertTrue(btree.path2id("grandparent2/parent2/file") is None)
 
211
 
 
212
    def test_moves(self):
 
213
        """Ensure that file moves have the proper effect on children"""
 
214
        btree = self.make_tree_1()[0]
 
215
        btree.note_rename("grandparent/parent/file",
 
216
                          "grandparent/alt_parent/file")
 
217
        self.assertEqual(btree.id2path("c"), "grandparent/alt_parent/file")
 
218
        self.assertEqual(btree.path2id("grandparent/alt_parent/file"), "c")
 
219
        self.assertTrue(btree.path2id("grandparent/parent/file") is None)
 
220
 
 
221
    def unified_diff(self, old, new):
 
222
        out = StringIO()
 
223
        diff.internal_diff("old", old, "new", new, out)
 
224
        out.seek(0,0)
 
225
        return out.read()
 
226
 
 
227
    def make_tree_2(self):
 
228
        btree = self.make_tree_1()[0]
 
229
        btree.note_rename("grandparent/parent/file",
 
230
                          "grandparent/alt_parent/file")
 
231
        self.assertTrue(btree.id2path("e") is None)
 
232
        self.assertTrue(btree.path2id("grandparent/parent/file") is None)
 
233
        btree.note_id("e", "grandparent/parent/file")
 
234
        return btree
 
235
 
 
236
    def test_adds(self):
 
237
        """File/inventory adds"""
 
238
        btree = self.make_tree_2()
 
239
        add_patch = self.unified_diff([], ["Extra cheese\n"])
 
240
        btree.note_patch("grandparent/parent/file", add_patch)
 
241
        btree.note_id('f', 'grandparent/parent/symlink', kind='symlink')
 
242
        btree.note_target('grandparent/parent/symlink', 'venus')
 
243
        self.adds_test(btree)
 
244
 
 
245
    def adds_test(self, btree):
 
246
        self.assertEqual(btree.id2path("e"), "grandparent/parent/file")
 
247
        self.assertEqual(btree.path2id("grandparent/parent/file"), "e")
 
248
        self.assertEqual(btree.get_file("e").read(), "Extra cheese\n")
 
249
        self.assertEqual(btree.get_symlink_target('f'), 'venus')
 
250
 
 
251
    def test_adds2(self):
 
252
        """File/inventory adds, with patch-compatibile renames"""
 
253
        btree = self.make_tree_2()
 
254
        btree.contents_by_id = False
 
255
        add_patch = self.unified_diff(["Hello\n"], ["Extra cheese\n"])
 
256
        btree.note_patch("grandparent/parent/file", add_patch)
 
257
        btree.note_id('f', 'grandparent/parent/symlink', kind='symlink')
 
258
        btree.note_target('grandparent/parent/symlink', 'venus')
 
259
        self.adds_test(btree)
 
260
 
 
261
    def make_tree_3(self):
 
262
        btree, mtree = self.make_tree_1()
 
263
        mtree.add_file("e", "grandparent/parent/topping", "Anchovies\n")
 
264
        btree.note_rename("grandparent/parent/file",
 
265
                          "grandparent/alt_parent/file")
 
266
        btree.note_rename("grandparent/parent/topping",
 
267
                          "grandparent/alt_parent/stopping")
 
268
        return btree
 
269
 
 
270
    def get_file_test(self, btree):
 
271
        self.assertEqual(btree.get_file("e").read(), "Lemon\n")
 
272
        self.assertEqual(btree.get_file("c").read(), "Hello\n")
 
273
 
 
274
    def test_get_file(self):
 
275
        """Get file contents"""
 
276
        btree = self.make_tree_3()
 
277
        mod_patch = self.unified_diff(["Anchovies\n"], ["Lemon\n"])
 
278
        btree.note_patch("grandparent/alt_parent/stopping", mod_patch)
 
279
        self.get_file_test(btree)
 
280
 
 
281
    def test_get_file2(self):
 
282
        """Get file contents, with patch-compatibile renames"""
 
283
        btree = self.make_tree_3()
 
284
        btree.contents_by_id = False
 
285
        mod_patch = self.unified_diff([], ["Lemon\n"])
 
286
        btree.note_patch("grandparent/alt_parent/stopping", mod_patch)
 
287
        mod_patch = self.unified_diff([], ["Hello\n"])
 
288
        btree.note_patch("grandparent/alt_parent/file", mod_patch)
 
289
        self.get_file_test(btree)
 
290
 
 
291
    def test_delete(self):
 
292
        "Deletion by bundle"
 
293
        btree = self.make_tree_1()[0]
 
294
        self.assertEqual(btree.get_file("c").read(), "Hello\n")
 
295
        btree.note_deletion("grandparent/parent/file")
 
296
        self.assertTrue(btree.id2path("c") is None)
 
297
        self.assertTrue(btree.path2id("grandparent/parent/file") is None)
 
298
 
 
299
    def sorted_ids(self, tree):
 
300
        ids = list(tree)
 
301
        ids.sort()
 
302
        return ids
 
303
 
 
304
    def test_iteration(self):
 
305
        """Ensure that iteration through ids works properly"""
 
306
        btree = self.make_tree_1()[0]
 
307
        self.assertEqual(self.sorted_ids(btree),
 
308
            [inventory.ROOT_ID, 'a', 'b', 'c', 'd'])
 
309
        btree.note_deletion("grandparent/parent/file")
 
310
        btree.note_id("e", "grandparent/alt_parent/fool", kind="directory")
 
311
        btree.note_last_changed("grandparent/alt_parent/fool",
 
312
                                "revisionidiguess")
 
313
        self.assertEqual(self.sorted_ids(btree),
 
314
            [inventory.ROOT_ID, 'a', 'b', 'd', 'e'])
 
315
 
 
316
 
 
317
class BundleTester1(tests.TestCaseWithTransport):
 
318
 
 
319
    def test_mismatched_bundle(self):
 
320
        format = bzrdir.BzrDirMetaFormat1()
 
321
        format.repository_format = knitrepo.RepositoryFormatKnit3()
 
322
        serializer = BundleSerializerV08('0.8')
 
323
        b = self.make_branch('.', format=format)
 
324
        self.assertRaises(errors.IncompatibleBundleFormat, serializer.write,
 
325
                          b.repository, [], {}, StringIO())
 
326
 
 
327
    def test_matched_bundle(self):
 
328
        """Don't raise IncompatibleBundleFormat for knit2 and bundle0.9"""
 
329
        format = bzrdir.BzrDirMetaFormat1()
 
330
        format.repository_format = knitrepo.RepositoryFormatKnit3()
 
331
        serializer = BundleSerializerV09('0.9')
 
332
        b = self.make_branch('.', format=format)
 
333
        serializer.write(b.repository, [], {}, StringIO())
 
334
 
 
335
    def test_mismatched_model(self):
 
336
        """Try copying a bundle from knit2 to knit1"""
 
337
        format = bzrdir.BzrDirMetaFormat1()
 
338
        format.repository_format = knitrepo.RepositoryFormatKnit3()
 
339
        source = self.make_branch_and_tree('source', format=format)
 
340
        source.commit('one', rev_id='one-id')
 
341
        source.commit('two', rev_id='two-id')
 
342
        text = StringIO()
 
343
        write_bundle(source.branch.repository, 'two-id', 'null:', text,
 
344
                     format='0.9')
 
345
        text.seek(0)
 
346
 
 
347
        format = bzrdir.BzrDirMetaFormat1()
 
348
        format.repository_format = knitrepo.RepositoryFormatKnit1()
 
349
        target = self.make_branch('target', format=format)
 
350
        self.assertRaises(errors.IncompatibleRevision, install_bundle,
 
351
                          target.repository, read_bundle(text))
 
352
 
 
353
 
 
354
class BundleTester(object):
 
355
 
 
356
    def bzrdir_format(self):
 
357
        format = bzrdir.BzrDirMetaFormat1()
 
358
        format.repository_format = knitrepo.RepositoryFormatKnit1()
 
359
        return format
 
360
 
 
361
    def make_branch_and_tree(self, path, format=None):
 
362
        if format is None:
 
363
            format = self.bzrdir_format()
 
364
        return tests.TestCaseWithTransport.make_branch_and_tree(
 
365
            self, path, format)
 
366
 
 
367
    def make_branch(self, path, format=None):
 
368
        if format is None:
 
369
            format = self.bzrdir_format()
 
370
        return tests.TestCaseWithTransport.make_branch(self, path, format)
 
371
 
 
372
    def create_bundle_text(self, base_rev_id, rev_id):
 
373
        bundle_txt = StringIO()
 
374
        rev_ids = write_bundle(self.b1.repository, rev_id, base_rev_id,
 
375
                               bundle_txt, format=self.format)
 
376
        bundle_txt.seek(0)
 
377
        self.assertEqual(bundle_txt.readline(),
 
378
                         '# Bazaar revision bundle v%s\n' % self.format)
 
379
        self.assertEqual(bundle_txt.readline(), '#\n')
 
380
 
 
381
        rev = self.b1.repository.get_revision(rev_id)
 
382
        self.assertEqual(bundle_txt.readline().decode('utf-8'),
 
383
                         u'# message:\n')
 
384
        bundle_txt.seek(0)
 
385
        return bundle_txt, rev_ids
 
386
 
 
387
    def get_valid_bundle(self, base_rev_id, rev_id, checkout_dir=None):
 
388
        """Create a bundle from base_rev_id -> rev_id in built-in branch.
 
389
        Make sure that the text generated is valid, and that it
 
390
        can be applied against the base, and generate the same information.
 
391
 
 
392
        :return: The in-memory bundle
 
393
        """
 
394
        bundle_txt, rev_ids = self.create_bundle_text(base_rev_id, rev_id)
 
395
 
 
396
        # This should also validate the generated bundle
 
397
        bundle = read_bundle(bundle_txt)
 
398
        repository = self.b1.repository
 
399
        for bundle_rev in bundle.real_revisions:
 
400
            # These really should have already been checked when we read the
 
401
            # bundle, since it computes the sha1 hash for the revision, which
 
402
            # only will match if everything is okay, but lets be explicit about
 
403
            # it
 
404
            branch_rev = repository.get_revision(bundle_rev.revision_id)
 
405
            for a in ('inventory_sha1', 'revision_id', 'parent_ids',
 
406
                      'timestamp', 'timezone', 'message', 'committer',
 
407
                      'parent_ids', 'properties'):
 
408
                self.assertEqual(getattr(branch_rev, a),
 
409
                                 getattr(bundle_rev, a))
 
410
            self.assertEqual(len(branch_rev.parent_ids),
 
411
                             len(bundle_rev.parent_ids))
 
412
        self.assertEqual(rev_ids,
 
413
                         [r.revision_id for r in bundle.real_revisions])
 
414
        self.valid_apply_bundle(base_rev_id, bundle,
 
415
                                   checkout_dir=checkout_dir)
 
416
 
 
417
        return bundle
 
418
 
 
419
    def get_invalid_bundle(self, base_rev_id, rev_id):
 
420
        """Create a bundle from base_rev_id -> rev_id in built-in branch.
 
421
        Munge the text so that it's invalid.
 
422
 
 
423
        :return: The in-memory bundle
 
424
        """
 
425
        bundle_txt, rev_ids = self.create_bundle_text(base_rev_id, rev_id)
 
426
        new_text = bundle_txt.getvalue().replace('executable:no',
 
427
                                               'executable:yes')
 
428
        bundle_txt = StringIO(new_text)
 
429
        bundle = read_bundle(bundle_txt)
 
430
        self.valid_apply_bundle(base_rev_id, bundle)
 
431
        return bundle
 
432
 
 
433
    def test_non_bundle(self):
 
434
        self.assertRaises(errors.NotABundle,
 
435
                          read_bundle, StringIO('#!/bin/sh\n'))
 
436
 
 
437
    def test_malformed(self):
 
438
        self.assertRaises(errors.BadBundle, read_bundle,
 
439
                          StringIO('# Bazaar revision bundle v'))
 
440
 
 
441
    def test_crlf_bundle(self):
 
442
        try:
 
443
            read_bundle(StringIO('# Bazaar revision bundle v0.8\r\n'))
 
444
        except errors.BadBundle:
 
445
            # It is currently permitted for bundles with crlf line endings to
 
446
            # make read_bundle raise a BadBundle, but this should be fixed.
 
447
            # Anything else, especially NotABundle, is an error.
 
448
            pass
 
449
 
 
450
    def get_checkout(self, rev_id, checkout_dir=None):
 
451
        """Get a new tree, with the specified revision in it.
 
452
        """
 
453
 
 
454
        if checkout_dir is None:
 
455
            checkout_dir = osutils.mkdtemp(prefix='test-branch-', dir='.')
 
456
        else:
 
457
            if not os.path.exists(checkout_dir):
 
458
                os.mkdir(checkout_dir)
 
459
        tree = self.make_branch_and_tree(checkout_dir)
 
460
        s = StringIO()
 
461
        ancestors = write_bundle(self.b1.repository, rev_id, 'null:', s,
 
462
                                 format=self.format)
 
463
        s.seek(0)
 
464
        self.assertIsInstance(s.getvalue(), str)
 
465
        install_bundle(tree.branch.repository, read_bundle(s))
 
466
        for ancestor in ancestors:
 
467
            old = self.b1.repository.revision_tree(ancestor)
 
468
            new = tree.branch.repository.revision_tree(ancestor)
 
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()
 
491
        if not _mod_revision.is_null(rev_id):
 
492
            rh = self.b1.revision_history()
 
493
            tree.branch.set_revision_history(rh[:rh.index(rev_id)+1])
 
494
            tree.update()
 
495
            delta = tree.changes_from(self.b1.repository.revision_tree(rev_id))
 
496
            self.assertFalse(delta.has_changed(),
 
497
                             'Working tree has modifications: %s' % delta)
 
498
        return tree
 
499
 
 
500
    def valid_apply_bundle(self, base_rev_id, info, checkout_dir=None):
 
501
        """Get the base revision, apply the changes, and make
 
502
        sure everything matches the builtin branch.
 
503
        """
 
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):
 
512
        original_parents = to_tree.get_parent_ids()
 
513
        repository = to_tree.branch.repository
 
514
        original_parents = to_tree.get_parent_ids()
 
515
        self.assertIs(repository.has_revision(base_rev_id), True)
 
516
        for rev in info.real_revisions:
 
517
            self.assert_(not repository.has_revision(rev.revision_id),
 
518
                'Revision {%s} present before applying bundle'
 
519
                % rev.revision_id)
 
520
        merge_bundle(info, to_tree, True, merge.Merge3Merger, False, False)
 
521
 
 
522
        for rev in info.real_revisions:
 
523
            self.assert_(repository.has_revision(rev.revision_id),
 
524
                'Missing revision {%s} after applying bundle'
 
525
                % rev.revision_id)
 
526
 
 
527
        self.assert_(to_tree.branch.repository.has_revision(info.target))
 
528
        # Do we also want to verify that all the texts have been added?
 
529
 
 
530
        self.assertEqual(original_parents + [info.target],
 
531
            to_tree.get_parent_ids())
 
532
 
 
533
        rev = info.real_revisions[-1]
 
534
        base_tree = self.b1.repository.revision_tree(rev.revision_id)
 
535
        to_tree = to_tree.branch.repository.revision_tree(rev.revision_id)
 
536
 
 
537
        # TODO: make sure the target tree is identical to base tree
 
538
        #       we might also check the working tree.
 
539
 
 
540
        base_files = list(base_tree.list_files())
 
541
        to_files = list(to_tree.list_files())
 
542
        self.assertEqual(len(base_files), len(to_files))
 
543
        for base_file, to_file in zip(base_files, to_files):
 
544
            self.assertEqual(base_file, to_file)
 
545
 
 
546
        for path, status, kind, fileid, entry in base_files:
 
547
            # Check that the meta information is the same
 
548
            self.assertEqual(base_tree.get_file_size(fileid),
 
549
                    to_tree.get_file_size(fileid))
 
550
            self.assertEqual(base_tree.get_file_sha1(fileid),
 
551
                    to_tree.get_file_sha1(fileid))
 
552
            # Check that the contents are the same
 
553
            # This is pretty expensive
 
554
            # self.assertEqual(base_tree.get_file(fileid).read(),
 
555
            #         to_tree.get_file(fileid).read())
 
556
 
 
557
    def test_bundle(self):
 
558
        self.tree1 = self.make_branch_and_tree('b1')
 
559
        self.b1 = self.tree1.branch
 
560
 
 
561
        open('b1/one', 'wb').write('one\n')
 
562
        self.tree1.add('one')
 
563
        self.tree1.commit('add one', rev_id='a@cset-0-1')
 
564
 
 
565
        bundle = self.get_valid_bundle('null:', 'a@cset-0-1')
 
566
 
 
567
        # Make sure we can handle files with spaces, tabs, other
 
568
        # bogus characters
 
569
        self.build_tree([
 
570
                'b1/with space.txt'
 
571
                , 'b1/dir/'
 
572
                , 'b1/dir/filein subdir.c'
 
573
                , 'b1/dir/WithCaps.txt'
 
574
                , 'b1/dir/ pre space'
 
575
                , 'b1/sub/'
 
576
                , 'b1/sub/sub/'
 
577
                , 'b1/sub/sub/nonempty.txt'
 
578
                ])
 
579
        open('b1/sub/sub/emptyfile.txt', 'wb').close()
 
580
        open('b1/dir/nolastnewline.txt', 'wb').write('bloop')
 
581
        tt = TreeTransform(self.tree1)
 
582
        tt.new_file('executable', tt.root, '#!/bin/sh\n', 'exe-1', True)
 
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')
 
587
        self.tree1.add([
 
588
                  'dir'
 
589
                , 'dir/filein subdir.c'
 
590
                , 'dir/WithCaps.txt'
 
591
                , 'dir/ pre space'
 
592
                , 'dir/nolastnewline.txt'
 
593
                , 'sub'
 
594
                , 'sub/sub'
 
595
                , 'sub/sub/nonempty.txt'
 
596
                , 'sub/sub/emptyfile.txt'
 
597
                ])
 
598
        self.tree1.commit('add whitespace', rev_id='a@cset-0-2')
 
599
 
 
600
        bundle = self.get_valid_bundle('a@cset-0-1', 'a@cset-0-2')
 
601
 
 
602
        # Check a rollup bundle
 
603
        bundle = self.get_valid_bundle('null:', 'a@cset-0-2')
 
604
 
 
605
        # Now delete entries
 
606
        self.tree1.remove(
 
607
                ['sub/sub/nonempty.txt'
 
608
                , 'sub/sub/emptyfile.txt'
 
609
                , 'sub/sub'
 
610
                ])
 
611
        tt = TreeTransform(self.tree1)
 
612
        trans_id = tt.trans_id_tree_file_id('exe-1')
 
613
        tt.set_executability(False, trans_id)
 
614
        tt.apply()
 
615
        self.tree1.commit('removed', rev_id='a@cset-0-3')
 
616
 
 
617
        bundle = self.get_valid_bundle('a@cset-0-2', 'a@cset-0-3')
 
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
 
622
        bundle = self.get_valid_bundle('null:', 'a@cset-0-3')
 
623
 
 
624
        # Now move the directory
 
625
        self.tree1.rename_one('dir', 'sub/dir')
 
626
        self.tree1.commit('rename dir', rev_id='a@cset-0-4')
 
627
 
 
628
        bundle = self.get_valid_bundle('a@cset-0-3', 'a@cset-0-4')
 
629
        # Check a rollup bundle
 
630
        bundle = self.get_valid_bundle('null:', 'a@cset-0-4')
 
631
 
 
632
        # Modified files
 
633
        open('b1/sub/dir/WithCaps.txt', 'ab').write('\nAdding some text\n')
 
634
        open('b1/sub/dir/ pre space', 'ab').write(
 
635
             '\r\nAdding some\r\nDOS format lines\r\n')
 
636
        open('b1/sub/dir/nolastnewline.txt', 'ab').write('\n')
 
637
        self.tree1.rename_one('sub/dir/ pre space',
 
638
                              'sub/ start space')
 
639
        self.tree1.commit('Modified files', rev_id='a@cset-0-5')
 
640
        bundle = self.get_valid_bundle('a@cset-0-4', 'a@cset-0-5')
 
641
 
 
642
        self.tree1.rename_one('sub/dir/WithCaps.txt', 'temp')
 
643
        self.tree1.rename_one('with space.txt', 'WithCaps.txt')
 
644
        self.tree1.rename_one('temp', 'with space.txt')
 
645
        self.tree1.commit(u'swap filenames', rev_id='a@cset-0-6',
 
646
                          verbose=False)
 
647
        bundle = self.get_valid_bundle('a@cset-0-5', 'a@cset-0-6')
 
648
        other = self.get_checkout('a@cset-0-5')
 
649
        tree1_inv = self.tree1.branch.repository.get_inventory_xml(
 
650
            'a@cset-0-5')
 
651
        tree2_inv = other.branch.repository.get_inventory_xml('a@cset-0-5')
 
652
        self.assertEqualDiff(tree1_inv, tree2_inv)
 
653
        other.rename_one('sub/dir/nolastnewline.txt', 'sub/nolastnewline.txt')
 
654
        other.commit('rename file', rev_id='a@cset-0-6b')
 
655
        self.tree1.merge_from_branch(other.branch)
 
656
        self.tree1.commit(u'Merge', rev_id='a@cset-0-7',
 
657
                          verbose=False)
 
658
        bundle = self.get_valid_bundle('a@cset-0-6', 'a@cset-0-7')
 
659
 
 
660
    def _test_symlink_bundle(self, link_name, link_target, new_link_target):
 
661
        link_id = 'link-1'
 
662
 
 
663
        self.requireFeature(tests.SymlinkFeature)
 
664
        self.tree1 = self.make_branch_and_tree('b1')
 
665
        self.b1 = self.tree1.branch
 
666
 
 
667
        tt = TreeTransform(self.tree1)
 
668
        tt.new_symlink(link_name, tt.root, link_target, link_id)
 
669
        tt.apply()
 
670
        self.tree1.commit('add symlink', rev_id='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
 
 
677
        tt = TreeTransform(self.tree1)
 
678
        trans_id = tt.trans_id_tree_file_id(link_id)
 
679
        tt.adjust_path('link2', tt.root, trans_id)
 
680
        tt.delete_contents(trans_id)
 
681
        tt.create_symlink(new_link_target, trans_id)
 
682
        tt.apply()
 
683
        self.tree1.commit('rename and change symlink', rev_id='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
 
 
691
        tt = TreeTransform(self.tree1)
 
692
        trans_id = tt.trans_id_tree_file_id(link_id)
 
693
        tt.delete_contents(trans_id)
 
694
        tt.create_symlink('jupiter', trans_id)
 
695
        tt.apply()
 
696
        self.tree1.commit('just change symlink target', rev_id='l@cset-0-3')
 
697
        bundle = self.get_valid_bundle('l@cset-0-2', 'l@cset-0-3')
 
698
 
 
699
        tt = TreeTransform(self.tree1)
 
700
        trans_id = tt.trans_id_tree_file_id(link_id)
 
701
        tt.delete_contents(trans_id)
 
702
        tt.apply()
 
703
        self.tree1.commit('Delete symlink', rev_id='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}')
 
714
 
 
715
    def test_binary_bundle(self):
 
716
        self.tree1 = self.make_branch_and_tree('b1')
 
717
        self.b1 = self.tree1.branch
 
718
        tt = TreeTransform(self.tree1)
 
719
 
 
720
        # Add
 
721
        tt.new_file('file', tt.root, '\x00\n\x00\r\x01\n\x02\r\xff', 'binary-1')
 
722
        tt.new_file('file2', tt.root, '\x01\n\x02\r\x03\n\x04\r\xff',
 
723
            'binary-2')
 
724
        tt.apply()
 
725
        self.tree1.commit('add binary', rev_id='b@cset-0-1')
 
726
        self.get_valid_bundle('null:', 'b@cset-0-1')
 
727
 
 
728
        # Delete
 
729
        tt = TreeTransform(self.tree1)
 
730
        trans_id = tt.trans_id_tree_file_id('binary-1')
 
731
        tt.delete_contents(trans_id)
 
732
        tt.apply()
 
733
        self.tree1.commit('delete binary', rev_id='b@cset-0-2')
 
734
        self.get_valid_bundle('b@cset-0-1', 'b@cset-0-2')
 
735
 
 
736
        # Rename & modify
 
737
        tt = TreeTransform(self.tree1)
 
738
        trans_id = tt.trans_id_tree_file_id('binary-2')
 
739
        tt.adjust_path('file3', tt.root, trans_id)
 
740
        tt.delete_contents(trans_id)
 
741
        tt.create_file('file\rcontents\x00\n\x00', trans_id)
 
742
        tt.apply()
 
743
        self.tree1.commit('rename and modify binary', rev_id='b@cset-0-3')
 
744
        self.get_valid_bundle('b@cset-0-2', 'b@cset-0-3')
 
745
 
 
746
        # Modify
 
747
        tt = TreeTransform(self.tree1)
 
748
        trans_id = tt.trans_id_tree_file_id('binary-2')
 
749
        tt.delete_contents(trans_id)
 
750
        tt.create_file('\x00file\rcontents', trans_id)
 
751
        tt.apply()
 
752
        self.tree1.commit('just modify binary', rev_id='b@cset-0-4')
 
753
        self.get_valid_bundle('b@cset-0-3', 'b@cset-0-4')
 
754
 
 
755
        # Rollup
 
756
        self.get_valid_bundle('null:', 'b@cset-0-4')
 
757
 
 
758
    def test_last_modified(self):
 
759
        self.tree1 = self.make_branch_and_tree('b1')
 
760
        self.b1 = self.tree1.branch
 
761
        tt = TreeTransform(self.tree1)
 
762
        tt.new_file('file', tt.root, 'file', 'file')
 
763
        tt.apply()
 
764
        self.tree1.commit('create file', rev_id='a@lmod-0-1')
 
765
 
 
766
        tt = TreeTransform(self.tree1)
 
767
        trans_id = tt.trans_id_tree_file_id('file')
 
768
        tt.delete_contents(trans_id)
 
769
        tt.create_file('file2', trans_id)
 
770
        tt.apply()
 
771
        self.tree1.commit('modify text', rev_id='a@lmod-0-2a')
 
772
 
 
773
        other = self.get_checkout('a@lmod-0-1')
 
774
        tt = TreeTransform(other)
 
775
        trans_id = tt.trans_id_tree_file_id('file')
 
776
        tt.delete_contents(trans_id)
 
777
        tt.create_file('file2', trans_id)
 
778
        tt.apply()
 
779
        other.commit('modify text in another tree', rev_id='a@lmod-0-2b')
 
780
        self.tree1.merge_from_branch(other.branch)
 
781
        self.tree1.commit(u'Merge', rev_id='a@lmod-0-3',
 
782
                          verbose=False)
 
783
        self.tree1.commit(u'Merge', rev_id='a@lmod-0-4')
 
784
        bundle = self.get_valid_bundle('a@lmod-0-2a', 'a@lmod-0-4')
 
785
 
 
786
    def test_hide_history(self):
 
787
        self.tree1 = self.make_branch_and_tree('b1')
 
788
        self.b1 = self.tree1.branch
 
789
 
 
790
        open('b1/one', 'wb').write('one\n')
 
791
        self.tree1.add('one')
 
792
        self.tree1.commit('add file', rev_id='a@cset-0-1')
 
793
        open('b1/one', 'wb').write('two\n')
 
794
        self.tree1.commit('modify', rev_id='a@cset-0-2')
 
795
        open('b1/one', 'wb').write('three\n')
 
796
        self.tree1.commit('modify', rev_id='a@cset-0-3')
 
797
        bundle_file = StringIO()
 
798
        rev_ids = write_bundle(self.tree1.branch.repository, 'a@cset-0-3',
 
799
                               'a@cset-0-1', bundle_file, format=self.format)
 
800
        self.assertNotContainsRe(bundle_file.getvalue(), '\btwo\b')
 
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()
 
815
 
 
816
    def test_unicode_bundle(self):
 
817
        self.requireFeature(tests.UnicodeFilenameFeature)
 
818
        # Handle international characters
 
819
        os.mkdir('b1')
 
820
        f = open(u'b1/with Dod\N{Euro Sign}', 'wb')
 
821
 
 
822
        self.tree1 = self.make_branch_and_tree('b1')
 
823
        self.b1 = self.tree1.branch
 
824
 
 
825
        f.write((u'A file\n'
 
826
            u'With international man of mystery\n'
 
827
            u'William Dod\xe9\n').encode('utf-8'))
 
828
        f.close()
 
829
 
 
830
        self.tree1.add([u'with Dod\N{Euro Sign}'], ['withdod-id'])
 
831
        self.tree1.commit(u'i18n commit from William Dod\xe9',
 
832
                          rev_id='i18n-1', committer=u'William Dod\xe9')
 
833
 
 
834
        # Add
 
835
        bundle = self.get_valid_bundle('null:', 'i18n-1')
 
836
 
 
837
        # Modified
 
838
        f = open(u'b1/with Dod\N{Euro Sign}', 'wb')
 
839
        f.write(u'Modified \xb5\n'.encode('utf8'))
 
840
        f.close()
 
841
        self.tree1.commit(u'modified', rev_id='i18n-2')
 
842
 
 
843
        bundle = self.get_valid_bundle('i18n-1', 'i18n-2')
 
844
 
 
845
        # Renamed
 
846
        self.tree1.rename_one(u'with Dod\N{Euro Sign}', u'B\N{Euro Sign}gfors')
 
847
        self.tree1.commit(u'renamed, the new i18n man', rev_id='i18n-3',
 
848
                          committer=u'Erik B\xe5gfors')
 
849
 
 
850
        bundle = self.get_valid_bundle('i18n-2', 'i18n-3')
 
851
 
 
852
        # Removed
 
853
        self.tree1.remove([u'B\N{Euro Sign}gfors'])
 
854
        self.tree1.commit(u'removed', rev_id='i18n-4')
 
855
 
 
856
        bundle = self.get_valid_bundle('i18n-3', 'i18n-4')
 
857
 
 
858
        # Rollup
 
859
        bundle = self.get_valid_bundle('null:', 'i18n-4')
 
860
 
 
861
 
 
862
    def test_whitespace_bundle(self):
 
863
        if sys.platform in ('win32', 'cygwin'):
 
864
            raise tests.TestSkipped('Windows doesn\'t support filenames'
 
865
                                    ' with tabs or trailing spaces')
 
866
        self.tree1 = self.make_branch_and_tree('b1')
 
867
        self.b1 = self.tree1.branch
 
868
 
 
869
        self.build_tree(['b1/trailing space '])
 
870
        self.tree1.add(['trailing space '])
 
871
        # TODO: jam 20060701 Check for handling files with '\t' characters
 
872
        #       once we actually support them
 
873
 
 
874
        # Added
 
875
        self.tree1.commit('funky whitespace', rev_id='white-1')
 
876
 
 
877
        bundle = self.get_valid_bundle('null:', 'white-1')
 
878
 
 
879
        # Modified
 
880
        open('b1/trailing space ', 'ab').write('add some text\n')
 
881
        self.tree1.commit('add text', rev_id='white-2')
 
882
 
 
883
        bundle = self.get_valid_bundle('white-1', 'white-2')
 
884
 
 
885
        # Renamed
 
886
        self.tree1.rename_one('trailing space ', ' start and end space ')
 
887
        self.tree1.commit('rename', rev_id='white-3')
 
888
 
 
889
        bundle = self.get_valid_bundle('white-2', 'white-3')
 
890
 
 
891
        # Removed
 
892
        self.tree1.remove([' start and end space '])
 
893
        self.tree1.commit('removed', rev_id='white-4')
 
894
 
 
895
        bundle = self.get_valid_bundle('white-3', 'white-4')
 
896
 
 
897
        # Now test a complet roll-up
 
898
        bundle = self.get_valid_bundle('null:', 'white-4')
 
899
 
 
900
    def test_alt_timezone_bundle(self):
 
901
        self.tree1 = self.make_branch_and_memory_tree('b1')
 
902
        self.b1 = self.tree1.branch
 
903
        builder = treebuilder.TreeBuilder()
 
904
 
 
905
        self.tree1.lock_write()
 
906
        builder.start_tree(self.tree1)
 
907
        builder.build(['newfile'])
 
908
        builder.finish_tree()
 
909
 
 
910
        # Asia/Colombo offset = 5 hours 30 minutes
 
911
        self.tree1.commit('non-hour offset timezone', rev_id='tz-1',
 
912
                          timezone=19800, timestamp=1152544886.0)
 
913
 
 
914
        bundle = self.get_valid_bundle('null:', 'tz-1')
 
915
 
 
916
        rev = bundle.revisions[0]
 
917
        self.assertEqual('Mon 2006-07-10 20:51:26.000000000 +0530', rev.date)
 
918
        self.assertEqual(19800, rev.timezone)
 
919
        self.assertEqual(1152544886.0, rev.timestamp)
 
920
        self.tree1.unlock()
 
921
 
 
922
    def test_bundle_root_id(self):
 
923
        self.tree1 = self.make_branch_and_tree('b1')
 
924
        self.b1 = self.tree1.branch
 
925
        self.tree1.commit('message', rev_id='revid1')
 
926
        bundle = self.get_valid_bundle('null:', 'revid1')
 
927
        tree = self.get_bundle_tree(bundle, 'revid1')
 
928
        self.assertEqual('revid1', tree.inventory.root.revision)
 
929
 
 
930
    def test_install_revisions(self):
 
931
        self.tree1 = self.make_branch_and_tree('b1')
 
932
        self.b1 = self.tree1.branch
 
933
        self.tree1.commit('message', rev_id='rev2a')
 
934
        bundle = self.get_valid_bundle('null:', 'rev2a')
 
935
        branch2 = self.make_branch('b2')
 
936
        self.assertFalse(branch2.repository.has_revision('rev2a'))
 
937
        target_revision = bundle.install_revisions(branch2.repository)
 
938
        self.assertTrue(branch2.repository.has_revision('rev2a'))
 
939
        self.assertEqual('rev2a', target_revision)
 
940
 
 
941
    def test_bundle_empty_property(self):
 
942
        """Test serializing revision properties with an empty value."""
 
943
        tree = self.make_branch_and_memory_tree('tree')
 
944
        tree.lock_write()
 
945
        self.addCleanup(tree.unlock)
 
946
        tree.add([''], ['TREE_ROOT'])
 
947
        tree.commit('One', revprops={'one':'two', 'empty':''}, rev_id='rev1')
 
948
        self.b1 = tree.branch
 
949
        bundle_sio, revision_ids = self.create_bundle_text('null:', 'rev1')
 
950
        bundle = read_bundle(bundle_sio)
 
951
        revision_info = bundle.revisions[0]
 
952
        self.assertEqual('rev1', revision_info.revision_id)
 
953
        rev = revision_info.as_revision()
 
954
        self.assertEqual({'branch-nick':'tree', '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')
 
1147
        self.assertContainsRe(bundle_sio.getvalue(),
 
1148
                              '# properties:\n'
 
1149
                              '#   branch-nick: tree\n'
 
1150
                              '#   empty: \n'
 
1151
                              '#   one: two\n'
 
1152
                             )
 
1153
        bundle = read_bundle(bundle_sio)
 
1154
        revision_info = bundle.revisions[0]
 
1155
        self.assertEqual('rev1', revision_info.revision_id)
 
1156
        rev = revision_info.as_revision()
 
1157
        self.assertEqual({'branch-nick':'tree', 'empty':'', 'one':'two'},
 
1158
                         rev.properties)
 
1159
 
 
1160
    def get_bundle_tree(self, bundle, revision_id):
 
1161
        repository = self.make_repository('repo')
 
1162
        return bundle.revision_tree(repository, 'revid1')
 
1163
 
 
1164
    def test_bundle_empty_property_alt(self):
 
1165
        """Test serializing revision properties with an empty value.
 
1166
 
 
1167
        Older readers had a bug when reading an empty property.
 
1168
        They assumed that all keys ended in ': \n'. However they would write an
 
1169
        empty value as ':\n'. This tests make sure that all newer bzr versions
 
1170
        can handle th second form.
 
1171
        """
 
1172
        tree = self.make_branch_and_memory_tree('tree')
 
1173
        tree.lock_write()
 
1174
        self.addCleanup(tree.unlock)
 
1175
        tree.add([''], ['TREE_ROOT'])
 
1176
        tree.commit('One', revprops={'one':'two', 'empty':''}, rev_id='rev1')
 
1177
        self.b1 = tree.branch
 
1178
        bundle_sio, revision_ids = self.create_bundle_text('null:', 'rev1')
 
1179
        txt = bundle_sio.getvalue()
 
1180
        loc = txt.find('#   empty: ') + len('#   empty:')
 
1181
        # Create a new bundle, which strips the trailing space after empty
 
1182
        bundle_sio = StringIO(txt[:loc] + txt[loc+1:])
 
1183
 
 
1184
        self.assertContainsRe(bundle_sio.getvalue(),
 
1185
                              '# properties:\n'
 
1186
                              '#   branch-nick: tree\n'
 
1187
                              '#   empty:\n'
 
1188
                              '#   one: two\n'
 
1189
                             )
 
1190
        bundle = read_bundle(bundle_sio)
 
1191
        revision_info = bundle.revisions[0]
 
1192
        self.assertEqual('rev1', revision_info.revision_id)
 
1193
        rev = revision_info.as_revision()
 
1194
        self.assertEqual({'branch-nick':'tree', 'empty':'', 'one':'two'},
 
1195
                         rev.properties)
 
1196
 
 
1197
    def test_bundle_sorted_properties(self):
 
1198
        """For stability the writer should write properties in sorted order."""
 
1199
        tree = self.make_branch_and_memory_tree('tree')
 
1200
        tree.lock_write()
 
1201
        self.addCleanup(tree.unlock)
 
1202
 
 
1203
        tree.add([''], ['TREE_ROOT'])
 
1204
        tree.commit('One', rev_id='rev1',
 
1205
                    revprops={'a':'4', 'b':'3', 'c':'2', 'd':'1'})
 
1206
        self.b1 = tree.branch
 
1207
        bundle_sio, revision_ids = self.create_bundle_text('null:', 'rev1')
 
1208
        self.assertContainsRe(bundle_sio.getvalue(),
 
1209
                              '# properties:\n'
 
1210
                              '#   a: 4\n'
 
1211
                              '#   b: 3\n'
 
1212
                              '#   branch-nick: tree\n'
 
1213
                              '#   c: 2\n'
 
1214
                              '#   d: 1\n'
 
1215
                             )
 
1216
        bundle = read_bundle(bundle_sio)
 
1217
        revision_info = bundle.revisions[0]
 
1218
        self.assertEqual('rev1', revision_info.revision_id)
 
1219
        rev = revision_info.as_revision()
 
1220
        self.assertEqual({'branch-nick':'tree', 'a':'4', 'b':'3', 'c':'2',
 
1221
                          'd':'1'}, rev.properties)
 
1222
 
 
1223
    def test_bundle_unicode_properties(self):
 
1224
        """We should be able to round trip a non-ascii property."""
 
1225
        tree = self.make_branch_and_memory_tree('tree')
 
1226
        tree.lock_write()
 
1227
        self.addCleanup(tree.unlock)
 
1228
 
 
1229
        tree.add([''], ['TREE_ROOT'])
 
1230
        # Revisions themselves do not require anything about revision property
 
1231
        # keys, other than that they are a basestring, and do not contain
 
1232
        # whitespace.
 
1233
        # However, Testaments assert than they are str(), and thus should not
 
1234
        # be Unicode.
 
1235
        tree.commit('One', rev_id='rev1',
 
1236
                    revprops={'omega':u'\u03a9', 'alpha':u'\u03b1'})
 
1237
        self.b1 = tree.branch
 
1238
        bundle_sio, revision_ids = self.create_bundle_text('null:', 'rev1')
 
1239
        self.assertContainsRe(bundle_sio.getvalue(),
 
1240
                              '# properties:\n'
 
1241
                              '#   alpha: \xce\xb1\n'
 
1242
                              '#   branch-nick: tree\n'
 
1243
                              '#   omega: \xce\xa9\n'
 
1244
                             )
 
1245
        bundle = read_bundle(bundle_sio)
 
1246
        revision_info = bundle.revisions[0]
 
1247
        self.assertEqual('rev1', revision_info.revision_id)
 
1248
        rev = revision_info.as_revision()
 
1249
        self.assertEqual({'branch-nick':'tree', 'omega':u'\u03a9',
 
1250
                          'alpha':u'\u03b1'}, rev.properties)
 
1251
 
 
1252
 
 
1253
class V09BundleKnit2Tester(V08BundleTester):
 
1254
 
 
1255
    format = '0.9'
 
1256
 
 
1257
    def bzrdir_format(self):
 
1258
        format = bzrdir.BzrDirMetaFormat1()
 
1259
        format.repository_format = knitrepo.RepositoryFormatKnit3()
 
1260
        return format
 
1261
 
 
1262
 
 
1263
class V09BundleKnit1Tester(V08BundleTester):
 
1264
 
 
1265
    format = '0.9'
 
1266
 
 
1267
    def bzrdir_format(self):
 
1268
        format = bzrdir.BzrDirMetaFormat1()
 
1269
        format.repository_format = knitrepo.RepositoryFormatKnit1()
 
1270
        return format
 
1271
 
 
1272
 
 
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):
 
1433
 
 
1434
    def build_test_bundle(self):
 
1435
        wt = self.make_branch_and_tree('b1')
 
1436
 
 
1437
        self.build_tree(['b1/one'])
 
1438
        wt.add('one')
 
1439
        wt.commit('add one', rev_id='a@cset-0-1')
 
1440
        self.build_tree(['b1/two'])
 
1441
        wt.add('two')
 
1442
        wt.commit('add two', rev_id='a@cset-0-2',
 
1443
                  revprops={'branch-nick':'test'})
 
1444
 
 
1445
        bundle_txt = StringIO()
 
1446
        rev_ids = write_bundle(wt.branch.repository, 'a@cset-0-2',
 
1447
                               'a@cset-0-1', bundle_txt, self.format)
 
1448
        self.assertEqual(set(['a@cset-0-2']), set(rev_ids))
 
1449
        bundle_txt.seek(0, 0)
 
1450
        return bundle_txt
 
1451
 
 
1452
    def check_valid(self, bundle):
 
1453
        """Check that after whatever munging, the final object is valid."""
 
1454
        self.assertEqual(['a@cset-0-2'],
 
1455
            [r.revision_id for r in bundle.real_revisions])
 
1456
 
 
1457
    def test_extra_whitespace(self):
 
1458
        bundle_txt = self.build_test_bundle()
 
1459
 
 
1460
        # Seek to the end of the file
 
1461
        # Adding one extra newline used to give us
 
1462
        # TypeError: float() argument must be a string or a number
 
1463
        bundle_txt.seek(0, 2)
 
1464
        bundle_txt.write('\n')
 
1465
        bundle_txt.seek(0)
 
1466
 
 
1467
        bundle = read_bundle(bundle_txt)
 
1468
        self.check_valid(bundle)
 
1469
 
 
1470
    def test_extra_whitespace_2(self):
 
1471
        bundle_txt = self.build_test_bundle()
 
1472
 
 
1473
        # Seek to the end of the file
 
1474
        # Adding two extra newlines used to give us
 
1475
        # MalformedPatches: The first line of all patches should be ...
 
1476
        bundle_txt.seek(0, 2)
 
1477
        bundle_txt.write('\n\n')
 
1478
        bundle_txt.seek(0)
 
1479
 
 
1480
        bundle = read_bundle(bundle_txt)
 
1481
        self.check_valid(bundle)
 
1482
 
 
1483
 
 
1484
class MungedBundleTesterV09(tests.TestCaseWithTransport, MungedBundleTester):
 
1485
 
 
1486
    format = '0.9'
 
1487
 
 
1488
    def test_missing_trailing_whitespace(self):
 
1489
        bundle_txt = self.build_test_bundle()
 
1490
 
 
1491
        # Remove a trailing newline, it shouldn't kill the parser
 
1492
        raw = bundle_txt.getvalue()
 
1493
        # The contents of the bundle don't have to be this, but this
 
1494
        # test is concerned with the exact case where the serializer
 
1495
        # creates a blank line at the end, and fails if that
 
1496
        # line is stripped
 
1497
        self.assertEqual('\n\n', raw[-2:])
 
1498
        bundle_txt = StringIO(raw[:-1])
 
1499
 
 
1500
        bundle = read_bundle(bundle_txt)
 
1501
        self.check_valid(bundle)
 
1502
 
 
1503
    def test_opening_text(self):
 
1504
        bundle_txt = self.build_test_bundle()
 
1505
 
 
1506
        bundle_txt = StringIO("Some random\nemail comments\n"
 
1507
                              + bundle_txt.getvalue())
 
1508
 
 
1509
        bundle = read_bundle(bundle_txt)
 
1510
        self.check_valid(bundle)
 
1511
 
 
1512
    def test_trailing_text(self):
 
1513
        bundle_txt = self.build_test_bundle()
 
1514
 
 
1515
        bundle_txt = StringIO(bundle_txt.getvalue() +
 
1516
                              "Some trailing\nrandom\ntext\n")
 
1517
 
 
1518
        bundle = read_bundle(bundle_txt)
 
1519
        self.check_valid(bundle)
 
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