~bzr-pqm/bzr/bzr.dev

« back to all changes in this revision

Viewing changes to bzrlib/tests/test_dirstate.py

  • Committer: Canonical.com Patch Queue Manager
  • Date: 2007-03-07 10:45:44 UTC
  • mfrom: (2321.1.2 integration)
  • Revision ID: pqm@pqm.ubuntu.com-20070307104544-59e3e6358e4bdb29
(robertc) Merge dirstate and subtrees. (Robert Collins, Martin Pool, Aaaron Bentley, John A Meinel, James Westby)

Show diffs side-by-side

added added

removed removed

Lines of Context:
 
1
# Copyright (C) 2006, 2007 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., 59 Temple Place, Suite 330, Boston, MA  02111-1307  USA
 
16
 
 
17
"""Tests of the dirstate functionality being built for WorkingTreeFormat4."""
 
18
 
 
19
import bisect
 
20
import os
 
21
import time
 
22
 
 
23
from bzrlib import (
 
24
    dirstate,
 
25
    errors,
 
26
    osutils,
 
27
    )
 
28
from bzrlib.memorytree import MemoryTree
 
29
from bzrlib.tests import TestCase, TestCaseWithTransport
 
30
 
 
31
 
 
32
# TODO:
 
33
# TESTS to write:
 
34
# general checks for NOT_IN_MEMORY error conditions.
 
35
# set_path_id on a NOT_IN_MEMORY dirstate
 
36
# set_path_id  unicode support
 
37
# set_path_id  setting id of a path not root
 
38
# set_path_id  setting id when there are parents without the id in the parents
 
39
# set_path_id  setting id when there are parents with the id in the parents
 
40
# set_path_id  setting id when state is not in memory
 
41
# set_path_id  setting id when state is in memory unmodified
 
42
# set_path_id  setting id when state is in memory modified
 
43
 
 
44
 
 
45
class TestCaseWithDirState(TestCaseWithTransport):
 
46
    """Helper functions for creating DirState objects with various content."""
 
47
 
 
48
    def create_empty_dirstate(self):
 
49
        """Return a locked but empty dirstate"""
 
50
        state = dirstate.DirState.initialize('dirstate')
 
51
        return state
 
52
 
 
53
    def create_dirstate_with_root(self):
 
54
        """Return a write-locked state with a single root entry."""
 
55
        packed_stat = 'AAAAREUHaIpFB2iKAAADAQAtkqUAAIGk'
 
56
        root_entry_direntry = ('', '', 'a-root-value'), [
 
57
            ('d', '', 0, False, packed_stat),
 
58
            ]
 
59
        dirblocks = []
 
60
        dirblocks.append(('', [root_entry_direntry]))
 
61
        dirblocks.append(('', []))
 
62
        state = self.create_empty_dirstate()
 
63
        try:
 
64
            state._set_data([], dirblocks)
 
65
            state._validate()
 
66
        except:
 
67
            state.unlock()
 
68
            raise
 
69
        return state
 
70
 
 
71
    def create_dirstate_with_root_and_subdir(self):
 
72
        """Return a locked DirState with a root and a subdir"""
 
73
        packed_stat = 'AAAAREUHaIpFB2iKAAADAQAtkqUAAIGk'
 
74
        subdir_entry = ('', 'subdir', 'subdir-id'), [
 
75
            ('d', '', 0, False, packed_stat),
 
76
            ]
 
77
        state = self.create_dirstate_with_root()
 
78
        try:
 
79
            dirblocks = list(state._dirblocks)
 
80
            dirblocks[1][1].append(subdir_entry)
 
81
            state._set_data([], dirblocks)
 
82
        except:
 
83
            state.unlock()
 
84
            raise
 
85
        return state
 
86
 
 
87
    def create_complex_dirstate(self):
 
88
        """This dirstate contains multiple files and directories.
 
89
 
 
90
         /        a-root-value
 
91
         a/       a-dir
 
92
         b/       b-dir
 
93
         c        c-file
 
94
         d        d-file
 
95
         a/e/     e-dir
 
96
         a/f      f-file
 
97
         b/g      g-file
 
98
         b/h\xc3\xa5  h-\xc3\xa5-file  #This is u'\xe5' encoded into utf-8
 
99
 
 
100
        # Notice that a/e is an empty directory.
 
101
        """
 
102
        packed_stat = 'AAAAREUHaIpFB2iKAAADAQAtkqUAAIGk'
 
103
        null_sha = 'xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx'
 
104
        root_entry = ('', '', 'a-root-value'), [
 
105
            ('d', '', 0, False, packed_stat),
 
106
            ]
 
107
        a_entry = ('', 'a', 'a-dir'), [
 
108
            ('d', '', 0, False, packed_stat),
 
109
            ]
 
110
        b_entry = ('', 'b', 'b-dir'), [
 
111
            ('d', '', 0, False, packed_stat),
 
112
            ]
 
113
        c_entry = ('', 'c', 'c-file'), [
 
114
            ('f', null_sha, 10, False, packed_stat),
 
115
            ]
 
116
        d_entry = ('', 'd', 'd-file'), [
 
117
            ('f', null_sha, 20, False, packed_stat),
 
118
            ]
 
119
        e_entry = ('a', 'e', 'e-dir'), [
 
120
            ('d', '', 0, False, packed_stat),
 
121
            ]
 
122
        f_entry = ('a', 'f', 'f-file'), [
 
123
            ('f', null_sha, 30, False, packed_stat),
 
124
            ]
 
125
        g_entry = ('b', 'g', 'g-file'), [
 
126
            ('f', null_sha, 30, False, packed_stat),
 
127
            ]
 
128
        h_entry = ('b', 'h\xc3\xa5', 'h-\xc3\xa5-file'), [
 
129
            ('f', null_sha, 40, False, packed_stat),
 
130
            ]
 
131
        dirblocks = []
 
132
        dirblocks.append(('', [root_entry]))
 
133
        dirblocks.append(('', [a_entry, b_entry, c_entry, d_entry]))
 
134
        dirblocks.append(('a', [e_entry, f_entry]))
 
135
        dirblocks.append(('b', [g_entry, h_entry]))
 
136
        state = dirstate.DirState.initialize('dirstate')
 
137
        state._validate()
 
138
        try:
 
139
            state._set_data([], dirblocks)
 
140
        except:
 
141
            state.unlock()
 
142
            raise
 
143
        return state
 
144
 
 
145
    def check_state_with_reopen(self, expected_result, state):
 
146
        """Check that state has current state expected_result.
 
147
 
 
148
        This will check the current state, open the file anew and check it
 
149
        again.
 
150
        This function expects the current state to be locked for writing, and
 
151
        will unlock it before re-opening.
 
152
        This is required because we can't open a lock_read() while something
 
153
        else has a lock_write().
 
154
            write => mutually exclusive lock
 
155
            read => shared lock
 
156
        """
 
157
        # The state should already be write locked, since we just had to do
 
158
        # some operation to get here.
 
159
        assert state._lock_token is not None
 
160
        try:
 
161
            self.assertEqual(expected_result[0],  state.get_parent_ids())
 
162
            # there should be no ghosts in this tree.
 
163
            self.assertEqual([], state.get_ghosts())
 
164
            # there should be one fileid in this tree - the root of the tree.
 
165
            self.assertEqual(expected_result[1], list(state._iter_entries()))
 
166
            state.save()
 
167
        finally:
 
168
            state.unlock()
 
169
        del state # Callers should unlock
 
170
        state = dirstate.DirState.on_file('dirstate')
 
171
        state.lock_read()
 
172
        try:
 
173
            self.assertEqual(expected_result[1], list(state._iter_entries()))
 
174
        finally:
 
175
            state.unlock()
 
176
 
 
177
 
 
178
class TestTreeToDirState(TestCaseWithDirState):
 
179
 
 
180
    def test_empty_to_dirstate(self):
 
181
        """We should be able to create a dirstate for an empty tree."""
 
182
        # There are no files on disk and no parents
 
183
        tree = self.make_branch_and_tree('tree')
 
184
        expected_result = ([], [
 
185
            (('', '', tree.path2id('')), # common details
 
186
             [('d', '', 0, False, dirstate.DirState.NULLSTAT), # current tree
 
187
             ])])
 
188
        state = dirstate.DirState.from_tree(tree, 'dirstate')
 
189
        state._validate()
 
190
        self.check_state_with_reopen(expected_result, state)
 
191
 
 
192
    def test_1_parents_empty_to_dirstate(self):
 
193
        # create a parent by doing a commit
 
194
        tree = self.make_branch_and_tree('tree')
 
195
        rev_id = tree.commit('first post').encode('utf8')
 
196
        root_stat_pack = dirstate.pack_stat(os.stat(tree.basedir))
 
197
        expected_result = ([rev_id], [
 
198
            (('', '', tree.path2id('')), # common details
 
199
             [('d', '', 0, False, dirstate.DirState.NULLSTAT), # current tree
 
200
              ('d', '', 0, False, rev_id), # first parent details
 
201
             ])])
 
202
        state = dirstate.DirState.from_tree(tree, 'dirstate')
 
203
        self.check_state_with_reopen(expected_result, state)
 
204
        state._validate()
 
205
 
 
206
    def test_2_parents_empty_to_dirstate(self):
 
207
        # create a parent by doing a commit
 
208
        tree = self.make_branch_and_tree('tree')
 
209
        rev_id = tree.commit('first post')
 
210
        tree2 = tree.bzrdir.sprout('tree2').open_workingtree()
 
211
        rev_id2 = tree2.commit('second post', allow_pointless=True)
 
212
        tree.merge_from_branch(tree2.branch)
 
213
        expected_result = ([rev_id, rev_id2], [
 
214
            (('', '', tree.path2id('')), # common details
 
215
             [('d', '', 0, False, dirstate.DirState.NULLSTAT), # current tree
 
216
              ('d', '', 0, False, rev_id), # first parent details
 
217
              ('d', '', 0, False, rev_id2), # second parent details
 
218
             ])])
 
219
        state = dirstate.DirState.from_tree(tree, 'dirstate')
 
220
        self.check_state_with_reopen(expected_result, state)
 
221
        state._validate()
 
222
 
 
223
    def test_empty_unknowns_are_ignored_to_dirstate(self):
 
224
        """We should be able to create a dirstate for an empty tree."""
 
225
        # There are no files on disk and no parents
 
226
        tree = self.make_branch_and_tree('tree')
 
227
        self.build_tree(['tree/unknown'])
 
228
        expected_result = ([], [
 
229
            (('', '', tree.path2id('')), # common details
 
230
             [('d', '', 0, False, dirstate.DirState.NULLSTAT), # current tree
 
231
             ])])
 
232
        state = dirstate.DirState.from_tree(tree, 'dirstate')
 
233
        self.check_state_with_reopen(expected_result, state)
 
234
 
 
235
    def get_tree_with_a_file(self):
 
236
        tree = self.make_branch_and_tree('tree')
 
237
        self.build_tree(['tree/a file'])
 
238
        tree.add('a file', 'a file id')
 
239
        return tree
 
240
 
 
241
    def test_non_empty_no_parents_to_dirstate(self):
 
242
        """We should be able to create a dirstate for an empty tree."""
 
243
        # There are files on disk and no parents
 
244
        tree = self.get_tree_with_a_file()
 
245
        expected_result = ([], [
 
246
            (('', '', tree.path2id('')), # common details
 
247
             [('d', '', 0, False, dirstate.DirState.NULLSTAT), # current tree
 
248
             ]),
 
249
            (('', 'a file', 'a file id'), # common
 
250
             [('f', '', 0, False, dirstate.DirState.NULLSTAT), # current
 
251
             ]),
 
252
            ])
 
253
        state = dirstate.DirState.from_tree(tree, 'dirstate')
 
254
        self.check_state_with_reopen(expected_result, state)
 
255
 
 
256
    def test_1_parents_not_empty_to_dirstate(self):
 
257
        # create a parent by doing a commit
 
258
        tree = self.get_tree_with_a_file()
 
259
        rev_id = tree.commit('first post').encode('utf8')
 
260
        # change the current content to be different this will alter stat, sha
 
261
        # and length:
 
262
        self.build_tree_contents([('tree/a file', 'new content\n')])
 
263
        expected_result = ([rev_id], [
 
264
            (('', '', tree.path2id('')), # common details
 
265
             [('d', '', 0, False, dirstate.DirState.NULLSTAT), # current tree
 
266
              ('d', '', 0, False, rev_id), # first parent details
 
267
             ]),
 
268
            (('', 'a file', 'a file id'), # common
 
269
             [('f', '', 0, False, dirstate.DirState.NULLSTAT), # current
 
270
              ('f', 'c3ed76e4bfd45ff1763ca206055bca8e9fc28aa8', 24, False,
 
271
               rev_id), # first parent
 
272
             ]),
 
273
            ])
 
274
        state = dirstate.DirState.from_tree(tree, 'dirstate')
 
275
        self.check_state_with_reopen(expected_result, state)
 
276
 
 
277
    def test_2_parents_not_empty_to_dirstate(self):
 
278
        # create a parent by doing a commit
 
279
        tree = self.get_tree_with_a_file()
 
280
        rev_id = tree.commit('first post').encode('utf8')
 
281
        tree2 = tree.bzrdir.sprout('tree2').open_workingtree()
 
282
        # change the current content to be different this will alter stat, sha
 
283
        # and length:
 
284
        self.build_tree_contents([('tree2/a file', 'merge content\n')])
 
285
        rev_id2 = tree2.commit('second post').encode('utf8')
 
286
        tree.merge_from_branch(tree2.branch)
 
287
        # change the current content to be different this will alter stat, sha
 
288
        # and length again, giving us three distinct values:
 
289
        self.build_tree_contents([('tree/a file', 'new content\n')])
 
290
        expected_result = ([rev_id, rev_id2], [
 
291
            (('', '', tree.path2id('')), # common details
 
292
             [('d', '', 0, False, dirstate.DirState.NULLSTAT), # current tree
 
293
              ('d', '', 0, False, rev_id), # first parent details
 
294
              ('d', '', 0, False, rev_id2), # second parent details
 
295
             ]),
 
296
            (('', 'a file', 'a file id'), # common
 
297
             [('f', '', 0, False, dirstate.DirState.NULLSTAT), # current
 
298
              ('f', 'c3ed76e4bfd45ff1763ca206055bca8e9fc28aa8', 24, False,
 
299
               rev_id), # first parent
 
300
              ('f', '314d796174c9412647c3ce07dfb5d36a94e72958', 14, False,
 
301
               rev_id2), # second parent
 
302
             ]),
 
303
            ])
 
304
        state = dirstate.DirState.from_tree(tree, 'dirstate')
 
305
        self.check_state_with_reopen(expected_result, state)
 
306
 
 
307
    def test_colliding_fileids(self):
 
308
        # test insertion of parents creating several entries at the same path.
 
309
        # we used to have a bug where they could cause the dirstate to break
 
310
        # its ordering invariants.
 
311
        # create some trees to test from
 
312
        parents = []
 
313
        for i in range(7):
 
314
            tree = self.make_branch_and_tree('tree%d' % i)
 
315
            self.build_tree(['tree%d/name' % i,])
 
316
            tree.add(['name'], ['file-id%d' % i])
 
317
            revision_id = 'revid-%d' % i
 
318
            tree.commit('message', rev_id=revision_id)
 
319
            parents.append((revision_id,
 
320
                tree.branch.repository.revision_tree(revision_id)))
 
321
        # now fold these trees into a dirstate
 
322
        state = dirstate.DirState.initialize('dirstate')
 
323
        try:
 
324
            state.set_parent_trees(parents, [])
 
325
            state._validate()
 
326
        finally:
 
327
            state.unlock()
 
328
 
 
329
 
 
330
class TestDirStateOnFile(TestCaseWithDirState):
 
331
 
 
332
    def test_construct_with_path(self):
 
333
        tree = self.make_branch_and_tree('tree')
 
334
        state = dirstate.DirState.from_tree(tree, 'dirstate.from_tree')
 
335
        # we want to be able to get the lines of the dirstate that we will
 
336
        # write to disk.
 
337
        lines = state.get_lines()
 
338
        state.unlock()
 
339
        self.build_tree_contents([('dirstate', ''.join(lines))])
 
340
        # get a state object
 
341
        # no parents, default tree content
 
342
        expected_result = ([], [
 
343
            (('', '', tree.path2id('')), # common details
 
344
             # current tree details, but new from_tree skips statting, it
 
345
             # uses set_state_from_inventory, and thus depends on the
 
346
             # inventory state.
 
347
             [('d', '', 0, False, dirstate.DirState.NULLSTAT),
 
348
             ])
 
349
            ])
 
350
        state = dirstate.DirState.on_file('dirstate')
 
351
        state.lock_write() # check_state_with_reopen will save() and unlock it
 
352
        self.check_state_with_reopen(expected_result, state)
 
353
 
 
354
    def test_can_save_clean_on_file(self):
 
355
        tree = self.make_branch_and_tree('tree')
 
356
        state = dirstate.DirState.from_tree(tree, 'dirstate')
 
357
        try:
 
358
            # doing a save should work here as there have been no changes.
 
359
            state.save()
 
360
            # TODO: stat it and check it hasn't changed; may require waiting
 
361
            # for the state accuracy window.
 
362
        finally:
 
363
            state.unlock()
 
364
 
 
365
 
 
366
class TestDirStateInitialize(TestCaseWithDirState):
 
367
 
 
368
    def test_initialize(self):
 
369
        expected_result = ([], [
 
370
            (('', '', 'TREE_ROOT'), # common details
 
371
             [('d', '', 0, False, dirstate.DirState.NULLSTAT), # current tree
 
372
             ])
 
373
            ])
 
374
        state = dirstate.DirState.initialize('dirstate')
 
375
        try:
 
376
            self.assertIsInstance(state, dirstate.DirState)
 
377
            lines = state.get_lines()
 
378
            self.assertFileEqual(''.join(state.get_lines()),
 
379
                'dirstate')
 
380
            self.check_state_with_reopen(expected_result, state)
 
381
        except:
 
382
            state.unlock()
 
383
            raise
 
384
 
 
385
 
 
386
class TestDirStateManipulations(TestCaseWithDirState):
 
387
 
 
388
    def test_set_state_from_inventory_no_content_no_parents(self):
 
389
        # setting the current inventory is a slow but important api to support.
 
390
        tree1 = self.make_branch_and_memory_tree('tree1')
 
391
        tree1.lock_write()
 
392
        try:
 
393
            tree1.add('')
 
394
            revid1 = tree1.commit('foo').encode('utf8')
 
395
            root_id = tree1.inventory.root.file_id
 
396
            inv = tree1.inventory
 
397
        finally:
 
398
            tree1.unlock()
 
399
        expected_result = [], [
 
400
            (('', '', root_id), [
 
401
             ('d', '', 0, False, dirstate.DirState.NULLSTAT)])]
 
402
        state = dirstate.DirState.initialize('dirstate')
 
403
        try:
 
404
            state.set_state_from_inventory(inv)
 
405
            self.assertEqual(dirstate.DirState.IN_MEMORY_UNMODIFIED,
 
406
                             state._header_state)
 
407
            self.assertEqual(dirstate.DirState.IN_MEMORY_MODIFIED,
 
408
                             state._dirblock_state)
 
409
        except:
 
410
            state.unlock()
 
411
            raise
 
412
        else:
 
413
            # This will unlock it
 
414
            self.check_state_with_reopen(expected_result, state)
 
415
 
 
416
    def test_set_path_id_no_parents(self):
 
417
        """The id of a path can be changed trivally with no parents."""
 
418
        state = dirstate.DirState.initialize('dirstate')
 
419
        try:
 
420
            # check precondition to be sure the state does change appropriately.
 
421
            self.assertEqual(
 
422
                [(('', '', 'TREE_ROOT'), [('d', '', 0, False,
 
423
                   'xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx')])],
 
424
                list(state._iter_entries()))
 
425
            state.set_path_id('', 'foobarbaz')
 
426
            expected_rows = [
 
427
                (('', '', 'foobarbaz'), [('d', '', 0, False,
 
428
                   'xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx')])]
 
429
            self.assertEqual(expected_rows, list(state._iter_entries()))
 
430
            # should work across save too
 
431
            state.save()
 
432
        finally:
 
433
            state.unlock()
 
434
        state = dirstate.DirState.on_file('dirstate')
 
435
        state._validate()
 
436
        state.lock_read()
 
437
        try:
 
438
            self.assertEqual(expected_rows, list(state._iter_entries()))
 
439
        finally:
 
440
            state.unlock()
 
441
 
 
442
    def test_set_path_id_with_parents(self):
 
443
        """Set the root file id in a dirstate with parents"""
 
444
        mt = self.make_branch_and_tree('mt')
 
445
        # in case the default tree format uses a different root id
 
446
        mt.set_root_id('TREE_ROOT')
 
447
        mt.commit('foo', rev_id='parent-revid')
 
448
        rt = mt.branch.repository.revision_tree('parent-revid')
 
449
        state = dirstate.DirState.initialize('dirstate')
 
450
        state._validate()
 
451
        try:
 
452
            state.set_parent_trees([('parent-revid', rt)], ghosts=[])
 
453
            state.set_path_id('', 'foobarbaz')
 
454
            state._validate()
 
455
            # now see that it is what we expected
 
456
            expected_rows = [
 
457
                (('', '', 'TREE_ROOT'),
 
458
                    [('a', '', 0, False, ''),
 
459
                     ('d', '', 0, False, 'parent-revid'),
 
460
                     ]),
 
461
                (('', '', 'foobarbaz'),
 
462
                    [('d', '', 0, False, ''),
 
463
                     ('a', '', 0, False, ''),
 
464
                     ]),
 
465
                ]
 
466
            state._validate()
 
467
            self.assertEqual(expected_rows, list(state._iter_entries()))
 
468
            # should work across save too
 
469
            state.save()
 
470
        finally:
 
471
            state.unlock()
 
472
        # now flush & check we get the same
 
473
        state = dirstate.DirState.on_file('dirstate')
 
474
        state.lock_read()
 
475
        try:
 
476
            state._validate()
 
477
            self.assertEqual(expected_rows, list(state._iter_entries()))
 
478
        finally:
 
479
            state.unlock()
 
480
        # now change within an existing file-backed state
 
481
        state.lock_write()
 
482
        try:
 
483
            state._validate()
 
484
            state.set_path_id('', 'tree-root-2')
 
485
            state._validate()
 
486
        finally:
 
487
            state.unlock()
 
488
 
 
489
 
 
490
    def test_set_parent_trees_no_content(self):
 
491
        # set_parent_trees is a slow but important api to support.
 
492
        tree1 = self.make_branch_and_memory_tree('tree1')
 
493
        tree1.lock_write()
 
494
        try:
 
495
            tree1.add('')
 
496
            revid1 = tree1.commit('foo')
 
497
        finally:
 
498
            tree1.unlock()
 
499
        branch2 = tree1.branch.bzrdir.clone('tree2').open_branch()
 
500
        tree2 = MemoryTree.create_on_branch(branch2)
 
501
        tree2.lock_write()
 
502
        try:
 
503
            revid2 = tree2.commit('foo')
 
504
            root_id = tree2.inventory.root.file_id
 
505
        finally:
 
506
            tree2.unlock()
 
507
        state = dirstate.DirState.initialize('dirstate')
 
508
        try:
 
509
            state.set_path_id('', root_id)
 
510
            state.set_parent_trees(
 
511
                ((revid1, tree1.branch.repository.revision_tree(revid1)),
 
512
                 (revid2, tree2.branch.repository.revision_tree(revid2)),
 
513
                 ('ghost-rev', None)),
 
514
                ['ghost-rev'])
 
515
            # check we can reopen and use the dirstate after setting parent
 
516
            # trees.
 
517
            state._validate()
 
518
            state.save()
 
519
            state._validate()
 
520
        finally:
 
521
            state.unlock()
 
522
        state = dirstate.DirState.on_file('dirstate')
 
523
        state.lock_write()
 
524
        try:
 
525
            self.assertEqual([revid1, revid2, 'ghost-rev'],
 
526
                             state.get_parent_ids())
 
527
            # iterating the entire state ensures that the state is parsable.
 
528
            list(state._iter_entries())
 
529
            # be sure that it sets not appends - change it
 
530
            state.set_parent_trees(
 
531
                ((revid1, tree1.branch.repository.revision_tree(revid1)),
 
532
                 ('ghost-rev', None)),
 
533
                ['ghost-rev'])
 
534
            # and now put it back.
 
535
            state.set_parent_trees(
 
536
                ((revid1, tree1.branch.repository.revision_tree(revid1)),
 
537
                 (revid2, tree2.branch.repository.revision_tree(revid2)),
 
538
                 ('ghost-rev', tree2.branch.repository.revision_tree(None))),
 
539
                ['ghost-rev'])
 
540
            self.assertEqual([revid1, revid2, 'ghost-rev'],
 
541
                             state.get_parent_ids())
 
542
            # the ghost should be recorded as such by set_parent_trees.
 
543
            self.assertEqual(['ghost-rev'], state.get_ghosts())
 
544
            self.assertEqual(
 
545
                [(('', '', root_id), [
 
546
                  ('d', '', 0, False, dirstate.DirState.NULLSTAT),
 
547
                  ('d', '', 0, False, revid1),
 
548
                  ('d', '', 0, False, revid2)
 
549
                  ])],
 
550
                list(state._iter_entries()))
 
551
        finally:
 
552
            state.unlock()
 
553
 
 
554
    def test_set_parent_trees_file_missing_from_tree(self):
 
555
        # Adding a parent tree may reference files not in the current state.
 
556
        # they should get listed just once by id, even if they are in two
 
557
        # separate trees.
 
558
        # set_parent_trees is a slow but important api to support.
 
559
        tree1 = self.make_branch_and_memory_tree('tree1')
 
560
        tree1.lock_write()
 
561
        try:
 
562
            tree1.add('')
 
563
            tree1.add(['a file'], ['file-id'], ['file'])
 
564
            tree1.put_file_bytes_non_atomic('file-id', 'file-content')
 
565
            revid1 = tree1.commit('foo')
 
566
        finally:
 
567
            tree1.unlock()
 
568
        branch2 = tree1.branch.bzrdir.clone('tree2').open_branch()
 
569
        tree2 = MemoryTree.create_on_branch(branch2)
 
570
        tree2.lock_write()
 
571
        try:
 
572
            tree2.put_file_bytes_non_atomic('file-id', 'new file-content')
 
573
            revid2 = tree2.commit('foo')
 
574
            root_id = tree2.inventory.root.file_id
 
575
        finally:
 
576
            tree2.unlock()
 
577
        # check the layout in memory
 
578
        expected_result = [revid1.encode('utf8'), revid2.encode('utf8')], [
 
579
            (('', '', root_id), [
 
580
             ('d', '', 0, False, dirstate.DirState.NULLSTAT),
 
581
             ('d', '', 0, False, revid1.encode('utf8')),
 
582
             ('d', '', 0, False, revid2.encode('utf8'))
 
583
             ]),
 
584
            (('', 'a file', 'file-id'), [
 
585
             ('a', '', 0, False, ''),
 
586
             ('f', '2439573625385400f2a669657a7db6ae7515d371', 12, False,
 
587
              revid1.encode('utf8')),
 
588
             ('f', '542e57dc1cda4af37cb8e55ec07ce60364bb3c7d', 16, False,
 
589
              revid2.encode('utf8'))
 
590
             ])
 
591
            ]
 
592
        state = dirstate.DirState.initialize('dirstate')
 
593
        try:
 
594
            state.set_path_id('', root_id)
 
595
            state.set_parent_trees(
 
596
                ((revid1, tree1.branch.repository.revision_tree(revid1)),
 
597
                 (revid2, tree2.branch.repository.revision_tree(revid2)),
 
598
                 ), [])
 
599
        except:
 
600
            state.unlock()
 
601
            raise
 
602
        else:
 
603
            # check_state_with_reopen will unlock
 
604
            self.check_state_with_reopen(expected_result, state)
 
605
 
 
606
    ### add a path via _set_data - so we dont need delta work, just
 
607
    # raw data in, and ensure that it comes out via get_lines happily.
 
608
 
 
609
    def test_add_path_to_root_no_parents_all_data(self):
 
610
        # The most trivial addition of a path is when there are no parents and
 
611
        # its in the root and all data about the file is supplied
 
612
        self.build_tree(['a file'])
 
613
        stat = os.lstat('a file')
 
614
        # the 1*20 is the sha1 pretend value.
 
615
        state = dirstate.DirState.initialize('dirstate')
 
616
        expected_entries = [
 
617
            (('', '', 'TREE_ROOT'), [
 
618
             ('d', '', 0, False, dirstate.DirState.NULLSTAT), # current tree
 
619
             ]),
 
620
            (('', 'a file', 'a file id'), [
 
621
             ('f', '1'*20, 19, False, dirstate.pack_stat(stat)), # current tree
 
622
             ]),
 
623
            ]
 
624
        try:
 
625
            state.add('a file', 'a file id', 'file', stat, '1'*20)
 
626
            # having added it, it should be in the output of iter_entries.
 
627
            self.assertEqual(expected_entries, list(state._iter_entries()))
 
628
            # saving and reloading should not affect this.
 
629
            state.save()
 
630
        finally:
 
631
            state.unlock()
 
632
        state = dirstate.DirState.on_file('dirstate')
 
633
        state.lock_read()
 
634
        try:
 
635
            self.assertEqual(expected_entries, list(state._iter_entries()))
 
636
        finally:
 
637
            state.unlock()
 
638
 
 
639
    def test_add_path_to_unversioned_directory(self):
 
640
        """Adding a path to an unversioned directory should error.
 
641
 
 
642
        This is a duplicate of TestWorkingTree.test_add_in_unversioned,
 
643
        once dirstate is stable and if it is merged with WorkingTree3, consider
 
644
        removing this copy of the test.
 
645
        """
 
646
        self.build_tree(['unversioned/', 'unversioned/a file'])
 
647
        state = dirstate.DirState.initialize('dirstate')
 
648
        try:
 
649
            self.assertRaises(errors.NotVersionedError, state.add,
 
650
                'unversioned/a file', 'a file id', 'file', None, None)
 
651
        finally:
 
652
            state.unlock()
 
653
 
 
654
    def test_add_directory_to_root_no_parents_all_data(self):
 
655
        # The most trivial addition of a dir is when there are no parents and
 
656
        # its in the root and all data about the file is supplied
 
657
        self.build_tree(['a dir/'])
 
658
        stat = os.lstat('a dir')
 
659
        expected_entries = [
 
660
            (('', '', 'TREE_ROOT'), [
 
661
             ('d', '', 0, False, dirstate.DirState.NULLSTAT), # current tree
 
662
             ]),
 
663
            (('', 'a dir', 'a dir id'), [
 
664
             ('d', '', 0, False, dirstate.pack_stat(stat)), # current tree
 
665
             ]),
 
666
            ]
 
667
        state = dirstate.DirState.initialize('dirstate')
 
668
        try:
 
669
            state.add('a dir', 'a dir id', 'directory', stat, None)
 
670
            # having added it, it should be in the output of iter_entries.
 
671
            self.assertEqual(expected_entries, list(state._iter_entries()))
 
672
            # saving and reloading should not affect this.
 
673
            state.save()
 
674
        finally:
 
675
            state.unlock()
 
676
        state = dirstate.DirState.on_file('dirstate')
 
677
        state.lock_read()
 
678
        state._validate()
 
679
        try:
 
680
            self.assertEqual(expected_entries, list(state._iter_entries()))
 
681
        finally:
 
682
            state.unlock()
 
683
 
 
684
    def test_add_symlink_to_root_no_parents_all_data(self):
 
685
        # The most trivial addition of a symlink when there are no parents and
 
686
        # its in the root and all data about the file is supplied
 
687
        ## TODO: windows: dont fail this test. Also, how are symlinks meant to
 
688
        # be represented on windows.
 
689
        os.symlink('target', 'a link')
 
690
        stat = os.lstat('a link')
 
691
        expected_entries = [
 
692
            (('', '', 'TREE_ROOT'), [
 
693
             ('d', '', 0, False, dirstate.DirState.NULLSTAT), # current tree
 
694
             ]),
 
695
            (('', 'a link', 'a link id'), [
 
696
             ('l', 'target', 6, False, dirstate.pack_stat(stat)), # current tree
 
697
             ]),
 
698
            ]
 
699
        state = dirstate.DirState.initialize('dirstate')
 
700
        try:
 
701
            state.add('a link', 'a link id', 'symlink', stat, 'target')
 
702
            # having added it, it should be in the output of iter_entries.
 
703
            self.assertEqual(expected_entries, list(state._iter_entries()))
 
704
            # saving and reloading should not affect this.
 
705
            state.save()
 
706
        finally:
 
707
            state.unlock()
 
708
        state = dirstate.DirState.on_file('dirstate')
 
709
        state.lock_read()
 
710
        try:
 
711
            self.assertEqual(expected_entries, list(state._iter_entries()))
 
712
        finally:
 
713
            state.unlock()
 
714
 
 
715
    def test_add_directory_and_child_no_parents_all_data(self):
 
716
        # after adding a directory, we should be able to add children to it.
 
717
        self.build_tree(['a dir/', 'a dir/a file'])
 
718
        dirstat = os.lstat('a dir')
 
719
        filestat = os.lstat('a dir/a file')
 
720
        expected_entries = [
 
721
            (('', '', 'TREE_ROOT'), [
 
722
             ('d', '', 0, False, dirstate.DirState.NULLSTAT), # current tree
 
723
             ]),
 
724
            (('', 'a dir', 'a dir id'), [
 
725
             ('d', '', 0, False, dirstate.pack_stat(dirstat)), # current tree
 
726
             ]),
 
727
            (('a dir', 'a file', 'a file id'), [
 
728
             ('f', '1'*20, 25, False,
 
729
              dirstate.pack_stat(filestat)), # current tree details
 
730
             ]),
 
731
            ]
 
732
        state = dirstate.DirState.initialize('dirstate')
 
733
        try:
 
734
            state.add('a dir', 'a dir id', 'directory', dirstat, None)
 
735
            state.add('a dir/a file', 'a file id', 'file', filestat, '1'*20)
 
736
            # added it, it should be in the output of iter_entries.
 
737
            self.assertEqual(expected_entries, list(state._iter_entries()))
 
738
            # saving and reloading should not affect this.
 
739
            state.save()
 
740
        finally:
 
741
            state.unlock()
 
742
        state = dirstate.DirState.on_file('dirstate')
 
743
        state.lock_read()
 
744
        try:
 
745
            self.assertEqual(expected_entries, list(state._iter_entries()))
 
746
        finally:
 
747
            state.unlock()
 
748
 
 
749
    def test_add_tree_reference(self):
 
750
        # make a dirstate and add a tree reference
 
751
        state = dirstate.DirState.initialize('dirstate')
 
752
        expected_entry = (
 
753
            ('', 'subdir', 'subdir-id'),
 
754
            [('t', 'subtree-123123', 0, False,
 
755
              'xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx')],
 
756
            )
 
757
        try:
 
758
            state.add('subdir', 'subdir-id', 'tree-reference', None, 'subtree-123123')
 
759
            entry = state._get_entry(0, 'subdir-id', 'subdir')
 
760
            self.assertEqual(entry, expected_entry)
 
761
            state._validate()
 
762
            state.save()
 
763
        finally:
 
764
            state.unlock()
 
765
        # now check we can read it back
 
766
        state.lock_read()
 
767
        state._validate()
 
768
        try:
 
769
            entry2 = state._get_entry(0, 'subdir-id', 'subdir')
 
770
            self.assertEqual(entry, entry2)
 
771
            self.assertEqual(entry, expected_entry)
 
772
            # and lookup by id should work too
 
773
            entry2 = state._get_entry(0, fileid_utf8='subdir-id')
 
774
            self.assertEqual(entry, expected_entry)
 
775
        finally:
 
776
            state.unlock()
 
777
 
 
778
    def test_add_forbidden_names(self):
 
779
        state = dirstate.DirState.initialize('dirstate')
 
780
        self.addCleanup(state.unlock)
 
781
        self.assertRaises(errors.BzrError,
 
782
            state.add, '.', 'ass-id', 'directory', None, None)
 
783
        self.assertRaises(errors.BzrError,
 
784
            state.add, '..', 'ass-id', 'directory', None, None)
 
785
 
 
786
 
 
787
class TestGetLines(TestCaseWithDirState):
 
788
 
 
789
    def test_get_line_with_2_rows(self):
 
790
        state = self.create_dirstate_with_root_and_subdir()
 
791
        try:
 
792
            self.assertEqual(['#bazaar dirstate flat format 3\n',
 
793
                'crc32: 41262208\n',
 
794
                'num_entries: 2\n',
 
795
                '0\x00\n\x00'
 
796
                '0\x00\n\x00'
 
797
                '\x00\x00a-root-value\x00'
 
798
                'd\x00\x000\x00n\x00AAAAREUHaIpFB2iKAAADAQAtkqUAAIGk\x00\n\x00'
 
799
                '\x00subdir\x00subdir-id\x00'
 
800
                'd\x00\x000\x00n\x00AAAAREUHaIpFB2iKAAADAQAtkqUAAIGk\x00\n\x00'
 
801
                ], state.get_lines())
 
802
        finally:
 
803
            state.unlock()
 
804
 
 
805
    def test_entry_to_line(self):
 
806
        state = self.create_dirstate_with_root()
 
807
        try:
 
808
            self.assertEqual(
 
809
                '\x00\x00a-root-value\x00d\x00\x000\x00n'
 
810
                '\x00AAAAREUHaIpFB2iKAAADAQAtkqUAAIGk',
 
811
                state._entry_to_line(state._dirblocks[0][1][0]))
 
812
        finally:
 
813
            state.unlock()
 
814
 
 
815
    def test_entry_to_line_with_parent(self):
 
816
        packed_stat = 'AAAAREUHaIpFB2iKAAADAQAtkqUAAIGk'
 
817
        root_entry = ('', '', 'a-root-value'), [
 
818
            ('d', '', 0, False, packed_stat), # current tree details
 
819
             # first: a pointer to the current location
 
820
            ('a', 'dirname/basename', 0, False, ''),
 
821
            ]
 
822
        state = dirstate.DirState.initialize('dirstate')
 
823
        try:
 
824
            self.assertEqual(
 
825
                '\x00\x00a-root-value\x00'
 
826
                'd\x00\x000\x00n\x00AAAAREUHaIpFB2iKAAADAQAtkqUAAIGk\x00'
 
827
                'a\x00dirname/basename\x000\x00n\x00',
 
828
                state._entry_to_line(root_entry))
 
829
        finally:
 
830
            state.unlock()
 
831
 
 
832
    def test_entry_to_line_with_two_parents_at_different_paths(self):
 
833
        # / in the tree, at / in one parent and /dirname/basename in the other.
 
834
        packed_stat = 'AAAAREUHaIpFB2iKAAADAQAtkqUAAIGk'
 
835
        root_entry = ('', '', 'a-root-value'), [
 
836
            ('d', '', 0, False, packed_stat), # current tree details
 
837
            ('d', '', 0, False, 'rev_id'), # first parent details
 
838
             # second: a pointer to the current location
 
839
            ('a', 'dirname/basename', 0, False, ''),
 
840
            ]
 
841
        state = dirstate.DirState.initialize('dirstate')
 
842
        try:
 
843
            self.assertEqual(
 
844
                '\x00\x00a-root-value\x00'
 
845
                'd\x00\x000\x00n\x00AAAAREUHaIpFB2iKAAADAQAtkqUAAIGk\x00'
 
846
                'd\x00\x000\x00n\x00rev_id\x00'
 
847
                'a\x00dirname/basename\x000\x00n\x00',
 
848
                state._entry_to_line(root_entry))
 
849
        finally:
 
850
            state.unlock()
 
851
 
 
852
    def test_iter_entries(self):
 
853
        # we should be able to iterate the dirstate entries from end to end
 
854
        # this is for get_lines to be easy to read.
 
855
        packed_stat = 'AAAAREUHaIpFB2iKAAADAQAtkqUAAIGk'
 
856
        dirblocks = []
 
857
        root_entries = [(('', '', 'a-root-value'), [
 
858
            ('d', '', 0, False, packed_stat), # current tree details
 
859
            ])]
 
860
        dirblocks.append(('', root_entries))
 
861
        # add two files in the root
 
862
        subdir_entry = ('', 'subdir', 'subdir-id'), [
 
863
            ('d', '', 0, False, packed_stat), # current tree details
 
864
            ]
 
865
        afile_entry = ('', 'afile', 'afile-id'), [
 
866
            ('f', 'sha1value', 34, False, packed_stat), # current tree details
 
867
            ]
 
868
        dirblocks.append(('', [subdir_entry, afile_entry]))
 
869
        # and one in subdir
 
870
        file_entry2 = ('subdir', '2file', '2file-id'), [
 
871
            ('f', 'sha1value', 23, False, packed_stat), # current tree details
 
872
            ]
 
873
        dirblocks.append(('subdir', [file_entry2]))
 
874
        state = dirstate.DirState.initialize('dirstate')
 
875
        try:
 
876
            state._set_data([], dirblocks)
 
877
            expected_entries = [root_entries[0], subdir_entry, afile_entry,
 
878
                                file_entry2]
 
879
            self.assertEqual(expected_entries, list(state._iter_entries()))
 
880
        finally:
 
881
            state.unlock()
 
882
 
 
883
 
 
884
class TestGetBlockRowIndex(TestCaseWithDirState):
 
885
 
 
886
    def assertBlockRowIndexEqual(self, block_index, row_index, dir_present,
 
887
        file_present, state, dirname, basename, tree_index):
 
888
        self.assertEqual((block_index, row_index, dir_present, file_present),
 
889
            state._get_block_entry_index(dirname, basename, tree_index))
 
890
        if dir_present:
 
891
            block = state._dirblocks[block_index]
 
892
            self.assertEqual(dirname, block[0])
 
893
        if dir_present and file_present:
 
894
            row = state._dirblocks[block_index][1][row_index]
 
895
            self.assertEqual(dirname, row[0][0])
 
896
            self.assertEqual(basename, row[0][1])
 
897
 
 
898
    def test_simple_structure(self):
 
899
        state = self.create_dirstate_with_root_and_subdir()
 
900
        self.addCleanup(state.unlock)
 
901
        self.assertBlockRowIndexEqual(1, 0, True, True, state, '', 'subdir', 0)
 
902
        self.assertBlockRowIndexEqual(1, 0, True, False, state, '', 'bdir', 0)
 
903
        self.assertBlockRowIndexEqual(1, 1, True, False, state, '', 'zdir', 0)
 
904
        self.assertBlockRowIndexEqual(2, 0, False, False, state, 'a', 'foo', 0)
 
905
        self.assertBlockRowIndexEqual(2, 0, False, False, state,
 
906
                                      'subdir', 'foo', 0)
 
907
 
 
908
    def test_complex_structure_exists(self):
 
909
        state = self.create_complex_dirstate()
 
910
        self.addCleanup(state.unlock)
 
911
        # Make sure we can find everything that exists
 
912
        self.assertBlockRowIndexEqual(0, 0, True, True, state, '', '', 0)
 
913
        self.assertBlockRowIndexEqual(1, 0, True, True, state, '', 'a', 0)
 
914
        self.assertBlockRowIndexEqual(1, 1, True, True, state, '', 'b', 0)
 
915
        self.assertBlockRowIndexEqual(1, 2, True, True, state, '', 'c', 0)
 
916
        self.assertBlockRowIndexEqual(1, 3, True, True, state, '', 'd', 0)
 
917
        self.assertBlockRowIndexEqual(2, 0, True, True, state, 'a', 'e', 0)
 
918
        self.assertBlockRowIndexEqual(2, 1, True, True, state, 'a', 'f', 0)
 
919
        self.assertBlockRowIndexEqual(3, 0, True, True, state, 'b', 'g', 0)
 
920
        self.assertBlockRowIndexEqual(3, 1, True, True, state,
 
921
                                      'b', 'h\xc3\xa5', 0)
 
922
 
 
923
    def test_complex_structure_missing(self):
 
924
        state = self.create_complex_dirstate()
 
925
        self.addCleanup(state.unlock)
 
926
        # Make sure things would be inserted in the right locations
 
927
        # '_' comes before 'a'
 
928
        self.assertBlockRowIndexEqual(0, 0, True, True, state, '', '', 0)
 
929
        self.assertBlockRowIndexEqual(1, 0, True, False, state, '', '_', 0)
 
930
        self.assertBlockRowIndexEqual(1, 1, True, False, state, '', 'aa', 0)
 
931
        self.assertBlockRowIndexEqual(1, 4, True, False, state,
 
932
                                      '', 'h\xc3\xa5', 0)
 
933
        self.assertBlockRowIndexEqual(2, 0, False, False, state, '_', 'a', 0)
 
934
        self.assertBlockRowIndexEqual(3, 0, False, False, state, 'aa', 'a', 0)
 
935
        self.assertBlockRowIndexEqual(4, 0, False, False, state, 'bb', 'a', 0)
 
936
        # This would be inserted between a/ and b/
 
937
        self.assertBlockRowIndexEqual(3, 0, False, False, state, 'a/e', 'a', 0)
 
938
        # Put at the end
 
939
        self.assertBlockRowIndexEqual(4, 0, False, False, state, 'e', 'a', 0)
 
940
 
 
941
 
 
942
class TestGetEntry(TestCaseWithDirState):
 
943
 
 
944
    def assertEntryEqual(self, dirname, basename, file_id, state, path, index):
 
945
        """Check that the right entry is returned for a request to getEntry."""
 
946
        entry = state._get_entry(index, path_utf8=path)
 
947
        if file_id is None:
 
948
            self.assertEqual((None, None), entry)
 
949
        else:
 
950
            cur = entry[0]
 
951
            self.assertEqual((dirname, basename, file_id), cur[:3])
 
952
 
 
953
    def test_simple_structure(self):
 
954
        state = self.create_dirstate_with_root_and_subdir()
 
955
        self.addCleanup(state.unlock)
 
956
        self.assertEntryEqual('', '', 'a-root-value', state, '', 0)
 
957
        self.assertEntryEqual('', 'subdir', 'subdir-id', state, 'subdir', 0)
 
958
        self.assertEntryEqual(None, None, None, state, 'missing', 0)
 
959
        self.assertEntryEqual(None, None, None, state, 'missing/foo', 0)
 
960
        self.assertEntryEqual(None, None, None, state, 'subdir/foo', 0)
 
961
 
 
962
    def test_complex_structure_exists(self):
 
963
        state = self.create_complex_dirstate()
 
964
        self.addCleanup(state.unlock)
 
965
        self.assertEntryEqual('', '', 'a-root-value', state, '', 0)
 
966
        self.assertEntryEqual('', 'a', 'a-dir', state, 'a', 0)
 
967
        self.assertEntryEqual('', 'b', 'b-dir', state, 'b', 0)
 
968
        self.assertEntryEqual('', 'c', 'c-file', state, 'c', 0)
 
969
        self.assertEntryEqual('', 'd', 'd-file', state, 'd', 0)
 
970
        self.assertEntryEqual('a', 'e', 'e-dir', state, 'a/e', 0)
 
971
        self.assertEntryEqual('a', 'f', 'f-file', state, 'a/f', 0)
 
972
        self.assertEntryEqual('b', 'g', 'g-file', state, 'b/g', 0)
 
973
        self.assertEntryEqual('b', 'h\xc3\xa5', 'h-\xc3\xa5-file', state,
 
974
                              'b/h\xc3\xa5', 0)
 
975
 
 
976
    def test_complex_structure_missing(self):
 
977
        state = self.create_complex_dirstate()
 
978
        self.addCleanup(state.unlock)
 
979
        self.assertEntryEqual(None, None, None, state, '_', 0)
 
980
        self.assertEntryEqual(None, None, None, state, '_\xc3\xa5', 0)
 
981
        self.assertEntryEqual(None, None, None, state, 'a/b', 0)
 
982
        self.assertEntryEqual(None, None, None, state, 'c/d', 0)
 
983
 
 
984
    def test_get_entry_uninitialized(self):
 
985
        """Calling get_entry will load data if it needs to"""
 
986
        state = self.create_dirstate_with_root()
 
987
        try:
 
988
            state.save()
 
989
        finally:
 
990
            state.unlock()
 
991
        del state
 
992
        state = dirstate.DirState.on_file('dirstate')
 
993
        state.lock_read()
 
994
        try:
 
995
            self.assertEqual(dirstate.DirState.NOT_IN_MEMORY,
 
996
                             state._header_state)
 
997
            self.assertEqual(dirstate.DirState.NOT_IN_MEMORY,
 
998
                             state._dirblock_state)
 
999
            self.assertEntryEqual('', '', 'a-root-value', state, '', 0)
 
1000
        finally:
 
1001
            state.unlock()
 
1002
 
 
1003
 
 
1004
class TestDirstateSortOrder(TestCaseWithTransport):
 
1005
    """Test that DirState adds entries in the right order."""
 
1006
 
 
1007
    def test_add_sorting(self):
 
1008
        """Add entries in lexicographical order, we get path sorted order.
 
1009
 
 
1010
        This tests it to a depth of 4, to make sure we don't just get it right
 
1011
        at a single depth. 'a/a' should come before 'a-a', even though it
 
1012
        doesn't lexicographically.
 
1013
        """
 
1014
        dirs = ['a', 'a/a', 'a/a/a', 'a/a/a/a',
 
1015
                'a-a', 'a/a-a', 'a/a/a-a', 'a/a/a/a-a',
 
1016
               ]
 
1017
        null_sha = ''
 
1018
        state = dirstate.DirState.initialize('dirstate')
 
1019
        self.addCleanup(state.unlock)
 
1020
 
 
1021
        fake_stat = os.stat('dirstate')
 
1022
        for d in dirs:
 
1023
            d_id = d.replace('/', '_')+'-id'
 
1024
            file_path = d + '/f'
 
1025
            file_id = file_path.replace('/', '_')+'-id'
 
1026
            state.add(d, d_id, 'directory', fake_stat, null_sha)
 
1027
            state.add(file_path, file_id, 'file', fake_stat, null_sha)
 
1028
 
 
1029
        expected = ['', '', 'a',
 
1030
                'a/a', 'a/a/a', 'a/a/a/a',
 
1031
                'a/a/a/a-a', 'a/a/a-a', 'a/a-a', 'a-a',
 
1032
               ]
 
1033
        split = lambda p:p.split('/')
 
1034
        self.assertEqual(sorted(expected, key=split), expected)
 
1035
        dirblock_names = [d[0] for d in state._dirblocks]
 
1036
        self.assertEqual(expected, dirblock_names)
 
1037
 
 
1038
    def test_set_parent_trees_correct_order(self):
 
1039
        """After calling set_parent_trees() we should maintain the order."""
 
1040
        dirs = ['a', 'a-a', 'a/a']
 
1041
        null_sha = ''
 
1042
        state = dirstate.DirState.initialize('dirstate')
 
1043
        self.addCleanup(state.unlock)
 
1044
 
 
1045
        fake_stat = os.stat('dirstate')
 
1046
        for d in dirs:
 
1047
            d_id = d.replace('/', '_')+'-id'
 
1048
            file_path = d + '/f'
 
1049
            file_id = file_path.replace('/', '_')+'-id'
 
1050
            state.add(d, d_id, 'directory', fake_stat, null_sha)
 
1051
            state.add(file_path, file_id, 'file', fake_stat, null_sha)
 
1052
 
 
1053
        expected = ['', '', 'a', 'a/a', 'a-a']
 
1054
        dirblock_names = [d[0] for d in state._dirblocks]
 
1055
        self.assertEqual(expected, dirblock_names)
 
1056
 
 
1057
        # *really* cheesy way to just get an empty tree
 
1058
        repo = self.make_repository('repo')
 
1059
        empty_tree = repo.revision_tree(None)
 
1060
        state.set_parent_trees([('null:', empty_tree)], [])
 
1061
 
 
1062
        dirblock_names = [d[0] for d in state._dirblocks]
 
1063
        self.assertEqual(expected, dirblock_names)
 
1064
 
 
1065
 
 
1066
class InstrumentedDirState(dirstate.DirState):
 
1067
    """An DirState with instrumented sha1 functionality."""
 
1068
 
 
1069
    def __init__(self, path):
 
1070
        super(InstrumentedDirState, self).__init__(path)
 
1071
        self._time_offset = 0
 
1072
        self._log = []
 
1073
 
 
1074
    def _sha_cutoff_time(self):
 
1075
        timestamp = super(InstrumentedDirState, self)._sha_cutoff_time()
 
1076
        self._cutoff_time = timestamp + self._time_offset
 
1077
 
 
1078
    def _sha1_file(self, abspath, entry):
 
1079
        self._log.append(('sha1', abspath))
 
1080
        return super(InstrumentedDirState, self)._sha1_file(abspath, entry)
 
1081
 
 
1082
    def _read_link(self, abspath, old_link):
 
1083
        self._log.append(('read_link', abspath, old_link))
 
1084
        return super(InstrumentedDirState, self)._read_link(abspath, old_link)
 
1085
 
 
1086
    def _lstat(self, abspath, entry):
 
1087
        self._log.append(('lstat', abspath))
 
1088
        return super(InstrumentedDirState, self)._lstat(abspath, entry)
 
1089
 
 
1090
    def _is_executable(self, mode, old_executable):
 
1091
        self._log.append(('is_exec', mode, old_executable))
 
1092
        return super(InstrumentedDirState, self)._is_executable(mode,
 
1093
                                                                old_executable)
 
1094
 
 
1095
    def adjust_time(self, secs):
 
1096
        """Move the clock forward or back.
 
1097
 
 
1098
        :param secs: The amount to adjust the clock by. Positive values make it
 
1099
        seem as if we are in the future, negative values make it seem like we
 
1100
        are in the past.
 
1101
        """
 
1102
        self._time_offset += secs
 
1103
        self._cutoff_time = None
 
1104
 
 
1105
 
 
1106
class _FakeStat(object):
 
1107
    """A class with the same attributes as a real stat result."""
 
1108
 
 
1109
    def __init__(self, size, mtime, ctime, dev, ino, mode):
 
1110
        self.st_size = size
 
1111
        self.st_mtime = mtime
 
1112
        self.st_ctime = ctime
 
1113
        self.st_dev = dev
 
1114
        self.st_ino = ino
 
1115
        self.st_mode = mode
 
1116
 
 
1117
 
 
1118
class TestUpdateEntry(TestCaseWithDirState):
 
1119
    """Test the DirState.update_entry functions"""
 
1120
 
 
1121
    def get_state_with_a(self):
 
1122
        """Create a DirState tracking a single object named 'a'"""
 
1123
        state = InstrumentedDirState.initialize('dirstate')
 
1124
        self.addCleanup(state.unlock)
 
1125
        state.add('a', 'a-id', 'file', None, '')
 
1126
        entry = state._get_entry(0, path_utf8='a')
 
1127
        return state, entry
 
1128
 
 
1129
    def test_update_entry(self):
 
1130
        state, entry = self.get_state_with_a()
 
1131
        self.build_tree(['a'])
 
1132
        # Add one where we don't provide the stat or sha already
 
1133
        self.assertEqual(('', 'a', 'a-id'), entry[0])
 
1134
        self.assertEqual([('f', '', 0, False, dirstate.DirState.NULLSTAT)],
 
1135
                         entry[1])
 
1136
        # Flush the buffers to disk
 
1137
        state.save()
 
1138
        self.assertEqual(dirstate.DirState.IN_MEMORY_UNMODIFIED,
 
1139
                         state._dirblock_state)
 
1140
 
 
1141
        stat_value = os.lstat('a')
 
1142
        packed_stat = dirstate.pack_stat(stat_value)
 
1143
        link_or_sha1 = state.update_entry(entry, abspath='a',
 
1144
                                          stat_value=stat_value)
 
1145
        self.assertEqual('b50e5406bb5e153ebbeb20268fcf37c87e1ecfb6',
 
1146
                         link_or_sha1)
 
1147
 
 
1148
        # The dirblock entry should be updated with the new info
 
1149
        self.assertEqual([('f', link_or_sha1, 14, False, packed_stat)],
 
1150
                         entry[1])
 
1151
        self.assertEqual(dirstate.DirState.IN_MEMORY_MODIFIED,
 
1152
                         state._dirblock_state)
 
1153
        mode = stat_value.st_mode
 
1154
        self.assertEqual([('sha1', 'a'), ('is_exec', mode, False)], state._log)
 
1155
 
 
1156
        state.save()
 
1157
        self.assertEqual(dirstate.DirState.IN_MEMORY_UNMODIFIED,
 
1158
                         state._dirblock_state)
 
1159
 
 
1160
        # If we do it again right away, we don't know if the file has changed
 
1161
        # so we will re-read the file. Roll the clock back so the file is
 
1162
        # guaranteed to look too new.
 
1163
        state.adjust_time(-10)
 
1164
 
 
1165
        link_or_sha1 = state.update_entry(entry, abspath='a',
 
1166
                                          stat_value=stat_value)
 
1167
        self.assertEqual([('sha1', 'a'), ('is_exec', mode, False),
 
1168
                          ('sha1', 'a'), ('is_exec', mode, False),
 
1169
                         ], state._log)
 
1170
        self.assertEqual('b50e5406bb5e153ebbeb20268fcf37c87e1ecfb6',
 
1171
                         link_or_sha1)
 
1172
        self.assertEqual(dirstate.DirState.IN_MEMORY_MODIFIED,
 
1173
                         state._dirblock_state)
 
1174
        state.save()
 
1175
 
 
1176
        # However, if we move the clock forward so the file is considered
 
1177
        # "stable", it should just returned the cached value.
 
1178
        state.adjust_time(20)
 
1179
        link_or_sha1 = state.update_entry(entry, abspath='a',
 
1180
                                          stat_value=stat_value)
 
1181
        self.assertEqual('b50e5406bb5e153ebbeb20268fcf37c87e1ecfb6',
 
1182
                         link_or_sha1)
 
1183
        self.assertEqual([('sha1', 'a'), ('is_exec', mode, False),
 
1184
                          ('sha1', 'a'), ('is_exec', mode, False),
 
1185
                         ], state._log)
 
1186
 
 
1187
    def test_update_entry_no_stat_value(self):
 
1188
        """Passing the stat_value is optional."""
 
1189
        state, entry = self.get_state_with_a()
 
1190
        state.adjust_time(-10) # Make sure the file looks new
 
1191
        self.build_tree(['a'])
 
1192
        # Add one where we don't provide the stat or sha already
 
1193
        link_or_sha1 = state.update_entry(entry, abspath='a')
 
1194
        self.assertEqual('b50e5406bb5e153ebbeb20268fcf37c87e1ecfb6',
 
1195
                         link_or_sha1)
 
1196
        stat_value = os.lstat('a')
 
1197
        self.assertEqual([('lstat', 'a'), ('sha1', 'a'),
 
1198
                          ('is_exec', stat_value.st_mode, False),
 
1199
                         ], state._log)
 
1200
 
 
1201
    def test_update_entry_symlink(self):
 
1202
        """Update entry should read symlinks."""
 
1203
        if not osutils.has_symlinks():
 
1204
            return # PlatformDeficiency / TestSkipped
 
1205
        state, entry = self.get_state_with_a()
 
1206
        state.save()
 
1207
        self.assertEqual(dirstate.DirState.IN_MEMORY_UNMODIFIED,
 
1208
                         state._dirblock_state)
 
1209
        os.symlink('target', 'a')
 
1210
 
 
1211
        state.adjust_time(-10) # Make the symlink look new
 
1212
        stat_value = os.lstat('a')
 
1213
        packed_stat = dirstate.pack_stat(stat_value)
 
1214
        link_or_sha1 = state.update_entry(entry, abspath='a',
 
1215
                                          stat_value=stat_value)
 
1216
        self.assertEqual('target', link_or_sha1)
 
1217
        self.assertEqual([('read_link', 'a', '')], state._log)
 
1218
        # Dirblock is updated
 
1219
        self.assertEqual([('l', link_or_sha1, 6, False, packed_stat)],
 
1220
                         entry[1])
 
1221
        self.assertEqual(dirstate.DirState.IN_MEMORY_MODIFIED,
 
1222
                         state._dirblock_state)
 
1223
 
 
1224
        # Because the stat_value looks new, we should re-read the target
 
1225
        link_or_sha1 = state.update_entry(entry, abspath='a',
 
1226
                                          stat_value=stat_value)
 
1227
        self.assertEqual('target', link_or_sha1)
 
1228
        self.assertEqual([('read_link', 'a', ''),
 
1229
                          ('read_link', 'a', 'target'),
 
1230
                         ], state._log)
 
1231
        state.adjust_time(+20) # Skip into the future, all files look old
 
1232
        link_or_sha1 = state.update_entry(entry, abspath='a',
 
1233
                                          stat_value=stat_value)
 
1234
        self.assertEqual('target', link_or_sha1)
 
1235
        # There should not be a new read_link call.
 
1236
        # (this is a weak assertion, because read_link is fairly inexpensive,
 
1237
        # versus the number of symlinks that we would have)
 
1238
        self.assertEqual([('read_link', 'a', ''),
 
1239
                          ('read_link', 'a', 'target'),
 
1240
                         ], state._log)
 
1241
 
 
1242
    def test_update_entry_dir(self):
 
1243
        state, entry = self.get_state_with_a()
 
1244
        self.build_tree(['a/'])
 
1245
        self.assertIs(None, state.update_entry(entry, 'a'))
 
1246
 
 
1247
    def create_and_test_file(self, state, entry):
 
1248
        """Create a file at 'a' and verify the state finds it.
 
1249
 
 
1250
        The state should already be versioning *something* at 'a'. This makes
 
1251
        sure that state.update_entry recognizes it as a file.
 
1252
        """
 
1253
        self.build_tree(['a'])
 
1254
        stat_value = os.lstat('a')
 
1255
        packed_stat = dirstate.pack_stat(stat_value)
 
1256
 
 
1257
        link_or_sha1 = state.update_entry(entry, abspath='a')
 
1258
        self.assertEqual('b50e5406bb5e153ebbeb20268fcf37c87e1ecfb6',
 
1259
                         link_or_sha1)
 
1260
        self.assertEqual([('f', link_or_sha1, 14, False, packed_stat)],
 
1261
                         entry[1])
 
1262
        return packed_stat
 
1263
 
 
1264
    def create_and_test_dir(self, state, entry):
 
1265
        """Create a directory at 'a' and verify the state finds it.
 
1266
 
 
1267
        The state should already be versioning *something* at 'a'. This makes
 
1268
        sure that state.update_entry recognizes it as a directory.
 
1269
        """
 
1270
        self.build_tree(['a/'])
 
1271
        stat_value = os.lstat('a')
 
1272
        packed_stat = dirstate.pack_stat(stat_value)
 
1273
 
 
1274
        link_or_sha1 = state.update_entry(entry, abspath='a')
 
1275
        self.assertIs(None, link_or_sha1)
 
1276
        self.assertEqual([('d', '', 0, False, packed_stat)], entry[1])
 
1277
 
 
1278
        return packed_stat
 
1279
 
 
1280
    def create_and_test_symlink(self, state, entry):
 
1281
        """Create a symlink at 'a' and verify the state finds it.
 
1282
 
 
1283
        The state should already be versioning *something* at 'a'. This makes
 
1284
        sure that state.update_entry recognizes it as a symlink.
 
1285
 
 
1286
        This should not be called if this platform does not have symlink
 
1287
        support.
 
1288
        """
 
1289
        os.symlink('path/to/foo', 'a')
 
1290
 
 
1291
        stat_value = os.lstat('a')
 
1292
        packed_stat = dirstate.pack_stat(stat_value)
 
1293
 
 
1294
        link_or_sha1 = state.update_entry(entry, abspath='a')
 
1295
        self.assertEqual('path/to/foo', link_or_sha1)
 
1296
        self.assertEqual([('l', 'path/to/foo', 11, False, packed_stat)],
 
1297
                         entry[1])
 
1298
        return packed_stat
 
1299
 
 
1300
    def test_update_missing_file(self):
 
1301
        state, entry = self.get_state_with_a()
 
1302
        packed_stat = self.create_and_test_file(state, entry)
 
1303
        # Now if we delete the file, update_entry should recover and
 
1304
        # return None.
 
1305
        os.remove('a')
 
1306
        self.assertIs(None, state.update_entry(entry, abspath='a'))
 
1307
        # And the record shouldn't be changed.
 
1308
        digest = 'b50e5406bb5e153ebbeb20268fcf37c87e1ecfb6'
 
1309
        self.assertEqual([('f', digest, 14, False, packed_stat)],
 
1310
                         entry[1])
 
1311
 
 
1312
    def test_update_missing_dir(self):
 
1313
        state, entry = self.get_state_with_a()
 
1314
        packed_stat = self.create_and_test_dir(state, entry)
 
1315
        # Now if we delete the directory, update_entry should recover and
 
1316
        # return None.
 
1317
        os.rmdir('a')
 
1318
        self.assertIs(None, state.update_entry(entry, abspath='a'))
 
1319
        self.assertEqual([('d', '', 0, False, packed_stat)], entry[1])
 
1320
 
 
1321
    def test_update_missing_symlink(self):
 
1322
        if not osutils.has_symlinks():
 
1323
            return # PlatformDeficiency / TestSkipped
 
1324
        state, entry = self.get_state_with_a()
 
1325
        packed_stat = self.create_and_test_symlink(state, entry)
 
1326
        os.remove('a')
 
1327
        self.assertIs(None, state.update_entry(entry, abspath='a'))
 
1328
        # And the record shouldn't be changed.
 
1329
        self.assertEqual([('l', 'path/to/foo', 11, False, packed_stat)],
 
1330
                         entry[1])
 
1331
 
 
1332
    def test_update_file_to_dir(self):
 
1333
        """If a file changes to a directory we return None for the sha.
 
1334
        We also update the inventory record.
 
1335
        """
 
1336
        state, entry = self.get_state_with_a()
 
1337
        self.create_and_test_file(state, entry)
 
1338
        os.remove('a')
 
1339
        self.create_and_test_dir(state, entry)
 
1340
 
 
1341
    def test_update_file_to_symlink(self):
 
1342
        """File becomes a symlink"""
 
1343
        if not osutils.has_symlinks():
 
1344
            return # PlatformDeficiency / TestSkipped
 
1345
        state, entry = self.get_state_with_a()
 
1346
        self.create_and_test_file(state, entry)
 
1347
        os.remove('a')
 
1348
        self.create_and_test_symlink(state, entry)
 
1349
 
 
1350
    def test_update_dir_to_file(self):
 
1351
        """Directory becoming a file updates the entry."""
 
1352
        state, entry = self.get_state_with_a()
 
1353
        self.create_and_test_dir(state, entry)
 
1354
        os.rmdir('a')
 
1355
        self.create_and_test_file(state, entry)
 
1356
 
 
1357
    def test_update_dir_to_symlink(self):
 
1358
        """Directory becomes a symlink"""
 
1359
        if not osutils.has_symlinks():
 
1360
            return # PlatformDeficiency / TestSkipped
 
1361
        state, entry = self.get_state_with_a()
 
1362
        self.create_and_test_dir(state, entry)
 
1363
        os.rmdir('a')
 
1364
        self.create_and_test_symlink(state, entry)
 
1365
 
 
1366
    def test_update_symlink_to_file(self):
 
1367
        """Symlink becomes a file"""
 
1368
        state, entry = self.get_state_with_a()
 
1369
        self.create_and_test_symlink(state, entry)
 
1370
        os.remove('a')
 
1371
        self.create_and_test_file(state, entry)
 
1372
 
 
1373
    def test_update_symlink_to_dir(self):
 
1374
        """Symlink becomes a directory"""
 
1375
        state, entry = self.get_state_with_a()
 
1376
        self.create_and_test_symlink(state, entry)
 
1377
        os.remove('a')
 
1378
        self.create_and_test_dir(state, entry)
 
1379
 
 
1380
    def test__is_executable_win32(self):
 
1381
        state, entry = self.get_state_with_a()
 
1382
        self.build_tree(['a'])
 
1383
 
 
1384
        # Make sure we are using the win32 implementation of _is_executable
 
1385
        state._is_executable = state._is_executable_win32
 
1386
 
 
1387
        # The file on disk is not executable, but we are marking it as though
 
1388
        # it is. With _is_executable_win32 we ignore what is on disk.
 
1389
        entry[1][0] = ('f', '', 0, True, dirstate.DirState.NULLSTAT)
 
1390
 
 
1391
        stat_value = os.lstat('a')
 
1392
        packed_stat = dirstate.pack_stat(stat_value)
 
1393
 
 
1394
        state.adjust_time(-10) # Make sure everything is new
 
1395
        # Make sure it wants to kkkkkkkk
 
1396
        state.update_entry(entry, abspath='a', stat_value=stat_value)
 
1397
 
 
1398
        # The row is updated, but the executable bit stays set.
 
1399
        digest = 'b50e5406bb5e153ebbeb20268fcf37c87e1ecfb6'
 
1400
        self.assertEqual([('f', digest, 14, True, packed_stat)], entry[1])
 
1401
 
 
1402
 
 
1403
class TestPackStat(TestCaseWithTransport):
 
1404
 
 
1405
    def assertPackStat(self, expected, stat_value):
 
1406
        """Check the packed and serialized form of a stat value."""
 
1407
        self.assertEqual(expected, dirstate.pack_stat(stat_value))
 
1408
 
 
1409
    def test_pack_stat_int(self):
 
1410
        st = _FakeStat(6859L, 1172758614, 1172758617, 777L, 6499538L, 0100644)
 
1411
        # Make sure that all parameters have an impact on the packed stat.
 
1412
        self.assertPackStat('AAAay0Xm4FZF5uBZAAADCQBjLNIAAIGk', st)
 
1413
        st.st_size = 7000L
 
1414
        #                ay0 => bWE
 
1415
        self.assertPackStat('AAAbWEXm4FZF5uBZAAADCQBjLNIAAIGk', st)
 
1416
        st.st_mtime = 1172758620
 
1417
        #                     4FZ => 4Fx
 
1418
        self.assertPackStat('AAAbWEXm4FxF5uBZAAADCQBjLNIAAIGk', st)
 
1419
        st.st_ctime = 1172758630
 
1420
        #                          uBZ => uBm
 
1421
        self.assertPackStat('AAAbWEXm4FxF5uBmAAADCQBjLNIAAIGk', st)
 
1422
        st.st_dev = 888L
 
1423
        #                                DCQ => DeA
 
1424
        self.assertPackStat('AAAbWEXm4FxF5uBmAAADeABjLNIAAIGk', st)
 
1425
        st.st_ino = 6499540L
 
1426
        #                                     LNI => LNQ
 
1427
        self.assertPackStat('AAAbWEXm4FxF5uBmAAADeABjLNQAAIGk', st)
 
1428
        st.st_mode = 0100744
 
1429
        #                                          IGk => IHk
 
1430
        self.assertPackStat('AAAbWEXm4FxF5uBmAAADeABjLNQAAIHk', st)
 
1431
 
 
1432
    def test_pack_stat_float(self):
 
1433
        """On some platforms mtime and ctime are floats.
 
1434
 
 
1435
        Make sure we don't get warnings or errors, and that we ignore changes <
 
1436
        1s
 
1437
        """
 
1438
        st = _FakeStat(7000L, 1172758614.0, 1172758617.0,
 
1439
                       777L, 6499538L, 0100644)
 
1440
        # These should all be the same as the integer counterparts
 
1441
        self.assertPackStat('AAAbWEXm4FZF5uBZAAADCQBjLNIAAIGk', st)
 
1442
        st.st_mtime = 1172758620.0
 
1443
        #                     FZF5 => FxF5
 
1444
        self.assertPackStat('AAAbWEXm4FxF5uBZAAADCQBjLNIAAIGk', st)
 
1445
        st.st_ctime = 1172758630.0
 
1446
        #                          uBZ => uBm
 
1447
        self.assertPackStat('AAAbWEXm4FxF5uBmAAADCQBjLNIAAIGk', st)
 
1448
        # fractional seconds are discarded, so no change from above
 
1449
        st.st_mtime = 1172758620.453
 
1450
        self.assertPackStat('AAAbWEXm4FxF5uBmAAADCQBjLNIAAIGk', st)
 
1451
        st.st_ctime = 1172758630.228
 
1452
        self.assertPackStat('AAAbWEXm4FxF5uBmAAADCQBjLNIAAIGk', st)
 
1453
 
 
1454
 
 
1455
class TestBisect(TestCaseWithTransport):
 
1456
    """Test the ability to bisect into the disk format."""
 
1457
 
 
1458
    def create_basic_dirstate(self):
 
1459
        """Create a dirstate with a few files and directories.
 
1460
 
 
1461
            a
 
1462
            b/
 
1463
              c
 
1464
              d/
 
1465
                e
 
1466
            f
 
1467
        """
 
1468
        tree = self.make_branch_and_tree('tree')
 
1469
        paths = ['a', 'b/', 'b/c', 'b/d/', 'b/d/e', 'f']
 
1470
        file_ids = ['a-id', 'b-id', 'c-id', 'd-id', 'e-id', 'f-id']
 
1471
        self.build_tree(['tree/' + p for p in paths])
 
1472
        tree.set_root_id('TREE_ROOT')
 
1473
        tree.add([p.rstrip('/') for p in paths], file_ids)
 
1474
        tree.commit('initial', rev_id='rev-1')
 
1475
        revision_id = 'rev-1'
 
1476
        # a_packed_stat = dirstate.pack_stat(os.stat('tree/a'))
 
1477
        t = self.get_transport().clone('tree')
 
1478
        a_text = t.get_bytes('a')
 
1479
        a_sha = osutils.sha_string(a_text)
 
1480
        a_len = len(a_text)
 
1481
        # b_packed_stat = dirstate.pack_stat(os.stat('tree/b'))
 
1482
        # c_packed_stat = dirstate.pack_stat(os.stat('tree/b/c'))
 
1483
        c_text = t.get_bytes('b/c')
 
1484
        c_sha = osutils.sha_string(c_text)
 
1485
        c_len = len(c_text)
 
1486
        # d_packed_stat = dirstate.pack_stat(os.stat('tree/b/d'))
 
1487
        # e_packed_stat = dirstate.pack_stat(os.stat('tree/b/d/e'))
 
1488
        e_text = t.get_bytes('b/d/e')
 
1489
        e_sha = osutils.sha_string(e_text)
 
1490
        e_len = len(e_text)
 
1491
        # f_packed_stat = dirstate.pack_stat(os.stat('tree/f'))
 
1492
        f_text = t.get_bytes('f')
 
1493
        f_sha = osutils.sha_string(f_text)
 
1494
        f_len = len(f_text)
 
1495
        null_stat = dirstate.DirState.NULLSTAT
 
1496
        expected = {
 
1497
            '':(('', '', 'TREE_ROOT'), [
 
1498
                  ('d', '', 0, False, null_stat),
 
1499
                  ('d', '', 0, False, revision_id),
 
1500
                ]),
 
1501
            'a':(('', 'a', 'a-id'), [
 
1502
                   ('f', '', 0, False, null_stat),
 
1503
                   ('f', a_sha, a_len, False, revision_id),
 
1504
                 ]),
 
1505
            'b':(('', 'b', 'b-id'), [
 
1506
                  ('d', '', 0, False, null_stat),
 
1507
                  ('d', '', 0, False, revision_id),
 
1508
                 ]),
 
1509
            'b/c':(('b', 'c', 'c-id'), [
 
1510
                    ('f', '', 0, False, null_stat),
 
1511
                    ('f', c_sha, c_len, False, revision_id),
 
1512
                   ]),
 
1513
            'b/d':(('b', 'd', 'd-id'), [
 
1514
                    ('d', '', 0, False, null_stat),
 
1515
                    ('d', '', 0, False, revision_id),
 
1516
                   ]),
 
1517
            'b/d/e':(('b/d', 'e', 'e-id'), [
 
1518
                      ('f', '', 0, False, null_stat),
 
1519
                      ('f', e_sha, e_len, False, revision_id),
 
1520
                     ]),
 
1521
            'f':(('', 'f', 'f-id'), [
 
1522
                  ('f', '', 0, False, null_stat),
 
1523
                  ('f', f_sha, f_len, False, revision_id),
 
1524
                 ]),
 
1525
        }
 
1526
        state = dirstate.DirState.from_tree(tree, 'dirstate')
 
1527
        try:
 
1528
            state.save()
 
1529
        finally:
 
1530
            state.unlock()
 
1531
        # Use a different object, to make sure nothing is pre-cached in memory.
 
1532
        state = dirstate.DirState.on_file('dirstate')
 
1533
        state.lock_read()
 
1534
        self.addCleanup(state.unlock)
 
1535
        self.assertEqual(dirstate.DirState.NOT_IN_MEMORY,
 
1536
                         state._dirblock_state)
 
1537
        # This is code is only really tested if we actually have to make more
 
1538
        # than one read, so set the page size to something smaller.
 
1539
        # We want it to contain about 2.2 records, so that we have a couple
 
1540
        # records that we can read per attempt
 
1541
        state._bisect_page_size = 200
 
1542
        return tree, state, expected
 
1543
 
 
1544
    def create_duplicated_dirstate(self):
 
1545
        """Create a dirstate with a deleted and added entries.
 
1546
 
 
1547
        This grabs a basic_dirstate, and then removes and re adds every entry
 
1548
        with a new file id.
 
1549
        """
 
1550
        tree, state, expected = self.create_basic_dirstate()
 
1551
        # Now we will just remove and add every file so we get an extra entry
 
1552
        # per entry. Unversion in reverse order so we handle subdirs
 
1553
        tree.unversion(['f-id', 'e-id', 'd-id', 'c-id', 'b-id', 'a-id'])
 
1554
        tree.add(['a', 'b', 'b/c', 'b/d', 'b/d/e', 'f'],
 
1555
                 ['a-id2', 'b-id2', 'c-id2', 'd-id2', 'e-id2', 'f-id2'])
 
1556
 
 
1557
        # Update the expected dictionary.
 
1558
        for path in ['a', 'b', 'b/c', 'b/d', 'b/d/e', 'f']:
 
1559
            orig = expected[path]
 
1560
            path2 = path + '2'
 
1561
            # This record was deleted in the current tree
 
1562
            expected[path] = (orig[0], [dirstate.DirState.NULL_PARENT_DETAILS,
 
1563
                                        orig[1][1]])
 
1564
            new_key = (orig[0][0], orig[0][1], orig[0][2]+'2')
 
1565
            # And didn't exist in the basis tree
 
1566
            expected[path2] = (new_key, [orig[1][0],
 
1567
                                         dirstate.DirState.NULL_PARENT_DETAILS])
 
1568
 
 
1569
        # We will replace the 'dirstate' file underneath 'state', but that is
 
1570
        # okay as lock as we unlock 'state' first.
 
1571
        state.unlock()
 
1572
        try:
 
1573
            new_state = dirstate.DirState.from_tree(tree, 'dirstate')
 
1574
            try:
 
1575
                new_state.save()
 
1576
            finally:
 
1577
                new_state.unlock()
 
1578
        finally:
 
1579
            # But we need to leave state in a read-lock because we already have
 
1580
            # a cleanup scheduled
 
1581
            state.lock_read()
 
1582
        return tree, state, expected
 
1583
 
 
1584
    def create_renamed_dirstate(self):
 
1585
        """Create a dirstate with a few internal renames.
 
1586
 
 
1587
        This takes the basic dirstate, and moves the paths around.
 
1588
        """
 
1589
        tree, state, expected = self.create_basic_dirstate()
 
1590
        # Rename a file
 
1591
        tree.rename_one('a', 'b/g')
 
1592
        # And a directory
 
1593
        tree.rename_one('b/d', 'h')
 
1594
 
 
1595
        old_a = expected['a']
 
1596
        expected['a'] = (old_a[0], [('r', 'b/g', 0, False, ''), old_a[1][1]])
 
1597
        expected['b/g'] = (('b', 'g', 'a-id'), [old_a[1][0],
 
1598
                                                ('r', 'a', 0, False, '')])
 
1599
        old_d = expected['b/d']
 
1600
        expected['b/d'] = (old_d[0], [('r', 'h', 0, False, ''), old_d[1][1]])
 
1601
        expected['h'] = (('', 'h', 'd-id'), [old_d[1][0],
 
1602
                                             ('r', 'b/d', 0, False, '')])
 
1603
 
 
1604
        old_e = expected['b/d/e']
 
1605
        expected['b/d/e'] = (old_e[0], [('r', 'h/e', 0, False, ''),
 
1606
                             old_e[1][1]])
 
1607
        expected['h/e'] = (('h', 'e', 'e-id'), [old_e[1][0],
 
1608
                                                ('r', 'b/d/e', 0, False, '')])
 
1609
 
 
1610
        state.unlock()
 
1611
        try:
 
1612
            new_state = dirstate.DirState.from_tree(tree, 'dirstate')
 
1613
            try:
 
1614
                new_state.save()
 
1615
            finally:
 
1616
                new_state.unlock()
 
1617
        finally:
 
1618
            state.lock_read()
 
1619
        return tree, state, expected
 
1620
 
 
1621
    def assertBisect(self, expected_map, map_keys, state, paths):
 
1622
        """Assert that bisecting for paths returns the right result.
 
1623
 
 
1624
        :param expected_map: A map from key => entry value
 
1625
        :param map_keys: The keys to expect for each path
 
1626
        :param state: The DirState object.
 
1627
        :param paths: A list of paths, these will automatically be split into
 
1628
                      (dir, name) tuples, and sorted according to how _bisect
 
1629
                      requires.
 
1630
        """
 
1631
        dir_names = sorted(osutils.split(p) for p in paths)
 
1632
        result = state._bisect(dir_names)
 
1633
        # For now, results are just returned in whatever order we read them.
 
1634
        # We could sort by (dir, name, file_id) or something like that, but in
 
1635
        # the end it would still be fairly arbitrary, and we don't want the
 
1636
        # extra overhead if we can avoid it. So sort everything to make sure
 
1637
        # equality is true
 
1638
        assert len(map_keys) == len(dir_names)
 
1639
        expected = {}
 
1640
        for dir_name, keys in zip(dir_names, map_keys):
 
1641
            if keys is None:
 
1642
                # This should not be present in the output
 
1643
                continue
 
1644
            expected[dir_name] = sorted(expected_map[k] for k in keys)
 
1645
 
 
1646
        for dir_name in result:
 
1647
            result[dir_name].sort()
 
1648
 
 
1649
        self.assertEqual(expected, result)
 
1650
 
 
1651
    def assertBisectDirBlocks(self, expected_map, map_keys, state, paths):
 
1652
        """Assert that bisecting for dirbblocks returns the right result.
 
1653
 
 
1654
        :param expected_map: A map from key => expected values
 
1655
        :param map_keys: A nested list of paths we expect to be returned.
 
1656
            Something like [['a', 'b', 'f'], ['b/c', 'b/d']]
 
1657
        :param state: The DirState object.
 
1658
        :param paths: A list of directories
 
1659
        """
 
1660
        result = state._bisect_dirblocks(paths)
 
1661
        assert len(map_keys) == len(paths)
 
1662
 
 
1663
        expected = {}
 
1664
        for path, keys in zip(paths, map_keys):
 
1665
            if keys is None:
 
1666
                # This should not be present in the output
 
1667
                continue
 
1668
            expected[path] = sorted(expected_map[k] for k in keys)
 
1669
        for path in result:
 
1670
            result[path].sort()
 
1671
 
 
1672
        self.assertEqual(expected, result)
 
1673
 
 
1674
    def assertBisectRecursive(self, expected_map, map_keys, state, paths):
 
1675
        """Assert the return value of a recursive bisection.
 
1676
 
 
1677
        :param expected_map: A map from key => entry value
 
1678
        :param map_keys: A list of paths we expect to be returned.
 
1679
            Something like ['a', 'b', 'f', 'b/d', 'b/d2']
 
1680
        :param state: The DirState object.
 
1681
        :param paths: A list of files and directories. It will be broken up
 
1682
            into (dir, name) pairs and sorted before calling _bisect_recursive.
 
1683
        """
 
1684
        expected = {}
 
1685
        for key in map_keys:
 
1686
            entry = expected_map[key]
 
1687
            dir_name_id, trees_info = entry
 
1688
            expected[dir_name_id] = trees_info
 
1689
 
 
1690
        dir_names = sorted(osutils.split(p) for p in paths)
 
1691
        result = state._bisect_recursive(dir_names)
 
1692
 
 
1693
        self.assertEqual(expected, result)
 
1694
 
 
1695
    def test_bisect_each(self):
 
1696
        """Find a single record using bisect."""
 
1697
        tree, state, expected = self.create_basic_dirstate()
 
1698
 
 
1699
        # Bisect should return the rows for the specified files.
 
1700
        self.assertBisect(expected, [['']], state, [''])
 
1701
        self.assertBisect(expected, [['a']], state, ['a'])
 
1702
        self.assertBisect(expected, [['b']], state, ['b'])
 
1703
        self.assertBisect(expected, [['b/c']], state, ['b/c'])
 
1704
        self.assertBisect(expected, [['b/d']], state, ['b/d'])
 
1705
        self.assertBisect(expected, [['b/d/e']], state, ['b/d/e'])
 
1706
        self.assertBisect(expected, [['f']], state, ['f'])
 
1707
 
 
1708
    def test_bisect_multi(self):
 
1709
        """Bisect can be used to find multiple records at the same time."""
 
1710
        tree, state, expected = self.create_basic_dirstate()
 
1711
        # Bisect should be capable of finding multiple entries at the same time
 
1712
        self.assertBisect(expected, [['a'], ['b'], ['f']],
 
1713
                          state, ['a', 'b', 'f'])
 
1714
        # ('', 'f') sorts before the others
 
1715
        self.assertBisect(expected, [['f'], ['b/d'], ['b/d/e']],
 
1716
                          state, ['b/d', 'b/d/e', 'f'])
 
1717
 
 
1718
    def test_bisect_one_page(self):
 
1719
        """Test bisect when there is only 1 page to read"""
 
1720
        tree, state, expected = self.create_basic_dirstate()
 
1721
        state._bisect_page_size = 5000
 
1722
        self.assertBisect(expected,[['']], state, [''])
 
1723
        self.assertBisect(expected,[['a']], state, ['a'])
 
1724
        self.assertBisect(expected,[['b']], state, ['b'])
 
1725
        self.assertBisect(expected,[['b/c']], state, ['b/c'])
 
1726
        self.assertBisect(expected,[['b/d']], state, ['b/d'])
 
1727
        self.assertBisect(expected,[['b/d/e']], state, ['b/d/e'])
 
1728
        self.assertBisect(expected,[['f']], state, ['f'])
 
1729
        self.assertBisect(expected,[['a'], ['b'], ['f']],
 
1730
                          state, ['a', 'b', 'f'])
 
1731
        # ('', 'f') sorts before the others
 
1732
        self.assertBisect(expected, [['f'], ['b/d'], ['b/d/e']],
 
1733
                          state, ['b/d', 'b/d/e', 'f'])
 
1734
 
 
1735
    def test_bisect_duplicate_paths(self):
 
1736
        """When bisecting for a path, handle multiple entries."""
 
1737
        tree, state, expected = self.create_duplicated_dirstate()
 
1738
 
 
1739
        # Now make sure that both records are properly returned.
 
1740
        self.assertBisect(expected, [['']], state, [''])
 
1741
        self.assertBisect(expected, [['a', 'a2']], state, ['a'])
 
1742
        self.assertBisect(expected, [['b', 'b2']], state, ['b'])
 
1743
        self.assertBisect(expected, [['b/c', 'b/c2']], state, ['b/c'])
 
1744
        self.assertBisect(expected, [['b/d', 'b/d2']], state, ['b/d'])
 
1745
        self.assertBisect(expected, [['b/d/e', 'b/d/e2']],
 
1746
                          state, ['b/d/e'])
 
1747
        self.assertBisect(expected, [['f', 'f2']], state, ['f'])
 
1748
 
 
1749
    def test_bisect_page_size_too_small(self):
 
1750
        """If the page size is too small, we will auto increase it."""
 
1751
        tree, state, expected = self.create_basic_dirstate()
 
1752
        state._bisect_page_size = 50
 
1753
        self.assertBisect(expected, [None], state, ['b/e'])
 
1754
        self.assertBisect(expected, [['a']], state, ['a'])
 
1755
        self.assertBisect(expected, [['b']], state, ['b'])
 
1756
        self.assertBisect(expected, [['b/c']], state, ['b/c'])
 
1757
        self.assertBisect(expected, [['b/d']], state, ['b/d'])
 
1758
        self.assertBisect(expected, [['b/d/e']], state, ['b/d/e'])
 
1759
        self.assertBisect(expected, [['f']], state, ['f'])
 
1760
 
 
1761
    def test_bisect_missing(self):
 
1762
        """Test that bisect return None if it cannot find a path."""
 
1763
        tree, state, expected = self.create_basic_dirstate()
 
1764
        self.assertBisect(expected, [None], state, ['foo'])
 
1765
        self.assertBisect(expected, [None], state, ['b/foo'])
 
1766
        self.assertBisect(expected, [None], state, ['bar/foo'])
 
1767
 
 
1768
        self.assertBisect(expected, [['a'], None, ['b/d']],
 
1769
                          state, ['a', 'foo', 'b/d'])
 
1770
 
 
1771
    def test_bisect_rename(self):
 
1772
        """Check that we find a renamed row."""
 
1773
        tree, state, expected = self.create_renamed_dirstate()
 
1774
 
 
1775
        # Search for the pre and post renamed entries
 
1776
        self.assertBisect(expected, [['a']], state, ['a'])
 
1777
        self.assertBisect(expected, [['b/g']], state, ['b/g'])
 
1778
        self.assertBisect(expected, [['b/d']], state, ['b/d'])
 
1779
        self.assertBisect(expected, [['h']], state, ['h'])
 
1780
 
 
1781
        # What about b/d/e? shouldn't that also get 2 directory entries?
 
1782
        self.assertBisect(expected, [['b/d/e']], state, ['b/d/e'])
 
1783
        self.assertBisect(expected, [['h/e']], state, ['h/e'])
 
1784
 
 
1785
    def test_bisect_dirblocks(self):
 
1786
        tree, state, expected = self.create_duplicated_dirstate()
 
1787
        self.assertBisectDirBlocks(expected,
 
1788
            [['', 'a', 'a2', 'b', 'b2', 'f', 'f2']], state, [''])
 
1789
        self.assertBisectDirBlocks(expected,
 
1790
            [['b/c', 'b/c2', 'b/d', 'b/d2']], state, ['b'])
 
1791
        self.assertBisectDirBlocks(expected,
 
1792
            [['b/d/e', 'b/d/e2']], state, ['b/d'])
 
1793
        self.assertBisectDirBlocks(expected,
 
1794
            [['', 'a', 'a2', 'b', 'b2', 'f', 'f2'],
 
1795
             ['b/c', 'b/c2', 'b/d', 'b/d2'],
 
1796
             ['b/d/e', 'b/d/e2'],
 
1797
            ], state, ['', 'b', 'b/d'])
 
1798
 
 
1799
    def test_bisect_dirblocks_missing(self):
 
1800
        tree, state, expected = self.create_basic_dirstate()
 
1801
        self.assertBisectDirBlocks(expected, [['b/d/e'], None],
 
1802
            state, ['b/d', 'b/e'])
 
1803
        # Files don't show up in this search
 
1804
        self.assertBisectDirBlocks(expected, [None], state, ['a'])
 
1805
        self.assertBisectDirBlocks(expected, [None], state, ['b/c'])
 
1806
        self.assertBisectDirBlocks(expected, [None], state, ['c'])
 
1807
        self.assertBisectDirBlocks(expected, [None], state, ['b/d/e'])
 
1808
        self.assertBisectDirBlocks(expected, [None], state, ['f'])
 
1809
 
 
1810
    def test_bisect_recursive_each(self):
 
1811
        tree, state, expected = self.create_basic_dirstate()
 
1812
        self.assertBisectRecursive(expected, ['a'], state, ['a'])
 
1813
        self.assertBisectRecursive(expected, ['b/c'], state, ['b/c'])
 
1814
        self.assertBisectRecursive(expected, ['b/d/e'], state, ['b/d/e'])
 
1815
        self.assertBisectRecursive(expected, ['b/d', 'b/d/e'],
 
1816
                                   state, ['b/d'])
 
1817
        self.assertBisectRecursive(expected, ['b', 'b/c', 'b/d', 'b/d/e'],
 
1818
                                   state, ['b'])
 
1819
        self.assertBisectRecursive(expected, ['', 'a', 'b', 'f', 'b/c',
 
1820
                                              'b/d', 'b/d/e'],
 
1821
                                   state, [''])
 
1822
 
 
1823
    def test_bisect_recursive_multiple(self):
 
1824
        tree, state, expected = self.create_basic_dirstate()
 
1825
        self.assertBisectRecursive(expected, ['a', 'b/c'], state, ['a', 'b/c'])
 
1826
        self.assertBisectRecursive(expected, ['b/d', 'b/d/e'],
 
1827
                                   state, ['b/d', 'b/d/e'])
 
1828
 
 
1829
    def test_bisect_recursive_missing(self):
 
1830
        tree, state, expected = self.create_basic_dirstate()
 
1831
        self.assertBisectRecursive(expected, [], state, ['d'])
 
1832
        self.assertBisectRecursive(expected, [], state, ['b/e'])
 
1833
        self.assertBisectRecursive(expected, [], state, ['g'])
 
1834
        self.assertBisectRecursive(expected, ['a'], state, ['a', 'g'])
 
1835
 
 
1836
    def test_bisect_recursive_renamed(self):
 
1837
        tree, state, expected = self.create_renamed_dirstate()
 
1838
 
 
1839
        # Looking for either renamed item should find the other
 
1840
        self.assertBisectRecursive(expected, ['a', 'b/g'], state, ['a'])
 
1841
        self.assertBisectRecursive(expected, ['a', 'b/g'], state, ['b/g'])
 
1842
        # Looking in the containing directory should find the rename target,
 
1843
        # and anything in a subdir of the renamed target.
 
1844
        self.assertBisectRecursive(expected, ['a', 'b', 'b/c', 'b/d',
 
1845
                                              'b/d/e', 'b/g', 'h', 'h/e'],
 
1846
                                   state, ['b'])
 
1847
 
 
1848
 
 
1849
class TestBisectDirblock(TestCase):
 
1850
    """Test that bisect_dirblock() returns the expected values.
 
1851
 
 
1852
    bisect_dirblock is intended to work like bisect.bisect_left() except it
 
1853
    knows it is working on dirblocks and that dirblocks are sorted by ('path',
 
1854
    'to', 'foo') chunks rather than by raw 'path/to/foo'.
 
1855
    """
 
1856
 
 
1857
    def assertBisect(self, dirblocks, split_dirblocks, path, *args, **kwargs):
 
1858
        """Assert that bisect_split works like bisect_left on the split paths.
 
1859
 
 
1860
        :param dirblocks: A list of (path, [info]) pairs.
 
1861
        :param split_dirblocks: A list of ((split, path), [info]) pairs.
 
1862
        :param path: The path we are indexing.
 
1863
 
 
1864
        All other arguments will be passed along.
 
1865
        """
 
1866
        bisect_split_idx = dirstate.bisect_dirblock(dirblocks, path,
 
1867
                                                 *args, **kwargs)
 
1868
        split_dirblock = (path.split('/'), [])
 
1869
        bisect_left_idx = bisect.bisect_left(split_dirblocks, split_dirblock,
 
1870
                                             *args)
 
1871
        self.assertEqual(bisect_left_idx, bisect_split_idx,
 
1872
                         'bisect_split disagreed. %s != %s'
 
1873
                         ' for key %s'
 
1874
                         % (bisect_left_idx, bisect_split_idx, path)
 
1875
                         )
 
1876
 
 
1877
    def paths_to_dirblocks(self, paths):
 
1878
        """Convert a list of paths into dirblock form.
 
1879
 
 
1880
        Also, ensure that the paths are in proper sorted order.
 
1881
        """
 
1882
        dirblocks = [(path, []) for path in paths]
 
1883
        split_dirblocks = [(path.split('/'), []) for path in paths]
 
1884
        self.assertEqual(sorted(split_dirblocks), split_dirblocks)
 
1885
        return dirblocks, split_dirblocks
 
1886
 
 
1887
    def test_simple(self):
 
1888
        """In the simple case it works just like bisect_left"""
 
1889
        paths = ['', 'a', 'b', 'c', 'd']
 
1890
        dirblocks, split_dirblocks = self.paths_to_dirblocks(paths)
 
1891
        for path in paths:
 
1892
            self.assertBisect(dirblocks, split_dirblocks, path)
 
1893
        self.assertBisect(dirblocks, split_dirblocks, '_')
 
1894
        self.assertBisect(dirblocks, split_dirblocks, 'aa')
 
1895
        self.assertBisect(dirblocks, split_dirblocks, 'bb')
 
1896
        self.assertBisect(dirblocks, split_dirblocks, 'cc')
 
1897
        self.assertBisect(dirblocks, split_dirblocks, 'dd')
 
1898
        self.assertBisect(dirblocks, split_dirblocks, 'a/a')
 
1899
        self.assertBisect(dirblocks, split_dirblocks, 'b/b')
 
1900
        self.assertBisect(dirblocks, split_dirblocks, 'c/c')
 
1901
        self.assertBisect(dirblocks, split_dirblocks, 'd/d')
 
1902
 
 
1903
    def test_involved(self):
 
1904
        """This is where bisect_left diverges slightly."""
 
1905
        paths = ['', 'a',
 
1906
                 'a/a', 'a/a/a', 'a/a/z', 'a/a-a', 'a/a-z',
 
1907
                 'a/z', 'a/z/a', 'a/z/z', 'a/z-a', 'a/z-z',
 
1908
                 'a-a', 'a-z',
 
1909
                 'z', 'z/a/a', 'z/a/z', 'z/a-a', 'z/a-z',
 
1910
                 'z/z', 'z/z/a', 'z/z/z', 'z/z-a', 'z/z-z',
 
1911
                 'z-a', 'z-z',
 
1912
                ]
 
1913
        dirblocks, split_dirblocks = self.paths_to_dirblocks(paths)
 
1914
        for path in paths:
 
1915
            self.assertBisect(dirblocks, split_dirblocks, path)
 
1916
 
 
1917
    def test_involved_cached(self):
 
1918
        """This is where bisect_left diverges slightly."""
 
1919
        paths = ['', 'a',
 
1920
                 'a/a', 'a/a/a', 'a/a/z', 'a/a-a', 'a/a-z',
 
1921
                 'a/z', 'a/z/a', 'a/z/z', 'a/z-a', 'a/z-z',
 
1922
                 'a-a', 'a-z',
 
1923
                 'z', 'z/a/a', 'z/a/z', 'z/a-a', 'z/a-z',
 
1924
                 'z/z', 'z/z/a', 'z/z/z', 'z/z-a', 'z/z-z',
 
1925
                 'z-a', 'z-z',
 
1926
                ]
 
1927
        cache = {}
 
1928
        dirblocks, split_dirblocks = self.paths_to_dirblocks(paths)
 
1929
        for path in paths:
 
1930
            self.assertBisect(dirblocks, split_dirblocks, path, cache=cache)
 
1931