/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 breezy/tests/test_dirstate.py

  • Committer: Jelmer Vernooij
  • Date: 2020-05-24 00:39:50 UTC
  • mto: This revision was merged to the branch mainline in revision 7504.
  • Revision ID: jelmer@jelmer.uk-20200524003950-bbc545r76vc5yajg
Add github action.

Show diffs side-by-side

added added

removed removed

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