1
# Copyright (C) 2005 by Canonical Ltd
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.
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.
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., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA
17
from cStringIO import StringIO
21
from bzrlib.branch import Branch
22
import bzrlib.errors as errors
23
from bzrlib.diff import internal_diff
24
from bzrlib.inventory import (Inventory, ROOT_ID, InventoryFile,
25
InventoryDirectory, InventoryEntry)
26
import bzrlib.inventory as inventory
27
from bzrlib.osutils import (has_symlinks, rename, pathjoin, is_inside_any,
28
is_inside_or_parent_of_any)
29
from bzrlib.tests import TestCase, TestCaseWithTransport
30
from bzrlib.transform import TreeTransform
31
from bzrlib.uncommit import uncommit
34
class TestInventory(TestCase):
36
def test_is_within(self):
38
SRC_FOO_C = pathjoin('src', 'foo.c')
39
for dirs, fn in [(['src', 'doc'], SRC_FOO_C),
43
self.assert_(is_inside_any(dirs, fn))
45
for dirs, fn in [(['src'], 'srccontrol'),
46
(['src'], 'srccontrol/foo')]:
47
self.assertFalse(is_inside_any(dirs, fn))
49
def test_is_within_or_parent(self):
50
for dirs, fn in [(['src', 'doc'], 'src/foo.c'),
51
(['src'], 'src/foo.c'),
52
(['src/bar.c'], 'src'),
53
(['src/bar.c', 'bla/foo.c'], 'src'),
56
self.assert_(is_inside_or_parent_of_any(dirs, fn))
58
for dirs, fn in [(['src'], 'srccontrol'),
59
(['srccontrol/foo.c'], 'src'),
60
(['src'], 'srccontrol/foo')]:
61
self.assertFalse(is_inside_or_parent_of_any(dirs, fn))
64
"""Test detection of files within selected directories."""
67
for args in [('src', 'directory', 'src-id'),
68
('doc', 'directory', 'doc-id'),
69
('src/hello.c', 'file'),
70
('src/bye.c', 'file', 'bye-id'),
71
('Makefile', 'file')]:
74
self.assertEqual(inv.path2id('src'), 'src-id')
75
self.assertEqual(inv.path2id('src/bye.c'), 'bye-id')
77
self.assert_('src-id' in inv)
79
def test_iter_entries(self):
82
for args in [('src', 'directory', 'src-id'),
83
('doc', 'directory', 'doc-id'),
84
('src/hello.c', 'file', 'hello-id'),
85
('src/bye.c', 'file', 'bye-id'),
86
('Makefile', 'file', 'makefile-id')]:
90
('Makefile', 'makefile-id'),
93
('src/bye.c', 'bye-id'),
94
('src/hello.c', 'hello-id'),
95
], [(path, ie.file_id) for path, ie in inv.iter_entries()])
97
def test_version(self):
98
"""Inventory remembers the text's version."""
100
ie = inv.add_path('foo.txt', 'file')
104
class TestInventoryEntry(TestCase):
106
def test_file_kind_character(self):
107
file = inventory.InventoryFile('123', 'hello.c', ROOT_ID)
108
self.assertEqual(file.kind_character(), '')
110
def test_dir_kind_character(self):
111
dir = inventory.InventoryDirectory('123', 'hello.c', ROOT_ID)
112
self.assertEqual(dir.kind_character(), '/')
114
def test_link_kind_character(self):
115
dir = inventory.InventoryLink('123', 'hello.c', ROOT_ID)
116
self.assertEqual(dir.kind_character(), '')
118
def test_dir_detect_changes(self):
119
left = inventory.InventoryDirectory('123', 'hello.c', ROOT_ID)
121
left.executable = True
122
left.symlink_target='foo'
123
right = inventory.InventoryDirectory('123', 'hello.c', ROOT_ID)
124
right.text_sha1 = 321
125
right.symlink_target='bar'
126
self.assertEqual((False, False), left.detect_changes(right))
127
self.assertEqual((False, False), right.detect_changes(left))
129
def test_file_detect_changes(self):
130
left = inventory.InventoryFile('123', 'hello.c', ROOT_ID)
132
right = inventory.InventoryFile('123', 'hello.c', ROOT_ID)
133
right.text_sha1 = 123
134
self.assertEqual((False, False), left.detect_changes(right))
135
self.assertEqual((False, False), right.detect_changes(left))
136
left.executable = True
137
self.assertEqual((False, True), left.detect_changes(right))
138
self.assertEqual((False, True), right.detect_changes(left))
139
right.text_sha1 = 321
140
self.assertEqual((True, True), left.detect_changes(right))
141
self.assertEqual((True, True), right.detect_changes(left))
143
def test_symlink_detect_changes(self):
144
left = inventory.InventoryLink('123', 'hello.c', ROOT_ID)
146
left.executable = True
147
left.symlink_target='foo'
148
right = inventory.InventoryLink('123', 'hello.c', ROOT_ID)
149
right.text_sha1 = 321
150
right.symlink_target='foo'
151
self.assertEqual((False, False), left.detect_changes(right))
152
self.assertEqual((False, False), right.detect_changes(left))
153
left.symlink_target = 'different'
154
self.assertEqual((True, False), left.detect_changes(right))
155
self.assertEqual((True, False), right.detect_changes(left))
157
def test_file_has_text(self):
158
file = inventory.InventoryFile('123', 'hello.c', ROOT_ID)
159
self.failUnless(file.has_text())
161
def test_directory_has_text(self):
162
dir = inventory.InventoryDirectory('123', 'hello.c', ROOT_ID)
163
self.failIf(dir.has_text())
165
def test_link_has_text(self):
166
link = inventory.InventoryLink('123', 'hello.c', ROOT_ID)
167
self.failIf(link.has_text())
169
def test_make_entry(self):
170
self.assertIsInstance(inventory.make_entry("file", "name", ROOT_ID),
171
inventory.InventoryFile)
172
self.assertIsInstance(inventory.make_entry("symlink", "name", ROOT_ID),
173
inventory.InventoryLink)
174
self.assertIsInstance(inventory.make_entry("directory", "name", ROOT_ID),
175
inventory.InventoryDirectory)
177
class TestEntryDiffing(TestCaseWithTransport):
180
super(TestEntryDiffing, self).setUp()
181
self.wt = self.make_branch_and_tree('.')
182
self.branch = self.wt.branch
183
print >> open('file', 'wb'), 'foo'
184
print >> open('binfile', 'wb'), 'foo'
185
self.wt.add(['file'], ['fileid'])
186
self.wt.add(['binfile'], ['binfileid'])
188
os.symlink('target1', 'symlink')
189
self.wt.add(['symlink'], ['linkid'])
190
self.wt.commit('message_1', rev_id = '1')
191
print >> open('file', 'wb'), 'bar'
192
print >> open('binfile', 'wb'), 'x' * 1023 + '\x00'
195
os.symlink('target2', 'symlink')
196
self.tree_1 = self.branch.repository.revision_tree('1')
197
self.inv_1 = self.branch.repository.get_inventory('1')
198
self.file_1 = self.inv_1['fileid']
199
self.file_1b = self.inv_1['binfileid']
200
self.tree_2 = self.wt
201
self.inv_2 = self.tree_2.read_working_inventory()
202
self.file_2 = self.inv_2['fileid']
203
self.file_2b = self.inv_2['binfileid']
205
self.link_1 = self.inv_1['linkid']
206
self.link_2 = self.inv_2['linkid']
208
def test_file_diff_deleted(self):
210
self.file_1.diff(internal_diff,
211
"old_label", self.tree_1,
212
"/dev/null", None, None,
214
self.assertEqual(output.getvalue(), "--- old_label\n"
220
def test_file_diff_added(self):
222
self.file_1.diff(internal_diff,
223
"new_label", self.tree_1,
224
"/dev/null", None, None,
225
output, reverse=True)
226
self.assertEqual(output.getvalue(), "--- /dev/null\n"
232
def test_file_diff_changed(self):
234
self.file_1.diff(internal_diff,
235
"/dev/null", self.tree_1,
236
"new_label", self.file_2, self.tree_2,
238
self.assertEqual(output.getvalue(), "--- /dev/null\n"
245
def test_file_diff_binary(self):
247
self.file_1.diff(internal_diff,
248
"/dev/null", self.tree_1,
249
"new_label", self.file_2b, self.tree_2,
251
self.assertEqual(output.getvalue(),
252
"Binary files /dev/null and new_label differ\n")
253
def test_link_diff_deleted(self):
254
if not has_symlinks():
257
self.link_1.diff(internal_diff,
258
"old_label", self.tree_1,
259
"/dev/null", None, None,
261
self.assertEqual(output.getvalue(),
262
"=== target was 'target1'\n")
264
def test_link_diff_added(self):
265
if not has_symlinks():
268
self.link_1.diff(internal_diff,
269
"new_label", self.tree_1,
270
"/dev/null", None, None,
271
output, reverse=True)
272
self.assertEqual(output.getvalue(),
273
"=== target is 'target1'\n")
275
def test_link_diff_changed(self):
276
if not has_symlinks():
279
self.link_1.diff(internal_diff,
280
"/dev/null", self.tree_1,
281
"new_label", self.link_2, self.tree_2,
283
self.assertEqual(output.getvalue(),
284
"=== target changed 'target1' => 'target2'\n")
287
class TestSnapshot(TestCaseWithTransport):
290
# for full testing we'll need a branch
291
# with a subdir to test parent changes.
292
# and a file, link and dir under that.
293
# but right now I only need one attribute
294
# to change, and then test merge patterns
295
# with fake parent entries.
296
super(TestSnapshot, self).setUp()
297
self.wt = self.make_branch_and_tree('.')
298
self.branch = self.wt.branch
299
self.build_tree(['subdir/', 'subdir/file'], line_endings='binary')
300
self.wt.add(['subdir', 'subdir/file'],
304
self.wt.commit('message_1', rev_id = '1')
305
self.tree_1 = self.branch.repository.revision_tree('1')
306
self.inv_1 = self.branch.repository.get_inventory('1')
307
self.file_1 = self.inv_1['fileid']
308
self.file_active = self.wt.inventory['fileid']
309
self.builder = self.branch.get_commit_builder([], timestamp=time.time(), revision_id='2')
311
def test_snapshot_new_revision(self):
312
# This tests that a simple commit with no parents makes a new
313
# revision value in the inventory entry
314
self.file_active.snapshot('2', 'subdir/file', {}, self.wt, self.builder)
315
# expected outcome - file_1 has a revision id of '2', and we can get
316
# its text of 'file contents' out of the weave.
317
self.assertEqual(self.file_1.revision, '1')
318
self.assertEqual(self.file_active.revision, '2')
319
# this should be a separate test probably, but lets check it once..
320
lines = self.branch.repository.weave_store.get_weave(
322
self.branch.get_transaction()).get_lines('2')
323
self.assertEqual(lines, ['contents of subdir/file\n'])
325
def test_snapshot_unchanged(self):
326
#This tests that a simple commit does not make a new entry for
327
# an unchanged inventory entry
328
self.file_active.snapshot('2', 'subdir/file', {'1':self.file_1},
329
self.wt, self.builder)
330
self.assertEqual(self.file_1.revision, '1')
331
self.assertEqual(self.file_active.revision, '1')
332
vf = self.branch.repository.weave_store.get_weave(
334
self.branch.repository.get_transaction())
335
self.assertRaises(errors.RevisionNotPresent,
339
def test_snapshot_merge_identical_different_revid(self):
340
# This tests that a commit with two identical parents, one of which has
341
# a different revision id, results in a new revision id in the entry.
342
# 1->other, commit a merge of other against 1, results in 2.
343
other_ie = inventory.InventoryFile('fileid', 'newname', self.file_1.parent_id)
344
other_ie = inventory.InventoryFile('fileid', 'file', self.file_1.parent_id)
345
other_ie.revision = '1'
346
other_ie.text_sha1 = self.file_1.text_sha1
347
other_ie.text_size = self.file_1.text_size
348
self.assertEqual(self.file_1, other_ie)
349
other_ie.revision = 'other'
350
self.assertNotEqual(self.file_1, other_ie)
351
versionfile = self.branch.repository.weave_store.get_weave(
352
'fileid', self.branch.repository.get_transaction())
353
versionfile.clone_text('other', '1', ['1'])
354
self.file_active.snapshot('2', 'subdir/file',
355
{'1':self.file_1, 'other':other_ie},
356
self.wt, self.builder)
357
self.assertEqual(self.file_active.revision, '2')
359
def test_snapshot_changed(self):
360
# This tests that a commit with one different parent results in a new
361
# revision id in the entry.
362
self.file_active.name='newname'
363
rename('subdir/file', 'subdir/newname')
364
self.file_active.snapshot('2', 'subdir/newname', {'1':self.file_1},
365
self.wt, self.builder)
366
# expected outcome - file_1 has a revision id of '2'
367
self.assertEqual(self.file_active.revision, '2')
370
class TestPreviousHeads(TestCaseWithTransport):
373
# we want several inventories, that respectively
374
# give use the following scenarios:
375
# A) fileid not in any inventory (A),
376
# B) fileid present in one inventory (B) and (A,B)
377
# C) fileid present in two inventories, and they
378
# are not mutual descendents (B, C)
379
# D) fileid present in two inventories and one is
380
# a descendent of the other. (B, D)
381
super(TestPreviousHeads, self).setUp()
382
self.wt = self.make_branch_and_tree('.')
383
self.branch = self.wt.branch
384
self.build_tree(['file'])
385
self.wt.commit('new branch', allow_pointless=True, rev_id='A')
386
self.inv_A = self.branch.repository.get_inventory('A')
387
self.wt.add(['file'], ['fileid'])
388
self.wt.commit('add file', rev_id='B')
389
self.inv_B = self.branch.repository.get_inventory('B')
390
uncommit(self.branch, tree=self.wt)
391
self.assertEqual(self.branch.revision_history(), ['A'])
392
self.wt.commit('another add of file', rev_id='C')
393
self.inv_C = self.branch.repository.get_inventory('C')
394
self.wt.add_pending_merge('B')
395
self.wt.commit('merge in B', rev_id='D')
396
self.inv_D = self.branch.repository.get_inventory('D')
397
self.file_active = self.wt.inventory['fileid']
398
self.weave = self.branch.repository.weave_store.get_weave('fileid',
399
self.branch.repository.get_transaction())
401
def get_previous_heads(self, inventories):
402
return self.file_active.find_previous_heads(
404
self.branch.repository.weave_store,
405
self.branch.repository.get_transaction())
407
def test_fileid_in_no_inventory(self):
408
self.assertEqual({}, self.get_previous_heads([self.inv_A]))
410
def test_fileid_in_one_inventory(self):
411
self.assertEqual({'B':self.inv_B['fileid']},
412
self.get_previous_heads([self.inv_B]))
413
self.assertEqual({'B':self.inv_B['fileid']},
414
self.get_previous_heads([self.inv_A, self.inv_B]))
415
self.assertEqual({'B':self.inv_B['fileid']},
416
self.get_previous_heads([self.inv_B, self.inv_A]))
418
def test_fileid_in_two_inventories_gives_both_entries(self):
419
self.assertEqual({'B':self.inv_B['fileid'],
420
'C':self.inv_C['fileid']},
421
self.get_previous_heads([self.inv_B, self.inv_C]))
422
self.assertEqual({'B':self.inv_B['fileid'],
423
'C':self.inv_C['fileid']},
424
self.get_previous_heads([self.inv_C, self.inv_B]))
426
def test_fileid_in_two_inventories_already_merged_gives_head(self):
427
self.assertEqual({'D':self.inv_D['fileid']},
428
self.get_previous_heads([self.inv_B, self.inv_D]))
429
self.assertEqual({'D':self.inv_D['fileid']},
430
self.get_previous_heads([self.inv_D, self.inv_B]))
432
# TODO: test two inventories with the same file revision
435
class TestDescribeChanges(TestCase):
437
def test_describe_change(self):
438
# we need to test the following change combinations:
444
# renamed/reparented and modified
445
# change kind (perhaps can't be done yet?)
446
# also, merged in combination with all of these?
447
old_a = InventoryFile('a-id', 'a_file', ROOT_ID)
448
old_a.text_sha1 = '123132'
450
new_a = InventoryFile('a-id', 'a_file', ROOT_ID)
451
new_a.text_sha1 = '123132'
454
self.assertChangeDescription('unchanged', old_a, new_a)
457
new_a.text_sha1 = 'abcabc'
458
self.assertChangeDescription('modified', old_a, new_a)
460
self.assertChangeDescription('added', None, new_a)
461
self.assertChangeDescription('removed', old_a, None)
462
# perhaps a bit questionable but seems like the most reasonable thing...
463
self.assertChangeDescription('unchanged', None, None)
465
# in this case it's both renamed and modified; show a rename and
467
new_a.name = 'newfilename'
468
self.assertChangeDescription('modified and renamed', old_a, new_a)
470
# reparenting is 'renaming'
471
new_a.name = old_a.name
472
new_a.parent_id = 'somedir-id'
473
self.assertChangeDescription('modified and renamed', old_a, new_a)
475
# reset the content values so its not modified
476
new_a.text_size = old_a.text_size
477
new_a.text_sha1 = old_a.text_sha1
478
new_a.name = old_a.name
480
new_a.name = 'newfilename'
481
self.assertChangeDescription('renamed', old_a, new_a)
483
# reparenting is 'renaming'
484
new_a.name = old_a.name
485
new_a.parent_id = 'somedir-id'
486
self.assertChangeDescription('renamed', old_a, new_a)
488
def assertChangeDescription(self, expected_change, old_ie, new_ie):
489
change = InventoryEntry.describe_change(old_ie, new_ie)
490
self.assertEqual(expected_change, change)
493
class TestExecutable(TestCaseWithTransport):
495
def test_stays_executable(self):
496
a_id = "a-20051208024829-849e76f7968d7a86"
497
b_id = "b-20051208024829-849e76f7968d7a86"
498
wt = self.make_branch_and_tree('b1')
500
tt = TreeTransform(wt)
501
tt.new_file('a', tt.root, 'a test\n', a_id, True)
502
tt.new_file('b', tt.root, 'b test\n', b_id, False)
505
self.failUnless(wt.is_executable(a_id), "'a' lost the execute bit")
507
# reopen the tree and ensure it stuck.
508
wt = wt.bzrdir.open_workingtree()
509
self.assertEqual(['a', 'b'], [cn for cn,ie in wt.inventory.iter_entries()])
511
self.failUnless(wt.is_executable(a_id), "'a' lost the execute bit")
512
self.failIf(wt.is_executable(b_id), "'b' gained an execute bit")
514
wt.commit('adding a,b', rev_id='r1')
516
rev_tree = b.repository.revision_tree('r1')
517
self.failUnless(rev_tree.is_executable(a_id), "'a' lost the execute bit")
518
self.failIf(rev_tree.is_executable(b_id), "'b' gained an execute bit")
520
self.failUnless(rev_tree.inventory[a_id].executable)
521
self.failIf(rev_tree.inventory[b_id].executable)
523
# Make sure the entries are gone
526
self.failIf(wt.has_id(a_id))
527
self.failIf(wt.has_filename('a'))
528
self.failIf(wt.has_id(b_id))
529
self.failIf(wt.has_filename('b'))
531
# Make sure that revert is able to bring them back,
532
# and sets 'a' back to being executable
534
wt.revert(['a', 'b'], rev_tree, backups=False)
535
self.assertEqual(['a', 'b'], [cn for cn,ie in wt.inventory.iter_entries()])
537
self.failUnless(wt.is_executable(a_id), "'a' lost the execute bit")
538
self.failIf(wt.is_executable(b_id), "'b' gained an execute bit")
540
# Now remove them again, and make sure that after a
541
# commit, they are still marked correctly
544
wt.commit('removed', rev_id='r2')
546
self.assertEqual([], [cn for cn,ie in wt.inventory.iter_entries()])
547
self.failIf(wt.has_id(a_id))
548
self.failIf(wt.has_filename('a'))
549
self.failIf(wt.has_id(b_id))
550
self.failIf(wt.has_filename('b'))
552
# Now revert back to the previous commit
553
wt.revert([], rev_tree, backups=False)
554
self.assertEqual(['a', 'b'], [cn for cn,ie in wt.inventory.iter_entries()])
556
self.failUnless(wt.is_executable(a_id), "'a' lost the execute bit")
557
self.failIf(wt.is_executable(b_id), "'b' gained an execute bit")
559
# Now make sure that 'bzr branch' also preserves the
561
# TODO: Maybe this should be a blackbox test
562
d2 = b.bzrdir.clone('b2', revision_id='r1')
563
t2 = d2.open_workingtree()
565
self.assertEquals('r1', b2.last_revision())
567
self.assertEqual(['a', 'b'], [cn for cn,ie in t2.inventory.iter_entries()])
568
self.failUnless(t2.is_executable(a_id), "'a' lost the execute bit")
569
self.failIf(t2.is_executable(b_id), "'b' gained an execute bit")
571
# Make sure pull will delete the files
573
self.assertEquals('r2', b2.last_revision())
574
self.assertEqual([], [cn for cn,ie in t2.inventory.iter_entries()])
576
# Now commit the changes on the first branch
577
# so that the second branch can pull the changes
578
# and make sure that the executable bit has been copied
579
wt.commit('resurrected', rev_id='r3')
582
self.assertEquals('r3', b2.last_revision())
583
self.assertEqual(['a', 'b'], [cn for cn,ie in t2.inventory.iter_entries()])
585
self.failUnless(t2.is_executable(a_id), "'a' lost the execute bit")
586
self.failIf(t2.is_executable(b_id), "'b' gained an execute bit")
589
class TestRevert(TestCaseWithTransport):
591
def test_dangling_id(self):
592
wt = self.make_branch_and_tree('b1')
593
self.assertEqual(len(wt.inventory), 1)
594
open('b1/a', 'wb').write('a test\n')
596
self.assertEqual(len(wt.inventory), 2)
599
self.assertEqual(len(wt.inventory), 1)