~bzr-pqm/bzr/bzr.dev

« back to all changes in this revision

Viewing changes to bzrlib/tests/test_diff.py

  • Committer: Ian Clatworthy
  • Date: 2008-03-27 07:51:10 UTC
  • mto: (3311.1.1 ianc-integration)
  • mto: This revision was merged to the branch mainline in revision 3312.
  • Revision ID: ian.clatworthy@canonical.com-20080327075110-afgd7x03ybju06ez
Reduce evangelism in the User Guide

Show diffs side-by-side

added added

removed removed

Lines of Context:
1
 
from bzrlib.selftest import TestCase
2
 
from bzrlib.diff import internal_diff
 
1
# Copyright (C) 2005, 2006 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., 59 Temple Place, Suite 330, Boston, MA  02111-1307  USA
 
16
 
 
17
import os
 
18
import os.path
3
19
from cStringIO import StringIO
4
 
def udiff_lines(old, new):
 
20
import errno
 
21
import subprocess
 
22
from tempfile import TemporaryFile
 
23
 
 
24
from bzrlib import tests
 
25
from bzrlib.diff import (
 
26
    DiffFromTool,
 
27
    DiffPath,
 
28
    DiffSymlink,
 
29
    DiffTree,
 
30
    DiffText,
 
31
    external_diff,
 
32
    internal_diff,
 
33
    show_diff_trees,
 
34
    )
 
35
from bzrlib.errors import BinaryFile, NoDiff, ExecutableMissing
 
36
import bzrlib.osutils as osutils
 
37
import bzrlib.transform as transform
 
38
import bzrlib.patiencediff
 
39
import bzrlib._patiencediff_py
 
40
from bzrlib.tests import (Feature, TestCase, TestCaseWithTransport,
 
41
                          TestCaseInTempDir, TestSkipped)
 
42
 
 
43
 
 
44
class _CompiledPatienceDiffFeature(Feature):
 
45
 
 
46
    def _probe(self):
 
47
        try:
 
48
            import bzrlib._patiencediff_c
 
49
        except ImportError:
 
50
            return False
 
51
        return True
 
52
 
 
53
    def feature_name(self):
 
54
        return 'bzrlib._patiencediff_c'
 
55
 
 
56
CompiledPatienceDiffFeature = _CompiledPatienceDiffFeature()
 
57
 
 
58
 
 
59
class _UnicodeFilename(Feature):
 
60
    """Does the filesystem support Unicode filenames?"""
 
61
 
 
62
    def _probe(self):
 
63
        try:
 
64
            os.stat(u'\u03b1')
 
65
        except UnicodeEncodeError:
 
66
            return False
 
67
        except (IOError, OSError):
 
68
            # The filesystem allows the Unicode filename but the file doesn't
 
69
            # exist.
 
70
            return True
 
71
        else:
 
72
            # The filesystem allows the Unicode filename and the file exists,
 
73
            # for some reason.
 
74
            return True
 
75
 
 
76
UnicodeFilename = _UnicodeFilename()
 
77
 
 
78
 
 
79
class TestUnicodeFilename(TestCase):
 
80
 
 
81
    def test_probe_passes(self):
 
82
        """UnicodeFilename._probe passes."""
 
83
        # We can't test much more than that because the behaviour depends
 
84
        # on the platform.
 
85
        UnicodeFilename._probe()
 
86
        
 
87
 
 
88
def udiff_lines(old, new, allow_binary=False):
5
89
    output = StringIO()
6
 
    internal_diff('old', old, 'new', new, output)
 
90
    internal_diff('old', old, 'new', new, output, allow_binary)
7
91
    output.seek(0, 0)
8
92
    return output.readlines()
9
93
 
10
 
def check_patch(lines):
11
 
    assert len(lines) > 1, \
12
 
        "Not enough lines for a file header for patch:\n%s" % "".join(lines)
13
 
    assert lines[0].startswith ('---'), \
14
 
        'No orig line for patch:\n%s' % "".join(lines)
15
 
    assert lines[1].startswith ('+++'), \
16
 
        'No mod line for patch:\n%s' % "".join(lines)
17
 
    assert len(lines) > 2, \
18
 
        "No hunks for patch:\n%s" % "".join(lines)
19
 
    assert lines[2].startswith('@@'),\
20
 
        "No hunk header for patch:\n%s" % "".join(lines)
21
 
    assert '@@' in lines[2][2:], \
22
 
        "Unterminated hunk header for patch:\n%s" % "".join(lines)
 
94
 
 
95
def external_udiff_lines(old, new, use_stringio=False):
 
96
    if use_stringio:
 
97
        # StringIO has no fileno, so it tests a different codepath
 
98
        output = StringIO()
 
99
    else:
 
100
        output = TemporaryFile()
 
101
    try:
 
102
        external_diff('old', old, 'new', new, output, diff_opts=['-u'])
 
103
    except NoDiff:
 
104
        raise TestSkipped('external "diff" not present to test')
 
105
    output.seek(0, 0)
 
106
    lines = output.readlines()
 
107
    output.close()
 
108
    return lines
 
109
 
23
110
 
24
111
class TestDiff(TestCase):
 
112
 
25
113
    def test_add_nl(self):
26
114
        """diff generates a valid diff for patches that add a newline"""
27
115
        lines = udiff_lines(['boo'], ['boo\n'])
28
 
        check_patch(lines)
29
 
        assert lines[4] == '\\ No newline at end of file\n', \
30
 
            "expected no-nl, got %r" % lines[4]
 
116
        self.check_patch(lines)
 
117
        self.assertEquals(lines[4], '\\ No newline at end of file\n')
 
118
            ## "expected no-nl, got %r" % lines[4]
31
119
 
32
120
    def test_add_nl_2(self):
33
121
        """diff generates a valid diff for patches that change last line and
34
122
        add a newline.
35
123
        """
36
124
        lines = udiff_lines(['boo'], ['goo\n'])
37
 
        check_patch(lines)
38
 
        assert lines[4] == '\\ No newline at end of file\n', \
39
 
            "expected no-nl, got %r" % lines[4]
 
125
        self.check_patch(lines)
 
126
        self.assertEquals(lines[4], '\\ No newline at end of file\n')
 
127
            ## "expected no-nl, got %r" % lines[4]
40
128
 
41
129
    def test_remove_nl(self):
42
130
        """diff generates a valid diff for patches that change last line and
43
131
        add a newline.
44
132
        """
45
133
        lines = udiff_lines(['boo\n'], ['boo'])
46
 
        check_patch(lines)
47
 
        assert lines[5] == '\\ No newline at end of file\n', \
48
 
            "expected no-nl, got %r" % lines[5]
 
134
        self.check_patch(lines)
 
135
        self.assertEquals(lines[5], '\\ No newline at end of file\n')
 
136
            ## "expected no-nl, got %r" % lines[5]
 
137
 
 
138
    def check_patch(self, lines):
 
139
        self.assert_(len(lines) > 1)
 
140
            ## "Not enough lines for a file header for patch:\n%s" % "".join(lines)
 
141
        self.assert_(lines[0].startswith ('---'))
 
142
            ## 'No orig line for patch:\n%s' % "".join(lines)
 
143
        self.assert_(lines[1].startswith ('+++'))
 
144
            ## 'No mod line for patch:\n%s' % "".join(lines)
 
145
        self.assert_(len(lines) > 2)
 
146
            ## "No hunks for patch:\n%s" % "".join(lines)
 
147
        self.assert_(lines[2].startswith('@@'))
 
148
            ## "No hunk header for patch:\n%s" % "".join(lines)
 
149
        self.assert_('@@' in lines[2][2:])
 
150
            ## "Unterminated hunk header for patch:\n%s" % "".join(lines)
 
151
 
 
152
    def test_binary_lines(self):
 
153
        self.assertRaises(BinaryFile, udiff_lines, [1023 * 'a' + '\x00'], [])
 
154
        self.assertRaises(BinaryFile, udiff_lines, [], [1023 * 'a' + '\x00'])
 
155
        udiff_lines([1023 * 'a' + '\x00'], [], allow_binary=True)
 
156
        udiff_lines([], [1023 * 'a' + '\x00'], 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
        old_env = {}
 
173
        for lang in ('LANG', 'LC_ALL', 'LANGUAGE'):
 
174
            old_env[lang] = osutils.set_or_unset_env(lang, 'C')
 
175
        try:
 
176
            lines = external_udiff_lines(['\x00foobar\n'], ['foo\x00bar\n'])
 
177
            # Older versions of diffutils say "Binary files", newer
 
178
            # versions just say "Files".
 
179
            self.assertContainsRe(lines[0],
 
180
                                  '(Binary f|F)iles old and new differ\n')
 
181
            self.assertEquals(lines[1:], ['\n'])
 
182
        finally:
 
183
            for lang, old_val in old_env.iteritems():
 
184
                osutils.set_or_unset_env(lang, old_val)
 
185
 
 
186
    def test_no_external_diff(self):
 
187
        """Check that NoDiff is raised when diff is not available"""
 
188
        # Use os.environ['PATH'] to make sure no 'diff' command is available
 
189
        orig_path = os.environ['PATH']
 
190
        try:
 
191
            os.environ['PATH'] = ''
 
192
            self.assertRaises(NoDiff, external_diff,
 
193
                              'old', ['boo\n'], 'new', ['goo\n'],
 
194
                              StringIO(), diff_opts=['-u'])
 
195
        finally:
 
196
            os.environ['PATH'] = orig_path
 
197
        
 
198
    def test_internal_diff_default(self):
 
199
        # Default internal diff encoding is utf8
 
200
        output = StringIO()
 
201
        internal_diff(u'old_\xb5', ['old_text\n'],
 
202
                    u'new_\xe5', ['new_text\n'], output)
 
203
        lines = output.getvalue().splitlines(True)
 
204
        self.check_patch(lines)
 
205
        self.assertEquals(['--- old_\xc2\xb5\n',
 
206
                           '+++ new_\xc3\xa5\n',
 
207
                           '@@ -1,1 +1,1 @@\n',
 
208
                           '-old_text\n',
 
209
                           '+new_text\n',
 
210
                           '\n',
 
211
                          ]
 
212
                          , lines)
 
213
 
 
214
    def test_internal_diff_utf8(self):
 
215
        output = StringIO()
 
216
        internal_diff(u'old_\xb5', ['old_text\n'],
 
217
                    u'new_\xe5', ['new_text\n'], output,
 
218
                    path_encoding='utf8')
 
219
        lines = output.getvalue().splitlines(True)
 
220
        self.check_patch(lines)
 
221
        self.assertEquals(['--- old_\xc2\xb5\n',
 
222
                           '+++ new_\xc3\xa5\n',
 
223
                           '@@ -1,1 +1,1 @@\n',
 
224
                           '-old_text\n',
 
225
                           '+new_text\n',
 
226
                           '\n',
 
227
                          ]
 
228
                          , lines)
 
229
 
 
230
    def test_internal_diff_iso_8859_1(self):
 
231
        output = StringIO()
 
232
        internal_diff(u'old_\xb5', ['old_text\n'],
 
233
                    u'new_\xe5', ['new_text\n'], output,
 
234
                    path_encoding='iso-8859-1')
 
235
        lines = output.getvalue().splitlines(True)
 
236
        self.check_patch(lines)
 
237
        self.assertEquals(['--- old_\xb5\n',
 
238
                           '+++ new_\xe5\n',
 
239
                           '@@ -1,1 +1,1 @@\n',
 
240
                           '-old_text\n',
 
241
                           '+new_text\n',
 
242
                           '\n',
 
243
                          ]
 
244
                          , lines)
 
245
 
 
246
    def test_internal_diff_no_content(self):
 
247
        output = StringIO()
 
248
        internal_diff(u'old', [], u'new', [], output)
 
249
        self.assertEqual('', output.getvalue())
 
250
 
 
251
    def test_internal_diff_no_changes(self):
 
252
        output = StringIO()
 
253
        internal_diff(u'old', ['text\n', 'contents\n'],
 
254
                      u'new', ['text\n', 'contents\n'],
 
255
                      output)
 
256
        self.assertEqual('', output.getvalue())
 
257
 
 
258
    def test_internal_diff_returns_bytes(self):
 
259
        import StringIO
 
260
        output = StringIO.StringIO()
 
261
        internal_diff(u'old_\xb5', ['old_text\n'],
 
262
                    u'new_\xe5', ['new_text\n'], output)
 
263
        self.failUnless(isinstance(output.getvalue(), str),
 
264
            'internal_diff should return bytestrings')
 
265
 
 
266
 
 
267
class TestDiffFiles(TestCaseInTempDir):
 
268
 
 
269
    def test_external_diff_binary(self):
 
270
        """The output when using external diff should use diff's i18n error"""
 
271
        # Make sure external_diff doesn't fail in the current LANG
 
272
        lines = external_udiff_lines(['\x00foobar\n'], ['foo\x00bar\n'])
 
273
 
 
274
        cmd = ['diff', '-u', '--binary', 'old', 'new']
 
275
        open('old', 'wb').write('\x00foobar\n')
 
276
        open('new', 'wb').write('foo\x00bar\n')
 
277
        pipe = subprocess.Popen(cmd, stdout=subprocess.PIPE,
 
278
                                     stdin=subprocess.PIPE)
 
279
        out, err = pipe.communicate()
 
280
        # Diff returns '2' on Binary files.
 
281
        self.assertEqual(2, pipe.returncode)
 
282
        # We should output whatever diff tells us, plus a trailing newline
 
283
        self.assertEqual(out.splitlines(True) + ['\n'], lines)
 
284
 
 
285
 
 
286
class TestShowDiffTreesHelper(TestCaseWithTransport):
 
287
    """Has a helper for running show_diff_trees"""
 
288
 
 
289
    def get_diff(self, tree1, tree2, specific_files=None, working_tree=None):
 
290
        output = StringIO()
 
291
        if working_tree is not None:
 
292
            extra_trees = (working_tree,)
 
293
        else:
 
294
            extra_trees = ()
 
295
        show_diff_trees(tree1, tree2, output, specific_files=specific_files,
 
296
                        extra_trees=extra_trees, old_label='old/',
 
297
                        new_label='new/')
 
298
        return output.getvalue()
 
299
 
 
300
 
 
301
class TestDiffDates(TestShowDiffTreesHelper):
 
302
 
 
303
    def setUp(self):
 
304
        super(TestDiffDates, self).setUp()
 
305
        self.wt = self.make_branch_and_tree('.')
 
306
        self.b = self.wt.branch
 
307
        self.build_tree_contents([
 
308
            ('file1', 'file1 contents at rev 1\n'),
 
309
            ('file2', 'file2 contents at rev 1\n')
 
310
            ])
 
311
        self.wt.add(['file1', 'file2'])
 
312
        self.wt.commit(
 
313
            message='Revision 1',
 
314
            timestamp=1143849600, # 2006-04-01 00:00:00 UTC
 
315
            timezone=0,
 
316
            rev_id='rev-1')
 
317
        self.build_tree_contents([('file1', 'file1 contents at rev 2\n')])
 
318
        self.wt.commit(
 
319
            message='Revision 2',
 
320
            timestamp=1143936000, # 2006-04-02 00:00:00 UTC
 
321
            timezone=28800,
 
322
            rev_id='rev-2')
 
323
        self.build_tree_contents([('file2', 'file2 contents at rev 3\n')])
 
324
        self.wt.commit(
 
325
            message='Revision 3',
 
326
            timestamp=1144022400, # 2006-04-03 00:00:00 UTC
 
327
            timezone=-3600,
 
328
            rev_id='rev-3')
 
329
        self.wt.remove(['file2'])
 
330
        self.wt.commit(
 
331
            message='Revision 4',
 
332
            timestamp=1144108800, # 2006-04-04 00:00:00 UTC
 
333
            timezone=0,
 
334
            rev_id='rev-4')
 
335
        self.build_tree_contents([
 
336
            ('file1', 'file1 contents in working tree\n')
 
337
            ])
 
338
        # set the date stamps for files in the working tree to known values
 
339
        os.utime('file1', (1144195200, 1144195200)) # 2006-04-05 00:00:00 UTC
 
340
 
 
341
    def test_diff_rev_tree_working_tree(self):
 
342
        output = self.get_diff(self.wt.basis_tree(), self.wt)
 
343
        # note that the date for old/file1 is from rev 2 rather than from
 
344
        # the basis revision (rev 4)
 
345
        self.assertEqualDiff(output, '''\
 
346
=== modified file 'file1'
 
347
--- old/file1\t2006-04-02 00:00:00 +0000
 
348
+++ new/file1\t2006-04-05 00:00:00 +0000
 
349
@@ -1,1 +1,1 @@
 
350
-file1 contents at rev 2
 
351
+file1 contents in working tree
 
352
 
 
353
''')
 
354
 
 
355
    def test_diff_rev_tree_rev_tree(self):
 
356
        tree1 = self.b.repository.revision_tree('rev-2')
 
357
        tree2 = self.b.repository.revision_tree('rev-3')
 
358
        output = self.get_diff(tree1, tree2)
 
359
        self.assertEqualDiff(output, '''\
 
360
=== modified file 'file2'
 
361
--- old/file2\t2006-04-01 00:00:00 +0000
 
362
+++ new/file2\t2006-04-03 00:00:00 +0000
 
363
@@ -1,1 +1,1 @@
 
364
-file2 contents at rev 1
 
365
+file2 contents at rev 3
 
366
 
 
367
''')
 
368
        
 
369
    def test_diff_add_files(self):
 
370
        tree1 = self.b.repository.revision_tree(None)
 
371
        tree2 = self.b.repository.revision_tree('rev-1')
 
372
        output = self.get_diff(tree1, tree2)
 
373
        # the files have the epoch time stamp for the tree in which
 
374
        # they don't exist.
 
375
        self.assertEqualDiff(output, '''\
 
376
=== added file 'file1'
 
377
--- old/file1\t1970-01-01 00:00:00 +0000
 
378
+++ new/file1\t2006-04-01 00:00:00 +0000
 
379
@@ -0,0 +1,1 @@
 
380
+file1 contents at rev 1
 
381
 
 
382
=== added file 'file2'
 
383
--- old/file2\t1970-01-01 00:00:00 +0000
 
384
+++ new/file2\t2006-04-01 00:00:00 +0000
 
385
@@ -0,0 +1,1 @@
 
386
+file2 contents at rev 1
 
387
 
 
388
''')
 
389
 
 
390
    def test_diff_remove_files(self):
 
391
        tree1 = self.b.repository.revision_tree('rev-3')
 
392
        tree2 = self.b.repository.revision_tree('rev-4')
 
393
        output = self.get_diff(tree1, tree2)
 
394
        # the file has the epoch time stamp for the tree in which
 
395
        # it doesn't exist.
 
396
        self.assertEqualDiff(output, '''\
 
397
=== removed file 'file2'
 
398
--- old/file2\t2006-04-03 00:00:00 +0000
 
399
+++ new/file2\t1970-01-01 00:00:00 +0000
 
400
@@ -1,1 +0,0 @@
 
401
-file2 contents at rev 3
 
402
 
 
403
''')
 
404
 
 
405
    def test_show_diff_specified(self):
 
406
        """A working tree filename can be used to identify a file"""
 
407
        self.wt.rename_one('file1', 'file1b')
 
408
        old_tree = self.b.repository.revision_tree('rev-1')
 
409
        new_tree = self.b.repository.revision_tree('rev-4')
 
410
        out = self.get_diff(old_tree, new_tree, specific_files=['file1b'], 
 
411
                            working_tree=self.wt)
 
412
        self.assertContainsRe(out, 'file1\t')
 
413
 
 
414
    def test_recursive_diff(self):
 
415
        """Children of directories are matched"""
 
416
        os.mkdir('dir1')
 
417
        os.mkdir('dir2')
 
418
        self.wt.add(['dir1', 'dir2'])
 
419
        self.wt.rename_one('file1', 'dir1/file1')
 
420
        old_tree = self.b.repository.revision_tree('rev-1')
 
421
        new_tree = self.b.repository.revision_tree('rev-4')
 
422
        out = self.get_diff(old_tree, new_tree, specific_files=['dir1'], 
 
423
                            working_tree=self.wt)
 
424
        self.assertContainsRe(out, 'file1\t')
 
425
        out = self.get_diff(old_tree, new_tree, specific_files=['dir2'], 
 
426
                            working_tree=self.wt)
 
427
        self.assertNotContainsRe(out, 'file1\t')
 
428
 
 
429
 
 
430
 
 
431
class TestShowDiffTrees(TestShowDiffTreesHelper):
 
432
    """Direct tests for show_diff_trees"""
 
433
 
 
434
    def test_modified_file(self):
 
435
        """Test when a file is modified."""
 
436
        tree = self.make_branch_and_tree('tree')
 
437
        self.build_tree_contents([('tree/file', 'contents\n')])
 
438
        tree.add(['file'], ['file-id'])
 
439
        tree.commit('one', rev_id='rev-1')
 
440
 
 
441
        self.build_tree_contents([('tree/file', 'new contents\n')])
 
442
        diff = self.get_diff(tree.basis_tree(), tree)
 
443
        self.assertContainsRe(diff, "=== modified file 'file'\n")
 
444
        self.assertContainsRe(diff, '--- old/file\t')
 
445
        self.assertContainsRe(diff, '\\+\\+\\+ new/file\t')
 
446
        self.assertContainsRe(diff, '-contents\n'
 
447
                                    '\\+new contents\n')
 
448
 
 
449
    def test_modified_file_in_renamed_dir(self):
 
450
        """Test when a file is modified in a renamed directory."""
 
451
        tree = self.make_branch_and_tree('tree')
 
452
        self.build_tree(['tree/dir/'])
 
453
        self.build_tree_contents([('tree/dir/file', 'contents\n')])
 
454
        tree.add(['dir', 'dir/file'], ['dir-id', 'file-id'])
 
455
        tree.commit('one', rev_id='rev-1')
 
456
 
 
457
        tree.rename_one('dir', 'other')
 
458
        self.build_tree_contents([('tree/other/file', 'new contents\n')])
 
459
        diff = self.get_diff(tree.basis_tree(), tree)
 
460
        self.assertContainsRe(diff, "=== renamed directory 'dir' => 'other'\n")
 
461
        self.assertContainsRe(diff, "=== modified file 'other/file'\n")
 
462
        # XXX: This is technically incorrect, because it used to be at another
 
463
        # location. What to do?
 
464
        self.assertContainsRe(diff, '--- old/dir/file\t')
 
465
        self.assertContainsRe(diff, '\\+\\+\\+ new/other/file\t')
 
466
        self.assertContainsRe(diff, '-contents\n'
 
467
                                    '\\+new contents\n')
 
468
 
 
469
    def test_renamed_directory(self):
 
470
        """Test when only a directory is only renamed."""
 
471
        tree = self.make_branch_and_tree('tree')
 
472
        self.build_tree(['tree/dir/'])
 
473
        self.build_tree_contents([('tree/dir/file', 'contents\n')])
 
474
        tree.add(['dir', 'dir/file'], ['dir-id', 'file-id'])
 
475
        tree.commit('one', rev_id='rev-1')
 
476
 
 
477
        tree.rename_one('dir', 'newdir')
 
478
        diff = self.get_diff(tree.basis_tree(), tree)
 
479
        # Renaming a directory should be a single "you renamed this dir" even
 
480
        # when there are files inside.
 
481
        self.assertEqual("=== renamed directory 'dir' => 'newdir'\n", diff)
 
482
 
 
483
    def test_renamed_file(self):
 
484
        """Test when a file is only renamed."""
 
485
        tree = self.make_branch_and_tree('tree')
 
486
        self.build_tree_contents([('tree/file', 'contents\n')])
 
487
        tree.add(['file'], ['file-id'])
 
488
        tree.commit('one', rev_id='rev-1')
 
489
 
 
490
        tree.rename_one('file', 'newname')
 
491
        diff = self.get_diff(tree.basis_tree(), tree)
 
492
        self.assertContainsRe(diff, "=== renamed file 'file' => 'newname'\n")
 
493
        # We shouldn't have a --- or +++ line, because there is no content
 
494
        # change
 
495
        self.assertNotContainsRe(diff, '---')
 
496
 
 
497
    def test_renamed_and_modified_file(self):
 
498
        """Test when a file is only renamed."""
 
499
        tree = self.make_branch_and_tree('tree')
 
500
        self.build_tree_contents([('tree/file', 'contents\n')])
 
501
        tree.add(['file'], ['file-id'])
 
502
        tree.commit('one', rev_id='rev-1')
 
503
 
 
504
        tree.rename_one('file', 'newname')
 
505
        self.build_tree_contents([('tree/newname', 'new contents\n')])
 
506
        diff = self.get_diff(tree.basis_tree(), tree)
 
507
        self.assertContainsRe(diff, "=== renamed file 'file' => 'newname'\n")
 
508
        self.assertContainsRe(diff, '--- old/file\t')
 
509
        self.assertContainsRe(diff, '\\+\\+\\+ new/newname\t')
 
510
        self.assertContainsRe(diff, '-contents\n'
 
511
                                    '\\+new contents\n')
 
512
 
 
513
 
 
514
    def test_internal_diff_exec_property(self):
 
515
        tree = self.make_branch_and_tree('tree')
 
516
 
 
517
        tt = transform.TreeTransform(tree)
 
518
        tt.new_file('a', tt.root, 'contents\n', 'a-id', True)
 
519
        tt.new_file('b', tt.root, 'contents\n', 'b-id', False)
 
520
        tt.new_file('c', tt.root, 'contents\n', 'c-id', True)
 
521
        tt.new_file('d', tt.root, 'contents\n', 'd-id', False)
 
522
        tt.new_file('e', tt.root, 'contents\n', 'control-e-id', True)
 
523
        tt.new_file('f', tt.root, 'contents\n', 'control-f-id', False)
 
524
        tt.apply()
 
525
        tree.commit('one', rev_id='rev-1')
 
526
 
 
527
        tt = transform.TreeTransform(tree)
 
528
        tt.set_executability(False, tt.trans_id_file_id('a-id'))
 
529
        tt.set_executability(True, tt.trans_id_file_id('b-id'))
 
530
        tt.set_executability(False, tt.trans_id_file_id('c-id'))
 
531
        tt.set_executability(True, tt.trans_id_file_id('d-id'))
 
532
        tt.apply()
 
533
        tree.rename_one('c', 'new-c')
 
534
        tree.rename_one('d', 'new-d')
 
535
 
 
536
        diff = self.get_diff(tree.basis_tree(), tree)
 
537
 
 
538
        self.assertContainsRe(diff, r"file 'a'.*\(properties changed:.*\+x to -x.*\)")
 
539
        self.assertContainsRe(diff, r"file 'b'.*\(properties changed:.*-x to \+x.*\)")
 
540
        self.assertContainsRe(diff, r"file 'c'.*\(properties changed:.*\+x to -x.*\)")
 
541
        self.assertContainsRe(diff, r"file 'd'.*\(properties changed:.*-x to \+x.*\)")
 
542
        self.assertNotContainsRe(diff, r"file 'e'")
 
543
        self.assertNotContainsRe(diff, r"file 'f'")
 
544
 
 
545
 
 
546
    def test_binary_unicode_filenames(self):
 
547
        """Test that contents of files are *not* encoded in UTF-8 when there
 
548
        is a binary file in the diff.
 
549
        """
 
550
        # See https://bugs.launchpad.net/bugs/110092.
 
551
        self.requireFeature(UnicodeFilename)
 
552
 
 
553
        # This bug isn't triggered with cStringIO.
 
554
        from StringIO import StringIO
 
555
        tree = self.make_branch_and_tree('tree')
 
556
        alpha, omega = u'\u03b1', u'\u03c9'
 
557
        alpha_utf8, omega_utf8 = alpha.encode('utf8'), omega.encode('utf8')
 
558
        self.build_tree_contents(
 
559
            [('tree/' + alpha, chr(0)),
 
560
             ('tree/' + omega,
 
561
              ('The %s and the %s\n' % (alpha_utf8, omega_utf8)))])
 
562
        tree.add([alpha], ['file-id'])
 
563
        tree.add([omega], ['file-id-2'])
 
564
        diff_content = StringIO()
 
565
        show_diff_trees(tree.basis_tree(), tree, diff_content)
 
566
        diff = diff_content.getvalue()
 
567
        self.assertContainsRe(diff, r"=== added file '%s'" % alpha_utf8)
 
568
        self.assertContainsRe(
 
569
            diff, "Binary files a/%s.*and b/%s.* differ\n" % (alpha_utf8, alpha_utf8))
 
570
        self.assertContainsRe(diff, r"=== added file '%s'" % omega_utf8)
 
571
        self.assertContainsRe(diff, r"--- a/%s" % (omega_utf8,))
 
572
        self.assertContainsRe(diff, r"\+\+\+ b/%s" % (omega_utf8,))
 
573
 
 
574
    def test_unicode_filename(self):
 
575
        """Test when the filename are unicode."""
 
576
        self.requireFeature(UnicodeFilename)
 
577
 
 
578
        alpha, omega = u'\u03b1', u'\u03c9'
 
579
        autf8, outf8 = alpha.encode('utf8'), omega.encode('utf8')
 
580
 
 
581
        tree = self.make_branch_and_tree('tree')
 
582
        self.build_tree_contents([('tree/ren_'+alpha, 'contents\n')])
 
583
        tree.add(['ren_'+alpha], ['file-id-2'])
 
584
        self.build_tree_contents([('tree/del_'+alpha, 'contents\n')])
 
585
        tree.add(['del_'+alpha], ['file-id-3'])
 
586
        self.build_tree_contents([('tree/mod_'+alpha, 'contents\n')])
 
587
        tree.add(['mod_'+alpha], ['file-id-4'])
 
588
 
 
589
        tree.commit('one', rev_id='rev-1')
 
590
 
 
591
        tree.rename_one('ren_'+alpha, 'ren_'+omega)
 
592
        tree.remove('del_'+alpha)
 
593
        self.build_tree_contents([('tree/add_'+alpha, 'contents\n')])
 
594
        tree.add(['add_'+alpha], ['file-id'])
 
595
        self.build_tree_contents([('tree/mod_'+alpha, 'contents_mod\n')])
 
596
 
 
597
        diff = self.get_diff(tree.basis_tree(), tree)
 
598
        self.assertContainsRe(diff,
 
599
                "=== renamed file 'ren_%s' => 'ren_%s'\n"%(autf8, outf8))
 
600
        self.assertContainsRe(diff, "=== added file 'add_%s'"%autf8)
 
601
        self.assertContainsRe(diff, "=== modified file 'mod_%s'"%autf8)
 
602
        self.assertContainsRe(diff, "=== removed file 'del_%s'"%autf8)
 
603
 
 
604
 
 
605
class DiffWasIs(DiffPath):
 
606
 
 
607
    def diff(self, file_id, old_path, new_path, old_kind, new_kind):
 
608
        self.to_file.write('was: ')
 
609
        self.to_file.write(self.old_tree.get_file(file_id).read())
 
610
        self.to_file.write('is: ')
 
611
        self.to_file.write(self.new_tree.get_file(file_id).read())
 
612
        pass
 
613
 
 
614
 
 
615
class TestDiffTree(TestCaseWithTransport):
 
616
 
 
617
    def setUp(self):
 
618
        TestCaseWithTransport.setUp(self)
 
619
        self.old_tree = self.make_branch_and_tree('old-tree')
 
620
        self.old_tree.lock_write()
 
621
        self.addCleanup(self.old_tree.unlock)
 
622
        self.new_tree = self.make_branch_and_tree('new-tree')
 
623
        self.new_tree.lock_write()
 
624
        self.addCleanup(self.new_tree.unlock)
 
625
        self.differ = DiffTree(self.old_tree, self.new_tree, StringIO())
 
626
 
 
627
    def test_diff_text(self):
 
628
        self.build_tree_contents([('old-tree/olddir/',),
 
629
                                  ('old-tree/olddir/oldfile', 'old\n')])
 
630
        self.old_tree.add('olddir')
 
631
        self.old_tree.add('olddir/oldfile', 'file-id')
 
632
        self.build_tree_contents([('new-tree/newdir/',),
 
633
                                  ('new-tree/newdir/newfile', 'new\n')])
 
634
        self.new_tree.add('newdir')
 
635
        self.new_tree.add('newdir/newfile', 'file-id')
 
636
        differ = DiffText(self.old_tree, self.new_tree, StringIO())
 
637
        differ.diff_text('file-id', None, 'old label', 'new label')
 
638
        self.assertEqual(
 
639
            '--- old label\n+++ new label\n@@ -1,1 +0,0 @@\n-old\n\n',
 
640
            differ.to_file.getvalue())
 
641
        differ.to_file.seek(0)
 
642
        differ.diff_text(None, 'file-id', 'old label', 'new label')
 
643
        self.assertEqual(
 
644
            '--- old label\n+++ new label\n@@ -0,0 +1,1 @@\n+new\n\n',
 
645
            differ.to_file.getvalue())
 
646
        differ.to_file.seek(0)
 
647
        differ.diff_text('file-id', 'file-id', 'old label', 'new label')
 
648
        self.assertEqual(
 
649
            '--- old label\n+++ new label\n@@ -1,1 +1,1 @@\n-old\n+new\n\n',
 
650
            differ.to_file.getvalue())
 
651
 
 
652
    def test_diff_deletion(self):
 
653
        self.build_tree_contents([('old-tree/file', 'contents'),
 
654
                                  ('new-tree/file', 'contents')])
 
655
        self.old_tree.add('file', 'file-id')
 
656
        self.new_tree.add('file', 'file-id')
 
657
        os.unlink('new-tree/file')
 
658
        self.differ.show_diff(None)
 
659
        self.assertContainsRe(self.differ.to_file.getvalue(), '-contents')
 
660
 
 
661
    def test_diff_creation(self):
 
662
        self.build_tree_contents([('old-tree/file', 'contents'),
 
663
                                  ('new-tree/file', 'contents')])
 
664
        self.old_tree.add('file', 'file-id')
 
665
        self.new_tree.add('file', 'file-id')
 
666
        os.unlink('old-tree/file')
 
667
        self.differ.show_diff(None)
 
668
        self.assertContainsRe(self.differ.to_file.getvalue(), '\+contents')
 
669
 
 
670
    def test_diff_symlink(self):
 
671
        differ = DiffSymlink(self.old_tree, self.new_tree, StringIO())
 
672
        differ.diff_symlink('old target', None)
 
673
        self.assertEqual("=== target was 'old target'\n",
 
674
                         differ.to_file.getvalue())
 
675
 
 
676
        differ = DiffSymlink(self.old_tree, self.new_tree, StringIO())
 
677
        differ.diff_symlink(None, 'new target')
 
678
        self.assertEqual("=== target is 'new target'\n",
 
679
                         differ.to_file.getvalue())
 
680
 
 
681
        differ = DiffSymlink(self.old_tree, self.new_tree, StringIO())
 
682
        differ.diff_symlink('old target', 'new target')
 
683
        self.assertEqual("=== target changed 'old target' => 'new target'\n",
 
684
                         differ.to_file.getvalue())
 
685
 
 
686
    def test_diff(self):
 
687
        self.build_tree_contents([('old-tree/olddir/',),
 
688
                                  ('old-tree/olddir/oldfile', 'old\n')])
 
689
        self.old_tree.add('olddir')
 
690
        self.old_tree.add('olddir/oldfile', 'file-id')
 
691
        self.build_tree_contents([('new-tree/newdir/',),
 
692
                                  ('new-tree/newdir/newfile', 'new\n')])
 
693
        self.new_tree.add('newdir')
 
694
        self.new_tree.add('newdir/newfile', 'file-id')
 
695
        self.differ.diff('file-id', 'olddir/oldfile', 'newdir/newfile')
 
696
        self.assertContainsRe(
 
697
            self.differ.to_file.getvalue(),
 
698
            r'--- olddir/oldfile.*\n\+\+\+ newdir/newfile.*\n\@\@ -1,1 \+1,1'
 
699
             ' \@\@\n-old\n\+new\n\n')
 
700
 
 
701
    def test_diff_kind_change(self):
 
702
        self.requireFeature(tests.SymlinkFeature)
 
703
        self.build_tree_contents([('old-tree/olddir/',),
 
704
                                  ('old-tree/olddir/oldfile', 'old\n')])
 
705
        self.old_tree.add('olddir')
 
706
        self.old_tree.add('olddir/oldfile', 'file-id')
 
707
        self.build_tree(['new-tree/newdir/'])
 
708
        os.symlink('new', 'new-tree/newdir/newfile')
 
709
        self.new_tree.add('newdir')
 
710
        self.new_tree.add('newdir/newfile', 'file-id')
 
711
        self.differ.diff('file-id', 'olddir/oldfile', 'newdir/newfile')
 
712
        self.assertContainsRe(
 
713
            self.differ.to_file.getvalue(),
 
714
            r'--- olddir/oldfile.*\n\+\+\+ newdir/newfile.*\n\@\@ -1,1 \+0,0'
 
715
             ' \@\@\n-old\n\n')
 
716
        self.assertContainsRe(self.differ.to_file.getvalue(),
 
717
                              "=== target is 'new'\n")
 
718
 
 
719
    def test_diff_directory(self):
 
720
        self.build_tree(['new-tree/new-dir/'])
 
721
        self.new_tree.add('new-dir', 'new-dir-id')
 
722
        self.differ.diff('new-dir-id', None, 'new-dir')
 
723
        self.assertEqual(self.differ.to_file.getvalue(), '')
 
724
 
 
725
    def create_old_new(self):
 
726
        self.build_tree_contents([('old-tree/olddir/',),
 
727
                                  ('old-tree/olddir/oldfile', 'old\n')])
 
728
        self.old_tree.add('olddir')
 
729
        self.old_tree.add('olddir/oldfile', 'file-id')
 
730
        self.build_tree_contents([('new-tree/newdir/',),
 
731
                                  ('new-tree/newdir/newfile', 'new\n')])
 
732
        self.new_tree.add('newdir')
 
733
        self.new_tree.add('newdir/newfile', 'file-id')
 
734
 
 
735
    def test_register_diff(self):
 
736
        self.create_old_new()
 
737
        old_diff_factories = DiffTree.diff_factories
 
738
        DiffTree.diff_factories=old_diff_factories[:]
 
739
        DiffTree.diff_factories.insert(0, DiffWasIs.from_diff_tree)
 
740
        try:
 
741
            differ = DiffTree(self.old_tree, self.new_tree, StringIO())
 
742
        finally:
 
743
            DiffTree.diff_factories = old_diff_factories
 
744
        differ.diff('file-id', 'olddir/oldfile', 'newdir/newfile')
 
745
        self.assertNotContainsRe(
 
746
            differ.to_file.getvalue(),
 
747
            r'--- olddir/oldfile.*\n\+\+\+ newdir/newfile.*\n\@\@ -1,1 \+1,1'
 
748
             ' \@\@\n-old\n\+new\n\n')
 
749
        self.assertContainsRe(differ.to_file.getvalue(),
 
750
                              'was: old\nis: new\n')
 
751
 
 
752
    def test_extra_factories(self):
 
753
        self.create_old_new()
 
754
        differ = DiffTree(self.old_tree, self.new_tree, StringIO(),
 
755
                            extra_factories=[DiffWasIs.from_diff_tree])
 
756
        differ.diff('file-id', 'olddir/oldfile', 'newdir/newfile')
 
757
        self.assertNotContainsRe(
 
758
            differ.to_file.getvalue(),
 
759
            r'--- olddir/oldfile.*\n\+\+\+ newdir/newfile.*\n\@\@ -1,1 \+1,1'
 
760
             ' \@\@\n-old\n\+new\n\n')
 
761
        self.assertContainsRe(differ.to_file.getvalue(),
 
762
                              'was: old\nis: new\n')
 
763
 
 
764
    def test_alphabetical_order(self):
 
765
        self.build_tree(['new-tree/a-file'])
 
766
        self.new_tree.add('a-file')
 
767
        self.build_tree(['old-tree/b-file'])
 
768
        self.old_tree.add('b-file')
 
769
        self.differ.show_diff(None)
 
770
        self.assertContainsRe(self.differ.to_file.getvalue(),
 
771
            '.*a-file(.|\n)*b-file')
 
772
 
 
773
 
 
774
class TestPatienceDiffLib(TestCase):
 
775
 
 
776
    def setUp(self):
 
777
        super(TestPatienceDiffLib, self).setUp()
 
778
        self._unique_lcs = bzrlib._patiencediff_py.unique_lcs_py
 
779
        self._recurse_matches = bzrlib._patiencediff_py.recurse_matches_py
 
780
        self._PatienceSequenceMatcher = \
 
781
            bzrlib._patiencediff_py.PatienceSequenceMatcher_py
 
782
 
 
783
    def test_unique_lcs(self):
 
784
        unique_lcs = self._unique_lcs
 
785
        self.assertEquals(unique_lcs('', ''), [])
 
786
        self.assertEquals(unique_lcs('', 'a'), [])
 
787
        self.assertEquals(unique_lcs('a', ''), [])
 
788
        self.assertEquals(unique_lcs('a', 'a'), [(0,0)])
 
789
        self.assertEquals(unique_lcs('a', 'b'), [])
 
790
        self.assertEquals(unique_lcs('ab', 'ab'), [(0,0), (1,1)])
 
791
        self.assertEquals(unique_lcs('abcde', 'cdeab'), [(2,0), (3,1), (4,2)])
 
792
        self.assertEquals(unique_lcs('cdeab', 'abcde'), [(0,2), (1,3), (2,4)])
 
793
        self.assertEquals(unique_lcs('abXde', 'abYde'), [(0,0), (1,1), 
 
794
                                                         (3,3), (4,4)])
 
795
        self.assertEquals(unique_lcs('acbac', 'abc'), [(2,1)])
 
796
 
 
797
    def test_recurse_matches(self):
 
798
        def test_one(a, b, matches):
 
799
            test_matches = []
 
800
            self._recurse_matches(
 
801
                a, b, 0, 0, len(a), len(b), test_matches, 10)
 
802
            self.assertEquals(test_matches, matches)
 
803
 
 
804
        test_one(['a', '', 'b', '', 'c'], ['a', 'a', 'b', 'c', 'c'],
 
805
                 [(0, 0), (2, 2), (4, 4)])
 
806
        test_one(['a', 'c', 'b', 'a', 'c'], ['a', 'b', 'c'],
 
807
                 [(0, 0), (2, 1), (4, 2)])
 
808
        # Even though 'bc' is not unique globally, and is surrounded by
 
809
        # non-matching lines, we should still match, because they are locally
 
810
        # unique
 
811
        test_one('abcdbce', 'afbcgdbce', [(0,0), (1, 2), (2, 3), (3, 5),
 
812
                                          (4, 6), (5, 7), (6, 8)])
 
813
 
 
814
        # recurse_matches doesn't match non-unique 
 
815
        # lines surrounded by bogus text.
 
816
        # The update has been done in patiencediff.SequenceMatcher instead
 
817
 
 
818
        # This is what it could be
 
819
        #test_one('aBccDe', 'abccde', [(0,0), (2,2), (3,3), (5,5)])
 
820
 
 
821
        # This is what it currently gives:
 
822
        test_one('aBccDe', 'abccde', [(0,0), (5,5)])
 
823
 
 
824
    def assertDiffBlocks(self, a, b, expected_blocks):
 
825
        """Check that the sequence matcher returns the correct blocks.
 
826
 
 
827
        :param a: A sequence to match
 
828
        :param b: Another sequence to match
 
829
        :param expected_blocks: The expected output, not including the final
 
830
            matching block (len(a), len(b), 0)
 
831
        """
 
832
        matcher = self._PatienceSequenceMatcher(None, a, b)
 
833
        blocks = matcher.get_matching_blocks()
 
834
        last = blocks.pop()
 
835
        self.assertEqual((len(a), len(b), 0), last)
 
836
        self.assertEqual(expected_blocks, blocks)
 
837
 
 
838
    def test_matching_blocks(self):
 
839
        # Some basic matching tests
 
840
        self.assertDiffBlocks('', '', [])
 
841
        self.assertDiffBlocks([], [], [])
 
842
        self.assertDiffBlocks('abc', '', [])
 
843
        self.assertDiffBlocks('', 'abc', [])
 
844
        self.assertDiffBlocks('abcd', 'abcd', [(0, 0, 4)])
 
845
        self.assertDiffBlocks('abcd', 'abce', [(0, 0, 3)])
 
846
        self.assertDiffBlocks('eabc', 'abce', [(1, 0, 3)])
 
847
        self.assertDiffBlocks('eabce', 'abce', [(1, 0, 4)])
 
848
        self.assertDiffBlocks('abcde', 'abXde', [(0, 0, 2), (3, 3, 2)])
 
849
        self.assertDiffBlocks('abcde', 'abXYZde', [(0, 0, 2), (3, 5, 2)])
 
850
        self.assertDiffBlocks('abde', 'abXYZde', [(0, 0, 2), (2, 5, 2)])
 
851
        # This may check too much, but it checks to see that
 
852
        # a copied block stays attached to the previous section,
 
853
        # not the later one.
 
854
        # difflib would tend to grab the trailing longest match
 
855
        # which would make the diff not look right
 
856
        self.assertDiffBlocks('abcdefghijklmnop', 'abcdefxydefghijklmnop',
 
857
                              [(0, 0, 6), (6, 11, 10)])
 
858
 
 
859
        # make sure it supports passing in lists
 
860
        self.assertDiffBlocks(
 
861
                   ['hello there\n',
 
862
                    'world\n',
 
863
                    'how are you today?\n'],
 
864
                   ['hello there\n',
 
865
                    'how are you today?\n'],
 
866
                [(0, 0, 1), (2, 1, 1)])
 
867
 
 
868
        # non unique lines surrounded by non-matching lines
 
869
        # won't be found
 
870
        self.assertDiffBlocks('aBccDe', 'abccde', [(0,0,1), (5,5,1)])
 
871
 
 
872
        # But they only need to be locally unique
 
873
        self.assertDiffBlocks('aBcDec', 'abcdec', [(0,0,1), (2,2,1), (4,4,2)])
 
874
 
 
875
        # non unique blocks won't be matched
 
876
        self.assertDiffBlocks('aBcdEcdFg', 'abcdecdfg', [(0,0,1), (8,8,1)])
 
877
 
 
878
        # but locally unique ones will
 
879
        self.assertDiffBlocks('aBcdEeXcdFg', 'abcdecdfg', [(0,0,1), (2,2,2),
 
880
                                              (5,4,1), (7,5,2), (10,8,1)])
 
881
 
 
882
        self.assertDiffBlocks('abbabbXd', 'cabbabxd', [(7,7,1)])
 
883
        self.assertDiffBlocks('abbabbbb', 'cabbabbc', [])
 
884
        self.assertDiffBlocks('bbbbbbbb', 'cbbbbbbc', [])
 
885
 
 
886
    def test_matching_blocks_tuples(self):
 
887
        # Some basic matching tests
 
888
        self.assertDiffBlocks([], [], [])
 
889
        self.assertDiffBlocks([('a',), ('b',), ('c,')], [], [])
 
890
        self.assertDiffBlocks([], [('a',), ('b',), ('c,')], [])
 
891
        self.assertDiffBlocks([('a',), ('b',), ('c,')],
 
892
                              [('a',), ('b',), ('c,')],
 
893
                              [(0, 0, 3)])
 
894
        self.assertDiffBlocks([('a',), ('b',), ('c,')],
 
895
                              [('a',), ('b',), ('d,')],
 
896
                              [(0, 0, 2)])
 
897
        self.assertDiffBlocks([('d',), ('b',), ('c,')],
 
898
                              [('a',), ('b',), ('c,')],
 
899
                              [(1, 1, 2)])
 
900
        self.assertDiffBlocks([('d',), ('a',), ('b',), ('c,')],
 
901
                              [('a',), ('b',), ('c,')],
 
902
                              [(1, 0, 3)])
 
903
        self.assertDiffBlocks([('a', 'b'), ('c', 'd'), ('e', 'f')],
 
904
                              [('a', 'b'), ('c', 'X'), ('e', 'f')],
 
905
                              [(0, 0, 1), (2, 2, 1)])
 
906
        self.assertDiffBlocks([('a', 'b'), ('c', 'd'), ('e', 'f')],
 
907
                              [('a', 'b'), ('c', 'dX'), ('e', 'f')],
 
908
                              [(0, 0, 1), (2, 2, 1)])
 
909
 
 
910
    def test_opcodes(self):
 
911
        def chk_ops(a, b, expected_codes):
 
912
            s = self._PatienceSequenceMatcher(None, a, b)
 
913
            self.assertEquals(expected_codes, s.get_opcodes())
 
914
 
 
915
        chk_ops('', '', [])
 
916
        chk_ops([], [], [])
 
917
        chk_ops('abc', '', [('delete', 0,3, 0,0)])
 
918
        chk_ops('', 'abc', [('insert', 0,0, 0,3)])
 
919
        chk_ops('abcd', 'abcd', [('equal',    0,4, 0,4)])
 
920
        chk_ops('abcd', 'abce', [('equal',   0,3, 0,3),
 
921
                                 ('replace', 3,4, 3,4)
 
922
                                ])
 
923
        chk_ops('eabc', 'abce', [('delete', 0,1, 0,0),
 
924
                                 ('equal',  1,4, 0,3),
 
925
                                 ('insert', 4,4, 3,4)
 
926
                                ])
 
927
        chk_ops('eabce', 'abce', [('delete', 0,1, 0,0),
 
928
                                  ('equal',  1,5, 0,4)
 
929
                                 ])
 
930
        chk_ops('abcde', 'abXde', [('equal',   0,2, 0,2),
 
931
                                   ('replace', 2,3, 2,3),
 
932
                                   ('equal',   3,5, 3,5)
 
933
                                  ])
 
934
        chk_ops('abcde', 'abXYZde', [('equal',   0,2, 0,2),
 
935
                                     ('replace', 2,3, 2,5),
 
936
                                     ('equal',   3,5, 5,7)
 
937
                                    ])
 
938
        chk_ops('abde', 'abXYZde', [('equal',  0,2, 0,2),
 
939
                                    ('insert', 2,2, 2,5),
 
940
                                    ('equal',  2,4, 5,7)
 
941
                                   ])
 
942
        chk_ops('abcdefghijklmnop', 'abcdefxydefghijklmnop',
 
943
                [('equal',  0,6,  0,6),
 
944
                 ('insert', 6,6,  6,11),
 
945
                 ('equal',  6,16, 11,21)
 
946
                ])
 
947
        chk_ops(
 
948
                [ 'hello there\n'
 
949
                , 'world\n'
 
950
                , 'how are you today?\n'],
 
951
                [ 'hello there\n'
 
952
                , 'how are you today?\n'],
 
953
                [('equal',  0,1, 0,1),
 
954
                 ('delete', 1,2, 1,1),
 
955
                 ('equal',  2,3, 1,2),
 
956
                ])
 
957
        chk_ops('aBccDe', 'abccde', 
 
958
                [('equal',   0,1, 0,1),
 
959
                 ('replace', 1,5, 1,5),
 
960
                 ('equal',   5,6, 5,6),
 
961
                ])
 
962
        chk_ops('aBcDec', 'abcdec', 
 
963
                [('equal',   0,1, 0,1),
 
964
                 ('replace', 1,2, 1,2),
 
965
                 ('equal',   2,3, 2,3),
 
966
                 ('replace', 3,4, 3,4),
 
967
                 ('equal',   4,6, 4,6),
 
968
                ])
 
969
        chk_ops('aBcdEcdFg', 'abcdecdfg', 
 
970
                [('equal',   0,1, 0,1),
 
971
                 ('replace', 1,8, 1,8),
 
972
                 ('equal',   8,9, 8,9)
 
973
                ])
 
974
        chk_ops('aBcdEeXcdFg', 'abcdecdfg', 
 
975
                [('equal',   0,1, 0,1),
 
976
                 ('replace', 1,2, 1,2),
 
977
                 ('equal',   2,4, 2,4),
 
978
                 ('delete', 4,5, 4,4),
 
979
                 ('equal',   5,6, 4,5),
 
980
                 ('delete', 6,7, 5,5),
 
981
                 ('equal',   7,9, 5,7),
 
982
                 ('replace', 9,10, 7,8),
 
983
                 ('equal',   10,11, 8,9)
 
984
                ])
 
985
 
 
986
    def test_grouped_opcodes(self):
 
987
        def chk_ops(a, b, expected_codes, n=3):
 
988
            s = self._PatienceSequenceMatcher(None, a, b)
 
989
            self.assertEquals(expected_codes, list(s.get_grouped_opcodes(n)))
 
990
 
 
991
        chk_ops('', '', [])
 
992
        chk_ops([], [], [])
 
993
        chk_ops('abc', '', [[('delete', 0,3, 0,0)]])
 
994
        chk_ops('', 'abc', [[('insert', 0,0, 0,3)]])
 
995
        chk_ops('abcd', 'abcd', [])
 
996
        chk_ops('abcd', 'abce', [[('equal',   0,3, 0,3),
 
997
                                  ('replace', 3,4, 3,4)
 
998
                                 ]])
 
999
        chk_ops('eabc', 'abce', [[('delete', 0,1, 0,0),
 
1000
                                 ('equal',  1,4, 0,3),
 
1001
                                 ('insert', 4,4, 3,4)
 
1002
                                ]])
 
1003
        chk_ops('abcdefghijklmnop', 'abcdefxydefghijklmnop',
 
1004
                [[('equal',  3,6, 3,6),
 
1005
                  ('insert', 6,6, 6,11),
 
1006
                  ('equal',  6,9, 11,14)
 
1007
                  ]])
 
1008
        chk_ops('abcdefghijklmnop', 'abcdefxydefghijklmnop',
 
1009
                [[('equal',  2,6, 2,6),
 
1010
                  ('insert', 6,6, 6,11),
 
1011
                  ('equal',  6,10, 11,15)
 
1012
                  ]], 4)
 
1013
        chk_ops('Xabcdef', 'abcdef',
 
1014
                [[('delete', 0,1, 0,0),
 
1015
                  ('equal',  1,4, 0,3)
 
1016
                  ]])
 
1017
        chk_ops('abcdef', 'abcdefX',
 
1018
                [[('equal',  3,6, 3,6),
 
1019
                  ('insert', 6,6, 6,7)
 
1020
                  ]])
 
1021
 
 
1022
 
 
1023
    def test_multiple_ranges(self):
 
1024
        # There was an earlier bug where we used a bad set of ranges,
 
1025
        # this triggers that specific bug, to make sure it doesn't regress
 
1026
        self.assertDiffBlocks('abcdefghijklmnop',
 
1027
                              'abcXghiYZQRSTUVWXYZijklmnop',
 
1028
                              [(0, 0, 3), (6, 4, 3), (9, 20, 7)])
 
1029
 
 
1030
        self.assertDiffBlocks('ABCd efghIjk  L',
 
1031
                              'AxyzBCn mo pqrstuvwI1 2  L',
 
1032
                              [(0,0,1), (1, 4, 2), (9, 19, 1), (12, 23, 3)])
 
1033
 
 
1034
        # These are rot13 code snippets.
 
1035
        self.assertDiffBlocks('''\
 
1036
    trg nqqrq jura lbh nqq n svyr va gur qverpgbel.
 
1037
    """
 
1038
    gnxrf_netf = ['svyr*']
 
1039
    gnxrf_bcgvbaf = ['ab-erphefr']
 
1040
  
 
1041
    qrs eha(frys, svyr_yvfg, ab_erphefr=Snyfr):
 
1042
        sebz omeyvo.nqq vzcbeg fzneg_nqq, nqq_ercbegre_cevag, nqq_ercbegre_ahyy
 
1043
        vs vf_dhvrg():
 
1044
            ercbegre = nqq_ercbegre_ahyy
 
1045
        ryfr:
 
1046
            ercbegre = nqq_ercbegre_cevag
 
1047
        fzneg_nqq(svyr_yvfg, abg ab_erphefr, ercbegre)
 
1048
 
 
1049
 
 
1050
pynff pzq_zxqve(Pbzznaq):
 
1051
'''.splitlines(True), '''\
 
1052
    trg nqqrq jura lbh nqq n svyr va gur qverpgbel.
 
1053
 
 
1054
    --qel-eha jvyy fubj juvpu svyrf jbhyq or nqqrq, ohg abg npghnyyl 
 
1055
    nqq gurz.
 
1056
    """
 
1057
    gnxrf_netf = ['svyr*']
 
1058
    gnxrf_bcgvbaf = ['ab-erphefr', 'qel-eha']
 
1059
 
 
1060
    qrs eha(frys, svyr_yvfg, ab_erphefr=Snyfr, qel_eha=Snyfr):
 
1061
        vzcbeg omeyvo.nqq
 
1062
 
 
1063
        vs qel_eha:
 
1064
            vs vf_dhvrg():
 
1065
                # Guvf vf cbvagyrff, ohg V'q engure abg envfr na reebe
 
1066
                npgvba = omeyvo.nqq.nqq_npgvba_ahyy
 
1067
            ryfr:
 
1068
  npgvba = omeyvo.nqq.nqq_npgvba_cevag
 
1069
        ryvs vf_dhvrg():
 
1070
            npgvba = omeyvo.nqq.nqq_npgvba_nqq
 
1071
        ryfr:
 
1072
       npgvba = omeyvo.nqq.nqq_npgvba_nqq_naq_cevag
 
1073
 
 
1074
        omeyvo.nqq.fzneg_nqq(svyr_yvfg, abg ab_erphefr, npgvba)
 
1075
 
 
1076
 
 
1077
pynff pzq_zxqve(Pbzznaq):
 
1078
'''.splitlines(True)
 
1079
, [(0,0,1), (1, 4, 2), (9, 19, 1), (12, 23, 3)])
 
1080
 
 
1081
    def test_patience_unified_diff(self):
 
1082
        txt_a = ['hello there\n',
 
1083
                 'world\n',
 
1084
                 'how are you today?\n']
 
1085
        txt_b = ['hello there\n',
 
1086
                 'how are you today?\n']
 
1087
        unified_diff = bzrlib.patiencediff.unified_diff
 
1088
        psm = self._PatienceSequenceMatcher
 
1089
        self.assertEquals([ '---  \n',
 
1090
                           '+++  \n',
 
1091
                           '@@ -1,3 +1,2 @@\n',
 
1092
                           ' hello there\n',
 
1093
                           '-world\n',
 
1094
                           ' how are you today?\n'
 
1095
                          ]
 
1096
                          , list(unified_diff(txt_a, txt_b,
 
1097
                                 sequencematcher=psm)))
 
1098
        txt_a = map(lambda x: x+'\n', 'abcdefghijklmnop')
 
1099
        txt_b = map(lambda x: x+'\n', 'abcdefxydefghijklmnop')
 
1100
        # This is the result with LongestCommonSubstring matching
 
1101
        self.assertEquals(['---  \n',
 
1102
                           '+++  \n',
 
1103
                           '@@ -1,6 +1,11 @@\n',
 
1104
                           ' a\n',
 
1105
                           ' b\n',
 
1106
                           ' c\n',
 
1107
                           '+d\n',
 
1108
                           '+e\n',
 
1109
                           '+f\n',
 
1110
                           '+x\n',
 
1111
                           '+y\n',
 
1112
                           ' d\n',
 
1113
                           ' e\n',
 
1114
                           ' f\n']
 
1115
                          , list(unified_diff(txt_a, txt_b)))
 
1116
        # And the patience diff
 
1117
        self.assertEquals(['---  \n',
 
1118
                           '+++  \n',
 
1119
                           '@@ -4,6 +4,11 @@\n',
 
1120
                           ' d\n',
 
1121
                           ' e\n',
 
1122
                           ' f\n',
 
1123
                           '+x\n',
 
1124
                           '+y\n',
 
1125
                           '+d\n',
 
1126
                           '+e\n',
 
1127
                           '+f\n',
 
1128
                           ' g\n',
 
1129
                           ' h\n',
 
1130
                           ' i\n',
 
1131
                          ]
 
1132
                          , list(unified_diff(txt_a, txt_b,
 
1133
                                 sequencematcher=psm)))
 
1134
 
 
1135
 
 
1136
class TestPatienceDiffLib_c(TestPatienceDiffLib):
 
1137
 
 
1138
    _test_needs_features = [CompiledPatienceDiffFeature]
 
1139
 
 
1140
    def setUp(self):
 
1141
        super(TestPatienceDiffLib_c, self).setUp()
 
1142
        import bzrlib._patiencediff_c
 
1143
        self._unique_lcs = bzrlib._patiencediff_c.unique_lcs_c
 
1144
        self._recurse_matches = bzrlib._patiencediff_c.recurse_matches_c
 
1145
        self._PatienceSequenceMatcher = \
 
1146
            bzrlib._patiencediff_c.PatienceSequenceMatcher_c
 
1147
 
 
1148
    def test_unhashable(self):
 
1149
        """We should get a proper exception here."""
 
1150
        # We need to be able to hash items in the sequence, lists are
 
1151
        # unhashable, and thus cannot be diffed
 
1152
        e = self.assertRaises(TypeError, self._PatienceSequenceMatcher,
 
1153
                                         None, [[]], [])
 
1154
        e = self.assertRaises(TypeError, self._PatienceSequenceMatcher,
 
1155
                                         None, ['valid', []], [])
 
1156
        e = self.assertRaises(TypeError, self._PatienceSequenceMatcher,
 
1157
                                         None, ['valid'], [[]])
 
1158
        e = self.assertRaises(TypeError, self._PatienceSequenceMatcher,
 
1159
                                         None, ['valid'], ['valid', []])
 
1160
 
 
1161
 
 
1162
class TestPatienceDiffLibFiles(TestCaseInTempDir):
 
1163
 
 
1164
    def setUp(self):
 
1165
        super(TestPatienceDiffLibFiles, self).setUp()
 
1166
        self._PatienceSequenceMatcher = \
 
1167
            bzrlib._patiencediff_py.PatienceSequenceMatcher_py
 
1168
 
 
1169
    def test_patience_unified_diff_files(self):
 
1170
        txt_a = ['hello there\n',
 
1171
                 'world\n',
 
1172
                 'how are you today?\n']
 
1173
        txt_b = ['hello there\n',
 
1174
                 'how are you today?\n']
 
1175
        open('a1', 'wb').writelines(txt_a)
 
1176
        open('b1', 'wb').writelines(txt_b)
 
1177
 
 
1178
        unified_diff_files = bzrlib.patiencediff.unified_diff_files
 
1179
        psm = self._PatienceSequenceMatcher
 
1180
        self.assertEquals(['--- a1 \n',
 
1181
                           '+++ b1 \n',
 
1182
                           '@@ -1,3 +1,2 @@\n',
 
1183
                           ' hello there\n',
 
1184
                           '-world\n',
 
1185
                           ' how are you today?\n',
 
1186
                          ]
 
1187
                          , list(unified_diff_files('a1', 'b1',
 
1188
                                 sequencematcher=psm)))
 
1189
 
 
1190
        txt_a = map(lambda x: x+'\n', 'abcdefghijklmnop')
 
1191
        txt_b = map(lambda x: x+'\n', 'abcdefxydefghijklmnop')
 
1192
        open('a2', 'wb').writelines(txt_a)
 
1193
        open('b2', 'wb').writelines(txt_b)
 
1194
 
 
1195
        # This is the result with LongestCommonSubstring matching
 
1196
        self.assertEquals(['--- a2 \n',
 
1197
                           '+++ b2 \n',
 
1198
                           '@@ -1,6 +1,11 @@\n',
 
1199
                           ' a\n',
 
1200
                           ' b\n',
 
1201
                           ' c\n',
 
1202
                           '+d\n',
 
1203
                           '+e\n',
 
1204
                           '+f\n',
 
1205
                           '+x\n',
 
1206
                           '+y\n',
 
1207
                           ' d\n',
 
1208
                           ' e\n',
 
1209
                           ' f\n']
 
1210
                          , list(unified_diff_files('a2', 'b2')))
 
1211
 
 
1212
        # And the patience diff
 
1213
        self.assertEquals(['--- a2 \n',
 
1214
                           '+++ b2 \n',
 
1215
                           '@@ -4,6 +4,11 @@\n',
 
1216
                           ' d\n',
 
1217
                           ' e\n',
 
1218
                           ' f\n',
 
1219
                           '+x\n',
 
1220
                           '+y\n',
 
1221
                           '+d\n',
 
1222
                           '+e\n',
 
1223
                           '+f\n',
 
1224
                           ' g\n',
 
1225
                           ' h\n',
 
1226
                           ' i\n',
 
1227
                          ]
 
1228
                          , list(unified_diff_files('a2', 'b2',
 
1229
                                 sequencematcher=psm)))
 
1230
 
 
1231
 
 
1232
class TestPatienceDiffLibFiles_c(TestPatienceDiffLibFiles):
 
1233
 
 
1234
    _test_needs_features = [CompiledPatienceDiffFeature]
 
1235
 
 
1236
    def setUp(self):
 
1237
        super(TestPatienceDiffLibFiles_c, self).setUp()
 
1238
        import bzrlib._patiencediff_c
 
1239
        self._PatienceSequenceMatcher = \
 
1240
            bzrlib._patiencediff_c.PatienceSequenceMatcher_c
 
1241
 
 
1242
 
 
1243
class TestUsingCompiledIfAvailable(TestCase):
 
1244
 
 
1245
    def test_PatienceSequenceMatcher(self):
 
1246
        if CompiledPatienceDiffFeature.available():
 
1247
            from bzrlib._patiencediff_c import PatienceSequenceMatcher_c
 
1248
            self.assertIs(PatienceSequenceMatcher_c,
 
1249
                          bzrlib.patiencediff.PatienceSequenceMatcher)
 
1250
        else:
 
1251
            from bzrlib._patiencediff_py import PatienceSequenceMatcher_py
 
1252
            self.assertIs(PatienceSequenceMatcher_py,
 
1253
                          bzrlib.patiencediff.PatienceSequenceMatcher)
 
1254
 
 
1255
    def test_unique_lcs(self):
 
1256
        if CompiledPatienceDiffFeature.available():
 
1257
            from bzrlib._patiencediff_c import unique_lcs_c
 
1258
            self.assertIs(unique_lcs_c,
 
1259
                          bzrlib.patiencediff.unique_lcs)
 
1260
        else:
 
1261
            from bzrlib._patiencediff_py import unique_lcs_py
 
1262
            self.assertIs(unique_lcs_py,
 
1263
                          bzrlib.patiencediff.unique_lcs)
 
1264
 
 
1265
    def test_recurse_matches(self):
 
1266
        if CompiledPatienceDiffFeature.available():
 
1267
            from bzrlib._patiencediff_c import recurse_matches_c
 
1268
            self.assertIs(recurse_matches_c,
 
1269
                          bzrlib.patiencediff.recurse_matches)
 
1270
        else:
 
1271
            from bzrlib._patiencediff_py import recurse_matches_py
 
1272
            self.assertIs(recurse_matches_py,
 
1273
                          bzrlib.patiencediff.recurse_matches)
 
1274
 
 
1275
 
 
1276
class TestDiffFromTool(TestCaseWithTransport):
 
1277
 
 
1278
    def test_from_string(self):
 
1279
        diff_obj = DiffFromTool.from_string('diff', None, None, None)
 
1280
        self.addCleanup(diff_obj.finish)
 
1281
        self.assertEqual(['diff', '%(old_path)s', '%(new_path)s'],
 
1282
            diff_obj.command_template)
 
1283
 
 
1284
    def test_from_string_u5(self):
 
1285
        diff_obj = DiffFromTool.from_string('diff -u\\ 5', None, None, None)
 
1286
        self.addCleanup(diff_obj.finish)
 
1287
        self.assertEqual(['diff', '-u 5', '%(old_path)s', '%(new_path)s'],
 
1288
                         diff_obj.command_template)
 
1289
        self.assertEqual(['diff', '-u 5', 'old-path', 'new-path'],
 
1290
                         diff_obj._get_command('old-path', 'new-path'))
 
1291
 
 
1292
    def test_execute(self):
 
1293
        output = StringIO()
 
1294
        diff_obj = DiffFromTool(['python', '-c',
 
1295
                                 'print "%(old_path)s %(new_path)s"'],
 
1296
                                None, None, output)
 
1297
        self.addCleanup(diff_obj.finish)
 
1298
        diff_obj._execute('old', 'new')
 
1299
        self.assertEqual(output.getvalue().rstrip(), 'old new')
 
1300
 
 
1301
    def test_excute_missing(self):
 
1302
        diff_obj = DiffFromTool(['a-tool-which-is-unlikely-to-exist'],
 
1303
                                None, None, None)
 
1304
        self.addCleanup(diff_obj.finish)
 
1305
        e = self.assertRaises(ExecutableMissing, diff_obj._execute, 'old',
 
1306
                              'new')
 
1307
        self.assertEqual('a-tool-which-is-unlikely-to-exist could not be found'
 
1308
                         ' on this machine', str(e))
 
1309
 
 
1310
    def test_prepare_files(self):
 
1311
        output = StringIO()
 
1312
        tree = self.make_branch_and_tree('tree')
 
1313
        self.build_tree_contents([('tree/oldname', 'oldcontent')])
 
1314
        tree.add('oldname', 'file-id')
 
1315
        tree.commit('old tree', timestamp=0)
 
1316
        tree.rename_one('oldname', 'newname')
 
1317
        self.build_tree_contents([('tree/newname', 'newcontent')])
 
1318
        old_tree = tree.basis_tree()
 
1319
        old_tree.lock_read()
 
1320
        self.addCleanup(old_tree.unlock)
 
1321
        tree.lock_read()
 
1322
        self.addCleanup(tree.unlock)
 
1323
        diff_obj = DiffFromTool(['python', '-c',
 
1324
                                 'print "%(old_path)s %(new_path)s"'],
 
1325
                                old_tree, tree, output)
 
1326
        self.addCleanup(diff_obj.finish)
 
1327
        self.assertContainsRe(diff_obj._root, 'bzr-diff-[^/]*')
 
1328
        old_path, new_path = diff_obj._prepare_files('file-id', 'oldname',
 
1329
                                                     'newname')
 
1330
        self.assertContainsRe(old_path, 'old/oldname$')
 
1331
        self.assertEqual(0, os.stat(old_path).st_mtime)
 
1332
        self.assertContainsRe(new_path, 'new/newname$')
 
1333
        self.assertFileEqual('oldcontent', old_path)
 
1334
        self.assertFileEqual('newcontent', new_path)
 
1335
        if osutils.has_symlinks():
 
1336
            self.assertTrue(os.path.samefile('tree/newname', new_path))
 
1337
        # make sure we can create files with the same parent directories
 
1338
        diff_obj._prepare_files('file-id', 'oldname2', 'newname2')