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', 'file1 contents at rev 1\n'),
359
('file2', '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', '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', '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', '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', 'contents\n')])
487
tree.add(['file'], ['file-id'])
488
tree.commit('one', rev_id='rev-1')
490
self.build_tree_contents([('tree/file', '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', 'contents\n')])
503
tree.add(['dir', 'dir/file'], ['dir-id', 'file-id'])
504
tree.commit('one', rev_id='rev-1')
506
tree.rename_one('dir', 'other')
507
self.build_tree_contents([('tree/other/file', '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', 'contents\n')])
523
tree.add(['dir', 'dir/file'], ['dir-id', 'file-id'])
524
tree.commit('one', rev_id='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', 'contents\n')])
536
tree.add(['file'], ['file-id'])
537
tree.commit('one', rev_id='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', 'contents\n')])
550
tree.add(['file'], ['file-id'])
551
tree.commit('one', rev_id='rev-1')
553
tree.rename_one('file', 'newname')
554
self.build_tree_contents([('tree/newname', '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='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], ['file-id'])
613
tree.add([omega], ['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, 'contents\n')])
634
tree.add(['ren_'+alpha], ['file-id-2'])
635
self.build_tree_contents([('tree/del_'+alpha, 'contents\n')])
636
tree.add(['del_'+alpha], ['file-id-3'])
637
self.build_tree_contents([('tree/mod_'+alpha, 'contents\n')])
638
tree.add(['mod_'+alpha], ['file-id-4'])
640
tree.commit('one', rev_id='rev-1')
642
tree.rename_one('ren_'+alpha, 'ren_'+omega)
643
tree.remove('del_'+alpha)
644
self.build_tree_contents([('tree/add_'+alpha, 'contents\n')])
645
tree.add(['add_'+alpha], ['file-id'])
646
self.build_tree_contents([('tree/mod_'+alpha, '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([
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(file_id).read())
704
self.to_file.write('is: ')
705
self.to_file.write(self.new_tree.get_file(file_id).read())
709
class TestDiffTree(tests.TestCaseWithTransport):
712
super(TestDiffTree, self).setUp()
713
self.old_tree = self.make_branch_and_tree('old-tree')
714
self.old_tree.lock_write()
715
self.addCleanup(self.old_tree.unlock)
716
self.new_tree = self.make_branch_and_tree('new-tree')
717
self.new_tree.lock_write()
718
self.addCleanup(self.new_tree.unlock)
719
self.differ = diff.DiffTree(self.old_tree, self.new_tree, BytesIO())
721
def test_diff_text(self):
722
self.build_tree_contents([('old-tree/olddir/',),
723
('old-tree/olddir/oldfile', 'old\n')])
724
self.old_tree.add('olddir')
725
self.old_tree.add('olddir/oldfile', 'file-id')
726
self.build_tree_contents([('new-tree/newdir/',),
727
('new-tree/newdir/newfile', 'new\n')])
728
self.new_tree.add('newdir')
729
self.new_tree.add('newdir/newfile', 'file-id')
730
differ = diff.DiffText(self.old_tree, self.new_tree, BytesIO())
731
differ.diff_text('file-id', None, 'old label', 'new label')
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, 'file-id', 'old label', 'new label')
738
'--- old label\n+++ new label\n@@ -0,0 +1,1 @@\n+new\n\n',
739
differ.to_file.getvalue())
740
differ.to_file.seek(0)
741
differ.diff_text('file-id', 'file-id', 'old label', 'new label')
743
'--- old label\n+++ new label\n@@ -1,1 +1,1 @@\n-old\n+new\n\n',
744
differ.to_file.getvalue())
746
def test_diff_deletion(self):
747
self.build_tree_contents([('old-tree/file', 'contents'),
748
('new-tree/file', 'contents')])
749
self.old_tree.add('file', 'file-id')
750
self.new_tree.add('file', 'file-id')
751
os.unlink('new-tree/file')
752
self.differ.show_diff(None)
753
self.assertContainsRe(self.differ.to_file.getvalue(), '-contents')
755
def test_diff_creation(self):
756
self.build_tree_contents([('old-tree/file', 'contents'),
757
('new-tree/file', 'contents')])
758
self.old_tree.add('file', 'file-id')
759
self.new_tree.add('file', 'file-id')
760
os.unlink('old-tree/file')
761
self.differ.show_diff(None)
762
self.assertContainsRe(self.differ.to_file.getvalue(), r'\+contents')
764
def test_diff_symlink(self):
765
differ = diff.DiffSymlink(self.old_tree, self.new_tree, BytesIO())
766
differ.diff_symlink('old target', None)
767
self.assertEqual("=== target was 'old target'\n",
768
differ.to_file.getvalue())
770
differ = diff.DiffSymlink(self.old_tree, self.new_tree, BytesIO())
771
differ.diff_symlink(None, 'new target')
772
self.assertEqual("=== target is 'new target'\n",
773
differ.to_file.getvalue())
775
differ = diff.DiffSymlink(self.old_tree, self.new_tree, BytesIO())
776
differ.diff_symlink('old target', 'new target')
777
self.assertEqual("=== target changed 'old target' => 'new target'\n",
778
differ.to_file.getvalue())
781
self.build_tree_contents([('old-tree/olddir/',),
782
('old-tree/olddir/oldfile', 'old\n')])
783
self.old_tree.add('olddir')
784
self.old_tree.add('olddir/oldfile', 'file-id')
785
self.build_tree_contents([('new-tree/newdir/',),
786
('new-tree/newdir/newfile', 'new\n')])
787
self.new_tree.add('newdir')
788
self.new_tree.add('newdir/newfile', 'file-id')
789
self.differ.diff('file-id', 'olddir/oldfile', 'newdir/newfile')
790
self.assertContainsRe(
791
self.differ.to_file.getvalue(),
792
r'--- olddir/oldfile.*\n\+\+\+ newdir/newfile.*\n\@\@ -1,1 \+1,1'
793
r' \@\@\n-old\n\+new\n\n')
795
def test_diff_kind_change(self):
796
self.requireFeature(features.SymlinkFeature)
797
self.build_tree_contents([('old-tree/olddir/',),
798
('old-tree/olddir/oldfile', 'old\n')])
799
self.old_tree.add('olddir')
800
self.old_tree.add('olddir/oldfile', 'file-id')
801
self.build_tree(['new-tree/newdir/'])
802
os.symlink('new', 'new-tree/newdir/newfile')
803
self.new_tree.add('newdir')
804
self.new_tree.add('newdir/newfile', 'file-id')
805
self.differ.diff('file-id', 'olddir/oldfile', 'newdir/newfile')
806
self.assertContainsRe(
807
self.differ.to_file.getvalue(),
808
r'--- olddir/oldfile.*\n\+\+\+ newdir/newfile.*\n\@\@ -1,1 \+0,0'
810
self.assertContainsRe(self.differ.to_file.getvalue(),
811
"=== target is u'new'\n")
813
def test_diff_directory(self):
814
self.build_tree(['new-tree/new-dir/'])
815
self.new_tree.add('new-dir', 'new-dir-id')
816
self.differ.diff('new-dir-id', None, 'new-dir')
817
self.assertEqual(self.differ.to_file.getvalue(), '')
819
def create_old_new(self):
820
self.build_tree_contents([('old-tree/olddir/',),
821
('old-tree/olddir/oldfile', 'old\n')])
822
self.old_tree.add('olddir')
823
self.old_tree.add('olddir/oldfile', 'file-id')
824
self.build_tree_contents([('new-tree/newdir/',),
825
('new-tree/newdir/newfile', 'new\n')])
826
self.new_tree.add('newdir')
827
self.new_tree.add('newdir/newfile', 'file-id')
829
def test_register_diff(self):
830
self.create_old_new()
831
old_diff_factories = diff.DiffTree.diff_factories
832
diff.DiffTree.diff_factories=old_diff_factories[:]
833
diff.DiffTree.diff_factories.insert(0, DiffWasIs.from_diff_tree)
835
differ = diff.DiffTree(self.old_tree, self.new_tree, BytesIO())
837
diff.DiffTree.diff_factories = old_diff_factories
838
differ.diff('file-id', 'olddir/oldfile', 'newdir/newfile')
839
self.assertNotContainsRe(
840
differ.to_file.getvalue(),
841
r'--- olddir/oldfile.*\n\+\+\+ newdir/newfile.*\n\@\@ -1,1 \+1,1'
842
r' \@\@\n-old\n\+new\n\n')
843
self.assertContainsRe(differ.to_file.getvalue(),
844
'was: old\nis: new\n')
846
def test_extra_factories(self):
847
self.create_old_new()
848
differ = diff.DiffTree(self.old_tree, self.new_tree, BytesIO(),
849
extra_factories=[DiffWasIs.from_diff_tree])
850
differ.diff('file-id', 'olddir/oldfile', 'newdir/newfile')
851
self.assertNotContainsRe(
852
differ.to_file.getvalue(),
853
r'--- olddir/oldfile.*\n\+\+\+ newdir/newfile.*\n\@\@ -1,1 \+1,1'
854
r' \@\@\n-old\n\+new\n\n')
855
self.assertContainsRe(differ.to_file.getvalue(),
856
'was: old\nis: new\n')
858
def test_alphabetical_order(self):
859
self.build_tree(['new-tree/a-file'])
860
self.new_tree.add('a-file')
861
self.build_tree(['old-tree/b-file'])
862
self.old_tree.add('b-file')
863
self.differ.show_diff(None)
864
self.assertContainsRe(self.differ.to_file.getvalue(),
865
'.*a-file(.|\n)*b-file')
868
class TestPatienceDiffLib(tests.TestCase):
871
super(TestPatienceDiffLib, self).setUp()
872
self._unique_lcs = _patiencediff_py.unique_lcs_py
873
self._recurse_matches = _patiencediff_py.recurse_matches_py
874
self._PatienceSequenceMatcher = \
875
_patiencediff_py.PatienceSequenceMatcher_py
877
def test_diff_unicode_string(self):
878
a = ''.join([unichr(i) for i in range(4000, 4500, 3)])
879
b = ''.join([unichr(i) for i in range(4300, 4800, 2)])
880
sm = self._PatienceSequenceMatcher(None, a, b)
881
mb = sm.get_matching_blocks()
882
self.assertEqual(35, len(mb))
884
def test_unique_lcs(self):
885
unique_lcs = self._unique_lcs
886
self.assertEqual(unique_lcs('', ''), [])
887
self.assertEqual(unique_lcs('', 'a'), [])
888
self.assertEqual(unique_lcs('a', ''), [])
889
self.assertEqual(unique_lcs('a', 'a'), [(0,0)])
890
self.assertEqual(unique_lcs('a', 'b'), [])
891
self.assertEqual(unique_lcs('ab', 'ab'), [(0,0), (1,1)])
892
self.assertEqual(unique_lcs('abcde', 'cdeab'), [(2,0), (3,1), (4,2)])
893
self.assertEqual(unique_lcs('cdeab', 'abcde'), [(0,2), (1,3), (2,4)])
894
self.assertEqual(unique_lcs('abXde', 'abYde'), [(0,0), (1,1),
896
self.assertEqual(unique_lcs('acbac', 'abc'), [(2,1)])
898
def test_recurse_matches(self):
899
def test_one(a, b, matches):
901
self._recurse_matches(
902
a, b, 0, 0, len(a), len(b), test_matches, 10)
903
self.assertEqual(test_matches, matches)
905
test_one(['a', '', 'b', '', 'c'], ['a', 'a', 'b', 'c', 'c'],
906
[(0, 0), (2, 2), (4, 4)])
907
test_one(['a', 'c', 'b', 'a', 'c'], ['a', 'b', 'c'],
908
[(0, 0), (2, 1), (4, 2)])
909
# Even though 'bc' is not unique globally, and is surrounded by
910
# non-matching lines, we should still match, because they are locally
912
test_one('abcdbce', 'afbcgdbce', [(0,0), (1, 2), (2, 3), (3, 5),
913
(4, 6), (5, 7), (6, 8)])
915
# recurse_matches doesn't match non-unique
916
# lines surrounded by bogus text.
917
# The update has been done in patiencediff.SequenceMatcher instead
919
# This is what it could be
920
#test_one('aBccDe', 'abccde', [(0,0), (2,2), (3,3), (5,5)])
922
# This is what it currently gives:
923
test_one('aBccDe', 'abccde', [(0,0), (5,5)])
925
def assertDiffBlocks(self, a, b, expected_blocks):
926
"""Check that the sequence matcher returns the correct blocks.
928
:param a: A sequence to match
929
:param b: Another sequence to match
930
:param expected_blocks: The expected output, not including the final
931
matching block (len(a), len(b), 0)
933
matcher = self._PatienceSequenceMatcher(None, a, b)
934
blocks = matcher.get_matching_blocks()
936
self.assertEqual((len(a), len(b), 0), last)
937
self.assertEqual(expected_blocks, blocks)
939
def test_matching_blocks(self):
940
# Some basic matching tests
941
self.assertDiffBlocks('', '', [])
942
self.assertDiffBlocks([], [], [])
943
self.assertDiffBlocks('abc', '', [])
944
self.assertDiffBlocks('', 'abc', [])
945
self.assertDiffBlocks('abcd', 'abcd', [(0, 0, 4)])
946
self.assertDiffBlocks('abcd', 'abce', [(0, 0, 3)])
947
self.assertDiffBlocks('eabc', 'abce', [(1, 0, 3)])
948
self.assertDiffBlocks('eabce', 'abce', [(1, 0, 4)])
949
self.assertDiffBlocks('abcde', 'abXde', [(0, 0, 2), (3, 3, 2)])
950
self.assertDiffBlocks('abcde', 'abXYZde', [(0, 0, 2), (3, 5, 2)])
951
self.assertDiffBlocks('abde', 'abXYZde', [(0, 0, 2), (2, 5, 2)])
952
# This may check too much, but it checks to see that
953
# a copied block stays attached to the previous section,
955
# difflib would tend to grab the trailing longest match
956
# which would make the diff not look right
957
self.assertDiffBlocks('abcdefghijklmnop', 'abcdefxydefghijklmnop',
958
[(0, 0, 6), (6, 11, 10)])
960
# make sure it supports passing in lists
961
self.assertDiffBlocks(
964
'how are you today?\n'],
966
'how are you today?\n'],
967
[(0, 0, 1), (2, 1, 1)])
969
# non unique lines surrounded by non-matching lines
971
self.assertDiffBlocks('aBccDe', 'abccde', [(0,0,1), (5,5,1)])
973
# But they only need to be locally unique
974
self.assertDiffBlocks('aBcDec', 'abcdec', [(0,0,1), (2,2,1), (4,4,2)])
976
# non unique blocks won't be matched
977
self.assertDiffBlocks('aBcdEcdFg', 'abcdecdfg', [(0,0,1), (8,8,1)])
979
# but locally unique ones will
980
self.assertDiffBlocks('aBcdEeXcdFg', 'abcdecdfg', [(0,0,1), (2,2,2),
981
(5,4,1), (7,5,2), (10,8,1)])
983
self.assertDiffBlocks('abbabbXd', 'cabbabxd', [(7,7,1)])
984
self.assertDiffBlocks('abbabbbb', 'cabbabbc', [])
985
self.assertDiffBlocks('bbbbbbbb', 'cbbbbbbc', [])
987
def test_matching_blocks_tuples(self):
988
# Some basic matching tests
989
self.assertDiffBlocks([], [], [])
990
self.assertDiffBlocks([('a',), ('b',), ('c,')], [], [])
991
self.assertDiffBlocks([], [('a',), ('b',), ('c,')], [])
992
self.assertDiffBlocks([('a',), ('b',), ('c,')],
993
[('a',), ('b',), ('c,')],
995
self.assertDiffBlocks([('a',), ('b',), ('c,')],
996
[('a',), ('b',), ('d,')],
998
self.assertDiffBlocks([('d',), ('b',), ('c,')],
999
[('a',), ('b',), ('c,')],
1001
self.assertDiffBlocks([('d',), ('a',), ('b',), ('c,')],
1002
[('a',), ('b',), ('c,')],
1004
self.assertDiffBlocks([('a', 'b'), ('c', 'd'), ('e', 'f')],
1005
[('a', 'b'), ('c', 'X'), ('e', 'f')],
1006
[(0, 0, 1), (2, 2, 1)])
1007
self.assertDiffBlocks([('a', 'b'), ('c', 'd'), ('e', 'f')],
1008
[('a', 'b'), ('c', 'dX'), ('e', 'f')],
1009
[(0, 0, 1), (2, 2, 1)])
1011
def test_opcodes(self):
1012
def chk_ops(a, b, expected_codes):
1013
s = self._PatienceSequenceMatcher(None, a, b)
1014
self.assertEqual(expected_codes, s.get_opcodes())
1018
chk_ops('abc', '', [('delete', 0,3, 0,0)])
1019
chk_ops('', 'abc', [('insert', 0,0, 0,3)])
1020
chk_ops('abcd', 'abcd', [('equal', 0,4, 0,4)])
1021
chk_ops('abcd', 'abce', [('equal', 0,3, 0,3),
1022
('replace', 3,4, 3,4)
1024
chk_ops('eabc', 'abce', [('delete', 0,1, 0,0),
1025
('equal', 1,4, 0,3),
1026
('insert', 4,4, 3,4)
1028
chk_ops('eabce', 'abce', [('delete', 0,1, 0,0),
1031
chk_ops('abcde', 'abXde', [('equal', 0,2, 0,2),
1032
('replace', 2,3, 2,3),
1035
chk_ops('abcde', 'abXYZde', [('equal', 0,2, 0,2),
1036
('replace', 2,3, 2,5),
1039
chk_ops('abde', 'abXYZde', [('equal', 0,2, 0,2),
1040
('insert', 2,2, 2,5),
1043
chk_ops('abcdefghijklmnop', 'abcdefxydefghijklmnop',
1044
[('equal', 0,6, 0,6),
1045
('insert', 6,6, 6,11),
1046
('equal', 6,16, 11,21)
1051
, 'how are you today?\n'],
1053
, 'how are you today?\n'],
1054
[('equal', 0,1, 0,1),
1055
('delete', 1,2, 1,1),
1056
('equal', 2,3, 1,2),
1058
chk_ops('aBccDe', 'abccde',
1059
[('equal', 0,1, 0,1),
1060
('replace', 1,5, 1,5),
1061
('equal', 5,6, 5,6),
1063
chk_ops('aBcDec', 'abcdec',
1064
[('equal', 0,1, 0,1),
1065
('replace', 1,2, 1,2),
1066
('equal', 2,3, 2,3),
1067
('replace', 3,4, 3,4),
1068
('equal', 4,6, 4,6),
1070
chk_ops('aBcdEcdFg', 'abcdecdfg',
1071
[('equal', 0,1, 0,1),
1072
('replace', 1,8, 1,8),
1075
chk_ops('aBcdEeXcdFg', 'abcdecdfg',
1076
[('equal', 0,1, 0,1),
1077
('replace', 1,2, 1,2),
1078
('equal', 2,4, 2,4),
1079
('delete', 4,5, 4,4),
1080
('equal', 5,6, 4,5),
1081
('delete', 6,7, 5,5),
1082
('equal', 7,9, 5,7),
1083
('replace', 9,10, 7,8),
1084
('equal', 10,11, 8,9)
1087
def test_grouped_opcodes(self):
1088
def chk_ops(a, b, expected_codes, n=3):
1089
s = self._PatienceSequenceMatcher(None, a, b)
1090
self.assertEqual(expected_codes, list(s.get_grouped_opcodes(n)))
1094
chk_ops('abc', '', [[('delete', 0,3, 0,0)]])
1095
chk_ops('', 'abc', [[('insert', 0,0, 0,3)]])
1096
chk_ops('abcd', 'abcd', [])
1097
chk_ops('abcd', 'abce', [[('equal', 0,3, 0,3),
1098
('replace', 3,4, 3,4)
1100
chk_ops('eabc', 'abce', [[('delete', 0,1, 0,0),
1101
('equal', 1,4, 0,3),
1102
('insert', 4,4, 3,4)
1104
chk_ops('abcdefghijklmnop', 'abcdefxydefghijklmnop',
1105
[[('equal', 3,6, 3,6),
1106
('insert', 6,6, 6,11),
1107
('equal', 6,9, 11,14)
1109
chk_ops('abcdefghijklmnop', 'abcdefxydefghijklmnop',
1110
[[('equal', 2,6, 2,6),
1111
('insert', 6,6, 6,11),
1112
('equal', 6,10, 11,15)
1114
chk_ops('Xabcdef', 'abcdef',
1115
[[('delete', 0,1, 0,0),
1118
chk_ops('abcdef', 'abcdefX',
1119
[[('equal', 3,6, 3,6),
1120
('insert', 6,6, 6,7)
1124
def test_multiple_ranges(self):
1125
# There was an earlier bug where we used a bad set of ranges,
1126
# this triggers that specific bug, to make sure it doesn't regress
1127
self.assertDiffBlocks('abcdefghijklmnop',
1128
'abcXghiYZQRSTUVWXYZijklmnop',
1129
[(0, 0, 3), (6, 4, 3), (9, 20, 7)])
1131
self.assertDiffBlocks('ABCd efghIjk L',
1132
'AxyzBCn mo pqrstuvwI1 2 L',
1133
[(0,0,1), (1, 4, 2), (9, 19, 1), (12, 23, 3)])
1135
# These are rot13 code snippets.
1136
self.assertDiffBlocks('''\
1137
trg nqqrq jura lbh nqq n svyr va gur qverpgbel.
1139
gnxrf_netf = ['svyr*']
1140
gnxrf_bcgvbaf = ['ab-erphefr']
1142
qrs eha(frys, svyr_yvfg, ab_erphefr=Snyfr):
1143
sebz omeyvo.nqq vzcbeg fzneg_nqq, nqq_ercbegre_cevag, nqq_ercbegre_ahyy
1145
ercbegre = nqq_ercbegre_ahyy
1147
ercbegre = nqq_ercbegre_cevag
1148
fzneg_nqq(svyr_yvfg, abg ab_erphefr, ercbegre)
1151
pynff pzq_zxqve(Pbzznaq):
1152
'''.splitlines(True), '''\
1153
trg nqqrq jura lbh nqq n svyr va gur qverpgbel.
1155
--qel-eha jvyy fubj juvpu svyrf jbhyq or nqqrq, ohg abg npghnyyl
1158
gnxrf_netf = ['svyr*']
1159
gnxrf_bcgvbaf = ['ab-erphefr', 'qel-eha']
1161
qrs eha(frys, svyr_yvfg, ab_erphefr=Snyfr, qel_eha=Snyfr):
1166
# Guvf vf cbvagyrff, ohg V'q engure abg envfr na reebe
1167
npgvba = omeyvo.nqq.nqq_npgvba_ahyy
1169
npgvba = omeyvo.nqq.nqq_npgvba_cevag
1171
npgvba = omeyvo.nqq.nqq_npgvba_nqq
1173
npgvba = omeyvo.nqq.nqq_npgvba_nqq_naq_cevag
1175
omeyvo.nqq.fzneg_nqq(svyr_yvfg, abg ab_erphefr, npgvba)
1178
pynff pzq_zxqve(Pbzznaq):
1179
'''.splitlines(True)
1180
, [(0,0,1), (1, 4, 2), (9, 19, 1), (12, 23, 3)])
1182
def test_patience_unified_diff(self):
1183
txt_a = ['hello there\n',
1185
'how are you today?\n']
1186
txt_b = ['hello there\n',
1187
'how are you today?\n']
1188
unified_diff = patiencediff.unified_diff
1189
psm = self._PatienceSequenceMatcher
1190
self.assertEqual(['--- \n',
1192
'@@ -1,3 +1,2 @@\n',
1195
' how are you today?\n'
1197
, list(unified_diff(txt_a, txt_b,
1198
sequencematcher=psm)))
1199
txt_a = [x+'\n' for x in 'abcdefghijklmnop']
1200
txt_b = [x+'\n' for x in 'abcdefxydefghijklmnop']
1201
# This is the result with LongestCommonSubstring matching
1202
self.assertEqual(['--- \n',
1204
'@@ -1,6 +1,11 @@\n',
1216
, list(unified_diff(txt_a, txt_b)))
1217
# And the patience diff
1218
self.assertEqual(['--- \n',
1220
'@@ -4,6 +4,11 @@\n',
1233
, list(unified_diff(txt_a, txt_b,
1234
sequencematcher=psm)))
1236
def test_patience_unified_diff_with_dates(self):
1237
txt_a = ['hello there\n',
1239
'how are you today?\n']
1240
txt_b = ['hello there\n',
1241
'how are you today?\n']
1242
unified_diff = patiencediff.unified_diff
1243
psm = self._PatienceSequenceMatcher
1244
self.assertEqual(['--- a\t2008-08-08\n',
1245
'+++ b\t2008-09-09\n',
1246
'@@ -1,3 +1,2 @@\n',
1249
' how are you today?\n'
1251
, list(unified_diff(txt_a, txt_b,
1252
fromfile='a', tofile='b',
1253
fromfiledate='2008-08-08',
1254
tofiledate='2008-09-09',
1255
sequencematcher=psm)))
1258
class TestPatienceDiffLib_c(TestPatienceDiffLib):
1260
_test_needs_features = [features.compiled_patiencediff_feature]
1263
super(TestPatienceDiffLib_c, self).setUp()
1264
from breezy import _patiencediff_c
1265
self._unique_lcs = _patiencediff_c.unique_lcs_c
1266
self._recurse_matches = _patiencediff_c.recurse_matches_c
1267
self._PatienceSequenceMatcher = \
1268
_patiencediff_c.PatienceSequenceMatcher_c
1270
def test_unhashable(self):
1271
"""We should get a proper exception here."""
1272
# We need to be able to hash items in the sequence, lists are
1273
# unhashable, and thus cannot be diffed
1274
e = self.assertRaises(TypeError, self._PatienceSequenceMatcher,
1276
e = self.assertRaises(TypeError, self._PatienceSequenceMatcher,
1277
None, ['valid', []], [])
1278
e = self.assertRaises(TypeError, self._PatienceSequenceMatcher,
1279
None, ['valid'], [[]])
1280
e = self.assertRaises(TypeError, self._PatienceSequenceMatcher,
1281
None, ['valid'], ['valid', []])
1284
class TestPatienceDiffLibFiles(tests.TestCaseInTempDir):
1287
super(TestPatienceDiffLibFiles, self).setUp()
1288
self._PatienceSequenceMatcher = \
1289
_patiencediff_py.PatienceSequenceMatcher_py
1291
def test_patience_unified_diff_files(self):
1292
txt_a = ['hello there\n',
1294
'how are you today?\n']
1295
txt_b = ['hello there\n',
1296
'how are you today?\n']
1297
with open('a1', 'wb') as f: f.writelines(txt_a)
1298
with open('b1', 'wb') as f: f.writelines(txt_b)
1300
unified_diff_files = patiencediff.unified_diff_files
1301
psm = self._PatienceSequenceMatcher
1302
self.assertEqual(['--- a1\n',
1304
'@@ -1,3 +1,2 @@\n',
1307
' how are you today?\n',
1309
, list(unified_diff_files('a1', 'b1',
1310
sequencematcher=psm)))
1312
txt_a = [x+'\n' for x in 'abcdefghijklmnop']
1313
txt_b = [x+'\n' for x in 'abcdefxydefghijklmnop']
1314
with open('a2', 'wb') as f: f.writelines(txt_a)
1315
with open('b2', 'wb') as f: f.writelines(txt_b)
1317
# This is the result with LongestCommonSubstring matching
1318
self.assertEqual(['--- a2\n',
1320
'@@ -1,6 +1,11 @@\n',
1332
, list(unified_diff_files('a2', 'b2')))
1334
# And the patience diff
1335
self.assertEqual(['--- a2\n',
1337
'@@ -4,6 +4,11 @@\n',
1349
list(unified_diff_files('a2', 'b2',
1350
sequencematcher=psm)))
1353
class TestPatienceDiffLibFiles_c(TestPatienceDiffLibFiles):
1355
_test_needs_features = [features.compiled_patiencediff_feature]
1358
super(TestPatienceDiffLibFiles_c, self).setUp()
1359
from breezy import _patiencediff_c
1360
self._PatienceSequenceMatcher = \
1361
_patiencediff_c.PatienceSequenceMatcher_c
1364
class TestUsingCompiledIfAvailable(tests.TestCase):
1366
def test_PatienceSequenceMatcher(self):
1367
if features.compiled_patiencediff_feature.available():
1368
from breezy._patiencediff_c import PatienceSequenceMatcher_c
1369
self.assertIs(PatienceSequenceMatcher_c,
1370
patiencediff.PatienceSequenceMatcher)
1372
from breezy._patiencediff_py import PatienceSequenceMatcher_py
1373
self.assertIs(PatienceSequenceMatcher_py,
1374
patiencediff.PatienceSequenceMatcher)
1376
def test_unique_lcs(self):
1377
if features.compiled_patiencediff_feature.available():
1378
from breezy._patiencediff_c import unique_lcs_c
1379
self.assertIs(unique_lcs_c,
1380
patiencediff.unique_lcs)
1382
from breezy._patiencediff_py import unique_lcs_py
1383
self.assertIs(unique_lcs_py,
1384
patiencediff.unique_lcs)
1386
def test_recurse_matches(self):
1387
if features.compiled_patiencediff_feature.available():
1388
from breezy._patiencediff_c import recurse_matches_c
1389
self.assertIs(recurse_matches_c,
1390
patiencediff.recurse_matches)
1392
from breezy._patiencediff_py import recurse_matches_py
1393
self.assertIs(recurse_matches_py,
1394
patiencediff.recurse_matches)
1397
class TestDiffFromTool(tests.TestCaseWithTransport):
1399
def test_from_string(self):
1400
diff_obj = diff.DiffFromTool.from_string('diff', None, None, None)
1401
self.addCleanup(diff_obj.finish)
1402
self.assertEqual(['diff', '@old_path', '@new_path'],
1403
diff_obj.command_template)
1405
def test_from_string_u5(self):
1406
diff_obj = diff.DiffFromTool.from_string('diff "-u 5"',
1408
self.addCleanup(diff_obj.finish)
1409
self.assertEqual(['diff', '-u 5', '@old_path', '@new_path'],
1410
diff_obj.command_template)
1411
self.assertEqual(['diff', '-u 5', 'old-path', 'new-path'],
1412
diff_obj._get_command('old-path', 'new-path'))
1414
def test_from_string_path_with_backslashes(self):
1415
self.requireFeature(features.backslashdir_feature)
1416
tool = 'C:\\Tools\\Diff.exe'
1417
diff_obj = diff.DiffFromTool.from_string(tool, None, None, None)
1418
self.addCleanup(diff_obj.finish)
1419
self.assertEqual(['C:\\Tools\\Diff.exe', '@old_path', '@new_path'],
1420
diff_obj.command_template)
1421
self.assertEqual(['C:\\Tools\\Diff.exe', 'old-path', 'new-path'],
1422
diff_obj._get_command('old-path', 'new-path'))
1424
def test_execute(self):
1426
diff_obj = diff.DiffFromTool(['python', '-c',
1427
'print "@old_path @new_path"'],
1429
self.addCleanup(diff_obj.finish)
1430
diff_obj._execute('old', 'new')
1431
self.assertEqual(output.getvalue().rstrip(), 'old new')
1433
def test_execute_missing(self):
1434
diff_obj = diff.DiffFromTool(['a-tool-which-is-unlikely-to-exist'],
1436
self.addCleanup(diff_obj.finish)
1437
e = self.assertRaises(errors.ExecutableMissing, diff_obj._execute,
1439
self.assertEqual('a-tool-which-is-unlikely-to-exist could not be found'
1440
' on this machine', str(e))
1442
def test_prepare_files_creates_paths_readable_by_windows_tool(self):
1443
self.requireFeature(features.AttribFeature)
1445
tree = self.make_branch_and_tree('tree')
1446
self.build_tree_contents([('tree/file', 'content')])
1447
tree.add('file', 'file-id')
1448
tree.commit('old tree')
1450
self.addCleanup(tree.unlock)
1451
basis_tree = tree.basis_tree()
1452
basis_tree.lock_read()
1453
self.addCleanup(basis_tree.unlock)
1454
diff_obj = diff.DiffFromTool(['python', '-c',
1455
'print "@old_path @new_path"'],
1456
basis_tree, tree, output)
1457
diff_obj._prepare_files('file-id', 'file', 'file')
1458
# The old content should be readonly
1459
self.assertReadableByAttrib(diff_obj._root, 'old\\file',
1461
# The new content should use the tree object, not a 'new' file anymore
1462
self.assertEndsWith(tree.basedir, 'work/tree')
1463
self.assertReadableByAttrib(tree.basedir, 'file', r'work\\tree\\file$')
1465
def assertReadableByAttrib(self, cwd, relpath, regex):
1466
proc = subprocess.Popen(['attrib', relpath],
1467
stdout=subprocess.PIPE,
1469
(result, err) = proc.communicate()
1470
self.assertContainsRe(result.replace('\r\n', '\n'), regex)
1472
def test_prepare_files(self):
1474
tree = self.make_branch_and_tree('tree')
1475
self.build_tree_contents([('tree/oldname', 'oldcontent')])
1476
self.build_tree_contents([('tree/oldname2', 'oldcontent2')])
1477
tree.add('oldname', 'file-id')
1478
tree.add('oldname2', 'file2-id')
1479
# Earliest allowable date on FAT32 filesystems is 1980-01-01
1480
tree.commit('old tree', timestamp=315532800)
1481
tree.rename_one('oldname', 'newname')
1482
tree.rename_one('oldname2', 'newname2')
1483
self.build_tree_contents([('tree/newname', 'newcontent')])
1484
self.build_tree_contents([('tree/newname2', 'newcontent2')])
1485
old_tree = tree.basis_tree()
1486
old_tree.lock_read()
1487
self.addCleanup(old_tree.unlock)
1489
self.addCleanup(tree.unlock)
1490
diff_obj = diff.DiffFromTool(['python', '-c',
1491
'print "@old_path @new_path"'],
1492
old_tree, tree, output)
1493
self.addCleanup(diff_obj.finish)
1494
self.assertContainsRe(diff_obj._root, 'brz-diff-[^/]*')
1495
old_path, new_path = diff_obj._prepare_files('file-id', 'oldname',
1497
self.assertContainsRe(old_path, 'old/oldname$')
1498
self.assertEqual(315532800, os.stat(old_path).st_mtime)
1499
self.assertContainsRe(new_path, 'tree/newname$')
1500
self.assertFileEqual('oldcontent', old_path)
1501
self.assertFileEqual('newcontent', new_path)
1502
if osutils.host_os_dereferences_symlinks():
1503
self.assertTrue(os.path.samefile('tree/newname', new_path))
1504
# make sure we can create files with the same parent directories
1505
diff_obj._prepare_files('file2-id', 'oldname2', 'newname2')
1508
class TestDiffFromToolEncodedFilename(tests.TestCaseWithTransport):
1510
def test_encodable_filename(self):
1511
# Just checks file path for external diff tool.
1512
# We cannot change CPython's internal encoding used by os.exec*.
1513
diffobj = diff.DiffFromTool(['dummy', '@old_path', '@new_path'],
1515
for _, scenario in EncodingAdapter.encoding_scenarios:
1516
encoding = scenario['encoding']
1517
dirname = scenario['info']['directory']
1518
filename = scenario['info']['filename']
1520
self.overrideAttr(diffobj, '_fenc', lambda: encoding)
1521
relpath = dirname + u'/' + filename
1522
fullpath = diffobj._safe_filename('safe', relpath)
1523
self.assertEqual(fullpath,
1524
fullpath.encode(encoding).decode(encoding))
1525
self.assertTrue(fullpath.startswith(diffobj._root + '/safe'))
1527
def test_unencodable_filename(self):
1528
diffobj = diff.DiffFromTool(['dummy', '@old_path', '@new_path'],
1530
for _, scenario in EncodingAdapter.encoding_scenarios:
1531
encoding = scenario['encoding']
1532
dirname = scenario['info']['directory']
1533
filename = scenario['info']['filename']
1535
if encoding == 'iso-8859-1':
1536
encoding = 'iso-8859-2'
1538
encoding = 'iso-8859-1'
1540
self.overrideAttr(diffobj, '_fenc', lambda: encoding)
1541
relpath = dirname + u'/' + filename
1542
fullpath = diffobj._safe_filename('safe', relpath)
1543
self.assertEqual(fullpath,
1544
fullpath.encode(encoding).decode(encoding))
1545
self.assertTrue(fullpath.startswith(diffobj._root + '/safe'))
1548
class TestGetTreesAndBranchesToDiffLocked(tests.TestCaseWithTransport):
1550
def call_gtabtd(self, path_list, revision_specs, old_url, new_url):
1551
"""Call get_trees_and_branches_to_diff_locked."""
1552
return diff.get_trees_and_branches_to_diff_locked(
1553
path_list, revision_specs, old_url, new_url, self.addCleanup)
1555
def test_basic(self):
1556
tree = self.make_branch_and_tree('tree')
1557
(old_tree, new_tree,
1558
old_branch, new_branch,
1559
specific_files, extra_trees) = self.call_gtabtd(
1560
['tree'], None, None, None)
1562
self.assertIsInstance(old_tree, revisiontree.RevisionTree)
1563
self.assertEqual(_mod_revision.NULL_REVISION,
1564
old_tree.get_revision_id())
1565
self.assertEqual(tree.basedir, new_tree.basedir)
1566
self.assertEqual(tree.branch.base, old_branch.base)
1567
self.assertEqual(tree.branch.base, new_branch.base)
1568
self.assertIs(None, specific_files)
1569
self.assertIs(None, extra_trees)
1571
def test_with_rev_specs(self):
1572
tree = self.make_branch_and_tree('tree')
1573
self.build_tree_contents([('tree/file', 'oldcontent')])
1574
tree.add('file', 'file-id')
1575
tree.commit('old tree', timestamp=0, rev_id="old-id")
1576
self.build_tree_contents([('tree/file', 'newcontent')])
1577
tree.commit('new tree', timestamp=0, rev_id="new-id")
1579
revisions = [revisionspec.RevisionSpec.from_string('1'),
1580
revisionspec.RevisionSpec.from_string('2')]
1581
(old_tree, new_tree,
1582
old_branch, new_branch,
1583
specific_files, extra_trees) = self.call_gtabtd(
1584
['tree'], revisions, None, None)
1586
self.assertIsInstance(old_tree, revisiontree.RevisionTree)
1587
self.assertEqual("old-id", old_tree.get_revision_id())
1588
self.assertIsInstance(new_tree, revisiontree.RevisionTree)
1589
self.assertEqual("new-id", new_tree.get_revision_id())
1590
self.assertEqual(tree.branch.base, old_branch.base)
1591
self.assertEqual(tree.branch.base, new_branch.base)
1592
self.assertIs(None, specific_files)
1593
self.assertEqual(tree.basedir, extra_trees[0].basedir)