1# Copyright (C) 2006-2011 Canonical Ltd 2# 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. 7# 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. 12# 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 16 17"""Tests of the dirstate functionality being built for WorkingTreeFormat4.""" 18 19import os 20import tempfile 21 22from ... import ( 23 controldir, 24 errors, 25 memorytree, 26 osutils, 27 revision as _mod_revision, 28 revisiontree, 29 tests, 30 ) 31from .. import ( 32 dirstate, 33 inventory, 34 inventorytree, 35 workingtree_4, 36 ) 37from ...tests import ( 38 features, 39 test_osutils, 40 ) 41from ...tests.scenarios import load_tests_apply_scenarios 42 43 44# TODO: 45# TESTS to write: 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 55 56 57class TestErrors(tests.TestCase): 58 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\"", 64 str(error)) 65 66 67load_tests = load_tests_apply_scenarios 68 69 70class TestCaseWithDirState(tests.TestCaseWithTransport): 71 """Helper functions for creating DirState objects with various content.""" 72 73 scenarios = test_osutils.dir_reader_scenarios() 74 75 # Set by load_tests 76 _dir_reader_class = None 77 _native_to_unicode = None # Not used yet 78 79 def setUp(self): 80 super(TestCaseWithDirState, self).setUp() 81 self.overrideAttr(osutils, 82 '_selected_dir_reader', self._dir_reader_class()) 83 84 def create_empty_dirstate(self): 85 """Return a locked but empty dirstate""" 86 state = dirstate.DirState.initialize('dirstate') 87 return state 88 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), 94 ] 95 dirblocks = [] 96 dirblocks.append((b'', [root_entry_direntry])) 97 dirblocks.append((b'', [])) 98 state = self.create_empty_dirstate() 99 try: 100 state._set_data([], dirblocks) 101 state._validate() 102 except: 103 state.unlock() 104 raise 105 return state 106 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), 112 ] 113 state = self.create_dirstate_with_root() 114 try: 115 dirblocks = list(state._dirblocks) 116 dirblocks[1][1].append(subdir_entry) 117 state._set_data([], dirblocks) 118 except: 119 state.unlock() 120 raise 121 return state 122 123 def create_complex_dirstate(self): 124 """This dirstate contains multiple files and directories. 125 126 / a-root-value 127 a/ a-dir 128 b/ b-dir 129 c c-file 130 d d-file 131 a/e/ e-dir 132 a/f f-file 133 b/g g-file 134 b/h\xc3\xa5 h-\xc3\xa5-file #This is u'\xe5' encoded into utf-8 135 136 Notice that a/e is an empty directory. 137 138 :return: The dirstate, still write-locked. 139 """ 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), 144 ] 145 a_entry = (b'', b'a', b'a-dir'), [ 146 (b'd', b'', 0, False, packed_stat), 147 ] 148 b_entry = (b'', b'b', b'b-dir'), [ 149 (b'd', b'', 0, False, packed_stat), 150 ] 151 c_entry = (b'', b'c', b'c-file'), [ 152 (b'f', null_sha, 10, False, packed_stat), 153 ] 154 d_entry = (b'', b'd', b'd-file'), [ 155 (b'f', null_sha, 20, False, packed_stat), 156 ] 157 e_entry = (b'a', b'e', b'e-dir'), [ 158 (b'd', b'', 0, False, packed_stat), 159 ] 160 f_entry = (b'a', b'f', b'f-file'), [ 161 (b'f', null_sha, 30, False, packed_stat), 162 ] 163 g_entry = (b'b', b'g', b'g-file'), [ 164 (b'f', null_sha, 30, False, packed_stat), 165 ] 166 h_entry = (b'b', b'h\xc3\xa5', b'h-\xc3\xa5-file'), [ 167 (b'f', null_sha, 40, False, packed_stat), 168 ] 169 dirblocks = [] 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') 175 state._validate() 176 try: 177 state._set_data([], dirblocks) 178 except: 179 state.unlock() 180 raise 181 return state 182 183 def check_state_with_reopen(self, expected_result, state): 184 """Check that state has current state expected_result. 185 186 This will check the current state, open the file anew and check it 187 again. 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 193 read => shared lock 194 """ 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) 198 try: 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())) 204 state.save() 205 finally: 206 state.unlock() 207 del state 208 state = dirstate.DirState.on_file('dirstate') 209 state.lock_read() 210 try: 211 self.assertEqual(expected_result[1], list(state._iter_entries())) 212 finally: 213 state.unlock() 214 215 def create_basic_dirstate(self): 216 """Create a dirstate with a few files and directories. 217 218 a 219 b/ 220 c 221 d/ 222 e 223 b-c 224 f 225 """ 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', 229 b'd-id', b'e-id', b'b-c-id', b'f-id'] 230 self.build_tree(['tree/' + p for p in paths]) 231 tree.set_root_id(b'TREE_ROOT') 232 tree.add([p.rstrip('/') for p in paths], file_ids) 233 tree.commit('initial', rev_id=b'rev-1') 234 revision_id = b'rev-1' 235 # a_packed_stat = dirstate.pack_stat(os.stat('tree/a')) 236 t = self.get_transport('tree') 237 a_text = t.get_bytes('a') 238 a_sha = osutils.sha_string(a_text) 239 a_len = len(a_text) 240 # b_packed_stat = dirstate.pack_stat(os.stat('tree/b')) 241 # c_packed_stat = dirstate.pack_stat(os.stat('tree/b/c')) 242 c_text = t.get_bytes('b/c') 243 c_sha = osutils.sha_string(c_text) 244 c_len = len(c_text) 245 # d_packed_stat = dirstate.pack_stat(os.stat('tree/b/d')) 246 # e_packed_stat = dirstate.pack_stat(os.stat('tree/b/d/e')) 247 e_text = t.get_bytes('b/d/e') 248 e_sha = osutils.sha_string(e_text) 249 e_len = len(e_text) 250 b_c_text = t.get_bytes('b-c') 251 b_c_sha = osutils.sha_string(b_c_text) 252 b_c_len = len(b_c_text) 253 # f_packed_stat = dirstate.pack_stat(os.stat('tree/f')) 254 f_text = t.get_bytes('f') 255 f_sha = osutils.sha_string(f_text) 256 f_len = len(f_text) 257 null_stat = dirstate.DirState.NULLSTAT 258 expected = { 259 b'': ((b'', b'', b'TREE_ROOT'), [ 260 (b'd', b'', 0, False, null_stat), 261 (b'd', b'', 0, False, revision_id), 262 ]), 263 b'a': ((b'', b'a', b'a-id'), [ 264 (b'f', b'', 0, False, null_stat), 265 (b'f', a_sha, a_len, False, revision_id), 266 ]), 267 b'b': ((b'', b'b', b'b-id'), [ 268 (b'd', b'', 0, False, null_stat), 269 (b'd', b'', 0, False, revision_id), 270 ]), 271 b'b/c': ((b'b', b'c', b'c-id'), [ 272 (b'f', b'', 0, False, null_stat), 273 (b'f', c_sha, c_len, False, revision_id), 274 ]), 275 b'b/d': ((b'b', b'd', b'd-id'), [ 276 (b'd', b'', 0, False, null_stat), 277 (b'd', b'', 0, False, revision_id), 278 ]), 279 b'b/d/e': ((b'b/d', b'e', b'e-id'), [ 280 (b'f', b'', 0, False, null_stat), 281 (b'f', e_sha, e_len, False, revision_id), 282 ]), 283 b'b-c': ((b'', b'b-c', b'b-c-id'), [ 284 (b'f', b'', 0, False, null_stat), 285 (b'f', b_c_sha, b_c_len, False, revision_id), 286 ]), 287 b'f': ((b'', b'f', b'f-id'), [ 288 (b'f', b'', 0, False, null_stat), 289 (b'f', f_sha, f_len, False, revision_id), 290 ]), 291 } 292 state = dirstate.DirState.from_tree(tree, 'dirstate') 293 try: 294 state.save() 295 finally: 296 state.unlock() 297 # Use a different object, to make sure nothing is pre-cached in memory. 298 state = dirstate.DirState.on_file('dirstate') 299 state.lock_read() 300 self.addCleanup(state.unlock) 301 self.assertEqual(dirstate.DirState.NOT_IN_MEMORY, 302 state._dirblock_state) 303 # This is code is only really tested if we actually have to make more 304 # than one read, so set the page size to something smaller. 305 # We want it to contain about 2.2 records, so that we have a couple 306 # records that we can read per attempt 307 state._bisect_page_size = 200 308 return tree, state, expected 309 310 def create_duplicated_dirstate(self): 311 """Create a dirstate with a deleted and added entries. 312 313 This grabs a basic_dirstate, and then removes and re adds every entry 314 with a new file id. 315 """ 316 tree, state, expected = self.create_basic_dirstate() 317 # Now we will just remove and add every file so we get an extra entry 318 # per entry. Unversion in reverse order so we handle subdirs 319 tree.unversion(['f', 'b-c', 'b/d/e', 'b/d', 'b/c', 'b', 'a']) 320 tree.add(['a', 'b', 'b/c', 'b/d', 'b/d/e', 'b-c', 'f'], 321 [b'a-id2', b'b-id2', b'c-id2', b'd-id2', b'e-id2', b'b-c-id2', b'f-id2']) 322 323 # Update the expected dictionary. 324 for path in [b'a', b'b', b'b/c', b'b/d', b'b/d/e', b'b-c', b'f']: 325 orig = expected[path] 326 path2 = path + b'2' 327 # This record was deleted in the current tree 328 expected[path] = (orig[0], [dirstate.DirState.NULL_PARENT_DETAILS, 329 orig[1][1]]) 330 new_key = (orig[0][0], orig[0][1], orig[0][2] + b'2') 331 # And didn't exist in the basis tree 332 expected[path2] = (new_key, [orig[1][0], 333 dirstate.DirState.NULL_PARENT_DETAILS]) 334 335 # We will replace the 'dirstate' file underneath 'state', but that is 336 # okay as lock as we unlock 'state' first. 337 state.unlock() 338 try: 339 new_state = dirstate.DirState.from_tree(tree, 'dirstate') 340 try: 341 new_state.save() 342 finally: 343 new_state.unlock() 344 finally: 345 # But we need to leave state in a read-lock because we already have 346 # a cleanup scheduled 347 state.lock_read() 348 return tree, state, expected 349 350 def create_renamed_dirstate(self): 351 """Create a dirstate with a few internal renames. 352 353 This takes the basic dirstate, and moves the paths around. 354 """ 355 tree, state, expected = self.create_basic_dirstate() 356 # Rename a file 357 tree.rename_one('a', 'b/g') 358 # And a directory 359 tree.rename_one('b/d', 'h') 360 361 old_a = expected[b'a'] 362 expected[b'a'] = ( 363 old_a[0], [(b'r', b'b/g', 0, False, b''), old_a[1][1]]) 364 expected[b'b/g'] = ((b'b', b'g', b'a-id'), [old_a[1][0], 365 (b'r', b'a', 0, False, b'')]) 366 old_d = expected[b'b/d'] 367 expected[b'b/d'] = (old_d[0], 368 [(b'r', b'h', 0, False, b''), old_d[1][1]]) 369 expected[b'h'] = ((b'', b'h', b'd-id'), [old_d[1][0], 370 (b'r', b'b/d', 0, False, b'')]) 371 372 old_e = expected[b'b/d/e'] 373 expected[b'b/d/e'] = (old_e[0], [(b'r', b'h/e', 0, False, b''), 374 old_e[1][1]]) 375 expected[b'h/e'] = ((b'h', b'e', b'e-id'), [old_e[1][0], 376 (b'r', b'b/d/e', 0, False, b'')]) 377 378 state.unlock() 379 try: 380 new_state = dirstate.DirState.from_tree(tree, 'dirstate') 381 try: 382 new_state.save() 383 finally: 384 new_state.unlock() 385 finally: 386 state.lock_read() 387 return tree, state, expected 388 389 390class TestTreeToDirState(TestCaseWithDirState): 391 392 def test_empty_to_dirstate(self): 393 """We should be able to create a dirstate for an empty tree.""" 394 # There are no files on disk and no parents 395 tree = self.make_branch_and_tree('tree') 396 expected_result = ([], [ 397 ((b'', b'', tree.path2id('')), # common details 398 [(b'd', b'', 0, False, dirstate.DirState.NULLSTAT), # current tree 399 ])]) 400 state = dirstate.DirState.from_tree(tree, 'dirstate') 401 state._validate() 402 self.check_state_with_reopen(expected_result, state) 403 404 def test_1_parents_empty_to_dirstate(self): 405 # create a parent by doing a commit 406 tree = self.make_branch_and_tree('tree') 407 rev_id = tree.commit('first post') 408 root_stat_pack = dirstate.pack_stat(os.stat(tree.basedir)) 409 expected_result = ([rev_id], [ 410 ((b'', b'', tree.path2id('')), # common details 411 [(b'd', b'', 0, False, dirstate.DirState.NULLSTAT), # current tree 412 (b'd', b'', 0, False, rev_id), # first parent details 413 ])]) 414 state = dirstate.DirState.from_tree(tree, 'dirstate') 415 self.check_state_with_reopen(expected_result, state) 416 state.lock_read() 417 try: 418 state._validate() 419 finally: 420 state.unlock() 421 422 def test_2_parents_empty_to_dirstate(self): 423 # create a parent by doing a commit 424 tree = self.make_branch_and_tree('tree') 425 rev_id = tree.commit('first post') 426 tree2 = tree.controldir.sprout('tree2').open_workingtree() 427 rev_id2 = tree2.commit('second post', allow_pointless=True) 428 tree.merge_from_branch(tree2.branch) 429 expected_result = ([rev_id, rev_id2], [ 430 ((b'', b'', tree.path2id('')), # common details 431 [(b'd', b'', 0, False, dirstate.DirState.NULLSTAT), # current tree 432 (b'd', b'', 0, False, rev_id), # first parent details 433 (b'd', b'', 0, False, rev_id), # second parent details 434 ])]) 435 state = dirstate.DirState.from_tree(tree, 'dirstate') 436 self.check_state_with_reopen(expected_result, state) 437 state.lock_read() 438 try: 439 state._validate() 440 finally: 441 state.unlock() 442 443 def test_empty_unknowns_are_ignored_to_dirstate(self): 444 """We should be able to create a dirstate for an empty tree.""" 445 # There are no files on disk and no parents 446 tree = self.make_branch_and_tree('tree') 447 self.build_tree(['tree/unknown']) 448 expected_result = ([], [ 449 ((b'', b'', tree.path2id('')), # common details 450 [(b'd', b'', 0, False, dirstate.DirState.NULLSTAT), # current tree 451 ])]) 452 state = dirstate.DirState.from_tree(tree, 'dirstate') 453 self.check_state_with_reopen(expected_result, state) 454 455 def get_tree_with_a_file(self): 456 tree = self.make_branch_and_tree('tree') 457 self.build_tree(['tree/a file']) 458 tree.add('a file', b'a-file-id') 459 return tree 460 461 def test_non_empty_no_parents_to_dirstate(self): 462 """We should be able to create a dirstate for an empty tree.""" 463 # There are files on disk and no parents 464 tree = self.get_tree_with_a_file() 465 expected_result = ([], [ 466 ((b'', b'', tree.path2id('')), # common details 467 [(b'd', b'', 0, False, dirstate.DirState.NULLSTAT), # current tree 468 ]), 469 ((b'', b'a file', b'a-file-id'), # common 470 [(b'f', b'', 0, False, dirstate.DirState.NULLSTAT), # current 471 ]), 472 ]) 473 state = dirstate.DirState.from_tree(tree, 'dirstate') 474 self.check_state_with_reopen(expected_result, state) 475 476 def test_1_parents_not_empty_to_dirstate(self): 477 # create a parent by doing a commit 478 tree = self.get_tree_with_a_file() 479 rev_id = tree.commit('first post') 480 # change the current content to be different this will alter stat, sha 481 # and length: 482 self.build_tree_contents([('tree/a file', b'new content\n')]) 483 expected_result = ([rev_id], [ 484 ((b'', b'', tree.path2id('')), # common details 485 [(b'd', b'', 0, False, dirstate.DirState.NULLSTAT), # current tree 486 (b'd', b'', 0, False, rev_id), # first parent details 487 ]), 488 ((b'', b'a file', b'a-file-id'), # common 489 [(b'f', b'', 0, False, dirstate.DirState.NULLSTAT), # current 490 (b'f', b'c3ed76e4bfd45ff1763ca206055bca8e9fc28aa8', 24, False, 491 rev_id), # first parent 492 ]), 493 ]) 494 state = dirstate.DirState.from_tree(tree, 'dirstate') 495 self.check_state_with_reopen(expected_result, state) 496 497 def test_2_parents_not_empty_to_dirstate(self): 498 # create a parent by doing a commit 499 tree = self.get_tree_with_a_file() 500 rev_id = tree.commit('first post') 501 tree2 = tree.controldir.sprout('tree2').open_workingtree() 502 # change the current content to be different this will alter stat, sha 503 # and length: 504 self.build_tree_contents([('tree2/a file', b'merge content\n')]) 505 rev_id2 = tree2.commit('second post') 506 tree.merge_from_branch(tree2.branch) 507 # change the current content to be different this will alter stat, sha 508 # and length again, giving us three distinct values: 509 self.build_tree_contents([('tree/a file', b'new content\n')]) 510 expected_result = ([rev_id, rev_id2], [ 511 ((b'', b'', tree.path2id('')), # common details 512 [(b'd', b'', 0, False, dirstate.DirState.NULLSTAT), # current tree 513 (b'd', b'', 0, False, rev_id), # first parent details 514 (b'd', b'', 0, False, rev_id), # second parent details 515 ]), 516 ((b'', b'a file', b'a-file-id'), # common 517 [(b'f', b'', 0, False, dirstate.DirState.NULLSTAT), # current 518 (b'f', b'c3ed76e4bfd45ff1763ca206055bca8e9fc28aa8', 24, False, 519 rev_id), # first parent 520 (b'f', b'314d796174c9412647c3ce07dfb5d36a94e72958', 14, False, 521 rev_id2), # second parent 522 ]), 523 ]) 524 state = dirstate.DirState.from_tree(tree, 'dirstate') 525 self.check_state_with_reopen(expected_result, state) 526 527 def test_colliding_fileids(self): 528 # test insertion of parents creating several entries at the same path. 529 # we used to have a bug where they could cause the dirstate to break 530 # its ordering invariants. 531 # create some trees to test from 532 parents = [] 533 for i in range(7): 534 tree = self.make_branch_and_tree('tree%d' % i) 535 self.build_tree(['tree%d/name' % i, ]) 536 tree.add(['name'], [b'file-id%d' % i]) 537 revision_id = b'revid-%d' % i 538 tree.commit('message', rev_id=revision_id) 539 parents.append((revision_id, 540 tree.branch.repository.revision_tree(revision_id))) 541 # now fold these trees into a dirstate 542 state = dirstate.DirState.initialize('dirstate') 543 try: 544 state.set_parent_trees(parents, []) 545 state._validate() 546 finally: 547 state.unlock() 548 549 550class TestDirStateOnFile(TestCaseWithDirState): 551 552 def create_updated_dirstate(self): 553 self.build_tree(['a-file']) 554 tree = self.make_branch_and_tree('.') 555 tree.add(['a-file'], [b'a-id']) 556 tree.commit('add a-file') 557 # Save and unlock the state, re-open it in readonly mode 558 state = dirstate.DirState.from_tree(tree, 'dirstate') 559 state.save() 560 state.unlock() 561 state = dirstate.DirState.on_file('dirstate') 562 state.lock_read() 563 return state 564 565 def test_construct_with_path(self): 566 tree = self.make_branch_and_tree('tree') 567 state = dirstate.DirState.from_tree(tree, 'dirstate.from_tree') 568 # we want to be able to get the lines of the dirstate that we will 569 # write to disk. 570 lines = state.get_lines() 571 state.unlock() 572 self.build_tree_contents([('dirstate', b''.join(lines))]) 573 # get a state object 574 # no parents, default tree content 575 expected_result = ([], [ 576 ((b'', b'', tree.path2id('')), # common details 577 # current tree details, but new from_tree skips statting, it 578 # uses set_state_from_inventory, and thus depends on the 579 # inventory state. 580 [(b'd', b'', 0, False, dirstate.DirState.NULLSTAT), 581 ]) 582 ]) 583 state = dirstate.DirState.on_file('dirstate') 584 state.lock_write() # check_state_with_reopen will save() and unlock it 585 self.check_state_with_reopen(expected_result, state) 586 587 def test_can_save_clean_on_file(self): 588 tree = self.make_branch_and_tree('tree') 589 state = dirstate.DirState.from_tree(tree, 'dirstate') 590 try: 591 # doing a save should work here as there have been no changes. 592 state.save() 593 # TODO: stat it and check it hasn't changed; may require waiting 594 # for the state accuracy window. 595 finally: 596 state.unlock() 597 598 def test_can_save_in_read_lock(self): 599 state = self.create_updated_dirstate() 600 try: 601 entry = state._get_entry(0, path_utf8=b'a-file') 602 # The current size should be 0 (default) 603 self.assertEqual(0, entry[1][0][2]) 604 # We should have a real entry. 605 self.assertNotEqual((None, None), entry) 606 # Set the cutoff-time into the future, so things look cacheable 607 state._sha_cutoff_time() 608 state._cutoff_time += 10.0 609 st = os.lstat('a-file') 610 sha1sum = dirstate.update_entry(state, entry, 'a-file', st) 611 # We updated the current sha1sum because the file is cacheable 612 self.assertEqual(b'ecc5374e9ed82ad3ea3b4d452ea995a5fd3e70e3', 613 sha1sum) 614 615 # The dirblock has been updated 616 self.assertEqual(st.st_size, entry[1][0][2]) 617 self.assertEqual(dirstate.DirState.IN_MEMORY_HASH_MODIFIED, 618 state._dirblock_state) 619 620 del entry 621 # Now, since we are the only one holding a lock, we should be able 622 # to save and have it written to disk 623 state.save() 624 finally: 625 state.unlock() 626 627 # Re-open the file, and ensure that the state has been updated. 628 state = dirstate.DirState.on_file('dirstate') 629 state.lock_read() 630 try: 631 entry = state._get_entry(0, path_utf8=b'a-file') 632 self.assertEqual(st.st_size, entry[1][0][2]) 633 finally: 634 state.unlock() 635 636 def test_save_fails_quietly_if_locked(self): 637 """If dirstate is locked, save will fail without complaining.""" 638 state = self.create_updated_dirstate() 639 try: 640 entry = state._get_entry(0, path_utf8=b'a-file') 641 # No cached sha1 yet. 642 self.assertEqual(b'', entry[1][0][1]) 643 # Set the cutoff-time into the future, so things look cacheable 644 state._sha_cutoff_time() 645 state._cutoff_time += 10.0 646 st = os.lstat('a-file') 647 sha1sum = dirstate.update_entry(state, entry, 'a-file', st) 648 self.assertEqual(b'ecc5374e9ed82ad3ea3b4d452ea995a5fd3e70e3', 649 sha1sum) 650 self.assertEqual(dirstate.DirState.IN_MEMORY_HASH_MODIFIED, 651 state._dirblock_state) 652 653 # Now, before we try to save, grab another dirstate, and take out a 654 # read lock. 655 # TODO: jam 20070315 Ideally this would be locked by another 656 # process. To make sure the file is really OS locked. 657 state2 = dirstate.DirState.on_file('dirstate') 658 state2.lock_read() 659 try: 660 # This won't actually write anything, because it couldn't grab 661 # a write lock. But it shouldn't raise an error, either. 662 # TODO: jam 20070315 We should probably distinguish between 663 # being dirty because of 'update_entry'. And dirty 664 # because of real modification. So that save() *does* 665 # raise a real error if it fails when we have real 666 # modifications. 667 state.save() 668 finally: 669 state2.unlock() 670 finally: 671 state.unlock() 672 673 # The file on disk should not be modified. 674 state = dirstate.DirState.on_file('dirstate') 675 state.lock_read() 676 try: 677 entry = state._get_entry(0, path_utf8=b'a-file') 678 self.assertEqual(b'', entry[1][0][1]) 679 finally: 680 state.unlock() 681 682 def test_save_refuses_if_changes_aborted(self): 683 self.build_tree(['a-file', 'a-dir/']) 684 state = dirstate.DirState.initialize('dirstate') 685 try: 686 # No stat and no sha1 sum. 687 state.add('a-file', b'a-file-id', 'file', None, b'') 688 state.save() 689 finally: 690 state.unlock() 691 692 # The dirstate should include TREE_ROOT and 'a-file' and nothing else 693 expected_blocks = [ 694 (b'', [((b'', b'', b'TREE_ROOT'), 695 [(b'd', b'', 0, False, dirstate.DirState.NULLSTAT)])]), 696 (b'', [((b'', b'a-file', b'a-file-id'), 697 [(b'f', b'', 0, False, dirstate.DirState.NULLSTAT)])]), 698 ] 699 700 state = dirstate.DirState.on_file('dirstate') 701 state.lock_write() 702 try: 703 state._read_dirblocks_if_needed() 704 self.assertEqual(expected_blocks, state._dirblocks) 705 706 # Now modify the state, but mark it as inconsistent 707 state.add('a-dir', b'a-dir-id', 'directory', None, b'') 708 state._changes_aborted = True 709 state.save() 710 finally: 711 state.unlock() 712 713 state = dirstate.DirState.on_file('dirstate') 714 state.lock_read() 715 try: 716 state._read_dirblocks_if_needed() 717 self.assertEqual(expected_blocks, state._dirblocks) 718 finally: 719 state.unlock() 720 721 722class TestDirStateInitialize(TestCaseWithDirState): 723 724 def test_initialize(self): 725 expected_result = ([], [ 726 ((b'', b'', b'TREE_ROOT'), # common details 727 [(b'd', b'', 0, False, dirstate.DirState.NULLSTAT), # current tree 728 ]) 729 ]) 730 state = dirstate.DirState.initialize('dirstate') 731 try: 732 self.assertIsInstance(state, dirstate.DirState) 733 lines = state.get_lines() 734 finally: 735 state.unlock() 736 # On win32 you can't read from a locked file, even within the same 737 # process. So we have to unlock and release before we check the file 738 # contents. 739 self.assertFileEqual(b''.join(lines), 'dirstate') 740 state.lock_read() # check_state_with_reopen will unlock 741 self.check_state_with_reopen(expected_result, state) 742 743 744class TestDirStateManipulations(TestCaseWithDirState): 745 746 def make_minimal_tree(self): 747 tree1 = self.make_branch_and_memory_tree('tree1') 748 tree1.lock_write() 749 self.addCleanup(tree1.unlock) 750 tree1.add('') 751 revid1 = tree1.commit('foo') 752 return tree1, revid1 753 754 def test_update_minimal_updates_id_index(self): 755 state = self.create_dirstate_with_root_and_subdir() 756 self.addCleanup(state.unlock) 757 id_index = state._get_id_index() 758 self.assertEqual([b'a-root-value', b'subdir-id'], sorted(id_index)) 759 state.add('file-name', b'file-id', 'file', None, '') 760 self.assertEqual([b'a-root-value', b'file-id', b'subdir-id'], 761 sorted(id_index)) 762 state.update_minimal((b'', b'new-name', b'file-id'), b'f', 763 path_utf8=b'new-name') 764 self.assertEqual([b'a-root-value', b'file-id', b'subdir-id'], 765 sorted(id_index)) 766 self.assertEqual([(b'', b'new-name', b'file-id')], 767 sorted(id_index[b'file-id'])) 768 state._validate() 769 770 def test_set_state_from_inventory_no_content_no_parents(self): 771 # setting the current inventory is a slow but important api to support. 772 tree1, revid1 = self.make_minimal_tree() 773 inv = tree1.root_inventory 774 root_id = inv.path2id('') 775 expected_result = [], [ 776 ((b'', b'', root_id), [ 777 (b'd', b'', 0, False, dirstate.DirState.NULLSTAT)])] 778 state = dirstate.DirState.initialize('dirstate') 779 try: 780 state.set_state_from_inventory(inv) 781 self.assertEqual(dirstate.DirState.IN_MEMORY_UNMODIFIED, 782 state._header_state) 783 self.assertEqual(dirstate.DirState.IN_MEMORY_MODIFIED, 784 state._dirblock_state) 785 except: 786 state.unlock() 787 raise 788 else: 789 # This will unlock it 790 self.check_state_with_reopen(expected_result, state) 791 792 def test_set_state_from_scratch_no_parents(self): 793 tree1, revid1 = self.make_minimal_tree() 794 inv = tree1.root_inventory 795 root_id = inv.path2id('') 796 expected_result = [], [ 797 ((b'', b'', root_id), [ 798 (b'd', b'', 0, False, dirstate.DirState.NULLSTAT)])] 799 state = dirstate.DirState.initialize('dirstate') 800 try: 801 state.set_state_from_scratch(inv, [], []) 802 self.assertEqual(dirstate.DirState.IN_MEMORY_MODIFIED, 803 state._header_state) 804 self.assertEqual(dirstate.DirState.IN_MEMORY_MODIFIED, 805 state._dirblock_state) 806 except: 807 state.unlock() 808 raise 809 else: 810 # This will unlock it 811 self.check_state_with_reopen(expected_result, state) 812 813 def test_set_state_from_scratch_identical_parent(self): 814 tree1, revid1 = self.make_minimal_tree() 815 inv = tree1.root_inventory 816 root_id = inv.path2id('') 817 rev_tree1 = tree1.branch.repository.revision_tree(revid1) 818 d_entry = (b'd', b'', 0, False, dirstate.DirState.NULLSTAT) 819 parent_entry = (b'd', b'', 0, False, revid1) 820 expected_result = [revid1], [ 821 ((b'', b'', root_id), [d_entry, parent_entry])] 822 state = dirstate.DirState.initialize('dirstate') 823 try: 824 state.set_state_from_scratch(inv, [(revid1, rev_tree1)], []) 825 self.assertEqual(dirstate.DirState.IN_MEMORY_MODIFIED, 826 state._header_state) 827 self.assertEqual(dirstate.DirState.IN_MEMORY_MODIFIED, 828 state._dirblock_state) 829 except: 830 state.unlock() 831 raise 832 else: 833 # This will unlock it 834 self.check_state_with_reopen(expected_result, state) 835 836 def test_set_state_from_inventory_preserves_hashcache(self): 837 # https://bugs.launchpad.net/bzr/+bug/146176 838 # set_state_from_inventory should preserve the stat and hash value for 839 # workingtree files that are not changed by the inventory. 840 841 tree = self.make_branch_and_tree('.') 842 # depends on the default format using dirstate... 843 with tree.lock_write(): 844 # make a dirstate with some valid hashcache data 845 # file on disk, but that's not needed for this test 846 foo_contents = b'contents of foo' 847 self.build_tree_contents([('foo', foo_contents)]) 848 tree.add('foo', b'foo-id') 849 850 foo_stat = os.stat('foo') 851 foo_packed = dirstate.pack_stat(foo_stat) 852 foo_sha = osutils.sha_string(foo_contents) 853 foo_size = len(foo_contents) 854 855 # should not be cached yet, because the file's too fresh 856 self.assertEqual( 857 ((b'', b'foo', b'foo-id',), 858 [(b'f', b'', 0, False, dirstate.DirState.NULLSTAT)]), 859 tree._dirstate._get_entry(0, b'foo-id')) 860 # poke in some hashcache information - it wouldn't normally be 861 # stored because it's too fresh 862 tree._dirstate.update_minimal( 863 (b'', b'foo', b'foo-id'), 864 b'f', False, foo_sha, foo_packed, foo_size, b'foo') 865 # now should be cached 866 self.assertEqual( 867 ((b'', b'foo', b'foo-id',), 868 [(b'f', foo_sha, foo_size, False, foo_packed)]), 869 tree._dirstate._get_entry(0, b'foo-id')) 870 871 # extract the inventory, and add something to it 872 inv = tree._get_root_inventory() 873 # should see the file we poked in... 874 self.assertTrue(inv.has_id(b'foo-id')) 875 self.assertTrue(inv.has_filename('foo')) 876 inv.add_path('bar', 'file', b'bar-id') 877 tree._dirstate._validate() 878 # this used to cause it to lose its hashcache 879 tree._dirstate.set_state_from_inventory(inv) 880 tree._dirstate._validate() 881 882 with tree.lock_read(): 883 # now check that the state still has the original hashcache value 884 state = tree._dirstate 885 state._validate() 886 foo_tuple = state._get_entry(0, path_utf8=b'foo') 887 self.assertEqual( 888 ((b'', b'foo', b'foo-id',), 889 [(b'f', foo_sha, len(foo_contents), False, 890 dirstate.pack_stat(foo_stat))]), 891 foo_tuple) 892 893 def test_set_state_from_inventory_mixed_paths(self): 894 tree1 = self.make_branch_and_tree('tree1') 895 self.build_tree(['tree1/a/', 'tree1/a/b/', 'tree1/a-b/', 896 'tree1/a/b/foo', 'tree1/a-b/bar']) 897 tree1.lock_write() 898 try: 899 tree1.add(['a', 'a/b', 'a-b', 'a/b/foo', 'a-b/bar'], 900 [b'a-id', b'b-id', b'a-b-id', b'foo-id', b'bar-id']) 901 tree1.commit('rev1', rev_id=b'rev1') 902 root_id = tree1.path2id('') 903 inv = tree1.root_inventory 904 finally: 905 tree1.unlock() 906 expected_result1 = [(b'', b'', root_id, b'd'), 907 (b'', b'a', b'a-id', b'd'), 908 (b'', b'a-b', b'a-b-id', b'd'), 909 (b'a', b'b', b'b-id', b'd'), 910 (b'a/b', b'foo', b'foo-id', b'f'), 911 (b'a-b', b'bar', b'bar-id', b'f'), 912 ] 913 expected_result2 = [(b'', b'', root_id, b'd'), 914 (b'', b'a', b'a-id', b'd'), 915 (b'', b'a-b', b'a-b-id', b'd'), 916 (b'a-b', b'bar', b'bar-id', b'f'), 917 ] 918 state = dirstate.DirState.initialize('dirstate') 919 try: 920 state.set_state_from_inventory(inv) 921 values = [] 922 for entry in state._iter_entries(): 923 values.append(entry[0] + entry[1][0][:1]) 924 self.assertEqual(expected_result1, values) 925 inv.delete(b'b-id') 926 state.set_state_from_inventory(inv) 927 values = [] 928 for entry in state._iter_entries(): 929 values.append(entry[0] + entry[1][0][:1]) 930 self.assertEqual(expected_result2, values) 931 finally: 932 state.unlock() 933 934 def test_set_path_id_no_parents(self): 935 """The id of a path can be changed trivally with no parents.""" 936 state = dirstate.DirState.initialize('dirstate') 937 try: 938 # check precondition to be sure the state does change appropriately. 939 root_entry = ((b'', b'', b'TREE_ROOT'), [ 940 (b'd', b'', 0, False, b'x' * 32)]) 941 self.assertEqual([root_entry], list(state._iter_entries())) 942 self.assertEqual(root_entry, state._get_entry(0, path_utf8=b'')) 943 self.assertEqual(root_entry, 944 state._get_entry(0, fileid_utf8=b'TREE_ROOT')) 945 self.assertEqual((None, None), 946 state._get_entry(0, fileid_utf8=b'second-root-id')) 947 state.set_path_id(b'', b'second-root-id') 948 new_root_entry = ((b'', b'', b'second-root-id'), 949 [(b'd', b'', 0, False, b'x' * 32)]) 950 expected_rows = [new_root_entry] 951 self.assertEqual(expected_rows, list(state._iter_entries())) 952 self.assertEqual( 953 new_root_entry, state._get_entry(0, path_utf8=b'')) 954 self.assertEqual(new_root_entry, 955 state._get_entry(0, fileid_utf8=b'second-root-id')) 956 self.assertEqual((None, None), 957 state._get_entry(0, fileid_utf8=b'TREE_ROOT')) 958 # should work across save too 959 state.save() 960 finally: 961 state.unlock() 962 state = dirstate.DirState.on_file('dirstate') 963 state.lock_read() 964 try: 965 state._validate() 966 self.assertEqual(expected_rows, list(state._iter_entries())) 967 finally: 968 state.unlock() 969 970 def test_set_path_id_with_parents(self): 971 """Set the root file id in a dirstate with parents""" 972 mt = self.make_branch_and_tree('mt') 973 # in case the default tree format uses a different root id 974 mt.set_root_id(b'TREE_ROOT') 975 mt.commit('foo', rev_id=b'parent-revid') 976 rt = mt.branch.repository.revision_tree(b'parent-revid') 977 state = dirstate.DirState.initialize('dirstate') 978 state._validate() 979 try: 980 state.set_parent_trees([(b'parent-revid', rt)], ghosts=[]) 981 root_entry = ((b'', b'', b'TREE_ROOT'), 982 [(b'd', b'', 0, False, b'x' * 32), 983 (b'd', b'', 0, False, b'parent-revid')]) 984 self.assertEqual(root_entry, state._get_entry(0, path_utf8=b'')) 985 self.assertEqual(root_entry, 986 state._get_entry(0, fileid_utf8=b'TREE_ROOT')) 987 self.assertEqual((None, None), 988 state._get_entry(0, fileid_utf8=b'Asecond-root-id')) 989 state.set_path_id(b'', b'Asecond-root-id') 990 state._validate() 991 # now see that it is what we expected 992 old_root_entry = ((b'', b'', b'TREE_ROOT'), 993 [(b'a', b'', 0, False, b''), 994 (b'd', b'', 0, False, b'parent-revid')]) 995 new_root_entry = ((b'', b'', b'Asecond-root-id'), 996 [(b'd', b'', 0, False, b''), 997 (b'a', b'', 0, False, b'')]) 998 expected_rows = [new_root_entry, old_root_entry] 999 state._validate() 1000 self.assertEqual(expected_rows, list(state._iter_entries())) 1001 self.assertEqual( 1002 new_root_entry, state._get_entry(0, path_utf8=b'')) 1003 self.assertEqual( 1004 old_root_entry, state._get_entry(1, path_utf8=b'')) 1005 self.assertEqual((None, None), 1006 state._get_entry(0, fileid_utf8=b'TREE_ROOT')) 1007 self.assertEqual(old_root_entry, 1008 state._get_entry(1, fileid_utf8=b'TREE_ROOT')) 1009 self.assertEqual(new_root_entry, 1010 state._get_entry(0, fileid_utf8=b'Asecond-root-id')) 1011 self.assertEqual((None, None), 1012 state._get_entry(1, fileid_utf8=b'Asecond-root-id')) 1013 # should work across save too 1014 state.save() 1015 finally: 1016 state.unlock() 1017 # now flush & check we get the same 1018 state = dirstate.DirState.on_file('dirstate') 1019 state.lock_read() 1020 try: 1021 state._validate() 1022 self.assertEqual(expected_rows, list(state._iter_entries())) 1023 finally: 1024 state.unlock() 1025 # now change within an existing file-backed state 1026 state.lock_write() 1027 try: 1028 state._validate() 1029 state.set_path_id(b'', b'tree-root-2') 1030 state._validate() 1031 finally: 1032 state.unlock() 1033 1034 def test_set_parent_trees_no_content(self): 1035 # set_parent_trees is a slow but important api to support. 1036 tree1 = self.make_branch_and_memory_tree('tree1') 1037 tree1.lock_write() 1038 try: 1039 tree1.add('') 1040 revid1 = tree1.commit('foo') 1041 finally: 1042 tree1.unlock() 1043 branch2 = tree1.branch.controldir.clone('tree2').open_branch() 1044 tree2 = memorytree.MemoryTree.create_on_branch(branch2) 1045 tree2.lock_write() 1046 try: 1047 revid2 = tree2.commit('foo') 1048 root_id = tree2.path2id('') 1049 finally: 1050 tree2.unlock() 1051 state = dirstate.DirState.initialize('dirstate') 1052 try: 1053 state.set_path_id(b'', root_id) 1054 state.set_parent_trees( 1055 ((revid1, tree1.branch.repository.revision_tree(revid1)), 1056 (revid2, tree2.branch.repository.revision_tree(revid2)), 1057 (b'ghost-rev', None)), 1058 [b'ghost-rev']) 1059 # check we can reopen and use the dirstate after setting parent 1060 # trees. 1061 state._validate() 1062 state.save() 1063 state._validate() 1064 finally: 1065 state.unlock() 1066 state = dirstate.DirState.on_file('dirstate') 1067 state.lock_write() 1068 try: 1069 self.assertEqual([revid1, revid2, b'ghost-rev'], 1070 state.get_parent_ids()) 1071 # iterating the entire state ensures that the state is parsable. 1072 list(state._iter_entries()) 1073 # be sure that it sets not appends - change it 1074 state.set_parent_trees( 1075 ((revid1, tree1.branch.repository.revision_tree(revid1)), 1076 (b'ghost-rev', None)), 1077 [b'ghost-rev']) 1078 # and now put it back. 1079 state.set_parent_trees( 1080 ((revid1, tree1.branch.repository.revision_tree(revid1)), 1081 (revid2, tree2.branch.repository.revision_tree(revid2)), 1082 (b'ghost-rev', tree2.branch.repository.revision_tree( 1083 _mod_revision.NULL_REVISION))), 1084 [b'ghost-rev']) 1085 self.assertEqual([revid1, revid2, b'ghost-rev'], 1086 state.get_parent_ids()) 1087 # the ghost should be recorded as such by set_parent_trees. 1088 self.assertEqual([b'ghost-rev'], state.get_ghosts()) 1089 self.assertEqual( 1090 [((b'', b'', root_id), [ 1091 (b'd', b'', 0, False, dirstate.DirState.NULLSTAT), 1092 (b'd', b'', 0, False, revid1), 1093 (b'd', b'', 0, False, revid1) 1094 ])], 1095 list(state._iter_entries())) 1096 finally: 1097 state.unlock() 1098 1099 def test_set_parent_trees_file_missing_from_tree(self): 1100 # Adding a parent tree may reference files not in the current state. 1101 # they should get listed just once by id, even if they are in two 1102 # separate trees. 1103 # set_parent_trees is a slow but important api to support. 1104 tree1 = self.make_branch_and_memory_tree('tree1') 1105 tree1.lock_write() 1106 try: 1107 tree1.add('') 1108 tree1.add(['a file'], [b'file-id'], ['file']) 1109 tree1.put_file_bytes_non_atomic('a file', b'file-content') 1110 revid1 = tree1.commit('foo') 1111 finally: 1112 tree1.unlock() 1113 branch2 = tree1.branch.controldir.clone('tree2').open_branch() 1114 tree2 = memorytree.MemoryTree.create_on_branch(branch2) 1115 tree2.lock_write() 1116 try: 1117 tree2.put_file_bytes_non_atomic('a file', b'new file-content') 1118 revid2 = tree2.commit('foo') 1119 root_id = tree2.path2id('') 1120 finally: 1121 tree2.unlock() 1122 # check the layout in memory 1123 expected_result = [revid1, revid2], [ 1124 ((b'', b'', root_id), [ 1125 (b'd', b'', 0, False, dirstate.DirState.NULLSTAT), 1126 (b'd', b'', 0, False, revid1), 1127 (b'd', b'', 0, False, revid1) 1128 ]), 1129 ((b'', b'a file', b'file-id'), [ 1130 (b'a', b'', 0, False, b''), 1131 (b'f', b'2439573625385400f2a669657a7db6ae7515d371', 12, False, 1132 revid1), 1133 (b'f', b'542e57dc1cda4af37cb8e55ec07ce60364bb3c7d', 16, False, 1134 revid2) 1135 ]) 1136 ] 1137 state = dirstate.DirState.initialize('dirstate') 1138 try: 1139 state.set_path_id(b'', root_id) 1140 state.set_parent_trees( 1141 ((revid1, tree1.branch.repository.revision_tree(revid1)), 1142 (revid2, tree2.branch.repository.revision_tree(revid2)), 1143 ), []) 1144 except: 1145 state.unlock() 1146 raise 1147 else: 1148 # check_state_with_reopen will unlock 1149 self.check_state_with_reopen(expected_result, state) 1150 1151 # add a path via _set_data - so we dont need delta work, just 1152 # raw data in, and ensure that it comes out via get_lines happily. 1153 1154 def test_add_path_to_root_no_parents_all_data(self): 1155 # The most trivial addition of a path is when there are no parents and 1156 # its in the root and all data about the file is supplied 1157 self.build_tree(['a file']) 1158 stat = os.lstat('a file') 1159 # the 1*20 is the sha1 pretend value. 1160 state = dirstate.DirState.initialize('dirstate') 1161 expected_entries = [ 1162 ((b'', b'', b'TREE_ROOT'), [ 1163 (b'd', b'', 0, False, dirstate.DirState.NULLSTAT), # current tree 1164 ]), 1165 ((b'', b'a file', b'a-file-id'), [ 1166 (b'f', b'1' * 20, 19, False, dirstate.pack_stat(stat)), # current tree 1167 ]), 1168 ] 1169 try: 1170 state.add('a file', b'a-file-id', 'file', stat, b'1' * 20) 1171 # having added it, it should be in the output of iter_entries. 1172 self.assertEqual(expected_entries, list(state._iter_entries())) 1173 # saving and reloading should not affect this. 1174 state.save() 1175 finally: 1176 state.unlock() 1177 state = dirstate.DirState.on_file('dirstate') 1178 state.lock_read() 1179 self.addCleanup(state.unlock) 1180 self.assertEqual(expected_entries, list(state._iter_entries())) 1181 1182 def test_add_path_to_unversioned_directory(self): 1183 """Adding a path to an unversioned directory should error. 1184 1185 This is a duplicate of TestWorkingTree.test_add_in_unversioned, 1186 once dirstate is stable and if it is merged with WorkingTree3, consider 1187 removing this copy of the test. 1188 """ 1189 self.build_tree(['unversioned/', 'unversioned/a file']) 1190 state = dirstate.DirState.initialize('dirstate') 1191 self.addCleanup(state.unlock) 1192 self.assertRaises(errors.NotVersionedError, state.add, 1193 'unversioned/a file', b'a-file-id', 'file', None, None) 1194 1195 def test_add_directory_to_root_no_parents_all_data(self): 1196 # The most trivial addition of a dir is when there are no parents and 1197 # its in the root and all data about the file is supplied 1198 self.build_tree(['a dir/']) 1199 stat = os.lstat('a dir') 1200 expected_entries = [ 1201 ((b'', b'', b'TREE_ROOT'), [ 1202 (b'd', b'', 0, False, dirstate.DirState.NULLSTAT), # current tree 1203 ]), 1204 ((b'', b'a dir', b'a dir id'), [ 1205 (b'd', b'', 0, False, dirstate.pack_stat(stat)), # current tree 1206 ]), 1207 ] 1208 state = dirstate.DirState.initialize('dirstate') 1209 try: 1210 state.add('a dir', b'a dir id', 'directory', stat, None) 1211 # having added it, it should be in the output of iter_entries. 1212 self.assertEqual(expected_entries, list(state._iter_entries())) 1213 # saving and reloading should not affect this. 1214 state.save() 1215 finally: 1216 state.unlock() 1217 state = dirstate.DirState.on_file('dirstate') 1218 state.lock_read() 1219 self.addCleanup(state.unlock) 1220 state._validate() 1221 self.assertEqual(expected_entries, list(state._iter_entries())) 1222 1223 def _test_add_symlink_to_root_no_parents_all_data(self, link_name, target): 1224 # The most trivial addition of a symlink when there are no parents and 1225 # its in the root and all data about the file is supplied 1226 # bzr doesn't support fake symlinks on windows, yet. 1227 self.requireFeature(features.SymlinkFeature) 1228 os.symlink(target, link_name) 1229 stat = os.lstat(link_name) 1230 expected_entries = [ 1231 ((b'', b'', b'TREE_ROOT'), [ 1232 (b'd', b'', 0, False, dirstate.DirState.NULLSTAT), # current tree 1233 ]), 1234 ((b'', link_name.encode('UTF-8'), b'a link id'), [ 1235 (b'l', target.encode('UTF-8'), stat[6], 1236 False, dirstate.pack_stat(stat)), # current tree 1237 ]), 1238 ] 1239 state = dirstate.DirState.initialize('dirstate') 1240 try: 1241 state.add(link_name, b'a link id', 'symlink', stat, 1242 target.encode('UTF-8')) 1243 # having added it, it should be in the output of iter_entries. 1244 self.assertEqual(expected_entries, list(state._iter_entries())) 1245 # saving and reloading should not affect this. 1246 state.save() 1247 finally: 1248 state.unlock() 1249 state = dirstate.DirState.on_file('dirstate') 1250 state.lock_read() 1251 self.addCleanup(state.unlock) 1252 self.assertEqual(expected_entries, list(state._iter_entries())) 1253 1254 def test_add_symlink_to_root_no_parents_all_data(self): 1255 self._test_add_symlink_to_root_no_parents_all_data( 1256 u'a link', u'target') 1257 1258 def test_add_symlink_unicode_to_root_no_parents_all_data(self): 1259 self.requireFeature(features.UnicodeFilenameFeature) 1260 self._test_add_symlink_to_root_no_parents_all_data( 1261 u'\N{Euro Sign}link', u'targ\N{Euro Sign}et') 1262 1263 def test_add_directory_and_child_no_parents_all_data(self): 1264 # after adding a directory, we should be able to add children to it. 1265 self.build_tree(['a dir/', 'a dir/a file']) 1266 dirstat = os.lstat('a dir') 1267 filestat = os.lstat('a dir/a file') 1268 expected_entries = [ 1269 ((b'', b'', b'TREE_ROOT'), [ 1270 (b'd', b'', 0, False, dirstate.DirState.NULLSTAT), # current tree 1271 ]), 1272 ((b'', b'a dir', b'a dir id'), [ 1273 (b'd', b'', 0, False, dirstate.pack_stat(dirstat)), # current tree 1274 ]), 1275 ((b'a dir', b'a file', b'a-file-id'), [ 1276 (b'f', b'1' * 20, 25, False, 1277 dirstate.pack_stat(filestat)), # current tree details 1278 ]), 1279 ] 1280 state = dirstate.DirState.initialize('dirstate') 1281 try: 1282 state.add('a dir', b'a dir id', 'directory', dirstat, None) 1283 state.add('a dir/a file', b'a-file-id', 1284 'file', filestat, b'1' * 20) 1285 # added it, it should be in the output of iter_entries. 1286 self.assertEqual(expected_entries, list(state._iter_entries())) 1287 # saving and reloading should not affect this. 1288 state.save() 1289 finally: 1290 state.unlock() 1291 state = dirstate.DirState.on_file('dirstate') 1292 state.lock_read() 1293 self.addCleanup(state.unlock) 1294 self.assertEqual(expected_entries, list(state._iter_entries())) 1295 1296 def test_add_tree_reference(self): 1297 # make a dirstate and add a tree reference 1298 state = dirstate.DirState.initialize('dirstate') 1299 expected_entry = ( 1300 (b'', b'subdir', b'subdir-id'), 1301 [(b't', b'subtree-123123', 0, False, 1302 b'xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx')], 1303 ) 1304 try: 1305 state.add('subdir', b'subdir-id', 'tree-reference', 1306 None, b'subtree-123123') 1307 entry = state._get_entry(0, b'subdir-id', b'subdir') 1308 self.assertEqual(entry, expected_entry) 1309 state._validate() 1310 state.save() 1311 finally: 1312 state.unlock() 1313 # now check we can read it back 1314 state.lock_read() 1315 self.addCleanup(state.unlock) 1316 state._validate() 1317 entry2 = state._get_entry(0, b'subdir-id', b'subdir') 1318 self.assertEqual(entry, entry2) 1319 self.assertEqual(entry, expected_entry) 1320 # and lookup by id should work too 1321 entry2 = state._get_entry(0, fileid_utf8=b'subdir-id') 1322 self.assertEqual(entry, expected_entry) 1323 1324 def test_add_forbidden_names(self): 1325 state = dirstate.DirState.initialize('dirstate') 1326 self.addCleanup(state.unlock) 1327 self.assertRaises(errors.BzrError, 1328 state.add, '.', b'ass-id', 'directory', None, None) 1329 self.assertRaises(errors.BzrError, 1330 state.add, '..', b'ass-id', 'directory', None, None) 1331 1332 def test_set_state_with_rename_b_a_bug_395556(self): 1333 # bug 395556 uncovered a bug where the dirstate ends up with a false 1334 # relocation record - in a tree with no parents there should be no 1335 # absent or relocated records. This then leads to further corruption 1336 # when a commit occurs, as the incorrect relocation gathers an 1337 # incorrect absent in tree 1, and future changes go to pot. 1338 tree1 = self.make_branch_and_tree('tree1') 1339 self.build_tree(['tree1/b']) 1340 with tree1.lock_write(): 1341 tree1.add(['b'], [b'b-id']) 1342 root_id = tree1.path2id('') 1343 inv = tree1.root_inventory 1344 state = dirstate.DirState.initialize('dirstate') 1345 try: 1346 # Set the initial state with 'b' 1347 state.set_state_from_inventory(inv) 1348 inv.rename(b'b-id', root_id, 'a') 1349 # Set the new state with 'a', which currently corrupts. 1350 state.set_state_from_inventory(inv) 1351 expected_result1 = [(b'', b'', root_id, b'd'), 1352 (b'', b'a', b'b-id', b'f'), 1353 ] 1354 values = [] 1355 for entry in state._iter_entries(): 1356 values.append(entry[0] + entry[1][0][:1]) 1357 self.assertEqual(expected_result1, values) 1358 finally: 1359 state.unlock() 1360 1361 1362class TestDirStateHashUpdates(TestCaseWithDirState): 1363 1364 def do_update_entry(self, state, path): 1365 entry = state._get_entry(0, path_utf8=path) 1366 stat = os.lstat(path) 1367 return dirstate.update_entry(state, entry, os.path.abspath(path), stat) 1368 1369 def _read_state_content(self, state): 1370 """Read the content of the dirstate file. 1371 1372 On Windows when one process locks a file, you can't even open() the 1373 file in another process (to read it). So we go directly to 1374 state._state_file. This should always be the exact disk representation, 1375 so it is reasonable to do so. 1376 DirState also always seeks before reading, so it doesn't matter if we 1377 bump the file pointer. 1378 """ 1379 state._state_file.seek(0) 1380 return state._state_file.read() 1381 1382 def test_worth_saving_limit_avoids_writing(self): 1383 tree = self.make_branch_and_tree('.') 1384 self.build_tree(['c', 'd']) 1385 tree.lock_write() 1386 tree.add(['c', 'd'], [b'c-id', b'd-id']) 1387 tree.commit('add c and d') 1388 state = InstrumentedDirState.on_file(tree.current_dirstate()._filename, 1389 worth_saving_limit=2) 1390 tree.unlock() 1391 state.lock_write() 1392 self.addCleanup(state.unlock) 1393 state._read_dirblocks_if_needed() 1394 state.adjust_time(+20) # Allow things to be cached 1395 self.assertEqual(dirstate.DirState.IN_MEMORY_UNMODIFIED, 1396 state._dirblock_state) 1397 content = self._read_state_content(state) 1398 self.do_update_entry(state, b'c') 1399 self.assertEqual(1, len(state._known_hash_changes)) 1400 self.assertEqual(dirstate.DirState.IN_MEMORY_HASH_MODIFIED, 1401 state._dirblock_state) 1402 state.save() 1403 # It should not have set the state to IN_MEMORY_UNMODIFIED because the 1404 # hash values haven't been written out. 1405 self.assertEqual(dirstate.DirState.IN_MEMORY_HASH_MODIFIED, 1406 state._dirblock_state) 1407 self.assertEqual(content, self._read_state_content(state)) 1408 self.assertEqual(dirstate.DirState.IN_MEMORY_HASH_MODIFIED, 1409 state._dirblock_state) 1410 self.do_update_entry(state, b'd') 1411 self.assertEqual(2, len(state._known_hash_changes)) 1412 state.save() 1413 self.assertEqual(dirstate.DirState.IN_MEMORY_UNMODIFIED, 1414 state._dirblock_state) 1415 self.assertEqual(0, len(state._known_hash_changes)) 1416 1417 1418class TestGetLines(TestCaseWithDirState): 1419 1420 def test_get_line_with_2_rows(self): 1421 state = self.create_dirstate_with_root_and_subdir() 1422 try: 1423 self.assertEqual([b'#bazaar dirstate flat format 3\n', 1424 b'crc32: 41262208\n', 1425 b'num_entries: 2\n', 1426 b'0\x00\n\x00' 1427 b'0\x00\n\x00' 1428 b'\x00\x00a-root-value\x00' 1429 b'd\x00\x000\x00n\x00AAAAREUHaIpFB2iKAAADAQAtkqUAAIGk\x00\n\x00' 1430 b'\x00subdir\x00subdir-id\x00' 1431 b'd\x00\x000\x00n\x00AAAAREUHaIpFB2iKAAADAQAtkqUAAIGk\x00\n\x00' 1432 ], state.get_lines()) 1433 finally: 1434 state.unlock() 1435 1436 def test_entry_to_line(self): 1437 state = self.create_dirstate_with_root() 1438 try: 1439 self.assertEqual( 1440 b'\x00\x00a-root-value\x00d\x00\x000\x00n' 1441 b'\x00AAAAREUHaIpFB2iKAAADAQAtkqUAAIGk', 1442 state._entry_to_line(state._dirblocks[0][1][0])) 1443 finally: 1444 state.unlock() 1445 1446 def test_entry_to_line_with_parent(self): 1447 packed_stat = b'AAAAREUHaIpFB2iKAAADAQAtkqUAAIGk' 1448 root_entry = (b'', b'', b'a-root-value'), [ 1449 (b'd', b'', 0, False, packed_stat), # current tree details 1450 # first: a pointer to the current location 1451 (b'a', b'dirname/basename', 0, False, b''), 1452 ] 1453 state = dirstate.DirState.initialize('dirstate') 1454 try: 1455 self.assertEqual( 1456 b'\x00\x00a-root-value\x00' 1457 b'd\x00\x000\x00n\x00AAAAREUHaIpFB2iKAAADAQAtkqUAAIGk\x00' 1458 b'a\x00dirname/basename\x000\x00n\x00', 1459 state._entry_to_line(root_entry)) 1460 finally: 1461 state.unlock() 1462 1463 def test_entry_to_line_with_two_parents_at_different_paths(self): 1464 # / in the tree, at / in one parent and /dirname/basename in the other. 1465 packed_stat = b'AAAAREUHaIpFB2iKAAADAQAtkqUAAIGk' 1466 root_entry = (b'', b'', b'a-root-value'), [ 1467 (b'd', b'', 0, False, packed_stat), # current tree details 1468 (b'd', b'', 0, False, b'rev_id'), # first parent details 1469 # second: a pointer to the current location 1470 (b'a', b'dirname/basename', 0, False, b''), 1471 ] 1472 state = dirstate.DirState.initialize('dirstate') 1473 try: 1474 self.assertEqual( 1475 b'\x00\x00a-root-value\x00' 1476 b'd\x00\x000\x00n\x00AAAAREUHaIpFB2iKAAADAQAtkqUAAIGk\x00' 1477 b'd\x00\x000\x00n\x00rev_id\x00' 1478 b'a\x00dirname/basename\x000\x00n\x00', 1479 state._entry_to_line(root_entry)) 1480 finally: 1481 state.unlock() 1482 1483 def test_iter_entries(self): 1484 # we should be able to iterate the dirstate entries from end to end 1485 # this is for get_lines to be easy to read. 1486 packed_stat = b'AAAAREUHaIpFB2iKAAADAQAtkqUAAIGk' 1487 dirblocks = [] 1488 root_entries = [((b'', b'', b'a-root-value'), [ 1489 (b'd', b'', 0, False, packed_stat), # current tree details 1490 ])] 1491 dirblocks.append(('', root_entries)) 1492 # add two files in the root 1493 subdir_entry = (b'', b'subdir', b'subdir-id'), [ 1494 (b'd', b'', 0, False, packed_stat), # current tree details 1495 ] 1496 afile_entry = (b'', b'afile', b'afile-id'), [ 1497 (b'f', b'sha1value', 34, False, packed_stat), # current tree details 1498 ] 1499 dirblocks.append(('', [subdir_entry, afile_entry])) 1500 # and one in subdir 1501 file_entry2 = (b'subdir', b'2file', b'2file-id'), [ 1502 (b'f', b'sha1value', 23, False, packed_stat), # current tree details 1503 ] 1504 dirblocks.append(('subdir', [file_entry2])) 1505 state = dirstate.DirState.initialize('dirstate') 1506 try: 1507 state._set_data([], dirblocks) 1508 expected_entries = [root_entries[0], subdir_entry, afile_entry, 1509 file_entry2] 1510 self.assertEqual(expected_entries, list(state._iter_entries())) 1511 finally: 1512 state.unlock() 1513 1514 1515class TestGetBlockRowIndex(TestCaseWithDirState): 1516 1517 def assertBlockRowIndexEqual(self, block_index, row_index, dir_present, 1518 file_present, state, dirname, basename, tree_index): 1519 self.assertEqual((block_index, row_index, dir_present, file_present), 1520 state._get_block_entry_index(dirname, basename, tree_index)) 1521 if dir_present: 1522 block = state._dirblocks[block_index] 1523 self.assertEqual(dirname, block[0]) 1524 if dir_present and file_present: 1525 row = state._dirblocks[block_index][1][row_index] 1526 self.assertEqual(dirname, row[0][0]) 1527 self.assertEqual(basename, row[0][1]) 1528 1529 def test_simple_structure(self): 1530 state = self.create_dirstate_with_root_and_subdir() 1531 self.addCleanup(state.unlock) 1532 self.assertBlockRowIndexEqual( 1533 1, 0, True, True, state, b'', b'subdir', 0) 1534 self.assertBlockRowIndexEqual( 1535 1, 0, True, False, state, b'', b'bdir', 0) 1536 self.assertBlockRowIndexEqual( 1537 1, 1, True, False, state, b'', b'zdir', 0) 1538 self.assertBlockRowIndexEqual( 1539 2, 0, False, False, state, b'a', b'foo', 0) 1540 self.assertBlockRowIndexEqual(2, 0, False, False, state, 1541 b'subdir', b'foo', 0) 1542 1543 def test_complex_structure_exists(self): 1544 state = self.create_complex_dirstate() 1545 self.addCleanup(state.unlock) 1546 # Make sure we can find everything that exists 1547 self.assertBlockRowIndexEqual(0, 0, True, True, state, b'', b'', 0) 1548 self.assertBlockRowIndexEqual(1, 0, True, True, state, b'', b'a', 0) 1549 self.assertBlockRowIndexEqual(1, 1, True, True, state, b'', b'b', 0) 1550 self.assertBlockRowIndexEqual(1, 2, True, True, state, b'', b'c', 0) 1551 self.assertBlockRowIndexEqual(1, 3, True, True, state, b'', b'd', 0) 1552 self.assertBlockRowIndexEqual(2, 0, True, True, state, b'a', b'e', 0) 1553 self.assertBlockRowIndexEqual(2, 1, True, True, state, b'a', b'f', 0) 1554 self.assertBlockRowIndexEqual(3, 0, True, True, state, b'b', b'g', 0) 1555 self.assertBlockRowIndexEqual(3, 1, True, True, state, 1556 b'b', b'h\xc3\xa5', 0) 1557 1558 def test_complex_structure_missing(self): 1559 state = self.create_complex_dirstate() 1560 self.addCleanup(state.unlock) 1561 # Make sure things would be inserted in the right locations 1562 # '_' comes before 'a' 1563 self.assertBlockRowIndexEqual(0, 0, True, True, state, b'', b'', 0) 1564 self.assertBlockRowIndexEqual(1, 0, True, False, state, b'', b'_', 0) 1565 self.assertBlockRowIndexEqual(1, 1, True, False, state, b'', b'aa', 0) 1566 self.assertBlockRowIndexEqual(1, 4, True, False, state, 1567 b'', b'h\xc3\xa5', 0) 1568 self.assertBlockRowIndexEqual(2, 0, False, False, state, b'_', b'a', 0) 1569 self.assertBlockRowIndexEqual( 1570 3, 0, False, False, state, b'aa', b'a', 0) 1571 self.assertBlockRowIndexEqual( 1572 4, 0, False, False, state, b'bb', b'a', 0) 1573 # This would be inserted between a/ and b/ 1574 self.assertBlockRowIndexEqual( 1575 3, 0, False, False, state, b'a/e', b'a', 0) 1576 # Put at the end 1577 self.assertBlockRowIndexEqual(4, 0, False, False, state, b'e', b'a', 0) 1578 1579 1580class TestGetEntry(TestCaseWithDirState): 1581 1582 def assertEntryEqual(self, dirname, basename, file_id, state, path, index): 1583 """Check that the right entry is returned for a request to getEntry.""" 1584 entry = state._get_entry(index, path_utf8=path) 1585 if file_id is None: 1586 self.assertEqual((None, None), entry) 1587 else: 1588 cur = entry[0] 1589 self.assertEqual((dirname, basename, file_id), cur[:3]) 1590 1591 def test_simple_structure(self): 1592 state = self.create_dirstate_with_root_and_subdir() 1593 self.addCleanup(state.unlock) 1594 self.assertEntryEqual(b'', b'', b'a-root-value', state, b'', 0) 1595 self.assertEntryEqual( 1596 b'', b'subdir', b'subdir-id', state, b'subdir', 0) 1597 self.assertEntryEqual(None, None, None, state, b'missing', 0) 1598 self.assertEntryEqual(None, None, None, state, b'missing/foo', 0) 1599 self.assertEntryEqual(None, None, None, state, b'subdir/foo', 0) 1600 1601 def test_complex_structure_exists(self): 1602 state = self.create_complex_dirstate() 1603 self.addCleanup(state.unlock) 1604 self.assertEntryEqual(b'', b'', b'a-root-value', state, b'', 0) 1605 self.assertEntryEqual(b'', b'a', b'a-dir', state, b'a', 0) 1606 self.assertEntryEqual(b'', b'b', b'b-dir', state, b'b', 0) 1607 self.assertEntryEqual(b'', b'c', b'c-file', state, b'c', 0) 1608 self.assertEntryEqual(b'', b'd', b'd-file', state, b'd', 0) 1609 self.assertEntryEqual(b'a', b'e', b'e-dir', state, b'a/e', 0) 1610 self.assertEntryEqual(b'a', b'f', b'f-file', state, b'a/f', 0) 1611 self.assertEntryEqual(b'b', b'g', b'g-file', state, b'b/g', 0) 1612 self.assertEntryEqual(b'b', b'h\xc3\xa5', b'h-\xc3\xa5-file', state, 1613 b'b/h\xc3\xa5', 0) 1614 1615 def test_complex_structure_missing(self): 1616 state = self.create_complex_dirstate() 1617 self.addCleanup(state.unlock) 1618 self.assertEntryEqual(None, None, None, state, b'_', 0) 1619 self.assertEntryEqual(None, None, None, state, b'_\xc3\xa5', 0) 1620 self.assertEntryEqual(None, None, None, state, b'a/b', 0) 1621 self.assertEntryEqual(None, None, None, state, b'c/d', 0) 1622 1623 def test_get_entry_uninitialized(self): 1624 """Calling get_entry will load data if it needs to""" 1625 state = self.create_dirstate_with_root() 1626 try: 1627 state.save() 1628 finally: 1629 state.unlock() 1630 del state 1631 state = dirstate.DirState.on_file('dirstate') 1632 state.lock_read() 1633 try: 1634 self.assertEqual(dirstate.DirState.NOT_IN_MEMORY, 1635 state._header_state) 1636 self.assertEqual(dirstate.DirState.NOT_IN_MEMORY, 1637 state._dirblock_state) 1638 self.assertEntryEqual(b'', b'', b'a-root-value', state, b'', 0) 1639 finally: 1640 state.unlock() 1641 1642 1643class TestIterChildEntries(TestCaseWithDirState): 1644 1645 def create_dirstate_with_two_trees(self): 1646 """This dirstate contains multiple files and directories. 1647 1648 / a-root-value 1649 a/ a-dir 1650 b/ b-dir 1651 c c-file 1652 d d-file 1653 a/e/ e-dir 1654 a/f f-file 1655 b/g g-file 1656 b/h\xc3\xa5 h-\xc3\xa5-file #This is u'\xe5' encoded into utf-8 1657 1658 Notice that a/e is an empty directory. 1659 1660 There is one parent tree, which has the same shape with the following variations: 1661 b/g in the parent is gone. 1662 b/h in the parent has a different id 1663 b/i is new in the parent 1664 c is renamed to b/j in the parent 1665 1666 :return: The dirstate, still write-locked. 1667 """ 1668 packed_stat = b'AAAAREUHaIpFB2iKAAADAQAtkqUAAIGk' 1669 null_sha = b'xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx' 1670 NULL_PARENT_DETAILS = dirstate.DirState.NULL_PARENT_DETAILS 1671 root_entry = (b'', b'', b'a-root-value'), [ 1672 (b'd', b'', 0, False, packed_stat), 1673 (b'd', b'', 0, False, b'parent-revid'), 1674 ] 1675 a_entry = (b'', b'a', b'a-dir'), [ 1676 (b'd', b'', 0, False, packed_stat), 1677 (b'd', b'', 0, False, b'parent-revid'), 1678 ] 1679 b_entry = (b'', b'b', b'b-dir'), [ 1680 (b'd', b'', 0, False, packed_stat), 1681 (b'd', b'', 0, False, b'parent-revid'), 1682 ] 1683 c_entry = (b'', b'c', b'c-file'), [ 1684 (b'f', null_sha, 10, False, packed_stat), 1685 (b'r', b'b/j', 0, False, b''), 1686 ] 1687 d_entry = (b'', b'd', b'd-file'), [ 1688 (b'f', null_sha, 20, False, packed_stat), 1689 (b'f', b'd', 20, False, b'parent-revid'), 1690 ] 1691 e_entry = (b'a', b'e', b'e-dir'), [ 1692 (b'd', b'', 0, False, packed_stat), 1693 (b'd', b'', 0, False, b'parent-revid'), 1694 ] 1695 f_entry = (b'a', b'f', b'f-file'), [ 1696 (b'f', null_sha, 30, False, packed_stat), 1697 (b'f', b'f', 20, False, b'parent-revid'), 1698 ] 1699 g_entry = (b'b', b'g', b'g-file'), [ 1700 (b'f', null_sha, 30, False, packed_stat), 1701 NULL_PARENT_DETAILS, 1702 ] 1703 h_entry1 = (b'b', b'h\xc3\xa5', b'h-\xc3\xa5-file1'), [ 1704 (b'f', null_sha, 40, False, packed_stat), 1705 NULL_PARENT_DETAILS, 1706 ] 1707 h_entry2 = (b'b', b'h\xc3\xa5', b'h-\xc3\xa5-file2'), [ 1708 NULL_PARENT_DETAILS, 1709 (b'f', b'h', 20, False, b'parent-revid'), 1710 ] 1711 i_entry = (b'b', b'i', b'i-file'), [ 1712 NULL_PARENT_DETAILS, 1713 (b'f', b'h', 20, False, b'parent-revid'), 1714 ] 1715 j_entry = (b'b', b'j', b'c-file'), [ 1716 (b'r', b'c', 0, False, b''), 1717 (b'f', b'j', 20, False, b'parent-revid'), 1718 ] 1719 dirblocks = [] 1720 dirblocks.append((b'', [root_entry])) 1721 dirblocks.append((b'', [a_entry, b_entry, c_entry, d_entry])) 1722 dirblocks.append((b'a', [e_entry, f_entry])) 1723 dirblocks.append( 1724 (b'b', [g_entry, h_entry1, h_entry2, i_entry, j_entry])) 1725 state = dirstate.DirState.initialize('dirstate') 1726 state._validate() 1727 try: 1728 state._set_data([b'parent'], dirblocks) 1729 except: 1730 state.unlock() 1731 raise 1732 return state, dirblocks 1733 1734 def test_iter_children_b(self): 1735 state, dirblocks = self.create_dirstate_with_two_trees() 1736 self.addCleanup(state.unlock) 1737 expected_result = [] 1738 expected_result.append(dirblocks[3][1][2]) # h2 1739 expected_result.append(dirblocks[3][1][3]) # i 1740 expected_result.append(dirblocks[3][1][4]) # j 1741 self.assertEqual(expected_result, 1742 list(state._iter_child_entries(1, b'b'))) 1743 1744 def test_iter_child_root(self): 1745 state, dirblocks = self.create_dirstate_with_two_trees() 1746 self.addCleanup(state.unlock) 1747 expected_result = [] 1748 expected_result.append(dirblocks[1][1][0]) # a 1749 expected_result.append(dirblocks[1][1][1]) # b 1750 expected_result.append(dirblocks[1][1][3]) # d 1751 expected_result.append(dirblocks[2][1][0]) # e 1752 expected_result.append(dirblocks[2][1][1]) # f 1753 expected_result.append(dirblocks[3][1][2]) # h2 1754 expected_result.append(dirblocks[3][1][3]) # i 1755 expected_result.append(dirblocks[3][1][4]) # j 1756 self.assertEqual(expected_result, 1757 list(state._iter_child_entries(1, b''))) 1758 1759 1760class TestDirstateSortOrder(tests.TestCaseWithTransport): 1761 """Test that DirState adds entries in the right order.""" 1762 1763 def test_add_sorting(self): 1764 """Add entries in lexicographical order, we get path sorted order. 1765 1766 This tests it to a depth of 4, to make sure we don't just get it right 1767 at a single depth. 'a/a' should come before 'a-a', even though it 1768 doesn't lexicographically. 1769 """ 1770 dirs = ['a', 'a/a', 'a/a/a', 'a/a/a/a', 1771 'a-a', 'a/a-a', 'a/a/a-a', 'a/a/a/a-a', 1772 ] 1773 null_sha = b'' 1774 state = dirstate.DirState.initialize('dirstate') 1775 self.addCleanup(state.unlock) 1776 1777 fake_stat = os.stat('dirstate') 1778 for d in dirs: 1779 d_id = d.encode('utf-8').replace(b'/', b'_') + b'-id' 1780 file_path = d + '/f' 1781 file_id = file_path.encode('utf-8').replace(b'/', b'_') + b'-id' 1782 state.add(d, d_id, 'directory', fake_stat, null_sha) 1783 state.add(file_path, file_id, 'file', fake_stat, null_sha) 1784 1785 expected = [b'', b'', b'a', 1786 b'a/a', b'a/a/a', b'a/a/a/a', 1787 b'a/a/a/a-a', b'a/a/a-a', b'a/a-a', b'a-a', 1788 ] 1789 1790 def split(p): return p.split(b'/') 1791 self.assertEqual(sorted(expected, key=split), expected) 1792 dirblock_names = [d[0] for d in state._dirblocks] 1793 self.assertEqual(expected, dirblock_names) 1794 1795 def test_set_parent_trees_correct_order(self): 1796 """After calling set_parent_trees() we should maintain the order.""" 1797 dirs = ['a', 'a-a', 'a/a'] 1798 null_sha = b'' 1799 state = dirstate.DirState.initialize('dirstate') 1800 self.addCleanup(state.unlock) 1801 1802 fake_stat = os.stat('dirstate') 1803 for d in dirs: 1804 d_id = d.encode('utf-8').replace(b'/', b'_') + b'-id' 1805 file_path = d + '/f' 1806 file_id = file_path.encode('utf-8').replace(b'/', b'_') + b'-id' 1807 state.add(d, d_id, 'directory', fake_stat, null_sha) 1808 state.add(file_path, file_id, 'file', fake_stat, null_sha) 1809 1810 expected = [b'', b'', b'a', b'a/a', b'a-a'] 1811 dirblock_names = [d[0] for d in state._dirblocks] 1812 self.assertEqual(expected, dirblock_names) 1813 1814 # *really* cheesy way to just get an empty tree 1815 repo = self.make_repository('repo') 1816 empty_tree = repo.revision_tree(_mod_revision.NULL_REVISION) 1817 state.set_parent_trees([('null:', empty_tree)], []) 1818 1819 dirblock_names = [d[0] for d in state._dirblocks] 1820 self.assertEqual(expected, dirblock_names) 1821 1822 1823class InstrumentedDirState(dirstate.DirState): 1824 """An DirState with instrumented sha1 functionality.""" 1825 1826 def __init__(self, path, sha1_provider, worth_saving_limit=0, 1827 use_filesystem_for_exec=True): 1828 super(InstrumentedDirState, self).__init__( 1829 path, sha1_provider, worth_saving_limit=worth_saving_limit, 1830 use_filesystem_for_exec=use_filesystem_for_exec) 1831 self._time_offset = 0 1832 self._log = [] 1833 # member is dynamically set in DirState.__init__ to turn on trace 1834 self._sha1_provider = sha1_provider 1835 self._sha1_file = self._sha1_file_and_log 1836 1837 def _sha_cutoff_time(self): 1838 timestamp = super(InstrumentedDirState, self)._sha_cutoff_time() 1839 self._cutoff_time = timestamp + self._time_offset 1840 1841 def _sha1_file_and_log(self, abspath): 1842 self._log.append(('sha1', abspath)) 1843 return self._sha1_provider.sha1(abspath) 1844 1845 def _read_link(self, abspath, old_link): 1846 self._log.append(('read_link', abspath, old_link)) 1847 return super(InstrumentedDirState, self)._read_link(abspath, old_link) 1848 1849 def _lstat(self, abspath, entry): 1850 self._log.append(('lstat', abspath)) 1851 return super(InstrumentedDirState, self)._lstat(abspath, entry) 1852 1853 def _is_executable(self, mode, old_executable): 1854 self._log.append(('is_exec', mode, old_executable)) 1855 return super(InstrumentedDirState, self)._is_executable(mode, 1856 old_executable) 1857 1858 def adjust_time(self, secs): 1859 """Move the clock forward or back. 1860 1861 :param secs: The amount to adjust the clock by. Positive values make it 1862 seem as if we are in the future, negative values make it seem like we 1863 are in the past. 1864 """ 1865 self._time_offset += secs 1866 self._cutoff_time = None 1867 1868 1869class _FakeStat(object): 1870 """A class with the same attributes as a real stat result.""" 1871 1872 def __init__(self, size, mtime, ctime, dev, ino, mode): 1873 self.st_size = size 1874 self.st_mtime = mtime 1875 self.st_ctime = ctime 1876 self.st_dev = dev 1877 self.st_ino = ino 1878 self.st_mode = mode 1879 1880 @staticmethod 1881 def from_stat(st): 1882 return _FakeStat(st.st_size, st.st_mtime, st.st_ctime, st.st_dev, 1883 st.st_ino, st.st_mode) 1884 1885 1886class TestPackStat(tests.TestCaseWithTransport): 1887 1888 def assertPackStat(self, expected, stat_value): 1889 """Check the packed and serialized form of a stat value.""" 1890 self.assertEqual(expected, dirstate.pack_stat(stat_value)) 1891 1892 def test_pack_stat_int(self): 1893 st = _FakeStat(6859, 1172758614, 1172758617, 777, 6499538, 0o100644) 1894 # Make sure that all parameters have an impact on the packed stat. 1895 self.assertPackStat(b'AAAay0Xm4FZF5uBZAAADCQBjLNIAAIGk', st) 1896 st.st_size = 7000 1897 # ay0 => bWE 1898 self.assertPackStat(b'AAAbWEXm4FZF5uBZAAADCQBjLNIAAIGk', st) 1899 st.st_mtime = 1172758620 1900 # 4FZ => 4Fx 1901 self.assertPackStat(b'AAAbWEXm4FxF5uBZAAADCQBjLNIAAIGk', st) 1902 st.st_ctime = 1172758630 1903 # uBZ => uBm 1904 self.assertPackStat(b'AAAbWEXm4FxF5uBmAAADCQBjLNIAAIGk', st) 1905 st.st_dev = 888 1906 # DCQ => DeA 1907 self.assertPackStat(b'AAAbWEXm4FxF5uBmAAADeABjLNIAAIGk', st) 1908 st.st_ino = 6499540 1909 # LNI => LNQ 1910 self.assertPackStat(b'AAAbWEXm4FxF5uBmAAADeABjLNQAAIGk', st) 1911 st.st_mode = 0o100744 1912 # IGk => IHk 1913 self.assertPackStat(b'AAAbWEXm4FxF5uBmAAADeABjLNQAAIHk', st) 1914 1915 def test_pack_stat_float(self): 1916 """On some platforms mtime and ctime are floats. 1917 1918 Make sure we don't get warnings or errors, and that we ignore changes < 1919 1s 1920 """ 1921 st = _FakeStat(7000, 1172758614.0, 1172758617.0, 1922 777, 6499538, 0o100644) 1923 # These should all be the same as the integer counterparts 1924 self.assertPackStat(b'AAAbWEXm4FZF5uBZAAADCQBjLNIAAIGk', st) 1925 st.st_mtime = 1172758620.0 1926 # FZF5 => FxF5 1927 self.assertPackStat(b'AAAbWEXm4FxF5uBZAAADCQBjLNIAAIGk', st) 1928 st.st_ctime = 1172758630.0 1929 # uBZ => uBm 1930 self.assertPackStat(b'AAAbWEXm4FxF5uBmAAADCQBjLNIAAIGk', st) 1931 # fractional seconds are discarded, so no change from above 1932 st.st_mtime = 1172758620.453 1933 self.assertPackStat(b'AAAbWEXm4FxF5uBmAAADCQBjLNIAAIGk', st) 1934 st.st_ctime = 1172758630.228 1935 self.assertPackStat(b'AAAbWEXm4FxF5uBmAAADCQBjLNIAAIGk', st) 1936 1937 1938class TestBisect(TestCaseWithDirState): 1939 """Test the ability to bisect into the disk format.""" 1940 1941 def assertBisect(self, expected_map, map_keys, state, paths): 1942 """Assert that bisecting for paths returns the right result. 1943 1944 :param expected_map: A map from key => entry value 1945 :param map_keys: The keys to expect for each path 1946 :param state: The DirState object. 1947 :param paths: A list of paths, these will automatically be split into 1948 (dir, name) tuples, and sorted according to how _bisect 1949 requires. 1950 """ 1951 result = state._bisect(paths) 1952 # For now, results are just returned in whatever order we read them. 1953 # We could sort by (dir, name, file_id) or something like that, but in 1954 # the end it would still be fairly arbitrary, and we don't want the 1955 # extra overhead if we can avoid it. So sort everything to make sure 1956 # equality is true 1957 self.assertEqual(len(map_keys), len(paths)) 1958 expected = {} 1959 for path, keys in zip(paths, map_keys): 1960 if keys is None: 1961 # This should not be present in the output 1962 continue 1963 expected[path] = sorted(expected_map[k] for k in keys) 1964 1965 # The returned values are just arranged randomly based on when they 1966 # were read, for testing, make sure it is properly sorted. 1967 for path in result: 1968 result[path].sort() 1969 1970 self.assertEqual(expected, result) 1971 1972 def assertBisectDirBlocks(self, expected_map, map_keys, state, paths): 1973 """Assert that bisecting for dirbblocks returns the right result. 1974 1975 :param expected_map: A map from key => expected values 1976 :param map_keys: A nested list of paths we expect to be returned. 1977 Something like [['a', 'b', 'f'], ['b/c', 'b/d']] 1978 :param state: The DirState object. 1979 :param paths: A list of directories 1980 """ 1981 result = state._bisect_dirblocks(paths) 1982 self.assertEqual(len(map_keys), len(paths)) 1983 expected = {} 1984 for path, keys in zip(paths, map_keys): 1985 if keys is None: 1986 # This should not be present in the output 1987 continue 1988 expected[path] = sorted(expected_map[k] for k in keys) 1989 for path in result: 1990 result[path].sort() 1991 1992 self.assertEqual(expected, result) 1993 1994 def assertBisectRecursive(self, expected_map, map_keys, state, paths): 1995 """Assert the return value of a recursive bisection. 1996 1997 :param expected_map: A map from key => entry value 1998 :param map_keys: A list of paths we expect to be returned. 1999 Something like ['a', 'b', 'f', 'b/d', 'b/d2'] 2000 :param state: The DirState object. 2001 :param paths: A list of files and directories. It will be broken up 2002 into (dir, name) pairs and sorted before calling _bisect_recursive. 2003 """ 2004 expected = {} 2005 for key in map_keys: 2006 entry = expected_map[key] 2007 dir_name_id, trees_info = entry 2008 expected[dir_name_id] = trees_info 2009 2010 result = state._bisect_recursive(paths) 2011 2012 self.assertEqual(expected, result) 2013 2014 def test_bisect_each(self): 2015 """Find a single record using bisect.""" 2016 tree, state, expected = self.create_basic_dirstate() 2017 2018 # Bisect should return the rows for the specified files. 2019 self.assertBisect(expected, [[b'']], state, [b'']) 2020 self.assertBisect(expected, [[b'a']], state, [b'a']) 2021 self.assertBisect(expected, [[b'b']], state, [b'b']) 2022 self.assertBisect(expected, [[b'b/c']], state, [b'b/c']) 2023 self.assertBisect(expected, [[b'b/d']], state, [b'b/d']) 2024 self.assertBisect(expected, [[b'b/d/e']], state, [b'b/d/e']) 2025 self.assertBisect(expected, [[b'b-c']], state, [b'b-c']) 2026 self.assertBisect(expected, [[b'f']], state, [b'f']) 2027 2028 def test_bisect_multi(self): 2029 """Bisect can be used to find multiple records at the same time.""" 2030 tree, state, expected = self.create_basic_dirstate() 2031 # Bisect should be capable of finding multiple entries at the same time 2032 self.assertBisect(expected, [[b'a'], [b'b'], [b'f']], 2033 state, [b'a', b'b', b'f']) 2034 self.assertBisect(expected, [[b'f'], [b'b/d'], [b'b/d/e']], 2035 state, [b'f', b'b/d', b'b/d/e']) 2036 self.assertBisect(expected, [[b'b'], [b'b-c'], [b'b/c']], 2037 state, [b'b', b'b-c', b'b/c']) 2038 2039 def test_bisect_one_page(self): 2040 """Test bisect when there is only 1 page to read""" 2041 tree, state, expected = self.create_basic_dirstate() 2042 state._bisect_page_size = 5000 2043 self.assertBisect(expected, [[b'']], state, [b'']) 2044 self.assertBisect(expected, [[b'a']], state, [b'a']) 2045 self.assertBisect(expected, [[b'b']], state, [b'b']) 2046 self.assertBisect(expected, [[b'b/c']], state, [b'b/c']) 2047 self.assertBisect(expected, [[b'b/d']], state, [b'b/d']) 2048 self.assertBisect(expected, [[b'b/d/e']], state, [b'b/d/e']) 2049 self.assertBisect(expected, [[b'b-c']], state, [b'b-c']) 2050 self.assertBisect(expected, [[b'f']], state, [b'f']) 2051 self.assertBisect(expected, [[b'a'], [b'b'], [b'f']], 2052 state, [b'a', b'b', b'f']) 2053 self.assertBisect(expected, [[b'b/d'], [b'b/d/e'], [b'f']], 2054 state, [b'b/d', b'b/d/e', b'f']) 2055 self.assertBisect(expected, [[b'b'], [b'b/c'], [b'b-c']], 2056 state, [b'b', b'b/c', b'b-c']) 2057 2058 def test_bisect_duplicate_paths(self): 2059 """When bisecting for a path, handle multiple entries.""" 2060 tree, state, expected = self.create_duplicated_dirstate() 2061 2062 # Now make sure that both records are properly returned. 2063 self.assertBisect(expected, [[b'']], state, [b'']) 2064 self.assertBisect(expected, [[b'a', b'a2']], state, [b'a']) 2065 self.assertBisect(expected, [[b'b', b'b2']], state, [b'b']) 2066 self.assertBisect(expected, [[b'b/c', b'b/c2']], state, [b'b/c']) 2067 self.assertBisect(expected, [[b'b/d', b'b/d2']], state, [b'b/d']) 2068 self.assertBisect(expected, [[b'b/d/e', b'b/d/e2']], 2069 state, [b'b/d/e']) 2070 self.assertBisect(expected, [[b'b-c', b'b-c2']], state, [b'b-c']) 2071 self.assertBisect(expected, [[b'f', b'f2']], state, [b'f']) 2072 2073 def test_bisect_page_size_too_small(self): 2074 """If the page size is too small, we will auto increase it.""" 2075 tree, state, expected = self.create_basic_dirstate() 2076 state._bisect_page_size = 50 2077 self.assertBisect(expected, [None], state, [b'b/e']) 2078 self.assertBisect(expected, [[b'a']], state, [b'a']) 2079 self.assertBisect(expected, [[b'b']], state, [b'b']) 2080 self.assertBisect(expected, [[b'b/c']], state, [b'b/c']) 2081 self.assertBisect(expected, [[b'b/d']], state, [b'b/d']) 2082 self.assertBisect(expected, [[b'b/d/e']], state, [b'b/d/e']) 2083 self.assertBisect(expected, [[b'b-c']], state, [b'b-c']) 2084 self.assertBisect(expected, [[b'f']], state, [b'f']) 2085 2086 def test_bisect_missing(self): 2087 """Test that bisect return None if it cannot find a path.""" 2088 tree, state, expected = self.create_basic_dirstate() 2089 self.assertBisect(expected, [None], state, [b'foo']) 2090 self.assertBisect(expected, [None], state, [b'b/foo']) 2091 self.assertBisect(expected, [None], state, [b'bar/foo']) 2092 self.assertBisect(expected, [None], state, [b'b-c/foo']) 2093 2094 self.assertBisect(expected, [[b'a'], None, [b'b/d']], 2095 state, [b'a', b'foo', b'b/d']) 2096 2097 def test_bisect_rename(self): 2098 """Check that we find a renamed row.""" 2099 tree, state, expected = self.create_renamed_dirstate() 2100 2101 # Search for the pre and post renamed entries 2102 self.assertBisect(expected, [[b'a']], state, [b'a']) 2103 self.assertBisect(expected, [[b'b/g']], state, [b'b/g']) 2104 self.assertBisect(expected, [[b'b/d']], state, [b'b/d']) 2105 self.assertBisect(expected, [[b'h']], state, [b'h']) 2106 2107 # What about b/d/e? shouldn't that also get 2 directory entries? 2108 self.assertBisect(expected, [[b'b/d/e']], state, [b'b/d/e']) 2109 self.assertBisect(expected, [[b'h/e']], state, [b'h/e']) 2110 2111 def test_bisect_dirblocks(self): 2112 tree, state, expected = self.create_duplicated_dirstate() 2113 self.assertBisectDirBlocks(expected, 2114 [[b'', b'a', b'a2', b'b', b'b2', 2115 b'b-c', b'b-c2', b'f', b'f2']], 2116 state, [b'']) 2117 self.assertBisectDirBlocks(expected, 2118 [[b'b/c', b'b/c2', b'b/d', b'b/d2']], state, [b'b']) 2119 self.assertBisectDirBlocks(expected, 2120 [[b'b/d/e', b'b/d/e2']], state, [b'b/d']) 2121 self.assertBisectDirBlocks(expected, 2122 [[b'', b'a', b'a2', b'b', b'b2', b'b-c', b'b-c2', b'f', b'f2'], 2123 [b'b/c', b'b/c2', b'b/d', b'b/d2'], 2124 [b'b/d/e', b'b/d/e2'], 2125 ], state, [b'', b'b', b'b/d']) 2126 2127 def test_bisect_dirblocks_missing(self): 2128 tree, state, expected = self.create_basic_dirstate() 2129 self.assertBisectDirBlocks(expected, [[b'b/d/e'], None], 2130 state, [b'b/d', b'b/e']) 2131 # Files don't show up in this search 2132 self.assertBisectDirBlocks(expected, [None], state, [b'a']) 2133 self.assertBisectDirBlocks(expected, [None], state, [b'b/c']) 2134 self.assertBisectDirBlocks(expected, [None], state, [b'c']) 2135 self.assertBisectDirBlocks(expected, [None], state, [b'b/d/e']) 2136 self.assertBisectDirBlocks(expected, [None], state, [b'f']) 2137 2138 def test_bisect_recursive_each(self): 2139 tree, state, expected = self.create_basic_dirstate() 2140 self.assertBisectRecursive(expected, [b'a'], state, [b'a']) 2141 self.assertBisectRecursive(expected, [b'b/c'], state, [b'b/c']) 2142 self.assertBisectRecursive(expected, [b'b/d/e'], state, [b'b/d/e']) 2143 self.assertBisectRecursive(expected, [b'b-c'], state, [b'b-c']) 2144 self.assertBisectRecursive(expected, [b'b/d', b'b/d/e'], 2145 state, [b'b/d']) 2146 self.assertBisectRecursive(expected, [b'b', b'b/c', b'b/d', b'b/d/e'], 2147 state, [b'b']) 2148 self.assertBisectRecursive(expected, [b'', b'a', b'b', b'b-c', b'f', b'b/c', 2149 b'b/d', b'b/d/e'], 2150 state, [b'']) 2151 2152 def test_bisect_recursive_multiple(self): 2153 tree, state, expected = self.create_basic_dirstate() 2154 self.assertBisectRecursive( 2155 expected, [b'a', b'b/c'], state, [b'a', b'b/c']) 2156 self.assertBisectRecursive(expected, [b'b/d', b'b/d/e'], 2157 state, [b'b/d', b'b/d/e']) 2158 2159 def test_bisect_recursive_missing(self): 2160 tree, state, expected = self.create_basic_dirstate() 2161 self.assertBisectRecursive(expected, [], state, [b'd']) 2162 self.assertBisectRecursive(expected, [], state, [b'b/e']) 2163 self.assertBisectRecursive(expected, [], state, [b'g']) 2164 self.assertBisectRecursive(expected, [b'a'], state, [b'a', b'g']) 2165 2166 def test_bisect_recursive_renamed(self): 2167 tree, state, expected = self.create_renamed_dirstate() 2168 2169 # Looking for either renamed item should find the other 2170 self.assertBisectRecursive(expected, [b'a', b'b/g'], state, [b'a']) 2171 self.assertBisectRecursive(expected, [b'a', b'b/g'], state, [b'b/g']) 2172 # Looking in the containing directory should find the rename target, 2173 # and anything in a subdir of the renamed target. 2174 self.assertBisectRecursive(expected, [b'a', b'b', b'b/c', b'b/d', 2175 b'b/d/e', b'b/g', b'h', b'h/e'], 2176 state, [b'b']) 2177 2178 2179class TestDirstateValidation(TestCaseWithDirState): 2180 2181 def test_validate_correct_dirstate(self): 2182 state = self.create_complex_dirstate() 2183 state._validate() 2184 state.unlock() 2185 # and make sure we can also validate with a read lock 2186 state.lock_read() 2187 try: 2188 state._validate() 2189 finally: 2190 state.unlock() 2191 2192 def test_dirblock_not_sorted(self): 2193 tree, state, expected = self.create_renamed_dirstate() 2194 state._read_dirblocks_if_needed() 2195 last_dirblock = state._dirblocks[-1] 2196 # we're appending to the dirblock, but this name comes before some of 2197 # the existing names; that's wrong 2198 last_dirblock[1].append( 2199 ((b'h', b'aaaa', b'a-id'), 2200 [(b'a', b'', 0, False, b''), 2201 (b'a', b'', 0, False, b'')])) 2202 e = self.assertRaises(AssertionError, 2203 state._validate) 2204 self.assertContainsRe(str(e), 'not sorted') 2205 2206 def test_dirblock_name_mismatch(self): 2207 tree, state, expected = self.create_renamed_dirstate() 2208 state._read_dirblocks_if_needed() 2209 last_dirblock = state._dirblocks[-1] 2210 # add an entry with the wrong directory name 2211 last_dirblock[1].append( 2212 ((b'', b'z', b'a-id'), 2213 [(b'a', b'', 0, False, b''), 2214 (b'a', b'', 0, False, b'')])) 2215 e = self.assertRaises(AssertionError, 2216 state._validate) 2217 self.assertContainsRe(str(e), 2218 "doesn't match directory name") 2219 2220 def test_dirblock_missing_rename(self): 2221 tree, state, expected = self.create_renamed_dirstate() 2222 state._read_dirblocks_if_needed() 2223 last_dirblock = state._dirblocks[-1] 2224 # make another entry for a-id, without a correct 'r' pointer to 2225 # the real occurrence in the working tree 2226 last_dirblock[1].append( 2227 ((b'h', b'z', b'a-id'), 2228 [(b'a', b'', 0, False, b''), 2229 (b'a', b'', 0, False, b'')])) 2230 e = self.assertRaises(AssertionError, 2231 state._validate) 2232 self.assertContainsRe(str(e), 2233 'file a-id is absent in row') 2234 2235 2236class TestDirstateTreeReference(TestCaseWithDirState): 2237 2238 def test_reference_revision_is_none(self): 2239 tree = self.make_branch_and_tree('tree', format='development-subtree') 2240 subtree = self.make_branch_and_tree('tree/subtree', 2241 format='development-subtree') 2242 subtree.set_root_id(b'subtree') 2243 tree.add_reference(subtree) 2244 tree.add('subtree') 2245 state = dirstate.DirState.from_tree(tree, 'dirstate') 2246 key = (b'', b'subtree', b'subtree') 2247 expected = (b'', [(key, 2248 [(b't', b'', 0, False, b'xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx')])]) 2249 2250 try: 2251 self.assertEqual(expected, state._find_block(key)) 2252 finally: 2253 state.unlock() 2254 2255 2256class TestDiscardMergeParents(TestCaseWithDirState): 2257 2258 def test_discard_no_parents(self): 2259 # This should be a no-op 2260 state = self.create_empty_dirstate() 2261 self.addCleanup(state.unlock) 2262 state._discard_merge_parents() 2263 state._validate() 2264 2265 def test_discard_one_parent(self): 2266 # No-op 2267 packed_stat = b'AAAAREUHaIpFB2iKAAADAQAtkqUAAIGk' 2268 root_entry_direntry = (b'', b'', b'a-root-value'), [ 2269 (b'd', b'', 0, False, packed_stat), 2270 (b'd', b'', 0, False, packed_stat), 2271 ] 2272 dirblocks = [] 2273 dirblocks.append((b'', [root_entry_direntry])) 2274 dirblocks.append((b'', [])) 2275 2276 state = self.create_empty_dirstate() 2277 self.addCleanup(state.unlock) 2278 state._set_data([b'parent-id'], dirblocks[:]) 2279 state._validate() 2280 2281 state._discard_merge_parents() 2282 state._validate() 2283 self.assertEqual(dirblocks, state._dirblocks) 2284 2285 def test_discard_simple(self): 2286 # No-op 2287 packed_stat = b'AAAAREUHaIpFB2iKAAADAQAtkqUAAIGk' 2288 root_entry_direntry = (b'', b'', b'a-root-value'), [ 2289 (b'd', b'', 0, False, packed_stat), 2290 (b'd', b'', 0, False, packed_stat), 2291 (b'd', b'', 0, False, packed_stat), 2292 ] 2293 expected_root_entry_direntry = (b'', b'', b'a-root-value'), [ 2294 (b'd', b'', 0, False, packed_stat), 2295 (b'd', b'', 0, False, packed_stat), 2296 ] 2297 dirblocks = [] 2298 dirblocks.append((b'', [root_entry_direntry])) 2299 dirblocks.append((b'', [])) 2300 2301 state = self.create_empty_dirstate() 2302 self.addCleanup(state.unlock) 2303 state._set_data([b'parent-id', b'merged-id'], dirblocks[:]) 2304 state._validate() 2305 2306 # This should strip of the extra column 2307 state._discard_merge_parents() 2308 state._validate() 2309 expected_dirblocks = [(b'', [expected_root_entry_direntry]), (b'', [])] 2310 self.assertEqual(expected_dirblocks, state._dirblocks) 2311 2312 def test_discard_absent(self): 2313 """If entries are only in a merge, discard should remove the entries""" 2314 null_stat = dirstate.DirState.NULLSTAT 2315 present_dir = (b'd', b'', 0, False, null_stat) 2316 present_file = (b'f', b'', 0, False, null_stat) 2317 absent = dirstate.DirState.NULL_PARENT_DETAILS 2318 root_key = (b'', b'', b'a-root-value') 2319 file_in_root_key = (b'', b'file-in-root', b'a-file-id') 2320 file_in_merged_key = (b'', b'file-in-merged', b'b-file-id') 2321 dirblocks = [(b'', [(root_key, [present_dir, present_dir, present_dir])]), 2322 (b'', [(file_in_merged_key, 2323 [absent, absent, present_file]), 2324 (file_in_root_key, 2325 [present_file, present_file, present_file]), 2326 ]), 2327 ] 2328 2329 state = self.create_empty_dirstate() 2330 self.addCleanup(state.unlock) 2331 state._set_data([b'parent-id', b'merged-id'], dirblocks[:]) 2332 state._validate() 2333 2334 exp_dirblocks = [(b'', [(root_key, [present_dir, present_dir])]), 2335 (b'', [(file_in_root_key, 2336 [present_file, present_file]), 2337 ]), 2338 ] 2339 state._discard_merge_parents() 2340 state._validate() 2341 self.assertEqual(exp_dirblocks, state._dirblocks) 2342 2343 def test_discard_renamed(self): 2344 null_stat = dirstate.DirState.NULLSTAT 2345 present_dir = (b'd', b'', 0, False, null_stat) 2346 present_file = (b'f', b'', 0, False, null_stat) 2347 absent = dirstate.DirState.NULL_PARENT_DETAILS 2348 root_key = (b'', b'', b'a-root-value') 2349 file_in_root_key = (b'', b'file-in-root', b'a-file-id') 2350 # Renamed relative to parent 2351 file_rename_s_key = (b'', b'file-s', b'b-file-id') 2352 file_rename_t_key = (b'', b'file-t', b'b-file-id') 2353 # And one that is renamed between the parents, but absent in this 2354 key_in_1 = (b'', b'file-in-1', b'c-file-id') 2355 key_in_2 = (b'', b'file-in-2', b'c-file-id') 2356 2357 dirblocks = [ 2358 (b'', [(root_key, [present_dir, present_dir, present_dir])]), 2359 (b'', [(key_in_1, 2360 [absent, present_file, (b'r', b'file-in-2', b'c-file-id')]), 2361 (key_in_2, 2362 [absent, (b'r', b'file-in-1', b'c-file-id'), present_file]), 2363 (file_in_root_key, 2364 [present_file, present_file, present_file]), 2365 (file_rename_s_key, 2366 [(b'r', b'file-t', b'b-file-id'), absent, present_file]), 2367 (file_rename_t_key, 2368 [present_file, absent, (b'r', b'file-s', b'b-file-id')]), 2369 ]), 2370 ] 2371 exp_dirblocks = [ 2372 (b'', [(root_key, [present_dir, present_dir])]), 2373 (b'', [(key_in_1, [absent, present_file]), 2374 (file_in_root_key, [present_file, present_file]), 2375 (file_rename_t_key, [present_file, absent]), 2376 ]), 2377 ] 2378 state = self.create_empty_dirstate() 2379 self.addCleanup(state.unlock) 2380 state._set_data([b'parent-id', b'merged-id'], dirblocks[:]) 2381 state._validate() 2382 2383 state._discard_merge_parents() 2384 state._validate() 2385 self.assertEqual(exp_dirblocks, state._dirblocks) 2386 2387 def test_discard_all_subdir(self): 2388 null_stat = dirstate.DirState.NULLSTAT 2389 present_dir = (b'd', b'', 0, False, null_stat) 2390 present_file = (b'f', b'', 0, False, null_stat) 2391 absent = dirstate.DirState.NULL_PARENT_DETAILS 2392 root_key = (b'', b'', b'a-root-value') 2393 subdir_key = (b'', b'sub', b'dir-id') 2394 child1_key = (b'sub', b'child1', b'child1-id') 2395 child2_key = (b'sub', b'child2', b'child2-id') 2396 child3_key = (b'sub', b'child3', b'child3-id') 2397 2398 dirblocks = [ 2399 (b'', [(root_key, [present_dir, present_dir, present_dir])]), 2400 (b'', [(subdir_key, [present_dir, present_dir, present_dir])]), 2401 (b'sub', [(child1_key, [absent, absent, present_file]), 2402 (child2_key, [absent, absent, present_file]), 2403 (child3_key, [absent, absent, present_file]), 2404 ]), 2405 ] 2406 exp_dirblocks = [ 2407 (b'', [(root_key, [present_dir, present_dir])]), 2408 (b'', [(subdir_key, [present_dir, present_dir])]), 2409 (b'sub', []), 2410 ] 2411 state = self.create_empty_dirstate() 2412 self.addCleanup(state.unlock) 2413 state._set_data([b'parent-id', b'merged-id'], dirblocks[:]) 2414 state._validate() 2415 2416 state._discard_merge_parents() 2417 state._validate() 2418 self.assertEqual(exp_dirblocks, state._dirblocks) 2419 2420 2421class Test_InvEntryToDetails(tests.TestCase): 2422 2423 def assertDetails(self, expected, inv_entry): 2424 details = dirstate.DirState._inv_entry_to_details(inv_entry) 2425 self.assertEqual(expected, details) 2426 # details should always allow join() and always be a plain str when 2427 # finished 2428 (minikind, fingerprint, size, executable, tree_data) = details 2429 self.assertIsInstance(minikind, bytes) 2430 self.assertIsInstance(fingerprint, bytes) 2431 self.assertIsInstance(tree_data, bytes) 2432 2433 def test_unicode_symlink(self): 2434 inv_entry = inventory.InventoryLink(b'link-file-id', 2435 u'nam\N{Euro Sign}e', 2436 b'link-parent-id') 2437 inv_entry.revision = b'link-revision-id' 2438 target = u'link-targ\N{Euro Sign}t' 2439 inv_entry.symlink_target = target 2440 self.assertDetails((b'l', target.encode('UTF-8'), 0, False, 2441 b'link-revision-id'), inv_entry) 2442 2443 2444class TestSHA1Provider(tests.TestCaseInTempDir): 2445 2446 def test_sha1provider_is_an_interface(self): 2447 p = dirstate.SHA1Provider() 2448 self.assertRaises(NotImplementedError, p.sha1, "foo") 2449 self.assertRaises(NotImplementedError, p.stat_and_sha1, "foo") 2450 2451 def test_defaultsha1provider_sha1(self): 2452 text = b'test\r\nwith\nall\rpossible line endings\r\n' 2453 self.build_tree_contents([('foo', text)]) 2454 expected_sha = osutils.sha_string(text) 2455 p = dirstate.DefaultSHA1Provider() 2456 self.assertEqual(expected_sha, p.sha1('foo')) 2457 2458 def test_defaultsha1provider_stat_and_sha1(self): 2459 text = b'test\r\nwith\nall\rpossible line endings\r\n' 2460 self.build_tree_contents([('foo', text)]) 2461 expected_sha = osutils.sha_string(text) 2462 p = dirstate.DefaultSHA1Provider() 2463 statvalue, sha1 = p.stat_and_sha1('foo') 2464 self.assertTrue(len(statvalue) >= 10) 2465 self.assertEqual(len(text), statvalue.st_size) 2466 self.assertEqual(expected_sha, sha1) 2467 2468 2469class _Repo(object): 2470 """A minimal api to get InventoryRevisionTree to work.""" 2471 2472 def __init__(self): 2473 default_format = controldir.format_registry.make_controldir('default') 2474 self._format = default_format.repository_format 2475 2476 def lock_read(self): 2477 pass 2478 2479 def unlock(self): 2480 pass 2481 2482 2483class TestUpdateBasisByDelta(tests.TestCase): 2484 2485 def path_to_ie(self, path, file_id, rev_id, dir_ids): 2486 if path.endswith('/'): 2487 is_dir = True 2488 path = path[:-1] 2489 else: 2490 is_dir = False 2491 dirname, basename = osutils.split(path) 2492 try: 2493 dir_id = dir_ids[dirname] 2494 except KeyError: 2495 dir_id = osutils.basename(dirname).encode('utf-8') + b'-id' 2496 if is_dir: 2497 ie = inventory.InventoryDirectory(file_id, basename, dir_id) 2498 dir_ids[path] = file_id 2499 else: 2500 ie = inventory.InventoryFile(file_id, basename, dir_id) 2501 ie.text_size = 0 2502 ie.text_sha1 = b'' 2503 ie.revision = rev_id 2504 return ie 2505 2506 def create_tree_from_shape(self, rev_id, shape): 2507 dir_ids = {'': b'root-id'} 2508 inv = inventory.Inventory(b'root-id', rev_id) 2509 for info in shape: 2510 if len(info) == 2: 2511 path, file_id = info 2512 ie_rev_id = rev_id 2513 else: 2514 path, file_id, ie_rev_id = info 2515 if path == '': 2516 # Replace the root entry 2517 del inv._byid[inv.root.file_id] 2518 inv.root.file_id = file_id 2519 inv._byid[file_id] = inv.root 2520 dir_ids[''] = file_id 2521 continue 2522 inv.add(self.path_to_ie(path, file_id, ie_rev_id, dir_ids)) 2523 return inventorytree.InventoryRevisionTree(_Repo(), inv, rev_id) 2524 2525 def create_empty_dirstate(self): 2526 fd, path = tempfile.mkstemp(prefix='bzr-dirstate') 2527 self.addCleanup(os.remove, path) 2528 os.close(fd) 2529 state = dirstate.DirState.initialize(path) 2530 self.addCleanup(state.unlock) 2531 return state 2532 2533 def create_inv_delta(self, delta, rev_id): 2534 """Translate a 'delta shape' into an actual InventoryDelta""" 2535 dir_ids = {'': b'root-id'} 2536 inv_delta = [] 2537 for old_path, new_path, file_id in delta: 2538 if old_path is not None and old_path.endswith('/'): 2539 # Don't have to actually do anything for this, because only 2540 # new_path creates InventoryEntries 2541 old_path = old_path[:-1] 2542 if new_path is None: # Delete 2543 inv_delta.append((old_path, None, file_id, None)) 2544 continue 2545 ie = self.path_to_ie(new_path, file_id, rev_id, dir_ids) 2546 inv_delta.append((old_path, new_path, file_id, ie)) 2547 return inv_delta 2548 2549 def assertUpdate(self, active, basis, target): 2550 """Assert that update_basis_by_delta works how we want. 2551 2552 Set up a DirState object with active_shape for tree 0, basis_shape for 2553 tree 1. Then apply the delta from basis_shape to target_shape, 2554 and assert that the DirState is still valid, and that its stored 2555 content matches the target_shape. 2556 """ 2557 active_tree = self.create_tree_from_shape(b'active', active) 2558 basis_tree = self.create_tree_from_shape(b'basis', basis) 2559 target_tree = self.create_tree_from_shape(b'target', target) 2560 state = self.create_empty_dirstate() 2561 state.set_state_from_scratch(active_tree.root_inventory, 2562 [(b'basis', basis_tree)], []) 2563 delta = target_tree.root_inventory._make_delta( 2564 basis_tree.root_inventory) 2565 state.update_basis_by_delta(delta, b'target') 2566 state._validate() 2567 dirstate_tree = workingtree_4.DirStateRevisionTree( 2568 state, b'target', _Repo(), None) 2569 # The target now that delta has been applied should match the 2570 # RevisionTree 2571 self.assertEqual([], list(dirstate_tree.iter_changes(target_tree))) 2572 # And the dirblock state should be identical to the state if we created 2573 # it from scratch. 2574 state2 = self.create_empty_dirstate() 2575 state2.set_state_from_scratch(active_tree.root_inventory, 2576 [(b'target', target_tree)], []) 2577 self.assertEqual(state2._dirblocks, state._dirblocks) 2578 return state 2579 2580 def assertBadDelta(self, active, basis, delta): 2581 """Test that we raise InconsistentDelta when appropriate. 2582 2583 :param active: The active tree shape 2584 :param basis: The basis tree shape 2585 :param delta: A description of the delta to apply. Similar to the form 2586 for regular inventory deltas, but omitting the InventoryEntry. 2587 So adding a file is: (None, 'path', b'file-id') 2588 Adding a directory is: (None, 'path/', b'dir-id') 2589 Renaming a dir is: ('old/', 'new/', b'dir-id') 2590 etc. 2591 """ 2592 active_tree = self.create_tree_from_shape(b'active', active) 2593 basis_tree = self.create_tree_from_shape(b'basis', basis) 2594 inv_delta = self.create_inv_delta(delta, b'target') 2595 state = self.create_empty_dirstate() 2596 state.set_state_from_scratch(active_tree.root_inventory, 2597 [(b'basis', basis_tree)], []) 2598 self.assertRaises(errors.InconsistentDelta, 2599 state.update_basis_by_delta, inv_delta, b'target') 2600 # try: 2601 ## state.update_basis_by_delta(inv_delta, b'target') 2602 # except errors.InconsistentDelta, e: 2603 ## import pdb; pdb.set_trace() 2604 # else: 2605 ## import pdb; pdb.set_trace() 2606 self.assertTrue(state._changes_aborted) 2607 2608 def test_remove_file_matching_active_state(self): 2609 state = self.assertUpdate( 2610 active=[], 2611 basis=[('file', b'file-id')], 2612 target=[], 2613 ) 2614 2615 def test_remove_file_present_in_active_state(self): 2616 state = self.assertUpdate( 2617 active=[('file', b'file-id')], 2618 basis=[('file', b'file-id')], 2619 target=[], 2620 ) 2621 2622 def test_remove_file_present_elsewhere_in_active_state(self): 2623 state = self.assertUpdate( 2624 active=[('other-file', b'file-id')], 2625 basis=[('file', b'file-id')], 2626 target=[], 2627 ) 2628 2629 def test_remove_file_active_state_has_diff_file(self): 2630 state = self.assertUpdate( 2631 active=[('file', b'file-id-2')], 2632 basis=[('file', b'file-id')], 2633 target=[], 2634 ) 2635 2636 def test_remove_file_active_state_has_diff_file_and_file_elsewhere(self): 2637 state = self.assertUpdate( 2638 active=[('file', b'file-id-2'), 2639 ('other-file', b'file-id')], 2640 basis=[('file', b'file-id')], 2641 target=[], 2642 ) 2643 2644 def test_add_file_matching_active_state(self): 2645 state = self.assertUpdate( 2646 active=[('file', b'file-id')], 2647 basis=[], 2648 target=[('file', b'file-id')], 2649 ) 2650 2651 def test_add_file_in_empty_dir_not_matching_active_state(self): 2652 state = self.assertUpdate( 2653 active=[], 2654 basis=[('dir/', b'dir-id')], 2655 target=[('dir/', b'dir-id', b'basis'), ('dir/file', b'file-id')], 2656 ) 2657 2658 def test_add_file_missing_in_active_state(self): 2659 state = self.assertUpdate( 2660 active=[], 2661 basis=[], 2662 target=[('file', b'file-id')], 2663 ) 2664 2665 def test_add_file_elsewhere_in_active_state(self): 2666 state = self.assertUpdate( 2667 active=[('other-file', b'file-id')], 2668 basis=[], 2669 target=[('file', b'file-id')], 2670 ) 2671 2672 def test_add_file_active_state_has_diff_file_and_file_elsewhere(self): 2673 state = self.assertUpdate( 2674 active=[('other-file', b'file-id'), 2675 ('file', b'file-id-2')], 2676 basis=[], 2677 target=[('file', b'file-id')], 2678 ) 2679 2680 def test_rename_file_matching_active_state(self): 2681 state = self.assertUpdate( 2682 active=[('other-file', b'file-id')], 2683 basis=[('file', b'file-id')], 2684 target=[('other-file', b'file-id')], 2685 ) 2686 2687 def test_rename_file_missing_in_active_state(self): 2688 state = self.assertUpdate( 2689 active=[], 2690 basis=[('file', b'file-id')], 2691 target=[('other-file', b'file-id')], 2692 ) 2693 2694 def test_rename_file_present_elsewhere_in_active_state(self): 2695 state = self.assertUpdate( 2696 active=[('third', b'file-id')], 2697 basis=[('file', b'file-id')], 2698 target=[('other-file', b'file-id')], 2699 ) 2700 2701 def test_rename_file_active_state_has_diff_source_file(self): 2702 state = self.assertUpdate( 2703 active=[('file', b'file-id-2')], 2704 basis=[('file', b'file-id')], 2705 target=[('other-file', b'file-id')], 2706 ) 2707 2708 def test_rename_file_active_state_has_diff_target_file(self): 2709 state = self.assertUpdate( 2710 active=[('other-file', b'file-id-2')], 2711 basis=[('file', b'file-id')], 2712 target=[('other-file', b'file-id')], 2713 ) 2714 2715 def test_rename_file_active_has_swapped_files(self): 2716 state = self.assertUpdate( 2717 active=[('file', b'file-id'), 2718 ('other-file', b'file-id-2')], 2719 basis=[('file', b'file-id'), 2720 ('other-file', b'file-id-2')], 2721 target=[('file', b'file-id-2'), 2722 ('other-file', b'file-id')]) 2723 2724 def test_rename_file_basis_has_swapped_files(self): 2725 state = self.assertUpdate( 2726 active=[('file', b'file-id'), 2727 ('other-file', b'file-id-2')], 2728 basis=[('file', b'file-id-2'), 2729 ('other-file', b'file-id')], 2730 target=[('file', b'file-id'), 2731 ('other-file', b'file-id-2')]) 2732 2733 def test_rename_directory_with_contents(self): 2734 state = self.assertUpdate( # active matches basis 2735 active=[('dir1/', b'dir-id'), 2736 ('dir1/file', b'file-id')], 2737 basis=[('dir1/', b'dir-id'), 2738 ('dir1/file', b'file-id')], 2739 target=[('dir2/', b'dir-id'), 2740 ('dir2/file', b'file-id')]) 2741 state = self.assertUpdate( # active matches target 2742 active=[('dir2/', b'dir-id'), 2743 ('dir2/file', b'file-id')], 2744 basis=[('dir1/', b'dir-id'), 2745 ('dir1/file', b'file-id')], 2746 target=[('dir2/', b'dir-id'), 2747 ('dir2/file', b'file-id')]) 2748 state = self.assertUpdate( # active empty 2749 active=[], 2750 basis=[('dir1/', b'dir-id'), 2751 ('dir1/file', b'file-id')], 2752 target=[('dir2/', b'dir-id'), 2753 ('dir2/file', b'file-id')]) 2754 state = self.assertUpdate( # active present at other location 2755 active=[('dir3/', b'dir-id'), 2756 ('dir3/file', b'file-id')], 2757 basis=[('dir1/', b'dir-id'), 2758 ('dir1/file', b'file-id')], 2759 target=[('dir2/', b'dir-id'), 2760 ('dir2/file', b'file-id')]) 2761 state = self.assertUpdate( # active has different ids 2762 active=[('dir1/', b'dir1-id'), 2763 ('dir1/file', b'file1-id'), 2764 ('dir2/', b'dir2-id'), 2765 ('dir2/file', b'file2-id')], 2766 basis=[('dir1/', b'dir-id'), 2767 ('dir1/file', b'file-id')], 2768 target=[('dir2/', b'dir-id'), 2769 ('dir2/file', b'file-id')]) 2770 2771 def test_invalid_file_not_present(self): 2772 state = self.assertBadDelta( 2773 active=[('file', b'file-id')], 2774 basis=[('file', b'file-id')], 2775 delta=[('other-file', 'file', b'file-id')]) 2776 2777 def test_invalid_new_id_same_path(self): 2778 # The bad entry comes after 2779 state = self.assertBadDelta( 2780 active=[('file', b'file-id')], 2781 basis=[('file', b'file-id')], 2782 delta=[(None, 'file', b'file-id-2')]) 2783 # The bad entry comes first 2784 state = self.assertBadDelta( 2785 active=[('file', b'file-id-2')], 2786 basis=[('file', b'file-id-2')], 2787 delta=[(None, 'file', b'file-id')]) 2788 2789 def test_invalid_existing_id(self): 2790 state = self.assertBadDelta( 2791 active=[('file', b'file-id')], 2792 basis=[('file', b'file-id')], 2793 delta=[(None, 'file', b'file-id')]) 2794 2795 def test_invalid_parent_missing(self): 2796 state = self.assertBadDelta( 2797 active=[], 2798 basis=[], 2799 delta=[(None, 'path/path2', b'file-id')]) 2800 # Note: we force the active tree to have the directory, by knowing how 2801 # path_to_ie handles entries with missing parents 2802 state = self.assertBadDelta( 2803 active=[('path/', b'path-id')], 2804 basis=[], 2805 delta=[(None, 'path/path2', b'file-id')]) 2806 state = self.assertBadDelta( 2807 active=[('path/', b'path-id'), 2808 ('path/path2', b'file-id')], 2809 basis=[], 2810 delta=[(None, 'path/path2', b'file-id')]) 2811 2812 def test_renamed_dir_same_path(self): 2813 # We replace the parent directory, with another parent dir. But the C 2814 # file doesn't look like it has been moved. 2815 state = self.assertUpdate( # Same as basis 2816 active=[('dir/', b'A-id'), 2817 ('dir/B', b'B-id')], 2818 basis=[('dir/', b'A-id'), 2819 ('dir/B', b'B-id')], 2820 target=[('dir/', b'C-id'), 2821 ('dir/B', b'B-id')]) 2822 state = self.assertUpdate( # Same as target 2823 active=[('dir/', b'C-id'), 2824 ('dir/B', b'B-id')], 2825 basis=[('dir/', b'A-id'), 2826 ('dir/B', b'B-id')], 2827 target=[('dir/', b'C-id'), 2828 ('dir/B', b'B-id')]) 2829 state = self.assertUpdate( # empty active 2830 active=[], 2831 basis=[('dir/', b'A-id'), 2832 ('dir/B', b'B-id')], 2833 target=[('dir/', b'C-id'), 2834 ('dir/B', b'B-id')]) 2835 state = self.assertUpdate( # different active 2836 active=[('dir/', b'D-id'), 2837 ('dir/B', b'B-id')], 2838 basis=[('dir/', b'A-id'), 2839 ('dir/B', b'B-id')], 2840 target=[('dir/', b'C-id'), 2841 ('dir/B', b'B-id')]) 2842 2843 def test_parent_child_swap(self): 2844 state = self.assertUpdate( # Same as basis 2845 active=[('A/', b'A-id'), 2846 ('A/B/', b'B-id'), 2847 ('A/B/C', b'C-id')], 2848 basis=[('A/', b'A-id'), 2849 ('A/B/', b'B-id'), 2850 ('A/B/C', b'C-id')], 2851 target=[('A/', b'B-id'), 2852 ('A/B/', b'A-id'), 2853 ('A/B/C', b'C-id')]) 2854 state = self.assertUpdate( # Same as target 2855 active=[('A/', b'B-id'), 2856 ('A/B/', b'A-id'), 2857 ('A/B/C', b'C-id')], 2858 basis=[('A/', b'A-id'), 2859 ('A/B/', b'B-id'), 2860 ('A/B/C', b'C-id')], 2861 target=[('A/', b'B-id'), 2862 ('A/B/', b'A-id'), 2863 ('A/B/C', b'C-id')]) 2864 state = self.assertUpdate( # empty active 2865 active=[], 2866 basis=[('A/', b'A-id'), 2867 ('A/B/', b'B-id'), 2868 ('A/B/C', b'C-id')], 2869 target=[('A/', b'B-id'), 2870 ('A/B/', b'A-id'), 2871 ('A/B/C', b'C-id')]) 2872 state = self.assertUpdate( # different active 2873 active=[('D/', b'A-id'), 2874 ('D/E/', b'B-id'), 2875 ('F', b'C-id')], 2876 basis=[('A/', b'A-id'), 2877 ('A/B/', b'B-id'), 2878 ('A/B/C', b'C-id')], 2879 target=[('A/', b'B-id'), 2880 ('A/B/', b'A-id'), 2881 ('A/B/C', b'C-id')]) 2882 2883 def test_change_root_id(self): 2884 state = self.assertUpdate( # same as basis 2885 active=[('', b'root-id'), 2886 ('file', b'file-id')], 2887 basis=[('', b'root-id'), 2888 ('file', b'file-id')], 2889 target=[('', b'target-root-id'), 2890 ('file', b'file-id')]) 2891 state = self.assertUpdate( # same as target 2892 active=[('', b'target-root-id'), 2893 ('file', b'file-id')], 2894 basis=[('', b'root-id'), 2895 ('file', b'file-id')], 2896 target=[('', b'target-root-id'), 2897 ('file', b'root-id')]) 2898 state = self.assertUpdate( # all different 2899 active=[('', b'active-root-id'), 2900 ('file', b'file-id')], 2901 basis=[('', b'root-id'), 2902 ('file', b'file-id')], 2903 target=[('', b'target-root-id'), 2904 ('file', b'root-id')]) 2905 2906 def test_change_file_absent_in_active(self): 2907 state = self.assertUpdate( 2908 active=[], 2909 basis=[('file', b'file-id')], 2910 target=[('file', b'file-id')]) 2911 2912 def test_invalid_changed_file(self): 2913 state = self.assertBadDelta( # Not present in basis 2914 active=[('file', b'file-id')], 2915 basis=[], 2916 delta=[('file', 'file', b'file-id')]) 2917 state = self.assertBadDelta( # present at another location in basis 2918 active=[('file', b'file-id')], 2919 basis=[('other-file', b'file-id')], 2920 delta=[('file', 'file', b'file-id')]) 2921