~bzr-pqm/bzr/bzr.dev

« back to all changes in this revision

Viewing changes to bzrlib/tests/test_dirstate.py

  • Committer: Patch Queue Manager
  • Date: 2012-01-18 16:23:31 UTC
  • mfrom: (6439.1.1 work)
  • Revision ID: pqm@pqm.ubuntu.com-20120118162331-md4sf1tw6hyuw344
(vila) Ensure people get an easy access to the release details from
 announcements. (Vincent Ladeuil)

Show diffs side-by-side

added added

removed removed

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