1
from bzrlib.tests import TestCase
2
from bzrlib.diff import internal_diff
3
from cStringIO import StringIO
4
def udiff_lines(old, new):
6
internal_diff('old', old, 'new', new, output)
1
# Copyright (C) 2005-2012, 2014, 2016, 2017 Canonical Ltd
3
# This program is free software; you can redistribute it and/or modify
4
# it under the terms of the GNU General Public License as published by
5
# the Free Software Foundation; either version 2 of the License, or
6
# (at your option) any later version.
8
# This program is distributed in the hope that it will be useful,
9
# but WITHOUT ANY WARRANTY; without even the implied warranty of
10
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
11
# GNU General Public License for more details.
13
# You should have received a copy of the GNU General Public License
14
# along with this program; if not, write to the Free Software
15
# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA
29
revision as _mod_revision,
35
from ..sixish import (
43
from ..tests.scenarios import load_tests_apply_scenarios
46
load_tests = load_tests_apply_scenarios
49
def subst_dates(string):
50
"""Replace date strings with constant values."""
51
return re.sub(br'\d{4}-\d{2}-\d{2} \d{2}:\d{2}:\d{2} [-\+]\d{4}',
52
b'YYYY-MM-DD HH:MM:SS +ZZZZ', string)
55
def udiff_lines(old, new, allow_binary=False):
57
diff.internal_diff('old', old, 'new', new, output, allow_binary)
8
59
return output.readlines()
10
class TestDiff(TestCase):
62
def external_udiff_lines(old, new, use_stringio=False):
64
# BytesIO has no fileno, so it tests a different codepath
67
output = tempfile.TemporaryFile()
69
diff.external_diff('old', old, 'new', new, output, diff_opts=['-u'])
71
raise tests.TestSkipped('external "diff" not present to test')
73
lines = output.readlines()
79
"""Simple file-like object that allows writes with any type and records."""
82
self.write_record = []
84
def write(self, data):
85
self.write_record.append(data)
87
def check_types(self, testcase, expected_type):
89
any(not isinstance(o, expected_type) for o in self.write_record),
90
"Not all writes of type %s: %r" % (
91
expected_type.__name__, self.write_record))
94
class TestDiffOptions(tests.TestCase):
96
def test_unified_added(self):
97
"""Check for default style '-u' only if no other style specified
100
# Verify that style defaults to unified, id est '-u' appended
101
# to option list, in the absence of an alternative style.
102
self.assertEqual(['-a', '-u'], diff.default_style_unified(['-a']))
105
class TestDiffOptionsScenarios(tests.TestCase):
107
scenarios = [(s, dict(style=s)) for s in diff.style_option_list]
108
style = None # Set by load_tests_apply_scenarios from scenarios
110
def test_unified_not_added(self):
111
# Verify that for all valid style options, '-u' is not
112
# appended to option list.
113
ret_opts = diff.default_style_unified(diff_opts=["%s" % (self.style,)])
114
self.assertEqual(["%s" % (self.style,)], ret_opts)
117
class TestDiff(tests.TestCase):
11
119
def test_add_nl(self):
12
120
"""diff generates a valid diff for patches that add a newline"""
13
lines = udiff_lines(['boo'], ['boo\n'])
121
lines = udiff_lines([b'boo'], [b'boo\n'])
14
122
self.check_patch(lines)
15
self.assertEquals(lines[4], '\\ No newline at end of file\n')
123
self.assertEqual(lines[4], b'\\ No newline at end of file\n')
16
124
## "expected no-nl, got %r" % lines[4]
18
126
def test_add_nl_2(self):
19
127
"""diff generates a valid diff for patches that change last line and
22
lines = udiff_lines(['boo'], ['goo\n'])
130
lines = udiff_lines([b'boo'], [b'goo\n'])
23
131
self.check_patch(lines)
24
self.assertEquals(lines[4], '\\ No newline at end of file\n')
132
self.assertEqual(lines[4], b'\\ No newline at end of file\n')
25
133
## "expected no-nl, got %r" % lines[4]
27
135
def test_remove_nl(self):
28
136
"""diff generates a valid diff for patches that change last line and
31
lines = udiff_lines(['boo\n'], ['boo'])
139
lines = udiff_lines([b'boo\n'], [b'boo'])
32
140
self.check_patch(lines)
33
self.assertEquals(lines[5], '\\ No newline at end of file\n')
141
self.assertEqual(lines[5], b'\\ No newline at end of file\n')
34
142
## "expected no-nl, got %r" % lines[5]
36
144
def check_patch(self, lines):
37
self.assert_(len(lines) > 1)
145
self.assertTrue(len(lines) > 1)
38
146
## "Not enough lines for a file header for patch:\n%s" % "".join(lines)
39
self.assert_(lines[0].startswith ('---'))
147
self.assertTrue(lines[0].startswith (b'---'))
40
148
## 'No orig line for patch:\n%s' % "".join(lines)
41
self.assert_(lines[1].startswith ('+++'))
149
self.assertTrue(lines[1].startswith (b'+++'))
42
150
## 'No mod line for patch:\n%s' % "".join(lines)
43
self.assert_(len(lines) > 2)
151
self.assertTrue(len(lines) > 2)
44
152
## "No hunks for patch:\n%s" % "".join(lines)
45
self.assert_(lines[2].startswith('@@'))
153
self.assertTrue(lines[2].startswith(b'@@'))
46
154
## "No hunk header for patch:\n%s" % "".join(lines)
47
self.assert_('@@' in lines[2][2:])
155
self.assertTrue(b'@@' in lines[2][2:])
48
156
## "Unterminated hunk header for patch:\n%s" % "".join(lines)
158
def test_binary_lines(self):
160
uni_lines = [1023 * b'a' + b'\x00']
161
self.assertRaises(errors.BinaryFile, udiff_lines, uni_lines, empty)
162
self.assertRaises(errors.BinaryFile, udiff_lines, empty, uni_lines)
163
udiff_lines(uni_lines, empty, allow_binary=True)
164
udiff_lines(empty, uni_lines, allow_binary=True)
166
def test_external_diff(self):
167
lines = external_udiff_lines([b'boo\n'], [b'goo\n'])
168
self.check_patch(lines)
169
self.assertEqual(b'\n', lines[-1])
171
def test_external_diff_no_fileno(self):
172
# Make sure that we can handle not having a fileno, even
173
# if the diff is large
174
lines = external_udiff_lines([b'boo\n']*10000,
177
self.check_patch(lines)
179
def test_external_diff_binary_lang_c(self):
180
for lang in ('LANG', 'LC_ALL', 'LANGUAGE'):
181
self.overrideEnv(lang, 'C')
182
lines = external_udiff_lines([b'\x00foobar\n'], [b'foo\x00bar\n'])
183
# Older versions of diffutils say "Binary files", newer
184
# versions just say "Files".
185
self.assertContainsRe(lines[0], b'(Binary f|F)iles old and new differ\n')
186
self.assertEqual(lines[1:], [b'\n'])
188
def test_no_external_diff(self):
189
"""Check that NoDiff is raised when diff is not available"""
190
# Make sure no 'diff' command is available
191
# XXX: Weird, using None instead of '' breaks the test -- vila 20101216
192
self.overrideEnv('PATH', '')
193
self.assertRaises(errors.NoDiff, diff.external_diff,
194
b'old', [b'boo\n'], b'new', [b'goo\n'],
195
BytesIO(), diff_opts=['-u'])
197
def test_internal_diff_default(self):
198
# Default internal diff encoding is utf8
200
diff.internal_diff(u'old_\xb5', [b'old_text\n'],
201
u'new_\xe5', [b'new_text\n'], output)
202
lines = output.getvalue().splitlines(True)
203
self.check_patch(lines)
204
self.assertEqual([b'--- old_\xc2\xb5\n',
205
b'+++ new_\xc3\xa5\n',
206
b'@@ -1,1 +1,1 @@\n',
213
def test_internal_diff_utf8(self):
215
diff.internal_diff(u'old_\xb5', [b'old_text\n'],
216
u'new_\xe5', [b'new_text\n'], output,
217
path_encoding='utf8')
218
lines = output.getvalue().splitlines(True)
219
self.check_patch(lines)
220
self.assertEqual([b'--- old_\xc2\xb5\n',
221
b'+++ new_\xc3\xa5\n',
222
b'@@ -1,1 +1,1 @@\n',
229
def test_internal_diff_iso_8859_1(self):
231
diff.internal_diff(u'old_\xb5', [b'old_text\n'],
232
u'new_\xe5', [b'new_text\n'], output,
233
path_encoding='iso-8859-1')
234
lines = output.getvalue().splitlines(True)
235
self.check_patch(lines)
236
self.assertEqual([b'--- old_\xb5\n',
238
b'@@ -1,1 +1,1 @@\n',
245
def test_internal_diff_no_content(self):
247
diff.internal_diff(u'old', [], u'new', [], output)
248
self.assertEqual(b'', output.getvalue())
250
def test_internal_diff_no_changes(self):
252
diff.internal_diff(u'old', [b'text\n', b'contents\n'],
253
u'new', [b'text\n', b'contents\n'],
255
self.assertEqual(b'', output.getvalue())
257
def test_internal_diff_returns_bytes(self):
259
diff.internal_diff(u'old_\xb5', [b'old_text\n'],
260
u'new_\xe5', [b'new_text\n'], output)
261
output.check_types(self, bytes)
263
def test_internal_diff_default_context(self):
265
diff.internal_diff('old', [b'same_text\n', b'same_text\n', b'same_text\n',
266
b'same_text\n', b'same_text\n', b'old_text\n'],
267
'new', [b'same_text\n', b'same_text\n', b'same_text\n',
268
b'same_text\n', b'same_text\n', b'new_text\n'], output)
269
lines = output.getvalue().splitlines(True)
270
self.check_patch(lines)
271
self.assertEqual([b'--- old\n',
273
b'@@ -3,4 +3,4 @@\n',
283
def test_internal_diff_no_context(self):
285
diff.internal_diff('old', [b'same_text\n', b'same_text\n', b'same_text\n',
286
b'same_text\n', b'same_text\n', b'old_text\n'],
287
'new', [b'same_text\n', b'same_text\n', b'same_text\n',
288
b'same_text\n', b'same_text\n', b'new_text\n'], output,
290
lines = output.getvalue().splitlines(True)
291
self.check_patch(lines)
292
self.assertEqual([b'--- old\n',
294
b'@@ -6,1 +6,1 @@\n',
301
def test_internal_diff_more_context(self):
303
diff.internal_diff('old', [b'same_text\n', b'same_text\n', b'same_text\n',
304
b'same_text\n', b'same_text\n', b'old_text\n'],
305
'new', [b'same_text\n', b'same_text\n', b'same_text\n',
306
b'same_text\n', b'same_text\n', b'new_text\n'], output,
308
lines = output.getvalue().splitlines(True)
309
self.check_patch(lines)
310
self.assertEqual([b'--- old\n',
312
b'@@ -2,5 +2,5 @@\n',
325
class TestDiffFiles(tests.TestCaseInTempDir):
327
def test_external_diff_binary(self):
328
"""The output when using external diff should use diff's i18n error"""
329
for lang in ('LANG', 'LC_ALL', 'LANGUAGE'):
330
self.overrideEnv(lang, 'C')
331
# Make sure external_diff doesn't fail in the current LANG
332
lines = external_udiff_lines([b'\x00foobar\n'], [b'foo\x00bar\n'])
334
cmd = ['diff', '-u', '--binary', 'old', 'new']
335
with open('old', 'wb') as f: f.write(b'\x00foobar\n')
336
with open('new', 'wb') as f: f.write(b'foo\x00bar\n')
337
pipe = subprocess.Popen(cmd, stdout=subprocess.PIPE,
338
stdin=subprocess.PIPE)
339
out, err = pipe.communicate()
340
# We should output whatever diff tells us, plus a trailing newline
341
self.assertEqual(out.splitlines(True) + [b'\n'], lines)
344
def get_diff_as_string(tree1, tree2, specific_files=None, working_tree=None):
346
if working_tree is not None:
347
extra_trees = (working_tree,)
350
diff.show_diff_trees(tree1, tree2, output,
351
specific_files=specific_files,
352
extra_trees=extra_trees, old_label='old/',
354
return output.getvalue()
357
class TestDiffDates(tests.TestCaseWithTransport):
360
super(TestDiffDates, self).setUp()
361
self.wt = self.make_branch_and_tree('.')
362
self.b = self.wt.branch
363
self.build_tree_contents([
364
('file1', b'file1 contents at rev 1\n'),
365
('file2', b'file2 contents at rev 1\n')
367
self.wt.add(['file1', 'file2'])
369
message='Revision 1',
370
timestamp=1143849600, # 2006-04-01 00:00:00 UTC
373
self.build_tree_contents([('file1', b'file1 contents at rev 2\n')])
375
message='Revision 2',
376
timestamp=1143936000, # 2006-04-02 00:00:00 UTC
379
self.build_tree_contents([('file2', b'file2 contents at rev 3\n')])
381
message='Revision 3',
382
timestamp=1144022400, # 2006-04-03 00:00:00 UTC
385
self.wt.remove(['file2'])
387
message='Revision 4',
388
timestamp=1144108800, # 2006-04-04 00:00:00 UTC
391
self.build_tree_contents([
392
('file1', b'file1 contents in working tree\n')
394
# set the date stamps for files in the working tree to known values
395
os.utime('file1', (1144195200, 1144195200)) # 2006-04-05 00:00:00 UTC
397
def test_diff_rev_tree_working_tree(self):
398
output = get_diff_as_string(self.wt.basis_tree(), self.wt)
399
# note that the date for old/file1 is from rev 2 rather than from
400
# the basis revision (rev 4)
401
self.assertEqualDiff(output, b'''\
402
=== modified file 'file1'
403
--- old/file1\t2006-04-02 00:00:00 +0000
404
+++ new/file1\t2006-04-05 00:00:00 +0000
406
-file1 contents at rev 2
407
+file1 contents in working tree
411
def test_diff_rev_tree_rev_tree(self):
412
tree1 = self.b.repository.revision_tree(b'rev-2')
413
tree2 = self.b.repository.revision_tree(b'rev-3')
414
output = get_diff_as_string(tree1, tree2)
415
self.assertEqualDiff(output, b'''\
416
=== modified file 'file2'
417
--- old/file2\t2006-04-01 00:00:00 +0000
418
+++ new/file2\t2006-04-03 00:00:00 +0000
420
-file2 contents at rev 1
421
+file2 contents at rev 3
425
def test_diff_add_files(self):
426
tree1 = self.b.repository.revision_tree(_mod_revision.NULL_REVISION)
427
tree2 = self.b.repository.revision_tree(b'rev-1')
428
output = get_diff_as_string(tree1, tree2)
429
# the files have the epoch time stamp for the tree in which
431
self.assertEqualDiff(output, b'''\
432
=== added file 'file1'
433
--- old/file1\t1970-01-01 00:00:00 +0000
434
+++ new/file1\t2006-04-01 00:00:00 +0000
436
+file1 contents at rev 1
438
=== added file 'file2'
439
--- old/file2\t1970-01-01 00:00:00 +0000
440
+++ new/file2\t2006-04-01 00:00:00 +0000
442
+file2 contents at rev 1
446
def test_diff_remove_files(self):
447
tree1 = self.b.repository.revision_tree(b'rev-3')
448
tree2 = self.b.repository.revision_tree(b'rev-4')
449
output = get_diff_as_string(tree1, tree2)
450
# the file has the epoch time stamp for the tree in which
452
self.assertEqualDiff(output, b'''\
453
=== removed file 'file2'
454
--- old/file2\t2006-04-03 00:00:00 +0000
455
+++ new/file2\t1970-01-01 00:00:00 +0000
457
-file2 contents at rev 3
461
def test_show_diff_specified(self):
462
"""A working tree filename can be used to identify a file"""
463
self.wt.rename_one('file1', 'file1b')
464
old_tree = self.b.repository.revision_tree(b'rev-1')
465
new_tree = self.b.repository.revision_tree(b'rev-4')
466
out = get_diff_as_string(old_tree, new_tree, specific_files=['file1b'],
467
working_tree=self.wt)
468
self.assertContainsRe(out, b'file1\t')
470
def test_recursive_diff(self):
471
"""Children of directories are matched"""
474
self.wt.add(['dir1', 'dir2'])
475
self.wt.rename_one('file1', 'dir1/file1')
476
old_tree = self.b.repository.revision_tree(b'rev-1')
477
new_tree = self.b.repository.revision_tree(b'rev-4')
478
out = get_diff_as_string(old_tree, new_tree, specific_files=['dir1'],
479
working_tree=self.wt)
480
self.assertContainsRe(out, b'file1\t')
481
out = get_diff_as_string(old_tree, new_tree, specific_files=['dir2'],
482
working_tree=self.wt)
483
self.assertNotContainsRe(out, b'file1\t')
486
class TestShowDiffTrees(tests.TestCaseWithTransport):
487
"""Direct tests for show_diff_trees"""
489
def test_modified_file(self):
490
"""Test when a file is modified."""
491
tree = self.make_branch_and_tree('tree')
492
self.build_tree_contents([('tree/file', b'contents\n')])
493
tree.add(['file'], [b'file-id'])
494
tree.commit('one', rev_id=b'rev-1')
496
self.build_tree_contents([('tree/file', b'new contents\n')])
497
d = get_diff_as_string(tree.basis_tree(), tree)
498
self.assertContainsRe(d, b"=== modified file 'file'\n")
499
self.assertContainsRe(d, b'--- old/file\t')
500
self.assertContainsRe(d, b'\\+\\+\\+ new/file\t')
501
self.assertContainsRe(d, b'-contents\n'
502
b'\\+new contents\n')
504
def test_modified_file_in_renamed_dir(self):
505
"""Test when a file is modified in a renamed directory."""
506
tree = self.make_branch_and_tree('tree')
507
self.build_tree(['tree/dir/'])
508
self.build_tree_contents([('tree/dir/file', b'contents\n')])
509
tree.add(['dir', 'dir/file'], [b'dir-id', b'file-id'])
510
tree.commit('one', rev_id=b'rev-1')
512
tree.rename_one('dir', 'other')
513
self.build_tree_contents([('tree/other/file', b'new contents\n')])
514
d = get_diff_as_string(tree.basis_tree(), tree)
515
self.assertContainsRe(d, b"=== renamed directory 'dir' => 'other'\n")
516
self.assertContainsRe(d, b"=== modified file 'other/file'\n")
517
# XXX: This is technically incorrect, because it used to be at another
518
# location. What to do?
519
self.assertContainsRe(d, b'--- old/dir/file\t')
520
self.assertContainsRe(d, b'\\+\\+\\+ new/other/file\t')
521
self.assertContainsRe(d, b'-contents\n'
522
b'\\+new contents\n')
524
def test_renamed_directory(self):
525
"""Test when only a directory is only renamed."""
526
tree = self.make_branch_and_tree('tree')
527
self.build_tree(['tree/dir/'])
528
self.build_tree_contents([('tree/dir/file', b'contents\n')])
529
tree.add(['dir', 'dir/file'], [b'dir-id', b'file-id'])
530
tree.commit('one', rev_id=b'rev-1')
532
tree.rename_one('dir', 'newdir')
533
d = get_diff_as_string(tree.basis_tree(), tree)
534
# Renaming a directory should be a single "you renamed this dir" even
535
# when there are files inside.
536
self.assertEqual(d, b"=== renamed directory 'dir' => 'newdir'\n")
538
def test_renamed_file(self):
539
"""Test when a file is only renamed."""
540
tree = self.make_branch_and_tree('tree')
541
self.build_tree_contents([('tree/file', b'contents\n')])
542
tree.add(['file'], [b'file-id'])
543
tree.commit('one', rev_id=b'rev-1')
545
tree.rename_one('file', 'newname')
546
d = get_diff_as_string(tree.basis_tree(), tree)
547
self.assertContainsRe(d, b"=== renamed file 'file' => 'newname'\n")
548
# We shouldn't have a --- or +++ line, because there is no content
550
self.assertNotContainsRe(d, b'---')
552
def test_renamed_and_modified_file(self):
553
"""Test when a file is only renamed."""
554
tree = self.make_branch_and_tree('tree')
555
self.build_tree_contents([('tree/file', b'contents\n')])
556
tree.add(['file'], [b'file-id'])
557
tree.commit('one', rev_id=b'rev-1')
559
tree.rename_one('file', 'newname')
560
self.build_tree_contents([('tree/newname', b'new contents\n')])
561
d = get_diff_as_string(tree.basis_tree(), tree)
562
self.assertContainsRe(d, b"=== renamed file 'file' => 'newname'\n")
563
self.assertContainsRe(d, b'--- old/file\t')
564
self.assertContainsRe(d, b'\\+\\+\\+ new/newname\t')
565
self.assertContainsRe(d, b'-contents\n'
566
b'\\+new contents\n')
569
def test_internal_diff_exec_property(self):
570
tree = self.make_branch_and_tree('tree')
572
tt = transform.TreeTransform(tree)
573
tt.new_file('a', tt.root, [b'contents\n'], b'a-id', True)
574
tt.new_file('b', tt.root, [b'contents\n'], b'b-id', False)
575
tt.new_file('c', tt.root, [b'contents\n'], b'c-id', True)
576
tt.new_file('d', tt.root, [b'contents\n'], b'd-id', False)
577
tt.new_file('e', tt.root, [b'contents\n'], b'control-e-id', True)
578
tt.new_file('f', tt.root, [b'contents\n'], b'control-f-id', False)
580
tree.commit('one', rev_id=b'rev-1')
582
tt = transform.TreeTransform(tree)
583
tt.set_executability(False, tt.trans_id_file_id(b'a-id'))
584
tt.set_executability(True, tt.trans_id_file_id(b'b-id'))
585
tt.set_executability(False, tt.trans_id_file_id(b'c-id'))
586
tt.set_executability(True, tt.trans_id_file_id(b'd-id'))
588
tree.rename_one('c', 'new-c')
589
tree.rename_one('d', 'new-d')
591
d = get_diff_as_string(tree.basis_tree(), tree)
593
self.assertContainsRe(d, br"file 'a'.*\(properties changed:"
595
self.assertContainsRe(d, br"file 'b'.*\(properties changed:"
597
self.assertContainsRe(d, br"file 'c'.*\(properties changed:"
599
self.assertContainsRe(d, br"file 'd'.*\(properties changed:"
601
self.assertNotContainsRe(d, br"file 'e'")
602
self.assertNotContainsRe(d, br"file 'f'")
604
def test_binary_unicode_filenames(self):
605
"""Test that contents of files are *not* encoded in UTF-8 when there
606
is a binary file in the diff.
608
# See https://bugs.launchpad.net/bugs/110092.
609
self.requireFeature(features.UnicodeFilenameFeature)
611
tree = self.make_branch_and_tree('tree')
612
alpha, omega = u'\u03b1', u'\u03c9'
613
alpha_utf8, omega_utf8 = alpha.encode('utf8'), omega.encode('utf8')
614
self.build_tree_contents(
615
[('tree/' + alpha, b'\0'),
617
(b'The %s and the %s\n' % (alpha_utf8, omega_utf8)))])
618
tree.add([alpha], [b'file-id'])
619
tree.add([omega], [b'file-id-2'])
620
diff_content = StubO()
621
diff.show_diff_trees(tree.basis_tree(), tree, diff_content)
622
diff_content.check_types(self, bytes)
623
d = b''.join(diff_content.write_record)
624
self.assertContainsRe(d, br"=== added file '%s'" % alpha_utf8)
625
self.assertContainsRe(d, b"Binary files a/%s.*and b/%s.* differ\n"
626
% (alpha_utf8, alpha_utf8))
627
self.assertContainsRe(d, br"=== added file '%s'" % omega_utf8)
628
self.assertContainsRe(d, br"--- a/%s" % (omega_utf8,))
629
self.assertContainsRe(d, br"\+\+\+ b/%s" % (omega_utf8,))
631
def test_unicode_filename(self):
632
"""Test when the filename are unicode."""
633
self.requireFeature(features.UnicodeFilenameFeature)
635
alpha, omega = u'\u03b1', u'\u03c9'
636
autf8, outf8 = alpha.encode('utf8'), omega.encode('utf8')
638
tree = self.make_branch_and_tree('tree')
639
self.build_tree_contents([('tree/ren_'+alpha, b'contents\n')])
640
tree.add(['ren_'+alpha], [b'file-id-2'])
641
self.build_tree_contents([('tree/del_'+alpha, b'contents\n')])
642
tree.add(['del_'+alpha], [b'file-id-3'])
643
self.build_tree_contents([('tree/mod_'+alpha, b'contents\n')])
644
tree.add(['mod_'+alpha], [b'file-id-4'])
646
tree.commit('one', rev_id=b'rev-1')
648
tree.rename_one('ren_'+alpha, 'ren_'+omega)
649
tree.remove('del_'+alpha)
650
self.build_tree_contents([('tree/add_'+alpha, b'contents\n')])
651
tree.add(['add_'+alpha], [b'file-id'])
652
self.build_tree_contents([('tree/mod_'+alpha, b'contents_mod\n')])
654
d = get_diff_as_string(tree.basis_tree(), tree)
655
self.assertContainsRe(d,
656
b"=== renamed file 'ren_%s' => 'ren_%s'\n"%(autf8, outf8))
657
self.assertContainsRe(d, b"=== added file 'add_%s'"%autf8)
658
self.assertContainsRe(d, b"=== modified file 'mod_%s'"%autf8)
659
self.assertContainsRe(d, b"=== removed file 'del_%s'"%autf8)
661
def test_unicode_filename_path_encoding(self):
662
"""Test for bug #382699: unicode filenames on Windows should be shown
665
self.requireFeature(features.UnicodeFilenameFeature)
666
# The word 'test' in Russian
667
_russian_test = u'\u0422\u0435\u0441\u0442'
668
directory = _russian_test + u'/'
669
test_txt = _russian_test + u'.txt'
670
u1234 = u'\u1234.txt'
672
tree = self.make_branch_and_tree('.')
673
self.build_tree_contents([
674
(test_txt, b'foo\n'),
678
tree.add([test_txt, u1234, directory])
681
diff.show_diff_trees(tree.basis_tree(), tree, sio,
682
path_encoding='cp1251')
684
output = subst_dates(sio.getvalue())
686
=== added directory '%(directory)s'
687
=== added file '%(test_txt)s'
688
--- a/%(test_txt)s\tYYYY-MM-DD HH:MM:SS +ZZZZ
689
+++ b/%(test_txt)s\tYYYY-MM-DD HH:MM:SS +ZZZZ
693
=== added file '?.txt'
694
--- a/?.txt\tYYYY-MM-DD HH:MM:SS +ZZZZ
695
+++ b/?.txt\tYYYY-MM-DD HH:MM:SS +ZZZZ
699
''' % {b'directory': _russian_test.encode('cp1251'),
700
b'test_txt': test_txt.encode('cp1251'),
702
self.assertEqualDiff(output, shouldbe)
705
class DiffWasIs(diff.DiffPath):
707
def diff(self, file_id, old_path, new_path, old_kind, new_kind):
708
self.to_file.write(b'was: ')
709
self.to_file.write(self.old_tree.get_file(old_path).read())
710
self.to_file.write(b'is: ')
711
self.to_file.write(self.new_tree.get_file(new_path).read())
714
class TestDiffTree(tests.TestCaseWithTransport):
717
super(TestDiffTree, self).setUp()
718
self.old_tree = self.make_branch_and_tree('old-tree')
719
self.old_tree.lock_write()
720
self.addCleanup(self.old_tree.unlock)
721
self.new_tree = self.make_branch_and_tree('new-tree')
722
self.new_tree.lock_write()
723
self.addCleanup(self.new_tree.unlock)
724
self.differ = diff.DiffTree(self.old_tree, self.new_tree, BytesIO())
726
def test_diff_text(self):
727
self.build_tree_contents([('old-tree/olddir/',),
728
('old-tree/olddir/oldfile', b'old\n')])
729
self.old_tree.add('olddir')
730
self.old_tree.add('olddir/oldfile', b'file-id')
731
self.build_tree_contents([('new-tree/newdir/',),
732
('new-tree/newdir/newfile', b'new\n')])
733
self.new_tree.add('newdir')
734
self.new_tree.add('newdir/newfile', b'file-id')
735
differ = diff.DiffText(self.old_tree, self.new_tree, BytesIO())
736
differ.diff_text('olddir/oldfile', None, 'old label',
737
'new label', b'file-id', None)
739
b'--- old label\n+++ new label\n@@ -1,1 +0,0 @@\n-old\n\n',
740
differ.to_file.getvalue())
741
differ.to_file.seek(0)
742
differ.diff_text(None, 'newdir/newfile',
743
'old label', 'new label', None, b'file-id')
745
b'--- old label\n+++ new label\n@@ -0,0 +1,1 @@\n+new\n\n',
746
differ.to_file.getvalue())
747
differ.to_file.seek(0)
748
differ.diff_text('olddir/oldfile', 'newdir/newfile',
749
'old label', 'new label', b'file-id', b'file-id')
751
b'--- old label\n+++ new label\n@@ -1,1 +1,1 @@\n-old\n+new\n\n',
752
differ.to_file.getvalue())
754
def test_diff_deletion(self):
755
self.build_tree_contents([('old-tree/file', b'contents'),
756
('new-tree/file', b'contents')])
757
self.old_tree.add('file', b'file-id')
758
self.new_tree.add('file', b'file-id')
759
os.unlink('new-tree/file')
760
self.differ.show_diff(None)
761
self.assertContainsRe(self.differ.to_file.getvalue(), b'-contents')
763
def test_diff_creation(self):
764
self.build_tree_contents([('old-tree/file', b'contents'),
765
('new-tree/file', b'contents')])
766
self.old_tree.add('file', b'file-id')
767
self.new_tree.add('file', b'file-id')
768
os.unlink('old-tree/file')
769
self.differ.show_diff(None)
770
self.assertContainsRe(self.differ.to_file.getvalue(), br'\+contents')
772
def test_diff_symlink(self):
773
differ = diff.DiffSymlink(self.old_tree, self.new_tree, BytesIO())
774
differ.diff_symlink('old target', None)
775
self.assertEqual(b"=== target was 'old target'\n",
776
differ.to_file.getvalue())
778
differ = diff.DiffSymlink(self.old_tree, self.new_tree, BytesIO())
779
differ.diff_symlink(None, 'new target')
780
self.assertEqual(b"=== target is 'new target'\n",
781
differ.to_file.getvalue())
783
differ = diff.DiffSymlink(self.old_tree, self.new_tree, BytesIO())
784
differ.diff_symlink('old target', 'new target')
785
self.assertEqual(b"=== target changed 'old target' => 'new target'\n",
786
differ.to_file.getvalue())
789
self.build_tree_contents([('old-tree/olddir/',),
790
('old-tree/olddir/oldfile', b'old\n')])
791
self.old_tree.add('olddir')
792
self.old_tree.add('olddir/oldfile', b'file-id')
793
self.build_tree_contents([('new-tree/newdir/',),
794
('new-tree/newdir/newfile', b'new\n')])
795
self.new_tree.add('newdir')
796
self.new_tree.add('newdir/newfile', b'file-id')
797
self.differ.diff(b'file-id', 'olddir/oldfile', 'newdir/newfile')
798
self.assertContainsRe(
799
self.differ.to_file.getvalue(),
800
br'--- olddir/oldfile.*\n\+\+\+ newdir/newfile.*\n\@\@ -1,1 \+1,1'
801
br' \@\@\n-old\n\+new\n\n')
803
def test_diff_kind_change(self):
804
self.requireFeature(features.SymlinkFeature)
805
self.build_tree_contents([('old-tree/olddir/',),
806
('old-tree/olddir/oldfile', b'old\n')])
807
self.old_tree.add('olddir')
808
self.old_tree.add('olddir/oldfile', b'file-id')
809
self.build_tree(['new-tree/newdir/'])
810
os.symlink('new', 'new-tree/newdir/newfile')
811
self.new_tree.add('newdir')
812
self.new_tree.add('newdir/newfile', b'file-id')
813
self.differ.diff(b'file-id', 'olddir/oldfile', 'newdir/newfile')
814
self.assertContainsRe(
815
self.differ.to_file.getvalue(),
816
br'--- olddir/oldfile.*\n\+\+\+ newdir/newfile.*\n\@\@ -1,1 \+0,0'
818
self.assertContainsRe(self.differ.to_file.getvalue(),
819
b"=== target is 'new'\n")
821
def test_diff_directory(self):
822
self.build_tree(['new-tree/new-dir/'])
823
self.new_tree.add('new-dir', b'new-dir-id')
824
self.differ.diff(b'new-dir-id', None, 'new-dir')
825
self.assertEqual(self.differ.to_file.getvalue(), b'')
827
def create_old_new(self):
828
self.build_tree_contents([('old-tree/olddir/',),
829
('old-tree/olddir/oldfile', b'old\n')])
830
self.old_tree.add('olddir')
831
self.old_tree.add('olddir/oldfile', b'file-id')
832
self.build_tree_contents([('new-tree/newdir/',),
833
('new-tree/newdir/newfile', b'new\n')])
834
self.new_tree.add('newdir')
835
self.new_tree.add('newdir/newfile', b'file-id')
837
def test_register_diff(self):
838
self.create_old_new()
839
old_diff_factories = diff.DiffTree.diff_factories
840
diff.DiffTree.diff_factories=old_diff_factories[:]
841
diff.DiffTree.diff_factories.insert(0, DiffWasIs.from_diff_tree)
843
differ = diff.DiffTree(self.old_tree, self.new_tree, BytesIO())
845
diff.DiffTree.diff_factories = old_diff_factories
846
differ.diff(b'file-id', 'olddir/oldfile', 'newdir/newfile')
847
self.assertNotContainsRe(
848
differ.to_file.getvalue(),
849
br'--- olddir/oldfile.*\n\+\+\+ newdir/newfile.*\n\@\@ -1,1 \+1,1'
850
br' \@\@\n-old\n\+new\n\n')
851
self.assertContainsRe(differ.to_file.getvalue(),
852
b'was: old\nis: new\n')
854
def test_extra_factories(self):
855
self.create_old_new()
856
differ = diff.DiffTree(self.old_tree, self.new_tree, BytesIO(),
857
extra_factories=[DiffWasIs.from_diff_tree])
858
differ.diff(b'file-id', 'olddir/oldfile', 'newdir/newfile')
859
self.assertNotContainsRe(
860
differ.to_file.getvalue(),
861
br'--- olddir/oldfile.*\n\+\+\+ newdir/newfile.*\n\@\@ -1,1 \+1,1'
862
br' \@\@\n-old\n\+new\n\n')
863
self.assertContainsRe(differ.to_file.getvalue(),
864
b'was: old\nis: new\n')
866
def test_alphabetical_order(self):
867
self.build_tree(['new-tree/a-file'])
868
self.new_tree.add('a-file')
869
self.build_tree(['old-tree/b-file'])
870
self.old_tree.add('b-file')
871
self.differ.show_diff(None)
872
self.assertContainsRe(self.differ.to_file.getvalue(),
873
b'.*a-file(.|\n)*b-file')
876
class TestPatienceDiffLib(tests.TestCase):
879
super(TestPatienceDiffLib, self).setUp()
880
self._unique_lcs = _patiencediff_py.unique_lcs_py
881
self._recurse_matches = _patiencediff_py.recurse_matches_py
882
self._PatienceSequenceMatcher = \
883
_patiencediff_py.PatienceSequenceMatcher_py
885
def test_diff_unicode_string(self):
886
a = ''.join([unichr(i) for i in range(4000, 4500, 3)])
887
b = ''.join([unichr(i) for i in range(4300, 4800, 2)])
888
sm = self._PatienceSequenceMatcher(None, a, b)
889
mb = sm.get_matching_blocks()
890
self.assertEqual(35, len(mb))
892
def test_unique_lcs(self):
893
unique_lcs = self._unique_lcs
894
self.assertEqual(unique_lcs('', ''), [])
895
self.assertEqual(unique_lcs('', 'a'), [])
896
self.assertEqual(unique_lcs('a', ''), [])
897
self.assertEqual(unique_lcs('a', 'a'), [(0, 0)])
898
self.assertEqual(unique_lcs('a', 'b'), [])
899
self.assertEqual(unique_lcs('ab', 'ab'), [(0, 0), (1, 1)])
900
self.assertEqual(unique_lcs('abcde', 'cdeab'), [(2, 0), (3, 1), (4, 2)])
901
self.assertEqual(unique_lcs('cdeab', 'abcde'), [(0, 2), (1, 3), (2, 4)])
902
self.assertEqual(unique_lcs('abXde', 'abYde'), [(0, 0), (1, 1),
904
self.assertEqual(unique_lcs('acbac', 'abc'), [(2, 1)])
906
def test_recurse_matches(self):
907
def test_one(a, b, matches):
909
self._recurse_matches(
910
a, b, 0, 0, len(a), len(b), test_matches, 10)
911
self.assertEqual(test_matches, matches)
913
test_one(['a', '', 'b', '', 'c'], ['a', 'a', 'b', 'c', 'c'],
914
[(0, 0), (2, 2), (4, 4)])
915
test_one(['a', 'c', 'b', 'a', 'c'], ['a', 'b', 'c'],
916
[(0, 0), (2, 1), (4, 2)])
917
# Even though 'bc' is not unique globally, and is surrounded by
918
# non-matching lines, we should still match, because they are locally
920
test_one('abcdbce', 'afbcgdbce', [(0, 0), (1, 2), (2, 3), (3, 5),
921
(4, 6), (5, 7), (6, 8)])
923
# recurse_matches doesn't match non-unique
924
# lines surrounded by bogus text.
925
# The update has been done in patiencediff.SequenceMatcher instead
927
# This is what it could be
928
#test_one('aBccDe', 'abccde', [(0,0), (2,2), (3,3), (5,5)])
930
# This is what it currently gives:
931
test_one('aBccDe', 'abccde', [(0, 0), (5, 5)])
933
def assertDiffBlocks(self, a, b, expected_blocks):
934
"""Check that the sequence matcher returns the correct blocks.
936
:param a: A sequence to match
937
:param b: Another sequence to match
938
:param expected_blocks: The expected output, not including the final
939
matching block (len(a), len(b), 0)
941
matcher = self._PatienceSequenceMatcher(None, a, b)
942
blocks = matcher.get_matching_blocks()
944
self.assertEqual((len(a), len(b), 0), last)
945
self.assertEqual(expected_blocks, blocks)
947
def test_matching_blocks(self):
948
# Some basic matching tests
949
self.assertDiffBlocks('', '', [])
950
self.assertDiffBlocks([], [], [])
951
self.assertDiffBlocks('abc', '', [])
952
self.assertDiffBlocks('', 'abc', [])
953
self.assertDiffBlocks('abcd', 'abcd', [(0, 0, 4)])
954
self.assertDiffBlocks('abcd', 'abce', [(0, 0, 3)])
955
self.assertDiffBlocks('eabc', 'abce', [(1, 0, 3)])
956
self.assertDiffBlocks('eabce', 'abce', [(1, 0, 4)])
957
self.assertDiffBlocks('abcde', 'abXde', [(0, 0, 2), (3, 3, 2)])
958
self.assertDiffBlocks('abcde', 'abXYZde', [(0, 0, 2), (3, 5, 2)])
959
self.assertDiffBlocks('abde', 'abXYZde', [(0, 0, 2), (2, 5, 2)])
960
# This may check too much, but it checks to see that
961
# a copied block stays attached to the previous section,
963
# difflib would tend to grab the trailing longest match
964
# which would make the diff not look right
965
self.assertDiffBlocks('abcdefghijklmnop', 'abcdefxydefghijklmnop',
966
[(0, 0, 6), (6, 11, 10)])
968
# make sure it supports passing in lists
969
self.assertDiffBlocks(
972
'how are you today?\n'],
974
'how are you today?\n'],
975
[(0, 0, 1), (2, 1, 1)])
977
# non unique lines surrounded by non-matching lines
979
self.assertDiffBlocks('aBccDe', 'abccde', [(0, 0, 1), (5, 5, 1)])
981
# But they only need to be locally unique
982
self.assertDiffBlocks('aBcDec', 'abcdec', [(0, 0, 1), (2, 2, 1), (4, 4, 2)])
984
# non unique blocks won't be matched
985
self.assertDiffBlocks('aBcdEcdFg', 'abcdecdfg', [(0, 0, 1), (8, 8, 1)])
987
# but locally unique ones will
988
self.assertDiffBlocks('aBcdEeXcdFg', 'abcdecdfg', [(0, 0, 1), (2, 2, 2),
989
(5, 4, 1), (7, 5, 2), (10, 8, 1)])
991
self.assertDiffBlocks('abbabbXd', 'cabbabxd', [(7, 7, 1)])
992
self.assertDiffBlocks('abbabbbb', 'cabbabbc', [])
993
self.assertDiffBlocks('bbbbbbbb', 'cbbbbbbc', [])
995
def test_matching_blocks_tuples(self):
996
# Some basic matching tests
997
self.assertDiffBlocks([], [], [])
998
self.assertDiffBlocks([('a',), ('b',), ('c,')], [], [])
999
self.assertDiffBlocks([], [('a',), ('b',), ('c,')], [])
1000
self.assertDiffBlocks([('a',), ('b',), ('c,')],
1001
[('a',), ('b',), ('c,')],
1003
self.assertDiffBlocks([('a',), ('b',), ('c,')],
1004
[('a',), ('b',), ('d,')],
1006
self.assertDiffBlocks([('d',), ('b',), ('c,')],
1007
[('a',), ('b',), ('c,')],
1009
self.assertDiffBlocks([('d',), ('a',), ('b',), ('c,')],
1010
[('a',), ('b',), ('c,')],
1012
self.assertDiffBlocks([('a', 'b'), ('c', 'd'), ('e', 'f')],
1013
[('a', 'b'), ('c', 'X'), ('e', 'f')],
1014
[(0, 0, 1), (2, 2, 1)])
1015
self.assertDiffBlocks([('a', 'b'), ('c', 'd'), ('e', 'f')],
1016
[('a', 'b'), ('c', 'dX'), ('e', 'f')],
1017
[(0, 0, 1), (2, 2, 1)])
1019
def test_opcodes(self):
1020
def chk_ops(a, b, expected_codes):
1021
s = self._PatienceSequenceMatcher(None, a, b)
1022
self.assertEqual(expected_codes, s.get_opcodes())
1026
chk_ops('abc', '', [('delete', 0, 3, 0, 0)])
1027
chk_ops('', 'abc', [('insert', 0, 0, 0, 3)])
1028
chk_ops('abcd', 'abcd', [('equal', 0, 4, 0, 4)])
1029
chk_ops('abcd', 'abce', [('equal', 0, 3, 0, 3),
1030
('replace', 3, 4, 3, 4)
1032
chk_ops('eabc', 'abce', [('delete', 0, 1, 0, 0),
1033
('equal', 1, 4, 0, 3),
1034
('insert', 4, 4, 3, 4)
1036
chk_ops('eabce', 'abce', [('delete', 0, 1, 0, 0),
1037
('equal', 1, 5, 0, 4)
1039
chk_ops('abcde', 'abXde', [('equal', 0, 2, 0, 2),
1040
('replace', 2, 3, 2, 3),
1041
('equal', 3, 5, 3, 5)
1043
chk_ops('abcde', 'abXYZde', [('equal', 0, 2, 0, 2),
1044
('replace', 2, 3, 2, 5),
1045
('equal', 3, 5, 5, 7)
1047
chk_ops('abde', 'abXYZde', [('equal', 0, 2, 0, 2),
1048
('insert', 2, 2, 2, 5),
1049
('equal', 2, 4, 5, 7)
1051
chk_ops('abcdefghijklmnop', 'abcdefxydefghijklmnop',
1052
[('equal', 0, 6, 0, 6),
1053
('insert', 6, 6, 6, 11),
1054
('equal', 6, 16, 11, 21)
1059
, 'how are you today?\n'],
1061
, 'how are you today?\n'],
1062
[('equal', 0, 1, 0, 1),
1063
('delete', 1, 2, 1, 1),
1064
('equal', 2, 3, 1, 2),
1066
chk_ops('aBccDe', 'abccde',
1067
[('equal', 0, 1, 0, 1),
1068
('replace', 1, 5, 1, 5),
1069
('equal', 5, 6, 5, 6),
1071
chk_ops('aBcDec', 'abcdec',
1072
[('equal', 0, 1, 0, 1),
1073
('replace', 1, 2, 1, 2),
1074
('equal', 2, 3, 2, 3),
1075
('replace', 3, 4, 3, 4),
1076
('equal', 4, 6, 4, 6),
1078
chk_ops('aBcdEcdFg', 'abcdecdfg',
1079
[('equal', 0, 1, 0, 1),
1080
('replace', 1, 8, 1, 8),
1081
('equal', 8, 9, 8, 9)
1083
chk_ops('aBcdEeXcdFg', 'abcdecdfg',
1084
[('equal', 0, 1, 0, 1),
1085
('replace', 1, 2, 1, 2),
1086
('equal', 2, 4, 2, 4),
1087
('delete', 4, 5, 4, 4),
1088
('equal', 5, 6, 4, 5),
1089
('delete', 6, 7, 5, 5),
1090
('equal', 7, 9, 5, 7),
1091
('replace', 9, 10, 7, 8),
1092
('equal', 10, 11, 8, 9)
1095
def test_grouped_opcodes(self):
1096
def chk_ops(a, b, expected_codes, n=3):
1097
s = self._PatienceSequenceMatcher(None, a, b)
1098
self.assertEqual(expected_codes, list(s.get_grouped_opcodes(n)))
1102
chk_ops('abc', '', [[('delete', 0, 3, 0, 0)]])
1103
chk_ops('', 'abc', [[('insert', 0, 0, 0, 3)]])
1104
chk_ops('abcd', 'abcd', [])
1105
chk_ops('abcd', 'abce', [[('equal', 0, 3, 0, 3),
1106
('replace', 3, 4, 3, 4)
1108
chk_ops('eabc', 'abce', [[('delete', 0, 1, 0, 0),
1109
('equal', 1, 4, 0, 3),
1110
('insert', 4, 4, 3, 4)
1112
chk_ops('abcdefghijklmnop', 'abcdefxydefghijklmnop',
1113
[[('equal', 3, 6, 3, 6),
1114
('insert', 6, 6, 6, 11),
1115
('equal', 6, 9, 11, 14)
1117
chk_ops('abcdefghijklmnop', 'abcdefxydefghijklmnop',
1118
[[('equal', 2, 6, 2, 6),
1119
('insert', 6, 6, 6, 11),
1120
('equal', 6, 10, 11, 15)
1122
chk_ops('Xabcdef', 'abcdef',
1123
[[('delete', 0, 1, 0, 0),
1124
('equal', 1, 4, 0, 3)
1126
chk_ops('abcdef', 'abcdefX',
1127
[[('equal', 3, 6, 3, 6),
1128
('insert', 6, 6, 6, 7)
1132
def test_multiple_ranges(self):
1133
# There was an earlier bug where we used a bad set of ranges,
1134
# this triggers that specific bug, to make sure it doesn't regress
1135
self.assertDiffBlocks('abcdefghijklmnop',
1136
'abcXghiYZQRSTUVWXYZijklmnop',
1137
[(0, 0, 3), (6, 4, 3), (9, 20, 7)])
1139
self.assertDiffBlocks('ABCd efghIjk L',
1140
'AxyzBCn mo pqrstuvwI1 2 L',
1141
[(0, 0, 1), (1, 4, 2), (9, 19, 1), (12, 23, 3)])
1143
# These are rot13 code snippets.
1144
self.assertDiffBlocks('''\
1145
trg nqqrq jura lbh nqq n svyr va gur qverpgbel.
1147
gnxrf_netf = ['svyr*']
1148
gnxrf_bcgvbaf = ['ab-erphefr']
1150
qrs eha(frys, svyr_yvfg, ab_erphefr=Snyfr):
1151
sebz omeyvo.nqq vzcbeg fzneg_nqq, nqq_ercbegre_cevag, nqq_ercbegre_ahyy
1153
ercbegre = nqq_ercbegre_ahyy
1155
ercbegre = nqq_ercbegre_cevag
1156
fzneg_nqq(svyr_yvfg, abg ab_erphefr, ercbegre)
1159
pynff pzq_zxqve(Pbzznaq):
1160
'''.splitlines(True), '''\
1161
trg nqqrq jura lbh nqq n svyr va gur qverpgbel.
1163
--qel-eha jvyy fubj juvpu svyrf jbhyq or nqqrq, ohg abg npghnyyl
1166
gnxrf_netf = ['svyr*']
1167
gnxrf_bcgvbaf = ['ab-erphefr', 'qel-eha']
1169
qrs eha(frys, svyr_yvfg, ab_erphefr=Snyfr, qel_eha=Snyfr):
1174
# Guvf vf cbvagyrff, ohg V'q engure abg envfr na reebe
1175
npgvba = omeyvo.nqq.nqq_npgvba_ahyy
1177
npgvba = omeyvo.nqq.nqq_npgvba_cevag
1179
npgvba = omeyvo.nqq.nqq_npgvba_nqq
1181
npgvba = omeyvo.nqq.nqq_npgvba_nqq_naq_cevag
1183
omeyvo.nqq.fzneg_nqq(svyr_yvfg, abg ab_erphefr, npgvba)
1186
pynff pzq_zxqve(Pbzznaq):
1187
'''.splitlines(True)
1188
, [(0, 0, 1), (1, 4, 2), (9, 19, 1), (12, 23, 3)])
1190
def test_patience_unified_diff(self):
1191
txt_a = ['hello there\n',
1193
'how are you today?\n']
1194
txt_b = ['hello there\n',
1195
'how are you today?\n']
1196
unified_diff = patiencediff.unified_diff
1197
psm = self._PatienceSequenceMatcher
1198
self.assertEqual(['--- \n',
1200
'@@ -1,3 +1,2 @@\n',
1203
' how are you today?\n'
1205
, list(unified_diff(txt_a, txt_b,
1206
sequencematcher=psm)))
1207
txt_a = [x+'\n' for x in 'abcdefghijklmnop']
1208
txt_b = [x+'\n' for x in 'abcdefxydefghijklmnop']
1209
# This is the result with LongestCommonSubstring matching
1210
self.assertEqual(['--- \n',
1212
'@@ -1,6 +1,11 @@\n',
1224
, list(unified_diff(txt_a, txt_b)))
1225
# And the patience diff
1226
self.assertEqual(['--- \n',
1228
'@@ -4,6 +4,11 @@\n',
1241
, list(unified_diff(txt_a, txt_b,
1242
sequencematcher=psm)))
1244
def test_patience_unified_diff_with_dates(self):
1245
txt_a = ['hello there\n',
1247
'how are you today?\n']
1248
txt_b = ['hello there\n',
1249
'how are you today?\n']
1250
unified_diff = patiencediff.unified_diff
1251
psm = self._PatienceSequenceMatcher
1252
self.assertEqual(['--- a\t2008-08-08\n',
1253
'+++ b\t2008-09-09\n',
1254
'@@ -1,3 +1,2 @@\n',
1257
' how are you today?\n'
1259
, list(unified_diff(txt_a, txt_b,
1260
fromfile='a', tofile='b',
1261
fromfiledate='2008-08-08',
1262
tofiledate='2008-09-09',
1263
sequencematcher=psm)))
1266
class TestPatienceDiffLib_c(TestPatienceDiffLib):
1268
_test_needs_features = [features.compiled_patiencediff_feature]
1271
super(TestPatienceDiffLib_c, self).setUp()
1272
from breezy import _patiencediff_c
1273
self._unique_lcs = _patiencediff_c.unique_lcs_c
1274
self._recurse_matches = _patiencediff_c.recurse_matches_c
1275
self._PatienceSequenceMatcher = \
1276
_patiencediff_c.PatienceSequenceMatcher_c
1278
def test_unhashable(self):
1279
"""We should get a proper exception here."""
1280
# We need to be able to hash items in the sequence, lists are
1281
# unhashable, and thus cannot be diffed
1282
e = self.assertRaises(TypeError, self._PatienceSequenceMatcher,
1284
e = self.assertRaises(TypeError, self._PatienceSequenceMatcher,
1285
None, ['valid', []], [])
1286
e = self.assertRaises(TypeError, self._PatienceSequenceMatcher,
1287
None, ['valid'], [[]])
1288
e = self.assertRaises(TypeError, self._PatienceSequenceMatcher,
1289
None, ['valid'], ['valid', []])
1292
class TestPatienceDiffLibFiles(tests.TestCaseInTempDir):
1295
super(TestPatienceDiffLibFiles, self).setUp()
1296
self._PatienceSequenceMatcher = \
1297
_patiencediff_py.PatienceSequenceMatcher_py
1299
def test_patience_unified_diff_files(self):
1300
txt_a = [b'hello there\n',
1302
b'how are you today?\n']
1303
txt_b = [b'hello there\n',
1304
b'how are you today?\n']
1305
with open('a1', 'wb') as f: f.writelines(txt_a)
1306
with open('b1', 'wb') as f: f.writelines(txt_b)
1308
unified_diff_files = patiencediff.unified_diff_files
1309
psm = self._PatienceSequenceMatcher
1310
self.assertEqual([b'--- a1\n',
1312
b'@@ -1,3 +1,2 @@\n',
1315
b' how are you today?\n',
1317
, list(unified_diff_files(b'a1', b'b1',
1318
sequencematcher=psm)))
1320
txt_a = [x+'\n' for x in 'abcdefghijklmnop']
1321
txt_b = [x+'\n' for x in 'abcdefxydefghijklmnop']
1322
with open('a2', 'wt') as f: f.writelines(txt_a)
1323
with open('b2', 'wt') as f: f.writelines(txt_b)
1325
# This is the result with LongestCommonSubstring matching
1326
self.assertEqual([b'--- a2\n',
1328
b'@@ -1,6 +1,11 @@\n',
1340
, list(unified_diff_files(b'a2', b'b2')))
1342
# And the patience diff
1343
self.assertEqual([b'--- a2\n',
1345
b'@@ -4,6 +4,11 @@\n',
1357
list(unified_diff_files(b'a2', b'b2',
1358
sequencematcher=psm)))
1361
class TestPatienceDiffLibFiles_c(TestPatienceDiffLibFiles):
1363
_test_needs_features = [features.compiled_patiencediff_feature]
1366
super(TestPatienceDiffLibFiles_c, self).setUp()
1367
from breezy import _patiencediff_c
1368
self._PatienceSequenceMatcher = \
1369
_patiencediff_c.PatienceSequenceMatcher_c
1372
class TestUsingCompiledIfAvailable(tests.TestCase):
1374
def test_PatienceSequenceMatcher(self):
1375
if features.compiled_patiencediff_feature.available():
1376
from breezy._patiencediff_c import PatienceSequenceMatcher_c
1377
self.assertIs(PatienceSequenceMatcher_c,
1378
patiencediff.PatienceSequenceMatcher)
1380
from breezy._patiencediff_py import PatienceSequenceMatcher_py
1381
self.assertIs(PatienceSequenceMatcher_py,
1382
patiencediff.PatienceSequenceMatcher)
1384
def test_unique_lcs(self):
1385
if features.compiled_patiencediff_feature.available():
1386
from breezy._patiencediff_c import unique_lcs_c
1387
self.assertIs(unique_lcs_c,
1388
patiencediff.unique_lcs)
1390
from breezy._patiencediff_py import unique_lcs_py
1391
self.assertIs(unique_lcs_py,
1392
patiencediff.unique_lcs)
1394
def test_recurse_matches(self):
1395
if features.compiled_patiencediff_feature.available():
1396
from breezy._patiencediff_c import recurse_matches_c
1397
self.assertIs(recurse_matches_c,
1398
patiencediff.recurse_matches)
1400
from breezy._patiencediff_py import recurse_matches_py
1401
self.assertIs(recurse_matches_py,
1402
patiencediff.recurse_matches)
1405
class TestDiffFromTool(tests.TestCaseWithTransport):
1407
def test_from_string(self):
1408
diff_obj = diff.DiffFromTool.from_string('diff', None, None, None)
1409
self.addCleanup(diff_obj.finish)
1410
self.assertEqual(['diff', '@old_path', '@new_path'],
1411
diff_obj.command_template)
1413
def test_from_string_u5(self):
1414
diff_obj = diff.DiffFromTool.from_string('diff "-u 5"',
1416
self.addCleanup(diff_obj.finish)
1417
self.assertEqual(['diff', '-u 5', '@old_path', '@new_path'],
1418
diff_obj.command_template)
1419
self.assertEqual(['diff', '-u 5', 'old-path', 'new-path'],
1420
diff_obj._get_command('old-path', 'new-path'))
1422
def test_from_string_path_with_backslashes(self):
1423
self.requireFeature(features.backslashdir_feature)
1424
tool = 'C:\\Tools\\Diff.exe'
1425
diff_obj = diff.DiffFromTool.from_string(tool, None, None, None)
1426
self.addCleanup(diff_obj.finish)
1427
self.assertEqual(['C:\\Tools\\Diff.exe', '@old_path', '@new_path'],
1428
diff_obj.command_template)
1429
self.assertEqual(['C:\\Tools\\Diff.exe', 'old-path', 'new-path'],
1430
diff_obj._get_command('old-path', 'new-path'))
1432
def test_execute(self):
1434
diff_obj = diff.DiffFromTool([sys.executable, '-c',
1435
'print("@old_path @new_path")'],
1437
self.addCleanup(diff_obj.finish)
1438
diff_obj._execute('old', 'new')
1439
self.assertEqual(output.getvalue().rstrip(), b'old new')
1441
def test_execute_missing(self):
1442
diff_obj = diff.DiffFromTool(['a-tool-which-is-unlikely-to-exist'],
1444
self.addCleanup(diff_obj.finish)
1445
e = self.assertRaises(errors.ExecutableMissing, diff_obj._execute,
1447
self.assertEqual('a-tool-which-is-unlikely-to-exist could not be found'
1448
' on this machine', str(e))
1450
def test_prepare_files_creates_paths_readable_by_windows_tool(self):
1451
self.requireFeature(features.AttribFeature)
1453
tree = self.make_branch_and_tree('tree')
1454
self.build_tree_contents([('tree/file', b'content')])
1455
tree.add('file', b'file-id')
1456
tree.commit('old tree')
1458
self.addCleanup(tree.unlock)
1459
basis_tree = tree.basis_tree()
1460
basis_tree.lock_read()
1461
self.addCleanup(basis_tree.unlock)
1462
diff_obj = diff.DiffFromTool([sys.executable, '-c',
1463
'print "@old_path @new_path"'],
1464
basis_tree, tree, output)
1465
diff_obj._prepare_files('file', 'file', file_id=b'file-id')
1466
# The old content should be readonly
1467
self.assertReadableByAttrib(diff_obj._root, 'old\\file',
1469
# The new content should use the tree object, not a 'new' file anymore
1470
self.assertEndsWith(tree.basedir, 'work/tree')
1471
self.assertReadableByAttrib(tree.basedir, 'file', r'work\\tree\\file$')
1473
def assertReadableByAttrib(self, cwd, relpath, regex):
1474
proc = subprocess.Popen(['attrib', relpath],
1475
stdout=subprocess.PIPE,
1477
(result, err) = proc.communicate()
1478
self.assertContainsRe(result.replace('\r\n', '\n'), regex)
1480
def test_prepare_files(self):
1482
tree = self.make_branch_and_tree('tree')
1483
self.build_tree_contents([('tree/oldname', b'oldcontent')])
1484
self.build_tree_contents([('tree/oldname2', b'oldcontent2')])
1485
tree.add('oldname', b'file-id')
1486
tree.add('oldname2', b'file2-id')
1487
# Earliest allowable date on FAT32 filesystems is 1980-01-01
1488
tree.commit('old tree', timestamp=315532800)
1489
tree.rename_one('oldname', 'newname')
1490
tree.rename_one('oldname2', 'newname2')
1491
self.build_tree_contents([('tree/newname', b'newcontent')])
1492
self.build_tree_contents([('tree/newname2', b'newcontent2')])
1493
old_tree = tree.basis_tree()
1494
old_tree.lock_read()
1495
self.addCleanup(old_tree.unlock)
1497
self.addCleanup(tree.unlock)
1498
diff_obj = diff.DiffFromTool([sys.executable, '-c',
1499
'print "@old_path @new_path"'],
1500
old_tree, tree, output)
1501
self.addCleanup(diff_obj.finish)
1502
self.assertContainsRe(diff_obj._root, 'brz-diff-[^/]*')
1503
old_path, new_path = diff_obj._prepare_files(
1504
'oldname', 'newname', file_id=b'file-id')
1505
self.assertContainsRe(old_path, 'old/oldname$')
1506
self.assertEqual(315532800, os.stat(old_path).st_mtime)
1507
self.assertContainsRe(new_path, 'tree/newname$')
1508
self.assertFileEqual(b'oldcontent', old_path)
1509
self.assertFileEqual(b'newcontent', new_path)
1510
if osutils.host_os_dereferences_symlinks():
1511
self.assertTrue(os.path.samefile('tree/newname', new_path))
1512
# make sure we can create files with the same parent directories
1513
diff_obj._prepare_files('oldname2', 'newname2', file_id=b'file2-id')
1516
class TestDiffFromToolEncodedFilename(tests.TestCaseWithTransport):
1518
def test_encodable_filename(self):
1519
# Just checks file path for external diff tool.
1520
# We cannot change CPython's internal encoding used by os.exec*.
1521
diffobj = diff.DiffFromTool(['dummy', '@old_path', '@new_path'],
1523
for _, scenario in EncodingAdapter.encoding_scenarios:
1524
encoding = scenario['encoding']
1525
dirname = scenario['info']['directory']
1526
filename = scenario['info']['filename']
1528
self.overrideAttr(diffobj, '_fenc', lambda: encoding)
1529
relpath = dirname + u'/' + filename
1530
fullpath = diffobj._safe_filename('safe', relpath)
1531
self.assertEqual(fullpath,
1532
fullpath.encode(encoding).decode(encoding))
1533
self.assertTrue(fullpath.startswith(diffobj._root + '/safe'))
1535
def test_unencodable_filename(self):
1536
diffobj = diff.DiffFromTool(['dummy', '@old_path', '@new_path'],
1538
for _, scenario in EncodingAdapter.encoding_scenarios:
1539
encoding = scenario['encoding']
1540
dirname = scenario['info']['directory']
1541
filename = scenario['info']['filename']
1543
if encoding == 'iso-8859-1':
1544
encoding = 'iso-8859-2'
1546
encoding = 'iso-8859-1'
1548
self.overrideAttr(diffobj, '_fenc', lambda: encoding)
1549
relpath = dirname + u'/' + filename
1550
fullpath = diffobj._safe_filename('safe', relpath)
1551
self.assertEqual(fullpath,
1552
fullpath.encode(encoding).decode(encoding))
1553
self.assertTrue(fullpath.startswith(diffobj._root + '/safe'))
1556
class TestGetTreesAndBranchesToDiffLocked(tests.TestCaseWithTransport):
1558
def call_gtabtd(self, path_list, revision_specs, old_url, new_url):
1559
"""Call get_trees_and_branches_to_diff_locked."""
1560
return diff.get_trees_and_branches_to_diff_locked(
1561
path_list, revision_specs, old_url, new_url, self.addCleanup)
1563
def test_basic(self):
1564
tree = self.make_branch_and_tree('tree')
1565
(old_tree, new_tree,
1566
old_branch, new_branch,
1567
specific_files, extra_trees) = self.call_gtabtd(
1568
['tree'], None, None, None)
1570
self.assertIsInstance(old_tree, revisiontree.RevisionTree)
1571
self.assertEqual(_mod_revision.NULL_REVISION,
1572
old_tree.get_revision_id())
1573
self.assertEqual(tree.basedir, new_tree.basedir)
1574
self.assertEqual(tree.branch.base, old_branch.base)
1575
self.assertEqual(tree.branch.base, new_branch.base)
1576
self.assertIs(None, specific_files)
1577
self.assertIs(None, extra_trees)
1579
def test_with_rev_specs(self):
1580
tree = self.make_branch_and_tree('tree')
1581
self.build_tree_contents([('tree/file', b'oldcontent')])
1582
tree.add('file', b'file-id')
1583
tree.commit('old tree', timestamp=0, rev_id=b"old-id")
1584
self.build_tree_contents([('tree/file', b'newcontent')])
1585
tree.commit('new tree', timestamp=0, rev_id=b"new-id")
1587
revisions = [revisionspec.RevisionSpec.from_string('1'),
1588
revisionspec.RevisionSpec.from_string('2')]
1589
(old_tree, new_tree,
1590
old_branch, new_branch,
1591
specific_files, extra_trees) = self.call_gtabtd(
1592
['tree'], revisions, None, None)
1594
self.assertIsInstance(old_tree, revisiontree.RevisionTree)
1595
self.assertEqual(b"old-id", old_tree.get_revision_id())
1596
self.assertIsInstance(new_tree, revisiontree.RevisionTree)
1597
self.assertEqual(b"new-id", new_tree.get_revision_id())
1598
self.assertEqual(tree.branch.base, old_branch.base)
1599
self.assertEqual(tree.branch.base, new_branch.base)
1600
self.assertIs(None, specific_files)
1601
self.assertEqual(tree.basedir, extra_trees[0].basedir)