/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

remove AB1 WorkingTree and experimental-knit3

Show diffs side-by-side

added added

removed removed

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