~bzr-pqm/bzr/bzr.dev

« back to all changes in this revision

Viewing changes to bzrlib/tests/test_diff.py

Compare URLs in RemoteRepository.__eq__, rather than '_client' attributes.

Show diffs side-by-side

added added

removed removed

Lines of Context:
1
 
from bzrlib.tests 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
3
18
from cStringIO import StringIO
4
 
def udiff_lines(old, new):
 
19
import errno
 
20
import subprocess
 
21
from tempfile import TemporaryFile
 
22
 
 
23
from bzrlib.diff import internal_diff, external_diff, show_diff_trees
 
24
from bzrlib.errors import BinaryFile, NoDiff
 
25
import bzrlib.osutils as osutils
 
26
import bzrlib.patiencediff
 
27
from bzrlib.tests import (Feature, TestCase, TestCaseWithTransport,
 
28
                          TestCaseInTempDir, TestSkipped)
 
29
 
 
30
 
 
31
class _UnicodeFilename(Feature):
 
32
    """Does the filesystem support Unicode filenames?"""
 
33
 
 
34
    def _probe(self):
 
35
        try:
 
36
            os.stat(u'\u03b1')
 
37
        except UnicodeEncodeError:
 
38
            return False
 
39
        except (IOError, OSError):
 
40
            # The filesystem allows the Unicode filename but the file doesn't
 
41
            # exist.
 
42
            return True
 
43
        else:
 
44
            # The filesystem allows the Unicode filename and the file exists,
 
45
            # for some reason.
 
46
            return True
 
47
 
 
48
UnicodeFilename = _UnicodeFilename()
 
49
 
 
50
 
 
51
class TestUnicodeFilename(TestCase):
 
52
 
 
53
    def test_probe_passes(self):
 
54
        """UnicodeFilename._probe passes."""
 
55
        # We can't test much more than that because the behaviour depends
 
56
        # on the platform.
 
57
        UnicodeFilename._probe()
 
58
        
 
59
 
 
60
def udiff_lines(old, new, allow_binary=False):
5
61
    output = StringIO()
6
 
    internal_diff('old', old, 'new', new, output)
 
62
    internal_diff('old', old, 'new', new, output, allow_binary)
7
63
    output.seek(0, 0)
8
64
    return output.readlines()
9
65
 
 
66
 
 
67
def external_udiff_lines(old, new, use_stringio=False):
 
68
    if use_stringio:
 
69
        # StringIO has no fileno, so it tests a different codepath
 
70
        output = StringIO()
 
71
    else:
 
72
        output = TemporaryFile()
 
73
    try:
 
74
        external_diff('old', old, 'new', new, output, diff_opts=['-u'])
 
75
    except NoDiff:
 
76
        raise TestSkipped('external "diff" not present to test')
 
77
    output.seek(0, 0)
 
78
    lines = output.readlines()
 
79
    output.close()
 
80
    return lines
 
81
 
 
82
 
10
83
class TestDiff(TestCase):
 
84
 
11
85
    def test_add_nl(self):
12
86
        """diff generates a valid diff for patches that add a newline"""
13
87
        lines = udiff_lines(['boo'], ['boo\n'])
47
121
        self.assert_('@@' in lines[2][2:])
48
122
            ## "Unterminated hunk header for patch:\n%s" % "".join(lines)
49
123
 
 
124
    def test_binary_lines(self):
 
125
        self.assertRaises(BinaryFile, udiff_lines, [1023 * 'a' + '\x00'], [])
 
126
        self.assertRaises(BinaryFile, udiff_lines, [], [1023 * 'a' + '\x00'])
 
127
        udiff_lines([1023 * 'a' + '\x00'], [], allow_binary=True)
 
128
        udiff_lines([], [1023 * 'a' + '\x00'], allow_binary=True)
 
129
 
 
130
    def test_external_diff(self):
 
131
        lines = external_udiff_lines(['boo\n'], ['goo\n'])
 
132
        self.check_patch(lines)
 
133
        self.assertEqual('\n', lines[-1])
 
134
 
 
135
    def test_external_diff_no_fileno(self):
 
136
        # Make sure that we can handle not having a fileno, even
 
137
        # if the diff is large
 
138
        lines = external_udiff_lines(['boo\n']*10000,
 
139
                                     ['goo\n']*10000,
 
140
                                     use_stringio=True)
 
141
        self.check_patch(lines)
 
142
 
 
143
    def test_external_diff_binary_lang_c(self):
 
144
        old_env = {}
 
145
        for lang in ('LANG', 'LC_ALL', 'LANGUAGE'):
 
146
            old_env[lang] = osutils.set_or_unset_env(lang, 'C')
 
147
        try:
 
148
            lines = external_udiff_lines(['\x00foobar\n'], ['foo\x00bar\n'])
 
149
            # Older versions of diffutils say "Binary files", newer
 
150
            # versions just say "Files".
 
151
            self.assertContainsRe(lines[0],
 
152
                                  '(Binary f|F)iles old and new differ\n')
 
153
            self.assertEquals(lines[1:], ['\n'])
 
154
        finally:
 
155
            for lang, old_val in old_env.iteritems():
 
156
                osutils.set_or_unset_env(lang, old_val)
 
157
 
 
158
    def test_no_external_diff(self):
 
159
        """Check that NoDiff is raised when diff is not available"""
 
160
        # Use os.environ['PATH'] to make sure no 'diff' command is available
 
161
        orig_path = os.environ['PATH']
 
162
        try:
 
163
            os.environ['PATH'] = ''
 
164
            self.assertRaises(NoDiff, external_diff,
 
165
                              'old', ['boo\n'], 'new', ['goo\n'],
 
166
                              StringIO(), diff_opts=['-u'])
 
167
        finally:
 
168
            os.environ['PATH'] = orig_path
 
169
        
 
170
    def test_internal_diff_default(self):
 
171
        # Default internal diff encoding is utf8
 
172
        output = StringIO()
 
173
        internal_diff(u'old_\xb5', ['old_text\n'],
 
174
                    u'new_\xe5', ['new_text\n'], output)
 
175
        lines = output.getvalue().splitlines(True)
 
176
        self.check_patch(lines)
 
177
        self.assertEquals(['--- old_\xc2\xb5\n',
 
178
                           '+++ new_\xc3\xa5\n',
 
179
                           '@@ -1,1 +1,1 @@\n',
 
180
                           '-old_text\n',
 
181
                           '+new_text\n',
 
182
                           '\n',
 
183
                          ]
 
184
                          , lines)
 
185
 
 
186
    def test_internal_diff_utf8(self):
 
187
        output = StringIO()
 
188
        internal_diff(u'old_\xb5', ['old_text\n'],
 
189
                    u'new_\xe5', ['new_text\n'], output,
 
190
                    path_encoding='utf8')
 
191
        lines = output.getvalue().splitlines(True)
 
192
        self.check_patch(lines)
 
193
        self.assertEquals(['--- old_\xc2\xb5\n',
 
194
                           '+++ new_\xc3\xa5\n',
 
195
                           '@@ -1,1 +1,1 @@\n',
 
196
                           '-old_text\n',
 
197
                           '+new_text\n',
 
198
                           '\n',
 
199
                          ]
 
200
                          , lines)
 
201
 
 
202
    def test_internal_diff_iso_8859_1(self):
 
203
        output = StringIO()
 
204
        internal_diff(u'old_\xb5', ['old_text\n'],
 
205
                    u'new_\xe5', ['new_text\n'], output,
 
206
                    path_encoding='iso-8859-1')
 
207
        lines = output.getvalue().splitlines(True)
 
208
        self.check_patch(lines)
 
209
        self.assertEquals(['--- old_\xb5\n',
 
210
                           '+++ new_\xe5\n',
 
211
                           '@@ -1,1 +1,1 @@\n',
 
212
                           '-old_text\n',
 
213
                           '+new_text\n',
 
214
                           '\n',
 
215
                          ]
 
216
                          , lines)
 
217
 
 
218
    def test_internal_diff_returns_bytes(self):
 
219
        import StringIO
 
220
        output = StringIO.StringIO()
 
221
        internal_diff(u'old_\xb5', ['old_text\n'],
 
222
                    u'new_\xe5', ['new_text\n'], output)
 
223
        self.failUnless(isinstance(output.getvalue(), str),
 
224
            'internal_diff should return bytestrings')
 
225
 
 
226
 
 
227
class TestDiffFiles(TestCaseInTempDir):
 
228
 
 
229
    def test_external_diff_binary(self):
 
230
        """The output when using external diff should use diff's i18n error"""
 
231
        # Make sure external_diff doesn't fail in the current LANG
 
232
        lines = external_udiff_lines(['\x00foobar\n'], ['foo\x00bar\n'])
 
233
 
 
234
        cmd = ['diff', '-u', '--binary', 'old', 'new']
 
235
        open('old', 'wb').write('\x00foobar\n')
 
236
        open('new', 'wb').write('foo\x00bar\n')
 
237
        pipe = subprocess.Popen(cmd, stdout=subprocess.PIPE,
 
238
                                     stdin=subprocess.PIPE)
 
239
        out, err = pipe.communicate()
 
240
        # Diff returns '2' on Binary files.
 
241
        self.assertEqual(2, pipe.returncode)
 
242
        # We should output whatever diff tells us, plus a trailing newline
 
243
        self.assertEqual(out.splitlines(True) + ['\n'], lines)
 
244
 
 
245
 
 
246
class TestShowDiffTreesHelper(TestCaseWithTransport):
 
247
    """Has a helper for running show_diff_trees"""
 
248
 
 
249
    def get_diff(self, tree1, tree2, specific_files=None, working_tree=None):
 
250
        output = StringIO()
 
251
        if working_tree is not None:
 
252
            extra_trees = (working_tree,)
 
253
        else:
 
254
            extra_trees = ()
 
255
        show_diff_trees(tree1, tree2, output, specific_files=specific_files,
 
256
                        extra_trees=extra_trees, old_label='old/',
 
257
                        new_label='new/')
 
258
        return output.getvalue()
 
259
 
 
260
 
 
261
class TestDiffDates(TestShowDiffTreesHelper):
 
262
 
 
263
    def setUp(self):
 
264
        super(TestDiffDates, self).setUp()
 
265
        self.wt = self.make_branch_and_tree('.')
 
266
        self.b = self.wt.branch
 
267
        self.build_tree_contents([
 
268
            ('file1', 'file1 contents at rev 1\n'),
 
269
            ('file2', 'file2 contents at rev 1\n')
 
270
            ])
 
271
        self.wt.add(['file1', 'file2'])
 
272
        self.wt.commit(
 
273
            message='Revision 1',
 
274
            timestamp=1143849600, # 2006-04-01 00:00:00 UTC
 
275
            timezone=0,
 
276
            rev_id='rev-1')
 
277
        self.build_tree_contents([('file1', 'file1 contents at rev 2\n')])
 
278
        self.wt.commit(
 
279
            message='Revision 2',
 
280
            timestamp=1143936000, # 2006-04-02 00:00:00 UTC
 
281
            timezone=28800,
 
282
            rev_id='rev-2')
 
283
        self.build_tree_contents([('file2', 'file2 contents at rev 3\n')])
 
284
        self.wt.commit(
 
285
            message='Revision 3',
 
286
            timestamp=1144022400, # 2006-04-03 00:00:00 UTC
 
287
            timezone=-3600,
 
288
            rev_id='rev-3')
 
289
        self.wt.remove(['file2'])
 
290
        self.wt.commit(
 
291
            message='Revision 4',
 
292
            timestamp=1144108800, # 2006-04-04 00:00:00 UTC
 
293
            timezone=0,
 
294
            rev_id='rev-4')
 
295
        self.build_tree_contents([
 
296
            ('file1', 'file1 contents in working tree\n')
 
297
            ])
 
298
        # set the date stamps for files in the working tree to known values
 
299
        os.utime('file1', (1144195200, 1144195200)) # 2006-04-05 00:00:00 UTC
 
300
 
 
301
    def test_diff_rev_tree_working_tree(self):
 
302
        output = self.get_diff(self.wt.basis_tree(), self.wt)
 
303
        # note that the date for old/file1 is from rev 2 rather than from
 
304
        # the basis revision (rev 4)
 
305
        self.assertEqualDiff(output, '''\
 
306
=== modified file 'file1'
 
307
--- old/file1\t2006-04-02 00:00:00 +0000
 
308
+++ new/file1\t2006-04-05 00:00:00 +0000
 
309
@@ -1,1 +1,1 @@
 
310
-file1 contents at rev 2
 
311
+file1 contents in working tree
 
312
 
 
313
''')
 
314
 
 
315
    def test_diff_rev_tree_rev_tree(self):
 
316
        tree1 = self.b.repository.revision_tree('rev-2')
 
317
        tree2 = self.b.repository.revision_tree('rev-3')
 
318
        output = self.get_diff(tree1, tree2)
 
319
        self.assertEqualDiff(output, '''\
 
320
=== modified file 'file2'
 
321
--- old/file2\t2006-04-01 00:00:00 +0000
 
322
+++ new/file2\t2006-04-03 00:00:00 +0000
 
323
@@ -1,1 +1,1 @@
 
324
-file2 contents at rev 1
 
325
+file2 contents at rev 3
 
326
 
 
327
''')
 
328
        
 
329
    def test_diff_add_files(self):
 
330
        tree1 = self.b.repository.revision_tree(None)
 
331
        tree2 = self.b.repository.revision_tree('rev-1')
 
332
        output = self.get_diff(tree1, tree2)
 
333
        # the files have the epoch time stamp for the tree in which
 
334
        # they don't exist.
 
335
        self.assertEqualDiff(output, '''\
 
336
=== added file 'file1'
 
337
--- old/file1\t1970-01-01 00:00:00 +0000
 
338
+++ new/file1\t2006-04-01 00:00:00 +0000
 
339
@@ -0,0 +1,1 @@
 
340
+file1 contents at rev 1
 
341
 
 
342
=== added file 'file2'
 
343
--- old/file2\t1970-01-01 00:00:00 +0000
 
344
+++ new/file2\t2006-04-01 00:00:00 +0000
 
345
@@ -0,0 +1,1 @@
 
346
+file2 contents at rev 1
 
347
 
 
348
''')
 
349
 
 
350
    def test_diff_remove_files(self):
 
351
        tree1 = self.b.repository.revision_tree('rev-3')
 
352
        tree2 = self.b.repository.revision_tree('rev-4')
 
353
        output = self.get_diff(tree1, tree2)
 
354
        # the file has the epoch time stamp for the tree in which
 
355
        # it doesn't exist.
 
356
        self.assertEqualDiff(output, '''\
 
357
=== removed file 'file2'
 
358
--- old/file2\t2006-04-03 00:00:00 +0000
 
359
+++ new/file2\t1970-01-01 00:00:00 +0000
 
360
@@ -1,1 +0,0 @@
 
361
-file2 contents at rev 3
 
362
 
 
363
''')
 
364
 
 
365
    def test_show_diff_specified(self):
 
366
        """A working tree filename can be used to identify a file"""
 
367
        self.wt.rename_one('file1', 'file1b')
 
368
        old_tree = self.b.repository.revision_tree('rev-1')
 
369
        new_tree = self.b.repository.revision_tree('rev-4')
 
370
        out = self.get_diff(old_tree, new_tree, specific_files=['file1b'], 
 
371
                            working_tree=self.wt)
 
372
        self.assertContainsRe(out, 'file1\t')
 
373
 
 
374
    def test_recursive_diff(self):
 
375
        """Children of directories are matched"""
 
376
        os.mkdir('dir1')
 
377
        os.mkdir('dir2')
 
378
        self.wt.add(['dir1', 'dir2'])
 
379
        self.wt.rename_one('file1', 'dir1/file1')
 
380
        old_tree = self.b.repository.revision_tree('rev-1')
 
381
        new_tree = self.b.repository.revision_tree('rev-4')
 
382
        out = self.get_diff(old_tree, new_tree, specific_files=['dir1'], 
 
383
                            working_tree=self.wt)
 
384
        self.assertContainsRe(out, 'file1\t')
 
385
        out = self.get_diff(old_tree, new_tree, specific_files=['dir2'], 
 
386
                            working_tree=self.wt)
 
387
        self.assertNotContainsRe(out, 'file1\t')
 
388
 
 
389
 
 
390
 
 
391
class TestShowDiffTrees(TestShowDiffTreesHelper):
 
392
    """Direct tests for show_diff_trees"""
 
393
 
 
394
    def test_modified_file(self):
 
395
        """Test when a file is modified."""
 
396
        tree = self.make_branch_and_tree('tree')
 
397
        self.build_tree_contents([('tree/file', 'contents\n')])
 
398
        tree.add(['file'], ['file-id'])
 
399
        tree.commit('one', rev_id='rev-1')
 
400
 
 
401
        self.build_tree_contents([('tree/file', 'new contents\n')])
 
402
        diff = self.get_diff(tree.basis_tree(), tree)
 
403
        self.assertContainsRe(diff, "=== modified file 'file'\n")
 
404
        self.assertContainsRe(diff, '--- old/file\t')
 
405
        self.assertContainsRe(diff, '\\+\\+\\+ new/file\t')
 
406
        self.assertContainsRe(diff, '-contents\n'
 
407
                                    '\\+new contents\n')
 
408
 
 
409
    def test_modified_file_in_renamed_dir(self):
 
410
        """Test when a file is modified in a renamed directory."""
 
411
        tree = self.make_branch_and_tree('tree')
 
412
        self.build_tree(['tree/dir/'])
 
413
        self.build_tree_contents([('tree/dir/file', 'contents\n')])
 
414
        tree.add(['dir', 'dir/file'], ['dir-id', 'file-id'])
 
415
        tree.commit('one', rev_id='rev-1')
 
416
 
 
417
        tree.rename_one('dir', 'other')
 
418
        self.build_tree_contents([('tree/other/file', 'new contents\n')])
 
419
        diff = self.get_diff(tree.basis_tree(), tree)
 
420
        self.assertContainsRe(diff, "=== renamed directory 'dir' => 'other'\n")
 
421
        self.assertContainsRe(diff, "=== modified file 'other/file'\n")
 
422
        # XXX: This is technically incorrect, because it used to be at another
 
423
        # location. What to do?
 
424
        self.assertContainsRe(diff, '--- old/dir/file\t')
 
425
        self.assertContainsRe(diff, '\\+\\+\\+ new/other/file\t')
 
426
        self.assertContainsRe(diff, '-contents\n'
 
427
                                    '\\+new contents\n')
 
428
 
 
429
    def test_renamed_directory(self):
 
430
        """Test when only a directory is only renamed."""
 
431
        tree = self.make_branch_and_tree('tree')
 
432
        self.build_tree(['tree/dir/'])
 
433
        self.build_tree_contents([('tree/dir/file', 'contents\n')])
 
434
        tree.add(['dir', 'dir/file'], ['dir-id', 'file-id'])
 
435
        tree.commit('one', rev_id='rev-1')
 
436
 
 
437
        tree.rename_one('dir', 'newdir')
 
438
        diff = self.get_diff(tree.basis_tree(), tree)
 
439
        # Renaming a directory should be a single "you renamed this dir" even
 
440
        # when there are files inside.
 
441
        self.assertEqual("=== renamed directory 'dir' => 'newdir'\n", diff)
 
442
 
 
443
    def test_renamed_file(self):
 
444
        """Test when a file is only renamed."""
 
445
        tree = self.make_branch_and_tree('tree')
 
446
        self.build_tree_contents([('tree/file', 'contents\n')])
 
447
        tree.add(['file'], ['file-id'])
 
448
        tree.commit('one', rev_id='rev-1')
 
449
 
 
450
        tree.rename_one('file', 'newname')
 
451
        diff = self.get_diff(tree.basis_tree(), tree)
 
452
        self.assertContainsRe(diff, "=== renamed file 'file' => 'newname'\n")
 
453
        # We shouldn't have a --- or +++ line, because there is no content
 
454
        # change
 
455
        self.assertNotContainsRe(diff, '---')
 
456
 
 
457
    def test_renamed_and_modified_file(self):
 
458
        """Test when a file is only renamed."""
 
459
        tree = self.make_branch_and_tree('tree')
 
460
        self.build_tree_contents([('tree/file', 'contents\n')])
 
461
        tree.add(['file'], ['file-id'])
 
462
        tree.commit('one', rev_id='rev-1')
 
463
 
 
464
        tree.rename_one('file', 'newname')
 
465
        self.build_tree_contents([('tree/newname', 'new contents\n')])
 
466
        diff = self.get_diff(tree.basis_tree(), tree)
 
467
        self.assertContainsRe(diff, "=== renamed file 'file' => 'newname'\n")
 
468
        self.assertContainsRe(diff, '--- old/file\t')
 
469
        self.assertContainsRe(diff, '\\+\\+\\+ new/newname\t')
 
470
        self.assertContainsRe(diff, '-contents\n'
 
471
                                    '\\+new contents\n')
 
472
 
 
473
    def test_binary_unicode_filenames(self):
 
474
        """Test that contents of files are *not* encoded in UTF-8 when there
 
475
        is a binary file in the diff.
 
476
        """
 
477
        # See https://bugs.launchpad.net/bugs/110092.
 
478
        self.requireFeature(UnicodeFilename)
 
479
 
 
480
        # This bug isn't triggered with cStringIO.
 
481
        from StringIO import StringIO
 
482
        tree = self.make_branch_and_tree('tree')
 
483
        alpha, omega = u'\u03b1', u'\u03c9'
 
484
        alpha_utf8, omega_utf8 = alpha.encode('utf8'), omega.encode('utf8')
 
485
        self.build_tree_contents(
 
486
            [('tree/' + alpha, chr(0)),
 
487
             ('tree/' + omega,
 
488
              ('The %s and the %s\n' % (alpha_utf8, omega_utf8)))])
 
489
        tree.add([alpha], ['file-id'])
 
490
        tree.add([omega], ['file-id-2'])
 
491
        diff_content = StringIO()
 
492
        show_diff_trees(tree.basis_tree(), tree, diff_content)
 
493
        diff = diff_content.getvalue()
 
494
        self.assertContainsRe(diff, r"=== added file '%s'" % alpha_utf8)
 
495
        self.assertContainsRe(
 
496
            diff, "Binary files a/%s.*and b/%s.* differ\n" % (alpha_utf8, alpha_utf8))
 
497
        self.assertContainsRe(diff, r"=== added file '%s'" % omega_utf8)
 
498
        self.assertContainsRe(diff, r"--- a/%s" % (omega_utf8,))
 
499
        self.assertContainsRe(diff, r"\+\+\+ b/%s" % (omega_utf8,))
 
500
 
 
501
 
 
502
class TestPatienceDiffLib(TestCase):
 
503
 
 
504
    def test_unique_lcs(self):
 
505
        unique_lcs = bzrlib.patiencediff.unique_lcs
 
506
        self.assertEquals(unique_lcs('', ''), [])
 
507
        self.assertEquals(unique_lcs('a', 'a'), [(0,0)])
 
508
        self.assertEquals(unique_lcs('a', 'b'), [])
 
509
        self.assertEquals(unique_lcs('ab', 'ab'), [(0,0), (1,1)])
 
510
        self.assertEquals(unique_lcs('abcde', 'cdeab'), [(2,0), (3,1), (4,2)])
 
511
        self.assertEquals(unique_lcs('cdeab', 'abcde'), [(0,2), (1,3), (2,4)])
 
512
        self.assertEquals(unique_lcs('abXde', 'abYde'), [(0,0), (1,1), 
 
513
                                                         (3,3), (4,4)])
 
514
        self.assertEquals(unique_lcs('acbac', 'abc'), [(2,1)])
 
515
 
 
516
    def test_recurse_matches(self):
 
517
        def test_one(a, b, matches):
 
518
            test_matches = []
 
519
            bzrlib.patiencediff.recurse_matches(a, b, 0, 0, len(a), len(b),
 
520
                test_matches, 10)
 
521
            self.assertEquals(test_matches, matches)
 
522
 
 
523
        test_one(['a', '', 'b', '', 'c'], ['a', 'a', 'b', 'c', 'c'],
 
524
                 [(0, 0), (2, 2), (4, 4)])
 
525
        test_one(['a', 'c', 'b', 'a', 'c'], ['a', 'b', 'c'],
 
526
                 [(0, 0), (2, 1), (4, 2)])
 
527
 
 
528
        # recurse_matches doesn't match non-unique 
 
529
        # lines surrounded by bogus text.
 
530
        # The update has been done in patiencediff.SequenceMatcher instead
 
531
 
 
532
        # This is what it could be
 
533
        #test_one('aBccDe', 'abccde', [(0,0), (2,2), (3,3), (5,5)])
 
534
 
 
535
        # This is what it currently gives:
 
536
        test_one('aBccDe', 'abccde', [(0,0), (5,5)])
 
537
 
 
538
    def test_matching_blocks(self):
 
539
        def chk_blocks(a, b, expected_blocks):
 
540
            # difflib always adds a signature of the total
 
541
            # length, with no matching entries at the end
 
542
            s = bzrlib.patiencediff.PatienceSequenceMatcher(None, a, b)
 
543
            blocks = s.get_matching_blocks()
 
544
            self.assertEquals((len(a), len(b), 0), blocks[-1])
 
545
            self.assertEquals(expected_blocks, blocks[:-1])
 
546
 
 
547
        # Some basic matching tests
 
548
        chk_blocks('', '', [])
 
549
        chk_blocks([], [], [])
 
550
        chk_blocks('abcd', 'abcd', [(0, 0, 4)])
 
551
        chk_blocks('abcd', 'abce', [(0, 0, 3)])
 
552
        chk_blocks('eabc', 'abce', [(1, 0, 3)])
 
553
        chk_blocks('eabce', 'abce', [(1, 0, 4)])
 
554
        chk_blocks('abcde', 'abXde', [(0, 0, 2), (3, 3, 2)])
 
555
        chk_blocks('abcde', 'abXYZde', [(0, 0, 2), (3, 5, 2)])
 
556
        chk_blocks('abde', 'abXYZde', [(0, 0, 2), (2, 5, 2)])
 
557
        # This may check too much, but it checks to see that 
 
558
        # a copied block stays attached to the previous section,
 
559
        # not the later one.
 
560
        # difflib would tend to grab the trailing longest match
 
561
        # which would make the diff not look right
 
562
        chk_blocks('abcdefghijklmnop', 'abcdefxydefghijklmnop',
 
563
                   [(0, 0, 6), (6, 11, 10)])
 
564
 
 
565
        # make sure it supports passing in lists
 
566
        chk_blocks(
 
567
                   ['hello there\n',
 
568
                    'world\n',
 
569
                    'how are you today?\n'],
 
570
                   ['hello there\n',
 
571
                    'how are you today?\n'],
 
572
                [(0, 0, 1), (2, 1, 1)])
 
573
 
 
574
        # non unique lines surrounded by non-matching lines
 
575
        # won't be found
 
576
        chk_blocks('aBccDe', 'abccde', [(0,0,1), (5,5,1)])
 
577
 
 
578
        # But they only need to be locally unique
 
579
        chk_blocks('aBcDec', 'abcdec', [(0,0,1), (2,2,1), (4,4,2)])
 
580
 
 
581
        # non unique blocks won't be matched
 
582
        chk_blocks('aBcdEcdFg', 'abcdecdfg', [(0,0,1), (8,8,1)])
 
583
 
 
584
        # but locally unique ones will
 
585
        chk_blocks('aBcdEeXcdFg', 'abcdecdfg', [(0,0,1), (2,2,2),
 
586
                                              (5,4,1), (7,5,2), (10,8,1)])
 
587
 
 
588
        chk_blocks('abbabbXd', 'cabbabxd', [(7,7,1)])
 
589
        chk_blocks('abbabbbb', 'cabbabbc', [])
 
590
        chk_blocks('bbbbbbbb', 'cbbbbbbc', [])
 
591
 
 
592
    def test_opcodes(self):
 
593
        def chk_ops(a, b, expected_codes):
 
594
            s = bzrlib.patiencediff.PatienceSequenceMatcher(None, a, b)
 
595
            self.assertEquals(expected_codes, s.get_opcodes())
 
596
 
 
597
        chk_ops('', '', [])
 
598
        chk_ops([], [], [])
 
599
        chk_ops('abcd', 'abcd', [('equal',    0,4, 0,4)])
 
600
        chk_ops('abcd', 'abce', [('equal',   0,3, 0,3),
 
601
                                 ('replace', 3,4, 3,4)
 
602
                                ])
 
603
        chk_ops('eabc', 'abce', [('delete', 0,1, 0,0),
 
604
                                 ('equal',  1,4, 0,3),
 
605
                                 ('insert', 4,4, 3,4)
 
606
                                ])
 
607
        chk_ops('eabce', 'abce', [('delete', 0,1, 0,0),
 
608
                                  ('equal',  1,5, 0,4)
 
609
                                 ])
 
610
        chk_ops('abcde', 'abXde', [('equal',   0,2, 0,2),
 
611
                                   ('replace', 2,3, 2,3),
 
612
                                   ('equal',   3,5, 3,5)
 
613
                                  ])
 
614
        chk_ops('abcde', 'abXYZde', [('equal',   0,2, 0,2),
 
615
                                     ('replace', 2,3, 2,5),
 
616
                                     ('equal',   3,5, 5,7)
 
617
                                    ])
 
618
        chk_ops('abde', 'abXYZde', [('equal',  0,2, 0,2),
 
619
                                    ('insert', 2,2, 2,5),
 
620
                                    ('equal',  2,4, 5,7)
 
621
                                   ])
 
622
        chk_ops('abcdefghijklmnop', 'abcdefxydefghijklmnop',
 
623
                [('equal',  0,6,  0,6),
 
624
                 ('insert', 6,6,  6,11),
 
625
                 ('equal',  6,16, 11,21)
 
626
                ])
 
627
        chk_ops(
 
628
                [ 'hello there\n'
 
629
                , 'world\n'
 
630
                , 'how are you today?\n'],
 
631
                [ 'hello there\n'
 
632
                , 'how are you today?\n'],
 
633
                [('equal',  0,1, 0,1),
 
634
                 ('delete', 1,2, 1,1),
 
635
                 ('equal',  2,3, 1,2),
 
636
                ])
 
637
        chk_ops('aBccDe', 'abccde', 
 
638
                [('equal',   0,1, 0,1),
 
639
                 ('replace', 1,5, 1,5),
 
640
                 ('equal',   5,6, 5,6),
 
641
                ])
 
642
        chk_ops('aBcDec', 'abcdec', 
 
643
                [('equal',   0,1, 0,1),
 
644
                 ('replace', 1,2, 1,2),
 
645
                 ('equal',   2,3, 2,3),
 
646
                 ('replace', 3,4, 3,4),
 
647
                 ('equal',   4,6, 4,6),
 
648
                ])
 
649
        chk_ops('aBcdEcdFg', 'abcdecdfg', 
 
650
                [('equal',   0,1, 0,1),
 
651
                 ('replace', 1,8, 1,8),
 
652
                 ('equal',   8,9, 8,9)
 
653
                ])
 
654
        chk_ops('aBcdEeXcdFg', 'abcdecdfg', 
 
655
                [('equal',   0,1, 0,1),
 
656
                 ('replace', 1,2, 1,2),
 
657
                 ('equal',   2,4, 2,4),
 
658
                 ('delete', 4,5, 4,4),
 
659
                 ('equal',   5,6, 4,5),
 
660
                 ('delete', 6,7, 5,5),
 
661
                 ('equal',   7,9, 5,7),
 
662
                 ('replace', 9,10, 7,8),
 
663
                 ('equal',   10,11, 8,9)
 
664
                ])
 
665
 
 
666
    def test_multiple_ranges(self):
 
667
        # There was an earlier bug where we used a bad set of ranges,
 
668
        # this triggers that specific bug, to make sure it doesn't regress
 
669
        def chk_blocks(a, b, expected_blocks):
 
670
            # difflib always adds a signature of the total
 
671
            # length, with no matching entries at the end
 
672
            s = bzrlib.patiencediff.PatienceSequenceMatcher(None, a, b)
 
673
            blocks = s.get_matching_blocks()
 
674
            x = blocks.pop()
 
675
            self.assertEquals(x, (len(a), len(b), 0))
 
676
            self.assertEquals(expected_blocks, blocks)
 
677
 
 
678
        chk_blocks('abcdefghijklmnop'
 
679
                 , 'abcXghiYZQRSTUVWXYZijklmnop'
 
680
                 , [(0, 0, 3), (6, 4, 3), (9, 20, 7)])
 
681
 
 
682
        chk_blocks('ABCd efghIjk  L'
 
683
                 , 'AxyzBCn mo pqrstuvwI1 2  L'
 
684
                 , [(0,0,1), (1, 4, 2), (9, 19, 1), (12, 23, 3)])
 
685
 
 
686
        # These are rot13 code snippets.
 
687
        chk_blocks('''\
 
688
    trg nqqrq jura lbh nqq n svyr va gur qverpgbel.
 
689
    """
 
690
    gnxrf_netf = ['svyr*']
 
691
    gnxrf_bcgvbaf = ['ab-erphefr']
 
692
  
 
693
    qrs eha(frys, svyr_yvfg, ab_erphefr=Snyfr):
 
694
        sebz omeyvo.nqq vzcbeg fzneg_nqq, nqq_ercbegre_cevag, nqq_ercbegre_ahyy
 
695
        vs vf_dhvrg():
 
696
            ercbegre = nqq_ercbegre_ahyy
 
697
        ryfr:
 
698
            ercbegre = nqq_ercbegre_cevag
 
699
        fzneg_nqq(svyr_yvfg, abg ab_erphefr, ercbegre)
 
700
 
 
701
 
 
702
pynff pzq_zxqve(Pbzznaq):
 
703
'''.splitlines(True), '''\
 
704
    trg nqqrq jura lbh nqq n svyr va gur qverpgbel.
 
705
 
 
706
    --qel-eha jvyy fubj juvpu svyrf jbhyq or nqqrq, ohg abg npghnyyl 
 
707
    nqq gurz.
 
708
    """
 
709
    gnxrf_netf = ['svyr*']
 
710
    gnxrf_bcgvbaf = ['ab-erphefr', 'qel-eha']
 
711
 
 
712
    qrs eha(frys, svyr_yvfg, ab_erphefr=Snyfr, qel_eha=Snyfr):
 
713
        vzcbeg omeyvo.nqq
 
714
 
 
715
        vs qel_eha:
 
716
            vs vf_dhvrg():
 
717
                # Guvf vf cbvagyrff, ohg V'q engure abg envfr na reebe
 
718
                npgvba = omeyvo.nqq.nqq_npgvba_ahyy
 
719
            ryfr:
 
720
  npgvba = omeyvo.nqq.nqq_npgvba_cevag
 
721
        ryvs vf_dhvrg():
 
722
            npgvba = omeyvo.nqq.nqq_npgvba_nqq
 
723
        ryfr:
 
724
       npgvba = omeyvo.nqq.nqq_npgvba_nqq_naq_cevag
 
725
 
 
726
        omeyvo.nqq.fzneg_nqq(svyr_yvfg, abg ab_erphefr, npgvba)
 
727
 
 
728
 
 
729
pynff pzq_zxqve(Pbzznaq):
 
730
'''.splitlines(True)
 
731
, [(0,0,1), (1, 4, 2), (9, 19, 1), (12, 23, 3)])
 
732
 
 
733
    def test_patience_unified_diff(self):
 
734
        txt_a = ['hello there\n',
 
735
                 'world\n',
 
736
                 'how are you today?\n']
 
737
        txt_b = ['hello there\n',
 
738
                 'how are you today?\n']
 
739
        unified_diff = bzrlib.patiencediff.unified_diff
 
740
        psm = bzrlib.patiencediff.PatienceSequenceMatcher
 
741
        self.assertEquals([ '---  \n',
 
742
                           '+++  \n',
 
743
                           '@@ -1,3 +1,2 @@\n',
 
744
                           ' hello there\n',
 
745
                           '-world\n',
 
746
                           ' how are you today?\n'
 
747
                          ]
 
748
                          , list(unified_diff(txt_a, txt_b,
 
749
                                 sequencematcher=psm)))
 
750
        txt_a = map(lambda x: x+'\n', 'abcdefghijklmnop')
 
751
        txt_b = map(lambda x: x+'\n', 'abcdefxydefghijklmnop')
 
752
        # This is the result with LongestCommonSubstring matching
 
753
        self.assertEquals(['---  \n',
 
754
                           '+++  \n',
 
755
                           '@@ -1,6 +1,11 @@\n',
 
756
                           ' a\n',
 
757
                           ' b\n',
 
758
                           ' c\n',
 
759
                           '+d\n',
 
760
                           '+e\n',
 
761
                           '+f\n',
 
762
                           '+x\n',
 
763
                           '+y\n',
 
764
                           ' d\n',
 
765
                           ' e\n',
 
766
                           ' f\n']
 
767
                          , list(unified_diff(txt_a, txt_b)))
 
768
        # And the patience diff
 
769
        self.assertEquals(['---  \n',
 
770
                           '+++  \n',
 
771
                           '@@ -4,6 +4,11 @@\n',
 
772
                           ' d\n',
 
773
                           ' e\n',
 
774
                           ' f\n',
 
775
                           '+x\n',
 
776
                           '+y\n',
 
777
                           '+d\n',
 
778
                           '+e\n',
 
779
                           '+f\n',
 
780
                           ' g\n',
 
781
                           ' h\n',
 
782
                           ' i\n',
 
783
                          ]
 
784
                          , list(unified_diff(txt_a, txt_b,
 
785
                                 sequencematcher=psm)))
 
786
 
 
787
 
 
788
class TestPatienceDiffLibFiles(TestCaseInTempDir):
 
789
 
 
790
    def test_patience_unified_diff_files(self):
 
791
        txt_a = ['hello there\n',
 
792
                 'world\n',
 
793
                 'how are you today?\n']
 
794
        txt_b = ['hello there\n',
 
795
                 'how are you today?\n']
 
796
        open('a1', 'wb').writelines(txt_a)
 
797
        open('b1', 'wb').writelines(txt_b)
 
798
 
 
799
        unified_diff_files = bzrlib.patiencediff.unified_diff_files
 
800
        psm = bzrlib.patiencediff.PatienceSequenceMatcher
 
801
        self.assertEquals(['--- a1 \n',
 
802
                           '+++ b1 \n',
 
803
                           '@@ -1,3 +1,2 @@\n',
 
804
                           ' hello there\n',
 
805
                           '-world\n',
 
806
                           ' how are you today?\n',
 
807
                          ]
 
808
                          , list(unified_diff_files('a1', 'b1',
 
809
                                 sequencematcher=psm)))
 
810
 
 
811
        txt_a = map(lambda x: x+'\n', 'abcdefghijklmnop')
 
812
        txt_b = map(lambda x: x+'\n', 'abcdefxydefghijklmnop')
 
813
        open('a2', 'wb').writelines(txt_a)
 
814
        open('b2', 'wb').writelines(txt_b)
 
815
 
 
816
        # This is the result with LongestCommonSubstring matching
 
817
        self.assertEquals(['--- a2 \n',
 
818
                           '+++ b2 \n',
 
819
                           '@@ -1,6 +1,11 @@\n',
 
820
                           ' a\n',
 
821
                           ' b\n',
 
822
                           ' c\n',
 
823
                           '+d\n',
 
824
                           '+e\n',
 
825
                           '+f\n',
 
826
                           '+x\n',
 
827
                           '+y\n',
 
828
                           ' d\n',
 
829
                           ' e\n',
 
830
                           ' f\n']
 
831
                          , list(unified_diff_files('a2', 'b2')))
 
832
 
 
833
        # And the patience diff
 
834
        self.assertEquals(['--- a2 \n',
 
835
                           '+++ b2 \n',
 
836
                           '@@ -4,6 +4,11 @@\n',
 
837
                           ' d\n',
 
838
                           ' e\n',
 
839
                           ' f\n',
 
840
                           '+x\n',
 
841
                           '+y\n',
 
842
                           '+d\n',
 
843
                           '+e\n',
 
844
                           '+f\n',
 
845
                           ' g\n',
 
846
                           ' h\n',
 
847
                           ' i\n',
 
848
                          ]
 
849
                          , list(unified_diff_files('a2', 'b2',
 
850
                                 sequencematcher=psm)))