~bzr-pqm/bzr/bzr.dev

« back to all changes in this revision

Viewing changes to bzrlib/tests/test_dirstate.py

  • Committer: John Arbash Meinel
  • Date: 2013-05-19 14:29:37 UTC
  • mfrom: (6437.63.9 2.5)
  • mto: (6437.63.10 2.5)
  • mto: This revision was merged to the branch mainline in revision 6575.
  • Revision ID: john@arbash-meinel.com-20130519142937-21ykz2n2y2f22za9
Merge in the actual 2.5 branch. It seems I failed before

Show diffs side-by-side

added added

removed removed

Lines of Context:
1
 
# Copyright (C) 2006, 2007 Canonical Ltd
 
1
# Copyright (C) 2006-2011 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 of the dirstate functionality being built for WorkingTreeFormat4."""
18
18
 
19
 
import bisect
20
19
import os
21
 
import time
 
20
import tempfile
22
21
 
23
22
from bzrlib import (
 
23
    bzrdir,
24
24
    dirstate,
25
25
    errors,
 
26
    inventory,
 
27
    memorytree,
26
28
    osutils,
 
29
    revision as _mod_revision,
 
30
    revisiontree,
 
31
    tests,
 
32
    workingtree_4,
27
33
    )
28
 
from bzrlib.memorytree import MemoryTree
29
 
from bzrlib.osutils import has_symlinks
 
34
from bzrlib.transport import memory
30
35
from bzrlib.tests import (
31
 
        TestCase,
32
 
        TestCaseWithTransport,
33
 
        TestSkipped,
34
 
        )
 
36
    features,
 
37
    test_osutils,
 
38
    )
 
39
from bzrlib.tests.scenarios import load_tests_apply_scenarios
35
40
 
36
41
 
37
42
# TODO:
47
52
# set_path_id  setting id when state is in memory modified
48
53
 
49
54
 
50
 
class TestCaseWithDirState(TestCaseWithTransport):
 
55
load_tests = load_tests_apply_scenarios
 
56
 
 
57
 
 
58
class TestCaseWithDirState(tests.TestCaseWithTransport):
51
59
    """Helper functions for creating DirState objects with various content."""
52
60
 
 
61
    scenarios = test_osutils.dir_reader_scenarios()
 
62
 
 
63
    # Set by load_tests
 
64
    _dir_reader_class = None
 
65
    _native_to_unicode = None # Not used yet
 
66
 
 
67
    def setUp(self):
 
68
        tests.TestCaseWithTransport.setUp(self)
 
69
 
 
70
        self.overrideAttr(osutils,
 
71
                          '_selected_dir_reader', self._dir_reader_class())
 
72
 
53
73
    def create_empty_dirstate(self):
54
74
        """Return a locked but empty dirstate"""
55
75
        state = dirstate.DirState.initialize('dirstate')
163
183
        """
164
184
        # The state should already be write locked, since we just had to do
165
185
        # some operation to get here.
166
 
        assert state._lock_token is not None
 
186
        self.assertTrue(state._lock_token is not None)
167
187
        try:
168
188
            self.assertEqual(expected_result[0],  state.get_parent_ids())
169
189
            # there should be no ghosts in this tree.
173
193
            state.save()
174
194
        finally:
175
195
            state.unlock()
176
 
        del state # Callers should unlock
 
196
        del state
177
197
        state = dirstate.DirState.on_file('dirstate')
178
198
        state.lock_read()
179
199
        try:
189
209
              c
190
210
              d/
191
211
                e
 
212
            b-c
192
213
            f
193
214
        """
194
215
        tree = self.make_branch_and_tree('tree')
195
 
        paths = ['a', 'b/', 'b/c', 'b/d/', 'b/d/e', 'f']
196
 
        file_ids = ['a-id', 'b-id', 'c-id', 'd-id', 'e-id', 'f-id']
 
216
        paths = ['a', 'b/', 'b/c', 'b/d/', 'b/d/e', 'b-c', 'f']
 
217
        file_ids = ['a-id', 'b-id', 'c-id', 'd-id', 'e-id', 'b-c-id', 'f-id']
197
218
        self.build_tree(['tree/' + p for p in paths])
198
219
        tree.set_root_id('TREE_ROOT')
199
220
        tree.add([p.rstrip('/') for p in paths], file_ids)
200
221
        tree.commit('initial', rev_id='rev-1')
201
222
        revision_id = 'rev-1'
202
223
        # a_packed_stat = dirstate.pack_stat(os.stat('tree/a'))
203
 
        t = self.get_transport().clone('tree')
 
224
        t = self.get_transport('tree')
204
225
        a_text = t.get_bytes('a')
205
226
        a_sha = osutils.sha_string(a_text)
206
227
        a_len = len(a_text)
214
235
        e_text = t.get_bytes('b/d/e')
215
236
        e_sha = osutils.sha_string(e_text)
216
237
        e_len = len(e_text)
 
238
        b_c_text = t.get_bytes('b-c')
 
239
        b_c_sha = osutils.sha_string(b_c_text)
 
240
        b_c_len = len(b_c_text)
217
241
        # f_packed_stat = dirstate.pack_stat(os.stat('tree/f'))
218
242
        f_text = t.get_bytes('f')
219
243
        f_sha = osutils.sha_string(f_text)
244
268
                      ('f', '', 0, False, null_stat),
245
269
                      ('f', e_sha, e_len, False, revision_id),
246
270
                     ]),
 
271
            'b-c':(('', 'b-c', 'b-c-id'), [
 
272
                      ('f', '', 0, False, null_stat),
 
273
                      ('f', b_c_sha, b_c_len, False, revision_id),
 
274
                     ]),
247
275
            'f':(('', 'f', 'f-id'), [
248
276
                  ('f', '', 0, False, null_stat),
249
277
                  ('f', f_sha, f_len, False, revision_id),
276
304
        tree, state, expected = self.create_basic_dirstate()
277
305
        # Now we will just remove and add every file so we get an extra entry
278
306
        # per entry. Unversion in reverse order so we handle subdirs
279
 
        tree.unversion(['f-id', 'e-id', 'd-id', 'c-id', 'b-id', 'a-id'])
280
 
        tree.add(['a', 'b', 'b/c', 'b/d', 'b/d/e', 'f'],
281
 
                 ['a-id2', 'b-id2', 'c-id2', 'd-id2', 'e-id2', 'f-id2'])
 
307
        tree.unversion(['f-id', 'b-c-id', 'e-id', 'd-id', 'c-id', 'b-id', 'a-id'])
 
308
        tree.add(['a', 'b', 'b/c', 'b/d', 'b/d/e', 'b-c', 'f'],
 
309
                 ['a-id2', 'b-id2', 'c-id2', 'd-id2', 'e-id2', 'b-c-id2', 'f-id2'])
282
310
 
283
311
        # Update the expected dictionary.
284
 
        for path in ['a', 'b', 'b/c', 'b/d', 'b/d/e', 'f']:
 
312
        for path in ['a', 'b', 'b/c', 'b/d', 'b/d/e', 'b-c', 'f']:
285
313
            orig = expected[path]
286
314
            path2 = path + '2'
287
315
            # This record was deleted in the current tree
344
372
            state.lock_read()
345
373
        return tree, state, expected
346
374
 
 
375
 
347
376
class TestTreeToDirState(TestCaseWithDirState):
348
377
 
349
378
    def test_empty_to_dirstate(self):
351
380
        # There are no files on disk and no parents
352
381
        tree = self.make_branch_and_tree('tree')
353
382
        expected_result = ([], [
354
 
            (('', '', tree.path2id('')), # common details
 
383
            (('', '', tree.get_root_id()), # common details
355
384
             [('d', '', 0, False, dirstate.DirState.NULLSTAT), # current tree
356
385
             ])])
357
386
        state = dirstate.DirState.from_tree(tree, 'dirstate')
364
393
        rev_id = tree.commit('first post').encode('utf8')
365
394
        root_stat_pack = dirstate.pack_stat(os.stat(tree.basedir))
366
395
        expected_result = ([rev_id], [
367
 
            (('', '', tree.path2id('')), # common details
 
396
            (('', '', tree.get_root_id()), # common details
368
397
             [('d', '', 0, False, dirstate.DirState.NULLSTAT), # current tree
369
398
              ('d', '', 0, False, rev_id), # first parent details
370
399
             ])])
384
413
        rev_id2 = tree2.commit('second post', allow_pointless=True)
385
414
        tree.merge_from_branch(tree2.branch)
386
415
        expected_result = ([rev_id, rev_id2], [
387
 
            (('', '', tree.path2id('')), # common details
 
416
            (('', '', tree.get_root_id()), # common details
388
417
             [('d', '', 0, False, dirstate.DirState.NULLSTAT), # current tree
389
418
              ('d', '', 0, False, rev_id), # first parent details
390
 
              ('d', '', 0, False, rev_id2), # second parent details
 
419
              ('d', '', 0, False, rev_id), # second parent details
391
420
             ])])
392
421
        state = dirstate.DirState.from_tree(tree, 'dirstate')
393
422
        self.check_state_with_reopen(expected_result, state)
403
432
        tree = self.make_branch_and_tree('tree')
404
433
        self.build_tree(['tree/unknown'])
405
434
        expected_result = ([], [
406
 
            (('', '', tree.path2id('')), # common details
 
435
            (('', '', tree.get_root_id()), # common details
407
436
             [('d', '', 0, False, dirstate.DirState.NULLSTAT), # current tree
408
437
             ])])
409
438
        state = dirstate.DirState.from_tree(tree, 'dirstate')
412
441
    def get_tree_with_a_file(self):
413
442
        tree = self.make_branch_and_tree('tree')
414
443
        self.build_tree(['tree/a file'])
415
 
        tree.add('a file', 'a file id')
 
444
        tree.add('a file', 'a-file-id')
416
445
        return tree
417
446
 
418
447
    def test_non_empty_no_parents_to_dirstate(self):
420
449
        # There are files on disk and no parents
421
450
        tree = self.get_tree_with_a_file()
422
451
        expected_result = ([], [
423
 
            (('', '', tree.path2id('')), # common details
 
452
            (('', '', tree.get_root_id()), # common details
424
453
             [('d', '', 0, False, dirstate.DirState.NULLSTAT), # current tree
425
454
             ]),
426
 
            (('', 'a file', 'a file id'), # common
 
455
            (('', 'a file', 'a-file-id'), # common
427
456
             [('f', '', 0, False, dirstate.DirState.NULLSTAT), # current
428
457
             ]),
429
458
            ])
438
467
        # and length:
439
468
        self.build_tree_contents([('tree/a file', 'new content\n')])
440
469
        expected_result = ([rev_id], [
441
 
            (('', '', tree.path2id('')), # common details
 
470
            (('', '', tree.get_root_id()), # common details
442
471
             [('d', '', 0, False, dirstate.DirState.NULLSTAT), # current tree
443
472
              ('d', '', 0, False, rev_id), # first parent details
444
473
             ]),
445
 
            (('', 'a file', 'a file id'), # common
 
474
            (('', 'a file', 'a-file-id'), # common
446
475
             [('f', '', 0, False, dirstate.DirState.NULLSTAT), # current
447
476
              ('f', 'c3ed76e4bfd45ff1763ca206055bca8e9fc28aa8', 24, False,
448
477
               rev_id), # first parent
465
494
        # and length again, giving us three distinct values:
466
495
        self.build_tree_contents([('tree/a file', 'new content\n')])
467
496
        expected_result = ([rev_id, rev_id2], [
468
 
            (('', '', tree.path2id('')), # common details
 
497
            (('', '', tree.get_root_id()), # common details
469
498
             [('d', '', 0, False, dirstate.DirState.NULLSTAT), # current tree
470
499
              ('d', '', 0, False, rev_id), # first parent details
471
 
              ('d', '', 0, False, rev_id2), # second parent details
 
500
              ('d', '', 0, False, rev_id), # second parent details
472
501
             ]),
473
 
            (('', 'a file', 'a file id'), # common
 
502
            (('', 'a file', 'a-file-id'), # common
474
503
             [('f', '', 0, False, dirstate.DirState.NULLSTAT), # current
475
504
              ('f', 'c3ed76e4bfd45ff1763ca206055bca8e9fc28aa8', 24, False,
476
505
               rev_id), # first parent
506
535
 
507
536
class TestDirStateOnFile(TestCaseWithDirState):
508
537
 
 
538
    def create_updated_dirstate(self):
 
539
        self.build_tree(['a-file'])
 
540
        tree = self.make_branch_and_tree('.')
 
541
        tree.add(['a-file'], ['a-id'])
 
542
        tree.commit('add a-file')
 
543
        # Save and unlock the state, re-open it in readonly mode
 
544
        state = dirstate.DirState.from_tree(tree, 'dirstate')
 
545
        state.save()
 
546
        state.unlock()
 
547
        state = dirstate.DirState.on_file('dirstate')
 
548
        state.lock_read()
 
549
        return state
 
550
 
509
551
    def test_construct_with_path(self):
510
552
        tree = self.make_branch_and_tree('tree')
511
553
        state = dirstate.DirState.from_tree(tree, 'dirstate.from_tree')
517
559
        # get a state object
518
560
        # no parents, default tree content
519
561
        expected_result = ([], [
520
 
            (('', '', tree.path2id('')), # common details
 
562
            (('', '', tree.get_root_id()), # common details
521
563
             # current tree details, but new from_tree skips statting, it
522
564
             # uses set_state_from_inventory, and thus depends on the
523
565
             # inventory state.
540
582
            state.unlock()
541
583
 
542
584
    def test_can_save_in_read_lock(self):
543
 
        self.build_tree(['a-file'])
544
 
        state = dirstate.DirState.initialize('dirstate')
545
 
        try:
546
 
            # No stat and no sha1 sum.
547
 
            state.add('a-file', 'a-file-id', 'file', None, '')
548
 
            state.save()
549
 
        finally:
550
 
            state.unlock()
551
 
 
552
 
        # Now open in readonly mode
553
 
        state = dirstate.DirState.on_file('dirstate')
554
 
        state.lock_read()
 
585
        state = self.create_updated_dirstate()
555
586
        try:
556
587
            entry = state._get_entry(0, path_utf8='a-file')
557
 
            # The current sha1 sum should be empty
558
 
            self.assertEqual('', entry[1][0][1])
 
588
            # The current size should be 0 (default)
 
589
            self.assertEqual(0, entry[1][0][2])
559
590
            # We should have a real entry.
560
591
            self.assertNotEqual((None, None), entry)
561
 
            sha1sum = state.update_entry(entry, 'a-file', os.lstat('a-file'))
562
 
            # We should have gotten a real sha1
 
592
            # Set the cutoff-time into the future, so things look cacheable
 
593
            state._sha_cutoff_time()
 
594
            state._cutoff_time += 10.0
 
595
            st = os.lstat('a-file')
 
596
            sha1sum = dirstate.update_entry(state, entry, 'a-file', st)
 
597
            # We updated the current sha1sum because the file is cacheable
563
598
            self.assertEqual('ecc5374e9ed82ad3ea3b4d452ea995a5fd3e70e3',
564
599
                             sha1sum)
565
600
 
566
601
            # The dirblock has been updated
567
 
            self.assertEqual(sha1sum, entry[1][0][1])
568
 
            self.assertEqual(dirstate.DirState.IN_MEMORY_MODIFIED,
 
602
            self.assertEqual(st.st_size, entry[1][0][2])
 
603
            self.assertEqual(dirstate.DirState.IN_MEMORY_HASH_MODIFIED,
569
604
                             state._dirblock_state)
570
605
 
571
606
            del entry
580
615
        state.lock_read()
581
616
        try:
582
617
            entry = state._get_entry(0, path_utf8='a-file')
583
 
            self.assertEqual(sha1sum, entry[1][0][1])
 
618
            self.assertEqual(st.st_size, entry[1][0][2])
584
619
        finally:
585
620
            state.unlock()
586
621
 
587
622
    def test_save_fails_quietly_if_locked(self):
588
623
        """If dirstate is locked, save will fail without complaining."""
589
 
        self.build_tree(['a-file'])
590
 
        state = dirstate.DirState.initialize('dirstate')
591
 
        try:
592
 
            # No stat and no sha1 sum.
593
 
            state.add('a-file', 'a-file-id', 'file', None, '')
594
 
            state.save()
595
 
        finally:
596
 
            state.unlock()
597
 
 
598
 
        state = dirstate.DirState.on_file('dirstate')
599
 
        state.lock_read()
 
624
        state = self.create_updated_dirstate()
600
625
        try:
601
626
            entry = state._get_entry(0, path_utf8='a-file')
602
 
            sha1sum = state.update_entry(entry, 'a-file', os.lstat('a-file'))
603
 
            # We should have gotten a real sha1
 
627
            # No cached sha1 yet.
 
628
            self.assertEqual('', entry[1][0][1])
 
629
            # Set the cutoff-time into the future, so things look cacheable
 
630
            state._sha_cutoff_time()
 
631
            state._cutoff_time += 10.0
 
632
            st = os.lstat('a-file')
 
633
            sha1sum = dirstate.update_entry(state, entry, 'a-file', st)
604
634
            self.assertEqual('ecc5374e9ed82ad3ea3b4d452ea995a5fd3e70e3',
605
635
                             sha1sum)
606
 
            self.assertEqual(dirstate.DirState.IN_MEMORY_MODIFIED,
 
636
            self.assertEqual(dirstate.DirState.IN_MEMORY_HASH_MODIFIED,
607
637
                             state._dirblock_state)
608
638
 
609
639
            # Now, before we try to save, grab another dirstate, and take out a
625
655
                state2.unlock()
626
656
        finally:
627
657
            state.unlock()
628
 
        
 
658
 
629
659
        # The file on disk should not be modified.
630
660
        state = dirstate.DirState.on_file('dirstate')
631
661
        state.lock_read()
635
665
        finally:
636
666
            state.unlock()
637
667
 
 
668
    def test_save_refuses_if_changes_aborted(self):
 
669
        self.build_tree(['a-file', 'a-dir/'])
 
670
        state = dirstate.DirState.initialize('dirstate')
 
671
        try:
 
672
            # No stat and no sha1 sum.
 
673
            state.add('a-file', 'a-file-id', 'file', None, '')
 
674
            state.save()
 
675
        finally:
 
676
            state.unlock()
 
677
 
 
678
        # The dirstate should include TREE_ROOT and 'a-file' and nothing else
 
679
        expected_blocks = [
 
680
            ('', [(('', '', 'TREE_ROOT'),
 
681
                   [('d', '', 0, False, dirstate.DirState.NULLSTAT)])]),
 
682
            ('', [(('', 'a-file', 'a-file-id'),
 
683
                   [('f', '', 0, False, dirstate.DirState.NULLSTAT)])]),
 
684
        ]
 
685
 
 
686
        state = dirstate.DirState.on_file('dirstate')
 
687
        state.lock_write()
 
688
        try:
 
689
            state._read_dirblocks_if_needed()
 
690
            self.assertEqual(expected_blocks, state._dirblocks)
 
691
 
 
692
            # Now modify the state, but mark it as inconsistent
 
693
            state.add('a-dir', 'a-dir-id', 'directory', None, '')
 
694
            state._changes_aborted = True
 
695
            state.save()
 
696
        finally:
 
697
            state.unlock()
 
698
 
 
699
        state = dirstate.DirState.on_file('dirstate')
 
700
        state.lock_read()
 
701
        try:
 
702
            state._read_dirblocks_if_needed()
 
703
            self.assertEqual(expected_blocks, state._dirblocks)
 
704
        finally:
 
705
            state.unlock()
 
706
 
638
707
 
639
708
class TestDirStateInitialize(TestCaseWithDirState):
640
709
 
648
717
        try:
649
718
            self.assertIsInstance(state, dirstate.DirState)
650
719
            lines = state.get_lines()
651
 
            self.assertFileEqual(''.join(state.get_lines()),
652
 
                'dirstate')
653
 
            self.check_state_with_reopen(expected_result, state)
654
 
        except:
 
720
        finally:
655
721
            state.unlock()
656
 
            raise
 
722
        # On win32 you can't read from a locked file, even within the same
 
723
        # process. So we have to unlock and release before we check the file
 
724
        # contents.
 
725
        self.assertFileEqual(''.join(lines), 'dirstate')
 
726
        state.lock_read() # check_state_with_reopen will unlock
 
727
        self.check_state_with_reopen(expected_result, state)
657
728
 
658
729
 
659
730
class TestDirStateManipulations(TestCaseWithDirState):
660
731
 
 
732
    def make_minimal_tree(self):
 
733
        tree1 = self.make_branch_and_memory_tree('tree1')
 
734
        tree1.lock_write()
 
735
        self.addCleanup(tree1.unlock)
 
736
        tree1.add('')
 
737
        revid1 = tree1.commit('foo')
 
738
        return tree1, revid1
 
739
 
 
740
    def test_update_minimal_updates_id_index(self):
 
741
        state = self.create_dirstate_with_root_and_subdir()
 
742
        self.addCleanup(state.unlock)
 
743
        id_index = state._get_id_index()
 
744
        self.assertEqual(['a-root-value', 'subdir-id'], sorted(id_index))
 
745
        state.add('file-name', 'file-id', 'file', None, '')
 
746
        self.assertEqual(['a-root-value', 'file-id', 'subdir-id'],
 
747
                         sorted(id_index))
 
748
        state.update_minimal(('', 'new-name', 'file-id'), 'f',
 
749
                             path_utf8='new-name')
 
750
        self.assertEqual(['a-root-value', 'file-id', 'subdir-id'],
 
751
                         sorted(id_index))
 
752
        self.assertEqual([('', 'new-name', 'file-id')],
 
753
                         sorted(id_index['file-id']))
 
754
        state._validate()
 
755
 
661
756
    def test_set_state_from_inventory_no_content_no_parents(self):
662
757
        # setting the current inventory is a slow but important api to support.
663
 
        tree1 = self.make_branch_and_memory_tree('tree1')
664
 
        tree1.lock_write()
665
 
        try:
666
 
            tree1.add('')
667
 
            revid1 = tree1.commit('foo').encode('utf8')
668
 
            root_id = tree1.inventory.root.file_id
669
 
            inv = tree1.inventory
670
 
        finally:
671
 
            tree1.unlock()
 
758
        tree1, revid1 = self.make_minimal_tree()
 
759
        inv = tree1.inventory
 
760
        root_id = inv.path2id('')
672
761
        expected_result = [], [
673
762
            (('', '', root_id), [
674
763
             ('d', '', 0, False, dirstate.DirState.NULLSTAT)])]
686
775
            # This will unlock it
687
776
            self.check_state_with_reopen(expected_result, state)
688
777
 
 
778
    def test_set_state_from_scratch_no_parents(self):
 
779
        tree1, revid1 = self.make_minimal_tree()
 
780
        inv = tree1.inventory
 
781
        root_id = inv.path2id('')
 
782
        expected_result = [], [
 
783
            (('', '', root_id), [
 
784
             ('d', '', 0, False, dirstate.DirState.NULLSTAT)])]
 
785
        state = dirstate.DirState.initialize('dirstate')
 
786
        try:
 
787
            state.set_state_from_scratch(inv, [], [])
 
788
            self.assertEqual(dirstate.DirState.IN_MEMORY_MODIFIED,
 
789
                             state._header_state)
 
790
            self.assertEqual(dirstate.DirState.IN_MEMORY_MODIFIED,
 
791
                             state._dirblock_state)
 
792
        except:
 
793
            state.unlock()
 
794
            raise
 
795
        else:
 
796
            # This will unlock it
 
797
            self.check_state_with_reopen(expected_result, state)
 
798
 
 
799
    def test_set_state_from_scratch_identical_parent(self):
 
800
        tree1, revid1 = self.make_minimal_tree()
 
801
        inv = tree1.inventory
 
802
        root_id = inv.path2id('')
 
803
        rev_tree1 = tree1.branch.repository.revision_tree(revid1)
 
804
        d_entry = ('d', '', 0, False, dirstate.DirState.NULLSTAT)
 
805
        parent_entry = ('d', '', 0, False, revid1)
 
806
        expected_result = [revid1], [
 
807
            (('', '', root_id), [d_entry, parent_entry])]
 
808
        state = dirstate.DirState.initialize('dirstate')
 
809
        try:
 
810
            state.set_state_from_scratch(inv, [(revid1, rev_tree1)], [])
 
811
            self.assertEqual(dirstate.DirState.IN_MEMORY_MODIFIED,
 
812
                             state._header_state)
 
813
            self.assertEqual(dirstate.DirState.IN_MEMORY_MODIFIED,
 
814
                             state._dirblock_state)
 
815
        except:
 
816
            state.unlock()
 
817
            raise
 
818
        else:
 
819
            # This will unlock it
 
820
            self.check_state_with_reopen(expected_result, state)
 
821
 
 
822
    def test_set_state_from_inventory_preserves_hashcache(self):
 
823
        # https://bugs.launchpad.net/bzr/+bug/146176
 
824
        # set_state_from_inventory should preserve the stat and hash value for
 
825
        # workingtree files that are not changed by the inventory.
 
826
 
 
827
        tree = self.make_branch_and_tree('.')
 
828
        # depends on the default format using dirstate...
 
829
        tree.lock_write()
 
830
        try:
 
831
            # make a dirstate with some valid hashcache data
 
832
            # file on disk, but that's not needed for this test
 
833
            foo_contents = 'contents of foo'
 
834
            self.build_tree_contents([('foo', foo_contents)])
 
835
            tree.add('foo', 'foo-id')
 
836
 
 
837
            foo_stat = os.stat('foo')
 
838
            foo_packed = dirstate.pack_stat(foo_stat)
 
839
            foo_sha = osutils.sha_string(foo_contents)
 
840
            foo_size = len(foo_contents)
 
841
 
 
842
            # should not be cached yet, because the file's too fresh
 
843
            self.assertEqual(
 
844
                (('', 'foo', 'foo-id',),
 
845
                 [('f', '', 0, False, dirstate.DirState.NULLSTAT)]),
 
846
                tree._dirstate._get_entry(0, 'foo-id'))
 
847
            # poke in some hashcache information - it wouldn't normally be
 
848
            # stored because it's too fresh
 
849
            tree._dirstate.update_minimal(
 
850
                ('', 'foo', 'foo-id'),
 
851
                'f', False, foo_sha, foo_packed, foo_size, 'foo')
 
852
            # now should be cached
 
853
            self.assertEqual(
 
854
                (('', 'foo', 'foo-id',),
 
855
                 [('f', foo_sha, foo_size, False, foo_packed)]),
 
856
                tree._dirstate._get_entry(0, 'foo-id'))
 
857
 
 
858
            # extract the inventory, and add something to it
 
859
            inv = tree._get_inventory()
 
860
            # should see the file we poked in...
 
861
            self.assertTrue(inv.has_id('foo-id'))
 
862
            self.assertTrue(inv.has_filename('foo'))
 
863
            inv.add_path('bar', 'file', 'bar-id')
 
864
            tree._dirstate._validate()
 
865
            # this used to cause it to lose its hashcache
 
866
            tree._dirstate.set_state_from_inventory(inv)
 
867
            tree._dirstate._validate()
 
868
        finally:
 
869
            tree.unlock()
 
870
 
 
871
        tree.lock_read()
 
872
        try:
 
873
            # now check that the state still has the original hashcache value
 
874
            state = tree._dirstate
 
875
            state._validate()
 
876
            foo_tuple = state._get_entry(0, path_utf8='foo')
 
877
            self.assertEqual(
 
878
                (('', 'foo', 'foo-id',),
 
879
                 [('f', foo_sha, len(foo_contents), False,
 
880
                   dirstate.pack_stat(foo_stat))]),
 
881
                foo_tuple)
 
882
        finally:
 
883
            tree.unlock()
 
884
 
 
885
    def test_set_state_from_inventory_mixed_paths(self):
 
886
        tree1 = self.make_branch_and_tree('tree1')
 
887
        self.build_tree(['tree1/a/', 'tree1/a/b/', 'tree1/a-b/',
 
888
                         'tree1/a/b/foo', 'tree1/a-b/bar'])
 
889
        tree1.lock_write()
 
890
        try:
 
891
            tree1.add(['a', 'a/b', 'a-b', 'a/b/foo', 'a-b/bar'],
 
892
                      ['a-id', 'b-id', 'a-b-id', 'foo-id', 'bar-id'])
 
893
            tree1.commit('rev1', rev_id='rev1')
 
894
            root_id = tree1.get_root_id()
 
895
            inv = tree1.inventory
 
896
        finally:
 
897
            tree1.unlock()
 
898
        expected_result1 = [('', '', root_id, 'd'),
 
899
                            ('', 'a', 'a-id', 'd'),
 
900
                            ('', 'a-b', 'a-b-id', 'd'),
 
901
                            ('a', 'b', 'b-id', 'd'),
 
902
                            ('a/b', 'foo', 'foo-id', 'f'),
 
903
                            ('a-b', 'bar', 'bar-id', 'f'),
 
904
                           ]
 
905
        expected_result2 = [('', '', root_id, 'd'),
 
906
                            ('', 'a', 'a-id', 'd'),
 
907
                            ('', 'a-b', 'a-b-id', 'd'),
 
908
                            ('a-b', 'bar', 'bar-id', 'f'),
 
909
                           ]
 
910
        state = dirstate.DirState.initialize('dirstate')
 
911
        try:
 
912
            state.set_state_from_inventory(inv)
 
913
            values = []
 
914
            for entry in state._iter_entries():
 
915
                values.append(entry[0] + entry[1][0][:1])
 
916
            self.assertEqual(expected_result1, values)
 
917
            del inv['b-id']
 
918
            state.set_state_from_inventory(inv)
 
919
            values = []
 
920
            for entry in state._iter_entries():
 
921
                values.append(entry[0] + entry[1][0][:1])
 
922
            self.assertEqual(expected_result2, values)
 
923
        finally:
 
924
            state.unlock()
 
925
 
689
926
    def test_set_path_id_no_parents(self):
690
927
        """The id of a path can be changed trivally with no parents."""
691
928
        state = dirstate.DirState.initialize('dirstate')
692
929
        try:
693
930
            # check precondition to be sure the state does change appropriately.
694
 
            self.assertEqual(
695
 
                [(('', '', 'TREE_ROOT'), [('d', '', 0, False,
696
 
                   'xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx')])],
697
 
                list(state._iter_entries()))
698
 
            state.set_path_id('', 'foobarbaz')
699
 
            expected_rows = [
700
 
                (('', '', 'foobarbaz'), [('d', '', 0, False,
701
 
                   'xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx')])]
 
931
            root_entry = (('', '', 'TREE_ROOT'), [('d', '', 0, False, 'x'*32)])
 
932
            self.assertEqual([root_entry], list(state._iter_entries()))
 
933
            self.assertEqual(root_entry, state._get_entry(0, path_utf8=''))
 
934
            self.assertEqual(root_entry,
 
935
                             state._get_entry(0, fileid_utf8='TREE_ROOT'))
 
936
            self.assertEqual((None, None),
 
937
                             state._get_entry(0, fileid_utf8='second-root-id'))
 
938
            state.set_path_id('', 'second-root-id')
 
939
            new_root_entry = (('', '', 'second-root-id'),
 
940
                              [('d', '', 0, False, 'x'*32)])
 
941
            expected_rows = [new_root_entry]
702
942
            self.assertEqual(expected_rows, list(state._iter_entries()))
 
943
            self.assertEqual(new_root_entry, state._get_entry(0, path_utf8=''))
 
944
            self.assertEqual(new_root_entry, 
 
945
                             state._get_entry(0, fileid_utf8='second-root-id'))
 
946
            self.assertEqual((None, None),
 
947
                             state._get_entry(0, fileid_utf8='TREE_ROOT'))
703
948
            # should work across save too
704
949
            state.save()
705
950
        finally:
723
968
        state._validate()
724
969
        try:
725
970
            state.set_parent_trees([('parent-revid', rt)], ghosts=[])
726
 
            state.set_path_id('', 'foobarbaz')
 
971
            root_entry = (('', '', 'TREE_ROOT'),
 
972
                          [('d', '', 0, False, 'x'*32),
 
973
                           ('d', '', 0, False, 'parent-revid')])
 
974
            self.assertEqual(root_entry, state._get_entry(0, path_utf8=''))
 
975
            self.assertEqual(root_entry,
 
976
                             state._get_entry(0, fileid_utf8='TREE_ROOT'))
 
977
            self.assertEqual((None, None),
 
978
                             state._get_entry(0, fileid_utf8='Asecond-root-id'))
 
979
            state.set_path_id('', 'Asecond-root-id')
727
980
            state._validate()
728
981
            # now see that it is what we expected
729
 
            expected_rows = [
730
 
                (('', '', 'TREE_ROOT'),
731
 
                    [('a', '', 0, False, ''),
732
 
                     ('d', '', 0, False, 'parent-revid'),
733
 
                     ]),
734
 
                (('', '', 'foobarbaz'),
735
 
                    [('d', '', 0, False, ''),
736
 
                     ('a', '', 0, False, ''),
737
 
                     ]),
738
 
                ]
 
982
            old_root_entry = (('', '', 'TREE_ROOT'),
 
983
                              [('a', '', 0, False, ''),
 
984
                               ('d', '', 0, False, 'parent-revid')])
 
985
            new_root_entry = (('', '', 'Asecond-root-id'),
 
986
                              [('d', '', 0, False, ''),
 
987
                               ('a', '', 0, False, '')])
 
988
            expected_rows = [new_root_entry, old_root_entry]
739
989
            state._validate()
740
990
            self.assertEqual(expected_rows, list(state._iter_entries()))
 
991
            self.assertEqual(new_root_entry, state._get_entry(0, path_utf8=''))
 
992
            self.assertEqual(old_root_entry, state._get_entry(1, path_utf8=''))
 
993
            self.assertEqual((None, None),
 
994
                             state._get_entry(0, fileid_utf8='TREE_ROOT'))
 
995
            self.assertEqual(old_root_entry,
 
996
                             state._get_entry(1, fileid_utf8='TREE_ROOT'))
 
997
            self.assertEqual(new_root_entry,
 
998
                             state._get_entry(0, fileid_utf8='Asecond-root-id'))
 
999
            self.assertEqual((None, None),
 
1000
                             state._get_entry(1, fileid_utf8='Asecond-root-id'))
741
1001
            # should work across save too
742
1002
            state.save()
743
1003
        finally:
759
1019
        finally:
760
1020
            state.unlock()
761
1021
 
762
 
 
763
1022
    def test_set_parent_trees_no_content(self):
764
1023
        # set_parent_trees is a slow but important api to support.
765
1024
        tree1 = self.make_branch_and_memory_tree('tree1')
770
1029
        finally:
771
1030
            tree1.unlock()
772
1031
        branch2 = tree1.branch.bzrdir.clone('tree2').open_branch()
773
 
        tree2 = MemoryTree.create_on_branch(branch2)
 
1032
        tree2 = memorytree.MemoryTree.create_on_branch(branch2)
774
1033
        tree2.lock_write()
775
1034
        try:
776
1035
            revid2 = tree2.commit('foo')
777
 
            root_id = tree2.inventory.root.file_id
 
1036
            root_id = tree2.get_root_id()
778
1037
        finally:
779
1038
            tree2.unlock()
780
1039
        state = dirstate.DirState.initialize('dirstate')
808
1067
            state.set_parent_trees(
809
1068
                ((revid1, tree1.branch.repository.revision_tree(revid1)),
810
1069
                 (revid2, tree2.branch.repository.revision_tree(revid2)),
811
 
                 ('ghost-rev', tree2.branch.repository.revision_tree(None))),
 
1070
                 ('ghost-rev', tree2.branch.repository.revision_tree(
 
1071
                                   _mod_revision.NULL_REVISION))),
812
1072
                ['ghost-rev'])
813
1073
            self.assertEqual([revid1, revid2, 'ghost-rev'],
814
1074
                             state.get_parent_ids())
818
1078
                [(('', '', root_id), [
819
1079
                  ('d', '', 0, False, dirstate.DirState.NULLSTAT),
820
1080
                  ('d', '', 0, False, revid1),
821
 
                  ('d', '', 0, False, revid2)
 
1081
                  ('d', '', 0, False, revid1)
822
1082
                  ])],
823
1083
                list(state._iter_entries()))
824
1084
        finally:
839
1099
        finally:
840
1100
            tree1.unlock()
841
1101
        branch2 = tree1.branch.bzrdir.clone('tree2').open_branch()
842
 
        tree2 = MemoryTree.create_on_branch(branch2)
 
1102
        tree2 = memorytree.MemoryTree.create_on_branch(branch2)
843
1103
        tree2.lock_write()
844
1104
        try:
845
1105
            tree2.put_file_bytes_non_atomic('file-id', 'new file-content')
846
1106
            revid2 = tree2.commit('foo')
847
 
            root_id = tree2.inventory.root.file_id
 
1107
            root_id = tree2.get_root_id()
848
1108
        finally:
849
1109
            tree2.unlock()
850
1110
        # check the layout in memory
852
1112
            (('', '', root_id), [
853
1113
             ('d', '', 0, False, dirstate.DirState.NULLSTAT),
854
1114
             ('d', '', 0, False, revid1.encode('utf8')),
855
 
             ('d', '', 0, False, revid2.encode('utf8'))
 
1115
             ('d', '', 0, False, revid1.encode('utf8'))
856
1116
             ]),
857
1117
            (('', 'a file', 'file-id'), [
858
1118
             ('a', '', 0, False, ''),
890
1150
            (('', '', 'TREE_ROOT'), [
891
1151
             ('d', '', 0, False, dirstate.DirState.NULLSTAT), # current tree
892
1152
             ]),
893
 
            (('', 'a file', 'a file id'), [
 
1153
            (('', 'a file', 'a-file-id'), [
894
1154
             ('f', '1'*20, 19, False, dirstate.pack_stat(stat)), # current tree
895
1155
             ]),
896
1156
            ]
897
1157
        try:
898
 
            state.add('a file', 'a file id', 'file', stat, '1'*20)
 
1158
            state.add('a file', 'a-file-id', 'file', stat, '1'*20)
899
1159
            # having added it, it should be in the output of iter_entries.
900
1160
            self.assertEqual(expected_entries, list(state._iter_entries()))
901
1161
            # saving and reloading should not affect this.
904
1164
            state.unlock()
905
1165
        state = dirstate.DirState.on_file('dirstate')
906
1166
        state.lock_read()
907
 
        try:
908
 
            self.assertEqual(expected_entries, list(state._iter_entries()))
909
 
        finally:
910
 
            state.unlock()
 
1167
        self.addCleanup(state.unlock)
 
1168
        self.assertEqual(expected_entries, list(state._iter_entries()))
911
1169
 
912
1170
    def test_add_path_to_unversioned_directory(self):
913
1171
        """Adding a path to an unversioned directory should error.
918
1176
        """
919
1177
        self.build_tree(['unversioned/', 'unversioned/a file'])
920
1178
        state = dirstate.DirState.initialize('dirstate')
921
 
        try:
922
 
            self.assertRaises(errors.NotVersionedError, state.add,
923
 
                'unversioned/a file', 'a file id', 'file', None, None)
924
 
        finally:
925
 
            state.unlock()
 
1179
        self.addCleanup(state.unlock)
 
1180
        self.assertRaises(errors.NotVersionedError, state.add,
 
1181
                          'unversioned/a file', 'a-file-id', 'file', None, None)
926
1182
 
927
1183
    def test_add_directory_to_root_no_parents_all_data(self):
928
1184
        # The most trivial addition of a dir is when there are no parents and
948
1204
            state.unlock()
949
1205
        state = dirstate.DirState.on_file('dirstate')
950
1206
        state.lock_read()
 
1207
        self.addCleanup(state.unlock)
951
1208
        state._validate()
952
 
        try:
953
 
            self.assertEqual(expected_entries, list(state._iter_entries()))
954
 
        finally:
955
 
            state.unlock()
 
1209
        self.assertEqual(expected_entries, list(state._iter_entries()))
956
1210
 
957
 
    def test_add_symlink_to_root_no_parents_all_data(self):
 
1211
    def _test_add_symlink_to_root_no_parents_all_data(self, link_name, target):
958
1212
        # The most trivial addition of a symlink when there are no parents and
959
1213
        # its in the root and all data about the file is supplied
960
1214
        # bzr doesn't support fake symlinks on windows, yet.
961
 
        if not has_symlinks():
962
 
            raise TestSkipped("No symlink support")
963
 
        os.symlink('target', 'a link')
964
 
        stat = os.lstat('a link')
 
1215
        self.requireFeature(features.SymlinkFeature)
 
1216
        os.symlink(target, link_name)
 
1217
        stat = os.lstat(link_name)
965
1218
        expected_entries = [
966
1219
            (('', '', 'TREE_ROOT'), [
967
1220
             ('d', '', 0, False, dirstate.DirState.NULLSTAT), # current tree
968
1221
             ]),
969
 
            (('', 'a link', 'a link id'), [
970
 
             ('l', 'target', 6, False, dirstate.pack_stat(stat)), # current tree
 
1222
            (('', link_name.encode('UTF-8'), 'a link id'), [
 
1223
             ('l', target.encode('UTF-8'), stat[6],
 
1224
              False, dirstate.pack_stat(stat)), # current tree
971
1225
             ]),
972
1226
            ]
973
1227
        state = dirstate.DirState.initialize('dirstate')
974
1228
        try:
975
 
            state.add('a link', 'a link id', 'symlink', stat, 'target')
 
1229
            state.add(link_name, 'a link id', 'symlink', stat,
 
1230
                      target.encode('UTF-8'))
976
1231
            # having added it, it should be in the output of iter_entries.
977
1232
            self.assertEqual(expected_entries, list(state._iter_entries()))
978
1233
            # saving and reloading should not affect this.
981
1236
            state.unlock()
982
1237
        state = dirstate.DirState.on_file('dirstate')
983
1238
        state.lock_read()
984
 
        try:
985
 
            self.assertEqual(expected_entries, list(state._iter_entries()))
986
 
        finally:
987
 
            state.unlock()
 
1239
        self.addCleanup(state.unlock)
 
1240
        self.assertEqual(expected_entries, list(state._iter_entries()))
 
1241
 
 
1242
    def test_add_symlink_to_root_no_parents_all_data(self):
 
1243
        self._test_add_symlink_to_root_no_parents_all_data('a link', 'target')
 
1244
 
 
1245
    def test_add_symlink_unicode_to_root_no_parents_all_data(self):
 
1246
        self.requireFeature(features.UnicodeFilenameFeature)
 
1247
        self._test_add_symlink_to_root_no_parents_all_data(
 
1248
            u'\N{Euro Sign}link', u'targ\N{Euro Sign}et')
988
1249
 
989
1250
    def test_add_directory_and_child_no_parents_all_data(self):
990
1251
        # after adding a directory, we should be able to add children to it.
998
1259
            (('', 'a dir', 'a dir id'), [
999
1260
             ('d', '', 0, False, dirstate.pack_stat(dirstat)), # current tree
1000
1261
             ]),
1001
 
            (('a dir', 'a file', 'a file id'), [
 
1262
            (('a dir', 'a file', 'a-file-id'), [
1002
1263
             ('f', '1'*20, 25, False,
1003
1264
              dirstate.pack_stat(filestat)), # current tree details
1004
1265
             ]),
1006
1267
        state = dirstate.DirState.initialize('dirstate')
1007
1268
        try:
1008
1269
            state.add('a dir', 'a dir id', 'directory', dirstat, None)
1009
 
            state.add('a dir/a file', 'a file id', 'file', filestat, '1'*20)
 
1270
            state.add('a dir/a file', 'a-file-id', 'file', filestat, '1'*20)
1010
1271
            # added it, it should be in the output of iter_entries.
1011
1272
            self.assertEqual(expected_entries, list(state._iter_entries()))
1012
1273
            # saving and reloading should not affect this.
1015
1276
            state.unlock()
1016
1277
        state = dirstate.DirState.on_file('dirstate')
1017
1278
        state.lock_read()
1018
 
        try:
1019
 
            self.assertEqual(expected_entries, list(state._iter_entries()))
1020
 
        finally:
1021
 
            state.unlock()
 
1279
        self.addCleanup(state.unlock)
 
1280
        self.assertEqual(expected_entries, list(state._iter_entries()))
1022
1281
 
1023
1282
    def test_add_tree_reference(self):
1024
1283
        # make a dirstate and add a tree reference
1038
1297
            state.unlock()
1039
1298
        # now check we can read it back
1040
1299
        state.lock_read()
 
1300
        self.addCleanup(state.unlock)
1041
1301
        state._validate()
1042
 
        try:
1043
 
            entry2 = state._get_entry(0, 'subdir-id', 'subdir')
1044
 
            self.assertEqual(entry, entry2)
1045
 
            self.assertEqual(entry, expected_entry)
1046
 
            # and lookup by id should work too
1047
 
            entry2 = state._get_entry(0, fileid_utf8='subdir-id')
1048
 
            self.assertEqual(entry, expected_entry)
1049
 
        finally:
1050
 
            state.unlock()
 
1302
        entry2 = state._get_entry(0, 'subdir-id', 'subdir')
 
1303
        self.assertEqual(entry, entry2)
 
1304
        self.assertEqual(entry, expected_entry)
 
1305
        # and lookup by id should work too
 
1306
        entry2 = state._get_entry(0, fileid_utf8='subdir-id')
 
1307
        self.assertEqual(entry, expected_entry)
1051
1308
 
1052
1309
    def test_add_forbidden_names(self):
1053
1310
        state = dirstate.DirState.initialize('dirstate')
1057
1314
        self.assertRaises(errors.BzrError,
1058
1315
            state.add, '..', 'ass-id', 'directory', None, None)
1059
1316
 
 
1317
    def test_set_state_with_rename_b_a_bug_395556(self):
 
1318
        # bug 395556 uncovered a bug where the dirstate ends up with a false
 
1319
        # relocation record - in a tree with no parents there should be no
 
1320
        # absent or relocated records. This then leads to further corruption
 
1321
        # when a commit occurs, as the incorrect relocation gathers an
 
1322
        # incorrect absent in tree 1, and future changes go to pot.
 
1323
        tree1 = self.make_branch_and_tree('tree1')
 
1324
        self.build_tree(['tree1/b'])
 
1325
        tree1.lock_write()
 
1326
        try:
 
1327
            tree1.add(['b'], ['b-id'])
 
1328
            root_id = tree1.get_root_id()
 
1329
            inv = tree1.inventory
 
1330
            state = dirstate.DirState.initialize('dirstate')
 
1331
            try:
 
1332
                # Set the initial state with 'b'
 
1333
                state.set_state_from_inventory(inv)
 
1334
                inv.rename('b-id', root_id, 'a')
 
1335
                # Set the new state with 'a', which currently corrupts.
 
1336
                state.set_state_from_inventory(inv)
 
1337
                expected_result1 = [('', '', root_id, 'd'),
 
1338
                                    ('', 'a', 'b-id', 'f'),
 
1339
                                   ]
 
1340
                values = []
 
1341
                for entry in state._iter_entries():
 
1342
                    values.append(entry[0] + entry[1][0][:1])
 
1343
                self.assertEqual(expected_result1, values)
 
1344
            finally:
 
1345
                state.unlock()
 
1346
        finally:
 
1347
            tree1.unlock()
 
1348
 
 
1349
 
 
1350
class TestDirStateHashUpdates(TestCaseWithDirState):
 
1351
 
 
1352
    def do_update_entry(self, state, path):
 
1353
        entry = state._get_entry(0, path_utf8=path)
 
1354
        stat = os.lstat(path)
 
1355
        return dirstate.update_entry(state, entry, os.path.abspath(path), stat)
 
1356
 
 
1357
    def _read_state_content(self, state):
 
1358
        """Read the content of the dirstate file.
 
1359
 
 
1360
        On Windows when one process locks a file, you can't even open() the
 
1361
        file in another process (to read it). So we go directly to
 
1362
        state._state_file. This should always be the exact disk representation,
 
1363
        so it is reasonable to do so.
 
1364
        DirState also always seeks before reading, so it doesn't matter if we
 
1365
        bump the file pointer.
 
1366
        """
 
1367
        state._state_file.seek(0)
 
1368
        return state._state_file.read()
 
1369
 
 
1370
    def test_worth_saving_limit_avoids_writing(self):
 
1371
        tree = self.make_branch_and_tree('.')
 
1372
        self.build_tree(['c', 'd'])
 
1373
        tree.lock_write()
 
1374
        tree.add(['c', 'd'], ['c-id', 'd-id'])
 
1375
        tree.commit('add c and d')
 
1376
        state = InstrumentedDirState.on_file(tree.current_dirstate()._filename,
 
1377
                                             worth_saving_limit=2)
 
1378
        tree.unlock()
 
1379
        state.lock_write()
 
1380
        self.addCleanup(state.unlock)
 
1381
        state._read_dirblocks_if_needed()
 
1382
        state.adjust_time(+20) # Allow things to be cached
 
1383
        self.assertEqual(dirstate.DirState.IN_MEMORY_UNMODIFIED,
 
1384
                         state._dirblock_state)
 
1385
        content = self._read_state_content(state)
 
1386
        self.do_update_entry(state, 'c')
 
1387
        self.assertEqual(1, len(state._known_hash_changes))
 
1388
        self.assertEqual(dirstate.DirState.IN_MEMORY_HASH_MODIFIED,
 
1389
                         state._dirblock_state)
 
1390
        state.save()
 
1391
        # It should not have set the state to IN_MEMORY_UNMODIFIED because the
 
1392
        # hash values haven't been written out.
 
1393
        self.assertEqual(dirstate.DirState.IN_MEMORY_HASH_MODIFIED,
 
1394
                         state._dirblock_state)
 
1395
        self.assertEqual(content, self._read_state_content(state))
 
1396
        self.assertEqual(dirstate.DirState.IN_MEMORY_HASH_MODIFIED,
 
1397
                         state._dirblock_state)
 
1398
        self.do_update_entry(state, 'd')
 
1399
        self.assertEqual(2, len(state._known_hash_changes))
 
1400
        state.save()
 
1401
        self.assertEqual(dirstate.DirState.IN_MEMORY_UNMODIFIED,
 
1402
                         state._dirblock_state)
 
1403
        self.assertEqual(0, len(state._known_hash_changes))
 
1404
 
1060
1405
 
1061
1406
class TestGetLines(TestCaseWithDirState):
1062
1407
 
1275
1620
            state.unlock()
1276
1621
 
1277
1622
 
1278
 
class TestDirstateSortOrder(TestCaseWithTransport):
 
1623
class TestIterChildEntries(TestCaseWithDirState):
 
1624
 
 
1625
    def create_dirstate_with_two_trees(self):
 
1626
        """This dirstate contains multiple files and directories.
 
1627
 
 
1628
         /        a-root-value
 
1629
         a/       a-dir
 
1630
         b/       b-dir
 
1631
         c        c-file
 
1632
         d        d-file
 
1633
         a/e/     e-dir
 
1634
         a/f      f-file
 
1635
         b/g      g-file
 
1636
         b/h\xc3\xa5  h-\xc3\xa5-file  #This is u'\xe5' encoded into utf-8
 
1637
 
 
1638
        Notice that a/e is an empty directory.
 
1639
 
 
1640
        There is one parent tree, which has the same shape with the following variations:
 
1641
        b/g in the parent is gone.
 
1642
        b/h in the parent has a different id
 
1643
        b/i is new in the parent
 
1644
        c is renamed to b/j in the parent
 
1645
 
 
1646
        :return: The dirstate, still write-locked.
 
1647
        """
 
1648
        packed_stat = 'AAAAREUHaIpFB2iKAAADAQAtkqUAAIGk'
 
1649
        null_sha = 'xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx'
 
1650
        NULL_PARENT_DETAILS = dirstate.DirState.NULL_PARENT_DETAILS
 
1651
        root_entry = ('', '', 'a-root-value'), [
 
1652
            ('d', '', 0, False, packed_stat),
 
1653
            ('d', '', 0, False, 'parent-revid'),
 
1654
            ]
 
1655
        a_entry = ('', 'a', 'a-dir'), [
 
1656
            ('d', '', 0, False, packed_stat),
 
1657
            ('d', '', 0, False, 'parent-revid'),
 
1658
            ]
 
1659
        b_entry = ('', 'b', 'b-dir'), [
 
1660
            ('d', '', 0, False, packed_stat),
 
1661
            ('d', '', 0, False, 'parent-revid'),
 
1662
            ]
 
1663
        c_entry = ('', 'c', 'c-file'), [
 
1664
            ('f', null_sha, 10, False, packed_stat),
 
1665
            ('r', 'b/j', 0, False, ''),
 
1666
            ]
 
1667
        d_entry = ('', 'd', 'd-file'), [
 
1668
            ('f', null_sha, 20, False, packed_stat),
 
1669
            ('f', 'd', 20, False, 'parent-revid'),
 
1670
            ]
 
1671
        e_entry = ('a', 'e', 'e-dir'), [
 
1672
            ('d', '', 0, False, packed_stat),
 
1673
            ('d', '', 0, False, 'parent-revid'),
 
1674
            ]
 
1675
        f_entry = ('a', 'f', 'f-file'), [
 
1676
            ('f', null_sha, 30, False, packed_stat),
 
1677
            ('f', 'f', 20, False, 'parent-revid'),
 
1678
            ]
 
1679
        g_entry = ('b', 'g', 'g-file'), [
 
1680
            ('f', null_sha, 30, False, packed_stat),
 
1681
            NULL_PARENT_DETAILS,
 
1682
            ]
 
1683
        h_entry1 = ('b', 'h\xc3\xa5', 'h-\xc3\xa5-file1'), [
 
1684
            ('f', null_sha, 40, False, packed_stat),
 
1685
            NULL_PARENT_DETAILS,
 
1686
            ]
 
1687
        h_entry2 = ('b', 'h\xc3\xa5', 'h-\xc3\xa5-file2'), [
 
1688
            NULL_PARENT_DETAILS,
 
1689
            ('f', 'h', 20, False, 'parent-revid'),
 
1690
            ]
 
1691
        i_entry = ('b', 'i', 'i-file'), [
 
1692
            NULL_PARENT_DETAILS,
 
1693
            ('f', 'h', 20, False, 'parent-revid'),
 
1694
            ]
 
1695
        j_entry = ('b', 'j', 'c-file'), [
 
1696
            ('r', 'c', 0, False, ''),
 
1697
            ('f', 'j', 20, False, 'parent-revid'),
 
1698
            ]
 
1699
        dirblocks = []
 
1700
        dirblocks.append(('', [root_entry]))
 
1701
        dirblocks.append(('', [a_entry, b_entry, c_entry, d_entry]))
 
1702
        dirblocks.append(('a', [e_entry, f_entry]))
 
1703
        dirblocks.append(('b', [g_entry, h_entry1, h_entry2, i_entry, j_entry]))
 
1704
        state = dirstate.DirState.initialize('dirstate')
 
1705
        state._validate()
 
1706
        try:
 
1707
            state._set_data(['parent'], dirblocks)
 
1708
        except:
 
1709
            state.unlock()
 
1710
            raise
 
1711
        return state, dirblocks
 
1712
 
 
1713
    def test_iter_children_b(self):
 
1714
        state, dirblocks = self.create_dirstate_with_two_trees()
 
1715
        self.addCleanup(state.unlock)
 
1716
        expected_result = []
 
1717
        expected_result.append(dirblocks[3][1][2]) # h2
 
1718
        expected_result.append(dirblocks[3][1][3]) # i
 
1719
        expected_result.append(dirblocks[3][1][4]) # j
 
1720
        self.assertEqual(expected_result,
 
1721
            list(state._iter_child_entries(1, 'b')))
 
1722
 
 
1723
    def test_iter_child_root(self):
 
1724
        state, dirblocks = self.create_dirstate_with_two_trees()
 
1725
        self.addCleanup(state.unlock)
 
1726
        expected_result = []
 
1727
        expected_result.append(dirblocks[1][1][0]) # a
 
1728
        expected_result.append(dirblocks[1][1][1]) # b
 
1729
        expected_result.append(dirblocks[1][1][3]) # d
 
1730
        expected_result.append(dirblocks[2][1][0]) # e
 
1731
        expected_result.append(dirblocks[2][1][1]) # f
 
1732
        expected_result.append(dirblocks[3][1][2]) # h2
 
1733
        expected_result.append(dirblocks[3][1][3]) # i
 
1734
        expected_result.append(dirblocks[3][1][4]) # j
 
1735
        self.assertEqual(expected_result,
 
1736
            list(state._iter_child_entries(1, '')))
 
1737
 
 
1738
 
 
1739
class TestDirstateSortOrder(tests.TestCaseWithTransport):
1279
1740
    """Test that DirState adds entries in the right order."""
1280
1741
 
1281
1742
    def test_add_sorting(self):
1330
1791
 
1331
1792
        # *really* cheesy way to just get an empty tree
1332
1793
        repo = self.make_repository('repo')
1333
 
        empty_tree = repo.revision_tree(None)
 
1794
        empty_tree = repo.revision_tree(_mod_revision.NULL_REVISION)
1334
1795
        state.set_parent_trees([('null:', empty_tree)], [])
1335
1796
 
1336
1797
        dirblock_names = [d[0] for d in state._dirblocks]
1340
1801
class InstrumentedDirState(dirstate.DirState):
1341
1802
    """An DirState with instrumented sha1 functionality."""
1342
1803
 
1343
 
    def __init__(self, path):
1344
 
        super(InstrumentedDirState, self).__init__(path)
 
1804
    def __init__(self, path, sha1_provider, worth_saving_limit=0):
 
1805
        super(InstrumentedDirState, self).__init__(path, sha1_provider,
 
1806
            worth_saving_limit=worth_saving_limit)
1345
1807
        self._time_offset = 0
1346
1808
        self._log = []
 
1809
        # member is dynamically set in DirState.__init__ to turn on trace
 
1810
        self._sha1_provider = sha1_provider
 
1811
        self._sha1_file = self._sha1_file_and_log
1347
1812
 
1348
1813
    def _sha_cutoff_time(self):
1349
1814
        timestamp = super(InstrumentedDirState, self)._sha_cutoff_time()
1350
1815
        self._cutoff_time = timestamp + self._time_offset
1351
1816
 
1352
 
    def _sha1_file(self, abspath, entry):
 
1817
    def _sha1_file_and_log(self, abspath):
1353
1818
        self._log.append(('sha1', abspath))
1354
 
        return super(InstrumentedDirState, self)._sha1_file(abspath, entry)
 
1819
        return self._sha1_provider.sha1(abspath)
1355
1820
 
1356
1821
    def _read_link(self, abspath, old_link):
1357
1822
        self._log.append(('read_link', abspath, old_link))
1388
1853
        self.st_ino = ino
1389
1854
        self.st_mode = mode
1390
1855
 
1391
 
 
1392
 
class TestUpdateEntry(TestCaseWithDirState):
1393
 
    """Test the DirState.update_entry functions"""
1394
 
 
1395
 
    def get_state_with_a(self):
1396
 
        """Create a DirState tracking a single object named 'a'"""
1397
 
        state = InstrumentedDirState.initialize('dirstate')
1398
 
        self.addCleanup(state.unlock)
1399
 
        state.add('a', 'a-id', 'file', None, '')
1400
 
        entry = state._get_entry(0, path_utf8='a')
1401
 
        return state, entry
1402
 
 
1403
 
    def test_update_entry(self):
1404
 
        state, entry = self.get_state_with_a()
1405
 
        self.build_tree(['a'])
1406
 
        # Add one where we don't provide the stat or sha already
1407
 
        self.assertEqual(('', 'a', 'a-id'), entry[0])
1408
 
        self.assertEqual([('f', '', 0, False, dirstate.DirState.NULLSTAT)],
1409
 
                         entry[1])
1410
 
        # Flush the buffers to disk
1411
 
        state.save()
1412
 
        self.assertEqual(dirstate.DirState.IN_MEMORY_UNMODIFIED,
1413
 
                         state._dirblock_state)
1414
 
 
1415
 
        stat_value = os.lstat('a')
1416
 
        packed_stat = dirstate.pack_stat(stat_value)
1417
 
        link_or_sha1 = state.update_entry(entry, abspath='a',
1418
 
                                          stat_value=stat_value)
1419
 
        self.assertEqual('b50e5406bb5e153ebbeb20268fcf37c87e1ecfb6',
1420
 
                         link_or_sha1)
1421
 
 
1422
 
        # The dirblock entry should be updated with the new info
1423
 
        self.assertEqual([('f', link_or_sha1, 14, False, packed_stat)],
1424
 
                         entry[1])
1425
 
        self.assertEqual(dirstate.DirState.IN_MEMORY_MODIFIED,
1426
 
                         state._dirblock_state)
1427
 
        mode = stat_value.st_mode
1428
 
        self.assertEqual([('sha1', 'a'), ('is_exec', mode, False)], state._log)
1429
 
 
1430
 
        state.save()
1431
 
        self.assertEqual(dirstate.DirState.IN_MEMORY_UNMODIFIED,
1432
 
                         state._dirblock_state)
1433
 
 
1434
 
        # If we do it again right away, we don't know if the file has changed
1435
 
        # so we will re-read the file. Roll the clock back so the file is
1436
 
        # guaranteed to look too new.
1437
 
        state.adjust_time(-10)
1438
 
 
1439
 
        link_or_sha1 = state.update_entry(entry, abspath='a',
1440
 
                                          stat_value=stat_value)
1441
 
        self.assertEqual([('sha1', 'a'), ('is_exec', mode, False),
1442
 
                          ('sha1', 'a'), ('is_exec', mode, False),
1443
 
                         ], state._log)
1444
 
        self.assertEqual('b50e5406bb5e153ebbeb20268fcf37c87e1ecfb6',
1445
 
                         link_or_sha1)
1446
 
        self.assertEqual(dirstate.DirState.IN_MEMORY_MODIFIED,
1447
 
                         state._dirblock_state)
1448
 
        state.save()
1449
 
 
1450
 
        # However, if we move the clock forward so the file is considered
1451
 
        # "stable", it should just returned the cached value.
1452
 
        state.adjust_time(20)
1453
 
        link_or_sha1 = state.update_entry(entry, abspath='a',
1454
 
                                          stat_value=stat_value)
1455
 
        self.assertEqual('b50e5406bb5e153ebbeb20268fcf37c87e1ecfb6',
1456
 
                         link_or_sha1)
1457
 
        self.assertEqual([('sha1', 'a'), ('is_exec', mode, False),
1458
 
                          ('sha1', 'a'), ('is_exec', mode, False),
1459
 
                         ], state._log)
1460
 
 
1461
 
    def test_update_entry_no_stat_value(self):
1462
 
        """Passing the stat_value is optional."""
1463
 
        state, entry = self.get_state_with_a()
1464
 
        state.adjust_time(-10) # Make sure the file looks new
1465
 
        self.build_tree(['a'])
1466
 
        # Add one where we don't provide the stat or sha already
1467
 
        link_or_sha1 = state.update_entry(entry, abspath='a')
1468
 
        self.assertEqual('b50e5406bb5e153ebbeb20268fcf37c87e1ecfb6',
1469
 
                         link_or_sha1)
1470
 
        stat_value = os.lstat('a')
1471
 
        self.assertEqual([('lstat', 'a'), ('sha1', 'a'),
1472
 
                          ('is_exec', stat_value.st_mode, False),
1473
 
                         ], state._log)
1474
 
 
1475
 
    def test_update_entry_symlink(self):
1476
 
        """Update entry should read symlinks."""
1477
 
        if not osutils.has_symlinks():
1478
 
            # PlatformDeficiency / TestSkipped
1479
 
            raise TestSkipped("No symlink support")
1480
 
        state, entry = self.get_state_with_a()
1481
 
        state.save()
1482
 
        self.assertEqual(dirstate.DirState.IN_MEMORY_UNMODIFIED,
1483
 
                         state._dirblock_state)
1484
 
        os.symlink('target', 'a')
1485
 
 
1486
 
        state.adjust_time(-10) # Make the symlink look new
1487
 
        stat_value = os.lstat('a')
1488
 
        packed_stat = dirstate.pack_stat(stat_value)
1489
 
        link_or_sha1 = state.update_entry(entry, abspath='a',
1490
 
                                          stat_value=stat_value)
1491
 
        self.assertEqual('target', link_or_sha1)
1492
 
        self.assertEqual([('read_link', 'a', '')], state._log)
1493
 
        # Dirblock is updated
1494
 
        self.assertEqual([('l', link_or_sha1, 6, False, packed_stat)],
1495
 
                         entry[1])
1496
 
        self.assertEqual(dirstate.DirState.IN_MEMORY_MODIFIED,
1497
 
                         state._dirblock_state)
1498
 
 
1499
 
        # Because the stat_value looks new, we should re-read the target
1500
 
        link_or_sha1 = state.update_entry(entry, abspath='a',
1501
 
                                          stat_value=stat_value)
1502
 
        self.assertEqual('target', link_or_sha1)
1503
 
        self.assertEqual([('read_link', 'a', ''),
1504
 
                          ('read_link', 'a', 'target'),
1505
 
                         ], state._log)
1506
 
        state.adjust_time(+20) # Skip into the future, all files look old
1507
 
        link_or_sha1 = state.update_entry(entry, abspath='a',
1508
 
                                          stat_value=stat_value)
1509
 
        self.assertEqual('target', link_or_sha1)
1510
 
        # There should not be a new read_link call.
1511
 
        # (this is a weak assertion, because read_link is fairly inexpensive,
1512
 
        # versus the number of symlinks that we would have)
1513
 
        self.assertEqual([('read_link', 'a', ''),
1514
 
                          ('read_link', 'a', 'target'),
1515
 
                         ], state._log)
1516
 
 
1517
 
    def test_update_entry_dir(self):
1518
 
        state, entry = self.get_state_with_a()
1519
 
        self.build_tree(['a/'])
1520
 
        self.assertIs(None, state.update_entry(entry, 'a'))
1521
 
 
1522
 
    def create_and_test_file(self, state, entry):
1523
 
        """Create a file at 'a' and verify the state finds it.
1524
 
 
1525
 
        The state should already be versioning *something* at 'a'. This makes
1526
 
        sure that state.update_entry recognizes it as a file.
1527
 
        """
1528
 
        self.build_tree(['a'])
1529
 
        stat_value = os.lstat('a')
1530
 
        packed_stat = dirstate.pack_stat(stat_value)
1531
 
 
1532
 
        link_or_sha1 = state.update_entry(entry, abspath='a')
1533
 
        self.assertEqual('b50e5406bb5e153ebbeb20268fcf37c87e1ecfb6',
1534
 
                         link_or_sha1)
1535
 
        self.assertEqual([('f', link_or_sha1, 14, False, packed_stat)],
1536
 
                         entry[1])
1537
 
        return packed_stat
1538
 
 
1539
 
    def create_and_test_dir(self, state, entry):
1540
 
        """Create a directory at 'a' and verify the state finds it.
1541
 
 
1542
 
        The state should already be versioning *something* at 'a'. This makes
1543
 
        sure that state.update_entry recognizes it as a directory.
1544
 
        """
1545
 
        self.build_tree(['a/'])
1546
 
        stat_value = os.lstat('a')
1547
 
        packed_stat = dirstate.pack_stat(stat_value)
1548
 
 
1549
 
        link_or_sha1 = state.update_entry(entry, abspath='a')
1550
 
        self.assertIs(None, link_or_sha1)
1551
 
        self.assertEqual([('d', '', 0, False, packed_stat)], entry[1])
1552
 
 
1553
 
        return packed_stat
1554
 
 
1555
 
    def create_and_test_symlink(self, state, entry):
1556
 
        """Create a symlink at 'a' and verify the state finds it.
1557
 
 
1558
 
        The state should already be versioning *something* at 'a'. This makes
1559
 
        sure that state.update_entry recognizes it as a symlink.
1560
 
 
1561
 
        This should not be called if this platform does not have symlink
1562
 
        support.
1563
 
        """
1564
 
        # caller should care about skipping test on platforms without symlinks
1565
 
        os.symlink('path/to/foo', 'a')
1566
 
 
1567
 
        stat_value = os.lstat('a')
1568
 
        packed_stat = dirstate.pack_stat(stat_value)
1569
 
 
1570
 
        link_or_sha1 = state.update_entry(entry, abspath='a')
1571
 
        self.assertEqual('path/to/foo', link_or_sha1)
1572
 
        self.assertEqual([('l', 'path/to/foo', 11, False, packed_stat)],
1573
 
                         entry[1])
1574
 
        return packed_stat
1575
 
 
1576
 
    def test_update_missing_file(self):
1577
 
        state, entry = self.get_state_with_a()
1578
 
        packed_stat = self.create_and_test_file(state, entry)
1579
 
        # Now if we delete the file, update_entry should recover and
1580
 
        # return None.
1581
 
        os.remove('a')
1582
 
        self.assertIs(None, state.update_entry(entry, abspath='a'))
1583
 
        # And the record shouldn't be changed.
1584
 
        digest = 'b50e5406bb5e153ebbeb20268fcf37c87e1ecfb6'
1585
 
        self.assertEqual([('f', digest, 14, False, packed_stat)],
1586
 
                         entry[1])
1587
 
 
1588
 
    def test_update_missing_dir(self):
1589
 
        state, entry = self.get_state_with_a()
1590
 
        packed_stat = self.create_and_test_dir(state, entry)
1591
 
        # Now if we delete the directory, update_entry should recover and
1592
 
        # return None.
1593
 
        os.rmdir('a')
1594
 
        self.assertIs(None, state.update_entry(entry, abspath='a'))
1595
 
        self.assertEqual([('d', '', 0, False, packed_stat)], entry[1])
1596
 
 
1597
 
    def test_update_missing_symlink(self):
1598
 
        if not osutils.has_symlinks():
1599
 
            # PlatformDeficiency / TestSkipped
1600
 
            raise TestSkipped("No symlink support")
1601
 
        state, entry = self.get_state_with_a()
1602
 
        packed_stat = self.create_and_test_symlink(state, entry)
1603
 
        os.remove('a')
1604
 
        self.assertIs(None, state.update_entry(entry, abspath='a'))
1605
 
        # And the record shouldn't be changed.
1606
 
        self.assertEqual([('l', 'path/to/foo', 11, False, packed_stat)],
1607
 
                         entry[1])
1608
 
 
1609
 
    def test_update_file_to_dir(self):
1610
 
        """If a file changes to a directory we return None for the sha.
1611
 
        We also update the inventory record.
1612
 
        """
1613
 
        state, entry = self.get_state_with_a()
1614
 
        self.create_and_test_file(state, entry)
1615
 
        os.remove('a')
1616
 
        self.create_and_test_dir(state, entry)
1617
 
 
1618
 
    def test_update_file_to_symlink(self):
1619
 
        """File becomes a symlink"""
1620
 
        if not osutils.has_symlinks():
1621
 
            # PlatformDeficiency / TestSkipped
1622
 
            raise TestSkipped("No symlink support")
1623
 
        state, entry = self.get_state_with_a()
1624
 
        self.create_and_test_file(state, entry)
1625
 
        os.remove('a')
1626
 
        self.create_and_test_symlink(state, entry)
1627
 
 
1628
 
    def test_update_dir_to_file(self):
1629
 
        """Directory becoming a file updates the entry."""
1630
 
        state, entry = self.get_state_with_a()
1631
 
        self.create_and_test_dir(state, entry)
1632
 
        os.rmdir('a')
1633
 
        self.create_and_test_file(state, entry)
1634
 
 
1635
 
    def test_update_dir_to_symlink(self):
1636
 
        """Directory becomes a symlink"""
1637
 
        if not osutils.has_symlinks():
1638
 
            # PlatformDeficiency / TestSkipped
1639
 
            raise TestSkipped("No symlink support")
1640
 
        state, entry = self.get_state_with_a()
1641
 
        self.create_and_test_dir(state, entry)
1642
 
        os.rmdir('a')
1643
 
        self.create_and_test_symlink(state, entry)
1644
 
 
1645
 
    def test_update_symlink_to_file(self):
1646
 
        """Symlink becomes a file"""
1647
 
        if not has_symlinks():
1648
 
            raise TestSkipped("No symlink support")
1649
 
        state, entry = self.get_state_with_a()
1650
 
        self.create_and_test_symlink(state, entry)
1651
 
        os.remove('a')
1652
 
        self.create_and_test_file(state, entry)
1653
 
 
1654
 
    def test_update_symlink_to_dir(self):
1655
 
        """Symlink becomes a directory"""
1656
 
        if not has_symlinks():
1657
 
            raise TestSkipped("No symlink support")
1658
 
        state, entry = self.get_state_with_a()
1659
 
        self.create_and_test_symlink(state, entry)
1660
 
        os.remove('a')
1661
 
        self.create_and_test_dir(state, entry)
1662
 
 
1663
 
    def test__is_executable_win32(self):
1664
 
        state, entry = self.get_state_with_a()
1665
 
        self.build_tree(['a'])
1666
 
 
1667
 
        # Make sure we are using the win32 implementation of _is_executable
1668
 
        state._is_executable = state._is_executable_win32
1669
 
 
1670
 
        # The file on disk is not executable, but we are marking it as though
1671
 
        # it is. With _is_executable_win32 we ignore what is on disk.
1672
 
        entry[1][0] = ('f', '', 0, True, dirstate.DirState.NULLSTAT)
1673
 
 
1674
 
        stat_value = os.lstat('a')
1675
 
        packed_stat = dirstate.pack_stat(stat_value)
1676
 
 
1677
 
        state.adjust_time(-10) # Make sure everything is new
1678
 
        # Make sure it wants to kkkkkkkk
1679
 
        state.update_entry(entry, abspath='a', stat_value=stat_value)
1680
 
 
1681
 
        # The row is updated, but the executable bit stays set.
1682
 
        digest = 'b50e5406bb5e153ebbeb20268fcf37c87e1ecfb6'
1683
 
        self.assertEqual([('f', digest, 14, True, packed_stat)], entry[1])
1684
 
 
1685
 
 
1686
 
class TestPackStat(TestCaseWithTransport):
 
1856
    @staticmethod
 
1857
    def from_stat(st):
 
1858
        return _FakeStat(st.st_size, st.st_mtime, st.st_ctime, st.st_dev,
 
1859
            st.st_ino, st.st_mode)
 
1860
 
 
1861
 
 
1862
class TestPackStat(tests.TestCaseWithTransport):
1687
1863
 
1688
1864
    def assertPackStat(self, expected, stat_value):
1689
1865
        """Check the packed and serialized form of a stat value."""
1738
1914
class TestBisect(TestCaseWithDirState):
1739
1915
    """Test the ability to bisect into the disk format."""
1740
1916
 
1741
 
 
1742
1917
    def assertBisect(self, expected_map, map_keys, state, paths):
1743
1918
        """Assert that bisecting for paths returns the right result.
1744
1919
 
1749
1924
                      (dir, name) tuples, and sorted according to how _bisect
1750
1925
                      requires.
1751
1926
        """
1752
 
        dir_names = sorted(osutils.split(p) for p in paths)
1753
 
        result = state._bisect(dir_names)
 
1927
        result = state._bisect(paths)
1754
1928
        # For now, results are just returned in whatever order we read them.
1755
1929
        # We could sort by (dir, name, file_id) or something like that, but in
1756
1930
        # the end it would still be fairly arbitrary, and we don't want the
1757
1931
        # extra overhead if we can avoid it. So sort everything to make sure
1758
1932
        # equality is true
1759
 
        assert len(map_keys) == len(dir_names)
 
1933
        self.assertEqual(len(map_keys), len(paths))
1760
1934
        expected = {}
1761
 
        for dir_name, keys in zip(dir_names, map_keys):
 
1935
        for path, keys in zip(paths, map_keys):
1762
1936
            if keys is None:
1763
1937
                # This should not be present in the output
1764
1938
                continue
1765
 
            expected[dir_name] = sorted(expected_map[k] for k in keys)
 
1939
            expected[path] = sorted(expected_map[k] for k in keys)
1766
1940
 
1767
 
        for dir_name in result:
1768
 
            result[dir_name].sort()
 
1941
        # The returned values are just arranged randomly based on when they
 
1942
        # were read, for testing, make sure it is properly sorted.
 
1943
        for path in result:
 
1944
            result[path].sort()
1769
1945
 
1770
1946
        self.assertEqual(expected, result)
1771
1947
 
1779
1955
        :param paths: A list of directories
1780
1956
        """
1781
1957
        result = state._bisect_dirblocks(paths)
1782
 
        assert len(map_keys) == len(paths)
1783
 
 
 
1958
        self.assertEqual(len(map_keys), len(paths))
1784
1959
        expected = {}
1785
1960
        for path, keys in zip(paths, map_keys):
1786
1961
            if keys is None:
1808
1983
            dir_name_id, trees_info = entry
1809
1984
            expected[dir_name_id] = trees_info
1810
1985
 
1811
 
        dir_names = sorted(osutils.split(p) for p in paths)
1812
 
        result = state._bisect_recursive(dir_names)
 
1986
        result = state._bisect_recursive(paths)
1813
1987
 
1814
1988
        self.assertEqual(expected, result)
1815
1989
 
1824
1998
        self.assertBisect(expected, [['b/c']], state, ['b/c'])
1825
1999
        self.assertBisect(expected, [['b/d']], state, ['b/d'])
1826
2000
        self.assertBisect(expected, [['b/d/e']], state, ['b/d/e'])
 
2001
        self.assertBisect(expected, [['b-c']], state, ['b-c'])
1827
2002
        self.assertBisect(expected, [['f']], state, ['f'])
1828
2003
 
1829
2004
    def test_bisect_multi(self):
1832
2007
        # Bisect should be capable of finding multiple entries at the same time
1833
2008
        self.assertBisect(expected, [['a'], ['b'], ['f']],
1834
2009
                          state, ['a', 'b', 'f'])
1835
 
        # ('', 'f') sorts before the others
1836
2010
        self.assertBisect(expected, [['f'], ['b/d'], ['b/d/e']],
1837
 
                          state, ['b/d', 'b/d/e', 'f'])
 
2011
                          state, ['f', 'b/d', 'b/d/e'])
 
2012
        self.assertBisect(expected, [['b'], ['b-c'], ['b/c']],
 
2013
                          state, ['b', 'b-c', 'b/c'])
1838
2014
 
1839
2015
    def test_bisect_one_page(self):
1840
2016
        """Test bisect when there is only 1 page to read"""
1846
2022
        self.assertBisect(expected,[['b/c']], state, ['b/c'])
1847
2023
        self.assertBisect(expected,[['b/d']], state, ['b/d'])
1848
2024
        self.assertBisect(expected,[['b/d/e']], state, ['b/d/e'])
 
2025
        self.assertBisect(expected,[['b-c']], state, ['b-c'])
1849
2026
        self.assertBisect(expected,[['f']], state, ['f'])
1850
2027
        self.assertBisect(expected,[['a'], ['b'], ['f']],
1851
2028
                          state, ['a', 'b', 'f'])
1852
 
        # ('', 'f') sorts before the others
1853
 
        self.assertBisect(expected, [['f'], ['b/d'], ['b/d/e']],
 
2029
        self.assertBisect(expected, [['b/d'], ['b/d/e'], ['f']],
1854
2030
                          state, ['b/d', 'b/d/e', 'f'])
 
2031
        self.assertBisect(expected, [['b'], ['b/c'], ['b-c']],
 
2032
                          state, ['b', 'b/c', 'b-c'])
1855
2033
 
1856
2034
    def test_bisect_duplicate_paths(self):
1857
2035
        """When bisecting for a path, handle multiple entries."""
1865
2043
        self.assertBisect(expected, [['b/d', 'b/d2']], state, ['b/d'])
1866
2044
        self.assertBisect(expected, [['b/d/e', 'b/d/e2']],
1867
2045
                          state, ['b/d/e'])
 
2046
        self.assertBisect(expected, [['b-c', 'b-c2']], state, ['b-c'])
1868
2047
        self.assertBisect(expected, [['f', 'f2']], state, ['f'])
1869
2048
 
1870
2049
    def test_bisect_page_size_too_small(self):
1877
2056
        self.assertBisect(expected, [['b/c']], state, ['b/c'])
1878
2057
        self.assertBisect(expected, [['b/d']], state, ['b/d'])
1879
2058
        self.assertBisect(expected, [['b/d/e']], state, ['b/d/e'])
 
2059
        self.assertBisect(expected, [['b-c']], state, ['b-c'])
1880
2060
        self.assertBisect(expected, [['f']], state, ['f'])
1881
2061
 
1882
2062
    def test_bisect_missing(self):
1885
2065
        self.assertBisect(expected, [None], state, ['foo'])
1886
2066
        self.assertBisect(expected, [None], state, ['b/foo'])
1887
2067
        self.assertBisect(expected, [None], state, ['bar/foo'])
 
2068
        self.assertBisect(expected, [None], state, ['b-c/foo'])
1888
2069
 
1889
2070
        self.assertBisect(expected, [['a'], None, ['b/d']],
1890
2071
                          state, ['a', 'foo', 'b/d'])
1906
2087
    def test_bisect_dirblocks(self):
1907
2088
        tree, state, expected = self.create_duplicated_dirstate()
1908
2089
        self.assertBisectDirBlocks(expected,
1909
 
            [['', 'a', 'a2', 'b', 'b2', 'f', 'f2']], state, [''])
 
2090
            [['', 'a', 'a2', 'b', 'b2', 'b-c', 'b-c2', 'f', 'f2']],
 
2091
            state, [''])
1910
2092
        self.assertBisectDirBlocks(expected,
1911
2093
            [['b/c', 'b/c2', 'b/d', 'b/d2']], state, ['b'])
1912
2094
        self.assertBisectDirBlocks(expected,
1913
2095
            [['b/d/e', 'b/d/e2']], state, ['b/d'])
1914
2096
        self.assertBisectDirBlocks(expected,
1915
 
            [['', 'a', 'a2', 'b', 'b2', 'f', 'f2'],
 
2097
            [['', 'a', 'a2', 'b', 'b2', 'b-c', 'b-c2', 'f', 'f2'],
1916
2098
             ['b/c', 'b/c2', 'b/d', 'b/d2'],
1917
2099
             ['b/d/e', 'b/d/e2'],
1918
2100
            ], state, ['', 'b', 'b/d'])
1933
2115
        self.assertBisectRecursive(expected, ['a'], state, ['a'])
1934
2116
        self.assertBisectRecursive(expected, ['b/c'], state, ['b/c'])
1935
2117
        self.assertBisectRecursive(expected, ['b/d/e'], state, ['b/d/e'])
 
2118
        self.assertBisectRecursive(expected, ['b-c'], state, ['b-c'])
1936
2119
        self.assertBisectRecursive(expected, ['b/d', 'b/d/e'],
1937
2120
                                   state, ['b/d'])
1938
2121
        self.assertBisectRecursive(expected, ['b', 'b/c', 'b/d', 'b/d/e'],
1939
2122
                                   state, ['b'])
1940
 
        self.assertBisectRecursive(expected, ['', 'a', 'b', 'f', 'b/c',
 
2123
        self.assertBisectRecursive(expected, ['', 'a', 'b', 'b-c', 'f', 'b/c',
1941
2124
                                              'b/d', 'b/d/e'],
1942
2125
                                   state, [''])
1943
2126
 
1967
2150
                                   state, ['b'])
1968
2151
 
1969
2152
 
1970
 
class TestBisectDirblock(TestCase):
1971
 
    """Test that bisect_dirblock() returns the expected values.
1972
 
 
1973
 
    bisect_dirblock is intended to work like bisect.bisect_left() except it
1974
 
    knows it is working on dirblocks and that dirblocks are sorted by ('path',
1975
 
    'to', 'foo') chunks rather than by raw 'path/to/foo'.
1976
 
    """
1977
 
 
1978
 
    def assertBisect(self, dirblocks, split_dirblocks, path, *args, **kwargs):
1979
 
        """Assert that bisect_split works like bisect_left on the split paths.
1980
 
 
1981
 
        :param dirblocks: A list of (path, [info]) pairs.
1982
 
        :param split_dirblocks: A list of ((split, path), [info]) pairs.
1983
 
        :param path: The path we are indexing.
1984
 
 
1985
 
        All other arguments will be passed along.
1986
 
        """
1987
 
        bisect_split_idx = dirstate.bisect_dirblock(dirblocks, path,
1988
 
                                                 *args, **kwargs)
1989
 
        split_dirblock = (path.split('/'), [])
1990
 
        bisect_left_idx = bisect.bisect_left(split_dirblocks, split_dirblock,
1991
 
                                             *args)
1992
 
        self.assertEqual(bisect_left_idx, bisect_split_idx,
1993
 
                         'bisect_split disagreed. %s != %s'
1994
 
                         ' for key %s'
1995
 
                         % (bisect_left_idx, bisect_split_idx, path)
1996
 
                         )
1997
 
 
1998
 
    def paths_to_dirblocks(self, paths):
1999
 
        """Convert a list of paths into dirblock form.
2000
 
 
2001
 
        Also, ensure that the paths are in proper sorted order.
2002
 
        """
2003
 
        dirblocks = [(path, []) for path in paths]
2004
 
        split_dirblocks = [(path.split('/'), []) for path in paths]
2005
 
        self.assertEqual(sorted(split_dirblocks), split_dirblocks)
2006
 
        return dirblocks, split_dirblocks
2007
 
 
2008
 
    def test_simple(self):
2009
 
        """In the simple case it works just like bisect_left"""
2010
 
        paths = ['', 'a', 'b', 'c', 'd']
2011
 
        dirblocks, split_dirblocks = self.paths_to_dirblocks(paths)
2012
 
        for path in paths:
2013
 
            self.assertBisect(dirblocks, split_dirblocks, path)
2014
 
        self.assertBisect(dirblocks, split_dirblocks, '_')
2015
 
        self.assertBisect(dirblocks, split_dirblocks, 'aa')
2016
 
        self.assertBisect(dirblocks, split_dirblocks, 'bb')
2017
 
        self.assertBisect(dirblocks, split_dirblocks, 'cc')
2018
 
        self.assertBisect(dirblocks, split_dirblocks, 'dd')
2019
 
        self.assertBisect(dirblocks, split_dirblocks, 'a/a')
2020
 
        self.assertBisect(dirblocks, split_dirblocks, 'b/b')
2021
 
        self.assertBisect(dirblocks, split_dirblocks, 'c/c')
2022
 
        self.assertBisect(dirblocks, split_dirblocks, 'd/d')
2023
 
 
2024
 
    def test_involved(self):
2025
 
        """This is where bisect_left diverges slightly."""
2026
 
        paths = ['', 'a',
2027
 
                 'a/a', 'a/a/a', 'a/a/z', 'a/a-a', 'a/a-z',
2028
 
                 'a/z', 'a/z/a', 'a/z/z', 'a/z-a', 'a/z-z',
2029
 
                 'a-a', 'a-z',
2030
 
                 'z', 'z/a/a', 'z/a/z', 'z/a-a', 'z/a-z',
2031
 
                 'z/z', 'z/z/a', 'z/z/z', 'z/z-a', 'z/z-z',
2032
 
                 'z-a', 'z-z',
2033
 
                ]
2034
 
        dirblocks, split_dirblocks = self.paths_to_dirblocks(paths)
2035
 
        for path in paths:
2036
 
            self.assertBisect(dirblocks, split_dirblocks, path)
2037
 
 
2038
 
    def test_involved_cached(self):
2039
 
        """This is where bisect_left diverges slightly."""
2040
 
        paths = ['', 'a',
2041
 
                 'a/a', 'a/a/a', 'a/a/z', 'a/a-a', 'a/a-z',
2042
 
                 'a/z', 'a/z/a', 'a/z/z', 'a/z-a', 'a/z-z',
2043
 
                 'a-a', 'a-z',
2044
 
                 'z', 'z/a/a', 'z/a/z', 'z/a-a', 'z/a-z',
2045
 
                 'z/z', 'z/z/a', 'z/z/z', 'z/z-a', 'z/z-z',
2046
 
                 'z-a', 'z-z',
2047
 
                ]
2048
 
        cache = {}
2049
 
        dirblocks, split_dirblocks = self.paths_to_dirblocks(paths)
2050
 
        for path in paths:
2051
 
            self.assertBisect(dirblocks, split_dirblocks, path, cache=cache)
2052
 
 
2053
 
 
2054
2153
class TestDirstateValidation(TestCaseWithDirState):
2055
2154
 
2056
2155
    def test_validate_correct_dirstate(self):
2106
2205
            state._validate)
2107
2206
        self.assertContainsRe(str(e),
2108
2207
            'file a-id is absent in row')
 
2208
 
 
2209
 
 
2210
class TestDirstateTreeReference(TestCaseWithDirState):
 
2211
 
 
2212
    def test_reference_revision_is_none(self):
 
2213
        tree = self.make_branch_and_tree('tree', format='dirstate-with-subtree')
 
2214
        subtree = self.make_branch_and_tree('tree/subtree',
 
2215
                            format='dirstate-with-subtree')
 
2216
        subtree.set_root_id('subtree')
 
2217
        tree.add_reference(subtree)
 
2218
        tree.add('subtree')
 
2219
        state = dirstate.DirState.from_tree(tree, 'dirstate')
 
2220
        key = ('', 'subtree', 'subtree')
 
2221
        expected = ('', [(key,
 
2222
            [('t', '', 0, False, 'xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx')])])
 
2223
 
 
2224
        try:
 
2225
            self.assertEqual(expected, state._find_block(key))
 
2226
        finally:
 
2227
            state.unlock()
 
2228
 
 
2229
 
 
2230
class TestDiscardMergeParents(TestCaseWithDirState):
 
2231
 
 
2232
    def test_discard_no_parents(self):
 
2233
        # This should be a no-op
 
2234
        state = self.create_empty_dirstate()
 
2235
        self.addCleanup(state.unlock)
 
2236
        state._discard_merge_parents()
 
2237
        state._validate()
 
2238
 
 
2239
    def test_discard_one_parent(self):
 
2240
        # No-op
 
2241
        packed_stat = 'AAAAREUHaIpFB2iKAAADAQAtkqUAAIGk'
 
2242
        root_entry_direntry = ('', '', 'a-root-value'), [
 
2243
            ('d', '', 0, False, packed_stat),
 
2244
            ('d', '', 0, False, packed_stat),
 
2245
            ]
 
2246
        dirblocks = []
 
2247
        dirblocks.append(('', [root_entry_direntry]))
 
2248
        dirblocks.append(('', []))
 
2249
 
 
2250
        state = self.create_empty_dirstate()
 
2251
        self.addCleanup(state.unlock)
 
2252
        state._set_data(['parent-id'], dirblocks[:])
 
2253
        state._validate()
 
2254
 
 
2255
        state._discard_merge_parents()
 
2256
        state._validate()
 
2257
        self.assertEqual(dirblocks, state._dirblocks)
 
2258
 
 
2259
    def test_discard_simple(self):
 
2260
        # No-op
 
2261
        packed_stat = 'AAAAREUHaIpFB2iKAAADAQAtkqUAAIGk'
 
2262
        root_entry_direntry = ('', '', 'a-root-value'), [
 
2263
            ('d', '', 0, False, packed_stat),
 
2264
            ('d', '', 0, False, packed_stat),
 
2265
            ('d', '', 0, False, packed_stat),
 
2266
            ]
 
2267
        expected_root_entry_direntry = ('', '', 'a-root-value'), [
 
2268
            ('d', '', 0, False, packed_stat),
 
2269
            ('d', '', 0, False, packed_stat),
 
2270
            ]
 
2271
        dirblocks = []
 
2272
        dirblocks.append(('', [root_entry_direntry]))
 
2273
        dirblocks.append(('', []))
 
2274
 
 
2275
        state = self.create_empty_dirstate()
 
2276
        self.addCleanup(state.unlock)
 
2277
        state._set_data(['parent-id', 'merged-id'], dirblocks[:])
 
2278
        state._validate()
 
2279
 
 
2280
        # This should strip of the extra column
 
2281
        state._discard_merge_parents()
 
2282
        state._validate()
 
2283
        expected_dirblocks = [('', [expected_root_entry_direntry]), ('', [])]
 
2284
        self.assertEqual(expected_dirblocks, state._dirblocks)
 
2285
 
 
2286
    def test_discard_absent(self):
 
2287
        """If entries are only in a merge, discard should remove the entries"""
 
2288
        null_stat = dirstate.DirState.NULLSTAT
 
2289
        present_dir = ('d', '', 0, False, null_stat)
 
2290
        present_file = ('f', '', 0, False, null_stat)
 
2291
        absent = dirstate.DirState.NULL_PARENT_DETAILS
 
2292
        root_key = ('', '', 'a-root-value')
 
2293
        file_in_root_key = ('', 'file-in-root', 'a-file-id')
 
2294
        file_in_merged_key = ('', 'file-in-merged', 'b-file-id')
 
2295
        dirblocks = [('', [(root_key, [present_dir, present_dir, present_dir])]),
 
2296
                     ('', [(file_in_merged_key,
 
2297
                            [absent, absent, present_file]),
 
2298
                           (file_in_root_key,
 
2299
                            [present_file, present_file, present_file]),
 
2300
                          ]),
 
2301
                    ]
 
2302
 
 
2303
        state = self.create_empty_dirstate()
 
2304
        self.addCleanup(state.unlock)
 
2305
        state._set_data(['parent-id', 'merged-id'], dirblocks[:])
 
2306
        state._validate()
 
2307
 
 
2308
        exp_dirblocks = [('', [(root_key, [present_dir, present_dir])]),
 
2309
                         ('', [(file_in_root_key,
 
2310
                                [present_file, present_file]),
 
2311
                              ]),
 
2312
                        ]
 
2313
        state._discard_merge_parents()
 
2314
        state._validate()
 
2315
        self.assertEqual(exp_dirblocks, state._dirblocks)
 
2316
 
 
2317
    def test_discard_renamed(self):
 
2318
        null_stat = dirstate.DirState.NULLSTAT
 
2319
        present_dir = ('d', '', 0, False, null_stat)
 
2320
        present_file = ('f', '', 0, False, null_stat)
 
2321
        absent = dirstate.DirState.NULL_PARENT_DETAILS
 
2322
        root_key = ('', '', 'a-root-value')
 
2323
        file_in_root_key = ('', 'file-in-root', 'a-file-id')
 
2324
        # Renamed relative to parent
 
2325
        file_rename_s_key = ('', 'file-s', 'b-file-id')
 
2326
        file_rename_t_key = ('', 'file-t', 'b-file-id')
 
2327
        # And one that is renamed between the parents, but absent in this
 
2328
        key_in_1 = ('', 'file-in-1', 'c-file-id')
 
2329
        key_in_2 = ('', 'file-in-2', 'c-file-id')
 
2330
 
 
2331
        dirblocks = [
 
2332
            ('', [(root_key, [present_dir, present_dir, present_dir])]),
 
2333
            ('', [(key_in_1,
 
2334
                   [absent, present_file, ('r', 'file-in-2', 'c-file-id')]),
 
2335
                  (key_in_2,
 
2336
                   [absent, ('r', 'file-in-1', 'c-file-id'), present_file]),
 
2337
                  (file_in_root_key,
 
2338
                   [present_file, present_file, present_file]),
 
2339
                  (file_rename_s_key,
 
2340
                   [('r', 'file-t', 'b-file-id'), absent, present_file]),
 
2341
                  (file_rename_t_key,
 
2342
                   [present_file, absent, ('r', 'file-s', 'b-file-id')]),
 
2343
                 ]),
 
2344
        ]
 
2345
        exp_dirblocks = [
 
2346
            ('', [(root_key, [present_dir, present_dir])]),
 
2347
            ('', [(key_in_1, [absent, present_file]),
 
2348
                  (file_in_root_key, [present_file, present_file]),
 
2349
                  (file_rename_t_key, [present_file, absent]),
 
2350
                 ]),
 
2351
        ]
 
2352
        state = self.create_empty_dirstate()
 
2353
        self.addCleanup(state.unlock)
 
2354
        state._set_data(['parent-id', 'merged-id'], dirblocks[:])
 
2355
        state._validate()
 
2356
 
 
2357
        state._discard_merge_parents()
 
2358
        state._validate()
 
2359
        self.assertEqual(exp_dirblocks, state._dirblocks)
 
2360
 
 
2361
    def test_discard_all_subdir(self):
 
2362
        null_stat = dirstate.DirState.NULLSTAT
 
2363
        present_dir = ('d', '', 0, False, null_stat)
 
2364
        present_file = ('f', '', 0, False, null_stat)
 
2365
        absent = dirstate.DirState.NULL_PARENT_DETAILS
 
2366
        root_key = ('', '', 'a-root-value')
 
2367
        subdir_key = ('', 'sub', 'dir-id')
 
2368
        child1_key = ('sub', 'child1', 'child1-id')
 
2369
        child2_key = ('sub', 'child2', 'child2-id')
 
2370
        child3_key = ('sub', 'child3', 'child3-id')
 
2371
 
 
2372
        dirblocks = [
 
2373
            ('', [(root_key, [present_dir, present_dir, present_dir])]),
 
2374
            ('', [(subdir_key, [present_dir, present_dir, present_dir])]),
 
2375
            ('sub', [(child1_key, [absent, absent, present_file]),
 
2376
                     (child2_key, [absent, absent, present_file]),
 
2377
                     (child3_key, [absent, absent, present_file]),
 
2378
                    ]),
 
2379
        ]
 
2380
        exp_dirblocks = [
 
2381
            ('', [(root_key, [present_dir, present_dir])]),
 
2382
            ('', [(subdir_key, [present_dir, present_dir])]),
 
2383
            ('sub', []),
 
2384
        ]
 
2385
        state = self.create_empty_dirstate()
 
2386
        self.addCleanup(state.unlock)
 
2387
        state._set_data(['parent-id', 'merged-id'], dirblocks[:])
 
2388
        state._validate()
 
2389
 
 
2390
        state._discard_merge_parents()
 
2391
        state._validate()
 
2392
        self.assertEqual(exp_dirblocks, state._dirblocks)
 
2393
 
 
2394
 
 
2395
class Test_InvEntryToDetails(tests.TestCase):
 
2396
 
 
2397
    def assertDetails(self, expected, inv_entry):
 
2398
        details = dirstate.DirState._inv_entry_to_details(inv_entry)
 
2399
        self.assertEqual(expected, details)
 
2400
        # details should always allow join() and always be a plain str when
 
2401
        # finished
 
2402
        (minikind, fingerprint, size, executable, tree_data) = details
 
2403
        self.assertIsInstance(minikind, str)
 
2404
        self.assertIsInstance(fingerprint, str)
 
2405
        self.assertIsInstance(tree_data, str)
 
2406
 
 
2407
    def test_unicode_symlink(self):
 
2408
        inv_entry = inventory.InventoryLink('link-file-id',
 
2409
                                            u'nam\N{Euro Sign}e',
 
2410
                                            'link-parent-id')
 
2411
        inv_entry.revision = 'link-revision-id'
 
2412
        target = u'link-targ\N{Euro Sign}t'
 
2413
        inv_entry.symlink_target = target
 
2414
        self.assertDetails(('l', target.encode('UTF-8'), 0, False,
 
2415
                            'link-revision-id'), inv_entry)
 
2416
 
 
2417
 
 
2418
class TestSHA1Provider(tests.TestCaseInTempDir):
 
2419
 
 
2420
    def test_sha1provider_is_an_interface(self):
 
2421
        p = dirstate.SHA1Provider()
 
2422
        self.assertRaises(NotImplementedError, p.sha1, "foo")
 
2423
        self.assertRaises(NotImplementedError, p.stat_and_sha1, "foo")
 
2424
 
 
2425
    def test_defaultsha1provider_sha1(self):
 
2426
        text = 'test\r\nwith\nall\rpossible line endings\r\n'
 
2427
        self.build_tree_contents([('foo', text)])
 
2428
        expected_sha = osutils.sha_string(text)
 
2429
        p = dirstate.DefaultSHA1Provider()
 
2430
        self.assertEqual(expected_sha, p.sha1('foo'))
 
2431
 
 
2432
    def test_defaultsha1provider_stat_and_sha1(self):
 
2433
        text = 'test\r\nwith\nall\rpossible line endings\r\n'
 
2434
        self.build_tree_contents([('foo', text)])
 
2435
        expected_sha = osutils.sha_string(text)
 
2436
        p = dirstate.DefaultSHA1Provider()
 
2437
        statvalue, sha1 = p.stat_and_sha1('foo')
 
2438
        self.assertTrue(len(statvalue) >= 10)
 
2439
        self.assertEqual(len(text), statvalue.st_size)
 
2440
        self.assertEqual(expected_sha, sha1)
 
2441
 
 
2442
 
 
2443
class _Repo(object):
 
2444
    """A minimal api to get InventoryRevisionTree to work."""
 
2445
 
 
2446
    def __init__(self):
 
2447
        default_format = bzrdir.format_registry.make_bzrdir('default')
 
2448
        self._format = default_format.repository_format
 
2449
 
 
2450
    def lock_read(self):
 
2451
        pass
 
2452
 
 
2453
    def unlock(self):
 
2454
        pass
 
2455
 
 
2456
 
 
2457
class TestUpdateBasisByDelta(tests.TestCase):
 
2458
 
 
2459
    def path_to_ie(self, path, file_id, rev_id, dir_ids):
 
2460
        if path.endswith('/'):
 
2461
            is_dir = True
 
2462
            path = path[:-1]
 
2463
        else:
 
2464
            is_dir = False
 
2465
        dirname, basename = osutils.split(path)
 
2466
        try:
 
2467
            dir_id = dir_ids[dirname]
 
2468
        except KeyError:
 
2469
            dir_id = osutils.basename(dirname) + '-id'
 
2470
        if is_dir:
 
2471
            ie = inventory.InventoryDirectory(file_id, basename, dir_id)
 
2472
            dir_ids[path] = file_id
 
2473
        else:
 
2474
            ie = inventory.InventoryFile(file_id, basename, dir_id)
 
2475
            ie.text_size = 0
 
2476
            ie.text_sha1 = ''
 
2477
        ie.revision = rev_id
 
2478
        return ie
 
2479
 
 
2480
    def create_tree_from_shape(self, rev_id, shape):
 
2481
        dir_ids = {'': 'root-id'}
 
2482
        inv = inventory.Inventory('root-id', rev_id)
 
2483
        for path, file_id in shape:
 
2484
            if path == '':
 
2485
                # Replace the root entry
 
2486
                del inv._byid[inv.root.file_id]
 
2487
                inv.root.file_id = file_id
 
2488
                inv._byid[file_id] = inv.root
 
2489
                dir_ids[''] = file_id
 
2490
                continue
 
2491
            inv.add(self.path_to_ie(path, file_id, rev_id, dir_ids))
 
2492
        return revisiontree.InventoryRevisionTree(_Repo(), inv, rev_id)
 
2493
 
 
2494
    def create_empty_dirstate(self):
 
2495
        fd, path = tempfile.mkstemp(prefix='bzr-dirstate')
 
2496
        self.addCleanup(os.remove, path)
 
2497
        os.close(fd)
 
2498
        state = dirstate.DirState.initialize(path)
 
2499
        self.addCleanup(state.unlock)
 
2500
        return state
 
2501
 
 
2502
    def create_inv_delta(self, delta, rev_id):
 
2503
        """Translate a 'delta shape' into an actual InventoryDelta"""
 
2504
        dir_ids = {'': 'root-id'}
 
2505
        inv_delta = []
 
2506
        for old_path, new_path, file_id in delta:
 
2507
            if old_path is not None and old_path.endswith('/'):
 
2508
                # Don't have to actually do anything for this, because only
 
2509
                # new_path creates InventoryEntries
 
2510
                old_path = old_path[:-1]
 
2511
            if new_path is None: # Delete
 
2512
                inv_delta.append((old_path, None, file_id, None))
 
2513
                continue
 
2514
            ie = self.path_to_ie(new_path, file_id, rev_id, dir_ids)
 
2515
            inv_delta.append((old_path, new_path, file_id, ie))
 
2516
        return inv_delta
 
2517
 
 
2518
    def assertUpdate(self, active, basis, target):
 
2519
        """Assert that update_basis_by_delta works how we want.
 
2520
 
 
2521
        Set up a DirState object with active_shape for tree 0, basis_shape for
 
2522
        tree 1. Then apply the delta from basis_shape to target_shape,
 
2523
        and assert that the DirState is still valid, and that its stored
 
2524
        content matches the target_shape.
 
2525
        """
 
2526
        active_tree = self.create_tree_from_shape('active', active)
 
2527
        basis_tree = self.create_tree_from_shape('basis', basis)
 
2528
        target_tree = self.create_tree_from_shape('target', target)
 
2529
        state = self.create_empty_dirstate()
 
2530
        state.set_state_from_scratch(active_tree.inventory,
 
2531
            [('basis', basis_tree)], [])
 
2532
        delta = target_tree.inventory._make_delta(basis_tree.inventory)
 
2533
        state.update_basis_by_delta(delta, 'target')
 
2534
        state._validate()
 
2535
        dirstate_tree = workingtree_4.DirStateRevisionTree(state,
 
2536
            'target', _Repo())
 
2537
        # The target now that delta has been applied should match the
 
2538
        # RevisionTree
 
2539
        self.assertEqual([], list(dirstate_tree.iter_changes(target_tree)))
 
2540
        # And the dirblock state should be identical to the state if we created
 
2541
        # it from scratch.
 
2542
        state2 = self.create_empty_dirstate()
 
2543
        state2.set_state_from_scratch(active_tree.inventory,
 
2544
            [('target', target_tree)], [])
 
2545
        self.assertEqual(state2._dirblocks, state._dirblocks)
 
2546
        return state
 
2547
 
 
2548
    def assertBadDelta(self, active, basis, delta):
 
2549
        """Test that we raise InconsistentDelta when appropriate.
 
2550
 
 
2551
        :param active: The active tree shape
 
2552
        :param basis: The basis tree shape
 
2553
        :param delta: A description of the delta to apply. Similar to the form
 
2554
            for regular inventory deltas, but omitting the InventoryEntry.
 
2555
            So adding a file is: (None, 'path', 'file-id')
 
2556
            Adding a directory is: (None, 'path/', 'dir-id')
 
2557
            Renaming a dir is: ('old/', 'new/', 'dir-id')
 
2558
            etc.
 
2559
        """
 
2560
        active_tree = self.create_tree_from_shape('active', active)
 
2561
        basis_tree = self.create_tree_from_shape('basis', basis)
 
2562
        inv_delta = self.create_inv_delta(delta, 'target')
 
2563
        state = self.create_empty_dirstate()
 
2564
        state.set_state_from_scratch(active_tree.inventory,
 
2565
            [('basis', basis_tree)], [])
 
2566
        self.assertRaises(errors.InconsistentDelta,
 
2567
            state.update_basis_by_delta, inv_delta, 'target')
 
2568
        ## try:
 
2569
        ##     state.update_basis_by_delta(inv_delta, 'target')
 
2570
        ## except errors.InconsistentDelta, e:
 
2571
        ##     import pdb; pdb.set_trace()
 
2572
        ## else:
 
2573
        ##     import pdb; pdb.set_trace()
 
2574
        self.assertTrue(state._changes_aborted)
 
2575
 
 
2576
    def test_remove_file_matching_active_state(self):
 
2577
        state = self.assertUpdate(
 
2578
            active=[],
 
2579
            basis =[('file', 'file-id')],
 
2580
            target=[],
 
2581
            )
 
2582
 
 
2583
    def test_remove_file_present_in_active_state(self):
 
2584
        state = self.assertUpdate(
 
2585
            active=[('file', 'file-id')],
 
2586
            basis =[('file', 'file-id')],
 
2587
            target=[],
 
2588
            )
 
2589
 
 
2590
    def test_remove_file_present_elsewhere_in_active_state(self):
 
2591
        state = self.assertUpdate(
 
2592
            active=[('other-file', 'file-id')],
 
2593
            basis =[('file', 'file-id')],
 
2594
            target=[],
 
2595
            )
 
2596
 
 
2597
    def test_remove_file_active_state_has_diff_file(self):
 
2598
        state = self.assertUpdate(
 
2599
            active=[('file', 'file-id-2')],
 
2600
            basis =[('file', 'file-id')],
 
2601
            target=[],
 
2602
            )
 
2603
 
 
2604
    def test_remove_file_active_state_has_diff_file_and_file_elsewhere(self):
 
2605
        state = self.assertUpdate(
 
2606
            active=[('file', 'file-id-2'),
 
2607
                    ('other-file', 'file-id')],
 
2608
            basis =[('file', 'file-id')],
 
2609
            target=[],
 
2610
            )
 
2611
 
 
2612
    def test_add_file_matching_active_state(self):
 
2613
        state = self.assertUpdate(
 
2614
            active=[('file', 'file-id')],
 
2615
            basis =[],
 
2616
            target=[('file', 'file-id')],
 
2617
            )
 
2618
 
 
2619
    def test_add_file_missing_in_active_state(self):
 
2620
        state = self.assertUpdate(
 
2621
            active=[],
 
2622
            basis =[],
 
2623
            target=[('file', 'file-id')],
 
2624
            )
 
2625
 
 
2626
    def test_add_file_elsewhere_in_active_state(self):
 
2627
        state = self.assertUpdate(
 
2628
            active=[('other-file', 'file-id')],
 
2629
            basis =[],
 
2630
            target=[('file', 'file-id')],
 
2631
            )
 
2632
 
 
2633
    def test_add_file_active_state_has_diff_file_and_file_elsewhere(self):
 
2634
        state = self.assertUpdate(
 
2635
            active=[('other-file', 'file-id'),
 
2636
                    ('file', 'file-id-2')],
 
2637
            basis =[],
 
2638
            target=[('file', 'file-id')],
 
2639
            )
 
2640
 
 
2641
    def test_rename_file_matching_active_state(self):
 
2642
        state = self.assertUpdate(
 
2643
            active=[('other-file', 'file-id')],
 
2644
            basis =[('file', 'file-id')],
 
2645
            target=[('other-file', 'file-id')],
 
2646
            )
 
2647
 
 
2648
    def test_rename_file_missing_in_active_state(self):
 
2649
        state = self.assertUpdate(
 
2650
            active=[],
 
2651
            basis =[('file', 'file-id')],
 
2652
            target=[('other-file', 'file-id')],
 
2653
            )
 
2654
 
 
2655
    def test_rename_file_present_elsewhere_in_active_state(self):
 
2656
        state = self.assertUpdate(
 
2657
            active=[('third', 'file-id')],
 
2658
            basis =[('file', 'file-id')],
 
2659
            target=[('other-file', 'file-id')],
 
2660
            )
 
2661
 
 
2662
    def test_rename_file_active_state_has_diff_source_file(self):
 
2663
        state = self.assertUpdate(
 
2664
            active=[('file', 'file-id-2')],
 
2665
            basis =[('file', 'file-id')],
 
2666
            target=[('other-file', 'file-id')],
 
2667
            )
 
2668
 
 
2669
    def test_rename_file_active_state_has_diff_target_file(self):
 
2670
        state = self.assertUpdate(
 
2671
            active=[('other-file', 'file-id-2')],
 
2672
            basis =[('file', 'file-id')],
 
2673
            target=[('other-file', 'file-id')],
 
2674
            )
 
2675
 
 
2676
    def test_rename_file_active_has_swapped_files(self):
 
2677
        state = self.assertUpdate(
 
2678
            active=[('file', 'file-id'),
 
2679
                    ('other-file', 'file-id-2')],
 
2680
            basis= [('file', 'file-id'),
 
2681
                    ('other-file', 'file-id-2')],
 
2682
            target=[('file', 'file-id-2'),
 
2683
                    ('other-file', 'file-id')])
 
2684
 
 
2685
    def test_rename_file_basis_has_swapped_files(self):
 
2686
        state = self.assertUpdate(
 
2687
            active=[('file', 'file-id'),
 
2688
                    ('other-file', 'file-id-2')],
 
2689
            basis= [('file', 'file-id-2'),
 
2690
                    ('other-file', 'file-id')],
 
2691
            target=[('file', 'file-id'),
 
2692
                    ('other-file', 'file-id-2')])
 
2693
 
 
2694
    def test_rename_directory_with_contents(self):
 
2695
        state = self.assertUpdate( # active matches basis
 
2696
            active=[('dir1/', 'dir-id'),
 
2697
                    ('dir1/file', 'file-id')],
 
2698
            basis= [('dir1/', 'dir-id'),
 
2699
                    ('dir1/file', 'file-id')],
 
2700
            target=[('dir2/', 'dir-id'),
 
2701
                    ('dir2/file', 'file-id')])
 
2702
        state = self.assertUpdate( # active matches target
 
2703
            active=[('dir2/', 'dir-id'),
 
2704
                    ('dir2/file', 'file-id')],
 
2705
            basis= [('dir1/', 'dir-id'),
 
2706
                    ('dir1/file', 'file-id')],
 
2707
            target=[('dir2/', 'dir-id'),
 
2708
                    ('dir2/file', 'file-id')])
 
2709
        state = self.assertUpdate( # active empty
 
2710
            active=[],
 
2711
            basis= [('dir1/', 'dir-id'),
 
2712
                    ('dir1/file', 'file-id')],
 
2713
            target=[('dir2/', 'dir-id'),
 
2714
                    ('dir2/file', 'file-id')])
 
2715
        state = self.assertUpdate( # active present at other location
 
2716
            active=[('dir3/', 'dir-id'),
 
2717
                    ('dir3/file', 'file-id')],
 
2718
            basis= [('dir1/', 'dir-id'),
 
2719
                    ('dir1/file', 'file-id')],
 
2720
            target=[('dir2/', 'dir-id'),
 
2721
                    ('dir2/file', 'file-id')])
 
2722
        state = self.assertUpdate( # active has different ids
 
2723
            active=[('dir1/', 'dir1-id'),
 
2724
                    ('dir1/file', 'file1-id'),
 
2725
                    ('dir2/', 'dir2-id'),
 
2726
                    ('dir2/file', 'file2-id')],
 
2727
            basis= [('dir1/', 'dir-id'),
 
2728
                    ('dir1/file', 'file-id')],
 
2729
            target=[('dir2/', 'dir-id'),
 
2730
                    ('dir2/file', 'file-id')])
 
2731
 
 
2732
    def test_invalid_file_not_present(self):
 
2733
        state = self.assertBadDelta(
 
2734
            active=[('file', 'file-id')],
 
2735
            basis= [('file', 'file-id')],
 
2736
            delta=[('other-file', 'file', 'file-id')])
 
2737
 
 
2738
    def test_invalid_new_id_same_path(self):
 
2739
        # The bad entry comes after
 
2740
        state = self.assertBadDelta(
 
2741
            active=[('file', 'file-id')],
 
2742
            basis= [('file', 'file-id')],
 
2743
            delta=[(None, 'file', 'file-id-2')])
 
2744
        # The bad entry comes first
 
2745
        state = self.assertBadDelta(
 
2746
            active=[('file', 'file-id-2')],
 
2747
            basis=[('file', 'file-id-2')],
 
2748
            delta=[(None, 'file', 'file-id')])
 
2749
 
 
2750
    def test_invalid_existing_id(self):
 
2751
        state = self.assertBadDelta(
 
2752
            active=[('file', 'file-id')],
 
2753
            basis= [('file', 'file-id')],
 
2754
            delta=[(None, 'file', 'file-id')])
 
2755
 
 
2756
    def test_invalid_parent_missing(self):
 
2757
        state = self.assertBadDelta(
 
2758
            active=[],
 
2759
            basis= [],
 
2760
            delta=[(None, 'path/path2', 'file-id')])
 
2761
        # Note: we force the active tree to have the directory, by knowing how
 
2762
        #       path_to_ie handles entries with missing parents
 
2763
        state = self.assertBadDelta(
 
2764
            active=[('path/', 'path-id')],
 
2765
            basis= [],
 
2766
            delta=[(None, 'path/path2', 'file-id')])
 
2767
        state = self.assertBadDelta(
 
2768
            active=[('path/', 'path-id'),
 
2769
                    ('path/path2', 'file-id')],
 
2770
            basis= [],
 
2771
            delta=[(None, 'path/path2', 'file-id')])
 
2772
 
 
2773
    def test_renamed_dir_same_path(self):
 
2774
        # We replace the parent directory, with another parent dir. But the C
 
2775
        # file doesn't look like it has been moved.
 
2776
        state = self.assertUpdate(# Same as basis
 
2777
            active=[('dir/', 'A-id'),
 
2778
                    ('dir/B', 'B-id')],
 
2779
            basis= [('dir/', 'A-id'),
 
2780
                    ('dir/B', 'B-id')],
 
2781
            target=[('dir/', 'C-id'),
 
2782
                    ('dir/B', 'B-id')])
 
2783
        state = self.assertUpdate(# Same as target
 
2784
            active=[('dir/', 'C-id'),
 
2785
                    ('dir/B', 'B-id')],
 
2786
            basis= [('dir/', 'A-id'),
 
2787
                    ('dir/B', 'B-id')],
 
2788
            target=[('dir/', 'C-id'),
 
2789
                    ('dir/B', 'B-id')])
 
2790
        state = self.assertUpdate(# empty active
 
2791
            active=[],
 
2792
            basis= [('dir/', 'A-id'),
 
2793
                    ('dir/B', 'B-id')],
 
2794
            target=[('dir/', 'C-id'),
 
2795
                    ('dir/B', 'B-id')])
 
2796
        state = self.assertUpdate(# different active
 
2797
            active=[('dir/', 'D-id'),
 
2798
                    ('dir/B', 'B-id')],
 
2799
            basis= [('dir/', 'A-id'),
 
2800
                    ('dir/B', 'B-id')],
 
2801
            target=[('dir/', 'C-id'),
 
2802
                    ('dir/B', 'B-id')])
 
2803
 
 
2804
    def test_parent_child_swap(self):
 
2805
        state = self.assertUpdate(# Same as basis
 
2806
            active=[('A/', 'A-id'),
 
2807
                    ('A/B/', 'B-id'),
 
2808
                    ('A/B/C', 'C-id')],
 
2809
            basis= [('A/', 'A-id'),
 
2810
                    ('A/B/', 'B-id'),
 
2811
                    ('A/B/C', 'C-id')],
 
2812
            target=[('A/', 'B-id'),
 
2813
                    ('A/B/', 'A-id'),
 
2814
                    ('A/B/C', 'C-id')])
 
2815
        state = self.assertUpdate(# Same as target
 
2816
            active=[('A/', 'B-id'),
 
2817
                    ('A/B/', 'A-id'),
 
2818
                    ('A/B/C', 'C-id')],
 
2819
            basis= [('A/', 'A-id'),
 
2820
                    ('A/B/', 'B-id'),
 
2821
                    ('A/B/C', 'C-id')],
 
2822
            target=[('A/', 'B-id'),
 
2823
                    ('A/B/', 'A-id'),
 
2824
                    ('A/B/C', 'C-id')])
 
2825
        state = self.assertUpdate(# empty active
 
2826
            active=[],
 
2827
            basis= [('A/', 'A-id'),
 
2828
                    ('A/B/', 'B-id'),
 
2829
                    ('A/B/C', 'C-id')],
 
2830
            target=[('A/', 'B-id'),
 
2831
                    ('A/B/', 'A-id'),
 
2832
                    ('A/B/C', 'C-id')])
 
2833
        state = self.assertUpdate(# different active
 
2834
            active=[('D/', 'A-id'),
 
2835
                    ('D/E/', 'B-id'),
 
2836
                    ('F', 'C-id')],
 
2837
            basis= [('A/', 'A-id'),
 
2838
                    ('A/B/', 'B-id'),
 
2839
                    ('A/B/C', 'C-id')],
 
2840
            target=[('A/', 'B-id'),
 
2841
                    ('A/B/', 'A-id'),
 
2842
                    ('A/B/C', 'C-id')])
 
2843
 
 
2844
    def test_change_root_id(self):
 
2845
        state = self.assertUpdate( # same as basis
 
2846
            active=[('', 'root-id'),
 
2847
                    ('file', 'file-id')],
 
2848
            basis= [('', 'root-id'),
 
2849
                    ('file', 'file-id')],
 
2850
            target=[('', 'target-root-id'),
 
2851
                    ('file', 'file-id')])
 
2852
        state = self.assertUpdate( # same as target
 
2853
            active=[('', 'target-root-id'),
 
2854
                    ('file', 'file-id')],
 
2855
            basis= [('', 'root-id'),
 
2856
                    ('file', 'file-id')],
 
2857
            target=[('', 'target-root-id'),
 
2858
                    ('file', 'root-id')])
 
2859
        state = self.assertUpdate( # all different
 
2860
            active=[('', 'active-root-id'),
 
2861
                    ('file', 'file-id')],
 
2862
            basis= [('', 'root-id'),
 
2863
                    ('file', 'file-id')],
 
2864
            target=[('', 'target-root-id'),
 
2865
                    ('file', 'root-id')])
 
2866
 
 
2867
    def test_change_file_absent_in_active(self):
 
2868
        state = self.assertUpdate(
 
2869
            active=[],
 
2870
            basis= [('file', 'file-id')],
 
2871
            target=[('file', 'file-id')])
 
2872
 
 
2873
    def test_invalid_changed_file(self):
 
2874
        state = self.assertBadDelta( # Not present in basis
 
2875
            active=[('file', 'file-id')],
 
2876
            basis= [],
 
2877
            delta=[('file', 'file', 'file-id')])
 
2878
        state = self.assertBadDelta( # present at another location in basis
 
2879
            active=[('file', 'file-id')],
 
2880
            basis= [('other-file', 'file-id')],
 
2881
            delta=[('file', 'file', 'file-id')])