1
from bzrlib.selftest import TestBase
2
from bzrlib.diff import internal_diff
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
3
18
from cStringIO import StringIO
4
def udiff_lines(old, new):
28
revision as _mod_revision,
34
from bzrlib.tests import (
38
from bzrlib.tests.blackbox.test_diff import subst_dates
39
from bzrlib.tests.scenarios import load_tests_apply_scenarios
42
load_tests = load_tests_apply_scenarios
45
def udiff_lines(old, new, allow_binary=False):
6
internal_diff('old', old, 'new', new, output)
47
diff.internal_diff('old', old, 'new', new, output, allow_binary)
8
49
return output.readlines()
10
def check_patch(lines):
11
assert len(lines) > 1, \
12
"Not enough lines for a file header for patch:\n%s" % "".join(lines)
13
assert lines[0].startswith ('---'), \
14
'No orig line for patch:\n%s' % "".join(lines)
15
assert lines[1].startswith ('+++'), \
16
'No mod line for patch:\n%s' % "".join(lines)
17
assert len(lines) > 2, \
18
"No hunks for patch:\n%s" % "".join(lines)
19
assert lines[2].startswith('@@'),\
20
"No hunk header for patch:\n%s" % "".join(lines)
21
assert '@@' in lines[2][2:], \
22
"Unterminated hunk header for patch:\n%s" % "".join(lines)
24
class AddNL(TestBase):
26
diff generates a valid diff for patches that add a newline
52
def external_udiff_lines(old, new, use_stringio=False):
54
# StringIO has no fileno, so it tests a different codepath
57
output = tempfile.TemporaryFile()
59
diff.external_diff('old', old, 'new', new, output, diff_opts=['-u'])
61
raise tests.TestSkipped('external "diff" not present to test')
63
lines = output.readlines()
68
class TestDiffOptions(tests.TestCase):
70
def test_unified_added(self):
71
"""Check for default style '-u' only if no other style specified
74
# Verify that style defaults to unified, id est '-u' appended
75
# to option list, in the absence of an alternative style.
76
self.assertEqual(['-a', '-u'], diff.default_style_unified(['-a']))
79
class TestDiffOptionsScenarios(tests.TestCase):
81
scenarios = [(s, dict(style=s)) for s in diff.style_option_list]
82
style = None # Set by load_tests_apply_scenarios from scenarios
84
def test_unified_not_added(self):
85
# Verify that for all valid style options, '-u' is not
86
# appended to option list.
87
ret_opts = diff.default_style_unified(diff_opts=["%s" % (self.style,)])
88
self.assertEqual(["%s" % (self.style,)], ret_opts)
91
class TestDiff(tests.TestCase):
93
def test_add_nl(self):
94
"""diff generates a valid diff for patches that add a newline"""
29
95
lines = udiff_lines(['boo'], ['boo\n'])
31
assert lines[4] == '\\ No newline at end of file\n', \
32
"expected no-nl, got %r" % lines[4]
35
class AddNL2(TestBase):
37
diff generates a valid diff for patches that change last line and add a
96
self.check_patch(lines)
97
self.assertEqual(lines[4], '\\ No newline at end of file\n')
98
## "expected no-nl, got %r" % lines[4]
100
def test_add_nl_2(self):
101
"""diff generates a valid diff for patches that change last line and
41
104
lines = udiff_lines(['boo'], ['goo\n'])
43
assert lines[4] == '\\ No newline at end of file\n', \
44
"expected no-nl, got %r" % lines[4]
105
self.check_patch(lines)
106
self.assertEqual(lines[4], '\\ No newline at end of file\n')
107
## "expected no-nl, got %r" % lines[4]
46
class RemoveNL(TestBase):
48
diff generates a valid diff for patches that change last line and add a
109
def test_remove_nl(self):
110
"""diff generates a valid diff for patches that change last line and
52
113
lines = udiff_lines(['boo\n'], ['boo'])
54
assert lines[5] == '\\ No newline at end of file\n', \
55
"expected no-nl, got %r" % lines[5]
114
self.check_patch(lines)
115
self.assertEqual(lines[5], '\\ No newline at end of file\n')
116
## "expected no-nl, got %r" % lines[5]
118
def check_patch(self, lines):
119
self.assertTrue(len(lines) > 1)
120
## "Not enough lines for a file header for patch:\n%s" % "".join(lines)
121
self.assertTrue(lines[0].startswith ('---'))
122
## 'No orig line for patch:\n%s' % "".join(lines)
123
self.assertTrue(lines[1].startswith ('+++'))
124
## 'No mod line for patch:\n%s' % "".join(lines)
125
self.assertTrue(len(lines) > 2)
126
## "No hunks for patch:\n%s" % "".join(lines)
127
self.assertTrue(lines[2].startswith('@@'))
128
## "No hunk header for patch:\n%s" % "".join(lines)
129
self.assertTrue('@@' in lines[2][2:])
130
## "Unterminated hunk header for patch:\n%s" % "".join(lines)
132
def test_binary_lines(self):
134
uni_lines = [1023 * 'a' + '\x00']
135
self.assertRaises(errors.BinaryFile, udiff_lines, uni_lines , empty)
136
self.assertRaises(errors.BinaryFile, udiff_lines, empty, uni_lines)
137
udiff_lines(uni_lines , empty, allow_binary=True)
138
udiff_lines(empty, uni_lines, allow_binary=True)
140
def test_external_diff(self):
141
lines = external_udiff_lines(['boo\n'], ['goo\n'])
142
self.check_patch(lines)
143
self.assertEqual('\n', lines[-1])
145
def test_external_diff_no_fileno(self):
146
# Make sure that we can handle not having a fileno, even
147
# if the diff is large
148
lines = external_udiff_lines(['boo\n']*10000,
151
self.check_patch(lines)
153
def test_external_diff_binary_lang_c(self):
154
for lang in ('LANG', 'LC_ALL', 'LANGUAGE'):
155
self.overrideEnv(lang, 'C')
156
lines = external_udiff_lines(['\x00foobar\n'], ['foo\x00bar\n'])
157
# Older versions of diffutils say "Binary files", newer
158
# versions just say "Files".
159
self.assertContainsRe(lines[0], '(Binary f|F)iles old and new differ\n')
160
self.assertEqual(lines[1:], ['\n'])
162
def test_no_external_diff(self):
163
"""Check that NoDiff is raised when diff is not available"""
164
# Make sure no 'diff' command is available
165
# XXX: Weird, using None instead of '' breaks the test -- vila 20101216
166
self.overrideEnv('PATH', '')
167
self.assertRaises(errors.NoDiff, diff.external_diff,
168
'old', ['boo\n'], 'new', ['goo\n'],
169
StringIO(), diff_opts=['-u'])
171
def test_internal_diff_default(self):
172
# Default internal diff encoding is utf8
174
diff.internal_diff(u'old_\xb5', ['old_text\n'],
175
u'new_\xe5', ['new_text\n'], output)
176
lines = output.getvalue().splitlines(True)
177
self.check_patch(lines)
178
self.assertEqual(['--- old_\xc2\xb5\n',
179
'+++ new_\xc3\xa5\n',
187
def test_internal_diff_utf8(self):
189
diff.internal_diff(u'old_\xb5', ['old_text\n'],
190
u'new_\xe5', ['new_text\n'], output,
191
path_encoding='utf8')
192
lines = output.getvalue().splitlines(True)
193
self.check_patch(lines)
194
self.assertEqual(['--- old_\xc2\xb5\n',
195
'+++ new_\xc3\xa5\n',
203
def test_internal_diff_iso_8859_1(self):
205
diff.internal_diff(u'old_\xb5', ['old_text\n'],
206
u'new_\xe5', ['new_text\n'], output,
207
path_encoding='iso-8859-1')
208
lines = output.getvalue().splitlines(True)
209
self.check_patch(lines)
210
self.assertEqual(['--- old_\xb5\n',
219
def test_internal_diff_no_content(self):
221
diff.internal_diff(u'old', [], u'new', [], output)
222
self.assertEqual('', output.getvalue())
224
def test_internal_diff_no_changes(self):
226
diff.internal_diff(u'old', ['text\n', 'contents\n'],
227
u'new', ['text\n', 'contents\n'],
229
self.assertEqual('', output.getvalue())
231
def test_internal_diff_returns_bytes(self):
233
output = StringIO.StringIO()
234
diff.internal_diff(u'old_\xb5', ['old_text\n'],
235
u'new_\xe5', ['new_text\n'], output)
236
self.assertIsInstance(output.getvalue(), str,
237
'internal_diff should return bytestrings')
239
def test_internal_diff_default_context(self):
241
diff.internal_diff('old', ['same_text\n','same_text\n','same_text\n',
242
'same_text\n','same_text\n','old_text\n'],
243
'new', ['same_text\n','same_text\n','same_text\n',
244
'same_text\n','same_text\n','new_text\n'], output)
245
lines = output.getvalue().splitlines(True)
246
self.check_patch(lines)
247
self.assertEqual(['--- old\n',
259
def test_internal_diff_no_context(self):
261
diff.internal_diff('old', ['same_text\n','same_text\n','same_text\n',
262
'same_text\n','same_text\n','old_text\n'],
263
'new', ['same_text\n','same_text\n','same_text\n',
264
'same_text\n','same_text\n','new_text\n'], output,
266
lines = output.getvalue().splitlines(True)
267
self.check_patch(lines)
268
self.assertEqual(['--- old\n',
277
def test_internal_diff_more_context(self):
279
diff.internal_diff('old', ['same_text\n','same_text\n','same_text\n',
280
'same_text\n','same_text\n','old_text\n'],
281
'new', ['same_text\n','same_text\n','same_text\n',
282
'same_text\n','same_text\n','new_text\n'], output,
284
lines = output.getvalue().splitlines(True)
285
self.check_patch(lines)
286
self.assertEqual(['--- old\n',
303
class TestDiffFiles(tests.TestCaseInTempDir):
305
def test_external_diff_binary(self):
306
"""The output when using external diff should use diff's i18n error"""
307
# Make sure external_diff doesn't fail in the current LANG
308
lines = external_udiff_lines(['\x00foobar\n'], ['foo\x00bar\n'])
310
cmd = ['diff', '-u', '--binary', 'old', 'new']
311
with open('old', 'wb') as f: f.write('\x00foobar\n')
312
with open('new', 'wb') as f: f.write('foo\x00bar\n')
313
pipe = subprocess.Popen(cmd, stdout=subprocess.PIPE,
314
stdin=subprocess.PIPE)
315
out, err = pipe.communicate()
316
# We should output whatever diff tells us, plus a trailing newline
317
self.assertEqual(out.splitlines(True) + ['\n'], lines)
320
def get_diff_as_string(tree1, tree2, specific_files=None, working_tree=None):
322
if working_tree is not None:
323
extra_trees = (working_tree,)
326
diff.show_diff_trees(tree1, tree2, output,
327
specific_files=specific_files,
328
extra_trees=extra_trees, old_label='old/',
330
return output.getvalue()
333
class TestDiffDates(tests.TestCaseWithTransport):
336
super(TestDiffDates, self).setUp()
337
self.wt = self.make_branch_and_tree('.')
338
self.b = self.wt.branch
339
self.build_tree_contents([
340
('file1', 'file1 contents at rev 1\n'),
341
('file2', 'file2 contents at rev 1\n')
343
self.wt.add(['file1', 'file2'])
345
message='Revision 1',
346
timestamp=1143849600, # 2006-04-01 00:00:00 UTC
349
self.build_tree_contents([('file1', 'file1 contents at rev 2\n')])
351
message='Revision 2',
352
timestamp=1143936000, # 2006-04-02 00:00:00 UTC
355
self.build_tree_contents([('file2', 'file2 contents at rev 3\n')])
357
message='Revision 3',
358
timestamp=1144022400, # 2006-04-03 00:00:00 UTC
361
self.wt.remove(['file2'])
363
message='Revision 4',
364
timestamp=1144108800, # 2006-04-04 00:00:00 UTC
367
self.build_tree_contents([
368
('file1', 'file1 contents in working tree\n')
370
# set the date stamps for files in the working tree to known values
371
os.utime('file1', (1144195200, 1144195200)) # 2006-04-05 00:00:00 UTC
373
def test_diff_rev_tree_working_tree(self):
374
output = get_diff_as_string(self.wt.basis_tree(), self.wt)
375
# note that the date for old/file1 is from rev 2 rather than from
376
# the basis revision (rev 4)
377
self.assertEqualDiff(output, '''\
378
=== modified file 'file1'
379
--- old/file1\t2006-04-02 00:00:00 +0000
380
+++ new/file1\t2006-04-05 00:00:00 +0000
382
-file1 contents at rev 2
383
+file1 contents in working tree
387
def test_diff_rev_tree_rev_tree(self):
388
tree1 = self.b.repository.revision_tree('rev-2')
389
tree2 = self.b.repository.revision_tree('rev-3')
390
output = get_diff_as_string(tree1, tree2)
391
self.assertEqualDiff(output, '''\
392
=== modified file 'file2'
393
--- old/file2\t2006-04-01 00:00:00 +0000
394
+++ new/file2\t2006-04-03 00:00:00 +0000
396
-file2 contents at rev 1
397
+file2 contents at rev 3
401
def test_diff_add_files(self):
402
tree1 = self.b.repository.revision_tree(_mod_revision.NULL_REVISION)
403
tree2 = self.b.repository.revision_tree('rev-1')
404
output = get_diff_as_string(tree1, tree2)
405
# the files have the epoch time stamp for the tree in which
407
self.assertEqualDiff(output, '''\
408
=== added file 'file1'
409
--- old/file1\t1970-01-01 00:00:00 +0000
410
+++ new/file1\t2006-04-01 00:00:00 +0000
412
+file1 contents at rev 1
414
=== added file 'file2'
415
--- old/file2\t1970-01-01 00:00:00 +0000
416
+++ new/file2\t2006-04-01 00:00:00 +0000
418
+file2 contents at rev 1
422
def test_diff_remove_files(self):
423
tree1 = self.b.repository.revision_tree('rev-3')
424
tree2 = self.b.repository.revision_tree('rev-4')
425
output = get_diff_as_string(tree1, tree2)
426
# the file has the epoch time stamp for the tree in which
428
self.assertEqualDiff(output, '''\
429
=== removed file 'file2'
430
--- old/file2\t2006-04-03 00:00:00 +0000
431
+++ new/file2\t1970-01-01 00:00:00 +0000
433
-file2 contents at rev 3
437
def test_show_diff_specified(self):
438
"""A working tree filename can be used to identify a file"""
439
self.wt.rename_one('file1', 'file1b')
440
old_tree = self.b.repository.revision_tree('rev-1')
441
new_tree = self.b.repository.revision_tree('rev-4')
442
out = get_diff_as_string(old_tree, new_tree, specific_files=['file1b'],
443
working_tree=self.wt)
444
self.assertContainsRe(out, 'file1\t')
446
def test_recursive_diff(self):
447
"""Children of directories are matched"""
450
self.wt.add(['dir1', 'dir2'])
451
self.wt.rename_one('file1', 'dir1/file1')
452
old_tree = self.b.repository.revision_tree('rev-1')
453
new_tree = self.b.repository.revision_tree('rev-4')
454
out = get_diff_as_string(old_tree, new_tree, specific_files=['dir1'],
455
working_tree=self.wt)
456
self.assertContainsRe(out, 'file1\t')
457
out = get_diff_as_string(old_tree, new_tree, specific_files=['dir2'],
458
working_tree=self.wt)
459
self.assertNotContainsRe(out, 'file1\t')
462
class TestShowDiffTrees(tests.TestCaseWithTransport):
463
"""Direct tests for show_diff_trees"""
465
def test_modified_file(self):
466
"""Test when a file is modified."""
467
tree = self.make_branch_and_tree('tree')
468
self.build_tree_contents([('tree/file', 'contents\n')])
469
tree.add(['file'], ['file-id'])
470
tree.commit('one', rev_id='rev-1')
472
self.build_tree_contents([('tree/file', 'new contents\n')])
473
d = get_diff_as_string(tree.basis_tree(), tree)
474
self.assertContainsRe(d, "=== modified file 'file'\n")
475
self.assertContainsRe(d, '--- old/file\t')
476
self.assertContainsRe(d, '\\+\\+\\+ new/file\t')
477
self.assertContainsRe(d, '-contents\n'
480
def test_modified_file_in_renamed_dir(self):
481
"""Test when a file is modified in a renamed directory."""
482
tree = self.make_branch_and_tree('tree')
483
self.build_tree(['tree/dir/'])
484
self.build_tree_contents([('tree/dir/file', 'contents\n')])
485
tree.add(['dir', 'dir/file'], ['dir-id', 'file-id'])
486
tree.commit('one', rev_id='rev-1')
488
tree.rename_one('dir', 'other')
489
self.build_tree_contents([('tree/other/file', 'new contents\n')])
490
d = get_diff_as_string(tree.basis_tree(), tree)
491
self.assertContainsRe(d, "=== renamed directory 'dir' => 'other'\n")
492
self.assertContainsRe(d, "=== modified file 'other/file'\n")
493
# XXX: This is technically incorrect, because it used to be at another
494
# location. What to do?
495
self.assertContainsRe(d, '--- old/dir/file\t')
496
self.assertContainsRe(d, '\\+\\+\\+ new/other/file\t')
497
self.assertContainsRe(d, '-contents\n'
500
def test_renamed_directory(self):
501
"""Test when only a directory is only renamed."""
502
tree = self.make_branch_and_tree('tree')
503
self.build_tree(['tree/dir/'])
504
self.build_tree_contents([('tree/dir/file', 'contents\n')])
505
tree.add(['dir', 'dir/file'], ['dir-id', 'file-id'])
506
tree.commit('one', rev_id='rev-1')
508
tree.rename_one('dir', 'newdir')
509
d = get_diff_as_string(tree.basis_tree(), tree)
510
# Renaming a directory should be a single "you renamed this dir" even
511
# when there are files inside.
512
self.assertEqual(d, "=== renamed directory 'dir' => 'newdir'\n")
514
def test_renamed_file(self):
515
"""Test when a file is only renamed."""
516
tree = self.make_branch_and_tree('tree')
517
self.build_tree_contents([('tree/file', 'contents\n')])
518
tree.add(['file'], ['file-id'])
519
tree.commit('one', rev_id='rev-1')
521
tree.rename_one('file', 'newname')
522
d = get_diff_as_string(tree.basis_tree(), tree)
523
self.assertContainsRe(d, "=== renamed file 'file' => 'newname'\n")
524
# We shouldn't have a --- or +++ line, because there is no content
526
self.assertNotContainsRe(d, '---')
528
def test_renamed_and_modified_file(self):
529
"""Test when a file is only renamed."""
530
tree = self.make_branch_and_tree('tree')
531
self.build_tree_contents([('tree/file', 'contents\n')])
532
tree.add(['file'], ['file-id'])
533
tree.commit('one', rev_id='rev-1')
535
tree.rename_one('file', 'newname')
536
self.build_tree_contents([('tree/newname', 'new contents\n')])
537
d = get_diff_as_string(tree.basis_tree(), tree)
538
self.assertContainsRe(d, "=== renamed file 'file' => 'newname'\n")
539
self.assertContainsRe(d, '--- old/file\t')
540
self.assertContainsRe(d, '\\+\\+\\+ new/newname\t')
541
self.assertContainsRe(d, '-contents\n'
545
def test_internal_diff_exec_property(self):
546
tree = self.make_branch_and_tree('tree')
548
tt = transform.TreeTransform(tree)
549
tt.new_file('a', tt.root, 'contents\n', 'a-id', True)
550
tt.new_file('b', tt.root, 'contents\n', 'b-id', False)
551
tt.new_file('c', tt.root, 'contents\n', 'c-id', True)
552
tt.new_file('d', tt.root, 'contents\n', 'd-id', False)
553
tt.new_file('e', tt.root, 'contents\n', 'control-e-id', True)
554
tt.new_file('f', tt.root, 'contents\n', 'control-f-id', False)
556
tree.commit('one', rev_id='rev-1')
558
tt = transform.TreeTransform(tree)
559
tt.set_executability(False, tt.trans_id_file_id('a-id'))
560
tt.set_executability(True, tt.trans_id_file_id('b-id'))
561
tt.set_executability(False, tt.trans_id_file_id('c-id'))
562
tt.set_executability(True, tt.trans_id_file_id('d-id'))
564
tree.rename_one('c', 'new-c')
565
tree.rename_one('d', 'new-d')
567
d = get_diff_as_string(tree.basis_tree(), tree)
569
self.assertContainsRe(d, r"file 'a'.*\(properties changed:"
571
self.assertContainsRe(d, r"file 'b'.*\(properties changed:"
573
self.assertContainsRe(d, r"file 'c'.*\(properties changed:"
575
self.assertContainsRe(d, r"file 'd'.*\(properties changed:"
577
self.assertNotContainsRe(d, r"file 'e'")
578
self.assertNotContainsRe(d, r"file 'f'")
580
def test_binary_unicode_filenames(self):
581
"""Test that contents of files are *not* encoded in UTF-8 when there
582
is a binary file in the diff.
584
# See https://bugs.launchpad.net/bugs/110092.
585
self.requireFeature(features.UnicodeFilenameFeature)
587
# This bug isn't triggered with cStringIO.
588
from StringIO import StringIO
589
tree = self.make_branch_and_tree('tree')
590
alpha, omega = u'\u03b1', u'\u03c9'
591
alpha_utf8, omega_utf8 = alpha.encode('utf8'), omega.encode('utf8')
592
self.build_tree_contents(
593
[('tree/' + alpha, chr(0)),
595
('The %s and the %s\n' % (alpha_utf8, omega_utf8)))])
596
tree.add([alpha], ['file-id'])
597
tree.add([omega], ['file-id-2'])
598
diff_content = StringIO()
599
diff.show_diff_trees(tree.basis_tree(), tree, diff_content)
600
d = diff_content.getvalue()
601
self.assertContainsRe(d, r"=== added file '%s'" % alpha_utf8)
602
self.assertContainsRe(d, "Binary files a/%s.*and b/%s.* differ\n"
603
% (alpha_utf8, alpha_utf8))
604
self.assertContainsRe(d, r"=== added file '%s'" % omega_utf8)
605
self.assertContainsRe(d, r"--- a/%s" % (omega_utf8,))
606
self.assertContainsRe(d, r"\+\+\+ b/%s" % (omega_utf8,))
608
def test_unicode_filename(self):
609
"""Test when the filename are unicode."""
610
self.requireFeature(features.UnicodeFilenameFeature)
612
alpha, omega = u'\u03b1', u'\u03c9'
613
autf8, outf8 = alpha.encode('utf8'), omega.encode('utf8')
615
tree = self.make_branch_and_tree('tree')
616
self.build_tree_contents([('tree/ren_'+alpha, 'contents\n')])
617
tree.add(['ren_'+alpha], ['file-id-2'])
618
self.build_tree_contents([('tree/del_'+alpha, 'contents\n')])
619
tree.add(['del_'+alpha], ['file-id-3'])
620
self.build_tree_contents([('tree/mod_'+alpha, 'contents\n')])
621
tree.add(['mod_'+alpha], ['file-id-4'])
623
tree.commit('one', rev_id='rev-1')
625
tree.rename_one('ren_'+alpha, 'ren_'+omega)
626
tree.remove('del_'+alpha)
627
self.build_tree_contents([('tree/add_'+alpha, 'contents\n')])
628
tree.add(['add_'+alpha], ['file-id'])
629
self.build_tree_contents([('tree/mod_'+alpha, 'contents_mod\n')])
631
d = get_diff_as_string(tree.basis_tree(), tree)
632
self.assertContainsRe(d,
633
"=== renamed file 'ren_%s' => 'ren_%s'\n"%(autf8, outf8))
634
self.assertContainsRe(d, "=== added file 'add_%s'"%autf8)
635
self.assertContainsRe(d, "=== modified file 'mod_%s'"%autf8)
636
self.assertContainsRe(d, "=== removed file 'del_%s'"%autf8)
638
def test_unicode_filename_path_encoding(self):
639
"""Test for bug #382699: unicode filenames on Windows should be shown
642
self.requireFeature(features.UnicodeFilenameFeature)
643
# The word 'test' in Russian
644
_russian_test = u'\u0422\u0435\u0441\u0442'
645
directory = _russian_test + u'/'
646
test_txt = _russian_test + u'.txt'
647
u1234 = u'\u1234.txt'
649
tree = self.make_branch_and_tree('.')
650
self.build_tree_contents([
655
tree.add([test_txt, u1234, directory])
658
diff.show_diff_trees(tree.basis_tree(), tree, sio,
659
path_encoding='cp1251')
661
output = subst_dates(sio.getvalue())
663
=== added directory '%(directory)s'
664
=== added file '%(test_txt)s'
665
--- a/%(test_txt)s\tYYYY-MM-DD HH:MM:SS +ZZZZ
666
+++ b/%(test_txt)s\tYYYY-MM-DD HH:MM:SS +ZZZZ
670
=== added file '?.txt'
671
--- a/?.txt\tYYYY-MM-DD HH:MM:SS +ZZZZ
672
+++ b/?.txt\tYYYY-MM-DD HH:MM:SS +ZZZZ
676
''' % {'directory': _russian_test.encode('cp1251'),
677
'test_txt': test_txt.encode('cp1251'),
679
self.assertEqualDiff(output, shouldbe)
682
class DiffWasIs(diff.DiffPath):
684
def diff(self, file_id, old_path, new_path, old_kind, new_kind):
685
self.to_file.write('was: ')
686
self.to_file.write(self.old_tree.get_file(file_id).read())
687
self.to_file.write('is: ')
688
self.to_file.write(self.new_tree.get_file(file_id).read())
692
class TestDiffTree(tests.TestCaseWithTransport):
695
super(TestDiffTree, self).setUp()
696
self.old_tree = self.make_branch_and_tree('old-tree')
697
self.old_tree.lock_write()
698
self.addCleanup(self.old_tree.unlock)
699
self.new_tree = self.make_branch_and_tree('new-tree')
700
self.new_tree.lock_write()
701
self.addCleanup(self.new_tree.unlock)
702
self.differ = diff.DiffTree(self.old_tree, self.new_tree, StringIO())
704
def test_diff_text(self):
705
self.build_tree_contents([('old-tree/olddir/',),
706
('old-tree/olddir/oldfile', 'old\n')])
707
self.old_tree.add('olddir')
708
self.old_tree.add('olddir/oldfile', 'file-id')
709
self.build_tree_contents([('new-tree/newdir/',),
710
('new-tree/newdir/newfile', 'new\n')])
711
self.new_tree.add('newdir')
712
self.new_tree.add('newdir/newfile', 'file-id')
713
differ = diff.DiffText(self.old_tree, self.new_tree, StringIO())
714
differ.diff_text('file-id', None, 'old label', 'new label')
716
'--- old label\n+++ new label\n@@ -1,1 +0,0 @@\n-old\n\n',
717
differ.to_file.getvalue())
718
differ.to_file.seek(0)
719
differ.diff_text(None, 'file-id', 'old label', 'new label')
721
'--- old label\n+++ new label\n@@ -0,0 +1,1 @@\n+new\n\n',
722
differ.to_file.getvalue())
723
differ.to_file.seek(0)
724
differ.diff_text('file-id', 'file-id', 'old label', 'new label')
726
'--- old label\n+++ new label\n@@ -1,1 +1,1 @@\n-old\n+new\n\n',
727
differ.to_file.getvalue())
729
def test_diff_deletion(self):
730
self.build_tree_contents([('old-tree/file', 'contents'),
731
('new-tree/file', 'contents')])
732
self.old_tree.add('file', 'file-id')
733
self.new_tree.add('file', 'file-id')
734
os.unlink('new-tree/file')
735
self.differ.show_diff(None)
736
self.assertContainsRe(self.differ.to_file.getvalue(), '-contents')
738
def test_diff_creation(self):
739
self.build_tree_contents([('old-tree/file', 'contents'),
740
('new-tree/file', 'contents')])
741
self.old_tree.add('file', 'file-id')
742
self.new_tree.add('file', 'file-id')
743
os.unlink('old-tree/file')
744
self.differ.show_diff(None)
745
self.assertContainsRe(self.differ.to_file.getvalue(), '\+contents')
747
def test_diff_symlink(self):
748
differ = diff.DiffSymlink(self.old_tree, self.new_tree, StringIO())
749
differ.diff_symlink('old target', None)
750
self.assertEqual("=== target was 'old target'\n",
751
differ.to_file.getvalue())
753
differ = diff.DiffSymlink(self.old_tree, self.new_tree, StringIO())
754
differ.diff_symlink(None, 'new target')
755
self.assertEqual("=== target is 'new target'\n",
756
differ.to_file.getvalue())
758
differ = diff.DiffSymlink(self.old_tree, self.new_tree, StringIO())
759
differ.diff_symlink('old target', 'new target')
760
self.assertEqual("=== target changed 'old target' => 'new target'\n",
761
differ.to_file.getvalue())
764
self.build_tree_contents([('old-tree/olddir/',),
765
('old-tree/olddir/oldfile', 'old\n')])
766
self.old_tree.add('olddir')
767
self.old_tree.add('olddir/oldfile', 'file-id')
768
self.build_tree_contents([('new-tree/newdir/',),
769
('new-tree/newdir/newfile', 'new\n')])
770
self.new_tree.add('newdir')
771
self.new_tree.add('newdir/newfile', 'file-id')
772
self.differ.diff('file-id', 'olddir/oldfile', 'newdir/newfile')
773
self.assertContainsRe(
774
self.differ.to_file.getvalue(),
775
r'--- olddir/oldfile.*\n\+\+\+ newdir/newfile.*\n\@\@ -1,1 \+1,1'
776
' \@\@\n-old\n\+new\n\n')
778
def test_diff_kind_change(self):
779
self.requireFeature(features.SymlinkFeature)
780
self.build_tree_contents([('old-tree/olddir/',),
781
('old-tree/olddir/oldfile', 'old\n')])
782
self.old_tree.add('olddir')
783
self.old_tree.add('olddir/oldfile', 'file-id')
784
self.build_tree(['new-tree/newdir/'])
785
os.symlink('new', 'new-tree/newdir/newfile')
786
self.new_tree.add('newdir')
787
self.new_tree.add('newdir/newfile', 'file-id')
788
self.differ.diff('file-id', 'olddir/oldfile', 'newdir/newfile')
789
self.assertContainsRe(
790
self.differ.to_file.getvalue(),
791
r'--- olddir/oldfile.*\n\+\+\+ newdir/newfile.*\n\@\@ -1,1 \+0,0'
793
self.assertContainsRe(self.differ.to_file.getvalue(),
794
"=== target is u'new'\n")
796
def test_diff_directory(self):
797
self.build_tree(['new-tree/new-dir/'])
798
self.new_tree.add('new-dir', 'new-dir-id')
799
self.differ.diff('new-dir-id', None, 'new-dir')
800
self.assertEqual(self.differ.to_file.getvalue(), '')
802
def create_old_new(self):
803
self.build_tree_contents([('old-tree/olddir/',),
804
('old-tree/olddir/oldfile', 'old\n')])
805
self.old_tree.add('olddir')
806
self.old_tree.add('olddir/oldfile', 'file-id')
807
self.build_tree_contents([('new-tree/newdir/',),
808
('new-tree/newdir/newfile', 'new\n')])
809
self.new_tree.add('newdir')
810
self.new_tree.add('newdir/newfile', 'file-id')
812
def test_register_diff(self):
813
self.create_old_new()
814
old_diff_factories = diff.DiffTree.diff_factories
815
diff.DiffTree.diff_factories=old_diff_factories[:]
816
diff.DiffTree.diff_factories.insert(0, DiffWasIs.from_diff_tree)
818
differ = diff.DiffTree(self.old_tree, self.new_tree, StringIO())
820
diff.DiffTree.diff_factories = old_diff_factories
821
differ.diff('file-id', 'olddir/oldfile', 'newdir/newfile')
822
self.assertNotContainsRe(
823
differ.to_file.getvalue(),
824
r'--- olddir/oldfile.*\n\+\+\+ newdir/newfile.*\n\@\@ -1,1 \+1,1'
825
' \@\@\n-old\n\+new\n\n')
826
self.assertContainsRe(differ.to_file.getvalue(),
827
'was: old\nis: new\n')
829
def test_extra_factories(self):
830
self.create_old_new()
831
differ = diff.DiffTree(self.old_tree, self.new_tree, StringIO(),
832
extra_factories=[DiffWasIs.from_diff_tree])
833
differ.diff('file-id', 'olddir/oldfile', 'newdir/newfile')
834
self.assertNotContainsRe(
835
differ.to_file.getvalue(),
836
r'--- olddir/oldfile.*\n\+\+\+ newdir/newfile.*\n\@\@ -1,1 \+1,1'
837
' \@\@\n-old\n\+new\n\n')
838
self.assertContainsRe(differ.to_file.getvalue(),
839
'was: old\nis: new\n')
841
def test_alphabetical_order(self):
842
self.build_tree(['new-tree/a-file'])
843
self.new_tree.add('a-file')
844
self.build_tree(['old-tree/b-file'])
845
self.old_tree.add('b-file')
846
self.differ.show_diff(None)
847
self.assertContainsRe(self.differ.to_file.getvalue(),
848
'.*a-file(.|\n)*b-file')
851
class TestPatienceDiffLib(tests.TestCase):
854
super(TestPatienceDiffLib, self).setUp()
855
self._unique_lcs = _patiencediff_py.unique_lcs_py
856
self._recurse_matches = _patiencediff_py.recurse_matches_py
857
self._PatienceSequenceMatcher = \
858
_patiencediff_py.PatienceSequenceMatcher_py
860
def test_diff_unicode_string(self):
861
a = ''.join([unichr(i) for i in range(4000, 4500, 3)])
862
b = ''.join([unichr(i) for i in range(4300, 4800, 2)])
863
sm = self._PatienceSequenceMatcher(None, a, b)
864
mb = sm.get_matching_blocks()
865
self.assertEqual(35, len(mb))
867
def test_unique_lcs(self):
868
unique_lcs = self._unique_lcs
869
self.assertEqual(unique_lcs('', ''), [])
870
self.assertEqual(unique_lcs('', 'a'), [])
871
self.assertEqual(unique_lcs('a', ''), [])
872
self.assertEqual(unique_lcs('a', 'a'), [(0,0)])
873
self.assertEqual(unique_lcs('a', 'b'), [])
874
self.assertEqual(unique_lcs('ab', 'ab'), [(0,0), (1,1)])
875
self.assertEqual(unique_lcs('abcde', 'cdeab'), [(2,0), (3,1), (4,2)])
876
self.assertEqual(unique_lcs('cdeab', 'abcde'), [(0,2), (1,3), (2,4)])
877
self.assertEqual(unique_lcs('abXde', 'abYde'), [(0,0), (1,1),
879
self.assertEqual(unique_lcs('acbac', 'abc'), [(2,1)])
881
def test_recurse_matches(self):
882
def test_one(a, b, matches):
884
self._recurse_matches(
885
a, b, 0, 0, len(a), len(b), test_matches, 10)
886
self.assertEqual(test_matches, matches)
888
test_one(['a', '', 'b', '', 'c'], ['a', 'a', 'b', 'c', 'c'],
889
[(0, 0), (2, 2), (4, 4)])
890
test_one(['a', 'c', 'b', 'a', 'c'], ['a', 'b', 'c'],
891
[(0, 0), (2, 1), (4, 2)])
892
# Even though 'bc' is not unique globally, and is surrounded by
893
# non-matching lines, we should still match, because they are locally
895
test_one('abcdbce', 'afbcgdbce', [(0,0), (1, 2), (2, 3), (3, 5),
896
(4, 6), (5, 7), (6, 8)])
898
# recurse_matches doesn't match non-unique
899
# lines surrounded by bogus text.
900
# The update has been done in patiencediff.SequenceMatcher instead
902
# This is what it could be
903
#test_one('aBccDe', 'abccde', [(0,0), (2,2), (3,3), (5,5)])
905
# This is what it currently gives:
906
test_one('aBccDe', 'abccde', [(0,0), (5,5)])
908
def assertDiffBlocks(self, a, b, expected_blocks):
909
"""Check that the sequence matcher returns the correct blocks.
911
:param a: A sequence to match
912
:param b: Another sequence to match
913
:param expected_blocks: The expected output, not including the final
914
matching block (len(a), len(b), 0)
916
matcher = self._PatienceSequenceMatcher(None, a, b)
917
blocks = matcher.get_matching_blocks()
919
self.assertEqual((len(a), len(b), 0), last)
920
self.assertEqual(expected_blocks, blocks)
922
def test_matching_blocks(self):
923
# Some basic matching tests
924
self.assertDiffBlocks('', '', [])
925
self.assertDiffBlocks([], [], [])
926
self.assertDiffBlocks('abc', '', [])
927
self.assertDiffBlocks('', 'abc', [])
928
self.assertDiffBlocks('abcd', 'abcd', [(0, 0, 4)])
929
self.assertDiffBlocks('abcd', 'abce', [(0, 0, 3)])
930
self.assertDiffBlocks('eabc', 'abce', [(1, 0, 3)])
931
self.assertDiffBlocks('eabce', 'abce', [(1, 0, 4)])
932
self.assertDiffBlocks('abcde', 'abXde', [(0, 0, 2), (3, 3, 2)])
933
self.assertDiffBlocks('abcde', 'abXYZde', [(0, 0, 2), (3, 5, 2)])
934
self.assertDiffBlocks('abde', 'abXYZde', [(0, 0, 2), (2, 5, 2)])
935
# This may check too much, but it checks to see that
936
# a copied block stays attached to the previous section,
938
# difflib would tend to grab the trailing longest match
939
# which would make the diff not look right
940
self.assertDiffBlocks('abcdefghijklmnop', 'abcdefxydefghijklmnop',
941
[(0, 0, 6), (6, 11, 10)])
943
# make sure it supports passing in lists
944
self.assertDiffBlocks(
947
'how are you today?\n'],
949
'how are you today?\n'],
950
[(0, 0, 1), (2, 1, 1)])
952
# non unique lines surrounded by non-matching lines
954
self.assertDiffBlocks('aBccDe', 'abccde', [(0,0,1), (5,5,1)])
956
# But they only need to be locally unique
957
self.assertDiffBlocks('aBcDec', 'abcdec', [(0,0,1), (2,2,1), (4,4,2)])
959
# non unique blocks won't be matched
960
self.assertDiffBlocks('aBcdEcdFg', 'abcdecdfg', [(0,0,1), (8,8,1)])
962
# but locally unique ones will
963
self.assertDiffBlocks('aBcdEeXcdFg', 'abcdecdfg', [(0,0,1), (2,2,2),
964
(5,4,1), (7,5,2), (10,8,1)])
966
self.assertDiffBlocks('abbabbXd', 'cabbabxd', [(7,7,1)])
967
self.assertDiffBlocks('abbabbbb', 'cabbabbc', [])
968
self.assertDiffBlocks('bbbbbbbb', 'cbbbbbbc', [])
970
def test_matching_blocks_tuples(self):
971
# Some basic matching tests
972
self.assertDiffBlocks([], [], [])
973
self.assertDiffBlocks([('a',), ('b',), ('c,')], [], [])
974
self.assertDiffBlocks([], [('a',), ('b',), ('c,')], [])
975
self.assertDiffBlocks([('a',), ('b',), ('c,')],
976
[('a',), ('b',), ('c,')],
978
self.assertDiffBlocks([('a',), ('b',), ('c,')],
979
[('a',), ('b',), ('d,')],
981
self.assertDiffBlocks([('d',), ('b',), ('c,')],
982
[('a',), ('b',), ('c,')],
984
self.assertDiffBlocks([('d',), ('a',), ('b',), ('c,')],
985
[('a',), ('b',), ('c,')],
987
self.assertDiffBlocks([('a', 'b'), ('c', 'd'), ('e', 'f')],
988
[('a', 'b'), ('c', 'X'), ('e', 'f')],
989
[(0, 0, 1), (2, 2, 1)])
990
self.assertDiffBlocks([('a', 'b'), ('c', 'd'), ('e', 'f')],
991
[('a', 'b'), ('c', 'dX'), ('e', 'f')],
992
[(0, 0, 1), (2, 2, 1)])
994
def test_opcodes(self):
995
def chk_ops(a, b, expected_codes):
996
s = self._PatienceSequenceMatcher(None, a, b)
997
self.assertEqual(expected_codes, s.get_opcodes())
1001
chk_ops('abc', '', [('delete', 0,3, 0,0)])
1002
chk_ops('', 'abc', [('insert', 0,0, 0,3)])
1003
chk_ops('abcd', 'abcd', [('equal', 0,4, 0,4)])
1004
chk_ops('abcd', 'abce', [('equal', 0,3, 0,3),
1005
('replace', 3,4, 3,4)
1007
chk_ops('eabc', 'abce', [('delete', 0,1, 0,0),
1008
('equal', 1,4, 0,3),
1009
('insert', 4,4, 3,4)
1011
chk_ops('eabce', 'abce', [('delete', 0,1, 0,0),
1014
chk_ops('abcde', 'abXde', [('equal', 0,2, 0,2),
1015
('replace', 2,3, 2,3),
1018
chk_ops('abcde', 'abXYZde', [('equal', 0,2, 0,2),
1019
('replace', 2,3, 2,5),
1022
chk_ops('abde', 'abXYZde', [('equal', 0,2, 0,2),
1023
('insert', 2,2, 2,5),
1026
chk_ops('abcdefghijklmnop', 'abcdefxydefghijklmnop',
1027
[('equal', 0,6, 0,6),
1028
('insert', 6,6, 6,11),
1029
('equal', 6,16, 11,21)
1034
, 'how are you today?\n'],
1036
, 'how are you today?\n'],
1037
[('equal', 0,1, 0,1),
1038
('delete', 1,2, 1,1),
1039
('equal', 2,3, 1,2),
1041
chk_ops('aBccDe', 'abccde',
1042
[('equal', 0,1, 0,1),
1043
('replace', 1,5, 1,5),
1044
('equal', 5,6, 5,6),
1046
chk_ops('aBcDec', 'abcdec',
1047
[('equal', 0,1, 0,1),
1048
('replace', 1,2, 1,2),
1049
('equal', 2,3, 2,3),
1050
('replace', 3,4, 3,4),
1051
('equal', 4,6, 4,6),
1053
chk_ops('aBcdEcdFg', 'abcdecdfg',
1054
[('equal', 0,1, 0,1),
1055
('replace', 1,8, 1,8),
1058
chk_ops('aBcdEeXcdFg', 'abcdecdfg',
1059
[('equal', 0,1, 0,1),
1060
('replace', 1,2, 1,2),
1061
('equal', 2,4, 2,4),
1062
('delete', 4,5, 4,4),
1063
('equal', 5,6, 4,5),
1064
('delete', 6,7, 5,5),
1065
('equal', 7,9, 5,7),
1066
('replace', 9,10, 7,8),
1067
('equal', 10,11, 8,9)
1070
def test_grouped_opcodes(self):
1071
def chk_ops(a, b, expected_codes, n=3):
1072
s = self._PatienceSequenceMatcher(None, a, b)
1073
self.assertEqual(expected_codes, list(s.get_grouped_opcodes(n)))
1077
chk_ops('abc', '', [[('delete', 0,3, 0,0)]])
1078
chk_ops('', 'abc', [[('insert', 0,0, 0,3)]])
1079
chk_ops('abcd', 'abcd', [])
1080
chk_ops('abcd', 'abce', [[('equal', 0,3, 0,3),
1081
('replace', 3,4, 3,4)
1083
chk_ops('eabc', 'abce', [[('delete', 0,1, 0,0),
1084
('equal', 1,4, 0,3),
1085
('insert', 4,4, 3,4)
1087
chk_ops('abcdefghijklmnop', 'abcdefxydefghijklmnop',
1088
[[('equal', 3,6, 3,6),
1089
('insert', 6,6, 6,11),
1090
('equal', 6,9, 11,14)
1092
chk_ops('abcdefghijklmnop', 'abcdefxydefghijklmnop',
1093
[[('equal', 2,6, 2,6),
1094
('insert', 6,6, 6,11),
1095
('equal', 6,10, 11,15)
1097
chk_ops('Xabcdef', 'abcdef',
1098
[[('delete', 0,1, 0,0),
1101
chk_ops('abcdef', 'abcdefX',
1102
[[('equal', 3,6, 3,6),
1103
('insert', 6,6, 6,7)
1107
def test_multiple_ranges(self):
1108
# There was an earlier bug where we used a bad set of ranges,
1109
# this triggers that specific bug, to make sure it doesn't regress
1110
self.assertDiffBlocks('abcdefghijklmnop',
1111
'abcXghiYZQRSTUVWXYZijklmnop',
1112
[(0, 0, 3), (6, 4, 3), (9, 20, 7)])
1114
self.assertDiffBlocks('ABCd efghIjk L',
1115
'AxyzBCn mo pqrstuvwI1 2 L',
1116
[(0,0,1), (1, 4, 2), (9, 19, 1), (12, 23, 3)])
1118
# These are rot13 code snippets.
1119
self.assertDiffBlocks('''\
1120
trg nqqrq jura lbh nqq n svyr va gur qverpgbel.
1122
gnxrf_netf = ['svyr*']
1123
gnxrf_bcgvbaf = ['ab-erphefr']
1125
qrs eha(frys, svyr_yvfg, ab_erphefr=Snyfr):
1126
sebz omeyvo.nqq vzcbeg fzneg_nqq, nqq_ercbegre_cevag, nqq_ercbegre_ahyy
1128
ercbegre = nqq_ercbegre_ahyy
1130
ercbegre = nqq_ercbegre_cevag
1131
fzneg_nqq(svyr_yvfg, abg ab_erphefr, ercbegre)
1134
pynff pzq_zxqve(Pbzznaq):
1135
'''.splitlines(True), '''\
1136
trg nqqrq jura lbh nqq n svyr va gur qverpgbel.
1138
--qel-eha jvyy fubj juvpu svyrf jbhyq or nqqrq, ohg abg npghnyyl
1141
gnxrf_netf = ['svyr*']
1142
gnxrf_bcgvbaf = ['ab-erphefr', 'qel-eha']
1144
qrs eha(frys, svyr_yvfg, ab_erphefr=Snyfr, qel_eha=Snyfr):
1149
# Guvf vf cbvagyrff, ohg V'q engure abg envfr na reebe
1150
npgvba = omeyvo.nqq.nqq_npgvba_ahyy
1152
npgvba = omeyvo.nqq.nqq_npgvba_cevag
1154
npgvba = omeyvo.nqq.nqq_npgvba_nqq
1156
npgvba = omeyvo.nqq.nqq_npgvba_nqq_naq_cevag
1158
omeyvo.nqq.fzneg_nqq(svyr_yvfg, abg ab_erphefr, npgvba)
1161
pynff pzq_zxqve(Pbzznaq):
1162
'''.splitlines(True)
1163
, [(0,0,1), (1, 4, 2), (9, 19, 1), (12, 23, 3)])
1165
def test_patience_unified_diff(self):
1166
txt_a = ['hello there\n',
1168
'how are you today?\n']
1169
txt_b = ['hello there\n',
1170
'how are you today?\n']
1171
unified_diff = patiencediff.unified_diff
1172
psm = self._PatienceSequenceMatcher
1173
self.assertEqual(['--- \n',
1175
'@@ -1,3 +1,2 @@\n',
1178
' how are you today?\n'
1180
, list(unified_diff(txt_a, txt_b,
1181
sequencematcher=psm)))
1182
txt_a = map(lambda x: x+'\n', 'abcdefghijklmnop')
1183
txt_b = map(lambda x: x+'\n', 'abcdefxydefghijklmnop')
1184
# This is the result with LongestCommonSubstring matching
1185
self.assertEqual(['--- \n',
1187
'@@ -1,6 +1,11 @@\n',
1199
, list(unified_diff(txt_a, txt_b)))
1200
# And the patience diff
1201
self.assertEqual(['--- \n',
1203
'@@ -4,6 +4,11 @@\n',
1216
, list(unified_diff(txt_a, txt_b,
1217
sequencematcher=psm)))
1219
def test_patience_unified_diff_with_dates(self):
1220
txt_a = ['hello there\n',
1222
'how are you today?\n']
1223
txt_b = ['hello there\n',
1224
'how are you today?\n']
1225
unified_diff = patiencediff.unified_diff
1226
psm = self._PatienceSequenceMatcher
1227
self.assertEqual(['--- a\t2008-08-08\n',
1228
'+++ b\t2008-09-09\n',
1229
'@@ -1,3 +1,2 @@\n',
1232
' how are you today?\n'
1234
, list(unified_diff(txt_a, txt_b,
1235
fromfile='a', tofile='b',
1236
fromfiledate='2008-08-08',
1237
tofiledate='2008-09-09',
1238
sequencematcher=psm)))
1241
class TestPatienceDiffLib_c(TestPatienceDiffLib):
1243
_test_needs_features = [features.compiled_patiencediff_feature]
1246
super(TestPatienceDiffLib_c, self).setUp()
1247
from bzrlib import _patiencediff_c
1248
self._unique_lcs = _patiencediff_c.unique_lcs_c
1249
self._recurse_matches = _patiencediff_c.recurse_matches_c
1250
self._PatienceSequenceMatcher = \
1251
_patiencediff_c.PatienceSequenceMatcher_c
1253
def test_unhashable(self):
1254
"""We should get a proper exception here."""
1255
# We need to be able to hash items in the sequence, lists are
1256
# unhashable, and thus cannot be diffed
1257
e = self.assertRaises(TypeError, self._PatienceSequenceMatcher,
1259
e = self.assertRaises(TypeError, self._PatienceSequenceMatcher,
1260
None, ['valid', []], [])
1261
e = self.assertRaises(TypeError, self._PatienceSequenceMatcher,
1262
None, ['valid'], [[]])
1263
e = self.assertRaises(TypeError, self._PatienceSequenceMatcher,
1264
None, ['valid'], ['valid', []])
1267
class TestPatienceDiffLibFiles(tests.TestCaseInTempDir):
1270
super(TestPatienceDiffLibFiles, self).setUp()
1271
self._PatienceSequenceMatcher = \
1272
_patiencediff_py.PatienceSequenceMatcher_py
1274
def test_patience_unified_diff_files(self):
1275
txt_a = ['hello there\n',
1277
'how are you today?\n']
1278
txt_b = ['hello there\n',
1279
'how are you today?\n']
1280
with open('a1', 'wb') as f: f.writelines(txt_a)
1281
with open('b1', 'wb') as f: f.writelines(txt_b)
1283
unified_diff_files = patiencediff.unified_diff_files
1284
psm = self._PatienceSequenceMatcher
1285
self.assertEqual(['--- a1\n',
1287
'@@ -1,3 +1,2 @@\n',
1290
' how are you today?\n',
1292
, list(unified_diff_files('a1', 'b1',
1293
sequencematcher=psm)))
1295
txt_a = map(lambda x: x+'\n', 'abcdefghijklmnop')
1296
txt_b = map(lambda x: x+'\n', 'abcdefxydefghijklmnop')
1297
with open('a2', 'wb') as f: f.writelines(txt_a)
1298
with open('b2', 'wb') as f: f.writelines(txt_b)
1300
# This is the result with LongestCommonSubstring matching
1301
self.assertEqual(['--- a2\n',
1303
'@@ -1,6 +1,11 @@\n',
1315
, list(unified_diff_files('a2', 'b2')))
1317
# And the patience diff
1318
self.assertEqual(['--- a2\n',
1320
'@@ -4,6 +4,11 @@\n',
1332
list(unified_diff_files('a2', 'b2',
1333
sequencematcher=psm)))
1336
class TestPatienceDiffLibFiles_c(TestPatienceDiffLibFiles):
1338
_test_needs_features = [features.compiled_patiencediff_feature]
1341
super(TestPatienceDiffLibFiles_c, self).setUp()
1342
from bzrlib import _patiencediff_c
1343
self._PatienceSequenceMatcher = \
1344
_patiencediff_c.PatienceSequenceMatcher_c
1347
class TestUsingCompiledIfAvailable(tests.TestCase):
1349
def test_PatienceSequenceMatcher(self):
1350
if features.compiled_patiencediff_feature.available():
1351
from bzrlib._patiencediff_c import PatienceSequenceMatcher_c
1352
self.assertIs(PatienceSequenceMatcher_c,
1353
patiencediff.PatienceSequenceMatcher)
1355
from bzrlib._patiencediff_py import PatienceSequenceMatcher_py
1356
self.assertIs(PatienceSequenceMatcher_py,
1357
patiencediff.PatienceSequenceMatcher)
1359
def test_unique_lcs(self):
1360
if features.compiled_patiencediff_feature.available():
1361
from bzrlib._patiencediff_c import unique_lcs_c
1362
self.assertIs(unique_lcs_c,
1363
patiencediff.unique_lcs)
1365
from bzrlib._patiencediff_py import unique_lcs_py
1366
self.assertIs(unique_lcs_py,
1367
patiencediff.unique_lcs)
1369
def test_recurse_matches(self):
1370
if features.compiled_patiencediff_feature.available():
1371
from bzrlib._patiencediff_c import recurse_matches_c
1372
self.assertIs(recurse_matches_c,
1373
patiencediff.recurse_matches)
1375
from bzrlib._patiencediff_py import recurse_matches_py
1376
self.assertIs(recurse_matches_py,
1377
patiencediff.recurse_matches)
1380
class TestDiffFromTool(tests.TestCaseWithTransport):
1382
def test_from_string(self):
1383
diff_obj = diff.DiffFromTool.from_string('diff', None, None, None)
1384
self.addCleanup(diff_obj.finish)
1385
self.assertEqual(['diff', '@old_path', '@new_path'],
1386
diff_obj.command_template)
1388
def test_from_string_u5(self):
1389
diff_obj = diff.DiffFromTool.from_string('diff "-u 5"',
1391
self.addCleanup(diff_obj.finish)
1392
self.assertEqual(['diff', '-u 5', '@old_path', '@new_path'],
1393
diff_obj.command_template)
1394
self.assertEqual(['diff', '-u 5', 'old-path', 'new-path'],
1395
diff_obj._get_command('old-path', 'new-path'))
1397
def test_from_string_path_with_backslashes(self):
1398
self.requireFeature(features.backslashdir_feature)
1399
tool = 'C:\\Tools\\Diff.exe'
1400
diff_obj = diff.DiffFromTool.from_string(tool, None, None, None)
1401
self.addCleanup(diff_obj.finish)
1402
self.assertEqual(['C:\\Tools\\Diff.exe', '@old_path', '@new_path'],
1403
diff_obj.command_template)
1404
self.assertEqual(['C:\\Tools\\Diff.exe', 'old-path', 'new-path'],
1405
diff_obj._get_command('old-path', 'new-path'))
1407
def test_execute(self):
1409
diff_obj = diff.DiffFromTool(['python', '-c',
1410
'print "@old_path @new_path"'],
1412
self.addCleanup(diff_obj.finish)
1413
diff_obj._execute('old', 'new')
1414
self.assertEqual(output.getvalue().rstrip(), 'old new')
1416
def test_execute_missing(self):
1417
diff_obj = diff.DiffFromTool(['a-tool-which-is-unlikely-to-exist'],
1419
self.addCleanup(diff_obj.finish)
1420
e = self.assertRaises(errors.ExecutableMissing, diff_obj._execute,
1422
self.assertEqual('a-tool-which-is-unlikely-to-exist could not be found'
1423
' on this machine', str(e))
1425
def test_prepare_files_creates_paths_readable_by_windows_tool(self):
1426
self.requireFeature(features.AttribFeature)
1428
tree = self.make_branch_and_tree('tree')
1429
self.build_tree_contents([('tree/file', 'content')])
1430
tree.add('file', 'file-id')
1431
tree.commit('old tree')
1433
self.addCleanup(tree.unlock)
1434
basis_tree = tree.basis_tree()
1435
basis_tree.lock_read()
1436
self.addCleanup(basis_tree.unlock)
1437
diff_obj = diff.DiffFromTool(['python', '-c',
1438
'print "@old_path @new_path"'],
1439
basis_tree, tree, output)
1440
diff_obj._prepare_files('file-id', 'file', 'file')
1441
# The old content should be readonly
1442
self.assertReadableByAttrib(diff_obj._root, 'old\\file',
1444
# The new content should use the tree object, not a 'new' file anymore
1445
self.assertEndsWith(tree.basedir, 'work/tree')
1446
self.assertReadableByAttrib(tree.basedir, 'file', r'work\\tree\\file$')
1448
def assertReadableByAttrib(self, cwd, relpath, regex):
1449
proc = subprocess.Popen(['attrib', relpath],
1450
stdout=subprocess.PIPE,
1452
(result, err) = proc.communicate()
1453
self.assertContainsRe(result.replace('\r\n', '\n'), regex)
1455
def test_prepare_files(self):
1457
tree = self.make_branch_and_tree('tree')
1458
self.build_tree_contents([('tree/oldname', 'oldcontent')])
1459
self.build_tree_contents([('tree/oldname2', 'oldcontent2')])
1460
tree.add('oldname', 'file-id')
1461
tree.add('oldname2', 'file2-id')
1462
# Earliest allowable date on FAT32 filesystems is 1980-01-01
1463
tree.commit('old tree', timestamp=315532800)
1464
tree.rename_one('oldname', 'newname')
1465
tree.rename_one('oldname2', 'newname2')
1466
self.build_tree_contents([('tree/newname', 'newcontent')])
1467
self.build_tree_contents([('tree/newname2', 'newcontent2')])
1468
old_tree = tree.basis_tree()
1469
old_tree.lock_read()
1470
self.addCleanup(old_tree.unlock)
1472
self.addCleanup(tree.unlock)
1473
diff_obj = diff.DiffFromTool(['python', '-c',
1474
'print "@old_path @new_path"'],
1475
old_tree, tree, output)
1476
self.addCleanup(diff_obj.finish)
1477
self.assertContainsRe(diff_obj._root, 'bzr-diff-[^/]*')
1478
old_path, new_path = diff_obj._prepare_files('file-id', 'oldname',
1480
self.assertContainsRe(old_path, 'old/oldname$')
1481
self.assertEqual(315532800, os.stat(old_path).st_mtime)
1482
self.assertContainsRe(new_path, 'tree/newname$')
1483
self.assertFileEqual('oldcontent', old_path)
1484
self.assertFileEqual('newcontent', new_path)
1485
if osutils.host_os_dereferences_symlinks():
1486
self.assertTrue(os.path.samefile('tree/newname', new_path))
1487
# make sure we can create files with the same parent directories
1488
diff_obj._prepare_files('file2-id', 'oldname2', 'newname2')
1491
class TestDiffFromToolEncodedFilename(tests.TestCaseWithTransport):
1493
def test_encodable_filename(self):
1494
# Just checks file path for external diff tool.
1495
# We cannot change CPython's internal encoding used by os.exec*.
1496
diffobj = diff.DiffFromTool(['dummy', '@old_path', '@new_path'],
1498
for _, scenario in EncodingAdapter.encoding_scenarios:
1499
encoding = scenario['encoding']
1500
dirname = scenario['info']['directory']
1501
filename = scenario['info']['filename']
1503
self.overrideAttr(diffobj, '_fenc', lambda: encoding)
1504
relpath = dirname + u'/' + filename
1505
fullpath = diffobj._safe_filename('safe', relpath)
1506
self.assertEqual(fullpath,
1507
fullpath.encode(encoding).decode(encoding))
1508
self.assertTrue(fullpath.startswith(diffobj._root + '/safe'))
1510
def test_unencodable_filename(self):
1511
diffobj = diff.DiffFromTool(['dummy', '@old_path', '@new_path'],
1513
for _, scenario in EncodingAdapter.encoding_scenarios:
1514
encoding = scenario['encoding']
1515
dirname = scenario['info']['directory']
1516
filename = scenario['info']['filename']
1518
if encoding == 'iso-8859-1':
1519
encoding = 'iso-8859-2'
1521
encoding = 'iso-8859-1'
1523
self.overrideAttr(diffobj, '_fenc', lambda: encoding)
1524
relpath = dirname + u'/' + filename
1525
fullpath = diffobj._safe_filename('safe', relpath)
1526
self.assertEqual(fullpath,
1527
fullpath.encode(encoding).decode(encoding))
1528
self.assertTrue(fullpath.startswith(diffobj._root + '/safe'))
1531
class TestGetTreesAndBranchesToDiffLocked(tests.TestCaseWithTransport):
1533
def call_gtabtd(self, path_list, revision_specs, old_url, new_url):
1534
"""Call get_trees_and_branches_to_diff_locked."""
1535
return diff.get_trees_and_branches_to_diff_locked(
1536
path_list, revision_specs, old_url, new_url, self.addCleanup)
1538
def test_basic(self):
1539
tree = self.make_branch_and_tree('tree')
1540
(old_tree, new_tree,
1541
old_branch, new_branch,
1542
specific_files, extra_trees) = self.call_gtabtd(
1543
['tree'], None, None, None)
1545
self.assertIsInstance(old_tree, revisiontree.RevisionTree)
1546
self.assertEqual(_mod_revision.NULL_REVISION,
1547
old_tree.get_revision_id())
1548
self.assertEqual(tree.basedir, new_tree.basedir)
1549
self.assertEqual(tree.branch.base, old_branch.base)
1550
self.assertEqual(tree.branch.base, new_branch.base)
1551
self.assertIs(None, specific_files)
1552
self.assertIs(None, extra_trees)
1554
def test_with_rev_specs(self):
1555
tree = self.make_branch_and_tree('tree')
1556
self.build_tree_contents([('tree/file', 'oldcontent')])
1557
tree.add('file', 'file-id')
1558
tree.commit('old tree', timestamp=0, rev_id="old-id")
1559
self.build_tree_contents([('tree/file', 'newcontent')])
1560
tree.commit('new tree', timestamp=0, rev_id="new-id")
1562
revisions = [revisionspec.RevisionSpec.from_string('1'),
1563
revisionspec.RevisionSpec.from_string('2')]
1564
(old_tree, new_tree,
1565
old_branch, new_branch,
1566
specific_files, extra_trees) = self.call_gtabtd(
1567
['tree'], revisions, None, None)
1569
self.assertIsInstance(old_tree, revisiontree.RevisionTree)
1570
self.assertEqual("old-id", old_tree.get_revision_id())
1571
self.assertIsInstance(new_tree, revisiontree.RevisionTree)
1572
self.assertEqual("new-id", new_tree.get_revision_id())
1573
self.assertEqual(tree.branch.base, old_branch.base)
1574
self.assertEqual(tree.branch.base, new_branch.base)
1575
self.assertIs(None, specific_files)
1576
self.assertEqual(tree.basedir, extra_trees[0].basedir)