~bzr-pqm/bzr/bzr.dev

« back to all changes in this revision

Viewing changes to bzrlib/tests/test_repository.py

NEWS section template into a separate file

Show diffs side-by-side

added added

removed removed

Lines of Context:
1
 
# Copyright (C) 2006, 2007 Canonical Ltd
 
1
# Copyright (C) 2006-2010 Canonical Ltd
2
2
#
3
3
# This program is free software; you can redistribute it and/or modify
4
4
# it under the terms of the GNU General Public License as published by
12
12
#
13
13
# You should have received a copy of the GNU General Public License
14
14
# along with this program; if not, write to the Free Software
15
 
# Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA  02111-1307  USA
 
15
# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA
16
16
 
17
17
"""Tests for the Repository facility that are not interface tests.
18
18
 
19
 
For interface tests see tests/repository_implementations/*.py.
 
19
For interface tests see tests/per_repository/*.py.
20
20
 
21
21
For concrete class tests see this file, and for storage formats tests
22
22
also see this file.
25
25
from stat import S_ISDIR
26
26
from StringIO import StringIO
27
27
 
28
 
from bzrlib import symbol_versioning
29
28
import bzrlib
30
 
import bzrlib.bzrdir as bzrdir
31
 
import bzrlib.errors as errors
32
29
from bzrlib.errors import (NotBranchError,
33
30
                           NoSuchFile,
34
31
                           UnknownFormatError,
35
32
                           UnsupportedFormatError,
36
33
                           )
 
34
from bzrlib import (
 
35
    graph,
 
36
    tests,
 
37
    )
 
38
from bzrlib.branchbuilder import BranchBuilder
 
39
from bzrlib.btree_index import BTreeBuilder, BTreeGraphIndex
 
40
from bzrlib.index import GraphIndex, InMemoryGraphIndex
37
41
from bzrlib.repository import RepositoryFormat
 
42
from bzrlib.smart import server
38
43
from bzrlib.tests import (
39
44
    TestCase,
40
45
    TestCaseWithTransport,
 
46
    TestSkipped,
41
47
    test_knit,
42
48
    )
43
 
from bzrlib.transport import get_transport
 
49
from bzrlib.transport import (
 
50
    fakenfs,
 
51
    get_transport,
 
52
    )
44
53
from bzrlib.transport.memory import MemoryServer
45
 
from bzrlib.util import bencode
46
54
from bzrlib import (
 
55
    bencode,
 
56
    bzrdir,
 
57
    errors,
 
58
    inventory,
 
59
    osutils,
 
60
    progress,
47
61
    repository,
 
62
    revision as _mod_revision,
 
63
    symbol_versioning,
48
64
    upgrade,
 
65
    versionedfile,
49
66
    workingtree,
50
67
    )
51
 
from bzrlib.repofmt import knitrepo, weaverepo
 
68
from bzrlib.repofmt import (
 
69
    groupcompress_repo,
 
70
    knitrepo,
 
71
    pack_repo,
 
72
    weaverepo,
 
73
    )
52
74
 
53
75
 
54
76
class TestDefaultFormat(TestCase):
83
105
class SampleRepositoryFormat(repository.RepositoryFormat):
84
106
    """A sample format
85
107
 
86
 
    this format is initializable, unsupported to aid in testing the 
 
108
    this format is initializable, unsupported to aid in testing the
87
109
    open and open(unsupported=True) routines.
88
110
    """
89
111
 
110
132
    def test_find_format(self):
111
133
        # is the right format object found for a repository?
112
134
        # create a branch with a few known format objects.
113
 
        # this is not quite the same as 
 
135
        # this is not quite the same as
114
136
        self.build_tree(["foo/", "bar/"])
115
137
        def check_format(format, url):
116
138
            dir = format._matchingbzrdir.initialize(url)
119
141
            found_format = repository.RepositoryFormat.find_format(dir)
120
142
            self.failUnless(isinstance(found_format, format.__class__))
121
143
        check_format(weaverepo.RepositoryFormat7(), "bar")
122
 
        
 
144
 
123
145
    def test_find_format_no_repository(self):
124
146
        dir = bzrdir.BzrDirMetaFormat1().initialize(self.get_url())
125
147
        self.assertRaises(errors.NoRepositoryPresent,
151
173
 
152
174
class TestFormat6(TestCaseWithTransport):
153
175
 
 
176
    def test_attribute__fetch_order(self):
 
177
        """Weaves need topological data insertion."""
 
178
        control = bzrdir.BzrDirFormat6().initialize(self.get_url())
 
179
        repo = weaverepo.RepositoryFormat6().initialize(control)
 
180
        self.assertEqual('topological', repo._format._fetch_order)
 
181
 
 
182
    def test_attribute__fetch_uses_deltas(self):
 
183
        """Weaves do not reuse deltas."""
 
184
        control = bzrdir.BzrDirFormat6().initialize(self.get_url())
 
185
        repo = weaverepo.RepositoryFormat6().initialize(control)
 
186
        self.assertEqual(False, repo._format._fetch_uses_deltas)
 
187
 
 
188
    def test_attribute__fetch_reconcile(self):
 
189
        """Weave repositories need a reconcile after fetch."""
 
190
        control = bzrdir.BzrDirFormat6().initialize(self.get_url())
 
191
        repo = weaverepo.RepositoryFormat6().initialize(control)
 
192
        self.assertEqual(True, repo._format._fetch_reconcile)
 
193
 
154
194
    def test_no_ancestry_weave(self):
155
195
        control = bzrdir.BzrDirFormat6().initialize(self.get_url())
156
196
        repo = weaverepo.RepositoryFormat6().initialize(control)
160
200
                          control.transport.get,
161
201
                          'ancestry.weave')
162
202
 
 
203
    def test_supports_external_lookups(self):
 
204
        control = bzrdir.BzrDirFormat6().initialize(self.get_url())
 
205
        repo = weaverepo.RepositoryFormat6().initialize(control)
 
206
        self.assertFalse(repo._format.supports_external_lookups)
 
207
 
163
208
 
164
209
class TestFormat7(TestCaseWithTransport):
165
 
    
 
210
 
 
211
    def test_attribute__fetch_order(self):
 
212
        """Weaves need topological data insertion."""
 
213
        control = bzrdir.BzrDirMetaFormat1().initialize(self.get_url())
 
214
        repo = weaverepo.RepositoryFormat7().initialize(control)
 
215
        self.assertEqual('topological', repo._format._fetch_order)
 
216
 
 
217
    def test_attribute__fetch_uses_deltas(self):
 
218
        """Weaves do not reuse deltas."""
 
219
        control = bzrdir.BzrDirMetaFormat1().initialize(self.get_url())
 
220
        repo = weaverepo.RepositoryFormat7().initialize(control)
 
221
        self.assertEqual(False, repo._format._fetch_uses_deltas)
 
222
 
 
223
    def test_attribute__fetch_reconcile(self):
 
224
        """Weave repositories need a reconcile after fetch."""
 
225
        control = bzrdir.BzrDirMetaFormat1().initialize(self.get_url())
 
226
        repo = weaverepo.RepositoryFormat7().initialize(control)
 
227
        self.assertEqual(True, repo._format._fetch_reconcile)
 
228
 
166
229
    def test_disk_layout(self):
167
230
        control = bzrdir.BzrDirMetaFormat1().initialize(self.get_url())
168
231
        repo = weaverepo.RepositoryFormat7().initialize(control)
184
247
                             'w\n'
185
248
                             'W\n',
186
249
                             t.get('inventory.weave').read())
 
250
        # Creating a file with id Foo:Bar results in a non-escaped file name on
 
251
        # disk.
 
252
        control.create_branch()
 
253
        tree = control.create_workingtree()
 
254
        tree.add(['foo'], ['Foo:Bar'], ['file'])
 
255
        tree.put_file_bytes_non_atomic('Foo:Bar', 'content\n')
 
256
        tree.commit('first post', rev_id='first')
 
257
        self.assertEqualDiff(
 
258
            '# bzr weave file v5\n'
 
259
            'i\n'
 
260
            '1 7fe70820e08a1aac0ef224d9c66ab66831cc4ab1\n'
 
261
            'n first\n'
 
262
            '\n'
 
263
            'w\n'
 
264
            '{ 0\n'
 
265
            '. content\n'
 
266
            '}\n'
 
267
            'W\n',
 
268
            t.get('weaves/74/Foo%3ABar.weave').read())
187
269
 
188
270
    def test_shared_disk_layout(self):
189
271
        control = bzrdir.BzrDirMetaFormat1().initialize(self.get_url())
212
294
        control = bzrdir.BzrDirMetaFormat1().initialize(self.get_url())
213
295
        repo = weaverepo.RepositoryFormat7().initialize(control, shared=True)
214
296
        t = control.get_repository_transport(None)
215
 
        # TODO: Should check there is a 'lock' toplevel directory, 
 
297
        # TODO: Should check there is a 'lock' toplevel directory,
216
298
        # regardless of contents
217
299
        self.assertFalse(t.has('lock/held/info'))
218
300
        repo.lock_write()
264
346
                             'W\n',
265
347
                             t.get('inventory.weave').read())
266
348
 
 
349
    def test_supports_external_lookups(self):
 
350
        control = bzrdir.BzrDirMetaFormat1().initialize(self.get_url())
 
351
        repo = weaverepo.RepositoryFormat7().initialize(control)
 
352
        self.assertFalse(repo._format.supports_external_lookups)
 
353
 
267
354
 
268
355
class TestFormatKnit1(TestCaseWithTransport):
269
 
    
 
356
 
 
357
    def test_attribute__fetch_order(self):
 
358
        """Knits need topological data insertion."""
 
359
        repo = self.make_repository('.',
 
360
                format=bzrdir.format_registry.get('knit')())
 
361
        self.assertEqual('topological', repo._format._fetch_order)
 
362
 
 
363
    def test_attribute__fetch_uses_deltas(self):
 
364
        """Knits reuse deltas."""
 
365
        repo = self.make_repository('.',
 
366
                format=bzrdir.format_registry.get('knit')())
 
367
        self.assertEqual(True, repo._format._fetch_uses_deltas)
 
368
 
270
369
    def test_disk_layout(self):
271
370
        control = bzrdir.BzrDirMetaFormat1().initialize(self.get_url())
272
371
        repo = knitrepo.RepositoryFormatKnit1().initialize(control)
286
385
        # self.assertEqualDiff('', t.get('lock').read())
287
386
        self.assertTrue(S_ISDIR(t.stat('knits').st_mode))
288
387
        self.check_knits(t)
 
388
        # Check per-file knits.
 
389
        branch = control.create_branch()
 
390
        tree = control.create_workingtree()
 
391
        tree.add(['foo'], ['Nasty-IdC:'], ['file'])
 
392
        tree.put_file_bytes_non_atomic('Nasty-IdC:', '')
 
393
        tree.commit('1st post', rev_id='foo')
 
394
        self.assertHasKnit(t, 'knits/e8/%254easty-%2549d%2543%253a',
 
395
            '\nfoo fulltext 0 81  :')
289
396
 
290
 
    def assertHasKnit(self, t, knit_name):
 
397
    def assertHasKnit(self, t, knit_name, extra_content=''):
291
398
        """Assert that knit_name exists on t."""
292
 
        self.assertEqualDiff('# bzr knit index 8\n',
 
399
        self.assertEqualDiff('# bzr knit index 8\n' + extra_content,
293
400
                             t.get(knit_name + '.kndx').read())
294
 
        # no default content
295
 
        self.assertTrue(t.has(knit_name + '.knit'))
296
401
 
297
402
    def check_knits(self, t):
298
403
        """check knit content for a repository."""
342
447
        self.assertTrue(S_ISDIR(t.stat('knits').st_mode))
343
448
        self.check_knits(t)
344
449
 
345
 
 
346
 
class KnitRepositoryStreamTests(test_knit.KnitTests):
347
 
    """Tests for knitrepo._get_stream_as_bytes."""
348
 
 
349
 
    def test_get_stream_as_bytes(self):
350
 
        # Make a simple knit
351
 
        k1 = self.make_test_knit()
352
 
        k1.add_lines('text-a', [], test_knit.split_lines(test_knit.TEXT_1))
353
 
        
354
 
        # Serialise it, check the output.
355
 
        bytes = knitrepo._get_stream_as_bytes(k1, ['text-a'])
356
 
        data = bencode.bdecode(bytes)
357
 
        format, record = data
358
 
        self.assertEqual('knit-plain', format)
359
 
        self.assertEqual(['text-a', ['fulltext'], []], record[:3])
360
 
        self.assertRecordContentEqual(k1, 'text-a', record[3])
361
 
 
362
 
    def test_get_stream_as_bytes_all(self):
363
 
        """Get a serialised data stream for all the records in a knit.
364
 
 
365
 
        Much like test_get_stream_all, except for get_stream_as_bytes.
 
450
    def test_deserialise_sets_root_revision(self):
 
451
        """We must have a inventory.root.revision
 
452
 
 
453
        Old versions of the XML5 serializer did not set the revision_id for
 
454
        the whole inventory. So we grab the one from the expected text. Which
 
455
        is valid when the api is not being abused.
366
456
        """
367
 
        k1 = self.make_test_knit()
368
 
        # Insert the same data as BasicKnitTests.test_knit_join, as they seem
369
 
        # to cover a range of cases (no parents, one parent, multiple parents).
370
 
        test_data = [
371
 
            ('text-a', [], test_knit.TEXT_1),
372
 
            ('text-b', ['text-a'], test_knit.TEXT_1),
373
 
            ('text-c', [], test_knit.TEXT_1),
374
 
            ('text-d', ['text-c'], test_knit.TEXT_1),
375
 
            ('text-m', ['text-b', 'text-d'], test_knit.TEXT_1),
376
 
           ]
377
 
        expected_data_list = [
378
 
            # version, options, parents
379
 
            ('text-a', ['fulltext'], []),
380
 
            ('text-b', ['line-delta'], ['text-a']),
381
 
            ('text-c', ['fulltext'], []),
382
 
            ('text-d', ['line-delta'], ['text-c']),
383
 
            ('text-m', ['line-delta'], ['text-b', 'text-d']),
384
 
            ]
385
 
        for version_id, parents, lines in test_data:
386
 
            k1.add_lines(version_id, parents, test_knit.split_lines(lines))
387
 
 
388
 
        bytes = knitrepo._get_stream_as_bytes(
389
 
            k1, ['text-a', 'text-b', 'text-c', 'text-d', 'text-m'])
390
 
 
391
 
        data = bencode.bdecode(bytes)
392
 
        format = data.pop(0)
393
 
        self.assertEqual('knit-plain', format)
394
 
 
395
 
        for expected, actual in zip(expected_data_list, data):
396
 
            expected_version = expected[0]
397
 
            expected_options = expected[1]
398
 
            expected_parents = expected[2]
399
 
            version, options, parents, bytes = actual
400
 
            self.assertEqual(expected_version, version)
401
 
            self.assertEqual(expected_options, options)
402
 
            self.assertEqual(expected_parents, parents)
403
 
            self.assertRecordContentEqual(k1, version, bytes)
 
457
        repo = self.make_repository('.',
 
458
                format=bzrdir.format_registry.get('knit')())
 
459
        inv_xml = '<inventory format="5">\n</inventory>\n'
 
460
        inv = repo.deserialise_inventory('test-rev-id', inv_xml)
 
461
        self.assertEqual('test-rev-id', inv.root.revision)
 
462
 
 
463
    def test_deserialise_uses_global_revision_id(self):
 
464
        """If it is set, then we re-use the global revision id"""
 
465
        repo = self.make_repository('.',
 
466
                format=bzrdir.format_registry.get('knit')())
 
467
        inv_xml = ('<inventory format="5" revision_id="other-rev-id">\n'
 
468
                   '</inventory>\n')
 
469
        # Arguably, the deserialise_inventory should detect a mismatch, and
 
470
        # raise an error, rather than silently using one revision_id over the
 
471
        # other.
 
472
        self.assertRaises(AssertionError, repo.deserialise_inventory,
 
473
            'test-rev-id', inv_xml)
 
474
        inv = repo.deserialise_inventory('other-rev-id', inv_xml)
 
475
        self.assertEqual('other-rev-id', inv.root.revision)
 
476
 
 
477
    def test_supports_external_lookups(self):
 
478
        repo = self.make_repository('.',
 
479
                format=bzrdir.format_registry.get('knit')())
 
480
        self.assertFalse(repo._format.supports_external_lookups)
404
481
 
405
482
 
406
483
class DummyRepository(object):
407
484
    """A dummy repository for testing."""
408
485
 
 
486
    _format = None
409
487
    _serializer = None
410
488
 
411
489
    def supports_rich_root(self):
 
490
        if self._format is not None:
 
491
            return self._format.rich_root_data
412
492
        return False
413
493
 
 
494
    def get_graph(self):
 
495
        raise NotImplementedError
 
496
 
 
497
    def get_parent_map(self, revision_ids):
 
498
        raise NotImplementedError
 
499
 
414
500
 
415
501
class InterDummy(repository.InterRepository):
416
502
    """An inter-repository optimised code path for DummyRepository.
417
503
 
418
504
    This is for use during testing where we use DummyRepository as repositories
419
505
    so that none of the default regsitered inter-repository classes will
420
 
    match.
 
506
    MATCH.
421
507
    """
422
508
 
423
509
    @staticmethod
424
510
    def is_compatible(repo_source, repo_target):
425
511
        """InterDummy is compatible with DummyRepository."""
426
 
        return (isinstance(repo_source, DummyRepository) and 
 
512
        return (isinstance(repo_source, DummyRepository) and
427
513
            isinstance(repo_target, DummyRepository))
428
514
 
429
515
 
442
528
 
443
529
    def assertGetsDefaultInterRepository(self, repo_a, repo_b):
444
530
        """Asserts that InterRepository.get(repo_a, repo_b) -> the default.
445
 
        
 
531
 
446
532
        The effective default is now InterSameDataRepository because there is
447
533
        no actual sane default in the presence of incompatible data models.
448
534
        """
459
545
        # pair that it returns true on for the is_compatible static method
460
546
        # check
461
547
        dummy_a = DummyRepository()
 
548
        dummy_a._format = RepositoryFormat()
462
549
        dummy_b = DummyRepository()
 
550
        dummy_b._format = RepositoryFormat()
463
551
        repo = self.make_repository('.')
464
552
        # hack dummies to look like repo somewhat.
465
553
        dummy_a._serializer = repo._serializer
 
554
        dummy_a._format.supports_tree_reference = repo._format.supports_tree_reference
 
555
        dummy_a._format.rich_root_data = repo._format.rich_root_data
466
556
        dummy_b._serializer = repo._serializer
 
557
        dummy_b._format.supports_tree_reference = repo._format.supports_tree_reference
 
558
        dummy_b._format.rich_root_data = repo._format.rich_root_data
467
559
        repository.InterRepository.register_optimiser(InterDummy)
468
560
        try:
469
561
            # we should get the default for something InterDummy returns False
532
624
 
533
625
 
534
626
class TestMisc(TestCase):
535
 
    
 
627
 
536
628
    def test_unescape_xml(self):
537
629
        """We get some kind of error when malformed entities are passed"""
538
 
        self.assertRaises(KeyError, repository._unescape_xml, 'foo&bar;') 
 
630
        self.assertRaises(KeyError, repository._unescape_xml, 'foo&bar;')
539
631
 
540
632
 
541
633
class TestRepositoryFormatKnit3(TestCaseWithTransport):
542
634
 
 
635
    def test_attribute__fetch_order(self):
 
636
        """Knits need topological data insertion."""
 
637
        format = bzrdir.BzrDirMetaFormat1()
 
638
        format.repository_format = knitrepo.RepositoryFormatKnit3()
 
639
        repo = self.make_repository('.', format=format)
 
640
        self.assertEqual('topological', repo._format._fetch_order)
 
641
 
 
642
    def test_attribute__fetch_uses_deltas(self):
 
643
        """Knits reuse deltas."""
 
644
        format = bzrdir.BzrDirMetaFormat1()
 
645
        format.repository_format = knitrepo.RepositoryFormatKnit3()
 
646
        repo = self.make_repository('.', format=format)
 
647
        self.assertEqual(True, repo._format._fetch_uses_deltas)
 
648
 
543
649
    def test_convert(self):
544
650
        """Ensure the upgrade adds weaves for roots"""
545
651
        format = bzrdir.BzrDirMetaFormat1()
547
653
        tree = self.make_branch_and_tree('.', format)
548
654
        tree.commit("Dull commit", rev_id="dull")
549
655
        revision_tree = tree.branch.repository.revision_tree('dull')
550
 
        self.assertRaises(errors.NoSuchFile, revision_tree.get_file_lines,
551
 
            revision_tree.inventory.root.file_id)
 
656
        revision_tree.lock_read()
 
657
        try:
 
658
            self.assertRaises(errors.NoSuchFile, revision_tree.get_file_lines,
 
659
                revision_tree.inventory.root.file_id)
 
660
        finally:
 
661
            revision_tree.unlock()
552
662
        format = bzrdir.BzrDirMetaFormat1()
553
663
        format.repository_format = knitrepo.RepositoryFormatKnit3()
554
664
        upgrade.Convert('.', format)
555
665
        tree = workingtree.WorkingTree.open('.')
556
666
        revision_tree = tree.branch.repository.revision_tree('dull')
557
 
        revision_tree.get_file_lines(revision_tree.inventory.root.file_id)
 
667
        revision_tree.lock_read()
 
668
        try:
 
669
            revision_tree.get_file_lines(revision_tree.inventory.root.file_id)
 
670
        finally:
 
671
            revision_tree.unlock()
558
672
        tree.commit("Another dull commit", rev_id='dull2')
559
673
        revision_tree = tree.branch.repository.revision_tree('dull2')
 
674
        revision_tree.lock_read()
 
675
        self.addCleanup(revision_tree.unlock)
560
676
        self.assertEqual('dull', revision_tree.inventory.root.revision)
561
677
 
 
678
    def test_supports_external_lookups(self):
 
679
        format = bzrdir.BzrDirMetaFormat1()
 
680
        format.repository_format = knitrepo.RepositoryFormatKnit3()
 
681
        repo = self.make_repository('.', format=format)
 
682
        self.assertFalse(repo._format.supports_external_lookups)
 
683
 
 
684
 
 
685
class Test2a(TestCaseWithTransport):
 
686
 
 
687
    def test_fetch_combines_groups(self):
 
688
        builder = self.make_branch_builder('source', format='2a')
 
689
        builder.start_series()
 
690
        builder.build_snapshot('1', None, [
 
691
            ('add', ('', 'root-id', 'directory', '')),
 
692
            ('add', ('file', 'file-id', 'file', 'content\n'))])
 
693
        builder.build_snapshot('2', ['1'], [
 
694
            ('modify', ('file-id', 'content-2\n'))])
 
695
        builder.finish_series()
 
696
        source = builder.get_branch()
 
697
        target = self.make_repository('target', format='2a')
 
698
        target.fetch(source.repository)
 
699
        target.lock_read()
 
700
        self.addCleanup(target.unlock)
 
701
        details = target.texts._index.get_build_details(
 
702
            [('file-id', '1',), ('file-id', '2',)])
 
703
        file_1_details = details[('file-id', '1')]
 
704
        file_2_details = details[('file-id', '2')]
 
705
        # The index, and what to read off disk, should be the same for both
 
706
        # versions of the file.
 
707
        self.assertEqual(file_1_details[0][:3], file_2_details[0][:3])
 
708
 
 
709
    def test_format_pack_compresses_True(self):
 
710
        repo = self.make_repository('repo', format='2a')
 
711
        self.assertTrue(repo._format.pack_compresses)
 
712
 
 
713
    def test_inventories_use_chk_map_with_parent_base_dict(self):
 
714
        tree = self.make_branch_and_tree('repo', format="2a")
 
715
        revid = tree.commit("foo")
 
716
        tree.lock_read()
 
717
        self.addCleanup(tree.unlock)
 
718
        inv = tree.branch.repository.get_inventory(revid)
 
719
        self.assertNotEqual(None, inv.parent_id_basename_to_file_id)
 
720
        inv.parent_id_basename_to_file_id._ensure_root()
 
721
        inv.id_to_entry._ensure_root()
 
722
        self.assertEqual(65536, inv.id_to_entry._root_node.maximum_size)
 
723
        self.assertEqual(65536,
 
724
            inv.parent_id_basename_to_file_id._root_node.maximum_size)
 
725
 
 
726
    def test_autopack_unchanged_chk_nodes(self):
 
727
        # at 20 unchanged commits, chk pages are packed that are split into
 
728
        # two groups such that the new pack being made doesn't have all its
 
729
        # pages in the source packs (though they are in the repository).
 
730
        tree = self.make_branch_and_tree('tree', format='2a')
 
731
        for pos in range(20):
 
732
            tree.commit(str(pos))
 
733
 
 
734
    def test_pack_with_hint(self):
 
735
        tree = self.make_branch_and_tree('tree', format='2a')
 
736
        # 1 commit to leave untouched
 
737
        tree.commit('1')
 
738
        to_keep = tree.branch.repository._pack_collection.names()
 
739
        # 2 to combine
 
740
        tree.commit('2')
 
741
        tree.commit('3')
 
742
        all = tree.branch.repository._pack_collection.names()
 
743
        combine = list(set(all) - set(to_keep))
 
744
        self.assertLength(3, all)
 
745
        self.assertLength(2, combine)
 
746
        tree.branch.repository.pack(hint=combine)
 
747
        final = tree.branch.repository._pack_collection.names()
 
748
        self.assertLength(2, final)
 
749
        self.assertFalse(combine[0] in final)
 
750
        self.assertFalse(combine[1] in final)
 
751
        self.assertSubset(to_keep, final)
 
752
 
 
753
    def test_stream_source_to_gc(self):
 
754
        source = self.make_repository('source', format='2a')
 
755
        target = self.make_repository('target', format='2a')
 
756
        stream = source._get_source(target._format)
 
757
        self.assertIsInstance(stream, groupcompress_repo.GroupCHKStreamSource)
 
758
 
 
759
    def test_stream_source_to_non_gc(self):
 
760
        source = self.make_repository('source', format='2a')
 
761
        target = self.make_repository('target', format='rich-root-pack')
 
762
        stream = source._get_source(target._format)
 
763
        # We don't want the child GroupCHKStreamSource
 
764
        self.assertIs(type(stream), repository.StreamSource)
 
765
 
 
766
    def test_get_stream_for_missing_keys_includes_all_chk_refs(self):
 
767
        source_builder = self.make_branch_builder('source',
 
768
                            format='2a')
 
769
        # We have to build a fairly large tree, so that we are sure the chk
 
770
        # pages will have split into multiple pages.
 
771
        entries = [('add', ('', 'a-root-id', 'directory', None))]
 
772
        for i in 'abcdefghijklmnopqrstuvwxyz123456789':
 
773
            for j in 'abcdefghijklmnopqrstuvwxyz123456789':
 
774
                fname = i + j
 
775
                fid = fname + '-id'
 
776
                content = 'content for %s\n' % (fname,)
 
777
                entries.append(('add', (fname, fid, 'file', content)))
 
778
        source_builder.start_series()
 
779
        source_builder.build_snapshot('rev-1', None, entries)
 
780
        # Now change a few of them, so we get a few new pages for the second
 
781
        # revision
 
782
        source_builder.build_snapshot('rev-2', ['rev-1'], [
 
783
            ('modify', ('aa-id', 'new content for aa-id\n')),
 
784
            ('modify', ('cc-id', 'new content for cc-id\n')),
 
785
            ('modify', ('zz-id', 'new content for zz-id\n')),
 
786
            ])
 
787
        source_builder.finish_series()
 
788
        source_branch = source_builder.get_branch()
 
789
        source_branch.lock_read()
 
790
        self.addCleanup(source_branch.unlock)
 
791
        target = self.make_repository('target', format='2a')
 
792
        source = source_branch.repository._get_source(target._format)
 
793
        self.assertIsInstance(source, groupcompress_repo.GroupCHKStreamSource)
 
794
 
 
795
        # On a regular pass, getting the inventories and chk pages for rev-2
 
796
        # would only get the newly created chk pages
 
797
        search = graph.SearchResult(set(['rev-2']), set(['rev-1']), 1,
 
798
                                    set(['rev-2']))
 
799
        simple_chk_records = []
 
800
        for vf_name, substream in source.get_stream(search):
 
801
            if vf_name == 'chk_bytes':
 
802
                for record in substream:
 
803
                    simple_chk_records.append(record.key)
 
804
            else:
 
805
                for _ in substream:
 
806
                    continue
 
807
        # 3 pages, the root (InternalNode), + 2 pages which actually changed
 
808
        self.assertEqual([('sha1:91481f539e802c76542ea5e4c83ad416bf219f73',),
 
809
                          ('sha1:4ff91971043668583985aec83f4f0ab10a907d3f',),
 
810
                          ('sha1:81e7324507c5ca132eedaf2d8414ee4bb2226187',),
 
811
                          ('sha1:b101b7da280596c71a4540e9a1eeba8045985ee0',)],
 
812
                         simple_chk_records)
 
813
        # Now, when we do a similar call using 'get_stream_for_missing_keys'
 
814
        # we should get a much larger set of pages.
 
815
        missing = [('inventories', 'rev-2')]
 
816
        full_chk_records = []
 
817
        for vf_name, substream in source.get_stream_for_missing_keys(missing):
 
818
            if vf_name == 'inventories':
 
819
                for record in substream:
 
820
                    self.assertEqual(('rev-2',), record.key)
 
821
            elif vf_name == 'chk_bytes':
 
822
                for record in substream:
 
823
                    full_chk_records.append(record.key)
 
824
            else:
 
825
                self.fail('Should not be getting a stream of %s' % (vf_name,))
 
826
        # We have 257 records now. This is because we have 1 root page, and 256
 
827
        # leaf pages in a complete listing.
 
828
        self.assertEqual(257, len(full_chk_records))
 
829
        self.assertSubset(simple_chk_records, full_chk_records)
 
830
 
 
831
    def test_inconsistency_fatal(self):
 
832
        repo = self.make_repository('repo', format='2a')
 
833
        self.assertTrue(repo.revisions._index._inconsistency_fatal)
 
834
        self.assertFalse(repo.texts._index._inconsistency_fatal)
 
835
        self.assertFalse(repo.inventories._index._inconsistency_fatal)
 
836
        self.assertFalse(repo.signatures._index._inconsistency_fatal)
 
837
        self.assertFalse(repo.chk_bytes._index._inconsistency_fatal)
 
838
 
 
839
 
 
840
class TestKnitPackStreamSource(tests.TestCaseWithMemoryTransport):
 
841
 
 
842
    def test_source_to_exact_pack_092(self):
 
843
        source = self.make_repository('source', format='pack-0.92')
 
844
        target = self.make_repository('target', format='pack-0.92')
 
845
        stream_source = source._get_source(target._format)
 
846
        self.assertIsInstance(stream_source, pack_repo.KnitPackStreamSource)
 
847
 
 
848
    def test_source_to_exact_pack_rich_root_pack(self):
 
849
        source = self.make_repository('source', format='rich-root-pack')
 
850
        target = self.make_repository('target', format='rich-root-pack')
 
851
        stream_source = source._get_source(target._format)
 
852
        self.assertIsInstance(stream_source, pack_repo.KnitPackStreamSource)
 
853
 
 
854
    def test_source_to_exact_pack_19(self):
 
855
        source = self.make_repository('source', format='1.9')
 
856
        target = self.make_repository('target', format='1.9')
 
857
        stream_source = source._get_source(target._format)
 
858
        self.assertIsInstance(stream_source, pack_repo.KnitPackStreamSource)
 
859
 
 
860
    def test_source_to_exact_pack_19_rich_root(self):
 
861
        source = self.make_repository('source', format='1.9-rich-root')
 
862
        target = self.make_repository('target', format='1.9-rich-root')
 
863
        stream_source = source._get_source(target._format)
 
864
        self.assertIsInstance(stream_source, pack_repo.KnitPackStreamSource)
 
865
 
 
866
    def test_source_to_remote_exact_pack_19(self):
 
867
        trans = self.make_smart_server('target')
 
868
        trans.ensure_base()
 
869
        source = self.make_repository('source', format='1.9')
 
870
        target = self.make_repository('target', format='1.9')
 
871
        target = repository.Repository.open(trans.base)
 
872
        stream_source = source._get_source(target._format)
 
873
        self.assertIsInstance(stream_source, pack_repo.KnitPackStreamSource)
 
874
 
 
875
    def test_stream_source_to_non_exact(self):
 
876
        source = self.make_repository('source', format='pack-0.92')
 
877
        target = self.make_repository('target', format='1.9')
 
878
        stream = source._get_source(target._format)
 
879
        self.assertIs(type(stream), repository.StreamSource)
 
880
 
 
881
    def test_stream_source_to_non_exact_rich_root(self):
 
882
        source = self.make_repository('source', format='1.9')
 
883
        target = self.make_repository('target', format='1.9-rich-root')
 
884
        stream = source._get_source(target._format)
 
885
        self.assertIs(type(stream), repository.StreamSource)
 
886
 
 
887
    def test_source_to_remote_non_exact_pack_19(self):
 
888
        trans = self.make_smart_server('target')
 
889
        trans.ensure_base()
 
890
        source = self.make_repository('source', format='1.9')
 
891
        target = self.make_repository('target', format='1.6')
 
892
        target = repository.Repository.open(trans.base)
 
893
        stream_source = source._get_source(target._format)
 
894
        self.assertIs(type(stream_source), repository.StreamSource)
 
895
 
 
896
    def test_stream_source_to_knit(self):
 
897
        source = self.make_repository('source', format='pack-0.92')
 
898
        target = self.make_repository('target', format='dirstate')
 
899
        stream = source._get_source(target._format)
 
900
        self.assertIs(type(stream), repository.StreamSource)
 
901
 
 
902
 
 
903
class TestDevelopment6FindParentIdsOfRevisions(TestCaseWithTransport):
 
904
    """Tests for _find_parent_ids_of_revisions."""
 
905
 
 
906
    def setUp(self):
 
907
        super(TestDevelopment6FindParentIdsOfRevisions, self).setUp()
 
908
        self.builder = self.make_branch_builder('source',
 
909
            format='development6-rich-root')
 
910
        self.builder.start_series()
 
911
        self.builder.build_snapshot('initial', None,
 
912
            [('add', ('', 'tree-root', 'directory', None))])
 
913
        self.repo = self.builder.get_branch().repository
 
914
        self.addCleanup(self.builder.finish_series)
 
915
 
 
916
    def assertParentIds(self, expected_result, rev_set):
 
917
        self.assertEqual(sorted(expected_result),
 
918
            sorted(self.repo._find_parent_ids_of_revisions(rev_set)))
 
919
 
 
920
    def test_simple(self):
 
921
        self.builder.build_snapshot('revid1', None, [])
 
922
        self.builder.build_snapshot('revid2', ['revid1'], [])
 
923
        rev_set = ['revid2']
 
924
        self.assertParentIds(['revid1'], rev_set)
 
925
 
 
926
    def test_not_first_parent(self):
 
927
        self.builder.build_snapshot('revid1', None, [])
 
928
        self.builder.build_snapshot('revid2', ['revid1'], [])
 
929
        self.builder.build_snapshot('revid3', ['revid2'], [])
 
930
        rev_set = ['revid3', 'revid2']
 
931
        self.assertParentIds(['revid1'], rev_set)
 
932
 
 
933
    def test_not_null(self):
 
934
        rev_set = ['initial']
 
935
        self.assertParentIds([], rev_set)
 
936
 
 
937
    def test_not_null_set(self):
 
938
        self.builder.build_snapshot('revid1', None, [])
 
939
        rev_set = [_mod_revision.NULL_REVISION]
 
940
        self.assertParentIds([], rev_set)
 
941
 
 
942
    def test_ghost(self):
 
943
        self.builder.build_snapshot('revid1', None, [])
 
944
        rev_set = ['ghost', 'revid1']
 
945
        self.assertParentIds(['initial'], rev_set)
 
946
 
 
947
    def test_ghost_parent(self):
 
948
        self.builder.build_snapshot('revid1', None, [])
 
949
        self.builder.build_snapshot('revid2', ['revid1', 'ghost'], [])
 
950
        rev_set = ['revid2', 'revid1']
 
951
        self.assertParentIds(['ghost', 'initial'], rev_set)
 
952
 
 
953
    def test_righthand_parent(self):
 
954
        self.builder.build_snapshot('revid1', None, [])
 
955
        self.builder.build_snapshot('revid2a', ['revid1'], [])
 
956
        self.builder.build_snapshot('revid2b', ['revid1'], [])
 
957
        self.builder.build_snapshot('revid3', ['revid2a', 'revid2b'], [])
 
958
        rev_set = ['revid3', 'revid2a']
 
959
        self.assertParentIds(['revid1', 'revid2b'], rev_set)
 
960
 
 
961
 
 
962
class TestWithBrokenRepo(TestCaseWithTransport):
 
963
    """These tests seem to be more appropriate as interface tests?"""
 
964
 
 
965
    def make_broken_repository(self):
 
966
        # XXX: This function is borrowed from Aaron's "Reconcile can fix bad
 
967
        # parent references" branch which is due to land in bzr.dev soon.  Once
 
968
        # it does, this duplication should be removed.
 
969
        repo = self.make_repository('broken-repo')
 
970
        cleanups = []
 
971
        try:
 
972
            repo.lock_write()
 
973
            cleanups.append(repo.unlock)
 
974
            repo.start_write_group()
 
975
            cleanups.append(repo.commit_write_group)
 
976
            # make rev1a: A well-formed revision, containing 'file1'
 
977
            inv = inventory.Inventory(revision_id='rev1a')
 
978
            inv.root.revision = 'rev1a'
 
979
            self.add_file(repo, inv, 'file1', 'rev1a', [])
 
980
            repo.texts.add_lines((inv.root.file_id, 'rev1a'), [], [])
 
981
            repo.add_inventory('rev1a', inv, [])
 
982
            revision = _mod_revision.Revision('rev1a',
 
983
                committer='jrandom@example.com', timestamp=0,
 
984
                inventory_sha1='', timezone=0, message='foo', parent_ids=[])
 
985
            repo.add_revision('rev1a',revision, inv)
 
986
 
 
987
            # make rev1b, which has no Revision, but has an Inventory, and
 
988
            # file1
 
989
            inv = inventory.Inventory(revision_id='rev1b')
 
990
            inv.root.revision = 'rev1b'
 
991
            self.add_file(repo, inv, 'file1', 'rev1b', [])
 
992
            repo.add_inventory('rev1b', inv, [])
 
993
 
 
994
            # make rev2, with file1 and file2
 
995
            # file2 is sane
 
996
            # file1 has 'rev1b' as an ancestor, even though this is not
 
997
            # mentioned by 'rev1a', making it an unreferenced ancestor
 
998
            inv = inventory.Inventory()
 
999
            self.add_file(repo, inv, 'file1', 'rev2', ['rev1a', 'rev1b'])
 
1000
            self.add_file(repo, inv, 'file2', 'rev2', [])
 
1001
            self.add_revision(repo, 'rev2', inv, ['rev1a'])
 
1002
 
 
1003
            # make ghost revision rev1c
 
1004
            inv = inventory.Inventory()
 
1005
            self.add_file(repo, inv, 'file2', 'rev1c', [])
 
1006
 
 
1007
            # make rev3 with file2
 
1008
            # file2 refers to 'rev1c', which is a ghost in this repository, so
 
1009
            # file2 cannot have rev1c as its ancestor.
 
1010
            inv = inventory.Inventory()
 
1011
            self.add_file(repo, inv, 'file2', 'rev3', ['rev1c'])
 
1012
            self.add_revision(repo, 'rev3', inv, ['rev1c'])
 
1013
            return repo
 
1014
        finally:
 
1015
            for cleanup in reversed(cleanups):
 
1016
                cleanup()
 
1017
 
 
1018
    def add_revision(self, repo, revision_id, inv, parent_ids):
 
1019
        inv.revision_id = revision_id
 
1020
        inv.root.revision = revision_id
 
1021
        repo.texts.add_lines((inv.root.file_id, revision_id), [], [])
 
1022
        repo.add_inventory(revision_id, inv, parent_ids)
 
1023
        revision = _mod_revision.Revision(revision_id,
 
1024
            committer='jrandom@example.com', timestamp=0, inventory_sha1='',
 
1025
            timezone=0, message='foo', parent_ids=parent_ids)
 
1026
        repo.add_revision(revision_id,revision, inv)
 
1027
 
 
1028
    def add_file(self, repo, inv, filename, revision, parents):
 
1029
        file_id = filename + '-id'
 
1030
        entry = inventory.InventoryFile(file_id, filename, 'TREE_ROOT')
 
1031
        entry.revision = revision
 
1032
        entry.text_size = 0
 
1033
        inv.add(entry)
 
1034
        text_key = (file_id, revision)
 
1035
        parent_keys = [(file_id, parent) for parent in parents]
 
1036
        repo.texts.add_lines(text_key, parent_keys, ['line\n'])
 
1037
 
 
1038
    def test_insert_from_broken_repo(self):
 
1039
        """Inserting a data stream from a broken repository won't silently
 
1040
        corrupt the target repository.
 
1041
        """
 
1042
        broken_repo = self.make_broken_repository()
 
1043
        empty_repo = self.make_repository('empty-repo')
 
1044
        try:
 
1045
            empty_repo.fetch(broken_repo)
 
1046
        except (errors.RevisionNotPresent, errors.BzrCheckError):
 
1047
            # Test successful: compression parent not being copied leads to
 
1048
            # error.
 
1049
            return
 
1050
        empty_repo.lock_read()
 
1051
        self.addCleanup(empty_repo.unlock)
 
1052
        text = empty_repo.texts.get_record_stream(
 
1053
            [('file2-id', 'rev3')], 'topological', True).next()
 
1054
        self.assertEqual('line\n', text.get_bytes_as('fulltext'))
 
1055
 
 
1056
 
 
1057
class TestRepositoryPackCollection(TestCaseWithTransport):
 
1058
 
 
1059
    def get_format(self):
 
1060
        return bzrdir.format_registry.make_bzrdir('pack-0.92')
 
1061
 
 
1062
    def get_packs(self):
 
1063
        format = self.get_format()
 
1064
        repo = self.make_repository('.', format=format)
 
1065
        return repo._pack_collection
 
1066
 
 
1067
    def make_packs_and_alt_repo(self, write_lock=False):
 
1068
        """Create a pack repo with 3 packs, and access it via a second repo."""
 
1069
        tree = self.make_branch_and_tree('.', format=self.get_format())
 
1070
        tree.lock_write()
 
1071
        self.addCleanup(tree.unlock)
 
1072
        rev1 = tree.commit('one')
 
1073
        rev2 = tree.commit('two')
 
1074
        rev3 = tree.commit('three')
 
1075
        r = repository.Repository.open('.')
 
1076
        if write_lock:
 
1077
            r.lock_write()
 
1078
        else:
 
1079
            r.lock_read()
 
1080
        self.addCleanup(r.unlock)
 
1081
        packs = r._pack_collection
 
1082
        packs.ensure_loaded()
 
1083
        return tree, r, packs, [rev1, rev2, rev3]
 
1084
 
 
1085
    def test__max_pack_count(self):
 
1086
        """The maximum pack count is a function of the number of revisions."""
 
1087
        # no revisions - one pack, so that we can have a revision free repo
 
1088
        # without it blowing up
 
1089
        packs = self.get_packs()
 
1090
        self.assertEqual(1, packs._max_pack_count(0))
 
1091
        # after that the sum of the digits, - check the first 1-9
 
1092
        self.assertEqual(1, packs._max_pack_count(1))
 
1093
        self.assertEqual(2, packs._max_pack_count(2))
 
1094
        self.assertEqual(3, packs._max_pack_count(3))
 
1095
        self.assertEqual(4, packs._max_pack_count(4))
 
1096
        self.assertEqual(5, packs._max_pack_count(5))
 
1097
        self.assertEqual(6, packs._max_pack_count(6))
 
1098
        self.assertEqual(7, packs._max_pack_count(7))
 
1099
        self.assertEqual(8, packs._max_pack_count(8))
 
1100
        self.assertEqual(9, packs._max_pack_count(9))
 
1101
        # check the boundary cases with two digits for the next decade
 
1102
        self.assertEqual(1, packs._max_pack_count(10))
 
1103
        self.assertEqual(2, packs._max_pack_count(11))
 
1104
        self.assertEqual(10, packs._max_pack_count(19))
 
1105
        self.assertEqual(2, packs._max_pack_count(20))
 
1106
        self.assertEqual(3, packs._max_pack_count(21))
 
1107
        # check some arbitrary big numbers
 
1108
        self.assertEqual(25, packs._max_pack_count(112894))
 
1109
 
 
1110
    def test_pack_distribution_zero(self):
 
1111
        packs = self.get_packs()
 
1112
        self.assertEqual([0], packs.pack_distribution(0))
 
1113
 
 
1114
    def test_ensure_loaded_unlocked(self):
 
1115
        packs = self.get_packs()
 
1116
        self.assertRaises(errors.ObjectNotLocked,
 
1117
                          packs.ensure_loaded)
 
1118
 
 
1119
    def test_pack_distribution_one_to_nine(self):
 
1120
        packs = self.get_packs()
 
1121
        self.assertEqual([1],
 
1122
            packs.pack_distribution(1))
 
1123
        self.assertEqual([1, 1],
 
1124
            packs.pack_distribution(2))
 
1125
        self.assertEqual([1, 1, 1],
 
1126
            packs.pack_distribution(3))
 
1127
        self.assertEqual([1, 1, 1, 1],
 
1128
            packs.pack_distribution(4))
 
1129
        self.assertEqual([1, 1, 1, 1, 1],
 
1130
            packs.pack_distribution(5))
 
1131
        self.assertEqual([1, 1, 1, 1, 1, 1],
 
1132
            packs.pack_distribution(6))
 
1133
        self.assertEqual([1, 1, 1, 1, 1, 1, 1],
 
1134
            packs.pack_distribution(7))
 
1135
        self.assertEqual([1, 1, 1, 1, 1, 1, 1, 1],
 
1136
            packs.pack_distribution(8))
 
1137
        self.assertEqual([1, 1, 1, 1, 1, 1, 1, 1, 1],
 
1138
            packs.pack_distribution(9))
 
1139
 
 
1140
    def test_pack_distribution_stable_at_boundaries(self):
 
1141
        """When there are multi-rev packs the counts are stable."""
 
1142
        packs = self.get_packs()
 
1143
        # in 10s:
 
1144
        self.assertEqual([10], packs.pack_distribution(10))
 
1145
        self.assertEqual([10, 1], packs.pack_distribution(11))
 
1146
        self.assertEqual([10, 10], packs.pack_distribution(20))
 
1147
        self.assertEqual([10, 10, 1], packs.pack_distribution(21))
 
1148
        # 100s
 
1149
        self.assertEqual([100], packs.pack_distribution(100))
 
1150
        self.assertEqual([100, 1], packs.pack_distribution(101))
 
1151
        self.assertEqual([100, 10, 1], packs.pack_distribution(111))
 
1152
        self.assertEqual([100, 100], packs.pack_distribution(200))
 
1153
        self.assertEqual([100, 100, 1], packs.pack_distribution(201))
 
1154
        self.assertEqual([100, 100, 10, 1], packs.pack_distribution(211))
 
1155
 
 
1156
    def test_plan_pack_operations_2009_revisions_skip_all_packs(self):
 
1157
        packs = self.get_packs()
 
1158
        existing_packs = [(2000, "big"), (9, "medium")]
 
1159
        # rev count - 2009 -> 2x1000 + 9x1
 
1160
        pack_operations = packs.plan_autopack_combinations(
 
1161
            existing_packs, [1000, 1000, 1, 1, 1, 1, 1, 1, 1, 1, 1])
 
1162
        self.assertEqual([], pack_operations)
 
1163
 
 
1164
    def test_plan_pack_operations_2010_revisions_skip_all_packs(self):
 
1165
        packs = self.get_packs()
 
1166
        existing_packs = [(2000, "big"), (9, "medium"), (1, "single")]
 
1167
        # rev count - 2010 -> 2x1000 + 1x10
 
1168
        pack_operations = packs.plan_autopack_combinations(
 
1169
            existing_packs, [1000, 1000, 10])
 
1170
        self.assertEqual([], pack_operations)
 
1171
 
 
1172
    def test_plan_pack_operations_2010_combines_smallest_two(self):
 
1173
        packs = self.get_packs()
 
1174
        existing_packs = [(1999, "big"), (9, "medium"), (1, "single2"),
 
1175
            (1, "single1")]
 
1176
        # rev count - 2010 -> 2x1000 + 1x10 (3)
 
1177
        pack_operations = packs.plan_autopack_combinations(
 
1178
            existing_packs, [1000, 1000, 10])
 
1179
        self.assertEqual([[2, ["single2", "single1"]]], pack_operations)
 
1180
 
 
1181
    def test_plan_pack_operations_creates_a_single_op(self):
 
1182
        packs = self.get_packs()
 
1183
        existing_packs = [(50, 'a'), (40, 'b'), (30, 'c'), (10, 'd'),
 
1184
                          (10, 'e'), (6, 'f'), (4, 'g')]
 
1185
        # rev count 150 -> 1x100 and 5x10
 
1186
        # The two size 10 packs do not need to be touched. The 50, 40, 30 would
 
1187
        # be combined into a single 120 size pack, and the 6 & 4 would
 
1188
        # becombined into a size 10 pack. However, if we have to rewrite them,
 
1189
        # we save a pack file with no increased I/O by putting them into the
 
1190
        # same file.
 
1191
        distribution = packs.pack_distribution(150)
 
1192
        pack_operations = packs.plan_autopack_combinations(existing_packs,
 
1193
                                                           distribution)
 
1194
        self.assertEqual([[130, ['a', 'b', 'c', 'f', 'g']]], pack_operations)
 
1195
 
 
1196
    def test_all_packs_none(self):
 
1197
        format = self.get_format()
 
1198
        tree = self.make_branch_and_tree('.', format=format)
 
1199
        tree.lock_read()
 
1200
        self.addCleanup(tree.unlock)
 
1201
        packs = tree.branch.repository._pack_collection
 
1202
        packs.ensure_loaded()
 
1203
        self.assertEqual([], packs.all_packs())
 
1204
 
 
1205
    def test_all_packs_one(self):
 
1206
        format = self.get_format()
 
1207
        tree = self.make_branch_and_tree('.', format=format)
 
1208
        tree.commit('start')
 
1209
        tree.lock_read()
 
1210
        self.addCleanup(tree.unlock)
 
1211
        packs = tree.branch.repository._pack_collection
 
1212
        packs.ensure_loaded()
 
1213
        self.assertEqual([
 
1214
            packs.get_pack_by_name(packs.names()[0])],
 
1215
            packs.all_packs())
 
1216
 
 
1217
    def test_all_packs_two(self):
 
1218
        format = self.get_format()
 
1219
        tree = self.make_branch_and_tree('.', format=format)
 
1220
        tree.commit('start')
 
1221
        tree.commit('continue')
 
1222
        tree.lock_read()
 
1223
        self.addCleanup(tree.unlock)
 
1224
        packs = tree.branch.repository._pack_collection
 
1225
        packs.ensure_loaded()
 
1226
        self.assertEqual([
 
1227
            packs.get_pack_by_name(packs.names()[0]),
 
1228
            packs.get_pack_by_name(packs.names()[1]),
 
1229
            ], packs.all_packs())
 
1230
 
 
1231
    def test_get_pack_by_name(self):
 
1232
        format = self.get_format()
 
1233
        tree = self.make_branch_and_tree('.', format=format)
 
1234
        tree.commit('start')
 
1235
        tree.lock_read()
 
1236
        self.addCleanup(tree.unlock)
 
1237
        packs = tree.branch.repository._pack_collection
 
1238
        packs.reset()
 
1239
        packs.ensure_loaded()
 
1240
        name = packs.names()[0]
 
1241
        pack_1 = packs.get_pack_by_name(name)
 
1242
        # the pack should be correctly initialised
 
1243
        sizes = packs._names[name]
 
1244
        rev_index = GraphIndex(packs._index_transport, name + '.rix', sizes[0])
 
1245
        inv_index = GraphIndex(packs._index_transport, name + '.iix', sizes[1])
 
1246
        txt_index = GraphIndex(packs._index_transport, name + '.tix', sizes[2])
 
1247
        sig_index = GraphIndex(packs._index_transport, name + '.six', sizes[3])
 
1248
        self.assertEqual(pack_repo.ExistingPack(packs._pack_transport,
 
1249
            name, rev_index, inv_index, txt_index, sig_index), pack_1)
 
1250
        # and the same instance should be returned on successive calls.
 
1251
        self.assertTrue(pack_1 is packs.get_pack_by_name(name))
 
1252
 
 
1253
    def test_reload_pack_names_new_entry(self):
 
1254
        tree, r, packs, revs = self.make_packs_and_alt_repo()
 
1255
        names = packs.names()
 
1256
        # Add a new pack file into the repository
 
1257
        rev4 = tree.commit('four')
 
1258
        new_names = tree.branch.repository._pack_collection.names()
 
1259
        new_name = set(new_names).difference(names)
 
1260
        self.assertEqual(1, len(new_name))
 
1261
        new_name = new_name.pop()
 
1262
        # The old collection hasn't noticed yet
 
1263
        self.assertEqual(names, packs.names())
 
1264
        self.assertTrue(packs.reload_pack_names())
 
1265
        self.assertEqual(new_names, packs.names())
 
1266
        # And the repository can access the new revision
 
1267
        self.assertEqual({rev4:(revs[-1],)}, r.get_parent_map([rev4]))
 
1268
        self.assertFalse(packs.reload_pack_names())
 
1269
 
 
1270
    def test_reload_pack_names_added_and_removed(self):
 
1271
        tree, r, packs, revs = self.make_packs_and_alt_repo()
 
1272
        names = packs.names()
 
1273
        # Now repack the whole thing
 
1274
        tree.branch.repository.pack()
 
1275
        new_names = tree.branch.repository._pack_collection.names()
 
1276
        # The other collection hasn't noticed yet
 
1277
        self.assertEqual(names, packs.names())
 
1278
        self.assertTrue(packs.reload_pack_names())
 
1279
        self.assertEqual(new_names, packs.names())
 
1280
        self.assertEqual({revs[-1]:(revs[-2],)}, r.get_parent_map([revs[-1]]))
 
1281
        self.assertFalse(packs.reload_pack_names())
 
1282
 
 
1283
    def test_reload_pack_names_preserves_pending(self):
 
1284
        # TODO: Update this to also test for pending-deleted names
 
1285
        tree, r, packs, revs = self.make_packs_and_alt_repo(write_lock=True)
 
1286
        # We will add one pack (via start_write_group + insert_record_stream),
 
1287
        # and remove another pack (via _remove_pack_from_memory)
 
1288
        orig_names = packs.names()
 
1289
        orig_at_load = packs._packs_at_load
 
1290
        to_remove_name = iter(orig_names).next()
 
1291
        r.start_write_group()
 
1292
        self.addCleanup(r.abort_write_group)
 
1293
        r.texts.insert_record_stream([versionedfile.FulltextContentFactory(
 
1294
            ('text', 'rev'), (), None, 'content\n')])
 
1295
        new_pack = packs._new_pack
 
1296
        self.assertTrue(new_pack.data_inserted())
 
1297
        new_pack.finish()
 
1298
        packs.allocate(new_pack)
 
1299
        packs._new_pack = None
 
1300
        removed_pack = packs.get_pack_by_name(to_remove_name)
 
1301
        packs._remove_pack_from_memory(removed_pack)
 
1302
        names = packs.names()
 
1303
        all_nodes, deleted_nodes, new_nodes = packs._diff_pack_names()
 
1304
        new_names = set([x[0][0] for x in new_nodes])
 
1305
        self.assertEqual(names, sorted([x[0][0] for x in all_nodes]))
 
1306
        self.assertEqual(set(names) - set(orig_names), new_names)
 
1307
        self.assertEqual(set([new_pack.name]), new_names)
 
1308
        self.assertEqual([to_remove_name],
 
1309
                         sorted([x[0][0] for x in deleted_nodes]))
 
1310
        packs.reload_pack_names()
 
1311
        reloaded_names = packs.names()
 
1312
        self.assertEqual(orig_at_load, packs._packs_at_load)
 
1313
        self.assertEqual(names, reloaded_names)
 
1314
        all_nodes, deleted_nodes, new_nodes = packs._diff_pack_names()
 
1315
        new_names = set([x[0][0] for x in new_nodes])
 
1316
        self.assertEqual(names, sorted([x[0][0] for x in all_nodes]))
 
1317
        self.assertEqual(set(names) - set(orig_names), new_names)
 
1318
        self.assertEqual(set([new_pack.name]), new_names)
 
1319
        self.assertEqual([to_remove_name],
 
1320
                         sorted([x[0][0] for x in deleted_nodes]))
 
1321
 
 
1322
    def test_autopack_reloads_and_stops(self):
 
1323
        tree, r, packs, revs = self.make_packs_and_alt_repo(write_lock=True)
 
1324
        # After we have determined what needs to be autopacked, trigger a
 
1325
        # full-pack via the other repo which will cause us to re-evaluate and
 
1326
        # decide we don't need to do anything
 
1327
        orig_execute = packs._execute_pack_operations
 
1328
        def _munged_execute_pack_ops(*args, **kwargs):
 
1329
            tree.branch.repository.pack()
 
1330
            return orig_execute(*args, **kwargs)
 
1331
        packs._execute_pack_operations = _munged_execute_pack_ops
 
1332
        packs._max_pack_count = lambda x: 1
 
1333
        packs.pack_distribution = lambda x: [10]
 
1334
        self.assertFalse(packs.autopack())
 
1335
        self.assertEqual(1, len(packs.names()))
 
1336
        self.assertEqual(tree.branch.repository._pack_collection.names(),
 
1337
                         packs.names())
 
1338
 
 
1339
 
 
1340
class TestPack(TestCaseWithTransport):
 
1341
    """Tests for the Pack object."""
 
1342
 
 
1343
    def assertCurrentlyEqual(self, left, right):
 
1344
        self.assertTrue(left == right)
 
1345
        self.assertTrue(right == left)
 
1346
        self.assertFalse(left != right)
 
1347
        self.assertFalse(right != left)
 
1348
 
 
1349
    def assertCurrentlyNotEqual(self, left, right):
 
1350
        self.assertFalse(left == right)
 
1351
        self.assertFalse(right == left)
 
1352
        self.assertTrue(left != right)
 
1353
        self.assertTrue(right != left)
 
1354
 
 
1355
    def test___eq____ne__(self):
 
1356
        left = pack_repo.ExistingPack('', '', '', '', '', '')
 
1357
        right = pack_repo.ExistingPack('', '', '', '', '', '')
 
1358
        self.assertCurrentlyEqual(left, right)
 
1359
        # change all attributes and ensure equality changes as we do.
 
1360
        left.revision_index = 'a'
 
1361
        self.assertCurrentlyNotEqual(left, right)
 
1362
        right.revision_index = 'a'
 
1363
        self.assertCurrentlyEqual(left, right)
 
1364
        left.inventory_index = 'a'
 
1365
        self.assertCurrentlyNotEqual(left, right)
 
1366
        right.inventory_index = 'a'
 
1367
        self.assertCurrentlyEqual(left, right)
 
1368
        left.text_index = 'a'
 
1369
        self.assertCurrentlyNotEqual(left, right)
 
1370
        right.text_index = 'a'
 
1371
        self.assertCurrentlyEqual(left, right)
 
1372
        left.signature_index = 'a'
 
1373
        self.assertCurrentlyNotEqual(left, right)
 
1374
        right.signature_index = 'a'
 
1375
        self.assertCurrentlyEqual(left, right)
 
1376
        left.name = 'a'
 
1377
        self.assertCurrentlyNotEqual(left, right)
 
1378
        right.name = 'a'
 
1379
        self.assertCurrentlyEqual(left, right)
 
1380
        left.transport = 'a'
 
1381
        self.assertCurrentlyNotEqual(left, right)
 
1382
        right.transport = 'a'
 
1383
        self.assertCurrentlyEqual(left, right)
 
1384
 
 
1385
    def test_file_name(self):
 
1386
        pack = pack_repo.ExistingPack('', 'a_name', '', '', '', '')
 
1387
        self.assertEqual('a_name.pack', pack.file_name())
 
1388
 
 
1389
 
 
1390
class TestNewPack(TestCaseWithTransport):
 
1391
    """Tests for pack_repo.NewPack."""
 
1392
 
 
1393
    def test_new_instance_attributes(self):
 
1394
        upload_transport = self.get_transport('upload')
 
1395
        pack_transport = self.get_transport('pack')
 
1396
        index_transport = self.get_transport('index')
 
1397
        upload_transport.mkdir('.')
 
1398
        collection = pack_repo.RepositoryPackCollection(
 
1399
            repo=None,
 
1400
            transport=self.get_transport('.'),
 
1401
            index_transport=index_transport,
 
1402
            upload_transport=upload_transport,
 
1403
            pack_transport=pack_transport,
 
1404
            index_builder_class=BTreeBuilder,
 
1405
            index_class=BTreeGraphIndex,
 
1406
            use_chk_index=False)
 
1407
        pack = pack_repo.NewPack(collection)
 
1408
        self.assertIsInstance(pack.revision_index, BTreeBuilder)
 
1409
        self.assertIsInstance(pack.inventory_index, BTreeBuilder)
 
1410
        self.assertIsInstance(pack._hash, type(osutils.md5()))
 
1411
        self.assertTrue(pack.upload_transport is upload_transport)
 
1412
        self.assertTrue(pack.index_transport is index_transport)
 
1413
        self.assertTrue(pack.pack_transport is pack_transport)
 
1414
        self.assertEqual(None, pack.index_sizes)
 
1415
        self.assertEqual(20, len(pack.random_name))
 
1416
        self.assertIsInstance(pack.random_name, str)
 
1417
        self.assertIsInstance(pack.start_time, float)
 
1418
 
 
1419
 
 
1420
class TestPacker(TestCaseWithTransport):
 
1421
    """Tests for the packs repository Packer class."""
 
1422
 
 
1423
    def test_pack_optimizes_pack_order(self):
 
1424
        builder = self.make_branch_builder('.', format="1.9")
 
1425
        builder.start_series()
 
1426
        builder.build_snapshot('A', None, [
 
1427
            ('add', ('', 'root-id', 'directory', None)),
 
1428
            ('add', ('f', 'f-id', 'file', 'content\n'))])
 
1429
        builder.build_snapshot('B', ['A'],
 
1430
            [('modify', ('f-id', 'new-content\n'))])
 
1431
        builder.build_snapshot('C', ['B'],
 
1432
            [('modify', ('f-id', 'third-content\n'))])
 
1433
        builder.build_snapshot('D', ['C'],
 
1434
            [('modify', ('f-id', 'fourth-content\n'))])
 
1435
        b = builder.get_branch()
 
1436
        b.lock_read()
 
1437
        builder.finish_series()
 
1438
        self.addCleanup(b.unlock)
 
1439
        # At this point, we should have 4 pack files available
 
1440
        # Because of how they were built, they correspond to
 
1441
        # ['D', 'C', 'B', 'A']
 
1442
        packs = b.repository._pack_collection.packs
 
1443
        packer = pack_repo.Packer(b.repository._pack_collection,
 
1444
                                  packs, 'testing',
 
1445
                                  revision_ids=['B', 'C'])
 
1446
        # Now, when we are copying the B & C revisions, their pack files should
 
1447
        # be moved to the front of the stack
 
1448
        # The new ordering moves B & C to the front of the .packs attribute,
 
1449
        # and leaves the others in the original order.
 
1450
        new_packs = [packs[1], packs[2], packs[0], packs[3]]
 
1451
        new_pack = packer.pack()
 
1452
        self.assertEqual(new_packs, packer.packs)
 
1453
 
 
1454
 
 
1455
class TestOptimisingPacker(TestCaseWithTransport):
 
1456
    """Tests for the OptimisingPacker class."""
 
1457
 
 
1458
    def get_pack_collection(self):
 
1459
        repo = self.make_repository('.')
 
1460
        return repo._pack_collection
 
1461
 
 
1462
    def test_open_pack_will_optimise(self):
 
1463
        packer = pack_repo.OptimisingPacker(self.get_pack_collection(),
 
1464
                                            [], '.test')
 
1465
        new_pack = packer.open_pack()
 
1466
        self.assertIsInstance(new_pack, pack_repo.NewPack)
 
1467
        self.assertTrue(new_pack.revision_index._optimize_for_size)
 
1468
        self.assertTrue(new_pack.inventory_index._optimize_for_size)
 
1469
        self.assertTrue(new_pack.text_index._optimize_for_size)
 
1470
        self.assertTrue(new_pack.signature_index._optimize_for_size)
 
1471
 
 
1472
 
 
1473
class TestCrossFormatPacks(TestCaseWithTransport):
 
1474
 
 
1475
    def log_pack(self, hint=None):
 
1476
        self.calls.append(('pack', hint))
 
1477
        self.orig_pack(hint=hint)
 
1478
        if self.expect_hint:
 
1479
            self.assertTrue(hint)
 
1480
 
 
1481
    def run_stream(self, src_fmt, target_fmt, expect_pack_called):
 
1482
        self.expect_hint = expect_pack_called
 
1483
        self.calls = []
 
1484
        source_tree = self.make_branch_and_tree('src', format=src_fmt)
 
1485
        source_tree.lock_write()
 
1486
        self.addCleanup(source_tree.unlock)
 
1487
        tip = source_tree.commit('foo')
 
1488
        target = self.make_repository('target', format=target_fmt)
 
1489
        target.lock_write()
 
1490
        self.addCleanup(target.unlock)
 
1491
        source = source_tree.branch.repository._get_source(target._format)
 
1492
        self.orig_pack = target.pack
 
1493
        target.pack = self.log_pack
 
1494
        search = target.search_missing_revision_ids(
 
1495
            source_tree.branch.repository, tip)
 
1496
        stream = source.get_stream(search)
 
1497
        from_format = source_tree.branch.repository._format
 
1498
        sink = target._get_sink()
 
1499
        sink.insert_stream(stream, from_format, [])
 
1500
        if expect_pack_called:
 
1501
            self.assertLength(1, self.calls)
 
1502
        else:
 
1503
            self.assertLength(0, self.calls)
 
1504
 
 
1505
    def run_fetch(self, src_fmt, target_fmt, expect_pack_called):
 
1506
        self.expect_hint = expect_pack_called
 
1507
        self.calls = []
 
1508
        source_tree = self.make_branch_and_tree('src', format=src_fmt)
 
1509
        source_tree.lock_write()
 
1510
        self.addCleanup(source_tree.unlock)
 
1511
        tip = source_tree.commit('foo')
 
1512
        target = self.make_repository('target', format=target_fmt)
 
1513
        target.lock_write()
 
1514
        self.addCleanup(target.unlock)
 
1515
        source = source_tree.branch.repository
 
1516
        self.orig_pack = target.pack
 
1517
        target.pack = self.log_pack
 
1518
        target.fetch(source)
 
1519
        if expect_pack_called:
 
1520
            self.assertLength(1, self.calls)
 
1521
        else:
 
1522
            self.assertLength(0, self.calls)
 
1523
 
 
1524
    def test_sink_format_hint_no(self):
 
1525
        # When the target format says packing makes no difference, pack is not
 
1526
        # called.
 
1527
        self.run_stream('1.9', 'rich-root-pack', False)
 
1528
 
 
1529
    def test_sink_format_hint_yes(self):
 
1530
        # When the target format says packing makes a difference, pack is
 
1531
        # called.
 
1532
        self.run_stream('1.9', '2a', True)
 
1533
 
 
1534
    def test_sink_format_same_no(self):
 
1535
        # When the formats are the same, pack is not called.
 
1536
        self.run_stream('2a', '2a', False)
 
1537
 
 
1538
    def test_IDS_format_hint_no(self):
 
1539
        # When the target format says packing makes no difference, pack is not
 
1540
        # called.
 
1541
        self.run_fetch('1.9', 'rich-root-pack', False)
 
1542
 
 
1543
    def test_IDS_format_hint_yes(self):
 
1544
        # When the target format says packing makes a difference, pack is
 
1545
        # called.
 
1546
        self.run_fetch('1.9', '2a', True)
 
1547
 
 
1548
    def test_IDS_format_same_no(self):
 
1549
        # When the formats are the same, pack is not called.
 
1550
        self.run_fetch('2a', '2a', False)