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, 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', 'new label')
733
b'--- old label\n+++ new label\n@@ -1,1 +0,0 @@\n-old\n\n',
734
differ.to_file.getvalue())
735
differ.to_file.seek(0)
736
differ.diff_text(None, 'newdir/newfile',
737
'old label', 'new label')
739
b'--- old label\n+++ new label\n@@ -0,0 +1,1 @@\n+new\n\n',
740
differ.to_file.getvalue())
741
differ.to_file.seek(0)
742
differ.diff_text('olddir/oldfile', 'newdir/newfile',
743
'old label', 'new label')
745
b'--- old label\n+++ new label\n@@ -1,1 +1,1 @@\n-old\n+new\n\n',
746
differ.to_file.getvalue())
748
def test_diff_deletion(self):
749
self.build_tree_contents([('old-tree/file', b'contents'),
750
('new-tree/file', b'contents')])
751
self.old_tree.add('file', b'file-id')
752
self.new_tree.add('file', b'file-id')
753
os.unlink('new-tree/file')
754
self.differ.show_diff(None)
755
self.assertContainsRe(self.differ.to_file.getvalue(), b'-contents')
757
def test_diff_creation(self):
758
self.build_tree_contents([('old-tree/file', b'contents'),
759
('new-tree/file', b'contents')])
760
self.old_tree.add('file', b'file-id')
761
self.new_tree.add('file', b'file-id')
762
os.unlink('old-tree/file')
763
self.differ.show_diff(None)
764
self.assertContainsRe(self.differ.to_file.getvalue(), br'\+contents')
766
def test_diff_symlink(self):
767
differ = diff.DiffSymlink(self.old_tree, self.new_tree, BytesIO())
768
differ.diff_symlink('old target', None)
769
self.assertEqual(b"=== target was 'old target'\n",
770
differ.to_file.getvalue())
772
differ = diff.DiffSymlink(self.old_tree, self.new_tree, BytesIO())
773
differ.diff_symlink(None, 'new target')
774
self.assertEqual(b"=== target is 'new target'\n",
775
differ.to_file.getvalue())
777
differ = diff.DiffSymlink(self.old_tree, self.new_tree, BytesIO())
778
differ.diff_symlink('old target', 'new target')
779
self.assertEqual(b"=== target changed 'old target' => 'new target'\n",
780
differ.to_file.getvalue())
783
self.build_tree_contents([('old-tree/olddir/',),
784
('old-tree/olddir/oldfile', b'old\n')])
785
self.old_tree.add('olddir')
786
self.old_tree.add('olddir/oldfile', b'file-id')
787
self.build_tree_contents([('new-tree/newdir/',),
788
('new-tree/newdir/newfile', b'new\n')])
789
self.new_tree.add('newdir')
790
self.new_tree.add('newdir/newfile', b'file-id')
791
self.differ.diff('olddir/oldfile', 'newdir/newfile')
792
self.assertContainsRe(
793
self.differ.to_file.getvalue(),
794
br'--- olddir/oldfile.*\n\+\+\+ newdir/newfile.*\n\@\@ -1,1 \+1,1'
795
br' \@\@\n-old\n\+new\n\n')
797
def test_diff_kind_change(self):
798
self.requireFeature(features.SymlinkFeature)
799
self.build_tree_contents([('old-tree/olddir/',),
800
('old-tree/olddir/oldfile', b'old\n')])
801
self.old_tree.add('olddir')
802
self.old_tree.add('olddir/oldfile', b'file-id')
803
self.build_tree(['new-tree/newdir/'])
804
os.symlink('new', 'new-tree/newdir/newfile')
805
self.new_tree.add('newdir')
806
self.new_tree.add('newdir/newfile', b'file-id')
807
self.differ.diff('olddir/oldfile', 'newdir/newfile')
808
self.assertContainsRe(
809
self.differ.to_file.getvalue(),
810
br'--- olddir/oldfile.*\n\+\+\+ newdir/newfile.*\n\@\@ -1,1 \+0,0'
812
self.assertContainsRe(self.differ.to_file.getvalue(),
813
b"=== target is 'new'\n")
815
def test_diff_directory(self):
816
self.build_tree(['new-tree/new-dir/'])
817
self.new_tree.add('new-dir', b'new-dir-id')
818
self.differ.diff(None, 'new-dir')
819
self.assertEqual(self.differ.to_file.getvalue(), b'')
821
def create_old_new(self):
822
self.build_tree_contents([('old-tree/olddir/',),
823
('old-tree/olddir/oldfile', b'old\n')])
824
self.old_tree.add('olddir')
825
self.old_tree.add('olddir/oldfile', b'file-id')
826
self.build_tree_contents([('new-tree/newdir/',),
827
('new-tree/newdir/newfile', b'new\n')])
828
self.new_tree.add('newdir')
829
self.new_tree.add('newdir/newfile', b'file-id')
831
def test_register_diff(self):
832
self.create_old_new()
833
old_diff_factories = diff.DiffTree.diff_factories
834
diff.DiffTree.diff_factories = old_diff_factories[:]
835
diff.DiffTree.diff_factories.insert(0, DiffWasIs.from_diff_tree)
837
differ = diff.DiffTree(self.old_tree, self.new_tree, BytesIO())
839
diff.DiffTree.diff_factories = old_diff_factories
840
differ.diff('olddir/oldfile', 'newdir/newfile')
841
self.assertNotContainsRe(
842
differ.to_file.getvalue(),
843
br'--- olddir/oldfile.*\n\+\+\+ newdir/newfile.*\n\@\@ -1,1 \+1,1'
844
br' \@\@\n-old\n\+new\n\n')
845
self.assertContainsRe(differ.to_file.getvalue(),
846
b'was: old\nis: new\n')
848
def test_extra_factories(self):
849
self.create_old_new()
850
differ = diff.DiffTree(self.old_tree, self.new_tree, BytesIO(),
851
extra_factories=[DiffWasIs.from_diff_tree])
852
differ.diff('olddir/oldfile', 'newdir/newfile')
853
self.assertNotContainsRe(
854
differ.to_file.getvalue(),
855
br'--- olddir/oldfile.*\n\+\+\+ newdir/newfile.*\n\@\@ -1,1 \+1,1'
856
br' \@\@\n-old\n\+new\n\n')
857
self.assertContainsRe(differ.to_file.getvalue(),
858
b'was: old\nis: new\n')
860
def test_alphabetical_order(self):
861
self.build_tree(['new-tree/a-file'])
862
self.new_tree.add('a-file')
863
self.build_tree(['old-tree/b-file'])
864
self.old_tree.add('b-file')
865
self.differ.show_diff(None)
866
self.assertContainsRe(self.differ.to_file.getvalue(),
867
b'.*a-file(.|\n)*b-file')
870
class TestPatienceDiffLib(tests.TestCase):
873
super(TestPatienceDiffLib, self).setUp()
874
self._unique_lcs = _patiencediff_py.unique_lcs_py
875
self._recurse_matches = _patiencediff_py.recurse_matches_py
876
self._PatienceSequenceMatcher = \
877
_patiencediff_py.PatienceSequenceMatcher_py
879
def test_diff_unicode_string(self):
880
a = ''.join([unichr(i) for i in range(4000, 4500, 3)])
881
b = ''.join([unichr(i) for i in range(4300, 4800, 2)])
882
sm = self._PatienceSequenceMatcher(None, a, b)
883
mb = sm.get_matching_blocks()
884
self.assertEqual(35, len(mb))
886
def test_unique_lcs(self):
887
unique_lcs = self._unique_lcs
888
self.assertEqual(unique_lcs('', ''), [])
889
self.assertEqual(unique_lcs('', 'a'), [])
890
self.assertEqual(unique_lcs('a', ''), [])
891
self.assertEqual(unique_lcs('a', 'a'), [(0, 0)])
892
self.assertEqual(unique_lcs('a', 'b'), [])
893
self.assertEqual(unique_lcs('ab', 'ab'), [(0, 0), (1, 1)])
894
self.assertEqual(unique_lcs('abcde', 'cdeab'),
895
[(2, 0), (3, 1), (4, 2)])
896
self.assertEqual(unique_lcs('cdeab', 'abcde'),
897
[(0, 2), (1, 3), (2, 4)])
898
self.assertEqual(unique_lcs('abXde', 'abYde'), [(0, 0), (1, 1),
900
self.assertEqual(unique_lcs('acbac', 'abc'), [(2, 1)])
902
def test_recurse_matches(self):
903
def test_one(a, b, matches):
905
self._recurse_matches(
906
a, b, 0, 0, len(a), len(b), test_matches, 10)
907
self.assertEqual(test_matches, matches)
909
test_one(['a', '', 'b', '', 'c'], ['a', 'a', 'b', 'c', 'c'],
910
[(0, 0), (2, 2), (4, 4)])
911
test_one(['a', 'c', 'b', 'a', 'c'], ['a', 'b', 'c'],
912
[(0, 0), (2, 1), (4, 2)])
913
# Even though 'bc' is not unique globally, and is surrounded by
914
# non-matching lines, we should still match, because they are locally
916
test_one('abcdbce', 'afbcgdbce', [(0, 0), (1, 2), (2, 3), (3, 5),
917
(4, 6), (5, 7), (6, 8)])
919
# recurse_matches doesn't match non-unique
920
# lines surrounded by bogus text.
921
# The update has been done in patiencediff.SequenceMatcher instead
923
# This is what it could be
924
#test_one('aBccDe', 'abccde', [(0,0), (2,2), (3,3), (5,5)])
926
# This is what it currently gives:
927
test_one('aBccDe', 'abccde', [(0, 0), (5, 5)])
929
def assertDiffBlocks(self, a, b, expected_blocks):
930
"""Check that the sequence matcher returns the correct blocks.
932
:param a: A sequence to match
933
:param b: Another sequence to match
934
:param expected_blocks: The expected output, not including the final
935
matching block (len(a), len(b), 0)
937
matcher = self._PatienceSequenceMatcher(None, a, b)
938
blocks = matcher.get_matching_blocks()
940
self.assertEqual((len(a), len(b), 0), last)
941
self.assertEqual(expected_blocks, blocks)
943
def test_matching_blocks(self):
944
# Some basic matching tests
945
self.assertDiffBlocks('', '', [])
946
self.assertDiffBlocks([], [], [])
947
self.assertDiffBlocks('abc', '', [])
948
self.assertDiffBlocks('', 'abc', [])
949
self.assertDiffBlocks('abcd', 'abcd', [(0, 0, 4)])
950
self.assertDiffBlocks('abcd', 'abce', [(0, 0, 3)])
951
self.assertDiffBlocks('eabc', 'abce', [(1, 0, 3)])
952
self.assertDiffBlocks('eabce', 'abce', [(1, 0, 4)])
953
self.assertDiffBlocks('abcde', 'abXde', [(0, 0, 2), (3, 3, 2)])
954
self.assertDiffBlocks('abcde', 'abXYZde', [(0, 0, 2), (3, 5, 2)])
955
self.assertDiffBlocks('abde', 'abXYZde', [(0, 0, 2), (2, 5, 2)])
956
# This may check too much, but it checks to see that
957
# a copied block stays attached to the previous section,
959
# difflib would tend to grab the trailing longest match
960
# which would make the diff not look right
961
self.assertDiffBlocks('abcdefghijklmnop', 'abcdefxydefghijklmnop',
962
[(0, 0, 6), (6, 11, 10)])
964
# make sure it supports passing in lists
965
self.assertDiffBlocks(
968
'how are you today?\n'],
970
'how are you today?\n'],
971
[(0, 0, 1), (2, 1, 1)])
973
# non unique lines surrounded by non-matching lines
975
self.assertDiffBlocks('aBccDe', 'abccde', [(0, 0, 1), (5, 5, 1)])
977
# But they only need to be locally unique
978
self.assertDiffBlocks('aBcDec', 'abcdec', [
979
(0, 0, 1), (2, 2, 1), (4, 4, 2)])
981
# non unique blocks won't be matched
982
self.assertDiffBlocks('aBcdEcdFg', 'abcdecdfg', [(0, 0, 1), (8, 8, 1)])
984
# but locally unique ones will
985
self.assertDiffBlocks('aBcdEeXcdFg', 'abcdecdfg', [(0, 0, 1), (2, 2, 2),
986
(5, 4, 1), (7, 5, 2), (10, 8, 1)])
988
self.assertDiffBlocks('abbabbXd', 'cabbabxd', [(7, 7, 1)])
989
self.assertDiffBlocks('abbabbbb', 'cabbabbc', [])
990
self.assertDiffBlocks('bbbbbbbb', 'cbbbbbbc', [])
992
def test_matching_blocks_tuples(self):
993
# Some basic matching tests
994
self.assertDiffBlocks([], [], [])
995
self.assertDiffBlocks([('a',), ('b',), ('c,')], [], [])
996
self.assertDiffBlocks([], [('a',), ('b',), ('c,')], [])
997
self.assertDiffBlocks([('a',), ('b',), ('c,')],
998
[('a',), ('b',), ('c,')],
1000
self.assertDiffBlocks([('a',), ('b',), ('c,')],
1001
[('a',), ('b',), ('d,')],
1003
self.assertDiffBlocks([('d',), ('b',), ('c,')],
1004
[('a',), ('b',), ('c,')],
1006
self.assertDiffBlocks([('d',), ('a',), ('b',), ('c,')],
1007
[('a',), ('b',), ('c,')],
1009
self.assertDiffBlocks([('a', 'b'), ('c', 'd'), ('e', 'f')],
1010
[('a', 'b'), ('c', 'X'), ('e', 'f')],
1011
[(0, 0, 1), (2, 2, 1)])
1012
self.assertDiffBlocks([('a', 'b'), ('c', 'd'), ('e', 'f')],
1013
[('a', 'b'), ('c', 'dX'), ('e', 'f')],
1014
[(0, 0, 1), (2, 2, 1)])
1016
def test_opcodes(self):
1017
def chk_ops(a, b, expected_codes):
1018
s = self._PatienceSequenceMatcher(None, a, b)
1019
self.assertEqual(expected_codes, s.get_opcodes())
1023
chk_ops('abc', '', [('delete', 0, 3, 0, 0)])
1024
chk_ops('', 'abc', [('insert', 0, 0, 0, 3)])
1025
chk_ops('abcd', 'abcd', [('equal', 0, 4, 0, 4)])
1026
chk_ops('abcd', 'abce', [('equal', 0, 3, 0, 3),
1027
('replace', 3, 4, 3, 4)
1029
chk_ops('eabc', 'abce', [('delete', 0, 1, 0, 0),
1030
('equal', 1, 4, 0, 3),
1031
('insert', 4, 4, 3, 4)
1033
chk_ops('eabce', 'abce', [('delete', 0, 1, 0, 0),
1034
('equal', 1, 5, 0, 4)
1036
chk_ops('abcde', 'abXde', [('equal', 0, 2, 0, 2),
1037
('replace', 2, 3, 2, 3),
1038
('equal', 3, 5, 3, 5)
1040
chk_ops('abcde', 'abXYZde', [('equal', 0, 2, 0, 2),
1041
('replace', 2, 3, 2, 5),
1042
('equal', 3, 5, 5, 7)
1044
chk_ops('abde', 'abXYZde', [('equal', 0, 2, 0, 2),
1045
('insert', 2, 2, 2, 5),
1046
('equal', 2, 4, 5, 7)
1048
chk_ops('abcdefghijklmnop', 'abcdefxydefghijklmnop',
1049
[('equal', 0, 6, 0, 6),
1050
('insert', 6, 6, 6, 11),
1051
('equal', 6, 16, 11, 21)
1054
['hello there\n', 'world\n', 'how are you today?\n'],
1055
['hello there\n', 'how are you today?\n'],
1056
[('equal', 0, 1, 0, 1),
1057
('delete', 1, 2, 1, 1),
1058
('equal', 2, 3, 1, 2),
1060
chk_ops('aBccDe', 'abccde',
1061
[('equal', 0, 1, 0, 1),
1062
('replace', 1, 5, 1, 5),
1063
('equal', 5, 6, 5, 6),
1065
chk_ops('aBcDec', 'abcdec',
1066
[('equal', 0, 1, 0, 1),
1067
('replace', 1, 2, 1, 2),
1068
('equal', 2, 3, 2, 3),
1069
('replace', 3, 4, 3, 4),
1070
('equal', 4, 6, 4, 6),
1072
chk_ops('aBcdEcdFg', 'abcdecdfg',
1073
[('equal', 0, 1, 0, 1),
1074
('replace', 1, 8, 1, 8),
1075
('equal', 8, 9, 8, 9)
1077
chk_ops('aBcdEeXcdFg', 'abcdecdfg',
1078
[('equal', 0, 1, 0, 1),
1079
('replace', 1, 2, 1, 2),
1080
('equal', 2, 4, 2, 4),
1081
('delete', 4, 5, 4, 4),
1082
('equal', 5, 6, 4, 5),
1083
('delete', 6, 7, 5, 5),
1084
('equal', 7, 9, 5, 7),
1085
('replace', 9, 10, 7, 8),
1086
('equal', 10, 11, 8, 9)
1089
def test_grouped_opcodes(self):
1090
def chk_ops(a, b, expected_codes, n=3):
1091
s = self._PatienceSequenceMatcher(None, a, b)
1092
self.assertEqual(expected_codes, list(s.get_grouped_opcodes(n)))
1096
chk_ops('abc', '', [[('delete', 0, 3, 0, 0)]])
1097
chk_ops('', 'abc', [[('insert', 0, 0, 0, 3)]])
1098
chk_ops('abcd', 'abcd', [])
1099
chk_ops('abcd', 'abce', [[('equal', 0, 3, 0, 3),
1100
('replace', 3, 4, 3, 4)
1102
chk_ops('eabc', 'abce', [[('delete', 0, 1, 0, 0),
1103
('equal', 1, 4, 0, 3),
1104
('insert', 4, 4, 3, 4)
1106
chk_ops('abcdefghijklmnop', 'abcdefxydefghijklmnop',
1107
[[('equal', 3, 6, 3, 6),
1108
('insert', 6, 6, 6, 11),
1109
('equal', 6, 9, 11, 14)
1111
chk_ops('abcdefghijklmnop', 'abcdefxydefghijklmnop',
1112
[[('equal', 2, 6, 2, 6),
1113
('insert', 6, 6, 6, 11),
1114
('equal', 6, 10, 11, 15)
1116
chk_ops('Xabcdef', 'abcdef',
1117
[[('delete', 0, 1, 0, 0),
1118
('equal', 1, 4, 0, 3)
1120
chk_ops('abcdef', 'abcdefX',
1121
[[('equal', 3, 6, 3, 6),
1122
('insert', 6, 6, 6, 7)
1125
def test_multiple_ranges(self):
1126
# There was an earlier bug where we used a bad set of ranges,
1127
# this triggers that specific bug, to make sure it doesn't regress
1128
self.assertDiffBlocks('abcdefghijklmnop',
1129
'abcXghiYZQRSTUVWXYZijklmnop',
1130
[(0, 0, 3), (6, 4, 3), (9, 20, 7)])
1132
self.assertDiffBlocks('ABCd efghIjk L',
1133
'AxyzBCn mo pqrstuvwI1 2 L',
1134
[(0, 0, 1), (1, 4, 2), (9, 19, 1), (12, 23, 3)])
1136
# These are rot13 code snippets.
1137
self.assertDiffBlocks('''\
1138
trg nqqrq jura lbh nqq n svyr va gur qverpgbel.
1140
gnxrf_netf = ['svyr*']
1141
gnxrf_bcgvbaf = ['ab-erphefr']
1143
qrs eha(frys, svyr_yvfg, ab_erphefr=Snyfr):
1144
sebz omeyvo.nqq vzcbeg fzneg_nqq, nqq_ercbegre_cevag, nqq_ercbegre_ahyy
1146
ercbegre = nqq_ercbegre_ahyy
1148
ercbegre = nqq_ercbegre_cevag
1149
fzneg_nqq(svyr_yvfg, abg ab_erphefr, ercbegre)
1152
pynff pzq_zxqve(Pbzznaq):
1153
'''.splitlines(True), '''\
1154
trg nqqrq jura lbh nqq n svyr va gur qverpgbel.
1156
--qel-eha jvyy fubj juvpu svyrf jbhyq or nqqrq, ohg abg npghnyyl
1159
gnxrf_netf = ['svyr*']
1160
gnxrf_bcgvbaf = ['ab-erphefr', 'qel-eha']
1162
qrs eha(frys, svyr_yvfg, ab_erphefr=Snyfr, qel_eha=Snyfr):
1167
# Guvf vf cbvagyrff, ohg V'q engure abg envfr na reebe
1168
npgvba = omeyvo.nqq.nqq_npgvba_ahyy
1170
npgvba = omeyvo.nqq.nqq_npgvba_cevag
1172
npgvba = omeyvo.nqq.nqq_npgvba_nqq
1174
npgvba = omeyvo.nqq.nqq_npgvba_nqq_naq_cevag
1176
omeyvo.nqq.fzneg_nqq(svyr_yvfg, abg ab_erphefr, npgvba)
1179
pynff pzq_zxqve(Pbzznaq):
1180
'''.splitlines(True), [(0, 0, 1), (1, 4, 2), (9, 19, 1), (12, 23, 3)])
1182
def test_patience_unified_diff(self):
1183
txt_a = ['hello there\n',
1185
'how are you today?\n']
1186
txt_b = ['hello there\n',
1187
'how are you today?\n']
1188
unified_diff = patiencediff.unified_diff
1189
psm = self._PatienceSequenceMatcher
1190
self.assertEqual(['--- \n',
1192
'@@ -1,3 +1,2 @@\n',
1195
' how are you today?\n'
1196
], list(unified_diff(txt_a, txt_b,
1197
sequencematcher=psm)))
1198
txt_a = [x + '\n' for x in 'abcdefghijklmnop']
1199
txt_b = [x + '\n' for x in 'abcdefxydefghijklmnop']
1200
# This is the result with LongestCommonSubstring matching
1201
self.assertEqual(['--- \n',
1203
'@@ -1,6 +1,11 @@\n',
1214
' f\n'], list(unified_diff(txt_a, txt_b)))
1215
# And the patience diff
1216
self.assertEqual(['--- \n',
1218
'@@ -4,6 +4,11 @@\n',
1230
], list(unified_diff(txt_a, txt_b,
1231
sequencematcher=psm)))
1233
def test_patience_unified_diff_with_dates(self):
1234
txt_a = ['hello there\n',
1236
'how are you today?\n']
1237
txt_b = ['hello there\n',
1238
'how are you today?\n']
1239
unified_diff = patiencediff.unified_diff
1240
psm = self._PatienceSequenceMatcher
1241
self.assertEqual(['--- a\t2008-08-08\n',
1242
'+++ b\t2008-09-09\n',
1243
'@@ -1,3 +1,2 @@\n',
1246
' how are you today?\n'
1247
], list(unified_diff(txt_a, txt_b,
1248
fromfile='a', tofile='b',
1249
fromfiledate='2008-08-08',
1250
tofiledate='2008-09-09',
1251
sequencematcher=psm)))
1254
class TestPatienceDiffLib_c(TestPatienceDiffLib):
1256
_test_needs_features = [features.compiled_patiencediff_feature]
1259
super(TestPatienceDiffLib_c, self).setUp()
1260
from breezy import _patiencediff_c
1261
self._unique_lcs = _patiencediff_c.unique_lcs_c
1262
self._recurse_matches = _patiencediff_c.recurse_matches_c
1263
self._PatienceSequenceMatcher = \
1264
_patiencediff_c.PatienceSequenceMatcher_c
1266
def test_unhashable(self):
1267
"""We should get a proper exception here."""
1268
# We need to be able to hash items in the sequence, lists are
1269
# unhashable, and thus cannot be diffed
1270
e = self.assertRaises(TypeError, self._PatienceSequenceMatcher,
1272
e = self.assertRaises(TypeError, self._PatienceSequenceMatcher,
1273
None, ['valid', []], [])
1274
e = self.assertRaises(TypeError, self._PatienceSequenceMatcher,
1275
None, ['valid'], [[]])
1276
e = self.assertRaises(TypeError, self._PatienceSequenceMatcher,
1277
None, ['valid'], ['valid', []])
1280
class TestPatienceDiffLibFiles(tests.TestCaseInTempDir):
1283
super(TestPatienceDiffLibFiles, self).setUp()
1284
self._PatienceSequenceMatcher = \
1285
_patiencediff_py.PatienceSequenceMatcher_py
1287
def test_patience_unified_diff_files(self):
1288
txt_a = [b'hello there\n',
1290
b'how are you today?\n']
1291
txt_b = [b'hello there\n',
1292
b'how are you today?\n']
1293
with open('a1', 'wb') as f:
1295
with open('b1', 'wb') as f:
1298
unified_diff_files = patiencediff.unified_diff_files
1299
psm = self._PatienceSequenceMatcher
1300
self.assertEqual([b'--- a1\n',
1302
b'@@ -1,3 +1,2 @@\n',
1305
b' how are you today?\n',
1306
], list(unified_diff_files(b'a1', b'b1',
1307
sequencematcher=psm)))
1309
txt_a = [x + '\n' for x in 'abcdefghijklmnop']
1310
txt_b = [x + '\n' for x in 'abcdefxydefghijklmnop']
1311
with open('a2', 'wt') as f:
1313
with open('b2', 'wt') as f:
1316
# This is the result with LongestCommonSubstring matching
1317
self.assertEqual([b'--- a2\n',
1319
b'@@ -1,6 +1,11 @@\n',
1330
b' f\n'], list(unified_diff_files(b'a2', b'b2')))
1332
# And the patience diff
1333
self.assertEqual([b'--- a2\n',
1335
b'@@ -4,6 +4,11 @@\n',
1347
list(unified_diff_files(b'a2', b'b2',
1348
sequencematcher=psm)))
1351
class TestPatienceDiffLibFiles_c(TestPatienceDiffLibFiles):
1353
_test_needs_features = [features.compiled_patiencediff_feature]
1356
super(TestPatienceDiffLibFiles_c, self).setUp()
1357
from breezy import _patiencediff_c
1358
self._PatienceSequenceMatcher = \
1359
_patiencediff_c.PatienceSequenceMatcher_c
1362
class TestUsingCompiledIfAvailable(tests.TestCase):
1364
def test_PatienceSequenceMatcher(self):
1365
if features.compiled_patiencediff_feature.available():
1366
from breezy._patiencediff_c import PatienceSequenceMatcher_c
1367
self.assertIs(PatienceSequenceMatcher_c,
1368
patiencediff.PatienceSequenceMatcher)
1370
from breezy._patiencediff_py import PatienceSequenceMatcher_py
1371
self.assertIs(PatienceSequenceMatcher_py,
1372
patiencediff.PatienceSequenceMatcher)
1374
def test_unique_lcs(self):
1375
if features.compiled_patiencediff_feature.available():
1376
from breezy._patiencediff_c import unique_lcs_c
1377
self.assertIs(unique_lcs_c,
1378
patiencediff.unique_lcs)
1380
from breezy._patiencediff_py import unique_lcs_py
1381
self.assertIs(unique_lcs_py,
1382
patiencediff.unique_lcs)
1384
def test_recurse_matches(self):
1385
if features.compiled_patiencediff_feature.available():
1386
from breezy._patiencediff_c import recurse_matches_c
1387
self.assertIs(recurse_matches_c,
1388
patiencediff.recurse_matches)
1390
from breezy._patiencediff_py import recurse_matches_py
1391
self.assertIs(recurse_matches_py,
1392
patiencediff.recurse_matches)
1395
class TestDiffFromTool(tests.TestCaseWithTransport):
1397
def test_from_string(self):
1398
diff_obj = diff.DiffFromTool.from_string('diff', None, None, None)
1399
self.addCleanup(diff_obj.finish)
1400
self.assertEqual(['diff', '@old_path', '@new_path'],
1401
diff_obj.command_template)
1403
def test_from_string_u5(self):
1404
diff_obj = diff.DiffFromTool.from_string('diff "-u 5"',
1406
self.addCleanup(diff_obj.finish)
1407
self.assertEqual(['diff', '-u 5', '@old_path', '@new_path'],
1408
diff_obj.command_template)
1409
self.assertEqual(['diff', '-u 5', 'old-path', 'new-path'],
1410
diff_obj._get_command('old-path', 'new-path'))
1412
def test_from_string_path_with_backslashes(self):
1413
self.requireFeature(features.backslashdir_feature)
1414
tool = 'C:\\Tools\\Diff.exe'
1415
diff_obj = diff.DiffFromTool.from_string(tool, None, None, None)
1416
self.addCleanup(diff_obj.finish)
1417
self.assertEqual(['C:\\Tools\\Diff.exe', '@old_path', '@new_path'],
1418
diff_obj.command_template)
1419
self.assertEqual(['C:\\Tools\\Diff.exe', 'old-path', 'new-path'],
1420
diff_obj._get_command('old-path', 'new-path'))
1422
def test_execute(self):
1424
diff_obj = diff.DiffFromTool([sys.executable, '-c',
1425
'print("@old_path @new_path")'],
1427
self.addCleanup(diff_obj.finish)
1428
diff_obj._execute('old', 'new')
1429
self.assertEqual(output.getvalue().rstrip(), b'old new')
1431
def test_execute_missing(self):
1432
diff_obj = diff.DiffFromTool(['a-tool-which-is-unlikely-to-exist'],
1434
self.addCleanup(diff_obj.finish)
1435
e = self.assertRaises(errors.ExecutableMissing, diff_obj._execute,
1437
self.assertEqual('a-tool-which-is-unlikely-to-exist could not be found'
1438
' on this machine', str(e))
1440
def test_prepare_files_creates_paths_readable_by_windows_tool(self):
1441
self.requireFeature(features.AttribFeature)
1443
tree = self.make_branch_and_tree('tree')
1444
self.build_tree_contents([('tree/file', b'content')])
1445
tree.add('file', b'file-id')
1446
tree.commit('old tree')
1448
self.addCleanup(tree.unlock)
1449
basis_tree = tree.basis_tree()
1450
basis_tree.lock_read()
1451
self.addCleanup(basis_tree.unlock)
1452
diff_obj = diff.DiffFromTool([sys.executable, '-c',
1453
'print "@old_path @new_path"'],
1454
basis_tree, tree, output)
1455
diff_obj._prepare_files('file', 'file', file_id=b'file-id')
1456
# The old content should be readonly
1457
self.assertReadableByAttrib(diff_obj._root, 'old\\file',
1459
# The new content should use the tree object, not a 'new' file anymore
1460
self.assertEndsWith(tree.basedir, 'work/tree')
1461
self.assertReadableByAttrib(tree.basedir, 'file', r'work\\tree\\file$')
1463
def assertReadableByAttrib(self, cwd, relpath, regex):
1464
proc = subprocess.Popen(['attrib', relpath],
1465
stdout=subprocess.PIPE,
1467
(result, err) = proc.communicate()
1468
self.assertContainsRe(result.replace('\r\n', '\n'), regex)
1470
def test_prepare_files(self):
1472
tree = self.make_branch_and_tree('tree')
1473
self.build_tree_contents([('tree/oldname', b'oldcontent')])
1474
self.build_tree_contents([('tree/oldname2', b'oldcontent2')])
1475
tree.add('oldname', b'file-id')
1476
tree.add('oldname2', b'file2-id')
1477
# Earliest allowable date on FAT32 filesystems is 1980-01-01
1478
tree.commit('old tree', timestamp=315532800)
1479
tree.rename_one('oldname', 'newname')
1480
tree.rename_one('oldname2', 'newname2')
1481
self.build_tree_contents([('tree/newname', b'newcontent')])
1482
self.build_tree_contents([('tree/newname2', b'newcontent2')])
1483
old_tree = tree.basis_tree()
1484
old_tree.lock_read()
1485
self.addCleanup(old_tree.unlock)
1487
self.addCleanup(tree.unlock)
1488
diff_obj = diff.DiffFromTool([sys.executable, '-c',
1489
'print "@old_path @new_path"'],
1490
old_tree, tree, output)
1491
self.addCleanup(diff_obj.finish)
1492
self.assertContainsRe(diff_obj._root, 'brz-diff-[^/]*')
1493
old_path, new_path = diff_obj._prepare_files(
1494
'oldname', 'newname')
1495
self.assertContainsRe(old_path, 'old/oldname$')
1496
self.assertEqual(315532800, os.stat(old_path).st_mtime)
1497
self.assertContainsRe(new_path, 'tree/newname$')
1498
self.assertFileEqual(b'oldcontent', old_path)
1499
self.assertFileEqual(b'newcontent', new_path)
1500
if osutils.host_os_dereferences_symlinks():
1501
self.assertTrue(os.path.samefile('tree/newname', new_path))
1502
# make sure we can create files with the same parent directories
1503
diff_obj._prepare_files('oldname2', 'newname2')
1506
class TestDiffFromToolEncodedFilename(tests.TestCaseWithTransport):
1508
def test_encodable_filename(self):
1509
# Just checks file path for external diff tool.
1510
# We cannot change CPython's internal encoding used by os.exec*.
1511
diffobj = diff.DiffFromTool(['dummy', '@old_path', '@new_path'],
1513
for _, scenario in EncodingAdapter.encoding_scenarios:
1514
encoding = scenario['encoding']
1515
dirname = scenario['info']['directory']
1516
filename = scenario['info']['filename']
1518
self.overrideAttr(diffobj, '_fenc', lambda: encoding)
1519
relpath = dirname + u'/' + filename
1520
fullpath = diffobj._safe_filename('safe', relpath)
1521
self.assertEqual(fullpath,
1522
fullpath.encode(encoding).decode(encoding))
1523
self.assertTrue(fullpath.startswith(diffobj._root + '/safe'))
1525
def test_unencodable_filename(self):
1526
diffobj = diff.DiffFromTool(['dummy', '@old_path', '@new_path'],
1528
for _, scenario in EncodingAdapter.encoding_scenarios:
1529
encoding = scenario['encoding']
1530
dirname = scenario['info']['directory']
1531
filename = scenario['info']['filename']
1533
if encoding == 'iso-8859-1':
1534
encoding = 'iso-8859-2'
1536
encoding = 'iso-8859-1'
1538
self.overrideAttr(diffobj, '_fenc', lambda: encoding)
1539
relpath = dirname + u'/' + filename
1540
fullpath = diffobj._safe_filename('safe', relpath)
1541
self.assertEqual(fullpath,
1542
fullpath.encode(encoding).decode(encoding))
1543
self.assertTrue(fullpath.startswith(diffobj._root + '/safe'))
1546
class TestGetTreesAndBranchesToDiffLocked(tests.TestCaseWithTransport):
1548
def call_gtabtd(self, path_list, revision_specs, old_url, new_url):
1549
"""Call get_trees_and_branches_to_diff_locked."""
1550
return diff.get_trees_and_branches_to_diff_locked(
1551
path_list, revision_specs, old_url, new_url, self.addCleanup)
1553
def test_basic(self):
1554
tree = self.make_branch_and_tree('tree')
1555
(old_tree, new_tree,
1556
old_branch, new_branch,
1557
specific_files, extra_trees) = self.call_gtabtd(
1558
['tree'], None, None, None)
1560
self.assertIsInstance(old_tree, revisiontree.RevisionTree)
1561
self.assertEqual(_mod_revision.NULL_REVISION,
1562
old_tree.get_revision_id())
1563
self.assertEqual(tree.basedir, new_tree.basedir)
1564
self.assertEqual(tree.branch.base, old_branch.base)
1565
self.assertEqual(tree.branch.base, new_branch.base)
1566
self.assertIs(None, specific_files)
1567
self.assertIs(None, extra_trees)
1569
def test_with_rev_specs(self):
1570
tree = self.make_branch_and_tree('tree')
1571
self.build_tree_contents([('tree/file', b'oldcontent')])
1572
tree.add('file', b'file-id')
1573
tree.commit('old tree', timestamp=0, rev_id=b"old-id")
1574
self.build_tree_contents([('tree/file', b'newcontent')])
1575
tree.commit('new tree', timestamp=0, rev_id=b"new-id")
1577
revisions = [revisionspec.RevisionSpec.from_string('1'),
1578
revisionspec.RevisionSpec.from_string('2')]
1579
(old_tree, new_tree,
1580
old_branch, new_branch,
1581
specific_files, extra_trees) = self.call_gtabtd(
1582
['tree'], revisions, None, None)
1584
self.assertIsInstance(old_tree, revisiontree.RevisionTree)
1585
self.assertEqual(b"old-id", old_tree.get_revision_id())
1586
self.assertIsInstance(new_tree, revisiontree.RevisionTree)
1587
self.assertEqual(b"new-id", new_tree.get_revision_id())
1588
self.assertEqual(tree.branch.base, old_branch.base)
1589
self.assertEqual(tree.branch.base, new_branch.base)
1590
self.assertIs(None, specific_files)
1591
self.assertEqual(tree.basedir, extra_trees[0].basedir)