1
# Copyright (C) 2005-2012, 2014, 2016 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., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA
1
from bzrlib.selftest import TestCase
2
from bzrlib.diff import internal_diff
18
3
from cStringIO import StringIO
28
revision as _mod_revision,
34
from bzrlib.tests import (
38
from bzrlib.tests.blackbox.test_diff import subst_dates
39
from bzrlib.tests.scenarios import load_tests_apply_scenarios
42
load_tests = load_tests_apply_scenarios
45
def udiff_lines(old, new, allow_binary=False):
4
def udiff_lines(old, new):
47
diff.internal_diff('old', old, 'new', new, output, allow_binary)
6
internal_diff('old', old, 'new', new, output)
49
8
return output.readlines()
52
def external_udiff_lines(old, new, use_stringio=False):
54
# StringIO has no fileno, so it tests a different codepath
57
output = tempfile.TemporaryFile()
59
diff.external_diff('old', old, 'new', new, output, diff_opts=['-u'])
61
raise tests.TestSkipped('external "diff" not present to test')
63
lines = output.readlines()
68
class TestDiffOptions(tests.TestCase):
70
def test_unified_added(self):
71
"""Check for default style '-u' only if no other style specified
74
# Verify that style defaults to unified, id est '-u' appended
75
# to option list, in the absence of an alternative style.
76
self.assertEqual(['-a', '-u'], diff.default_style_unified(['-a']))
79
class TestDiffOptionsScenarios(tests.TestCase):
81
scenarios = [(s, dict(style=s)) for s in diff.style_option_list]
82
style = None # Set by load_tests_apply_scenarios from scenarios
84
def test_unified_not_added(self):
85
# Verify that for all valid style options, '-u' is not
86
# appended to option list.
87
ret_opts = diff.default_style_unified(diff_opts=["%s" % (self.style,)])
88
self.assertEqual(["%s" % (self.style,)], ret_opts)
91
class TestDiff(tests.TestCase):
10
def check_patch(lines):
11
assert len(lines) > 1, \
12
"Not enough lines for a file header for patch:\n%s" % "".join(lines)
13
assert lines[0].startswith ('---'), \
14
'No orig line for patch:\n%s' % "".join(lines)
15
assert lines[1].startswith ('+++'), \
16
'No mod line for patch:\n%s' % "".join(lines)
17
assert len(lines) > 2, \
18
"No hunks for patch:\n%s" % "".join(lines)
19
assert lines[2].startswith('@@'),\
20
"No hunk header for patch:\n%s" % "".join(lines)
21
assert '@@' in lines[2][2:], \
22
"Unterminated hunk header for patch:\n%s" % "".join(lines)
24
class TestDiff(TestCase):
93
25
def test_add_nl(self):
94
26
"""diff generates a valid diff for patches that add a newline"""
95
27
lines = udiff_lines(['boo'], ['boo\n'])
96
self.check_patch(lines)
97
self.assertEqual(lines[4], '\\ No newline at end of file\n')
98
## "expected no-nl, got %r" % lines[4]
29
assert lines[4] == '\\ No newline at end of file\n', \
30
"expected no-nl, got %r" % lines[4]
100
32
def test_add_nl_2(self):
101
33
"""diff generates a valid diff for patches that change last line and
104
36
lines = udiff_lines(['boo'], ['goo\n'])
105
self.check_patch(lines)
106
self.assertEqual(lines[4], '\\ No newline at end of file\n')
107
## "expected no-nl, got %r" % lines[4]
38
assert lines[4] == '\\ No newline at end of file\n', \
39
"expected no-nl, got %r" % lines[4]
109
41
def test_remove_nl(self):
110
42
"""diff generates a valid diff for patches that change last line and
113
45
lines = udiff_lines(['boo\n'], ['boo'])
114
self.check_patch(lines)
115
self.assertEqual(lines[5], '\\ No newline at end of file\n')
116
## "expected no-nl, got %r" % lines[5]
118
def check_patch(self, lines):
119
self.assertTrue(len(lines) > 1)
120
## "Not enough lines for a file header for patch:\n%s" % "".join(lines)
121
self.assertTrue(lines[0].startswith ('---'))
122
## 'No orig line for patch:\n%s' % "".join(lines)
123
self.assertTrue(lines[1].startswith ('+++'))
124
## 'No mod line for patch:\n%s' % "".join(lines)
125
self.assertTrue(len(lines) > 2)
126
## "No hunks for patch:\n%s" % "".join(lines)
127
self.assertTrue(lines[2].startswith('@@'))
128
## "No hunk header for patch:\n%s" % "".join(lines)
129
self.assertTrue('@@' in lines[2][2:])
130
## "Unterminated hunk header for patch:\n%s" % "".join(lines)
132
def test_binary_lines(self):
134
uni_lines = [1023 * 'a' + '\x00']
135
self.assertRaises(errors.BinaryFile, udiff_lines, uni_lines , empty)
136
self.assertRaises(errors.BinaryFile, udiff_lines, empty, uni_lines)
137
udiff_lines(uni_lines , empty, allow_binary=True)
138
udiff_lines(empty, uni_lines, allow_binary=True)
140
def test_external_diff(self):
141
lines = external_udiff_lines(['boo\n'], ['goo\n'])
142
self.check_patch(lines)
143
self.assertEqual('\n', lines[-1])
145
def test_external_diff_no_fileno(self):
146
# Make sure that we can handle not having a fileno, even
147
# if the diff is large
148
lines = external_udiff_lines(['boo\n']*10000,
151
self.check_patch(lines)
153
def test_external_diff_binary_lang_c(self):
154
for lang in ('LANG', 'LC_ALL', 'LANGUAGE'):
155
self.overrideEnv(lang, 'C')
156
lines = external_udiff_lines(['\x00foobar\n'], ['foo\x00bar\n'])
157
# Older versions of diffutils say "Binary files", newer
158
# versions just say "Files".
159
self.assertContainsRe(lines[0], '(Binary f|F)iles old and new differ\n')
160
self.assertEqual(lines[1:], ['\n'])
162
def test_no_external_diff(self):
163
"""Check that NoDiff is raised when diff is not available"""
164
# Make sure no 'diff' command is available
165
# XXX: Weird, using None instead of '' breaks the test -- vila 20101216
166
self.overrideEnv('PATH', '')
167
self.assertRaises(errors.NoDiff, diff.external_diff,
168
'old', ['boo\n'], 'new', ['goo\n'],
169
StringIO(), diff_opts=['-u'])
171
def test_internal_diff_default(self):
172
# Default internal diff encoding is utf8
174
diff.internal_diff(u'old_\xb5', ['old_text\n'],
175
u'new_\xe5', ['new_text\n'], output)
176
lines = output.getvalue().splitlines(True)
177
self.check_patch(lines)
178
self.assertEqual(['--- old_\xc2\xb5\n',
179
'+++ new_\xc3\xa5\n',
187
def test_internal_diff_utf8(self):
189
diff.internal_diff(u'old_\xb5', ['old_text\n'],
190
u'new_\xe5', ['new_text\n'], output,
191
path_encoding='utf8')
192
lines = output.getvalue().splitlines(True)
193
self.check_patch(lines)
194
self.assertEqual(['--- old_\xc2\xb5\n',
195
'+++ new_\xc3\xa5\n',
203
def test_internal_diff_iso_8859_1(self):
205
diff.internal_diff(u'old_\xb5', ['old_text\n'],
206
u'new_\xe5', ['new_text\n'], output,
207
path_encoding='iso-8859-1')
208
lines = output.getvalue().splitlines(True)
209
self.check_patch(lines)
210
self.assertEqual(['--- old_\xb5\n',
219
def test_internal_diff_no_content(self):
221
diff.internal_diff(u'old', [], u'new', [], output)
222
self.assertEqual('', output.getvalue())
224
def test_internal_diff_no_changes(self):
226
diff.internal_diff(u'old', ['text\n', 'contents\n'],
227
u'new', ['text\n', 'contents\n'],
229
self.assertEqual('', output.getvalue())
231
def test_internal_diff_returns_bytes(self):
233
output = StringIO.StringIO()
234
diff.internal_diff(u'old_\xb5', ['old_text\n'],
235
u'new_\xe5', ['new_text\n'], output)
236
self.assertIsInstance(output.getvalue(), str,
237
'internal_diff should return bytestrings')
239
def test_internal_diff_default_context(self):
241
diff.internal_diff('old', ['same_text\n','same_text\n','same_text\n',
242
'same_text\n','same_text\n','old_text\n'],
243
'new', ['same_text\n','same_text\n','same_text\n',
244
'same_text\n','same_text\n','new_text\n'], output)
245
lines = output.getvalue().splitlines(True)
246
self.check_patch(lines)
247
self.assertEqual(['--- old\n',
259
def test_internal_diff_no_context(self):
261
diff.internal_diff('old', ['same_text\n','same_text\n','same_text\n',
262
'same_text\n','same_text\n','old_text\n'],
263
'new', ['same_text\n','same_text\n','same_text\n',
264
'same_text\n','same_text\n','new_text\n'], output,
266
lines = output.getvalue().splitlines(True)
267
self.check_patch(lines)
268
self.assertEqual(['--- old\n',
277
def test_internal_diff_more_context(self):
279
diff.internal_diff('old', ['same_text\n','same_text\n','same_text\n',
280
'same_text\n','same_text\n','old_text\n'],
281
'new', ['same_text\n','same_text\n','same_text\n',
282
'same_text\n','same_text\n','new_text\n'], output,
284
lines = output.getvalue().splitlines(True)
285
self.check_patch(lines)
286
self.assertEqual(['--- old\n',
303
class TestDiffFiles(tests.TestCaseInTempDir):
305
def test_external_diff_binary(self):
306
"""The output when using external diff should use diff's i18n error"""
307
# Make sure external_diff doesn't fail in the current LANG
308
lines = external_udiff_lines(['\x00foobar\n'], ['foo\x00bar\n'])
310
cmd = ['diff', '-u', '--binary', 'old', 'new']
311
with open('old', 'wb') as f: f.write('\x00foobar\n')
312
with open('new', 'wb') as f: f.write('foo\x00bar\n')
313
pipe = subprocess.Popen(cmd, stdout=subprocess.PIPE,
314
stdin=subprocess.PIPE)
315
out, err = pipe.communicate()
316
# Diff returns '2' on Binary files.
317
self.assertEqual(2, pipe.returncode)
318
# We should output whatever diff tells us, plus a trailing newline
319
self.assertEqual(out.splitlines(True) + ['\n'], lines)
322
def get_diff_as_string(tree1, tree2, specific_files=None, working_tree=None):
324
if working_tree is not None:
325
extra_trees = (working_tree,)
328
diff.show_diff_trees(tree1, tree2, output,
329
specific_files=specific_files,
330
extra_trees=extra_trees, old_label='old/',
332
return output.getvalue()
335
class TestDiffDates(tests.TestCaseWithTransport):
338
super(TestDiffDates, self).setUp()
339
self.wt = self.make_branch_and_tree('.')
340
self.b = self.wt.branch
341
self.build_tree_contents([
342
('file1', 'file1 contents at rev 1\n'),
343
('file2', 'file2 contents at rev 1\n')
345
self.wt.add(['file1', 'file2'])
347
message='Revision 1',
348
timestamp=1143849600, # 2006-04-01 00:00:00 UTC
351
self.build_tree_contents([('file1', 'file1 contents at rev 2\n')])
353
message='Revision 2',
354
timestamp=1143936000, # 2006-04-02 00:00:00 UTC
357
self.build_tree_contents([('file2', 'file2 contents at rev 3\n')])
359
message='Revision 3',
360
timestamp=1144022400, # 2006-04-03 00:00:00 UTC
363
self.wt.remove(['file2'])
365
message='Revision 4',
366
timestamp=1144108800, # 2006-04-04 00:00:00 UTC
369
self.build_tree_contents([
370
('file1', 'file1 contents in working tree\n')
372
# set the date stamps for files in the working tree to known values
373
os.utime('file1', (1144195200, 1144195200)) # 2006-04-05 00:00:00 UTC
375
def test_diff_rev_tree_working_tree(self):
376
output = get_diff_as_string(self.wt.basis_tree(), self.wt)
377
# note that the date for old/file1 is from rev 2 rather than from
378
# the basis revision (rev 4)
379
self.assertEqualDiff(output, '''\
380
=== modified file 'file1'
381
--- old/file1\t2006-04-02 00:00:00 +0000
382
+++ new/file1\t2006-04-05 00:00:00 +0000
384
-file1 contents at rev 2
385
+file1 contents in working tree
389
def test_diff_rev_tree_rev_tree(self):
390
tree1 = self.b.repository.revision_tree('rev-2')
391
tree2 = self.b.repository.revision_tree('rev-3')
392
output = get_diff_as_string(tree1, tree2)
393
self.assertEqualDiff(output, '''\
394
=== modified file 'file2'
395
--- old/file2\t2006-04-01 00:00:00 +0000
396
+++ new/file2\t2006-04-03 00:00:00 +0000
398
-file2 contents at rev 1
399
+file2 contents at rev 3
403
def test_diff_add_files(self):
404
tree1 = self.b.repository.revision_tree(_mod_revision.NULL_REVISION)
405
tree2 = self.b.repository.revision_tree('rev-1')
406
output = get_diff_as_string(tree1, tree2)
407
# the files have the epoch time stamp for the tree in which
409
self.assertEqualDiff(output, '''\
410
=== added file 'file1'
411
--- old/file1\t1970-01-01 00:00:00 +0000
412
+++ new/file1\t2006-04-01 00:00:00 +0000
414
+file1 contents at rev 1
416
=== added file 'file2'
417
--- old/file2\t1970-01-01 00:00:00 +0000
418
+++ new/file2\t2006-04-01 00:00:00 +0000
420
+file2 contents at rev 1
424
def test_diff_remove_files(self):
425
tree1 = self.b.repository.revision_tree('rev-3')
426
tree2 = self.b.repository.revision_tree('rev-4')
427
output = get_diff_as_string(tree1, tree2)
428
# the file has the epoch time stamp for the tree in which
430
self.assertEqualDiff(output, '''\
431
=== removed file 'file2'
432
--- old/file2\t2006-04-03 00:00:00 +0000
433
+++ new/file2\t1970-01-01 00:00:00 +0000
435
-file2 contents at rev 3
439
def test_show_diff_specified(self):
440
"""A working tree filename can be used to identify a file"""
441
self.wt.rename_one('file1', 'file1b')
442
old_tree = self.b.repository.revision_tree('rev-1')
443
new_tree = self.b.repository.revision_tree('rev-4')
444
out = get_diff_as_string(old_tree, new_tree, specific_files=['file1b'],
445
working_tree=self.wt)
446
self.assertContainsRe(out, 'file1\t')
448
def test_recursive_diff(self):
449
"""Children of directories are matched"""
452
self.wt.add(['dir1', 'dir2'])
453
self.wt.rename_one('file1', 'dir1/file1')
454
old_tree = self.b.repository.revision_tree('rev-1')
455
new_tree = self.b.repository.revision_tree('rev-4')
456
out = get_diff_as_string(old_tree, new_tree, specific_files=['dir1'],
457
working_tree=self.wt)
458
self.assertContainsRe(out, 'file1\t')
459
out = get_diff_as_string(old_tree, new_tree, specific_files=['dir2'],
460
working_tree=self.wt)
461
self.assertNotContainsRe(out, 'file1\t')
464
class TestShowDiffTrees(tests.TestCaseWithTransport):
465
"""Direct tests for show_diff_trees"""
467
def test_modified_file(self):
468
"""Test when a file is modified."""
469
tree = self.make_branch_and_tree('tree')
470
self.build_tree_contents([('tree/file', 'contents\n')])
471
tree.add(['file'], ['file-id'])
472
tree.commit('one', rev_id='rev-1')
474
self.build_tree_contents([('tree/file', 'new contents\n')])
475
d = get_diff_as_string(tree.basis_tree(), tree)
476
self.assertContainsRe(d, "=== modified file 'file'\n")
477
self.assertContainsRe(d, '--- old/file\t')
478
self.assertContainsRe(d, '\\+\\+\\+ new/file\t')
479
self.assertContainsRe(d, '-contents\n'
482
def test_modified_file_in_renamed_dir(self):
483
"""Test when a file is modified in a renamed directory."""
484
tree = self.make_branch_and_tree('tree')
485
self.build_tree(['tree/dir/'])
486
self.build_tree_contents([('tree/dir/file', 'contents\n')])
487
tree.add(['dir', 'dir/file'], ['dir-id', 'file-id'])
488
tree.commit('one', rev_id='rev-1')
490
tree.rename_one('dir', 'other')
491
self.build_tree_contents([('tree/other/file', 'new contents\n')])
492
d = get_diff_as_string(tree.basis_tree(), tree)
493
self.assertContainsRe(d, "=== renamed directory 'dir' => 'other'\n")
494
self.assertContainsRe(d, "=== modified file 'other/file'\n")
495
# XXX: This is technically incorrect, because it used to be at another
496
# location. What to do?
497
self.assertContainsRe(d, '--- old/dir/file\t')
498
self.assertContainsRe(d, '\\+\\+\\+ new/other/file\t')
499
self.assertContainsRe(d, '-contents\n'
502
def test_renamed_directory(self):
503
"""Test when only a directory is only renamed."""
504
tree = self.make_branch_and_tree('tree')
505
self.build_tree(['tree/dir/'])
506
self.build_tree_contents([('tree/dir/file', 'contents\n')])
507
tree.add(['dir', 'dir/file'], ['dir-id', 'file-id'])
508
tree.commit('one', rev_id='rev-1')
510
tree.rename_one('dir', 'newdir')
511
d = get_diff_as_string(tree.basis_tree(), tree)
512
# Renaming a directory should be a single "you renamed this dir" even
513
# when there are files inside.
514
self.assertEqual(d, "=== renamed directory 'dir' => 'newdir'\n")
516
def test_renamed_file(self):
517
"""Test when a file is only renamed."""
518
tree = self.make_branch_and_tree('tree')
519
self.build_tree_contents([('tree/file', 'contents\n')])
520
tree.add(['file'], ['file-id'])
521
tree.commit('one', rev_id='rev-1')
523
tree.rename_one('file', 'newname')
524
d = get_diff_as_string(tree.basis_tree(), tree)
525
self.assertContainsRe(d, "=== renamed file 'file' => 'newname'\n")
526
# We shouldn't have a --- or +++ line, because there is no content
528
self.assertNotContainsRe(d, '---')
530
def test_renamed_and_modified_file(self):
531
"""Test when a file is only renamed."""
532
tree = self.make_branch_and_tree('tree')
533
self.build_tree_contents([('tree/file', 'contents\n')])
534
tree.add(['file'], ['file-id'])
535
tree.commit('one', rev_id='rev-1')
537
tree.rename_one('file', 'newname')
538
self.build_tree_contents([('tree/newname', 'new contents\n')])
539
d = get_diff_as_string(tree.basis_tree(), tree)
540
self.assertContainsRe(d, "=== renamed file 'file' => 'newname'\n")
541
self.assertContainsRe(d, '--- old/file\t')
542
self.assertContainsRe(d, '\\+\\+\\+ new/newname\t')
543
self.assertContainsRe(d, '-contents\n'
547
def test_internal_diff_exec_property(self):
548
tree = self.make_branch_and_tree('tree')
550
tt = transform.TreeTransform(tree)
551
tt.new_file('a', tt.root, 'contents\n', 'a-id', True)
552
tt.new_file('b', tt.root, 'contents\n', 'b-id', False)
553
tt.new_file('c', tt.root, 'contents\n', 'c-id', True)
554
tt.new_file('d', tt.root, 'contents\n', 'd-id', False)
555
tt.new_file('e', tt.root, 'contents\n', 'control-e-id', True)
556
tt.new_file('f', tt.root, 'contents\n', 'control-f-id', False)
558
tree.commit('one', rev_id='rev-1')
560
tt = transform.TreeTransform(tree)
561
tt.set_executability(False, tt.trans_id_file_id('a-id'))
562
tt.set_executability(True, tt.trans_id_file_id('b-id'))
563
tt.set_executability(False, tt.trans_id_file_id('c-id'))
564
tt.set_executability(True, tt.trans_id_file_id('d-id'))
566
tree.rename_one('c', 'new-c')
567
tree.rename_one('d', 'new-d')
569
d = get_diff_as_string(tree.basis_tree(), tree)
571
self.assertContainsRe(d, r"file 'a'.*\(properties changed:"
573
self.assertContainsRe(d, r"file 'b'.*\(properties changed:"
575
self.assertContainsRe(d, r"file 'c'.*\(properties changed:"
577
self.assertContainsRe(d, r"file 'd'.*\(properties changed:"
579
self.assertNotContainsRe(d, r"file 'e'")
580
self.assertNotContainsRe(d, r"file 'f'")
582
def test_binary_unicode_filenames(self):
583
"""Test that contents of files are *not* encoded in UTF-8 when there
584
is a binary file in the diff.
586
# See https://bugs.launchpad.net/bugs/110092.
587
self.requireFeature(features.UnicodeFilenameFeature)
589
# This bug isn't triggered with cStringIO.
590
from StringIO import StringIO
591
tree = self.make_branch_and_tree('tree')
592
alpha, omega = u'\u03b1', u'\u03c9'
593
alpha_utf8, omega_utf8 = alpha.encode('utf8'), omega.encode('utf8')
594
self.build_tree_contents(
595
[('tree/' + alpha, chr(0)),
597
('The %s and the %s\n' % (alpha_utf8, omega_utf8)))])
598
tree.add([alpha], ['file-id'])
599
tree.add([omega], ['file-id-2'])
600
diff_content = StringIO()
601
diff.show_diff_trees(tree.basis_tree(), tree, diff_content)
602
d = diff_content.getvalue()
603
self.assertContainsRe(d, r"=== added file '%s'" % alpha_utf8)
604
self.assertContainsRe(d, "Binary files a/%s.*and b/%s.* differ\n"
605
% (alpha_utf8, alpha_utf8))
606
self.assertContainsRe(d, r"=== added file '%s'" % omega_utf8)
607
self.assertContainsRe(d, r"--- a/%s" % (omega_utf8,))
608
self.assertContainsRe(d, r"\+\+\+ b/%s" % (omega_utf8,))
610
def test_unicode_filename(self):
611
"""Test when the filename are unicode."""
612
self.requireFeature(features.UnicodeFilenameFeature)
614
alpha, omega = u'\u03b1', u'\u03c9'
615
autf8, outf8 = alpha.encode('utf8'), omega.encode('utf8')
617
tree = self.make_branch_and_tree('tree')
618
self.build_tree_contents([('tree/ren_'+alpha, 'contents\n')])
619
tree.add(['ren_'+alpha], ['file-id-2'])
620
self.build_tree_contents([('tree/del_'+alpha, 'contents\n')])
621
tree.add(['del_'+alpha], ['file-id-3'])
622
self.build_tree_contents([('tree/mod_'+alpha, 'contents\n')])
623
tree.add(['mod_'+alpha], ['file-id-4'])
625
tree.commit('one', rev_id='rev-1')
627
tree.rename_one('ren_'+alpha, 'ren_'+omega)
628
tree.remove('del_'+alpha)
629
self.build_tree_contents([('tree/add_'+alpha, 'contents\n')])
630
tree.add(['add_'+alpha], ['file-id'])
631
self.build_tree_contents([('tree/mod_'+alpha, 'contents_mod\n')])
633
d = get_diff_as_string(tree.basis_tree(), tree)
634
self.assertContainsRe(d,
635
"=== renamed file 'ren_%s' => 'ren_%s'\n"%(autf8, outf8))
636
self.assertContainsRe(d, "=== added file 'add_%s'"%autf8)
637
self.assertContainsRe(d, "=== modified file 'mod_%s'"%autf8)
638
self.assertContainsRe(d, "=== removed file 'del_%s'"%autf8)
640
def test_unicode_filename_path_encoding(self):
641
"""Test for bug #382699: unicode filenames on Windows should be shown
644
self.requireFeature(features.UnicodeFilenameFeature)
645
# The word 'test' in Russian
646
_russian_test = u'\u0422\u0435\u0441\u0442'
647
directory = _russian_test + u'/'
648
test_txt = _russian_test + u'.txt'
649
u1234 = u'\u1234.txt'
651
tree = self.make_branch_and_tree('.')
652
self.build_tree_contents([
657
tree.add([test_txt, u1234, directory])
660
diff.show_diff_trees(tree.basis_tree(), tree, sio,
661
path_encoding='cp1251')
663
output = subst_dates(sio.getvalue())
665
=== added directory '%(directory)s'
666
=== added file '%(test_txt)s'
667
--- a/%(test_txt)s\tYYYY-MM-DD HH:MM:SS +ZZZZ
668
+++ b/%(test_txt)s\tYYYY-MM-DD HH:MM:SS +ZZZZ
672
=== added file '?.txt'
673
--- a/?.txt\tYYYY-MM-DD HH:MM:SS +ZZZZ
674
+++ b/?.txt\tYYYY-MM-DD HH:MM:SS +ZZZZ
678
''' % {'directory': _russian_test.encode('cp1251'),
679
'test_txt': test_txt.encode('cp1251'),
681
self.assertEqualDiff(output, shouldbe)
684
class DiffWasIs(diff.DiffPath):
686
def diff(self, file_id, old_path, new_path, old_kind, new_kind):
687
self.to_file.write('was: ')
688
self.to_file.write(self.old_tree.get_file(file_id).read())
689
self.to_file.write('is: ')
690
self.to_file.write(self.new_tree.get_file(file_id).read())
694
class TestDiffTree(tests.TestCaseWithTransport):
697
super(TestDiffTree, self).setUp()
698
self.old_tree = self.make_branch_and_tree('old-tree')
699
self.old_tree.lock_write()
700
self.addCleanup(self.old_tree.unlock)
701
self.new_tree = self.make_branch_and_tree('new-tree')
702
self.new_tree.lock_write()
703
self.addCleanup(self.new_tree.unlock)
704
self.differ = diff.DiffTree(self.old_tree, self.new_tree, StringIO())
706
def test_diff_text(self):
707
self.build_tree_contents([('old-tree/olddir/',),
708
('old-tree/olddir/oldfile', 'old\n')])
709
self.old_tree.add('olddir')
710
self.old_tree.add('olddir/oldfile', 'file-id')
711
self.build_tree_contents([('new-tree/newdir/',),
712
('new-tree/newdir/newfile', 'new\n')])
713
self.new_tree.add('newdir')
714
self.new_tree.add('newdir/newfile', 'file-id')
715
differ = diff.DiffText(self.old_tree, self.new_tree, StringIO())
716
differ.diff_text('file-id', None, 'old label', 'new label')
718
'--- old label\n+++ new label\n@@ -1,1 +0,0 @@\n-old\n\n',
719
differ.to_file.getvalue())
720
differ.to_file.seek(0)
721
differ.diff_text(None, 'file-id', 'old label', 'new label')
723
'--- old label\n+++ new label\n@@ -0,0 +1,1 @@\n+new\n\n',
724
differ.to_file.getvalue())
725
differ.to_file.seek(0)
726
differ.diff_text('file-id', 'file-id', 'old label', 'new label')
728
'--- old label\n+++ new label\n@@ -1,1 +1,1 @@\n-old\n+new\n\n',
729
differ.to_file.getvalue())
731
def test_diff_deletion(self):
732
self.build_tree_contents([('old-tree/file', 'contents'),
733
('new-tree/file', 'contents')])
734
self.old_tree.add('file', 'file-id')
735
self.new_tree.add('file', 'file-id')
736
os.unlink('new-tree/file')
737
self.differ.show_diff(None)
738
self.assertContainsRe(self.differ.to_file.getvalue(), '-contents')
740
def test_diff_creation(self):
741
self.build_tree_contents([('old-tree/file', 'contents'),
742
('new-tree/file', 'contents')])
743
self.old_tree.add('file', 'file-id')
744
self.new_tree.add('file', 'file-id')
745
os.unlink('old-tree/file')
746
self.differ.show_diff(None)
747
self.assertContainsRe(self.differ.to_file.getvalue(), '\+contents')
749
def test_diff_symlink(self):
750
differ = diff.DiffSymlink(self.old_tree, self.new_tree, StringIO())
751
differ.diff_symlink('old target', None)
752
self.assertEqual("=== target was 'old target'\n",
753
differ.to_file.getvalue())
755
differ = diff.DiffSymlink(self.old_tree, self.new_tree, StringIO())
756
differ.diff_symlink(None, 'new target')
757
self.assertEqual("=== target is 'new target'\n",
758
differ.to_file.getvalue())
760
differ = diff.DiffSymlink(self.old_tree, self.new_tree, StringIO())
761
differ.diff_symlink('old target', 'new target')
762
self.assertEqual("=== target changed 'old target' => 'new target'\n",
763
differ.to_file.getvalue())
766
self.build_tree_contents([('old-tree/olddir/',),
767
('old-tree/olddir/oldfile', 'old\n')])
768
self.old_tree.add('olddir')
769
self.old_tree.add('olddir/oldfile', 'file-id')
770
self.build_tree_contents([('new-tree/newdir/',),
771
('new-tree/newdir/newfile', 'new\n')])
772
self.new_tree.add('newdir')
773
self.new_tree.add('newdir/newfile', 'file-id')
774
self.differ.diff('file-id', 'olddir/oldfile', 'newdir/newfile')
775
self.assertContainsRe(
776
self.differ.to_file.getvalue(),
777
r'--- olddir/oldfile.*\n\+\+\+ newdir/newfile.*\n\@\@ -1,1 \+1,1'
778
' \@\@\n-old\n\+new\n\n')
780
def test_diff_kind_change(self):
781
self.requireFeature(features.SymlinkFeature)
782
self.build_tree_contents([('old-tree/olddir/',),
783
('old-tree/olddir/oldfile', 'old\n')])
784
self.old_tree.add('olddir')
785
self.old_tree.add('olddir/oldfile', 'file-id')
786
self.build_tree(['new-tree/newdir/'])
787
os.symlink('new', 'new-tree/newdir/newfile')
788
self.new_tree.add('newdir')
789
self.new_tree.add('newdir/newfile', 'file-id')
790
self.differ.diff('file-id', 'olddir/oldfile', 'newdir/newfile')
791
self.assertContainsRe(
792
self.differ.to_file.getvalue(),
793
r'--- olddir/oldfile.*\n\+\+\+ newdir/newfile.*\n\@\@ -1,1 \+0,0'
795
self.assertContainsRe(self.differ.to_file.getvalue(),
796
"=== target is u'new'\n")
798
def test_diff_directory(self):
799
self.build_tree(['new-tree/new-dir/'])
800
self.new_tree.add('new-dir', 'new-dir-id')
801
self.differ.diff('new-dir-id', None, 'new-dir')
802
self.assertEqual(self.differ.to_file.getvalue(), '')
804
def create_old_new(self):
805
self.build_tree_contents([('old-tree/olddir/',),
806
('old-tree/olddir/oldfile', 'old\n')])
807
self.old_tree.add('olddir')
808
self.old_tree.add('olddir/oldfile', 'file-id')
809
self.build_tree_contents([('new-tree/newdir/',),
810
('new-tree/newdir/newfile', 'new\n')])
811
self.new_tree.add('newdir')
812
self.new_tree.add('newdir/newfile', 'file-id')
814
def test_register_diff(self):
815
self.create_old_new()
816
old_diff_factories = diff.DiffTree.diff_factories
817
diff.DiffTree.diff_factories=old_diff_factories[:]
818
diff.DiffTree.diff_factories.insert(0, DiffWasIs.from_diff_tree)
820
differ = diff.DiffTree(self.old_tree, self.new_tree, StringIO())
822
diff.DiffTree.diff_factories = old_diff_factories
823
differ.diff('file-id', 'olddir/oldfile', 'newdir/newfile')
824
self.assertNotContainsRe(
825
differ.to_file.getvalue(),
826
r'--- olddir/oldfile.*\n\+\+\+ newdir/newfile.*\n\@\@ -1,1 \+1,1'
827
' \@\@\n-old\n\+new\n\n')
828
self.assertContainsRe(differ.to_file.getvalue(),
829
'was: old\nis: new\n')
831
def test_extra_factories(self):
832
self.create_old_new()
833
differ = diff.DiffTree(self.old_tree, self.new_tree, StringIO(),
834
extra_factories=[DiffWasIs.from_diff_tree])
835
differ.diff('file-id', 'olddir/oldfile', 'newdir/newfile')
836
self.assertNotContainsRe(
837
differ.to_file.getvalue(),
838
r'--- olddir/oldfile.*\n\+\+\+ newdir/newfile.*\n\@\@ -1,1 \+1,1'
839
' \@\@\n-old\n\+new\n\n')
840
self.assertContainsRe(differ.to_file.getvalue(),
841
'was: old\nis: new\n')
843
def test_alphabetical_order(self):
844
self.build_tree(['new-tree/a-file'])
845
self.new_tree.add('a-file')
846
self.build_tree(['old-tree/b-file'])
847
self.old_tree.add('b-file')
848
self.differ.show_diff(None)
849
self.assertContainsRe(self.differ.to_file.getvalue(),
850
'.*a-file(.|\n)*b-file')
853
class TestPatienceDiffLib(tests.TestCase):
856
super(TestPatienceDiffLib, self).setUp()
857
self._unique_lcs = _patiencediff_py.unique_lcs_py
858
self._recurse_matches = _patiencediff_py.recurse_matches_py
859
self._PatienceSequenceMatcher = \
860
_patiencediff_py.PatienceSequenceMatcher_py
862
def test_diff_unicode_string(self):
863
a = ''.join([unichr(i) for i in range(4000, 4500, 3)])
864
b = ''.join([unichr(i) for i in range(4300, 4800, 2)])
865
sm = self._PatienceSequenceMatcher(None, a, b)
866
mb = sm.get_matching_blocks()
867
self.assertEqual(35, len(mb))
869
def test_unique_lcs(self):
870
unique_lcs = self._unique_lcs
871
self.assertEqual(unique_lcs('', ''), [])
872
self.assertEqual(unique_lcs('', 'a'), [])
873
self.assertEqual(unique_lcs('a', ''), [])
874
self.assertEqual(unique_lcs('a', 'a'), [(0,0)])
875
self.assertEqual(unique_lcs('a', 'b'), [])
876
self.assertEqual(unique_lcs('ab', 'ab'), [(0,0), (1,1)])
877
self.assertEqual(unique_lcs('abcde', 'cdeab'), [(2,0), (3,1), (4,2)])
878
self.assertEqual(unique_lcs('cdeab', 'abcde'), [(0,2), (1,3), (2,4)])
879
self.assertEqual(unique_lcs('abXde', 'abYde'), [(0,0), (1,1),
881
self.assertEqual(unique_lcs('acbac', 'abc'), [(2,1)])
883
def test_recurse_matches(self):
884
def test_one(a, b, matches):
886
self._recurse_matches(
887
a, b, 0, 0, len(a), len(b), test_matches, 10)
888
self.assertEqual(test_matches, matches)
890
test_one(['a', '', 'b', '', 'c'], ['a', 'a', 'b', 'c', 'c'],
891
[(0, 0), (2, 2), (4, 4)])
892
test_one(['a', 'c', 'b', 'a', 'c'], ['a', 'b', 'c'],
893
[(0, 0), (2, 1), (4, 2)])
894
# Even though 'bc' is not unique globally, and is surrounded by
895
# non-matching lines, we should still match, because they are locally
897
test_one('abcdbce', 'afbcgdbce', [(0,0), (1, 2), (2, 3), (3, 5),
898
(4, 6), (5, 7), (6, 8)])
900
# recurse_matches doesn't match non-unique
901
# lines surrounded by bogus text.
902
# The update has been done in patiencediff.SequenceMatcher instead
904
# This is what it could be
905
#test_one('aBccDe', 'abccde', [(0,0), (2,2), (3,3), (5,5)])
907
# This is what it currently gives:
908
test_one('aBccDe', 'abccde', [(0,0), (5,5)])
910
def assertDiffBlocks(self, a, b, expected_blocks):
911
"""Check that the sequence matcher returns the correct blocks.
913
:param a: A sequence to match
914
:param b: Another sequence to match
915
:param expected_blocks: The expected output, not including the final
916
matching block (len(a), len(b), 0)
918
matcher = self._PatienceSequenceMatcher(None, a, b)
919
blocks = matcher.get_matching_blocks()
921
self.assertEqual((len(a), len(b), 0), last)
922
self.assertEqual(expected_blocks, blocks)
924
def test_matching_blocks(self):
925
# Some basic matching tests
926
self.assertDiffBlocks('', '', [])
927
self.assertDiffBlocks([], [], [])
928
self.assertDiffBlocks('abc', '', [])
929
self.assertDiffBlocks('', 'abc', [])
930
self.assertDiffBlocks('abcd', 'abcd', [(0, 0, 4)])
931
self.assertDiffBlocks('abcd', 'abce', [(0, 0, 3)])
932
self.assertDiffBlocks('eabc', 'abce', [(1, 0, 3)])
933
self.assertDiffBlocks('eabce', 'abce', [(1, 0, 4)])
934
self.assertDiffBlocks('abcde', 'abXde', [(0, 0, 2), (3, 3, 2)])
935
self.assertDiffBlocks('abcde', 'abXYZde', [(0, 0, 2), (3, 5, 2)])
936
self.assertDiffBlocks('abde', 'abXYZde', [(0, 0, 2), (2, 5, 2)])
937
# This may check too much, but it checks to see that
938
# a copied block stays attached to the previous section,
940
# difflib would tend to grab the trailing longest match
941
# which would make the diff not look right
942
self.assertDiffBlocks('abcdefghijklmnop', 'abcdefxydefghijklmnop',
943
[(0, 0, 6), (6, 11, 10)])
945
# make sure it supports passing in lists
946
self.assertDiffBlocks(
949
'how are you today?\n'],
951
'how are you today?\n'],
952
[(0, 0, 1), (2, 1, 1)])
954
# non unique lines surrounded by non-matching lines
956
self.assertDiffBlocks('aBccDe', 'abccde', [(0,0,1), (5,5,1)])
958
# But they only need to be locally unique
959
self.assertDiffBlocks('aBcDec', 'abcdec', [(0,0,1), (2,2,1), (4,4,2)])
961
# non unique blocks won't be matched
962
self.assertDiffBlocks('aBcdEcdFg', 'abcdecdfg', [(0,0,1), (8,8,1)])
964
# but locally unique ones will
965
self.assertDiffBlocks('aBcdEeXcdFg', 'abcdecdfg', [(0,0,1), (2,2,2),
966
(5,4,1), (7,5,2), (10,8,1)])
968
self.assertDiffBlocks('abbabbXd', 'cabbabxd', [(7,7,1)])
969
self.assertDiffBlocks('abbabbbb', 'cabbabbc', [])
970
self.assertDiffBlocks('bbbbbbbb', 'cbbbbbbc', [])
972
def test_matching_blocks_tuples(self):
973
# Some basic matching tests
974
self.assertDiffBlocks([], [], [])
975
self.assertDiffBlocks([('a',), ('b',), ('c,')], [], [])
976
self.assertDiffBlocks([], [('a',), ('b',), ('c,')], [])
977
self.assertDiffBlocks([('a',), ('b',), ('c,')],
978
[('a',), ('b',), ('c,')],
980
self.assertDiffBlocks([('a',), ('b',), ('c,')],
981
[('a',), ('b',), ('d,')],
983
self.assertDiffBlocks([('d',), ('b',), ('c,')],
984
[('a',), ('b',), ('c,')],
986
self.assertDiffBlocks([('d',), ('a',), ('b',), ('c,')],
987
[('a',), ('b',), ('c,')],
989
self.assertDiffBlocks([('a', 'b'), ('c', 'd'), ('e', 'f')],
990
[('a', 'b'), ('c', 'X'), ('e', 'f')],
991
[(0, 0, 1), (2, 2, 1)])
992
self.assertDiffBlocks([('a', 'b'), ('c', 'd'), ('e', 'f')],
993
[('a', 'b'), ('c', 'dX'), ('e', 'f')],
994
[(0, 0, 1), (2, 2, 1)])
996
def test_opcodes(self):
997
def chk_ops(a, b, expected_codes):
998
s = self._PatienceSequenceMatcher(None, a, b)
999
self.assertEqual(expected_codes, s.get_opcodes())
1003
chk_ops('abc', '', [('delete', 0,3, 0,0)])
1004
chk_ops('', 'abc', [('insert', 0,0, 0,3)])
1005
chk_ops('abcd', 'abcd', [('equal', 0,4, 0,4)])
1006
chk_ops('abcd', 'abce', [('equal', 0,3, 0,3),
1007
('replace', 3,4, 3,4)
1009
chk_ops('eabc', 'abce', [('delete', 0,1, 0,0),
1010
('equal', 1,4, 0,3),
1011
('insert', 4,4, 3,4)
1013
chk_ops('eabce', 'abce', [('delete', 0,1, 0,0),
1016
chk_ops('abcde', 'abXde', [('equal', 0,2, 0,2),
1017
('replace', 2,3, 2,3),
1020
chk_ops('abcde', 'abXYZde', [('equal', 0,2, 0,2),
1021
('replace', 2,3, 2,5),
1024
chk_ops('abde', 'abXYZde', [('equal', 0,2, 0,2),
1025
('insert', 2,2, 2,5),
1028
chk_ops('abcdefghijklmnop', 'abcdefxydefghijklmnop',
1029
[('equal', 0,6, 0,6),
1030
('insert', 6,6, 6,11),
1031
('equal', 6,16, 11,21)
1036
, 'how are you today?\n'],
1038
, 'how are you today?\n'],
1039
[('equal', 0,1, 0,1),
1040
('delete', 1,2, 1,1),
1041
('equal', 2,3, 1,2),
1043
chk_ops('aBccDe', 'abccde',
1044
[('equal', 0,1, 0,1),
1045
('replace', 1,5, 1,5),
1046
('equal', 5,6, 5,6),
1048
chk_ops('aBcDec', 'abcdec',
1049
[('equal', 0,1, 0,1),
1050
('replace', 1,2, 1,2),
1051
('equal', 2,3, 2,3),
1052
('replace', 3,4, 3,4),
1053
('equal', 4,6, 4,6),
1055
chk_ops('aBcdEcdFg', 'abcdecdfg',
1056
[('equal', 0,1, 0,1),
1057
('replace', 1,8, 1,8),
1060
chk_ops('aBcdEeXcdFg', 'abcdecdfg',
1061
[('equal', 0,1, 0,1),
1062
('replace', 1,2, 1,2),
1063
('equal', 2,4, 2,4),
1064
('delete', 4,5, 4,4),
1065
('equal', 5,6, 4,5),
1066
('delete', 6,7, 5,5),
1067
('equal', 7,9, 5,7),
1068
('replace', 9,10, 7,8),
1069
('equal', 10,11, 8,9)
1072
def test_grouped_opcodes(self):
1073
def chk_ops(a, b, expected_codes, n=3):
1074
s = self._PatienceSequenceMatcher(None, a, b)
1075
self.assertEqual(expected_codes, list(s.get_grouped_opcodes(n)))
1079
chk_ops('abc', '', [[('delete', 0,3, 0,0)]])
1080
chk_ops('', 'abc', [[('insert', 0,0, 0,3)]])
1081
chk_ops('abcd', 'abcd', [])
1082
chk_ops('abcd', 'abce', [[('equal', 0,3, 0,3),
1083
('replace', 3,4, 3,4)
1085
chk_ops('eabc', 'abce', [[('delete', 0,1, 0,0),
1086
('equal', 1,4, 0,3),
1087
('insert', 4,4, 3,4)
1089
chk_ops('abcdefghijklmnop', 'abcdefxydefghijklmnop',
1090
[[('equal', 3,6, 3,6),
1091
('insert', 6,6, 6,11),
1092
('equal', 6,9, 11,14)
1094
chk_ops('abcdefghijklmnop', 'abcdefxydefghijklmnop',
1095
[[('equal', 2,6, 2,6),
1096
('insert', 6,6, 6,11),
1097
('equal', 6,10, 11,15)
1099
chk_ops('Xabcdef', 'abcdef',
1100
[[('delete', 0,1, 0,0),
1103
chk_ops('abcdef', 'abcdefX',
1104
[[('equal', 3,6, 3,6),
1105
('insert', 6,6, 6,7)
1109
def test_multiple_ranges(self):
1110
# There was an earlier bug where we used a bad set of ranges,
1111
# this triggers that specific bug, to make sure it doesn't regress
1112
self.assertDiffBlocks('abcdefghijklmnop',
1113
'abcXghiYZQRSTUVWXYZijklmnop',
1114
[(0, 0, 3), (6, 4, 3), (9, 20, 7)])
1116
self.assertDiffBlocks('ABCd efghIjk L',
1117
'AxyzBCn mo pqrstuvwI1 2 L',
1118
[(0,0,1), (1, 4, 2), (9, 19, 1), (12, 23, 3)])
1120
# These are rot13 code snippets.
1121
self.assertDiffBlocks('''\
1122
trg nqqrq jura lbh nqq n svyr va gur qverpgbel.
1124
gnxrf_netf = ['svyr*']
1125
gnxrf_bcgvbaf = ['ab-erphefr']
1127
qrs eha(frys, svyr_yvfg, ab_erphefr=Snyfr):
1128
sebz omeyvo.nqq vzcbeg fzneg_nqq, nqq_ercbegre_cevag, nqq_ercbegre_ahyy
1130
ercbegre = nqq_ercbegre_ahyy
1132
ercbegre = nqq_ercbegre_cevag
1133
fzneg_nqq(svyr_yvfg, abg ab_erphefr, ercbegre)
1136
pynff pzq_zxqve(Pbzznaq):
1137
'''.splitlines(True), '''\
1138
trg nqqrq jura lbh nqq n svyr va gur qverpgbel.
1140
--qel-eha jvyy fubj juvpu svyrf jbhyq or nqqrq, ohg abg npghnyyl
1143
gnxrf_netf = ['svyr*']
1144
gnxrf_bcgvbaf = ['ab-erphefr', 'qel-eha']
1146
qrs eha(frys, svyr_yvfg, ab_erphefr=Snyfr, qel_eha=Snyfr):
1151
# Guvf vf cbvagyrff, ohg V'q engure abg envfr na reebe
1152
npgvba = omeyvo.nqq.nqq_npgvba_ahyy
1154
npgvba = omeyvo.nqq.nqq_npgvba_cevag
1156
npgvba = omeyvo.nqq.nqq_npgvba_nqq
1158
npgvba = omeyvo.nqq.nqq_npgvba_nqq_naq_cevag
1160
omeyvo.nqq.fzneg_nqq(svyr_yvfg, abg ab_erphefr, npgvba)
1163
pynff pzq_zxqve(Pbzznaq):
1164
'''.splitlines(True)
1165
, [(0,0,1), (1, 4, 2), (9, 19, 1), (12, 23, 3)])
1167
def test_patience_unified_diff(self):
1168
txt_a = ['hello there\n',
1170
'how are you today?\n']
1171
txt_b = ['hello there\n',
1172
'how are you today?\n']
1173
unified_diff = patiencediff.unified_diff
1174
psm = self._PatienceSequenceMatcher
1175
self.assertEqual(['--- \n',
1177
'@@ -1,3 +1,2 @@\n',
1180
' how are you today?\n'
1182
, list(unified_diff(txt_a, txt_b,
1183
sequencematcher=psm)))
1184
txt_a = map(lambda x: x+'\n', 'abcdefghijklmnop')
1185
txt_b = map(lambda x: x+'\n', 'abcdefxydefghijklmnop')
1186
# This is the result with LongestCommonSubstring matching
1187
self.assertEqual(['--- \n',
1189
'@@ -1,6 +1,11 @@\n',
1201
, list(unified_diff(txt_a, txt_b)))
1202
# And the patience diff
1203
self.assertEqual(['--- \n',
1205
'@@ -4,6 +4,11 @@\n',
1218
, list(unified_diff(txt_a, txt_b,
1219
sequencematcher=psm)))
1221
def test_patience_unified_diff_with_dates(self):
1222
txt_a = ['hello there\n',
1224
'how are you today?\n']
1225
txt_b = ['hello there\n',
1226
'how are you today?\n']
1227
unified_diff = patiencediff.unified_diff
1228
psm = self._PatienceSequenceMatcher
1229
self.assertEqual(['--- a\t2008-08-08\n',
1230
'+++ b\t2008-09-09\n',
1231
'@@ -1,3 +1,2 @@\n',
1234
' how are you today?\n'
1236
, list(unified_diff(txt_a, txt_b,
1237
fromfile='a', tofile='b',
1238
fromfiledate='2008-08-08',
1239
tofiledate='2008-09-09',
1240
sequencematcher=psm)))
1243
class TestPatienceDiffLib_c(TestPatienceDiffLib):
1245
_test_needs_features = [features.compiled_patiencediff_feature]
1248
super(TestPatienceDiffLib_c, self).setUp()
1249
from bzrlib import _patiencediff_c
1250
self._unique_lcs = _patiencediff_c.unique_lcs_c
1251
self._recurse_matches = _patiencediff_c.recurse_matches_c
1252
self._PatienceSequenceMatcher = \
1253
_patiencediff_c.PatienceSequenceMatcher_c
1255
def test_unhashable(self):
1256
"""We should get a proper exception here."""
1257
# We need to be able to hash items in the sequence, lists are
1258
# unhashable, and thus cannot be diffed
1259
e = self.assertRaises(TypeError, self._PatienceSequenceMatcher,
1261
e = self.assertRaises(TypeError, self._PatienceSequenceMatcher,
1262
None, ['valid', []], [])
1263
e = self.assertRaises(TypeError, self._PatienceSequenceMatcher,
1264
None, ['valid'], [[]])
1265
e = self.assertRaises(TypeError, self._PatienceSequenceMatcher,
1266
None, ['valid'], ['valid', []])
1269
class TestPatienceDiffLibFiles(tests.TestCaseInTempDir):
1272
super(TestPatienceDiffLibFiles, self).setUp()
1273
self._PatienceSequenceMatcher = \
1274
_patiencediff_py.PatienceSequenceMatcher_py
1276
def test_patience_unified_diff_files(self):
1277
txt_a = ['hello there\n',
1279
'how are you today?\n']
1280
txt_b = ['hello there\n',
1281
'how are you today?\n']
1282
with open('a1', 'wb') as f: f.writelines(txt_a)
1283
with open('b1', 'wb') as f: f.writelines(txt_b)
1285
unified_diff_files = patiencediff.unified_diff_files
1286
psm = self._PatienceSequenceMatcher
1287
self.assertEqual(['--- a1\n',
1289
'@@ -1,3 +1,2 @@\n',
1292
' how are you today?\n',
1294
, list(unified_diff_files('a1', 'b1',
1295
sequencematcher=psm)))
1297
txt_a = map(lambda x: x+'\n', 'abcdefghijklmnop')
1298
txt_b = map(lambda x: x+'\n', 'abcdefxydefghijklmnop')
1299
with open('a2', 'wb') as f: f.writelines(txt_a)
1300
with open('b2', 'wb') as f: f.writelines(txt_b)
1302
# This is the result with LongestCommonSubstring matching
1303
self.assertEqual(['--- a2\n',
1305
'@@ -1,6 +1,11 @@\n',
1317
, list(unified_diff_files('a2', 'b2')))
1319
# And the patience diff
1320
self.assertEqual(['--- a2\n',
1322
'@@ -4,6 +4,11 @@\n',
1334
list(unified_diff_files('a2', 'b2',
1335
sequencematcher=psm)))
1338
class TestPatienceDiffLibFiles_c(TestPatienceDiffLibFiles):
1340
_test_needs_features = [features.compiled_patiencediff_feature]
1343
super(TestPatienceDiffLibFiles_c, self).setUp()
1344
from bzrlib import _patiencediff_c
1345
self._PatienceSequenceMatcher = \
1346
_patiencediff_c.PatienceSequenceMatcher_c
1349
class TestUsingCompiledIfAvailable(tests.TestCase):
1351
def test_PatienceSequenceMatcher(self):
1352
if features.compiled_patiencediff_feature.available():
1353
from bzrlib._patiencediff_c import PatienceSequenceMatcher_c
1354
self.assertIs(PatienceSequenceMatcher_c,
1355
patiencediff.PatienceSequenceMatcher)
1357
from bzrlib._patiencediff_py import PatienceSequenceMatcher_py
1358
self.assertIs(PatienceSequenceMatcher_py,
1359
patiencediff.PatienceSequenceMatcher)
1361
def test_unique_lcs(self):
1362
if features.compiled_patiencediff_feature.available():
1363
from bzrlib._patiencediff_c import unique_lcs_c
1364
self.assertIs(unique_lcs_c,
1365
patiencediff.unique_lcs)
1367
from bzrlib._patiencediff_py import unique_lcs_py
1368
self.assertIs(unique_lcs_py,
1369
patiencediff.unique_lcs)
1371
def test_recurse_matches(self):
1372
if features.compiled_patiencediff_feature.available():
1373
from bzrlib._patiencediff_c import recurse_matches_c
1374
self.assertIs(recurse_matches_c,
1375
patiencediff.recurse_matches)
1377
from bzrlib._patiencediff_py import recurse_matches_py
1378
self.assertIs(recurse_matches_py,
1379
patiencediff.recurse_matches)
1382
class TestDiffFromTool(tests.TestCaseWithTransport):
1384
def test_from_string(self):
1385
diff_obj = diff.DiffFromTool.from_string('diff', None, None, None)
1386
self.addCleanup(diff_obj.finish)
1387
self.assertEqual(['diff', '@old_path', '@new_path'],
1388
diff_obj.command_template)
1390
def test_from_string_u5(self):
1391
diff_obj = diff.DiffFromTool.from_string('diff "-u 5"',
1393
self.addCleanup(diff_obj.finish)
1394
self.assertEqual(['diff', '-u 5', '@old_path', '@new_path'],
1395
diff_obj.command_template)
1396
self.assertEqual(['diff', '-u 5', 'old-path', 'new-path'],
1397
diff_obj._get_command('old-path', 'new-path'))
1399
def test_from_string_path_with_backslashes(self):
1400
self.requireFeature(features.backslashdir_feature)
1401
tool = 'C:\\Tools\\Diff.exe'
1402
diff_obj = diff.DiffFromTool.from_string(tool, None, None, None)
1403
self.addCleanup(diff_obj.finish)
1404
self.assertEqual(['C:\\Tools\\Diff.exe', '@old_path', '@new_path'],
1405
diff_obj.command_template)
1406
self.assertEqual(['C:\\Tools\\Diff.exe', 'old-path', 'new-path'],
1407
diff_obj._get_command('old-path', 'new-path'))
1409
def test_execute(self):
1411
diff_obj = diff.DiffFromTool(['python', '-c',
1412
'print "@old_path @new_path"'],
1414
self.addCleanup(diff_obj.finish)
1415
diff_obj._execute('old', 'new')
1416
self.assertEqual(output.getvalue().rstrip(), 'old new')
1418
def test_execute_missing(self):
1419
diff_obj = diff.DiffFromTool(['a-tool-which-is-unlikely-to-exist'],
1421
self.addCleanup(diff_obj.finish)
1422
e = self.assertRaises(errors.ExecutableMissing, diff_obj._execute,
1424
self.assertEqual('a-tool-which-is-unlikely-to-exist could not be found'
1425
' on this machine', str(e))
1427
def test_prepare_files_creates_paths_readable_by_windows_tool(self):
1428
self.requireFeature(features.AttribFeature)
1430
tree = self.make_branch_and_tree('tree')
1431
self.build_tree_contents([('tree/file', 'content')])
1432
tree.add('file', 'file-id')
1433
tree.commit('old tree')
1435
self.addCleanup(tree.unlock)
1436
basis_tree = tree.basis_tree()
1437
basis_tree.lock_read()
1438
self.addCleanup(basis_tree.unlock)
1439
diff_obj = diff.DiffFromTool(['python', '-c',
1440
'print "@old_path @new_path"'],
1441
basis_tree, tree, output)
1442
diff_obj._prepare_files('file-id', 'file', 'file')
1443
# The old content should be readonly
1444
self.assertReadableByAttrib(diff_obj._root, 'old\\file',
1446
# The new content should use the tree object, not a 'new' file anymore
1447
self.assertEndsWith(tree.basedir, 'work/tree')
1448
self.assertReadableByAttrib(tree.basedir, 'file', r'work\\tree\\file$')
1450
def assertReadableByAttrib(self, cwd, relpath, regex):
1451
proc = subprocess.Popen(['attrib', relpath],
1452
stdout=subprocess.PIPE,
1454
(result, err) = proc.communicate()
1455
self.assertContainsRe(result.replace('\r\n', '\n'), regex)
1457
def test_prepare_files(self):
1459
tree = self.make_branch_and_tree('tree')
1460
self.build_tree_contents([('tree/oldname', 'oldcontent')])
1461
self.build_tree_contents([('tree/oldname2', 'oldcontent2')])
1462
tree.add('oldname', 'file-id')
1463
tree.add('oldname2', 'file2-id')
1464
# Earliest allowable date on FAT32 filesystems is 1980-01-01
1465
tree.commit('old tree', timestamp=315532800)
1466
tree.rename_one('oldname', 'newname')
1467
tree.rename_one('oldname2', 'newname2')
1468
self.build_tree_contents([('tree/newname', 'newcontent')])
1469
self.build_tree_contents([('tree/newname2', 'newcontent2')])
1470
old_tree = tree.basis_tree()
1471
old_tree.lock_read()
1472
self.addCleanup(old_tree.unlock)
1474
self.addCleanup(tree.unlock)
1475
diff_obj = diff.DiffFromTool(['python', '-c',
1476
'print "@old_path @new_path"'],
1477
old_tree, tree, output)
1478
self.addCleanup(diff_obj.finish)
1479
self.assertContainsRe(diff_obj._root, 'bzr-diff-[^/]*')
1480
old_path, new_path = diff_obj._prepare_files('file-id', 'oldname',
1482
self.assertContainsRe(old_path, 'old/oldname$')
1483
self.assertEqual(315532800, os.stat(old_path).st_mtime)
1484
self.assertContainsRe(new_path, 'tree/newname$')
1485
self.assertFileEqual('oldcontent', old_path)
1486
self.assertFileEqual('newcontent', new_path)
1487
if osutils.host_os_dereferences_symlinks():
1488
self.assertTrue(os.path.samefile('tree/newname', new_path))
1489
# make sure we can create files with the same parent directories
1490
diff_obj._prepare_files('file2-id', 'oldname2', 'newname2')
1493
class TestDiffFromToolEncodedFilename(tests.TestCaseWithTransport):
1495
def test_encodable_filename(self):
1496
# Just checks file path for external diff tool.
1497
# We cannot change CPython's internal encoding used by os.exec*.
1498
diffobj = diff.DiffFromTool(['dummy', '@old_path', '@new_path'],
1500
for _, scenario in EncodingAdapter.encoding_scenarios:
1501
encoding = scenario['encoding']
1502
dirname = scenario['info']['directory']
1503
filename = scenario['info']['filename']
1505
self.overrideAttr(diffobj, '_fenc', lambda: encoding)
1506
relpath = dirname + u'/' + filename
1507
fullpath = diffobj._safe_filename('safe', relpath)
1508
self.assertEqual(fullpath,
1509
fullpath.encode(encoding).decode(encoding))
1510
self.assertTrue(fullpath.startswith(diffobj._root + '/safe'))
1512
def test_unencodable_filename(self):
1513
diffobj = diff.DiffFromTool(['dummy', '@old_path', '@new_path'],
1515
for _, scenario in EncodingAdapter.encoding_scenarios:
1516
encoding = scenario['encoding']
1517
dirname = scenario['info']['directory']
1518
filename = scenario['info']['filename']
1520
if encoding == 'iso-8859-1':
1521
encoding = 'iso-8859-2'
1523
encoding = 'iso-8859-1'
1525
self.overrideAttr(diffobj, '_fenc', lambda: encoding)
1526
relpath = dirname + u'/' + filename
1527
fullpath = diffobj._safe_filename('safe', relpath)
1528
self.assertEqual(fullpath,
1529
fullpath.encode(encoding).decode(encoding))
1530
self.assertTrue(fullpath.startswith(diffobj._root + '/safe'))
1533
class TestGetTreesAndBranchesToDiffLocked(tests.TestCaseWithTransport):
1535
def call_gtabtd(self, path_list, revision_specs, old_url, new_url):
1536
"""Call get_trees_and_branches_to_diff_locked."""
1537
return diff.get_trees_and_branches_to_diff_locked(
1538
path_list, revision_specs, old_url, new_url, self.addCleanup)
1540
def test_basic(self):
1541
tree = self.make_branch_and_tree('tree')
1542
(old_tree, new_tree,
1543
old_branch, new_branch,
1544
specific_files, extra_trees) = self.call_gtabtd(
1545
['tree'], None, None, None)
1547
self.assertIsInstance(old_tree, revisiontree.RevisionTree)
1548
self.assertEqual(_mod_revision.NULL_REVISION,
1549
old_tree.get_revision_id())
1550
self.assertEqual(tree.basedir, new_tree.basedir)
1551
self.assertEqual(tree.branch.base, old_branch.base)
1552
self.assertEqual(tree.branch.base, new_branch.base)
1553
self.assertIs(None, specific_files)
1554
self.assertIs(None, extra_trees)
1556
def test_with_rev_specs(self):
1557
tree = self.make_branch_and_tree('tree')
1558
self.build_tree_contents([('tree/file', 'oldcontent')])
1559
tree.add('file', 'file-id')
1560
tree.commit('old tree', timestamp=0, rev_id="old-id")
1561
self.build_tree_contents([('tree/file', 'newcontent')])
1562
tree.commit('new tree', timestamp=0, rev_id="new-id")
1564
revisions = [revisionspec.RevisionSpec.from_string('1'),
1565
revisionspec.RevisionSpec.from_string('2')]
1566
(old_tree, new_tree,
1567
old_branch, new_branch,
1568
specific_files, extra_trees) = self.call_gtabtd(
1569
['tree'], revisions, None, None)
1571
self.assertIsInstance(old_tree, revisiontree.RevisionTree)
1572
self.assertEqual("old-id", old_tree.get_revision_id())
1573
self.assertIsInstance(new_tree, revisiontree.RevisionTree)
1574
self.assertEqual("new-id", new_tree.get_revision_id())
1575
self.assertEqual(tree.branch.base, old_branch.base)
1576
self.assertEqual(tree.branch.base, new_branch.base)
1577
self.assertIs(None, specific_files)
1578
self.assertEqual(tree.basedir, extra_trees[0].basedir)
47
assert lines[5] == '\\ No newline at end of file\n', \
48
"expected no-nl, got %r" % lines[5]