/brz/remove-bazaar

To get this branch, use:
bzr branch http://gegoxaren.bato24.eu/bzr/brz/remove-bazaar

« back to all changes in this revision

Viewing changes to breezy/tests/test_diff.py

  • Committer: Martin
  • Date: 2018-11-16 19:10:17 UTC
  • mto: This revision was merged to the branch mainline in revision 7177.
  • Revision ID: gzlist@googlemail.com-20181116191017-kyedz1qck0ovon3h
Remove lazy_regexp reset in bt.test_source

Show diffs side-by-side

added added

removed removed

Lines of Context:
 
1
# Copyright (C) 2005-2012, 2014, 2016, 2017 Canonical Ltd
 
2
#
 
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.
 
7
#
 
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.
 
12
#
 
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
 
16
 
 
17
import os
 
18
import re
 
19
import subprocess
 
20
import sys
 
21
import tempfile
 
22
 
 
23
from .. import (
 
24
    diff,
 
25
    errors,
 
26
    osutils,
 
27
    patiencediff,
 
28
    _patiencediff_py,
 
29
    revision as _mod_revision,
 
30
    revisionspec,
 
31
    revisiontree,
 
32
    tests,
 
33
    transform,
 
34
    )
 
35
from ..sixish import (
 
36
    BytesIO,
 
37
    unichr,
 
38
    )
 
39
from ..tests import (
 
40
    features,
 
41
    EncodingAdapter,
 
42
    )
 
43
from ..tests.scenarios import load_tests_apply_scenarios
 
44
 
 
45
 
 
46
load_tests = load_tests_apply_scenarios
 
47
 
 
48
 
 
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)
 
53
 
 
54
 
 
55
def udiff_lines(old, new, allow_binary=False):
 
56
    output = BytesIO()
 
57
    diff.internal_diff('old', old, 'new', new, output, allow_binary)
 
58
    output.seek(0, 0)
 
59
    return output.readlines()
 
60
 
 
61
 
 
62
def external_udiff_lines(old, new, use_stringio=False):
 
63
    if use_stringio:
 
64
        # BytesIO has no fileno, so it tests a different codepath
 
65
        output = BytesIO()
 
66
    else:
 
67
        output = tempfile.TemporaryFile()
 
68
    try:
 
69
        diff.external_diff('old', old, 'new', new, output, diff_opts=['-u'])
 
70
    except errors.NoDiff:
 
71
        raise tests.TestSkipped('external "diff" not present to test')
 
72
    output.seek(0, 0)
 
73
    lines = output.readlines()
 
74
    output.close()
 
75
    return lines
 
76
 
 
77
 
 
78
class StubO(object):
 
79
    """Simple file-like object that allows writes with any type and records."""
 
80
 
 
81
    def __init__(self):
 
82
        self.write_record = []
 
83
 
 
84
    def write(self, data):
 
85
        self.write_record.append(data)
 
86
 
 
87
    def check_types(self, testcase, expected_type):
 
88
        testcase.assertFalse(
 
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))
 
92
 
 
93
 
 
94
class TestDiffOptions(tests.TestCase):
 
95
 
 
96
    def test_unified_added(self):
 
97
        """Check for default style '-u' only if no other style specified
 
98
        in 'diff-options'.
 
99
        """
 
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']))
 
103
 
 
104
 
 
105
class TestDiffOptionsScenarios(tests.TestCase):
 
106
 
 
107
    scenarios = [(s, dict(style=s)) for s in diff.style_option_list]
 
108
    style = None  # Set by load_tests_apply_scenarios from scenarios
 
109
 
 
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)
 
115
 
 
116
 
 
117
class TestDiff(tests.TestCase):
 
118
 
 
119
    def test_add_nl(self):
 
120
        """diff generates a valid diff for patches that add a newline"""
 
121
        lines = udiff_lines([b'boo'], [b'boo\n'])
 
122
        self.check_patch(lines)
 
123
        self.assertEqual(lines[4], b'\\ No newline at end of file\n')
 
124
        ## "expected no-nl, got %r" % lines[4]
 
125
 
 
126
    def test_add_nl_2(self):
 
127
        """diff generates a valid diff for patches that change last line and
 
128
        add a newline.
 
129
        """
 
130
        lines = udiff_lines([b'boo'], [b'goo\n'])
 
131
        self.check_patch(lines)
 
132
        self.assertEqual(lines[4], b'\\ No newline at end of file\n')
 
133
        ## "expected no-nl, got %r" % lines[4]
 
134
 
 
135
    def test_remove_nl(self):
 
136
        """diff generates a valid diff for patches that change last line and
 
137
        add a newline.
 
138
        """
 
139
        lines = udiff_lines([b'boo\n'], [b'boo'])
 
140
        self.check_patch(lines)
 
141
        self.assertEqual(lines[5], b'\\ No newline at end of file\n')
 
142
        ## "expected no-nl, got %r" % lines[5]
 
143
 
 
144
    def check_patch(self, lines):
 
145
        self.assertTrue(len(lines) > 1)
 
146
        ## "Not enough lines for a file header for patch:\n%s" % "".join(lines)
 
147
        self.assertTrue(lines[0].startswith(b'---'))
 
148
        ## 'No orig line for patch:\n%s' % "".join(lines)
 
149
        self.assertTrue(lines[1].startswith(b'+++'))
 
150
        ## 'No mod line for patch:\n%s' % "".join(lines)
 
151
        self.assertTrue(len(lines) > 2)
 
152
        ## "No hunks for patch:\n%s" % "".join(lines)
 
153
        self.assertTrue(lines[2].startswith(b'@@'))
 
154
        ## "No hunk header for patch:\n%s" % "".join(lines)
 
155
        self.assertTrue(b'@@' in lines[2][2:])
 
156
        ## "Unterminated hunk header for patch:\n%s" % "".join(lines)
 
157
 
 
158
    def test_binary_lines(self):
 
159
        empty = []
 
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)
 
165
 
 
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])
 
170
 
 
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,
 
175
                                     [b'goo\n'] * 10000,
 
176
                                     use_stringio=True)
 
177
        self.check_patch(lines)
 
178
 
 
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(
 
186
            lines[0], b'(Binary f|F)iles old and new differ\n')
 
187
        self.assertEqual(lines[1:], [b'\n'])
 
188
 
 
189
    def test_no_external_diff(self):
 
190
        """Check that NoDiff is raised when diff is not available"""
 
191
        # Make sure no 'diff' command is available
 
192
        # XXX: Weird, using None instead of '' breaks the test -- vila 20101216
 
193
        self.overrideEnv('PATH', '')
 
194
        self.assertRaises(errors.NoDiff, diff.external_diff,
 
195
                          b'old', [b'boo\n'], b'new', [b'goo\n'],
 
196
                          BytesIO(), diff_opts=['-u'])
 
197
 
 
198
    def test_internal_diff_default(self):
 
199
        # Default internal diff encoding is utf8
 
200
        output = BytesIO()
 
201
        diff.internal_diff(u'old_\xb5', [b'old_text\n'],
 
202
                           u'new_\xe5', [b'new_text\n'], output)
 
203
        lines = output.getvalue().splitlines(True)
 
204
        self.check_patch(lines)
 
205
        self.assertEqual([b'--- old_\xc2\xb5\n',
 
206
                          b'+++ new_\xc3\xa5\n',
 
207
                          b'@@ -1,1 +1,1 @@\n',
 
208
                          b'-old_text\n',
 
209
                          b'+new_text\n',
 
210
                          b'\n',
 
211
                          ], lines)
 
212
 
 
213
    def test_internal_diff_utf8(self):
 
214
        output = BytesIO()
 
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',
 
223
                          b'-old_text\n',
 
224
                          b'+new_text\n',
 
225
                          b'\n',
 
226
                          ], lines)
 
227
 
 
228
    def test_internal_diff_iso_8859_1(self):
 
229
        output = BytesIO()
 
230
        diff.internal_diff(u'old_\xb5', [b'old_text\n'],
 
231
                           u'new_\xe5', [b'new_text\n'], output,
 
232
                           path_encoding='iso-8859-1')
 
233
        lines = output.getvalue().splitlines(True)
 
234
        self.check_patch(lines)
 
235
        self.assertEqual([b'--- old_\xb5\n',
 
236
                          b'+++ new_\xe5\n',
 
237
                          b'@@ -1,1 +1,1 @@\n',
 
238
                          b'-old_text\n',
 
239
                          b'+new_text\n',
 
240
                          b'\n',
 
241
                          ], lines)
 
242
 
 
243
    def test_internal_diff_no_content(self):
 
244
        output = BytesIO()
 
245
        diff.internal_diff(u'old', [], u'new', [], output)
 
246
        self.assertEqual(b'', output.getvalue())
 
247
 
 
248
    def test_internal_diff_no_changes(self):
 
249
        output = BytesIO()
 
250
        diff.internal_diff(u'old', [b'text\n', b'contents\n'],
 
251
                           u'new', [b'text\n', b'contents\n'],
 
252
                           output)
 
253
        self.assertEqual(b'', output.getvalue())
 
254
 
 
255
    def test_internal_diff_returns_bytes(self):
 
256
        output = StubO()
 
257
        diff.internal_diff(u'old_\xb5', [b'old_text\n'],
 
258
                           u'new_\xe5', [b'new_text\n'], output)
 
259
        output.check_types(self, bytes)
 
260
 
 
261
    def test_internal_diff_default_context(self):
 
262
        output = BytesIO()
 
263
        diff.internal_diff('old', [b'same_text\n', b'same_text\n', b'same_text\n',
 
264
                                   b'same_text\n', b'same_text\n', b'old_text\n'],
 
265
                           'new', [b'same_text\n', b'same_text\n', b'same_text\n',
 
266
                                   b'same_text\n', b'same_text\n', b'new_text\n'], output)
 
267
        lines = output.getvalue().splitlines(True)
 
268
        self.check_patch(lines)
 
269
        self.assertEqual([b'--- old\n',
 
270
                          b'+++ new\n',
 
271
                          b'@@ -3,4 +3,4 @@\n',
 
272
                          b' same_text\n',
 
273
                          b' same_text\n',
 
274
                          b' same_text\n',
 
275
                          b'-old_text\n',
 
276
                          b'+new_text\n',
 
277
                          b'\n',
 
278
                          ], lines)
 
279
 
 
280
    def test_internal_diff_no_context(self):
 
281
        output = BytesIO()
 
282
        diff.internal_diff('old', [b'same_text\n', b'same_text\n', b'same_text\n',
 
283
                                   b'same_text\n', b'same_text\n', b'old_text\n'],
 
284
                           'new', [b'same_text\n', b'same_text\n', b'same_text\n',
 
285
                                   b'same_text\n', b'same_text\n', b'new_text\n'], output,
 
286
                           context_lines=0)
 
287
        lines = output.getvalue().splitlines(True)
 
288
        self.check_patch(lines)
 
289
        self.assertEqual([b'--- old\n',
 
290
                          b'+++ new\n',
 
291
                          b'@@ -6,1 +6,1 @@\n',
 
292
                          b'-old_text\n',
 
293
                          b'+new_text\n',
 
294
                          b'\n',
 
295
                          ], lines)
 
296
 
 
297
    def test_internal_diff_more_context(self):
 
298
        output = BytesIO()
 
299
        diff.internal_diff('old', [b'same_text\n', b'same_text\n', b'same_text\n',
 
300
                                   b'same_text\n', b'same_text\n', b'old_text\n'],
 
301
                           'new', [b'same_text\n', b'same_text\n', b'same_text\n',
 
302
                                   b'same_text\n', b'same_text\n', b'new_text\n'], output,
 
303
                           context_lines=4)
 
304
        lines = output.getvalue().splitlines(True)
 
305
        self.check_patch(lines)
 
306
        self.assertEqual([b'--- old\n',
 
307
                          b'+++ new\n',
 
308
                          b'@@ -2,5 +2,5 @@\n',
 
309
                          b' same_text\n',
 
310
                          b' same_text\n',
 
311
                          b' same_text\n',
 
312
                          b' same_text\n',
 
313
                          b'-old_text\n',
 
314
                          b'+new_text\n',
 
315
                          b'\n',
 
316
                          ], lines)
 
317
 
 
318
 
 
319
class TestDiffFiles(tests.TestCaseInTempDir):
 
320
 
 
321
    def test_external_diff_binary(self):
 
322
        """The output when using external diff should use diff's i18n error"""
 
323
        for lang in ('LANG', 'LC_ALL', 'LANGUAGE'):
 
324
            self.overrideEnv(lang, 'C')
 
325
        # Make sure external_diff doesn't fail in the current LANG
 
326
        lines = external_udiff_lines([b'\x00foobar\n'], [b'foo\x00bar\n'])
 
327
 
 
328
        cmd = ['diff', '-u', '--binary', 'old', 'new']
 
329
        with open('old', 'wb') as f:
 
330
            f.write(b'\x00foobar\n')
 
331
        with open('new', 'wb') as f:
 
332
            f.write(b'foo\x00bar\n')
 
333
        pipe = subprocess.Popen(cmd, stdout=subprocess.PIPE,
 
334
                                stdin=subprocess.PIPE)
 
335
        out, err = pipe.communicate()
 
336
        # We should output whatever diff tells us, plus a trailing newline
 
337
        self.assertEqual(out.splitlines(True) + [b'\n'], lines)
 
338
 
 
339
 
 
340
def get_diff_as_string(tree1, tree2, specific_files=None, working_tree=None):
 
341
    output = BytesIO()
 
342
    if working_tree is not None:
 
343
        extra_trees = (working_tree,)
 
344
    else:
 
345
        extra_trees = ()
 
346
    diff.show_diff_trees(tree1, tree2, output,
 
347
                         specific_files=specific_files,
 
348
                         extra_trees=extra_trees, old_label='old/',
 
349
                         new_label='new/')
 
350
    return output.getvalue()
 
351
 
 
352
 
 
353
class TestDiffDates(tests.TestCaseWithTransport):
 
354
 
 
355
    def setUp(self):
 
356
        super(TestDiffDates, self).setUp()
 
357
        self.wt = self.make_branch_and_tree('.')
 
358
        self.b = self.wt.branch
 
359
        self.build_tree_contents([
 
360
            ('file1', b'file1 contents at rev 1\n'),
 
361
            ('file2', b'file2 contents at rev 1\n')
 
362
            ])
 
363
        self.wt.add(['file1', 'file2'])
 
364
        self.wt.commit(
 
365
            message='Revision 1',
 
366
            timestamp=1143849600,  # 2006-04-01 00:00:00 UTC
 
367
            timezone=0,
 
368
            rev_id=b'rev-1')
 
369
        self.build_tree_contents([('file1', b'file1 contents at rev 2\n')])
 
370
        self.wt.commit(
 
371
            message='Revision 2',
 
372
            timestamp=1143936000,  # 2006-04-02 00:00:00 UTC
 
373
            timezone=28800,
 
374
            rev_id=b'rev-2')
 
375
        self.build_tree_contents([('file2', b'file2 contents at rev 3\n')])
 
376
        self.wt.commit(
 
377
            message='Revision 3',
 
378
            timestamp=1144022400,  # 2006-04-03 00:00:00 UTC
 
379
            timezone=-3600,
 
380
            rev_id=b'rev-3')
 
381
        self.wt.remove(['file2'])
 
382
        self.wt.commit(
 
383
            message='Revision 4',
 
384
            timestamp=1144108800,  # 2006-04-04 00:00:00 UTC
 
385
            timezone=0,
 
386
            rev_id=b'rev-4')
 
387
        self.build_tree_contents([
 
388
            ('file1', b'file1 contents in working tree\n')
 
389
            ])
 
390
        # set the date stamps for files in the working tree to known values
 
391
        os.utime('file1', (1144195200, 1144195200))  # 2006-04-05 00:00:00 UTC
 
392
 
 
393
    def test_diff_rev_tree_working_tree(self):
 
394
        output = get_diff_as_string(self.wt.basis_tree(), self.wt)
 
395
        # note that the date for old/file1 is from rev 2 rather than from
 
396
        # the basis revision (rev 4)
 
397
        self.assertEqualDiff(output, b'''\
 
398
=== modified file 'file1'
 
399
--- old/file1\t2006-04-02 00:00:00 +0000
 
400
+++ new/file1\t2006-04-05 00:00:00 +0000
 
401
@@ -1,1 +1,1 @@
 
402
-file1 contents at rev 2
 
403
+file1 contents in working tree
 
404
 
 
405
''')
 
406
 
 
407
    def test_diff_rev_tree_rev_tree(self):
 
408
        tree1 = self.b.repository.revision_tree(b'rev-2')
 
409
        tree2 = self.b.repository.revision_tree(b'rev-3')
 
410
        output = get_diff_as_string(tree1, tree2)
 
411
        self.assertEqualDiff(output, b'''\
 
412
=== modified file 'file2'
 
413
--- old/file2\t2006-04-01 00:00:00 +0000
 
414
+++ new/file2\t2006-04-03 00:00:00 +0000
 
415
@@ -1,1 +1,1 @@
 
416
-file2 contents at rev 1
 
417
+file2 contents at rev 3
 
418
 
 
419
''')
 
420
 
 
421
    def test_diff_add_files(self):
 
422
        tree1 = self.b.repository.revision_tree(_mod_revision.NULL_REVISION)
 
423
        tree2 = self.b.repository.revision_tree(b'rev-1')
 
424
        output = get_diff_as_string(tree1, tree2)
 
425
        # the files have the epoch time stamp for the tree in which
 
426
        # they don't exist.
 
427
        self.assertEqualDiff(output, b'''\
 
428
=== added file 'file1'
 
429
--- old/file1\t1970-01-01 00:00:00 +0000
 
430
+++ new/file1\t2006-04-01 00:00:00 +0000
 
431
@@ -0,0 +1,1 @@
 
432
+file1 contents at rev 1
 
433
 
 
434
=== added file 'file2'
 
435
--- old/file2\t1970-01-01 00:00:00 +0000
 
436
+++ new/file2\t2006-04-01 00:00:00 +0000
 
437
@@ -0,0 +1,1 @@
 
438
+file2 contents at rev 1
 
439
 
 
440
''')
 
441
 
 
442
    def test_diff_remove_files(self):
 
443
        tree1 = self.b.repository.revision_tree(b'rev-3')
 
444
        tree2 = self.b.repository.revision_tree(b'rev-4')
 
445
        output = get_diff_as_string(tree1, tree2)
 
446
        # the file has the epoch time stamp for the tree in which
 
447
        # it doesn't exist.
 
448
        self.assertEqualDiff(output, b'''\
 
449
=== removed file 'file2'
 
450
--- old/file2\t2006-04-03 00:00:00 +0000
 
451
+++ new/file2\t1970-01-01 00:00:00 +0000
 
452
@@ -1,1 +0,0 @@
 
453
-file2 contents at rev 3
 
454
 
 
455
''')
 
456
 
 
457
    def test_show_diff_specified(self):
 
458
        """A working tree filename can be used to identify a file"""
 
459
        self.wt.rename_one('file1', 'file1b')
 
460
        old_tree = self.b.repository.revision_tree(b'rev-1')
 
461
        new_tree = self.b.repository.revision_tree(b'rev-4')
 
462
        out = get_diff_as_string(old_tree, new_tree, specific_files=['file1b'],
 
463
                                 working_tree=self.wt)
 
464
        self.assertContainsRe(out, b'file1\t')
 
465
 
 
466
    def test_recursive_diff(self):
 
467
        """Children of directories are matched"""
 
468
        os.mkdir('dir1')
 
469
        os.mkdir('dir2')
 
470
        self.wt.add(['dir1', 'dir2'])
 
471
        self.wt.rename_one('file1', 'dir1/file1')
 
472
        old_tree = self.b.repository.revision_tree(b'rev-1')
 
473
        new_tree = self.b.repository.revision_tree(b'rev-4')
 
474
        out = get_diff_as_string(old_tree, new_tree, specific_files=['dir1'],
 
475
                                 working_tree=self.wt)
 
476
        self.assertContainsRe(out, b'file1\t')
 
477
        out = get_diff_as_string(old_tree, new_tree, specific_files=['dir2'],
 
478
                                 working_tree=self.wt)
 
479
        self.assertNotContainsRe(out, b'file1\t')
 
480
 
 
481
 
 
482
class TestShowDiffTrees(tests.TestCaseWithTransport):
 
483
    """Direct tests for show_diff_trees"""
 
484
 
 
485
    def test_modified_file(self):
 
486
        """Test when a file is modified."""
 
487
        tree = self.make_branch_and_tree('tree')
 
488
        self.build_tree_contents([('tree/file', b'contents\n')])
 
489
        tree.add(['file'], [b'file-id'])
 
490
        tree.commit('one', rev_id=b'rev-1')
 
491
 
 
492
        self.build_tree_contents([('tree/file', b'new contents\n')])
 
493
        d = get_diff_as_string(tree.basis_tree(), tree)
 
494
        self.assertContainsRe(d, b"=== modified file 'file'\n")
 
495
        self.assertContainsRe(d, b'--- old/file\t')
 
496
        self.assertContainsRe(d, b'\\+\\+\\+ new/file\t')
 
497
        self.assertContainsRe(d, b'-contents\n'
 
498
                                 b'\\+new contents\n')
 
499
 
 
500
    def test_modified_file_in_renamed_dir(self):
 
501
        """Test when a file is modified in a renamed directory."""
 
502
        tree = self.make_branch_and_tree('tree')
 
503
        self.build_tree(['tree/dir/'])
 
504
        self.build_tree_contents([('tree/dir/file', b'contents\n')])
 
505
        tree.add(['dir', 'dir/file'], [b'dir-id', b'file-id'])
 
506
        tree.commit('one', rev_id=b'rev-1')
 
507
 
 
508
        tree.rename_one('dir', 'other')
 
509
        self.build_tree_contents([('tree/other/file', b'new contents\n')])
 
510
        d = get_diff_as_string(tree.basis_tree(), tree)
 
511
        self.assertContainsRe(d, b"=== renamed directory 'dir' => 'other'\n")
 
512
        self.assertContainsRe(d, b"=== modified file 'other/file'\n")
 
513
        # XXX: This is technically incorrect, because it used to be at another
 
514
        # location. What to do?
 
515
        self.assertContainsRe(d, b'--- old/dir/file\t')
 
516
        self.assertContainsRe(d, b'\\+\\+\\+ new/other/file\t')
 
517
        self.assertContainsRe(d, b'-contents\n'
 
518
                                 b'\\+new contents\n')
 
519
 
 
520
    def test_renamed_directory(self):
 
521
        """Test when only a directory is only renamed."""
 
522
        tree = self.make_branch_and_tree('tree')
 
523
        self.build_tree(['tree/dir/'])
 
524
        self.build_tree_contents([('tree/dir/file', b'contents\n')])
 
525
        tree.add(['dir', 'dir/file'], [b'dir-id', b'file-id'])
 
526
        tree.commit('one', rev_id=b'rev-1')
 
527
 
 
528
        tree.rename_one('dir', 'newdir')
 
529
        d = get_diff_as_string(tree.basis_tree(), tree)
 
530
        # Renaming a directory should be a single "you renamed this dir" even
 
531
        # when there are files inside.
 
532
        self.assertEqual(d, b"=== renamed directory 'dir' => 'newdir'\n")
 
533
 
 
534
    def test_renamed_file(self):
 
535
        """Test when a file is only renamed."""
 
536
        tree = self.make_branch_and_tree('tree')
 
537
        self.build_tree_contents([('tree/file', b'contents\n')])
 
538
        tree.add(['file'], [b'file-id'])
 
539
        tree.commit('one', rev_id=b'rev-1')
 
540
 
 
541
        tree.rename_one('file', 'newname')
 
542
        d = get_diff_as_string(tree.basis_tree(), tree)
 
543
        self.assertContainsRe(d, b"=== renamed file 'file' => 'newname'\n")
 
544
        # We shouldn't have a --- or +++ line, because there is no content
 
545
        # change
 
546
        self.assertNotContainsRe(d, b'---')
 
547
 
 
548
    def test_renamed_and_modified_file(self):
 
549
        """Test when a file is only renamed."""
 
550
        tree = self.make_branch_and_tree('tree')
 
551
        self.build_tree_contents([('tree/file', b'contents\n')])
 
552
        tree.add(['file'], [b'file-id'])
 
553
        tree.commit('one', rev_id=b'rev-1')
 
554
 
 
555
        tree.rename_one('file', 'newname')
 
556
        self.build_tree_contents([('tree/newname', b'new contents\n')])
 
557
        d = get_diff_as_string(tree.basis_tree(), tree)
 
558
        self.assertContainsRe(d, b"=== renamed file 'file' => 'newname'\n")
 
559
        self.assertContainsRe(d, b'--- old/file\t')
 
560
        self.assertContainsRe(d, b'\\+\\+\\+ new/newname\t')
 
561
        self.assertContainsRe(d, b'-contents\n'
 
562
                                 b'\\+new contents\n')
 
563
 
 
564
    def test_internal_diff_exec_property(self):
 
565
        tree = self.make_branch_and_tree('tree')
 
566
 
 
567
        tt = transform.TreeTransform(tree)
 
568
        tt.new_file('a', tt.root, [b'contents\n'], b'a-id', True)
 
569
        tt.new_file('b', tt.root, [b'contents\n'], b'b-id', False)
 
570
        tt.new_file('c', tt.root, [b'contents\n'], b'c-id', True)
 
571
        tt.new_file('d', tt.root, [b'contents\n'], b'd-id', False)
 
572
        tt.new_file('e', tt.root, [b'contents\n'], b'control-e-id', True)
 
573
        tt.new_file('f', tt.root, [b'contents\n'], b'control-f-id', False)
 
574
        tt.apply()
 
575
        tree.commit('one', rev_id=b'rev-1')
 
576
 
 
577
        tt = transform.TreeTransform(tree)
 
578
        tt.set_executability(False, tt.trans_id_file_id(b'a-id'))
 
579
        tt.set_executability(True, tt.trans_id_file_id(b'b-id'))
 
580
        tt.set_executability(False, tt.trans_id_file_id(b'c-id'))
 
581
        tt.set_executability(True, tt.trans_id_file_id(b'd-id'))
 
582
        tt.apply()
 
583
        tree.rename_one('c', 'new-c')
 
584
        tree.rename_one('d', 'new-d')
 
585
 
 
586
        d = get_diff_as_string(tree.basis_tree(), tree)
 
587
 
 
588
        self.assertContainsRe(d, br"file 'a'.*\(properties changed:"
 
589
                                 br".*\+x to -x.*\)")
 
590
        self.assertContainsRe(d, br"file 'b'.*\(properties changed:"
 
591
                                 br".*-x to \+x.*\)")
 
592
        self.assertContainsRe(d, br"file 'c'.*\(properties changed:"
 
593
                                 br".*\+x to -x.*\)")
 
594
        self.assertContainsRe(d, br"file 'd'.*\(properties changed:"
 
595
                                 br".*-x to \+x.*\)")
 
596
        self.assertNotContainsRe(d, br"file 'e'")
 
597
        self.assertNotContainsRe(d, br"file 'f'")
 
598
 
 
599
    def test_binary_unicode_filenames(self):
 
600
        """Test that contents of files are *not* encoded in UTF-8 when there
 
601
        is a binary file in the diff.
 
602
        """
 
603
        # See https://bugs.launchpad.net/bugs/110092.
 
604
        self.requireFeature(features.UnicodeFilenameFeature)
 
605
 
 
606
        tree = self.make_branch_and_tree('tree')
 
607
        alpha, omega = u'\u03b1', u'\u03c9'
 
608
        alpha_utf8, omega_utf8 = alpha.encode('utf8'), omega.encode('utf8')
 
609
        self.build_tree_contents(
 
610
            [('tree/' + alpha, b'\0'),
 
611
             ('tree/' + omega,
 
612
              (b'The %s and the %s\n' % (alpha_utf8, omega_utf8)))])
 
613
        tree.add([alpha], [b'file-id'])
 
614
        tree.add([omega], [b'file-id-2'])
 
615
        diff_content = StubO()
 
616
        diff.show_diff_trees(tree.basis_tree(), tree, diff_content)
 
617
        diff_content.check_types(self, bytes)
 
618
        d = b''.join(diff_content.write_record)
 
619
        self.assertContainsRe(d, br"=== added file '%s'" % alpha_utf8)
 
620
        self.assertContainsRe(d, b"Binary files a/%s.*and b/%s.* differ\n"
 
621
                              % (alpha_utf8, alpha_utf8))
 
622
        self.assertContainsRe(d, br"=== added file '%s'" % omega_utf8)
 
623
        self.assertContainsRe(d, br"--- a/%s" % (omega_utf8,))
 
624
        self.assertContainsRe(d, br"\+\+\+ b/%s" % (omega_utf8,))
 
625
 
 
626
    def test_unicode_filename(self):
 
627
        """Test when the filename are unicode."""
 
628
        self.requireFeature(features.UnicodeFilenameFeature)
 
629
 
 
630
        alpha, omega = u'\u03b1', u'\u03c9'
 
631
        autf8, outf8 = alpha.encode('utf8'), omega.encode('utf8')
 
632
 
 
633
        tree = self.make_branch_and_tree('tree')
 
634
        self.build_tree_contents([('tree/ren_' + alpha, b'contents\n')])
 
635
        tree.add(['ren_' + alpha], [b'file-id-2'])
 
636
        self.build_tree_contents([('tree/del_' + alpha, b'contents\n')])
 
637
        tree.add(['del_' + alpha], [b'file-id-3'])
 
638
        self.build_tree_contents([('tree/mod_' + alpha, b'contents\n')])
 
639
        tree.add(['mod_' + alpha], [b'file-id-4'])
 
640
 
 
641
        tree.commit('one', rev_id=b'rev-1')
 
642
 
 
643
        tree.rename_one('ren_' + alpha, 'ren_' + omega)
 
644
        tree.remove('del_' + alpha)
 
645
        self.build_tree_contents([('tree/add_' + alpha, b'contents\n')])
 
646
        tree.add(['add_' + alpha], [b'file-id'])
 
647
        self.build_tree_contents([('tree/mod_' + alpha, b'contents_mod\n')])
 
648
 
 
649
        d = get_diff_as_string(tree.basis_tree(), tree)
 
650
        self.assertContainsRe(d,
 
651
                              b"=== renamed file 'ren_%s' => 'ren_%s'\n" % (autf8, outf8))
 
652
        self.assertContainsRe(d, b"=== added file 'add_%s'" % autf8)
 
653
        self.assertContainsRe(d, b"=== modified file 'mod_%s'" % autf8)
 
654
        self.assertContainsRe(d, b"=== removed file 'del_%s'" % autf8)
 
655
 
 
656
    def test_unicode_filename_path_encoding(self):
 
657
        """Test for bug #382699: unicode filenames on Windows should be shown
 
658
        in user encoding.
 
659
        """
 
660
        self.requireFeature(features.UnicodeFilenameFeature)
 
661
        # The word 'test' in Russian
 
662
        _russian_test = u'\u0422\u0435\u0441\u0442'
 
663
        directory = _russian_test + u'/'
 
664
        test_txt = _russian_test + u'.txt'
 
665
        u1234 = u'\u1234.txt'
 
666
 
 
667
        tree = self.make_branch_and_tree('.')
 
668
        self.build_tree_contents([
 
669
            (test_txt, b'foo\n'),
 
670
            (u1234, b'foo\n'),
 
671
            (directory, None),
 
672
            ])
 
673
        tree.add([test_txt, u1234, directory])
 
674
 
 
675
        sio = BytesIO()
 
676
        diff.show_diff_trees(tree.basis_tree(), tree, sio,
 
677
                             path_encoding='cp1251')
 
678
 
 
679
        output = subst_dates(sio.getvalue())
 
680
        shouldbe = (b'''\
 
681
=== added directory '%(directory)s'
 
682
=== added file '%(test_txt)s'
 
683
--- a/%(test_txt)s\tYYYY-MM-DD HH:MM:SS +ZZZZ
 
684
+++ b/%(test_txt)s\tYYYY-MM-DD HH:MM:SS +ZZZZ
 
685
@@ -0,0 +1,1 @@
 
686
+foo
 
687
 
 
688
=== added file '?.txt'
 
689
--- a/?.txt\tYYYY-MM-DD HH:MM:SS +ZZZZ
 
690
+++ b/?.txt\tYYYY-MM-DD HH:MM:SS +ZZZZ
 
691
@@ -0,0 +1,1 @@
 
692
+foo
 
693
 
 
694
''' % {b'directory': _russian_test.encode('cp1251'),
 
695
            b'test_txt': test_txt.encode('cp1251'),
 
696
       })
 
697
        self.assertEqualDiff(output, shouldbe)
 
698
 
 
699
 
 
700
class DiffWasIs(diff.DiffPath):
 
701
 
 
702
    def diff(self, file_id, old_path, new_path, old_kind, new_kind):
 
703
        self.to_file.write(b'was: ')
 
704
        self.to_file.write(self.old_tree.get_file(old_path).read())
 
705
        self.to_file.write(b'is: ')
 
706
        self.to_file.write(self.new_tree.get_file(new_path).read())
 
707
 
 
708
 
 
709
class TestDiffTree(tests.TestCaseWithTransport):
 
710
 
 
711
    def setUp(self):
 
712
        super(TestDiffTree, self).setUp()
 
713
        self.old_tree = self.make_branch_and_tree('old-tree')
 
714
        self.old_tree.lock_write()
 
715
        self.addCleanup(self.old_tree.unlock)
 
716
        self.new_tree = self.make_branch_and_tree('new-tree')
 
717
        self.new_tree.lock_write()
 
718
        self.addCleanup(self.new_tree.unlock)
 
719
        self.differ = diff.DiffTree(self.old_tree, self.new_tree, BytesIO())
 
720
 
 
721
    def test_diff_text(self):
 
722
        self.build_tree_contents([('old-tree/olddir/',),
 
723
                                  ('old-tree/olddir/oldfile', b'old\n')])
 
724
        self.old_tree.add('olddir')
 
725
        self.old_tree.add('olddir/oldfile', b'file-id')
 
726
        self.build_tree_contents([('new-tree/newdir/',),
 
727
                                  ('new-tree/newdir/newfile', b'new\n')])
 
728
        self.new_tree.add('newdir')
 
729
        self.new_tree.add('newdir/newfile', b'file-id')
 
730
        differ = diff.DiffText(self.old_tree, self.new_tree, BytesIO())
 
731
        differ.diff_text('olddir/oldfile', None, 'old label',
 
732
                         'new label', b'file-id', None)
 
733
        self.assertEqual(
 
734
            b'--- old label\n+++ new label\n@@ -1,1 +0,0 @@\n-old\n\n',
 
735
            differ.to_file.getvalue())
 
736
        differ.to_file.seek(0)
 
737
        differ.diff_text(None, 'newdir/newfile',
 
738
                         'old label', 'new label', None, b'file-id')
 
739
        self.assertEqual(
 
740
            b'--- old label\n+++ new label\n@@ -0,0 +1,1 @@\n+new\n\n',
 
741
            differ.to_file.getvalue())
 
742
        differ.to_file.seek(0)
 
743
        differ.diff_text('olddir/oldfile', 'newdir/newfile',
 
744
                         'old label', 'new label', b'file-id', b'file-id')
 
745
        self.assertEqual(
 
746
            b'--- old label\n+++ new label\n@@ -1,1 +1,1 @@\n-old\n+new\n\n',
 
747
            differ.to_file.getvalue())
 
748
 
 
749
    def test_diff_deletion(self):
 
750
        self.build_tree_contents([('old-tree/file', b'contents'),
 
751
                                  ('new-tree/file', b'contents')])
 
752
        self.old_tree.add('file', b'file-id')
 
753
        self.new_tree.add('file', b'file-id')
 
754
        os.unlink('new-tree/file')
 
755
        self.differ.show_diff(None)
 
756
        self.assertContainsRe(self.differ.to_file.getvalue(), b'-contents')
 
757
 
 
758
    def test_diff_creation(self):
 
759
        self.build_tree_contents([('old-tree/file', b'contents'),
 
760
                                  ('new-tree/file', b'contents')])
 
761
        self.old_tree.add('file', b'file-id')
 
762
        self.new_tree.add('file', b'file-id')
 
763
        os.unlink('old-tree/file')
 
764
        self.differ.show_diff(None)
 
765
        self.assertContainsRe(self.differ.to_file.getvalue(), br'\+contents')
 
766
 
 
767
    def test_diff_symlink(self):
 
768
        differ = diff.DiffSymlink(self.old_tree, self.new_tree, BytesIO())
 
769
        differ.diff_symlink('old target', None)
 
770
        self.assertEqual(b"=== target was 'old target'\n",
 
771
                         differ.to_file.getvalue())
 
772
 
 
773
        differ = diff.DiffSymlink(self.old_tree, self.new_tree, BytesIO())
 
774
        differ.diff_symlink(None, 'new target')
 
775
        self.assertEqual(b"=== target is 'new target'\n",
 
776
                         differ.to_file.getvalue())
 
777
 
 
778
        differ = diff.DiffSymlink(self.old_tree, self.new_tree, BytesIO())
 
779
        differ.diff_symlink('old target', 'new target')
 
780
        self.assertEqual(b"=== target changed 'old target' => 'new target'\n",
 
781
                         differ.to_file.getvalue())
 
782
 
 
783
    def test_diff(self):
 
784
        self.build_tree_contents([('old-tree/olddir/',),
 
785
                                  ('old-tree/olddir/oldfile', b'old\n')])
 
786
        self.old_tree.add('olddir')
 
787
        self.old_tree.add('olddir/oldfile', b'file-id')
 
788
        self.build_tree_contents([('new-tree/newdir/',),
 
789
                                  ('new-tree/newdir/newfile', b'new\n')])
 
790
        self.new_tree.add('newdir')
 
791
        self.new_tree.add('newdir/newfile', b'file-id')
 
792
        self.differ.diff(b'file-id', 'olddir/oldfile', 'newdir/newfile')
 
793
        self.assertContainsRe(
 
794
            self.differ.to_file.getvalue(),
 
795
            br'--- olddir/oldfile.*\n\+\+\+ newdir/newfile.*\n\@\@ -1,1 \+1,1'
 
796
            br' \@\@\n-old\n\+new\n\n')
 
797
 
 
798
    def test_diff_kind_change(self):
 
799
        self.requireFeature(features.SymlinkFeature)
 
800
        self.build_tree_contents([('old-tree/olddir/',),
 
801
                                  ('old-tree/olddir/oldfile', b'old\n')])
 
802
        self.old_tree.add('olddir')
 
803
        self.old_tree.add('olddir/oldfile', b'file-id')
 
804
        self.build_tree(['new-tree/newdir/'])
 
805
        os.symlink('new', 'new-tree/newdir/newfile')
 
806
        self.new_tree.add('newdir')
 
807
        self.new_tree.add('newdir/newfile', b'file-id')
 
808
        self.differ.diff(b'file-id', 'olddir/oldfile', 'newdir/newfile')
 
809
        self.assertContainsRe(
 
810
            self.differ.to_file.getvalue(),
 
811
            br'--- olddir/oldfile.*\n\+\+\+ newdir/newfile.*\n\@\@ -1,1 \+0,0'
 
812
            br' \@\@\n-old\n\n')
 
813
        self.assertContainsRe(self.differ.to_file.getvalue(),
 
814
                              b"=== target is 'new'\n")
 
815
 
 
816
    def test_diff_directory(self):
 
817
        self.build_tree(['new-tree/new-dir/'])
 
818
        self.new_tree.add('new-dir', b'new-dir-id')
 
819
        self.differ.diff(b'new-dir-id', None, 'new-dir')
 
820
        self.assertEqual(self.differ.to_file.getvalue(), b'')
 
821
 
 
822
    def create_old_new(self):
 
823
        self.build_tree_contents([('old-tree/olddir/',),
 
824
                                  ('old-tree/olddir/oldfile', b'old\n')])
 
825
        self.old_tree.add('olddir')
 
826
        self.old_tree.add('olddir/oldfile', b'file-id')
 
827
        self.build_tree_contents([('new-tree/newdir/',),
 
828
                                  ('new-tree/newdir/newfile', b'new\n')])
 
829
        self.new_tree.add('newdir')
 
830
        self.new_tree.add('newdir/newfile', b'file-id')
 
831
 
 
832
    def test_register_diff(self):
 
833
        self.create_old_new()
 
834
        old_diff_factories = diff.DiffTree.diff_factories
 
835
        diff.DiffTree.diff_factories = old_diff_factories[:]
 
836
        diff.DiffTree.diff_factories.insert(0, DiffWasIs.from_diff_tree)
 
837
        try:
 
838
            differ = diff.DiffTree(self.old_tree, self.new_tree, BytesIO())
 
839
        finally:
 
840
            diff.DiffTree.diff_factories = old_diff_factories
 
841
        differ.diff(b'file-id', 'olddir/oldfile', 'newdir/newfile')
 
842
        self.assertNotContainsRe(
 
843
            differ.to_file.getvalue(),
 
844
            br'--- olddir/oldfile.*\n\+\+\+ newdir/newfile.*\n\@\@ -1,1 \+1,1'
 
845
            br' \@\@\n-old\n\+new\n\n')
 
846
        self.assertContainsRe(differ.to_file.getvalue(),
 
847
                              b'was: old\nis: new\n')
 
848
 
 
849
    def test_extra_factories(self):
 
850
        self.create_old_new()
 
851
        differ = diff.DiffTree(self.old_tree, self.new_tree, BytesIO(),
 
852
                               extra_factories=[DiffWasIs.from_diff_tree])
 
853
        differ.diff(b'file-id', 'olddir/oldfile', 'newdir/newfile')
 
854
        self.assertNotContainsRe(
 
855
            differ.to_file.getvalue(),
 
856
            br'--- olddir/oldfile.*\n\+\+\+ newdir/newfile.*\n\@\@ -1,1 \+1,1'
 
857
            br' \@\@\n-old\n\+new\n\n')
 
858
        self.assertContainsRe(differ.to_file.getvalue(),
 
859
                              b'was: old\nis: new\n')
 
860
 
 
861
    def test_alphabetical_order(self):
 
862
        self.build_tree(['new-tree/a-file'])
 
863
        self.new_tree.add('a-file')
 
864
        self.build_tree(['old-tree/b-file'])
 
865
        self.old_tree.add('b-file')
 
866
        self.differ.show_diff(None)
 
867
        self.assertContainsRe(self.differ.to_file.getvalue(),
 
868
                              b'.*a-file(.|\n)*b-file')
 
869
 
 
870
 
 
871
class TestPatienceDiffLib(tests.TestCase):
 
872
 
 
873
    def setUp(self):
 
874
        super(TestPatienceDiffLib, self).setUp()
 
875
        self._unique_lcs = _patiencediff_py.unique_lcs_py
 
876
        self._recurse_matches = _patiencediff_py.recurse_matches_py
 
877
        self._PatienceSequenceMatcher = \
 
878
            _patiencediff_py.PatienceSequenceMatcher_py
 
879
 
 
880
    def test_diff_unicode_string(self):
 
881
        a = ''.join([unichr(i) for i in range(4000, 4500, 3)])
 
882
        b = ''.join([unichr(i) for i in range(4300, 4800, 2)])
 
883
        sm = self._PatienceSequenceMatcher(None, a, b)
 
884
        mb = sm.get_matching_blocks()
 
885
        self.assertEqual(35, len(mb))
 
886
 
 
887
    def test_unique_lcs(self):
 
888
        unique_lcs = self._unique_lcs
 
889
        self.assertEqual(unique_lcs('', ''), [])
 
890
        self.assertEqual(unique_lcs('', 'a'), [])
 
891
        self.assertEqual(unique_lcs('a', ''), [])
 
892
        self.assertEqual(unique_lcs('a', 'a'), [(0, 0)])
 
893
        self.assertEqual(unique_lcs('a', 'b'), [])
 
894
        self.assertEqual(unique_lcs('ab', 'ab'), [(0, 0), (1, 1)])
 
895
        self.assertEqual(unique_lcs('abcde', 'cdeab'),
 
896
                         [(2, 0), (3, 1), (4, 2)])
 
897
        self.assertEqual(unique_lcs('cdeab', 'abcde'),
 
898
                         [(0, 2), (1, 3), (2, 4)])
 
899
        self.assertEqual(unique_lcs('abXde', 'abYde'), [(0, 0), (1, 1),
 
900
                                                        (3, 3), (4, 4)])
 
901
        self.assertEqual(unique_lcs('acbac', 'abc'), [(2, 1)])
 
902
 
 
903
    def test_recurse_matches(self):
 
904
        def test_one(a, b, matches):
 
905
            test_matches = []
 
906
            self._recurse_matches(
 
907
                a, b, 0, 0, len(a), len(b), test_matches, 10)
 
908
            self.assertEqual(test_matches, matches)
 
909
 
 
910
        test_one(['a', '', 'b', '', 'c'], ['a', 'a', 'b', 'c', 'c'],
 
911
                 [(0, 0), (2, 2), (4, 4)])
 
912
        test_one(['a', 'c', 'b', 'a', 'c'], ['a', 'b', 'c'],
 
913
                 [(0, 0), (2, 1), (4, 2)])
 
914
        # Even though 'bc' is not unique globally, and is surrounded by
 
915
        # non-matching lines, we should still match, because they are locally
 
916
        # unique
 
917
        test_one('abcdbce', 'afbcgdbce', [(0, 0), (1, 2), (2, 3), (3, 5),
 
918
                                          (4, 6), (5, 7), (6, 8)])
 
919
 
 
920
        # recurse_matches doesn't match non-unique
 
921
        # lines surrounded by bogus text.
 
922
        # The update has been done in patiencediff.SequenceMatcher instead
 
923
 
 
924
        # This is what it could be
 
925
        #test_one('aBccDe', 'abccde', [(0,0), (2,2), (3,3), (5,5)])
 
926
 
 
927
        # This is what it currently gives:
 
928
        test_one('aBccDe', 'abccde', [(0, 0), (5, 5)])
 
929
 
 
930
    def assertDiffBlocks(self, a, b, expected_blocks):
 
931
        """Check that the sequence matcher returns the correct blocks.
 
932
 
 
933
        :param a: A sequence to match
 
934
        :param b: Another sequence to match
 
935
        :param expected_blocks: The expected output, not including the final
 
936
            matching block (len(a), len(b), 0)
 
937
        """
 
938
        matcher = self._PatienceSequenceMatcher(None, a, b)
 
939
        blocks = matcher.get_matching_blocks()
 
940
        last = blocks.pop()
 
941
        self.assertEqual((len(a), len(b), 0), last)
 
942
        self.assertEqual(expected_blocks, blocks)
 
943
 
 
944
    def test_matching_blocks(self):
 
945
        # Some basic matching tests
 
946
        self.assertDiffBlocks('', '', [])
 
947
        self.assertDiffBlocks([], [], [])
 
948
        self.assertDiffBlocks('abc', '', [])
 
949
        self.assertDiffBlocks('', 'abc', [])
 
950
        self.assertDiffBlocks('abcd', 'abcd', [(0, 0, 4)])
 
951
        self.assertDiffBlocks('abcd', 'abce', [(0, 0, 3)])
 
952
        self.assertDiffBlocks('eabc', 'abce', [(1, 0, 3)])
 
953
        self.assertDiffBlocks('eabce', 'abce', [(1, 0, 4)])
 
954
        self.assertDiffBlocks('abcde', 'abXde', [(0, 0, 2), (3, 3, 2)])
 
955
        self.assertDiffBlocks('abcde', 'abXYZde', [(0, 0, 2), (3, 5, 2)])
 
956
        self.assertDiffBlocks('abde', 'abXYZde', [(0, 0, 2), (2, 5, 2)])
 
957
        # This may check too much, but it checks to see that
 
958
        # a copied block stays attached to the previous section,
 
959
        # not the later one.
 
960
        # difflib would tend to grab the trailing longest match
 
961
        # which would make the diff not look right
 
962
        self.assertDiffBlocks('abcdefghijklmnop', 'abcdefxydefghijklmnop',
 
963
                              [(0, 0, 6), (6, 11, 10)])
 
964
 
 
965
        # make sure it supports passing in lists
 
966
        self.assertDiffBlocks(
 
967
            ['hello there\n',
 
968
             'world\n',
 
969
             'how are you today?\n'],
 
970
            ['hello there\n',
 
971
             'how are you today?\n'],
 
972
            [(0, 0, 1), (2, 1, 1)])
 
973
 
 
974
        # non unique lines surrounded by non-matching lines
 
975
        # won't be found
 
976
        self.assertDiffBlocks('aBccDe', 'abccde', [(0, 0, 1), (5, 5, 1)])
 
977
 
 
978
        # But they only need to be locally unique
 
979
        self.assertDiffBlocks('aBcDec', 'abcdec', [
 
980
                              (0, 0, 1), (2, 2, 1), (4, 4, 2)])
 
981
 
 
982
        # non unique blocks won't be matched
 
983
        self.assertDiffBlocks('aBcdEcdFg', 'abcdecdfg', [(0, 0, 1), (8, 8, 1)])
 
984
 
 
985
        # but locally unique ones will
 
986
        self.assertDiffBlocks('aBcdEeXcdFg', 'abcdecdfg', [(0, 0, 1), (2, 2, 2),
 
987
                                                           (5, 4, 1), (7, 5, 2), (10, 8, 1)])
 
988
 
 
989
        self.assertDiffBlocks('abbabbXd', 'cabbabxd', [(7, 7, 1)])
 
990
        self.assertDiffBlocks('abbabbbb', 'cabbabbc', [])
 
991
        self.assertDiffBlocks('bbbbbbbb', 'cbbbbbbc', [])
 
992
 
 
993
    def test_matching_blocks_tuples(self):
 
994
        # Some basic matching tests
 
995
        self.assertDiffBlocks([], [], [])
 
996
        self.assertDiffBlocks([('a',), ('b',), ('c,')], [], [])
 
997
        self.assertDiffBlocks([], [('a',), ('b',), ('c,')], [])
 
998
        self.assertDiffBlocks([('a',), ('b',), ('c,')],
 
999
                              [('a',), ('b',), ('c,')],
 
1000
                              [(0, 0, 3)])
 
1001
        self.assertDiffBlocks([('a',), ('b',), ('c,')],
 
1002
                              [('a',), ('b',), ('d,')],
 
1003
                              [(0, 0, 2)])
 
1004
        self.assertDiffBlocks([('d',), ('b',), ('c,')],
 
1005
                              [('a',), ('b',), ('c,')],
 
1006
                              [(1, 1, 2)])
 
1007
        self.assertDiffBlocks([('d',), ('a',), ('b',), ('c,')],
 
1008
                              [('a',), ('b',), ('c,')],
 
1009
                              [(1, 0, 3)])
 
1010
        self.assertDiffBlocks([('a', 'b'), ('c', 'd'), ('e', 'f')],
 
1011
                              [('a', 'b'), ('c', 'X'), ('e', 'f')],
 
1012
                              [(0, 0, 1), (2, 2, 1)])
 
1013
        self.assertDiffBlocks([('a', 'b'), ('c', 'd'), ('e', 'f')],
 
1014
                              [('a', 'b'), ('c', 'dX'), ('e', 'f')],
 
1015
                              [(0, 0, 1), (2, 2, 1)])
 
1016
 
 
1017
    def test_opcodes(self):
 
1018
        def chk_ops(a, b, expected_codes):
 
1019
            s = self._PatienceSequenceMatcher(None, a, b)
 
1020
            self.assertEqual(expected_codes, s.get_opcodes())
 
1021
 
 
1022
        chk_ops('', '', [])
 
1023
        chk_ops([], [], [])
 
1024
        chk_ops('abc', '', [('delete', 0, 3, 0, 0)])
 
1025
        chk_ops('', 'abc', [('insert', 0, 0, 0, 3)])
 
1026
        chk_ops('abcd', 'abcd', [('equal', 0, 4, 0, 4)])
 
1027
        chk_ops('abcd', 'abce', [('equal', 0, 3, 0, 3),
 
1028
                                 ('replace', 3, 4, 3, 4)
 
1029
                                 ])
 
1030
        chk_ops('eabc', 'abce', [('delete', 0, 1, 0, 0),
 
1031
                                 ('equal', 1, 4, 0, 3),
 
1032
                                 ('insert', 4, 4, 3, 4)
 
1033
                                 ])
 
1034
        chk_ops('eabce', 'abce', [('delete', 0, 1, 0, 0),
 
1035
                                  ('equal', 1, 5, 0, 4)
 
1036
                                  ])
 
1037
        chk_ops('abcde', 'abXde', [('equal', 0, 2, 0, 2),
 
1038
                                   ('replace', 2, 3, 2, 3),
 
1039
                                   ('equal', 3, 5, 3, 5)
 
1040
                                   ])
 
1041
        chk_ops('abcde', 'abXYZde', [('equal', 0, 2, 0, 2),
 
1042
                                     ('replace', 2, 3, 2, 5),
 
1043
                                     ('equal', 3, 5, 5, 7)
 
1044
                                     ])
 
1045
        chk_ops('abde', 'abXYZde', [('equal', 0, 2, 0, 2),
 
1046
                                    ('insert', 2, 2, 2, 5),
 
1047
                                    ('equal', 2, 4, 5, 7)
 
1048
                                    ])
 
1049
        chk_ops('abcdefghijklmnop', 'abcdefxydefghijklmnop',
 
1050
                [('equal', 0, 6, 0, 6),
 
1051
                 ('insert', 6, 6, 6, 11),
 
1052
                 ('equal', 6, 16, 11, 21)
 
1053
                 ])
 
1054
        chk_ops(
 
1055
            ['hello there\n', 'world\n', 'how are you today?\n'],
 
1056
            ['hello there\n', 'how are you today?\n'],
 
1057
            [('equal', 0, 1, 0, 1),
 
1058
             ('delete', 1, 2, 1, 1),
 
1059
             ('equal', 2, 3, 1, 2),
 
1060
             ])
 
1061
        chk_ops('aBccDe', 'abccde',
 
1062
                [('equal', 0, 1, 0, 1),
 
1063
                 ('replace', 1, 5, 1, 5),
 
1064
                 ('equal', 5, 6, 5, 6),
 
1065
                 ])
 
1066
        chk_ops('aBcDec', 'abcdec',
 
1067
                [('equal', 0, 1, 0, 1),
 
1068
                 ('replace', 1, 2, 1, 2),
 
1069
                 ('equal', 2, 3, 2, 3),
 
1070
                 ('replace', 3, 4, 3, 4),
 
1071
                 ('equal', 4, 6, 4, 6),
 
1072
                 ])
 
1073
        chk_ops('aBcdEcdFg', 'abcdecdfg',
 
1074
                [('equal', 0, 1, 0, 1),
 
1075
                 ('replace', 1, 8, 1, 8),
 
1076
                 ('equal', 8, 9, 8, 9)
 
1077
                 ])
 
1078
        chk_ops('aBcdEeXcdFg', 'abcdecdfg',
 
1079
                [('equal', 0, 1, 0, 1),
 
1080
                 ('replace', 1, 2, 1, 2),
 
1081
                 ('equal', 2, 4, 2, 4),
 
1082
                 ('delete', 4, 5, 4, 4),
 
1083
                 ('equal', 5, 6, 4, 5),
 
1084
                 ('delete', 6, 7, 5, 5),
 
1085
                 ('equal', 7, 9, 5, 7),
 
1086
                 ('replace', 9, 10, 7, 8),
 
1087
                 ('equal', 10, 11, 8, 9)
 
1088
                 ])
 
1089
 
 
1090
    def test_grouped_opcodes(self):
 
1091
        def chk_ops(a, b, expected_codes, n=3):
 
1092
            s = self._PatienceSequenceMatcher(None, a, b)
 
1093
            self.assertEqual(expected_codes, list(s.get_grouped_opcodes(n)))
 
1094
 
 
1095
        chk_ops('', '', [])
 
1096
        chk_ops([], [], [])
 
1097
        chk_ops('abc', '', [[('delete', 0, 3, 0, 0)]])
 
1098
        chk_ops('', 'abc', [[('insert', 0, 0, 0, 3)]])
 
1099
        chk_ops('abcd', 'abcd', [])
 
1100
        chk_ops('abcd', 'abce', [[('equal', 0, 3, 0, 3),
 
1101
                                  ('replace', 3, 4, 3, 4)
 
1102
                                  ]])
 
1103
        chk_ops('eabc', 'abce', [[('delete', 0, 1, 0, 0),
 
1104
                                  ('equal', 1, 4, 0, 3),
 
1105
                                  ('insert', 4, 4, 3, 4)
 
1106
                                  ]])
 
1107
        chk_ops('abcdefghijklmnop', 'abcdefxydefghijklmnop',
 
1108
                [[('equal', 3, 6, 3, 6),
 
1109
                  ('insert', 6, 6, 6, 11),
 
1110
                  ('equal', 6, 9, 11, 14)
 
1111
                  ]])
 
1112
        chk_ops('abcdefghijklmnop', 'abcdefxydefghijklmnop',
 
1113
                [[('equal', 2, 6, 2, 6),
 
1114
                  ('insert', 6, 6, 6, 11),
 
1115
                  ('equal', 6, 10, 11, 15)
 
1116
                  ]], 4)
 
1117
        chk_ops('Xabcdef', 'abcdef',
 
1118
                [[('delete', 0, 1, 0, 0),
 
1119
                  ('equal', 1, 4, 0, 3)
 
1120
                  ]])
 
1121
        chk_ops('abcdef', 'abcdefX',
 
1122
                [[('equal', 3, 6, 3, 6),
 
1123
                  ('insert', 6, 6, 6, 7)
 
1124
                  ]])
 
1125
 
 
1126
    def test_multiple_ranges(self):
 
1127
        # There was an earlier bug where we used a bad set of ranges,
 
1128
        # this triggers that specific bug, to make sure it doesn't regress
 
1129
        self.assertDiffBlocks('abcdefghijklmnop',
 
1130
                              'abcXghiYZQRSTUVWXYZijklmnop',
 
1131
                              [(0, 0, 3), (6, 4, 3), (9, 20, 7)])
 
1132
 
 
1133
        self.assertDiffBlocks('ABCd efghIjk  L',
 
1134
                              'AxyzBCn mo pqrstuvwI1 2  L',
 
1135
                              [(0, 0, 1), (1, 4, 2), (9, 19, 1), (12, 23, 3)])
 
1136
 
 
1137
        # These are rot13 code snippets.
 
1138
        self.assertDiffBlocks('''\
 
1139
    trg nqqrq jura lbh nqq n svyr va gur qverpgbel.
 
1140
    """
 
1141
    gnxrf_netf = ['svyr*']
 
1142
    gnxrf_bcgvbaf = ['ab-erphefr']
 
1143
 
 
1144
    qrs eha(frys, svyr_yvfg, ab_erphefr=Snyfr):
 
1145
        sebz omeyvo.nqq vzcbeg fzneg_nqq, nqq_ercbegre_cevag, nqq_ercbegre_ahyy
 
1146
        vs vf_dhvrg():
 
1147
            ercbegre = nqq_ercbegre_ahyy
 
1148
        ryfr:
 
1149
            ercbegre = nqq_ercbegre_cevag
 
1150
        fzneg_nqq(svyr_yvfg, abg ab_erphefr, ercbegre)
 
1151
 
 
1152
 
 
1153
pynff pzq_zxqve(Pbzznaq):
 
1154
'''.splitlines(True), '''\
 
1155
    trg nqqrq jura lbh nqq n svyr va gur qverpgbel.
 
1156
 
 
1157
    --qel-eha jvyy fubj juvpu svyrf jbhyq or nqqrq, ohg abg npghnyyl
 
1158
    nqq gurz.
 
1159
    """
 
1160
    gnxrf_netf = ['svyr*']
 
1161
    gnxrf_bcgvbaf = ['ab-erphefr', 'qel-eha']
 
1162
 
 
1163
    qrs eha(frys, svyr_yvfg, ab_erphefr=Snyfr, qel_eha=Snyfr):
 
1164
        vzcbeg omeyvo.nqq
 
1165
 
 
1166
        vs qel_eha:
 
1167
            vs vf_dhvrg():
 
1168
                # Guvf vf cbvagyrff, ohg V'q engure abg envfr na reebe
 
1169
                npgvba = omeyvo.nqq.nqq_npgvba_ahyy
 
1170
            ryfr:
 
1171
  npgvba = omeyvo.nqq.nqq_npgvba_cevag
 
1172
        ryvs vf_dhvrg():
 
1173
            npgvba = omeyvo.nqq.nqq_npgvba_nqq
 
1174
        ryfr:
 
1175
       npgvba = omeyvo.nqq.nqq_npgvba_nqq_naq_cevag
 
1176
 
 
1177
        omeyvo.nqq.fzneg_nqq(svyr_yvfg, abg ab_erphefr, npgvba)
 
1178
 
 
1179
 
 
1180
pynff pzq_zxqve(Pbzznaq):
 
1181
'''.splitlines(True), [(0, 0, 1), (1, 4, 2), (9, 19, 1), (12, 23, 3)])
 
1182
 
 
1183
    def test_patience_unified_diff(self):
 
1184
        txt_a = ['hello there\n',
 
1185
                 'world\n',
 
1186
                 'how are you today?\n']
 
1187
        txt_b = ['hello there\n',
 
1188
                 'how are you today?\n']
 
1189
        unified_diff = patiencediff.unified_diff
 
1190
        psm = self._PatienceSequenceMatcher
 
1191
        self.assertEqual(['--- \n',
 
1192
                          '+++ \n',
 
1193
                          '@@ -1,3 +1,2 @@\n',
 
1194
                          ' hello there\n',
 
1195
                          '-world\n',
 
1196
                          ' how are you today?\n'
 
1197
                          ], list(unified_diff(txt_a, txt_b,
 
1198
                                               sequencematcher=psm)))
 
1199
        txt_a = [x + '\n' for x in 'abcdefghijklmnop']
 
1200
        txt_b = [x + '\n' for x in 'abcdefxydefghijklmnop']
 
1201
        # This is the result with LongestCommonSubstring matching
 
1202
        self.assertEqual(['--- \n',
 
1203
                          '+++ \n',
 
1204
                          '@@ -1,6 +1,11 @@\n',
 
1205
                          ' a\n',
 
1206
                          ' b\n',
 
1207
                          ' c\n',
 
1208
                          '+d\n',
 
1209
                          '+e\n',
 
1210
                          '+f\n',
 
1211
                          '+x\n',
 
1212
                          '+y\n',
 
1213
                          ' d\n',
 
1214
                          ' e\n',
 
1215
                          ' f\n'], list(unified_diff(txt_a, txt_b)))
 
1216
        # And the patience diff
 
1217
        self.assertEqual(['--- \n',
 
1218
                          '+++ \n',
 
1219
                          '@@ -4,6 +4,11 @@\n',
 
1220
                          ' d\n',
 
1221
                          ' e\n',
 
1222
                          ' f\n',
 
1223
                          '+x\n',
 
1224
                          '+y\n',
 
1225
                          '+d\n',
 
1226
                          '+e\n',
 
1227
                          '+f\n',
 
1228
                          ' g\n',
 
1229
                          ' h\n',
 
1230
                          ' i\n',
 
1231
                          ], list(unified_diff(txt_a, txt_b,
 
1232
                                               sequencematcher=psm)))
 
1233
 
 
1234
    def test_patience_unified_diff_with_dates(self):
 
1235
        txt_a = ['hello there\n',
 
1236
                 'world\n',
 
1237
                 'how are you today?\n']
 
1238
        txt_b = ['hello there\n',
 
1239
                 'how are you today?\n']
 
1240
        unified_diff = patiencediff.unified_diff
 
1241
        psm = self._PatienceSequenceMatcher
 
1242
        self.assertEqual(['--- a\t2008-08-08\n',
 
1243
                          '+++ b\t2008-09-09\n',
 
1244
                          '@@ -1,3 +1,2 @@\n',
 
1245
                          ' hello there\n',
 
1246
                          '-world\n',
 
1247
                          ' how are you today?\n'
 
1248
                          ], list(unified_diff(txt_a, txt_b,
 
1249
                                               fromfile='a', tofile='b',
 
1250
                                               fromfiledate='2008-08-08',
 
1251
                                               tofiledate='2008-09-09',
 
1252
                                               sequencematcher=psm)))
 
1253
 
 
1254
 
 
1255
class TestPatienceDiffLib_c(TestPatienceDiffLib):
 
1256
 
 
1257
    _test_needs_features = [features.compiled_patiencediff_feature]
 
1258
 
 
1259
    def setUp(self):
 
1260
        super(TestPatienceDiffLib_c, self).setUp()
 
1261
        from breezy import _patiencediff_c
 
1262
        self._unique_lcs = _patiencediff_c.unique_lcs_c
 
1263
        self._recurse_matches = _patiencediff_c.recurse_matches_c
 
1264
        self._PatienceSequenceMatcher = \
 
1265
            _patiencediff_c.PatienceSequenceMatcher_c
 
1266
 
 
1267
    def test_unhashable(self):
 
1268
        """We should get a proper exception here."""
 
1269
        # We need to be able to hash items in the sequence, lists are
 
1270
        # unhashable, and thus cannot be diffed
 
1271
        e = self.assertRaises(TypeError, self._PatienceSequenceMatcher,
 
1272
                              None, [[]], [])
 
1273
        e = self.assertRaises(TypeError, self._PatienceSequenceMatcher,
 
1274
                              None, ['valid', []], [])
 
1275
        e = self.assertRaises(TypeError, self._PatienceSequenceMatcher,
 
1276
                              None, ['valid'], [[]])
 
1277
        e = self.assertRaises(TypeError, self._PatienceSequenceMatcher,
 
1278
                              None, ['valid'], ['valid', []])
 
1279
 
 
1280
 
 
1281
class TestPatienceDiffLibFiles(tests.TestCaseInTempDir):
 
1282
 
 
1283
    def setUp(self):
 
1284
        super(TestPatienceDiffLibFiles, self).setUp()
 
1285
        self._PatienceSequenceMatcher = \
 
1286
            _patiencediff_py.PatienceSequenceMatcher_py
 
1287
 
 
1288
    def test_patience_unified_diff_files(self):
 
1289
        txt_a = [b'hello there\n',
 
1290
                 b'world\n',
 
1291
                 b'how are you today?\n']
 
1292
        txt_b = [b'hello there\n',
 
1293
                 b'how are you today?\n']
 
1294
        with open('a1', 'wb') as f:
 
1295
            f.writelines(txt_a)
 
1296
        with open('b1', 'wb') as f:
 
1297
            f.writelines(txt_b)
 
1298
 
 
1299
        unified_diff_files = patiencediff.unified_diff_files
 
1300
        psm = self._PatienceSequenceMatcher
 
1301
        self.assertEqual([b'--- a1\n',
 
1302
                          b'+++ b1\n',
 
1303
                          b'@@ -1,3 +1,2 @@\n',
 
1304
                          b' hello there\n',
 
1305
                          b'-world\n',
 
1306
                          b' how are you today?\n',
 
1307
                          ], list(unified_diff_files(b'a1', b'b1',
 
1308
                                                     sequencematcher=psm)))
 
1309
 
 
1310
        txt_a = [x + '\n' for x in 'abcdefghijklmnop']
 
1311
        txt_b = [x + '\n' for x in 'abcdefxydefghijklmnop']
 
1312
        with open('a2', 'wt') as f:
 
1313
            f.writelines(txt_a)
 
1314
        with open('b2', 'wt') as f:
 
1315
            f.writelines(txt_b)
 
1316
 
 
1317
        # This is the result with LongestCommonSubstring matching
 
1318
        self.assertEqual([b'--- a2\n',
 
1319
                          b'+++ b2\n',
 
1320
                          b'@@ -1,6 +1,11 @@\n',
 
1321
                          b' a\n',
 
1322
                          b' b\n',
 
1323
                          b' c\n',
 
1324
                          b'+d\n',
 
1325
                          b'+e\n',
 
1326
                          b'+f\n',
 
1327
                          b'+x\n',
 
1328
                          b'+y\n',
 
1329
                          b' d\n',
 
1330
                          b' e\n',
 
1331
                          b' f\n'], list(unified_diff_files(b'a2', b'b2')))
 
1332
 
 
1333
        # And the patience diff
 
1334
        self.assertEqual([b'--- a2\n',
 
1335
                          b'+++ b2\n',
 
1336
                          b'@@ -4,6 +4,11 @@\n',
 
1337
                          b' d\n',
 
1338
                          b' e\n',
 
1339
                          b' f\n',
 
1340
                          b'+x\n',
 
1341
                          b'+y\n',
 
1342
                          b'+d\n',
 
1343
                          b'+e\n',
 
1344
                          b'+f\n',
 
1345
                          b' g\n',
 
1346
                          b' h\n',
 
1347
                          b' i\n'],
 
1348
                         list(unified_diff_files(b'a2', b'b2',
 
1349
                                                 sequencematcher=psm)))
 
1350
 
 
1351
 
 
1352
class TestPatienceDiffLibFiles_c(TestPatienceDiffLibFiles):
 
1353
 
 
1354
    _test_needs_features = [features.compiled_patiencediff_feature]
 
1355
 
 
1356
    def setUp(self):
 
1357
        super(TestPatienceDiffLibFiles_c, self).setUp()
 
1358
        from breezy import _patiencediff_c
 
1359
        self._PatienceSequenceMatcher = \
 
1360
            _patiencediff_c.PatienceSequenceMatcher_c
 
1361
 
 
1362
 
 
1363
class TestUsingCompiledIfAvailable(tests.TestCase):
 
1364
 
 
1365
    def test_PatienceSequenceMatcher(self):
 
1366
        if features.compiled_patiencediff_feature.available():
 
1367
            from breezy._patiencediff_c import PatienceSequenceMatcher_c
 
1368
            self.assertIs(PatienceSequenceMatcher_c,
 
1369
                          patiencediff.PatienceSequenceMatcher)
 
1370
        else:
 
1371
            from breezy._patiencediff_py import PatienceSequenceMatcher_py
 
1372
            self.assertIs(PatienceSequenceMatcher_py,
 
1373
                          patiencediff.PatienceSequenceMatcher)
 
1374
 
 
1375
    def test_unique_lcs(self):
 
1376
        if features.compiled_patiencediff_feature.available():
 
1377
            from breezy._patiencediff_c import unique_lcs_c
 
1378
            self.assertIs(unique_lcs_c,
 
1379
                          patiencediff.unique_lcs)
 
1380
        else:
 
1381
            from breezy._patiencediff_py import unique_lcs_py
 
1382
            self.assertIs(unique_lcs_py,
 
1383
                          patiencediff.unique_lcs)
 
1384
 
 
1385
    def test_recurse_matches(self):
 
1386
        if features.compiled_patiencediff_feature.available():
 
1387
            from breezy._patiencediff_c import recurse_matches_c
 
1388
            self.assertIs(recurse_matches_c,
 
1389
                          patiencediff.recurse_matches)
 
1390
        else:
 
1391
            from breezy._patiencediff_py import recurse_matches_py
 
1392
            self.assertIs(recurse_matches_py,
 
1393
                          patiencediff.recurse_matches)
 
1394
 
 
1395
 
 
1396
class TestDiffFromTool(tests.TestCaseWithTransport):
 
1397
 
 
1398
    def test_from_string(self):
 
1399
        diff_obj = diff.DiffFromTool.from_string('diff', None, None, None)
 
1400
        self.addCleanup(diff_obj.finish)
 
1401
        self.assertEqual(['diff', '@old_path', '@new_path'],
 
1402
                         diff_obj.command_template)
 
1403
 
 
1404
    def test_from_string_u5(self):
 
1405
        diff_obj = diff.DiffFromTool.from_string('diff "-u 5"',
 
1406
                                                 None, None, None)
 
1407
        self.addCleanup(diff_obj.finish)
 
1408
        self.assertEqual(['diff', '-u 5', '@old_path', '@new_path'],
 
1409
                         diff_obj.command_template)
 
1410
        self.assertEqual(['diff', '-u 5', 'old-path', 'new-path'],
 
1411
                         diff_obj._get_command('old-path', 'new-path'))
 
1412
 
 
1413
    def test_from_string_path_with_backslashes(self):
 
1414
        self.requireFeature(features.backslashdir_feature)
 
1415
        tool = 'C:\\Tools\\Diff.exe'
 
1416
        diff_obj = diff.DiffFromTool.from_string(tool, None, None, None)
 
1417
        self.addCleanup(diff_obj.finish)
 
1418
        self.assertEqual(['C:\\Tools\\Diff.exe', '@old_path', '@new_path'],
 
1419
                         diff_obj.command_template)
 
1420
        self.assertEqual(['C:\\Tools\\Diff.exe', 'old-path', 'new-path'],
 
1421
                         diff_obj._get_command('old-path', 'new-path'))
 
1422
 
 
1423
    def test_execute(self):
 
1424
        output = BytesIO()
 
1425
        diff_obj = diff.DiffFromTool([sys.executable, '-c',
 
1426
                                      'print("@old_path @new_path")'],
 
1427
                                     None, None, output)
 
1428
        self.addCleanup(diff_obj.finish)
 
1429
        diff_obj._execute('old', 'new')
 
1430
        self.assertEqual(output.getvalue().rstrip(), b'old new')
 
1431
 
 
1432
    def test_execute_missing(self):
 
1433
        diff_obj = diff.DiffFromTool(['a-tool-which-is-unlikely-to-exist'],
 
1434
                                     None, None, None)
 
1435
        self.addCleanup(diff_obj.finish)
 
1436
        e = self.assertRaises(errors.ExecutableMissing, diff_obj._execute,
 
1437
                              'old', 'new')
 
1438
        self.assertEqual('a-tool-which-is-unlikely-to-exist could not be found'
 
1439
                         ' on this machine', str(e))
 
1440
 
 
1441
    def test_prepare_files_creates_paths_readable_by_windows_tool(self):
 
1442
        self.requireFeature(features.AttribFeature)
 
1443
        output = BytesIO()
 
1444
        tree = self.make_branch_and_tree('tree')
 
1445
        self.build_tree_contents([('tree/file', b'content')])
 
1446
        tree.add('file', b'file-id')
 
1447
        tree.commit('old tree')
 
1448
        tree.lock_read()
 
1449
        self.addCleanup(tree.unlock)
 
1450
        basis_tree = tree.basis_tree()
 
1451
        basis_tree.lock_read()
 
1452
        self.addCleanup(basis_tree.unlock)
 
1453
        diff_obj = diff.DiffFromTool([sys.executable, '-c',
 
1454
                                      'print "@old_path @new_path"'],
 
1455
                                     basis_tree, tree, output)
 
1456
        diff_obj._prepare_files('file', 'file', file_id=b'file-id')
 
1457
        # The old content should be readonly
 
1458
        self.assertReadableByAttrib(diff_obj._root, 'old\\file',
 
1459
                                    r'R.*old\\file$')
 
1460
        # The new content should use the tree object, not a 'new' file anymore
 
1461
        self.assertEndsWith(tree.basedir, 'work/tree')
 
1462
        self.assertReadableByAttrib(tree.basedir, 'file', r'work\\tree\\file$')
 
1463
 
 
1464
    def assertReadableByAttrib(self, cwd, relpath, regex):
 
1465
        proc = subprocess.Popen(['attrib', relpath],
 
1466
                                stdout=subprocess.PIPE,
 
1467
                                cwd=cwd)
 
1468
        (result, err) = proc.communicate()
 
1469
        self.assertContainsRe(result.replace('\r\n', '\n'), regex)
 
1470
 
 
1471
    def test_prepare_files(self):
 
1472
        output = BytesIO()
 
1473
        tree = self.make_branch_and_tree('tree')
 
1474
        self.build_tree_contents([('tree/oldname', b'oldcontent')])
 
1475
        self.build_tree_contents([('tree/oldname2', b'oldcontent2')])
 
1476
        tree.add('oldname', b'file-id')
 
1477
        tree.add('oldname2', b'file2-id')
 
1478
        # Earliest allowable date on FAT32 filesystems is 1980-01-01
 
1479
        tree.commit('old tree', timestamp=315532800)
 
1480
        tree.rename_one('oldname', 'newname')
 
1481
        tree.rename_one('oldname2', 'newname2')
 
1482
        self.build_tree_contents([('tree/newname', b'newcontent')])
 
1483
        self.build_tree_contents([('tree/newname2', b'newcontent2')])
 
1484
        old_tree = tree.basis_tree()
 
1485
        old_tree.lock_read()
 
1486
        self.addCleanup(old_tree.unlock)
 
1487
        tree.lock_read()
 
1488
        self.addCleanup(tree.unlock)
 
1489
        diff_obj = diff.DiffFromTool([sys.executable, '-c',
 
1490
                                      'print "@old_path @new_path"'],
 
1491
                                     old_tree, tree, output)
 
1492
        self.addCleanup(diff_obj.finish)
 
1493
        self.assertContainsRe(diff_obj._root, 'brz-diff-[^/]*')
 
1494
        old_path, new_path = diff_obj._prepare_files(
 
1495
            'oldname', 'newname', file_id=b'file-id')
 
1496
        self.assertContainsRe(old_path, 'old/oldname$')
 
1497
        self.assertEqual(315532800, os.stat(old_path).st_mtime)
 
1498
        self.assertContainsRe(new_path, 'tree/newname$')
 
1499
        self.assertFileEqual(b'oldcontent', old_path)
 
1500
        self.assertFileEqual(b'newcontent', new_path)
 
1501
        if osutils.host_os_dereferences_symlinks():
 
1502
            self.assertTrue(os.path.samefile('tree/newname', new_path))
 
1503
        # make sure we can create files with the same parent directories
 
1504
        diff_obj._prepare_files('oldname2', 'newname2', file_id=b'file2-id')
 
1505
 
 
1506
 
 
1507
class TestDiffFromToolEncodedFilename(tests.TestCaseWithTransport):
 
1508
 
 
1509
    def test_encodable_filename(self):
 
1510
        # Just checks file path for external diff tool.
 
1511
        # We cannot change CPython's internal encoding used by os.exec*.
 
1512
        diffobj = diff.DiffFromTool(['dummy', '@old_path', '@new_path'],
 
1513
                                    None, None, None)
 
1514
        for _, scenario in EncodingAdapter.encoding_scenarios:
 
1515
            encoding = scenario['encoding']
 
1516
            dirname = scenario['info']['directory']
 
1517
            filename = scenario['info']['filename']
 
1518
 
 
1519
            self.overrideAttr(diffobj, '_fenc', lambda: encoding)
 
1520
            relpath = dirname + u'/' + filename
 
1521
            fullpath = diffobj._safe_filename('safe', relpath)
 
1522
            self.assertEqual(fullpath,
 
1523
                             fullpath.encode(encoding).decode(encoding))
 
1524
            self.assertTrue(fullpath.startswith(diffobj._root + '/safe'))
 
1525
 
 
1526
    def test_unencodable_filename(self):
 
1527
        diffobj = diff.DiffFromTool(['dummy', '@old_path', '@new_path'],
 
1528
                                    None, None, None)
 
1529
        for _, scenario in EncodingAdapter.encoding_scenarios:
 
1530
            encoding = scenario['encoding']
 
1531
            dirname = scenario['info']['directory']
 
1532
            filename = scenario['info']['filename']
 
1533
 
 
1534
            if encoding == 'iso-8859-1':
 
1535
                encoding = 'iso-8859-2'
 
1536
            else:
 
1537
                encoding = 'iso-8859-1'
 
1538
 
 
1539
            self.overrideAttr(diffobj, '_fenc', lambda: encoding)
 
1540
            relpath = dirname + u'/' + filename
 
1541
            fullpath = diffobj._safe_filename('safe', relpath)
 
1542
            self.assertEqual(fullpath,
 
1543
                             fullpath.encode(encoding).decode(encoding))
 
1544
            self.assertTrue(fullpath.startswith(diffobj._root + '/safe'))
 
1545
 
 
1546
 
 
1547
class TestGetTreesAndBranchesToDiffLocked(tests.TestCaseWithTransport):
 
1548
 
 
1549
    def call_gtabtd(self, path_list, revision_specs, old_url, new_url):
 
1550
        """Call get_trees_and_branches_to_diff_locked."""
 
1551
        return diff.get_trees_and_branches_to_diff_locked(
 
1552
            path_list, revision_specs, old_url, new_url, self.addCleanup)
 
1553
 
 
1554
    def test_basic(self):
 
1555
        tree = self.make_branch_and_tree('tree')
 
1556
        (old_tree, new_tree,
 
1557
         old_branch, new_branch,
 
1558
         specific_files, extra_trees) = self.call_gtabtd(
 
1559
             ['tree'], None, None, None)
 
1560
 
 
1561
        self.assertIsInstance(old_tree, revisiontree.RevisionTree)
 
1562
        self.assertEqual(_mod_revision.NULL_REVISION,
 
1563
                         old_tree.get_revision_id())
 
1564
        self.assertEqual(tree.basedir, new_tree.basedir)
 
1565
        self.assertEqual(tree.branch.base, old_branch.base)
 
1566
        self.assertEqual(tree.branch.base, new_branch.base)
 
1567
        self.assertIs(None, specific_files)
 
1568
        self.assertIs(None, extra_trees)
 
1569
 
 
1570
    def test_with_rev_specs(self):
 
1571
        tree = self.make_branch_and_tree('tree')
 
1572
        self.build_tree_contents([('tree/file', b'oldcontent')])
 
1573
        tree.add('file', b'file-id')
 
1574
        tree.commit('old tree', timestamp=0, rev_id=b"old-id")
 
1575
        self.build_tree_contents([('tree/file', b'newcontent')])
 
1576
        tree.commit('new tree', timestamp=0, rev_id=b"new-id")
 
1577
 
 
1578
        revisions = [revisionspec.RevisionSpec.from_string('1'),
 
1579
                     revisionspec.RevisionSpec.from_string('2')]
 
1580
        (old_tree, new_tree,
 
1581
         old_branch, new_branch,
 
1582
         specific_files, extra_trees) = self.call_gtabtd(
 
1583
            ['tree'], revisions, None, None)
 
1584
 
 
1585
        self.assertIsInstance(old_tree, revisiontree.RevisionTree)
 
1586
        self.assertEqual(b"old-id", old_tree.get_revision_id())
 
1587
        self.assertIsInstance(new_tree, revisiontree.RevisionTree)
 
1588
        self.assertEqual(b"new-id", new_tree.get_revision_id())
 
1589
        self.assertEqual(tree.branch.base, old_branch.base)
 
1590
        self.assertEqual(tree.branch.base, new_branch.base)
 
1591
        self.assertIs(None, specific_files)
 
1592
        self.assertEqual(tree.basedir, extra_trees[0].basedir)