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
29
revision as _mod_revision,
35
from ..sixish import (
43
from ..tests.scenarios import load_tests_apply_scenarios
46
load_tests = load_tests_apply_scenarios
49
def subst_dates(string):
50
"""Replace date strings with constant values."""
51
return re.sub(br'\d{4}-\d{2}-\d{2} \d{2}:\d{2}:\d{2} [-\+]\d{4}',
52
b'YYYY-MM-DD HH:MM:SS +ZZZZ', string)
55
def udiff_lines(old, new, allow_binary=False):
57
diff.internal_diff('old', old, 'new', new, output, allow_binary)
59
return output.readlines()
62
def external_udiff_lines(old, new, use_stringio=False):
64
# BytesIO has no fileno, so it tests a different codepath
67
output = tempfile.TemporaryFile()
69
diff.external_diff('old', old, 'new', new, output, diff_opts=['-u'])
71
raise tests.TestSkipped('external "diff" not present to test')
73
lines = output.readlines()
79
"""Simple file-like object that allows writes with any type and records."""
82
self.write_record = []
84
def write(self, data):
85
self.write_record.append(data)
87
def check_types(self, testcase, expected_type):
89
any(not isinstance(o, expected_type) for o in self.write_record),
90
"Not all writes of type %s: %r" % (
91
expected_type.__name__, self.write_record))
94
class TestDiffOptions(tests.TestCase):
96
def test_unified_added(self):
97
"""Check for default style '-u' only if no other style specified
100
# Verify that style defaults to unified, id est '-u' appended
101
# to option list, in the absence of an alternative style.
102
self.assertEqual(['-a', '-u'], diff.default_style_unified(['-a']))
105
class TestDiffOptionsScenarios(tests.TestCase):
107
scenarios = [(s, dict(style=s)) for s in diff.style_option_list]
108
style = None # Set by load_tests_apply_scenarios from scenarios
110
def test_unified_not_added(self):
111
# Verify that for all valid style options, '-u' is not
112
# appended to option list.
113
ret_opts = diff.default_style_unified(diff_opts=["%s" % (self.style,)])
114
self.assertEqual(["%s" % (self.style,)], ret_opts)
117
class TestDiff(tests.TestCase):
119
def test_add_nl(self):
120
"""diff generates a valid diff for patches that add a newline"""
121
lines = udiff_lines([b'boo'], [b'boo\n'])
122
self.check_patch(lines)
123
self.assertEqual(lines[4], b'\\ No newline at end of file\n')
124
## "expected no-nl, got %r" % lines[4]
126
def test_add_nl_2(self):
127
"""diff generates a valid diff for patches that change last line and
130
lines = udiff_lines([b'boo'], [b'goo\n'])
131
self.check_patch(lines)
132
self.assertEqual(lines[4], b'\\ No newline at end of file\n')
133
## "expected no-nl, got %r" % lines[4]
135
def test_remove_nl(self):
136
"""diff generates a valid diff for patches that change last line and
139
lines = udiff_lines([b'boo\n'], [b'boo'])
140
self.check_patch(lines)
141
self.assertEqual(lines[5], b'\\ No newline at end of file\n')
142
## "expected no-nl, got %r" % lines[5]
144
def check_patch(self, lines):
145
self.assertTrue(len(lines) > 1)
146
## "Not enough lines for a file header for patch:\n%s" % "".join(lines)
147
self.assertTrue(lines[0].startswith(b'---'))
148
## 'No orig line for patch:\n%s' % "".join(lines)
149
self.assertTrue(lines[1].startswith(b'+++'))
150
## 'No mod line for patch:\n%s' % "".join(lines)
151
self.assertTrue(len(lines) > 2)
152
## "No hunks for patch:\n%s" % "".join(lines)
153
self.assertTrue(lines[2].startswith(b'@@'))
154
## "No hunk header for patch:\n%s" % "".join(lines)
155
self.assertTrue(b'@@' in lines[2][2:])
156
## "Unterminated hunk header for patch:\n%s" % "".join(lines)
158
def test_binary_lines(self):
160
uni_lines = [1023 * b'a' + b'\x00']
161
self.assertRaises(errors.BinaryFile, udiff_lines, uni_lines, empty)
162
self.assertRaises(errors.BinaryFile, udiff_lines, empty, uni_lines)
163
udiff_lines(uni_lines, empty, allow_binary=True)
164
udiff_lines(empty, uni_lines, allow_binary=True)
166
def test_external_diff(self):
167
lines = external_udiff_lines([b'boo\n'], [b'goo\n'])
168
self.check_patch(lines)
169
self.assertEqual(b'\n', lines[-1])
171
def test_external_diff_no_fileno(self):
172
# Make sure that we can handle not having a fileno, even
173
# if the diff is large
174
lines = external_udiff_lines([b'boo\n'] * 10000,
177
self.check_patch(lines)
179
def test_external_diff_binary_lang_c(self):
180
for lang in ('LANG', 'LC_ALL', 'LANGUAGE'):
181
self.overrideEnv(lang, 'C')
182
lines = external_udiff_lines([b'\x00foobar\n'], [b'foo\x00bar\n'])
183
# Older versions of diffutils say "Binary files", newer
184
# versions just say "Files".
185
self.assertContainsRe(
186
lines[0], b'(Binary f|F)iles old and new differ\n')
187
self.assertEqual(lines[1:], [b'\n'])
189
def test_no_external_diff(self):
190
"""Check that NoDiff is raised when diff is not available"""
191
# Make sure no 'diff' command is available
192
# XXX: Weird, using None instead of '' breaks the test -- vila 20101216
193
self.overrideEnv('PATH', '')
194
self.assertRaises(errors.NoDiff, diff.external_diff,
195
b'old', [b'boo\n'], b'new', [b'goo\n'],
196
BytesIO(), diff_opts=['-u'])
198
def test_internal_diff_default(self):
199
# Default internal diff encoding is utf8
201
diff.internal_diff(u'old_\xb5', [b'old_text\n'],
202
u'new_\xe5', [b'new_text\n'], output)
203
lines = output.getvalue().splitlines(True)
204
self.check_patch(lines)
205
self.assertEqual([b'--- old_\xc2\xb5\n',
206
b'+++ new_\xc3\xa5\n',
207
b'@@ -1,1 +1,1 @@\n',
213
def test_internal_diff_utf8(self):
215
diff.internal_diff(u'old_\xb5', [b'old_text\n'],
216
u'new_\xe5', [b'new_text\n'], output,
217
path_encoding='utf8')
218
lines = output.getvalue().splitlines(True)
219
self.check_patch(lines)
220
self.assertEqual([b'--- old_\xc2\xb5\n',
221
b'+++ new_\xc3\xa5\n',
222
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',
243
def test_internal_diff_no_content(self):
245
diff.internal_diff(u'old', [], u'new', [], output)
246
self.assertEqual(b'', output.getvalue())
248
def test_internal_diff_no_changes(self):
250
diff.internal_diff(u'old', [b'text\n', b'contents\n'],
251
u'new', [b'text\n', b'contents\n'],
253
self.assertEqual(b'', output.getvalue())
255
def test_internal_diff_returns_bytes(self):
257
diff.internal_diff(u'old_\xb5', [b'old_text\n'],
258
u'new_\xe5', [b'new_text\n'], output)
259
output.check_types(self, bytes)
261
def test_internal_diff_default_context(self):
263
diff.internal_diff('old', [b'same_text\n', b'same_text\n', b'same_text\n',
264
b'same_text\n', b'same_text\n', b'old_text\n'],
265
'new', [b'same_text\n', b'same_text\n', b'same_text\n',
266
b'same_text\n', b'same_text\n', b'new_text\n'], output)
267
lines = output.getvalue().splitlines(True)
268
self.check_patch(lines)
269
self.assertEqual([b'--- old\n',
271
b'@@ -3,4 +3,4 @@\n',
280
def test_internal_diff_no_context(self):
282
diff.internal_diff('old', [b'same_text\n', b'same_text\n', b'same_text\n',
283
b'same_text\n', b'same_text\n', b'old_text\n'],
284
'new', [b'same_text\n', b'same_text\n', b'same_text\n',
285
b'same_text\n', b'same_text\n', b'new_text\n'], output,
287
lines = output.getvalue().splitlines(True)
288
self.check_patch(lines)
289
self.assertEqual([b'--- old\n',
291
b'@@ -6,1 +6,1 @@\n',
297
def test_internal_diff_more_context(self):
299
diff.internal_diff('old', [b'same_text\n', b'same_text\n', b'same_text\n',
300
b'same_text\n', b'same_text\n', b'old_text\n'],
301
'new', [b'same_text\n', b'same_text\n', b'same_text\n',
302
b'same_text\n', b'same_text\n', b'new_text\n'], output,
304
lines = output.getvalue().splitlines(True)
305
self.check_patch(lines)
306
self.assertEqual([b'--- old\n',
308
b'@@ -2,5 +2,5 @@\n',
319
class TestDiffFiles(tests.TestCaseInTempDir):
321
def test_external_diff_binary(self):
322
"""The output when using external diff should use diff's i18n error"""
323
for lang in ('LANG', 'LC_ALL', 'LANGUAGE'):
324
self.overrideEnv(lang, 'C')
325
# Make sure external_diff doesn't fail in the current LANG
326
lines = external_udiff_lines([b'\x00foobar\n'], [b'foo\x00bar\n'])
328
cmd = ['diff', '-u', '--binary', 'old', 'new']
329
with open('old', 'wb') as f:
330
f.write(b'\x00foobar\n')
331
with open('new', 'wb') as f:
332
f.write(b'foo\x00bar\n')
333
pipe = subprocess.Popen(cmd, stdout=subprocess.PIPE,
334
stdin=subprocess.PIPE)
335
out, err = pipe.communicate()
336
# We should output whatever diff tells us, plus a trailing newline
337
self.assertEqual(out.splitlines(True) + [b'\n'], lines)
340
def get_diff_as_string(tree1, tree2, specific_files=None, working_tree=None):
342
if working_tree is not None:
343
extra_trees = (working_tree,)
346
diff.show_diff_trees(tree1, tree2, output,
347
specific_files=specific_files,
348
extra_trees=extra_trees, old_label='old/',
350
return output.getvalue()
353
class TestDiffDates(tests.TestCaseWithTransport):
356
super(TestDiffDates, self).setUp()
357
self.wt = self.make_branch_and_tree('.')
358
self.b = self.wt.branch
359
self.build_tree_contents([
360
('file1', b'file1 contents at rev 1\n'),
361
('file2', b'file2 contents at rev 1\n')
363
self.wt.add(['file1', 'file2'])
365
message='Revision 1',
366
timestamp=1143849600, # 2006-04-01 00:00:00 UTC
369
self.build_tree_contents([('file1', b'file1 contents at rev 2\n')])
371
message='Revision 2',
372
timestamp=1143936000, # 2006-04-02 00:00:00 UTC
375
self.build_tree_contents([('file2', b'file2 contents at rev 3\n')])
377
message='Revision 3',
378
timestamp=1144022400, # 2006-04-03 00:00:00 UTC
381
self.wt.remove(['file2'])
383
message='Revision 4',
384
timestamp=1144108800, # 2006-04-04 00:00:00 UTC
387
self.build_tree_contents([
388
('file1', b'file1 contents in working tree\n')
390
# set the date stamps for files in the working tree to known values
391
os.utime('file1', (1144195200, 1144195200)) # 2006-04-05 00:00:00 UTC
393
def test_diff_rev_tree_working_tree(self):
394
output = get_diff_as_string(self.wt.basis_tree(), self.wt)
395
# note that the date for old/file1 is from rev 2 rather than from
396
# the basis revision (rev 4)
397
self.assertEqualDiff(output, b'''\
398
=== modified file 'file1'
399
--- old/file1\t2006-04-02 00:00:00 +0000
400
+++ new/file1\t2006-04-05 00:00:00 +0000
402
-file1 contents at rev 2
403
+file1 contents in working tree
407
def test_diff_rev_tree_rev_tree(self):
408
tree1 = self.b.repository.revision_tree(b'rev-2')
409
tree2 = self.b.repository.revision_tree(b'rev-3')
410
output = get_diff_as_string(tree1, tree2)
411
self.assertEqualDiff(output, b'''\
412
=== modified file 'file2'
413
--- old/file2\t2006-04-01 00:00:00 +0000
414
+++ new/file2\t2006-04-03 00:00:00 +0000
416
-file2 contents at rev 1
417
+file2 contents at rev 3
421
def test_diff_add_files(self):
422
tree1 = self.b.repository.revision_tree(_mod_revision.NULL_REVISION)
423
tree2 = self.b.repository.revision_tree(b'rev-1')
424
output = get_diff_as_string(tree1, tree2)
425
# the files have the epoch time stamp for the tree in which
427
self.assertEqualDiff(output, b'''\
428
=== added file 'file1'
429
--- old/file1\t1970-01-01 00:00:00 +0000
430
+++ new/file1\t2006-04-01 00:00:00 +0000
432
+file1 contents at rev 1
434
=== added file 'file2'
435
--- old/file2\t1970-01-01 00:00:00 +0000
436
+++ new/file2\t2006-04-01 00:00:00 +0000
438
+file2 contents at rev 1
442
def test_diff_remove_files(self):
443
tree1 = self.b.repository.revision_tree(b'rev-3')
444
tree2 = self.b.repository.revision_tree(b'rev-4')
445
output = get_diff_as_string(tree1, tree2)
446
# the file has the epoch time stamp for the tree in which
448
self.assertEqualDiff(output, b'''\
449
=== removed file 'file2'
450
--- old/file2\t2006-04-03 00:00:00 +0000
451
+++ new/file2\t1970-01-01 00:00:00 +0000
453
-file2 contents at rev 3
457
def test_show_diff_specified(self):
458
"""A working tree filename can be used to identify a file"""
459
self.wt.rename_one('file1', 'file1b')
460
old_tree = self.b.repository.revision_tree(b'rev-1')
461
new_tree = self.b.repository.revision_tree(b'rev-4')
462
out = get_diff_as_string(old_tree, new_tree, specific_files=['file1b'],
463
working_tree=self.wt)
464
self.assertContainsRe(out, b'file1\t')
466
def test_recursive_diff(self):
467
"""Children of directories are matched"""
470
self.wt.add(['dir1', 'dir2'])
471
self.wt.rename_one('file1', 'dir1/file1')
472
old_tree = self.b.repository.revision_tree(b'rev-1')
473
new_tree = self.b.repository.revision_tree(b'rev-4')
474
out = get_diff_as_string(old_tree, new_tree, specific_files=['dir1'],
475
working_tree=self.wt)
476
self.assertContainsRe(out, b'file1\t')
477
out = get_diff_as_string(old_tree, new_tree, specific_files=['dir2'],
478
working_tree=self.wt)
479
self.assertNotContainsRe(out, b'file1\t')
482
class TestShowDiffTrees(tests.TestCaseWithTransport):
483
"""Direct tests for show_diff_trees"""
485
def test_modified_file(self):
486
"""Test when a file is modified."""
487
tree = self.make_branch_and_tree('tree')
488
self.build_tree_contents([('tree/file', b'contents\n')])
489
tree.add(['file'], [b'file-id'])
490
tree.commit('one', rev_id=b'rev-1')
492
self.build_tree_contents([('tree/file', b'new contents\n')])
493
d = get_diff_as_string(tree.basis_tree(), tree)
494
self.assertContainsRe(d, b"=== modified file 'file'\n")
495
self.assertContainsRe(d, b'--- old/file\t')
496
self.assertContainsRe(d, b'\\+\\+\\+ new/file\t')
497
self.assertContainsRe(d, b'-contents\n'
498
b'\\+new contents\n')
500
def test_modified_file_in_renamed_dir(self):
501
"""Test when a file is modified in a renamed directory."""
502
tree = self.make_branch_and_tree('tree')
503
self.build_tree(['tree/dir/'])
504
self.build_tree_contents([('tree/dir/file', b'contents\n')])
505
tree.add(['dir', 'dir/file'], [b'dir-id', b'file-id'])
506
tree.commit('one', rev_id=b'rev-1')
508
tree.rename_one('dir', 'other')
509
self.build_tree_contents([('tree/other/file', b'new contents\n')])
510
d = get_diff_as_string(tree.basis_tree(), tree)
511
self.assertContainsRe(d, b"=== renamed directory 'dir' => 'other'\n")
512
self.assertContainsRe(d, b"=== modified file 'other/file'\n")
513
# XXX: This is technically incorrect, because it used to be at another
514
# location. What to do?
515
self.assertContainsRe(d, b'--- old/dir/file\t')
516
self.assertContainsRe(d, b'\\+\\+\\+ new/other/file\t')
517
self.assertContainsRe(d, b'-contents\n'
518
b'\\+new contents\n')
520
def test_renamed_directory(self):
521
"""Test when only a directory is only renamed."""
522
tree = self.make_branch_and_tree('tree')
523
self.build_tree(['tree/dir/'])
524
self.build_tree_contents([('tree/dir/file', b'contents\n')])
525
tree.add(['dir', 'dir/file'], [b'dir-id', b'file-id'])
526
tree.commit('one', rev_id=b'rev-1')
528
tree.rename_one('dir', 'newdir')
529
d = get_diff_as_string(tree.basis_tree(), tree)
530
# Renaming a directory should be a single "you renamed this dir" even
531
# when there are files inside.
532
self.assertEqual(d, b"=== renamed directory 'dir' => 'newdir'\n")
534
def test_renamed_file(self):
535
"""Test when a file is only renamed."""
536
tree = self.make_branch_and_tree('tree')
537
self.build_tree_contents([('tree/file', b'contents\n')])
538
tree.add(['file'], [b'file-id'])
539
tree.commit('one', rev_id=b'rev-1')
541
tree.rename_one('file', 'newname')
542
d = get_diff_as_string(tree.basis_tree(), tree)
543
self.assertContainsRe(d, b"=== renamed file 'file' => 'newname'\n")
544
# We shouldn't have a --- or +++ line, because there is no content
546
self.assertNotContainsRe(d, b'---')
548
def test_renamed_and_modified_file(self):
549
"""Test when a file is only renamed."""
550
tree = self.make_branch_and_tree('tree')
551
self.build_tree_contents([('tree/file', b'contents\n')])
552
tree.add(['file'], [b'file-id'])
553
tree.commit('one', rev_id=b'rev-1')
555
tree.rename_one('file', 'newname')
556
self.build_tree_contents([('tree/newname', b'new contents\n')])
557
d = get_diff_as_string(tree.basis_tree(), tree)
558
self.assertContainsRe(d, b"=== renamed file 'file' => 'newname'\n")
559
self.assertContainsRe(d, b'--- old/file\t')
560
self.assertContainsRe(d, b'\\+\\+\\+ new/newname\t')
561
self.assertContainsRe(d, b'-contents\n'
562
b'\\+new contents\n')
564
def test_internal_diff_exec_property(self):
565
tree = self.make_branch_and_tree('tree')
567
tt = transform.TreeTransform(tree)
568
tt.new_file('a', tt.root, [b'contents\n'], b'a-id', True)
569
tt.new_file('b', tt.root, [b'contents\n'], b'b-id', False)
570
tt.new_file('c', tt.root, [b'contents\n'], b'c-id', True)
571
tt.new_file('d', tt.root, [b'contents\n'], b'd-id', False)
572
tt.new_file('e', tt.root, [b'contents\n'], b'control-e-id', True)
573
tt.new_file('f', tt.root, [b'contents\n'], b'control-f-id', False)
575
tree.commit('one', rev_id=b'rev-1')
577
tt = transform.TreeTransform(tree)
578
tt.set_executability(False, tt.trans_id_file_id(b'a-id'))
579
tt.set_executability(True, tt.trans_id_file_id(b'b-id'))
580
tt.set_executability(False, tt.trans_id_file_id(b'c-id'))
581
tt.set_executability(True, tt.trans_id_file_id(b'd-id'))
583
tree.rename_one('c', 'new-c')
584
tree.rename_one('d', 'new-d')
586
d = get_diff_as_string(tree.basis_tree(), tree)
588
self.assertContainsRe(d, br"file 'a'.*\(properties changed:"
590
self.assertContainsRe(d, br"file 'b'.*\(properties changed:"
592
self.assertContainsRe(d, br"file 'c'.*\(properties changed:"
594
self.assertContainsRe(d, br"file 'd'.*\(properties changed:"
596
self.assertNotContainsRe(d, br"file 'e'")
597
self.assertNotContainsRe(d, br"file 'f'")
599
def test_binary_unicode_filenames(self):
600
"""Test that contents of files are *not* encoded in UTF-8 when there
601
is a binary file in the diff.
603
# See https://bugs.launchpad.net/bugs/110092.
604
self.requireFeature(features.UnicodeFilenameFeature)
606
tree = self.make_branch_and_tree('tree')
607
alpha, omega = u'\u03b1', u'\u03c9'
608
alpha_utf8, omega_utf8 = alpha.encode('utf8'), omega.encode('utf8')
609
self.build_tree_contents(
610
[('tree/' + alpha, b'\0'),
612
(b'The %s and the %s\n' % (alpha_utf8, omega_utf8)))])
613
tree.add([alpha], [b'file-id'])
614
tree.add([omega], [b'file-id-2'])
615
diff_content = StubO()
616
diff.show_diff_trees(tree.basis_tree(), tree, diff_content)
617
diff_content.check_types(self, bytes)
618
d = b''.join(diff_content.write_record)
619
self.assertContainsRe(d, br"=== added file '%s'" % alpha_utf8)
620
self.assertContainsRe(d, b"Binary files a/%s.*and b/%s.* differ\n"
621
% (alpha_utf8, alpha_utf8))
622
self.assertContainsRe(d, br"=== added file '%s'" % omega_utf8)
623
self.assertContainsRe(d, br"--- a/%s" % (omega_utf8,))
624
self.assertContainsRe(d, br"\+\+\+ b/%s" % (omega_utf8,))
626
def test_unicode_filename(self):
627
"""Test when the filename are unicode."""
628
self.requireFeature(features.UnicodeFilenameFeature)
630
alpha, omega = u'\u03b1', u'\u03c9'
631
autf8, outf8 = alpha.encode('utf8'), omega.encode('utf8')
633
tree = self.make_branch_and_tree('tree')
634
self.build_tree_contents([('tree/ren_' + alpha, b'contents\n')])
635
tree.add(['ren_' + alpha], [b'file-id-2'])
636
self.build_tree_contents([('tree/del_' + alpha, b'contents\n')])
637
tree.add(['del_' + alpha], [b'file-id-3'])
638
self.build_tree_contents([('tree/mod_' + alpha, b'contents\n')])
639
tree.add(['mod_' + alpha], [b'file-id-4'])
641
tree.commit('one', rev_id=b'rev-1')
643
tree.rename_one('ren_' + alpha, 'ren_' + omega)
644
tree.remove('del_' + alpha)
645
self.build_tree_contents([('tree/add_' + alpha, b'contents\n')])
646
tree.add(['add_' + alpha], [b'file-id'])
647
self.build_tree_contents([('tree/mod_' + alpha, b'contents_mod\n')])
649
d = get_diff_as_string(tree.basis_tree(), tree)
650
self.assertContainsRe(d,
651
b"=== renamed file 'ren_%s' => 'ren_%s'\n" % (autf8, outf8))
652
self.assertContainsRe(d, b"=== added file 'add_%s'" % autf8)
653
self.assertContainsRe(d, b"=== modified file 'mod_%s'" % autf8)
654
self.assertContainsRe(d, b"=== removed file 'del_%s'" % autf8)
656
def test_unicode_filename_path_encoding(self):
657
"""Test for bug #382699: unicode filenames on Windows should be shown
660
self.requireFeature(features.UnicodeFilenameFeature)
661
# The word 'test' in Russian
662
_russian_test = u'\u0422\u0435\u0441\u0442'
663
directory = _russian_test + u'/'
664
test_txt = _russian_test + u'.txt'
665
u1234 = u'\u1234.txt'
667
tree = self.make_branch_and_tree('.')
668
self.build_tree_contents([
669
(test_txt, b'foo\n'),
673
tree.add([test_txt, u1234, directory])
676
diff.show_diff_trees(tree.basis_tree(), tree, sio,
677
path_encoding='cp1251')
679
output = subst_dates(sio.getvalue())
681
=== added directory '%(directory)s'
682
=== added file '%(test_txt)s'
683
--- a/%(test_txt)s\tYYYY-MM-DD HH:MM:SS +ZZZZ
684
+++ b/%(test_txt)s\tYYYY-MM-DD HH:MM:SS +ZZZZ
688
=== added file '?.txt'
689
--- a/?.txt\tYYYY-MM-DD HH:MM:SS +ZZZZ
690
+++ b/?.txt\tYYYY-MM-DD HH:MM:SS +ZZZZ
694
''' % {b'directory': _russian_test.encode('cp1251'),
695
b'test_txt': test_txt.encode('cp1251'),
697
self.assertEqualDiff(output, shouldbe)
700
class DiffWasIs(diff.DiffPath):
702
def diff(self, file_id, old_path, new_path, old_kind, new_kind):
703
self.to_file.write(b'was: ')
704
self.to_file.write(self.old_tree.get_file(old_path).read())
705
self.to_file.write(b'is: ')
706
self.to_file.write(self.new_tree.get_file(new_path).read())
709
class TestDiffTree(tests.TestCaseWithTransport):
712
super(TestDiffTree, self).setUp()
713
self.old_tree = self.make_branch_and_tree('old-tree')
714
self.old_tree.lock_write()
715
self.addCleanup(self.old_tree.unlock)
716
self.new_tree = self.make_branch_and_tree('new-tree')
717
self.new_tree.lock_write()
718
self.addCleanup(self.new_tree.unlock)
719
self.differ = diff.DiffTree(self.old_tree, self.new_tree, BytesIO())
721
def test_diff_text(self):
722
self.build_tree_contents([('old-tree/olddir/',),
723
('old-tree/olddir/oldfile', b'old\n')])
724
self.old_tree.add('olddir')
725
self.old_tree.add('olddir/oldfile', b'file-id')
726
self.build_tree_contents([('new-tree/newdir/',),
727
('new-tree/newdir/newfile', b'new\n')])
728
self.new_tree.add('newdir')
729
self.new_tree.add('newdir/newfile', b'file-id')
730
differ = diff.DiffText(self.old_tree, self.new_tree, BytesIO())
731
differ.diff_text('olddir/oldfile', None, 'old label',
732
'new label', b'file-id', None)
734
b'--- old label\n+++ new label\n@@ -1,1 +0,0 @@\n-old\n\n',
735
differ.to_file.getvalue())
736
differ.to_file.seek(0)
737
differ.diff_text(None, 'newdir/newfile',
738
'old label', 'new label', None, b'file-id')
740
b'--- old label\n+++ new label\n@@ -0,0 +1,1 @@\n+new\n\n',
741
differ.to_file.getvalue())
742
differ.to_file.seek(0)
743
differ.diff_text('olddir/oldfile', 'newdir/newfile',
744
'old label', 'new label', b'file-id', b'file-id')
746
b'--- old label\n+++ new label\n@@ -1,1 +1,1 @@\n-old\n+new\n\n',
747
differ.to_file.getvalue())
749
def test_diff_deletion(self):
750
self.build_tree_contents([('old-tree/file', b'contents'),
751
('new-tree/file', b'contents')])
752
self.old_tree.add('file', b'file-id')
753
self.new_tree.add('file', b'file-id')
754
os.unlink('new-tree/file')
755
self.differ.show_diff(None)
756
self.assertContainsRe(self.differ.to_file.getvalue(), b'-contents')
758
def test_diff_creation(self):
759
self.build_tree_contents([('old-tree/file', b'contents'),
760
('new-tree/file', b'contents')])
761
self.old_tree.add('file', b'file-id')
762
self.new_tree.add('file', b'file-id')
763
os.unlink('old-tree/file')
764
self.differ.show_diff(None)
765
self.assertContainsRe(self.differ.to_file.getvalue(), br'\+contents')
767
def test_diff_symlink(self):
768
differ = diff.DiffSymlink(self.old_tree, self.new_tree, BytesIO())
769
differ.diff_symlink('old target', None)
770
self.assertEqual(b"=== target was 'old target'\n",
771
differ.to_file.getvalue())
773
differ = diff.DiffSymlink(self.old_tree, self.new_tree, BytesIO())
774
differ.diff_symlink(None, 'new target')
775
self.assertEqual(b"=== target is 'new target'\n",
776
differ.to_file.getvalue())
778
differ = diff.DiffSymlink(self.old_tree, self.new_tree, BytesIO())
779
differ.diff_symlink('old target', 'new target')
780
self.assertEqual(b"=== target changed 'old target' => 'new target'\n",
781
differ.to_file.getvalue())
784
self.build_tree_contents([('old-tree/olddir/',),
785
('old-tree/olddir/oldfile', b'old\n')])
786
self.old_tree.add('olddir')
787
self.old_tree.add('olddir/oldfile', b'file-id')
788
self.build_tree_contents([('new-tree/newdir/',),
789
('new-tree/newdir/newfile', b'new\n')])
790
self.new_tree.add('newdir')
791
self.new_tree.add('newdir/newfile', b'file-id')
792
self.differ.diff(b'file-id', 'olddir/oldfile', 'newdir/newfile')
793
self.assertContainsRe(
794
self.differ.to_file.getvalue(),
795
br'--- olddir/oldfile.*\n\+\+\+ newdir/newfile.*\n\@\@ -1,1 \+1,1'
796
br' \@\@\n-old\n\+new\n\n')
798
def test_diff_kind_change(self):
799
self.requireFeature(features.SymlinkFeature)
800
self.build_tree_contents([('old-tree/olddir/',),
801
('old-tree/olddir/oldfile', b'old\n')])
802
self.old_tree.add('olddir')
803
self.old_tree.add('olddir/oldfile', b'file-id')
804
self.build_tree(['new-tree/newdir/'])
805
os.symlink('new', 'new-tree/newdir/newfile')
806
self.new_tree.add('newdir')
807
self.new_tree.add('newdir/newfile', b'file-id')
808
self.differ.diff(b'file-id', 'olddir/oldfile', 'newdir/newfile')
809
self.assertContainsRe(
810
self.differ.to_file.getvalue(),
811
br'--- olddir/oldfile.*\n\+\+\+ newdir/newfile.*\n\@\@ -1,1 \+0,0'
813
self.assertContainsRe(self.differ.to_file.getvalue(),
814
b"=== target is 'new'\n")
816
def test_diff_directory(self):
817
self.build_tree(['new-tree/new-dir/'])
818
self.new_tree.add('new-dir', b'new-dir-id')
819
self.differ.diff(b'new-dir-id', None, 'new-dir')
820
self.assertEqual(self.differ.to_file.getvalue(), b'')
822
def create_old_new(self):
823
self.build_tree_contents([('old-tree/olddir/',),
824
('old-tree/olddir/oldfile', b'old\n')])
825
self.old_tree.add('olddir')
826
self.old_tree.add('olddir/oldfile', b'file-id')
827
self.build_tree_contents([('new-tree/newdir/',),
828
('new-tree/newdir/newfile', b'new\n')])
829
self.new_tree.add('newdir')
830
self.new_tree.add('newdir/newfile', b'file-id')
832
def test_register_diff(self):
833
self.create_old_new()
834
old_diff_factories = diff.DiffTree.diff_factories
835
diff.DiffTree.diff_factories = old_diff_factories[:]
836
diff.DiffTree.diff_factories.insert(0, DiffWasIs.from_diff_tree)
838
differ = diff.DiffTree(self.old_tree, self.new_tree, BytesIO())
840
diff.DiffTree.diff_factories = old_diff_factories
841
differ.diff(b'file-id', 'olddir/oldfile', 'newdir/newfile')
842
self.assertNotContainsRe(
843
differ.to_file.getvalue(),
844
br'--- olddir/oldfile.*\n\+\+\+ newdir/newfile.*\n\@\@ -1,1 \+1,1'
845
br' \@\@\n-old\n\+new\n\n')
846
self.assertContainsRe(differ.to_file.getvalue(),
847
b'was: old\nis: new\n')
849
def test_extra_factories(self):
850
self.create_old_new()
851
differ = diff.DiffTree(self.old_tree, self.new_tree, BytesIO(),
852
extra_factories=[DiffWasIs.from_diff_tree])
853
differ.diff(b'file-id', 'olddir/oldfile', 'newdir/newfile')
854
self.assertNotContainsRe(
855
differ.to_file.getvalue(),
856
br'--- olddir/oldfile.*\n\+\+\+ newdir/newfile.*\n\@\@ -1,1 \+1,1'
857
br' \@\@\n-old\n\+new\n\n')
858
self.assertContainsRe(differ.to_file.getvalue(),
859
b'was: old\nis: new\n')
861
def test_alphabetical_order(self):
862
self.build_tree(['new-tree/a-file'])
863
self.new_tree.add('a-file')
864
self.build_tree(['old-tree/b-file'])
865
self.old_tree.add('b-file')
866
self.differ.show_diff(None)
867
self.assertContainsRe(self.differ.to_file.getvalue(),
868
b'.*a-file(.|\n)*b-file')
871
class TestPatienceDiffLib(tests.TestCase):
874
super(TestPatienceDiffLib, self).setUp()
875
self._unique_lcs = _patiencediff_py.unique_lcs_py
876
self._recurse_matches = _patiencediff_py.recurse_matches_py
877
self._PatienceSequenceMatcher = \
878
_patiencediff_py.PatienceSequenceMatcher_py
880
def test_diff_unicode_string(self):
881
a = ''.join([unichr(i) for i in range(4000, 4500, 3)])
882
b = ''.join([unichr(i) for i in range(4300, 4800, 2)])
883
sm = self._PatienceSequenceMatcher(None, a, b)
884
mb = sm.get_matching_blocks()
885
self.assertEqual(35, len(mb))
887
def test_unique_lcs(self):
888
unique_lcs = self._unique_lcs
889
self.assertEqual(unique_lcs('', ''), [])
890
self.assertEqual(unique_lcs('', 'a'), [])
891
self.assertEqual(unique_lcs('a', ''), [])
892
self.assertEqual(unique_lcs('a', 'a'), [(0, 0)])
893
self.assertEqual(unique_lcs('a', 'b'), [])
894
self.assertEqual(unique_lcs('ab', 'ab'), [(0, 0), (1, 1)])
895
self.assertEqual(unique_lcs('abcde', 'cdeab'),
896
[(2, 0), (3, 1), (4, 2)])
897
self.assertEqual(unique_lcs('cdeab', 'abcde'),
898
[(0, 2), (1, 3), (2, 4)])
899
self.assertEqual(unique_lcs('abXde', 'abYde'), [(0, 0), (1, 1),
901
self.assertEqual(unique_lcs('acbac', 'abc'), [(2, 1)])
903
def test_recurse_matches(self):
904
def test_one(a, b, matches):
906
self._recurse_matches(
907
a, b, 0, 0, len(a), len(b), test_matches, 10)
908
self.assertEqual(test_matches, matches)
910
test_one(['a', '', 'b', '', 'c'], ['a', 'a', 'b', 'c', 'c'],
911
[(0, 0), (2, 2), (4, 4)])
912
test_one(['a', 'c', 'b', 'a', 'c'], ['a', 'b', 'c'],
913
[(0, 0), (2, 1), (4, 2)])
914
# Even though 'bc' is not unique globally, and is surrounded by
915
# non-matching lines, we should still match, because they are locally
917
test_one('abcdbce', 'afbcgdbce', [(0, 0), (1, 2), (2, 3), (3, 5),
918
(4, 6), (5, 7), (6, 8)])
920
# recurse_matches doesn't match non-unique
921
# lines surrounded by bogus text.
922
# The update has been done in patiencediff.SequenceMatcher instead
924
# This is what it could be
925
#test_one('aBccDe', 'abccde', [(0,0), (2,2), (3,3), (5,5)])
927
# This is what it currently gives:
928
test_one('aBccDe', 'abccde', [(0, 0), (5, 5)])
930
def assertDiffBlocks(self, a, b, expected_blocks):
931
"""Check that the sequence matcher returns the correct blocks.
933
:param a: A sequence to match
934
:param b: Another sequence to match
935
:param expected_blocks: The expected output, not including the final
936
matching block (len(a), len(b), 0)
938
matcher = self._PatienceSequenceMatcher(None, a, b)
939
blocks = matcher.get_matching_blocks()
941
self.assertEqual((len(a), len(b), 0), last)
942
self.assertEqual(expected_blocks, blocks)
944
def test_matching_blocks(self):
945
# Some basic matching tests
946
self.assertDiffBlocks('', '', [])
947
self.assertDiffBlocks([], [], [])
948
self.assertDiffBlocks('abc', '', [])
949
self.assertDiffBlocks('', 'abc', [])
950
self.assertDiffBlocks('abcd', 'abcd', [(0, 0, 4)])
951
self.assertDiffBlocks('abcd', 'abce', [(0, 0, 3)])
952
self.assertDiffBlocks('eabc', 'abce', [(1, 0, 3)])
953
self.assertDiffBlocks('eabce', 'abce', [(1, 0, 4)])
954
self.assertDiffBlocks('abcde', 'abXde', [(0, 0, 2), (3, 3, 2)])
955
self.assertDiffBlocks('abcde', 'abXYZde', [(0, 0, 2), (3, 5, 2)])
956
self.assertDiffBlocks('abde', 'abXYZde', [(0, 0, 2), (2, 5, 2)])
957
# This may check too much, but it checks to see that
958
# a copied block stays attached to the previous section,
960
# difflib would tend to grab the trailing longest match
961
# which would make the diff not look right
962
self.assertDiffBlocks('abcdefghijklmnop', 'abcdefxydefghijklmnop',
963
[(0, 0, 6), (6, 11, 10)])
965
# make sure it supports passing in lists
966
self.assertDiffBlocks(
969
'how are you today?\n'],
971
'how are you today?\n'],
972
[(0, 0, 1), (2, 1, 1)])
974
# non unique lines surrounded by non-matching lines
976
self.assertDiffBlocks('aBccDe', 'abccde', [(0, 0, 1), (5, 5, 1)])
978
# But they only need to be locally unique
979
self.assertDiffBlocks('aBcDec', 'abcdec', [
980
(0, 0, 1), (2, 2, 1), (4, 4, 2)])
982
# non unique blocks won't be matched
983
self.assertDiffBlocks('aBcdEcdFg', 'abcdecdfg', [(0, 0, 1), (8, 8, 1)])
985
# but locally unique ones will
986
self.assertDiffBlocks('aBcdEeXcdFg', 'abcdecdfg', [(0, 0, 1), (2, 2, 2),
987
(5, 4, 1), (7, 5, 2), (10, 8, 1)])
989
self.assertDiffBlocks('abbabbXd', 'cabbabxd', [(7, 7, 1)])
990
self.assertDiffBlocks('abbabbbb', 'cabbabbc', [])
991
self.assertDiffBlocks('bbbbbbbb', 'cbbbbbbc', [])
993
def test_matching_blocks_tuples(self):
994
# Some basic matching tests
995
self.assertDiffBlocks([], [], [])
996
self.assertDiffBlocks([('a',), ('b',), ('c,')], [], [])
997
self.assertDiffBlocks([], [('a',), ('b',), ('c,')], [])
998
self.assertDiffBlocks([('a',), ('b',), ('c,')],
999
[('a',), ('b',), ('c,')],
1001
self.assertDiffBlocks([('a',), ('b',), ('c,')],
1002
[('a',), ('b',), ('d,')],
1004
self.assertDiffBlocks([('d',), ('b',), ('c,')],
1005
[('a',), ('b',), ('c,')],
1007
self.assertDiffBlocks([('d',), ('a',), ('b',), ('c,')],
1008
[('a',), ('b',), ('c,')],
1010
self.assertDiffBlocks([('a', 'b'), ('c', 'd'), ('e', 'f')],
1011
[('a', 'b'), ('c', 'X'), ('e', 'f')],
1012
[(0, 0, 1), (2, 2, 1)])
1013
self.assertDiffBlocks([('a', 'b'), ('c', 'd'), ('e', 'f')],
1014
[('a', 'b'), ('c', 'dX'), ('e', 'f')],
1015
[(0, 0, 1), (2, 2, 1)])
1017
def test_opcodes(self):
1018
def chk_ops(a, b, expected_codes):
1019
s = self._PatienceSequenceMatcher(None, a, b)
1020
self.assertEqual(expected_codes, s.get_opcodes())
1024
chk_ops('abc', '', [('delete', 0, 3, 0, 0)])
1025
chk_ops('', 'abc', [('insert', 0, 0, 0, 3)])
1026
chk_ops('abcd', 'abcd', [('equal', 0, 4, 0, 4)])
1027
chk_ops('abcd', 'abce', [('equal', 0, 3, 0, 3),
1028
('replace', 3, 4, 3, 4)
1030
chk_ops('eabc', 'abce', [('delete', 0, 1, 0, 0),
1031
('equal', 1, 4, 0, 3),
1032
('insert', 4, 4, 3, 4)
1034
chk_ops('eabce', 'abce', [('delete', 0, 1, 0, 0),
1035
('equal', 1, 5, 0, 4)
1037
chk_ops('abcde', 'abXde', [('equal', 0, 2, 0, 2),
1038
('replace', 2, 3, 2, 3),
1039
('equal', 3, 5, 3, 5)
1041
chk_ops('abcde', 'abXYZde', [('equal', 0, 2, 0, 2),
1042
('replace', 2, 3, 2, 5),
1043
('equal', 3, 5, 5, 7)
1045
chk_ops('abde', 'abXYZde', [('equal', 0, 2, 0, 2),
1046
('insert', 2, 2, 2, 5),
1047
('equal', 2, 4, 5, 7)
1049
chk_ops('abcdefghijklmnop', 'abcdefxydefghijklmnop',
1050
[('equal', 0, 6, 0, 6),
1051
('insert', 6, 6, 6, 11),
1052
('equal', 6, 16, 11, 21)
1055
['hello there\n', 'world\n', 'how are you today?\n'],
1056
['hello there\n', 'how are you today?\n'],
1057
[('equal', 0, 1, 0, 1),
1058
('delete', 1, 2, 1, 1),
1059
('equal', 2, 3, 1, 2),
1061
chk_ops('aBccDe', 'abccde',
1062
[('equal', 0, 1, 0, 1),
1063
('replace', 1, 5, 1, 5),
1064
('equal', 5, 6, 5, 6),
1066
chk_ops('aBcDec', 'abcdec',
1067
[('equal', 0, 1, 0, 1),
1068
('replace', 1, 2, 1, 2),
1069
('equal', 2, 3, 2, 3),
1070
('replace', 3, 4, 3, 4),
1071
('equal', 4, 6, 4, 6),
1073
chk_ops('aBcdEcdFg', 'abcdecdfg',
1074
[('equal', 0, 1, 0, 1),
1075
('replace', 1, 8, 1, 8),
1076
('equal', 8, 9, 8, 9)
1078
chk_ops('aBcdEeXcdFg', 'abcdecdfg',
1079
[('equal', 0, 1, 0, 1),
1080
('replace', 1, 2, 1, 2),
1081
('equal', 2, 4, 2, 4),
1082
('delete', 4, 5, 4, 4),
1083
('equal', 5, 6, 4, 5),
1084
('delete', 6, 7, 5, 5),
1085
('equal', 7, 9, 5, 7),
1086
('replace', 9, 10, 7, 8),
1087
('equal', 10, 11, 8, 9)
1090
def test_grouped_opcodes(self):
1091
def chk_ops(a, b, expected_codes, n=3):
1092
s = self._PatienceSequenceMatcher(None, a, b)
1093
self.assertEqual(expected_codes, list(s.get_grouped_opcodes(n)))
1097
chk_ops('abc', '', [[('delete', 0, 3, 0, 0)]])
1098
chk_ops('', 'abc', [[('insert', 0, 0, 0, 3)]])
1099
chk_ops('abcd', 'abcd', [])
1100
chk_ops('abcd', 'abce', [[('equal', 0, 3, 0, 3),
1101
('replace', 3, 4, 3, 4)
1103
chk_ops('eabc', 'abce', [[('delete', 0, 1, 0, 0),
1104
('equal', 1, 4, 0, 3),
1105
('insert', 4, 4, 3, 4)
1107
chk_ops('abcdefghijklmnop', 'abcdefxydefghijklmnop',
1108
[[('equal', 3, 6, 3, 6),
1109
('insert', 6, 6, 6, 11),
1110
('equal', 6, 9, 11, 14)
1112
chk_ops('abcdefghijklmnop', 'abcdefxydefghijklmnop',
1113
[[('equal', 2, 6, 2, 6),
1114
('insert', 6, 6, 6, 11),
1115
('equal', 6, 10, 11, 15)
1117
chk_ops('Xabcdef', 'abcdef',
1118
[[('delete', 0, 1, 0, 0),
1119
('equal', 1, 4, 0, 3)
1121
chk_ops('abcdef', 'abcdefX',
1122
[[('equal', 3, 6, 3, 6),
1123
('insert', 6, 6, 6, 7)
1126
def test_multiple_ranges(self):
1127
# There was an earlier bug where we used a bad set of ranges,
1128
# this triggers that specific bug, to make sure it doesn't regress
1129
self.assertDiffBlocks('abcdefghijklmnop',
1130
'abcXghiYZQRSTUVWXYZijklmnop',
1131
[(0, 0, 3), (6, 4, 3), (9, 20, 7)])
1133
self.assertDiffBlocks('ABCd efghIjk L',
1134
'AxyzBCn mo pqrstuvwI1 2 L',
1135
[(0, 0, 1), (1, 4, 2), (9, 19, 1), (12, 23, 3)])
1137
# These are rot13 code snippets.
1138
self.assertDiffBlocks('''\
1139
trg nqqrq jura lbh nqq n svyr va gur qverpgbel.
1141
gnxrf_netf = ['svyr*']
1142
gnxrf_bcgvbaf = ['ab-erphefr']
1144
qrs eha(frys, svyr_yvfg, ab_erphefr=Snyfr):
1145
sebz omeyvo.nqq vzcbeg fzneg_nqq, nqq_ercbegre_cevag, nqq_ercbegre_ahyy
1147
ercbegre = nqq_ercbegre_ahyy
1149
ercbegre = nqq_ercbegre_cevag
1150
fzneg_nqq(svyr_yvfg, abg ab_erphefr, ercbegre)
1153
pynff pzq_zxqve(Pbzznaq):
1154
'''.splitlines(True), '''\
1155
trg nqqrq jura lbh nqq n svyr va gur qverpgbel.
1157
--qel-eha jvyy fubj juvpu svyrf jbhyq or nqqrq, ohg abg npghnyyl
1160
gnxrf_netf = ['svyr*']
1161
gnxrf_bcgvbaf = ['ab-erphefr', 'qel-eha']
1163
qrs eha(frys, svyr_yvfg, ab_erphefr=Snyfr, qel_eha=Snyfr):
1168
# Guvf vf cbvagyrff, ohg V'q engure abg envfr na reebe
1169
npgvba = omeyvo.nqq.nqq_npgvba_ahyy
1171
npgvba = omeyvo.nqq.nqq_npgvba_cevag
1173
npgvba = omeyvo.nqq.nqq_npgvba_nqq
1175
npgvba = omeyvo.nqq.nqq_npgvba_nqq_naq_cevag
1177
omeyvo.nqq.fzneg_nqq(svyr_yvfg, abg ab_erphefr, npgvba)
1180
pynff pzq_zxqve(Pbzznaq):
1181
'''.splitlines(True), [(0, 0, 1), (1, 4, 2), (9, 19, 1), (12, 23, 3)])
1183
def test_patience_unified_diff(self):
1184
txt_a = ['hello there\n',
1186
'how are you today?\n']
1187
txt_b = ['hello there\n',
1188
'how are you today?\n']
1189
unified_diff = patiencediff.unified_diff
1190
psm = self._PatienceSequenceMatcher
1191
self.assertEqual(['--- \n',
1193
'@@ -1,3 +1,2 @@\n',
1196
' how are you today?\n'
1197
], list(unified_diff(txt_a, txt_b,
1198
sequencematcher=psm)))
1199
txt_a = [x + '\n' for x in 'abcdefghijklmnop']
1200
txt_b = [x + '\n' for x in 'abcdefxydefghijklmnop']
1201
# This is the result with LongestCommonSubstring matching
1202
self.assertEqual(['--- \n',
1204
'@@ -1,6 +1,11 @@\n',
1215
' f\n'], list(unified_diff(txt_a, txt_b)))
1216
# And the patience diff
1217
self.assertEqual(['--- \n',
1219
'@@ -4,6 +4,11 @@\n',
1231
], list(unified_diff(txt_a, txt_b,
1232
sequencematcher=psm)))
1234
def test_patience_unified_diff_with_dates(self):
1235
txt_a = ['hello there\n',
1237
'how are you today?\n']
1238
txt_b = ['hello there\n',
1239
'how are you today?\n']
1240
unified_diff = patiencediff.unified_diff
1241
psm = self._PatienceSequenceMatcher
1242
self.assertEqual(['--- a\t2008-08-08\n',
1243
'+++ b\t2008-09-09\n',
1244
'@@ -1,3 +1,2 @@\n',
1247
' how are you today?\n'
1248
], list(unified_diff(txt_a, txt_b,
1249
fromfile='a', tofile='b',
1250
fromfiledate='2008-08-08',
1251
tofiledate='2008-09-09',
1252
sequencematcher=psm)))
1255
class TestPatienceDiffLib_c(TestPatienceDiffLib):
1257
_test_needs_features = [features.compiled_patiencediff_feature]
1260
super(TestPatienceDiffLib_c, self).setUp()
1261
from breezy import _patiencediff_c
1262
self._unique_lcs = _patiencediff_c.unique_lcs_c
1263
self._recurse_matches = _patiencediff_c.recurse_matches_c
1264
self._PatienceSequenceMatcher = \
1265
_patiencediff_c.PatienceSequenceMatcher_c
1267
def test_unhashable(self):
1268
"""We should get a proper exception here."""
1269
# We need to be able to hash items in the sequence, lists are
1270
# unhashable, and thus cannot be diffed
1271
e = self.assertRaises(TypeError, self._PatienceSequenceMatcher,
1273
e = self.assertRaises(TypeError, self._PatienceSequenceMatcher,
1274
None, ['valid', []], [])
1275
e = self.assertRaises(TypeError, self._PatienceSequenceMatcher,
1276
None, ['valid'], [[]])
1277
e = self.assertRaises(TypeError, self._PatienceSequenceMatcher,
1278
None, ['valid'], ['valid', []])
1281
class TestPatienceDiffLibFiles(tests.TestCaseInTempDir):
1284
super(TestPatienceDiffLibFiles, self).setUp()
1285
self._PatienceSequenceMatcher = \
1286
_patiencediff_py.PatienceSequenceMatcher_py
1288
def test_patience_unified_diff_files(self):
1289
txt_a = [b'hello there\n',
1291
b'how are you today?\n']
1292
txt_b = [b'hello there\n',
1293
b'how are you today?\n']
1294
with open('a1', 'wb') as f:
1296
with open('b1', 'wb') as f:
1299
unified_diff_files = patiencediff.unified_diff_files
1300
psm = self._PatienceSequenceMatcher
1301
self.assertEqual([b'--- a1\n',
1303
b'@@ -1,3 +1,2 @@\n',
1306
b' how are you today?\n',
1307
], list(unified_diff_files(b'a1', b'b1',
1308
sequencematcher=psm)))
1310
txt_a = [x + '\n' for x in 'abcdefghijklmnop']
1311
txt_b = [x + '\n' for x in 'abcdefxydefghijklmnop']
1312
with open('a2', 'wt') as f:
1314
with open('b2', 'wt') as f:
1317
# This is the result with LongestCommonSubstring matching
1318
self.assertEqual([b'--- a2\n',
1320
b'@@ -1,6 +1,11 @@\n',
1331
b' f\n'], list(unified_diff_files(b'a2', b'b2')))
1333
# And the patience diff
1334
self.assertEqual([b'--- a2\n',
1336
b'@@ -4,6 +4,11 @@\n',
1348
list(unified_diff_files(b'a2', b'b2',
1349
sequencematcher=psm)))
1352
class TestPatienceDiffLibFiles_c(TestPatienceDiffLibFiles):
1354
_test_needs_features = [features.compiled_patiencediff_feature]
1357
super(TestPatienceDiffLibFiles_c, self).setUp()
1358
from breezy import _patiencediff_c
1359
self._PatienceSequenceMatcher = \
1360
_patiencediff_c.PatienceSequenceMatcher_c
1363
class TestUsingCompiledIfAvailable(tests.TestCase):
1365
def test_PatienceSequenceMatcher(self):
1366
if features.compiled_patiencediff_feature.available():
1367
from breezy._patiencediff_c import PatienceSequenceMatcher_c
1368
self.assertIs(PatienceSequenceMatcher_c,
1369
patiencediff.PatienceSequenceMatcher)
1371
from breezy._patiencediff_py import PatienceSequenceMatcher_py
1372
self.assertIs(PatienceSequenceMatcher_py,
1373
patiencediff.PatienceSequenceMatcher)
1375
def test_unique_lcs(self):
1376
if features.compiled_patiencediff_feature.available():
1377
from breezy._patiencediff_c import unique_lcs_c
1378
self.assertIs(unique_lcs_c,
1379
patiencediff.unique_lcs)
1381
from breezy._patiencediff_py import unique_lcs_py
1382
self.assertIs(unique_lcs_py,
1383
patiencediff.unique_lcs)
1385
def test_recurse_matches(self):
1386
if features.compiled_patiencediff_feature.available():
1387
from breezy._patiencediff_c import recurse_matches_c
1388
self.assertIs(recurse_matches_c,
1389
patiencediff.recurse_matches)
1391
from breezy._patiencediff_py import recurse_matches_py
1392
self.assertIs(recurse_matches_py,
1393
patiencediff.recurse_matches)
1396
class TestDiffFromTool(tests.TestCaseWithTransport):
1398
def test_from_string(self):
1399
diff_obj = diff.DiffFromTool.from_string('diff', None, None, None)
1400
self.addCleanup(diff_obj.finish)
1401
self.assertEqual(['diff', '@old_path', '@new_path'],
1402
diff_obj.command_template)
1404
def test_from_string_u5(self):
1405
diff_obj = diff.DiffFromTool.from_string('diff "-u 5"',
1407
self.addCleanup(diff_obj.finish)
1408
self.assertEqual(['diff', '-u 5', '@old_path', '@new_path'],
1409
diff_obj.command_template)
1410
self.assertEqual(['diff', '-u 5', 'old-path', 'new-path'],
1411
diff_obj._get_command('old-path', 'new-path'))
1413
def test_from_string_path_with_backslashes(self):
1414
self.requireFeature(features.backslashdir_feature)
1415
tool = 'C:\\Tools\\Diff.exe'
1416
diff_obj = diff.DiffFromTool.from_string(tool, None, None, None)
1417
self.addCleanup(diff_obj.finish)
1418
self.assertEqual(['C:\\Tools\\Diff.exe', '@old_path', '@new_path'],
1419
diff_obj.command_template)
1420
self.assertEqual(['C:\\Tools\\Diff.exe', 'old-path', 'new-path'],
1421
diff_obj._get_command('old-path', 'new-path'))
1423
def test_execute(self):
1425
diff_obj = diff.DiffFromTool([sys.executable, '-c',
1426
'print("@old_path @new_path")'],
1428
self.addCleanup(diff_obj.finish)
1429
diff_obj._execute('old', 'new')
1430
self.assertEqual(output.getvalue().rstrip(), b'old new')
1432
def test_execute_missing(self):
1433
diff_obj = diff.DiffFromTool(['a-tool-which-is-unlikely-to-exist'],
1435
self.addCleanup(diff_obj.finish)
1436
e = self.assertRaises(errors.ExecutableMissing, diff_obj._execute,
1438
self.assertEqual('a-tool-which-is-unlikely-to-exist could not be found'
1439
' on this machine', str(e))
1441
def test_prepare_files_creates_paths_readable_by_windows_tool(self):
1442
self.requireFeature(features.AttribFeature)
1444
tree = self.make_branch_and_tree('tree')
1445
self.build_tree_contents([('tree/file', b'content')])
1446
tree.add('file', b'file-id')
1447
tree.commit('old tree')
1449
self.addCleanup(tree.unlock)
1450
basis_tree = tree.basis_tree()
1451
basis_tree.lock_read()
1452
self.addCleanup(basis_tree.unlock)
1453
diff_obj = diff.DiffFromTool([sys.executable, '-c',
1454
'print "@old_path @new_path"'],
1455
basis_tree, tree, output)
1456
diff_obj._prepare_files('file', 'file', file_id=b'file-id')
1457
# The old content should be readonly
1458
self.assertReadableByAttrib(diff_obj._root, 'old\\file',
1460
# The new content should use the tree object, not a 'new' file anymore
1461
self.assertEndsWith(tree.basedir, 'work/tree')
1462
self.assertReadableByAttrib(tree.basedir, 'file', r'work\\tree\\file$')
1464
def assertReadableByAttrib(self, cwd, relpath, regex):
1465
proc = subprocess.Popen(['attrib', relpath],
1466
stdout=subprocess.PIPE,
1468
(result, err) = proc.communicate()
1469
self.assertContainsRe(result.replace('\r\n', '\n'), regex)
1471
def test_prepare_files(self):
1473
tree = self.make_branch_and_tree('tree')
1474
self.build_tree_contents([('tree/oldname', b'oldcontent')])
1475
self.build_tree_contents([('tree/oldname2', b'oldcontent2')])
1476
tree.add('oldname', b'file-id')
1477
tree.add('oldname2', b'file2-id')
1478
# Earliest allowable date on FAT32 filesystems is 1980-01-01
1479
tree.commit('old tree', timestamp=315532800)
1480
tree.rename_one('oldname', 'newname')
1481
tree.rename_one('oldname2', 'newname2')
1482
self.build_tree_contents([('tree/newname', b'newcontent')])
1483
self.build_tree_contents([('tree/newname2', b'newcontent2')])
1484
old_tree = tree.basis_tree()
1485
old_tree.lock_read()
1486
self.addCleanup(old_tree.unlock)
1488
self.addCleanup(tree.unlock)
1489
diff_obj = diff.DiffFromTool([sys.executable, '-c',
1490
'print "@old_path @new_path"'],
1491
old_tree, tree, output)
1492
self.addCleanup(diff_obj.finish)
1493
self.assertContainsRe(diff_obj._root, 'brz-diff-[^/]*')
1494
old_path, new_path = diff_obj._prepare_files(
1495
'oldname', 'newname', file_id=b'file-id')
1496
self.assertContainsRe(old_path, 'old/oldname$')
1497
self.assertEqual(315532800, os.stat(old_path).st_mtime)
1498
self.assertContainsRe(new_path, 'tree/newname$')
1499
self.assertFileEqual(b'oldcontent', old_path)
1500
self.assertFileEqual(b'newcontent', new_path)
1501
if osutils.host_os_dereferences_symlinks():
1502
self.assertTrue(os.path.samefile('tree/newname', new_path))
1503
# make sure we can create files with the same parent directories
1504
diff_obj._prepare_files('oldname2', 'newname2', file_id=b'file2-id')
1507
class TestDiffFromToolEncodedFilename(tests.TestCaseWithTransport):
1509
def test_encodable_filename(self):
1510
# Just checks file path for external diff tool.
1511
# We cannot change CPython's internal encoding used by os.exec*.
1512
diffobj = diff.DiffFromTool(['dummy', '@old_path', '@new_path'],
1514
for _, scenario in EncodingAdapter.encoding_scenarios:
1515
encoding = scenario['encoding']
1516
dirname = scenario['info']['directory']
1517
filename = scenario['info']['filename']
1519
self.overrideAttr(diffobj, '_fenc', lambda: encoding)
1520
relpath = dirname + u'/' + filename
1521
fullpath = diffobj._safe_filename('safe', relpath)
1522
self.assertEqual(fullpath,
1523
fullpath.encode(encoding).decode(encoding))
1524
self.assertTrue(fullpath.startswith(diffobj._root + '/safe'))
1526
def test_unencodable_filename(self):
1527
diffobj = diff.DiffFromTool(['dummy', '@old_path', '@new_path'],
1529
for _, scenario in EncodingAdapter.encoding_scenarios:
1530
encoding = scenario['encoding']
1531
dirname = scenario['info']['directory']
1532
filename = scenario['info']['filename']
1534
if encoding == 'iso-8859-1':
1535
encoding = 'iso-8859-2'
1537
encoding = 'iso-8859-1'
1539
self.overrideAttr(diffobj, '_fenc', lambda: encoding)
1540
relpath = dirname + u'/' + filename
1541
fullpath = diffobj._safe_filename('safe', relpath)
1542
self.assertEqual(fullpath,
1543
fullpath.encode(encoding).decode(encoding))
1544
self.assertTrue(fullpath.startswith(diffobj._root + '/safe'))
1547
class TestGetTreesAndBranchesToDiffLocked(tests.TestCaseWithTransport):
1549
def call_gtabtd(self, path_list, revision_specs, old_url, new_url):
1550
"""Call get_trees_and_branches_to_diff_locked."""
1551
return diff.get_trees_and_branches_to_diff_locked(
1552
path_list, revision_specs, old_url, new_url, self.addCleanup)
1554
def test_basic(self):
1555
tree = self.make_branch_and_tree('tree')
1556
(old_tree, new_tree,
1557
old_branch, new_branch,
1558
specific_files, extra_trees) = self.call_gtabtd(
1559
['tree'], None, None, None)
1561
self.assertIsInstance(old_tree, revisiontree.RevisionTree)
1562
self.assertEqual(_mod_revision.NULL_REVISION,
1563
old_tree.get_revision_id())
1564
self.assertEqual(tree.basedir, new_tree.basedir)
1565
self.assertEqual(tree.branch.base, old_branch.base)
1566
self.assertEqual(tree.branch.base, new_branch.base)
1567
self.assertIs(None, specific_files)
1568
self.assertIs(None, extra_trees)
1570
def test_with_rev_specs(self):
1571
tree = self.make_branch_and_tree('tree')
1572
self.build_tree_contents([('tree/file', b'oldcontent')])
1573
tree.add('file', b'file-id')
1574
tree.commit('old tree', timestamp=0, rev_id=b"old-id")
1575
self.build_tree_contents([('tree/file', b'newcontent')])
1576
tree.commit('new tree', timestamp=0, rev_id=b"new-id")
1578
revisions = [revisionspec.RevisionSpec.from_string('1'),
1579
revisionspec.RevisionSpec.from_string('2')]
1580
(old_tree, new_tree,
1581
old_branch, new_branch,
1582
specific_files, extra_trees) = self.call_gtabtd(
1583
['tree'], revisions, None, None)
1585
self.assertIsInstance(old_tree, revisiontree.RevisionTree)
1586
self.assertEqual(b"old-id", old_tree.get_revision_id())
1587
self.assertIsInstance(new_tree, revisiontree.RevisionTree)
1588
self.assertEqual(b"new-id", new_tree.get_revision_id())
1589
self.assertEqual(tree.branch.base, old_branch.base)
1590
self.assertEqual(tree.branch.base, new_branch.base)
1591
self.assertIs(None, specific_files)
1592
self.assertEqual(tree.basedir, extra_trees[0].basedir)