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
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(['\x00foobar\n'], ['foo\x00bar\n'])
328
cmd = ['diff', '-u', '--binary', 'old', 'new']
329
with open('old', 'wb') as f: f.write('\x00foobar\n')
330
with open('new', 'wb') as f: f.write('foo\x00bar\n')
331
pipe = subprocess.Popen(cmd, stdout=subprocess.PIPE,
332
stdin=subprocess.PIPE)
333
out, err = pipe.communicate()
334
# We should output whatever diff tells us, plus a trailing newline
335
self.assertEqual(out.splitlines(True) + ['\n'], lines)
338
def get_diff_as_string(tree1, tree2, specific_files=None, working_tree=None):
340
if working_tree is not None:
341
extra_trees = (working_tree,)
344
diff.show_diff_trees(tree1, tree2, output,
345
specific_files=specific_files,
346
extra_trees=extra_trees, old_label='old/',
348
return output.getvalue()
351
class TestDiffDates(tests.TestCaseWithTransport):
354
super(TestDiffDates, self).setUp()
355
self.wt = self.make_branch_and_tree('.')
356
self.b = self.wt.branch
357
self.build_tree_contents([
358
('file1', b'file1 contents at rev 1\n'),
359
('file2', b'file2 contents at rev 1\n')
361
self.wt.add(['file1', 'file2'])
363
message='Revision 1',
364
timestamp=1143849600, # 2006-04-01 00:00:00 UTC
367
self.build_tree_contents([('file1', b'file1 contents at rev 2\n')])
369
message='Revision 2',
370
timestamp=1143936000, # 2006-04-02 00:00:00 UTC
373
self.build_tree_contents([('file2', b'file2 contents at rev 3\n')])
375
message='Revision 3',
376
timestamp=1144022400, # 2006-04-03 00:00:00 UTC
379
self.wt.remove(['file2'])
381
message='Revision 4',
382
timestamp=1144108800, # 2006-04-04 00:00:00 UTC
385
self.build_tree_contents([
386
('file1', b'file1 contents in working tree\n')
388
# set the date stamps for files in the working tree to known values
389
os.utime('file1', (1144195200, 1144195200)) # 2006-04-05 00:00:00 UTC
391
def test_diff_rev_tree_working_tree(self):
392
output = get_diff_as_string(self.wt.basis_tree(), self.wt)
393
# note that the date for old/file1 is from rev 2 rather than from
394
# the basis revision (rev 4)
395
self.assertEqualDiff(output, '''\
396
=== modified file 'file1'
397
--- old/file1\t2006-04-02 00:00:00 +0000
398
+++ new/file1\t2006-04-05 00:00:00 +0000
400
-file1 contents at rev 2
401
+file1 contents in working tree
405
def test_diff_rev_tree_rev_tree(self):
406
tree1 = self.b.repository.revision_tree('rev-2')
407
tree2 = self.b.repository.revision_tree('rev-3')
408
output = get_diff_as_string(tree1, tree2)
409
self.assertEqualDiff(output, '''\
410
=== modified file 'file2'
411
--- old/file2\t2006-04-01 00:00:00 +0000
412
+++ new/file2\t2006-04-03 00:00:00 +0000
414
-file2 contents at rev 1
415
+file2 contents at rev 3
419
def test_diff_add_files(self):
420
tree1 = self.b.repository.revision_tree(_mod_revision.NULL_REVISION)
421
tree2 = self.b.repository.revision_tree('rev-1')
422
output = get_diff_as_string(tree1, tree2)
423
# the files have the epoch time stamp for the tree in which
425
self.assertEqualDiff(output, '''\
426
=== added file 'file1'
427
--- old/file1\t1970-01-01 00:00:00 +0000
428
+++ new/file1\t2006-04-01 00:00:00 +0000
430
+file1 contents at rev 1
432
=== added file 'file2'
433
--- old/file2\t1970-01-01 00:00:00 +0000
434
+++ new/file2\t2006-04-01 00:00:00 +0000
436
+file2 contents at rev 1
440
def test_diff_remove_files(self):
441
tree1 = self.b.repository.revision_tree('rev-3')
442
tree2 = self.b.repository.revision_tree('rev-4')
443
output = get_diff_as_string(tree1, tree2)
444
# the file has the epoch time stamp for the tree in which
446
self.assertEqualDiff(output, '''\
447
=== removed file 'file2'
448
--- old/file2\t2006-04-03 00:00:00 +0000
449
+++ new/file2\t1970-01-01 00:00:00 +0000
451
-file2 contents at rev 3
455
def test_show_diff_specified(self):
456
"""A working tree filename can be used to identify a file"""
457
self.wt.rename_one('file1', 'file1b')
458
old_tree = self.b.repository.revision_tree('rev-1')
459
new_tree = self.b.repository.revision_tree('rev-4')
460
out = get_diff_as_string(old_tree, new_tree, specific_files=['file1b'],
461
working_tree=self.wt)
462
self.assertContainsRe(out, 'file1\t')
464
def test_recursive_diff(self):
465
"""Children of directories are matched"""
468
self.wt.add(['dir1', 'dir2'])
469
self.wt.rename_one('file1', 'dir1/file1')
470
old_tree = self.b.repository.revision_tree('rev-1')
471
new_tree = self.b.repository.revision_tree('rev-4')
472
out = get_diff_as_string(old_tree, new_tree, specific_files=['dir1'],
473
working_tree=self.wt)
474
self.assertContainsRe(out, 'file1\t')
475
out = get_diff_as_string(old_tree, new_tree, specific_files=['dir2'],
476
working_tree=self.wt)
477
self.assertNotContainsRe(out, 'file1\t')
480
class TestShowDiffTrees(tests.TestCaseWithTransport):
481
"""Direct tests for show_diff_trees"""
483
def test_modified_file(self):
484
"""Test when a file is modified."""
485
tree = self.make_branch_and_tree('tree')
486
self.build_tree_contents([('tree/file', b'contents\n')])
487
tree.add(['file'], [b'file-id'])
488
tree.commit('one', rev_id=b'rev-1')
490
self.build_tree_contents([('tree/file', b'new contents\n')])
491
d = get_diff_as_string(tree.basis_tree(), tree)
492
self.assertContainsRe(d, "=== modified file 'file'\n")
493
self.assertContainsRe(d, '--- old/file\t')
494
self.assertContainsRe(d, '\\+\\+\\+ new/file\t')
495
self.assertContainsRe(d, '-contents\n'
498
def test_modified_file_in_renamed_dir(self):
499
"""Test when a file is modified in a renamed directory."""
500
tree = self.make_branch_and_tree('tree')
501
self.build_tree(['tree/dir/'])
502
self.build_tree_contents([('tree/dir/file', b'contents\n')])
503
tree.add(['dir', 'dir/file'], [b'dir-id', b'file-id'])
504
tree.commit('one', rev_id=b'rev-1')
506
tree.rename_one('dir', 'other')
507
self.build_tree_contents([('tree/other/file', b'new contents\n')])
508
d = get_diff_as_string(tree.basis_tree(), tree)
509
self.assertContainsRe(d, "=== renamed directory 'dir' => 'other'\n")
510
self.assertContainsRe(d, "=== modified file 'other/file'\n")
511
# XXX: This is technically incorrect, because it used to be at another
512
# location. What to do?
513
self.assertContainsRe(d, '--- old/dir/file\t')
514
self.assertContainsRe(d, '\\+\\+\\+ new/other/file\t')
515
self.assertContainsRe(d, '-contents\n'
518
def test_renamed_directory(self):
519
"""Test when only a directory is only renamed."""
520
tree = self.make_branch_and_tree('tree')
521
self.build_tree(['tree/dir/'])
522
self.build_tree_contents([('tree/dir/file', b'contents\n')])
523
tree.add(['dir', 'dir/file'], [b'dir-id', b'file-id'])
524
tree.commit('one', rev_id=b'rev-1')
526
tree.rename_one('dir', 'newdir')
527
d = get_diff_as_string(tree.basis_tree(), tree)
528
# Renaming a directory should be a single "you renamed this dir" even
529
# when there are files inside.
530
self.assertEqual(d, "=== renamed directory 'dir' => 'newdir'\n")
532
def test_renamed_file(self):
533
"""Test when a file is only renamed."""
534
tree = self.make_branch_and_tree('tree')
535
self.build_tree_contents([('tree/file', b'contents\n')])
536
tree.add(['file'], [b'file-id'])
537
tree.commit('one', rev_id=b'rev-1')
539
tree.rename_one('file', 'newname')
540
d = get_diff_as_string(tree.basis_tree(), tree)
541
self.assertContainsRe(d, "=== renamed file 'file' => 'newname'\n")
542
# We shouldn't have a --- or +++ line, because there is no content
544
self.assertNotContainsRe(d, '---')
546
def test_renamed_and_modified_file(self):
547
"""Test when a file is only renamed."""
548
tree = self.make_branch_and_tree('tree')
549
self.build_tree_contents([('tree/file', b'contents\n')])
550
tree.add(['file'], [b'file-id'])
551
tree.commit('one', rev_id=b'rev-1')
553
tree.rename_one('file', 'newname')
554
self.build_tree_contents([('tree/newname', b'new contents\n')])
555
d = get_diff_as_string(tree.basis_tree(), tree)
556
self.assertContainsRe(d, "=== renamed file 'file' => 'newname'\n")
557
self.assertContainsRe(d, '--- old/file\t')
558
self.assertContainsRe(d, '\\+\\+\\+ new/newname\t')
559
self.assertContainsRe(d, '-contents\n'
563
def test_internal_diff_exec_property(self):
564
tree = self.make_branch_and_tree('tree')
566
tt = transform.TreeTransform(tree)
567
tt.new_file('a', tt.root, 'contents\n', 'a-id', True)
568
tt.new_file('b', tt.root, 'contents\n', 'b-id', False)
569
tt.new_file('c', tt.root, 'contents\n', 'c-id', True)
570
tt.new_file('d', tt.root, 'contents\n', 'd-id', False)
571
tt.new_file('e', tt.root, 'contents\n', 'control-e-id', True)
572
tt.new_file('f', tt.root, 'contents\n', 'control-f-id', False)
574
tree.commit('one', rev_id=b'rev-1')
576
tt = transform.TreeTransform(tree)
577
tt.set_executability(False, tt.trans_id_file_id('a-id'))
578
tt.set_executability(True, tt.trans_id_file_id('b-id'))
579
tt.set_executability(False, tt.trans_id_file_id('c-id'))
580
tt.set_executability(True, tt.trans_id_file_id('d-id'))
582
tree.rename_one('c', 'new-c')
583
tree.rename_one('d', 'new-d')
585
d = get_diff_as_string(tree.basis_tree(), tree)
587
self.assertContainsRe(d, r"file 'a'.*\(properties changed:"
589
self.assertContainsRe(d, r"file 'b'.*\(properties changed:"
591
self.assertContainsRe(d, r"file 'c'.*\(properties changed:"
593
self.assertContainsRe(d, r"file 'd'.*\(properties changed:"
595
self.assertNotContainsRe(d, r"file 'e'")
596
self.assertNotContainsRe(d, r"file 'f'")
598
def test_binary_unicode_filenames(self):
599
"""Test that contents of files are *not* encoded in UTF-8 when there
600
is a binary file in the diff.
602
# See https://bugs.launchpad.net/bugs/110092.
603
self.requireFeature(features.UnicodeFilenameFeature)
605
tree = self.make_branch_and_tree('tree')
606
alpha, omega = u'\u03b1', u'\u03c9'
607
alpha_utf8, omega_utf8 = alpha.encode('utf8'), omega.encode('utf8')
608
self.build_tree_contents(
609
[('tree/' + alpha, chr(0)),
611
('The %s and the %s\n' % (alpha_utf8, omega_utf8)))])
612
tree.add([alpha], [b'file-id'])
613
tree.add([omega], [b'file-id-2'])
614
diff_content = StubO()
615
diff.show_diff_trees(tree.basis_tree(), tree, diff_content)
616
diff_content.check_types(self, bytes)
617
d = b''.join(diff_content.write_record)
618
self.assertContainsRe(d, r"=== added file '%s'" % alpha_utf8)
619
self.assertContainsRe(d, "Binary files a/%s.*and b/%s.* differ\n"
620
% (alpha_utf8, alpha_utf8))
621
self.assertContainsRe(d, r"=== added file '%s'" % omega_utf8)
622
self.assertContainsRe(d, r"--- a/%s" % (omega_utf8,))
623
self.assertContainsRe(d, r"\+\+\+ b/%s" % (omega_utf8,))
625
def test_unicode_filename(self):
626
"""Test when the filename are unicode."""
627
self.requireFeature(features.UnicodeFilenameFeature)
629
alpha, omega = u'\u03b1', u'\u03c9'
630
autf8, outf8 = alpha.encode('utf8'), omega.encode('utf8')
632
tree = self.make_branch_and_tree('tree')
633
self.build_tree_contents([('tree/ren_'+alpha, b'contents\n')])
634
tree.add(['ren_'+alpha], [b'file-id-2'])
635
self.build_tree_contents([('tree/del_'+alpha, b'contents\n')])
636
tree.add(['del_'+alpha], [b'file-id-3'])
637
self.build_tree_contents([('tree/mod_'+alpha, b'contents\n')])
638
tree.add(['mod_'+alpha], [b'file-id-4'])
640
tree.commit('one', rev_id=b'rev-1')
642
tree.rename_one('ren_'+alpha, 'ren_'+omega)
643
tree.remove('del_'+alpha)
644
self.build_tree_contents([('tree/add_'+alpha, b'contents\n')])
645
tree.add(['add_'+alpha], [b'file-id'])
646
self.build_tree_contents([('tree/mod_'+alpha, b'contents_mod\n')])
648
d = get_diff_as_string(tree.basis_tree(), tree)
649
self.assertContainsRe(d,
650
"=== renamed file 'ren_%s' => 'ren_%s'\n"%(autf8, outf8))
651
self.assertContainsRe(d, "=== added file 'add_%s'"%autf8)
652
self.assertContainsRe(d, "=== modified file 'mod_%s'"%autf8)
653
self.assertContainsRe(d, "=== removed file 'del_%s'"%autf8)
655
def test_unicode_filename_path_encoding(self):
656
"""Test for bug #382699: unicode filenames on Windows should be shown
659
self.requireFeature(features.UnicodeFilenameFeature)
660
# The word 'test' in Russian
661
_russian_test = u'\u0422\u0435\u0441\u0442'
662
directory = _russian_test + u'/'
663
test_txt = _russian_test + u'.txt'
664
u1234 = u'\u1234.txt'
666
tree = self.make_branch_and_tree('.')
667
self.build_tree_contents([
668
(test_txt, b'foo\n'),
672
tree.add([test_txt, u1234, directory])
675
diff.show_diff_trees(tree.basis_tree(), tree, sio,
676
path_encoding='cp1251')
678
output = subst_dates(sio.getvalue())
680
=== added directory '%(directory)s'
681
=== added file '%(test_txt)s'
682
--- a/%(test_txt)s\tYYYY-MM-DD HH:MM:SS +ZZZZ
683
+++ b/%(test_txt)s\tYYYY-MM-DD HH:MM:SS +ZZZZ
687
=== added file '?.txt'
688
--- a/?.txt\tYYYY-MM-DD HH:MM:SS +ZZZZ
689
+++ b/?.txt\tYYYY-MM-DD HH:MM:SS +ZZZZ
693
''' % {'directory': _russian_test.encode('cp1251'),
694
'test_txt': test_txt.encode('cp1251'),
696
self.assertEqualDiff(output, shouldbe)
699
class DiffWasIs(diff.DiffPath):
701
def diff(self, file_id, old_path, new_path, old_kind, new_kind):
702
self.to_file.write('was: ')
703
self.to_file.write(self.old_tree.get_file(old_path).read())
704
self.to_file.write('is: ')
705
self.to_file.write(self.new_tree.get_file(new_path).read())
708
class TestDiffTree(tests.TestCaseWithTransport):
711
super(TestDiffTree, self).setUp()
712
self.old_tree = self.make_branch_and_tree('old-tree')
713
self.old_tree.lock_write()
714
self.addCleanup(self.old_tree.unlock)
715
self.new_tree = self.make_branch_and_tree('new-tree')
716
self.new_tree.lock_write()
717
self.addCleanup(self.new_tree.unlock)
718
self.differ = diff.DiffTree(self.old_tree, self.new_tree, BytesIO())
720
def test_diff_text(self):
721
self.build_tree_contents([('old-tree/olddir/',),
722
('old-tree/olddir/oldfile', b'old\n')])
723
self.old_tree.add('olddir')
724
self.old_tree.add('olddir/oldfile', b'file-id')
725
self.build_tree_contents([('new-tree/newdir/',),
726
('new-tree/newdir/newfile', b'new\n')])
727
self.new_tree.add('newdir')
728
self.new_tree.add('newdir/newfile', b'file-id')
729
differ = diff.DiffText(self.old_tree, self.new_tree, BytesIO())
730
differ.diff_text('olddir/oldfile', None, 'old label',
731
'new label', b'file-id', None)
733
'--- 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', None, b'file-id')
739
'--- 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', b'file-id', b'file-id')
745
'--- 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(), '-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(), r'\+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("=== 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("=== 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("=== 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('file-id', 'olddir/oldfile', 'newdir/newfile')
792
self.assertContainsRe(
793
self.differ.to_file.getvalue(),
794
r'--- olddir/oldfile.*\n\+\+\+ newdir/newfile.*\n\@\@ -1,1 \+1,1'
795
r' \@\@\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('file-id', 'olddir/oldfile', 'newdir/newfile')
808
self.assertContainsRe(
809
self.differ.to_file.getvalue(),
810
r'--- olddir/oldfile.*\n\+\+\+ newdir/newfile.*\n\@\@ -1,1 \+0,0'
812
self.assertContainsRe(self.differ.to_file.getvalue(),
813
"=== target is u'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(b'new-dir-id', None, 'new-dir')
819
self.assertEqual(self.differ.to_file.getvalue(), '')
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('file-id', 'olddir/oldfile', 'newdir/newfile')
841
self.assertNotContainsRe(
842
differ.to_file.getvalue(),
843
r'--- olddir/oldfile.*\n\+\+\+ newdir/newfile.*\n\@\@ -1,1 \+1,1'
844
r' \@\@\n-old\n\+new\n\n')
845
self.assertContainsRe(differ.to_file.getvalue(),
846
'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('file-id', 'olddir/oldfile', 'newdir/newfile')
853
self.assertNotContainsRe(
854
differ.to_file.getvalue(),
855
r'--- olddir/oldfile.*\n\+\+\+ newdir/newfile.*\n\@\@ -1,1 \+1,1'
856
r' \@\@\n-old\n\+new\n\n')
857
self.assertContainsRe(differ.to_file.getvalue(),
858
'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
'.*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'), [(2, 0), (3, 1), (4, 2)])
895
self.assertEqual(unique_lcs('cdeab', 'abcde'), [(0, 2), (1, 3), (2, 4)])
896
self.assertEqual(unique_lcs('abXde', 'abYde'), [(0, 0), (1, 1),
898
self.assertEqual(unique_lcs('acbac', 'abc'), [(2, 1)])
900
def test_recurse_matches(self):
901
def test_one(a, b, matches):
903
self._recurse_matches(
904
a, b, 0, 0, len(a), len(b), test_matches, 10)
905
self.assertEqual(test_matches, matches)
907
test_one(['a', '', 'b', '', 'c'], ['a', 'a', 'b', 'c', 'c'],
908
[(0, 0), (2, 2), (4, 4)])
909
test_one(['a', 'c', 'b', 'a', 'c'], ['a', 'b', 'c'],
910
[(0, 0), (2, 1), (4, 2)])
911
# Even though 'bc' is not unique globally, and is surrounded by
912
# non-matching lines, we should still match, because they are locally
914
test_one('abcdbce', 'afbcgdbce', [(0, 0), (1, 2), (2, 3), (3, 5),
915
(4, 6), (5, 7), (6, 8)])
917
# recurse_matches doesn't match non-unique
918
# lines surrounded by bogus text.
919
# The update has been done in patiencediff.SequenceMatcher instead
921
# This is what it could be
922
#test_one('aBccDe', 'abccde', [(0,0), (2,2), (3,3), (5,5)])
924
# This is what it currently gives:
925
test_one('aBccDe', 'abccde', [(0, 0), (5, 5)])
927
def assertDiffBlocks(self, a, b, expected_blocks):
928
"""Check that the sequence matcher returns the correct blocks.
930
:param a: A sequence to match
931
:param b: Another sequence to match
932
:param expected_blocks: The expected output, not including the final
933
matching block (len(a), len(b), 0)
935
matcher = self._PatienceSequenceMatcher(None, a, b)
936
blocks = matcher.get_matching_blocks()
938
self.assertEqual((len(a), len(b), 0), last)
939
self.assertEqual(expected_blocks, blocks)
941
def test_matching_blocks(self):
942
# Some basic matching tests
943
self.assertDiffBlocks('', '', [])
944
self.assertDiffBlocks([], [], [])
945
self.assertDiffBlocks('abc', '', [])
946
self.assertDiffBlocks('', 'abc', [])
947
self.assertDiffBlocks('abcd', 'abcd', [(0, 0, 4)])
948
self.assertDiffBlocks('abcd', 'abce', [(0, 0, 3)])
949
self.assertDiffBlocks('eabc', 'abce', [(1, 0, 3)])
950
self.assertDiffBlocks('eabce', 'abce', [(1, 0, 4)])
951
self.assertDiffBlocks('abcde', 'abXde', [(0, 0, 2), (3, 3, 2)])
952
self.assertDiffBlocks('abcde', 'abXYZde', [(0, 0, 2), (3, 5, 2)])
953
self.assertDiffBlocks('abde', 'abXYZde', [(0, 0, 2), (2, 5, 2)])
954
# This may check too much, but it checks to see that
955
# a copied block stays attached to the previous section,
957
# difflib would tend to grab the trailing longest match
958
# which would make the diff not look right
959
self.assertDiffBlocks('abcdefghijklmnop', 'abcdefxydefghijklmnop',
960
[(0, 0, 6), (6, 11, 10)])
962
# make sure it supports passing in lists
963
self.assertDiffBlocks(
966
'how are you today?\n'],
968
'how are you today?\n'],
969
[(0, 0, 1), (2, 1, 1)])
971
# non unique lines surrounded by non-matching lines
973
self.assertDiffBlocks('aBccDe', 'abccde', [(0, 0, 1), (5, 5, 1)])
975
# But they only need to be locally unique
976
self.assertDiffBlocks('aBcDec', 'abcdec', [(0, 0, 1), (2, 2, 1), (4, 4, 2)])
978
# non unique blocks won't be matched
979
self.assertDiffBlocks('aBcdEcdFg', 'abcdecdfg', [(0, 0, 1), (8, 8, 1)])
981
# but locally unique ones will
982
self.assertDiffBlocks('aBcdEeXcdFg', 'abcdecdfg', [(0, 0, 1), (2, 2, 2),
983
(5, 4, 1), (7, 5, 2), (10, 8, 1)])
985
self.assertDiffBlocks('abbabbXd', 'cabbabxd', [(7, 7, 1)])
986
self.assertDiffBlocks('abbabbbb', 'cabbabbc', [])
987
self.assertDiffBlocks('bbbbbbbb', 'cbbbbbbc', [])
989
def test_matching_blocks_tuples(self):
990
# Some basic matching tests
991
self.assertDiffBlocks([], [], [])
992
self.assertDiffBlocks([('a',), ('b',), ('c,')], [], [])
993
self.assertDiffBlocks([], [('a',), ('b',), ('c,')], [])
994
self.assertDiffBlocks([('a',), ('b',), ('c,')],
995
[('a',), ('b',), ('c,')],
997
self.assertDiffBlocks([('a',), ('b',), ('c,')],
998
[('a',), ('b',), ('d,')],
1000
self.assertDiffBlocks([('d',), ('b',), ('c,')],
1001
[('a',), ('b',), ('c,')],
1003
self.assertDiffBlocks([('d',), ('a',), ('b',), ('c,')],
1004
[('a',), ('b',), ('c,')],
1006
self.assertDiffBlocks([('a', 'b'), ('c', 'd'), ('e', 'f')],
1007
[('a', 'b'), ('c', 'X'), ('e', 'f')],
1008
[(0, 0, 1), (2, 2, 1)])
1009
self.assertDiffBlocks([('a', 'b'), ('c', 'd'), ('e', 'f')],
1010
[('a', 'b'), ('c', 'dX'), ('e', 'f')],
1011
[(0, 0, 1), (2, 2, 1)])
1013
def test_opcodes(self):
1014
def chk_ops(a, b, expected_codes):
1015
s = self._PatienceSequenceMatcher(None, a, b)
1016
self.assertEqual(expected_codes, s.get_opcodes())
1020
chk_ops('abc', '', [('delete', 0, 3, 0, 0)])
1021
chk_ops('', 'abc', [('insert', 0, 0, 0, 3)])
1022
chk_ops('abcd', 'abcd', [('equal', 0, 4, 0, 4)])
1023
chk_ops('abcd', 'abce', [('equal', 0, 3, 0, 3),
1024
('replace', 3, 4, 3, 4)
1026
chk_ops('eabc', 'abce', [('delete', 0, 1, 0, 0),
1027
('equal', 1, 4, 0, 3),
1028
('insert', 4, 4, 3, 4)
1030
chk_ops('eabce', 'abce', [('delete', 0, 1, 0, 0),
1031
('equal', 1, 5, 0, 4)
1033
chk_ops('abcde', 'abXde', [('equal', 0, 2, 0, 2),
1034
('replace', 2, 3, 2, 3),
1035
('equal', 3, 5, 3, 5)
1037
chk_ops('abcde', 'abXYZde', [('equal', 0, 2, 0, 2),
1038
('replace', 2, 3, 2, 5),
1039
('equal', 3, 5, 5, 7)
1041
chk_ops('abde', 'abXYZde', [('equal', 0, 2, 0, 2),
1042
('insert', 2, 2, 2, 5),
1043
('equal', 2, 4, 5, 7)
1045
chk_ops('abcdefghijklmnop', 'abcdefxydefghijklmnop',
1046
[('equal', 0, 6, 0, 6),
1047
('insert', 6, 6, 6, 11),
1048
('equal', 6, 16, 11, 21)
1053
, 'how are you today?\n'],
1055
, '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)
1126
def test_multiple_ranges(self):
1127
# There was an earlier bug where we used a bad set of ranges,
1128
# this triggers that specific bug, to make sure it doesn't regress
1129
self.assertDiffBlocks('abcdefghijklmnop',
1130
'abcXghiYZQRSTUVWXYZijklmnop',
1131
[(0, 0, 3), (6, 4, 3), (9, 20, 7)])
1133
self.assertDiffBlocks('ABCd efghIjk L',
1134
'AxyzBCn mo pqrstuvwI1 2 L',
1135
[(0, 0, 1), (1, 4, 2), (9, 19, 1), (12, 23, 3)])
1137
# These are rot13 code snippets.
1138
self.assertDiffBlocks('''\
1139
trg nqqrq jura lbh nqq n svyr va gur qverpgbel.
1141
gnxrf_netf = ['svyr*']
1142
gnxrf_bcgvbaf = ['ab-erphefr']
1144
qrs eha(frys, svyr_yvfg, ab_erphefr=Snyfr):
1145
sebz omeyvo.nqq vzcbeg fzneg_nqq, nqq_ercbegre_cevag, nqq_ercbegre_ahyy
1147
ercbegre = nqq_ercbegre_ahyy
1149
ercbegre = nqq_ercbegre_cevag
1150
fzneg_nqq(svyr_yvfg, abg ab_erphefr, ercbegre)
1153
pynff pzq_zxqve(Pbzznaq):
1154
'''.splitlines(True), '''\
1155
trg nqqrq jura lbh nqq n svyr va gur qverpgbel.
1157
--qel-eha jvyy fubj juvpu svyrf jbhyq or nqqrq, ohg abg npghnyyl
1160
gnxrf_netf = ['svyr*']
1161
gnxrf_bcgvbaf = ['ab-erphefr', 'qel-eha']
1163
qrs eha(frys, svyr_yvfg, ab_erphefr=Snyfr, qel_eha=Snyfr):
1168
# Guvf vf cbvagyrff, ohg V'q engure abg envfr na reebe
1169
npgvba = omeyvo.nqq.nqq_npgvba_ahyy
1171
npgvba = omeyvo.nqq.nqq_npgvba_cevag
1173
npgvba = omeyvo.nqq.nqq_npgvba_nqq
1175
npgvba = omeyvo.nqq.nqq_npgvba_nqq_naq_cevag
1177
omeyvo.nqq.fzneg_nqq(svyr_yvfg, abg ab_erphefr, npgvba)
1180
pynff pzq_zxqve(Pbzznaq):
1181
'''.splitlines(True)
1182
, [(0, 0, 1), (1, 4, 2), (9, 19, 1), (12, 23, 3)])
1184
def test_patience_unified_diff(self):
1185
txt_a = ['hello there\n',
1187
'how are you today?\n']
1188
txt_b = ['hello there\n',
1189
'how are you today?\n']
1190
unified_diff = patiencediff.unified_diff
1191
psm = self._PatienceSequenceMatcher
1192
self.assertEqual(['--- \n',
1194
'@@ -1,3 +1,2 @@\n',
1197
' how are you today?\n'
1199
, list(unified_diff(txt_a, txt_b,
1200
sequencematcher=psm)))
1201
txt_a = [x+'\n' for x in 'abcdefghijklmnop']
1202
txt_b = [x+'\n' for x in 'abcdefxydefghijklmnop']
1203
# This is the result with LongestCommonSubstring matching
1204
self.assertEqual(['--- \n',
1206
'@@ -1,6 +1,11 @@\n',
1218
, list(unified_diff(txt_a, txt_b)))
1219
# And the patience diff
1220
self.assertEqual(['--- \n',
1222
'@@ -4,6 +4,11 @@\n',
1235
, list(unified_diff(txt_a, txt_b,
1236
sequencematcher=psm)))
1238
def test_patience_unified_diff_with_dates(self):
1239
txt_a = ['hello there\n',
1241
'how are you today?\n']
1242
txt_b = ['hello there\n',
1243
'how are you today?\n']
1244
unified_diff = patiencediff.unified_diff
1245
psm = self._PatienceSequenceMatcher
1246
self.assertEqual(['--- a\t2008-08-08\n',
1247
'+++ b\t2008-09-09\n',
1248
'@@ -1,3 +1,2 @@\n',
1251
' how are you today?\n'
1253
, list(unified_diff(txt_a, txt_b,
1254
fromfile='a', tofile='b',
1255
fromfiledate='2008-08-08',
1256
tofiledate='2008-09-09',
1257
sequencematcher=psm)))
1260
class TestPatienceDiffLib_c(TestPatienceDiffLib):
1262
_test_needs_features = [features.compiled_patiencediff_feature]
1265
super(TestPatienceDiffLib_c, self).setUp()
1266
from breezy import _patiencediff_c
1267
self._unique_lcs = _patiencediff_c.unique_lcs_c
1268
self._recurse_matches = _patiencediff_c.recurse_matches_c
1269
self._PatienceSequenceMatcher = \
1270
_patiencediff_c.PatienceSequenceMatcher_c
1272
def test_unhashable(self):
1273
"""We should get a proper exception here."""
1274
# We need to be able to hash items in the sequence, lists are
1275
# unhashable, and thus cannot be diffed
1276
e = self.assertRaises(TypeError, self._PatienceSequenceMatcher,
1278
e = self.assertRaises(TypeError, self._PatienceSequenceMatcher,
1279
None, ['valid', []], [])
1280
e = self.assertRaises(TypeError, self._PatienceSequenceMatcher,
1281
None, ['valid'], [[]])
1282
e = self.assertRaises(TypeError, self._PatienceSequenceMatcher,
1283
None, ['valid'], ['valid', []])
1286
class TestPatienceDiffLibFiles(tests.TestCaseInTempDir):
1289
super(TestPatienceDiffLibFiles, self).setUp()
1290
self._PatienceSequenceMatcher = \
1291
_patiencediff_py.PatienceSequenceMatcher_py
1293
def test_patience_unified_diff_files(self):
1294
txt_a = ['hello there\n',
1296
'how are you today?\n']
1297
txt_b = ['hello there\n',
1298
'how are you today?\n']
1299
with open('a1', 'wb') as f: f.writelines(txt_a)
1300
with open('b1', 'wb') as f: f.writelines(txt_b)
1302
unified_diff_files = patiencediff.unified_diff_files
1303
psm = self._PatienceSequenceMatcher
1304
self.assertEqual(['--- a1\n',
1306
'@@ -1,3 +1,2 @@\n',
1309
' how are you today?\n',
1311
, list(unified_diff_files('a1', 'b1',
1312
sequencematcher=psm)))
1314
txt_a = [x+'\n' for x in 'abcdefghijklmnop']
1315
txt_b = [x+'\n' for x in 'abcdefxydefghijklmnop']
1316
with open('a2', 'wb') as f: f.writelines(txt_a)
1317
with open('b2', 'wb') as f: f.writelines(txt_b)
1319
# This is the result with LongestCommonSubstring matching
1320
self.assertEqual(['--- a2\n',
1322
'@@ -1,6 +1,11 @@\n',
1334
, list(unified_diff_files('a2', 'b2')))
1336
# And the patience diff
1337
self.assertEqual(['--- a2\n',
1339
'@@ -4,6 +4,11 @@\n',
1351
list(unified_diff_files('a2', 'b2',
1352
sequencematcher=psm)))
1355
class TestPatienceDiffLibFiles_c(TestPatienceDiffLibFiles):
1357
_test_needs_features = [features.compiled_patiencediff_feature]
1360
super(TestPatienceDiffLibFiles_c, self).setUp()
1361
from breezy import _patiencediff_c
1362
self._PatienceSequenceMatcher = \
1363
_patiencediff_c.PatienceSequenceMatcher_c
1366
class TestUsingCompiledIfAvailable(tests.TestCase):
1368
def test_PatienceSequenceMatcher(self):
1369
if features.compiled_patiencediff_feature.available():
1370
from breezy._patiencediff_c import PatienceSequenceMatcher_c
1371
self.assertIs(PatienceSequenceMatcher_c,
1372
patiencediff.PatienceSequenceMatcher)
1374
from breezy._patiencediff_py import PatienceSequenceMatcher_py
1375
self.assertIs(PatienceSequenceMatcher_py,
1376
patiencediff.PatienceSequenceMatcher)
1378
def test_unique_lcs(self):
1379
if features.compiled_patiencediff_feature.available():
1380
from breezy._patiencediff_c import unique_lcs_c
1381
self.assertIs(unique_lcs_c,
1382
patiencediff.unique_lcs)
1384
from breezy._patiencediff_py import unique_lcs_py
1385
self.assertIs(unique_lcs_py,
1386
patiencediff.unique_lcs)
1388
def test_recurse_matches(self):
1389
if features.compiled_patiencediff_feature.available():
1390
from breezy._patiencediff_c import recurse_matches_c
1391
self.assertIs(recurse_matches_c,
1392
patiencediff.recurse_matches)
1394
from breezy._patiencediff_py import recurse_matches_py
1395
self.assertIs(recurse_matches_py,
1396
patiencediff.recurse_matches)
1399
class TestDiffFromTool(tests.TestCaseWithTransport):
1401
def test_from_string(self):
1402
diff_obj = diff.DiffFromTool.from_string('diff', None, None, None)
1403
self.addCleanup(diff_obj.finish)
1404
self.assertEqual(['diff', '@old_path', '@new_path'],
1405
diff_obj.command_template)
1407
def test_from_string_u5(self):
1408
diff_obj = diff.DiffFromTool.from_string('diff "-u 5"',
1410
self.addCleanup(diff_obj.finish)
1411
self.assertEqual(['diff', '-u 5', '@old_path', '@new_path'],
1412
diff_obj.command_template)
1413
self.assertEqual(['diff', '-u 5', 'old-path', 'new-path'],
1414
diff_obj._get_command('old-path', 'new-path'))
1416
def test_from_string_path_with_backslashes(self):
1417
self.requireFeature(features.backslashdir_feature)
1418
tool = 'C:\\Tools\\Diff.exe'
1419
diff_obj = diff.DiffFromTool.from_string(tool, None, None, None)
1420
self.addCleanup(diff_obj.finish)
1421
self.assertEqual(['C:\\Tools\\Diff.exe', '@old_path', '@new_path'],
1422
diff_obj.command_template)
1423
self.assertEqual(['C:\\Tools\\Diff.exe', 'old-path', 'new-path'],
1424
diff_obj._get_command('old-path', 'new-path'))
1426
def test_execute(self):
1428
diff_obj = diff.DiffFromTool(['python', '-c',
1429
'print "@old_path @new_path"'],
1431
self.addCleanup(diff_obj.finish)
1432
diff_obj._execute('old', 'new')
1433
self.assertEqual(output.getvalue().rstrip(), 'old new')
1435
def test_execute_missing(self):
1436
diff_obj = diff.DiffFromTool(['a-tool-which-is-unlikely-to-exist'],
1438
self.addCleanup(diff_obj.finish)
1439
e = self.assertRaises(errors.ExecutableMissing, diff_obj._execute,
1441
self.assertEqual('a-tool-which-is-unlikely-to-exist could not be found'
1442
' on this machine', str(e))
1444
def test_prepare_files_creates_paths_readable_by_windows_tool(self):
1445
self.requireFeature(features.AttribFeature)
1447
tree = self.make_branch_and_tree('tree')
1448
self.build_tree_contents([('tree/file', b'content')])
1449
tree.add('file', b'file-id')
1450
tree.commit('old tree')
1452
self.addCleanup(tree.unlock)
1453
basis_tree = tree.basis_tree()
1454
basis_tree.lock_read()
1455
self.addCleanup(basis_tree.unlock)
1456
diff_obj = diff.DiffFromTool(['python', '-c',
1457
'print "@old_path @new_path"'],
1458
basis_tree, tree, output)
1459
diff_obj._prepare_files('file', 'file', file_id=b'file-id')
1460
# The old content should be readonly
1461
self.assertReadableByAttrib(diff_obj._root, 'old\\file',
1463
# The new content should use the tree object, not a 'new' file anymore
1464
self.assertEndsWith(tree.basedir, 'work/tree')
1465
self.assertReadableByAttrib(tree.basedir, 'file', r'work\\tree\\file$')
1467
def assertReadableByAttrib(self, cwd, relpath, regex):
1468
proc = subprocess.Popen(['attrib', relpath],
1469
stdout=subprocess.PIPE,
1471
(result, err) = proc.communicate()
1472
self.assertContainsRe(result.replace('\r\n', '\n'), regex)
1474
def test_prepare_files(self):
1476
tree = self.make_branch_and_tree('tree')
1477
self.build_tree_contents([('tree/oldname', b'oldcontent')])
1478
self.build_tree_contents([('tree/oldname2', b'oldcontent2')])
1479
tree.add('oldname', b'file-id')
1480
tree.add('oldname2', b'file2-id')
1481
# Earliest allowable date on FAT32 filesystems is 1980-01-01
1482
tree.commit('old tree', timestamp=315532800)
1483
tree.rename_one('oldname', 'newname')
1484
tree.rename_one('oldname2', 'newname2')
1485
self.build_tree_contents([('tree/newname', b'newcontent')])
1486
self.build_tree_contents([('tree/newname2', b'newcontent2')])
1487
old_tree = tree.basis_tree()
1488
old_tree.lock_read()
1489
self.addCleanup(old_tree.unlock)
1491
self.addCleanup(tree.unlock)
1492
diff_obj = diff.DiffFromTool(['python', '-c',
1493
'print "@old_path @new_path"'],
1494
old_tree, tree, output)
1495
self.addCleanup(diff_obj.finish)
1496
self.assertContainsRe(diff_obj._root, 'brz-diff-[^/]*')
1497
old_path, new_path = diff_obj._prepare_files(
1498
'oldname', 'newname', file_id='file-id')
1499
self.assertContainsRe(old_path, 'old/oldname$')
1500
self.assertEqual(315532800, os.stat(old_path).st_mtime)
1501
self.assertContainsRe(new_path, 'tree/newname$')
1502
self.assertFileEqual('oldcontent', old_path)
1503
self.assertFileEqual('newcontent', new_path)
1504
if osutils.host_os_dereferences_symlinks():
1505
self.assertTrue(os.path.samefile('tree/newname', new_path))
1506
# make sure we can create files with the same parent directories
1507
diff_obj._prepare_files('oldname2', 'newname2', file_id='file2-id')
1510
class TestDiffFromToolEncodedFilename(tests.TestCaseWithTransport):
1512
def test_encodable_filename(self):
1513
# Just checks file path for external diff tool.
1514
# We cannot change CPython's internal encoding used by os.exec*.
1515
diffobj = diff.DiffFromTool(['dummy', '@old_path', '@new_path'],
1517
for _, scenario in EncodingAdapter.encoding_scenarios:
1518
encoding = scenario['encoding']
1519
dirname = scenario['info']['directory']
1520
filename = scenario['info']['filename']
1522
self.overrideAttr(diffobj, '_fenc', lambda: encoding)
1523
relpath = dirname + u'/' + filename
1524
fullpath = diffobj._safe_filename('safe', relpath)
1525
self.assertEqual(fullpath,
1526
fullpath.encode(encoding).decode(encoding))
1527
self.assertTrue(fullpath.startswith(diffobj._root + '/safe'))
1529
def test_unencodable_filename(self):
1530
diffobj = diff.DiffFromTool(['dummy', '@old_path', '@new_path'],
1532
for _, scenario in EncodingAdapter.encoding_scenarios:
1533
encoding = scenario['encoding']
1534
dirname = scenario['info']['directory']
1535
filename = scenario['info']['filename']
1537
if encoding == 'iso-8859-1':
1538
encoding = 'iso-8859-2'
1540
encoding = 'iso-8859-1'
1542
self.overrideAttr(diffobj, '_fenc', lambda: encoding)
1543
relpath = dirname + u'/' + filename
1544
fullpath = diffobj._safe_filename('safe', relpath)
1545
self.assertEqual(fullpath,
1546
fullpath.encode(encoding).decode(encoding))
1547
self.assertTrue(fullpath.startswith(diffobj._root + '/safe'))
1550
class TestGetTreesAndBranchesToDiffLocked(tests.TestCaseWithTransport):
1552
def call_gtabtd(self, path_list, revision_specs, old_url, new_url):
1553
"""Call get_trees_and_branches_to_diff_locked."""
1554
return diff.get_trees_and_branches_to_diff_locked(
1555
path_list, revision_specs, old_url, new_url, self.addCleanup)
1557
def test_basic(self):
1558
tree = self.make_branch_and_tree('tree')
1559
(old_tree, new_tree,
1560
old_branch, new_branch,
1561
specific_files, extra_trees) = self.call_gtabtd(
1562
['tree'], None, None, None)
1564
self.assertIsInstance(old_tree, revisiontree.RevisionTree)
1565
self.assertEqual(_mod_revision.NULL_REVISION,
1566
old_tree.get_revision_id())
1567
self.assertEqual(tree.basedir, new_tree.basedir)
1568
self.assertEqual(tree.branch.base, old_branch.base)
1569
self.assertEqual(tree.branch.base, new_branch.base)
1570
self.assertIs(None, specific_files)
1571
self.assertIs(None, extra_trees)
1573
def test_with_rev_specs(self):
1574
tree = self.make_branch_and_tree('tree')
1575
self.build_tree_contents([('tree/file', b'oldcontent')])
1576
tree.add('file', b'file-id')
1577
tree.commit('old tree', timestamp=0, rev_id=b"old-id")
1578
self.build_tree_contents([('tree/file', b'newcontent')])
1579
tree.commit('new tree', timestamp=0, rev_id="new-id")
1581
revisions = [revisionspec.RevisionSpec.from_string('1'),
1582
revisionspec.RevisionSpec.from_string('2')]
1583
(old_tree, new_tree,
1584
old_branch, new_branch,
1585
specific_files, extra_trees) = self.call_gtabtd(
1586
['tree'], revisions, None, None)
1588
self.assertIsInstance(old_tree, revisiontree.RevisionTree)
1589
self.assertEqual("old-id", old_tree.get_revision_id())
1590
self.assertIsInstance(new_tree, revisiontree.RevisionTree)
1591
self.assertEqual("new-id", new_tree.get_revision_id())
1592
self.assertEqual(tree.branch.base, old_branch.base)
1593
self.assertEqual(tree.branch.base, new_branch.base)
1594
self.assertIs(None, specific_files)
1595
self.assertEqual(tree.basedir, extra_trees[0].basedir)