/brz/remove-bazaar

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

« back to all changes in this revision

Viewing changes to bzrlib/tests/test_dirstate.py

  • Committer: Vincent Ladeuil
  • Date: 2009-04-06 14:18:33 UTC
  • mto: (4241.10.1 bzr.1.14)
  • mto: This revision was merged to the branch mainline in revision 4267.
  • Revision ID: v.ladeuil+lp@free.fr-20090406141833-cboy3q70jn1xhfad
Add NEWS entry.

Show diffs side-by-side

added added

removed removed

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