/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

1st cut merge of bzr.dev r3907

Show diffs side-by-side

added added

removed removed

Lines of Context:
 
1
# Copyright (C) 2006, 2007 Canonical Ltd
 
2
#
 
3
# This program is free software; you can redistribute it and/or modify
 
4
# it under the terms of the GNU General Public License as published by
 
5
# the Free Software Foundation; either version 2 of the License, or
 
6
# (at your option) any later version.
 
7
#
 
8
# This program is distributed in the hope that it will be useful,
 
9
# but WITHOUT ANY WARRANTY; without even the implied warranty of
 
10
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
 
11
# GNU General Public License for more details.
 
12
#
 
13
# You should have received a copy of the GNU General Public License
 
14
# along with this program; if not, write to the Free Software
 
15
# Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA  02111-1307  USA
 
16
 
 
17
"""Tests of the dirstate functionality being built for WorkingTreeFormat4."""
 
18
 
 
19
import bisect
 
20
import os
 
21
 
 
22
from bzrlib import (
 
23
    dirstate,
 
24
    errors,
 
25
    filters,
 
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 size should be 0 (default)
 
568
            self.assertEqual(0, entry[1][0][2])
 
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
            # Change the file length
 
575
            self.build_tree_contents([('a-file', 'shorter')])
 
576
            sha1sum = dirstate.update_entry(state, entry, 'a-file',
 
577
                os.lstat('a-file'))
 
578
            # new file, no cached sha:
 
579
            self.assertEqual(None, sha1sum)
 
580
 
 
581
            # The dirblock has been updated
 
582
            self.assertEqual(7, entry[1][0][2])
 
583
            self.assertEqual(dirstate.DirState.IN_MEMORY_MODIFIED,
 
584
                             state._dirblock_state)
 
585
 
 
586
            del entry
 
587
            # Now, since we are the only one holding a lock, we should be able
 
588
            # to save and have it written to disk
 
589
            state.save()
 
590
        finally:
 
591
            state.unlock()
 
592
 
 
593
        # Re-open the file, and ensure that the state has been updated.
 
594
        state = dirstate.DirState.on_file('dirstate')
 
595
        state.lock_read()
 
596
        try:
 
597
            entry = state._get_entry(0, path_utf8='a-file')
 
598
            self.assertEqual(7, entry[1][0][2])
 
599
        finally:
 
600
            state.unlock()
 
601
 
 
602
    def test_save_fails_quietly_if_locked(self):
 
603
        """If dirstate is locked, save will fail without complaining."""
 
604
        self.build_tree(['a-file'])
 
605
        state = dirstate.DirState.initialize('dirstate')
 
606
        try:
 
607
            # No stat and no sha1 sum.
 
608
            state.add('a-file', 'a-file-id', 'file', None, '')
 
609
            state.save()
 
610
        finally:
 
611
            state.unlock()
 
612
 
 
613
        state = dirstate.DirState.on_file('dirstate')
 
614
        state.lock_read()
 
615
        try:
 
616
            entry = state._get_entry(0, path_utf8='a-file')
 
617
            sha1sum = dirstate.update_entry(state, entry, 'a-file',
 
618
                os.lstat('a-file'))
 
619
            # No sha - too new
 
620
            self.assertEqual(None, sha1sum)
 
621
            self.assertEqual(dirstate.DirState.IN_MEMORY_MODIFIED,
 
622
                             state._dirblock_state)
 
623
 
 
624
            # Now, before we try to save, grab another dirstate, and take out a
 
625
            # read lock.
 
626
            # TODO: jam 20070315 Ideally this would be locked by another
 
627
            #       process. To make sure the file is really OS locked.
 
628
            state2 = dirstate.DirState.on_file('dirstate')
 
629
            state2.lock_read()
 
630
            try:
 
631
                # This won't actually write anything, because it couldn't grab
 
632
                # a write lock. But it shouldn't raise an error, either.
 
633
                # TODO: jam 20070315 We should probably distinguish between
 
634
                #       being dirty because of 'update_entry'. And dirty
 
635
                #       because of real modification. So that save() *does*
 
636
                #       raise a real error if it fails when we have real
 
637
                #       modifications.
 
638
                state.save()
 
639
            finally:
 
640
                state2.unlock()
 
641
        finally:
 
642
            state.unlock()
 
643
        
 
644
        # The file on disk should not be modified.
 
645
        state = dirstate.DirState.on_file('dirstate')
 
646
        state.lock_read()
 
647
        try:
 
648
            entry = state._get_entry(0, path_utf8='a-file')
 
649
            self.assertEqual('', entry[1][0][1])
 
650
        finally:
 
651
            state.unlock()
 
652
 
 
653
    def test_save_refuses_if_changes_aborted(self):
 
654
        self.build_tree(['a-file', 'a-dir/'])
 
655
        state = dirstate.DirState.initialize('dirstate')
 
656
        try:
 
657
            # No stat and no sha1 sum.
 
658
            state.add('a-file', 'a-file-id', 'file', None, '')
 
659
            state.save()
 
660
        finally:
 
661
            state.unlock()
 
662
 
 
663
        # The dirstate should include TREE_ROOT and 'a-file' and nothing else
 
664
        expected_blocks = [
 
665
            ('', [(('', '', 'TREE_ROOT'),
 
666
                   [('d', '', 0, False, dirstate.DirState.NULLSTAT)])]),
 
667
            ('', [(('', 'a-file', 'a-file-id'),
 
668
                   [('f', '', 0, False, dirstate.DirState.NULLSTAT)])]),
 
669
        ]
 
670
 
 
671
        state = dirstate.DirState.on_file('dirstate')
 
672
        state.lock_write()
 
673
        try:
 
674
            state._read_dirblocks_if_needed()
 
675
            self.assertEqual(expected_blocks, state._dirblocks)
 
676
 
 
677
            # Now modify the state, but mark it as inconsistent
 
678
            state.add('a-dir', 'a-dir-id', 'directory', None, '')
 
679
            state._changes_aborted = True
 
680
            state.save()
 
681
        finally:
 
682
            state.unlock()
 
683
 
 
684
        state = dirstate.DirState.on_file('dirstate')
 
685
        state.lock_read()
 
686
        try:
 
687
            state._read_dirblocks_if_needed()
 
688
            self.assertEqual(expected_blocks, state._dirblocks)
 
689
        finally:
 
690
            state.unlock()
 
691
 
 
692
 
 
693
class TestDirStateInitialize(TestCaseWithDirState):
 
694
 
 
695
    def test_initialize(self):
 
696
        expected_result = ([], [
 
697
            (('', '', 'TREE_ROOT'), # common details
 
698
             [('d', '', 0, False, dirstate.DirState.NULLSTAT), # current tree
 
699
             ])
 
700
            ])
 
701
        state = dirstate.DirState.initialize('dirstate')
 
702
        try:
 
703
            self.assertIsInstance(state, dirstate.DirState)
 
704
            lines = state.get_lines()
 
705
        finally:
 
706
            state.unlock()
 
707
        # On win32 you can't read from a locked file, even within the same
 
708
        # process. So we have to unlock and release before we check the file
 
709
        # contents.
 
710
        self.assertFileEqual(''.join(lines), 'dirstate')
 
711
        state.lock_read() # check_state_with_reopen will unlock
 
712
        self.check_state_with_reopen(expected_result, state)
 
713
 
 
714
 
 
715
class TestDirStateManipulations(TestCaseWithDirState):
 
716
 
 
717
    def test_set_state_from_inventory_no_content_no_parents(self):
 
718
        # setting the current inventory is a slow but important api to support.
 
719
        tree1 = self.make_branch_and_memory_tree('tree1')
 
720
        tree1.lock_write()
 
721
        try:
 
722
            tree1.add('')
 
723
            revid1 = tree1.commit('foo').encode('utf8')
 
724
            root_id = tree1.get_root_id()
 
725
            inv = tree1.inventory
 
726
        finally:
 
727
            tree1.unlock()
 
728
        expected_result = [], [
 
729
            (('', '', root_id), [
 
730
             ('d', '', 0, False, dirstate.DirState.NULLSTAT)])]
 
731
        state = dirstate.DirState.initialize('dirstate')
 
732
        try:
 
733
            state.set_state_from_inventory(inv)
 
734
            self.assertEqual(dirstate.DirState.IN_MEMORY_UNMODIFIED,
 
735
                             state._header_state)
 
736
            self.assertEqual(dirstate.DirState.IN_MEMORY_MODIFIED,
 
737
                             state._dirblock_state)
 
738
        except:
 
739
            state.unlock()
 
740
            raise
 
741
        else:
 
742
            # This will unlock it
 
743
            self.check_state_with_reopen(expected_result, state)
 
744
 
 
745
    def test_set_state_from_inventory_preserves_hashcache(self):
 
746
        # https://bugs.launchpad.net/bzr/+bug/146176
 
747
        # set_state_from_inventory should preserve the stat and hash value for
 
748
        # workingtree files that are not changed by the inventory.
 
749
       
 
750
        tree = self.make_branch_and_tree('.')
 
751
        # depends on the default format using dirstate...
 
752
        tree.lock_write()
 
753
        try:
 
754
            # make a dirstate with some valid hashcache data 
 
755
            # file on disk, but that's not needed for this test
 
756
            foo_contents = 'contents of foo'
 
757
            self.build_tree_contents([('foo', foo_contents)])
 
758
            tree.add('foo', 'foo-id')
 
759
 
 
760
            foo_stat = os.stat('foo')
 
761
            foo_packed = dirstate.pack_stat(foo_stat)
 
762
            foo_sha = osutils.sha_string(foo_contents)
 
763
            foo_size = len(foo_contents)
 
764
 
 
765
            # should not be cached yet, because the file's too fresh
 
766
            self.assertEqual(
 
767
                (('', 'foo', 'foo-id',),
 
768
                 [('f', '', 0, False, dirstate.DirState.NULLSTAT)]),
 
769
                tree._dirstate._get_entry(0, 'foo-id'))
 
770
            # poke in some hashcache information - it wouldn't normally be
 
771
            # stored because it's too fresh
 
772
            tree._dirstate.update_minimal(
 
773
                ('', 'foo', 'foo-id'),
 
774
                'f', False, foo_sha, foo_packed, foo_size, 'foo')
 
775
            # now should be cached
 
776
            self.assertEqual(
 
777
                (('', 'foo', 'foo-id',),
 
778
                 [('f', foo_sha, foo_size, False, foo_packed)]),
 
779
                tree._dirstate._get_entry(0, 'foo-id'))
 
780
           
 
781
            # extract the inventory, and add something to it
 
782
            inv = tree._get_inventory()
 
783
            # should see the file we poked in...
 
784
            self.assertTrue(inv.has_id('foo-id'))
 
785
            self.assertTrue(inv.has_filename('foo'))
 
786
            inv.add_path('bar', 'file', 'bar-id')
 
787
            tree._dirstate._validate()
 
788
            # this used to cause it to lose its hashcache
 
789
            tree._dirstate.set_state_from_inventory(inv)
 
790
            tree._dirstate._validate()
 
791
        finally:
 
792
            tree.unlock()
 
793
 
 
794
        tree.lock_read()
 
795
        try:
 
796
            # now check that the state still has the original hashcache value
 
797
            state = tree._dirstate
 
798
            state._validate()
 
799
            foo_tuple = state._get_entry(0, path_utf8='foo')
 
800
            self.assertEqual(
 
801
                (('', 'foo', 'foo-id',),
 
802
                 [('f', foo_sha, len(foo_contents), False,
 
803
                   dirstate.pack_stat(foo_stat))]),
 
804
                foo_tuple)
 
805
        finally:
 
806
            tree.unlock()
 
807
 
 
808
 
 
809
    def test_set_state_from_inventory_mixed_paths(self):
 
810
        tree1 = self.make_branch_and_tree('tree1')
 
811
        self.build_tree(['tree1/a/', 'tree1/a/b/', 'tree1/a-b/',
 
812
                         'tree1/a/b/foo', 'tree1/a-b/bar'])
 
813
        tree1.lock_write()
 
814
        try:
 
815
            tree1.add(['a', 'a/b', 'a-b', 'a/b/foo', 'a-b/bar'],
 
816
                      ['a-id', 'b-id', 'a-b-id', 'foo-id', 'bar-id'])
 
817
            tree1.commit('rev1', rev_id='rev1')
 
818
            root_id = tree1.get_root_id()
 
819
            inv = tree1.inventory
 
820
        finally:
 
821
            tree1.unlock()
 
822
        expected_result1 = [('', '', root_id, 'd'),
 
823
                            ('', 'a', 'a-id', 'd'),
 
824
                            ('', 'a-b', 'a-b-id', 'd'),
 
825
                            ('a', 'b', 'b-id', 'd'),
 
826
                            ('a/b', 'foo', 'foo-id', 'f'),
 
827
                            ('a-b', 'bar', 'bar-id', 'f'),
 
828
                           ]
 
829
        expected_result2 = [('', '', root_id, 'd'),
 
830
                            ('', 'a', 'a-id', 'd'),
 
831
                            ('', 'a-b', 'a-b-id', 'd'),
 
832
                            ('a-b', 'bar', 'bar-id', 'f'),
 
833
                           ]
 
834
        state = dirstate.DirState.initialize('dirstate')
 
835
        try:
 
836
            state.set_state_from_inventory(inv)
 
837
            values = []
 
838
            for entry in state._iter_entries():
 
839
                values.append(entry[0] + entry[1][0][:1])
 
840
            self.assertEqual(expected_result1, values)
 
841
            del inv['b-id']
 
842
            state.set_state_from_inventory(inv)
 
843
            values = []
 
844
            for entry in state._iter_entries():
 
845
                values.append(entry[0] + entry[1][0][:1])
 
846
            self.assertEqual(expected_result2, values)
 
847
        finally:
 
848
            state.unlock()
 
849
 
 
850
    def test_set_path_id_no_parents(self):
 
851
        """The id of a path can be changed trivally with no parents."""
 
852
        state = dirstate.DirState.initialize('dirstate')
 
853
        try:
 
854
            # check precondition to be sure the state does change appropriately.
 
855
            self.assertEqual(
 
856
                [(('', '', 'TREE_ROOT'), [('d', '', 0, False,
 
857
                   'xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx')])],
 
858
                list(state._iter_entries()))
 
859
            state.set_path_id('', 'foobarbaz')
 
860
            expected_rows = [
 
861
                (('', '', 'foobarbaz'), [('d', '', 0, False,
 
862
                   'xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx')])]
 
863
            self.assertEqual(expected_rows, list(state._iter_entries()))
 
864
            # should work across save too
 
865
            state.save()
 
866
        finally:
 
867
            state.unlock()
 
868
        state = dirstate.DirState.on_file('dirstate')
 
869
        state.lock_read()
 
870
        try:
 
871
            state._validate()
 
872
            self.assertEqual(expected_rows, list(state._iter_entries()))
 
873
        finally:
 
874
            state.unlock()
 
875
 
 
876
    def test_set_path_id_with_parents(self):
 
877
        """Set the root file id in a dirstate with parents"""
 
878
        mt = self.make_branch_and_tree('mt')
 
879
        # in case the default tree format uses a different root id
 
880
        mt.set_root_id('TREE_ROOT')
 
881
        mt.commit('foo', rev_id='parent-revid')
 
882
        rt = mt.branch.repository.revision_tree('parent-revid')
 
883
        state = dirstate.DirState.initialize('dirstate')
 
884
        state._validate()
 
885
        try:
 
886
            state.set_parent_trees([('parent-revid', rt)], ghosts=[])
 
887
            state.set_path_id('', 'foobarbaz')
 
888
            state._validate()
 
889
            # now see that it is what we expected
 
890
            expected_rows = [
 
891
                (('', '', 'TREE_ROOT'),
 
892
                    [('a', '', 0, False, ''),
 
893
                     ('d', '', 0, False, 'parent-revid'),
 
894
                     ]),
 
895
                (('', '', 'foobarbaz'),
 
896
                    [('d', '', 0, False, ''),
 
897
                     ('a', '', 0, False, ''),
 
898
                     ]),
 
899
                ]
 
900
            state._validate()
 
901
            self.assertEqual(expected_rows, list(state._iter_entries()))
 
902
            # should work across save too
 
903
            state.save()
 
904
        finally:
 
905
            state.unlock()
 
906
        # now flush & check we get the same
 
907
        state = dirstate.DirState.on_file('dirstate')
 
908
        state.lock_read()
 
909
        try:
 
910
            state._validate()
 
911
            self.assertEqual(expected_rows, list(state._iter_entries()))
 
912
        finally:
 
913
            state.unlock()
 
914
        # now change within an existing file-backed state
 
915
        state.lock_write()
 
916
        try:
 
917
            state._validate()
 
918
            state.set_path_id('', 'tree-root-2')
 
919
            state._validate()
 
920
        finally:
 
921
            state.unlock()
 
922
 
 
923
 
 
924
    def test_set_parent_trees_no_content(self):
 
925
        # set_parent_trees is a slow but important api to support.
 
926
        tree1 = self.make_branch_and_memory_tree('tree1')
 
927
        tree1.lock_write()
 
928
        try:
 
929
            tree1.add('')
 
930
            revid1 = tree1.commit('foo')
 
931
        finally:
 
932
            tree1.unlock()
 
933
        branch2 = tree1.branch.bzrdir.clone('tree2').open_branch()
 
934
        tree2 = MemoryTree.create_on_branch(branch2)
 
935
        tree2.lock_write()
 
936
        try:
 
937
            revid2 = tree2.commit('foo')
 
938
            root_id = tree2.get_root_id()
 
939
        finally:
 
940
            tree2.unlock()
 
941
        state = dirstate.DirState.initialize('dirstate')
 
942
        try:
 
943
            state.set_path_id('', root_id)
 
944
            state.set_parent_trees(
 
945
                ((revid1, tree1.branch.repository.revision_tree(revid1)),
 
946
                 (revid2, tree2.branch.repository.revision_tree(revid2)),
 
947
                 ('ghost-rev', None)),
 
948
                ['ghost-rev'])
 
949
            # check we can reopen and use the dirstate after setting parent
 
950
            # trees.
 
951
            state._validate()
 
952
            state.save()
 
953
            state._validate()
 
954
        finally:
 
955
            state.unlock()
 
956
        state = dirstate.DirState.on_file('dirstate')
 
957
        state.lock_write()
 
958
        try:
 
959
            self.assertEqual([revid1, revid2, 'ghost-rev'],
 
960
                             state.get_parent_ids())
 
961
            # iterating the entire state ensures that the state is parsable.
 
962
            list(state._iter_entries())
 
963
            # be sure that it sets not appends - change it
 
964
            state.set_parent_trees(
 
965
                ((revid1, tree1.branch.repository.revision_tree(revid1)),
 
966
                 ('ghost-rev', None)),
 
967
                ['ghost-rev'])
 
968
            # and now put it back.
 
969
            state.set_parent_trees(
 
970
                ((revid1, tree1.branch.repository.revision_tree(revid1)),
 
971
                 (revid2, tree2.branch.repository.revision_tree(revid2)),
 
972
                 ('ghost-rev', tree2.branch.repository.revision_tree(
 
973
                                   _mod_revision.NULL_REVISION))),
 
974
                ['ghost-rev'])
 
975
            self.assertEqual([revid1, revid2, 'ghost-rev'],
 
976
                             state.get_parent_ids())
 
977
            # the ghost should be recorded as such by set_parent_trees.
 
978
            self.assertEqual(['ghost-rev'], state.get_ghosts())
 
979
            self.assertEqual(
 
980
                [(('', '', root_id), [
 
981
                  ('d', '', 0, False, dirstate.DirState.NULLSTAT),
 
982
                  ('d', '', 0, False, revid1),
 
983
                  ('d', '', 0, False, revid2)
 
984
                  ])],
 
985
                list(state._iter_entries()))
 
986
        finally:
 
987
            state.unlock()
 
988
 
 
989
    def test_set_parent_trees_file_missing_from_tree(self):
 
990
        # Adding a parent tree may reference files not in the current state.
 
991
        # they should get listed just once by id, even if they are in two
 
992
        # separate trees.
 
993
        # set_parent_trees is a slow but important api to support.
 
994
        tree1 = self.make_branch_and_memory_tree('tree1')
 
995
        tree1.lock_write()
 
996
        try:
 
997
            tree1.add('')
 
998
            tree1.add(['a file'], ['file-id'], ['file'])
 
999
            tree1.put_file_bytes_non_atomic('file-id', 'file-content')
 
1000
            revid1 = tree1.commit('foo')
 
1001
        finally:
 
1002
            tree1.unlock()
 
1003
        branch2 = tree1.branch.bzrdir.clone('tree2').open_branch()
 
1004
        tree2 = MemoryTree.create_on_branch(branch2)
 
1005
        tree2.lock_write()
 
1006
        try:
 
1007
            tree2.put_file_bytes_non_atomic('file-id', 'new file-content')
 
1008
            revid2 = tree2.commit('foo')
 
1009
            root_id = tree2.get_root_id()
 
1010
        finally:
 
1011
            tree2.unlock()
 
1012
        # check the layout in memory
 
1013
        expected_result = [revid1.encode('utf8'), revid2.encode('utf8')], [
 
1014
            (('', '', root_id), [
 
1015
             ('d', '', 0, False, dirstate.DirState.NULLSTAT),
 
1016
             ('d', '', 0, False, revid1.encode('utf8')),
 
1017
             ('d', '', 0, False, revid2.encode('utf8'))
 
1018
             ]),
 
1019
            (('', 'a file', 'file-id'), [
 
1020
             ('a', '', 0, False, ''),
 
1021
             ('f', '2439573625385400f2a669657a7db6ae7515d371', 12, False,
 
1022
              revid1.encode('utf8')),
 
1023
             ('f', '542e57dc1cda4af37cb8e55ec07ce60364bb3c7d', 16, False,
 
1024
              revid2.encode('utf8'))
 
1025
             ])
 
1026
            ]
 
1027
        state = dirstate.DirState.initialize('dirstate')
 
1028
        try:
 
1029
            state.set_path_id('', root_id)
 
1030
            state.set_parent_trees(
 
1031
                ((revid1, tree1.branch.repository.revision_tree(revid1)),
 
1032
                 (revid2, tree2.branch.repository.revision_tree(revid2)),
 
1033
                 ), [])
 
1034
        except:
 
1035
            state.unlock()
 
1036
            raise
 
1037
        else:
 
1038
            # check_state_with_reopen will unlock
 
1039
            self.check_state_with_reopen(expected_result, state)
 
1040
 
 
1041
    ### add a path via _set_data - so we dont need delta work, just
 
1042
    # raw data in, and ensure that it comes out via get_lines happily.
 
1043
 
 
1044
    def test_add_path_to_root_no_parents_all_data(self):
 
1045
        # The most trivial addition of a path is when there are no parents and
 
1046
        # its in the root and all data about the file is supplied
 
1047
        self.build_tree(['a file'])
 
1048
        stat = os.lstat('a file')
 
1049
        # the 1*20 is the sha1 pretend value.
 
1050
        state = dirstate.DirState.initialize('dirstate')
 
1051
        expected_entries = [
 
1052
            (('', '', 'TREE_ROOT'), [
 
1053
             ('d', '', 0, False, dirstate.DirState.NULLSTAT), # current tree
 
1054
             ]),
 
1055
            (('', 'a file', 'a-file-id'), [
 
1056
             ('f', '1'*20, 19, False, dirstate.pack_stat(stat)), # current tree
 
1057
             ]),
 
1058
            ]
 
1059
        try:
 
1060
            state.add('a file', 'a-file-id', 'file', stat, '1'*20)
 
1061
            # having added it, it should be in the output of iter_entries.
 
1062
            self.assertEqual(expected_entries, list(state._iter_entries()))
 
1063
            # saving and reloading should not affect this.
 
1064
            state.save()
 
1065
        finally:
 
1066
            state.unlock()
 
1067
        state = dirstate.DirState.on_file('dirstate')
 
1068
        state.lock_read()
 
1069
        try:
 
1070
            self.assertEqual(expected_entries, list(state._iter_entries()))
 
1071
        finally:
 
1072
            state.unlock()
 
1073
 
 
1074
    def test_add_path_to_unversioned_directory(self):
 
1075
        """Adding a path to an unversioned directory should error.
 
1076
 
 
1077
        This is a duplicate of TestWorkingTree.test_add_in_unversioned,
 
1078
        once dirstate is stable and if it is merged with WorkingTree3, consider
 
1079
        removing this copy of the test.
 
1080
        """
 
1081
        self.build_tree(['unversioned/', 'unversioned/a file'])
 
1082
        state = dirstate.DirState.initialize('dirstate')
 
1083
        try:
 
1084
            self.assertRaises(errors.NotVersionedError, state.add,
 
1085
                'unversioned/a file', 'a-file-id', 'file', None, None)
 
1086
        finally:
 
1087
            state.unlock()
 
1088
 
 
1089
    def test_add_directory_to_root_no_parents_all_data(self):
 
1090
        # The most trivial addition of a dir is when there are no parents and
 
1091
        # its in the root and all data about the file is supplied
 
1092
        self.build_tree(['a dir/'])
 
1093
        stat = os.lstat('a dir')
 
1094
        expected_entries = [
 
1095
            (('', '', 'TREE_ROOT'), [
 
1096
             ('d', '', 0, False, dirstate.DirState.NULLSTAT), # current tree
 
1097
             ]),
 
1098
            (('', 'a dir', 'a dir id'), [
 
1099
             ('d', '', 0, False, dirstate.pack_stat(stat)), # current tree
 
1100
             ]),
 
1101
            ]
 
1102
        state = dirstate.DirState.initialize('dirstate')
 
1103
        try:
 
1104
            state.add('a dir', 'a dir id', 'directory', stat, None)
 
1105
            # having added it, it should be in the output of iter_entries.
 
1106
            self.assertEqual(expected_entries, list(state._iter_entries()))
 
1107
            # saving and reloading should not affect this.
 
1108
            state.save()
 
1109
        finally:
 
1110
            state.unlock()
 
1111
        state = dirstate.DirState.on_file('dirstate')
 
1112
        state.lock_read()
 
1113
        state._validate()
 
1114
        try:
 
1115
            self.assertEqual(expected_entries, list(state._iter_entries()))
 
1116
        finally:
 
1117
            state.unlock()
 
1118
 
 
1119
    def test_add_symlink_to_root_no_parents_all_data(self):
 
1120
        # The most trivial addition of a symlink when there are no parents and
 
1121
        # its in the root and all data about the file is supplied
 
1122
        # bzr doesn't support fake symlinks on windows, yet.
 
1123
        self.requireFeature(SymlinkFeature)
 
1124
        os.symlink('target', 'a link')
 
1125
        stat = os.lstat('a link')
 
1126
        expected_entries = [
 
1127
            (('', '', 'TREE_ROOT'), [
 
1128
             ('d', '', 0, False, dirstate.DirState.NULLSTAT), # current tree
 
1129
             ]),
 
1130
            (('', 'a link', 'a link id'), [
 
1131
             ('l', 'target', 6, False, dirstate.pack_stat(stat)), # current tree
 
1132
             ]),
 
1133
            ]
 
1134
        state = dirstate.DirState.initialize('dirstate')
 
1135
        try:
 
1136
            state.add('a link', 'a link id', 'symlink', stat, 'target')
 
1137
            # having added it, it should be in the output of iter_entries.
 
1138
            self.assertEqual(expected_entries, list(state._iter_entries()))
 
1139
            # saving and reloading should not affect this.
 
1140
            state.save()
 
1141
        finally:
 
1142
            state.unlock()
 
1143
        state = dirstate.DirState.on_file('dirstate')
 
1144
        state.lock_read()
 
1145
        try:
 
1146
            self.assertEqual(expected_entries, list(state._iter_entries()))
 
1147
        finally:
 
1148
            state.unlock()
 
1149
 
 
1150
    def test_add_directory_and_child_no_parents_all_data(self):
 
1151
        # after adding a directory, we should be able to add children to it.
 
1152
        self.build_tree(['a dir/', 'a dir/a file'])
 
1153
        dirstat = os.lstat('a dir')
 
1154
        filestat = os.lstat('a dir/a file')
 
1155
        expected_entries = [
 
1156
            (('', '', 'TREE_ROOT'), [
 
1157
             ('d', '', 0, False, dirstate.DirState.NULLSTAT), # current tree
 
1158
             ]),
 
1159
            (('', 'a dir', 'a dir id'), [
 
1160
             ('d', '', 0, False, dirstate.pack_stat(dirstat)), # current tree
 
1161
             ]),
 
1162
            (('a dir', 'a file', 'a-file-id'), [
 
1163
             ('f', '1'*20, 25, False,
 
1164
              dirstate.pack_stat(filestat)), # current tree details
 
1165
             ]),
 
1166
            ]
 
1167
        state = dirstate.DirState.initialize('dirstate')
 
1168
        try:
 
1169
            state.add('a dir', 'a dir id', 'directory', dirstat, None)
 
1170
            state.add('a dir/a file', 'a-file-id', 'file', filestat, '1'*20)
 
1171
            # added it, it should be in the output of iter_entries.
 
1172
            self.assertEqual(expected_entries, list(state._iter_entries()))
 
1173
            # saving and reloading should not affect this.
 
1174
            state.save()
 
1175
        finally:
 
1176
            state.unlock()
 
1177
        state = dirstate.DirState.on_file('dirstate')
 
1178
        state.lock_read()
 
1179
        try:
 
1180
            self.assertEqual(expected_entries, list(state._iter_entries()))
 
1181
        finally:
 
1182
            state.unlock()
 
1183
 
 
1184
    def test_add_tree_reference(self):
 
1185
        # make a dirstate and add a tree reference
 
1186
        state = dirstate.DirState.initialize('dirstate')
 
1187
        expected_entry = (
 
1188
            ('', 'subdir', 'subdir-id'),
 
1189
            [('t', 'subtree-123123', 0, False,
 
1190
              'xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx')],
 
1191
            )
 
1192
        try:
 
1193
            state.add('subdir', 'subdir-id', 'tree-reference', None, 'subtree-123123')
 
1194
            entry = state._get_entry(0, 'subdir-id', 'subdir')
 
1195
            self.assertEqual(entry, expected_entry)
 
1196
            state._validate()
 
1197
            state.save()
 
1198
        finally:
 
1199
            state.unlock()
 
1200
        # now check we can read it back
 
1201
        state.lock_read()
 
1202
        state._validate()
 
1203
        try:
 
1204
            entry2 = state._get_entry(0, 'subdir-id', 'subdir')
 
1205
            self.assertEqual(entry, entry2)
 
1206
            self.assertEqual(entry, expected_entry)
 
1207
            # and lookup by id should work too
 
1208
            entry2 = state._get_entry(0, fileid_utf8='subdir-id')
 
1209
            self.assertEqual(entry, expected_entry)
 
1210
        finally:
 
1211
            state.unlock()
 
1212
 
 
1213
    def test_add_forbidden_names(self):
 
1214
        state = dirstate.DirState.initialize('dirstate')
 
1215
        self.addCleanup(state.unlock)
 
1216
        self.assertRaises(errors.BzrError,
 
1217
            state.add, '.', 'ass-id', 'directory', None, None)
 
1218
        self.assertRaises(errors.BzrError,
 
1219
            state.add, '..', 'ass-id', 'directory', None, None)
 
1220
 
 
1221
 
 
1222
class TestGetLines(TestCaseWithDirState):
 
1223
 
 
1224
    def test_get_line_with_2_rows(self):
 
1225
        state = self.create_dirstate_with_root_and_subdir()
 
1226
        try:
 
1227
            self.assertEqual(['#bazaar dirstate flat format 3\n',
 
1228
                'crc32: 41262208\n',
 
1229
                'num_entries: 2\n',
 
1230
                '0\x00\n\x00'
 
1231
                '0\x00\n\x00'
 
1232
                '\x00\x00a-root-value\x00'
 
1233
                'd\x00\x000\x00n\x00AAAAREUHaIpFB2iKAAADAQAtkqUAAIGk\x00\n\x00'
 
1234
                '\x00subdir\x00subdir-id\x00'
 
1235
                'd\x00\x000\x00n\x00AAAAREUHaIpFB2iKAAADAQAtkqUAAIGk\x00\n\x00'
 
1236
                ], state.get_lines())
 
1237
        finally:
 
1238
            state.unlock()
 
1239
 
 
1240
    def test_entry_to_line(self):
 
1241
        state = self.create_dirstate_with_root()
 
1242
        try:
 
1243
            self.assertEqual(
 
1244
                '\x00\x00a-root-value\x00d\x00\x000\x00n'
 
1245
                '\x00AAAAREUHaIpFB2iKAAADAQAtkqUAAIGk',
 
1246
                state._entry_to_line(state._dirblocks[0][1][0]))
 
1247
        finally:
 
1248
            state.unlock()
 
1249
 
 
1250
    def test_entry_to_line_with_parent(self):
 
1251
        packed_stat = 'AAAAREUHaIpFB2iKAAADAQAtkqUAAIGk'
 
1252
        root_entry = ('', '', 'a-root-value'), [
 
1253
            ('d', '', 0, False, packed_stat), # current tree details
 
1254
             # first: a pointer to the current location
 
1255
            ('a', 'dirname/basename', 0, False, ''),
 
1256
            ]
 
1257
        state = dirstate.DirState.initialize('dirstate')
 
1258
        try:
 
1259
            self.assertEqual(
 
1260
                '\x00\x00a-root-value\x00'
 
1261
                'd\x00\x000\x00n\x00AAAAREUHaIpFB2iKAAADAQAtkqUAAIGk\x00'
 
1262
                'a\x00dirname/basename\x000\x00n\x00',
 
1263
                state._entry_to_line(root_entry))
 
1264
        finally:
 
1265
            state.unlock()
 
1266
 
 
1267
    def test_entry_to_line_with_two_parents_at_different_paths(self):
 
1268
        # / in the tree, at / in one parent and /dirname/basename in the other.
 
1269
        packed_stat = 'AAAAREUHaIpFB2iKAAADAQAtkqUAAIGk'
 
1270
        root_entry = ('', '', 'a-root-value'), [
 
1271
            ('d', '', 0, False, packed_stat), # current tree details
 
1272
            ('d', '', 0, False, 'rev_id'), # first parent details
 
1273
             # second: a pointer to the current location
 
1274
            ('a', 'dirname/basename', 0, False, ''),
 
1275
            ]
 
1276
        state = dirstate.DirState.initialize('dirstate')
 
1277
        try:
 
1278
            self.assertEqual(
 
1279
                '\x00\x00a-root-value\x00'
 
1280
                'd\x00\x000\x00n\x00AAAAREUHaIpFB2iKAAADAQAtkqUAAIGk\x00'
 
1281
                'd\x00\x000\x00n\x00rev_id\x00'
 
1282
                'a\x00dirname/basename\x000\x00n\x00',
 
1283
                state._entry_to_line(root_entry))
 
1284
        finally:
 
1285
            state.unlock()
 
1286
 
 
1287
    def test_iter_entries(self):
 
1288
        # we should be able to iterate the dirstate entries from end to end
 
1289
        # this is for get_lines to be easy to read.
 
1290
        packed_stat = 'AAAAREUHaIpFB2iKAAADAQAtkqUAAIGk'
 
1291
        dirblocks = []
 
1292
        root_entries = [(('', '', 'a-root-value'), [
 
1293
            ('d', '', 0, False, packed_stat), # current tree details
 
1294
            ])]
 
1295
        dirblocks.append(('', root_entries))
 
1296
        # add two files in the root
 
1297
        subdir_entry = ('', 'subdir', 'subdir-id'), [
 
1298
            ('d', '', 0, False, packed_stat), # current tree details
 
1299
            ]
 
1300
        afile_entry = ('', 'afile', 'afile-id'), [
 
1301
            ('f', 'sha1value', 34, False, packed_stat), # current tree details
 
1302
            ]
 
1303
        dirblocks.append(('', [subdir_entry, afile_entry]))
 
1304
        # and one in subdir
 
1305
        file_entry2 = ('subdir', '2file', '2file-id'), [
 
1306
            ('f', 'sha1value', 23, False, packed_stat), # current tree details
 
1307
            ]
 
1308
        dirblocks.append(('subdir', [file_entry2]))
 
1309
        state = dirstate.DirState.initialize('dirstate')
 
1310
        try:
 
1311
            state._set_data([], dirblocks)
 
1312
            expected_entries = [root_entries[0], subdir_entry, afile_entry,
 
1313
                                file_entry2]
 
1314
            self.assertEqual(expected_entries, list(state._iter_entries()))
 
1315
        finally:
 
1316
            state.unlock()
 
1317
 
 
1318
 
 
1319
class TestGetBlockRowIndex(TestCaseWithDirState):
 
1320
 
 
1321
    def assertBlockRowIndexEqual(self, block_index, row_index, dir_present,
 
1322
        file_present, state, dirname, basename, tree_index):
 
1323
        self.assertEqual((block_index, row_index, dir_present, file_present),
 
1324
            state._get_block_entry_index(dirname, basename, tree_index))
 
1325
        if dir_present:
 
1326
            block = state._dirblocks[block_index]
 
1327
            self.assertEqual(dirname, block[0])
 
1328
        if dir_present and file_present:
 
1329
            row = state._dirblocks[block_index][1][row_index]
 
1330
            self.assertEqual(dirname, row[0][0])
 
1331
            self.assertEqual(basename, row[0][1])
 
1332
 
 
1333
    def test_simple_structure(self):
 
1334
        state = self.create_dirstate_with_root_and_subdir()
 
1335
        self.addCleanup(state.unlock)
 
1336
        self.assertBlockRowIndexEqual(1, 0, True, True, state, '', 'subdir', 0)
 
1337
        self.assertBlockRowIndexEqual(1, 0, True, False, state, '', 'bdir', 0)
 
1338
        self.assertBlockRowIndexEqual(1, 1, True, False, state, '', 'zdir', 0)
 
1339
        self.assertBlockRowIndexEqual(2, 0, False, False, state, 'a', 'foo', 0)
 
1340
        self.assertBlockRowIndexEqual(2, 0, False, False, state,
 
1341
                                      'subdir', 'foo', 0)
 
1342
 
 
1343
    def test_complex_structure_exists(self):
 
1344
        state = self.create_complex_dirstate()
 
1345
        self.addCleanup(state.unlock)
 
1346
        # Make sure we can find everything that exists
 
1347
        self.assertBlockRowIndexEqual(0, 0, True, True, state, '', '', 0)
 
1348
        self.assertBlockRowIndexEqual(1, 0, True, True, state, '', 'a', 0)
 
1349
        self.assertBlockRowIndexEqual(1, 1, True, True, state, '', 'b', 0)
 
1350
        self.assertBlockRowIndexEqual(1, 2, True, True, state, '', 'c', 0)
 
1351
        self.assertBlockRowIndexEqual(1, 3, True, True, state, '', 'd', 0)
 
1352
        self.assertBlockRowIndexEqual(2, 0, True, True, state, 'a', 'e', 0)
 
1353
        self.assertBlockRowIndexEqual(2, 1, True, True, state, 'a', 'f', 0)
 
1354
        self.assertBlockRowIndexEqual(3, 0, True, True, state, 'b', 'g', 0)
 
1355
        self.assertBlockRowIndexEqual(3, 1, True, True, state,
 
1356
                                      'b', 'h\xc3\xa5', 0)
 
1357
 
 
1358
    def test_complex_structure_missing(self):
 
1359
        state = self.create_complex_dirstate()
 
1360
        self.addCleanup(state.unlock)
 
1361
        # Make sure things would be inserted in the right locations
 
1362
        # '_' comes before 'a'
 
1363
        self.assertBlockRowIndexEqual(0, 0, True, True, state, '', '', 0)
 
1364
        self.assertBlockRowIndexEqual(1, 0, True, False, state, '', '_', 0)
 
1365
        self.assertBlockRowIndexEqual(1, 1, True, False, state, '', 'aa', 0)
 
1366
        self.assertBlockRowIndexEqual(1, 4, True, False, state,
 
1367
                                      '', 'h\xc3\xa5', 0)
 
1368
        self.assertBlockRowIndexEqual(2, 0, False, False, state, '_', 'a', 0)
 
1369
        self.assertBlockRowIndexEqual(3, 0, False, False, state, 'aa', 'a', 0)
 
1370
        self.assertBlockRowIndexEqual(4, 0, False, False, state, 'bb', 'a', 0)
 
1371
        # This would be inserted between a/ and b/
 
1372
        self.assertBlockRowIndexEqual(3, 0, False, False, state, 'a/e', 'a', 0)
 
1373
        # Put at the end
 
1374
        self.assertBlockRowIndexEqual(4, 0, False, False, state, 'e', 'a', 0)
 
1375
 
 
1376
 
 
1377
class TestGetEntry(TestCaseWithDirState):
 
1378
 
 
1379
    def assertEntryEqual(self, dirname, basename, file_id, state, path, index):
 
1380
        """Check that the right entry is returned for a request to getEntry."""
 
1381
        entry = state._get_entry(index, path_utf8=path)
 
1382
        if file_id is None:
 
1383
            self.assertEqual((None, None), entry)
 
1384
        else:
 
1385
            cur = entry[0]
 
1386
            self.assertEqual((dirname, basename, file_id), cur[:3])
 
1387
 
 
1388
    def test_simple_structure(self):
 
1389
        state = self.create_dirstate_with_root_and_subdir()
 
1390
        self.addCleanup(state.unlock)
 
1391
        self.assertEntryEqual('', '', 'a-root-value', state, '', 0)
 
1392
        self.assertEntryEqual('', 'subdir', 'subdir-id', state, 'subdir', 0)
 
1393
        self.assertEntryEqual(None, None, None, state, 'missing', 0)
 
1394
        self.assertEntryEqual(None, None, None, state, 'missing/foo', 0)
 
1395
        self.assertEntryEqual(None, None, None, state, 'subdir/foo', 0)
 
1396
 
 
1397
    def test_complex_structure_exists(self):
 
1398
        state = self.create_complex_dirstate()
 
1399
        self.addCleanup(state.unlock)
 
1400
        self.assertEntryEqual('', '', 'a-root-value', state, '', 0)
 
1401
        self.assertEntryEqual('', 'a', 'a-dir', state, 'a', 0)
 
1402
        self.assertEntryEqual('', 'b', 'b-dir', state, 'b', 0)
 
1403
        self.assertEntryEqual('', 'c', 'c-file', state, 'c', 0)
 
1404
        self.assertEntryEqual('', 'd', 'd-file', state, 'd', 0)
 
1405
        self.assertEntryEqual('a', 'e', 'e-dir', state, 'a/e', 0)
 
1406
        self.assertEntryEqual('a', 'f', 'f-file', state, 'a/f', 0)
 
1407
        self.assertEntryEqual('b', 'g', 'g-file', state, 'b/g', 0)
 
1408
        self.assertEntryEqual('b', 'h\xc3\xa5', 'h-\xc3\xa5-file', state,
 
1409
                              'b/h\xc3\xa5', 0)
 
1410
 
 
1411
    def test_complex_structure_missing(self):
 
1412
        state = self.create_complex_dirstate()
 
1413
        self.addCleanup(state.unlock)
 
1414
        self.assertEntryEqual(None, None, None, state, '_', 0)
 
1415
        self.assertEntryEqual(None, None, None, state, '_\xc3\xa5', 0)
 
1416
        self.assertEntryEqual(None, None, None, state, 'a/b', 0)
 
1417
        self.assertEntryEqual(None, None, None, state, 'c/d', 0)
 
1418
 
 
1419
    def test_get_entry_uninitialized(self):
 
1420
        """Calling get_entry will load data if it needs to"""
 
1421
        state = self.create_dirstate_with_root()
 
1422
        try:
 
1423
            state.save()
 
1424
        finally:
 
1425
            state.unlock()
 
1426
        del state
 
1427
        state = dirstate.DirState.on_file('dirstate')
 
1428
        state.lock_read()
 
1429
        try:
 
1430
            self.assertEqual(dirstate.DirState.NOT_IN_MEMORY,
 
1431
                             state._header_state)
 
1432
            self.assertEqual(dirstate.DirState.NOT_IN_MEMORY,
 
1433
                             state._dirblock_state)
 
1434
            self.assertEntryEqual('', '', 'a-root-value', state, '', 0)
 
1435
        finally:
 
1436
            state.unlock()
 
1437
 
 
1438
 
 
1439
class TestIterChildEntries(TestCaseWithDirState):
 
1440
 
 
1441
    def create_dirstate_with_two_trees(self):
 
1442
        """This dirstate contains multiple files and directories.
 
1443
 
 
1444
         /        a-root-value
 
1445
         a/       a-dir
 
1446
         b/       b-dir
 
1447
         c        c-file
 
1448
         d        d-file
 
1449
         a/e/     e-dir
 
1450
         a/f      f-file
 
1451
         b/g      g-file
 
1452
         b/h\xc3\xa5  h-\xc3\xa5-file  #This is u'\xe5' encoded into utf-8
 
1453
 
 
1454
        Notice that a/e is an empty directory.
 
1455
 
 
1456
        There is one parent tree, which has the same shape with the following variations:
 
1457
        b/g in the parent is gone.
 
1458
        b/h in the parent has a different id
 
1459
        b/i is new in the parent 
 
1460
        c is renamed to b/j in the parent
 
1461
 
 
1462
        :return: The dirstate, still write-locked.
 
1463
        """
 
1464
        packed_stat = 'AAAAREUHaIpFB2iKAAADAQAtkqUAAIGk'
 
1465
        null_sha = 'xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx'
 
1466
        NULL_PARENT_DETAILS = dirstate.DirState.NULL_PARENT_DETAILS
 
1467
        root_entry = ('', '', 'a-root-value'), [
 
1468
            ('d', '', 0, False, packed_stat),
 
1469
            ('d', '', 0, False, 'parent-revid'),
 
1470
            ]
 
1471
        a_entry = ('', 'a', 'a-dir'), [
 
1472
            ('d', '', 0, False, packed_stat),
 
1473
            ('d', '', 0, False, 'parent-revid'),
 
1474
            ]
 
1475
        b_entry = ('', 'b', 'b-dir'), [
 
1476
            ('d', '', 0, False, packed_stat),
 
1477
            ('d', '', 0, False, 'parent-revid'),
 
1478
            ]
 
1479
        c_entry = ('', 'c', 'c-file'), [
 
1480
            ('f', null_sha, 10, False, packed_stat),
 
1481
            ('r', 'b/j', 0, False, ''),
 
1482
            ]
 
1483
        d_entry = ('', 'd', 'd-file'), [
 
1484
            ('f', null_sha, 20, False, packed_stat),
 
1485
            ('f', 'd', 20, False, 'parent-revid'),
 
1486
            ]
 
1487
        e_entry = ('a', 'e', 'e-dir'), [
 
1488
            ('d', '', 0, False, packed_stat),
 
1489
            ('d', '', 0, False, 'parent-revid'),
 
1490
            ]
 
1491
        f_entry = ('a', 'f', 'f-file'), [
 
1492
            ('f', null_sha, 30, False, packed_stat),
 
1493
            ('f', 'f', 20, False, 'parent-revid'),
 
1494
            ]
 
1495
        g_entry = ('b', 'g', 'g-file'), [
 
1496
            ('f', null_sha, 30, False, packed_stat),
 
1497
            NULL_PARENT_DETAILS,
 
1498
            ]
 
1499
        h_entry1 = ('b', 'h\xc3\xa5', 'h-\xc3\xa5-file1'), [
 
1500
            ('f', null_sha, 40, False, packed_stat),
 
1501
            NULL_PARENT_DETAILS,
 
1502
            ]
 
1503
        h_entry2 = ('b', 'h\xc3\xa5', 'h-\xc3\xa5-file2'), [
 
1504
            NULL_PARENT_DETAILS,
 
1505
            ('f', 'h', 20, False, 'parent-revid'),
 
1506
            ]
 
1507
        i_entry = ('b', 'i', 'i-file'), [
 
1508
            NULL_PARENT_DETAILS,
 
1509
            ('f', 'h', 20, False, 'parent-revid'),
 
1510
            ]
 
1511
        j_entry = ('b', 'j', 'c-file'), [
 
1512
            ('r', 'c', 0, False, ''),
 
1513
            ('f', 'j', 20, False, 'parent-revid'),
 
1514
            ]
 
1515
        dirblocks = []
 
1516
        dirblocks.append(('', [root_entry]))
 
1517
        dirblocks.append(('', [a_entry, b_entry, c_entry, d_entry]))
 
1518
        dirblocks.append(('a', [e_entry, f_entry]))
 
1519
        dirblocks.append(('b', [g_entry, h_entry1, h_entry2, i_entry, j_entry]))
 
1520
        state = dirstate.DirState.initialize('dirstate')
 
1521
        state._validate()
 
1522
        try:
 
1523
            state._set_data(['parent'], dirblocks)
 
1524
        except:
 
1525
            state.unlock()
 
1526
            raise
 
1527
        return state, dirblocks
 
1528
 
 
1529
    def test_iter_children_b(self):
 
1530
        state, dirblocks = self.create_dirstate_with_two_trees()
 
1531
        self.addCleanup(state.unlock)
 
1532
        expected_result = []
 
1533
        expected_result.append(dirblocks[3][1][2]) # h2
 
1534
        expected_result.append(dirblocks[3][1][3]) # i
 
1535
        expected_result.append(dirblocks[3][1][4]) # j
 
1536
        self.assertEqual(expected_result,
 
1537
            list(state._iter_child_entries(1, 'b')))
 
1538
 
 
1539
    def test_iter_child_root(self):
 
1540
        state, dirblocks = self.create_dirstate_with_two_trees()
 
1541
        self.addCleanup(state.unlock)
 
1542
        expected_result = []
 
1543
        expected_result.append(dirblocks[1][1][0]) # a
 
1544
        expected_result.append(dirblocks[1][1][1]) # b
 
1545
        expected_result.append(dirblocks[1][1][3]) # d
 
1546
        expected_result.append(dirblocks[2][1][0]) # e
 
1547
        expected_result.append(dirblocks[2][1][1]) # f
 
1548
        expected_result.append(dirblocks[3][1][2]) # h2
 
1549
        expected_result.append(dirblocks[3][1][3]) # i
 
1550
        expected_result.append(dirblocks[3][1][4]) # j
 
1551
        self.assertEqual(expected_result,
 
1552
            list(state._iter_child_entries(1, '')))
 
1553
 
 
1554
 
 
1555
class TestDirstateSortOrder(TestCaseWithTransport):
 
1556
    """Test that DirState adds entries in the right order."""
 
1557
 
 
1558
    def test_add_sorting(self):
 
1559
        """Add entries in lexicographical order, we get path sorted order.
 
1560
 
 
1561
        This tests it to a depth of 4, to make sure we don't just get it right
 
1562
        at a single depth. 'a/a' should come before 'a-a', even though it
 
1563
        doesn't lexicographically.
 
1564
        """
 
1565
        dirs = ['a', 'a/a', 'a/a/a', 'a/a/a/a',
 
1566
                'a-a', 'a/a-a', 'a/a/a-a', 'a/a/a/a-a',
 
1567
               ]
 
1568
        null_sha = ''
 
1569
        state = dirstate.DirState.initialize('dirstate')
 
1570
        self.addCleanup(state.unlock)
 
1571
 
 
1572
        fake_stat = os.stat('dirstate')
 
1573
        for d in dirs:
 
1574
            d_id = d.replace('/', '_')+'-id'
 
1575
            file_path = d + '/f'
 
1576
            file_id = file_path.replace('/', '_')+'-id'
 
1577
            state.add(d, d_id, 'directory', fake_stat, null_sha)
 
1578
            state.add(file_path, file_id, 'file', fake_stat, null_sha)
 
1579
 
 
1580
        expected = ['', '', 'a',
 
1581
                'a/a', 'a/a/a', 'a/a/a/a',
 
1582
                'a/a/a/a-a', 'a/a/a-a', 'a/a-a', 'a-a',
 
1583
               ]
 
1584
        split = lambda p:p.split('/')
 
1585
        self.assertEqual(sorted(expected, key=split), expected)
 
1586
        dirblock_names = [d[0] for d in state._dirblocks]
 
1587
        self.assertEqual(expected, dirblock_names)
 
1588
 
 
1589
    def test_set_parent_trees_correct_order(self):
 
1590
        """After calling set_parent_trees() we should maintain the order."""
 
1591
        dirs = ['a', 'a-a', 'a/a']
 
1592
        null_sha = ''
 
1593
        state = dirstate.DirState.initialize('dirstate')
 
1594
        self.addCleanup(state.unlock)
 
1595
 
 
1596
        fake_stat = os.stat('dirstate')
 
1597
        for d in dirs:
 
1598
            d_id = d.replace('/', '_')+'-id'
 
1599
            file_path = d + '/f'
 
1600
            file_id = file_path.replace('/', '_')+'-id'
 
1601
            state.add(d, d_id, 'directory', fake_stat, null_sha)
 
1602
            state.add(file_path, file_id, 'file', fake_stat, null_sha)
 
1603
 
 
1604
        expected = ['', '', 'a', 'a/a', 'a-a']
 
1605
        dirblock_names = [d[0] for d in state._dirblocks]
 
1606
        self.assertEqual(expected, dirblock_names)
 
1607
 
 
1608
        # *really* cheesy way to just get an empty tree
 
1609
        repo = self.make_repository('repo')
 
1610
        empty_tree = repo.revision_tree(_mod_revision.NULL_REVISION)
 
1611
        state.set_parent_trees([('null:', empty_tree)], [])
 
1612
 
 
1613
        dirblock_names = [d[0] for d in state._dirblocks]
 
1614
        self.assertEqual(expected, dirblock_names)
 
1615
 
 
1616
 
 
1617
class InstrumentedDirState(dirstate.DirState):
 
1618
    """An DirState with instrumented sha1 functionality."""
 
1619
 
 
1620
    def __init__(self, path):
 
1621
        super(InstrumentedDirState, self).__init__(path)
 
1622
        self._time_offset = 0
 
1623
        self._log = []
 
1624
        # member is dynamically set in DirState.__init__ to turn on trace
 
1625
        self._size_sha1_file = self._size_sha1_file_and_log
 
1626
 
 
1627
    def _sha_cutoff_time(self):
 
1628
        timestamp = super(InstrumentedDirState, self)._sha_cutoff_time()
 
1629
        self._cutoff_time = timestamp + self._time_offset
 
1630
 
 
1631
    def _size_sha1_file_and_log(self, abspath, filter_list):
 
1632
        self._log.append(('sha1', abspath))
 
1633
        return filters.internal_size_sha_file_byname(abspath, filter_list)
 
1634
 
 
1635
    def _read_link(self, abspath, old_link):
 
1636
        self._log.append(('read_link', abspath, old_link))
 
1637
        return super(InstrumentedDirState, self)._read_link(abspath, old_link)
 
1638
 
 
1639
    def _lstat(self, abspath, entry):
 
1640
        self._log.append(('lstat', abspath))
 
1641
        return super(InstrumentedDirState, self)._lstat(abspath, entry)
 
1642
 
 
1643
    def _is_executable(self, mode, old_executable):
 
1644
        self._log.append(('is_exec', mode, old_executable))
 
1645
        return super(InstrumentedDirState, self)._is_executable(mode,
 
1646
                                                                old_executable)
 
1647
 
 
1648
    def adjust_time(self, secs):
 
1649
        """Move the clock forward or back.
 
1650
 
 
1651
        :param secs: The amount to adjust the clock by. Positive values make it
 
1652
        seem as if we are in the future, negative values make it seem like we
 
1653
        are in the past.
 
1654
        """
 
1655
        self._time_offset += secs
 
1656
        self._cutoff_time = None
 
1657
 
 
1658
 
 
1659
class _FakeStat(object):
 
1660
    """A class with the same attributes as a real stat result."""
 
1661
 
 
1662
    def __init__(self, size, mtime, ctime, dev, ino, mode):
 
1663
        self.st_size = size
 
1664
        self.st_mtime = mtime
 
1665
        self.st_ctime = ctime
 
1666
        self.st_dev = dev
 
1667
        self.st_ino = ino
 
1668
        self.st_mode = mode
 
1669
 
 
1670
 
 
1671
class TestPackStat(TestCaseWithTransport):
 
1672
 
 
1673
    def assertPackStat(self, expected, stat_value):
 
1674
        """Check the packed and serialized form of a stat value."""
 
1675
        self.assertEqual(expected, dirstate.pack_stat(stat_value))
 
1676
 
 
1677
    def test_pack_stat_int(self):
 
1678
        st = _FakeStat(6859L, 1172758614, 1172758617, 777L, 6499538L, 0100644)
 
1679
        # Make sure that all parameters have an impact on the packed stat.
 
1680
        self.assertPackStat('AAAay0Xm4FZF5uBZAAADCQBjLNIAAIGk', st)
 
1681
        st.st_size = 7000L
 
1682
        #                ay0 => bWE
 
1683
        self.assertPackStat('AAAbWEXm4FZF5uBZAAADCQBjLNIAAIGk', st)
 
1684
        st.st_mtime = 1172758620
 
1685
        #                     4FZ => 4Fx
 
1686
        self.assertPackStat('AAAbWEXm4FxF5uBZAAADCQBjLNIAAIGk', st)
 
1687
        st.st_ctime = 1172758630
 
1688
        #                          uBZ => uBm
 
1689
        self.assertPackStat('AAAbWEXm4FxF5uBmAAADCQBjLNIAAIGk', st)
 
1690
        st.st_dev = 888L
 
1691
        #                                DCQ => DeA
 
1692
        self.assertPackStat('AAAbWEXm4FxF5uBmAAADeABjLNIAAIGk', st)
 
1693
        st.st_ino = 6499540L
 
1694
        #                                     LNI => LNQ
 
1695
        self.assertPackStat('AAAbWEXm4FxF5uBmAAADeABjLNQAAIGk', st)
 
1696
        st.st_mode = 0100744
 
1697
        #                                          IGk => IHk
 
1698
        self.assertPackStat('AAAbWEXm4FxF5uBmAAADeABjLNQAAIHk', st)
 
1699
 
 
1700
    def test_pack_stat_float(self):
 
1701
        """On some platforms mtime and ctime are floats.
 
1702
 
 
1703
        Make sure we don't get warnings or errors, and that we ignore changes <
 
1704
        1s
 
1705
        """
 
1706
        st = _FakeStat(7000L, 1172758614.0, 1172758617.0,
 
1707
                       777L, 6499538L, 0100644)
 
1708
        # These should all be the same as the integer counterparts
 
1709
        self.assertPackStat('AAAbWEXm4FZF5uBZAAADCQBjLNIAAIGk', st)
 
1710
        st.st_mtime = 1172758620.0
 
1711
        #                     FZF5 => FxF5
 
1712
        self.assertPackStat('AAAbWEXm4FxF5uBZAAADCQBjLNIAAIGk', st)
 
1713
        st.st_ctime = 1172758630.0
 
1714
        #                          uBZ => uBm
 
1715
        self.assertPackStat('AAAbWEXm4FxF5uBmAAADCQBjLNIAAIGk', st)
 
1716
        # fractional seconds are discarded, so no change from above
 
1717
        st.st_mtime = 1172758620.453
 
1718
        self.assertPackStat('AAAbWEXm4FxF5uBmAAADCQBjLNIAAIGk', st)
 
1719
        st.st_ctime = 1172758630.228
 
1720
        self.assertPackStat('AAAbWEXm4FxF5uBmAAADCQBjLNIAAIGk', st)
 
1721
 
 
1722
 
 
1723
class TestBisect(TestCaseWithDirState):
 
1724
    """Test the ability to bisect into the disk format."""
 
1725
 
 
1726
    def assertBisect(self, expected_map, map_keys, state, paths):
 
1727
        """Assert that bisecting for paths returns the right result.
 
1728
 
 
1729
        :param expected_map: A map from key => entry value
 
1730
        :param map_keys: The keys to expect for each path
 
1731
        :param state: The DirState object.
 
1732
        :param paths: A list of paths, these will automatically be split into
 
1733
                      (dir, name) tuples, and sorted according to how _bisect
 
1734
                      requires.
 
1735
        """
 
1736
        result = state._bisect(paths)
 
1737
        # For now, results are just returned in whatever order we read them.
 
1738
        # We could sort by (dir, name, file_id) or something like that, but in
 
1739
        # the end it would still be fairly arbitrary, and we don't want the
 
1740
        # extra overhead if we can avoid it. So sort everything to make sure
 
1741
        # equality is true
 
1742
        self.assertEqual(len(map_keys), len(paths))
 
1743
        expected = {}
 
1744
        for path, keys in zip(paths, map_keys):
 
1745
            if keys is None:
 
1746
                # This should not be present in the output
 
1747
                continue
 
1748
            expected[path] = sorted(expected_map[k] for k in keys)
 
1749
 
 
1750
        # The returned values are just arranged randomly based on when they
 
1751
        # were read, for testing, make sure it is properly sorted.
 
1752
        for path in result:
 
1753
            result[path].sort()
 
1754
 
 
1755
        self.assertEqual(expected, result)
 
1756
 
 
1757
    def assertBisectDirBlocks(self, expected_map, map_keys, state, paths):
 
1758
        """Assert that bisecting for dirbblocks returns the right result.
 
1759
 
 
1760
        :param expected_map: A map from key => expected values
 
1761
        :param map_keys: A nested list of paths we expect to be returned.
 
1762
            Something like [['a', 'b', 'f'], ['b/c', 'b/d']]
 
1763
        :param state: The DirState object.
 
1764
        :param paths: A list of directories
 
1765
        """
 
1766
        result = state._bisect_dirblocks(paths)
 
1767
        self.assertEqual(len(map_keys), len(paths))
 
1768
        expected = {}
 
1769
        for path, keys in zip(paths, map_keys):
 
1770
            if keys is None:
 
1771
                # This should not be present in the output
 
1772
                continue
 
1773
            expected[path] = sorted(expected_map[k] for k in keys)
 
1774
        for path in result:
 
1775
            result[path].sort()
 
1776
 
 
1777
        self.assertEqual(expected, result)
 
1778
 
 
1779
    def assertBisectRecursive(self, expected_map, map_keys, state, paths):
 
1780
        """Assert the return value of a recursive bisection.
 
1781
 
 
1782
        :param expected_map: A map from key => entry value
 
1783
        :param map_keys: A list of paths we expect to be returned.
 
1784
            Something like ['a', 'b', 'f', 'b/d', 'b/d2']
 
1785
        :param state: The DirState object.
 
1786
        :param paths: A list of files and directories. It will be broken up
 
1787
            into (dir, name) pairs and sorted before calling _bisect_recursive.
 
1788
        """
 
1789
        expected = {}
 
1790
        for key in map_keys:
 
1791
            entry = expected_map[key]
 
1792
            dir_name_id, trees_info = entry
 
1793
            expected[dir_name_id] = trees_info
 
1794
 
 
1795
        result = state._bisect_recursive(paths)
 
1796
 
 
1797
        self.assertEqual(expected, result)
 
1798
 
 
1799
    def test_bisect_each(self):
 
1800
        """Find a single record using bisect."""
 
1801
        tree, state, expected = self.create_basic_dirstate()
 
1802
 
 
1803
        # Bisect should return the rows for the specified files.
 
1804
        self.assertBisect(expected, [['']], state, [''])
 
1805
        self.assertBisect(expected, [['a']], state, ['a'])
 
1806
        self.assertBisect(expected, [['b']], state, ['b'])
 
1807
        self.assertBisect(expected, [['b/c']], state, ['b/c'])
 
1808
        self.assertBisect(expected, [['b/d']], state, ['b/d'])
 
1809
        self.assertBisect(expected, [['b/d/e']], state, ['b/d/e'])
 
1810
        self.assertBisect(expected, [['b-c']], state, ['b-c'])
 
1811
        self.assertBisect(expected, [['f']], state, ['f'])
 
1812
 
 
1813
    def test_bisect_multi(self):
 
1814
        """Bisect can be used to find multiple records at the same time."""
 
1815
        tree, state, expected = self.create_basic_dirstate()
 
1816
        # Bisect should be capable of finding multiple entries at the same time
 
1817
        self.assertBisect(expected, [['a'], ['b'], ['f']],
 
1818
                          state, ['a', 'b', 'f'])
 
1819
        self.assertBisect(expected, [['f'], ['b/d'], ['b/d/e']],
 
1820
                          state, ['f', 'b/d', 'b/d/e'])
 
1821
        self.assertBisect(expected, [['b'], ['b-c'], ['b/c']],
 
1822
                          state, ['b', 'b-c', 'b/c'])
 
1823
 
 
1824
    def test_bisect_one_page(self):
 
1825
        """Test bisect when there is only 1 page to read"""
 
1826
        tree, state, expected = self.create_basic_dirstate()
 
1827
        state._bisect_page_size = 5000
 
1828
        self.assertBisect(expected,[['']], state, [''])
 
1829
        self.assertBisect(expected,[['a']], state, ['a'])
 
1830
        self.assertBisect(expected,[['b']], state, ['b'])
 
1831
        self.assertBisect(expected,[['b/c']], state, ['b/c'])
 
1832
        self.assertBisect(expected,[['b/d']], state, ['b/d'])
 
1833
        self.assertBisect(expected,[['b/d/e']], state, ['b/d/e'])
 
1834
        self.assertBisect(expected,[['b-c']], state, ['b-c'])
 
1835
        self.assertBisect(expected,[['f']], state, ['f'])
 
1836
        self.assertBisect(expected,[['a'], ['b'], ['f']],
 
1837
                          state, ['a', 'b', 'f'])
 
1838
        self.assertBisect(expected, [['b/d'], ['b/d/e'], ['f']],
 
1839
                          state, ['b/d', 'b/d/e', 'f'])
 
1840
        self.assertBisect(expected, [['b'], ['b/c'], ['b-c']],
 
1841
                          state, ['b', 'b/c', 'b-c'])
 
1842
 
 
1843
    def test_bisect_duplicate_paths(self):
 
1844
        """When bisecting for a path, handle multiple entries."""
 
1845
        tree, state, expected = self.create_duplicated_dirstate()
 
1846
 
 
1847
        # Now make sure that both records are properly returned.
 
1848
        self.assertBisect(expected, [['']], state, [''])
 
1849
        self.assertBisect(expected, [['a', 'a2']], state, ['a'])
 
1850
        self.assertBisect(expected, [['b', 'b2']], state, ['b'])
 
1851
        self.assertBisect(expected, [['b/c', 'b/c2']], state, ['b/c'])
 
1852
        self.assertBisect(expected, [['b/d', 'b/d2']], state, ['b/d'])
 
1853
        self.assertBisect(expected, [['b/d/e', 'b/d/e2']],
 
1854
                          state, ['b/d/e'])
 
1855
        self.assertBisect(expected, [['b-c', 'b-c2']], state, ['b-c'])
 
1856
        self.assertBisect(expected, [['f', 'f2']], state, ['f'])
 
1857
 
 
1858
    def test_bisect_page_size_too_small(self):
 
1859
        """If the page size is too small, we will auto increase it."""
 
1860
        tree, state, expected = self.create_basic_dirstate()
 
1861
        state._bisect_page_size = 50
 
1862
        self.assertBisect(expected, [None], state, ['b/e'])
 
1863
        self.assertBisect(expected, [['a']], state, ['a'])
 
1864
        self.assertBisect(expected, [['b']], state, ['b'])
 
1865
        self.assertBisect(expected, [['b/c']], state, ['b/c'])
 
1866
        self.assertBisect(expected, [['b/d']], state, ['b/d'])
 
1867
        self.assertBisect(expected, [['b/d/e']], state, ['b/d/e'])
 
1868
        self.assertBisect(expected, [['b-c']], state, ['b-c'])
 
1869
        self.assertBisect(expected, [['f']], state, ['f'])
 
1870
 
 
1871
    def test_bisect_missing(self):
 
1872
        """Test that bisect return None if it cannot find a path."""
 
1873
        tree, state, expected = self.create_basic_dirstate()
 
1874
        self.assertBisect(expected, [None], state, ['foo'])
 
1875
        self.assertBisect(expected, [None], state, ['b/foo'])
 
1876
        self.assertBisect(expected, [None], state, ['bar/foo'])
 
1877
        self.assertBisect(expected, [None], state, ['b-c/foo'])
 
1878
 
 
1879
        self.assertBisect(expected, [['a'], None, ['b/d']],
 
1880
                          state, ['a', 'foo', 'b/d'])
 
1881
 
 
1882
    def test_bisect_rename(self):
 
1883
        """Check that we find a renamed row."""
 
1884
        tree, state, expected = self.create_renamed_dirstate()
 
1885
 
 
1886
        # Search for the pre and post renamed entries
 
1887
        self.assertBisect(expected, [['a']], state, ['a'])
 
1888
        self.assertBisect(expected, [['b/g']], state, ['b/g'])
 
1889
        self.assertBisect(expected, [['b/d']], state, ['b/d'])
 
1890
        self.assertBisect(expected, [['h']], state, ['h'])
 
1891
 
 
1892
        # What about b/d/e? shouldn't that also get 2 directory entries?
 
1893
        self.assertBisect(expected, [['b/d/e']], state, ['b/d/e'])
 
1894
        self.assertBisect(expected, [['h/e']], state, ['h/e'])
 
1895
 
 
1896
    def test_bisect_dirblocks(self):
 
1897
        tree, state, expected = self.create_duplicated_dirstate()
 
1898
        self.assertBisectDirBlocks(expected,
 
1899
            [['', 'a', 'a2', 'b', 'b2', 'b-c', 'b-c2', 'f', 'f2']],
 
1900
            state, [''])
 
1901
        self.assertBisectDirBlocks(expected,
 
1902
            [['b/c', 'b/c2', 'b/d', 'b/d2']], state, ['b'])
 
1903
        self.assertBisectDirBlocks(expected,
 
1904
            [['b/d/e', 'b/d/e2']], state, ['b/d'])
 
1905
        self.assertBisectDirBlocks(expected,
 
1906
            [['', 'a', 'a2', 'b', 'b2', 'b-c', 'b-c2', 'f', 'f2'],
 
1907
             ['b/c', 'b/c2', 'b/d', 'b/d2'],
 
1908
             ['b/d/e', 'b/d/e2'],
 
1909
            ], state, ['', 'b', 'b/d'])
 
1910
 
 
1911
    def test_bisect_dirblocks_missing(self):
 
1912
        tree, state, expected = self.create_basic_dirstate()
 
1913
        self.assertBisectDirBlocks(expected, [['b/d/e'], None],
 
1914
            state, ['b/d', 'b/e'])
 
1915
        # Files don't show up in this search
 
1916
        self.assertBisectDirBlocks(expected, [None], state, ['a'])
 
1917
        self.assertBisectDirBlocks(expected, [None], state, ['b/c'])
 
1918
        self.assertBisectDirBlocks(expected, [None], state, ['c'])
 
1919
        self.assertBisectDirBlocks(expected, [None], state, ['b/d/e'])
 
1920
        self.assertBisectDirBlocks(expected, [None], state, ['f'])
 
1921
 
 
1922
    def test_bisect_recursive_each(self):
 
1923
        tree, state, expected = self.create_basic_dirstate()
 
1924
        self.assertBisectRecursive(expected, ['a'], state, ['a'])
 
1925
        self.assertBisectRecursive(expected, ['b/c'], state, ['b/c'])
 
1926
        self.assertBisectRecursive(expected, ['b/d/e'], state, ['b/d/e'])
 
1927
        self.assertBisectRecursive(expected, ['b-c'], state, ['b-c'])
 
1928
        self.assertBisectRecursive(expected, ['b/d', 'b/d/e'],
 
1929
                                   state, ['b/d'])
 
1930
        self.assertBisectRecursive(expected, ['b', 'b/c', 'b/d', 'b/d/e'],
 
1931
                                   state, ['b'])
 
1932
        self.assertBisectRecursive(expected, ['', 'a', 'b', 'b-c', 'f', 'b/c',
 
1933
                                              'b/d', 'b/d/e'],
 
1934
                                   state, [''])
 
1935
 
 
1936
    def test_bisect_recursive_multiple(self):
 
1937
        tree, state, expected = self.create_basic_dirstate()
 
1938
        self.assertBisectRecursive(expected, ['a', 'b/c'], state, ['a', 'b/c'])
 
1939
        self.assertBisectRecursive(expected, ['b/d', 'b/d/e'],
 
1940
                                   state, ['b/d', 'b/d/e'])
 
1941
 
 
1942
    def test_bisect_recursive_missing(self):
 
1943
        tree, state, expected = self.create_basic_dirstate()
 
1944
        self.assertBisectRecursive(expected, [], state, ['d'])
 
1945
        self.assertBisectRecursive(expected, [], state, ['b/e'])
 
1946
        self.assertBisectRecursive(expected, [], state, ['g'])
 
1947
        self.assertBisectRecursive(expected, ['a'], state, ['a', 'g'])
 
1948
 
 
1949
    def test_bisect_recursive_renamed(self):
 
1950
        tree, state, expected = self.create_renamed_dirstate()
 
1951
 
 
1952
        # Looking for either renamed item should find the other
 
1953
        self.assertBisectRecursive(expected, ['a', 'b/g'], state, ['a'])
 
1954
        self.assertBisectRecursive(expected, ['a', 'b/g'], state, ['b/g'])
 
1955
        # Looking in the containing directory should find the rename target,
 
1956
        # and anything in a subdir of the renamed target.
 
1957
        self.assertBisectRecursive(expected, ['a', 'b', 'b/c', 'b/d',
 
1958
                                              'b/d/e', 'b/g', 'h', 'h/e'],
 
1959
                                   state, ['b'])
 
1960
 
 
1961
 
 
1962
class TestDirstateValidation(TestCaseWithDirState):
 
1963
 
 
1964
    def test_validate_correct_dirstate(self):
 
1965
        state = self.create_complex_dirstate()
 
1966
        state._validate()
 
1967
        state.unlock()
 
1968
        # and make sure we can also validate with a read lock
 
1969
        state.lock_read()
 
1970
        try:
 
1971
            state._validate()
 
1972
        finally:
 
1973
            state.unlock()
 
1974
 
 
1975
    def test_dirblock_not_sorted(self):
 
1976
        tree, state, expected = self.create_renamed_dirstate()
 
1977
        state._read_dirblocks_if_needed()
 
1978
        last_dirblock = state._dirblocks[-1]
 
1979
        # we're appending to the dirblock, but this name comes before some of
 
1980
        # the existing names; that's wrong
 
1981
        last_dirblock[1].append(
 
1982
            (('h', 'aaaa', 'a-id'),
 
1983
             [('a', '', 0, False, ''),
 
1984
              ('a', '', 0, False, '')]))
 
1985
        e = self.assertRaises(AssertionError,
 
1986
            state._validate)
 
1987
        self.assertContainsRe(str(e), 'not sorted')
 
1988
 
 
1989
    def test_dirblock_name_mismatch(self):
 
1990
        tree, state, expected = self.create_renamed_dirstate()
 
1991
        state._read_dirblocks_if_needed()
 
1992
        last_dirblock = state._dirblocks[-1]
 
1993
        # add an entry with the wrong directory name
 
1994
        last_dirblock[1].append(
 
1995
            (('', 'z', 'a-id'),
 
1996
             [('a', '', 0, False, ''),
 
1997
              ('a', '', 0, False, '')]))
 
1998
        e = self.assertRaises(AssertionError,
 
1999
            state._validate)
 
2000
        self.assertContainsRe(str(e),
 
2001
            "doesn't match directory name")
 
2002
 
 
2003
    def test_dirblock_missing_rename(self):
 
2004
        tree, state, expected = self.create_renamed_dirstate()
 
2005
        state._read_dirblocks_if_needed()
 
2006
        last_dirblock = state._dirblocks[-1]
 
2007
        # make another entry for a-id, without a correct 'r' pointer to
 
2008
        # the real occurrence in the working tree
 
2009
        last_dirblock[1].append(
 
2010
            (('h', 'z', 'a-id'),
 
2011
             [('a', '', 0, False, ''),
 
2012
              ('a', '', 0, False, '')]))
 
2013
        e = self.assertRaises(AssertionError,
 
2014
            state._validate)
 
2015
        self.assertContainsRe(str(e),
 
2016
            'file a-id is absent in row')
 
2017
 
 
2018
 
 
2019
class TestDirstateTreeReference(TestCaseWithDirState):
 
2020
 
 
2021
    def test_reference_revision_is_none(self):
 
2022
        tree = self.make_branch_and_tree('tree', format='dirstate-with-subtree')
 
2023
        subtree = self.make_branch_and_tree('tree/subtree',
 
2024
                            format='dirstate-with-subtree')
 
2025
        subtree.set_root_id('subtree')
 
2026
        tree.add_reference(subtree)
 
2027
        tree.add('subtree')
 
2028
        state = dirstate.DirState.from_tree(tree, 'dirstate')
 
2029
        key = ('', 'subtree', 'subtree')
 
2030
        expected = ('', [(key,
 
2031
            [('t', '', 0, False, 'xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx')])])
 
2032
 
 
2033
        try:
 
2034
            self.assertEqual(expected, state._find_block(key))
 
2035
        finally:
 
2036
            state.unlock()
 
2037
 
 
2038
 
 
2039
class TestDiscardMergeParents(TestCaseWithDirState):
 
2040
 
 
2041
    def test_discard_no_parents(self):
 
2042
        # This should be a no-op
 
2043
        state = self.create_empty_dirstate()
 
2044
        self.addCleanup(state.unlock)
 
2045
        state._discard_merge_parents()
 
2046
        state._validate()
 
2047
 
 
2048
    def test_discard_one_parent(self):
 
2049
        # No-op
 
2050
        packed_stat = 'AAAAREUHaIpFB2iKAAADAQAtkqUAAIGk'
 
2051
        root_entry_direntry = ('', '', 'a-root-value'), [
 
2052
            ('d', '', 0, False, packed_stat),
 
2053
            ('d', '', 0, False, packed_stat),
 
2054
            ]
 
2055
        dirblocks = []
 
2056
        dirblocks.append(('', [root_entry_direntry]))
 
2057
        dirblocks.append(('', []))
 
2058
 
 
2059
        state = self.create_empty_dirstate()
 
2060
        self.addCleanup(state.unlock)
 
2061
        state._set_data(['parent-id'], dirblocks[:])
 
2062
        state._validate()
 
2063
 
 
2064
        state._discard_merge_parents()
 
2065
        state._validate()
 
2066
        self.assertEqual(dirblocks, state._dirblocks)
 
2067
 
 
2068
    def test_discard_simple(self):
 
2069
        # No-op
 
2070
        packed_stat = 'AAAAREUHaIpFB2iKAAADAQAtkqUAAIGk'
 
2071
        root_entry_direntry = ('', '', 'a-root-value'), [
 
2072
            ('d', '', 0, False, packed_stat),
 
2073
            ('d', '', 0, False, packed_stat),
 
2074
            ('d', '', 0, False, packed_stat),
 
2075
            ]
 
2076
        expected_root_entry_direntry = ('', '', 'a-root-value'), [
 
2077
            ('d', '', 0, False, packed_stat),
 
2078
            ('d', '', 0, False, packed_stat),
 
2079
            ]
 
2080
        dirblocks = []
 
2081
        dirblocks.append(('', [root_entry_direntry]))
 
2082
        dirblocks.append(('', []))
 
2083
 
 
2084
        state = self.create_empty_dirstate()
 
2085
        self.addCleanup(state.unlock)
 
2086
        state._set_data(['parent-id', 'merged-id'], dirblocks[:])
 
2087
        state._validate()
 
2088
 
 
2089
        # This should strip of the extra column
 
2090
        state._discard_merge_parents()
 
2091
        state._validate()
 
2092
        expected_dirblocks = [('', [expected_root_entry_direntry]), ('', [])]
 
2093
        self.assertEqual(expected_dirblocks, state._dirblocks)
 
2094
 
 
2095
    def test_discard_absent(self):
 
2096
        """If entries are only in a merge, discard should remove the entries"""
 
2097
        null_stat = dirstate.DirState.NULLSTAT
 
2098
        present_dir = ('d', '', 0, False, null_stat)
 
2099
        present_file = ('f', '', 0, False, null_stat)
 
2100
        absent = dirstate.DirState.NULL_PARENT_DETAILS
 
2101
        root_key = ('', '', 'a-root-value')
 
2102
        file_in_root_key = ('', 'file-in-root', 'a-file-id')
 
2103
        file_in_merged_key = ('', 'file-in-merged', 'b-file-id')
 
2104
        dirblocks = [('', [(root_key, [present_dir, present_dir, present_dir])]),
 
2105
                     ('', [(file_in_merged_key,
 
2106
                            [absent, absent, present_file]),
 
2107
                           (file_in_root_key,
 
2108
                            [present_file, present_file, present_file]),
 
2109
                          ]),
 
2110
                    ]
 
2111
 
 
2112
        state = self.create_empty_dirstate()
 
2113
        self.addCleanup(state.unlock)
 
2114
        state._set_data(['parent-id', 'merged-id'], dirblocks[:])
 
2115
        state._validate()
 
2116
 
 
2117
        exp_dirblocks = [('', [(root_key, [present_dir, present_dir])]),
 
2118
                         ('', [(file_in_root_key,
 
2119
                                [present_file, present_file]),
 
2120
                              ]),
 
2121
                        ]
 
2122
        state._discard_merge_parents()
 
2123
        state._validate()
 
2124
        self.assertEqual(exp_dirblocks, state._dirblocks)
 
2125
 
 
2126
    def test_discard_renamed(self):
 
2127
        null_stat = dirstate.DirState.NULLSTAT
 
2128
        present_dir = ('d', '', 0, False, null_stat)
 
2129
        present_file = ('f', '', 0, False, null_stat)
 
2130
        absent = dirstate.DirState.NULL_PARENT_DETAILS
 
2131
        root_key = ('', '', 'a-root-value')
 
2132
        file_in_root_key = ('', 'file-in-root', 'a-file-id')
 
2133
        # Renamed relative to parent
 
2134
        file_rename_s_key = ('', 'file-s', 'b-file-id')
 
2135
        file_rename_t_key = ('', 'file-t', 'b-file-id')
 
2136
        # And one that is renamed between the parents, but absent in this
 
2137
        key_in_1 = ('', 'file-in-1', 'c-file-id')
 
2138
        key_in_2 = ('', 'file-in-2', 'c-file-id')
 
2139
 
 
2140
        dirblocks = [
 
2141
            ('', [(root_key, [present_dir, present_dir, present_dir])]),
 
2142
            ('', [(key_in_1,
 
2143
                   [absent, present_file, ('r', 'file-in-2', 'c-file-id')]),
 
2144
                  (key_in_2,
 
2145
                   [absent, ('r', 'file-in-1', 'c-file-id'), present_file]),
 
2146
                  (file_in_root_key,
 
2147
                   [present_file, present_file, present_file]),
 
2148
                  (file_rename_s_key,
 
2149
                   [('r', 'file-t', 'b-file-id'), absent, present_file]),
 
2150
                  (file_rename_t_key,
 
2151
                   [present_file, absent, ('r', 'file-s', 'b-file-id')]),
 
2152
                 ]),
 
2153
        ]
 
2154
        exp_dirblocks = [
 
2155
            ('', [(root_key, [present_dir, present_dir])]),
 
2156
            ('', [(key_in_1, [absent, present_file]),
 
2157
                  (file_in_root_key, [present_file, present_file]),
 
2158
                  (file_rename_t_key, [present_file, absent]),
 
2159
                 ]),
 
2160
        ]
 
2161
        state = self.create_empty_dirstate()
 
2162
        self.addCleanup(state.unlock)
 
2163
        state._set_data(['parent-id', 'merged-id'], dirblocks[:])
 
2164
        state._validate()
 
2165
 
 
2166
        state._discard_merge_parents()
 
2167
        state._validate()
 
2168
        self.assertEqual(exp_dirblocks, state._dirblocks)
 
2169
 
 
2170
    def test_discard_all_subdir(self):
 
2171
        null_stat = dirstate.DirState.NULLSTAT
 
2172
        present_dir = ('d', '', 0, False, null_stat)
 
2173
        present_file = ('f', '', 0, False, null_stat)
 
2174
        absent = dirstate.DirState.NULL_PARENT_DETAILS
 
2175
        root_key = ('', '', 'a-root-value')
 
2176
        subdir_key = ('', 'sub', 'dir-id')
 
2177
        child1_key = ('sub', 'child1', 'child1-id')
 
2178
        child2_key = ('sub', 'child2', 'child2-id')
 
2179
        child3_key = ('sub', 'child3', 'child3-id')
 
2180
 
 
2181
        dirblocks = [
 
2182
            ('', [(root_key, [present_dir, present_dir, present_dir])]),
 
2183
            ('', [(subdir_key, [present_dir, present_dir, present_dir])]),
 
2184
            ('sub', [(child1_key, [absent, absent, present_file]),
 
2185
                     (child2_key, [absent, absent, present_file]),
 
2186
                     (child3_key, [absent, absent, present_file]),
 
2187
                    ]),
 
2188
        ]
 
2189
        exp_dirblocks = [
 
2190
            ('', [(root_key, [present_dir, present_dir])]),
 
2191
            ('', [(subdir_key, [present_dir, present_dir])]),
 
2192
            ('sub', []),
 
2193
        ]
 
2194
        state = self.create_empty_dirstate()
 
2195
        self.addCleanup(state.unlock)
 
2196
        state._set_data(['parent-id', 'merged-id'], dirblocks[:])
 
2197
        state._validate()
 
2198
 
 
2199
        state._discard_merge_parents()
 
2200
        state._validate()
 
2201
        self.assertEqual(exp_dirblocks, state._dirblocks)
 
2202
 
 
2203
 
 
2204
class Test_InvEntryToDetails(TestCaseWithDirState):
 
2205
 
 
2206
    def assertDetails(self, expected, inv_entry):
 
2207
        details = dirstate.DirState._inv_entry_to_details(inv_entry)
 
2208
        self.assertEqual(expected, details)
 
2209
        # details should always allow join() and always be a plain str when
 
2210
        # finished
 
2211
        (minikind, fingerprint, size, executable, tree_data) = details
 
2212
        self.assertIsInstance(minikind, str)
 
2213
        self.assertIsInstance(fingerprint, str)
 
2214
        self.assertIsInstance(tree_data, str)
 
2215
 
 
2216
    def test_unicode_symlink(self):
 
2217
        # In general, the code base doesn't support a target that contains
 
2218
        # non-ascii characters. So we just assert tha 
 
2219
        inv_entry = inventory.InventoryLink('link-file-id', 'name',
 
2220
                                            'link-parent-id')
 
2221
        inv_entry.revision = 'link-revision-id'
 
2222
        inv_entry.symlink_target = u'link-target'
 
2223
        details = self.assertDetails(('l', 'link-target', 0, False,
 
2224
                                      'link-revision-id'), inv_entry)