/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: 2018-11-21 03:39:28 UTC
  • mto: This revision was merged to the branch mainline in revision 7206.
  • Revision ID: jelmer@jelmer.uk-20181121033928-ck4sb5zfdwosw35b
Fix test.

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.get_root_id()),  # 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.get_root_id()),  # 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.get_root_id()),  # 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.get_root_id()),  # 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.get_root_id()),  # 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.get_root_id()),  # 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.get_root_id()),  # 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.get_root_id()),  # 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.get_root_id()
 
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.get_root_id()
 
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.get_root_id()
 
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
        tree1.lock_write()
 
1341
        try:
 
1342
            tree1.add(['b'], [b'b-id'])
 
1343
            root_id = tree1.get_root_id()
 
1344
            inv = tree1.root_inventory
 
1345
            state = dirstate.DirState.initialize('dirstate')
 
1346
            try:
 
1347
                # Set the initial state with 'b'
 
1348
                state.set_state_from_inventory(inv)
 
1349
                inv.rename(b'b-id', root_id, 'a')
 
1350
                # Set the new state with 'a', which currently corrupts.
 
1351
                state.set_state_from_inventory(inv)
 
1352
                expected_result1 = [(b'', b'', root_id, b'd'),
 
1353
                                    (b'', b'a', b'b-id', b'f'),
 
1354
                                    ]
 
1355
                values = []
 
1356
                for entry in state._iter_entries():
 
1357
                    values.append(entry[0] + entry[1][0][:1])
 
1358
                self.assertEqual(expected_result1, values)
 
1359
            finally:
 
1360
                state.unlock()
 
1361
        finally:
 
1362
            tree1.unlock()
 
1363
 
 
1364
 
 
1365
class TestDirStateHashUpdates(TestCaseWithDirState):
 
1366
 
 
1367
    def do_update_entry(self, state, path):
 
1368
        entry = state._get_entry(0, path_utf8=path)
 
1369
        stat = os.lstat(path)
 
1370
        return dirstate.update_entry(state, entry, os.path.abspath(path), stat)
 
1371
 
 
1372
    def _read_state_content(self, state):
 
1373
        """Read the content of the dirstate file.
 
1374
 
 
1375
        On Windows when one process locks a file, you can't even open() the
 
1376
        file in another process (to read it). So we go directly to
 
1377
        state._state_file. This should always be the exact disk representation,
 
1378
        so it is reasonable to do so.
 
1379
        DirState also always seeks before reading, so it doesn't matter if we
 
1380
        bump the file pointer.
 
1381
        """
 
1382
        state._state_file.seek(0)
 
1383
        return state._state_file.read()
 
1384
 
 
1385
    def test_worth_saving_limit_avoids_writing(self):
 
1386
        tree = self.make_branch_and_tree('.')
 
1387
        self.build_tree(['c', 'd'])
 
1388
        tree.lock_write()
 
1389
        tree.add(['c', 'd'], [b'c-id', b'd-id'])
 
1390
        tree.commit('add c and d')
 
1391
        state = InstrumentedDirState.on_file(tree.current_dirstate()._filename,
 
1392
                                             worth_saving_limit=2)
 
1393
        tree.unlock()
 
1394
        state.lock_write()
 
1395
        self.addCleanup(state.unlock)
 
1396
        state._read_dirblocks_if_needed()
 
1397
        state.adjust_time(+20)  # Allow things to be cached
 
1398
        self.assertEqual(dirstate.DirState.IN_MEMORY_UNMODIFIED,
 
1399
                         state._dirblock_state)
 
1400
        content = self._read_state_content(state)
 
1401
        self.do_update_entry(state, b'c')
 
1402
        self.assertEqual(1, len(state._known_hash_changes))
 
1403
        self.assertEqual(dirstate.DirState.IN_MEMORY_HASH_MODIFIED,
 
1404
                         state._dirblock_state)
 
1405
        state.save()
 
1406
        # It should not have set the state to IN_MEMORY_UNMODIFIED because the
 
1407
        # hash values haven't been written out.
 
1408
        self.assertEqual(dirstate.DirState.IN_MEMORY_HASH_MODIFIED,
 
1409
                         state._dirblock_state)
 
1410
        self.assertEqual(content, self._read_state_content(state))
 
1411
        self.assertEqual(dirstate.DirState.IN_MEMORY_HASH_MODIFIED,
 
1412
                         state._dirblock_state)
 
1413
        self.do_update_entry(state, b'd')
 
1414
        self.assertEqual(2, len(state._known_hash_changes))
 
1415
        state.save()
 
1416
        self.assertEqual(dirstate.DirState.IN_MEMORY_UNMODIFIED,
 
1417
                         state._dirblock_state)
 
1418
        self.assertEqual(0, len(state._known_hash_changes))
 
1419
 
 
1420
 
 
1421
class TestGetLines(TestCaseWithDirState):
 
1422
 
 
1423
    def test_get_line_with_2_rows(self):
 
1424
        state = self.create_dirstate_with_root_and_subdir()
 
1425
        try:
 
1426
            self.assertEqual([b'#bazaar dirstate flat format 3\n',
 
1427
                              b'crc32: 41262208\n',
 
1428
                              b'num_entries: 2\n',
 
1429
                              b'0\x00\n\x00'
 
1430
                              b'0\x00\n\x00'
 
1431
                              b'\x00\x00a-root-value\x00'
 
1432
                              b'd\x00\x000\x00n\x00AAAAREUHaIpFB2iKAAADAQAtkqUAAIGk\x00\n\x00'
 
1433
                              b'\x00subdir\x00subdir-id\x00'
 
1434
                              b'd\x00\x000\x00n\x00AAAAREUHaIpFB2iKAAADAQAtkqUAAIGk\x00\n\x00'
 
1435
                              ], state.get_lines())
 
1436
        finally:
 
1437
            state.unlock()
 
1438
 
 
1439
    def test_entry_to_line(self):
 
1440
        state = self.create_dirstate_with_root()
 
1441
        try:
 
1442
            self.assertEqual(
 
1443
                b'\x00\x00a-root-value\x00d\x00\x000\x00n'
 
1444
                b'\x00AAAAREUHaIpFB2iKAAADAQAtkqUAAIGk',
 
1445
                state._entry_to_line(state._dirblocks[0][1][0]))
 
1446
        finally:
 
1447
            state.unlock()
 
1448
 
 
1449
    def test_entry_to_line_with_parent(self):
 
1450
        packed_stat = b'AAAAREUHaIpFB2iKAAADAQAtkqUAAIGk'
 
1451
        root_entry = (b'', b'', b'a-root-value'), [
 
1452
            (b'd', b'', 0, False, packed_stat),  # current tree details
 
1453
            # first: a pointer to the current location
 
1454
            (b'a', b'dirname/basename', 0, False, b''),
 
1455
            ]
 
1456
        state = dirstate.DirState.initialize('dirstate')
 
1457
        try:
 
1458
            self.assertEqual(
 
1459
                b'\x00\x00a-root-value\x00'
 
1460
                b'd\x00\x000\x00n\x00AAAAREUHaIpFB2iKAAADAQAtkqUAAIGk\x00'
 
1461
                b'a\x00dirname/basename\x000\x00n\x00',
 
1462
                state._entry_to_line(root_entry))
 
1463
        finally:
 
1464
            state.unlock()
 
1465
 
 
1466
    def test_entry_to_line_with_two_parents_at_different_paths(self):
 
1467
        # / in the tree, at / in one parent and /dirname/basename in the other.
 
1468
        packed_stat = b'AAAAREUHaIpFB2iKAAADAQAtkqUAAIGk'
 
1469
        root_entry = (b'', b'', b'a-root-value'), [
 
1470
            (b'd', b'', 0, False, packed_stat),  # current tree details
 
1471
            (b'd', b'', 0, False, b'rev_id'),  # first parent details
 
1472
            # second: a pointer to the current location
 
1473
            (b'a', b'dirname/basename', 0, False, b''),
 
1474
            ]
 
1475
        state = dirstate.DirState.initialize('dirstate')
 
1476
        try:
 
1477
            self.assertEqual(
 
1478
                b'\x00\x00a-root-value\x00'
 
1479
                b'd\x00\x000\x00n\x00AAAAREUHaIpFB2iKAAADAQAtkqUAAIGk\x00'
 
1480
                b'd\x00\x000\x00n\x00rev_id\x00'
 
1481
                b'a\x00dirname/basename\x000\x00n\x00',
 
1482
                state._entry_to_line(root_entry))
 
1483
        finally:
 
1484
            state.unlock()
 
1485
 
 
1486
    def test_iter_entries(self):
 
1487
        # we should be able to iterate the dirstate entries from end to end
 
1488
        # this is for get_lines to be easy to read.
 
1489
        packed_stat = b'AAAAREUHaIpFB2iKAAADAQAtkqUAAIGk'
 
1490
        dirblocks = []
 
1491
        root_entries = [((b'', b'', b'a-root-value'), [
 
1492
            (b'd', b'', 0, False, packed_stat),  # current tree details
 
1493
            ])]
 
1494
        dirblocks.append(('', root_entries))
 
1495
        # add two files in the root
 
1496
        subdir_entry = (b'', b'subdir', b'subdir-id'), [
 
1497
            (b'd', b'', 0, False, packed_stat),  # current tree details
 
1498
            ]
 
1499
        afile_entry = (b'', b'afile', b'afile-id'), [
 
1500
            (b'f', b'sha1value', 34, False, packed_stat),  # current tree details
 
1501
            ]
 
1502
        dirblocks.append(('', [subdir_entry, afile_entry]))
 
1503
        # and one in subdir
 
1504
        file_entry2 = (b'subdir', b'2file', b'2file-id'), [
 
1505
            (b'f', b'sha1value', 23, False, packed_stat),  # current tree details
 
1506
            ]
 
1507
        dirblocks.append(('subdir', [file_entry2]))
 
1508
        state = dirstate.DirState.initialize('dirstate')
 
1509
        try:
 
1510
            state._set_data([], dirblocks)
 
1511
            expected_entries = [root_entries[0], subdir_entry, afile_entry,
 
1512
                                file_entry2]
 
1513
            self.assertEqual(expected_entries, list(state._iter_entries()))
 
1514
        finally:
 
1515
            state.unlock()
 
1516
 
 
1517
 
 
1518
class TestGetBlockRowIndex(TestCaseWithDirState):
 
1519
 
 
1520
    def assertBlockRowIndexEqual(self, block_index, row_index, dir_present,
 
1521
                                 file_present, state, dirname, basename, tree_index):
 
1522
        self.assertEqual((block_index, row_index, dir_present, file_present),
 
1523
                         state._get_block_entry_index(dirname, basename, tree_index))
 
1524
        if dir_present:
 
1525
            block = state._dirblocks[block_index]
 
1526
            self.assertEqual(dirname, block[0])
 
1527
        if dir_present and file_present:
 
1528
            row = state._dirblocks[block_index][1][row_index]
 
1529
            self.assertEqual(dirname, row[0][0])
 
1530
            self.assertEqual(basename, row[0][1])
 
1531
 
 
1532
    def test_simple_structure(self):
 
1533
        state = self.create_dirstate_with_root_and_subdir()
 
1534
        self.addCleanup(state.unlock)
 
1535
        self.assertBlockRowIndexEqual(
 
1536
            1, 0, True, True, state, b'', b'subdir', 0)
 
1537
        self.assertBlockRowIndexEqual(
 
1538
            1, 0, True, False, state, b'', b'bdir', 0)
 
1539
        self.assertBlockRowIndexEqual(
 
1540
            1, 1, True, False, state, b'', b'zdir', 0)
 
1541
        self.assertBlockRowIndexEqual(
 
1542
            2, 0, False, False, state, b'a', b'foo', 0)
 
1543
        self.assertBlockRowIndexEqual(2, 0, False, False, state,
 
1544
                                      b'subdir', b'foo', 0)
 
1545
 
 
1546
    def test_complex_structure_exists(self):
 
1547
        state = self.create_complex_dirstate()
 
1548
        self.addCleanup(state.unlock)
 
1549
        # Make sure we can find everything that exists
 
1550
        self.assertBlockRowIndexEqual(0, 0, True, True, state, b'', b'', 0)
 
1551
        self.assertBlockRowIndexEqual(1, 0, True, True, state, b'', b'a', 0)
 
1552
        self.assertBlockRowIndexEqual(1, 1, True, True, state, b'', b'b', 0)
 
1553
        self.assertBlockRowIndexEqual(1, 2, True, True, state, b'', b'c', 0)
 
1554
        self.assertBlockRowIndexEqual(1, 3, True, True, state, b'', b'd', 0)
 
1555
        self.assertBlockRowIndexEqual(2, 0, True, True, state, b'a', b'e', 0)
 
1556
        self.assertBlockRowIndexEqual(2, 1, True, True, state, b'a', b'f', 0)
 
1557
        self.assertBlockRowIndexEqual(3, 0, True, True, state, b'b', b'g', 0)
 
1558
        self.assertBlockRowIndexEqual(3, 1, True, True, state,
 
1559
                                      b'b', b'h\xc3\xa5', 0)
 
1560
 
 
1561
    def test_complex_structure_missing(self):
 
1562
        state = self.create_complex_dirstate()
 
1563
        self.addCleanup(state.unlock)
 
1564
        # Make sure things would be inserted in the right locations
 
1565
        # '_' comes before 'a'
 
1566
        self.assertBlockRowIndexEqual(0, 0, True, True, state, b'', b'', 0)
 
1567
        self.assertBlockRowIndexEqual(1, 0, True, False, state, b'', b'_', 0)
 
1568
        self.assertBlockRowIndexEqual(1, 1, True, False, state, b'', b'aa', 0)
 
1569
        self.assertBlockRowIndexEqual(1, 4, True, False, state,
 
1570
                                      b'', b'h\xc3\xa5', 0)
 
1571
        self.assertBlockRowIndexEqual(2, 0, False, False, state, b'_', b'a', 0)
 
1572
        self.assertBlockRowIndexEqual(
 
1573
            3, 0, False, False, state, b'aa', b'a', 0)
 
1574
        self.assertBlockRowIndexEqual(
 
1575
            4, 0, False, False, state, b'bb', b'a', 0)
 
1576
        # This would be inserted between a/ and b/
 
1577
        self.assertBlockRowIndexEqual(
 
1578
            3, 0, False, False, state, b'a/e', b'a', 0)
 
1579
        # Put at the end
 
1580
        self.assertBlockRowIndexEqual(4, 0, False, False, state, b'e', b'a', 0)
 
1581
 
 
1582
 
 
1583
class TestGetEntry(TestCaseWithDirState):
 
1584
 
 
1585
    def assertEntryEqual(self, dirname, basename, file_id, state, path, index):
 
1586
        """Check that the right entry is returned for a request to getEntry."""
 
1587
        entry = state._get_entry(index, path_utf8=path)
 
1588
        if file_id is None:
 
1589
            self.assertEqual((None, None), entry)
 
1590
        else:
 
1591
            cur = entry[0]
 
1592
            self.assertEqual((dirname, basename, file_id), cur[:3])
 
1593
 
 
1594
    def test_simple_structure(self):
 
1595
        state = self.create_dirstate_with_root_and_subdir()
 
1596
        self.addCleanup(state.unlock)
 
1597
        self.assertEntryEqual(b'', b'', b'a-root-value', state, b'', 0)
 
1598
        self.assertEntryEqual(
 
1599
            b'', b'subdir', b'subdir-id', state, b'subdir', 0)
 
1600
        self.assertEntryEqual(None, None, None, state, b'missing', 0)
 
1601
        self.assertEntryEqual(None, None, None, state, b'missing/foo', 0)
 
1602
        self.assertEntryEqual(None, None, None, state, b'subdir/foo', 0)
 
1603
 
 
1604
    def test_complex_structure_exists(self):
 
1605
        state = self.create_complex_dirstate()
 
1606
        self.addCleanup(state.unlock)
 
1607
        self.assertEntryEqual(b'', b'', b'a-root-value', state, b'', 0)
 
1608
        self.assertEntryEqual(b'', b'a', b'a-dir', state, b'a', 0)
 
1609
        self.assertEntryEqual(b'', b'b', b'b-dir', state, b'b', 0)
 
1610
        self.assertEntryEqual(b'', b'c', b'c-file', state, b'c', 0)
 
1611
        self.assertEntryEqual(b'', b'd', b'd-file', state, b'd', 0)
 
1612
        self.assertEntryEqual(b'a', b'e', b'e-dir', state, b'a/e', 0)
 
1613
        self.assertEntryEqual(b'a', b'f', b'f-file', state, b'a/f', 0)
 
1614
        self.assertEntryEqual(b'b', b'g', b'g-file', state, b'b/g', 0)
 
1615
        self.assertEntryEqual(b'b', b'h\xc3\xa5', b'h-\xc3\xa5-file', state,
 
1616
                              b'b/h\xc3\xa5', 0)
 
1617
 
 
1618
    def test_complex_structure_missing(self):
 
1619
        state = self.create_complex_dirstate()
 
1620
        self.addCleanup(state.unlock)
 
1621
        self.assertEntryEqual(None, None, None, state, b'_', 0)
 
1622
        self.assertEntryEqual(None, None, None, state, b'_\xc3\xa5', 0)
 
1623
        self.assertEntryEqual(None, None, None, state, b'a/b', 0)
 
1624
        self.assertEntryEqual(None, None, None, state, b'c/d', 0)
 
1625
 
 
1626
    def test_get_entry_uninitialized(self):
 
1627
        """Calling get_entry will load data if it needs to"""
 
1628
        state = self.create_dirstate_with_root()
 
1629
        try:
 
1630
            state.save()
 
1631
        finally:
 
1632
            state.unlock()
 
1633
        del state
 
1634
        state = dirstate.DirState.on_file('dirstate')
 
1635
        state.lock_read()
 
1636
        try:
 
1637
            self.assertEqual(dirstate.DirState.NOT_IN_MEMORY,
 
1638
                             state._header_state)
 
1639
            self.assertEqual(dirstate.DirState.NOT_IN_MEMORY,
 
1640
                             state._dirblock_state)
 
1641
            self.assertEntryEqual(b'', b'', b'a-root-value', state, b'', 0)
 
1642
        finally:
 
1643
            state.unlock()
 
1644
 
 
1645
 
 
1646
class TestIterChildEntries(TestCaseWithDirState):
 
1647
 
 
1648
    def create_dirstate_with_two_trees(self):
 
1649
        """This dirstate contains multiple files and directories.
 
1650
 
 
1651
         /        a-root-value
 
1652
         a/       a-dir
 
1653
         b/       b-dir
 
1654
         c        c-file
 
1655
         d        d-file
 
1656
         a/e/     e-dir
 
1657
         a/f      f-file
 
1658
         b/g      g-file
 
1659
         b/h\xc3\xa5  h-\xc3\xa5-file  #This is u'\xe5' encoded into utf-8
 
1660
 
 
1661
        Notice that a/e is an empty directory.
 
1662
 
 
1663
        There is one parent tree, which has the same shape with the following variations:
 
1664
        b/g in the parent is gone.
 
1665
        b/h in the parent has a different id
 
1666
        b/i is new in the parent
 
1667
        c is renamed to b/j in the parent
 
1668
 
 
1669
        :return: The dirstate, still write-locked.
 
1670
        """
 
1671
        packed_stat = b'AAAAREUHaIpFB2iKAAADAQAtkqUAAIGk'
 
1672
        null_sha = b'xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx'
 
1673
        NULL_PARENT_DETAILS = dirstate.DirState.NULL_PARENT_DETAILS
 
1674
        root_entry = (b'', b'', b'a-root-value'), [
 
1675
            (b'd', b'', 0, False, packed_stat),
 
1676
            (b'd', b'', 0, False, b'parent-revid'),
 
1677
            ]
 
1678
        a_entry = (b'', b'a', b'a-dir'), [
 
1679
            (b'd', b'', 0, False, packed_stat),
 
1680
            (b'd', b'', 0, False, b'parent-revid'),
 
1681
            ]
 
1682
        b_entry = (b'', b'b', b'b-dir'), [
 
1683
            (b'd', b'', 0, False, packed_stat),
 
1684
            (b'd', b'', 0, False, b'parent-revid'),
 
1685
            ]
 
1686
        c_entry = (b'', b'c', b'c-file'), [
 
1687
            (b'f', null_sha, 10, False, packed_stat),
 
1688
            (b'r', b'b/j', 0, False, b''),
 
1689
            ]
 
1690
        d_entry = (b'', b'd', b'd-file'), [
 
1691
            (b'f', null_sha, 20, False, packed_stat),
 
1692
            (b'f', b'd', 20, False, b'parent-revid'),
 
1693
            ]
 
1694
        e_entry = (b'a', b'e', b'e-dir'), [
 
1695
            (b'd', b'', 0, False, packed_stat),
 
1696
            (b'd', b'', 0, False, b'parent-revid'),
 
1697
            ]
 
1698
        f_entry = (b'a', b'f', b'f-file'), [
 
1699
            (b'f', null_sha, 30, False, packed_stat),
 
1700
            (b'f', b'f', 20, False, b'parent-revid'),
 
1701
            ]
 
1702
        g_entry = (b'b', b'g', b'g-file'), [
 
1703
            (b'f', null_sha, 30, False, packed_stat),
 
1704
            NULL_PARENT_DETAILS,
 
1705
            ]
 
1706
        h_entry1 = (b'b', b'h\xc3\xa5', b'h-\xc3\xa5-file1'), [
 
1707
            (b'f', null_sha, 40, False, packed_stat),
 
1708
            NULL_PARENT_DETAILS,
 
1709
            ]
 
1710
        h_entry2 = (b'b', b'h\xc3\xa5', b'h-\xc3\xa5-file2'), [
 
1711
            NULL_PARENT_DETAILS,
 
1712
            (b'f', b'h', 20, False, b'parent-revid'),
 
1713
            ]
 
1714
        i_entry = (b'b', b'i', b'i-file'), [
 
1715
            NULL_PARENT_DETAILS,
 
1716
            (b'f', b'h', 20, False, b'parent-revid'),
 
1717
            ]
 
1718
        j_entry = (b'b', b'j', b'c-file'), [
 
1719
            (b'r', b'c', 0, False, b''),
 
1720
            (b'f', b'j', 20, False, b'parent-revid'),
 
1721
            ]
 
1722
        dirblocks = []
 
1723
        dirblocks.append((b'', [root_entry]))
 
1724
        dirblocks.append((b'', [a_entry, b_entry, c_entry, d_entry]))
 
1725
        dirblocks.append((b'a', [e_entry, f_entry]))
 
1726
        dirblocks.append(
 
1727
            (b'b', [g_entry, h_entry1, h_entry2, i_entry, j_entry]))
 
1728
        state = dirstate.DirState.initialize('dirstate')
 
1729
        state._validate()
 
1730
        try:
 
1731
            state._set_data([b'parent'], dirblocks)
 
1732
        except:
 
1733
            state.unlock()
 
1734
            raise
 
1735
        return state, dirblocks
 
1736
 
 
1737
    def test_iter_children_b(self):
 
1738
        state, dirblocks = self.create_dirstate_with_two_trees()
 
1739
        self.addCleanup(state.unlock)
 
1740
        expected_result = []
 
1741
        expected_result.append(dirblocks[3][1][2])  # h2
 
1742
        expected_result.append(dirblocks[3][1][3])  # i
 
1743
        expected_result.append(dirblocks[3][1][4])  # j
 
1744
        self.assertEqual(expected_result,
 
1745
                         list(state._iter_child_entries(1, b'b')))
 
1746
 
 
1747
    def test_iter_child_root(self):
 
1748
        state, dirblocks = self.create_dirstate_with_two_trees()
 
1749
        self.addCleanup(state.unlock)
 
1750
        expected_result = []
 
1751
        expected_result.append(dirblocks[1][1][0])  # a
 
1752
        expected_result.append(dirblocks[1][1][1])  # b
 
1753
        expected_result.append(dirblocks[1][1][3])  # d
 
1754
        expected_result.append(dirblocks[2][1][0])  # e
 
1755
        expected_result.append(dirblocks[2][1][1])  # f
 
1756
        expected_result.append(dirblocks[3][1][2])  # h2
 
1757
        expected_result.append(dirblocks[3][1][3])  # i
 
1758
        expected_result.append(dirblocks[3][1][4])  # j
 
1759
        self.assertEqual(expected_result,
 
1760
                         list(state._iter_child_entries(1, b'')))
 
1761
 
 
1762
 
 
1763
class TestDirstateSortOrder(tests.TestCaseWithTransport):
 
1764
    """Test that DirState adds entries in the right order."""
 
1765
 
 
1766
    def test_add_sorting(self):
 
1767
        """Add entries in lexicographical order, we get path sorted order.
 
1768
 
 
1769
        This tests it to a depth of 4, to make sure we don't just get it right
 
1770
        at a single depth. 'a/a' should come before 'a-a', even though it
 
1771
        doesn't lexicographically.
 
1772
        """
 
1773
        dirs = ['a', 'a/a', 'a/a/a', 'a/a/a/a',
 
1774
                'a-a', 'a/a-a', 'a/a/a-a', 'a/a/a/a-a',
 
1775
                ]
 
1776
        null_sha = b''
 
1777
        state = dirstate.DirState.initialize('dirstate')
 
1778
        self.addCleanup(state.unlock)
 
1779
 
 
1780
        fake_stat = os.stat('dirstate')
 
1781
        for d in dirs:
 
1782
            d_id = d.encode('utf-8').replace(b'/', b'_') + b'-id'
 
1783
            file_path = d + '/f'
 
1784
            file_id = file_path.encode('utf-8').replace(b'/', b'_') + b'-id'
 
1785
            state.add(d, d_id, 'directory', fake_stat, null_sha)
 
1786
            state.add(file_path, file_id, 'file', fake_stat, null_sha)
 
1787
 
 
1788
        expected = [b'', b'', b'a',
 
1789
                    b'a/a', b'a/a/a', b'a/a/a/a',
 
1790
                    b'a/a/a/a-a', b'a/a/a-a', b'a/a-a', b'a-a',
 
1791
                    ]
 
1792
 
 
1793
        def split(p): return p.split(b'/')
 
1794
        self.assertEqual(sorted(expected, key=split), expected)
 
1795
        dirblock_names = [d[0] for d in state._dirblocks]
 
1796
        self.assertEqual(expected, dirblock_names)
 
1797
 
 
1798
    def test_set_parent_trees_correct_order(self):
 
1799
        """After calling set_parent_trees() we should maintain the order."""
 
1800
        dirs = ['a', 'a-a', 'a/a']
 
1801
        null_sha = b''
 
1802
        state = dirstate.DirState.initialize('dirstate')
 
1803
        self.addCleanup(state.unlock)
 
1804
 
 
1805
        fake_stat = os.stat('dirstate')
 
1806
        for d in dirs:
 
1807
            d_id = d.encode('utf-8').replace(b'/', b'_') + b'-id'
 
1808
            file_path = d + '/f'
 
1809
            file_id = file_path.encode('utf-8').replace(b'/', b'_') + b'-id'
 
1810
            state.add(d, d_id, 'directory', fake_stat, null_sha)
 
1811
            state.add(file_path, file_id, 'file', fake_stat, null_sha)
 
1812
 
 
1813
        expected = [b'', b'', b'a', b'a/a', b'a-a']
 
1814
        dirblock_names = [d[0] for d in state._dirblocks]
 
1815
        self.assertEqual(expected, dirblock_names)
 
1816
 
 
1817
        # *really* cheesy way to just get an empty tree
 
1818
        repo = self.make_repository('repo')
 
1819
        empty_tree = repo.revision_tree(_mod_revision.NULL_REVISION)
 
1820
        state.set_parent_trees([('null:', empty_tree)], [])
 
1821
 
 
1822
        dirblock_names = [d[0] for d in state._dirblocks]
 
1823
        self.assertEqual(expected, dirblock_names)
 
1824
 
 
1825
 
 
1826
class InstrumentedDirState(dirstate.DirState):
 
1827
    """An DirState with instrumented sha1 functionality."""
 
1828
 
 
1829
    def __init__(self, path, sha1_provider, worth_saving_limit=0):
 
1830
        super(InstrumentedDirState, self).__init__(path, sha1_provider,
 
1831
                                                   worth_saving_limit=worth_saving_limit)
 
1832
        self._time_offset = 0
 
1833
        self._log = []
 
1834
        # member is dynamically set in DirState.__init__ to turn on trace
 
1835
        self._sha1_provider = sha1_provider
 
1836
        self._sha1_file = self._sha1_file_and_log
 
1837
 
 
1838
    def _sha_cutoff_time(self):
 
1839
        timestamp = super(InstrumentedDirState, self)._sha_cutoff_time()
 
1840
        self._cutoff_time = timestamp + self._time_offset
 
1841
 
 
1842
    def _sha1_file_and_log(self, abspath):
 
1843
        self._log.append(('sha1', abspath))
 
1844
        return self._sha1_provider.sha1(abspath)
 
1845
 
 
1846
    def _read_link(self, abspath, old_link):
 
1847
        self._log.append(('read_link', abspath, old_link))
 
1848
        return super(InstrumentedDirState, self)._read_link(abspath, old_link)
 
1849
 
 
1850
    def _lstat(self, abspath, entry):
 
1851
        self._log.append(('lstat', abspath))
 
1852
        return super(InstrumentedDirState, self)._lstat(abspath, entry)
 
1853
 
 
1854
    def _is_executable(self, mode, old_executable):
 
1855
        self._log.append(('is_exec', mode, old_executable))
 
1856
        return super(InstrumentedDirState, self)._is_executable(mode,
 
1857
                                                                old_executable)
 
1858
 
 
1859
    def adjust_time(self, secs):
 
1860
        """Move the clock forward or back.
 
1861
 
 
1862
        :param secs: The amount to adjust the clock by. Positive values make it
 
1863
        seem as if we are in the future, negative values make it seem like we
 
1864
        are in the past.
 
1865
        """
 
1866
        self._time_offset += secs
 
1867
        self._cutoff_time = None
 
1868
 
 
1869
 
 
1870
class _FakeStat(object):
 
1871
    """A class with the same attributes as a real stat result."""
 
1872
 
 
1873
    def __init__(self, size, mtime, ctime, dev, ino, mode):
 
1874
        self.st_size = size
 
1875
        self.st_mtime = mtime
 
1876
        self.st_ctime = ctime
 
1877
        self.st_dev = dev
 
1878
        self.st_ino = ino
 
1879
        self.st_mode = mode
 
1880
 
 
1881
    @staticmethod
 
1882
    def from_stat(st):
 
1883
        return _FakeStat(st.st_size, st.st_mtime, st.st_ctime, st.st_dev,
 
1884
                         st.st_ino, st.st_mode)
 
1885
 
 
1886
 
 
1887
class TestPackStat(tests.TestCaseWithTransport):
 
1888
 
 
1889
    def assertPackStat(self, expected, stat_value):
 
1890
        """Check the packed and serialized form of a stat value."""
 
1891
        self.assertEqual(expected, dirstate.pack_stat(stat_value))
 
1892
 
 
1893
    def test_pack_stat_int(self):
 
1894
        st = _FakeStat(6859, 1172758614, 1172758617, 777, 6499538, 0o100644)
 
1895
        # Make sure that all parameters have an impact on the packed stat.
 
1896
        self.assertPackStat(b'AAAay0Xm4FZF5uBZAAADCQBjLNIAAIGk', st)
 
1897
        st.st_size = 7000
 
1898
        #                ay0 => bWE
 
1899
        self.assertPackStat(b'AAAbWEXm4FZF5uBZAAADCQBjLNIAAIGk', st)
 
1900
        st.st_mtime = 1172758620
 
1901
        #                     4FZ => 4Fx
 
1902
        self.assertPackStat(b'AAAbWEXm4FxF5uBZAAADCQBjLNIAAIGk', st)
 
1903
        st.st_ctime = 1172758630
 
1904
        #                          uBZ => uBm
 
1905
        self.assertPackStat(b'AAAbWEXm4FxF5uBmAAADCQBjLNIAAIGk', st)
 
1906
        st.st_dev = 888
 
1907
        #                                DCQ => DeA
 
1908
        self.assertPackStat(b'AAAbWEXm4FxF5uBmAAADeABjLNIAAIGk', st)
 
1909
        st.st_ino = 6499540
 
1910
        #                                     LNI => LNQ
 
1911
        self.assertPackStat(b'AAAbWEXm4FxF5uBmAAADeABjLNQAAIGk', st)
 
1912
        st.st_mode = 0o100744
 
1913
        #                                          IGk => IHk
 
1914
        self.assertPackStat(b'AAAbWEXm4FxF5uBmAAADeABjLNQAAIHk', st)
 
1915
 
 
1916
    def test_pack_stat_float(self):
 
1917
        """On some platforms mtime and ctime are floats.
 
1918
 
 
1919
        Make sure we don't get warnings or errors, and that we ignore changes <
 
1920
        1s
 
1921
        """
 
1922
        st = _FakeStat(7000, 1172758614.0, 1172758617.0,
 
1923
                       777, 6499538, 0o100644)
 
1924
        # These should all be the same as the integer counterparts
 
1925
        self.assertPackStat(b'AAAbWEXm4FZF5uBZAAADCQBjLNIAAIGk', st)
 
1926
        st.st_mtime = 1172758620.0
 
1927
        #                     FZF5 => FxF5
 
1928
        self.assertPackStat(b'AAAbWEXm4FxF5uBZAAADCQBjLNIAAIGk', st)
 
1929
        st.st_ctime = 1172758630.0
 
1930
        #                          uBZ => uBm
 
1931
        self.assertPackStat(b'AAAbWEXm4FxF5uBmAAADCQBjLNIAAIGk', st)
 
1932
        # fractional seconds are discarded, so no change from above
 
1933
        st.st_mtime = 1172758620.453
 
1934
        self.assertPackStat(b'AAAbWEXm4FxF5uBmAAADCQBjLNIAAIGk', st)
 
1935
        st.st_ctime = 1172758630.228
 
1936
        self.assertPackStat(b'AAAbWEXm4FxF5uBmAAADCQBjLNIAAIGk', st)
 
1937
 
 
1938
 
 
1939
class TestBisect(TestCaseWithDirState):
 
1940
    """Test the ability to bisect into the disk format."""
 
1941
 
 
1942
    def assertBisect(self, expected_map, map_keys, state, paths):
 
1943
        """Assert that bisecting for paths returns the right result.
 
1944
 
 
1945
        :param expected_map: A map from key => entry value
 
1946
        :param map_keys: The keys to expect for each path
 
1947
        :param state: The DirState object.
 
1948
        :param paths: A list of paths, these will automatically be split into
 
1949
                      (dir, name) tuples, and sorted according to how _bisect
 
1950
                      requires.
 
1951
        """
 
1952
        result = state._bisect(paths)
 
1953
        # For now, results are just returned in whatever order we read them.
 
1954
        # We could sort by (dir, name, file_id) or something like that, but in
 
1955
        # the end it would still be fairly arbitrary, and we don't want the
 
1956
        # extra overhead if we can avoid it. So sort everything to make sure
 
1957
        # equality is true
 
1958
        self.assertEqual(len(map_keys), len(paths))
 
1959
        expected = {}
 
1960
        for path, keys in zip(paths, map_keys):
 
1961
            if keys is None:
 
1962
                # This should not be present in the output
 
1963
                continue
 
1964
            expected[path] = sorted(expected_map[k] for k in keys)
 
1965
 
 
1966
        # The returned values are just arranged randomly based on when they
 
1967
        # were read, for testing, make sure it is properly sorted.
 
1968
        for path in result:
 
1969
            result[path].sort()
 
1970
 
 
1971
        self.assertEqual(expected, result)
 
1972
 
 
1973
    def assertBisectDirBlocks(self, expected_map, map_keys, state, paths):
 
1974
        """Assert that bisecting for dirbblocks returns the right result.
 
1975
 
 
1976
        :param expected_map: A map from key => expected values
 
1977
        :param map_keys: A nested list of paths we expect to be returned.
 
1978
            Something like [['a', 'b', 'f'], ['b/c', 'b/d']]
 
1979
        :param state: The DirState object.
 
1980
        :param paths: A list of directories
 
1981
        """
 
1982
        result = state._bisect_dirblocks(paths)
 
1983
        self.assertEqual(len(map_keys), len(paths))
 
1984
        expected = {}
 
1985
        for path, keys in zip(paths, map_keys):
 
1986
            if keys is None:
 
1987
                # This should not be present in the output
 
1988
                continue
 
1989
            expected[path] = sorted(expected_map[k] for k in keys)
 
1990
        for path in result:
 
1991
            result[path].sort()
 
1992
 
 
1993
        self.assertEqual(expected, result)
 
1994
 
 
1995
    def assertBisectRecursive(self, expected_map, map_keys, state, paths):
 
1996
        """Assert the return value of a recursive bisection.
 
1997
 
 
1998
        :param expected_map: A map from key => entry value
 
1999
        :param map_keys: A list of paths we expect to be returned.
 
2000
            Something like ['a', 'b', 'f', 'b/d', 'b/d2']
 
2001
        :param state: The DirState object.
 
2002
        :param paths: A list of files and directories. It will be broken up
 
2003
            into (dir, name) pairs and sorted before calling _bisect_recursive.
 
2004
        """
 
2005
        expected = {}
 
2006
        for key in map_keys:
 
2007
            entry = expected_map[key]
 
2008
            dir_name_id, trees_info = entry
 
2009
            expected[dir_name_id] = trees_info
 
2010
 
 
2011
        result = state._bisect_recursive(paths)
 
2012
 
 
2013
        self.assertEqual(expected, result)
 
2014
 
 
2015
    def test_bisect_each(self):
 
2016
        """Find a single record using bisect."""
 
2017
        tree, state, expected = self.create_basic_dirstate()
 
2018
 
 
2019
        # Bisect should return the rows for the specified files.
 
2020
        self.assertBisect(expected, [[b'']], state, [b''])
 
2021
        self.assertBisect(expected, [[b'a']], state, [b'a'])
 
2022
        self.assertBisect(expected, [[b'b']], state, [b'b'])
 
2023
        self.assertBisect(expected, [[b'b/c']], state, [b'b/c'])
 
2024
        self.assertBisect(expected, [[b'b/d']], state, [b'b/d'])
 
2025
        self.assertBisect(expected, [[b'b/d/e']], state, [b'b/d/e'])
 
2026
        self.assertBisect(expected, [[b'b-c']], state, [b'b-c'])
 
2027
        self.assertBisect(expected, [[b'f']], state, [b'f'])
 
2028
 
 
2029
    def test_bisect_multi(self):
 
2030
        """Bisect can be used to find multiple records at the same time."""
 
2031
        tree, state, expected = self.create_basic_dirstate()
 
2032
        # Bisect should be capable of finding multiple entries at the same time
 
2033
        self.assertBisect(expected, [[b'a'], [b'b'], [b'f']],
 
2034
                          state, [b'a', b'b', b'f'])
 
2035
        self.assertBisect(expected, [[b'f'], [b'b/d'], [b'b/d/e']],
 
2036
                          state, [b'f', b'b/d', b'b/d/e'])
 
2037
        self.assertBisect(expected, [[b'b'], [b'b-c'], [b'b/c']],
 
2038
                          state, [b'b', b'b-c', b'b/c'])
 
2039
 
 
2040
    def test_bisect_one_page(self):
 
2041
        """Test bisect when there is only 1 page to read"""
 
2042
        tree, state, expected = self.create_basic_dirstate()
 
2043
        state._bisect_page_size = 5000
 
2044
        self.assertBisect(expected, [[b'']], state, [b''])
 
2045
        self.assertBisect(expected, [[b'a']], state, [b'a'])
 
2046
        self.assertBisect(expected, [[b'b']], state, [b'b'])
 
2047
        self.assertBisect(expected, [[b'b/c']], state, [b'b/c'])
 
2048
        self.assertBisect(expected, [[b'b/d']], state, [b'b/d'])
 
2049
        self.assertBisect(expected, [[b'b/d/e']], state, [b'b/d/e'])
 
2050
        self.assertBisect(expected, [[b'b-c']], state, [b'b-c'])
 
2051
        self.assertBisect(expected, [[b'f']], state, [b'f'])
 
2052
        self.assertBisect(expected, [[b'a'], [b'b'], [b'f']],
 
2053
                          state, [b'a', b'b', b'f'])
 
2054
        self.assertBisect(expected, [[b'b/d'], [b'b/d/e'], [b'f']],
 
2055
                          state, [b'b/d', b'b/d/e', b'f'])
 
2056
        self.assertBisect(expected, [[b'b'], [b'b/c'], [b'b-c']],
 
2057
                          state, [b'b', b'b/c', b'b-c'])
 
2058
 
 
2059
    def test_bisect_duplicate_paths(self):
 
2060
        """When bisecting for a path, handle multiple entries."""
 
2061
        tree, state, expected = self.create_duplicated_dirstate()
 
2062
 
 
2063
        # Now make sure that both records are properly returned.
 
2064
        self.assertBisect(expected, [[b'']], state, [b''])
 
2065
        self.assertBisect(expected, [[b'a', b'a2']], state, [b'a'])
 
2066
        self.assertBisect(expected, [[b'b', b'b2']], state, [b'b'])
 
2067
        self.assertBisect(expected, [[b'b/c', b'b/c2']], state, [b'b/c'])
 
2068
        self.assertBisect(expected, [[b'b/d', b'b/d2']], state, [b'b/d'])
 
2069
        self.assertBisect(expected, [[b'b/d/e', b'b/d/e2']],
 
2070
                          state, [b'b/d/e'])
 
2071
        self.assertBisect(expected, [[b'b-c', b'b-c2']], state, [b'b-c'])
 
2072
        self.assertBisect(expected, [[b'f', b'f2']], state, [b'f'])
 
2073
 
 
2074
    def test_bisect_page_size_too_small(self):
 
2075
        """If the page size is too small, we will auto increase it."""
 
2076
        tree, state, expected = self.create_basic_dirstate()
 
2077
        state._bisect_page_size = 50
 
2078
        self.assertBisect(expected, [None], state, [b'b/e'])
 
2079
        self.assertBisect(expected, [[b'a']], state, [b'a'])
 
2080
        self.assertBisect(expected, [[b'b']], state, [b'b'])
 
2081
        self.assertBisect(expected, [[b'b/c']], state, [b'b/c'])
 
2082
        self.assertBisect(expected, [[b'b/d']], state, [b'b/d'])
 
2083
        self.assertBisect(expected, [[b'b/d/e']], state, [b'b/d/e'])
 
2084
        self.assertBisect(expected, [[b'b-c']], state, [b'b-c'])
 
2085
        self.assertBisect(expected, [[b'f']], state, [b'f'])
 
2086
 
 
2087
    def test_bisect_missing(self):
 
2088
        """Test that bisect return None if it cannot find a path."""
 
2089
        tree, state, expected = self.create_basic_dirstate()
 
2090
        self.assertBisect(expected, [None], state, [b'foo'])
 
2091
        self.assertBisect(expected, [None], state, [b'b/foo'])
 
2092
        self.assertBisect(expected, [None], state, [b'bar/foo'])
 
2093
        self.assertBisect(expected, [None], state, [b'b-c/foo'])
 
2094
 
 
2095
        self.assertBisect(expected, [[b'a'], None, [b'b/d']],
 
2096
                          state, [b'a', b'foo', b'b/d'])
 
2097
 
 
2098
    def test_bisect_rename(self):
 
2099
        """Check that we find a renamed row."""
 
2100
        tree, state, expected = self.create_renamed_dirstate()
 
2101
 
 
2102
        # Search for the pre and post renamed entries
 
2103
        self.assertBisect(expected, [[b'a']], state, [b'a'])
 
2104
        self.assertBisect(expected, [[b'b/g']], state, [b'b/g'])
 
2105
        self.assertBisect(expected, [[b'b/d']], state, [b'b/d'])
 
2106
        self.assertBisect(expected, [[b'h']], state, [b'h'])
 
2107
 
 
2108
        # What about b/d/e? shouldn't that also get 2 directory entries?
 
2109
        self.assertBisect(expected, [[b'b/d/e']], state, [b'b/d/e'])
 
2110
        self.assertBisect(expected, [[b'h/e']], state, [b'h/e'])
 
2111
 
 
2112
    def test_bisect_dirblocks(self):
 
2113
        tree, state, expected = self.create_duplicated_dirstate()
 
2114
        self.assertBisectDirBlocks(expected,
 
2115
                                   [[b'', b'a', b'a2', b'b', b'b2',
 
2116
                                       b'b-c', b'b-c2', b'f', b'f2']],
 
2117
                                   state, [b''])
 
2118
        self.assertBisectDirBlocks(expected,
 
2119
                                   [[b'b/c', b'b/c2', b'b/d', b'b/d2']], state, [b'b'])
 
2120
        self.assertBisectDirBlocks(expected,
 
2121
                                   [[b'b/d/e', b'b/d/e2']], state, [b'b/d'])
 
2122
        self.assertBisectDirBlocks(expected,
 
2123
                                   [[b'', b'a', b'a2', b'b', b'b2', b'b-c', b'b-c2', b'f', b'f2'],
 
2124
                                    [b'b/c', b'b/c2', b'b/d', b'b/d2'],
 
2125
                                       [b'b/d/e', b'b/d/e2'],
 
2126
                                    ], state, [b'', b'b', b'b/d'])
 
2127
 
 
2128
    def test_bisect_dirblocks_missing(self):
 
2129
        tree, state, expected = self.create_basic_dirstate()
 
2130
        self.assertBisectDirBlocks(expected, [[b'b/d/e'], None],
 
2131
                                   state, [b'b/d', b'b/e'])
 
2132
        # Files don't show up in this search
 
2133
        self.assertBisectDirBlocks(expected, [None], state, [b'a'])
 
2134
        self.assertBisectDirBlocks(expected, [None], state, [b'b/c'])
 
2135
        self.assertBisectDirBlocks(expected, [None], state, [b'c'])
 
2136
        self.assertBisectDirBlocks(expected, [None], state, [b'b/d/e'])
 
2137
        self.assertBisectDirBlocks(expected, [None], state, [b'f'])
 
2138
 
 
2139
    def test_bisect_recursive_each(self):
 
2140
        tree, state, expected = self.create_basic_dirstate()
 
2141
        self.assertBisectRecursive(expected, [b'a'], state, [b'a'])
 
2142
        self.assertBisectRecursive(expected, [b'b/c'], state, [b'b/c'])
 
2143
        self.assertBisectRecursive(expected, [b'b/d/e'], state, [b'b/d/e'])
 
2144
        self.assertBisectRecursive(expected, [b'b-c'], state, [b'b-c'])
 
2145
        self.assertBisectRecursive(expected, [b'b/d', b'b/d/e'],
 
2146
                                   state, [b'b/d'])
 
2147
        self.assertBisectRecursive(expected, [b'b', b'b/c', b'b/d', b'b/d/e'],
 
2148
                                   state, [b'b'])
 
2149
        self.assertBisectRecursive(expected, [b'', b'a', b'b', b'b-c', b'f', b'b/c',
 
2150
                                              b'b/d', b'b/d/e'],
 
2151
                                   state, [b''])
 
2152
 
 
2153
    def test_bisect_recursive_multiple(self):
 
2154
        tree, state, expected = self.create_basic_dirstate()
 
2155
        self.assertBisectRecursive(
 
2156
            expected, [b'a', b'b/c'], state, [b'a', b'b/c'])
 
2157
        self.assertBisectRecursive(expected, [b'b/d', b'b/d/e'],
 
2158
                                   state, [b'b/d', b'b/d/e'])
 
2159
 
 
2160
    def test_bisect_recursive_missing(self):
 
2161
        tree, state, expected = self.create_basic_dirstate()
 
2162
        self.assertBisectRecursive(expected, [], state, [b'd'])
 
2163
        self.assertBisectRecursive(expected, [], state, [b'b/e'])
 
2164
        self.assertBisectRecursive(expected, [], state, [b'g'])
 
2165
        self.assertBisectRecursive(expected, [b'a'], state, [b'a', b'g'])
 
2166
 
 
2167
    def test_bisect_recursive_renamed(self):
 
2168
        tree, state, expected = self.create_renamed_dirstate()
 
2169
 
 
2170
        # Looking for either renamed item should find the other
 
2171
        self.assertBisectRecursive(expected, [b'a', b'b/g'], state, [b'a'])
 
2172
        self.assertBisectRecursive(expected, [b'a', b'b/g'], state, [b'b/g'])
 
2173
        # Looking in the containing directory should find the rename target,
 
2174
        # and anything in a subdir of the renamed target.
 
2175
        self.assertBisectRecursive(expected, [b'a', b'b', b'b/c', b'b/d',
 
2176
                                              b'b/d/e', b'b/g', b'h', b'h/e'],
 
2177
                                   state, [b'b'])
 
2178
 
 
2179
 
 
2180
class TestDirstateValidation(TestCaseWithDirState):
 
2181
 
 
2182
    def test_validate_correct_dirstate(self):
 
2183
        state = self.create_complex_dirstate()
 
2184
        state._validate()
 
2185
        state.unlock()
 
2186
        # and make sure we can also validate with a read lock
 
2187
        state.lock_read()
 
2188
        try:
 
2189
            state._validate()
 
2190
        finally:
 
2191
            state.unlock()
 
2192
 
 
2193
    def test_dirblock_not_sorted(self):
 
2194
        tree, state, expected = self.create_renamed_dirstate()
 
2195
        state._read_dirblocks_if_needed()
 
2196
        last_dirblock = state._dirblocks[-1]
 
2197
        # we're appending to the dirblock, but this name comes before some of
 
2198
        # the existing names; that's wrong
 
2199
        last_dirblock[1].append(
 
2200
            ((b'h', b'aaaa', b'a-id'),
 
2201
             [(b'a', b'', 0, False, b''),
 
2202
              (b'a', b'', 0, False, b'')]))
 
2203
        e = self.assertRaises(AssertionError,
 
2204
                              state._validate)
 
2205
        self.assertContainsRe(str(e), 'not sorted')
 
2206
 
 
2207
    def test_dirblock_name_mismatch(self):
 
2208
        tree, state, expected = self.create_renamed_dirstate()
 
2209
        state._read_dirblocks_if_needed()
 
2210
        last_dirblock = state._dirblocks[-1]
 
2211
        # add an entry with the wrong directory name
 
2212
        last_dirblock[1].append(
 
2213
            ((b'', b'z', b'a-id'),
 
2214
             [(b'a', b'', 0, False, b''),
 
2215
              (b'a', b'', 0, False, b'')]))
 
2216
        e = self.assertRaises(AssertionError,
 
2217
                              state._validate)
 
2218
        self.assertContainsRe(str(e),
 
2219
                              "doesn't match directory name")
 
2220
 
 
2221
    def test_dirblock_missing_rename(self):
 
2222
        tree, state, expected = self.create_renamed_dirstate()
 
2223
        state._read_dirblocks_if_needed()
 
2224
        last_dirblock = state._dirblocks[-1]
 
2225
        # make another entry for a-id, without a correct 'r' pointer to
 
2226
        # the real occurrence in the working tree
 
2227
        last_dirblock[1].append(
 
2228
            ((b'h', b'z', b'a-id'),
 
2229
             [(b'a', b'', 0, False, b''),
 
2230
              (b'a', b'', 0, False, b'')]))
 
2231
        e = self.assertRaises(AssertionError,
 
2232
                              state._validate)
 
2233
        self.assertContainsRe(str(e),
 
2234
                              'file a-id is absent in row')
 
2235
 
 
2236
 
 
2237
class TestDirstateTreeReference(TestCaseWithDirState):
 
2238
 
 
2239
    def test_reference_revision_is_none(self):
 
2240
        tree = self.make_branch_and_tree('tree', format='development-subtree')
 
2241
        subtree = self.make_branch_and_tree('tree/subtree',
 
2242
                                            format='development-subtree')
 
2243
        subtree.set_root_id(b'subtree')
 
2244
        tree.add_reference(subtree)
 
2245
        tree.add('subtree')
 
2246
        state = dirstate.DirState.from_tree(tree, 'dirstate')
 
2247
        key = (b'', b'subtree', b'subtree')
 
2248
        expected = (b'', [(key,
 
2249
                           [(b't', b'', 0, False, b'xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx')])])
 
2250
 
 
2251
        try:
 
2252
            self.assertEqual(expected, state._find_block(key))
 
2253
        finally:
 
2254
            state.unlock()
 
2255
 
 
2256
 
 
2257
class TestDiscardMergeParents(TestCaseWithDirState):
 
2258
 
 
2259
    def test_discard_no_parents(self):
 
2260
        # This should be a no-op
 
2261
        state = self.create_empty_dirstate()
 
2262
        self.addCleanup(state.unlock)
 
2263
        state._discard_merge_parents()
 
2264
        state._validate()
 
2265
 
 
2266
    def test_discard_one_parent(self):
 
2267
        # No-op
 
2268
        packed_stat = b'AAAAREUHaIpFB2iKAAADAQAtkqUAAIGk'
 
2269
        root_entry_direntry = (b'', b'', b'a-root-value'), [
 
2270
            (b'd', b'', 0, False, packed_stat),
 
2271
            (b'd', b'', 0, False, packed_stat),
 
2272
            ]
 
2273
        dirblocks = []
 
2274
        dirblocks.append((b'', [root_entry_direntry]))
 
2275
        dirblocks.append((b'', []))
 
2276
 
 
2277
        state = self.create_empty_dirstate()
 
2278
        self.addCleanup(state.unlock)
 
2279
        state._set_data([b'parent-id'], dirblocks[:])
 
2280
        state._validate()
 
2281
 
 
2282
        state._discard_merge_parents()
 
2283
        state._validate()
 
2284
        self.assertEqual(dirblocks, state._dirblocks)
 
2285
 
 
2286
    def test_discard_simple(self):
 
2287
        # No-op
 
2288
        packed_stat = b'AAAAREUHaIpFB2iKAAADAQAtkqUAAIGk'
 
2289
        root_entry_direntry = (b'', b'', b'a-root-value'), [
 
2290
            (b'd', b'', 0, False, packed_stat),
 
2291
            (b'd', b'', 0, False, packed_stat),
 
2292
            (b'd', b'', 0, False, packed_stat),
 
2293
            ]
 
2294
        expected_root_entry_direntry = (b'', b'', b'a-root-value'), [
 
2295
            (b'd', b'', 0, False, packed_stat),
 
2296
            (b'd', b'', 0, False, packed_stat),
 
2297
            ]
 
2298
        dirblocks = []
 
2299
        dirblocks.append((b'', [root_entry_direntry]))
 
2300
        dirblocks.append((b'', []))
 
2301
 
 
2302
        state = self.create_empty_dirstate()
 
2303
        self.addCleanup(state.unlock)
 
2304
        state._set_data([b'parent-id', b'merged-id'], dirblocks[:])
 
2305
        state._validate()
 
2306
 
 
2307
        # This should strip of the extra column
 
2308
        state._discard_merge_parents()
 
2309
        state._validate()
 
2310
        expected_dirblocks = [(b'', [expected_root_entry_direntry]), (b'', [])]
 
2311
        self.assertEqual(expected_dirblocks, state._dirblocks)
 
2312
 
 
2313
    def test_discard_absent(self):
 
2314
        """If entries are only in a merge, discard should remove the entries"""
 
2315
        null_stat = dirstate.DirState.NULLSTAT
 
2316
        present_dir = (b'd', b'', 0, False, null_stat)
 
2317
        present_file = (b'f', b'', 0, False, null_stat)
 
2318
        absent = dirstate.DirState.NULL_PARENT_DETAILS
 
2319
        root_key = (b'', b'', b'a-root-value')
 
2320
        file_in_root_key = (b'', b'file-in-root', b'a-file-id')
 
2321
        file_in_merged_key = (b'', b'file-in-merged', b'b-file-id')
 
2322
        dirblocks = [(b'', [(root_key, [present_dir, present_dir, present_dir])]),
 
2323
                     (b'', [(file_in_merged_key,
 
2324
                             [absent, absent, present_file]),
 
2325
                            (file_in_root_key,
 
2326
                             [present_file, present_file, present_file]),
 
2327
                            ]),
 
2328
                     ]
 
2329
 
 
2330
        state = self.create_empty_dirstate()
 
2331
        self.addCleanup(state.unlock)
 
2332
        state._set_data([b'parent-id', b'merged-id'], dirblocks[:])
 
2333
        state._validate()
 
2334
 
 
2335
        exp_dirblocks = [(b'', [(root_key, [present_dir, present_dir])]),
 
2336
                         (b'', [(file_in_root_key,
 
2337
                                 [present_file, present_file]),
 
2338
                                ]),
 
2339
                         ]
 
2340
        state._discard_merge_parents()
 
2341
        state._validate()
 
2342
        self.assertEqual(exp_dirblocks, state._dirblocks)
 
2343
 
 
2344
    def test_discard_renamed(self):
 
2345
        null_stat = dirstate.DirState.NULLSTAT
 
2346
        present_dir = (b'd', b'', 0, False, null_stat)
 
2347
        present_file = (b'f', b'', 0, False, null_stat)
 
2348
        absent = dirstate.DirState.NULL_PARENT_DETAILS
 
2349
        root_key = (b'', b'', b'a-root-value')
 
2350
        file_in_root_key = (b'', b'file-in-root', b'a-file-id')
 
2351
        # Renamed relative to parent
 
2352
        file_rename_s_key = (b'', b'file-s', b'b-file-id')
 
2353
        file_rename_t_key = (b'', b'file-t', b'b-file-id')
 
2354
        # And one that is renamed between the parents, but absent in this
 
2355
        key_in_1 = (b'', b'file-in-1', b'c-file-id')
 
2356
        key_in_2 = (b'', b'file-in-2', b'c-file-id')
 
2357
 
 
2358
        dirblocks = [
 
2359
            (b'', [(root_key, [present_dir, present_dir, present_dir])]),
 
2360
            (b'', [(key_in_1,
 
2361
                    [absent, present_file, (b'r', b'file-in-2', b'c-file-id')]),
 
2362
                   (key_in_2,
 
2363
                    [absent, (b'r', b'file-in-1', b'c-file-id'), present_file]),
 
2364
                   (file_in_root_key,
 
2365
                    [present_file, present_file, present_file]),
 
2366
                   (file_rename_s_key,
 
2367
                    [(b'r', b'file-t', b'b-file-id'), absent, present_file]),
 
2368
                   (file_rename_t_key,
 
2369
                    [present_file, absent, (b'r', b'file-s', b'b-file-id')]),
 
2370
                   ]),
 
2371
        ]
 
2372
        exp_dirblocks = [
 
2373
            (b'', [(root_key, [present_dir, present_dir])]),
 
2374
            (b'', [(key_in_1, [absent, present_file]),
 
2375
                   (file_in_root_key, [present_file, present_file]),
 
2376
                   (file_rename_t_key, [present_file, absent]),
 
2377
                   ]),
 
2378
        ]
 
2379
        state = self.create_empty_dirstate()
 
2380
        self.addCleanup(state.unlock)
 
2381
        state._set_data([b'parent-id', b'merged-id'], dirblocks[:])
 
2382
        state._validate()
 
2383
 
 
2384
        state._discard_merge_parents()
 
2385
        state._validate()
 
2386
        self.assertEqual(exp_dirblocks, state._dirblocks)
 
2387
 
 
2388
    def test_discard_all_subdir(self):
 
2389
        null_stat = dirstate.DirState.NULLSTAT
 
2390
        present_dir = (b'd', b'', 0, False, null_stat)
 
2391
        present_file = (b'f', b'', 0, False, null_stat)
 
2392
        absent = dirstate.DirState.NULL_PARENT_DETAILS
 
2393
        root_key = (b'', b'', b'a-root-value')
 
2394
        subdir_key = (b'', b'sub', b'dir-id')
 
2395
        child1_key = (b'sub', b'child1', b'child1-id')
 
2396
        child2_key = (b'sub', b'child2', b'child2-id')
 
2397
        child3_key = (b'sub', b'child3', b'child3-id')
 
2398
 
 
2399
        dirblocks = [
 
2400
            (b'', [(root_key, [present_dir, present_dir, present_dir])]),
 
2401
            (b'', [(subdir_key, [present_dir, present_dir, present_dir])]),
 
2402
            (b'sub', [(child1_key, [absent, absent, present_file]),
 
2403
                      (child2_key, [absent, absent, present_file]),
 
2404
                      (child3_key, [absent, absent, present_file]),
 
2405
                      ]),
 
2406
        ]
 
2407
        exp_dirblocks = [
 
2408
            (b'', [(root_key, [present_dir, present_dir])]),
 
2409
            (b'', [(subdir_key, [present_dir, present_dir])]),
 
2410
            (b'sub', []),
 
2411
        ]
 
2412
        state = self.create_empty_dirstate()
 
2413
        self.addCleanup(state.unlock)
 
2414
        state._set_data([b'parent-id', b'merged-id'], dirblocks[:])
 
2415
        state._validate()
 
2416
 
 
2417
        state._discard_merge_parents()
 
2418
        state._validate()
 
2419
        self.assertEqual(exp_dirblocks, state._dirblocks)
 
2420
 
 
2421
 
 
2422
class Test_InvEntryToDetails(tests.TestCase):
 
2423
 
 
2424
    def assertDetails(self, expected, inv_entry):
 
2425
        details = dirstate.DirState._inv_entry_to_details(inv_entry)
 
2426
        self.assertEqual(expected, details)
 
2427
        # details should always allow join() and always be a plain str when
 
2428
        # finished
 
2429
        (minikind, fingerprint, size, executable, tree_data) = details
 
2430
        self.assertIsInstance(minikind, bytes)
 
2431
        self.assertIsInstance(fingerprint, bytes)
 
2432
        self.assertIsInstance(tree_data, bytes)
 
2433
 
 
2434
    def test_unicode_symlink(self):
 
2435
        inv_entry = inventory.InventoryLink(b'link-file-id',
 
2436
                                            u'nam\N{Euro Sign}e',
 
2437
                                            b'link-parent-id')
 
2438
        inv_entry.revision = b'link-revision-id'
 
2439
        target = u'link-targ\N{Euro Sign}t'
 
2440
        inv_entry.symlink_target = target
 
2441
        self.assertDetails((b'l', target.encode('UTF-8'), 0, False,
 
2442
                            b'link-revision-id'), inv_entry)
 
2443
 
 
2444
 
 
2445
class TestSHA1Provider(tests.TestCaseInTempDir):
 
2446
 
 
2447
    def test_sha1provider_is_an_interface(self):
 
2448
        p = dirstate.SHA1Provider()
 
2449
        self.assertRaises(NotImplementedError, p.sha1, "foo")
 
2450
        self.assertRaises(NotImplementedError, p.stat_and_sha1, "foo")
 
2451
 
 
2452
    def test_defaultsha1provider_sha1(self):
 
2453
        text = b'test\r\nwith\nall\rpossible line endings\r\n'
 
2454
        self.build_tree_contents([('foo', text)])
 
2455
        expected_sha = osutils.sha_string(text)
 
2456
        p = dirstate.DefaultSHA1Provider()
 
2457
        self.assertEqual(expected_sha, p.sha1('foo'))
 
2458
 
 
2459
    def test_defaultsha1provider_stat_and_sha1(self):
 
2460
        text = b'test\r\nwith\nall\rpossible line endings\r\n'
 
2461
        self.build_tree_contents([('foo', text)])
 
2462
        expected_sha = osutils.sha_string(text)
 
2463
        p = dirstate.DefaultSHA1Provider()
 
2464
        statvalue, sha1 = p.stat_and_sha1('foo')
 
2465
        self.assertTrue(len(statvalue) >= 10)
 
2466
        self.assertEqual(len(text), statvalue.st_size)
 
2467
        self.assertEqual(expected_sha, sha1)
 
2468
 
 
2469
 
 
2470
class _Repo(object):
 
2471
    """A minimal api to get InventoryRevisionTree to work."""
 
2472
 
 
2473
    def __init__(self):
 
2474
        default_format = controldir.format_registry.make_controldir('default')
 
2475
        self._format = default_format.repository_format
 
2476
 
 
2477
    def lock_read(self):
 
2478
        pass
 
2479
 
 
2480
    def unlock(self):
 
2481
        pass
 
2482
 
 
2483
 
 
2484
class TestUpdateBasisByDelta(tests.TestCase):
 
2485
 
 
2486
    def path_to_ie(self, path, file_id, rev_id, dir_ids):
 
2487
        if path.endswith('/'):
 
2488
            is_dir = True
 
2489
            path = path[:-1]
 
2490
        else:
 
2491
            is_dir = False
 
2492
        dirname, basename = osutils.split(path)
 
2493
        try:
 
2494
            dir_id = dir_ids[dirname]
 
2495
        except KeyError:
 
2496
            dir_id = osutils.basename(dirname).encode('utf-8') + b'-id'
 
2497
        if is_dir:
 
2498
            ie = inventory.InventoryDirectory(file_id, basename, dir_id)
 
2499
            dir_ids[path] = file_id
 
2500
        else:
 
2501
            ie = inventory.InventoryFile(file_id, basename, dir_id)
 
2502
            ie.text_size = 0
 
2503
            ie.text_sha1 = b''
 
2504
        ie.revision = rev_id
 
2505
        return ie
 
2506
 
 
2507
    def create_tree_from_shape(self, rev_id, shape):
 
2508
        dir_ids = {'': b'root-id'}
 
2509
        inv = inventory.Inventory(b'root-id', rev_id)
 
2510
        for info in shape:
 
2511
            if len(info) == 2:
 
2512
                path, file_id = info
 
2513
                ie_rev_id = rev_id
 
2514
            else:
 
2515
                path, file_id, ie_rev_id = info
 
2516
            if path == '':
 
2517
                # Replace the root entry
 
2518
                del inv._byid[inv.root.file_id]
 
2519
                inv.root.file_id = file_id
 
2520
                inv._byid[file_id] = inv.root
 
2521
                dir_ids[''] = file_id
 
2522
                continue
 
2523
            inv.add(self.path_to_ie(path, file_id, ie_rev_id, dir_ids))
 
2524
        return inventorytree.InventoryRevisionTree(_Repo(), inv, rev_id)
 
2525
 
 
2526
    def create_empty_dirstate(self):
 
2527
        fd, path = tempfile.mkstemp(prefix='bzr-dirstate')
 
2528
        self.addCleanup(os.remove, path)
 
2529
        os.close(fd)
 
2530
        state = dirstate.DirState.initialize(path)
 
2531
        self.addCleanup(state.unlock)
 
2532
        return state
 
2533
 
 
2534
    def create_inv_delta(self, delta, rev_id):
 
2535
        """Translate a 'delta shape' into an actual InventoryDelta"""
 
2536
        dir_ids = {'': b'root-id'}
 
2537
        inv_delta = []
 
2538
        for old_path, new_path, file_id in delta:
 
2539
            if old_path is not None and old_path.endswith('/'):
 
2540
                # Don't have to actually do anything for this, because only
 
2541
                # new_path creates InventoryEntries
 
2542
                old_path = old_path[:-1]
 
2543
            if new_path is None:  # Delete
 
2544
                inv_delta.append((old_path, None, file_id, None))
 
2545
                continue
 
2546
            ie = self.path_to_ie(new_path, file_id, rev_id, dir_ids)
 
2547
            inv_delta.append((old_path, new_path, file_id, ie))
 
2548
        return inv_delta
 
2549
 
 
2550
    def assertUpdate(self, active, basis, target):
 
2551
        """Assert that update_basis_by_delta works how we want.
 
2552
 
 
2553
        Set up a DirState object with active_shape for tree 0, basis_shape for
 
2554
        tree 1. Then apply the delta from basis_shape to target_shape,
 
2555
        and assert that the DirState is still valid, and that its stored
 
2556
        content matches the target_shape.
 
2557
        """
 
2558
        active_tree = self.create_tree_from_shape(b'active', active)
 
2559
        basis_tree = self.create_tree_from_shape(b'basis', basis)
 
2560
        target_tree = self.create_tree_from_shape(b'target', target)
 
2561
        state = self.create_empty_dirstate()
 
2562
        state.set_state_from_scratch(active_tree.root_inventory,
 
2563
                                     [(b'basis', basis_tree)], [])
 
2564
        delta = target_tree.root_inventory._make_delta(
 
2565
            basis_tree.root_inventory)
 
2566
        state.update_basis_by_delta(delta, b'target')
 
2567
        state._validate()
 
2568
        dirstate_tree = workingtree_4.DirStateRevisionTree(state,
 
2569
                                                           b'target', _Repo())
 
2570
        # The target now that delta has been applied should match the
 
2571
        # RevisionTree
 
2572
        self.assertEqual([], list(dirstate_tree.iter_changes(target_tree)))
 
2573
        # And the dirblock state should be identical to the state if we created
 
2574
        # it from scratch.
 
2575
        state2 = self.create_empty_dirstate()
 
2576
        state2.set_state_from_scratch(active_tree.root_inventory,
 
2577
                                      [(b'target', target_tree)], [])
 
2578
        self.assertEqual(state2._dirblocks, state._dirblocks)
 
2579
        return state
 
2580
 
 
2581
    def assertBadDelta(self, active, basis, delta):
 
2582
        """Test that we raise InconsistentDelta when appropriate.
 
2583
 
 
2584
        :param active: The active tree shape
 
2585
        :param basis: The basis tree shape
 
2586
        :param delta: A description of the delta to apply. Similar to the form
 
2587
            for regular inventory deltas, but omitting the InventoryEntry.
 
2588
            So adding a file is: (None, 'path', b'file-id')
 
2589
            Adding a directory is: (None, 'path/', b'dir-id')
 
2590
            Renaming a dir is: ('old/', 'new/', b'dir-id')
 
2591
            etc.
 
2592
        """
 
2593
        active_tree = self.create_tree_from_shape(b'active', active)
 
2594
        basis_tree = self.create_tree_from_shape(b'basis', basis)
 
2595
        inv_delta = self.create_inv_delta(delta, b'target')
 
2596
        state = self.create_empty_dirstate()
 
2597
        state.set_state_from_scratch(active_tree.root_inventory,
 
2598
                                     [(b'basis', basis_tree)], [])
 
2599
        self.assertRaises(errors.InconsistentDelta,
 
2600
                          state.update_basis_by_delta, inv_delta, b'target')
 
2601
        # try:
 
2602
        ##     state.update_basis_by_delta(inv_delta, b'target')
 
2603
        # except errors.InconsistentDelta, e:
 
2604
        ##     import pdb; pdb.set_trace()
 
2605
        # else:
 
2606
        ##     import pdb; pdb.set_trace()
 
2607
        self.assertTrue(state._changes_aborted)
 
2608
 
 
2609
    def test_remove_file_matching_active_state(self):
 
2610
        state = self.assertUpdate(
 
2611
            active=[],
 
2612
            basis=[('file', b'file-id')],
 
2613
            target=[],
 
2614
            )
 
2615
 
 
2616
    def test_remove_file_present_in_active_state(self):
 
2617
        state = self.assertUpdate(
 
2618
            active=[('file', b'file-id')],
 
2619
            basis=[('file', b'file-id')],
 
2620
            target=[],
 
2621
            )
 
2622
 
 
2623
    def test_remove_file_present_elsewhere_in_active_state(self):
 
2624
        state = self.assertUpdate(
 
2625
            active=[('other-file', b'file-id')],
 
2626
            basis=[('file', b'file-id')],
 
2627
            target=[],
 
2628
            )
 
2629
 
 
2630
    def test_remove_file_active_state_has_diff_file(self):
 
2631
        state = self.assertUpdate(
 
2632
            active=[('file', b'file-id-2')],
 
2633
            basis=[('file', b'file-id')],
 
2634
            target=[],
 
2635
            )
 
2636
 
 
2637
    def test_remove_file_active_state_has_diff_file_and_file_elsewhere(self):
 
2638
        state = self.assertUpdate(
 
2639
            active=[('file', b'file-id-2'),
 
2640
                    ('other-file', b'file-id')],
 
2641
            basis=[('file', b'file-id')],
 
2642
            target=[],
 
2643
            )
 
2644
 
 
2645
    def test_add_file_matching_active_state(self):
 
2646
        state = self.assertUpdate(
 
2647
            active=[('file', b'file-id')],
 
2648
            basis=[],
 
2649
            target=[('file', b'file-id')],
 
2650
            )
 
2651
 
 
2652
    def test_add_file_in_empty_dir_not_matching_active_state(self):
 
2653
        state = self.assertUpdate(
 
2654
            active=[],
 
2655
            basis=[('dir/', b'dir-id')],
 
2656
            target=[('dir/', b'dir-id', b'basis'), ('dir/file', b'file-id')],
 
2657
            )
 
2658
 
 
2659
    def test_add_file_missing_in_active_state(self):
 
2660
        state = self.assertUpdate(
 
2661
            active=[],
 
2662
            basis=[],
 
2663
            target=[('file', b'file-id')],
 
2664
            )
 
2665
 
 
2666
    def test_add_file_elsewhere_in_active_state(self):
 
2667
        state = self.assertUpdate(
 
2668
            active=[('other-file', b'file-id')],
 
2669
            basis=[],
 
2670
            target=[('file', b'file-id')],
 
2671
            )
 
2672
 
 
2673
    def test_add_file_active_state_has_diff_file_and_file_elsewhere(self):
 
2674
        state = self.assertUpdate(
 
2675
            active=[('other-file', b'file-id'),
 
2676
                    ('file', b'file-id-2')],
 
2677
            basis=[],
 
2678
            target=[('file', b'file-id')],
 
2679
            )
 
2680
 
 
2681
    def test_rename_file_matching_active_state(self):
 
2682
        state = self.assertUpdate(
 
2683
            active=[('other-file', b'file-id')],
 
2684
            basis=[('file', b'file-id')],
 
2685
            target=[('other-file', b'file-id')],
 
2686
            )
 
2687
 
 
2688
    def test_rename_file_missing_in_active_state(self):
 
2689
        state = self.assertUpdate(
 
2690
            active=[],
 
2691
            basis=[('file', b'file-id')],
 
2692
            target=[('other-file', b'file-id')],
 
2693
            )
 
2694
 
 
2695
    def test_rename_file_present_elsewhere_in_active_state(self):
 
2696
        state = self.assertUpdate(
 
2697
            active=[('third', b'file-id')],
 
2698
            basis=[('file', b'file-id')],
 
2699
            target=[('other-file', b'file-id')],
 
2700
            )
 
2701
 
 
2702
    def test_rename_file_active_state_has_diff_source_file(self):
 
2703
        state = self.assertUpdate(
 
2704
            active=[('file', b'file-id-2')],
 
2705
            basis=[('file', b'file-id')],
 
2706
            target=[('other-file', b'file-id')],
 
2707
            )
 
2708
 
 
2709
    def test_rename_file_active_state_has_diff_target_file(self):
 
2710
        state = self.assertUpdate(
 
2711
            active=[('other-file', b'file-id-2')],
 
2712
            basis=[('file', b'file-id')],
 
2713
            target=[('other-file', b'file-id')],
 
2714
            )
 
2715
 
 
2716
    def test_rename_file_active_has_swapped_files(self):
 
2717
        state = self.assertUpdate(
 
2718
            active=[('file', b'file-id'),
 
2719
                    ('other-file', b'file-id-2')],
 
2720
            basis=[('file', b'file-id'),
 
2721
                   ('other-file', b'file-id-2')],
 
2722
            target=[('file', b'file-id-2'),
 
2723
                    ('other-file', b'file-id')])
 
2724
 
 
2725
    def test_rename_file_basis_has_swapped_files(self):
 
2726
        state = self.assertUpdate(
 
2727
            active=[('file', b'file-id'),
 
2728
                    ('other-file', b'file-id-2')],
 
2729
            basis=[('file', b'file-id-2'),
 
2730
                   ('other-file', b'file-id')],
 
2731
            target=[('file', b'file-id'),
 
2732
                    ('other-file', b'file-id-2')])
 
2733
 
 
2734
    def test_rename_directory_with_contents(self):
 
2735
        state = self.assertUpdate(  # active matches basis
 
2736
            active=[('dir1/', b'dir-id'),
 
2737
                    ('dir1/file', b'file-id')],
 
2738
            basis=[('dir1/', b'dir-id'),
 
2739
                   ('dir1/file', b'file-id')],
 
2740
            target=[('dir2/', b'dir-id'),
 
2741
                    ('dir2/file', b'file-id')])
 
2742
        state = self.assertUpdate(  # active matches target
 
2743
            active=[('dir2/', b'dir-id'),
 
2744
                    ('dir2/file', b'file-id')],
 
2745
            basis=[('dir1/', b'dir-id'),
 
2746
                   ('dir1/file', b'file-id')],
 
2747
            target=[('dir2/', b'dir-id'),
 
2748
                    ('dir2/file', b'file-id')])
 
2749
        state = self.assertUpdate(  # active empty
 
2750
            active=[],
 
2751
            basis=[('dir1/', b'dir-id'),
 
2752
                   ('dir1/file', b'file-id')],
 
2753
            target=[('dir2/', b'dir-id'),
 
2754
                    ('dir2/file', b'file-id')])
 
2755
        state = self.assertUpdate(  # active present at other location
 
2756
            active=[('dir3/', b'dir-id'),
 
2757
                    ('dir3/file', b'file-id')],
 
2758
            basis=[('dir1/', b'dir-id'),
 
2759
                   ('dir1/file', b'file-id')],
 
2760
            target=[('dir2/', b'dir-id'),
 
2761
                    ('dir2/file', b'file-id')])
 
2762
        state = self.assertUpdate(  # active has different ids
 
2763
            active=[('dir1/', b'dir1-id'),
 
2764
                    ('dir1/file', b'file1-id'),
 
2765
                    ('dir2/', b'dir2-id'),
 
2766
                    ('dir2/file', b'file2-id')],
 
2767
            basis=[('dir1/', b'dir-id'),
 
2768
                   ('dir1/file', b'file-id')],
 
2769
            target=[('dir2/', b'dir-id'),
 
2770
                    ('dir2/file', b'file-id')])
 
2771
 
 
2772
    def test_invalid_file_not_present(self):
 
2773
        state = self.assertBadDelta(
 
2774
            active=[('file', b'file-id')],
 
2775
            basis=[('file', b'file-id')],
 
2776
            delta=[('other-file', 'file', b'file-id')])
 
2777
 
 
2778
    def test_invalid_new_id_same_path(self):
 
2779
        # The bad entry comes after
 
2780
        state = self.assertBadDelta(
 
2781
            active=[('file', b'file-id')],
 
2782
            basis=[('file', b'file-id')],
 
2783
            delta=[(None, 'file', b'file-id-2')])
 
2784
        # The bad entry comes first
 
2785
        state = self.assertBadDelta(
 
2786
            active=[('file', b'file-id-2')],
 
2787
            basis=[('file', b'file-id-2')],
 
2788
            delta=[(None, 'file', b'file-id')])
 
2789
 
 
2790
    def test_invalid_existing_id(self):
 
2791
        state = self.assertBadDelta(
 
2792
            active=[('file', b'file-id')],
 
2793
            basis=[('file', b'file-id')],
 
2794
            delta=[(None, 'file', b'file-id')])
 
2795
 
 
2796
    def test_invalid_parent_missing(self):
 
2797
        state = self.assertBadDelta(
 
2798
            active=[],
 
2799
            basis=[],
 
2800
            delta=[(None, 'path/path2', b'file-id')])
 
2801
        # Note: we force the active tree to have the directory, by knowing how
 
2802
        #       path_to_ie handles entries with missing parents
 
2803
        state = self.assertBadDelta(
 
2804
            active=[('path/', b'path-id')],
 
2805
            basis=[],
 
2806
            delta=[(None, 'path/path2', b'file-id')])
 
2807
        state = self.assertBadDelta(
 
2808
            active=[('path/', b'path-id'),
 
2809
                    ('path/path2', b'file-id')],
 
2810
            basis=[],
 
2811
            delta=[(None, 'path/path2', b'file-id')])
 
2812
 
 
2813
    def test_renamed_dir_same_path(self):
 
2814
        # We replace the parent directory, with another parent dir. But the C
 
2815
        # file doesn't look like it has been moved.
 
2816
        state = self.assertUpdate(  # Same as basis
 
2817
            active=[('dir/', b'A-id'),
 
2818
                    ('dir/B', b'B-id')],
 
2819
            basis=[('dir/', b'A-id'),
 
2820
                   ('dir/B', b'B-id')],
 
2821
            target=[('dir/', b'C-id'),
 
2822
                    ('dir/B', b'B-id')])
 
2823
        state = self.assertUpdate(  # Same as target
 
2824
            active=[('dir/', b'C-id'),
 
2825
                    ('dir/B', b'B-id')],
 
2826
            basis=[('dir/', b'A-id'),
 
2827
                   ('dir/B', b'B-id')],
 
2828
            target=[('dir/', b'C-id'),
 
2829
                    ('dir/B', b'B-id')])
 
2830
        state = self.assertUpdate(  # empty active
 
2831
            active=[],
 
2832
            basis=[('dir/', b'A-id'),
 
2833
                   ('dir/B', b'B-id')],
 
2834
            target=[('dir/', b'C-id'),
 
2835
                    ('dir/B', b'B-id')])
 
2836
        state = self.assertUpdate(  # different active
 
2837
            active=[('dir/', b'D-id'),
 
2838
                    ('dir/B', b'B-id')],
 
2839
            basis=[('dir/', b'A-id'),
 
2840
                   ('dir/B', b'B-id')],
 
2841
            target=[('dir/', b'C-id'),
 
2842
                    ('dir/B', b'B-id')])
 
2843
 
 
2844
    def test_parent_child_swap(self):
 
2845
        state = self.assertUpdate(  # Same as basis
 
2846
            active=[('A/', b'A-id'),
 
2847
                    ('A/B/', b'B-id'),
 
2848
                    ('A/B/C', b'C-id')],
 
2849
            basis=[('A/', b'A-id'),
 
2850
                   ('A/B/', b'B-id'),
 
2851
                   ('A/B/C', b'C-id')],
 
2852
            target=[('A/', b'B-id'),
 
2853
                    ('A/B/', b'A-id'),
 
2854
                    ('A/B/C', b'C-id')])
 
2855
        state = self.assertUpdate(  # Same as target
 
2856
            active=[('A/', b'B-id'),
 
2857
                    ('A/B/', b'A-id'),
 
2858
                    ('A/B/C', b'C-id')],
 
2859
            basis=[('A/', b'A-id'),
 
2860
                   ('A/B/', b'B-id'),
 
2861
                   ('A/B/C', b'C-id')],
 
2862
            target=[('A/', b'B-id'),
 
2863
                    ('A/B/', b'A-id'),
 
2864
                    ('A/B/C', b'C-id')])
 
2865
        state = self.assertUpdate(  # empty active
 
2866
            active=[],
 
2867
            basis=[('A/', b'A-id'),
 
2868
                   ('A/B/', b'B-id'),
 
2869
                   ('A/B/C', b'C-id')],
 
2870
            target=[('A/', b'B-id'),
 
2871
                    ('A/B/', b'A-id'),
 
2872
                    ('A/B/C', b'C-id')])
 
2873
        state = self.assertUpdate(  # different active
 
2874
            active=[('D/', b'A-id'),
 
2875
                    ('D/E/', b'B-id'),
 
2876
                    ('F', b'C-id')],
 
2877
            basis=[('A/', b'A-id'),
 
2878
                   ('A/B/', b'B-id'),
 
2879
                   ('A/B/C', b'C-id')],
 
2880
            target=[('A/', b'B-id'),
 
2881
                    ('A/B/', b'A-id'),
 
2882
                    ('A/B/C', b'C-id')])
 
2883
 
 
2884
    def test_change_root_id(self):
 
2885
        state = self.assertUpdate(  # same as basis
 
2886
            active=[('', b'root-id'),
 
2887
                    ('file', b'file-id')],
 
2888
            basis=[('', b'root-id'),
 
2889
                   ('file', b'file-id')],
 
2890
            target=[('', b'target-root-id'),
 
2891
                    ('file', b'file-id')])
 
2892
        state = self.assertUpdate(  # same as target
 
2893
            active=[('', b'target-root-id'),
 
2894
                    ('file', b'file-id')],
 
2895
            basis=[('', b'root-id'),
 
2896
                   ('file', b'file-id')],
 
2897
            target=[('', b'target-root-id'),
 
2898
                    ('file', b'root-id')])
 
2899
        state = self.assertUpdate(  # all different
 
2900
            active=[('', b'active-root-id'),
 
2901
                    ('file', b'file-id')],
 
2902
            basis=[('', b'root-id'),
 
2903
                   ('file', b'file-id')],
 
2904
            target=[('', b'target-root-id'),
 
2905
                    ('file', b'root-id')])
 
2906
 
 
2907
    def test_change_file_absent_in_active(self):
 
2908
        state = self.assertUpdate(
 
2909
            active=[],
 
2910
            basis=[('file', b'file-id')],
 
2911
            target=[('file', b'file-id')])
 
2912
 
 
2913
    def test_invalid_changed_file(self):
 
2914
        state = self.assertBadDelta(  # Not present in basis
 
2915
            active=[('file', b'file-id')],
 
2916
            basis=[],
 
2917
            delta=[('file', 'file', b'file-id')])
 
2918
        state = self.assertBadDelta(  # present at another location in basis
 
2919
            active=[('file', b'file-id')],
 
2920
            basis=[('other-file', b'file-id')],
 
2921
            delta=[('file', 'file', b'file-id')])