1
# Copyright (C) 2006-2011 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., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA
17
"""Tests of the dirstate functionality being built for WorkingTreeFormat4."""
27
revision as _mod_revision,
41
from .scenarios import load_tests_apply_scenarios
46
# general checks for NOT_IN_MEMORY error conditions.
47
# set_path_id on a NOT_IN_MEMORY dirstate
48
# set_path_id unicode support
49
# set_path_id setting id of a path not root
50
# set_path_id setting id when there are parents without the id in the parents
51
# set_path_id setting id when there are parents with the id in the parents
52
# set_path_id setting id when state is not in memory
53
# set_path_id setting id when state is in memory unmodified
54
# set_path_id setting id when state is in memory modified
57
class TestErrors(tests.TestCase):
59
def test_dirstate_corrupt(self):
60
error = dirstate.DirstateCorrupt('.bzr/checkout/dirstate',
61
'trailing garbage: "x"')
62
self.assertEqualDiff("The dirstate file (.bzr/checkout/dirstate)"
63
" appears to be corrupt: trailing garbage: \"x\"",
67
load_tests = load_tests_apply_scenarios
70
class TestCaseWithDirState(tests.TestCaseWithTransport):
71
"""Helper functions for creating DirState objects with various content."""
73
scenarios = test_osutils.dir_reader_scenarios()
76
_dir_reader_class = None
77
_native_to_unicode = None # Not used yet
80
super(TestCaseWithDirState, self).setUp()
81
self.overrideAttr(osutils,
82
'_selected_dir_reader', self._dir_reader_class())
84
def create_empty_dirstate(self):
85
"""Return a locked but empty dirstate"""
86
state = dirstate.DirState.initialize('dirstate')
89
def create_dirstate_with_root(self):
90
"""Return a write-locked state with a single root entry."""
91
packed_stat = b'AAAAREUHaIpFB2iKAAADAQAtkqUAAIGk'
92
root_entry_direntry = (b'', b'', b'a-root-value'), [
93
(b'd', b'', 0, False, packed_stat),
96
dirblocks.append((b'', [root_entry_direntry]))
97
dirblocks.append((b'', []))
98
state = self.create_empty_dirstate()
100
state._set_data([], dirblocks)
107
def create_dirstate_with_root_and_subdir(self):
108
"""Return a locked DirState with a root and a subdir"""
109
packed_stat = b'AAAAREUHaIpFB2iKAAADAQAtkqUAAIGk'
110
subdir_entry = (b'', b'subdir', b'subdir-id'), [
111
(b'd', b'', 0, False, packed_stat),
113
state = self.create_dirstate_with_root()
115
dirblocks = list(state._dirblocks)
116
dirblocks[1][1].append(subdir_entry)
117
state._set_data([], dirblocks)
123
def create_complex_dirstate(self):
124
"""This dirstate contains multiple files and directories.
134
b/h\xc3\xa5 h-\xc3\xa5-file #This is u'\xe5' encoded into utf-8
136
Notice that a/e is an empty directory.
138
:return: The dirstate, still write-locked.
140
packed_stat = b'AAAAREUHaIpFB2iKAAADAQAtkqUAAIGk'
141
null_sha = b'xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx'
142
root_entry = (b'', b'', b'a-root-value'), [
143
(b'd', b'', 0, False, packed_stat),
145
a_entry = (b'', b'a', b'a-dir'), [
146
(b'd', b'', 0, False, packed_stat),
148
b_entry = (b'', b'b', b'b-dir'), [
149
(b'd', b'', 0, False, packed_stat),
151
c_entry = (b'', b'c', b'c-file'), [
152
(b'f', null_sha, 10, False, packed_stat),
154
d_entry = (b'', b'd', b'd-file'), [
155
(b'f', null_sha, 20, False, packed_stat),
157
e_entry = (b'a', b'e', b'e-dir'), [
158
(b'd', b'', 0, False, packed_stat),
160
f_entry = (b'a', b'f', b'f-file'), [
161
(b'f', null_sha, 30, False, packed_stat),
163
g_entry = (b'b', b'g', b'g-file'), [
164
(b'f', null_sha, 30, False, packed_stat),
166
h_entry = (b'b', b'h\xc3\xa5', b'h-\xc3\xa5-file'), [
167
(b'f', null_sha, 40, False, packed_stat),
170
dirblocks.append((b'', [root_entry]))
171
dirblocks.append((b'', [a_entry, b_entry, c_entry, d_entry]))
172
dirblocks.append((b'a', [e_entry, f_entry]))
173
dirblocks.append((b'b', [g_entry, h_entry]))
174
state = dirstate.DirState.initialize('dirstate')
177
state._set_data([], dirblocks)
183
def check_state_with_reopen(self, expected_result, state):
184
"""Check that state has current state expected_result.
186
This will check the current state, open the file anew and check it
188
This function expects the current state to be locked for writing, and
189
will unlock it before re-opening.
190
This is required because we can't open a lock_read() while something
191
else has a lock_write().
192
write => mutually exclusive lock
195
# The state should already be write locked, since we just had to do
196
# some operation to get here.
197
self.assertTrue(state._lock_token is not None)
199
self.assertEqual(expected_result[0], state.get_parent_ids())
200
# there should be no ghosts in this tree.
201
self.assertEqual([], state.get_ghosts())
202
# there should be one fileid in this tree - the root of the tree.
203
self.assertEqual(expected_result[1], list(state._iter_entries()))
208
state = dirstate.DirState.on_file('dirstate')
211
self.assertEqual(expected_result[1], list(state._iter_entries()))
215
def create_basic_dirstate(self):
216
"""Create a dirstate with a few files and directories.
226
tree = self.make_branch_and_tree('tree')
227
paths = ['a', 'b/', 'b/c', 'b/d/', 'b/d/e', 'b-c', 'f']
228
file_ids = [b'a-id', b'b-id', b'c-id', b'd-id', b'e-id', b'b-c-id', b'f-id']
229
self.build_tree(['tree/' + p for p in paths])
230
tree.set_root_id(b'TREE_ROOT')
231
tree.add([p.rstrip('/') for p in paths], file_ids)
232
tree.commit('initial', rev_id=b'rev-1')
233
revision_id = b'rev-1'
234
# a_packed_stat = dirstate.pack_stat(os.stat('tree/a'))
235
t = self.get_transport('tree')
236
a_text = t.get_bytes('a')
237
a_sha = osutils.sha_string(a_text)
239
# b_packed_stat = dirstate.pack_stat(os.stat('tree/b'))
240
# c_packed_stat = dirstate.pack_stat(os.stat('tree/b/c'))
241
c_text = t.get_bytes('b/c')
242
c_sha = osutils.sha_string(c_text)
244
# d_packed_stat = dirstate.pack_stat(os.stat('tree/b/d'))
245
# e_packed_stat = dirstate.pack_stat(os.stat('tree/b/d/e'))
246
e_text = t.get_bytes('b/d/e')
247
e_sha = osutils.sha_string(e_text)
249
b_c_text = t.get_bytes('b-c')
250
b_c_sha = osutils.sha_string(b_c_text)
251
b_c_len = len(b_c_text)
252
# f_packed_stat = dirstate.pack_stat(os.stat('tree/f'))
253
f_text = t.get_bytes('f')
254
f_sha = osutils.sha_string(f_text)
256
null_stat = dirstate.DirState.NULLSTAT
258
b'': ((b'', b'', b'TREE_ROOT'), [
259
(b'd', b'', 0, False, null_stat),
260
(b'd', b'', 0, False, revision_id),
262
b'a': ((b'', b'a', b'a-id'), [
263
(b'f', b'', 0, False, null_stat),
264
(b'f', a_sha, a_len, False, revision_id),
266
b'b': ((b'', b'b', b'b-id'), [
267
(b'd', b'', 0, False, null_stat),
268
(b'd', b'', 0, False, revision_id),
270
b'b/c': ((b'b', b'c', b'c-id'), [
271
(b'f', b'', 0, False, null_stat),
272
(b'f', c_sha, c_len, False, revision_id),
274
b'b/d': ((b'b', b'd', b'd-id'), [
275
(b'd', b'', 0, False, null_stat),
276
(b'd', b'', 0, False, revision_id),
278
b'b/d/e': ((b'b/d', b'e', b'e-id'), [
279
(b'f', b'', 0, False, null_stat),
280
(b'f', e_sha, e_len, False, revision_id),
282
b'b-c': ((b'', b'b-c', b'b-c-id'), [
283
(b'f', b'', 0, False, null_stat),
284
(b'f', b_c_sha, b_c_len, False, revision_id),
286
b'f': ((b'', b'f', b'f-id'), [
287
(b'f', b'', 0, False, null_stat),
288
(b'f', f_sha, f_len, False, revision_id),
291
state = dirstate.DirState.from_tree(tree, 'dirstate')
296
# Use a different object, to make sure nothing is pre-cached in memory.
297
state = dirstate.DirState.on_file('dirstate')
299
self.addCleanup(state.unlock)
300
self.assertEqual(dirstate.DirState.NOT_IN_MEMORY,
301
state._dirblock_state)
302
# This is code is only really tested if we actually have to make more
303
# than one read, so set the page size to something smaller.
304
# We want it to contain about 2.2 records, so that we have a couple
305
# records that we can read per attempt
306
state._bisect_page_size = 200
307
return tree, state, expected
309
def create_duplicated_dirstate(self):
310
"""Create a dirstate with a deleted and added entries.
312
This grabs a basic_dirstate, and then removes and re adds every entry
315
tree, state, expected = self.create_basic_dirstate()
316
# Now we will just remove and add every file so we get an extra entry
317
# per entry. Unversion in reverse order so we handle subdirs
318
tree.unversion(['f', 'b-c', 'b/d/e', 'b/d', 'b/c', 'b', 'a'])
319
tree.add(['a', 'b', 'b/c', 'b/d', 'b/d/e', 'b-c', 'f'],
320
[b'a-id2', b'b-id2', b'c-id2', b'd-id2', b'e-id2', b'b-c-id2', b'f-id2'])
322
# Update the expected dictionary.
323
for path in [b'a', b'b', b'b/c', b'b/d', b'b/d/e', b'b-c', b'f']:
324
orig = expected[path]
326
# This record was deleted in the current tree
327
expected[path] = (orig[0], [dirstate.DirState.NULL_PARENT_DETAILS,
329
new_key = (orig[0][0], orig[0][1], orig[0][2]+b'2')
330
# And didn't exist in the basis tree
331
expected[path2] = (new_key, [orig[1][0],
332
dirstate.DirState.NULL_PARENT_DETAILS])
334
# We will replace the 'dirstate' file underneath 'state', but that is
335
# okay as lock as we unlock 'state' first.
338
new_state = dirstate.DirState.from_tree(tree, 'dirstate')
344
# But we need to leave state in a read-lock because we already have
345
# a cleanup scheduled
347
return tree, state, expected
349
def create_renamed_dirstate(self):
350
"""Create a dirstate with a few internal renames.
352
This takes the basic dirstate, and moves the paths around.
354
tree, state, expected = self.create_basic_dirstate()
356
tree.rename_one('a', 'b/g')
358
tree.rename_one('b/d', 'h')
360
old_a = expected[b'a']
361
expected[b'a'] = (old_a[0], [(b'r', b'b/g', 0, False, b''), old_a[1][1]])
362
expected[b'b/g'] = ((b'b', b'g', b'a-id'), [old_a[1][0],
363
(b'r', b'a', 0, False, b'')])
364
old_d = expected[b'b/d']
365
expected[b'b/d'] = (old_d[0], [(b'r', b'h', 0, False, b''), old_d[1][1]])
366
expected[b'h'] = ((b'', b'h', b'd-id'), [old_d[1][0],
367
(b'r', b'b/d', 0, False, b'')])
369
old_e = expected[b'b/d/e']
370
expected[b'b/d/e'] = (old_e[0], [(b'r', b'h/e', 0, False, b''),
372
expected[b'h/e'] = ((b'h', b'e', b'e-id'), [old_e[1][0],
373
(b'r', b'b/d/e', 0, False, b'')])
377
new_state = dirstate.DirState.from_tree(tree, 'dirstate')
384
return tree, state, expected
387
class TestTreeToDirState(TestCaseWithDirState):
389
def test_empty_to_dirstate(self):
390
"""We should be able to create a dirstate for an empty tree."""
391
# There are no files on disk and no parents
392
tree = self.make_branch_and_tree('tree')
393
expected_result = ([], [
394
((b'', b'', tree.get_root_id()), # common details
395
[(b'd', b'', 0, False, dirstate.DirState.NULLSTAT), # current tree
397
state = dirstate.DirState.from_tree(tree, 'dirstate')
399
self.check_state_with_reopen(expected_result, state)
401
def test_1_parents_empty_to_dirstate(self):
402
# create a parent by doing a commit
403
tree = self.make_branch_and_tree('tree')
404
rev_id = tree.commit('first post')
405
root_stat_pack = dirstate.pack_stat(os.stat(tree.basedir))
406
expected_result = ([rev_id], [
407
((b'', b'', tree.get_root_id()), # common details
408
[(b'd', b'', 0, False, dirstate.DirState.NULLSTAT), # current tree
409
(b'd', b'', 0, False, rev_id), # first parent details
411
state = dirstate.DirState.from_tree(tree, 'dirstate')
412
self.check_state_with_reopen(expected_result, state)
419
def test_2_parents_empty_to_dirstate(self):
420
# create a parent by doing a commit
421
tree = self.make_branch_and_tree('tree')
422
rev_id = tree.commit('first post')
423
tree2 = tree.controldir.sprout('tree2').open_workingtree()
424
rev_id2 = tree2.commit('second post', allow_pointless=True)
425
tree.merge_from_branch(tree2.branch)
426
expected_result = ([rev_id, rev_id2], [
427
((b'', b'', tree.get_root_id()), # common details
428
[(b'd', b'', 0, False, dirstate.DirState.NULLSTAT), # current tree
429
(b'd', b'', 0, False, rev_id), # first parent details
430
(b'd', b'', 0, False, rev_id), # second parent details
432
state = dirstate.DirState.from_tree(tree, 'dirstate')
433
self.check_state_with_reopen(expected_result, state)
440
def test_empty_unknowns_are_ignored_to_dirstate(self):
441
"""We should be able to create a dirstate for an empty tree."""
442
# There are no files on disk and no parents
443
tree = self.make_branch_and_tree('tree')
444
self.build_tree(['tree/unknown'])
445
expected_result = ([], [
446
((b'', b'', tree.get_root_id()), # common details
447
[(b'd', b'', 0, False, dirstate.DirState.NULLSTAT), # current tree
449
state = dirstate.DirState.from_tree(tree, 'dirstate')
450
self.check_state_with_reopen(expected_result, state)
452
def get_tree_with_a_file(self):
453
tree = self.make_branch_and_tree('tree')
454
self.build_tree(['tree/a file'])
455
tree.add('a file', b'a-file-id')
458
def test_non_empty_no_parents_to_dirstate(self):
459
"""We should be able to create a dirstate for an empty tree."""
460
# There are files on disk and no parents
461
tree = self.get_tree_with_a_file()
462
expected_result = ([], [
463
((b'', b'', tree.get_root_id()), # common details
464
[(b'd', b'', 0, False, dirstate.DirState.NULLSTAT), # current tree
466
((b'', b'a file', b'a-file-id'), # common
467
[(b'f', b'', 0, False, dirstate.DirState.NULLSTAT), # current
470
state = dirstate.DirState.from_tree(tree, 'dirstate')
471
self.check_state_with_reopen(expected_result, state)
473
def test_1_parents_not_empty_to_dirstate(self):
474
# create a parent by doing a commit
475
tree = self.get_tree_with_a_file()
476
rev_id = tree.commit('first post')
477
# change the current content to be different this will alter stat, sha
479
self.build_tree_contents([('tree/a file', b'new content\n')])
480
expected_result = ([rev_id], [
481
((b'', b'', tree.get_root_id()), # common details
482
[(b'd', b'', 0, False, dirstate.DirState.NULLSTAT), # current tree
483
(b'd', b'', 0, False, rev_id), # first parent details
485
((b'', b'a file', b'a-file-id'), # common
486
[(b'f', b'', 0, False, dirstate.DirState.NULLSTAT), # current
487
(b'f', b'c3ed76e4bfd45ff1763ca206055bca8e9fc28aa8', 24, False,
488
rev_id), # first parent
491
state = dirstate.DirState.from_tree(tree, 'dirstate')
492
self.check_state_with_reopen(expected_result, state)
494
def test_2_parents_not_empty_to_dirstate(self):
495
# create a parent by doing a commit
496
tree = self.get_tree_with_a_file()
497
rev_id = tree.commit('first post')
498
tree2 = tree.controldir.sprout('tree2').open_workingtree()
499
# change the current content to be different this will alter stat, sha
501
self.build_tree_contents([('tree2/a file', b'merge content\n')])
502
rev_id2 = tree2.commit('second post')
503
tree.merge_from_branch(tree2.branch)
504
# change the current content to be different this will alter stat, sha
505
# and length again, giving us three distinct values:
506
self.build_tree_contents([('tree/a file', b'new content\n')])
507
expected_result = ([rev_id, rev_id2], [
508
((b'', b'', tree.get_root_id()), # common details
509
[(b'd', b'', 0, False, dirstate.DirState.NULLSTAT), # current tree
510
(b'd', b'', 0, False, rev_id), # first parent details
511
(b'd', b'', 0, False, rev_id), # second parent details
513
((b'', b'a file', b'a-file-id'), # common
514
[(b'f', b'', 0, False, dirstate.DirState.NULLSTAT), # current
515
(b'f', b'c3ed76e4bfd45ff1763ca206055bca8e9fc28aa8', 24, False,
516
rev_id), # first parent
517
(b'f', b'314d796174c9412647c3ce07dfb5d36a94e72958', 14, False,
518
rev_id2), # second parent
521
state = dirstate.DirState.from_tree(tree, 'dirstate')
522
self.check_state_with_reopen(expected_result, state)
524
def test_colliding_fileids(self):
525
# test insertion of parents creating several entries at the same path.
526
# we used to have a bug where they could cause the dirstate to break
527
# its ordering invariants.
528
# create some trees to test from
531
tree = self.make_branch_and_tree('tree%d' % i)
532
self.build_tree(['tree%d/name' % i,])
533
tree.add(['name'], [b'file-id%d' % i])
534
revision_id = b'revid-%d' % i
535
tree.commit('message', rev_id=revision_id)
536
parents.append((revision_id,
537
tree.branch.repository.revision_tree(revision_id)))
538
# now fold these trees into a dirstate
539
state = dirstate.DirState.initialize('dirstate')
541
state.set_parent_trees(parents, [])
547
class TestDirStateOnFile(TestCaseWithDirState):
549
def create_updated_dirstate(self):
550
self.build_tree(['a-file'])
551
tree = self.make_branch_and_tree('.')
552
tree.add(['a-file'], [b'a-id'])
553
tree.commit('add a-file')
554
# Save and unlock the state, re-open it in readonly mode
555
state = dirstate.DirState.from_tree(tree, 'dirstate')
558
state = dirstate.DirState.on_file('dirstate')
562
def test_construct_with_path(self):
563
tree = self.make_branch_and_tree('tree')
564
state = dirstate.DirState.from_tree(tree, 'dirstate.from_tree')
565
# we want to be able to get the lines of the dirstate that we will
567
lines = state.get_lines()
569
self.build_tree_contents([('dirstate', b''.join(lines))])
571
# no parents, default tree content
572
expected_result = ([], [
573
((b'', b'', tree.get_root_id()), # common details
574
# current tree details, but new from_tree skips statting, it
575
# uses set_state_from_inventory, and thus depends on the
577
[(b'd', b'', 0, False, dirstate.DirState.NULLSTAT),
580
state = dirstate.DirState.on_file('dirstate')
581
state.lock_write() # check_state_with_reopen will save() and unlock it
582
self.check_state_with_reopen(expected_result, state)
584
def test_can_save_clean_on_file(self):
585
tree = self.make_branch_and_tree('tree')
586
state = dirstate.DirState.from_tree(tree, 'dirstate')
588
# doing a save should work here as there have been no changes.
590
# TODO: stat it and check it hasn't changed; may require waiting
591
# for the state accuracy window.
595
def test_can_save_in_read_lock(self):
596
state = self.create_updated_dirstate()
598
entry = state._get_entry(0, path_utf8=b'a-file')
599
# The current size should be 0 (default)
600
self.assertEqual(0, entry[1][0][2])
601
# We should have a real entry.
602
self.assertNotEqual((None, None), entry)
603
# Set the cutoff-time into the future, so things look cacheable
604
state._sha_cutoff_time()
605
state._cutoff_time += 10.0
606
st = os.lstat('a-file')
607
sha1sum = dirstate.update_entry(state, entry, 'a-file', st)
608
# We updated the current sha1sum because the file is cacheable
609
self.assertEqual(b'ecc5374e9ed82ad3ea3b4d452ea995a5fd3e70e3',
612
# The dirblock has been updated
613
self.assertEqual(st.st_size, entry[1][0][2])
614
self.assertEqual(dirstate.DirState.IN_MEMORY_HASH_MODIFIED,
615
state._dirblock_state)
618
# Now, since we are the only one holding a lock, we should be able
619
# to save and have it written to disk
624
# Re-open the file, and ensure that the state has been updated.
625
state = dirstate.DirState.on_file('dirstate')
628
entry = state._get_entry(0, path_utf8=b'a-file')
629
self.assertEqual(st.st_size, entry[1][0][2])
633
def test_save_fails_quietly_if_locked(self):
634
"""If dirstate is locked, save will fail without complaining."""
635
state = self.create_updated_dirstate()
637
entry = state._get_entry(0, path_utf8=b'a-file')
638
# No cached sha1 yet.
639
self.assertEqual(b'', entry[1][0][1])
640
# Set the cutoff-time into the future, so things look cacheable
641
state._sha_cutoff_time()
642
state._cutoff_time += 10.0
643
st = os.lstat('a-file')
644
sha1sum = dirstate.update_entry(state, entry, 'a-file', st)
645
self.assertEqual(b'ecc5374e9ed82ad3ea3b4d452ea995a5fd3e70e3',
647
self.assertEqual(dirstate.DirState.IN_MEMORY_HASH_MODIFIED,
648
state._dirblock_state)
650
# Now, before we try to save, grab another dirstate, and take out a
652
# TODO: jam 20070315 Ideally this would be locked by another
653
# process. To make sure the file is really OS locked.
654
state2 = dirstate.DirState.on_file('dirstate')
657
# This won't actually write anything, because it couldn't grab
658
# a write lock. But it shouldn't raise an error, either.
659
# TODO: jam 20070315 We should probably distinguish between
660
# being dirty because of 'update_entry'. And dirty
661
# because of real modification. So that save() *does*
662
# raise a real error if it fails when we have real
670
# The file on disk should not be modified.
671
state = dirstate.DirState.on_file('dirstate')
674
entry = state._get_entry(0, path_utf8=b'a-file')
675
self.assertEqual(b'', entry[1][0][1])
679
def test_save_refuses_if_changes_aborted(self):
680
self.build_tree(['a-file', 'a-dir/'])
681
state = dirstate.DirState.initialize('dirstate')
683
# No stat and no sha1 sum.
684
state.add('a-file', b'a-file-id', 'file', None, b'')
689
# The dirstate should include TREE_ROOT and 'a-file' and nothing else
691
(b'', [((b'', b'', b'TREE_ROOT'),
692
[(b'd', b'', 0, False, dirstate.DirState.NULLSTAT)])]),
693
(b'', [((b'', b'a-file', b'a-file-id'),
694
[(b'f', b'', 0, False, dirstate.DirState.NULLSTAT)])]),
697
state = dirstate.DirState.on_file('dirstate')
700
state._read_dirblocks_if_needed()
701
self.assertEqual(expected_blocks, state._dirblocks)
703
# Now modify the state, but mark it as inconsistent
704
state.add('a-dir', b'a-dir-id', 'directory', None, b'')
705
state._changes_aborted = True
710
state = dirstate.DirState.on_file('dirstate')
713
state._read_dirblocks_if_needed()
714
self.assertEqual(expected_blocks, state._dirblocks)
719
class TestDirStateInitialize(TestCaseWithDirState):
721
def test_initialize(self):
722
expected_result = ([], [
723
((b'', b'', b'TREE_ROOT'), # common details
724
[(b'd', b'', 0, False, dirstate.DirState.NULLSTAT), # current tree
727
state = dirstate.DirState.initialize('dirstate')
729
self.assertIsInstance(state, dirstate.DirState)
730
lines = state.get_lines()
733
# On win32 you can't read from a locked file, even within the same
734
# process. So we have to unlock and release before we check the file
736
self.assertFileEqual(b''.join(lines), 'dirstate')
737
state.lock_read() # check_state_with_reopen will unlock
738
self.check_state_with_reopen(expected_result, state)
741
class TestDirStateManipulations(TestCaseWithDirState):
743
def make_minimal_tree(self):
744
tree1 = self.make_branch_and_memory_tree('tree1')
746
self.addCleanup(tree1.unlock)
748
revid1 = tree1.commit('foo')
751
def test_update_minimal_updates_id_index(self):
752
state = self.create_dirstate_with_root_and_subdir()
753
self.addCleanup(state.unlock)
754
id_index = state._get_id_index()
755
self.assertEqual([b'a-root-value', b'subdir-id'], sorted(id_index))
756
state.add('file-name', b'file-id', 'file', None, '')
757
self.assertEqual([b'a-root-value', b'file-id', b'subdir-id'],
759
state.update_minimal((b'', b'new-name', b'file-id'), b'f',
760
path_utf8=b'new-name')
761
self.assertEqual([b'a-root-value', b'file-id', b'subdir-id'],
763
self.assertEqual([(b'', b'new-name', b'file-id')],
764
sorted(id_index[b'file-id']))
767
def test_set_state_from_inventory_no_content_no_parents(self):
768
# setting the current inventory is a slow but important api to support.
769
tree1, revid1 = self.make_minimal_tree()
770
inv = tree1.root_inventory
771
root_id = inv.path2id('')
772
expected_result = [], [
773
((b'', b'', root_id), [
774
(b'd', b'', 0, False, dirstate.DirState.NULLSTAT)])]
775
state = dirstate.DirState.initialize('dirstate')
777
state.set_state_from_inventory(inv)
778
self.assertEqual(dirstate.DirState.IN_MEMORY_UNMODIFIED,
780
self.assertEqual(dirstate.DirState.IN_MEMORY_MODIFIED,
781
state._dirblock_state)
786
# This will unlock it
787
self.check_state_with_reopen(expected_result, state)
789
def test_set_state_from_scratch_no_parents(self):
790
tree1, revid1 = self.make_minimal_tree()
791
inv = tree1.root_inventory
792
root_id = inv.path2id('')
793
expected_result = [], [
794
((b'', b'', root_id), [
795
(b'd', b'', 0, False, dirstate.DirState.NULLSTAT)])]
796
state = dirstate.DirState.initialize('dirstate')
798
state.set_state_from_scratch(inv, [], [])
799
self.assertEqual(dirstate.DirState.IN_MEMORY_MODIFIED,
801
self.assertEqual(dirstate.DirState.IN_MEMORY_MODIFIED,
802
state._dirblock_state)
807
# This will unlock it
808
self.check_state_with_reopen(expected_result, state)
810
def test_set_state_from_scratch_identical_parent(self):
811
tree1, revid1 = self.make_minimal_tree()
812
inv = tree1.root_inventory
813
root_id = inv.path2id('')
814
rev_tree1 = tree1.branch.repository.revision_tree(revid1)
815
d_entry = (b'd', b'', 0, False, dirstate.DirState.NULLSTAT)
816
parent_entry = (b'd', b'', 0, False, revid1)
817
expected_result = [revid1], [
818
((b'', b'', root_id), [d_entry, parent_entry])]
819
state = dirstate.DirState.initialize('dirstate')
821
state.set_state_from_scratch(inv, [(revid1, rev_tree1)], [])
822
self.assertEqual(dirstate.DirState.IN_MEMORY_MODIFIED,
824
self.assertEqual(dirstate.DirState.IN_MEMORY_MODIFIED,
825
state._dirblock_state)
830
# This will unlock it
831
self.check_state_with_reopen(expected_result, state)
833
def test_set_state_from_inventory_preserves_hashcache(self):
834
# https://bugs.launchpad.net/bzr/+bug/146176
835
# set_state_from_inventory should preserve the stat and hash value for
836
# workingtree files that are not changed by the inventory.
838
tree = self.make_branch_and_tree('.')
839
# depends on the default format using dirstate...
840
with tree.lock_write():
841
# make a dirstate with some valid hashcache data
842
# file on disk, but that's not needed for this test
843
foo_contents = b'contents of foo'
844
self.build_tree_contents([('foo', foo_contents)])
845
tree.add('foo', b'foo-id')
847
foo_stat = os.stat('foo')
848
foo_packed = dirstate.pack_stat(foo_stat)
849
foo_sha = osutils.sha_string(foo_contents)
850
foo_size = len(foo_contents)
852
# should not be cached yet, because the file's too fresh
854
((b'', b'foo', b'foo-id',),
855
[(b'f', b'', 0, False, dirstate.DirState.NULLSTAT)]),
856
tree._dirstate._get_entry(0, b'foo-id'))
857
# poke in some hashcache information - it wouldn't normally be
858
# stored because it's too fresh
859
tree._dirstate.update_minimal(
860
(b'', b'foo', b'foo-id'),
861
b'f', False, foo_sha, foo_packed, foo_size, b'foo')
862
# now should be cached
864
((b'', b'foo', b'foo-id',),
865
[(b'f', foo_sha, foo_size, False, foo_packed)]),
866
tree._dirstate._get_entry(0, b'foo-id'))
868
# extract the inventory, and add something to it
869
inv = tree._get_root_inventory()
870
# should see the file we poked in...
871
self.assertTrue(inv.has_id(b'foo-id'))
872
self.assertTrue(inv.has_filename('foo'))
873
inv.add_path('bar', 'file', b'bar-id')
874
tree._dirstate._validate()
875
# this used to cause it to lose its hashcache
876
tree._dirstate.set_state_from_inventory(inv)
877
tree._dirstate._validate()
879
with tree.lock_read():
880
# now check that the state still has the original hashcache value
881
state = tree._dirstate
883
foo_tuple = state._get_entry(0, path_utf8=b'foo')
885
((b'', b'foo', b'foo-id',),
886
[(b'f', foo_sha, len(foo_contents), False,
887
dirstate.pack_stat(foo_stat))]),
890
def test_set_state_from_inventory_mixed_paths(self):
891
tree1 = self.make_branch_and_tree('tree1')
892
self.build_tree(['tree1/a/', 'tree1/a/b/', 'tree1/a-b/',
893
'tree1/a/b/foo', 'tree1/a-b/bar'])
896
tree1.add(['a', 'a/b', 'a-b', 'a/b/foo', 'a-b/bar'],
897
[b'a-id', b'b-id', b'a-b-id', b'foo-id', b'bar-id'])
898
tree1.commit('rev1', rev_id=b'rev1')
899
root_id = tree1.get_root_id()
900
inv = tree1.root_inventory
903
expected_result1 = [(b'', b'', root_id, b'd'),
904
(b'', b'a', b'a-id', b'd'),
905
(b'', b'a-b', b'a-b-id', b'd'),
906
(b'a', b'b', b'b-id', b'd'),
907
(b'a/b', b'foo', b'foo-id', b'f'),
908
(b'a-b', b'bar', b'bar-id', b'f'),
910
expected_result2 = [(b'', b'', root_id, b'd'),
911
(b'', b'a', b'a-id', b'd'),
912
(b'', b'a-b', b'a-b-id', b'd'),
913
(b'a-b', b'bar', b'bar-id', b'f'),
915
state = dirstate.DirState.initialize('dirstate')
917
state.set_state_from_inventory(inv)
919
for entry in state._iter_entries():
920
values.append(entry[0] + entry[1][0][:1])
921
self.assertEqual(expected_result1, values)
923
state.set_state_from_inventory(inv)
925
for entry in state._iter_entries():
926
values.append(entry[0] + entry[1][0][:1])
927
self.assertEqual(expected_result2, values)
931
def test_set_path_id_no_parents(self):
932
"""The id of a path can be changed trivally with no parents."""
933
state = dirstate.DirState.initialize('dirstate')
935
# check precondition to be sure the state does change appropriately.
936
root_entry = ((b'', b'', b'TREE_ROOT'), [(b'd', b'', 0, False, b'x'*32)])
937
self.assertEqual([root_entry], list(state._iter_entries()))
938
self.assertEqual(root_entry, state._get_entry(0, path_utf8=b''))
939
self.assertEqual(root_entry,
940
state._get_entry(0, fileid_utf8=b'TREE_ROOT'))
941
self.assertEqual((None, None),
942
state._get_entry(0, fileid_utf8=b'second-root-id'))
943
state.set_path_id(b'', b'second-root-id')
944
new_root_entry = ((b'', b'', b'second-root-id'),
945
[(b'd', b'', 0, False, b'x'*32)])
946
expected_rows = [new_root_entry]
947
self.assertEqual(expected_rows, list(state._iter_entries()))
948
self.assertEqual(new_root_entry, state._get_entry(0, path_utf8=b''))
949
self.assertEqual(new_root_entry,
950
state._get_entry(0, fileid_utf8=b'second-root-id'))
951
self.assertEqual((None, None),
952
state._get_entry(0, fileid_utf8=b'TREE_ROOT'))
953
# should work across save too
957
state = dirstate.DirState.on_file('dirstate')
961
self.assertEqual(expected_rows, list(state._iter_entries()))
965
def test_set_path_id_with_parents(self):
966
"""Set the root file id in a dirstate with parents"""
967
mt = self.make_branch_and_tree('mt')
968
# in case the default tree format uses a different root id
969
mt.set_root_id(b'TREE_ROOT')
970
mt.commit('foo', rev_id=b'parent-revid')
971
rt = mt.branch.repository.revision_tree(b'parent-revid')
972
state = dirstate.DirState.initialize('dirstate')
975
state.set_parent_trees([(b'parent-revid', rt)], ghosts=[])
976
root_entry = ((b'', b'', b'TREE_ROOT'),
977
[(b'd', b'', 0, False, b'x'*32),
978
(b'd', b'', 0, False, b'parent-revid')])
979
self.assertEqual(root_entry, state._get_entry(0, path_utf8=b''))
980
self.assertEqual(root_entry,
981
state._get_entry(0, fileid_utf8=b'TREE_ROOT'))
982
self.assertEqual((None, None),
983
state._get_entry(0, fileid_utf8=b'Asecond-root-id'))
984
state.set_path_id(b'', b'Asecond-root-id')
986
# now see that it is what we expected
987
old_root_entry = ((b'', b'', b'TREE_ROOT'),
988
[(b'a', b'', 0, False, b''),
989
(b'd', b'', 0, False, b'parent-revid')])
990
new_root_entry = ((b'', b'', b'Asecond-root-id'),
991
[(b'd', b'', 0, False, b''),
992
(b'a', b'', 0, False, b'')])
993
expected_rows = [new_root_entry, old_root_entry]
995
self.assertEqual(expected_rows, list(state._iter_entries()))
996
self.assertEqual(new_root_entry, state._get_entry(0, path_utf8=b''))
997
self.assertEqual(old_root_entry, state._get_entry(1, path_utf8=b''))
998
self.assertEqual((None, None),
999
state._get_entry(0, fileid_utf8=b'TREE_ROOT'))
1000
self.assertEqual(old_root_entry,
1001
state._get_entry(1, fileid_utf8=b'TREE_ROOT'))
1002
self.assertEqual(new_root_entry,
1003
state._get_entry(0, fileid_utf8=b'Asecond-root-id'))
1004
self.assertEqual((None, None),
1005
state._get_entry(1, fileid_utf8=b'Asecond-root-id'))
1006
# should work across save too
1010
# now flush & check we get the same
1011
state = dirstate.DirState.on_file('dirstate')
1015
self.assertEqual(expected_rows, list(state._iter_entries()))
1018
# now change within an existing file-backed state
1022
state.set_path_id(b'', b'tree-root-2')
1027
def test_set_parent_trees_no_content(self):
1028
# set_parent_trees is a slow but important api to support.
1029
tree1 = self.make_branch_and_memory_tree('tree1')
1033
revid1 = tree1.commit('foo')
1036
branch2 = tree1.branch.controldir.clone('tree2').open_branch()
1037
tree2 = memorytree.MemoryTree.create_on_branch(branch2)
1040
revid2 = tree2.commit('foo')
1041
root_id = tree2.get_root_id()
1044
state = dirstate.DirState.initialize('dirstate')
1046
state.set_path_id(b'', root_id)
1047
state.set_parent_trees(
1048
((revid1, tree1.branch.repository.revision_tree(revid1)),
1049
(revid2, tree2.branch.repository.revision_tree(revid2)),
1050
(b'ghost-rev', None)),
1052
# check we can reopen and use the dirstate after setting parent
1059
state = dirstate.DirState.on_file('dirstate')
1062
self.assertEqual([revid1, revid2, b'ghost-rev'],
1063
state.get_parent_ids())
1064
# iterating the entire state ensures that the state is parsable.
1065
list(state._iter_entries())
1066
# be sure that it sets not appends - change it
1067
state.set_parent_trees(
1068
((revid1, tree1.branch.repository.revision_tree(revid1)),
1069
(b'ghost-rev', None)),
1071
# and now put it back.
1072
state.set_parent_trees(
1073
((revid1, tree1.branch.repository.revision_tree(revid1)),
1074
(revid2, tree2.branch.repository.revision_tree(revid2)),
1075
(b'ghost-rev', tree2.branch.repository.revision_tree(
1076
_mod_revision.NULL_REVISION))),
1078
self.assertEqual([revid1, revid2, b'ghost-rev'],
1079
state.get_parent_ids())
1080
# the ghost should be recorded as such by set_parent_trees.
1081
self.assertEqual([b'ghost-rev'], state.get_ghosts())
1083
[((b'', b'', root_id), [
1084
(b'd', b'', 0, False, dirstate.DirState.NULLSTAT),
1085
(b'd', b'', 0, False, revid1),
1086
(b'd', b'', 0, False, revid1)
1088
list(state._iter_entries()))
1092
def test_set_parent_trees_file_missing_from_tree(self):
1093
# Adding a parent tree may reference files not in the current state.
1094
# they should get listed just once by id, even if they are in two
1096
# set_parent_trees is a slow but important api to support.
1097
tree1 = self.make_branch_and_memory_tree('tree1')
1101
tree1.add(['a file'], [b'file-id'], ['file'])
1102
tree1.put_file_bytes_non_atomic('a file', b'file-content')
1103
revid1 = tree1.commit('foo')
1106
branch2 = tree1.branch.controldir.clone('tree2').open_branch()
1107
tree2 = memorytree.MemoryTree.create_on_branch(branch2)
1110
tree2.put_file_bytes_non_atomic('a file', b'new file-content')
1111
revid2 = tree2.commit('foo')
1112
root_id = tree2.get_root_id()
1115
# check the layout in memory
1116
expected_result = [revid1, revid2], [
1117
((b'', b'', root_id), [
1118
(b'd', b'', 0, False, dirstate.DirState.NULLSTAT),
1119
(b'd', b'', 0, False, revid1),
1120
(b'd', b'', 0, False, revid1)
1122
((b'', b'a file', b'file-id'), [
1123
(b'a', b'', 0, False, b''),
1124
(b'f', b'2439573625385400f2a669657a7db6ae7515d371', 12, False,
1126
(b'f', b'542e57dc1cda4af37cb8e55ec07ce60364bb3c7d', 16, False,
1130
state = dirstate.DirState.initialize('dirstate')
1132
state.set_path_id(b'', root_id)
1133
state.set_parent_trees(
1134
((revid1, tree1.branch.repository.revision_tree(revid1)),
1135
(revid2, tree2.branch.repository.revision_tree(revid2)),
1141
# check_state_with_reopen will unlock
1142
self.check_state_with_reopen(expected_result, state)
1144
### add a path via _set_data - so we dont need delta work, just
1145
# raw data in, and ensure that it comes out via get_lines happily.
1147
def test_add_path_to_root_no_parents_all_data(self):
1148
# The most trivial addition of a path is when there are no parents and
1149
# its in the root and all data about the file is supplied
1150
self.build_tree(['a file'])
1151
stat = os.lstat('a file')
1152
# the 1*20 is the sha1 pretend value.
1153
state = dirstate.DirState.initialize('dirstate')
1154
expected_entries = [
1155
((b'', b'', b'TREE_ROOT'), [
1156
(b'd', b'', 0, False, dirstate.DirState.NULLSTAT), # current tree
1158
((b'', b'a file', b'a-file-id'), [
1159
(b'f', b'1'*20, 19, False, dirstate.pack_stat(stat)), # current tree
1163
state.add('a file', b'a-file-id', 'file', stat, b'1'*20)
1164
# having added it, it should be in the output of iter_entries.
1165
self.assertEqual(expected_entries, list(state._iter_entries()))
1166
# saving and reloading should not affect this.
1170
state = dirstate.DirState.on_file('dirstate')
1172
self.addCleanup(state.unlock)
1173
self.assertEqual(expected_entries, list(state._iter_entries()))
1175
def test_add_path_to_unversioned_directory(self):
1176
"""Adding a path to an unversioned directory should error.
1178
This is a duplicate of TestWorkingTree.test_add_in_unversioned,
1179
once dirstate is stable and if it is merged with WorkingTree3, consider
1180
removing this copy of the test.
1182
self.build_tree(['unversioned/', 'unversioned/a file'])
1183
state = dirstate.DirState.initialize('dirstate')
1184
self.addCleanup(state.unlock)
1185
self.assertRaises(errors.NotVersionedError, state.add,
1186
'unversioned/a file', b'a-file-id', 'file', None, None)
1188
def test_add_directory_to_root_no_parents_all_data(self):
1189
# The most trivial addition of a dir is when there are no parents and
1190
# its in the root and all data about the file is supplied
1191
self.build_tree(['a dir/'])
1192
stat = os.lstat('a dir')
1193
expected_entries = [
1194
((b'', b'', b'TREE_ROOT'), [
1195
(b'd', b'', 0, False, dirstate.DirState.NULLSTAT), # current tree
1197
((b'', b'a dir', b'a dir id'), [
1198
(b'd', b'', 0, False, dirstate.pack_stat(stat)), # current tree
1201
state = dirstate.DirState.initialize('dirstate')
1203
state.add('a dir', b'a dir id', 'directory', stat, None)
1204
# having added it, it should be in the output of iter_entries.
1205
self.assertEqual(expected_entries, list(state._iter_entries()))
1206
# saving and reloading should not affect this.
1210
state = dirstate.DirState.on_file('dirstate')
1212
self.addCleanup(state.unlock)
1214
self.assertEqual(expected_entries, list(state._iter_entries()))
1216
def _test_add_symlink_to_root_no_parents_all_data(self, link_name, target):
1217
# The most trivial addition of a symlink when there are no parents and
1218
# its in the root and all data about the file is supplied
1219
# bzr doesn't support fake symlinks on windows, yet.
1220
self.requireFeature(features.SymlinkFeature)
1221
os.symlink(target, link_name)
1222
stat = os.lstat(link_name)
1223
expected_entries = [
1224
((b'', b'', b'TREE_ROOT'), [
1225
(b'd', b'', 0, False, dirstate.DirState.NULLSTAT), # current tree
1227
((b'', link_name.encode('UTF-8'), b'a link id'), [
1228
(b'l', target.encode('UTF-8'), stat[6],
1229
False, dirstate.pack_stat(stat)), # current tree
1232
state = dirstate.DirState.initialize('dirstate')
1234
state.add(link_name, b'a link id', 'symlink', stat,
1235
target.encode('UTF-8'))
1236
# having added it, it should be in the output of iter_entries.
1237
self.assertEqual(expected_entries, list(state._iter_entries()))
1238
# saving and reloading should not affect this.
1242
state = dirstate.DirState.on_file('dirstate')
1244
self.addCleanup(state.unlock)
1245
self.assertEqual(expected_entries, list(state._iter_entries()))
1247
def test_add_symlink_to_root_no_parents_all_data(self):
1248
self._test_add_symlink_to_root_no_parents_all_data(u'a link', u'target')
1250
def test_add_symlink_unicode_to_root_no_parents_all_data(self):
1251
self.requireFeature(features.UnicodeFilenameFeature)
1252
self._test_add_symlink_to_root_no_parents_all_data(
1253
u'\N{Euro Sign}link', u'targ\N{Euro Sign}et')
1255
def test_add_directory_and_child_no_parents_all_data(self):
1256
# after adding a directory, we should be able to add children to it.
1257
self.build_tree(['a dir/', 'a dir/a file'])
1258
dirstat = os.lstat('a dir')
1259
filestat = os.lstat('a dir/a file')
1260
expected_entries = [
1261
((b'', b'', b'TREE_ROOT'), [
1262
(b'd', b'', 0, False, dirstate.DirState.NULLSTAT), # current tree
1264
((b'', b'a dir', b'a dir id'), [
1265
(b'd', b'', 0, False, dirstate.pack_stat(dirstat)), # current tree
1267
((b'a dir', b'a file', b'a-file-id'), [
1268
(b'f', b'1'*20, 25, False,
1269
dirstate.pack_stat(filestat)), # current tree details
1272
state = dirstate.DirState.initialize('dirstate')
1274
state.add('a dir', b'a dir id', 'directory', dirstat, None)
1275
state.add('a dir/a file', b'a-file-id', 'file', filestat, b'1'*20)
1276
# added it, it should be in the output of iter_entries.
1277
self.assertEqual(expected_entries, list(state._iter_entries()))
1278
# saving and reloading should not affect this.
1282
state = dirstate.DirState.on_file('dirstate')
1284
self.addCleanup(state.unlock)
1285
self.assertEqual(expected_entries, list(state._iter_entries()))
1287
def test_add_tree_reference(self):
1288
# make a dirstate and add a tree reference
1289
state = dirstate.DirState.initialize('dirstate')
1291
(b'', b'subdir', b'subdir-id'),
1292
[(b't', b'subtree-123123', 0, False,
1293
b'xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx')],
1296
state.add('subdir', b'subdir-id', 'tree-reference', None, b'subtree-123123')
1297
entry = state._get_entry(0, b'subdir-id', b'subdir')
1298
self.assertEqual(entry, expected_entry)
1303
# now check we can read it back
1305
self.addCleanup(state.unlock)
1307
entry2 = state._get_entry(0, b'subdir-id', b'subdir')
1308
self.assertEqual(entry, entry2)
1309
self.assertEqual(entry, expected_entry)
1310
# and lookup by id should work too
1311
entry2 = state._get_entry(0, fileid_utf8=b'subdir-id')
1312
self.assertEqual(entry, expected_entry)
1314
def test_add_forbidden_names(self):
1315
state = dirstate.DirState.initialize('dirstate')
1316
self.addCleanup(state.unlock)
1317
self.assertRaises(errors.BzrError,
1318
state.add, '.', b'ass-id', 'directory', None, None)
1319
self.assertRaises(errors.BzrError,
1320
state.add, '..', b'ass-id', 'directory', None, None)
1322
def test_set_state_with_rename_b_a_bug_395556(self):
1323
# bug 395556 uncovered a bug where the dirstate ends up with a false
1324
# relocation record - in a tree with no parents there should be no
1325
# absent or relocated records. This then leads to further corruption
1326
# when a commit occurs, as the incorrect relocation gathers an
1327
# incorrect absent in tree 1, and future changes go to pot.
1328
tree1 = self.make_branch_and_tree('tree1')
1329
self.build_tree(['tree1/b'])
1332
tree1.add(['b'], [b'b-id'])
1333
root_id = tree1.get_root_id()
1334
inv = tree1.root_inventory
1335
state = dirstate.DirState.initialize('dirstate')
1337
# Set the initial state with 'b'
1338
state.set_state_from_inventory(inv)
1339
inv.rename(b'b-id', root_id, 'a')
1340
# Set the new state with 'a', which currently corrupts.
1341
state.set_state_from_inventory(inv)
1342
expected_result1 = [(b'', b'', root_id, b'd'),
1343
(b'', b'a', b'b-id', b'f'),
1346
for entry in state._iter_entries():
1347
values.append(entry[0] + entry[1][0][:1])
1348
self.assertEqual(expected_result1, values)
1355
class TestDirStateHashUpdates(TestCaseWithDirState):
1357
def do_update_entry(self, state, path):
1358
entry = state._get_entry(0, path_utf8=path)
1359
stat = os.lstat(path)
1360
return dirstate.update_entry(state, entry, os.path.abspath(path), stat)
1362
def _read_state_content(self, state):
1363
"""Read the content of the dirstate file.
1365
On Windows when one process locks a file, you can't even open() the
1366
file in another process (to read it). So we go directly to
1367
state._state_file. This should always be the exact disk representation,
1368
so it is reasonable to do so.
1369
DirState also always seeks before reading, so it doesn't matter if we
1370
bump the file pointer.
1372
state._state_file.seek(0)
1373
return state._state_file.read()
1375
def test_worth_saving_limit_avoids_writing(self):
1376
tree = self.make_branch_and_tree('.')
1377
self.build_tree(['c', 'd'])
1379
tree.add(['c', 'd'], [b'c-id', b'd-id'])
1380
tree.commit('add c and d')
1381
state = InstrumentedDirState.on_file(tree.current_dirstate()._filename,
1382
worth_saving_limit=2)
1385
self.addCleanup(state.unlock)
1386
state._read_dirblocks_if_needed()
1387
state.adjust_time(+20) # Allow things to be cached
1388
self.assertEqual(dirstate.DirState.IN_MEMORY_UNMODIFIED,
1389
state._dirblock_state)
1390
content = self._read_state_content(state)
1391
self.do_update_entry(state, b'c')
1392
self.assertEqual(1, len(state._known_hash_changes))
1393
self.assertEqual(dirstate.DirState.IN_MEMORY_HASH_MODIFIED,
1394
state._dirblock_state)
1396
# It should not have set the state to IN_MEMORY_UNMODIFIED because the
1397
# hash values haven't been written out.
1398
self.assertEqual(dirstate.DirState.IN_MEMORY_HASH_MODIFIED,
1399
state._dirblock_state)
1400
self.assertEqual(content, self._read_state_content(state))
1401
self.assertEqual(dirstate.DirState.IN_MEMORY_HASH_MODIFIED,
1402
state._dirblock_state)
1403
self.do_update_entry(state, b'd')
1404
self.assertEqual(2, len(state._known_hash_changes))
1406
self.assertEqual(dirstate.DirState.IN_MEMORY_UNMODIFIED,
1407
state._dirblock_state)
1408
self.assertEqual(0, len(state._known_hash_changes))
1411
class TestGetLines(TestCaseWithDirState):
1413
def test_get_line_with_2_rows(self):
1414
state = self.create_dirstate_with_root_and_subdir()
1416
self.assertEqual([b'#bazaar dirstate flat format 3\n',
1417
b'crc32: 41262208\n',
1418
b'num_entries: 2\n',
1421
b'\x00\x00a-root-value\x00'
1422
b'd\x00\x000\x00n\x00AAAAREUHaIpFB2iKAAADAQAtkqUAAIGk\x00\n\x00'
1423
b'\x00subdir\x00subdir-id\x00'
1424
b'd\x00\x000\x00n\x00AAAAREUHaIpFB2iKAAADAQAtkqUAAIGk\x00\n\x00'
1425
], state.get_lines())
1429
def test_entry_to_line(self):
1430
state = self.create_dirstate_with_root()
1433
b'\x00\x00a-root-value\x00d\x00\x000\x00n'
1434
b'\x00AAAAREUHaIpFB2iKAAADAQAtkqUAAIGk',
1435
state._entry_to_line(state._dirblocks[0][1][0]))
1439
def test_entry_to_line_with_parent(self):
1440
packed_stat = b'AAAAREUHaIpFB2iKAAADAQAtkqUAAIGk'
1441
root_entry = (b'', b'', b'a-root-value'), [
1442
(b'd', b'', 0, False, packed_stat), # current tree details
1443
# first: a pointer to the current location
1444
(b'a', b'dirname/basename', 0, False, b''),
1446
state = dirstate.DirState.initialize('dirstate')
1449
b'\x00\x00a-root-value\x00'
1450
b'd\x00\x000\x00n\x00AAAAREUHaIpFB2iKAAADAQAtkqUAAIGk\x00'
1451
b'a\x00dirname/basename\x000\x00n\x00',
1452
state._entry_to_line(root_entry))
1456
def test_entry_to_line_with_two_parents_at_different_paths(self):
1457
# / in the tree, at / in one parent and /dirname/basename in the other.
1458
packed_stat = b'AAAAREUHaIpFB2iKAAADAQAtkqUAAIGk'
1459
root_entry = (b'', b'', b'a-root-value'), [
1460
(b'd', b'', 0, False, packed_stat), # current tree details
1461
(b'd', b'', 0, False, b'rev_id'), # first parent details
1462
# second: a pointer to the current location
1463
(b'a', b'dirname/basename', 0, False, b''),
1465
state = dirstate.DirState.initialize('dirstate')
1468
b'\x00\x00a-root-value\x00'
1469
b'd\x00\x000\x00n\x00AAAAREUHaIpFB2iKAAADAQAtkqUAAIGk\x00'
1470
b'd\x00\x000\x00n\x00rev_id\x00'
1471
b'a\x00dirname/basename\x000\x00n\x00',
1472
state._entry_to_line(root_entry))
1476
def test_iter_entries(self):
1477
# we should be able to iterate the dirstate entries from end to end
1478
# this is for get_lines to be easy to read.
1479
packed_stat = b'AAAAREUHaIpFB2iKAAADAQAtkqUAAIGk'
1481
root_entries = [((b'', b'', b'a-root-value'), [
1482
(b'd', b'', 0, False, packed_stat), # current tree details
1484
dirblocks.append(('', root_entries))
1485
# add two files in the root
1486
subdir_entry = (b'', b'subdir', b'subdir-id'), [
1487
(b'd', b'', 0, False, packed_stat), # current tree details
1489
afile_entry = (b'', b'afile', b'afile-id'), [
1490
(b'f', b'sha1value', 34, False, packed_stat), # current tree details
1492
dirblocks.append(('', [subdir_entry, afile_entry]))
1494
file_entry2 = (b'subdir', b'2file', b'2file-id'), [
1495
(b'f', b'sha1value', 23, False, packed_stat), # current tree details
1497
dirblocks.append(('subdir', [file_entry2]))
1498
state = dirstate.DirState.initialize('dirstate')
1500
state._set_data([], dirblocks)
1501
expected_entries = [root_entries[0], subdir_entry, afile_entry,
1503
self.assertEqual(expected_entries, list(state._iter_entries()))
1508
class TestGetBlockRowIndex(TestCaseWithDirState):
1510
def assertBlockRowIndexEqual(self, block_index, row_index, dir_present,
1511
file_present, state, dirname, basename, tree_index):
1512
self.assertEqual((block_index, row_index, dir_present, file_present),
1513
state._get_block_entry_index(dirname, basename, tree_index))
1515
block = state._dirblocks[block_index]
1516
self.assertEqual(dirname, block[0])
1517
if dir_present and file_present:
1518
row = state._dirblocks[block_index][1][row_index]
1519
self.assertEqual(dirname, row[0][0])
1520
self.assertEqual(basename, row[0][1])
1522
def test_simple_structure(self):
1523
state = self.create_dirstate_with_root_and_subdir()
1524
self.addCleanup(state.unlock)
1525
self.assertBlockRowIndexEqual(1, 0, True, True, state, b'', b'subdir', 0)
1526
self.assertBlockRowIndexEqual(1, 0, True, False, state, b'', b'bdir', 0)
1527
self.assertBlockRowIndexEqual(1, 1, True, False, state, b'', b'zdir', 0)
1528
self.assertBlockRowIndexEqual(2, 0, False, False, state, b'a', b'foo', 0)
1529
self.assertBlockRowIndexEqual(2, 0, False, False, state,
1530
b'subdir', b'foo', 0)
1532
def test_complex_structure_exists(self):
1533
state = self.create_complex_dirstate()
1534
self.addCleanup(state.unlock)
1535
# Make sure we can find everything that exists
1536
self.assertBlockRowIndexEqual(0, 0, True, True, state, b'', b'', 0)
1537
self.assertBlockRowIndexEqual(1, 0, True, True, state, b'', b'a', 0)
1538
self.assertBlockRowIndexEqual(1, 1, True, True, state, b'', b'b', 0)
1539
self.assertBlockRowIndexEqual(1, 2, True, True, state, b'', b'c', 0)
1540
self.assertBlockRowIndexEqual(1, 3, True, True, state, b'', b'd', 0)
1541
self.assertBlockRowIndexEqual(2, 0, True, True, state, b'a', b'e', 0)
1542
self.assertBlockRowIndexEqual(2, 1, True, True, state, b'a', b'f', 0)
1543
self.assertBlockRowIndexEqual(3, 0, True, True, state, b'b', b'g', 0)
1544
self.assertBlockRowIndexEqual(3, 1, True, True, state,
1545
b'b', b'h\xc3\xa5', 0)
1547
def test_complex_structure_missing(self):
1548
state = self.create_complex_dirstate()
1549
self.addCleanup(state.unlock)
1550
# Make sure things would be inserted in the right locations
1551
# '_' comes before 'a'
1552
self.assertBlockRowIndexEqual(0, 0, True, True, state, b'', b'', 0)
1553
self.assertBlockRowIndexEqual(1, 0, True, False, state, b'', b'_', 0)
1554
self.assertBlockRowIndexEqual(1, 1, True, False, state, b'', b'aa', 0)
1555
self.assertBlockRowIndexEqual(1, 4, True, False, state,
1556
b'', b'h\xc3\xa5', 0)
1557
self.assertBlockRowIndexEqual(2, 0, False, False, state, b'_', b'a', 0)
1558
self.assertBlockRowIndexEqual(3, 0, False, False, state, b'aa', b'a', 0)
1559
self.assertBlockRowIndexEqual(4, 0, False, False, state, b'bb', b'a', 0)
1560
# This would be inserted between a/ and b/
1561
self.assertBlockRowIndexEqual(3, 0, False, False, state, b'a/e', b'a', 0)
1563
self.assertBlockRowIndexEqual(4, 0, False, False, state, b'e', b'a', 0)
1566
class TestGetEntry(TestCaseWithDirState):
1568
def assertEntryEqual(self, dirname, basename, file_id, state, path, index):
1569
"""Check that the right entry is returned for a request to getEntry."""
1570
entry = state._get_entry(index, path_utf8=path)
1572
self.assertEqual((None, None), entry)
1575
self.assertEqual((dirname, basename, file_id), cur[:3])
1577
def test_simple_structure(self):
1578
state = self.create_dirstate_with_root_and_subdir()
1579
self.addCleanup(state.unlock)
1580
self.assertEntryEqual(b'', b'', b'a-root-value', state, b'', 0)
1581
self.assertEntryEqual(b'', b'subdir', b'subdir-id', state, b'subdir', 0)
1582
self.assertEntryEqual(None, None, None, state, b'missing', 0)
1583
self.assertEntryEqual(None, None, None, state, b'missing/foo', 0)
1584
self.assertEntryEqual(None, None, None, state, b'subdir/foo', 0)
1586
def test_complex_structure_exists(self):
1587
state = self.create_complex_dirstate()
1588
self.addCleanup(state.unlock)
1589
self.assertEntryEqual(b'', b'', b'a-root-value', state, b'', 0)
1590
self.assertEntryEqual(b'', b'a', b'a-dir', state, b'a', 0)
1591
self.assertEntryEqual(b'', b'b', b'b-dir', state, b'b', 0)
1592
self.assertEntryEqual(b'', b'c', b'c-file', state, b'c', 0)
1593
self.assertEntryEqual(b'', b'd', b'd-file', state, b'd', 0)
1594
self.assertEntryEqual(b'a', b'e', b'e-dir', state, b'a/e', 0)
1595
self.assertEntryEqual(b'a', b'f', b'f-file', state, b'a/f', 0)
1596
self.assertEntryEqual(b'b', b'g', b'g-file', state, b'b/g', 0)
1597
self.assertEntryEqual(b'b', b'h\xc3\xa5', b'h-\xc3\xa5-file', state,
1600
def test_complex_structure_missing(self):
1601
state = self.create_complex_dirstate()
1602
self.addCleanup(state.unlock)
1603
self.assertEntryEqual(None, None, None, state, b'_', 0)
1604
self.assertEntryEqual(None, None, None, state, b'_\xc3\xa5', 0)
1605
self.assertEntryEqual(None, None, None, state, b'a/b', 0)
1606
self.assertEntryEqual(None, None, None, state, b'c/d', 0)
1608
def test_get_entry_uninitialized(self):
1609
"""Calling get_entry will load data if it needs to"""
1610
state = self.create_dirstate_with_root()
1616
state = dirstate.DirState.on_file('dirstate')
1619
self.assertEqual(dirstate.DirState.NOT_IN_MEMORY,
1620
state._header_state)
1621
self.assertEqual(dirstate.DirState.NOT_IN_MEMORY,
1622
state._dirblock_state)
1623
self.assertEntryEqual(b'', b'', b'a-root-value', state, b'', 0)
1628
class TestIterChildEntries(TestCaseWithDirState):
1630
def create_dirstate_with_two_trees(self):
1631
"""This dirstate contains multiple files and directories.
1641
b/h\xc3\xa5 h-\xc3\xa5-file #This is u'\xe5' encoded into utf-8
1643
Notice that a/e is an empty directory.
1645
There is one parent tree, which has the same shape with the following variations:
1646
b/g in the parent is gone.
1647
b/h in the parent has a different id
1648
b/i is new in the parent
1649
c is renamed to b/j in the parent
1651
:return: The dirstate, still write-locked.
1653
packed_stat = b'AAAAREUHaIpFB2iKAAADAQAtkqUAAIGk'
1654
null_sha = b'xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx'
1655
NULL_PARENT_DETAILS = dirstate.DirState.NULL_PARENT_DETAILS
1656
root_entry = (b'', b'', b'a-root-value'), [
1657
(b'd', b'', 0, False, packed_stat),
1658
(b'd', b'', 0, False, b'parent-revid'),
1660
a_entry = (b'', b'a', b'a-dir'), [
1661
(b'd', b'', 0, False, packed_stat),
1662
(b'd', b'', 0, False, b'parent-revid'),
1664
b_entry = (b'', b'b', b'b-dir'), [
1665
(b'd', b'', 0, False, packed_stat),
1666
(b'd', b'', 0, False, b'parent-revid'),
1668
c_entry = (b'', b'c', b'c-file'), [
1669
(b'f', null_sha, 10, False, packed_stat),
1670
(b'r', b'b/j', 0, False, b''),
1672
d_entry = (b'', b'd', b'd-file'), [
1673
(b'f', null_sha, 20, False, packed_stat),
1674
(b'f', b'd', 20, False, b'parent-revid'),
1676
e_entry = (b'a', b'e', b'e-dir'), [
1677
(b'd', b'', 0, False, packed_stat),
1678
(b'd', b'', 0, False, b'parent-revid'),
1680
f_entry = (b'a', b'f', b'f-file'), [
1681
(b'f', null_sha, 30, False, packed_stat),
1682
(b'f', b'f', 20, False, b'parent-revid'),
1684
g_entry = (b'b', b'g', b'g-file'), [
1685
(b'f', null_sha, 30, False, packed_stat),
1686
NULL_PARENT_DETAILS,
1688
h_entry1 = (b'b', b'h\xc3\xa5', b'h-\xc3\xa5-file1'), [
1689
(b'f', null_sha, 40, False, packed_stat),
1690
NULL_PARENT_DETAILS,
1692
h_entry2 = (b'b', b'h\xc3\xa5', b'h-\xc3\xa5-file2'), [
1693
NULL_PARENT_DETAILS,
1694
(b'f', b'h', 20, False, b'parent-revid'),
1696
i_entry = (b'b', b'i', b'i-file'), [
1697
NULL_PARENT_DETAILS,
1698
(b'f', b'h', 20, False, b'parent-revid'),
1700
j_entry = (b'b', b'j', b'c-file'), [
1701
(b'r', b'c', 0, False, b''),
1702
(b'f', b'j', 20, False, b'parent-revid'),
1705
dirblocks.append((b'', [root_entry]))
1706
dirblocks.append((b'', [a_entry, b_entry, c_entry, d_entry]))
1707
dirblocks.append((b'a', [e_entry, f_entry]))
1708
dirblocks.append((b'b', [g_entry, h_entry1, h_entry2, i_entry, j_entry]))
1709
state = dirstate.DirState.initialize('dirstate')
1712
state._set_data([b'parent'], dirblocks)
1716
return state, dirblocks
1718
def test_iter_children_b(self):
1719
state, dirblocks = self.create_dirstate_with_two_trees()
1720
self.addCleanup(state.unlock)
1721
expected_result = []
1722
expected_result.append(dirblocks[3][1][2]) # h2
1723
expected_result.append(dirblocks[3][1][3]) # i
1724
expected_result.append(dirblocks[3][1][4]) # j
1725
self.assertEqual(expected_result,
1726
list(state._iter_child_entries(1, b'b')))
1728
def test_iter_child_root(self):
1729
state, dirblocks = self.create_dirstate_with_two_trees()
1730
self.addCleanup(state.unlock)
1731
expected_result = []
1732
expected_result.append(dirblocks[1][1][0]) # a
1733
expected_result.append(dirblocks[1][1][1]) # b
1734
expected_result.append(dirblocks[1][1][3]) # d
1735
expected_result.append(dirblocks[2][1][0]) # e
1736
expected_result.append(dirblocks[2][1][1]) # f
1737
expected_result.append(dirblocks[3][1][2]) # h2
1738
expected_result.append(dirblocks[3][1][3]) # i
1739
expected_result.append(dirblocks[3][1][4]) # j
1740
self.assertEqual(expected_result,
1741
list(state._iter_child_entries(1, b'')))
1744
class TestDirstateSortOrder(tests.TestCaseWithTransport):
1745
"""Test that DirState adds entries in the right order."""
1747
def test_add_sorting(self):
1748
"""Add entries in lexicographical order, we get path sorted order.
1750
This tests it to a depth of 4, to make sure we don't just get it right
1751
at a single depth. 'a/a' should come before 'a-a', even though it
1752
doesn't lexicographically.
1754
dirs = ['a', 'a/a', 'a/a/a', 'a/a/a/a',
1755
'a-a', 'a/a-a', 'a/a/a-a', 'a/a/a/a-a',
1758
state = dirstate.DirState.initialize('dirstate')
1759
self.addCleanup(state.unlock)
1761
fake_stat = os.stat('dirstate')
1763
d_id = d.encode('utf-8').replace(b'/', b'_')+b'-id'
1764
file_path = d + '/f'
1765
file_id = file_path.encode('utf-8').replace(b'/', b'_')+b'-id'
1766
state.add(d, d_id, 'directory', fake_stat, null_sha)
1767
state.add(file_path, file_id, 'file', fake_stat, null_sha)
1769
expected = [b'', b'', b'a',
1770
b'a/a', b'a/a/a', b'a/a/a/a',
1771
b'a/a/a/a-a', b'a/a/a-a', b'a/a-a', b'a-a',
1773
split = lambda p:p.split(b'/')
1774
self.assertEqual(sorted(expected, key=split), expected)
1775
dirblock_names = [d[0] for d in state._dirblocks]
1776
self.assertEqual(expected, dirblock_names)
1778
def test_set_parent_trees_correct_order(self):
1779
"""After calling set_parent_trees() we should maintain the order."""
1780
dirs = ['a', 'a-a', 'a/a']
1782
state = dirstate.DirState.initialize('dirstate')
1783
self.addCleanup(state.unlock)
1785
fake_stat = os.stat('dirstate')
1787
d_id = d.encode('utf-8').replace(b'/', b'_')+b'-id'
1788
file_path = d + '/f'
1789
file_id = file_path.encode('utf-8').replace(b'/', b'_')+b'-id'
1790
state.add(d, d_id, 'directory', fake_stat, null_sha)
1791
state.add(file_path, file_id, 'file', fake_stat, null_sha)
1793
expected = [b'', b'', b'a', b'a/a', b'a-a']
1794
dirblock_names = [d[0] for d in state._dirblocks]
1795
self.assertEqual(expected, dirblock_names)
1797
# *really* cheesy way to just get an empty tree
1798
repo = self.make_repository('repo')
1799
empty_tree = repo.revision_tree(_mod_revision.NULL_REVISION)
1800
state.set_parent_trees([('null:', empty_tree)], [])
1802
dirblock_names = [d[0] for d in state._dirblocks]
1803
self.assertEqual(expected, dirblock_names)
1806
class InstrumentedDirState(dirstate.DirState):
1807
"""An DirState with instrumented sha1 functionality."""
1809
def __init__(self, path, sha1_provider, worth_saving_limit=0):
1810
super(InstrumentedDirState, self).__init__(path, sha1_provider,
1811
worth_saving_limit=worth_saving_limit)
1812
self._time_offset = 0
1814
# member is dynamically set in DirState.__init__ to turn on trace
1815
self._sha1_provider = sha1_provider
1816
self._sha1_file = self._sha1_file_and_log
1818
def _sha_cutoff_time(self):
1819
timestamp = super(InstrumentedDirState, self)._sha_cutoff_time()
1820
self._cutoff_time = timestamp + self._time_offset
1822
def _sha1_file_and_log(self, abspath):
1823
self._log.append(('sha1', abspath))
1824
return self._sha1_provider.sha1(abspath)
1826
def _read_link(self, abspath, old_link):
1827
self._log.append(('read_link', abspath, old_link))
1828
return super(InstrumentedDirState, self)._read_link(abspath, old_link)
1830
def _lstat(self, abspath, entry):
1831
self._log.append(('lstat', abspath))
1832
return super(InstrumentedDirState, self)._lstat(abspath, entry)
1834
def _is_executable(self, mode, old_executable):
1835
self._log.append(('is_exec', mode, old_executable))
1836
return super(InstrumentedDirState, self)._is_executable(mode,
1839
def adjust_time(self, secs):
1840
"""Move the clock forward or back.
1842
:param secs: The amount to adjust the clock by. Positive values make it
1843
seem as if we are in the future, negative values make it seem like we
1846
self._time_offset += secs
1847
self._cutoff_time = None
1850
class _FakeStat(object):
1851
"""A class with the same attributes as a real stat result."""
1853
def __init__(self, size, mtime, ctime, dev, ino, mode):
1855
self.st_mtime = mtime
1856
self.st_ctime = ctime
1863
return _FakeStat(st.st_size, st.st_mtime, st.st_ctime, st.st_dev,
1864
st.st_ino, st.st_mode)
1867
class TestPackStat(tests.TestCaseWithTransport):
1869
def assertPackStat(self, expected, stat_value):
1870
"""Check the packed and serialized form of a stat value."""
1871
self.assertEqual(expected, dirstate.pack_stat(stat_value))
1873
def test_pack_stat_int(self):
1874
st = _FakeStat(6859, 1172758614, 1172758617, 777, 6499538, 0o100644)
1875
# Make sure that all parameters have an impact on the packed stat.
1876
self.assertPackStat(b'AAAay0Xm4FZF5uBZAAADCQBjLNIAAIGk', st)
1879
self.assertPackStat(b'AAAbWEXm4FZF5uBZAAADCQBjLNIAAIGk', st)
1880
st.st_mtime = 1172758620
1882
self.assertPackStat(b'AAAbWEXm4FxF5uBZAAADCQBjLNIAAIGk', st)
1883
st.st_ctime = 1172758630
1885
self.assertPackStat(b'AAAbWEXm4FxF5uBmAAADCQBjLNIAAIGk', st)
1888
self.assertPackStat(b'AAAbWEXm4FxF5uBmAAADeABjLNIAAIGk', st)
1891
self.assertPackStat(b'AAAbWEXm4FxF5uBmAAADeABjLNQAAIGk', st)
1892
st.st_mode = 0o100744
1894
self.assertPackStat(b'AAAbWEXm4FxF5uBmAAADeABjLNQAAIHk', st)
1896
def test_pack_stat_float(self):
1897
"""On some platforms mtime and ctime are floats.
1899
Make sure we don't get warnings or errors, and that we ignore changes <
1902
st = _FakeStat(7000, 1172758614.0, 1172758617.0,
1903
777, 6499538, 0o100644)
1904
# These should all be the same as the integer counterparts
1905
self.assertPackStat(b'AAAbWEXm4FZF5uBZAAADCQBjLNIAAIGk', st)
1906
st.st_mtime = 1172758620.0
1908
self.assertPackStat(b'AAAbWEXm4FxF5uBZAAADCQBjLNIAAIGk', st)
1909
st.st_ctime = 1172758630.0
1911
self.assertPackStat(b'AAAbWEXm4FxF5uBmAAADCQBjLNIAAIGk', st)
1912
# fractional seconds are discarded, so no change from above
1913
st.st_mtime = 1172758620.453
1914
self.assertPackStat(b'AAAbWEXm4FxF5uBmAAADCQBjLNIAAIGk', st)
1915
st.st_ctime = 1172758630.228
1916
self.assertPackStat(b'AAAbWEXm4FxF5uBmAAADCQBjLNIAAIGk', st)
1919
class TestBisect(TestCaseWithDirState):
1920
"""Test the ability to bisect into the disk format."""
1922
def assertBisect(self, expected_map, map_keys, state, paths):
1923
"""Assert that bisecting for paths returns the right result.
1925
:param expected_map: A map from key => entry value
1926
:param map_keys: The keys to expect for each path
1927
:param state: The DirState object.
1928
:param paths: A list of paths, these will automatically be split into
1929
(dir, name) tuples, and sorted according to how _bisect
1932
result = state._bisect(paths)
1933
# For now, results are just returned in whatever order we read them.
1934
# We could sort by (dir, name, file_id) or something like that, but in
1935
# the end it would still be fairly arbitrary, and we don't want the
1936
# extra overhead if we can avoid it. So sort everything to make sure
1938
self.assertEqual(len(map_keys), len(paths))
1940
for path, keys in zip(paths, map_keys):
1942
# This should not be present in the output
1944
expected[path] = sorted(expected_map[k] for k in keys)
1946
# The returned values are just arranged randomly based on when they
1947
# were read, for testing, make sure it is properly sorted.
1951
self.assertEqual(expected, result)
1953
def assertBisectDirBlocks(self, expected_map, map_keys, state, paths):
1954
"""Assert that bisecting for dirbblocks returns the right result.
1956
:param expected_map: A map from key => expected values
1957
:param map_keys: A nested list of paths we expect to be returned.
1958
Something like [['a', 'b', 'f'], ['b/c', 'b/d']]
1959
:param state: The DirState object.
1960
:param paths: A list of directories
1962
result = state._bisect_dirblocks(paths)
1963
self.assertEqual(len(map_keys), len(paths))
1965
for path, keys in zip(paths, map_keys):
1967
# This should not be present in the output
1969
expected[path] = sorted(expected_map[k] for k in keys)
1973
self.assertEqual(expected, result)
1975
def assertBisectRecursive(self, expected_map, map_keys, state, paths):
1976
"""Assert the return value of a recursive bisection.
1978
:param expected_map: A map from key => entry value
1979
:param map_keys: A list of paths we expect to be returned.
1980
Something like ['a', 'b', 'f', 'b/d', 'b/d2']
1981
:param state: The DirState object.
1982
:param paths: A list of files and directories. It will be broken up
1983
into (dir, name) pairs and sorted before calling _bisect_recursive.
1986
for key in map_keys:
1987
entry = expected_map[key]
1988
dir_name_id, trees_info = entry
1989
expected[dir_name_id] = trees_info
1991
result = state._bisect_recursive(paths)
1993
self.assertEqual(expected, result)
1995
def test_bisect_each(self):
1996
"""Find a single record using bisect."""
1997
tree, state, expected = self.create_basic_dirstate()
1999
# Bisect should return the rows for the specified files.
2000
self.assertBisect(expected, [[b'']], state, [b''])
2001
self.assertBisect(expected, [[b'a']], state, [b'a'])
2002
self.assertBisect(expected, [[b'b']], state, [b'b'])
2003
self.assertBisect(expected, [[b'b/c']], state, [b'b/c'])
2004
self.assertBisect(expected, [[b'b/d']], state, [b'b/d'])
2005
self.assertBisect(expected, [[b'b/d/e']], state, [b'b/d/e'])
2006
self.assertBisect(expected, [[b'b-c']], state, [b'b-c'])
2007
self.assertBisect(expected, [[b'f']], state, [b'f'])
2009
def test_bisect_multi(self):
2010
"""Bisect can be used to find multiple records at the same time."""
2011
tree, state, expected = self.create_basic_dirstate()
2012
# Bisect should be capable of finding multiple entries at the same time
2013
self.assertBisect(expected, [[b'a'], [b'b'], [b'f']],
2014
state, [b'a', b'b', b'f'])
2015
self.assertBisect(expected, [[b'f'], [b'b/d'], [b'b/d/e']],
2016
state, [b'f', b'b/d', b'b/d/e'])
2017
self.assertBisect(expected, [[b'b'], [b'b-c'], [b'b/c']],
2018
state, [b'b', b'b-c', b'b/c'])
2020
def test_bisect_one_page(self):
2021
"""Test bisect when there is only 1 page to read"""
2022
tree, state, expected = self.create_basic_dirstate()
2023
state._bisect_page_size = 5000
2024
self.assertBisect(expected, [[b'']], state, [b''])
2025
self.assertBisect(expected, [[b'a']], state, [b'a'])
2026
self.assertBisect(expected, [[b'b']], state, [b'b'])
2027
self.assertBisect(expected, [[b'b/c']], state, [b'b/c'])
2028
self.assertBisect(expected, [[b'b/d']], state, [b'b/d'])
2029
self.assertBisect(expected, [[b'b/d/e']], state, [b'b/d/e'])
2030
self.assertBisect(expected, [[b'b-c']], state, [b'b-c'])
2031
self.assertBisect(expected, [[b'f']], state, [b'f'])
2032
self.assertBisect(expected, [[b'a'], [b'b'], [b'f']],
2033
state, [b'a', b'b', b'f'])
2034
self.assertBisect(expected, [[b'b/d'], [b'b/d/e'], [b'f']],
2035
state, [b'b/d', b'b/d/e', b'f'])
2036
self.assertBisect(expected, [[b'b'], [b'b/c'], [b'b-c']],
2037
state, [b'b', b'b/c', b'b-c'])
2039
def test_bisect_duplicate_paths(self):
2040
"""When bisecting for a path, handle multiple entries."""
2041
tree, state, expected = self.create_duplicated_dirstate()
2043
# Now make sure that both records are properly returned.
2044
self.assertBisect(expected, [[b'']], state, [b''])
2045
self.assertBisect(expected, [[b'a', b'a2']], state, [b'a'])
2046
self.assertBisect(expected, [[b'b', b'b2']], state, [b'b'])
2047
self.assertBisect(expected, [[b'b/c', b'b/c2']], state, [b'b/c'])
2048
self.assertBisect(expected, [[b'b/d', b'b/d2']], state, [b'b/d'])
2049
self.assertBisect(expected, [[b'b/d/e', b'b/d/e2']],
2051
self.assertBisect(expected, [[b'b-c', b'b-c2']], state, [b'b-c'])
2052
self.assertBisect(expected, [[b'f', b'f2']], state, [b'f'])
2054
def test_bisect_page_size_too_small(self):
2055
"""If the page size is too small, we will auto increase it."""
2056
tree, state, expected = self.create_basic_dirstate()
2057
state._bisect_page_size = 50
2058
self.assertBisect(expected, [None], state, [b'b/e'])
2059
self.assertBisect(expected, [[b'a']], state, [b'a'])
2060
self.assertBisect(expected, [[b'b']], state, [b'b'])
2061
self.assertBisect(expected, [[b'b/c']], state, [b'b/c'])
2062
self.assertBisect(expected, [[b'b/d']], state, [b'b/d'])
2063
self.assertBisect(expected, [[b'b/d/e']], state, [b'b/d/e'])
2064
self.assertBisect(expected, [[b'b-c']], state, [b'b-c'])
2065
self.assertBisect(expected, [[b'f']], state, [b'f'])
2067
def test_bisect_missing(self):
2068
"""Test that bisect return None if it cannot find a path."""
2069
tree, state, expected = self.create_basic_dirstate()
2070
self.assertBisect(expected, [None], state, [b'foo'])
2071
self.assertBisect(expected, [None], state, [b'b/foo'])
2072
self.assertBisect(expected, [None], state, [b'bar/foo'])
2073
self.assertBisect(expected, [None], state, [b'b-c/foo'])
2075
self.assertBisect(expected, [[b'a'], None, [b'b/d']],
2076
state, [b'a', b'foo', b'b/d'])
2078
def test_bisect_rename(self):
2079
"""Check that we find a renamed row."""
2080
tree, state, expected = self.create_renamed_dirstate()
2082
# Search for the pre and post renamed entries
2083
self.assertBisect(expected, [[b'a']], state, [b'a'])
2084
self.assertBisect(expected, [[b'b/g']], state, [b'b/g'])
2085
self.assertBisect(expected, [[b'b/d']], state, [b'b/d'])
2086
self.assertBisect(expected, [[b'h']], state, [b'h'])
2088
# What about b/d/e? shouldn't that also get 2 directory entries?
2089
self.assertBisect(expected, [[b'b/d/e']], state, [b'b/d/e'])
2090
self.assertBisect(expected, [[b'h/e']], state, [b'h/e'])
2092
def test_bisect_dirblocks(self):
2093
tree, state, expected = self.create_duplicated_dirstate()
2094
self.assertBisectDirBlocks(expected,
2095
[[b'', b'a', b'a2', b'b', b'b2', b'b-c', b'b-c2', b'f', b'f2']],
2097
self.assertBisectDirBlocks(expected,
2098
[[b'b/c', b'b/c2', b'b/d', b'b/d2']], state, [b'b'])
2099
self.assertBisectDirBlocks(expected,
2100
[[b'b/d/e', b'b/d/e2']], state, [b'b/d'])
2101
self.assertBisectDirBlocks(expected,
2102
[[b'', b'a', b'a2', b'b', b'b2', b'b-c', b'b-c2', b'f', b'f2'],
2103
[b'b/c', b'b/c2', b'b/d', b'b/d2'],
2104
[b'b/d/e', b'b/d/e2'],
2105
], state, [b'', b'b', b'b/d'])
2107
def test_bisect_dirblocks_missing(self):
2108
tree, state, expected = self.create_basic_dirstate()
2109
self.assertBisectDirBlocks(expected, [[b'b/d/e'], None],
2110
state, [b'b/d', b'b/e'])
2111
# Files don't show up in this search
2112
self.assertBisectDirBlocks(expected, [None], state, [b'a'])
2113
self.assertBisectDirBlocks(expected, [None], state, [b'b/c'])
2114
self.assertBisectDirBlocks(expected, [None], state, [b'c'])
2115
self.assertBisectDirBlocks(expected, [None], state, [b'b/d/e'])
2116
self.assertBisectDirBlocks(expected, [None], state, [b'f'])
2118
def test_bisect_recursive_each(self):
2119
tree, state, expected = self.create_basic_dirstate()
2120
self.assertBisectRecursive(expected, [b'a'], state, [b'a'])
2121
self.assertBisectRecursive(expected, [b'b/c'], state, [b'b/c'])
2122
self.assertBisectRecursive(expected, [b'b/d/e'], state, [b'b/d/e'])
2123
self.assertBisectRecursive(expected, [b'b-c'], state, [b'b-c'])
2124
self.assertBisectRecursive(expected, [b'b/d', b'b/d/e'],
2126
self.assertBisectRecursive(expected, [b'b', b'b/c', b'b/d', b'b/d/e'],
2128
self.assertBisectRecursive(expected, [b'', b'a', b'b', b'b-c', b'f', b'b/c',
2132
def test_bisect_recursive_multiple(self):
2133
tree, state, expected = self.create_basic_dirstate()
2134
self.assertBisectRecursive(expected, [b'a', b'b/c'], state, [b'a', b'b/c'])
2135
self.assertBisectRecursive(expected, [b'b/d', b'b/d/e'],
2136
state, [b'b/d', b'b/d/e'])
2138
def test_bisect_recursive_missing(self):
2139
tree, state, expected = self.create_basic_dirstate()
2140
self.assertBisectRecursive(expected, [], state, [b'd'])
2141
self.assertBisectRecursive(expected, [], state, [b'b/e'])
2142
self.assertBisectRecursive(expected, [], state, [b'g'])
2143
self.assertBisectRecursive(expected, [b'a'], state, [b'a', b'g'])
2145
def test_bisect_recursive_renamed(self):
2146
tree, state, expected = self.create_renamed_dirstate()
2148
# Looking for either renamed item should find the other
2149
self.assertBisectRecursive(expected, [b'a', b'b/g'], state, [b'a'])
2150
self.assertBisectRecursive(expected, [b'a', b'b/g'], state, [b'b/g'])
2151
# Looking in the containing directory should find the rename target,
2152
# and anything in a subdir of the renamed target.
2153
self.assertBisectRecursive(expected, [b'a', b'b', b'b/c', b'b/d',
2154
b'b/d/e', b'b/g', b'h', b'h/e'],
2158
class TestDirstateValidation(TestCaseWithDirState):
2160
def test_validate_correct_dirstate(self):
2161
state = self.create_complex_dirstate()
2164
# and make sure we can also validate with a read lock
2171
def test_dirblock_not_sorted(self):
2172
tree, state, expected = self.create_renamed_dirstate()
2173
state._read_dirblocks_if_needed()
2174
last_dirblock = state._dirblocks[-1]
2175
# we're appending to the dirblock, but this name comes before some of
2176
# the existing names; that's wrong
2177
last_dirblock[1].append(
2178
((b'h', b'aaaa', b'a-id'),
2179
[(b'a', b'', 0, False, b''),
2180
(b'a', b'', 0, False, b'')]))
2181
e = self.assertRaises(AssertionError,
2183
self.assertContainsRe(str(e), 'not sorted')
2185
def test_dirblock_name_mismatch(self):
2186
tree, state, expected = self.create_renamed_dirstate()
2187
state._read_dirblocks_if_needed()
2188
last_dirblock = state._dirblocks[-1]
2189
# add an entry with the wrong directory name
2190
last_dirblock[1].append(
2191
((b'', b'z', b'a-id'),
2192
[(b'a', b'', 0, False, b''),
2193
(b'a', b'', 0, False, b'')]))
2194
e = self.assertRaises(AssertionError,
2196
self.assertContainsRe(str(e),
2197
"doesn't match directory name")
2199
def test_dirblock_missing_rename(self):
2200
tree, state, expected = self.create_renamed_dirstate()
2201
state._read_dirblocks_if_needed()
2202
last_dirblock = state._dirblocks[-1]
2203
# make another entry for a-id, without a correct 'r' pointer to
2204
# the real occurrence in the working tree
2205
last_dirblock[1].append(
2206
((b'h', b'z', b'a-id'),
2207
[(b'a', b'', 0, False, b''),
2208
(b'a', b'', 0, False, b'')]))
2209
e = self.assertRaises(AssertionError,
2211
self.assertContainsRe(str(e),
2212
'file a-id is absent in row')
2215
class TestDirstateTreeReference(TestCaseWithDirState):
2217
def test_reference_revision_is_none(self):
2218
tree = self.make_branch_and_tree('tree', format='development-subtree')
2219
subtree = self.make_branch_and_tree('tree/subtree',
2220
format='development-subtree')
2221
subtree.set_root_id(b'subtree')
2222
tree.add_reference(subtree)
2224
state = dirstate.DirState.from_tree(tree, 'dirstate')
2225
key = (b'', b'subtree', b'subtree')
2226
expected = (b'', [(key,
2227
[(b't', b'', 0, False, b'xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx')])])
2230
self.assertEqual(expected, state._find_block(key))
2235
class TestDiscardMergeParents(TestCaseWithDirState):
2237
def test_discard_no_parents(self):
2238
# This should be a no-op
2239
state = self.create_empty_dirstate()
2240
self.addCleanup(state.unlock)
2241
state._discard_merge_parents()
2244
def test_discard_one_parent(self):
2246
packed_stat = b'AAAAREUHaIpFB2iKAAADAQAtkqUAAIGk'
2247
root_entry_direntry = (b'', b'', b'a-root-value'), [
2248
(b'd', b'', 0, False, packed_stat),
2249
(b'd', b'', 0, False, packed_stat),
2252
dirblocks.append((b'', [root_entry_direntry]))
2253
dirblocks.append((b'', []))
2255
state = self.create_empty_dirstate()
2256
self.addCleanup(state.unlock)
2257
state._set_data([b'parent-id'], dirblocks[:])
2260
state._discard_merge_parents()
2262
self.assertEqual(dirblocks, state._dirblocks)
2264
def test_discard_simple(self):
2266
packed_stat = b'AAAAREUHaIpFB2iKAAADAQAtkqUAAIGk'
2267
root_entry_direntry = (b'', b'', b'a-root-value'), [
2268
(b'd', b'', 0, False, packed_stat),
2269
(b'd', b'', 0, False, packed_stat),
2270
(b'd', b'', 0, False, packed_stat),
2272
expected_root_entry_direntry = (b'', b'', b'a-root-value'), [
2273
(b'd', b'', 0, False, packed_stat),
2274
(b'd', b'', 0, False, packed_stat),
2277
dirblocks.append((b'', [root_entry_direntry]))
2278
dirblocks.append((b'', []))
2280
state = self.create_empty_dirstate()
2281
self.addCleanup(state.unlock)
2282
state._set_data([b'parent-id', b'merged-id'], dirblocks[:])
2285
# This should strip of the extra column
2286
state._discard_merge_parents()
2288
expected_dirblocks = [(b'', [expected_root_entry_direntry]), (b'', [])]
2289
self.assertEqual(expected_dirblocks, state._dirblocks)
2291
def test_discard_absent(self):
2292
"""If entries are only in a merge, discard should remove the entries"""
2293
null_stat = dirstate.DirState.NULLSTAT
2294
present_dir = (b'd', b'', 0, False, null_stat)
2295
present_file = (b'f', b'', 0, False, null_stat)
2296
absent = dirstate.DirState.NULL_PARENT_DETAILS
2297
root_key = (b'', b'', b'a-root-value')
2298
file_in_root_key = (b'', b'file-in-root', b'a-file-id')
2299
file_in_merged_key = (b'', b'file-in-merged', b'b-file-id')
2300
dirblocks = [(b'', [(root_key, [present_dir, present_dir, present_dir])]),
2301
(b'', [(file_in_merged_key,
2302
[absent, absent, present_file]),
2304
[present_file, present_file, present_file]),
2308
state = self.create_empty_dirstate()
2309
self.addCleanup(state.unlock)
2310
state._set_data([b'parent-id', b'merged-id'], dirblocks[:])
2313
exp_dirblocks = [(b'', [(root_key, [present_dir, present_dir])]),
2314
(b'', [(file_in_root_key,
2315
[present_file, present_file]),
2318
state._discard_merge_parents()
2320
self.assertEqual(exp_dirblocks, state._dirblocks)
2322
def test_discard_renamed(self):
2323
null_stat = dirstate.DirState.NULLSTAT
2324
present_dir = (b'd', b'', 0, False, null_stat)
2325
present_file = (b'f', b'', 0, False, null_stat)
2326
absent = dirstate.DirState.NULL_PARENT_DETAILS
2327
root_key = (b'', b'', b'a-root-value')
2328
file_in_root_key = (b'', b'file-in-root', b'a-file-id')
2329
# Renamed relative to parent
2330
file_rename_s_key = (b'', b'file-s', b'b-file-id')
2331
file_rename_t_key = (b'', b'file-t', b'b-file-id')
2332
# And one that is renamed between the parents, but absent in this
2333
key_in_1 = (b'', b'file-in-1', b'c-file-id')
2334
key_in_2 = (b'', b'file-in-2', b'c-file-id')
2337
(b'', [(root_key, [present_dir, present_dir, present_dir])]),
2339
[absent, present_file, (b'r', b'file-in-2', b'c-file-id')]),
2341
[absent, (b'r', b'file-in-1', b'c-file-id'), present_file]),
2343
[present_file, present_file, present_file]),
2345
[(b'r', b'file-t', b'b-file-id'), absent, present_file]),
2347
[present_file, absent, (b'r', b'file-s', b'b-file-id')]),
2351
(b'', [(root_key, [present_dir, present_dir])]),
2352
(b'', [(key_in_1, [absent, present_file]),
2353
(file_in_root_key, [present_file, present_file]),
2354
(file_rename_t_key, [present_file, absent]),
2357
state = self.create_empty_dirstate()
2358
self.addCleanup(state.unlock)
2359
state._set_data([b'parent-id', b'merged-id'], dirblocks[:])
2362
state._discard_merge_parents()
2364
self.assertEqual(exp_dirblocks, state._dirblocks)
2366
def test_discard_all_subdir(self):
2367
null_stat = dirstate.DirState.NULLSTAT
2368
present_dir = (b'd', b'', 0, False, null_stat)
2369
present_file = (b'f', b'', 0, False, null_stat)
2370
absent = dirstate.DirState.NULL_PARENT_DETAILS
2371
root_key = (b'', b'', b'a-root-value')
2372
subdir_key = (b'', b'sub', b'dir-id')
2373
child1_key = (b'sub', b'child1', b'child1-id')
2374
child2_key = (b'sub', b'child2', b'child2-id')
2375
child3_key = (b'sub', b'child3', b'child3-id')
2378
(b'', [(root_key, [present_dir, present_dir, present_dir])]),
2379
(b'', [(subdir_key, [present_dir, present_dir, present_dir])]),
2380
(b'sub', [(child1_key, [absent, absent, present_file]),
2381
(child2_key, [absent, absent, present_file]),
2382
(child3_key, [absent, absent, present_file]),
2386
(b'', [(root_key, [present_dir, present_dir])]),
2387
(b'', [(subdir_key, [present_dir, present_dir])]),
2390
state = self.create_empty_dirstate()
2391
self.addCleanup(state.unlock)
2392
state._set_data([b'parent-id', b'merged-id'], dirblocks[:])
2395
state._discard_merge_parents()
2397
self.assertEqual(exp_dirblocks, state._dirblocks)
2400
class Test_InvEntryToDetails(tests.TestCase):
2402
def assertDetails(self, expected, inv_entry):
2403
details = dirstate.DirState._inv_entry_to_details(inv_entry)
2404
self.assertEqual(expected, details)
2405
# details should always allow join() and always be a plain str when
2407
(minikind, fingerprint, size, executable, tree_data) = details
2408
self.assertIsInstance(minikind, bytes)
2409
self.assertIsInstance(fingerprint, bytes)
2410
self.assertIsInstance(tree_data, bytes)
2412
def test_unicode_symlink(self):
2413
inv_entry = inventory.InventoryLink(b'link-file-id',
2414
u'nam\N{Euro Sign}e',
2416
inv_entry.revision = b'link-revision-id'
2417
target = u'link-targ\N{Euro Sign}t'
2418
inv_entry.symlink_target = target
2419
self.assertDetails((b'l', target.encode('UTF-8'), 0, False,
2420
b'link-revision-id'), inv_entry)
2423
class TestSHA1Provider(tests.TestCaseInTempDir):
2425
def test_sha1provider_is_an_interface(self):
2426
p = dirstate.SHA1Provider()
2427
self.assertRaises(NotImplementedError, p.sha1, "foo")
2428
self.assertRaises(NotImplementedError, p.stat_and_sha1, "foo")
2430
def test_defaultsha1provider_sha1(self):
2431
text = b'test\r\nwith\nall\rpossible line endings\r\n'
2432
self.build_tree_contents([('foo', text)])
2433
expected_sha = osutils.sha_string(text)
2434
p = dirstate.DefaultSHA1Provider()
2435
self.assertEqual(expected_sha, p.sha1('foo'))
2437
def test_defaultsha1provider_stat_and_sha1(self):
2438
text = b'test\r\nwith\nall\rpossible line endings\r\n'
2439
self.build_tree_contents([('foo', text)])
2440
expected_sha = osutils.sha_string(text)
2441
p = dirstate.DefaultSHA1Provider()
2442
statvalue, sha1 = p.stat_and_sha1('foo')
2443
self.assertTrue(len(statvalue) >= 10)
2444
self.assertEqual(len(text), statvalue.st_size)
2445
self.assertEqual(expected_sha, sha1)
2448
class _Repo(object):
2449
"""A minimal api to get InventoryRevisionTree to work."""
2452
default_format = controldir.format_registry.make_controldir('default')
2453
self._format = default_format.repository_format
2455
def lock_read(self):
2462
class TestUpdateBasisByDelta(tests.TestCase):
2464
def path_to_ie(self, path, file_id, rev_id, dir_ids):
2465
if path.endswith('/'):
2470
dirname, basename = osutils.split(path)
2472
dir_id = dir_ids[dirname]
2474
dir_id = osutils.basename(dirname).encode('utf-8') + b'-id'
2476
ie = inventory.InventoryDirectory(file_id, basename, dir_id)
2477
dir_ids[path] = file_id
2479
ie = inventory.InventoryFile(file_id, basename, dir_id)
2482
ie.revision = rev_id
2485
def create_tree_from_shape(self, rev_id, shape):
2486
dir_ids = {'': b'root-id'}
2487
inv = inventory.Inventory(b'root-id', rev_id)
2490
path, file_id = info
2493
path, file_id, ie_rev_id = info
2495
# Replace the root entry
2496
del inv._byid[inv.root.file_id]
2497
inv.root.file_id = file_id
2498
inv._byid[file_id] = inv.root
2499
dir_ids[''] = file_id
2501
inv.add(self.path_to_ie(path, file_id, ie_rev_id, dir_ids))
2502
return inventorytree.InventoryRevisionTree(_Repo(), inv, rev_id)
2504
def create_empty_dirstate(self):
2505
fd, path = tempfile.mkstemp(prefix='bzr-dirstate')
2506
self.addCleanup(os.remove, path)
2508
state = dirstate.DirState.initialize(path)
2509
self.addCleanup(state.unlock)
2512
def create_inv_delta(self, delta, rev_id):
2513
"""Translate a 'delta shape' into an actual InventoryDelta"""
2514
dir_ids = {'': b'root-id'}
2516
for old_path, new_path, file_id in delta:
2517
if old_path is not None and old_path.endswith('/'):
2518
# Don't have to actually do anything for this, because only
2519
# new_path creates InventoryEntries
2520
old_path = old_path[:-1]
2521
if new_path is None: # Delete
2522
inv_delta.append((old_path, None, file_id, None))
2524
ie = self.path_to_ie(new_path, file_id, rev_id, dir_ids)
2525
inv_delta.append((old_path, new_path, file_id, ie))
2528
def assertUpdate(self, active, basis, target):
2529
"""Assert that update_basis_by_delta works how we want.
2531
Set up a DirState object with active_shape for tree 0, basis_shape for
2532
tree 1. Then apply the delta from basis_shape to target_shape,
2533
and assert that the DirState is still valid, and that its stored
2534
content matches the target_shape.
2536
active_tree = self.create_tree_from_shape(b'active', active)
2537
basis_tree = self.create_tree_from_shape(b'basis', basis)
2538
target_tree = self.create_tree_from_shape(b'target', target)
2539
state = self.create_empty_dirstate()
2540
state.set_state_from_scratch(active_tree.root_inventory,
2541
[(b'basis', basis_tree)], [])
2542
delta = target_tree.root_inventory._make_delta(
2543
basis_tree.root_inventory)
2544
state.update_basis_by_delta(delta, b'target')
2546
dirstate_tree = workingtree_4.DirStateRevisionTree(state,
2548
# The target now that delta has been applied should match the
2550
self.assertEqual([], list(dirstate_tree.iter_changes(target_tree)))
2551
# And the dirblock state should be identical to the state if we created
2553
state2 = self.create_empty_dirstate()
2554
state2.set_state_from_scratch(active_tree.root_inventory,
2555
[(b'target', target_tree)], [])
2556
self.assertEqual(state2._dirblocks, state._dirblocks)
2559
def assertBadDelta(self, active, basis, delta):
2560
"""Test that we raise InconsistentDelta when appropriate.
2562
:param active: The active tree shape
2563
:param basis: The basis tree shape
2564
:param delta: A description of the delta to apply. Similar to the form
2565
for regular inventory deltas, but omitting the InventoryEntry.
2566
So adding a file is: (None, 'path', b'file-id')
2567
Adding a directory is: (None, 'path/', b'dir-id')
2568
Renaming a dir is: ('old/', 'new/', b'dir-id')
2571
active_tree = self.create_tree_from_shape(b'active', active)
2572
basis_tree = self.create_tree_from_shape(b'basis', basis)
2573
inv_delta = self.create_inv_delta(delta, b'target')
2574
state = self.create_empty_dirstate()
2575
state.set_state_from_scratch(active_tree.root_inventory,
2576
[(b'basis', basis_tree)], [])
2577
self.assertRaises(errors.InconsistentDelta,
2578
state.update_basis_by_delta, inv_delta, b'target')
2580
## state.update_basis_by_delta(inv_delta, b'target')
2581
## except errors.InconsistentDelta, e:
2582
## import pdb; pdb.set_trace()
2584
## import pdb; pdb.set_trace()
2585
self.assertTrue(state._changes_aborted)
2587
def test_remove_file_matching_active_state(self):
2588
state = self.assertUpdate(
2590
basis =[('file', b'file-id')],
2594
def test_remove_file_present_in_active_state(self):
2595
state = self.assertUpdate(
2596
active=[('file', b'file-id')],
2597
basis =[('file', b'file-id')],
2601
def test_remove_file_present_elsewhere_in_active_state(self):
2602
state = self.assertUpdate(
2603
active=[('other-file', b'file-id')],
2604
basis =[('file', b'file-id')],
2608
def test_remove_file_active_state_has_diff_file(self):
2609
state = self.assertUpdate(
2610
active=[('file', b'file-id-2')],
2611
basis =[('file', b'file-id')],
2615
def test_remove_file_active_state_has_diff_file_and_file_elsewhere(self):
2616
state = self.assertUpdate(
2617
active=[('file', b'file-id-2'),
2618
('other-file', b'file-id')],
2619
basis =[('file', b'file-id')],
2623
def test_add_file_matching_active_state(self):
2624
state = self.assertUpdate(
2625
active=[('file', b'file-id')],
2627
target=[('file', b'file-id')],
2630
def test_add_file_in_empty_dir_not_matching_active_state(self):
2631
state = self.assertUpdate(
2633
basis=[('dir/', b'dir-id')],
2634
target=[('dir/', b'dir-id', b'basis'), ('dir/file', b'file-id')],
2637
def test_add_file_missing_in_active_state(self):
2638
state = self.assertUpdate(
2641
target=[('file', b'file-id')],
2644
def test_add_file_elsewhere_in_active_state(self):
2645
state = self.assertUpdate(
2646
active=[('other-file', b'file-id')],
2648
target=[('file', b'file-id')],
2651
def test_add_file_active_state_has_diff_file_and_file_elsewhere(self):
2652
state = self.assertUpdate(
2653
active=[('other-file', b'file-id'),
2654
('file', b'file-id-2')],
2656
target=[('file', b'file-id')],
2659
def test_rename_file_matching_active_state(self):
2660
state = self.assertUpdate(
2661
active=[('other-file', b'file-id')],
2662
basis =[('file', b'file-id')],
2663
target=[('other-file', b'file-id')],
2666
def test_rename_file_missing_in_active_state(self):
2667
state = self.assertUpdate(
2669
basis =[('file', b'file-id')],
2670
target=[('other-file', b'file-id')],
2673
def test_rename_file_present_elsewhere_in_active_state(self):
2674
state = self.assertUpdate(
2675
active=[('third', b'file-id')],
2676
basis =[('file', b'file-id')],
2677
target=[('other-file', b'file-id')],
2680
def test_rename_file_active_state_has_diff_source_file(self):
2681
state = self.assertUpdate(
2682
active=[('file', b'file-id-2')],
2683
basis =[('file', b'file-id')],
2684
target=[('other-file', b'file-id')],
2687
def test_rename_file_active_state_has_diff_target_file(self):
2688
state = self.assertUpdate(
2689
active=[('other-file', b'file-id-2')],
2690
basis =[('file', b'file-id')],
2691
target=[('other-file', b'file-id')],
2694
def test_rename_file_active_has_swapped_files(self):
2695
state = self.assertUpdate(
2696
active=[('file', b'file-id'),
2697
('other-file', b'file-id-2')],
2698
basis= [('file', b'file-id'),
2699
('other-file', b'file-id-2')],
2700
target=[('file', b'file-id-2'),
2701
('other-file', b'file-id')])
2703
def test_rename_file_basis_has_swapped_files(self):
2704
state = self.assertUpdate(
2705
active=[('file', b'file-id'),
2706
('other-file', b'file-id-2')],
2707
basis= [('file', b'file-id-2'),
2708
('other-file', b'file-id')],
2709
target=[('file', b'file-id'),
2710
('other-file', b'file-id-2')])
2712
def test_rename_directory_with_contents(self):
2713
state = self.assertUpdate( # active matches basis
2714
active=[('dir1/', b'dir-id'),
2715
('dir1/file', b'file-id')],
2716
basis= [('dir1/', b'dir-id'),
2717
('dir1/file', b'file-id')],
2718
target=[('dir2/', b'dir-id'),
2719
('dir2/file', b'file-id')])
2720
state = self.assertUpdate( # active matches target
2721
active=[('dir2/', b'dir-id'),
2722
('dir2/file', b'file-id')],
2723
basis= [('dir1/', b'dir-id'),
2724
('dir1/file', b'file-id')],
2725
target=[('dir2/', b'dir-id'),
2726
('dir2/file', b'file-id')])
2727
state = self.assertUpdate( # active empty
2729
basis= [('dir1/', b'dir-id'),
2730
('dir1/file', b'file-id')],
2731
target=[('dir2/', b'dir-id'),
2732
('dir2/file', b'file-id')])
2733
state = self.assertUpdate( # active present at other location
2734
active=[('dir3/', b'dir-id'),
2735
('dir3/file', b'file-id')],
2736
basis= [('dir1/', b'dir-id'),
2737
('dir1/file', b'file-id')],
2738
target=[('dir2/', b'dir-id'),
2739
('dir2/file', b'file-id')])
2740
state = self.assertUpdate( # active has different ids
2741
active=[('dir1/', b'dir1-id'),
2742
('dir1/file', b'file1-id'),
2743
('dir2/', b'dir2-id'),
2744
('dir2/file', b'file2-id')],
2745
basis= [('dir1/', b'dir-id'),
2746
('dir1/file', b'file-id')],
2747
target=[('dir2/', b'dir-id'),
2748
('dir2/file', b'file-id')])
2750
def test_invalid_file_not_present(self):
2751
state = self.assertBadDelta(
2752
active=[('file', b'file-id')],
2753
basis= [('file', b'file-id')],
2754
delta=[('other-file', 'file', b'file-id')])
2756
def test_invalid_new_id_same_path(self):
2757
# The bad entry comes after
2758
state = self.assertBadDelta(
2759
active=[('file', b'file-id')],
2760
basis= [('file', b'file-id')],
2761
delta=[(None, 'file', b'file-id-2')])
2762
# The bad entry comes first
2763
state = self.assertBadDelta(
2764
active=[('file', b'file-id-2')],
2765
basis=[('file', b'file-id-2')],
2766
delta=[(None, 'file', b'file-id')])
2768
def test_invalid_existing_id(self):
2769
state = self.assertBadDelta(
2770
active=[('file', b'file-id')],
2771
basis= [('file', b'file-id')],
2772
delta=[(None, 'file', b'file-id')])
2774
def test_invalid_parent_missing(self):
2775
state = self.assertBadDelta(
2778
delta=[(None, 'path/path2', b'file-id')])
2779
# Note: we force the active tree to have the directory, by knowing how
2780
# path_to_ie handles entries with missing parents
2781
state = self.assertBadDelta(
2782
active=[('path/', b'path-id')],
2784
delta=[(None, 'path/path2', b'file-id')])
2785
state = self.assertBadDelta(
2786
active=[('path/', b'path-id'),
2787
('path/path2', b'file-id')],
2789
delta=[(None, 'path/path2', b'file-id')])
2791
def test_renamed_dir_same_path(self):
2792
# We replace the parent directory, with another parent dir. But the C
2793
# file doesn't look like it has been moved.
2794
state = self.assertUpdate(# Same as basis
2795
active=[('dir/', b'A-id'),
2796
('dir/B', b'B-id')],
2797
basis= [('dir/', b'A-id'),
2798
('dir/B', b'B-id')],
2799
target=[('dir/', b'C-id'),
2800
('dir/B', b'B-id')])
2801
state = self.assertUpdate(# Same as target
2802
active=[('dir/', b'C-id'),
2803
('dir/B', b'B-id')],
2804
basis= [('dir/', b'A-id'),
2805
('dir/B', b'B-id')],
2806
target=[('dir/', b'C-id'),
2807
('dir/B', b'B-id')])
2808
state = self.assertUpdate(# empty active
2810
basis= [('dir/', b'A-id'),
2811
('dir/B', b'B-id')],
2812
target=[('dir/', b'C-id'),
2813
('dir/B', b'B-id')])
2814
state = self.assertUpdate(# different active
2815
active=[('dir/', b'D-id'),
2816
('dir/B', b'B-id')],
2817
basis= [('dir/', b'A-id'),
2818
('dir/B', b'B-id')],
2819
target=[('dir/', b'C-id'),
2820
('dir/B', b'B-id')])
2822
def test_parent_child_swap(self):
2823
state = self.assertUpdate(# Same as basis
2824
active=[('A/', b'A-id'),
2826
('A/B/C', b'C-id')],
2827
basis= [('A/', b'A-id'),
2829
('A/B/C', b'C-id')],
2830
target=[('A/', b'B-id'),
2832
('A/B/C', b'C-id')])
2833
state = self.assertUpdate(# Same as target
2834
active=[('A/', b'B-id'),
2836
('A/B/C', b'C-id')],
2837
basis= [('A/', b'A-id'),
2839
('A/B/C', b'C-id')],
2840
target=[('A/', b'B-id'),
2842
('A/B/C', b'C-id')])
2843
state = self.assertUpdate(# empty active
2845
basis= [('A/', b'A-id'),
2847
('A/B/C', b'C-id')],
2848
target=[('A/', b'B-id'),
2850
('A/B/C', b'C-id')])
2851
state = self.assertUpdate(# different active
2852
active=[('D/', b'A-id'),
2855
basis= [('A/', b'A-id'),
2857
('A/B/C', b'C-id')],
2858
target=[('A/', b'B-id'),
2860
('A/B/C', b'C-id')])
2862
def test_change_root_id(self):
2863
state = self.assertUpdate( # same as basis
2864
active=[('', b'root-id'),
2865
('file', b'file-id')],
2866
basis= [('', b'root-id'),
2867
('file', b'file-id')],
2868
target=[('', b'target-root-id'),
2869
('file', b'file-id')])
2870
state = self.assertUpdate( # same as target
2871
active=[('', b'target-root-id'),
2872
('file', b'file-id')],
2873
basis= [('', b'root-id'),
2874
('file', b'file-id')],
2875
target=[('', b'target-root-id'),
2876
('file', b'root-id')])
2877
state = self.assertUpdate( # all different
2878
active=[('', b'active-root-id'),
2879
('file', b'file-id')],
2880
basis= [('', b'root-id'),
2881
('file', b'file-id')],
2882
target=[('', b'target-root-id'),
2883
('file', b'root-id')])
2885
def test_change_file_absent_in_active(self):
2886
state = self.assertUpdate(
2888
basis= [('file', b'file-id')],
2889
target=[('file', b'file-id')])
2891
def test_invalid_changed_file(self):
2892
state = self.assertBadDelta( # Not present in basis
2893
active=[('file', b'file-id')],
2895
delta=[('file', 'file', b'file-id')])
2896
state = self.assertBadDelta( # present at another location in basis
2897
active=[('file', b'file-id')],
2898
basis= [('other-file', b'file-id')],
2899
delta=[('file', 'file', b'file-id')])