1
# Copyright (C) 2006, 2007 Canonical Ltd
3
# This program is free software; you can redistribute it and/or modify
4
# it under the terms of the GNU General Public License as published by
5
# the Free Software Foundation; either version 2 of the License, or
6
# (at your option) any later version.
8
# This program is distributed in the hope that it will be useful,
9
# but WITHOUT ANY WARRANTY; without even the implied warranty of
10
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
11
# GNU General Public License for more details.
13
# You should have received a copy of the GNU General Public License
14
# along with this program; if not, write to the Free Software
15
# Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA
17
"""Tests of the dirstate functionality being built for WorkingTreeFormat4."""
28
from bzrlib.memorytree import MemoryTree
29
from bzrlib.osutils import has_symlinks
30
from bzrlib.tests import (
32
TestCaseWithTransport,
39
# general checks for NOT_IN_MEMORY error conditions.
40
# set_path_id on a NOT_IN_MEMORY dirstate
41
# set_path_id unicode support
42
# set_path_id setting id of a path not root
43
# set_path_id setting id when there are parents without the id in the parents
44
# set_path_id setting id when there are parents with the id in the parents
45
# set_path_id setting id when state is not in memory
46
# set_path_id setting id when state is in memory unmodified
47
# set_path_id setting id when state is in memory modified
50
class TestCaseWithDirState(TestCaseWithTransport):
51
"""Helper functions for creating DirState objects with various content."""
53
def create_empty_dirstate(self):
54
"""Return a locked but empty dirstate"""
55
state = dirstate.DirState.initialize('dirstate')
58
def create_dirstate_with_root(self):
59
"""Return a write-locked state with a single root entry."""
60
packed_stat = 'AAAAREUHaIpFB2iKAAADAQAtkqUAAIGk'
61
root_entry_direntry = ('', '', 'a-root-value'), [
62
('d', '', 0, False, packed_stat),
65
dirblocks.append(('', [root_entry_direntry]))
66
dirblocks.append(('', []))
67
state = self.create_empty_dirstate()
69
state._set_data([], dirblocks)
76
def create_dirstate_with_root_and_subdir(self):
77
"""Return a locked DirState with a root and a subdir"""
78
packed_stat = 'AAAAREUHaIpFB2iKAAADAQAtkqUAAIGk'
79
subdir_entry = ('', 'subdir', 'subdir-id'), [
80
('d', '', 0, False, packed_stat),
82
state = self.create_dirstate_with_root()
84
dirblocks = list(state._dirblocks)
85
dirblocks[1][1].append(subdir_entry)
86
state._set_data([], dirblocks)
92
def create_complex_dirstate(self):
93
"""This dirstate contains multiple files and directories.
103
b/h\xc3\xa5 h-\xc3\xa5-file #This is u'\xe5' encoded into utf-8
105
# Notice that a/e is an empty directory.
107
packed_stat = 'AAAAREUHaIpFB2iKAAADAQAtkqUAAIGk'
108
null_sha = 'xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx'
109
root_entry = ('', '', 'a-root-value'), [
110
('d', '', 0, False, packed_stat),
112
a_entry = ('', 'a', 'a-dir'), [
113
('d', '', 0, False, packed_stat),
115
b_entry = ('', 'b', 'b-dir'), [
116
('d', '', 0, False, packed_stat),
118
c_entry = ('', 'c', 'c-file'), [
119
('f', null_sha, 10, False, packed_stat),
121
d_entry = ('', 'd', 'd-file'), [
122
('f', null_sha, 20, False, packed_stat),
124
e_entry = ('a', 'e', 'e-dir'), [
125
('d', '', 0, False, packed_stat),
127
f_entry = ('a', 'f', 'f-file'), [
128
('f', null_sha, 30, False, packed_stat),
130
g_entry = ('b', 'g', 'g-file'), [
131
('f', null_sha, 30, False, packed_stat),
133
h_entry = ('b', 'h\xc3\xa5', 'h-\xc3\xa5-file'), [
134
('f', null_sha, 40, False, packed_stat),
137
dirblocks.append(('', [root_entry]))
138
dirblocks.append(('', [a_entry, b_entry, c_entry, d_entry]))
139
dirblocks.append(('a', [e_entry, f_entry]))
140
dirblocks.append(('b', [g_entry, h_entry]))
141
state = dirstate.DirState.initialize('dirstate')
144
state._set_data([], dirblocks)
150
def check_state_with_reopen(self, expected_result, state):
151
"""Check that state has current state expected_result.
153
This will check the current state, open the file anew and check it
155
This function expects the current state to be locked for writing, and
156
will unlock it before re-opening.
157
This is required because we can't open a lock_read() while something
158
else has a lock_write().
159
write => mutually exclusive lock
162
# The state should already be write locked, since we just had to do
163
# some operation to get here.
164
assert state._lock_token is not None
166
self.assertEqual(expected_result[0], state.get_parent_ids())
167
# there should be no ghosts in this tree.
168
self.assertEqual([], state.get_ghosts())
169
# there should be one fileid in this tree - the root of the tree.
170
self.assertEqual(expected_result[1], list(state._iter_entries()))
174
del state # Callers should unlock
175
state = dirstate.DirState.on_file('dirstate')
178
self.assertEqual(expected_result[1], list(state._iter_entries()))
183
class TestTreeToDirState(TestCaseWithDirState):
185
def test_empty_to_dirstate(self):
186
"""We should be able to create a dirstate for an empty tree."""
187
# There are no files on disk and no parents
188
tree = self.make_branch_and_tree('tree')
189
expected_result = ([], [
190
(('', '', tree.path2id('')), # common details
191
[('d', '', 0, False, dirstate.DirState.NULLSTAT), # current tree
193
state = dirstate.DirState.from_tree(tree, 'dirstate')
195
self.check_state_with_reopen(expected_result, state)
197
def test_1_parents_empty_to_dirstate(self):
198
# create a parent by doing a commit
199
tree = self.make_branch_and_tree('tree')
200
rev_id = tree.commit('first post').encode('utf8')
201
root_stat_pack = dirstate.pack_stat(os.stat(tree.basedir))
202
expected_result = ([rev_id], [
203
(('', '', tree.path2id('')), # common details
204
[('d', '', 0, False, dirstate.DirState.NULLSTAT), # current tree
205
('d', '', 0, False, rev_id), # first parent details
207
state = dirstate.DirState.from_tree(tree, 'dirstate')
208
self.check_state_with_reopen(expected_result, state)
211
def test_2_parents_empty_to_dirstate(self):
212
# create a parent by doing a commit
213
tree = self.make_branch_and_tree('tree')
214
rev_id = tree.commit('first post')
215
tree2 = tree.bzrdir.sprout('tree2').open_workingtree()
216
rev_id2 = tree2.commit('second post', allow_pointless=True)
217
tree.merge_from_branch(tree2.branch)
218
expected_result = ([rev_id, rev_id2], [
219
(('', '', tree.path2id('')), # common details
220
[('d', '', 0, False, dirstate.DirState.NULLSTAT), # current tree
221
('d', '', 0, False, rev_id), # first parent details
222
('d', '', 0, False, rev_id2), # second parent details
224
state = dirstate.DirState.from_tree(tree, 'dirstate')
225
self.check_state_with_reopen(expected_result, state)
228
def test_empty_unknowns_are_ignored_to_dirstate(self):
229
"""We should be able to create a dirstate for an empty tree."""
230
# There are no files on disk and no parents
231
tree = self.make_branch_and_tree('tree')
232
self.build_tree(['tree/unknown'])
233
expected_result = ([], [
234
(('', '', tree.path2id('')), # common details
235
[('d', '', 0, False, dirstate.DirState.NULLSTAT), # current tree
237
state = dirstate.DirState.from_tree(tree, 'dirstate')
238
self.check_state_with_reopen(expected_result, state)
240
def get_tree_with_a_file(self):
241
tree = self.make_branch_and_tree('tree')
242
self.build_tree(['tree/a file'])
243
tree.add('a file', 'a file id')
246
def test_non_empty_no_parents_to_dirstate(self):
247
"""We should be able to create a dirstate for an empty tree."""
248
# There are files on disk and no parents
249
tree = self.get_tree_with_a_file()
250
expected_result = ([], [
251
(('', '', tree.path2id('')), # common details
252
[('d', '', 0, False, dirstate.DirState.NULLSTAT), # current tree
254
(('', 'a file', 'a file id'), # common
255
[('f', '', 0, False, dirstate.DirState.NULLSTAT), # current
258
state = dirstate.DirState.from_tree(tree, 'dirstate')
259
self.check_state_with_reopen(expected_result, state)
261
def test_1_parents_not_empty_to_dirstate(self):
262
# create a parent by doing a commit
263
tree = self.get_tree_with_a_file()
264
rev_id = tree.commit('first post').encode('utf8')
265
# change the current content to be different this will alter stat, sha
267
self.build_tree_contents([('tree/a file', 'new content\n')])
268
expected_result = ([rev_id], [
269
(('', '', tree.path2id('')), # common details
270
[('d', '', 0, False, dirstate.DirState.NULLSTAT), # current tree
271
('d', '', 0, False, rev_id), # first parent details
273
(('', 'a file', 'a file id'), # common
274
[('f', '', 0, False, dirstate.DirState.NULLSTAT), # current
275
('f', 'c3ed76e4bfd45ff1763ca206055bca8e9fc28aa8', 24, False,
276
rev_id), # first parent
279
state = dirstate.DirState.from_tree(tree, 'dirstate')
280
self.check_state_with_reopen(expected_result, state)
282
def test_2_parents_not_empty_to_dirstate(self):
283
# create a parent by doing a commit
284
tree = self.get_tree_with_a_file()
285
rev_id = tree.commit('first post').encode('utf8')
286
tree2 = tree.bzrdir.sprout('tree2').open_workingtree()
287
# change the current content to be different this will alter stat, sha
289
self.build_tree_contents([('tree2/a file', 'merge content\n')])
290
rev_id2 = tree2.commit('second post').encode('utf8')
291
tree.merge_from_branch(tree2.branch)
292
# change the current content to be different this will alter stat, sha
293
# and length again, giving us three distinct values:
294
self.build_tree_contents([('tree/a file', 'new content\n')])
295
expected_result = ([rev_id, rev_id2], [
296
(('', '', tree.path2id('')), # common details
297
[('d', '', 0, False, dirstate.DirState.NULLSTAT), # current tree
298
('d', '', 0, False, rev_id), # first parent details
299
('d', '', 0, False, rev_id2), # second parent details
301
(('', 'a file', 'a file id'), # common
302
[('f', '', 0, False, dirstate.DirState.NULLSTAT), # current
303
('f', 'c3ed76e4bfd45ff1763ca206055bca8e9fc28aa8', 24, False,
304
rev_id), # first parent
305
('f', '314d796174c9412647c3ce07dfb5d36a94e72958', 14, False,
306
rev_id2), # second parent
309
state = dirstate.DirState.from_tree(tree, 'dirstate')
310
self.check_state_with_reopen(expected_result, state)
312
def test_colliding_fileids(self):
313
# test insertion of parents creating several entries at the same path.
314
# we used to have a bug where they could cause the dirstate to break
315
# its ordering invariants.
316
# create some trees to test from
319
tree = self.make_branch_and_tree('tree%d' % i)
320
self.build_tree(['tree%d/name' % i,])
321
tree.add(['name'], ['file-id%d' % i])
322
revision_id = 'revid-%d' % i
323
tree.commit('message', rev_id=revision_id)
324
parents.append((revision_id,
325
tree.branch.repository.revision_tree(revision_id)))
326
# now fold these trees into a dirstate
327
state = dirstate.DirState.initialize('dirstate')
329
state.set_parent_trees(parents, [])
335
class TestDirStateOnFile(TestCaseWithDirState):
337
def test_construct_with_path(self):
338
tree = self.make_branch_and_tree('tree')
339
state = dirstate.DirState.from_tree(tree, 'dirstate.from_tree')
340
# we want to be able to get the lines of the dirstate that we will
342
lines = state.get_lines()
344
self.build_tree_contents([('dirstate', ''.join(lines))])
346
# no parents, default tree content
347
expected_result = ([], [
348
(('', '', tree.path2id('')), # common details
349
# current tree details, but new from_tree skips statting, it
350
# uses set_state_from_inventory, and thus depends on the
352
[('d', '', 0, False, dirstate.DirState.NULLSTAT),
355
state = dirstate.DirState.on_file('dirstate')
356
state.lock_write() # check_state_with_reopen will save() and unlock it
357
self.check_state_with_reopen(expected_result, state)
359
def test_can_save_clean_on_file(self):
360
tree = self.make_branch_and_tree('tree')
361
state = dirstate.DirState.from_tree(tree, 'dirstate')
363
# doing a save should work here as there have been no changes.
365
# TODO: stat it and check it hasn't changed; may require waiting
366
# for the state accuracy window.
370
def test_can_save_in_read_lock(self):
371
self.build_tree(['a-file'])
372
state = dirstate.DirState.initialize('dirstate')
374
# No stat and no sha1 sum.
375
state.add('a-file', 'a-file-id', 'file', None, '')
380
# Now open in readonly mode
381
state = dirstate.DirState.on_file('dirstate')
384
entry = state._get_entry(0, path_utf8='a-file')
385
# The current sha1 sum should be empty
386
self.assertEqual('', entry[1][0][1])
387
# We should have a real entry.
388
self.assertNotEqual((None, None), entry)
389
sha1sum = state.update_entry(entry, 'a-file', os.lstat('a-file'))
390
# We should have gotten a real sha1
391
self.assertEqual('ecc5374e9ed82ad3ea3b4d452ea995a5fd3e70e3',
394
# The dirblock has been updated
395
self.assertEqual(sha1sum, entry[1][0][1])
396
self.assertEqual(dirstate.DirState.IN_MEMORY_MODIFIED,
397
state._dirblock_state)
400
# Now, since we are the only one holding a lock, we should be able
401
# to save and have it written to disk
406
# Re-open the file, and ensure that the state has been updated.
407
state = dirstate.DirState.on_file('dirstate')
410
entry = state._get_entry(0, path_utf8='a-file')
411
self.assertEqual(sha1sum, entry[1][0][1])
415
def test_save_fails_quietly_if_locked(self):
416
"""If dirstate is locked, save will fail without complaining."""
417
self.build_tree(['a-file'])
418
state = dirstate.DirState.initialize('dirstate')
420
# No stat and no sha1 sum.
421
state.add('a-file', 'a-file-id', 'file', None, '')
426
state = dirstate.DirState.on_file('dirstate')
429
entry = state._get_entry(0, path_utf8='a-file')
430
sha1sum = state.update_entry(entry, 'a-file', os.lstat('a-file'))
431
# We should have gotten a real sha1
432
self.assertEqual('ecc5374e9ed82ad3ea3b4d452ea995a5fd3e70e3',
434
self.assertEqual(dirstate.DirState.IN_MEMORY_MODIFIED,
435
state._dirblock_state)
437
# Now, before we try to save, grab another dirstate, and take out a
439
# TODO: jam 20070315 Ideally this would be locked by another
440
# process. To make sure the file is really OS locked.
441
state2 = dirstate.DirState.on_file('dirstate')
444
# This won't actually write anything, because it couldn't grab
445
# a write lock. But it shouldn't raise an error, either.
446
# TODO: jam 20070315 We should probably distinguish between
447
# being dirty because of 'update_entry'. And dirty
448
# because of real modification. So that save() *does*
449
# raise a real error if it fails when we have real
457
# The file on disk should not be modified.
458
state = dirstate.DirState.on_file('dirstate')
461
entry = state._get_entry(0, path_utf8='a-file')
462
self.assertEqual('', entry[1][0][1])
467
class TestDirStateInitialize(TestCaseWithDirState):
469
def test_initialize(self):
470
expected_result = ([], [
471
(('', '', 'TREE_ROOT'), # common details
472
[('d', '', 0, False, dirstate.DirState.NULLSTAT), # current tree
475
state = dirstate.DirState.initialize('dirstate')
477
self.assertIsInstance(state, dirstate.DirState)
478
lines = state.get_lines()
479
self.assertFileEqual(''.join(state.get_lines()),
481
self.check_state_with_reopen(expected_result, state)
487
class TestDirStateManipulations(TestCaseWithDirState):
489
def test_set_state_from_inventory_no_content_no_parents(self):
490
# setting the current inventory is a slow but important api to support.
491
tree1 = self.make_branch_and_memory_tree('tree1')
495
revid1 = tree1.commit('foo').encode('utf8')
496
root_id = tree1.inventory.root.file_id
497
inv = tree1.inventory
500
expected_result = [], [
501
(('', '', root_id), [
502
('d', '', 0, False, dirstate.DirState.NULLSTAT)])]
503
state = dirstate.DirState.initialize('dirstate')
505
state.set_state_from_inventory(inv)
506
self.assertEqual(dirstate.DirState.IN_MEMORY_UNMODIFIED,
508
self.assertEqual(dirstate.DirState.IN_MEMORY_MODIFIED,
509
state._dirblock_state)
514
# This will unlock it
515
self.check_state_with_reopen(expected_result, state)
517
def test_set_path_id_no_parents(self):
518
"""The id of a path can be changed trivally with no parents."""
519
state = dirstate.DirState.initialize('dirstate')
521
# check precondition to be sure the state does change appropriately.
523
[(('', '', 'TREE_ROOT'), [('d', '', 0, False,
524
'xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx')])],
525
list(state._iter_entries()))
526
state.set_path_id('', 'foobarbaz')
528
(('', '', 'foobarbaz'), [('d', '', 0, False,
529
'xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx')])]
530
self.assertEqual(expected_rows, list(state._iter_entries()))
531
# should work across save too
535
state = dirstate.DirState.on_file('dirstate')
539
self.assertEqual(expected_rows, list(state._iter_entries()))
543
def test_set_path_id_with_parents(self):
544
"""Set the root file id in a dirstate with parents"""
545
mt = self.make_branch_and_tree('mt')
546
# in case the default tree format uses a different root id
547
mt.set_root_id('TREE_ROOT')
548
mt.commit('foo', rev_id='parent-revid')
549
rt = mt.branch.repository.revision_tree('parent-revid')
550
state = dirstate.DirState.initialize('dirstate')
553
state.set_parent_trees([('parent-revid', rt)], ghosts=[])
554
state.set_path_id('', 'foobarbaz')
556
# now see that it is what we expected
558
(('', '', 'TREE_ROOT'),
559
[('a', '', 0, False, ''),
560
('d', '', 0, False, 'parent-revid'),
562
(('', '', 'foobarbaz'),
563
[('d', '', 0, False, ''),
564
('a', '', 0, False, ''),
568
self.assertEqual(expected_rows, list(state._iter_entries()))
569
# should work across save too
573
# now flush & check we get the same
574
state = dirstate.DirState.on_file('dirstate')
578
self.assertEqual(expected_rows, list(state._iter_entries()))
581
# now change within an existing file-backed state
585
state.set_path_id('', 'tree-root-2')
591
def test_set_parent_trees_no_content(self):
592
# set_parent_trees is a slow but important api to support.
593
tree1 = self.make_branch_and_memory_tree('tree1')
597
revid1 = tree1.commit('foo')
600
branch2 = tree1.branch.bzrdir.clone('tree2').open_branch()
601
tree2 = MemoryTree.create_on_branch(branch2)
604
revid2 = tree2.commit('foo')
605
root_id = tree2.inventory.root.file_id
608
state = dirstate.DirState.initialize('dirstate')
610
state.set_path_id('', root_id)
611
state.set_parent_trees(
612
((revid1, tree1.branch.repository.revision_tree(revid1)),
613
(revid2, tree2.branch.repository.revision_tree(revid2)),
614
('ghost-rev', None)),
616
# check we can reopen and use the dirstate after setting parent
623
state = dirstate.DirState.on_file('dirstate')
626
self.assertEqual([revid1, revid2, 'ghost-rev'],
627
state.get_parent_ids())
628
# iterating the entire state ensures that the state is parsable.
629
list(state._iter_entries())
630
# be sure that it sets not appends - change it
631
state.set_parent_trees(
632
((revid1, tree1.branch.repository.revision_tree(revid1)),
633
('ghost-rev', None)),
635
# and now put it back.
636
state.set_parent_trees(
637
((revid1, tree1.branch.repository.revision_tree(revid1)),
638
(revid2, tree2.branch.repository.revision_tree(revid2)),
639
('ghost-rev', tree2.branch.repository.revision_tree(None))),
641
self.assertEqual([revid1, revid2, 'ghost-rev'],
642
state.get_parent_ids())
643
# the ghost should be recorded as such by set_parent_trees.
644
self.assertEqual(['ghost-rev'], state.get_ghosts())
646
[(('', '', root_id), [
647
('d', '', 0, False, dirstate.DirState.NULLSTAT),
648
('d', '', 0, False, revid1),
649
('d', '', 0, False, revid2)
651
list(state._iter_entries()))
655
def test_set_parent_trees_file_missing_from_tree(self):
656
# Adding a parent tree may reference files not in the current state.
657
# they should get listed just once by id, even if they are in two
659
# set_parent_trees is a slow but important api to support.
660
tree1 = self.make_branch_and_memory_tree('tree1')
664
tree1.add(['a file'], ['file-id'], ['file'])
665
tree1.put_file_bytes_non_atomic('file-id', 'file-content')
666
revid1 = tree1.commit('foo')
669
branch2 = tree1.branch.bzrdir.clone('tree2').open_branch()
670
tree2 = MemoryTree.create_on_branch(branch2)
673
tree2.put_file_bytes_non_atomic('file-id', 'new file-content')
674
revid2 = tree2.commit('foo')
675
root_id = tree2.inventory.root.file_id
678
# check the layout in memory
679
expected_result = [revid1.encode('utf8'), revid2.encode('utf8')], [
680
(('', '', root_id), [
681
('d', '', 0, False, dirstate.DirState.NULLSTAT),
682
('d', '', 0, False, revid1.encode('utf8')),
683
('d', '', 0, False, revid2.encode('utf8'))
685
(('', 'a file', 'file-id'), [
686
('a', '', 0, False, ''),
687
('f', '2439573625385400f2a669657a7db6ae7515d371', 12, False,
688
revid1.encode('utf8')),
689
('f', '542e57dc1cda4af37cb8e55ec07ce60364bb3c7d', 16, False,
690
revid2.encode('utf8'))
693
state = dirstate.DirState.initialize('dirstate')
695
state.set_path_id('', root_id)
696
state.set_parent_trees(
697
((revid1, tree1.branch.repository.revision_tree(revid1)),
698
(revid2, tree2.branch.repository.revision_tree(revid2)),
704
# check_state_with_reopen will unlock
705
self.check_state_with_reopen(expected_result, state)
707
### add a path via _set_data - so we dont need delta work, just
708
# raw data in, and ensure that it comes out via get_lines happily.
710
def test_add_path_to_root_no_parents_all_data(self):
711
# The most trivial addition of a path is when there are no parents and
712
# its in the root and all data about the file is supplied
713
self.build_tree(['a file'])
714
stat = os.lstat('a file')
715
# the 1*20 is the sha1 pretend value.
716
state = dirstate.DirState.initialize('dirstate')
718
(('', '', 'TREE_ROOT'), [
719
('d', '', 0, False, dirstate.DirState.NULLSTAT), # current tree
721
(('', 'a file', 'a file id'), [
722
('f', '1'*20, 19, False, dirstate.pack_stat(stat)), # current tree
726
state.add('a file', 'a file id', 'file', stat, '1'*20)
727
# having added it, it should be in the output of iter_entries.
728
self.assertEqual(expected_entries, list(state._iter_entries()))
729
# saving and reloading should not affect this.
733
state = dirstate.DirState.on_file('dirstate')
736
self.assertEqual(expected_entries, list(state._iter_entries()))
740
def test_add_path_to_unversioned_directory(self):
741
"""Adding a path to an unversioned directory should error.
743
This is a duplicate of TestWorkingTree.test_add_in_unversioned,
744
once dirstate is stable and if it is merged with WorkingTree3, consider
745
removing this copy of the test.
747
self.build_tree(['unversioned/', 'unversioned/a file'])
748
state = dirstate.DirState.initialize('dirstate')
750
self.assertRaises(errors.NotVersionedError, state.add,
751
'unversioned/a file', 'a file id', 'file', None, None)
755
def test_add_directory_to_root_no_parents_all_data(self):
756
# The most trivial addition of a dir is when there are no parents and
757
# its in the root and all data about the file is supplied
758
self.build_tree(['a dir/'])
759
stat = os.lstat('a dir')
761
(('', '', 'TREE_ROOT'), [
762
('d', '', 0, False, dirstate.DirState.NULLSTAT), # current tree
764
(('', 'a dir', 'a dir id'), [
765
('d', '', 0, False, dirstate.pack_stat(stat)), # current tree
768
state = dirstate.DirState.initialize('dirstate')
770
state.add('a dir', 'a dir id', 'directory', stat, None)
771
# having added it, it should be in the output of iter_entries.
772
self.assertEqual(expected_entries, list(state._iter_entries()))
773
# saving and reloading should not affect this.
777
state = dirstate.DirState.on_file('dirstate')
781
self.assertEqual(expected_entries, list(state._iter_entries()))
785
def test_add_symlink_to_root_no_parents_all_data(self):
786
# The most trivial addition of a symlink when there are no parents and
787
# its in the root and all data about the file is supplied
788
# bzr doesn't support fake symlinks on windows, yet.
789
if not has_symlinks():
790
raise TestSkipped("No symlink support")
791
os.symlink('target', 'a link')
792
stat = os.lstat('a link')
794
(('', '', 'TREE_ROOT'), [
795
('d', '', 0, False, dirstate.DirState.NULLSTAT), # current tree
797
(('', 'a link', 'a link id'), [
798
('l', 'target', 6, False, dirstate.pack_stat(stat)), # current tree
801
state = dirstate.DirState.initialize('dirstate')
803
state.add('a link', 'a link id', 'symlink', stat, 'target')
804
# having added it, it should be in the output of iter_entries.
805
self.assertEqual(expected_entries, list(state._iter_entries()))
806
# saving and reloading should not affect this.
810
state = dirstate.DirState.on_file('dirstate')
813
self.assertEqual(expected_entries, list(state._iter_entries()))
817
def test_add_directory_and_child_no_parents_all_data(self):
818
# after adding a directory, we should be able to add children to it.
819
self.build_tree(['a dir/', 'a dir/a file'])
820
dirstat = os.lstat('a dir')
821
filestat = os.lstat('a dir/a file')
823
(('', '', 'TREE_ROOT'), [
824
('d', '', 0, False, dirstate.DirState.NULLSTAT), # current tree
826
(('', 'a dir', 'a dir id'), [
827
('d', '', 0, False, dirstate.pack_stat(dirstat)), # current tree
829
(('a dir', 'a file', 'a file id'), [
830
('f', '1'*20, 25, False,
831
dirstate.pack_stat(filestat)), # current tree details
834
state = dirstate.DirState.initialize('dirstate')
836
state.add('a dir', 'a dir id', 'directory', dirstat, None)
837
state.add('a dir/a file', 'a file id', 'file', filestat, '1'*20)
838
# added it, it should be in the output of iter_entries.
839
self.assertEqual(expected_entries, list(state._iter_entries()))
840
# saving and reloading should not affect this.
844
state = dirstate.DirState.on_file('dirstate')
847
self.assertEqual(expected_entries, list(state._iter_entries()))
851
def test_add_tree_reference(self):
852
# make a dirstate and add a tree reference
853
state = dirstate.DirState.initialize('dirstate')
855
('', 'subdir', 'subdir-id'),
856
[('t', 'subtree-123123', 0, False,
857
'xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx')],
860
state.add('subdir', 'subdir-id', 'tree-reference', None, 'subtree-123123')
861
entry = state._get_entry(0, 'subdir-id', 'subdir')
862
self.assertEqual(entry, expected_entry)
867
# now check we can read it back
871
entry2 = state._get_entry(0, 'subdir-id', 'subdir')
872
self.assertEqual(entry, entry2)
873
self.assertEqual(entry, expected_entry)
874
# and lookup by id should work too
875
entry2 = state._get_entry(0, fileid_utf8='subdir-id')
876
self.assertEqual(entry, expected_entry)
880
def test_add_forbidden_names(self):
881
state = dirstate.DirState.initialize('dirstate')
882
self.addCleanup(state.unlock)
883
self.assertRaises(errors.BzrError,
884
state.add, '.', 'ass-id', 'directory', None, None)
885
self.assertRaises(errors.BzrError,
886
state.add, '..', 'ass-id', 'directory', None, None)
889
class TestGetLines(TestCaseWithDirState):
891
def test_get_line_with_2_rows(self):
892
state = self.create_dirstate_with_root_and_subdir()
894
self.assertEqual(['#bazaar dirstate flat format 3\n',
899
'\x00\x00a-root-value\x00'
900
'd\x00\x000\x00n\x00AAAAREUHaIpFB2iKAAADAQAtkqUAAIGk\x00\n\x00'
901
'\x00subdir\x00subdir-id\x00'
902
'd\x00\x000\x00n\x00AAAAREUHaIpFB2iKAAADAQAtkqUAAIGk\x00\n\x00'
903
], state.get_lines())
907
def test_entry_to_line(self):
908
state = self.create_dirstate_with_root()
911
'\x00\x00a-root-value\x00d\x00\x000\x00n'
912
'\x00AAAAREUHaIpFB2iKAAADAQAtkqUAAIGk',
913
state._entry_to_line(state._dirblocks[0][1][0]))
917
def test_entry_to_line_with_parent(self):
918
packed_stat = 'AAAAREUHaIpFB2iKAAADAQAtkqUAAIGk'
919
root_entry = ('', '', 'a-root-value'), [
920
('d', '', 0, False, packed_stat), # current tree details
921
# first: a pointer to the current location
922
('a', 'dirname/basename', 0, False, ''),
924
state = dirstate.DirState.initialize('dirstate')
927
'\x00\x00a-root-value\x00'
928
'd\x00\x000\x00n\x00AAAAREUHaIpFB2iKAAADAQAtkqUAAIGk\x00'
929
'a\x00dirname/basename\x000\x00n\x00',
930
state._entry_to_line(root_entry))
934
def test_entry_to_line_with_two_parents_at_different_paths(self):
935
# / in the tree, at / in one parent and /dirname/basename in the other.
936
packed_stat = 'AAAAREUHaIpFB2iKAAADAQAtkqUAAIGk'
937
root_entry = ('', '', 'a-root-value'), [
938
('d', '', 0, False, packed_stat), # current tree details
939
('d', '', 0, False, 'rev_id'), # first parent details
940
# second: a pointer to the current location
941
('a', 'dirname/basename', 0, False, ''),
943
state = dirstate.DirState.initialize('dirstate')
946
'\x00\x00a-root-value\x00'
947
'd\x00\x000\x00n\x00AAAAREUHaIpFB2iKAAADAQAtkqUAAIGk\x00'
948
'd\x00\x000\x00n\x00rev_id\x00'
949
'a\x00dirname/basename\x000\x00n\x00',
950
state._entry_to_line(root_entry))
954
def test_iter_entries(self):
955
# we should be able to iterate the dirstate entries from end to end
956
# this is for get_lines to be easy to read.
957
packed_stat = 'AAAAREUHaIpFB2iKAAADAQAtkqUAAIGk'
959
root_entries = [(('', '', 'a-root-value'), [
960
('d', '', 0, False, packed_stat), # current tree details
962
dirblocks.append(('', root_entries))
963
# add two files in the root
964
subdir_entry = ('', 'subdir', 'subdir-id'), [
965
('d', '', 0, False, packed_stat), # current tree details
967
afile_entry = ('', 'afile', 'afile-id'), [
968
('f', 'sha1value', 34, False, packed_stat), # current tree details
970
dirblocks.append(('', [subdir_entry, afile_entry]))
972
file_entry2 = ('subdir', '2file', '2file-id'), [
973
('f', 'sha1value', 23, False, packed_stat), # current tree details
975
dirblocks.append(('subdir', [file_entry2]))
976
state = dirstate.DirState.initialize('dirstate')
978
state._set_data([], dirblocks)
979
expected_entries = [root_entries[0], subdir_entry, afile_entry,
981
self.assertEqual(expected_entries, list(state._iter_entries()))
986
class TestGetBlockRowIndex(TestCaseWithDirState):
988
def assertBlockRowIndexEqual(self, block_index, row_index, dir_present,
989
file_present, state, dirname, basename, tree_index):
990
self.assertEqual((block_index, row_index, dir_present, file_present),
991
state._get_block_entry_index(dirname, basename, tree_index))
993
block = state._dirblocks[block_index]
994
self.assertEqual(dirname, block[0])
995
if dir_present and file_present:
996
row = state._dirblocks[block_index][1][row_index]
997
self.assertEqual(dirname, row[0][0])
998
self.assertEqual(basename, row[0][1])
1000
def test_simple_structure(self):
1001
state = self.create_dirstate_with_root_and_subdir()
1002
self.addCleanup(state.unlock)
1003
self.assertBlockRowIndexEqual(1, 0, True, True, state, '', 'subdir', 0)
1004
self.assertBlockRowIndexEqual(1, 0, True, False, state, '', 'bdir', 0)
1005
self.assertBlockRowIndexEqual(1, 1, True, False, state, '', 'zdir', 0)
1006
self.assertBlockRowIndexEqual(2, 0, False, False, state, 'a', 'foo', 0)
1007
self.assertBlockRowIndexEqual(2, 0, False, False, state,
1010
def test_complex_structure_exists(self):
1011
state = self.create_complex_dirstate()
1012
self.addCleanup(state.unlock)
1013
# Make sure we can find everything that exists
1014
self.assertBlockRowIndexEqual(0, 0, True, True, state, '', '', 0)
1015
self.assertBlockRowIndexEqual(1, 0, True, True, state, '', 'a', 0)
1016
self.assertBlockRowIndexEqual(1, 1, True, True, state, '', 'b', 0)
1017
self.assertBlockRowIndexEqual(1, 2, True, True, state, '', 'c', 0)
1018
self.assertBlockRowIndexEqual(1, 3, True, True, state, '', 'd', 0)
1019
self.assertBlockRowIndexEqual(2, 0, True, True, state, 'a', 'e', 0)
1020
self.assertBlockRowIndexEqual(2, 1, True, True, state, 'a', 'f', 0)
1021
self.assertBlockRowIndexEqual(3, 0, True, True, state, 'b', 'g', 0)
1022
self.assertBlockRowIndexEqual(3, 1, True, True, state,
1023
'b', 'h\xc3\xa5', 0)
1025
def test_complex_structure_missing(self):
1026
state = self.create_complex_dirstate()
1027
self.addCleanup(state.unlock)
1028
# Make sure things would be inserted in the right locations
1029
# '_' comes before 'a'
1030
self.assertBlockRowIndexEqual(0, 0, True, True, state, '', '', 0)
1031
self.assertBlockRowIndexEqual(1, 0, True, False, state, '', '_', 0)
1032
self.assertBlockRowIndexEqual(1, 1, True, False, state, '', 'aa', 0)
1033
self.assertBlockRowIndexEqual(1, 4, True, False, state,
1035
self.assertBlockRowIndexEqual(2, 0, False, False, state, '_', 'a', 0)
1036
self.assertBlockRowIndexEqual(3, 0, False, False, state, 'aa', 'a', 0)
1037
self.assertBlockRowIndexEqual(4, 0, False, False, state, 'bb', 'a', 0)
1038
# This would be inserted between a/ and b/
1039
self.assertBlockRowIndexEqual(3, 0, False, False, state, 'a/e', 'a', 0)
1041
self.assertBlockRowIndexEqual(4, 0, False, False, state, 'e', 'a', 0)
1044
class TestGetEntry(TestCaseWithDirState):
1046
def assertEntryEqual(self, dirname, basename, file_id, state, path, index):
1047
"""Check that the right entry is returned for a request to getEntry."""
1048
entry = state._get_entry(index, path_utf8=path)
1050
self.assertEqual((None, None), entry)
1053
self.assertEqual((dirname, basename, file_id), cur[:3])
1055
def test_simple_structure(self):
1056
state = self.create_dirstate_with_root_and_subdir()
1057
self.addCleanup(state.unlock)
1058
self.assertEntryEqual('', '', 'a-root-value', state, '', 0)
1059
self.assertEntryEqual('', 'subdir', 'subdir-id', state, 'subdir', 0)
1060
self.assertEntryEqual(None, None, None, state, 'missing', 0)
1061
self.assertEntryEqual(None, None, None, state, 'missing/foo', 0)
1062
self.assertEntryEqual(None, None, None, state, 'subdir/foo', 0)
1064
def test_complex_structure_exists(self):
1065
state = self.create_complex_dirstate()
1066
self.addCleanup(state.unlock)
1067
self.assertEntryEqual('', '', 'a-root-value', state, '', 0)
1068
self.assertEntryEqual('', 'a', 'a-dir', state, 'a', 0)
1069
self.assertEntryEqual('', 'b', 'b-dir', state, 'b', 0)
1070
self.assertEntryEqual('', 'c', 'c-file', state, 'c', 0)
1071
self.assertEntryEqual('', 'd', 'd-file', state, 'd', 0)
1072
self.assertEntryEqual('a', 'e', 'e-dir', state, 'a/e', 0)
1073
self.assertEntryEqual('a', 'f', 'f-file', state, 'a/f', 0)
1074
self.assertEntryEqual('b', 'g', 'g-file', state, 'b/g', 0)
1075
self.assertEntryEqual('b', 'h\xc3\xa5', 'h-\xc3\xa5-file', state,
1078
def test_complex_structure_missing(self):
1079
state = self.create_complex_dirstate()
1080
self.addCleanup(state.unlock)
1081
self.assertEntryEqual(None, None, None, state, '_', 0)
1082
self.assertEntryEqual(None, None, None, state, '_\xc3\xa5', 0)
1083
self.assertEntryEqual(None, None, None, state, 'a/b', 0)
1084
self.assertEntryEqual(None, None, None, state, 'c/d', 0)
1086
def test_get_entry_uninitialized(self):
1087
"""Calling get_entry will load data if it needs to"""
1088
state = self.create_dirstate_with_root()
1094
state = dirstate.DirState.on_file('dirstate')
1097
self.assertEqual(dirstate.DirState.NOT_IN_MEMORY,
1098
state._header_state)
1099
self.assertEqual(dirstate.DirState.NOT_IN_MEMORY,
1100
state._dirblock_state)
1101
self.assertEntryEqual('', '', 'a-root-value', state, '', 0)
1106
class TestDirstateSortOrder(TestCaseWithTransport):
1107
"""Test that DirState adds entries in the right order."""
1109
def test_add_sorting(self):
1110
"""Add entries in lexicographical order, we get path sorted order.
1112
This tests it to a depth of 4, to make sure we don't just get it right
1113
at a single depth. 'a/a' should come before 'a-a', even though it
1114
doesn't lexicographically.
1116
dirs = ['a', 'a/a', 'a/a/a', 'a/a/a/a',
1117
'a-a', 'a/a-a', 'a/a/a-a', 'a/a/a/a-a',
1120
state = dirstate.DirState.initialize('dirstate')
1121
self.addCleanup(state.unlock)
1123
fake_stat = os.stat('dirstate')
1125
d_id = d.replace('/', '_')+'-id'
1126
file_path = d + '/f'
1127
file_id = file_path.replace('/', '_')+'-id'
1128
state.add(d, d_id, 'directory', fake_stat, null_sha)
1129
state.add(file_path, file_id, 'file', fake_stat, null_sha)
1131
expected = ['', '', 'a',
1132
'a/a', 'a/a/a', 'a/a/a/a',
1133
'a/a/a/a-a', 'a/a/a-a', 'a/a-a', 'a-a',
1135
split = lambda p:p.split('/')
1136
self.assertEqual(sorted(expected, key=split), expected)
1137
dirblock_names = [d[0] for d in state._dirblocks]
1138
self.assertEqual(expected, dirblock_names)
1140
def test_set_parent_trees_correct_order(self):
1141
"""After calling set_parent_trees() we should maintain the order."""
1142
dirs = ['a', 'a-a', 'a/a']
1144
state = dirstate.DirState.initialize('dirstate')
1145
self.addCleanup(state.unlock)
1147
fake_stat = os.stat('dirstate')
1149
d_id = d.replace('/', '_')+'-id'
1150
file_path = d + '/f'
1151
file_id = file_path.replace('/', '_')+'-id'
1152
state.add(d, d_id, 'directory', fake_stat, null_sha)
1153
state.add(file_path, file_id, 'file', fake_stat, null_sha)
1155
expected = ['', '', 'a', 'a/a', 'a-a']
1156
dirblock_names = [d[0] for d in state._dirblocks]
1157
self.assertEqual(expected, dirblock_names)
1159
# *really* cheesy way to just get an empty tree
1160
repo = self.make_repository('repo')
1161
empty_tree = repo.revision_tree(None)
1162
state.set_parent_trees([('null:', empty_tree)], [])
1164
dirblock_names = [d[0] for d in state._dirblocks]
1165
self.assertEqual(expected, dirblock_names)
1168
class InstrumentedDirState(dirstate.DirState):
1169
"""An DirState with instrumented sha1 functionality."""
1171
def __init__(self, path):
1172
super(InstrumentedDirState, self).__init__(path)
1173
self._time_offset = 0
1176
def _sha_cutoff_time(self):
1177
timestamp = super(InstrumentedDirState, self)._sha_cutoff_time()
1178
self._cutoff_time = timestamp + self._time_offset
1180
def _sha1_file(self, abspath, entry):
1181
self._log.append(('sha1', abspath))
1182
return super(InstrumentedDirState, self)._sha1_file(abspath, entry)
1184
def _read_link(self, abspath, old_link):
1185
self._log.append(('read_link', abspath, old_link))
1186
return super(InstrumentedDirState, self)._read_link(abspath, old_link)
1188
def _lstat(self, abspath, entry):
1189
self._log.append(('lstat', abspath))
1190
return super(InstrumentedDirState, self)._lstat(abspath, entry)
1192
def _is_executable(self, mode, old_executable):
1193
self._log.append(('is_exec', mode, old_executable))
1194
return super(InstrumentedDirState, self)._is_executable(mode,
1197
def adjust_time(self, secs):
1198
"""Move the clock forward or back.
1200
:param secs: The amount to adjust the clock by. Positive values make it
1201
seem as if we are in the future, negative values make it seem like we
1204
self._time_offset += secs
1205
self._cutoff_time = None
1208
class _FakeStat(object):
1209
"""A class with the same attributes as a real stat result."""
1211
def __init__(self, size, mtime, ctime, dev, ino, mode):
1213
self.st_mtime = mtime
1214
self.st_ctime = ctime
1220
class TestUpdateEntry(TestCaseWithDirState):
1221
"""Test the DirState.update_entry functions"""
1223
def get_state_with_a(self):
1224
"""Create a DirState tracking a single object named 'a'"""
1225
state = InstrumentedDirState.initialize('dirstate')
1226
self.addCleanup(state.unlock)
1227
state.add('a', 'a-id', 'file', None, '')
1228
entry = state._get_entry(0, path_utf8='a')
1231
def test_update_entry(self):
1232
state, entry = self.get_state_with_a()
1233
self.build_tree(['a'])
1234
# Add one where we don't provide the stat or sha already
1235
self.assertEqual(('', 'a', 'a-id'), entry[0])
1236
self.assertEqual([('f', '', 0, False, dirstate.DirState.NULLSTAT)],
1238
# Flush the buffers to disk
1240
self.assertEqual(dirstate.DirState.IN_MEMORY_UNMODIFIED,
1241
state._dirblock_state)
1243
stat_value = os.lstat('a')
1244
packed_stat = dirstate.pack_stat(stat_value)
1245
link_or_sha1 = state.update_entry(entry, abspath='a',
1246
stat_value=stat_value)
1247
self.assertEqual('b50e5406bb5e153ebbeb20268fcf37c87e1ecfb6',
1250
# The dirblock entry should be updated with the new info
1251
self.assertEqual([('f', link_or_sha1, 14, False, packed_stat)],
1253
self.assertEqual(dirstate.DirState.IN_MEMORY_MODIFIED,
1254
state._dirblock_state)
1255
mode = stat_value.st_mode
1256
self.assertEqual([('sha1', 'a'), ('is_exec', mode, False)], state._log)
1259
self.assertEqual(dirstate.DirState.IN_MEMORY_UNMODIFIED,
1260
state._dirblock_state)
1262
# If we do it again right away, we don't know if the file has changed
1263
# so we will re-read the file. Roll the clock back so the file is
1264
# guaranteed to look too new.
1265
state.adjust_time(-10)
1267
link_or_sha1 = state.update_entry(entry, abspath='a',
1268
stat_value=stat_value)
1269
self.assertEqual([('sha1', 'a'), ('is_exec', mode, False),
1270
('sha1', 'a'), ('is_exec', mode, False),
1272
self.assertEqual('b50e5406bb5e153ebbeb20268fcf37c87e1ecfb6',
1274
self.assertEqual(dirstate.DirState.IN_MEMORY_MODIFIED,
1275
state._dirblock_state)
1278
# However, if we move the clock forward so the file is considered
1279
# "stable", it should just returned the cached value.
1280
state.adjust_time(20)
1281
link_or_sha1 = state.update_entry(entry, abspath='a',
1282
stat_value=stat_value)
1283
self.assertEqual('b50e5406bb5e153ebbeb20268fcf37c87e1ecfb6',
1285
self.assertEqual([('sha1', 'a'), ('is_exec', mode, False),
1286
('sha1', 'a'), ('is_exec', mode, False),
1289
def test_update_entry_no_stat_value(self):
1290
"""Passing the stat_value is optional."""
1291
state, entry = self.get_state_with_a()
1292
state.adjust_time(-10) # Make sure the file looks new
1293
self.build_tree(['a'])
1294
# Add one where we don't provide the stat or sha already
1295
link_or_sha1 = state.update_entry(entry, abspath='a')
1296
self.assertEqual('b50e5406bb5e153ebbeb20268fcf37c87e1ecfb6',
1298
stat_value = os.lstat('a')
1299
self.assertEqual([('lstat', 'a'), ('sha1', 'a'),
1300
('is_exec', stat_value.st_mode, False),
1303
def test_update_entry_symlink(self):
1304
"""Update entry should read symlinks."""
1305
if not osutils.has_symlinks():
1306
# PlatformDeficiency / TestSkipped
1307
raise TestSkipped("No symlink support")
1308
state, entry = self.get_state_with_a()
1310
self.assertEqual(dirstate.DirState.IN_MEMORY_UNMODIFIED,
1311
state._dirblock_state)
1312
os.symlink('target', 'a')
1314
state.adjust_time(-10) # Make the symlink look new
1315
stat_value = os.lstat('a')
1316
packed_stat = dirstate.pack_stat(stat_value)
1317
link_or_sha1 = state.update_entry(entry, abspath='a',
1318
stat_value=stat_value)
1319
self.assertEqual('target', link_or_sha1)
1320
self.assertEqual([('read_link', 'a', '')], state._log)
1321
# Dirblock is updated
1322
self.assertEqual([('l', link_or_sha1, 6, False, packed_stat)],
1324
self.assertEqual(dirstate.DirState.IN_MEMORY_MODIFIED,
1325
state._dirblock_state)
1327
# Because the stat_value looks new, we should re-read the target
1328
link_or_sha1 = state.update_entry(entry, abspath='a',
1329
stat_value=stat_value)
1330
self.assertEqual('target', link_or_sha1)
1331
self.assertEqual([('read_link', 'a', ''),
1332
('read_link', 'a', 'target'),
1334
state.adjust_time(+20) # Skip into the future, all files look old
1335
link_or_sha1 = state.update_entry(entry, abspath='a',
1336
stat_value=stat_value)
1337
self.assertEqual('target', link_or_sha1)
1338
# There should not be a new read_link call.
1339
# (this is a weak assertion, because read_link is fairly inexpensive,
1340
# versus the number of symlinks that we would have)
1341
self.assertEqual([('read_link', 'a', ''),
1342
('read_link', 'a', 'target'),
1345
def test_update_entry_dir(self):
1346
state, entry = self.get_state_with_a()
1347
self.build_tree(['a/'])
1348
self.assertIs(None, state.update_entry(entry, 'a'))
1350
def create_and_test_file(self, state, entry):
1351
"""Create a file at 'a' and verify the state finds it.
1353
The state should already be versioning *something* at 'a'. This makes
1354
sure that state.update_entry recognizes it as a file.
1356
self.build_tree(['a'])
1357
stat_value = os.lstat('a')
1358
packed_stat = dirstate.pack_stat(stat_value)
1360
link_or_sha1 = state.update_entry(entry, abspath='a')
1361
self.assertEqual('b50e5406bb5e153ebbeb20268fcf37c87e1ecfb6',
1363
self.assertEqual([('f', link_or_sha1, 14, False, packed_stat)],
1367
def create_and_test_dir(self, state, entry):
1368
"""Create a directory at 'a' and verify the state finds it.
1370
The state should already be versioning *something* at 'a'. This makes
1371
sure that state.update_entry recognizes it as a directory.
1373
self.build_tree(['a/'])
1374
stat_value = os.lstat('a')
1375
packed_stat = dirstate.pack_stat(stat_value)
1377
link_or_sha1 = state.update_entry(entry, abspath='a')
1378
self.assertIs(None, link_or_sha1)
1379
self.assertEqual([('d', '', 0, False, packed_stat)], entry[1])
1383
def create_and_test_symlink(self, state, entry):
1384
"""Create a symlink at 'a' and verify the state finds it.
1386
The state should already be versioning *something* at 'a'. This makes
1387
sure that state.update_entry recognizes it as a symlink.
1389
This should not be called if this platform does not have symlink
1392
# caller should care about skipping test on platforms without symlinks
1393
os.symlink('path/to/foo', 'a')
1395
stat_value = os.lstat('a')
1396
packed_stat = dirstate.pack_stat(stat_value)
1398
link_or_sha1 = state.update_entry(entry, abspath='a')
1399
self.assertEqual('path/to/foo', link_or_sha1)
1400
self.assertEqual([('l', 'path/to/foo', 11, False, packed_stat)],
1404
def test_update_missing_file(self):
1405
state, entry = self.get_state_with_a()
1406
packed_stat = self.create_and_test_file(state, entry)
1407
# Now if we delete the file, update_entry should recover and
1410
self.assertIs(None, state.update_entry(entry, abspath='a'))
1411
# And the record shouldn't be changed.
1412
digest = 'b50e5406bb5e153ebbeb20268fcf37c87e1ecfb6'
1413
self.assertEqual([('f', digest, 14, False, packed_stat)],
1416
def test_update_missing_dir(self):
1417
state, entry = self.get_state_with_a()
1418
packed_stat = self.create_and_test_dir(state, entry)
1419
# Now if we delete the directory, update_entry should recover and
1422
self.assertIs(None, state.update_entry(entry, abspath='a'))
1423
self.assertEqual([('d', '', 0, False, packed_stat)], entry[1])
1425
def test_update_missing_symlink(self):
1426
if not osutils.has_symlinks():
1427
# PlatformDeficiency / TestSkipped
1428
raise TestSkipped("No symlink support")
1429
state, entry = self.get_state_with_a()
1430
packed_stat = self.create_and_test_symlink(state, entry)
1432
self.assertIs(None, state.update_entry(entry, abspath='a'))
1433
# And the record shouldn't be changed.
1434
self.assertEqual([('l', 'path/to/foo', 11, False, packed_stat)],
1437
def test_update_file_to_dir(self):
1438
"""If a file changes to a directory we return None for the sha.
1439
We also update the inventory record.
1441
state, entry = self.get_state_with_a()
1442
self.create_and_test_file(state, entry)
1444
self.create_and_test_dir(state, entry)
1446
def test_update_file_to_symlink(self):
1447
"""File becomes a symlink"""
1448
if not osutils.has_symlinks():
1449
# PlatformDeficiency / TestSkipped
1450
raise TestSkipped("No symlink support")
1451
state, entry = self.get_state_with_a()
1452
self.create_and_test_file(state, entry)
1454
self.create_and_test_symlink(state, entry)
1456
def test_update_dir_to_file(self):
1457
"""Directory becoming a file updates the entry."""
1458
state, entry = self.get_state_with_a()
1459
self.create_and_test_dir(state, entry)
1461
self.create_and_test_file(state, entry)
1463
def test_update_dir_to_symlink(self):
1464
"""Directory becomes a symlink"""
1465
if not osutils.has_symlinks():
1466
# PlatformDeficiency / TestSkipped
1467
raise TestSkipped("No symlink support")
1468
state, entry = self.get_state_with_a()
1469
self.create_and_test_dir(state, entry)
1471
self.create_and_test_symlink(state, entry)
1473
def test_update_symlink_to_file(self):
1474
"""Symlink becomes a file"""
1475
if not has_symlinks():
1476
raise TestSkipped("No symlink support")
1477
state, entry = self.get_state_with_a()
1478
self.create_and_test_symlink(state, entry)
1480
self.create_and_test_file(state, entry)
1482
def test_update_symlink_to_dir(self):
1483
"""Symlink becomes a directory"""
1484
if not has_symlinks():
1485
raise TestSkipped("No symlink support")
1486
state, entry = self.get_state_with_a()
1487
self.create_and_test_symlink(state, entry)
1489
self.create_and_test_dir(state, entry)
1491
def test__is_executable_win32(self):
1492
state, entry = self.get_state_with_a()
1493
self.build_tree(['a'])
1495
# Make sure we are using the win32 implementation of _is_executable
1496
state._is_executable = state._is_executable_win32
1498
# The file on disk is not executable, but we are marking it as though
1499
# it is. With _is_executable_win32 we ignore what is on disk.
1500
entry[1][0] = ('f', '', 0, True, dirstate.DirState.NULLSTAT)
1502
stat_value = os.lstat('a')
1503
packed_stat = dirstate.pack_stat(stat_value)
1505
state.adjust_time(-10) # Make sure everything is new
1506
# Make sure it wants to kkkkkkkk
1507
state.update_entry(entry, abspath='a', stat_value=stat_value)
1509
# The row is updated, but the executable bit stays set.
1510
digest = 'b50e5406bb5e153ebbeb20268fcf37c87e1ecfb6'
1511
self.assertEqual([('f', digest, 14, True, packed_stat)], entry[1])
1514
class TestPackStat(TestCaseWithTransport):
1516
def assertPackStat(self, expected, stat_value):
1517
"""Check the packed and serialized form of a stat value."""
1518
self.assertEqual(expected, dirstate.pack_stat(stat_value))
1520
def test_pack_stat_int(self):
1521
st = _FakeStat(6859L, 1172758614, 1172758617, 777L, 6499538L, 0100644)
1522
# Make sure that all parameters have an impact on the packed stat.
1523
self.assertPackStat('AAAay0Xm4FZF5uBZAAADCQBjLNIAAIGk', st)
1526
self.assertPackStat('AAAbWEXm4FZF5uBZAAADCQBjLNIAAIGk', st)
1527
st.st_mtime = 1172758620
1529
self.assertPackStat('AAAbWEXm4FxF5uBZAAADCQBjLNIAAIGk', st)
1530
st.st_ctime = 1172758630
1532
self.assertPackStat('AAAbWEXm4FxF5uBmAAADCQBjLNIAAIGk', st)
1535
self.assertPackStat('AAAbWEXm4FxF5uBmAAADeABjLNIAAIGk', st)
1536
st.st_ino = 6499540L
1538
self.assertPackStat('AAAbWEXm4FxF5uBmAAADeABjLNQAAIGk', st)
1539
st.st_mode = 0100744
1541
self.assertPackStat('AAAbWEXm4FxF5uBmAAADeABjLNQAAIHk', st)
1543
def test_pack_stat_float(self):
1544
"""On some platforms mtime and ctime are floats.
1546
Make sure we don't get warnings or errors, and that we ignore changes <
1549
st = _FakeStat(7000L, 1172758614.0, 1172758617.0,
1550
777L, 6499538L, 0100644)
1551
# These should all be the same as the integer counterparts
1552
self.assertPackStat('AAAbWEXm4FZF5uBZAAADCQBjLNIAAIGk', st)
1553
st.st_mtime = 1172758620.0
1555
self.assertPackStat('AAAbWEXm4FxF5uBZAAADCQBjLNIAAIGk', st)
1556
st.st_ctime = 1172758630.0
1558
self.assertPackStat('AAAbWEXm4FxF5uBmAAADCQBjLNIAAIGk', st)
1559
# fractional seconds are discarded, so no change from above
1560
st.st_mtime = 1172758620.453
1561
self.assertPackStat('AAAbWEXm4FxF5uBmAAADCQBjLNIAAIGk', st)
1562
st.st_ctime = 1172758630.228
1563
self.assertPackStat('AAAbWEXm4FxF5uBmAAADCQBjLNIAAIGk', st)
1566
class TestBisect(TestCaseWithTransport):
1567
"""Test the ability to bisect into the disk format."""
1569
def create_basic_dirstate(self):
1570
"""Create a dirstate with a few files and directories.
1579
tree = self.make_branch_and_tree('tree')
1580
paths = ['a', 'b/', 'b/c', 'b/d/', 'b/d/e', 'f']
1581
file_ids = ['a-id', 'b-id', 'c-id', 'd-id', 'e-id', 'f-id']
1582
self.build_tree(['tree/' + p for p in paths])
1583
tree.set_root_id('TREE_ROOT')
1584
tree.add([p.rstrip('/') for p in paths], file_ids)
1585
tree.commit('initial', rev_id='rev-1')
1586
revision_id = 'rev-1'
1587
# a_packed_stat = dirstate.pack_stat(os.stat('tree/a'))
1588
t = self.get_transport().clone('tree')
1589
a_text = t.get_bytes('a')
1590
a_sha = osutils.sha_string(a_text)
1592
# b_packed_stat = dirstate.pack_stat(os.stat('tree/b'))
1593
# c_packed_stat = dirstate.pack_stat(os.stat('tree/b/c'))
1594
c_text = t.get_bytes('b/c')
1595
c_sha = osutils.sha_string(c_text)
1597
# d_packed_stat = dirstate.pack_stat(os.stat('tree/b/d'))
1598
# e_packed_stat = dirstate.pack_stat(os.stat('tree/b/d/e'))
1599
e_text = t.get_bytes('b/d/e')
1600
e_sha = osutils.sha_string(e_text)
1602
# f_packed_stat = dirstate.pack_stat(os.stat('tree/f'))
1603
f_text = t.get_bytes('f')
1604
f_sha = osutils.sha_string(f_text)
1606
null_stat = dirstate.DirState.NULLSTAT
1608
'':(('', '', 'TREE_ROOT'), [
1609
('d', '', 0, False, null_stat),
1610
('d', '', 0, False, revision_id),
1612
'a':(('', 'a', 'a-id'), [
1613
('f', '', 0, False, null_stat),
1614
('f', a_sha, a_len, False, revision_id),
1616
'b':(('', 'b', 'b-id'), [
1617
('d', '', 0, False, null_stat),
1618
('d', '', 0, False, revision_id),
1620
'b/c':(('b', 'c', 'c-id'), [
1621
('f', '', 0, False, null_stat),
1622
('f', c_sha, c_len, False, revision_id),
1624
'b/d':(('b', 'd', 'd-id'), [
1625
('d', '', 0, False, null_stat),
1626
('d', '', 0, False, revision_id),
1628
'b/d/e':(('b/d', 'e', 'e-id'), [
1629
('f', '', 0, False, null_stat),
1630
('f', e_sha, e_len, False, revision_id),
1632
'f':(('', 'f', 'f-id'), [
1633
('f', '', 0, False, null_stat),
1634
('f', f_sha, f_len, False, revision_id),
1637
state = dirstate.DirState.from_tree(tree, 'dirstate')
1642
# Use a different object, to make sure nothing is pre-cached in memory.
1643
state = dirstate.DirState.on_file('dirstate')
1645
self.addCleanup(state.unlock)
1646
self.assertEqual(dirstate.DirState.NOT_IN_MEMORY,
1647
state._dirblock_state)
1648
# This is code is only really tested if we actually have to make more
1649
# than one read, so set the page size to something smaller.
1650
# We want it to contain about 2.2 records, so that we have a couple
1651
# records that we can read per attempt
1652
state._bisect_page_size = 200
1653
return tree, state, expected
1655
def create_duplicated_dirstate(self):
1656
"""Create a dirstate with a deleted and added entries.
1658
This grabs a basic_dirstate, and then removes and re adds every entry
1661
tree, state, expected = self.create_basic_dirstate()
1662
# Now we will just remove and add every file so we get an extra entry
1663
# per entry. Unversion in reverse order so we handle subdirs
1664
tree.unversion(['f-id', 'e-id', 'd-id', 'c-id', 'b-id', 'a-id'])
1665
tree.add(['a', 'b', 'b/c', 'b/d', 'b/d/e', 'f'],
1666
['a-id2', 'b-id2', 'c-id2', 'd-id2', 'e-id2', 'f-id2'])
1668
# Update the expected dictionary.
1669
for path in ['a', 'b', 'b/c', 'b/d', 'b/d/e', 'f']:
1670
orig = expected[path]
1672
# This record was deleted in the current tree
1673
expected[path] = (orig[0], [dirstate.DirState.NULL_PARENT_DETAILS,
1675
new_key = (orig[0][0], orig[0][1], orig[0][2]+'2')
1676
# And didn't exist in the basis tree
1677
expected[path2] = (new_key, [orig[1][0],
1678
dirstate.DirState.NULL_PARENT_DETAILS])
1680
# We will replace the 'dirstate' file underneath 'state', but that is
1681
# okay as lock as we unlock 'state' first.
1684
new_state = dirstate.DirState.from_tree(tree, 'dirstate')
1690
# But we need to leave state in a read-lock because we already have
1691
# a cleanup scheduled
1693
return tree, state, expected
1695
def create_renamed_dirstate(self):
1696
"""Create a dirstate with a few internal renames.
1698
This takes the basic dirstate, and moves the paths around.
1700
tree, state, expected = self.create_basic_dirstate()
1702
tree.rename_one('a', 'b/g')
1704
tree.rename_one('b/d', 'h')
1706
old_a = expected['a']
1707
expected['a'] = (old_a[0], [('r', 'b/g', 0, False, ''), old_a[1][1]])
1708
expected['b/g'] = (('b', 'g', 'a-id'), [old_a[1][0],
1709
('r', 'a', 0, False, '')])
1710
old_d = expected['b/d']
1711
expected['b/d'] = (old_d[0], [('r', 'h', 0, False, ''), old_d[1][1]])
1712
expected['h'] = (('', 'h', 'd-id'), [old_d[1][0],
1713
('r', 'b/d', 0, False, '')])
1715
old_e = expected['b/d/e']
1716
expected['b/d/e'] = (old_e[0], [('r', 'h/e', 0, False, ''),
1718
expected['h/e'] = (('h', 'e', 'e-id'), [old_e[1][0],
1719
('r', 'b/d/e', 0, False, '')])
1723
new_state = dirstate.DirState.from_tree(tree, 'dirstate')
1730
return tree, state, expected
1732
def assertBisect(self, expected_map, map_keys, state, paths):
1733
"""Assert that bisecting for paths returns the right result.
1735
:param expected_map: A map from key => entry value
1736
:param map_keys: The keys to expect for each path
1737
:param state: The DirState object.
1738
:param paths: A list of paths, these will automatically be split into
1739
(dir, name) tuples, and sorted according to how _bisect
1742
dir_names = sorted(osutils.split(p) for p in paths)
1743
result = state._bisect(dir_names)
1744
# For now, results are just returned in whatever order we read them.
1745
# We could sort by (dir, name, file_id) or something like that, but in
1746
# the end it would still be fairly arbitrary, and we don't want the
1747
# extra overhead if we can avoid it. So sort everything to make sure
1749
assert len(map_keys) == len(dir_names)
1751
for dir_name, keys in zip(dir_names, map_keys):
1753
# This should not be present in the output
1755
expected[dir_name] = sorted(expected_map[k] for k in keys)
1757
for dir_name in result:
1758
result[dir_name].sort()
1760
self.assertEqual(expected, result)
1762
def assertBisectDirBlocks(self, expected_map, map_keys, state, paths):
1763
"""Assert that bisecting for dirbblocks returns the right result.
1765
:param expected_map: A map from key => expected values
1766
:param map_keys: A nested list of paths we expect to be returned.
1767
Something like [['a', 'b', 'f'], ['b/c', 'b/d']]
1768
:param state: The DirState object.
1769
:param paths: A list of directories
1771
result = state._bisect_dirblocks(paths)
1772
assert len(map_keys) == len(paths)
1775
for path, keys in zip(paths, map_keys):
1777
# This should not be present in the output
1779
expected[path] = sorted(expected_map[k] for k in keys)
1783
self.assertEqual(expected, result)
1785
def assertBisectRecursive(self, expected_map, map_keys, state, paths):
1786
"""Assert the return value of a recursive bisection.
1788
:param expected_map: A map from key => entry value
1789
:param map_keys: A list of paths we expect to be returned.
1790
Something like ['a', 'b', 'f', 'b/d', 'b/d2']
1791
:param state: The DirState object.
1792
:param paths: A list of files and directories. It will be broken up
1793
into (dir, name) pairs and sorted before calling _bisect_recursive.
1796
for key in map_keys:
1797
entry = expected_map[key]
1798
dir_name_id, trees_info = entry
1799
expected[dir_name_id] = trees_info
1801
dir_names = sorted(osutils.split(p) for p in paths)
1802
result = state._bisect_recursive(dir_names)
1804
self.assertEqual(expected, result)
1806
def test_bisect_each(self):
1807
"""Find a single record using bisect."""
1808
tree, state, expected = self.create_basic_dirstate()
1810
# Bisect should return the rows for the specified files.
1811
self.assertBisect(expected, [['']], state, [''])
1812
self.assertBisect(expected, [['a']], state, ['a'])
1813
self.assertBisect(expected, [['b']], state, ['b'])
1814
self.assertBisect(expected, [['b/c']], state, ['b/c'])
1815
self.assertBisect(expected, [['b/d']], state, ['b/d'])
1816
self.assertBisect(expected, [['b/d/e']], state, ['b/d/e'])
1817
self.assertBisect(expected, [['f']], state, ['f'])
1819
def test_bisect_multi(self):
1820
"""Bisect can be used to find multiple records at the same time."""
1821
tree, state, expected = self.create_basic_dirstate()
1822
# Bisect should be capable of finding multiple entries at the same time
1823
self.assertBisect(expected, [['a'], ['b'], ['f']],
1824
state, ['a', 'b', 'f'])
1825
# ('', 'f') sorts before the others
1826
self.assertBisect(expected, [['f'], ['b/d'], ['b/d/e']],
1827
state, ['b/d', 'b/d/e', 'f'])
1829
def test_bisect_one_page(self):
1830
"""Test bisect when there is only 1 page to read"""
1831
tree, state, expected = self.create_basic_dirstate()
1832
state._bisect_page_size = 5000
1833
self.assertBisect(expected,[['']], state, [''])
1834
self.assertBisect(expected,[['a']], state, ['a'])
1835
self.assertBisect(expected,[['b']], state, ['b'])
1836
self.assertBisect(expected,[['b/c']], state, ['b/c'])
1837
self.assertBisect(expected,[['b/d']], state, ['b/d'])
1838
self.assertBisect(expected,[['b/d/e']], state, ['b/d/e'])
1839
self.assertBisect(expected,[['f']], state, ['f'])
1840
self.assertBisect(expected,[['a'], ['b'], ['f']],
1841
state, ['a', 'b', 'f'])
1842
# ('', 'f') sorts before the others
1843
self.assertBisect(expected, [['f'], ['b/d'], ['b/d/e']],
1844
state, ['b/d', 'b/d/e', 'f'])
1846
def test_bisect_duplicate_paths(self):
1847
"""When bisecting for a path, handle multiple entries."""
1848
tree, state, expected = self.create_duplicated_dirstate()
1850
# Now make sure that both records are properly returned.
1851
self.assertBisect(expected, [['']], state, [''])
1852
self.assertBisect(expected, [['a', 'a2']], state, ['a'])
1853
self.assertBisect(expected, [['b', 'b2']], state, ['b'])
1854
self.assertBisect(expected, [['b/c', 'b/c2']], state, ['b/c'])
1855
self.assertBisect(expected, [['b/d', 'b/d2']], state, ['b/d'])
1856
self.assertBisect(expected, [['b/d/e', 'b/d/e2']],
1858
self.assertBisect(expected, [['f', 'f2']], state, ['f'])
1860
def test_bisect_page_size_too_small(self):
1861
"""If the page size is too small, we will auto increase it."""
1862
tree, state, expected = self.create_basic_dirstate()
1863
state._bisect_page_size = 50
1864
self.assertBisect(expected, [None], state, ['b/e'])
1865
self.assertBisect(expected, [['a']], state, ['a'])
1866
self.assertBisect(expected, [['b']], state, ['b'])
1867
self.assertBisect(expected, [['b/c']], state, ['b/c'])
1868
self.assertBisect(expected, [['b/d']], state, ['b/d'])
1869
self.assertBisect(expected, [['b/d/e']], state, ['b/d/e'])
1870
self.assertBisect(expected, [['f']], state, ['f'])
1872
def test_bisect_missing(self):
1873
"""Test that bisect return None if it cannot find a path."""
1874
tree, state, expected = self.create_basic_dirstate()
1875
self.assertBisect(expected, [None], state, ['foo'])
1876
self.assertBisect(expected, [None], state, ['b/foo'])
1877
self.assertBisect(expected, [None], state, ['bar/foo'])
1879
self.assertBisect(expected, [['a'], None, ['b/d']],
1880
state, ['a', 'foo', 'b/d'])
1882
def test_bisect_rename(self):
1883
"""Check that we find a renamed row."""
1884
tree, state, expected = self.create_renamed_dirstate()
1886
# Search for the pre and post renamed entries
1887
self.assertBisect(expected, [['a']], state, ['a'])
1888
self.assertBisect(expected, [['b/g']], state, ['b/g'])
1889
self.assertBisect(expected, [['b/d']], state, ['b/d'])
1890
self.assertBisect(expected, [['h']], state, ['h'])
1892
# What about b/d/e? shouldn't that also get 2 directory entries?
1893
self.assertBisect(expected, [['b/d/e']], state, ['b/d/e'])
1894
self.assertBisect(expected, [['h/e']], state, ['h/e'])
1896
def test_bisect_dirblocks(self):
1897
tree, state, expected = self.create_duplicated_dirstate()
1898
self.assertBisectDirBlocks(expected,
1899
[['', 'a', 'a2', 'b', 'b2', 'f', 'f2']], state, [''])
1900
self.assertBisectDirBlocks(expected,
1901
[['b/c', 'b/c2', 'b/d', 'b/d2']], state, ['b'])
1902
self.assertBisectDirBlocks(expected,
1903
[['b/d/e', 'b/d/e2']], state, ['b/d'])
1904
self.assertBisectDirBlocks(expected,
1905
[['', 'a', 'a2', 'b', 'b2', 'f', 'f2'],
1906
['b/c', 'b/c2', 'b/d', 'b/d2'],
1907
['b/d/e', 'b/d/e2'],
1908
], state, ['', 'b', 'b/d'])
1910
def test_bisect_dirblocks_missing(self):
1911
tree, state, expected = self.create_basic_dirstate()
1912
self.assertBisectDirBlocks(expected, [['b/d/e'], None],
1913
state, ['b/d', 'b/e'])
1914
# Files don't show up in this search
1915
self.assertBisectDirBlocks(expected, [None], state, ['a'])
1916
self.assertBisectDirBlocks(expected, [None], state, ['b/c'])
1917
self.assertBisectDirBlocks(expected, [None], state, ['c'])
1918
self.assertBisectDirBlocks(expected, [None], state, ['b/d/e'])
1919
self.assertBisectDirBlocks(expected, [None], state, ['f'])
1921
def test_bisect_recursive_each(self):
1922
tree, state, expected = self.create_basic_dirstate()
1923
self.assertBisectRecursive(expected, ['a'], state, ['a'])
1924
self.assertBisectRecursive(expected, ['b/c'], state, ['b/c'])
1925
self.assertBisectRecursive(expected, ['b/d/e'], state, ['b/d/e'])
1926
self.assertBisectRecursive(expected, ['b/d', 'b/d/e'],
1928
self.assertBisectRecursive(expected, ['b', 'b/c', 'b/d', 'b/d/e'],
1930
self.assertBisectRecursive(expected, ['', 'a', 'b', 'f', 'b/c',
1934
def test_bisect_recursive_multiple(self):
1935
tree, state, expected = self.create_basic_dirstate()
1936
self.assertBisectRecursive(expected, ['a', 'b/c'], state, ['a', 'b/c'])
1937
self.assertBisectRecursive(expected, ['b/d', 'b/d/e'],
1938
state, ['b/d', 'b/d/e'])
1940
def test_bisect_recursive_missing(self):
1941
tree, state, expected = self.create_basic_dirstate()
1942
self.assertBisectRecursive(expected, [], state, ['d'])
1943
self.assertBisectRecursive(expected, [], state, ['b/e'])
1944
self.assertBisectRecursive(expected, [], state, ['g'])
1945
self.assertBisectRecursive(expected, ['a'], state, ['a', 'g'])
1947
def test_bisect_recursive_renamed(self):
1948
tree, state, expected = self.create_renamed_dirstate()
1950
# Looking for either renamed item should find the other
1951
self.assertBisectRecursive(expected, ['a', 'b/g'], state, ['a'])
1952
self.assertBisectRecursive(expected, ['a', 'b/g'], state, ['b/g'])
1953
# Looking in the containing directory should find the rename target,
1954
# and anything in a subdir of the renamed target.
1955
self.assertBisectRecursive(expected, ['a', 'b', 'b/c', 'b/d',
1956
'b/d/e', 'b/g', 'h', 'h/e'],
1960
class TestBisectDirblock(TestCase):
1961
"""Test that bisect_dirblock() returns the expected values.
1963
bisect_dirblock is intended to work like bisect.bisect_left() except it
1964
knows it is working on dirblocks and that dirblocks are sorted by ('path',
1965
'to', 'foo') chunks rather than by raw 'path/to/foo'.
1968
def assertBisect(self, dirblocks, split_dirblocks, path, *args, **kwargs):
1969
"""Assert that bisect_split works like bisect_left on the split paths.
1971
:param dirblocks: A list of (path, [info]) pairs.
1972
:param split_dirblocks: A list of ((split, path), [info]) pairs.
1973
:param path: The path we are indexing.
1975
All other arguments will be passed along.
1977
bisect_split_idx = dirstate.bisect_dirblock(dirblocks, path,
1979
split_dirblock = (path.split('/'), [])
1980
bisect_left_idx = bisect.bisect_left(split_dirblocks, split_dirblock,
1982
self.assertEqual(bisect_left_idx, bisect_split_idx,
1983
'bisect_split disagreed. %s != %s'
1985
% (bisect_left_idx, bisect_split_idx, path)
1988
def paths_to_dirblocks(self, paths):
1989
"""Convert a list of paths into dirblock form.
1991
Also, ensure that the paths are in proper sorted order.
1993
dirblocks = [(path, []) for path in paths]
1994
split_dirblocks = [(path.split('/'), []) for path in paths]
1995
self.assertEqual(sorted(split_dirblocks), split_dirblocks)
1996
return dirblocks, split_dirblocks
1998
def test_simple(self):
1999
"""In the simple case it works just like bisect_left"""
2000
paths = ['', 'a', 'b', 'c', 'd']
2001
dirblocks, split_dirblocks = self.paths_to_dirblocks(paths)
2003
self.assertBisect(dirblocks, split_dirblocks, path)
2004
self.assertBisect(dirblocks, split_dirblocks, '_')
2005
self.assertBisect(dirblocks, split_dirblocks, 'aa')
2006
self.assertBisect(dirblocks, split_dirblocks, 'bb')
2007
self.assertBisect(dirblocks, split_dirblocks, 'cc')
2008
self.assertBisect(dirblocks, split_dirblocks, 'dd')
2009
self.assertBisect(dirblocks, split_dirblocks, 'a/a')
2010
self.assertBisect(dirblocks, split_dirblocks, 'b/b')
2011
self.assertBisect(dirblocks, split_dirblocks, 'c/c')
2012
self.assertBisect(dirblocks, split_dirblocks, 'd/d')
2014
def test_involved(self):
2015
"""This is where bisect_left diverges slightly."""
2017
'a/a', 'a/a/a', 'a/a/z', 'a/a-a', 'a/a-z',
2018
'a/z', 'a/z/a', 'a/z/z', 'a/z-a', 'a/z-z',
2020
'z', 'z/a/a', 'z/a/z', 'z/a-a', 'z/a-z',
2021
'z/z', 'z/z/a', 'z/z/z', 'z/z-a', 'z/z-z',
2024
dirblocks, split_dirblocks = self.paths_to_dirblocks(paths)
2026
self.assertBisect(dirblocks, split_dirblocks, path)
2028
def test_involved_cached(self):
2029
"""This is where bisect_left diverges slightly."""
2031
'a/a', 'a/a/a', 'a/a/z', 'a/a-a', 'a/a-z',
2032
'a/z', 'a/z/a', 'a/z/z', 'a/z-a', 'a/z-z',
2034
'z', 'z/a/a', 'z/a/z', 'z/a-a', 'z/a-z',
2035
'z/z', 'z/z/a', 'z/z/z', 'z/z-a', 'z/z-z',
2039
dirblocks, split_dirblocks = self.paths_to_dirblocks(paths)
2041
self.assertBisect(dirblocks, split_dirblocks, path, cache=cache)