/brz/remove-bazaar

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

« back to all changes in this revision

Viewing changes to bzrlib/tests/test_dirstate.py

Fix up inter_changes with dirstate both C and python.

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