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