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
27
revision as _mod_revision,
33
from ..sixish import (
40
from ..tests.blackbox.test_diff import subst_dates
41
from ..tests.scenarios import load_tests_apply_scenarios
44
load_tests = load_tests_apply_scenarios
47
def udiff_lines(old, new, allow_binary=False):
49
diff.internal_diff('old', old, 'new', new, output, allow_binary)
51
return output.readlines()
54
def external_udiff_lines(old, new, use_stringio=False):
56
# BytesIO has no fileno, so it tests a different codepath
59
output = tempfile.TemporaryFile()
61
diff.external_diff('old', old, 'new', new, output, diff_opts=['-u'])
63
raise tests.TestSkipped('external "diff" not present to test')
65
lines = output.readlines()
71
"""Simple file-like object that allows writes with any type and records."""
74
self.write_record = []
76
def write(self, data):
77
self.write_record.append(data)
79
def check_types(self, testcase, expected_type):
81
any(not isinstance(o, expected_type) for o in self.write_record),
82
"Not all writes of type %s: %r" % (
83
expected_type.__name__, self.write_record))
86
class TestDiffOptions(tests.TestCase):
88
def test_unified_added(self):
89
"""Check for default style '-u' only if no other style specified
92
# Verify that style defaults to unified, id est '-u' appended
93
# to option list, in the absence of an alternative style.
94
self.assertEqual(['-a', '-u'], diff.default_style_unified(['-a']))
97
class TestDiffOptionsScenarios(tests.TestCase):
99
scenarios = [(s, dict(style=s)) for s in diff.style_option_list]
100
style = None # Set by load_tests_apply_scenarios from scenarios
102
def test_unified_not_added(self):
103
# Verify that for all valid style options, '-u' is not
104
# appended to option list.
105
ret_opts = diff.default_style_unified(diff_opts=["%s" % (self.style,)])
106
self.assertEqual(["%s" % (self.style,)], ret_opts)
109
class TestDiff(tests.TestCase):
111
def test_add_nl(self):
112
"""diff generates a valid diff for patches that add a newline"""
113
lines = udiff_lines(['boo'], ['boo\n'])
114
self.check_patch(lines)
115
self.assertEqual(lines[4], '\\ No newline at end of file\n')
116
## "expected no-nl, got %r" % lines[4]
118
def test_add_nl_2(self):
119
"""diff generates a valid diff for patches that change last line and
122
lines = udiff_lines(['boo'], ['goo\n'])
123
self.check_patch(lines)
124
self.assertEqual(lines[4], '\\ No newline at end of file\n')
125
## "expected no-nl, got %r" % lines[4]
127
def test_remove_nl(self):
128
"""diff generates a valid diff for patches that change last line and
131
lines = udiff_lines(['boo\n'], ['boo'])
132
self.check_patch(lines)
133
self.assertEqual(lines[5], '\\ No newline at end of file\n')
134
## "expected no-nl, got %r" % lines[5]
136
def check_patch(self, lines):
137
self.assertTrue(len(lines) > 1)
138
## "Not enough lines for a file header for patch:\n%s" % "".join(lines)
139
self.assertTrue(lines[0].startswith ('---'))
140
## 'No orig line for patch:\n%s' % "".join(lines)
141
self.assertTrue(lines[1].startswith ('+++'))
142
## 'No mod line for patch:\n%s' % "".join(lines)
143
self.assertTrue(len(lines) > 2)
144
## "No hunks for patch:\n%s" % "".join(lines)
145
self.assertTrue(lines[2].startswith('@@'))
146
## "No hunk header for patch:\n%s" % "".join(lines)
147
self.assertTrue('@@' in lines[2][2:])
148
## "Unterminated hunk header for patch:\n%s" % "".join(lines)
150
def test_binary_lines(self):
152
uni_lines = [1023 * 'a' + '\x00']
153
self.assertRaises(errors.BinaryFile, udiff_lines, uni_lines , empty)
154
self.assertRaises(errors.BinaryFile, udiff_lines, empty, uni_lines)
155
udiff_lines(uni_lines , empty, allow_binary=True)
156
udiff_lines(empty, uni_lines, allow_binary=True)
158
def test_external_diff(self):
159
lines = external_udiff_lines(['boo\n'], ['goo\n'])
160
self.check_patch(lines)
161
self.assertEqual('\n', lines[-1])
163
def test_external_diff_no_fileno(self):
164
# Make sure that we can handle not having a fileno, even
165
# if the diff is large
166
lines = external_udiff_lines(['boo\n']*10000,
169
self.check_patch(lines)
171
def test_external_diff_binary_lang_c(self):
172
for lang in ('LANG', 'LC_ALL', 'LANGUAGE'):
173
self.overrideEnv(lang, 'C')
174
lines = external_udiff_lines(['\x00foobar\n'], ['foo\x00bar\n'])
175
# Older versions of diffutils say "Binary files", newer
176
# versions just say "Files".
177
self.assertContainsRe(lines[0], '(Binary f|F)iles old and new differ\n')
178
self.assertEqual(lines[1:], ['\n'])
180
def test_no_external_diff(self):
181
"""Check that NoDiff is raised when diff is not available"""
182
# Make sure no 'diff' command is available
183
# XXX: Weird, using None instead of '' breaks the test -- vila 20101216
184
self.overrideEnv('PATH', '')
185
self.assertRaises(errors.NoDiff, diff.external_diff,
186
'old', ['boo\n'], 'new', ['goo\n'],
187
BytesIO(), diff_opts=['-u'])
189
def test_internal_diff_default(self):
190
# Default internal diff encoding is utf8
192
diff.internal_diff(u'old_\xb5', ['old_text\n'],
193
u'new_\xe5', ['new_text\n'], output)
194
lines = output.getvalue().splitlines(True)
195
self.check_patch(lines)
196
self.assertEqual(['--- old_\xc2\xb5\n',
197
'+++ new_\xc3\xa5\n',
205
def test_internal_diff_utf8(self):
207
diff.internal_diff(u'old_\xb5', ['old_text\n'],
208
u'new_\xe5', ['new_text\n'], output,
209
path_encoding='utf8')
210
lines = output.getvalue().splitlines(True)
211
self.check_patch(lines)
212
self.assertEqual(['--- old_\xc2\xb5\n',
213
'+++ new_\xc3\xa5\n',
221
def test_internal_diff_iso_8859_1(self):
223
diff.internal_diff(u'old_\xb5', ['old_text\n'],
224
u'new_\xe5', ['new_text\n'], output,
225
path_encoding='iso-8859-1')
226
lines = output.getvalue().splitlines(True)
227
self.check_patch(lines)
228
self.assertEqual(['--- old_\xb5\n',
237
def test_internal_diff_no_content(self):
239
diff.internal_diff(u'old', [], u'new', [], output)
240
self.assertEqual('', output.getvalue())
242
def test_internal_diff_no_changes(self):
244
diff.internal_diff(u'old', ['text\n', 'contents\n'],
245
u'new', ['text\n', 'contents\n'],
247
self.assertEqual('', output.getvalue())
249
def test_internal_diff_returns_bytes(self):
251
diff.internal_diff(u'old_\xb5', ['old_text\n'],
252
u'new_\xe5', ['new_text\n'], output)
253
output.check_types(self, bytes)
255
def test_internal_diff_default_context(self):
257
diff.internal_diff('old', ['same_text\n','same_text\n','same_text\n',
258
'same_text\n','same_text\n','old_text\n'],
259
'new', ['same_text\n','same_text\n','same_text\n',
260
'same_text\n','same_text\n','new_text\n'], output)
261
lines = output.getvalue().splitlines(True)
262
self.check_patch(lines)
263
self.assertEqual(['--- old\n',
275
def test_internal_diff_no_context(self):
277
diff.internal_diff('old', ['same_text\n','same_text\n','same_text\n',
278
'same_text\n','same_text\n','old_text\n'],
279
'new', ['same_text\n','same_text\n','same_text\n',
280
'same_text\n','same_text\n','new_text\n'], output,
282
lines = output.getvalue().splitlines(True)
283
self.check_patch(lines)
284
self.assertEqual(['--- old\n',
293
def test_internal_diff_more_context(self):
295
diff.internal_diff('old', ['same_text\n','same_text\n','same_text\n',
296
'same_text\n','same_text\n','old_text\n'],
297
'new', ['same_text\n','same_text\n','same_text\n',
298
'same_text\n','same_text\n','new_text\n'], output,
300
lines = output.getvalue().splitlines(True)
301
self.check_patch(lines)
302
self.assertEqual(['--- old\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
# Make sure external_diff doesn't fail in the current LANG
324
lines = external_udiff_lines(['\x00foobar\n'], ['foo\x00bar\n'])
326
cmd = ['diff', '-u', '--binary', 'old', 'new']
327
with open('old', 'wb') as f: f.write('\x00foobar\n')
328
with open('new', 'wb') as f: f.write('foo\x00bar\n')
329
pipe = subprocess.Popen(cmd, stdout=subprocess.PIPE,
330
stdin=subprocess.PIPE)
331
out, err = pipe.communicate()
332
# We should output whatever diff tells us, plus a trailing newline
333
self.assertEqual(out.splitlines(True) + ['\n'], lines)
336
def get_diff_as_string(tree1, tree2, specific_files=None, working_tree=None):
338
if working_tree is not None:
339
extra_trees = (working_tree,)
342
diff.show_diff_trees(tree1, tree2, output,
343
specific_files=specific_files,
344
extra_trees=extra_trees, old_label='old/',
346
return output.getvalue()
349
class TestDiffDates(tests.TestCaseWithTransport):
352
super(TestDiffDates, self).setUp()
353
self.wt = self.make_branch_and_tree('.')
354
self.b = self.wt.branch
355
self.build_tree_contents([
356
('file1', 'file1 contents at rev 1\n'),
357
('file2', 'file2 contents at rev 1\n')
359
self.wt.add(['file1', 'file2'])
361
message='Revision 1',
362
timestamp=1143849600, # 2006-04-01 00:00:00 UTC
365
self.build_tree_contents([('file1', 'file1 contents at rev 2\n')])
367
message='Revision 2',
368
timestamp=1143936000, # 2006-04-02 00:00:00 UTC
371
self.build_tree_contents([('file2', 'file2 contents at rev 3\n')])
373
message='Revision 3',
374
timestamp=1144022400, # 2006-04-03 00:00:00 UTC
377
self.wt.remove(['file2'])
379
message='Revision 4',
380
timestamp=1144108800, # 2006-04-04 00:00:00 UTC
383
self.build_tree_contents([
384
('file1', 'file1 contents in working tree\n')
386
# set the date stamps for files in the working tree to known values
387
os.utime('file1', (1144195200, 1144195200)) # 2006-04-05 00:00:00 UTC
389
def test_diff_rev_tree_working_tree(self):
390
output = get_diff_as_string(self.wt.basis_tree(), self.wt)
391
# note that the date for old/file1 is from rev 2 rather than from
392
# the basis revision (rev 4)
393
self.assertEqualDiff(output, '''\
394
=== modified file 'file1'
395
--- old/file1\t2006-04-02 00:00:00 +0000
396
+++ new/file1\t2006-04-05 00:00:00 +0000
398
-file1 contents at rev 2
399
+file1 contents in working tree
403
def test_diff_rev_tree_rev_tree(self):
404
tree1 = self.b.repository.revision_tree('rev-2')
405
tree2 = self.b.repository.revision_tree('rev-3')
406
output = get_diff_as_string(tree1, tree2)
407
self.assertEqualDiff(output, '''\
408
=== modified file 'file2'
409
--- old/file2\t2006-04-01 00:00:00 +0000
410
+++ new/file2\t2006-04-03 00:00:00 +0000
412
-file2 contents at rev 1
413
+file2 contents at rev 3
417
def test_diff_add_files(self):
418
tree1 = self.b.repository.revision_tree(_mod_revision.NULL_REVISION)
419
tree2 = self.b.repository.revision_tree('rev-1')
420
output = get_diff_as_string(tree1, tree2)
421
# the files have the epoch time stamp for the tree in which
423
self.assertEqualDiff(output, '''\
424
=== added file 'file1'
425
--- old/file1\t1970-01-01 00:00:00 +0000
426
+++ new/file1\t2006-04-01 00:00:00 +0000
428
+file1 contents at rev 1
430
=== added file 'file2'
431
--- old/file2\t1970-01-01 00:00:00 +0000
432
+++ new/file2\t2006-04-01 00:00:00 +0000
434
+file2 contents at rev 1
438
def test_diff_remove_files(self):
439
tree1 = self.b.repository.revision_tree('rev-3')
440
tree2 = self.b.repository.revision_tree('rev-4')
441
output = get_diff_as_string(tree1, tree2)
442
# the file has the epoch time stamp for the tree in which
444
self.assertEqualDiff(output, '''\
445
=== removed file 'file2'
446
--- old/file2\t2006-04-03 00:00:00 +0000
447
+++ new/file2\t1970-01-01 00:00:00 +0000
449
-file2 contents at rev 3
453
def test_show_diff_specified(self):
454
"""A working tree filename can be used to identify a file"""
455
self.wt.rename_one('file1', 'file1b')
456
old_tree = self.b.repository.revision_tree('rev-1')
457
new_tree = self.b.repository.revision_tree('rev-4')
458
out = get_diff_as_string(old_tree, new_tree, specific_files=['file1b'],
459
working_tree=self.wt)
460
self.assertContainsRe(out, 'file1\t')
462
def test_recursive_diff(self):
463
"""Children of directories are matched"""
466
self.wt.add(['dir1', 'dir2'])
467
self.wt.rename_one('file1', 'dir1/file1')
468
old_tree = self.b.repository.revision_tree('rev-1')
469
new_tree = self.b.repository.revision_tree('rev-4')
470
out = get_diff_as_string(old_tree, new_tree, specific_files=['dir1'],
471
working_tree=self.wt)
472
self.assertContainsRe(out, 'file1\t')
473
out = get_diff_as_string(old_tree, new_tree, specific_files=['dir2'],
474
working_tree=self.wt)
475
self.assertNotContainsRe(out, 'file1\t')
478
class TestShowDiffTrees(tests.TestCaseWithTransport):
479
"""Direct tests for show_diff_trees"""
481
def test_modified_file(self):
482
"""Test when a file is modified."""
483
tree = self.make_branch_and_tree('tree')
484
self.build_tree_contents([('tree/file', 'contents\n')])
485
tree.add(['file'], ['file-id'])
486
tree.commit('one', rev_id='rev-1')
488
self.build_tree_contents([('tree/file', 'new contents\n')])
489
d = get_diff_as_string(tree.basis_tree(), tree)
490
self.assertContainsRe(d, "=== modified file 'file'\n")
491
self.assertContainsRe(d, '--- old/file\t')
492
self.assertContainsRe(d, '\\+\\+\\+ new/file\t')
493
self.assertContainsRe(d, '-contents\n'
496
def test_modified_file_in_renamed_dir(self):
497
"""Test when a file is modified in a renamed directory."""
498
tree = self.make_branch_and_tree('tree')
499
self.build_tree(['tree/dir/'])
500
self.build_tree_contents([('tree/dir/file', 'contents\n')])
501
tree.add(['dir', 'dir/file'], ['dir-id', 'file-id'])
502
tree.commit('one', rev_id='rev-1')
504
tree.rename_one('dir', 'other')
505
self.build_tree_contents([('tree/other/file', 'new contents\n')])
506
d = get_diff_as_string(tree.basis_tree(), tree)
507
self.assertContainsRe(d, "=== renamed directory 'dir' => 'other'\n")
508
self.assertContainsRe(d, "=== modified file 'other/file'\n")
509
# XXX: This is technically incorrect, because it used to be at another
510
# location. What to do?
511
self.assertContainsRe(d, '--- old/dir/file\t')
512
self.assertContainsRe(d, '\\+\\+\\+ new/other/file\t')
513
self.assertContainsRe(d, '-contents\n'
516
def test_renamed_directory(self):
517
"""Test when only a directory is only renamed."""
518
tree = self.make_branch_and_tree('tree')
519
self.build_tree(['tree/dir/'])
520
self.build_tree_contents([('tree/dir/file', 'contents\n')])
521
tree.add(['dir', 'dir/file'], ['dir-id', 'file-id'])
522
tree.commit('one', rev_id='rev-1')
524
tree.rename_one('dir', 'newdir')
525
d = get_diff_as_string(tree.basis_tree(), tree)
526
# Renaming a directory should be a single "you renamed this dir" even
527
# when there are files inside.
528
self.assertEqual(d, "=== renamed directory 'dir' => 'newdir'\n")
530
def test_renamed_file(self):
531
"""Test when a file is only renamed."""
532
tree = self.make_branch_and_tree('tree')
533
self.build_tree_contents([('tree/file', 'contents\n')])
534
tree.add(['file'], ['file-id'])
535
tree.commit('one', rev_id='rev-1')
537
tree.rename_one('file', 'newname')
538
d = get_diff_as_string(tree.basis_tree(), tree)
539
self.assertContainsRe(d, "=== renamed file 'file' => 'newname'\n")
540
# We shouldn't have a --- or +++ line, because there is no content
542
self.assertNotContainsRe(d, '---')
544
def test_renamed_and_modified_file(self):
545
"""Test when a file is only renamed."""
546
tree = self.make_branch_and_tree('tree')
547
self.build_tree_contents([('tree/file', 'contents\n')])
548
tree.add(['file'], ['file-id'])
549
tree.commit('one', rev_id='rev-1')
551
tree.rename_one('file', 'newname')
552
self.build_tree_contents([('tree/newname', 'new contents\n')])
553
d = get_diff_as_string(tree.basis_tree(), tree)
554
self.assertContainsRe(d, "=== renamed file 'file' => 'newname'\n")
555
self.assertContainsRe(d, '--- old/file\t')
556
self.assertContainsRe(d, '\\+\\+\\+ new/newname\t')
557
self.assertContainsRe(d, '-contents\n'
561
def test_internal_diff_exec_property(self):
562
tree = self.make_branch_and_tree('tree')
564
tt = transform.TreeTransform(tree)
565
tt.new_file('a', tt.root, 'contents\n', 'a-id', True)
566
tt.new_file('b', tt.root, 'contents\n', 'b-id', False)
567
tt.new_file('c', tt.root, 'contents\n', 'c-id', True)
568
tt.new_file('d', tt.root, 'contents\n', 'd-id', False)
569
tt.new_file('e', tt.root, 'contents\n', 'control-e-id', True)
570
tt.new_file('f', tt.root, 'contents\n', 'control-f-id', False)
572
tree.commit('one', rev_id='rev-1')
574
tt = transform.TreeTransform(tree)
575
tt.set_executability(False, tt.trans_id_file_id('a-id'))
576
tt.set_executability(True, tt.trans_id_file_id('b-id'))
577
tt.set_executability(False, tt.trans_id_file_id('c-id'))
578
tt.set_executability(True, tt.trans_id_file_id('d-id'))
580
tree.rename_one('c', 'new-c')
581
tree.rename_one('d', 'new-d')
583
d = get_diff_as_string(tree.basis_tree(), tree)
585
self.assertContainsRe(d, r"file 'a'.*\(properties changed:"
587
self.assertContainsRe(d, r"file 'b'.*\(properties changed:"
589
self.assertContainsRe(d, r"file 'c'.*\(properties changed:"
591
self.assertContainsRe(d, r"file 'd'.*\(properties changed:"
593
self.assertNotContainsRe(d, r"file 'e'")
594
self.assertNotContainsRe(d, r"file 'f'")
596
def test_binary_unicode_filenames(self):
597
"""Test that contents of files are *not* encoded in UTF-8 when there
598
is a binary file in the diff.
600
# See https://bugs.launchpad.net/bugs/110092.
601
self.requireFeature(features.UnicodeFilenameFeature)
603
tree = self.make_branch_and_tree('tree')
604
alpha, omega = u'\u03b1', u'\u03c9'
605
alpha_utf8, omega_utf8 = alpha.encode('utf8'), omega.encode('utf8')
606
self.build_tree_contents(
607
[('tree/' + alpha, chr(0)),
609
('The %s and the %s\n' % (alpha_utf8, omega_utf8)))])
610
tree.add([alpha], ['file-id'])
611
tree.add([omega], ['file-id-2'])
612
diff_content = StubO()
613
diff.show_diff_trees(tree.basis_tree(), tree, diff_content)
614
diff_content.check_types(self, bytes)
615
d = b''.join(diff_content.write_record)
616
self.assertContainsRe(d, r"=== added file '%s'" % alpha_utf8)
617
self.assertContainsRe(d, "Binary files a/%s.*and b/%s.* differ\n"
618
% (alpha_utf8, alpha_utf8))
619
self.assertContainsRe(d, r"=== added file '%s'" % omega_utf8)
620
self.assertContainsRe(d, r"--- a/%s" % (omega_utf8,))
621
self.assertContainsRe(d, r"\+\+\+ b/%s" % (omega_utf8,))
623
def test_unicode_filename(self):
624
"""Test when the filename are unicode."""
625
self.requireFeature(features.UnicodeFilenameFeature)
627
alpha, omega = u'\u03b1', u'\u03c9'
628
autf8, outf8 = alpha.encode('utf8'), omega.encode('utf8')
630
tree = self.make_branch_and_tree('tree')
631
self.build_tree_contents([('tree/ren_'+alpha, 'contents\n')])
632
tree.add(['ren_'+alpha], ['file-id-2'])
633
self.build_tree_contents([('tree/del_'+alpha, 'contents\n')])
634
tree.add(['del_'+alpha], ['file-id-3'])
635
self.build_tree_contents([('tree/mod_'+alpha, 'contents\n')])
636
tree.add(['mod_'+alpha], ['file-id-4'])
638
tree.commit('one', rev_id='rev-1')
640
tree.rename_one('ren_'+alpha, 'ren_'+omega)
641
tree.remove('del_'+alpha)
642
self.build_tree_contents([('tree/add_'+alpha, 'contents\n')])
643
tree.add(['add_'+alpha], ['file-id'])
644
self.build_tree_contents([('tree/mod_'+alpha, 'contents_mod\n')])
646
d = get_diff_as_string(tree.basis_tree(), tree)
647
self.assertContainsRe(d,
648
"=== renamed file 'ren_%s' => 'ren_%s'\n"%(autf8, outf8))
649
self.assertContainsRe(d, "=== added file 'add_%s'"%autf8)
650
self.assertContainsRe(d, "=== modified file 'mod_%s'"%autf8)
651
self.assertContainsRe(d, "=== removed file 'del_%s'"%autf8)
653
def test_unicode_filename_path_encoding(self):
654
"""Test for bug #382699: unicode filenames on Windows should be shown
657
self.requireFeature(features.UnicodeFilenameFeature)
658
# The word 'test' in Russian
659
_russian_test = u'\u0422\u0435\u0441\u0442'
660
directory = _russian_test + u'/'
661
test_txt = _russian_test + u'.txt'
662
u1234 = u'\u1234.txt'
664
tree = self.make_branch_and_tree('.')
665
self.build_tree_contents([
670
tree.add([test_txt, u1234, directory])
673
diff.show_diff_trees(tree.basis_tree(), tree, sio,
674
path_encoding='cp1251')
676
output = subst_dates(sio.getvalue())
678
=== added directory '%(directory)s'
679
=== added file '%(test_txt)s'
680
--- a/%(test_txt)s\tYYYY-MM-DD HH:MM:SS +ZZZZ
681
+++ b/%(test_txt)s\tYYYY-MM-DD HH:MM:SS +ZZZZ
685
=== added file '?.txt'
686
--- a/?.txt\tYYYY-MM-DD HH:MM:SS +ZZZZ
687
+++ b/?.txt\tYYYY-MM-DD HH:MM:SS +ZZZZ
691
''' % {'directory': _russian_test.encode('cp1251'),
692
'test_txt': test_txt.encode('cp1251'),
694
self.assertEqualDiff(output, shouldbe)
697
class DiffWasIs(diff.DiffPath):
699
def diff(self, file_id, old_path, new_path, old_kind, new_kind):
700
self.to_file.write('was: ')
701
self.to_file.write(self.old_tree.get_file(file_id).read())
702
self.to_file.write('is: ')
703
self.to_file.write(self.new_tree.get_file(file_id).read())
707
class TestDiffTree(tests.TestCaseWithTransport):
710
super(TestDiffTree, self).setUp()
711
self.old_tree = self.make_branch_and_tree('old-tree')
712
self.old_tree.lock_write()
713
self.addCleanup(self.old_tree.unlock)
714
self.new_tree = self.make_branch_and_tree('new-tree')
715
self.new_tree.lock_write()
716
self.addCleanup(self.new_tree.unlock)
717
self.differ = diff.DiffTree(self.old_tree, self.new_tree, BytesIO())
719
def test_diff_text(self):
720
self.build_tree_contents([('old-tree/olddir/',),
721
('old-tree/olddir/oldfile', 'old\n')])
722
self.old_tree.add('olddir')
723
self.old_tree.add('olddir/oldfile', 'file-id')
724
self.build_tree_contents([('new-tree/newdir/',),
725
('new-tree/newdir/newfile', 'new\n')])
726
self.new_tree.add('newdir')
727
self.new_tree.add('newdir/newfile', 'file-id')
728
differ = diff.DiffText(self.old_tree, self.new_tree, BytesIO())
729
differ.diff_text('file-id', None, 'old label', 'new label')
731
'--- old label\n+++ new label\n@@ -1,1 +0,0 @@\n-old\n\n',
732
differ.to_file.getvalue())
733
differ.to_file.seek(0)
734
differ.diff_text(None, 'file-id', 'old label', 'new label')
736
'--- old label\n+++ new label\n@@ -0,0 +1,1 @@\n+new\n\n',
737
differ.to_file.getvalue())
738
differ.to_file.seek(0)
739
differ.diff_text('file-id', 'file-id', 'old label', 'new label')
741
'--- old label\n+++ new label\n@@ -1,1 +1,1 @@\n-old\n+new\n\n',
742
differ.to_file.getvalue())
744
def test_diff_deletion(self):
745
self.build_tree_contents([('old-tree/file', 'contents'),
746
('new-tree/file', 'contents')])
747
self.old_tree.add('file', 'file-id')
748
self.new_tree.add('file', 'file-id')
749
os.unlink('new-tree/file')
750
self.differ.show_diff(None)
751
self.assertContainsRe(self.differ.to_file.getvalue(), '-contents')
753
def test_diff_creation(self):
754
self.build_tree_contents([('old-tree/file', 'contents'),
755
('new-tree/file', 'contents')])
756
self.old_tree.add('file', 'file-id')
757
self.new_tree.add('file', 'file-id')
758
os.unlink('old-tree/file')
759
self.differ.show_diff(None)
760
self.assertContainsRe(self.differ.to_file.getvalue(), '\+contents')
762
def test_diff_symlink(self):
763
differ = diff.DiffSymlink(self.old_tree, self.new_tree, BytesIO())
764
differ.diff_symlink('old target', None)
765
self.assertEqual("=== target was 'old target'\n",
766
differ.to_file.getvalue())
768
differ = diff.DiffSymlink(self.old_tree, self.new_tree, BytesIO())
769
differ.diff_symlink(None, 'new target')
770
self.assertEqual("=== target is 'new target'\n",
771
differ.to_file.getvalue())
773
differ = diff.DiffSymlink(self.old_tree, self.new_tree, BytesIO())
774
differ.diff_symlink('old target', 'new target')
775
self.assertEqual("=== target changed 'old target' => 'new target'\n",
776
differ.to_file.getvalue())
779
self.build_tree_contents([('old-tree/olddir/',),
780
('old-tree/olddir/oldfile', 'old\n')])
781
self.old_tree.add('olddir')
782
self.old_tree.add('olddir/oldfile', 'file-id')
783
self.build_tree_contents([('new-tree/newdir/',),
784
('new-tree/newdir/newfile', 'new\n')])
785
self.new_tree.add('newdir')
786
self.new_tree.add('newdir/newfile', 'file-id')
787
self.differ.diff('file-id', 'olddir/oldfile', 'newdir/newfile')
788
self.assertContainsRe(
789
self.differ.to_file.getvalue(),
790
r'--- olddir/oldfile.*\n\+\+\+ newdir/newfile.*\n\@\@ -1,1 \+1,1'
791
' \@\@\n-old\n\+new\n\n')
793
def test_diff_kind_change(self):
794
self.requireFeature(features.SymlinkFeature)
795
self.build_tree_contents([('old-tree/olddir/',),
796
('old-tree/olddir/oldfile', 'old\n')])
797
self.old_tree.add('olddir')
798
self.old_tree.add('olddir/oldfile', 'file-id')
799
self.build_tree(['new-tree/newdir/'])
800
os.symlink('new', 'new-tree/newdir/newfile')
801
self.new_tree.add('newdir')
802
self.new_tree.add('newdir/newfile', 'file-id')
803
self.differ.diff('file-id', 'olddir/oldfile', 'newdir/newfile')
804
self.assertContainsRe(
805
self.differ.to_file.getvalue(),
806
r'--- olddir/oldfile.*\n\+\+\+ newdir/newfile.*\n\@\@ -1,1 \+0,0'
808
self.assertContainsRe(self.differ.to_file.getvalue(),
809
"=== target is u'new'\n")
811
def test_diff_directory(self):
812
self.build_tree(['new-tree/new-dir/'])
813
self.new_tree.add('new-dir', 'new-dir-id')
814
self.differ.diff('new-dir-id', None, 'new-dir')
815
self.assertEqual(self.differ.to_file.getvalue(), '')
817
def create_old_new(self):
818
self.build_tree_contents([('old-tree/olddir/',),
819
('old-tree/olddir/oldfile', 'old\n')])
820
self.old_tree.add('olddir')
821
self.old_tree.add('olddir/oldfile', 'file-id')
822
self.build_tree_contents([('new-tree/newdir/',),
823
('new-tree/newdir/newfile', 'new\n')])
824
self.new_tree.add('newdir')
825
self.new_tree.add('newdir/newfile', 'file-id')
827
def test_register_diff(self):
828
self.create_old_new()
829
old_diff_factories = diff.DiffTree.diff_factories
830
diff.DiffTree.diff_factories=old_diff_factories[:]
831
diff.DiffTree.diff_factories.insert(0, DiffWasIs.from_diff_tree)
833
differ = diff.DiffTree(self.old_tree, self.new_tree, BytesIO())
835
diff.DiffTree.diff_factories = old_diff_factories
836
differ.diff('file-id', 'olddir/oldfile', 'newdir/newfile')
837
self.assertNotContainsRe(
838
differ.to_file.getvalue(),
839
r'--- olddir/oldfile.*\n\+\+\+ newdir/newfile.*\n\@\@ -1,1 \+1,1'
840
' \@\@\n-old\n\+new\n\n')
841
self.assertContainsRe(differ.to_file.getvalue(),
842
'was: old\nis: new\n')
844
def test_extra_factories(self):
845
self.create_old_new()
846
differ = diff.DiffTree(self.old_tree, self.new_tree, BytesIO(),
847
extra_factories=[DiffWasIs.from_diff_tree])
848
differ.diff('file-id', 'olddir/oldfile', 'newdir/newfile')
849
self.assertNotContainsRe(
850
differ.to_file.getvalue(),
851
r'--- olddir/oldfile.*\n\+\+\+ newdir/newfile.*\n\@\@ -1,1 \+1,1'
852
' \@\@\n-old\n\+new\n\n')
853
self.assertContainsRe(differ.to_file.getvalue(),
854
'was: old\nis: new\n')
856
def test_alphabetical_order(self):
857
self.build_tree(['new-tree/a-file'])
858
self.new_tree.add('a-file')
859
self.build_tree(['old-tree/b-file'])
860
self.old_tree.add('b-file')
861
self.differ.show_diff(None)
862
self.assertContainsRe(self.differ.to_file.getvalue(),
863
'.*a-file(.|\n)*b-file')
866
class TestPatienceDiffLib(tests.TestCase):
869
super(TestPatienceDiffLib, self).setUp()
870
self._unique_lcs = _patiencediff_py.unique_lcs_py
871
self._recurse_matches = _patiencediff_py.recurse_matches_py
872
self._PatienceSequenceMatcher = \
873
_patiencediff_py.PatienceSequenceMatcher_py
875
def test_diff_unicode_string(self):
876
a = ''.join([unichr(i) for i in range(4000, 4500, 3)])
877
b = ''.join([unichr(i) for i in range(4300, 4800, 2)])
878
sm = self._PatienceSequenceMatcher(None, a, b)
879
mb = sm.get_matching_blocks()
880
self.assertEqual(35, len(mb))
882
def test_unique_lcs(self):
883
unique_lcs = self._unique_lcs
884
self.assertEqual(unique_lcs('', ''), [])
885
self.assertEqual(unique_lcs('', 'a'), [])
886
self.assertEqual(unique_lcs('a', ''), [])
887
self.assertEqual(unique_lcs('a', 'a'), [(0,0)])
888
self.assertEqual(unique_lcs('a', 'b'), [])
889
self.assertEqual(unique_lcs('ab', 'ab'), [(0,0), (1,1)])
890
self.assertEqual(unique_lcs('abcde', 'cdeab'), [(2,0), (3,1), (4,2)])
891
self.assertEqual(unique_lcs('cdeab', 'abcde'), [(0,2), (1,3), (2,4)])
892
self.assertEqual(unique_lcs('abXde', 'abYde'), [(0,0), (1,1),
894
self.assertEqual(unique_lcs('acbac', 'abc'), [(2,1)])
896
def test_recurse_matches(self):
897
def test_one(a, b, matches):
899
self._recurse_matches(
900
a, b, 0, 0, len(a), len(b), test_matches, 10)
901
self.assertEqual(test_matches, matches)
903
test_one(['a', '', 'b', '', 'c'], ['a', 'a', 'b', 'c', 'c'],
904
[(0, 0), (2, 2), (4, 4)])
905
test_one(['a', 'c', 'b', 'a', 'c'], ['a', 'b', 'c'],
906
[(0, 0), (2, 1), (4, 2)])
907
# Even though 'bc' is not unique globally, and is surrounded by
908
# non-matching lines, we should still match, because they are locally
910
test_one('abcdbce', 'afbcgdbce', [(0,0), (1, 2), (2, 3), (3, 5),
911
(4, 6), (5, 7), (6, 8)])
913
# recurse_matches doesn't match non-unique
914
# lines surrounded by bogus text.
915
# The update has been done in patiencediff.SequenceMatcher instead
917
# This is what it could be
918
#test_one('aBccDe', 'abccde', [(0,0), (2,2), (3,3), (5,5)])
920
# This is what it currently gives:
921
test_one('aBccDe', 'abccde', [(0,0), (5,5)])
923
def assertDiffBlocks(self, a, b, expected_blocks):
924
"""Check that the sequence matcher returns the correct blocks.
926
:param a: A sequence to match
927
:param b: Another sequence to match
928
:param expected_blocks: The expected output, not including the final
929
matching block (len(a), len(b), 0)
931
matcher = self._PatienceSequenceMatcher(None, a, b)
932
blocks = matcher.get_matching_blocks()
934
self.assertEqual((len(a), len(b), 0), last)
935
self.assertEqual(expected_blocks, blocks)
937
def test_matching_blocks(self):
938
# Some basic matching tests
939
self.assertDiffBlocks('', '', [])
940
self.assertDiffBlocks([], [], [])
941
self.assertDiffBlocks('abc', '', [])
942
self.assertDiffBlocks('', 'abc', [])
943
self.assertDiffBlocks('abcd', 'abcd', [(0, 0, 4)])
944
self.assertDiffBlocks('abcd', 'abce', [(0, 0, 3)])
945
self.assertDiffBlocks('eabc', 'abce', [(1, 0, 3)])
946
self.assertDiffBlocks('eabce', 'abce', [(1, 0, 4)])
947
self.assertDiffBlocks('abcde', 'abXde', [(0, 0, 2), (3, 3, 2)])
948
self.assertDiffBlocks('abcde', 'abXYZde', [(0, 0, 2), (3, 5, 2)])
949
self.assertDiffBlocks('abde', 'abXYZde', [(0, 0, 2), (2, 5, 2)])
950
# This may check too much, but it checks to see that
951
# a copied block stays attached to the previous section,
953
# difflib would tend to grab the trailing longest match
954
# which would make the diff not look right
955
self.assertDiffBlocks('abcdefghijklmnop', 'abcdefxydefghijklmnop',
956
[(0, 0, 6), (6, 11, 10)])
958
# make sure it supports passing in lists
959
self.assertDiffBlocks(
962
'how are you today?\n'],
964
'how are you today?\n'],
965
[(0, 0, 1), (2, 1, 1)])
967
# non unique lines surrounded by non-matching lines
969
self.assertDiffBlocks('aBccDe', 'abccde', [(0,0,1), (5,5,1)])
971
# But they only need to be locally unique
972
self.assertDiffBlocks('aBcDec', 'abcdec', [(0,0,1), (2,2,1), (4,4,2)])
974
# non unique blocks won't be matched
975
self.assertDiffBlocks('aBcdEcdFg', 'abcdecdfg', [(0,0,1), (8,8,1)])
977
# but locally unique ones will
978
self.assertDiffBlocks('aBcdEeXcdFg', 'abcdecdfg', [(0,0,1), (2,2,2),
979
(5,4,1), (7,5,2), (10,8,1)])
981
self.assertDiffBlocks('abbabbXd', 'cabbabxd', [(7,7,1)])
982
self.assertDiffBlocks('abbabbbb', 'cabbabbc', [])
983
self.assertDiffBlocks('bbbbbbbb', 'cbbbbbbc', [])
985
def test_matching_blocks_tuples(self):
986
# Some basic matching tests
987
self.assertDiffBlocks([], [], [])
988
self.assertDiffBlocks([('a',), ('b',), ('c,')], [], [])
989
self.assertDiffBlocks([], [('a',), ('b',), ('c,')], [])
990
self.assertDiffBlocks([('a',), ('b',), ('c,')],
991
[('a',), ('b',), ('c,')],
993
self.assertDiffBlocks([('a',), ('b',), ('c,')],
994
[('a',), ('b',), ('d,')],
996
self.assertDiffBlocks([('d',), ('b',), ('c,')],
997
[('a',), ('b',), ('c,')],
999
self.assertDiffBlocks([('d',), ('a',), ('b',), ('c,')],
1000
[('a',), ('b',), ('c,')],
1002
self.assertDiffBlocks([('a', 'b'), ('c', 'd'), ('e', 'f')],
1003
[('a', 'b'), ('c', 'X'), ('e', 'f')],
1004
[(0, 0, 1), (2, 2, 1)])
1005
self.assertDiffBlocks([('a', 'b'), ('c', 'd'), ('e', 'f')],
1006
[('a', 'b'), ('c', 'dX'), ('e', 'f')],
1007
[(0, 0, 1), (2, 2, 1)])
1009
def test_opcodes(self):
1010
def chk_ops(a, b, expected_codes):
1011
s = self._PatienceSequenceMatcher(None, a, b)
1012
self.assertEqual(expected_codes, s.get_opcodes())
1016
chk_ops('abc', '', [('delete', 0,3, 0,0)])
1017
chk_ops('', 'abc', [('insert', 0,0, 0,3)])
1018
chk_ops('abcd', 'abcd', [('equal', 0,4, 0,4)])
1019
chk_ops('abcd', 'abce', [('equal', 0,3, 0,3),
1020
('replace', 3,4, 3,4)
1022
chk_ops('eabc', 'abce', [('delete', 0,1, 0,0),
1023
('equal', 1,4, 0,3),
1024
('insert', 4,4, 3,4)
1026
chk_ops('eabce', 'abce', [('delete', 0,1, 0,0),
1029
chk_ops('abcde', 'abXde', [('equal', 0,2, 0,2),
1030
('replace', 2,3, 2,3),
1033
chk_ops('abcde', 'abXYZde', [('equal', 0,2, 0,2),
1034
('replace', 2,3, 2,5),
1037
chk_ops('abde', 'abXYZde', [('equal', 0,2, 0,2),
1038
('insert', 2,2, 2,5),
1041
chk_ops('abcdefghijklmnop', 'abcdefxydefghijklmnop',
1042
[('equal', 0,6, 0,6),
1043
('insert', 6,6, 6,11),
1044
('equal', 6,16, 11,21)
1049
, 'how are you today?\n'],
1051
, 'how are you today?\n'],
1052
[('equal', 0,1, 0,1),
1053
('delete', 1,2, 1,1),
1054
('equal', 2,3, 1,2),
1056
chk_ops('aBccDe', 'abccde',
1057
[('equal', 0,1, 0,1),
1058
('replace', 1,5, 1,5),
1059
('equal', 5,6, 5,6),
1061
chk_ops('aBcDec', 'abcdec',
1062
[('equal', 0,1, 0,1),
1063
('replace', 1,2, 1,2),
1064
('equal', 2,3, 2,3),
1065
('replace', 3,4, 3,4),
1066
('equal', 4,6, 4,6),
1068
chk_ops('aBcdEcdFg', 'abcdecdfg',
1069
[('equal', 0,1, 0,1),
1070
('replace', 1,8, 1,8),
1073
chk_ops('aBcdEeXcdFg', 'abcdecdfg',
1074
[('equal', 0,1, 0,1),
1075
('replace', 1,2, 1,2),
1076
('equal', 2,4, 2,4),
1077
('delete', 4,5, 4,4),
1078
('equal', 5,6, 4,5),
1079
('delete', 6,7, 5,5),
1080
('equal', 7,9, 5,7),
1081
('replace', 9,10, 7,8),
1082
('equal', 10,11, 8,9)
1085
def test_grouped_opcodes(self):
1086
def chk_ops(a, b, expected_codes, n=3):
1087
s = self._PatienceSequenceMatcher(None, a, b)
1088
self.assertEqual(expected_codes, list(s.get_grouped_opcodes(n)))
1092
chk_ops('abc', '', [[('delete', 0,3, 0,0)]])
1093
chk_ops('', 'abc', [[('insert', 0,0, 0,3)]])
1094
chk_ops('abcd', 'abcd', [])
1095
chk_ops('abcd', 'abce', [[('equal', 0,3, 0,3),
1096
('replace', 3,4, 3,4)
1098
chk_ops('eabc', 'abce', [[('delete', 0,1, 0,0),
1099
('equal', 1,4, 0,3),
1100
('insert', 4,4, 3,4)
1102
chk_ops('abcdefghijklmnop', 'abcdefxydefghijklmnop',
1103
[[('equal', 3,6, 3,6),
1104
('insert', 6,6, 6,11),
1105
('equal', 6,9, 11,14)
1107
chk_ops('abcdefghijklmnop', 'abcdefxydefghijklmnop',
1108
[[('equal', 2,6, 2,6),
1109
('insert', 6,6, 6,11),
1110
('equal', 6,10, 11,15)
1112
chk_ops('Xabcdef', 'abcdef',
1113
[[('delete', 0,1, 0,0),
1116
chk_ops('abcdef', 'abcdefX',
1117
[[('equal', 3,6, 3,6),
1118
('insert', 6,6, 6,7)
1122
def test_multiple_ranges(self):
1123
# There was an earlier bug where we used a bad set of ranges,
1124
# this triggers that specific bug, to make sure it doesn't regress
1125
self.assertDiffBlocks('abcdefghijklmnop',
1126
'abcXghiYZQRSTUVWXYZijklmnop',
1127
[(0, 0, 3), (6, 4, 3), (9, 20, 7)])
1129
self.assertDiffBlocks('ABCd efghIjk L',
1130
'AxyzBCn mo pqrstuvwI1 2 L',
1131
[(0,0,1), (1, 4, 2), (9, 19, 1), (12, 23, 3)])
1133
# These are rot13 code snippets.
1134
self.assertDiffBlocks('''\
1135
trg nqqrq jura lbh nqq n svyr va gur qverpgbel.
1137
gnxrf_netf = ['svyr*']
1138
gnxrf_bcgvbaf = ['ab-erphefr']
1140
qrs eha(frys, svyr_yvfg, ab_erphefr=Snyfr):
1141
sebz omeyvo.nqq vzcbeg fzneg_nqq, nqq_ercbegre_cevag, nqq_ercbegre_ahyy
1143
ercbegre = nqq_ercbegre_ahyy
1145
ercbegre = nqq_ercbegre_cevag
1146
fzneg_nqq(svyr_yvfg, abg ab_erphefr, ercbegre)
1149
pynff pzq_zxqve(Pbzznaq):
1150
'''.splitlines(True), '''\
1151
trg nqqrq jura lbh nqq n svyr va gur qverpgbel.
1153
--qel-eha jvyy fubj juvpu svyrf jbhyq or nqqrq, ohg abg npghnyyl
1156
gnxrf_netf = ['svyr*']
1157
gnxrf_bcgvbaf = ['ab-erphefr', 'qel-eha']
1159
qrs eha(frys, svyr_yvfg, ab_erphefr=Snyfr, qel_eha=Snyfr):
1164
# Guvf vf cbvagyrff, ohg V'q engure abg envfr na reebe
1165
npgvba = omeyvo.nqq.nqq_npgvba_ahyy
1167
npgvba = omeyvo.nqq.nqq_npgvba_cevag
1169
npgvba = omeyvo.nqq.nqq_npgvba_nqq
1171
npgvba = omeyvo.nqq.nqq_npgvba_nqq_naq_cevag
1173
omeyvo.nqq.fzneg_nqq(svyr_yvfg, abg ab_erphefr, npgvba)
1176
pynff pzq_zxqve(Pbzznaq):
1177
'''.splitlines(True)
1178
, [(0,0,1), (1, 4, 2), (9, 19, 1), (12, 23, 3)])
1180
def test_patience_unified_diff(self):
1181
txt_a = ['hello there\n',
1183
'how are you today?\n']
1184
txt_b = ['hello there\n',
1185
'how are you today?\n']
1186
unified_diff = patiencediff.unified_diff
1187
psm = self._PatienceSequenceMatcher
1188
self.assertEqual(['--- \n',
1190
'@@ -1,3 +1,2 @@\n',
1193
' how are you today?\n'
1195
, list(unified_diff(txt_a, txt_b,
1196
sequencematcher=psm)))
1197
txt_a = [x+'\n' for x in 'abcdefghijklmnop']
1198
txt_b = [x+'\n' for x in 'abcdefxydefghijklmnop']
1199
# This is the result with LongestCommonSubstring matching
1200
self.assertEqual(['--- \n',
1202
'@@ -1,6 +1,11 @@\n',
1214
, list(unified_diff(txt_a, txt_b)))
1215
# And the patience diff
1216
self.assertEqual(['--- \n',
1218
'@@ -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'
1249
, list(unified_diff(txt_a, txt_b,
1250
fromfile='a', tofile='b',
1251
fromfiledate='2008-08-08',
1252
tofiledate='2008-09-09',
1253
sequencematcher=psm)))
1256
class TestPatienceDiffLib_c(TestPatienceDiffLib):
1258
_test_needs_features = [features.compiled_patiencediff_feature]
1261
super(TestPatienceDiffLib_c, self).setUp()
1262
from breezy import _patiencediff_c
1263
self._unique_lcs = _patiencediff_c.unique_lcs_c
1264
self._recurse_matches = _patiencediff_c.recurse_matches_c
1265
self._PatienceSequenceMatcher = \
1266
_patiencediff_c.PatienceSequenceMatcher_c
1268
def test_unhashable(self):
1269
"""We should get a proper exception here."""
1270
# We need to be able to hash items in the sequence, lists are
1271
# unhashable, and thus cannot be diffed
1272
e = self.assertRaises(TypeError, self._PatienceSequenceMatcher,
1274
e = self.assertRaises(TypeError, self._PatienceSequenceMatcher,
1275
None, ['valid', []], [])
1276
e = self.assertRaises(TypeError, self._PatienceSequenceMatcher,
1277
None, ['valid'], [[]])
1278
e = self.assertRaises(TypeError, self._PatienceSequenceMatcher,
1279
None, ['valid'], ['valid', []])
1282
class TestPatienceDiffLibFiles(tests.TestCaseInTempDir):
1285
super(TestPatienceDiffLibFiles, self).setUp()
1286
self._PatienceSequenceMatcher = \
1287
_patiencediff_py.PatienceSequenceMatcher_py
1289
def test_patience_unified_diff_files(self):
1290
txt_a = ['hello there\n',
1292
'how are you today?\n']
1293
txt_b = ['hello there\n',
1294
'how are you today?\n']
1295
with open('a1', 'wb') as f: f.writelines(txt_a)
1296
with open('b1', 'wb') as f: f.writelines(txt_b)
1298
unified_diff_files = patiencediff.unified_diff_files
1299
psm = self._PatienceSequenceMatcher
1300
self.assertEqual(['--- a1\n',
1302
'@@ -1,3 +1,2 @@\n',
1305
' how are you today?\n',
1307
, list(unified_diff_files('a1', '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', 'wb') as f: f.writelines(txt_a)
1313
with open('b2', 'wb') as f: f.writelines(txt_b)
1315
# This is the result with LongestCommonSubstring matching
1316
self.assertEqual(['--- a2\n',
1318
'@@ -1,6 +1,11 @@\n',
1330
, list(unified_diff_files('a2', 'b2')))
1332
# And the patience diff
1333
self.assertEqual(['--- a2\n',
1335
'@@ -4,6 +4,11 @@\n',
1347
list(unified_diff_files('a2', '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(['python', '-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(), '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', 'content')])
1445
tree.add('file', '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(['python', '-c',
1453
'print "@old_path @new_path"'],
1454
basis_tree, tree, output)
1455
diff_obj._prepare_files('file-id', 'file', 'file')
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', 'oldcontent')])
1474
self.build_tree_contents([('tree/oldname2', 'oldcontent2')])
1475
tree.add('oldname', 'file-id')
1476
tree.add('oldname2', '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', 'newcontent')])
1482
self.build_tree_contents([('tree/newname2', '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(['python', '-c',
1489
'print "@old_path @new_path"'],
1490
old_tree, tree, output)
1491
self.addCleanup(diff_obj.finish)
1492
self.assertContainsRe(diff_obj._root, 'bzr-diff-[^/]*')
1493
old_path, new_path = diff_obj._prepare_files('file-id', 'oldname',
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('oldcontent', old_path)
1499
self.assertFileEqual('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('file2-id', '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', 'oldcontent')])
1572
tree.add('file', 'file-id')
1573
tree.commit('old tree', timestamp=0, rev_id="old-id")
1574
self.build_tree_contents([('tree/file', 'newcontent')])
1575
tree.commit('new tree', timestamp=0, rev_id="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("old-id", old_tree.get_revision_id())
1586
self.assertIsInstance(new_tree, revisiontree.RevisionTree)
1587
self.assertEqual("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)