~bzr-pqm/bzr/bzr.dev

« back to all changes in this revision

Viewing changes to bzrlib/tests/test_dirstate.py

[merge] robert's knit-performance work

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')])