1
# Copyright (C) 2005-2012, 2014, 2016, 2017 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
28
revision as _mod_revision,
34
from ..sixish import (
42
from ..tests.scenarios import load_tests_apply_scenarios
45
load_tests = load_tests_apply_scenarios
48
def subst_dates(string):
49
"""Replace date strings with constant values."""
50
return re.sub(br'\d{4}-\d{2}-\d{2} \d{2}:\d{2}:\d{2} [-\+]\d{4}',
51
b'YYYY-MM-DD HH:MM:SS +ZZZZ', string)
54
def udiff_lines(old, new, allow_binary=False):
56
diff.internal_diff('old', old, 'new', new, output, allow_binary)
58
return output.readlines()
61
def external_udiff_lines(old, new, use_stringio=False):
63
# BytesIO has no fileno, so it tests a different codepath
66
output = tempfile.TemporaryFile()
68
diff.external_diff('old', old, 'new', new, output, diff_opts=['-u'])
70
raise tests.TestSkipped('external "diff" not present to test')
72
lines = output.readlines()
78
"""Simple file-like object that allows writes with any type and records."""
81
self.write_record = []
83
def write(self, data):
84
self.write_record.append(data)
86
def check_types(self, testcase, expected_type):
88
any(not isinstance(o, expected_type) for o in self.write_record),
89
"Not all writes of type %s: %r" % (
90
expected_type.__name__, self.write_record))
93
class TestDiffOptions(tests.TestCase):
95
def test_unified_added(self):
96
"""Check for default style '-u' only if no other style specified
99
# Verify that style defaults to unified, id est '-u' appended
100
# to option list, in the absence of an alternative style.
101
self.assertEqual(['-a', '-u'], diff.default_style_unified(['-a']))
104
class TestDiffOptionsScenarios(tests.TestCase):
106
scenarios = [(s, dict(style=s)) for s in diff.style_option_list]
107
style = None # Set by load_tests_apply_scenarios from scenarios
109
def test_unified_not_added(self):
110
# Verify that for all valid style options, '-u' is not
111
# appended to option list.
112
ret_opts = diff.default_style_unified(diff_opts=["%s" % (self.style,)])
113
self.assertEqual(["%s" % (self.style,)], ret_opts)
116
class TestDiff(tests.TestCase):
118
def test_add_nl(self):
119
"""diff generates a valid diff for patches that add a newline"""
120
lines = udiff_lines([b'boo'], [b'boo\n'])
121
self.check_patch(lines)
122
self.assertEqual(lines[4], b'\\ No newline at end of file\n')
123
## "expected no-nl, got %r" % lines[4]
125
def test_add_nl_2(self):
126
"""diff generates a valid diff for patches that change last line and
129
lines = udiff_lines([b'boo'], [b'goo\n'])
130
self.check_patch(lines)
131
self.assertEqual(lines[4], b'\\ No newline at end of file\n')
132
## "expected no-nl, got %r" % lines[4]
134
def test_remove_nl(self):
135
"""diff generates a valid diff for patches that change last line and
138
lines = udiff_lines([b'boo\n'], [b'boo'])
139
self.check_patch(lines)
140
self.assertEqual(lines[5], b'\\ No newline at end of file\n')
141
## "expected no-nl, got %r" % lines[5]
143
def check_patch(self, lines):
144
self.assertTrue(len(lines) > 1)
145
## "Not enough lines for a file header for patch:\n%s" % "".join(lines)
146
self.assertTrue(lines[0].startswith (b'---'))
147
## 'No orig line for patch:\n%s' % "".join(lines)
148
self.assertTrue(lines[1].startswith (b'+++'))
149
## 'No mod line for patch:\n%s' % "".join(lines)
150
self.assertTrue(len(lines) > 2)
151
## "No hunks for patch:\n%s" % "".join(lines)
152
self.assertTrue(lines[2].startswith(b'@@'))
153
## "No hunk header for patch:\n%s" % "".join(lines)
154
self.assertTrue(b'@@' in lines[2][2:])
155
## "Unterminated hunk header for patch:\n%s" % "".join(lines)
157
def test_binary_lines(self):
159
uni_lines = [1023 * b'a' + b'\x00']
160
self.assertRaises(errors.BinaryFile, udiff_lines, uni_lines, empty)
161
self.assertRaises(errors.BinaryFile, udiff_lines, empty, uni_lines)
162
udiff_lines(uni_lines, empty, allow_binary=True)
163
udiff_lines(empty, uni_lines, allow_binary=True)
165
def test_external_diff(self):
166
lines = external_udiff_lines([b'boo\n'], [b'goo\n'])
167
self.check_patch(lines)
168
self.assertEqual(b'\n', lines[-1])
170
def test_external_diff_no_fileno(self):
171
# Make sure that we can handle not having a fileno, even
172
# if the diff is large
173
lines = external_udiff_lines([b'boo\n']*10000,
176
self.check_patch(lines)
178
def test_external_diff_binary_lang_c(self):
179
for lang in ('LANG', 'LC_ALL', 'LANGUAGE'):
180
self.overrideEnv(lang, 'C')
181
lines = external_udiff_lines([b'\x00foobar\n'], [b'foo\x00bar\n'])
182
# Older versions of diffutils say "Binary files", newer
183
# versions just say "Files".
184
self.assertContainsRe(lines[0], b'(Binary f|F)iles old and new differ\n')
185
self.assertEqual(lines[1:], [b'\n'])
187
def test_no_external_diff(self):
188
"""Check that NoDiff is raised when diff is not available"""
189
# Make sure no 'diff' command is available
190
# XXX: Weird, using None instead of '' breaks the test -- vila 20101216
191
self.overrideEnv('PATH', '')
192
self.assertRaises(errors.NoDiff, diff.external_diff,
193
b'old', [b'boo\n'], b'new', [b'goo\n'],
194
BytesIO(), diff_opts=['-u'])
196
def test_internal_diff_default(self):
197
# Default internal diff encoding is utf8
199
diff.internal_diff(u'old_\xb5', [b'old_text\n'],
200
u'new_\xe5', [b'new_text\n'], output)
201
lines = output.getvalue().splitlines(True)
202
self.check_patch(lines)
203
self.assertEqual([b'--- old_\xc2\xb5\n',
204
b'+++ new_\xc3\xa5\n',
205
b'@@ -1,1 +1,1 @@\n',
212
def test_internal_diff_utf8(self):
214
diff.internal_diff(u'old_\xb5', [b'old_text\n'],
215
u'new_\xe5', [b'new_text\n'], output,
216
path_encoding='utf8')
217
lines = output.getvalue().splitlines(True)
218
self.check_patch(lines)
219
self.assertEqual([b'--- old_\xc2\xb5\n',
220
b'+++ new_\xc3\xa5\n',
221
b'@@ -1,1 +1,1 @@\n',
228
def test_internal_diff_iso_8859_1(self):
230
diff.internal_diff(u'old_\xb5', [b'old_text\n'],
231
u'new_\xe5', [b'new_text\n'], output,
232
path_encoding='iso-8859-1')
233
lines = output.getvalue().splitlines(True)
234
self.check_patch(lines)
235
self.assertEqual([b'--- old_\xb5\n',
237
b'@@ -1,1 +1,1 @@\n',
244
def test_internal_diff_no_content(self):
246
diff.internal_diff(u'old', [], u'new', [], output)
247
self.assertEqual(b'', output.getvalue())
249
def test_internal_diff_no_changes(self):
251
diff.internal_diff(u'old', [b'text\n', b'contents\n'],
252
u'new', [b'text\n', b'contents\n'],
254
self.assertEqual(b'', output.getvalue())
256
def test_internal_diff_returns_bytes(self):
258
diff.internal_diff(u'old_\xb5', [b'old_text\n'],
259
u'new_\xe5', [b'new_text\n'], output)
260
output.check_types(self, bytes)
262
def test_internal_diff_default_context(self):
264
diff.internal_diff('old', [b'same_text\n', b'same_text\n', b'same_text\n',
265
b'same_text\n', b'same_text\n', b'old_text\n'],
266
'new', [b'same_text\n', b'same_text\n', b'same_text\n',
267
b'same_text\n', b'same_text\n', b'new_text\n'], output)
268
lines = output.getvalue().splitlines(True)
269
self.check_patch(lines)
270
self.assertEqual([b'--- old\n',
272
b'@@ -3,4 +3,4 @@\n',
282
def test_internal_diff_no_context(self):
284
diff.internal_diff('old', [b'same_text\n', b'same_text\n', b'same_text\n',
285
b'same_text\n', b'same_text\n', b'old_text\n'],
286
'new', [b'same_text\n', b'same_text\n', b'same_text\n',
287
b'same_text\n', b'same_text\n', b'new_text\n'], output,
289
lines = output.getvalue().splitlines(True)
290
self.check_patch(lines)
291
self.assertEqual([b'--- old\n',
293
b'@@ -6,1 +6,1 @@\n',
300
def test_internal_diff_more_context(self):
302
diff.internal_diff('old', [b'same_text\n', b'same_text\n', b'same_text\n',
303
b'same_text\n', b'same_text\n', b'old_text\n'],
304
'new', [b'same_text\n', b'same_text\n', b'same_text\n',
305
b'same_text\n', b'same_text\n', b'new_text\n'], output,
307
lines = output.getvalue().splitlines(True)
308
self.check_patch(lines)
309
self.assertEqual([b'--- old\n',
311
b'@@ -2,5 +2,5 @@\n',
324
class TestDiffFiles(tests.TestCaseInTempDir):
326
def test_external_diff_binary(self):
327
"""The output when using external diff should use diff's i18n error"""
328
for lang in ('LANG', 'LC_ALL', 'LANGUAGE'):
329
self.overrideEnv(lang, 'C')
330
# Make sure external_diff doesn't fail in the current LANG
331
lines = external_udiff_lines([b'\x00foobar\n'], [b'foo\x00bar\n'])
333
cmd = ['diff', '-u', '--binary', 'old', 'new']
334
with open('old', 'wb') as f: f.write(b'\x00foobar\n')
335
with open('new', 'wb') as f: f.write(b'foo\x00bar\n')
336
pipe = subprocess.Popen(cmd, stdout=subprocess.PIPE,
337
stdin=subprocess.PIPE)
338
out, err = pipe.communicate()
339
# We should output whatever diff tells us, plus a trailing newline
340
self.assertEqual(out.splitlines(True) + [b'\n'], lines)
343
def get_diff_as_string(tree1, tree2, specific_files=None, working_tree=None):
345
if working_tree is not None:
346
extra_trees = (working_tree,)
349
diff.show_diff_trees(tree1, tree2, output,
350
specific_files=specific_files,
351
extra_trees=extra_trees, old_label='old/',
353
return output.getvalue()
356
class TestDiffDates(tests.TestCaseWithTransport):
359
super(TestDiffDates, self).setUp()
360
self.wt = self.make_branch_and_tree('.')
361
self.b = self.wt.branch
362
self.build_tree_contents([
363
('file1', b'file1 contents at rev 1\n'),
364
('file2', b'file2 contents at rev 1\n')
366
self.wt.add(['file1', 'file2'])
368
message='Revision 1',
369
timestamp=1143849600, # 2006-04-01 00:00:00 UTC
372
self.build_tree_contents([('file1', b'file1 contents at rev 2\n')])
374
message='Revision 2',
375
timestamp=1143936000, # 2006-04-02 00:00:00 UTC
378
self.build_tree_contents([('file2', b'file2 contents at rev 3\n')])
380
message='Revision 3',
381
timestamp=1144022400, # 2006-04-03 00:00:00 UTC
384
self.wt.remove(['file2'])
386
message='Revision 4',
387
timestamp=1144108800, # 2006-04-04 00:00:00 UTC
390
self.build_tree_contents([
391
('file1', b'file1 contents in working tree\n')
393
# set the date stamps for files in the working tree to known values
394
os.utime('file1', (1144195200, 1144195200)) # 2006-04-05 00:00:00 UTC
396
def test_diff_rev_tree_working_tree(self):
397
output = get_diff_as_string(self.wt.basis_tree(), self.wt)
398
# note that the date for old/file1 is from rev 2 rather than from
399
# the basis revision (rev 4)
400
self.assertEqualDiff(output, b'''\
401
=== modified file 'file1'
402
--- old/file1\t2006-04-02 00:00:00 +0000
403
+++ new/file1\t2006-04-05 00:00:00 +0000
405
-file1 contents at rev 2
406
+file1 contents in working tree
410
def test_diff_rev_tree_rev_tree(self):
411
tree1 = self.b.repository.revision_tree(b'rev-2')
412
tree2 = self.b.repository.revision_tree(b'rev-3')
413
output = get_diff_as_string(tree1, tree2)
414
self.assertEqualDiff(output, b'''\
415
=== modified file 'file2'
416
--- old/file2\t2006-04-01 00:00:00 +0000
417
+++ new/file2\t2006-04-03 00:00:00 +0000
419
-file2 contents at rev 1
420
+file2 contents at rev 3
424
def test_diff_add_files(self):
425
tree1 = self.b.repository.revision_tree(_mod_revision.NULL_REVISION)
426
tree2 = self.b.repository.revision_tree(b'rev-1')
427
output = get_diff_as_string(tree1, tree2)
428
# the files have the epoch time stamp for the tree in which
430
self.assertEqualDiff(output, b'''\
431
=== added file 'file1'
432
--- old/file1\t1970-01-01 00:00:00 +0000
433
+++ new/file1\t2006-04-01 00:00:00 +0000
435
+file1 contents at rev 1
437
=== added file 'file2'
438
--- old/file2\t1970-01-01 00:00:00 +0000
439
+++ new/file2\t2006-04-01 00:00:00 +0000
441
+file2 contents at rev 1
445
def test_diff_remove_files(self):
446
tree1 = self.b.repository.revision_tree(b'rev-3')
447
tree2 = self.b.repository.revision_tree(b'rev-4')
448
output = get_diff_as_string(tree1, tree2)
449
# the file has the epoch time stamp for the tree in which
451
self.assertEqualDiff(output, b'''\
452
=== removed file 'file2'
453
--- old/file2\t2006-04-03 00:00:00 +0000
454
+++ new/file2\t1970-01-01 00:00:00 +0000
456
-file2 contents at rev 3
460
def test_show_diff_specified(self):
461
"""A working tree filename can be used to identify a file"""
462
self.wt.rename_one('file1', 'file1b')
463
old_tree = self.b.repository.revision_tree(b'rev-1')
464
new_tree = self.b.repository.revision_tree(b'rev-4')
465
out = get_diff_as_string(old_tree, new_tree, specific_files=['file1b'],
466
working_tree=self.wt)
467
self.assertContainsRe(out, b'file1\t')
469
def test_recursive_diff(self):
470
"""Children of directories are matched"""
473
self.wt.add(['dir1', 'dir2'])
474
self.wt.rename_one('file1', 'dir1/file1')
475
old_tree = self.b.repository.revision_tree(b'rev-1')
476
new_tree = self.b.repository.revision_tree(b'rev-4')
477
out = get_diff_as_string(old_tree, new_tree, specific_files=['dir1'],
478
working_tree=self.wt)
479
self.assertContainsRe(out, b'file1\t')
480
out = get_diff_as_string(old_tree, new_tree, specific_files=['dir2'],
481
working_tree=self.wt)
482
self.assertNotContainsRe(out, b'file1\t')
485
class TestShowDiffTrees(tests.TestCaseWithTransport):
486
"""Direct tests for show_diff_trees"""
488
def test_modified_file(self):
489
"""Test when a file is modified."""
490
tree = self.make_branch_and_tree('tree')
491
self.build_tree_contents([('tree/file', b'contents\n')])
492
tree.add(['file'], [b'file-id'])
493
tree.commit('one', rev_id=b'rev-1')
495
self.build_tree_contents([('tree/file', b'new contents\n')])
496
d = get_diff_as_string(tree.basis_tree(), tree)
497
self.assertContainsRe(d, b"=== modified file 'file'\n")
498
self.assertContainsRe(d, b'--- old/file\t')
499
self.assertContainsRe(d, b'\\+\\+\\+ new/file\t')
500
self.assertContainsRe(d, b'-contents\n'
501
b'\\+new contents\n')
503
def test_modified_file_in_renamed_dir(self):
504
"""Test when a file is modified in a renamed directory."""
505
tree = self.make_branch_and_tree('tree')
506
self.build_tree(['tree/dir/'])
507
self.build_tree_contents([('tree/dir/file', b'contents\n')])
508
tree.add(['dir', 'dir/file'], [b'dir-id', b'file-id'])
509
tree.commit('one', rev_id=b'rev-1')
511
tree.rename_one('dir', 'other')
512
self.build_tree_contents([('tree/other/file', b'new contents\n')])
513
d = get_diff_as_string(tree.basis_tree(), tree)
514
self.assertContainsRe(d, b"=== renamed directory 'dir' => 'other'\n")
515
self.assertContainsRe(d, b"=== modified file 'other/file'\n")
516
# XXX: This is technically incorrect, because it used to be at another
517
# location. What to do?
518
self.assertContainsRe(d, b'--- old/dir/file\t')
519
self.assertContainsRe(d, b'\\+\\+\\+ new/other/file\t')
520
self.assertContainsRe(d, b'-contents\n'
521
b'\\+new contents\n')
523
def test_renamed_directory(self):
524
"""Test when only a directory is only renamed."""
525
tree = self.make_branch_and_tree('tree')
526
self.build_tree(['tree/dir/'])
527
self.build_tree_contents([('tree/dir/file', b'contents\n')])
528
tree.add(['dir', 'dir/file'], [b'dir-id', b'file-id'])
529
tree.commit('one', rev_id=b'rev-1')
531
tree.rename_one('dir', 'newdir')
532
d = get_diff_as_string(tree.basis_tree(), tree)
533
# Renaming a directory should be a single "you renamed this dir" even
534
# when there are files inside.
535
self.assertEqual(d, b"=== renamed directory 'dir' => 'newdir'\n")
537
def test_renamed_file(self):
538
"""Test when a file is only renamed."""
539
tree = self.make_branch_and_tree('tree')
540
self.build_tree_contents([('tree/file', b'contents\n')])
541
tree.add(['file'], [b'file-id'])
542
tree.commit('one', rev_id=b'rev-1')
544
tree.rename_one('file', 'newname')
545
d = get_diff_as_string(tree.basis_tree(), tree)
546
self.assertContainsRe(d, b"=== renamed file 'file' => 'newname'\n")
547
# We shouldn't have a --- or +++ line, because there is no content
549
self.assertNotContainsRe(d, b'---')
551
def test_renamed_and_modified_file(self):
552
"""Test when a file is only renamed."""
553
tree = self.make_branch_and_tree('tree')
554
self.build_tree_contents([('tree/file', b'contents\n')])
555
tree.add(['file'], [b'file-id'])
556
tree.commit('one', rev_id=b'rev-1')
558
tree.rename_one('file', 'newname')
559
self.build_tree_contents([('tree/newname', b'new contents\n')])
560
d = get_diff_as_string(tree.basis_tree(), tree)
561
self.assertContainsRe(d, b"=== renamed file 'file' => 'newname'\n")
562
self.assertContainsRe(d, b'--- old/file\t')
563
self.assertContainsRe(d, b'\\+\\+\\+ new/newname\t')
564
self.assertContainsRe(d, b'-contents\n'
565
b'\\+new contents\n')
568
def test_internal_diff_exec_property(self):
569
tree = self.make_branch_and_tree('tree')
571
tt = transform.TreeTransform(tree)
572
tt.new_file('a', tt.root, [b'contents\n'], b'a-id', True)
573
tt.new_file('b', tt.root, [b'contents\n'], b'b-id', False)
574
tt.new_file('c', tt.root, [b'contents\n'], b'c-id', True)
575
tt.new_file('d', tt.root, [b'contents\n'], b'd-id', False)
576
tt.new_file('e', tt.root, [b'contents\n'], b'control-e-id', True)
577
tt.new_file('f', tt.root, [b'contents\n'], b'control-f-id', False)
579
tree.commit('one', rev_id=b'rev-1')
581
tt = transform.TreeTransform(tree)
582
tt.set_executability(False, tt.trans_id_file_id(b'a-id'))
583
tt.set_executability(True, tt.trans_id_file_id(b'b-id'))
584
tt.set_executability(False, tt.trans_id_file_id(b'c-id'))
585
tt.set_executability(True, tt.trans_id_file_id(b'd-id'))
587
tree.rename_one('c', 'new-c')
588
tree.rename_one('d', 'new-d')
590
d = get_diff_as_string(tree.basis_tree(), tree)
592
self.assertContainsRe(d, br"file 'a'.*\(properties changed:"
594
self.assertContainsRe(d, br"file 'b'.*\(properties changed:"
596
self.assertContainsRe(d, br"file 'c'.*\(properties changed:"
598
self.assertContainsRe(d, br"file 'd'.*\(properties changed:"
600
self.assertNotContainsRe(d, br"file 'e'")
601
self.assertNotContainsRe(d, br"file 'f'")
603
def test_binary_unicode_filenames(self):
604
"""Test that contents of files are *not* encoded in UTF-8 when there
605
is a binary file in the diff.
607
# See https://bugs.launchpad.net/bugs/110092.
608
self.requireFeature(features.UnicodeFilenameFeature)
610
tree = self.make_branch_and_tree('tree')
611
alpha, omega = u'\u03b1', u'\u03c9'
612
alpha_utf8, omega_utf8 = alpha.encode('utf8'), omega.encode('utf8')
613
self.build_tree_contents(
614
[('tree/' + alpha, b'\0'),
616
(b'The %s and the %s\n' % (alpha_utf8, omega_utf8)))])
617
tree.add([alpha], [b'file-id'])
618
tree.add([omega], [b'file-id-2'])
619
diff_content = StubO()
620
diff.show_diff_trees(tree.basis_tree(), tree, diff_content)
621
diff_content.check_types(self, bytes)
622
d = b''.join(diff_content.write_record)
623
self.assertContainsRe(d, br"=== added file '%s'" % alpha_utf8)
624
self.assertContainsRe(d, b"Binary files a/%s.*and b/%s.* differ\n"
625
% (alpha_utf8, alpha_utf8))
626
self.assertContainsRe(d, br"=== added file '%s'" % omega_utf8)
627
self.assertContainsRe(d, br"--- a/%s" % (omega_utf8,))
628
self.assertContainsRe(d, br"\+\+\+ b/%s" % (omega_utf8,))
630
def test_unicode_filename(self):
631
"""Test when the filename are unicode."""
632
self.requireFeature(features.UnicodeFilenameFeature)
634
alpha, omega = u'\u03b1', u'\u03c9'
635
autf8, outf8 = alpha.encode('utf8'), omega.encode('utf8')
637
tree = self.make_branch_and_tree('tree')
638
self.build_tree_contents([('tree/ren_'+alpha, b'contents\n')])
639
tree.add(['ren_'+alpha], [b'file-id-2'])
640
self.build_tree_contents([('tree/del_'+alpha, b'contents\n')])
641
tree.add(['del_'+alpha], [b'file-id-3'])
642
self.build_tree_contents([('tree/mod_'+alpha, b'contents\n')])
643
tree.add(['mod_'+alpha], [b'file-id-4'])
645
tree.commit('one', rev_id=b'rev-1')
647
tree.rename_one('ren_'+alpha, 'ren_'+omega)
648
tree.remove('del_'+alpha)
649
self.build_tree_contents([('tree/add_'+alpha, b'contents\n')])
650
tree.add(['add_'+alpha], [b'file-id'])
651
self.build_tree_contents([('tree/mod_'+alpha, b'contents_mod\n')])
653
d = get_diff_as_string(tree.basis_tree(), tree)
654
self.assertContainsRe(d,
655
b"=== renamed file 'ren_%s' => 'ren_%s'\n"%(autf8, outf8))
656
self.assertContainsRe(d, b"=== added file 'add_%s'"%autf8)
657
self.assertContainsRe(d, b"=== modified file 'mod_%s'"%autf8)
658
self.assertContainsRe(d, b"=== removed file 'del_%s'"%autf8)
660
def test_unicode_filename_path_encoding(self):
661
"""Test for bug #382699: unicode filenames on Windows should be shown
664
self.requireFeature(features.UnicodeFilenameFeature)
665
# The word 'test' in Russian
666
_russian_test = u'\u0422\u0435\u0441\u0442'
667
directory = _russian_test + u'/'
668
test_txt = _russian_test + u'.txt'
669
u1234 = u'\u1234.txt'
671
tree = self.make_branch_and_tree('.')
672
self.build_tree_contents([
673
(test_txt, b'foo\n'),
677
tree.add([test_txt, u1234, directory])
680
diff.show_diff_trees(tree.basis_tree(), tree, sio,
681
path_encoding='cp1251')
683
output = subst_dates(sio.getvalue())
685
=== added directory '%(directory)s'
686
=== added file '%(test_txt)s'
687
--- a/%(test_txt)s\tYYYY-MM-DD HH:MM:SS +ZZZZ
688
+++ b/%(test_txt)s\tYYYY-MM-DD HH:MM:SS +ZZZZ
692
=== added file '?.txt'
693
--- a/?.txt\tYYYY-MM-DD HH:MM:SS +ZZZZ
694
+++ b/?.txt\tYYYY-MM-DD HH:MM:SS +ZZZZ
698
''' % {b'directory': _russian_test.encode('cp1251'),
699
b'test_txt': test_txt.encode('cp1251'),
701
self.assertEqualDiff(output, shouldbe)
704
class DiffWasIs(diff.DiffPath):
706
def diff(self, file_id, old_path, new_path, old_kind, new_kind):
707
self.to_file.write(b'was: ')
708
self.to_file.write(self.old_tree.get_file(old_path).read())
709
self.to_file.write(b'is: ')
710
self.to_file.write(self.new_tree.get_file(new_path).read())
713
class TestDiffTree(tests.TestCaseWithTransport):
716
super(TestDiffTree, self).setUp()
717
self.old_tree = self.make_branch_and_tree('old-tree')
718
self.old_tree.lock_write()
719
self.addCleanup(self.old_tree.unlock)
720
self.new_tree = self.make_branch_and_tree('new-tree')
721
self.new_tree.lock_write()
722
self.addCleanup(self.new_tree.unlock)
723
self.differ = diff.DiffTree(self.old_tree, self.new_tree, BytesIO())
725
def test_diff_text(self):
726
self.build_tree_contents([('old-tree/olddir/',),
727
('old-tree/olddir/oldfile', b'old\n')])
728
self.old_tree.add('olddir')
729
self.old_tree.add('olddir/oldfile', b'file-id')
730
self.build_tree_contents([('new-tree/newdir/',),
731
('new-tree/newdir/newfile', b'new\n')])
732
self.new_tree.add('newdir')
733
self.new_tree.add('newdir/newfile', b'file-id')
734
differ = diff.DiffText(self.old_tree, self.new_tree, BytesIO())
735
differ.diff_text('olddir/oldfile', None, 'old label',
736
'new label', b'file-id', None)
738
b'--- old label\n+++ new label\n@@ -1,1 +0,0 @@\n-old\n\n',
739
differ.to_file.getvalue())
740
differ.to_file.seek(0)
741
differ.diff_text(None, 'newdir/newfile',
742
'old label', 'new label', None, b'file-id')
744
b'--- old label\n+++ new label\n@@ -0,0 +1,1 @@\n+new\n\n',
745
differ.to_file.getvalue())
746
differ.to_file.seek(0)
747
differ.diff_text('olddir/oldfile', 'newdir/newfile',
748
'old label', 'new label', b'file-id', b'file-id')
750
b'--- old label\n+++ new label\n@@ -1,1 +1,1 @@\n-old\n+new\n\n',
751
differ.to_file.getvalue())
753
def test_diff_deletion(self):
754
self.build_tree_contents([('old-tree/file', b'contents'),
755
('new-tree/file', b'contents')])
756
self.old_tree.add('file', b'file-id')
757
self.new_tree.add('file', b'file-id')
758
os.unlink('new-tree/file')
759
self.differ.show_diff(None)
760
self.assertContainsRe(self.differ.to_file.getvalue(), b'-contents')
762
def test_diff_creation(self):
763
self.build_tree_contents([('old-tree/file', b'contents'),
764
('new-tree/file', b'contents')])
765
self.old_tree.add('file', b'file-id')
766
self.new_tree.add('file', b'file-id')
767
os.unlink('old-tree/file')
768
self.differ.show_diff(None)
769
self.assertContainsRe(self.differ.to_file.getvalue(), br'\+contents')
771
def test_diff_symlink(self):
772
differ = diff.DiffSymlink(self.old_tree, self.new_tree, BytesIO())
773
differ.diff_symlink('old target', None)
774
self.assertEqual(b"=== target was 'old target'\n",
775
differ.to_file.getvalue())
777
differ = diff.DiffSymlink(self.old_tree, self.new_tree, BytesIO())
778
differ.diff_symlink(None, 'new target')
779
self.assertEqual(b"=== target is 'new target'\n",
780
differ.to_file.getvalue())
782
differ = diff.DiffSymlink(self.old_tree, self.new_tree, BytesIO())
783
differ.diff_symlink('old target', 'new target')
784
self.assertEqual(b"=== target changed 'old target' => 'new target'\n",
785
differ.to_file.getvalue())
788
self.build_tree_contents([('old-tree/olddir/',),
789
('old-tree/olddir/oldfile', b'old\n')])
790
self.old_tree.add('olddir')
791
self.old_tree.add('olddir/oldfile', b'file-id')
792
self.build_tree_contents([('new-tree/newdir/',),
793
('new-tree/newdir/newfile', b'new\n')])
794
self.new_tree.add('newdir')
795
self.new_tree.add('newdir/newfile', b'file-id')
796
self.differ.diff(b'file-id', 'olddir/oldfile', 'newdir/newfile')
797
self.assertContainsRe(
798
self.differ.to_file.getvalue(),
799
br'--- olddir/oldfile.*\n\+\+\+ newdir/newfile.*\n\@\@ -1,1 \+1,1'
800
br' \@\@\n-old\n\+new\n\n')
802
def test_diff_kind_change(self):
803
self.requireFeature(features.SymlinkFeature)
804
self.build_tree_contents([('old-tree/olddir/',),
805
('old-tree/olddir/oldfile', b'old\n')])
806
self.old_tree.add('olddir')
807
self.old_tree.add('olddir/oldfile', b'file-id')
808
self.build_tree(['new-tree/newdir/'])
809
os.symlink('new', 'new-tree/newdir/newfile')
810
self.new_tree.add('newdir')
811
self.new_tree.add('newdir/newfile', b'file-id')
812
self.differ.diff(b'file-id', 'olddir/oldfile', 'newdir/newfile')
813
self.assertContainsRe(
814
self.differ.to_file.getvalue(),
815
br'--- olddir/oldfile.*\n\+\+\+ newdir/newfile.*\n\@\@ -1,1 \+0,0'
817
self.assertContainsRe(self.differ.to_file.getvalue(),
818
b"=== target is 'new'\n")
820
def test_diff_directory(self):
821
self.build_tree(['new-tree/new-dir/'])
822
self.new_tree.add('new-dir', b'new-dir-id')
823
self.differ.diff(b'new-dir-id', None, 'new-dir')
824
self.assertEqual(self.differ.to_file.getvalue(), b'')
826
def create_old_new(self):
827
self.build_tree_contents([('old-tree/olddir/',),
828
('old-tree/olddir/oldfile', b'old\n')])
829
self.old_tree.add('olddir')
830
self.old_tree.add('olddir/oldfile', b'file-id')
831
self.build_tree_contents([('new-tree/newdir/',),
832
('new-tree/newdir/newfile', b'new\n')])
833
self.new_tree.add('newdir')
834
self.new_tree.add('newdir/newfile', b'file-id')
836
def test_register_diff(self):
837
self.create_old_new()
838
old_diff_factories = diff.DiffTree.diff_factories
839
diff.DiffTree.diff_factories=old_diff_factories[:]
840
diff.DiffTree.diff_factories.insert(0, DiffWasIs.from_diff_tree)
842
differ = diff.DiffTree(self.old_tree, self.new_tree, BytesIO())
844
diff.DiffTree.diff_factories = old_diff_factories
845
differ.diff(b'file-id', 'olddir/oldfile', 'newdir/newfile')
846
self.assertNotContainsRe(
847
differ.to_file.getvalue(),
848
br'--- olddir/oldfile.*\n\+\+\+ newdir/newfile.*\n\@\@ -1,1 \+1,1'
849
br' \@\@\n-old\n\+new\n\n')
850
self.assertContainsRe(differ.to_file.getvalue(),
851
b'was: old\nis: new\n')
853
def test_extra_factories(self):
854
self.create_old_new()
855
differ = diff.DiffTree(self.old_tree, self.new_tree, BytesIO(),
856
extra_factories=[DiffWasIs.from_diff_tree])
857
differ.diff(b'file-id', 'olddir/oldfile', 'newdir/newfile')
858
self.assertNotContainsRe(
859
differ.to_file.getvalue(),
860
br'--- olddir/oldfile.*\n\+\+\+ newdir/newfile.*\n\@\@ -1,1 \+1,1'
861
br' \@\@\n-old\n\+new\n\n')
862
self.assertContainsRe(differ.to_file.getvalue(),
863
b'was: old\nis: new\n')
865
def test_alphabetical_order(self):
866
self.build_tree(['new-tree/a-file'])
867
self.new_tree.add('a-file')
868
self.build_tree(['old-tree/b-file'])
869
self.old_tree.add('b-file')
870
self.differ.show_diff(None)
871
self.assertContainsRe(self.differ.to_file.getvalue(),
872
b'.*a-file(.|\n)*b-file')
875
class TestPatienceDiffLib(tests.TestCase):
878
super(TestPatienceDiffLib, self).setUp()
879
self._unique_lcs = _patiencediff_py.unique_lcs_py
880
self._recurse_matches = _patiencediff_py.recurse_matches_py
881
self._PatienceSequenceMatcher = \
882
_patiencediff_py.PatienceSequenceMatcher_py
884
def test_diff_unicode_string(self):
885
a = ''.join([unichr(i) for i in range(4000, 4500, 3)])
886
b = ''.join([unichr(i) for i in range(4300, 4800, 2)])
887
sm = self._PatienceSequenceMatcher(None, a, b)
888
mb = sm.get_matching_blocks()
889
self.assertEqual(35, len(mb))
891
def test_unique_lcs(self):
892
unique_lcs = self._unique_lcs
893
self.assertEqual(unique_lcs('', ''), [])
894
self.assertEqual(unique_lcs('', 'a'), [])
895
self.assertEqual(unique_lcs('a', ''), [])
896
self.assertEqual(unique_lcs('a', 'a'), [(0, 0)])
897
self.assertEqual(unique_lcs('a', 'b'), [])
898
self.assertEqual(unique_lcs('ab', 'ab'), [(0, 0), (1, 1)])
899
self.assertEqual(unique_lcs('abcde', 'cdeab'), [(2, 0), (3, 1), (4, 2)])
900
self.assertEqual(unique_lcs('cdeab', 'abcde'), [(0, 2), (1, 3), (2, 4)])
901
self.assertEqual(unique_lcs('abXde', 'abYde'), [(0, 0), (1, 1),
903
self.assertEqual(unique_lcs('acbac', 'abc'), [(2, 1)])
905
def test_recurse_matches(self):
906
def test_one(a, b, matches):
908
self._recurse_matches(
909
a, b, 0, 0, len(a), len(b), test_matches, 10)
910
self.assertEqual(test_matches, matches)
912
test_one(['a', '', 'b', '', 'c'], ['a', 'a', 'b', 'c', 'c'],
913
[(0, 0), (2, 2), (4, 4)])
914
test_one(['a', 'c', 'b', 'a', 'c'], ['a', 'b', 'c'],
915
[(0, 0), (2, 1), (4, 2)])
916
# Even though 'bc' is not unique globally, and is surrounded by
917
# non-matching lines, we should still match, because they are locally
919
test_one('abcdbce', 'afbcgdbce', [(0, 0), (1, 2), (2, 3), (3, 5),
920
(4, 6), (5, 7), (6, 8)])
922
# recurse_matches doesn't match non-unique
923
# lines surrounded by bogus text.
924
# The update has been done in patiencediff.SequenceMatcher instead
926
# This is what it could be
927
#test_one('aBccDe', 'abccde', [(0,0), (2,2), (3,3), (5,5)])
929
# This is what it currently gives:
930
test_one('aBccDe', 'abccde', [(0, 0), (5, 5)])
932
def assertDiffBlocks(self, a, b, expected_blocks):
933
"""Check that the sequence matcher returns the correct blocks.
935
:param a: A sequence to match
936
:param b: Another sequence to match
937
:param expected_blocks: The expected output, not including the final
938
matching block (len(a), len(b), 0)
940
matcher = self._PatienceSequenceMatcher(None, a, b)
941
blocks = matcher.get_matching_blocks()
943
self.assertEqual((len(a), len(b), 0), last)
944
self.assertEqual(expected_blocks, blocks)
946
def test_matching_blocks(self):
947
# Some basic matching tests
948
self.assertDiffBlocks('', '', [])
949
self.assertDiffBlocks([], [], [])
950
self.assertDiffBlocks('abc', '', [])
951
self.assertDiffBlocks('', 'abc', [])
952
self.assertDiffBlocks('abcd', 'abcd', [(0, 0, 4)])
953
self.assertDiffBlocks('abcd', 'abce', [(0, 0, 3)])
954
self.assertDiffBlocks('eabc', 'abce', [(1, 0, 3)])
955
self.assertDiffBlocks('eabce', 'abce', [(1, 0, 4)])
956
self.assertDiffBlocks('abcde', 'abXde', [(0, 0, 2), (3, 3, 2)])
957
self.assertDiffBlocks('abcde', 'abXYZde', [(0, 0, 2), (3, 5, 2)])
958
self.assertDiffBlocks('abde', 'abXYZde', [(0, 0, 2), (2, 5, 2)])
959
# This may check too much, but it checks to see that
960
# a copied block stays attached to the previous section,
962
# difflib would tend to grab the trailing longest match
963
# which would make the diff not look right
964
self.assertDiffBlocks('abcdefghijklmnop', 'abcdefxydefghijklmnop',
965
[(0, 0, 6), (6, 11, 10)])
967
# make sure it supports passing in lists
968
self.assertDiffBlocks(
971
'how are you today?\n'],
973
'how are you today?\n'],
974
[(0, 0, 1), (2, 1, 1)])
976
# non unique lines surrounded by non-matching lines
978
self.assertDiffBlocks('aBccDe', 'abccde', [(0, 0, 1), (5, 5, 1)])
980
# But they only need to be locally unique
981
self.assertDiffBlocks('aBcDec', 'abcdec', [(0, 0, 1), (2, 2, 1), (4, 4, 2)])
983
# non unique blocks won't be matched
984
self.assertDiffBlocks('aBcdEcdFg', 'abcdecdfg', [(0, 0, 1), (8, 8, 1)])
986
# but locally unique ones will
987
self.assertDiffBlocks('aBcdEeXcdFg', 'abcdecdfg', [(0, 0, 1), (2, 2, 2),
988
(5, 4, 1), (7, 5, 2), (10, 8, 1)])
990
self.assertDiffBlocks('abbabbXd', 'cabbabxd', [(7, 7, 1)])
991
self.assertDiffBlocks('abbabbbb', 'cabbabbc', [])
992
self.assertDiffBlocks('bbbbbbbb', 'cbbbbbbc', [])
994
def test_matching_blocks_tuples(self):
995
# Some basic matching tests
996
self.assertDiffBlocks([], [], [])
997
self.assertDiffBlocks([('a',), ('b',), ('c,')], [], [])
998
self.assertDiffBlocks([], [('a',), ('b',), ('c,')], [])
999
self.assertDiffBlocks([('a',), ('b',), ('c,')],
1000
[('a',), ('b',), ('c,')],
1002
self.assertDiffBlocks([('a',), ('b',), ('c,')],
1003
[('a',), ('b',), ('d,')],
1005
self.assertDiffBlocks([('d',), ('b',), ('c,')],
1006
[('a',), ('b',), ('c,')],
1008
self.assertDiffBlocks([('d',), ('a',), ('b',), ('c,')],
1009
[('a',), ('b',), ('c,')],
1011
self.assertDiffBlocks([('a', 'b'), ('c', 'd'), ('e', 'f')],
1012
[('a', 'b'), ('c', 'X'), ('e', 'f')],
1013
[(0, 0, 1), (2, 2, 1)])
1014
self.assertDiffBlocks([('a', 'b'), ('c', 'd'), ('e', 'f')],
1015
[('a', 'b'), ('c', 'dX'), ('e', 'f')],
1016
[(0, 0, 1), (2, 2, 1)])
1018
def test_opcodes(self):
1019
def chk_ops(a, b, expected_codes):
1020
s = self._PatienceSequenceMatcher(None, a, b)
1021
self.assertEqual(expected_codes, s.get_opcodes())
1025
chk_ops('abc', '', [('delete', 0, 3, 0, 0)])
1026
chk_ops('', 'abc', [('insert', 0, 0, 0, 3)])
1027
chk_ops('abcd', 'abcd', [('equal', 0, 4, 0, 4)])
1028
chk_ops('abcd', 'abce', [('equal', 0, 3, 0, 3),
1029
('replace', 3, 4, 3, 4)
1031
chk_ops('eabc', 'abce', [('delete', 0, 1, 0, 0),
1032
('equal', 1, 4, 0, 3),
1033
('insert', 4, 4, 3, 4)
1035
chk_ops('eabce', 'abce', [('delete', 0, 1, 0, 0),
1036
('equal', 1, 5, 0, 4)
1038
chk_ops('abcde', 'abXde', [('equal', 0, 2, 0, 2),
1039
('replace', 2, 3, 2, 3),
1040
('equal', 3, 5, 3, 5)
1042
chk_ops('abcde', 'abXYZde', [('equal', 0, 2, 0, 2),
1043
('replace', 2, 3, 2, 5),
1044
('equal', 3, 5, 5, 7)
1046
chk_ops('abde', 'abXYZde', [('equal', 0, 2, 0, 2),
1047
('insert', 2, 2, 2, 5),
1048
('equal', 2, 4, 5, 7)
1050
chk_ops('abcdefghijklmnop', 'abcdefxydefghijklmnop',
1051
[('equal', 0, 6, 0, 6),
1052
('insert', 6, 6, 6, 11),
1053
('equal', 6, 16, 11, 21)
1058
, 'how are you today?\n'],
1060
, 'how are you today?\n'],
1061
[('equal', 0, 1, 0, 1),
1062
('delete', 1, 2, 1, 1),
1063
('equal', 2, 3, 1, 2),
1065
chk_ops('aBccDe', 'abccde',
1066
[('equal', 0, 1, 0, 1),
1067
('replace', 1, 5, 1, 5),
1068
('equal', 5, 6, 5, 6),
1070
chk_ops('aBcDec', 'abcdec',
1071
[('equal', 0, 1, 0, 1),
1072
('replace', 1, 2, 1, 2),
1073
('equal', 2, 3, 2, 3),
1074
('replace', 3, 4, 3, 4),
1075
('equal', 4, 6, 4, 6),
1077
chk_ops('aBcdEcdFg', 'abcdecdfg',
1078
[('equal', 0, 1, 0, 1),
1079
('replace', 1, 8, 1, 8),
1080
('equal', 8, 9, 8, 9)
1082
chk_ops('aBcdEeXcdFg', 'abcdecdfg',
1083
[('equal', 0, 1, 0, 1),
1084
('replace', 1, 2, 1, 2),
1085
('equal', 2, 4, 2, 4),
1086
('delete', 4, 5, 4, 4),
1087
('equal', 5, 6, 4, 5),
1088
('delete', 6, 7, 5, 5),
1089
('equal', 7, 9, 5, 7),
1090
('replace', 9, 10, 7, 8),
1091
('equal', 10, 11, 8, 9)
1094
def test_grouped_opcodes(self):
1095
def chk_ops(a, b, expected_codes, n=3):
1096
s = self._PatienceSequenceMatcher(None, a, b)
1097
self.assertEqual(expected_codes, list(s.get_grouped_opcodes(n)))
1101
chk_ops('abc', '', [[('delete', 0, 3, 0, 0)]])
1102
chk_ops('', 'abc', [[('insert', 0, 0, 0, 3)]])
1103
chk_ops('abcd', 'abcd', [])
1104
chk_ops('abcd', 'abce', [[('equal', 0, 3, 0, 3),
1105
('replace', 3, 4, 3, 4)
1107
chk_ops('eabc', 'abce', [[('delete', 0, 1, 0, 0),
1108
('equal', 1, 4, 0, 3),
1109
('insert', 4, 4, 3, 4)
1111
chk_ops('abcdefghijklmnop', 'abcdefxydefghijklmnop',
1112
[[('equal', 3, 6, 3, 6),
1113
('insert', 6, 6, 6, 11),
1114
('equal', 6, 9, 11, 14)
1116
chk_ops('abcdefghijklmnop', 'abcdefxydefghijklmnop',
1117
[[('equal', 2, 6, 2, 6),
1118
('insert', 6, 6, 6, 11),
1119
('equal', 6, 10, 11, 15)
1121
chk_ops('Xabcdef', 'abcdef',
1122
[[('delete', 0, 1, 0, 0),
1123
('equal', 1, 4, 0, 3)
1125
chk_ops('abcdef', 'abcdefX',
1126
[[('equal', 3, 6, 3, 6),
1127
('insert', 6, 6, 6, 7)
1131
def test_multiple_ranges(self):
1132
# There was an earlier bug where we used a bad set of ranges,
1133
# this triggers that specific bug, to make sure it doesn't regress
1134
self.assertDiffBlocks('abcdefghijklmnop',
1135
'abcXghiYZQRSTUVWXYZijklmnop',
1136
[(0, 0, 3), (6, 4, 3), (9, 20, 7)])
1138
self.assertDiffBlocks('ABCd efghIjk L',
1139
'AxyzBCn mo pqrstuvwI1 2 L',
1140
[(0, 0, 1), (1, 4, 2), (9, 19, 1), (12, 23, 3)])
1142
# These are rot13 code snippets.
1143
self.assertDiffBlocks('''\
1144
trg nqqrq jura lbh nqq n svyr va gur qverpgbel.
1146
gnxrf_netf = ['svyr*']
1147
gnxrf_bcgvbaf = ['ab-erphefr']
1149
qrs eha(frys, svyr_yvfg, ab_erphefr=Snyfr):
1150
sebz omeyvo.nqq vzcbeg fzneg_nqq, nqq_ercbegre_cevag, nqq_ercbegre_ahyy
1152
ercbegre = nqq_ercbegre_ahyy
1154
ercbegre = nqq_ercbegre_cevag
1155
fzneg_nqq(svyr_yvfg, abg ab_erphefr, ercbegre)
1158
pynff pzq_zxqve(Pbzznaq):
1159
'''.splitlines(True), '''\
1160
trg nqqrq jura lbh nqq n svyr va gur qverpgbel.
1162
--qel-eha jvyy fubj juvpu svyrf jbhyq or nqqrq, ohg abg npghnyyl
1165
gnxrf_netf = ['svyr*']
1166
gnxrf_bcgvbaf = ['ab-erphefr', 'qel-eha']
1168
qrs eha(frys, svyr_yvfg, ab_erphefr=Snyfr, qel_eha=Snyfr):
1173
# Guvf vf cbvagyrff, ohg V'q engure abg envfr na reebe
1174
npgvba = omeyvo.nqq.nqq_npgvba_ahyy
1176
npgvba = omeyvo.nqq.nqq_npgvba_cevag
1178
npgvba = omeyvo.nqq.nqq_npgvba_nqq
1180
npgvba = omeyvo.nqq.nqq_npgvba_nqq_naq_cevag
1182
omeyvo.nqq.fzneg_nqq(svyr_yvfg, abg ab_erphefr, npgvba)
1185
pynff pzq_zxqve(Pbzznaq):
1186
'''.splitlines(True)
1187
, [(0, 0, 1), (1, 4, 2), (9, 19, 1), (12, 23, 3)])
1189
def test_patience_unified_diff(self):
1190
txt_a = ['hello there\n',
1192
'how are you today?\n']
1193
txt_b = ['hello there\n',
1194
'how are you today?\n']
1195
unified_diff = patiencediff.unified_diff
1196
psm = self._PatienceSequenceMatcher
1197
self.assertEqual(['--- \n',
1199
'@@ -1,3 +1,2 @@\n',
1202
' how are you today?\n'
1204
, list(unified_diff(txt_a, txt_b,
1205
sequencematcher=psm)))
1206
txt_a = [x+'\n' for x in 'abcdefghijklmnop']
1207
txt_b = [x+'\n' for x in 'abcdefxydefghijklmnop']
1208
# This is the result with LongestCommonSubstring matching
1209
self.assertEqual(['--- \n',
1211
'@@ -1,6 +1,11 @@\n',
1223
, list(unified_diff(txt_a, txt_b)))
1224
# And the patience diff
1225
self.assertEqual(['--- \n',
1227
'@@ -4,6 +4,11 @@\n',
1240
, list(unified_diff(txt_a, txt_b,
1241
sequencematcher=psm)))
1243
def test_patience_unified_diff_with_dates(self):
1244
txt_a = ['hello there\n',
1246
'how are you today?\n']
1247
txt_b = ['hello there\n',
1248
'how are you today?\n']
1249
unified_diff = patiencediff.unified_diff
1250
psm = self._PatienceSequenceMatcher
1251
self.assertEqual(['--- a\t2008-08-08\n',
1252
'+++ b\t2008-09-09\n',
1253
'@@ -1,3 +1,2 @@\n',
1256
' how are you today?\n'
1258
, list(unified_diff(txt_a, txt_b,
1259
fromfile='a', tofile='b',
1260
fromfiledate='2008-08-08',
1261
tofiledate='2008-09-09',
1262
sequencematcher=psm)))
1265
class TestPatienceDiffLib_c(TestPatienceDiffLib):
1267
_test_needs_features = [features.compiled_patiencediff_feature]
1270
super(TestPatienceDiffLib_c, self).setUp()
1271
from breezy import _patiencediff_c
1272
self._unique_lcs = _patiencediff_c.unique_lcs_c
1273
self._recurse_matches = _patiencediff_c.recurse_matches_c
1274
self._PatienceSequenceMatcher = \
1275
_patiencediff_c.PatienceSequenceMatcher_c
1277
def test_unhashable(self):
1278
"""We should get a proper exception here."""
1279
# We need to be able to hash items in the sequence, lists are
1280
# unhashable, and thus cannot be diffed
1281
e = self.assertRaises(TypeError, self._PatienceSequenceMatcher,
1283
e = self.assertRaises(TypeError, self._PatienceSequenceMatcher,
1284
None, ['valid', []], [])
1285
e = self.assertRaises(TypeError, self._PatienceSequenceMatcher,
1286
None, ['valid'], [[]])
1287
e = self.assertRaises(TypeError, self._PatienceSequenceMatcher,
1288
None, ['valid'], ['valid', []])
1291
class TestPatienceDiffLibFiles(tests.TestCaseInTempDir):
1294
super(TestPatienceDiffLibFiles, self).setUp()
1295
self._PatienceSequenceMatcher = \
1296
_patiencediff_py.PatienceSequenceMatcher_py
1298
def test_patience_unified_diff_files(self):
1299
txt_a = [b'hello there\n',
1301
b'how are you today?\n']
1302
txt_b = [b'hello there\n',
1303
b'how are you today?\n']
1304
with open('a1', 'wb') as f: f.writelines(txt_a)
1305
with open('b1', 'wb') as f: f.writelines(txt_b)
1307
unified_diff_files = patiencediff.unified_diff_files
1308
psm = self._PatienceSequenceMatcher
1309
self.assertEqual([b'--- a1\n',
1311
b'@@ -1,3 +1,2 @@\n',
1314
b' how are you today?\n',
1316
, list(unified_diff_files(b'a1', b'b1',
1317
sequencematcher=psm)))
1319
txt_a = [x+'\n' for x in 'abcdefghijklmnop']
1320
txt_b = [x+'\n' for x in 'abcdefxydefghijklmnop']
1321
with open('a2', 'wt') as f: f.writelines(txt_a)
1322
with open('b2', 'wt') as f: f.writelines(txt_b)
1324
# This is the result with LongestCommonSubstring matching
1325
self.assertEqual([b'--- a2\n',
1327
b'@@ -1,6 +1,11 @@\n',
1339
, list(unified_diff_files(b'a2', b'b2')))
1341
# And the patience diff
1342
self.assertEqual([b'--- a2\n',
1344
b'@@ -4,6 +4,11 @@\n',
1356
list(unified_diff_files(b'a2', b'b2',
1357
sequencematcher=psm)))
1360
class TestPatienceDiffLibFiles_c(TestPatienceDiffLibFiles):
1362
_test_needs_features = [features.compiled_patiencediff_feature]
1365
super(TestPatienceDiffLibFiles_c, self).setUp()
1366
from breezy import _patiencediff_c
1367
self._PatienceSequenceMatcher = \
1368
_patiencediff_c.PatienceSequenceMatcher_c
1371
class TestUsingCompiledIfAvailable(tests.TestCase):
1373
def test_PatienceSequenceMatcher(self):
1374
if features.compiled_patiencediff_feature.available():
1375
from breezy._patiencediff_c import PatienceSequenceMatcher_c
1376
self.assertIs(PatienceSequenceMatcher_c,
1377
patiencediff.PatienceSequenceMatcher)
1379
from breezy._patiencediff_py import PatienceSequenceMatcher_py
1380
self.assertIs(PatienceSequenceMatcher_py,
1381
patiencediff.PatienceSequenceMatcher)
1383
def test_unique_lcs(self):
1384
if features.compiled_patiencediff_feature.available():
1385
from breezy._patiencediff_c import unique_lcs_c
1386
self.assertIs(unique_lcs_c,
1387
patiencediff.unique_lcs)
1389
from breezy._patiencediff_py import unique_lcs_py
1390
self.assertIs(unique_lcs_py,
1391
patiencediff.unique_lcs)
1393
def test_recurse_matches(self):
1394
if features.compiled_patiencediff_feature.available():
1395
from breezy._patiencediff_c import recurse_matches_c
1396
self.assertIs(recurse_matches_c,
1397
patiencediff.recurse_matches)
1399
from breezy._patiencediff_py import recurse_matches_py
1400
self.assertIs(recurse_matches_py,
1401
patiencediff.recurse_matches)
1404
class TestDiffFromTool(tests.TestCaseWithTransport):
1406
def test_from_string(self):
1407
diff_obj = diff.DiffFromTool.from_string('diff', None, None, None)
1408
self.addCleanup(diff_obj.finish)
1409
self.assertEqual(['diff', '@old_path', '@new_path'],
1410
diff_obj.command_template)
1412
def test_from_string_u5(self):
1413
diff_obj = diff.DiffFromTool.from_string('diff "-u 5"',
1415
self.addCleanup(diff_obj.finish)
1416
self.assertEqual(['diff', '-u 5', '@old_path', '@new_path'],
1417
diff_obj.command_template)
1418
self.assertEqual(['diff', '-u 5', 'old-path', 'new-path'],
1419
diff_obj._get_command('old-path', 'new-path'))
1421
def test_from_string_path_with_backslashes(self):
1422
self.requireFeature(features.backslashdir_feature)
1423
tool = 'C:\\Tools\\Diff.exe'
1424
diff_obj = diff.DiffFromTool.from_string(tool, None, None, None)
1425
self.addCleanup(diff_obj.finish)
1426
self.assertEqual(['C:\\Tools\\Diff.exe', '@old_path', '@new_path'],
1427
diff_obj.command_template)
1428
self.assertEqual(['C:\\Tools\\Diff.exe', 'old-path', 'new-path'],
1429
diff_obj._get_command('old-path', 'new-path'))
1431
def test_execute(self):
1433
diff_obj = diff.DiffFromTool(['python', '-c',
1434
'print("@old_path @new_path")'],
1436
self.addCleanup(diff_obj.finish)
1437
diff_obj._execute('old', 'new')
1438
self.assertEqual(output.getvalue().rstrip(), b'old new')
1440
def test_execute_missing(self):
1441
diff_obj = diff.DiffFromTool(['a-tool-which-is-unlikely-to-exist'],
1443
self.addCleanup(diff_obj.finish)
1444
e = self.assertRaises(errors.ExecutableMissing, diff_obj._execute,
1446
self.assertEqual('a-tool-which-is-unlikely-to-exist could not be found'
1447
' on this machine', str(e))
1449
def test_prepare_files_creates_paths_readable_by_windows_tool(self):
1450
self.requireFeature(features.AttribFeature)
1452
tree = self.make_branch_and_tree('tree')
1453
self.build_tree_contents([('tree/file', b'content')])
1454
tree.add('file', b'file-id')
1455
tree.commit('old tree')
1457
self.addCleanup(tree.unlock)
1458
basis_tree = tree.basis_tree()
1459
basis_tree.lock_read()
1460
self.addCleanup(basis_tree.unlock)
1461
diff_obj = diff.DiffFromTool(['python', '-c',
1462
'print "@old_path @new_path"'],
1463
basis_tree, tree, output)
1464
diff_obj._prepare_files('file', 'file', file_id=b'file-id')
1465
# The old content should be readonly
1466
self.assertReadableByAttrib(diff_obj._root, 'old\\file',
1468
# The new content should use the tree object, not a 'new' file anymore
1469
self.assertEndsWith(tree.basedir, 'work/tree')
1470
self.assertReadableByAttrib(tree.basedir, 'file', r'work\\tree\\file$')
1472
def assertReadableByAttrib(self, cwd, relpath, regex):
1473
proc = subprocess.Popen(['attrib', relpath],
1474
stdout=subprocess.PIPE,
1476
(result, err) = proc.communicate()
1477
self.assertContainsRe(result.replace('\r\n', '\n'), regex)
1479
def test_prepare_files(self):
1481
tree = self.make_branch_and_tree('tree')
1482
self.build_tree_contents([('tree/oldname', b'oldcontent')])
1483
self.build_tree_contents([('tree/oldname2', b'oldcontent2')])
1484
tree.add('oldname', b'file-id')
1485
tree.add('oldname2', b'file2-id')
1486
# Earliest allowable date on FAT32 filesystems is 1980-01-01
1487
tree.commit('old tree', timestamp=315532800)
1488
tree.rename_one('oldname', 'newname')
1489
tree.rename_one('oldname2', 'newname2')
1490
self.build_tree_contents([('tree/newname', b'newcontent')])
1491
self.build_tree_contents([('tree/newname2', b'newcontent2')])
1492
old_tree = tree.basis_tree()
1493
old_tree.lock_read()
1494
self.addCleanup(old_tree.unlock)
1496
self.addCleanup(tree.unlock)
1497
diff_obj = diff.DiffFromTool(['python', '-c',
1498
'print "@old_path @new_path"'],
1499
old_tree, tree, output)
1500
self.addCleanup(diff_obj.finish)
1501
self.assertContainsRe(diff_obj._root, 'brz-diff-[^/]*')
1502
old_path, new_path = diff_obj._prepare_files(
1503
'oldname', 'newname', file_id=b'file-id')
1504
self.assertContainsRe(old_path, 'old/oldname$')
1505
self.assertEqual(315532800, os.stat(old_path).st_mtime)
1506
self.assertContainsRe(new_path, 'tree/newname$')
1507
self.assertFileEqual(b'oldcontent', old_path)
1508
self.assertFileEqual(b'newcontent', new_path)
1509
if osutils.host_os_dereferences_symlinks():
1510
self.assertTrue(os.path.samefile('tree/newname', new_path))
1511
# make sure we can create files with the same parent directories
1512
diff_obj._prepare_files('oldname2', 'newname2', file_id=b'file2-id')
1515
class TestDiffFromToolEncodedFilename(tests.TestCaseWithTransport):
1517
def test_encodable_filename(self):
1518
# Just checks file path for external diff tool.
1519
# We cannot change CPython's internal encoding used by os.exec*.
1520
diffobj = diff.DiffFromTool(['dummy', '@old_path', '@new_path'],
1522
for _, scenario in EncodingAdapter.encoding_scenarios:
1523
encoding = scenario['encoding']
1524
dirname = scenario['info']['directory']
1525
filename = scenario['info']['filename']
1527
self.overrideAttr(diffobj, '_fenc', lambda: encoding)
1528
relpath = dirname + u'/' + filename
1529
fullpath = diffobj._safe_filename('safe', relpath)
1530
self.assertEqual(fullpath,
1531
fullpath.encode(encoding).decode(encoding))
1532
self.assertTrue(fullpath.startswith(diffobj._root + '/safe'))
1534
def test_unencodable_filename(self):
1535
diffobj = diff.DiffFromTool(['dummy', '@old_path', '@new_path'],
1537
for _, scenario in EncodingAdapter.encoding_scenarios:
1538
encoding = scenario['encoding']
1539
dirname = scenario['info']['directory']
1540
filename = scenario['info']['filename']
1542
if encoding == 'iso-8859-1':
1543
encoding = 'iso-8859-2'
1545
encoding = 'iso-8859-1'
1547
self.overrideAttr(diffobj, '_fenc', lambda: encoding)
1548
relpath = dirname + u'/' + filename
1549
fullpath = diffobj._safe_filename('safe', relpath)
1550
self.assertEqual(fullpath,
1551
fullpath.encode(encoding).decode(encoding))
1552
self.assertTrue(fullpath.startswith(diffobj._root + '/safe'))
1555
class TestGetTreesAndBranchesToDiffLocked(tests.TestCaseWithTransport):
1557
def call_gtabtd(self, path_list, revision_specs, old_url, new_url):
1558
"""Call get_trees_and_branches_to_diff_locked."""
1559
return diff.get_trees_and_branches_to_diff_locked(
1560
path_list, revision_specs, old_url, new_url, self.addCleanup)
1562
def test_basic(self):
1563
tree = self.make_branch_and_tree('tree')
1564
(old_tree, new_tree,
1565
old_branch, new_branch,
1566
specific_files, extra_trees) = self.call_gtabtd(
1567
['tree'], None, None, None)
1569
self.assertIsInstance(old_tree, revisiontree.RevisionTree)
1570
self.assertEqual(_mod_revision.NULL_REVISION,
1571
old_tree.get_revision_id())
1572
self.assertEqual(tree.basedir, new_tree.basedir)
1573
self.assertEqual(tree.branch.base, old_branch.base)
1574
self.assertEqual(tree.branch.base, new_branch.base)
1575
self.assertIs(None, specific_files)
1576
self.assertIs(None, extra_trees)
1578
def test_with_rev_specs(self):
1579
tree = self.make_branch_and_tree('tree')
1580
self.build_tree_contents([('tree/file', b'oldcontent')])
1581
tree.add('file', b'file-id')
1582
tree.commit('old tree', timestamp=0, rev_id=b"old-id")
1583
self.build_tree_contents([('tree/file', b'newcontent')])
1584
tree.commit('new tree', timestamp=0, rev_id=b"new-id")
1586
revisions = [revisionspec.RevisionSpec.from_string('1'),
1587
revisionspec.RevisionSpec.from_string('2')]
1588
(old_tree, new_tree,
1589
old_branch, new_branch,
1590
specific_files, extra_trees) = self.call_gtabtd(
1591
['tree'], revisions, None, None)
1593
self.assertIsInstance(old_tree, revisiontree.RevisionTree)
1594
self.assertEqual(b"old-id", old_tree.get_revision_id())
1595
self.assertIsInstance(new_tree, revisiontree.RevisionTree)
1596
self.assertEqual(b"new-id", new_tree.get_revision_id())
1597
self.assertEqual(tree.branch.base, old_branch.base)
1598
self.assertEqual(tree.branch.base, new_branch.base)
1599
self.assertIs(None, specific_files)
1600
self.assertEqual(tree.basedir, extra_trees[0].basedir)