/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: Jelmer Vernooij
  • Date: 2018-07-26 19:15:27 UTC
  • mto: This revision was merged to the branch mainline in revision 7055.
  • Revision ID: jelmer@jelmer.uk-20180726191527-wniq205k6tzfo1xx
Install fastimport from git.

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