~bzr-pqm/bzr/bzr.dev

« back to all changes in this revision

Viewing changes to bzrlib/tests/test_diff.py

  • Committer: Alexander Belchenko
  • Date: 2008-03-11 08:49:42 UTC
  • mto: This revision was merged to the branch mainline in revision 3268.
  • Revision ID: bialix@ukr.net-20080311084942-w1w0w3v0m20p2pbc
use sys.version_info

Show diffs side-by-side

added added

removed removed

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