~bzr-pqm/bzr/bzr.dev

« back to all changes in this revision

Viewing changes to bzrlib/tests/test_dirstate.py

- improved handling of non-ascii branch names and test
  patch from Joel Rosdahl

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 os
20
 
 
21
 
from bzrlib import (
22
 
    dirstate,
23
 
    errors,
24
 
    osutils,
25
 
    )
26
 
from bzrlib.memorytree import MemoryTree
27
 
from bzrlib.tests import TestCaseWithTransport
28
 
 
29
 
 
30
 
# TODO:
31
 
# test DirStateRevisionTree : test filtering out of deleted files does not
32
 
#         filter out files called RECYCLED.BIN ;)
33
 
# test 0 parents, 1 parent, 4 parents.
34
 
# test unicode parents, non unicode parents
35
 
# test all change permutations in one and two parents.
36
 
# i.e. file in parent 1, dir in parent 2, symlink in tree.
37
 
# test that renames in the tree result in correct parent paths
38
 
# Test get state from a file, then asking for lines.
39
 
# write a smaller state, and check the file has been truncated.
40
 
# add a entry when its in state deleted
41
 
# revision attribute for root entries.
42
 
# test that utf8 strings are preserved in _row_to_line
43
 
# test parent manipulation
44
 
# test parents that are null in save : i.e. no record in the parent tree for this.
45
 
# todo: _set_data records ghost parents.
46
 
# TESTS to write:
47
 
# general checks for NOT_IN_MEMORY error conditions.
48
 
# set_path_id on a NOT_IN_MEMORY dirstate
49
 
# set_path_id  unicode support
50
 
# set_path_id  setting id of a path not root
51
 
# set_path_id  setting id when there are parents without the id in the parents
52
 
# set_path_id  setting id when there are parents with the id in the parents
53
 
# set_path_id  setting id when state is not in memory
54
 
# set_path_id  setting id when state is in memory unmodified
55
 
# set_path_id  setting id when state is in memory modified
56
 
 
57
 
class TestCaseWithDirState(TestCaseWithTransport):
58
 
    """Helper functions for creating DirState objects with various content."""
59
 
 
60
 
    def create_empty_dirstate(self):
61
 
        """Return a locked but empty dirstate"""
62
 
        state = dirstate.DirState.initialize('dirstate')
63
 
        return state
64
 
 
65
 
    def create_dirstate_with_root(self):
66
 
        """Return a write-locked state with a single root entry."""
67
 
        packed_stat = 'AAAAREUHaIpFB2iKAAADAQAtkqUAAIGk'
68
 
        root_entry_direntry = ('', '', 'a-root-value'), [
69
 
            ('d', '', 0, False, packed_stat),
70
 
            ]
71
 
        dirblocks = []
72
 
        dirblocks.append(('', [root_entry_direntry]))
73
 
        dirblocks.append(('', []))
74
 
        state = self.create_empty_dirstate()
75
 
        try:
76
 
            state._set_data([], dirblocks)
77
 
        except:
78
 
            state.unlock()
79
 
            raise
80
 
        return state
81
 
 
82
 
    def create_dirstate_with_root_and_subdir(self):
83
 
        """Return a locked DirState with a root and a subdir"""
84
 
        packed_stat = 'AAAAREUHaIpFB2iKAAADAQAtkqUAAIGk'
85
 
        subdir_entry = ('', 'subdir', 'subdir-id'), [
86
 
            ('d', '', 0, False, packed_stat),
87
 
            ]
88
 
        state = self.create_dirstate_with_root()
89
 
        try:
90
 
            dirblocks = list(state._dirblocks)
91
 
            dirblocks[1][1].append(subdir_entry)
92
 
            state._set_data([], dirblocks)
93
 
        except:
94
 
            state.unlock()
95
 
            raise
96
 
        return state
97
 
 
98
 
    def create_complex_dirstate(self):
99
 
        """This dirstate contains multiple files and directories.
100
 
 
101
 
         /        a-root-value
102
 
         a/       a-dir
103
 
         b/       b-dir
104
 
         c        c-file
105
 
         d        d-file
106
 
         a/e/     e-dir
107
 
         a/f      f-file
108
 
         b/g      g-file
109
 
         b/h\xc3\xa5  h-\xc3\xa5-file  #This is u'\xe5' encoded into utf-8
110
 
 
111
 
        # Notice that a/e is an empty directory.
112
 
        """
113
 
        packed_stat = 'AAAAREUHaIpFB2iKAAADAQAtkqUAAIGk'
114
 
        null_sha = 'xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx'
115
 
        root_entry = ('', '', 'a-root-value'), [
116
 
            ('d', '', 0, False, packed_stat),
117
 
            ]
118
 
        a_entry = ('', 'a', 'a-dir'), [
119
 
            ('d', '', 0, False, packed_stat),
120
 
            ]
121
 
        b_entry = ('', 'b', 'b-dir'), [
122
 
            ('d', '', 0, False, packed_stat),
123
 
            ]
124
 
        c_entry = ('', 'c', 'c-file'), [
125
 
            ('f', null_sha, 10, False, packed_stat),
126
 
            ]
127
 
        d_entry = ('', 'd', 'd-file'), [
128
 
            ('f', null_sha, 20, False, packed_stat),
129
 
            ]
130
 
        e_entry = ('a', 'e', 'e-dir'), [
131
 
            ('d', '', 0, False, packed_stat),
132
 
            ]
133
 
        f_entry = ('a', 'f', 'f-file'), [
134
 
            ('f', null_sha, 30, False, packed_stat),
135
 
            ]
136
 
        g_entry = ('b', 'g', 'g-file'), [
137
 
            ('f', null_sha, 30, False, packed_stat),
138
 
            ]
139
 
        h_entry = ('b', 'h\xc3\xa5', 'h-\xc3\xa5-file'), [
140
 
            ('f', null_sha, 40, False, packed_stat),
141
 
            ]
142
 
        dirblocks = []
143
 
        dirblocks.append(('', [root_entry]))
144
 
        dirblocks.append(('', [a_entry, b_entry, c_entry, d_entry]))
145
 
        dirblocks.append(('a', [e_entry, f_entry]))
146
 
        dirblocks.append(('b', [g_entry, h_entry]))
147
 
        state = dirstate.DirState.initialize('dirstate')
148
 
        try:
149
 
            state._set_data([], dirblocks)
150
 
        except:
151
 
            state.unlock()
152
 
            raise
153
 
        return state
154
 
 
155
 
    def check_state_with_reopen(self, expected_result, state):
156
 
        """Check that state has current state expected_result.
157
 
 
158
 
        This will check the current state, open the file anew and check it
159
 
        again.
160
 
        This function expects the current state to be locked for writing, and
161
 
        will unlock it before re-opening.
162
 
        This is required because we can't open a lock_read() while something
163
 
        else has a lock_write().
164
 
            write => mutually exclusive lock
165
 
            read => shared lock
166
 
        """
167
 
        # The state should already be write locked, since we just had to do
168
 
        # some operation to get here.
169
 
        assert state._lock_token is not None
170
 
        try:
171
 
            self.assertEqual(expected_result[0],  state.get_parent_ids())
172
 
            # there should be no ghosts in this tree.
173
 
            self.assertEqual([], state.get_ghosts())
174
 
            # there should be one fileid in this tree - the root of the tree.
175
 
            self.assertEqual(expected_result[1], list(state._iter_entries()))
176
 
            state.save()
177
 
        finally:
178
 
            state.unlock()
179
 
        del state # Callers should unlock
180
 
        state = dirstate.DirState.on_file('dirstate')
181
 
        state.lock_read()
182
 
        try:
183
 
            self.assertEqual(expected_result[1], list(state._iter_entries()))
184
 
        finally:
185
 
            state.unlock()
186
 
 
187
 
 
188
 
class TestTreeToDirState(TestCaseWithDirState):
189
 
 
190
 
    def test_empty_to_dirstate(self):
191
 
        """We should be able to create a dirstate for an empty tree."""
192
 
        # There are no files on disk and no parents
193
 
        tree = self.make_branch_and_tree('tree')
194
 
        expected_result = ([], [
195
 
            (('', '', tree.path2id('')), # common details
196
 
             [('d', '', 0, False, dirstate.DirState.NULLSTAT), # current tree
197
 
             ])])
198
 
        state = dirstate.DirState.from_tree(tree, 'dirstate')
199
 
        self.check_state_with_reopen(expected_result, state)
200
 
 
201
 
    def test_1_parents_empty_to_dirstate(self):
202
 
        # create a parent by doing a commit
203
 
        tree = self.make_branch_and_tree('tree')
204
 
        rev_id = tree.commit('first post').encode('utf8')
205
 
        root_stat_pack = dirstate.pack_stat(os.stat(tree.basedir))
206
 
        expected_result = ([rev_id], [
207
 
            (('', '', tree.path2id('')), # common details
208
 
             [('d', '', 0, False, dirstate.DirState.NULLSTAT), # current tree
209
 
              ('d', '', 0, False, rev_id), # first parent details
210
 
             ])])
211
 
        state = dirstate.DirState.from_tree(tree, 'dirstate')
212
 
        self.check_state_with_reopen(expected_result, state)
213
 
 
214
 
    def test_2_parents_empty_to_dirstate(self):
215
 
        # create a parent by doing a commit
216
 
        tree = self.make_branch_and_tree('tree')
217
 
        rev_id = tree.commit('first post')
218
 
        tree2 = tree.bzrdir.sprout('tree2').open_workingtree()
219
 
        rev_id2 = tree2.commit('second post', allow_pointless=True)
220
 
        tree.merge_from_branch(tree2.branch)
221
 
        expected_result = ([rev_id, rev_id2], [
222
 
            (('', '', tree.path2id('')), # common details
223
 
             [('d', '', 0, False, dirstate.DirState.NULLSTAT), # current tree
224
 
              ('d', '', 0, False, rev_id), # first parent details
225
 
              ('d', '', 0, False, rev_id2), # second parent details
226
 
             ])])
227
 
        state = dirstate.DirState.from_tree(tree, 'dirstate')
228
 
        self.check_state_with_reopen(expected_result, state)
229
 
 
230
 
    def test_empty_unknowns_are_ignored_to_dirstate(self):
231
 
        """We should be able to create a dirstate for an empty tree."""
232
 
        # There are no files on disk and no parents
233
 
        tree = self.make_branch_and_tree('tree')
234
 
        self.build_tree(['tree/unknown'])
235
 
        expected_result = ([], [
236
 
            (('', '', tree.path2id('')), # common details
237
 
             [('d', '', 0, False, dirstate.DirState.NULLSTAT), # current tree
238
 
             ])])
239
 
        state = dirstate.DirState.from_tree(tree, 'dirstate')
240
 
        self.check_state_with_reopen(expected_result, state)
241
 
 
242
 
    def get_tree_with_a_file(self):
243
 
        tree = self.make_branch_and_tree('tree')
244
 
        self.build_tree(['tree/a file'])
245
 
        tree.add('a file', 'a file id')
246
 
        return tree
247
 
 
248
 
    def test_non_empty_no_parents_to_dirstate(self):
249
 
        """We should be able to create a dirstate for an empty tree."""
250
 
        # There are files on disk and no parents
251
 
        tree = self.get_tree_with_a_file()
252
 
        expected_result = ([], [
253
 
            (('', '', tree.path2id('')), # common details
254
 
             [('d', '', 0, False, dirstate.DirState.NULLSTAT), # current tree
255
 
             ]),
256
 
            (('', 'a file', 'a file id'), # common
257
 
             [('f', '', 0, False, dirstate.DirState.NULLSTAT), # current
258
 
             ]),
259
 
            ])
260
 
        state = dirstate.DirState.from_tree(tree, 'dirstate')
261
 
        self.check_state_with_reopen(expected_result, state)
262
 
 
263
 
    def test_1_parents_not_empty_to_dirstate(self):
264
 
        # create a parent by doing a commit
265
 
        tree = self.get_tree_with_a_file()
266
 
        rev_id = tree.commit('first post').encode('utf8')
267
 
        # change the current content to be different this will alter stat, sha
268
 
        # and length:
269
 
        self.build_tree_contents([('tree/a file', 'new content\n')])
270
 
        expected_result = ([rev_id], [
271
 
            (('', '', tree.path2id('')), # common details
272
 
             [('d', '', 0, False, dirstate.DirState.NULLSTAT), # current tree
273
 
              ('d', '', 0, False, rev_id), # first parent details
274
 
             ]),
275
 
            (('', 'a file', 'a file id'), # common
276
 
             [('f', '', 0, False, dirstate.DirState.NULLSTAT), # current
277
 
              ('f', 'c3ed76e4bfd45ff1763ca206055bca8e9fc28aa8', 24, False,
278
 
               rev_id), # first parent
279
 
             ]),
280
 
            ])
281
 
        state = dirstate.DirState.from_tree(tree, 'dirstate')
282
 
        self.check_state_with_reopen(expected_result, state)
283
 
 
284
 
    def test_2_parents_not_empty_to_dirstate(self):
285
 
        # create a parent by doing a commit
286
 
        tree = self.get_tree_with_a_file()
287
 
        rev_id = tree.commit('first post').encode('utf8')
288
 
        tree2 = tree.bzrdir.sprout('tree2').open_workingtree()
289
 
        # change the current content to be different this will alter stat, sha
290
 
        # and length:
291
 
        self.build_tree_contents([('tree2/a file', 'merge content\n')])
292
 
        rev_id2 = tree2.commit('second post').encode('utf8')
293
 
        tree.merge_from_branch(tree2.branch)
294
 
        # change the current content to be different this will alter stat, sha
295
 
        # and length again, giving us three distinct values:
296
 
        self.build_tree_contents([('tree/a file', 'new content\n')])
297
 
        expected_result = ([rev_id, rev_id2], [
298
 
            (('', '', tree.path2id('')), # common details
299
 
             [('d', '', 0, False, dirstate.DirState.NULLSTAT), # current tree
300
 
              ('d', '', 0, False, rev_id), # first parent details
301
 
              ('d', '', 0, False, rev_id2), # second parent details
302
 
             ]),
303
 
            (('', 'a file', 'a file id'), # common
304
 
             [('f', '', 0, False, dirstate.DirState.NULLSTAT), # current
305
 
              ('f', 'c3ed76e4bfd45ff1763ca206055bca8e9fc28aa8', 24, False,
306
 
               rev_id), # first parent
307
 
              ('f', '314d796174c9412647c3ce07dfb5d36a94e72958', 14, False,
308
 
               rev_id2), # second parent
309
 
             ]),
310
 
            ])
311
 
        state = dirstate.DirState.from_tree(tree, 'dirstate')
312
 
        self.check_state_with_reopen(expected_result, state)
313
 
 
314
 
 
315
 
class TestDirStateOnFile(TestCaseWithDirState):
316
 
 
317
 
    def test_construct_with_path(self):
318
 
        tree = self.make_branch_and_tree('tree')
319
 
        state = dirstate.DirState.from_tree(tree, 'dirstate.from_tree')
320
 
        # we want to be able to get the lines of the dirstate that we will
321
 
        # write to disk.
322
 
        lines = state.get_lines()
323
 
        state.unlock()
324
 
        self.build_tree_contents([('dirstate', ''.join(lines))])
325
 
        # get a state object
326
 
        # no parents, default tree content
327
 
        expected_result = ([], [
328
 
            (('', '', tree.path2id('')), # common details
329
 
             # current tree details, but new from_tree skips statting, it
330
 
             # uses set_state_from_inventory, and thus depends on the
331
 
             # inventory state.
332
 
             [('d', '', 0, False, dirstate.DirState.NULLSTAT),
333
 
             ])
334
 
            ])
335
 
        state = dirstate.DirState.on_file('dirstate')
336
 
        state.lock_write() # check_state_with_reopen will save() and unlock it
337
 
        self.check_state_with_reopen(expected_result, state)
338
 
 
339
 
    def test_can_save_clean_on_file(self):
340
 
        tree = self.make_branch_and_tree('tree')
341
 
        state = dirstate.DirState.from_tree(tree, 'dirstate')
342
 
        try:
343
 
            # doing a save should work here as there have been no changes.
344
 
            state.save()
345
 
            # TODO: stat it and check it hasn't changed; may require waiting
346
 
            # for the state accuracy window.
347
 
        finally:
348
 
            state.unlock()
349
 
 
350
 
 
351
 
class TestDirStateInitialize(TestCaseWithDirState):
352
 
 
353
 
    def test_initialize(self):
354
 
        expected_result = ([], [
355
 
            (('', '', 'TREE_ROOT'), # common details
356
 
             [('d', '', 0, False, dirstate.DirState.NULLSTAT), # current tree
357
 
             ])
358
 
            ])
359
 
        state = dirstate.DirState.initialize('dirstate')
360
 
        try:
361
 
            self.assertIsInstance(state, dirstate.DirState)
362
 
            lines = state.get_lines()
363
 
            self.assertFileEqual(''.join(state.get_lines()),
364
 
                'dirstate')
365
 
            self.check_state_with_reopen(expected_result, state)
366
 
        except:
367
 
            state.unlock()
368
 
            raise
369
 
 
370
 
 
371
 
class TestDirStateManipulations(TestCaseWithDirState):
372
 
 
373
 
    def test_set_state_from_inventory_no_content_no_parents(self):
374
 
        # setting the current inventory is a slow but important api to support.
375
 
        tree1 = self.make_branch_and_memory_tree('tree1')
376
 
        tree1.lock_write()
377
 
        try:
378
 
            tree1.add('')
379
 
            revid1 = tree1.commit('foo').encode('utf8')
380
 
            root_id = tree1.inventory.root.file_id
381
 
            inv = tree1.inventory
382
 
        finally:
383
 
            tree1.unlock()
384
 
        expected_result = [], [
385
 
            (('', '', root_id), [
386
 
             ('d', '', 0, False, dirstate.DirState.NULLSTAT)])]
387
 
        state = dirstate.DirState.initialize('dirstate')
388
 
        try:
389
 
            state.set_state_from_inventory(inv)
390
 
            self.assertEqual(dirstate.DirState.IN_MEMORY_UNMODIFIED,
391
 
                             state._header_state)
392
 
            self.assertEqual(dirstate.DirState.IN_MEMORY_MODIFIED,
393
 
                             state._dirblock_state)
394
 
        except:
395
 
            state.unlock()
396
 
            raise
397
 
        else:
398
 
            # This will unlock it
399
 
            self.check_state_with_reopen(expected_result, state)
400
 
 
401
 
    def test_set_path_id_no_parents(self):
402
 
        """The id of a path can be changed trivally with no parents."""
403
 
        state = dirstate.DirState.initialize('dirstate')
404
 
        try:
405
 
            # check precondition to be sure the state does change appropriately.
406
 
            self.assertEqual(
407
 
                [(('', '', 'TREE_ROOT'), [('d', '', 0, False,
408
 
                   'xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx')])],
409
 
                list(state._iter_entries()))
410
 
            state.set_path_id('', 'foobarbaz')
411
 
            expected_rows = [
412
 
                (('', '', 'foobarbaz'), [('d', '', 0, False,
413
 
                   'xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx')])]
414
 
            self.assertEqual(expected_rows, list(state._iter_entries()))
415
 
            # should work across save too
416
 
            state.save()
417
 
        finally:
418
 
            state.unlock()
419
 
        state = dirstate.DirState.on_file('dirstate')
420
 
        state.lock_read()
421
 
        try:
422
 
            self.assertEqual(expected_rows, list(state._iter_entries()))
423
 
        finally:
424
 
            state.unlock()
425
 
 
426
 
    def test_set_parent_trees_no_content(self):
427
 
        # set_parent_trees is a slow but important api to support.
428
 
        tree1 = self.make_branch_and_memory_tree('tree1')
429
 
        tree1.lock_write()
430
 
        try:
431
 
            tree1.add('')
432
 
            revid1 = tree1.commit('foo')
433
 
        finally:
434
 
            tree1.unlock()
435
 
        branch2 = tree1.branch.bzrdir.clone('tree2').open_branch()
436
 
        tree2 = MemoryTree.create_on_branch(branch2)
437
 
        tree2.lock_write()
438
 
        try:
439
 
            revid2 = tree2.commit('foo')
440
 
            root_id = tree2.inventory.root.file_id
441
 
        finally:
442
 
            tree2.unlock()
443
 
        state = dirstate.DirState.initialize('dirstate')
444
 
        try:
445
 
            state.set_path_id('', root_id)
446
 
            state.set_parent_trees(
447
 
                ((revid1, tree1.branch.repository.revision_tree(revid1)),
448
 
                 (revid2, tree2.branch.repository.revision_tree(revid2)),
449
 
                 ('ghost-rev', None)),
450
 
                ['ghost-rev'])
451
 
            # check we can reopen and use the dirstate after setting parent
452
 
            # trees.
453
 
            state.save()
454
 
        finally:
455
 
            state.unlock()
456
 
        state = dirstate.DirState.on_file('dirstate')
457
 
        state.lock_write()
458
 
        try:
459
 
            self.assertEqual([revid1, revid2, 'ghost-rev'],
460
 
                             state.get_parent_ids())
461
 
            # iterating the entire state ensures that the state is parsable.
462
 
            list(state._iter_entries())
463
 
            # be sure that it sets not appends - change it
464
 
            state.set_parent_trees(
465
 
                ((revid1, tree1.branch.repository.revision_tree(revid1)),
466
 
                 ('ghost-rev', None)),
467
 
                ['ghost-rev'])
468
 
            # and now put it back.
469
 
            state.set_parent_trees(
470
 
                ((revid1, tree1.branch.repository.revision_tree(revid1)),
471
 
                 (revid2, tree2.branch.repository.revision_tree(revid2)),
472
 
                 ('ghost-rev', tree2.branch.repository.revision_tree(None))),
473
 
                ['ghost-rev'])
474
 
            self.assertEqual([revid1, revid2, 'ghost-rev'],
475
 
                             state.get_parent_ids())
476
 
            # the ghost should be recorded as such by set_parent_trees.
477
 
            self.assertEqual(['ghost-rev'], state.get_ghosts())
478
 
            self.assertEqual(
479
 
                [(('', '', root_id), [
480
 
                  ('d', '', 0, False, dirstate.DirState.NULLSTAT),
481
 
                  ('d', '', 0, False, revid1),
482
 
                  ('d', '', 0, False, revid2)
483
 
                  ])],
484
 
                list(state._iter_entries()))
485
 
        finally:
486
 
            state.unlock()
487
 
 
488
 
    def test_set_parent_trees_file_missing_from_tree(self):
489
 
        # Adding a parent tree may reference files not in the current state.
490
 
        # they should get listed just once by id, even if they are in two
491
 
        # separate trees.
492
 
        # set_parent_trees is a slow but important api to support.
493
 
        tree1 = self.make_branch_and_memory_tree('tree1')
494
 
        tree1.lock_write()
495
 
        try:
496
 
            tree1.add('')
497
 
            tree1.add(['a file'], ['file-id'], ['file'])
498
 
            tree1.put_file_bytes_non_atomic('file-id', 'file-content')
499
 
            revid1 = tree1.commit('foo')
500
 
        finally:
501
 
            tree1.unlock()
502
 
        branch2 = tree1.branch.bzrdir.clone('tree2').open_branch()
503
 
        tree2 = MemoryTree.create_on_branch(branch2)
504
 
        tree2.lock_write()
505
 
        try:
506
 
            tree2.put_file_bytes_non_atomic('file-id', 'new file-content')
507
 
            revid2 = tree2.commit('foo')
508
 
            root_id = tree2.inventory.root.file_id
509
 
        finally:
510
 
            tree2.unlock()
511
 
        # check the layout in memory
512
 
        expected_result = [revid1.encode('utf8'), revid2.encode('utf8')], [
513
 
            (('', '', root_id), [
514
 
             ('d', '', 0, False, dirstate.DirState.NULLSTAT),
515
 
             ('d', '', 0, False, revid1.encode('utf8')),
516
 
             ('d', '', 0, False, revid2.encode('utf8'))
517
 
             ]),
518
 
            (('', 'a file', 'file-id'), [
519
 
             ('a', '', 0, False, ''),
520
 
             ('f', '2439573625385400f2a669657a7db6ae7515d371', 12, False,
521
 
              revid1.encode('utf8')),
522
 
             ('f', '542e57dc1cda4af37cb8e55ec07ce60364bb3c7d', 16, False,
523
 
              revid2.encode('utf8'))
524
 
             ])
525
 
            ]
526
 
        state = dirstate.DirState.initialize('dirstate')
527
 
        try:
528
 
            state.set_path_id('', root_id)
529
 
            state.set_parent_trees(
530
 
                ((revid1, tree1.branch.repository.revision_tree(revid1)),
531
 
                 (revid2, tree2.branch.repository.revision_tree(revid2)),
532
 
                 ), [])
533
 
        except:
534
 
            state.unlock()
535
 
            raise
536
 
        else:
537
 
            # check_state_with_reopen will unlock
538
 
            self.check_state_with_reopen(expected_result, state)
539
 
 
540
 
    ### add a path via _set_data - so we dont need delta work, just
541
 
    # raw data in, and ensure that it comes out via get_lines happily.
542
 
 
543
 
    def test_add_path_to_root_no_parents_all_data(self):
544
 
        # The most trivial addition of a path is when there are no parents and
545
 
        # its in the root and all data about the file is supplied
546
 
        self.build_tree(['a file'])
547
 
        stat = os.lstat('a file')
548
 
        # the 1*20 is the sha1 pretend value.
549
 
        state = dirstate.DirState.initialize('dirstate')
550
 
        expected_entries = [
551
 
            (('', '', 'TREE_ROOT'), [
552
 
             ('d', '', 0, False, dirstate.DirState.NULLSTAT), # current tree
553
 
             ]),
554
 
            (('', 'a file', 'a file id'), [
555
 
             ('f', '1'*20, 19, False, dirstate.pack_stat(stat)), # current tree
556
 
             ]),
557
 
            ]
558
 
        try:
559
 
            state.add('a file', 'a file id', 'file', stat, '1'*20)
560
 
            # having added it, it should be in the output of iter_entries.
561
 
            self.assertEqual(expected_entries, list(state._iter_entries()))
562
 
            # saving and reloading should not affect this.
563
 
            state.save()
564
 
        finally:
565
 
            state.unlock()
566
 
        state = dirstate.DirState.on_file('dirstate')
567
 
        state.lock_read()
568
 
        try:
569
 
            self.assertEqual(expected_entries, list(state._iter_entries()))
570
 
        finally:
571
 
            state.unlock()
572
 
 
573
 
    def test_add_path_to_unversioned_directory(self):
574
 
        """Adding a path to an unversioned directory should error.
575
 
 
576
 
        This is a duplicate of TestWorkingTree.test_add_in_unversioned,
577
 
        once dirstate is stable and if it is merged with WorkingTree3, consider
578
 
        removing this copy of the test.
579
 
        """
580
 
        self.build_tree(['unversioned/', 'unversioned/a file'])
581
 
        state = dirstate.DirState.initialize('dirstate')
582
 
        try:
583
 
            self.assertRaises(errors.NotVersionedError, state.add,
584
 
                'unversioned/a file', 'a file id', 'file', None, None)
585
 
        finally:
586
 
            state.unlock()
587
 
 
588
 
    def test_add_directory_to_root_no_parents_all_data(self):
589
 
        # The most trivial addition of a dir is when there are no parents and
590
 
        # its in the root and all data about the file is supplied
591
 
        self.build_tree(['a dir/'])
592
 
        stat = os.lstat('a dir')
593
 
        expected_entries = [
594
 
            (('', '', 'TREE_ROOT'), [
595
 
             ('d', '', 0, False, dirstate.DirState.NULLSTAT), # current tree
596
 
             ]),
597
 
            (('', 'a dir', 'a dir id'), [
598
 
             ('d', '', 0, False, dirstate.pack_stat(stat)), # current tree
599
 
             ]),
600
 
            ]
601
 
        state = dirstate.DirState.initialize('dirstate')
602
 
        try:
603
 
            state.add('a dir', 'a dir id', 'directory', stat, None)
604
 
            # having added it, it should be in the output of iter_entries.
605
 
            self.assertEqual(expected_entries, list(state._iter_entries()))
606
 
            # saving and reloading should not affect this.
607
 
            state.save()
608
 
        finally:
609
 
            state.unlock()
610
 
        state = dirstate.DirState.on_file('dirstate')
611
 
        state.lock_read()
612
 
        try:
613
 
            self.assertEqual(expected_entries, list(state._iter_entries()))
614
 
        finally:
615
 
            state.unlock()
616
 
 
617
 
    def test_add_symlink_to_root_no_parents_all_data(self):
618
 
        # The most trivial addition of a symlink when there are no parents and
619
 
        # its in the root and all data about the file is supplied
620
 
        ## TODO: windows: dont fail this test. Also, how are symlinks meant to
621
 
        # be represented on windows.
622
 
        os.symlink('target', 'a link')
623
 
        stat = os.lstat('a link')
624
 
        expected_entries = [
625
 
            (('', '', 'TREE_ROOT'), [
626
 
             ('d', '', 0, False, dirstate.DirState.NULLSTAT), # current tree
627
 
             ]),
628
 
            (('', 'a link', 'a link id'), [
629
 
             ('l', 'target', 6, False, dirstate.pack_stat(stat)), # current tree
630
 
             ]),
631
 
            ]
632
 
        state = dirstate.DirState.initialize('dirstate')
633
 
        try:
634
 
            state.add('a link', 'a link id', 'symlink', stat, 'target')
635
 
            # having added it, it should be in the output of iter_entries.
636
 
            self.assertEqual(expected_entries, list(state._iter_entries()))
637
 
            # saving and reloading should not affect this.
638
 
            state.save()
639
 
        finally:
640
 
            state.unlock()
641
 
        state = dirstate.DirState.on_file('dirstate')
642
 
        state.lock_read()
643
 
        try:
644
 
            self.assertEqual(expected_entries, list(state._iter_entries()))
645
 
        finally:
646
 
            state.unlock()
647
 
 
648
 
    def test_add_directory_and_child_no_parents_all_data(self):
649
 
        # after adding a directory, we should be able to add children to it.
650
 
        self.build_tree(['a dir/', 'a dir/a file'])
651
 
        dirstat = os.lstat('a dir')
652
 
        filestat = os.lstat('a dir/a file')
653
 
        expected_entries = [
654
 
            (('', '', 'TREE_ROOT'), [
655
 
             ('d', '', 0, False, dirstate.DirState.NULLSTAT), # current tree
656
 
             ]),
657
 
            (('', 'a dir', 'a dir id'), [
658
 
             ('d', '', 0, False, dirstate.pack_stat(dirstat)), # current tree
659
 
             ]),
660
 
            (('a dir', 'a file', 'a file id'), [
661
 
             ('f', '1'*20, 25, False,
662
 
              dirstate.pack_stat(filestat)), # current tree details
663
 
             ]),
664
 
            ]
665
 
        state = dirstate.DirState.initialize('dirstate')
666
 
        try:
667
 
            state.add('a dir', 'a dir id', 'directory', dirstat, None)
668
 
            state.add('a dir/a file', 'a file id', 'file', filestat, '1'*20)
669
 
            # added it, it should be in the output of iter_entries.
670
 
            self.assertEqual(expected_entries, list(state._iter_entries()))
671
 
            # saving and reloading should not affect this.
672
 
            state.save()
673
 
        finally:
674
 
            state.unlock()
675
 
        state = dirstate.DirState.on_file('dirstate')
676
 
        state.lock_read()
677
 
        try:
678
 
            self.assertEqual(expected_entries, list(state._iter_entries()))
679
 
        finally:
680
 
            state.unlock()
681
 
 
682
 
 
683
 
class TestGetLines(TestCaseWithDirState):
684
 
 
685
 
    def test_get_line_with_2_rows(self):
686
 
        state = self.create_dirstate_with_root_and_subdir()
687
 
        try:
688
 
            self.assertEqual(['#bazaar dirstate flat format 2\n',
689
 
                'adler32: -1327947603\n',
690
 
                'num_entries: 2\n',
691
 
                '0\x00\n\x00'
692
 
                '0\x00\n\x00'
693
 
                '\x00\x00a-root-value\x00'
694
 
                'd\x00\x000\x00n\x00AAAAREUHaIpFB2iKAAADAQAtkqUAAIGk\x00\n\x00'
695
 
                '\x00subdir\x00subdir-id\x00'
696
 
                'd\x00\x000\x00n\x00AAAAREUHaIpFB2iKAAADAQAtkqUAAIGk\x00\n\x00'
697
 
                ], state.get_lines())
698
 
        finally:
699
 
            state.unlock()
700
 
 
701
 
    def test_entry_to_line(self):
702
 
        state = self.create_dirstate_with_root()
703
 
        try:
704
 
            self.assertEqual(
705
 
                '\x00\x00a-root-value\x00d\x00\x000\x00n'
706
 
                '\x00AAAAREUHaIpFB2iKAAADAQAtkqUAAIGk',
707
 
                state._entry_to_line(state._dirblocks[0][1][0]))
708
 
        finally:
709
 
            state.unlock()
710
 
 
711
 
    def test_entry_to_line_with_parent(self):
712
 
        packed_stat = 'AAAAREUHaIpFB2iKAAADAQAtkqUAAIGk'
713
 
        root_entry = ('', '', 'a-root-value'), [
714
 
            ('d', '', 0, False, packed_stat), # current tree details
715
 
             # first: a pointer to the current location
716
 
            ('a', 'dirname/basename', 0, False, ''),
717
 
            ]
718
 
        state = dirstate.DirState.initialize('dirstate')
719
 
        try:
720
 
            self.assertEqual(
721
 
                '\x00\x00a-root-value\x00'
722
 
                'd\x00\x000\x00n\x00AAAAREUHaIpFB2iKAAADAQAtkqUAAIGk\x00'
723
 
                'a\x00dirname/basename\x000\x00n\x00',
724
 
                state._entry_to_line(root_entry))
725
 
        finally:
726
 
            state.unlock()
727
 
 
728
 
    def test_entry_to_line_with_two_parents_at_different_paths(self):
729
 
        # / in the tree, at / in one parent and /dirname/basename in the other.
730
 
        packed_stat = 'AAAAREUHaIpFB2iKAAADAQAtkqUAAIGk'
731
 
        root_entry = ('', '', 'a-root-value'), [
732
 
            ('d', '', 0, False, packed_stat), # current tree details
733
 
            ('d', '', 0, False, 'rev_id'), # first parent details
734
 
             # second: a pointer to the current location
735
 
            ('a', 'dirname/basename', 0, False, ''),
736
 
            ]
737
 
        state = dirstate.DirState.initialize('dirstate')
738
 
        try:
739
 
            self.assertEqual(
740
 
                '\x00\x00a-root-value\x00'
741
 
                'd\x00\x000\x00n\x00AAAAREUHaIpFB2iKAAADAQAtkqUAAIGk\x00'
742
 
                'd\x00\x000\x00n\x00rev_id\x00'
743
 
                'a\x00dirname/basename\x000\x00n\x00',
744
 
                state._entry_to_line(root_entry))
745
 
        finally:
746
 
            state.unlock()
747
 
 
748
 
    def test_iter_entries(self):
749
 
        # we should be able to iterate the dirstate entries from end to end
750
 
        # this is for get_lines to be easy to read.
751
 
        packed_stat = 'AAAAREUHaIpFB2iKAAADAQAtkqUAAIGk'
752
 
        dirblocks = []
753
 
        root_entries = [(('', '', 'a-root-value'), [
754
 
            ('d', '', 0, False, packed_stat), # current tree details
755
 
            ])]
756
 
        dirblocks.append(('', root_entries))
757
 
        # add two files in the root
758
 
        subdir_entry = ('', 'subdir', 'subdir-id'), [
759
 
            ('d', '', 0, False, packed_stat), # current tree details
760
 
            ]
761
 
        afile_entry = ('', 'afile', 'afile-id'), [
762
 
            ('f', 'sha1value', 34, False, packed_stat), # current tree details
763
 
            ]
764
 
        dirblocks.append(('', [subdir_entry, afile_entry]))
765
 
        # and one in subdir
766
 
        file_entry2 = ('subdir', '2file', '2file-id'), [
767
 
            ('f', 'sha1value', 23, False, packed_stat), # current tree details
768
 
            ]
769
 
        dirblocks.append(('subdir', [file_entry2]))
770
 
        state = dirstate.DirState.initialize('dirstate')
771
 
        try:
772
 
            state._set_data([], dirblocks)
773
 
            expected_entries = [root_entries[0], subdir_entry, afile_entry,
774
 
                                file_entry2]
775
 
            self.assertEqual(expected_entries, list(state._iter_entries()))
776
 
        finally:
777
 
            state.unlock()
778
 
 
779
 
 
780
 
class TestGetBlockRowIndex(TestCaseWithDirState):
781
 
 
782
 
    def assertBlockRowIndexEqual(self, block_index, row_index, dir_present,
783
 
        file_present, state, dirname, basename, tree_index):
784
 
        self.assertEqual((block_index, row_index, dir_present, file_present),
785
 
            state._get_block_entry_index(dirname, basename, tree_index))
786
 
        if dir_present:
787
 
            block = state._dirblocks[block_index]
788
 
            self.assertEqual(dirname, block[0])
789
 
        if dir_present and file_present:
790
 
            row = state._dirblocks[block_index][1][row_index]
791
 
            self.assertEqual(dirname, row[0][0])
792
 
            self.assertEqual(basename, row[0][1])
793
 
 
794
 
    def test_simple_structure(self):
795
 
        state = self.create_dirstate_with_root_and_subdir()
796
 
        self.addCleanup(state.unlock)
797
 
        self.assertBlockRowIndexEqual(1, 0, True, True, state, '', 'subdir', 0)
798
 
        self.assertBlockRowIndexEqual(1, 0, True, False, state, '', 'bdir', 0)
799
 
        self.assertBlockRowIndexEqual(1, 1, True, False, state, '', 'zdir', 0)
800
 
        self.assertBlockRowIndexEqual(2, 0, False, False, state, 'a', 'foo', 0)
801
 
        self.assertBlockRowIndexEqual(2, 0, False, False, state,
802
 
                                      'subdir', 'foo', 0)
803
 
 
804
 
    def test_complex_structure_exists(self):
805
 
        state = self.create_complex_dirstate()
806
 
        self.addCleanup(state.unlock)
807
 
        # Make sure we can find everything that exists
808
 
        self.assertBlockRowIndexEqual(0, 0, True, True, state, '', '', 0)
809
 
        self.assertBlockRowIndexEqual(1, 0, True, True, state, '', 'a', 0)
810
 
        self.assertBlockRowIndexEqual(1, 1, True, True, state, '', 'b', 0)
811
 
        self.assertBlockRowIndexEqual(1, 2, True, True, state, '', 'c', 0)
812
 
        self.assertBlockRowIndexEqual(1, 3, True, True, state, '', 'd', 0)
813
 
        self.assertBlockRowIndexEqual(2, 0, True, True, state, 'a', 'e', 0)
814
 
        self.assertBlockRowIndexEqual(2, 1, True, True, state, 'a', 'f', 0)
815
 
        self.assertBlockRowIndexEqual(3, 0, True, True, state, 'b', 'g', 0)
816
 
        self.assertBlockRowIndexEqual(3, 1, True, True, state,
817
 
                                      'b', 'h\xc3\xa5', 0)
818
 
 
819
 
    def test_complex_structure_missing(self):
820
 
        state = self.create_complex_dirstate()
821
 
        self.addCleanup(state.unlock)
822
 
        # Make sure things would be inserted in the right locations
823
 
        # '_' comes before 'a'
824
 
        self.assertBlockRowIndexEqual(0, 0, True, True, state, '', '', 0)
825
 
        self.assertBlockRowIndexEqual(1, 0, True, False, state, '', '_', 0)
826
 
        self.assertBlockRowIndexEqual(1, 1, True, False, state, '', 'aa', 0)
827
 
        self.assertBlockRowIndexEqual(1, 4, True, False, state,
828
 
                                      '', 'h\xc3\xa5', 0)
829
 
        self.assertBlockRowIndexEqual(2, 0, False, False, state, '_', 'a', 0)
830
 
        self.assertBlockRowIndexEqual(3, 0, False, False, state, 'aa', 'a', 0)
831
 
        self.assertBlockRowIndexEqual(4, 0, False, False, state, 'bb', 'a', 0)
832
 
        # This would be inserted between a/ and b/
833
 
        self.assertBlockRowIndexEqual(3, 0, False, False, state, 'a/e', 'a', 0)
834
 
        # Put at the end
835
 
        self.assertBlockRowIndexEqual(4, 0, False, False, state, 'e', 'a', 0)
836
 
 
837
 
 
838
 
class TestGetEntry(TestCaseWithDirState):
839
 
 
840
 
    def assertEntryEqual(self, dirname, basename, file_id, state, path, index):
841
 
        """Check that the right entry is returned for a request to getEntry."""
842
 
        entry = state._get_entry(index, path_utf8=path)
843
 
        if file_id is None:
844
 
            self.assertEqual((None, None), entry)
845
 
        else:
846
 
            cur = entry[0]
847
 
            self.assertEqual((dirname, basename, file_id), cur[:3])
848
 
 
849
 
    def test_simple_structure(self):
850
 
        state = self.create_dirstate_with_root_and_subdir()
851
 
        self.addCleanup(state.unlock)
852
 
        self.assertEntryEqual('', '', 'a-root-value', state, '', 0)
853
 
        self.assertEntryEqual('', 'subdir', 'subdir-id', state, 'subdir', 0)
854
 
        self.assertEntryEqual(None, None, None, state, 'missing', 0)
855
 
        self.assertEntryEqual(None, None, None, state, 'missing/foo', 0)
856
 
        self.assertEntryEqual(None, None, None, state, 'subdir/foo', 0)
857
 
 
858
 
    def test_complex_structure_exists(self):
859
 
        state = self.create_complex_dirstate()
860
 
        self.addCleanup(state.unlock)
861
 
        self.assertEntryEqual('', '', 'a-root-value', state, '', 0)
862
 
        self.assertEntryEqual('', 'a', 'a-dir', state, 'a', 0)
863
 
        self.assertEntryEqual('', 'b', 'b-dir', state, 'b', 0)
864
 
        self.assertEntryEqual('', 'c', 'c-file', state, 'c', 0)
865
 
        self.assertEntryEqual('', 'd', 'd-file', state, 'd', 0)
866
 
        self.assertEntryEqual('a', 'e', 'e-dir', state, 'a/e', 0)
867
 
        self.assertEntryEqual('a', 'f', 'f-file', state, 'a/f', 0)
868
 
        self.assertEntryEqual('b', 'g', 'g-file', state, 'b/g', 0)
869
 
        self.assertEntryEqual('b', 'h\xc3\xa5', 'h-\xc3\xa5-file', state,
870
 
                              'b/h\xc3\xa5', 0)
871
 
 
872
 
    def test_complex_structure_missing(self):
873
 
        state = self.create_complex_dirstate()
874
 
        self.addCleanup(state.unlock)
875
 
        self.assertEntryEqual(None, None, None, state, '_', 0)
876
 
        self.assertEntryEqual(None, None, None, state, '_\xc3\xa5', 0)
877
 
        self.assertEntryEqual(None, None, None, state, 'a/b', 0)
878
 
        self.assertEntryEqual(None, None, None, state, 'c/d', 0)
879
 
 
880
 
    def test_get_entry_uninitialized(self):
881
 
        """Calling get_entry will load data if it needs to"""
882
 
        state = self.create_dirstate_with_root()
883
 
        try:
884
 
            state.save()
885
 
        finally:
886
 
            state.unlock()
887
 
        del state
888
 
        state = dirstate.DirState.on_file('dirstate')
889
 
        state.lock_read()
890
 
        try:
891
 
            self.assertEqual(dirstate.DirState.NOT_IN_MEMORY,
892
 
                             state._header_state)
893
 
            self.assertEqual(dirstate.DirState.NOT_IN_MEMORY,
894
 
                             state._dirblock_state)
895
 
            self.assertEntryEqual('', '', 'a-root-value', state, '', 0)
896
 
        finally:
897
 
            state.unlock()
898
 
 
899
 
 
900
 
class TestBisect(TestCaseWithTransport):
901
 
    """Test the ability to bisect into the disk format."""
902
 
 
903
 
    def create_basic_dirstate(self):
904
 
        """Create a dirstate with a few files and directories.
905
 
 
906
 
            a
907
 
            b/
908
 
              c
909
 
              d/
910
 
                e
911
 
            f
912
 
        """
913
 
        tree = self.make_branch_and_tree('tree')
914
 
        paths = ['a', 'b/', 'b/c', 'b/d/', 'b/d/e', 'f']
915
 
        file_ids = ['a-id', 'b-id', 'c-id', 'd-id', 'e-id', 'f-id']
916
 
        self.build_tree(['tree/' + p for p in paths])
917
 
        tree.set_root_id('TREE_ROOT')
918
 
        tree.add([p.rstrip('/') for p in paths], file_ids)
919
 
        tree.commit('initial', rev_id='rev-1')
920
 
        revision_id = 'rev-1'
921
 
        # a_packed_stat = dirstate.pack_stat(os.stat('tree/a'))
922
 
        t = self.get_transport().clone('tree')
923
 
        a_text = t.get_bytes('a')
924
 
        a_sha = osutils.sha_string(a_text)
925
 
        a_len = len(a_text)
926
 
        # b_packed_stat = dirstate.pack_stat(os.stat('tree/b'))
927
 
        # c_packed_stat = dirstate.pack_stat(os.stat('tree/b/c'))
928
 
        c_text = t.get_bytes('b/c')
929
 
        c_sha = osutils.sha_string(c_text)
930
 
        c_len = len(c_text)
931
 
        # d_packed_stat = dirstate.pack_stat(os.stat('tree/b/d'))
932
 
        # e_packed_stat = dirstate.pack_stat(os.stat('tree/b/d/e'))
933
 
        e_text = t.get_bytes('b/d/e')
934
 
        e_sha = osutils.sha_string(e_text)
935
 
        e_len = len(e_text)
936
 
        # f_packed_stat = dirstate.pack_stat(os.stat('tree/f'))
937
 
        f_text = t.get_bytes('f')
938
 
        f_sha = osutils.sha_string(f_text)
939
 
        f_len = len(f_text)
940
 
        null_stat = dirstate.DirState.NULLSTAT
941
 
        expected = {
942
 
            '':(('', '', 'TREE_ROOT'), [
943
 
                  ('d', '', 0, False, null_stat),
944
 
                  ('d', '', 0, False, revision_id),
945
 
                ]),
946
 
            'a':(('', 'a', 'a-id'), [
947
 
                   ('f', '', 0, False, null_stat),
948
 
                   ('f', a_sha, a_len, False, revision_id),
949
 
                 ]),
950
 
            'b':(('', 'b', 'b-id'), [
951
 
                  ('d', '', 0, False, null_stat),
952
 
                  ('d', '', 0, False, revision_id),
953
 
                 ]),
954
 
            'b/c':(('b', 'c', 'c-id'), [
955
 
                    ('f', '', 0, False, null_stat),
956
 
                    ('f', c_sha, c_len, False, revision_id),
957
 
                   ]),
958
 
            'b/d':(('b', 'd', 'd-id'), [
959
 
                    ('d', '', 0, False, null_stat),
960
 
                    ('d', '', 0, False, revision_id),
961
 
                   ]),
962
 
            'b/d/e':(('b/d', 'e', 'e-id'), [
963
 
                      ('f', '', 0, False, null_stat),
964
 
                      ('f', e_sha, e_len, False, revision_id),
965
 
                     ]),
966
 
            'f':(('', 'f', 'f-id'), [
967
 
                  ('f', '', 0, False, null_stat),
968
 
                  ('f', f_sha, f_len, False, revision_id),
969
 
                 ]),
970
 
        }
971
 
        state = dirstate.DirState.from_tree(tree, 'dirstate')
972
 
        try:
973
 
            state.save()
974
 
        finally:
975
 
            state.unlock()
976
 
        # Use a different object, to make sure nothing is pre-cached in memory.
977
 
        state = dirstate.DirState.on_file('dirstate')
978
 
        state.lock_read()
979
 
        self.addCleanup(state.unlock)
980
 
        self.assertEqual(dirstate.DirState.NOT_IN_MEMORY,
981
 
                         state._dirblock_state)
982
 
        # This is code is only really tested if we actually have to make more
983
 
        # than one read, so set the page size to something smaller.
984
 
        # We want it to contain about 2.2 records, so that we have a couple
985
 
        # records that we can read per attempt
986
 
        state._bisect_page_size = 200
987
 
        return tree, state, expected
988
 
 
989
 
    def create_duplicated_dirstate(self):
990
 
        """Create a dirstate with a deleted and added entries.
991
 
 
992
 
        This grabs a basic_dirstate, and then removes and re adds every entry
993
 
        with a new file id.
994
 
        """
995
 
        tree, state, expected = self.create_basic_dirstate()
996
 
        # Now we will just remove and add every file so we get an extra entry
997
 
        # per entry. Unversion in reverse order so we handle subdirs
998
 
        tree.unversion(['f-id', 'e-id', 'd-id', 'c-id', 'b-id', 'a-id'])
999
 
        tree.add(['a', 'b', 'b/c', 'b/d', 'b/d/e', 'f'],
1000
 
                 ['a-id2', 'b-id2', 'c-id2', 'd-id2', 'e-id2', 'f-id2'])
1001
 
 
1002
 
        # Update the expected dictionary.
1003
 
        for path in ['a', 'b', 'b/c', 'b/d', 'b/d/e', 'f']:
1004
 
            orig = expected[path]
1005
 
            path2 = path + '2'
1006
 
            # This record was deleted in the current tree
1007
 
            expected[path] = (orig[0], [dirstate.DirState.NULL_PARENT_DETAILS,
1008
 
                                        orig[1][1]])
1009
 
            new_key = (orig[0][0], orig[0][1], orig[0][2]+'2')
1010
 
            # And didn't exist in the basis tree
1011
 
            expected[path2] = (new_key, [orig[1][0],
1012
 
                                         dirstate.DirState.NULL_PARENT_DETAILS])
1013
 
 
1014
 
        # We will replace the 'dirstate' file underneath 'state', but that is
1015
 
        # okay as lock as we unlock 'state' first.
1016
 
        state.unlock()
1017
 
        try:
1018
 
            new_state = dirstate.DirState.from_tree(tree, 'dirstate')
1019
 
            try:
1020
 
                new_state.save()
1021
 
            finally:
1022
 
                new_state.unlock()
1023
 
        finally:
1024
 
            # But we need to leave state in a read-lock because we already have
1025
 
            # a cleanup scheduled
1026
 
            state.lock_read()
1027
 
        return tree, state, expected
1028
 
 
1029
 
    def create_renamed_dirstate(self):
1030
 
        """Create a dirstate with a few internal renames.
1031
 
 
1032
 
        This takes the basic dirstate, and moves the paths around.
1033
 
        """
1034
 
        tree, state, expected = self.create_basic_dirstate()
1035
 
        # Rename a file
1036
 
        tree.rename_one('a', 'b/g')
1037
 
        # And a directory
1038
 
        tree.rename_one('b/d', 'h')
1039
 
 
1040
 
        old_a = expected['a']
1041
 
        expected['a'] = (old_a[0], [('r', 'b/g', 0, False, ''), old_a[1][1]])
1042
 
        expected['b/g'] = (('b', 'g', 'a-id'), [old_a[1][0],
1043
 
                                                ('r', 'a', 0, False, '')])
1044
 
        old_d = expected['b/d']
1045
 
        expected['b/d'] = (old_d[0], [('r', 'h', 0, False, ''), old_d[1][1]])
1046
 
        expected['h'] = (('', 'h', 'd-id'), [old_d[1][0],
1047
 
                                             ('r', 'b/d', 0, False, '')])
1048
 
 
1049
 
        old_e = expected['b/d/e']
1050
 
        expected['b/d/e'] = (old_e[0], [('r', 'h/e', 0, False, ''),
1051
 
                             old_e[1][1]])
1052
 
        expected['h/e'] = (('h', 'e', 'e-id'), [old_e[1][0],
1053
 
                                                ('r', 'b/d/e', 0, False, '')])
1054
 
 
1055
 
        state.unlock()
1056
 
        try:
1057
 
            new_state = dirstate.DirState.from_tree(tree, 'dirstate')
1058
 
            try:
1059
 
                new_state.save()
1060
 
            finally:
1061
 
                new_state.unlock()
1062
 
        finally:
1063
 
            state.lock_read()
1064
 
        return tree, state, expected
1065
 
 
1066
 
    def assertBisect(self, expected_map, map_keys, state, paths):
1067
 
        """Assert that bisecting for paths returns the right result.
1068
 
 
1069
 
        :param expected_map: A map from key => entry value
1070
 
        :param map_keys: The keys to expect for each path
1071
 
        :param state: The DirState object.
1072
 
        :param paths: A list of paths, these will automatically be split into
1073
 
                      (dir, name) tuples, and sorted according to how _bisect
1074
 
                      requires.
1075
 
        """
1076
 
        dir_names = sorted(osutils.split(p) for p in paths)
1077
 
        result = state._bisect(dir_names)
1078
 
        # For now, results are just returned in whatever order we read them.
1079
 
        # We could sort by (dir, name, file_id) or something like that, but in
1080
 
        # the end it would still be fairly arbitrary, and we don't want the
1081
 
        # extra overhead if we can avoid it. So sort everything to make sure
1082
 
        # equality is true
1083
 
        assert len(map_keys) == len(dir_names)
1084
 
        expected = {}
1085
 
        for dir_name, keys in zip(dir_names, map_keys):
1086
 
            if keys is None:
1087
 
                # This should not be present in the output
1088
 
                continue
1089
 
            expected[dir_name] = sorted(expected_map[k] for k in keys)
1090
 
 
1091
 
        for dir_name in result:
1092
 
            result[dir_name].sort()
1093
 
 
1094
 
        self.assertEqual(expected, result)
1095
 
 
1096
 
    def assertBisectDirBlocks(self, expected_map, map_keys, state, paths):
1097
 
        """Assert that bisecting for dirbblocks returns the right result.
1098
 
 
1099
 
        :param expected_map: A map from key => expected values
1100
 
        :param map_keys: A nested list of paths we expect to be returned.
1101
 
            Something like [['a', 'b', 'f'], ['b/c', 'b/d']]
1102
 
        :param state: The DirState object.
1103
 
        :param paths: A list of directories
1104
 
        """
1105
 
        result = state._bisect_dirblocks(paths)
1106
 
        assert len(map_keys) == len(paths)
1107
 
 
1108
 
        expected = {}
1109
 
        for path, keys in zip(paths, map_keys):
1110
 
            if keys is None:
1111
 
                # This should not be present in the output
1112
 
                continue
1113
 
            expected[path] = sorted(expected_map[k] for k in keys)
1114
 
        for path in result:
1115
 
            result[path].sort()
1116
 
 
1117
 
        self.assertEqual(expected, result)
1118
 
 
1119
 
    def assertBisectRecursive(self, expected_map, map_keys, state, paths):
1120
 
        """Assert the return value of a recursive bisection.
1121
 
 
1122
 
        :param expected_map: A map from key => entry value
1123
 
        :param map_keys: A list of paths we expect to be returned.
1124
 
            Something like ['a', 'b', 'f', 'b/d', 'b/d2']
1125
 
        :param state: The DirState object.
1126
 
        :param paths: A list of files and directories. It will be broken up
1127
 
            into (dir, name) pairs and sorted before calling _bisect_recursive.
1128
 
        """
1129
 
        expected = {}
1130
 
        for key in map_keys:
1131
 
            entry = expected_map[key]
1132
 
            dir_name_id, trees_info = entry
1133
 
            expected[dir_name_id] = trees_info
1134
 
 
1135
 
        dir_names = sorted(osutils.split(p) for p in paths)
1136
 
        result = state._bisect_recursive(dir_names)
1137
 
 
1138
 
        self.assertEqual(expected, result)
1139
 
 
1140
 
    def test_bisect_each(self):
1141
 
        """Find a single record using bisect."""
1142
 
        tree, state, expected = self.create_basic_dirstate()
1143
 
 
1144
 
        # Bisect should return the rows for the specified files.
1145
 
        self.assertBisect(expected, [['']], state, [''])
1146
 
        self.assertBisect(expected, [['a']], state, ['a'])
1147
 
        self.assertBisect(expected, [['b']], state, ['b'])
1148
 
        self.assertBisect(expected, [['b/c']], state, ['b/c'])
1149
 
        self.assertBisect(expected, [['b/d']], state, ['b/d'])
1150
 
        self.assertBisect(expected, [['b/d/e']], state, ['b/d/e'])
1151
 
        self.assertBisect(expected, [['f']], state, ['f'])
1152
 
 
1153
 
    def test_bisect_multi(self):
1154
 
        """Bisect can be used to find multiple records at the same time."""
1155
 
        tree, state, expected = self.create_basic_dirstate()
1156
 
        # Bisect should be capable of finding multiple entries at the same time
1157
 
        self.assertBisect(expected, [['a'], ['b'], ['f']],
1158
 
                          state, ['a', 'b', 'f'])
1159
 
        # ('', 'f') sorts before the others
1160
 
        self.assertBisect(expected, [['f'], ['b/d'], ['b/d/e']],
1161
 
                          state, ['b/d', 'b/d/e', 'f'])
1162
 
 
1163
 
    def test_bisect_one_page(self):
1164
 
        """Test bisect when there is only 1 page to read"""
1165
 
        tree, state, expected = self.create_basic_dirstate()
1166
 
        state._bisect_page_size = 5000
1167
 
        self.assertBisect(expected,[['']], state, [''])
1168
 
        self.assertBisect(expected,[['a']], state, ['a'])
1169
 
        self.assertBisect(expected,[['b']], state, ['b'])
1170
 
        self.assertBisect(expected,[['b/c']], state, ['b/c'])
1171
 
        self.assertBisect(expected,[['b/d']], state, ['b/d'])
1172
 
        self.assertBisect(expected,[['b/d/e']], state, ['b/d/e'])
1173
 
        self.assertBisect(expected,[['f']], state, ['f'])
1174
 
        self.assertBisect(expected,[['a'], ['b'], ['f']],
1175
 
                          state, ['a', 'b', 'f'])
1176
 
        # ('', 'f') sorts before the others
1177
 
        self.assertBisect(expected, [['f'], ['b/d'], ['b/d/e']],
1178
 
                          state, ['b/d', 'b/d/e', 'f'])
1179
 
 
1180
 
    def test_bisect_duplicate_paths(self):
1181
 
        """When bisecting for a path, handle multiple entries."""
1182
 
        tree, state, expected = self.create_duplicated_dirstate()
1183
 
 
1184
 
        # Now make sure that both records are properly returned.
1185
 
        self.assertBisect(expected, [['']], state, [''])
1186
 
        self.assertBisect(expected, [['a', 'a2']], state, ['a'])
1187
 
        self.assertBisect(expected, [['b', 'b2']], state, ['b'])
1188
 
        self.assertBisect(expected, [['b/c', 'b/c2']], state, ['b/c'])
1189
 
        self.assertBisect(expected, [['b/d', 'b/d2']], state, ['b/d'])
1190
 
        self.assertBisect(expected, [['b/d/e', 'b/d/e2']],
1191
 
                          state, ['b/d/e'])
1192
 
        self.assertBisect(expected, [['f', 'f2']], state, ['f'])
1193
 
 
1194
 
    def test_bisect_page_size_too_small(self):
1195
 
        """If the page size is too small, we will auto increase it."""
1196
 
        tree, state, expected = self.create_basic_dirstate()
1197
 
        state._bisect_page_size = 50
1198
 
        self.assertBisect(expected, [None], state, ['b/e'])
1199
 
        self.assertBisect(expected, [['a']], state, ['a'])
1200
 
        self.assertBisect(expected, [['b']], state, ['b'])
1201
 
        self.assertBisect(expected, [['b/c']], state, ['b/c'])
1202
 
        self.assertBisect(expected, [['b/d']], state, ['b/d'])
1203
 
        self.assertBisect(expected, [['b/d/e']], state, ['b/d/e'])
1204
 
        self.assertBisect(expected, [['f']], state, ['f'])
1205
 
 
1206
 
    def test_bisect_missing(self):
1207
 
        """Test that bisect return None if it cannot find a path."""
1208
 
        tree, state, expected = self.create_basic_dirstate()
1209
 
        self.assertBisect(expected, [None], state, ['foo'])
1210
 
        self.assertBisect(expected, [None], state, ['b/foo'])
1211
 
        self.assertBisect(expected, [None], state, ['bar/foo'])
1212
 
 
1213
 
        self.assertBisect(expected, [['a'], None, ['b/d']],
1214
 
                          state, ['a', 'foo', 'b/d'])
1215
 
 
1216
 
    def test_bisect_rename(self):
1217
 
        """Check that we find a renamed row."""
1218
 
        tree, state, expected = self.create_renamed_dirstate()
1219
 
 
1220
 
        # Search for the pre and post renamed entries
1221
 
        self.assertBisect(expected, [['a']], state, ['a'])
1222
 
        self.assertBisect(expected, [['b/g']], state, ['b/g'])
1223
 
        self.assertBisect(expected, [['b/d']], state, ['b/d'])
1224
 
        self.assertBisect(expected, [['h']], state, ['h'])
1225
 
 
1226
 
        # What about b/d/e? shouldn't that also get 2 directory entries?
1227
 
        self.assertBisect(expected, [['b/d/e']], state, ['b/d/e'])
1228
 
        self.assertBisect(expected, [['h/e']], state, ['h/e'])
1229
 
 
1230
 
    def test_bisect_dirblocks(self):
1231
 
        tree, state, expected = self.create_duplicated_dirstate()
1232
 
        self.assertBisectDirBlocks(expected,
1233
 
            [['', 'a', 'a2', 'b', 'b2', 'f', 'f2']], state, [''])
1234
 
        self.assertBisectDirBlocks(expected,
1235
 
            [['b/c', 'b/c2', 'b/d', 'b/d2']], state, ['b'])
1236
 
        self.assertBisectDirBlocks(expected,
1237
 
            [['b/d/e', 'b/d/e2']], state, ['b/d'])
1238
 
        self.assertBisectDirBlocks(expected,
1239
 
            [['', 'a', 'a2', 'b', 'b2', 'f', 'f2'],
1240
 
             ['b/c', 'b/c2', 'b/d', 'b/d2'],
1241
 
             ['b/d/e', 'b/d/e2'],
1242
 
            ], state, ['', 'b', 'b/d'])
1243
 
 
1244
 
    def test_bisect_dirblocks_missing(self):
1245
 
        tree, state, expected = self.create_basic_dirstate()
1246
 
        self.assertBisectDirBlocks(expected, [['b/d/e'], None],
1247
 
            state, ['b/d', 'b/e'])
1248
 
        # Files don't show up in this search
1249
 
        self.assertBisectDirBlocks(expected, [None], state, ['a'])
1250
 
        self.assertBisectDirBlocks(expected, [None], state, ['b/c'])
1251
 
        self.assertBisectDirBlocks(expected, [None], state, ['c'])
1252
 
        self.assertBisectDirBlocks(expected, [None], state, ['b/d/e'])
1253
 
        self.assertBisectDirBlocks(expected, [None], state, ['f'])
1254
 
 
1255
 
    def test_bisect_recursive_each(self):
1256
 
        tree, state, expected = self.create_basic_dirstate()
1257
 
        self.assertBisectRecursive(expected, ['a'], state, ['a'])
1258
 
        self.assertBisectRecursive(expected, ['b/c'], state, ['b/c'])
1259
 
        self.assertBisectRecursive(expected, ['b/d/e'], state, ['b/d/e'])
1260
 
        self.assertBisectRecursive(expected, ['b/d', 'b/d/e'],
1261
 
                                   state, ['b/d'])
1262
 
        self.assertBisectRecursive(expected, ['b', 'b/c', 'b/d', 'b/d/e'],
1263
 
                                   state, ['b'])
1264
 
        self.assertBisectRecursive(expected, ['', 'a', 'b', 'f', 'b/c',
1265
 
                                              'b/d', 'b/d/e'],
1266
 
                                   state, [''])
1267
 
 
1268
 
    def test_bisect_recursive_multiple(self):
1269
 
        tree, state, expected = self.create_basic_dirstate()
1270
 
        self.assertBisectRecursive(expected, ['a', 'b/c'], state, ['a', 'b/c'])
1271
 
        self.assertBisectRecursive(expected, ['b/d', 'b/d/e'],
1272
 
                                   state, ['b/d', 'b/d/e'])
1273
 
 
1274
 
    def test_bisect_recursive_missing(self):
1275
 
        tree, state, expected = self.create_basic_dirstate()
1276
 
        self.assertBisectRecursive(expected, [], state, ['d'])
1277
 
        self.assertBisectRecursive(expected, [], state, ['b/e'])
1278
 
        self.assertBisectRecursive(expected, [], state, ['g'])
1279
 
        self.assertBisectRecursive(expected, ['a'], state, ['a', 'g'])
1280
 
 
1281
 
    def test_bisect_recursive_renamed(self):
1282
 
        tree, state, expected = self.create_renamed_dirstate()
1283
 
 
1284
 
        # Looking for either renamed item should find the other
1285
 
        self.assertBisectRecursive(expected, ['a', 'b/g'], state, ['a'])
1286
 
        self.assertBisectRecursive(expected, ['a', 'b/g'], state, ['b/g'])
1287
 
        # Looking in the containing directory should find the rename target,
1288
 
        # and anything in a subdir of the renamed target.
1289
 
        self.assertBisectRecursive(expected, ['a', 'b', 'b/c', 'b/d',
1290
 
                                              'b/d/e', 'b/g', 'h', 'h/e'],
1291
 
                                   state, ['b'])
1292