/brz/remove-bazaar

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

« back to all changes in this revision

Viewing changes to bzrlib/tests/test_dirstate.py

  • Committer: John Arbash Meinel
  • Date: 2007-03-02 01:27:53 UTC
  • mto: (2255.7.86 dirstate)
  • mto: This revision was merged to the branch mainline in revision 2322.
  • Revision ID: john@arbash-meinel.com-20070302012753-5jwb15csi4j2mi4w
Fix a small bug when we have a symlink that does not need to be re-read.

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