~bzr-pqm/bzr/bzr.dev

« back to all changes in this revision

Viewing changes to bzrlib/tests/test_dirstate.py

  • Committer: Alexander Belchenko
  • Date: 2007-04-19 19:28:39 UTC
  • mto: This revision was merged to the branch mainline in revision 2439.
  • Revision ID: bialix@ukr.net-20070419192839-p964uu06n6vbjgrt
changes after John's review

Show diffs side-by-side

added added

removed removed

Lines of Context:
18
18
 
19
19
import bisect
20
20
import os
 
21
import time
21
22
 
22
23
from bzrlib import (
23
24
    dirstate,
25
26
    osutils,
26
27
    )
27
28
from bzrlib.memorytree import MemoryTree
28
 
from bzrlib.tests import TestCase, TestCaseWithTransport
 
29
from bzrlib.osutils import has_symlinks
 
30
from bzrlib.tests import (
 
31
        TestCase,
 
32
        TestCaseWithTransport,
 
33
        TestSkipped,
 
34
        )
29
35
 
30
36
 
31
37
# TODO:
32
 
# test DirStateRevisionTree : test filtering out of deleted files does not
33
 
#         filter out files called RECYCLED.BIN ;)
34
 
# test 0 parents, 1 parent, 4 parents.
35
 
# test unicode parents, non unicode parents
36
 
# test all change permutations in one and two parents.
37
 
# i.e. file in parent 1, dir in parent 2, symlink in tree.
38
 
# test that renames in the tree result in correct parent paths
39
 
# Test get state from a file, then asking for lines.
40
 
# write a smaller state, and check the file has been truncated.
41
 
# add a entry when its in state deleted
42
 
# revision attribute for root entries.
43
 
# test that utf8 strings are preserved in _row_to_line
44
 
# test parent manipulation
45
 
# test parents that are null in save : i.e. no record in the parent tree for this.
46
 
# todo: _set_data records ghost parents.
47
38
# TESTS to write:
48
39
# general checks for NOT_IN_MEMORY error conditions.
49
40
# set_path_id on a NOT_IN_MEMORY dirstate
55
46
# set_path_id  setting id when state is in memory unmodified
56
47
# set_path_id  setting id when state is in memory modified
57
48
 
 
49
 
58
50
class TestCaseWithDirState(TestCaseWithTransport):
59
51
    """Helper functions for creating DirState objects with various content."""
60
52
 
75
67
        state = self.create_empty_dirstate()
76
68
        try:
77
69
            state._set_data([], dirblocks)
 
70
            state._validate()
78
71
        except:
79
72
            state.unlock()
80
73
            raise
109
102
         b/g      g-file
110
103
         b/h\xc3\xa5  h-\xc3\xa5-file  #This is u'\xe5' encoded into utf-8
111
104
 
112
 
        # Notice that a/e is an empty directory.
 
105
        Notice that a/e is an empty directory.
 
106
 
 
107
        :return: The dirstate, still write-locked.
113
108
        """
114
109
        packed_stat = 'AAAAREUHaIpFB2iKAAADAQAtkqUAAIGk'
115
110
        null_sha = 'xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx'
146
141
        dirblocks.append(('a', [e_entry, f_entry]))
147
142
        dirblocks.append(('b', [g_entry, h_entry]))
148
143
        state = dirstate.DirState.initialize('dirstate')
 
144
        state._validate()
149
145
        try:
150
146
            state._set_data([], dirblocks)
151
147
        except:
185
181
        finally:
186
182
            state.unlock()
187
183
 
 
184
    def create_basic_dirstate(self):
 
185
        """Create a dirstate with a few files and directories.
 
186
 
 
187
            a
 
188
            b/
 
189
              c
 
190
              d/
 
191
                e
 
192
            f
 
193
        """
 
194
        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']
 
197
        self.build_tree(['tree/' + p for p in paths])
 
198
        tree.set_root_id('TREE_ROOT')
 
199
        tree.add([p.rstrip('/') for p in paths], file_ids)
 
200
        tree.commit('initial', rev_id='rev-1')
 
201
        revision_id = 'rev-1'
 
202
        # a_packed_stat = dirstate.pack_stat(os.stat('tree/a'))
 
203
        t = self.get_transport().clone('tree')
 
204
        a_text = t.get_bytes('a')
 
205
        a_sha = osutils.sha_string(a_text)
 
206
        a_len = len(a_text)
 
207
        # b_packed_stat = dirstate.pack_stat(os.stat('tree/b'))
 
208
        # c_packed_stat = dirstate.pack_stat(os.stat('tree/b/c'))
 
209
        c_text = t.get_bytes('b/c')
 
210
        c_sha = osutils.sha_string(c_text)
 
211
        c_len = len(c_text)
 
212
        # d_packed_stat = dirstate.pack_stat(os.stat('tree/b/d'))
 
213
        # e_packed_stat = dirstate.pack_stat(os.stat('tree/b/d/e'))
 
214
        e_text = t.get_bytes('b/d/e')
 
215
        e_sha = osutils.sha_string(e_text)
 
216
        e_len = len(e_text)
 
217
        # f_packed_stat = dirstate.pack_stat(os.stat('tree/f'))
 
218
        f_text = t.get_bytes('f')
 
219
        f_sha = osutils.sha_string(f_text)
 
220
        f_len = len(f_text)
 
221
        null_stat = dirstate.DirState.NULLSTAT
 
222
        expected = {
 
223
            '':(('', '', 'TREE_ROOT'), [
 
224
                  ('d', '', 0, False, null_stat),
 
225
                  ('d', '', 0, False, revision_id),
 
226
                ]),
 
227
            'a':(('', 'a', 'a-id'), [
 
228
                   ('f', '', 0, False, null_stat),
 
229
                   ('f', a_sha, a_len, False, revision_id),
 
230
                 ]),
 
231
            'b':(('', 'b', 'b-id'), [
 
232
                  ('d', '', 0, False, null_stat),
 
233
                  ('d', '', 0, False, revision_id),
 
234
                 ]),
 
235
            'b/c':(('b', 'c', 'c-id'), [
 
236
                    ('f', '', 0, False, null_stat),
 
237
                    ('f', c_sha, c_len, False, revision_id),
 
238
                   ]),
 
239
            'b/d':(('b', 'd', 'd-id'), [
 
240
                    ('d', '', 0, False, null_stat),
 
241
                    ('d', '', 0, False, revision_id),
 
242
                   ]),
 
243
            'b/d/e':(('b/d', 'e', 'e-id'), [
 
244
                      ('f', '', 0, False, null_stat),
 
245
                      ('f', e_sha, e_len, False, revision_id),
 
246
                     ]),
 
247
            'f':(('', 'f', 'f-id'), [
 
248
                  ('f', '', 0, False, null_stat),
 
249
                  ('f', f_sha, f_len, False, revision_id),
 
250
                 ]),
 
251
        }
 
252
        state = dirstate.DirState.from_tree(tree, 'dirstate')
 
253
        try:
 
254
            state.save()
 
255
        finally:
 
256
            state.unlock()
 
257
        # Use a different object, to make sure nothing is pre-cached in memory.
 
258
        state = dirstate.DirState.on_file('dirstate')
 
259
        state.lock_read()
 
260
        self.addCleanup(state.unlock)
 
261
        self.assertEqual(dirstate.DirState.NOT_IN_MEMORY,
 
262
                         state._dirblock_state)
 
263
        # This is code is only really tested if we actually have to make more
 
264
        # than one read, so set the page size to something smaller.
 
265
        # We want it to contain about 2.2 records, so that we have a couple
 
266
        # records that we can read per attempt
 
267
        state._bisect_page_size = 200
 
268
        return tree, state, expected
 
269
 
 
270
    def create_duplicated_dirstate(self):
 
271
        """Create a dirstate with a deleted and added entries.
 
272
 
 
273
        This grabs a basic_dirstate, and then removes and re adds every entry
 
274
        with a new file id.
 
275
        """
 
276
        tree, state, expected = self.create_basic_dirstate()
 
277
        # Now we will just remove and add every file so we get an extra entry
 
278
        # 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'])
 
282
 
 
283
        # Update the expected dictionary.
 
284
        for path in ['a', 'b', 'b/c', 'b/d', 'b/d/e', 'f']:
 
285
            orig = expected[path]
 
286
            path2 = path + '2'
 
287
            # This record was deleted in the current tree
 
288
            expected[path] = (orig[0], [dirstate.DirState.NULL_PARENT_DETAILS,
 
289
                                        orig[1][1]])
 
290
            new_key = (orig[0][0], orig[0][1], orig[0][2]+'2')
 
291
            # And didn't exist in the basis tree
 
292
            expected[path2] = (new_key, [orig[1][0],
 
293
                                         dirstate.DirState.NULL_PARENT_DETAILS])
 
294
 
 
295
        # We will replace the 'dirstate' file underneath 'state', but that is
 
296
        # okay as lock as we unlock 'state' first.
 
297
        state.unlock()
 
298
        try:
 
299
            new_state = dirstate.DirState.from_tree(tree, 'dirstate')
 
300
            try:
 
301
                new_state.save()
 
302
            finally:
 
303
                new_state.unlock()
 
304
        finally:
 
305
            # But we need to leave state in a read-lock because we already have
 
306
            # a cleanup scheduled
 
307
            state.lock_read()
 
308
        return tree, state, expected
 
309
 
 
310
    def create_renamed_dirstate(self):
 
311
        """Create a dirstate with a few internal renames.
 
312
 
 
313
        This takes the basic dirstate, and moves the paths around.
 
314
        """
 
315
        tree, state, expected = self.create_basic_dirstate()
 
316
        # Rename a file
 
317
        tree.rename_one('a', 'b/g')
 
318
        # And a directory
 
319
        tree.rename_one('b/d', 'h')
 
320
 
 
321
        old_a = expected['a']
 
322
        expected['a'] = (old_a[0], [('r', 'b/g', 0, False, ''), old_a[1][1]])
 
323
        expected['b/g'] = (('b', 'g', 'a-id'), [old_a[1][0],
 
324
                                                ('r', 'a', 0, False, '')])
 
325
        old_d = expected['b/d']
 
326
        expected['b/d'] = (old_d[0], [('r', 'h', 0, False, ''), old_d[1][1]])
 
327
        expected['h'] = (('', 'h', 'd-id'), [old_d[1][0],
 
328
                                             ('r', 'b/d', 0, False, '')])
 
329
 
 
330
        old_e = expected['b/d/e']
 
331
        expected['b/d/e'] = (old_e[0], [('r', 'h/e', 0, False, ''),
 
332
                             old_e[1][1]])
 
333
        expected['h/e'] = (('h', 'e', 'e-id'), [old_e[1][0],
 
334
                                                ('r', 'b/d/e', 0, False, '')])
 
335
 
 
336
        state.unlock()
 
337
        try:
 
338
            new_state = dirstate.DirState.from_tree(tree, 'dirstate')
 
339
            try:
 
340
                new_state.save()
 
341
            finally:
 
342
                new_state.unlock()
 
343
        finally:
 
344
            state.lock_read()
 
345
        return tree, state, expected
188
346
 
189
347
class TestTreeToDirState(TestCaseWithDirState):
190
348
 
197
355
             [('d', '', 0, False, dirstate.DirState.NULLSTAT), # current tree
198
356
             ])])
199
357
        state = dirstate.DirState.from_tree(tree, 'dirstate')
 
358
        state._validate()
200
359
        self.check_state_with_reopen(expected_result, state)
201
360
 
202
361
    def test_1_parents_empty_to_dirstate(self):
211
370
             ])])
212
371
        state = dirstate.DirState.from_tree(tree, 'dirstate')
213
372
        self.check_state_with_reopen(expected_result, state)
 
373
        state.lock_read()
 
374
        try:
 
375
            state._validate()
 
376
        finally:
 
377
            state.unlock()
214
378
 
215
379
    def test_2_parents_empty_to_dirstate(self):
216
380
        # create a parent by doing a commit
227
391
             ])])
228
392
        state = dirstate.DirState.from_tree(tree, 'dirstate')
229
393
        self.check_state_with_reopen(expected_result, state)
 
394
        state.lock_read()
 
395
        try:
 
396
            state._validate()
 
397
        finally:
 
398
            state.unlock()
230
399
 
231
400
    def test_empty_unknowns_are_ignored_to_dirstate(self):
232
401
        """We should be able to create a dirstate for an empty tree."""
312
481
        state = dirstate.DirState.from_tree(tree, 'dirstate')
313
482
        self.check_state_with_reopen(expected_result, state)
314
483
 
 
484
    def test_colliding_fileids(self):
 
485
        # test insertion of parents creating several entries at the same path.
 
486
        # we used to have a bug where they could cause the dirstate to break
 
487
        # its ordering invariants.
 
488
        # create some trees to test from
 
489
        parents = []
 
490
        for i in range(7):
 
491
            tree = self.make_branch_and_tree('tree%d' % i)
 
492
            self.build_tree(['tree%d/name' % i,])
 
493
            tree.add(['name'], ['file-id%d' % i])
 
494
            revision_id = 'revid-%d' % i
 
495
            tree.commit('message', rev_id=revision_id)
 
496
            parents.append((revision_id,
 
497
                tree.branch.repository.revision_tree(revision_id)))
 
498
        # now fold these trees into a dirstate
 
499
        state = dirstate.DirState.initialize('dirstate')
 
500
        try:
 
501
            state.set_parent_trees(parents, [])
 
502
            state._validate()
 
503
        finally:
 
504
            state.unlock()
 
505
 
315
506
 
316
507
class TestDirStateOnFile(TestCaseWithDirState):
317
508
 
348
539
        finally:
349
540
            state.unlock()
350
541
 
 
542
    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()
 
555
        try:
 
556
            entry = state._get_entry(0, path_utf8='a-file')
 
557
            # The current sha1 sum should be empty
 
558
            self.assertEqual('', entry[1][0][1])
 
559
            # We should have a real entry.
 
560
            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
 
563
            self.assertEqual('ecc5374e9ed82ad3ea3b4d452ea995a5fd3e70e3',
 
564
                             sha1sum)
 
565
 
 
566
            # The dirblock has been updated
 
567
            self.assertEqual(sha1sum, entry[1][0][1])
 
568
            self.assertEqual(dirstate.DirState.IN_MEMORY_MODIFIED,
 
569
                             state._dirblock_state)
 
570
 
 
571
            del entry
 
572
            # Now, since we are the only one holding a lock, we should be able
 
573
            # to save and have it written to disk
 
574
            state.save()
 
575
        finally:
 
576
            state.unlock()
 
577
 
 
578
        # Re-open the file, and ensure that the state has been updated.
 
579
        state = dirstate.DirState.on_file('dirstate')
 
580
        state.lock_read()
 
581
        try:
 
582
            entry = state._get_entry(0, path_utf8='a-file')
 
583
            self.assertEqual(sha1sum, entry[1][0][1])
 
584
        finally:
 
585
            state.unlock()
 
586
 
 
587
    def test_save_fails_quietly_if_locked(self):
 
588
        """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()
 
600
        try:
 
601
            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
 
604
            self.assertEqual('ecc5374e9ed82ad3ea3b4d452ea995a5fd3e70e3',
 
605
                             sha1sum)
 
606
            self.assertEqual(dirstate.DirState.IN_MEMORY_MODIFIED,
 
607
                             state._dirblock_state)
 
608
 
 
609
            # Now, before we try to save, grab another dirstate, and take out a
 
610
            # read lock.
 
611
            # TODO: jam 20070315 Ideally this would be locked by another
 
612
            #       process. To make sure the file is really OS locked.
 
613
            state2 = dirstate.DirState.on_file('dirstate')
 
614
            state2.lock_read()
 
615
            try:
 
616
                # This won't actually write anything, because it couldn't grab
 
617
                # a write lock. But it shouldn't raise an error, either.
 
618
                # TODO: jam 20070315 We should probably distinguish between
 
619
                #       being dirty because of 'update_entry'. And dirty
 
620
                #       because of real modification. So that save() *does*
 
621
                #       raise a real error if it fails when we have real
 
622
                #       modifications.
 
623
                state.save()
 
624
            finally:
 
625
                state2.unlock()
 
626
        finally:
 
627
            state.unlock()
 
628
        
 
629
        # The file on disk should not be modified.
 
630
        state = dirstate.DirState.on_file('dirstate')
 
631
        state.lock_read()
 
632
        try:
 
633
            entry = state._get_entry(0, path_utf8='a-file')
 
634
            self.assertEqual('', entry[1][0][1])
 
635
        finally:
 
636
            state.unlock()
 
637
 
351
638
 
352
639
class TestDirStateInitialize(TestCaseWithDirState):
353
640
 
420
707
        state = dirstate.DirState.on_file('dirstate')
421
708
        state.lock_read()
422
709
        try:
423
 
            self.assertEqual(expected_rows, list(state._iter_entries()))
424
 
        finally:
425
 
            state.unlock()
 
710
            state._validate()
 
711
            self.assertEqual(expected_rows, list(state._iter_entries()))
 
712
        finally:
 
713
            state.unlock()
 
714
 
 
715
    def test_set_path_id_with_parents(self):
 
716
        """Set the root file id in a dirstate with parents"""
 
717
        mt = self.make_branch_and_tree('mt')
 
718
        # in case the default tree format uses a different root id
 
719
        mt.set_root_id('TREE_ROOT')
 
720
        mt.commit('foo', rev_id='parent-revid')
 
721
        rt = mt.branch.repository.revision_tree('parent-revid')
 
722
        state = dirstate.DirState.initialize('dirstate')
 
723
        state._validate()
 
724
        try:
 
725
            state.set_parent_trees([('parent-revid', rt)], ghosts=[])
 
726
            state.set_path_id('', 'foobarbaz')
 
727
            state._validate()
 
728
            # 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
                ]
 
739
            state._validate()
 
740
            self.assertEqual(expected_rows, list(state._iter_entries()))
 
741
            # should work across save too
 
742
            state.save()
 
743
        finally:
 
744
            state.unlock()
 
745
        # now flush & check we get the same
 
746
        state = dirstate.DirState.on_file('dirstate')
 
747
        state.lock_read()
 
748
        try:
 
749
            state._validate()
 
750
            self.assertEqual(expected_rows, list(state._iter_entries()))
 
751
        finally:
 
752
            state.unlock()
 
753
        # now change within an existing file-backed state
 
754
        state.lock_write()
 
755
        try:
 
756
            state._validate()
 
757
            state.set_path_id('', 'tree-root-2')
 
758
            state._validate()
 
759
        finally:
 
760
            state.unlock()
 
761
 
426
762
 
427
763
    def test_set_parent_trees_no_content(self):
428
764
        # set_parent_trees is a slow but important api to support.
451
787
                ['ghost-rev'])
452
788
            # check we can reopen and use the dirstate after setting parent
453
789
            # trees.
 
790
            state._validate()
454
791
            state.save()
 
792
            state._validate()
455
793
        finally:
456
794
            state.unlock()
457
795
        state = dirstate.DirState.on_file('dirstate')
610
948
            state.unlock()
611
949
        state = dirstate.DirState.on_file('dirstate')
612
950
        state.lock_read()
 
951
        state._validate()
613
952
        try:
614
953
            self.assertEqual(expected_entries, list(state._iter_entries()))
615
954
        finally:
618
957
    def test_add_symlink_to_root_no_parents_all_data(self):
619
958
        # The most trivial addition of a symlink when there are no parents and
620
959
        # its in the root and all data about the file is supplied
621
 
        ## TODO: windows: dont fail this test. Also, how are symlinks meant to
622
 
        # be represented on windows.
 
960
        # bzr doesn't support fake symlinks on windows, yet.
 
961
        if not has_symlinks():
 
962
            raise TestSkipped("No symlink support")
623
963
        os.symlink('target', 'a link')
624
964
        stat = os.lstat('a link')
625
965
        expected_entries = [
680
1020
        finally:
681
1021
            state.unlock()
682
1022
 
 
1023
    def test_add_tree_reference(self):
 
1024
        # make a dirstate and add a tree reference
 
1025
        state = dirstate.DirState.initialize('dirstate')
 
1026
        expected_entry = (
 
1027
            ('', 'subdir', 'subdir-id'),
 
1028
            [('t', 'subtree-123123', 0, False,
 
1029
              'xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx')],
 
1030
            )
 
1031
        try:
 
1032
            state.add('subdir', 'subdir-id', 'tree-reference', None, 'subtree-123123')
 
1033
            entry = state._get_entry(0, 'subdir-id', 'subdir')
 
1034
            self.assertEqual(entry, expected_entry)
 
1035
            state._validate()
 
1036
            state.save()
 
1037
        finally:
 
1038
            state.unlock()
 
1039
        # now check we can read it back
 
1040
        state.lock_read()
 
1041
        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()
 
1051
 
 
1052
    def test_add_forbidden_names(self):
 
1053
        state = dirstate.DirState.initialize('dirstate')
 
1054
        self.addCleanup(state.unlock)
 
1055
        self.assertRaises(errors.BzrError,
 
1056
            state.add, '.', 'ass-id', 'directory', None, None)
 
1057
        self.assertRaises(errors.BzrError,
 
1058
            state.add, '..', 'ass-id', 'directory', None, None)
 
1059
 
683
1060
 
684
1061
class TestGetLines(TestCaseWithDirState):
685
1062
 
687
1064
        state = self.create_dirstate_with_root_and_subdir()
688
1065
        try:
689
1066
            self.assertEqual(['#bazaar dirstate flat format 3\n',
690
 
                'adler32: -1327947603\n',
 
1067
                'crc32: 41262208\n',
691
1068
                'num_entries: 2\n',
692
1069
                '0\x00\n\x00'
693
1070
                '0\x00\n\x00'
960
1337
        self.assertEqual(expected, dirblock_names)
961
1338
 
962
1339
 
963
 
class TestBisect(TestCaseWithTransport):
 
1340
class InstrumentedDirState(dirstate.DirState):
 
1341
    """An DirState with instrumented sha1 functionality."""
 
1342
 
 
1343
    def __init__(self, path):
 
1344
        super(InstrumentedDirState, self).__init__(path)
 
1345
        self._time_offset = 0
 
1346
        self._log = []
 
1347
 
 
1348
    def _sha_cutoff_time(self):
 
1349
        timestamp = super(InstrumentedDirState, self)._sha_cutoff_time()
 
1350
        self._cutoff_time = timestamp + self._time_offset
 
1351
 
 
1352
    def _sha1_file(self, abspath, entry):
 
1353
        self._log.append(('sha1', abspath))
 
1354
        return super(InstrumentedDirState, self)._sha1_file(abspath, entry)
 
1355
 
 
1356
    def _read_link(self, abspath, old_link):
 
1357
        self._log.append(('read_link', abspath, old_link))
 
1358
        return super(InstrumentedDirState, self)._read_link(abspath, old_link)
 
1359
 
 
1360
    def _lstat(self, abspath, entry):
 
1361
        self._log.append(('lstat', abspath))
 
1362
        return super(InstrumentedDirState, self)._lstat(abspath, entry)
 
1363
 
 
1364
    def _is_executable(self, mode, old_executable):
 
1365
        self._log.append(('is_exec', mode, old_executable))
 
1366
        return super(InstrumentedDirState, self)._is_executable(mode,
 
1367
                                                                old_executable)
 
1368
 
 
1369
    def adjust_time(self, secs):
 
1370
        """Move the clock forward or back.
 
1371
 
 
1372
        :param secs: The amount to adjust the clock by. Positive values make it
 
1373
        seem as if we are in the future, negative values make it seem like we
 
1374
        are in the past.
 
1375
        """
 
1376
        self._time_offset += secs
 
1377
        self._cutoff_time = None
 
1378
 
 
1379
 
 
1380
class _FakeStat(object):
 
1381
    """A class with the same attributes as a real stat result."""
 
1382
 
 
1383
    def __init__(self, size, mtime, ctime, dev, ino, mode):
 
1384
        self.st_size = size
 
1385
        self.st_mtime = mtime
 
1386
        self.st_ctime = ctime
 
1387
        self.st_dev = dev
 
1388
        self.st_ino = ino
 
1389
        self.st_mode = mode
 
1390
 
 
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):
 
1687
 
 
1688
    def assertPackStat(self, expected, stat_value):
 
1689
        """Check the packed and serialized form of a stat value."""
 
1690
        self.assertEqual(expected, dirstate.pack_stat(stat_value))
 
1691
 
 
1692
    def test_pack_stat_int(self):
 
1693
        st = _FakeStat(6859L, 1172758614, 1172758617, 777L, 6499538L, 0100644)
 
1694
        # Make sure that all parameters have an impact on the packed stat.
 
1695
        self.assertPackStat('AAAay0Xm4FZF5uBZAAADCQBjLNIAAIGk', st)
 
1696
        st.st_size = 7000L
 
1697
        #                ay0 => bWE
 
1698
        self.assertPackStat('AAAbWEXm4FZF5uBZAAADCQBjLNIAAIGk', st)
 
1699
        st.st_mtime = 1172758620
 
1700
        #                     4FZ => 4Fx
 
1701
        self.assertPackStat('AAAbWEXm4FxF5uBZAAADCQBjLNIAAIGk', st)
 
1702
        st.st_ctime = 1172758630
 
1703
        #                          uBZ => uBm
 
1704
        self.assertPackStat('AAAbWEXm4FxF5uBmAAADCQBjLNIAAIGk', st)
 
1705
        st.st_dev = 888L
 
1706
        #                                DCQ => DeA
 
1707
        self.assertPackStat('AAAbWEXm4FxF5uBmAAADeABjLNIAAIGk', st)
 
1708
        st.st_ino = 6499540L
 
1709
        #                                     LNI => LNQ
 
1710
        self.assertPackStat('AAAbWEXm4FxF5uBmAAADeABjLNQAAIGk', st)
 
1711
        st.st_mode = 0100744
 
1712
        #                                          IGk => IHk
 
1713
        self.assertPackStat('AAAbWEXm4FxF5uBmAAADeABjLNQAAIHk', st)
 
1714
 
 
1715
    def test_pack_stat_float(self):
 
1716
        """On some platforms mtime and ctime are floats.
 
1717
 
 
1718
        Make sure we don't get warnings or errors, and that we ignore changes <
 
1719
        1s
 
1720
        """
 
1721
        st = _FakeStat(7000L, 1172758614.0, 1172758617.0,
 
1722
                       777L, 6499538L, 0100644)
 
1723
        # These should all be the same as the integer counterparts
 
1724
        self.assertPackStat('AAAbWEXm4FZF5uBZAAADCQBjLNIAAIGk', st)
 
1725
        st.st_mtime = 1172758620.0
 
1726
        #                     FZF5 => FxF5
 
1727
        self.assertPackStat('AAAbWEXm4FxF5uBZAAADCQBjLNIAAIGk', st)
 
1728
        st.st_ctime = 1172758630.0
 
1729
        #                          uBZ => uBm
 
1730
        self.assertPackStat('AAAbWEXm4FxF5uBmAAADCQBjLNIAAIGk', st)
 
1731
        # fractional seconds are discarded, so no change from above
 
1732
        st.st_mtime = 1172758620.453
 
1733
        self.assertPackStat('AAAbWEXm4FxF5uBmAAADCQBjLNIAAIGk', st)
 
1734
        st.st_ctime = 1172758630.228
 
1735
        self.assertPackStat('AAAbWEXm4FxF5uBmAAADCQBjLNIAAIGk', st)
 
1736
 
 
1737
 
 
1738
class TestBisect(TestCaseWithDirState):
964
1739
    """Test the ability to bisect into the disk format."""
965
1740
 
966
 
    def create_basic_dirstate(self):
967
 
        """Create a dirstate with a few files and directories.
968
 
 
969
 
            a
970
 
            b/
971
 
              c
972
 
              d/
973
 
                e
974
 
            f
975
 
        """
976
 
        tree = self.make_branch_and_tree('tree')
977
 
        paths = ['a', 'b/', 'b/c', 'b/d/', 'b/d/e', 'f']
978
 
        file_ids = ['a-id', 'b-id', 'c-id', 'd-id', 'e-id', 'f-id']
979
 
        self.build_tree(['tree/' + p for p in paths])
980
 
        tree.set_root_id('TREE_ROOT')
981
 
        tree.add([p.rstrip('/') for p in paths], file_ids)
982
 
        tree.commit('initial', rev_id='rev-1')
983
 
        revision_id = 'rev-1'
984
 
        # a_packed_stat = dirstate.pack_stat(os.stat('tree/a'))
985
 
        t = self.get_transport().clone('tree')
986
 
        a_text = t.get_bytes('a')
987
 
        a_sha = osutils.sha_string(a_text)
988
 
        a_len = len(a_text)
989
 
        # b_packed_stat = dirstate.pack_stat(os.stat('tree/b'))
990
 
        # c_packed_stat = dirstate.pack_stat(os.stat('tree/b/c'))
991
 
        c_text = t.get_bytes('b/c')
992
 
        c_sha = osutils.sha_string(c_text)
993
 
        c_len = len(c_text)
994
 
        # d_packed_stat = dirstate.pack_stat(os.stat('tree/b/d'))
995
 
        # e_packed_stat = dirstate.pack_stat(os.stat('tree/b/d/e'))
996
 
        e_text = t.get_bytes('b/d/e')
997
 
        e_sha = osutils.sha_string(e_text)
998
 
        e_len = len(e_text)
999
 
        # f_packed_stat = dirstate.pack_stat(os.stat('tree/f'))
1000
 
        f_text = t.get_bytes('f')
1001
 
        f_sha = osutils.sha_string(f_text)
1002
 
        f_len = len(f_text)
1003
 
        null_stat = dirstate.DirState.NULLSTAT
1004
 
        expected = {
1005
 
            '':(('', '', 'TREE_ROOT'), [
1006
 
                  ('d', '', 0, False, null_stat),
1007
 
                  ('d', '', 0, False, revision_id),
1008
 
                ]),
1009
 
            'a':(('', 'a', 'a-id'), [
1010
 
                   ('f', '', 0, False, null_stat),
1011
 
                   ('f', a_sha, a_len, False, revision_id),
1012
 
                 ]),
1013
 
            'b':(('', 'b', 'b-id'), [
1014
 
                  ('d', '', 0, False, null_stat),
1015
 
                  ('d', '', 0, False, revision_id),
1016
 
                 ]),
1017
 
            'b/c':(('b', 'c', 'c-id'), [
1018
 
                    ('f', '', 0, False, null_stat),
1019
 
                    ('f', c_sha, c_len, False, revision_id),
1020
 
                   ]),
1021
 
            'b/d':(('b', 'd', 'd-id'), [
1022
 
                    ('d', '', 0, False, null_stat),
1023
 
                    ('d', '', 0, False, revision_id),
1024
 
                   ]),
1025
 
            'b/d/e':(('b/d', 'e', 'e-id'), [
1026
 
                      ('f', '', 0, False, null_stat),
1027
 
                      ('f', e_sha, e_len, False, revision_id),
1028
 
                     ]),
1029
 
            'f':(('', 'f', 'f-id'), [
1030
 
                  ('f', '', 0, False, null_stat),
1031
 
                  ('f', f_sha, f_len, False, revision_id),
1032
 
                 ]),
1033
 
        }
1034
 
        state = dirstate.DirState.from_tree(tree, 'dirstate')
1035
 
        try:
1036
 
            state.save()
1037
 
        finally:
1038
 
            state.unlock()
1039
 
        # Use a different object, to make sure nothing is pre-cached in memory.
1040
 
        state = dirstate.DirState.on_file('dirstate')
1041
 
        state.lock_read()
1042
 
        self.addCleanup(state.unlock)
1043
 
        self.assertEqual(dirstate.DirState.NOT_IN_MEMORY,
1044
 
                         state._dirblock_state)
1045
 
        # This is code is only really tested if we actually have to make more
1046
 
        # than one read, so set the page size to something smaller.
1047
 
        # We want it to contain about 2.2 records, so that we have a couple
1048
 
        # records that we can read per attempt
1049
 
        state._bisect_page_size = 200
1050
 
        return tree, state, expected
1051
 
 
1052
 
    def create_duplicated_dirstate(self):
1053
 
        """Create a dirstate with a deleted and added entries.
1054
 
 
1055
 
        This grabs a basic_dirstate, and then removes and re adds every entry
1056
 
        with a new file id.
1057
 
        """
1058
 
        tree, state, expected = self.create_basic_dirstate()
1059
 
        # Now we will just remove and add every file so we get an extra entry
1060
 
        # per entry. Unversion in reverse order so we handle subdirs
1061
 
        tree.unversion(['f-id', 'e-id', 'd-id', 'c-id', 'b-id', 'a-id'])
1062
 
        tree.add(['a', 'b', 'b/c', 'b/d', 'b/d/e', 'f'],
1063
 
                 ['a-id2', 'b-id2', 'c-id2', 'd-id2', 'e-id2', 'f-id2'])
1064
 
 
1065
 
        # Update the expected dictionary.
1066
 
        for path in ['a', 'b', 'b/c', 'b/d', 'b/d/e', 'f']:
1067
 
            orig = expected[path]
1068
 
            path2 = path + '2'
1069
 
            # This record was deleted in the current tree
1070
 
            expected[path] = (orig[0], [dirstate.DirState.NULL_PARENT_DETAILS,
1071
 
                                        orig[1][1]])
1072
 
            new_key = (orig[0][0], orig[0][1], orig[0][2]+'2')
1073
 
            # And didn't exist in the basis tree
1074
 
            expected[path2] = (new_key, [orig[1][0],
1075
 
                                         dirstate.DirState.NULL_PARENT_DETAILS])
1076
 
 
1077
 
        # We will replace the 'dirstate' file underneath 'state', but that is
1078
 
        # okay as lock as we unlock 'state' first.
1079
 
        state.unlock()
1080
 
        try:
1081
 
            new_state = dirstate.DirState.from_tree(tree, 'dirstate')
1082
 
            try:
1083
 
                new_state.save()
1084
 
            finally:
1085
 
                new_state.unlock()
1086
 
        finally:
1087
 
            # But we need to leave state in a read-lock because we already have
1088
 
            # a cleanup scheduled
1089
 
            state.lock_read()
1090
 
        return tree, state, expected
1091
 
 
1092
 
    def create_renamed_dirstate(self):
1093
 
        """Create a dirstate with a few internal renames.
1094
 
 
1095
 
        This takes the basic dirstate, and moves the paths around.
1096
 
        """
1097
 
        tree, state, expected = self.create_basic_dirstate()
1098
 
        # Rename a file
1099
 
        tree.rename_one('a', 'b/g')
1100
 
        # And a directory
1101
 
        tree.rename_one('b/d', 'h')
1102
 
 
1103
 
        old_a = expected['a']
1104
 
        expected['a'] = (old_a[0], [('r', 'b/g', 0, False, ''), old_a[1][1]])
1105
 
        expected['b/g'] = (('b', 'g', 'a-id'), [old_a[1][0],
1106
 
                                                ('r', 'a', 0, False, '')])
1107
 
        old_d = expected['b/d']
1108
 
        expected['b/d'] = (old_d[0], [('r', 'h', 0, False, ''), old_d[1][1]])
1109
 
        expected['h'] = (('', 'h', 'd-id'), [old_d[1][0],
1110
 
                                             ('r', 'b/d', 0, False, '')])
1111
 
 
1112
 
        old_e = expected['b/d/e']
1113
 
        expected['b/d/e'] = (old_e[0], [('r', 'h/e', 0, False, ''),
1114
 
                             old_e[1][1]])
1115
 
        expected['h/e'] = (('h', 'e', 'e-id'), [old_e[1][0],
1116
 
                                                ('r', 'b/d/e', 0, False, '')])
1117
 
 
1118
 
        state.unlock()
1119
 
        try:
1120
 
            new_state = dirstate.DirState.from_tree(tree, 'dirstate')
1121
 
            try:
1122
 
                new_state.save()
1123
 
            finally:
1124
 
                new_state.unlock()
1125
 
        finally:
1126
 
            state.lock_read()
1127
 
        return tree, state, expected
1128
1741
 
1129
1742
    def assertBisect(self, expected_map, map_keys, state, paths):
1130
1743
        """Assert that bisecting for paths returns the right result.
1437
2050
        for path in paths:
1438
2051
            self.assertBisect(dirblocks, split_dirblocks, path, cache=cache)
1439
2052
 
 
2053
 
 
2054
class TestDirstateValidation(TestCaseWithDirState):
 
2055
 
 
2056
    def test_validate_correct_dirstate(self):
 
2057
        state = self.create_complex_dirstate()
 
2058
        state._validate()
 
2059
        state.unlock()
 
2060
        # and make sure we can also validate with a read lock
 
2061
        state.lock_read()
 
2062
        try:
 
2063
            state._validate()
 
2064
        finally:
 
2065
            state.unlock()
 
2066
 
 
2067
    def test_dirblock_not_sorted(self):
 
2068
        tree, state, expected = self.create_renamed_dirstate()
 
2069
        state._read_dirblocks_if_needed()
 
2070
        last_dirblock = state._dirblocks[-1]
 
2071
        # we're appending to the dirblock, but this name comes before some of
 
2072
        # the existing names; that's wrong
 
2073
        last_dirblock[1].append(
 
2074
            (('h', 'aaaa', 'a-id'),
 
2075
             [('a', '', 0, False, ''),
 
2076
              ('a', '', 0, False, '')]))
 
2077
        e = self.assertRaises(AssertionError,
 
2078
            state._validate)
 
2079
        self.assertContainsRe(str(e), 'not sorted')
 
2080
 
 
2081
    def test_dirblock_name_mismatch(self):
 
2082
        tree, state, expected = self.create_renamed_dirstate()
 
2083
        state._read_dirblocks_if_needed()
 
2084
        last_dirblock = state._dirblocks[-1]
 
2085
        # add an entry with the wrong directory name
 
2086
        last_dirblock[1].append(
 
2087
            (('', 'z', 'a-id'),
 
2088
             [('a', '', 0, False, ''),
 
2089
              ('a', '', 0, False, '')]))
 
2090
        e = self.assertRaises(AssertionError,
 
2091
            state._validate)
 
2092
        self.assertContainsRe(str(e),
 
2093
            "doesn't match directory name")
 
2094
 
 
2095
    def test_dirblock_missing_rename(self):
 
2096
        tree, state, expected = self.create_renamed_dirstate()
 
2097
        state._read_dirblocks_if_needed()
 
2098
        last_dirblock = state._dirblocks[-1]
 
2099
        # make another entry for a-id, without a correct 'r' pointer to
 
2100
        # the real occurrence in the working tree
 
2101
        last_dirblock[1].append(
 
2102
            (('h', 'z', 'a-id'),
 
2103
             [('a', '', 0, False, ''),
 
2104
              ('a', '', 0, False, '')]))
 
2105
        e = self.assertRaises(AssertionError,
 
2106
            state._validate)
 
2107
        self.assertContainsRe(str(e),
 
2108
            'file a-id is absent in row')