/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: 2008-11-25 18:51:48 UTC
  • mto: This revision was merged to the branch mainline in revision 3854.
  • Revision ID: john@arbash-meinel.com-20081125185148-jsfkqnzfjjqsleds
It seems we have some direct tests that don't use strings and expect a value error as well.

They would be sanitized later on by Revision. We could use that code, but this test
depends on the serializer, which Revision wouldn't know about.

Show diffs side-by-side

added added

removed removed

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