/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-05-19 13:16:11 UTC
  • mto: (6968.4.3 git-archive)
  • mto: This revision was merged to the branch mainline in revision 6972.
  • Revision ID: jelmer@jelmer.uk-20180519131611-l9h9ud41j7qg1m03
Move tar/zip to breezy.archive.

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