/brz/remove-bazaar

To get this branch, use:
bzr branch http://gegoxaren.bato24.eu/bzr/brz/remove-bazaar

« back to all changes in this revision

Viewing changes to bzrlib/tests/test_dirstate.py

  • Committer: John Arbash Meinel
  • Date: 2007-02-26 15:27:17 UTC
  • mto: (2255.11.3 dirstate)
  • mto: This revision was merged to the branch mainline in revision 2322.
  • Revision ID: john@arbash-meinel.com-20070226152717-3so5kz4v7wz7dmpk
Lock the tree when using a commit builder.

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