~bzr-pqm/bzr/bzr.dev

« back to all changes in this revision

Viewing changes to bzrlib/tests/test_dirstate.py

  • Committer: Andrew Bennetts
  • Date: 2008-09-05 10:48:03 UTC
  • mto: This revision was merged to the branch mainline in revision 3693.
  • Revision ID: andrew.bennetts@canonical.com-20080905104803-6g72dz6wcldosfs2
Remove monkey-patching of branch._ensure_real from test_remote.py.

Show diffs side-by-side

added added

removed removed

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