~bzr-pqm/bzr/bzr.dev

« back to all changes in this revision

Viewing changes to bzrlib/tests/test_dirstate.py

  • Committer: Andrew Bennetts
  • Date: 2007-03-28 07:08:42 UTC
  • mfrom: (2380 +trunk)
  • mto: (2018.5.146 hpss)
  • mto: This revision was merged to the branch mainline in revision 2414.
  • Revision ID: andrew.bennetts@canonical.com-20070328070842-r843houy668oxb9o
Merge from bzr.dev.

Show diffs side-by-side

added added

removed removed

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