~bzr-pqm/bzr/bzr.dev

« back to all changes in this revision

Viewing changes to bzrlib/tests/test_diff.py

Change topic to hidden-commands

Show diffs side-by-side

added added

removed removed

Lines of Context:
1
 
from bzrlib.selftest import TestCase
2
 
from bzrlib.diff import internal_diff
 
1
# Copyright (C) 2005, 2006 Canonical Ltd
 
2
#
 
3
# This program is free software; you can redistribute it and/or modify
 
4
# it under the terms of the GNU General Public License as published by
 
5
# the Free Software Foundation; either version 2 of the License, or
 
6
# (at your option) any later version.
 
7
#
 
8
# This program is distributed in the hope that it will be useful,
 
9
# but WITHOUT ANY WARRANTY; without even the implied warranty of
 
10
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
 
11
# GNU General Public License for more details.
 
12
#
 
13
# You should have received a copy of the GNU General Public License
 
14
# along with this program; if not, write to the Free Software
 
15
# Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA  02111-1307  USA
 
16
 
 
17
import os
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.patiencediff
 
26
from bzrlib.tests import (TestCase, TestCaseWithTransport,
 
27
                          TestCaseInTempDir, TestSkipped)
 
28
 
 
29
 
 
30
def udiff_lines(old, new, allow_binary=False):
5
31
    output = StringIO()
6
 
    internal_diff('old', old, 'new', new, output)
 
32
    internal_diff('old', old, 'new', new, output, allow_binary)
7
33
    output.seek(0, 0)
8
34
    return output.readlines()
9
35
 
10
 
def check_patch(lines):
11
 
    assert len(lines) > 1, \
12
 
        "Not enough lines for a file header for patch:\n%s" % "".join(lines)
13
 
    assert lines[0].startswith ('---'), \
14
 
        'No orig line for patch:\n%s' % "".join(lines)
15
 
    assert lines[1].startswith ('+++'), \
16
 
        'No mod line for patch:\n%s' % "".join(lines)
17
 
    assert len(lines) > 2, \
18
 
        "No hunks for patch:\n%s" % "".join(lines)
19
 
    assert lines[2].startswith('@@'),\
20
 
        "No hunk header for patch:\n%s" % "".join(lines)
21
 
    assert '@@' in lines[2][2:], \
22
 
        "Unterminated hunk header for patch:\n%s" % "".join(lines)
 
36
 
 
37
def external_udiff_lines(old, new, use_stringio=False):
 
38
    if use_stringio:
 
39
        # StringIO has no fileno, so it tests a different codepath
 
40
        output = StringIO()
 
41
    else:
 
42
        output = TemporaryFile()
 
43
    try:
 
44
        external_diff('old', old, 'new', new, output, diff_opts=['-u'])
 
45
    except NoDiff:
 
46
        raise TestSkipped('external "diff" not present to test')
 
47
    output.seek(0, 0)
 
48
    lines = output.readlines()
 
49
    output.close()
 
50
    return lines
 
51
 
23
52
 
24
53
class TestDiff(TestCase):
 
54
 
25
55
    def test_add_nl(self):
26
56
        """diff generates a valid diff for patches that add a newline"""
27
57
        lines = udiff_lines(['boo'], ['boo\n'])
28
 
        check_patch(lines)
29
 
        assert lines[4] == '\\ No newline at end of file\n', \
30
 
            "expected no-nl, got %r" % lines[4]
 
58
        self.check_patch(lines)
 
59
        self.assertEquals(lines[4], '\\ No newline at end of file\n')
 
60
            ## "expected no-nl, got %r" % lines[4]
31
61
 
32
62
    def test_add_nl_2(self):
33
63
        """diff generates a valid diff for patches that change last line and
34
64
        add a newline.
35
65
        """
36
66
        lines = udiff_lines(['boo'], ['goo\n'])
37
 
        check_patch(lines)
38
 
        assert lines[4] == '\\ No newline at end of file\n', \
39
 
            "expected no-nl, got %r" % lines[4]
 
67
        self.check_patch(lines)
 
68
        self.assertEquals(lines[4], '\\ No newline at end of file\n')
 
69
            ## "expected no-nl, got %r" % lines[4]
40
70
 
41
71
    def test_remove_nl(self):
42
72
        """diff generates a valid diff for patches that change last line and
43
73
        add a newline.
44
74
        """
45
75
        lines = udiff_lines(['boo\n'], ['boo'])
46
 
        check_patch(lines)
47
 
        assert lines[5] == '\\ No newline at end of file\n', \
48
 
            "expected no-nl, got %r" % lines[5]
 
76
        self.check_patch(lines)
 
77
        self.assertEquals(lines[5], '\\ No newline at end of file\n')
 
78
            ## "expected no-nl, got %r" % lines[5]
 
79
 
 
80
    def check_patch(self, lines):
 
81
        self.assert_(len(lines) > 1)
 
82
            ## "Not enough lines for a file header for patch:\n%s" % "".join(lines)
 
83
        self.assert_(lines[0].startswith ('---'))
 
84
            ## 'No orig line for patch:\n%s' % "".join(lines)
 
85
        self.assert_(lines[1].startswith ('+++'))
 
86
            ## 'No mod line for patch:\n%s' % "".join(lines)
 
87
        self.assert_(len(lines) > 2)
 
88
            ## "No hunks for patch:\n%s" % "".join(lines)
 
89
        self.assert_(lines[2].startswith('@@'))
 
90
            ## "No hunk header for patch:\n%s" % "".join(lines)
 
91
        self.assert_('@@' in lines[2][2:])
 
92
            ## "Unterminated hunk header for patch:\n%s" % "".join(lines)
 
93
 
 
94
    def test_binary_lines(self):
 
95
        self.assertRaises(BinaryFile, udiff_lines, [1023 * 'a' + '\x00'], [])
 
96
        self.assertRaises(BinaryFile, udiff_lines, [], [1023 * 'a' + '\x00'])
 
97
        udiff_lines([1023 * 'a' + '\x00'], [], allow_binary=True)
 
98
        udiff_lines([], [1023 * 'a' + '\x00'], allow_binary=True)
 
99
 
 
100
    def test_external_diff(self):
 
101
        lines = external_udiff_lines(['boo\n'], ['goo\n'])
 
102
        self.check_patch(lines)
 
103
        self.assertEqual('\n', lines[-1])
 
104
 
 
105
    def test_external_diff_no_fileno(self):
 
106
        # Make sure that we can handle not having a fileno, even
 
107
        # if the diff is large
 
108
        lines = external_udiff_lines(['boo\n']*10000,
 
109
                                     ['goo\n']*10000,
 
110
                                     use_stringio=True)
 
111
        self.check_patch(lines)
 
112
 
 
113
    def test_external_diff_binary_lang_c(self):
 
114
        orig_lang = os.environ.get('LANG')
 
115
        orig_lc_all = os.environ.get('LC_ALL')
 
116
        try:
 
117
            os.environ['LANG'] = 'C'
 
118
            os.environ['LC_ALL'] = 'C'
 
119
            lines = external_udiff_lines(['\x00foobar\n'], ['foo\x00bar\n'])
 
120
            # Older versions of diffutils say "Binary files", newer
 
121
            # versions just say "Files".
 
122
            self.assertContainsRe(lines[0],
 
123
                                  '(Binary f|F)iles old and new differ\n')
 
124
            self.assertEquals(lines[1:], ['\n'])
 
125
        finally:
 
126
            for name, value in [('LANG', orig_lang), ('LC_ALL', orig_lc_all)]:
 
127
                if value is None:
 
128
                    del os.environ[name]
 
129
                else:
 
130
                    os.environ[name] = value
 
131
 
 
132
    def test_no_external_diff(self):
 
133
        """Check that NoDiff is raised when diff is not available"""
 
134
        # Use os.environ['PATH'] to make sure no 'diff' command is available
 
135
        orig_path = os.environ['PATH']
 
136
        try:
 
137
            os.environ['PATH'] = ''
 
138
            self.assertRaises(NoDiff, external_diff,
 
139
                              'old', ['boo\n'], 'new', ['goo\n'],
 
140
                              StringIO(), diff_opts=['-u'])
 
141
        finally:
 
142
            os.environ['PATH'] = orig_path
 
143
        
 
144
    def test_internal_diff_default(self):
 
145
        # Default internal diff encoding is utf8
 
146
        output = StringIO()
 
147
        internal_diff(u'old_\xb5', ['old_text\n'],
 
148
                    u'new_\xe5', ['new_text\n'], output)
 
149
        lines = output.getvalue().splitlines(True)
 
150
        self.check_patch(lines)
 
151
        self.assertEquals(['--- old_\xc2\xb5\n',
 
152
                           '+++ new_\xc3\xa5\n',
 
153
                           '@@ -1,1 +1,1 @@\n',
 
154
                           '-old_text\n',
 
155
                           '+new_text\n',
 
156
                           '\n',
 
157
                          ]
 
158
                          , lines)
 
159
 
 
160
    def test_internal_diff_utf8(self):
 
161
        output = StringIO()
 
162
        internal_diff(u'old_\xb5', ['old_text\n'],
 
163
                    u'new_\xe5', ['new_text\n'], output,
 
164
                    path_encoding='utf8')
 
165
        lines = output.getvalue().splitlines(True)
 
166
        self.check_patch(lines)
 
167
        self.assertEquals(['--- old_\xc2\xb5\n',
 
168
                           '+++ new_\xc3\xa5\n',
 
169
                           '@@ -1,1 +1,1 @@\n',
 
170
                           '-old_text\n',
 
171
                           '+new_text\n',
 
172
                           '\n',
 
173
                          ]
 
174
                          , lines)
 
175
 
 
176
    def test_internal_diff_iso_8859_1(self):
 
177
        output = StringIO()
 
178
        internal_diff(u'old_\xb5', ['old_text\n'],
 
179
                    u'new_\xe5', ['new_text\n'], output,
 
180
                    path_encoding='iso-8859-1')
 
181
        lines = output.getvalue().splitlines(True)
 
182
        self.check_patch(lines)
 
183
        self.assertEquals(['--- old_\xb5\n',
 
184
                           '+++ new_\xe5\n',
 
185
                           '@@ -1,1 +1,1 @@\n',
 
186
                           '-old_text\n',
 
187
                           '+new_text\n',
 
188
                           '\n',
 
189
                          ]
 
190
                          , lines)
 
191
 
 
192
    def test_internal_diff_returns_bytes(self):
 
193
        import StringIO
 
194
        output = StringIO.StringIO()
 
195
        internal_diff(u'old_\xb5', ['old_text\n'],
 
196
                    u'new_\xe5', ['new_text\n'], output)
 
197
        self.failUnless(isinstance(output.getvalue(), str),
 
198
            'internal_diff should return bytestrings')
 
199
 
 
200
 
 
201
class TestDiffFiles(TestCaseInTempDir):
 
202
 
 
203
    def test_external_diff_binary(self):
 
204
        """The output when using external diff should use diff's i18n error"""
 
205
        # Make sure external_diff doesn't fail in the current LANG
 
206
        lines = external_udiff_lines(['\x00foobar\n'], ['foo\x00bar\n'])
 
207
 
 
208
        cmd = ['diff', '-u', 'old', 'new']
 
209
        open('old', 'wb').write('\x00foobar\n')
 
210
        open('new', 'wb').write('foo\x00bar\n')
 
211
        pipe = subprocess.Popen(cmd, stdout=subprocess.PIPE,
 
212
                                     stdin=subprocess.PIPE)
 
213
        out, err = pipe.communicate()
 
214
        # Diff returns '2' on Binary files.
 
215
        self.assertEqual(2, pipe.returncode)
 
216
        # We should output whatever diff tells us, plus a trailing newline
 
217
        self.assertEqual(out.splitlines(True) + ['\n'], lines)
 
218
 
 
219
 
 
220
class TestDiffDates(TestCaseWithTransport):
 
221
 
 
222
    def setUp(self):
 
223
        super(TestDiffDates, self).setUp()
 
224
        self.wt = self.make_branch_and_tree('.')
 
225
        self.b = self.wt.branch
 
226
        self.build_tree_contents([
 
227
            ('file1', 'file1 contents at rev 1\n'),
 
228
            ('file2', 'file2 contents at rev 1\n')
 
229
            ])
 
230
        self.wt.add(['file1', 'file2'])
 
231
        self.wt.commit(
 
232
            message='Revision 1',
 
233
            timestamp=1143849600, # 2006-04-01 00:00:00 UTC
 
234
            timezone=0,
 
235
            rev_id='rev-1')
 
236
        self.build_tree_contents([('file1', 'file1 contents at rev 2\n')])
 
237
        self.wt.commit(
 
238
            message='Revision 2',
 
239
            timestamp=1143936000, # 2006-04-02 00:00:00 UTC
 
240
            timezone=28800,
 
241
            rev_id='rev-2')
 
242
        self.build_tree_contents([('file2', 'file2 contents at rev 3\n')])
 
243
        self.wt.commit(
 
244
            message='Revision 3',
 
245
            timestamp=1144022400, # 2006-04-03 00:00:00 UTC
 
246
            timezone=-3600,
 
247
            rev_id='rev-3')
 
248
        self.wt.remove(['file2'])
 
249
        self.wt.commit(
 
250
            message='Revision 4',
 
251
            timestamp=1144108800, # 2006-04-04 00:00:00 UTC
 
252
            timezone=0,
 
253
            rev_id='rev-4')
 
254
        self.build_tree_contents([
 
255
            ('file1', 'file1 contents in working tree\n')
 
256
            ])
 
257
        # set the date stamps for files in the working tree to known values
 
258
        os.utime('file1', (1144195200, 1144195200)) # 2006-04-05 00:00:00 UTC
 
259
 
 
260
    def get_diff(self, tree1, tree2, specific_files=None, working_tree=None):
 
261
        output = StringIO()
 
262
        if working_tree is not None:
 
263
            extra_trees = (working_tree,)
 
264
        else:
 
265
            extra_trees = ()
 
266
        show_diff_trees(tree1, tree2, output, specific_files=specific_files,
 
267
                        extra_trees=extra_trees, old_label='old/', 
 
268
                        new_label='new/')
 
269
        return output.getvalue()
 
270
 
 
271
    def test_diff_rev_tree_working_tree(self):
 
272
        output = self.get_diff(self.wt.basis_tree(), self.wt)
 
273
        # note that the date for old/file1 is from rev 2 rather than from
 
274
        # the basis revision (rev 4)
 
275
        self.assertEqualDiff(output, '''\
 
276
=== modified file 'file1'
 
277
--- old/file1\t2006-04-02 00:00:00 +0000
 
278
+++ new/file1\t2006-04-05 00:00:00 +0000
 
279
@@ -1,1 +1,1 @@
 
280
-file1 contents at rev 2
 
281
+file1 contents in working tree
 
282
 
 
283
''')
 
284
 
 
285
    def test_diff_rev_tree_rev_tree(self):
 
286
        tree1 = self.b.repository.revision_tree('rev-2')
 
287
        tree2 = self.b.repository.revision_tree('rev-3')
 
288
        output = self.get_diff(tree1, tree2)
 
289
        self.assertEqualDiff(output, '''\
 
290
=== modified file 'file2'
 
291
--- old/file2\t2006-04-01 00:00:00 +0000
 
292
+++ new/file2\t2006-04-03 00:00:00 +0000
 
293
@@ -1,1 +1,1 @@
 
294
-file2 contents at rev 1
 
295
+file2 contents at rev 3
 
296
 
 
297
''')
 
298
        
 
299
    def test_diff_add_files(self):
 
300
        tree1 = self.b.repository.revision_tree(None)
 
301
        tree2 = self.b.repository.revision_tree('rev-1')
 
302
        output = self.get_diff(tree1, tree2)
 
303
        # the files have the epoch time stamp for the tree in which
 
304
        # they don't exist.
 
305
        self.assertEqualDiff(output, '''\
 
306
=== added file 'file1'
 
307
--- old/file1\t1970-01-01 00:00:00 +0000
 
308
+++ new/file1\t2006-04-01 00:00:00 +0000
 
309
@@ -0,0 +1,1 @@
 
310
+file1 contents at rev 1
 
311
 
 
312
=== added file 'file2'
 
313
--- old/file2\t1970-01-01 00:00:00 +0000
 
314
+++ new/file2\t2006-04-01 00:00:00 +0000
 
315
@@ -0,0 +1,1 @@
 
316
+file2 contents at rev 1
 
317
 
 
318
''')
 
319
 
 
320
    def test_diff_remove_files(self):
 
321
        tree1 = self.b.repository.revision_tree('rev-3')
 
322
        tree2 = self.b.repository.revision_tree('rev-4')
 
323
        output = self.get_diff(tree1, tree2)
 
324
        # the file has the epoch time stamp for the tree in which
 
325
        # it doesn't exist.
 
326
        self.assertEqualDiff(output, '''\
 
327
=== removed file 'file2'
 
328
--- old/file2\t2006-04-03 00:00:00 +0000
 
329
+++ new/file2\t1970-01-01 00:00:00 +0000
 
330
@@ -1,1 +0,0 @@
 
331
-file2 contents at rev 3
 
332
 
 
333
''')
 
334
 
 
335
    def test_show_diff_specified(self):
 
336
        """A working tree filename can be used to identify a file"""
 
337
        self.wt.rename_one('file1', 'file1b')
 
338
        old_tree = self.b.repository.revision_tree('rev-1')
 
339
        new_tree = self.b.repository.revision_tree('rev-4')
 
340
        out = self.get_diff(old_tree, new_tree, specific_files=['file1b'], 
 
341
                            working_tree=self.wt)
 
342
        self.assertContainsRe(out, 'file1\t')
 
343
 
 
344
    def test_recursive_diff(self):
 
345
        """Children of directories are matched"""
 
346
        os.mkdir('dir1')
 
347
        os.mkdir('dir2')
 
348
        self.wt.add(['dir1', 'dir2'])
 
349
        self.wt.rename_one('file1', 'dir1/file1')
 
350
        old_tree = self.b.repository.revision_tree('rev-1')
 
351
        new_tree = self.b.repository.revision_tree('rev-4')
 
352
        out = self.get_diff(old_tree, new_tree, specific_files=['dir1'], 
 
353
                            working_tree=self.wt)
 
354
        self.assertContainsRe(out, 'file1\t')
 
355
        out = self.get_diff(old_tree, new_tree, specific_files=['dir2'], 
 
356
                            working_tree=self.wt)
 
357
        self.assertNotContainsRe(out, 'file1\t')
 
358
 
 
359
 
 
360
class TestPatienceDiffLib(TestCase):
 
361
 
 
362
    def test_unique_lcs(self):
 
363
        unique_lcs = bzrlib.patiencediff.unique_lcs
 
364
        self.assertEquals(unique_lcs('', ''), [])
 
365
        self.assertEquals(unique_lcs('a', 'a'), [(0,0)])
 
366
        self.assertEquals(unique_lcs('a', 'b'), [])
 
367
        self.assertEquals(unique_lcs('ab', 'ab'), [(0,0), (1,1)])
 
368
        self.assertEquals(unique_lcs('abcde', 'cdeab'), [(2,0), (3,1), (4,2)])
 
369
        self.assertEquals(unique_lcs('cdeab', 'abcde'), [(0,2), (1,3), (2,4)])
 
370
        self.assertEquals(unique_lcs('abXde', 'abYde'), [(0,0), (1,1), 
 
371
                                                         (3,3), (4,4)])
 
372
        self.assertEquals(unique_lcs('acbac', 'abc'), [(2,1)])
 
373
 
 
374
    def test_recurse_matches(self):
 
375
        def test_one(a, b, matches):
 
376
            test_matches = []
 
377
            bzrlib.patiencediff.recurse_matches(a, b, 0, 0, len(a), len(b),
 
378
                test_matches, 10)
 
379
            self.assertEquals(test_matches, matches)
 
380
 
 
381
        test_one(['a', '', 'b', '', 'c'], ['a', 'a', 'b', 'c', 'c'],
 
382
                 [(0, 0), (2, 2), (4, 4)])
 
383
        test_one(['a', 'c', 'b', 'a', 'c'], ['a', 'b', 'c'],
 
384
                 [(0, 0), (2, 1), (4, 2)])
 
385
 
 
386
        # recurse_matches doesn't match non-unique 
 
387
        # lines surrounded by bogus text.
 
388
        # The update has been done in patiencediff.SequenceMatcher instead
 
389
 
 
390
        # This is what it could be
 
391
        #test_one('aBccDe', 'abccde', [(0,0), (2,2), (3,3), (5,5)])
 
392
 
 
393
        # This is what it currently gives:
 
394
        test_one('aBccDe', 'abccde', [(0,0), (5,5)])
 
395
 
 
396
    def test_matching_blocks(self):
 
397
        def chk_blocks(a, b, expected_blocks):
 
398
            # difflib always adds a signature of the total
 
399
            # length, with no matching entries at the end
 
400
            s = bzrlib.patiencediff.PatienceSequenceMatcher(None, a, b)
 
401
            blocks = s.get_matching_blocks()
 
402
            self.assertEquals((len(a), len(b), 0), blocks[-1])
 
403
            self.assertEquals(expected_blocks, blocks[:-1])
 
404
 
 
405
        # Some basic matching tests
 
406
        chk_blocks('', '', [])
 
407
        chk_blocks([], [], [])
 
408
        chk_blocks('abcd', 'abcd', [(0, 0, 4)])
 
409
        chk_blocks('abcd', 'abce', [(0, 0, 3)])
 
410
        chk_blocks('eabc', 'abce', [(1, 0, 3)])
 
411
        chk_blocks('eabce', 'abce', [(1, 0, 4)])
 
412
        chk_blocks('abcde', 'abXde', [(0, 0, 2), (3, 3, 2)])
 
413
        chk_blocks('abcde', 'abXYZde', [(0, 0, 2), (3, 5, 2)])
 
414
        chk_blocks('abde', 'abXYZde', [(0, 0, 2), (2, 5, 2)])
 
415
        # This may check too much, but it checks to see that 
 
416
        # a copied block stays attached to the previous section,
 
417
        # not the later one.
 
418
        # difflib would tend to grab the trailing longest match
 
419
        # which would make the diff not look right
 
420
        chk_blocks('abcdefghijklmnop', 'abcdefxydefghijklmnop',
 
421
                   [(0, 0, 6), (6, 11, 10)])
 
422
 
 
423
        # make sure it supports passing in lists
 
424
        chk_blocks(
 
425
                   ['hello there\n',
 
426
                    'world\n',
 
427
                    'how are you today?\n'],
 
428
                   ['hello there\n',
 
429
                    'how are you today?\n'],
 
430
                [(0, 0, 1), (2, 1, 1)])
 
431
 
 
432
        # non unique lines surrounded by non-matching lines
 
433
        # won't be found
 
434
        chk_blocks('aBccDe', 'abccde', [(0,0,1), (5,5,1)])
 
435
 
 
436
        # But they only need to be locally unique
 
437
        chk_blocks('aBcDec', 'abcdec', [(0,0,1), (2,2,1), (4,4,2)])
 
438
 
 
439
        # non unique blocks won't be matched
 
440
        chk_blocks('aBcdEcdFg', 'abcdecdfg', [(0,0,1), (8,8,1)])
 
441
 
 
442
        # but locally unique ones will
 
443
        chk_blocks('aBcdEeXcdFg', 'abcdecdfg', [(0,0,1), (2,2,2),
 
444
                                              (5,4,1), (7,5,2), (10,8,1)])
 
445
 
 
446
        chk_blocks('abbabbXd', 'cabbabxd', [(7,7,1)])
 
447
        chk_blocks('abbabbbb', 'cabbabbc', [])
 
448
        chk_blocks('bbbbbbbb', 'cbbbbbbc', [])
 
449
 
 
450
    def test_opcodes(self):
 
451
        def chk_ops(a, b, expected_codes):
 
452
            s = bzrlib.patiencediff.PatienceSequenceMatcher(None, a, b)
 
453
            self.assertEquals(expected_codes, s.get_opcodes())
 
454
 
 
455
        chk_ops('', '', [])
 
456
        chk_ops([], [], [])
 
457
        chk_ops('abcd', 'abcd', [('equal',    0,4, 0,4)])
 
458
        chk_ops('abcd', 'abce', [('equal',   0,3, 0,3),
 
459
                                 ('replace', 3,4, 3,4)
 
460
                                ])
 
461
        chk_ops('eabc', 'abce', [('delete', 0,1, 0,0),
 
462
                                 ('equal',  1,4, 0,3),
 
463
                                 ('insert', 4,4, 3,4)
 
464
                                ])
 
465
        chk_ops('eabce', 'abce', [('delete', 0,1, 0,0),
 
466
                                  ('equal',  1,5, 0,4)
 
467
                                 ])
 
468
        chk_ops('abcde', 'abXde', [('equal',   0,2, 0,2),
 
469
                                   ('replace', 2,3, 2,3),
 
470
                                   ('equal',   3,5, 3,5)
 
471
                                  ])
 
472
        chk_ops('abcde', 'abXYZde', [('equal',   0,2, 0,2),
 
473
                                     ('replace', 2,3, 2,5),
 
474
                                     ('equal',   3,5, 5,7)
 
475
                                    ])
 
476
        chk_ops('abde', 'abXYZde', [('equal',  0,2, 0,2),
 
477
                                    ('insert', 2,2, 2,5),
 
478
                                    ('equal',  2,4, 5,7)
 
479
                                   ])
 
480
        chk_ops('abcdefghijklmnop', 'abcdefxydefghijklmnop',
 
481
                [('equal',  0,6,  0,6),
 
482
                 ('insert', 6,6,  6,11),
 
483
                 ('equal',  6,16, 11,21)
 
484
                ])
 
485
        chk_ops(
 
486
                [ 'hello there\n'
 
487
                , 'world\n'
 
488
                , 'how are you today?\n'],
 
489
                [ 'hello there\n'
 
490
                , 'how are you today?\n'],
 
491
                [('equal',  0,1, 0,1),
 
492
                 ('delete', 1,2, 1,1),
 
493
                 ('equal',  2,3, 1,2),
 
494
                ])
 
495
        chk_ops('aBccDe', 'abccde', 
 
496
                [('equal',   0,1, 0,1),
 
497
                 ('replace', 1,5, 1,5),
 
498
                 ('equal',   5,6, 5,6),
 
499
                ])
 
500
        chk_ops('aBcDec', 'abcdec', 
 
501
                [('equal',   0,1, 0,1),
 
502
                 ('replace', 1,2, 1,2),
 
503
                 ('equal',   2,3, 2,3),
 
504
                 ('replace', 3,4, 3,4),
 
505
                 ('equal',   4,6, 4,6),
 
506
                ])
 
507
        chk_ops('aBcdEcdFg', 'abcdecdfg', 
 
508
                [('equal',   0,1, 0,1),
 
509
                 ('replace', 1,8, 1,8),
 
510
                 ('equal',   8,9, 8,9)
 
511
                ])
 
512
        chk_ops('aBcdEeXcdFg', 'abcdecdfg', 
 
513
                [('equal',   0,1, 0,1),
 
514
                 ('replace', 1,2, 1,2),
 
515
                 ('equal',   2,4, 2,4),
 
516
                 ('delete', 4,5, 4,4),
 
517
                 ('equal',   5,6, 4,5),
 
518
                 ('delete', 6,7, 5,5),
 
519
                 ('equal',   7,9, 5,7),
 
520
                 ('replace', 9,10, 7,8),
 
521
                 ('equal',   10,11, 8,9)
 
522
                ])
 
523
 
 
524
    def test_multiple_ranges(self):
 
525
        # There was an earlier bug where we used a bad set of ranges,
 
526
        # this triggers that specific bug, to make sure it doesn't regress
 
527
        def chk_blocks(a, b, expected_blocks):
 
528
            # difflib always adds a signature of the total
 
529
            # length, with no matching entries at the end
 
530
            s = bzrlib.patiencediff.PatienceSequenceMatcher(None, a, b)
 
531
            blocks = s.get_matching_blocks()
 
532
            x = blocks.pop()
 
533
            self.assertEquals(x, (len(a), len(b), 0))
 
534
            self.assertEquals(expected_blocks, blocks)
 
535
 
 
536
        chk_blocks('abcdefghijklmnop'
 
537
                 , 'abcXghiYZQRSTUVWXYZijklmnop'
 
538
                 , [(0, 0, 3), (6, 4, 3), (9, 20, 7)])
 
539
 
 
540
        chk_blocks('ABCd efghIjk  L'
 
541
                 , 'AxyzBCn mo pqrstuvwI1 2  L'
 
542
                 , [(0,0,1), (1, 4, 2), (9, 19, 1), (12, 23, 3)])
 
543
 
 
544
        # These are rot13 code snippets.
 
545
        chk_blocks('''\
 
546
    trg nqqrq jura lbh nqq n svyr va gur qverpgbel.
 
547
    """
 
548
    gnxrf_netf = ['svyr*']
 
549
    gnxrf_bcgvbaf = ['ab-erphefr']
 
550
  
 
551
    qrs eha(frys, svyr_yvfg, ab_erphefr=Snyfr):
 
552
        sebz omeyvo.nqq vzcbeg fzneg_nqq, nqq_ercbegre_cevag, nqq_ercbegre_ahyy
 
553
        vs vf_dhvrg():
 
554
            ercbegre = nqq_ercbegre_ahyy
 
555
        ryfr:
 
556
            ercbegre = nqq_ercbegre_cevag
 
557
        fzneg_nqq(svyr_yvfg, abg ab_erphefr, ercbegre)
 
558
 
 
559
 
 
560
pynff pzq_zxqve(Pbzznaq):
 
561
'''.splitlines(True), '''\
 
562
    trg nqqrq jura lbh nqq n svyr va gur qverpgbel.
 
563
 
 
564
    --qel-eha jvyy fubj juvpu svyrf jbhyq or nqqrq, ohg abg npghnyyl 
 
565
    nqq gurz.
 
566
    """
 
567
    gnxrf_netf = ['svyr*']
 
568
    gnxrf_bcgvbaf = ['ab-erphefr', 'qel-eha']
 
569
 
 
570
    qrs eha(frys, svyr_yvfg, ab_erphefr=Snyfr, qel_eha=Snyfr):
 
571
        vzcbeg omeyvo.nqq
 
572
 
 
573
        vs qel_eha:
 
574
            vs vf_dhvrg():
 
575
                # Guvf vf cbvagyrff, ohg V'q engure abg envfr na reebe
 
576
                npgvba = omeyvo.nqq.nqq_npgvba_ahyy
 
577
            ryfr:
 
578
  npgvba = omeyvo.nqq.nqq_npgvba_cevag
 
579
        ryvs vf_dhvrg():
 
580
            npgvba = omeyvo.nqq.nqq_npgvba_nqq
 
581
        ryfr:
 
582
       npgvba = omeyvo.nqq.nqq_npgvba_nqq_naq_cevag
 
583
 
 
584
        omeyvo.nqq.fzneg_nqq(svyr_yvfg, abg ab_erphefr, npgvba)
 
585
 
 
586
 
 
587
pynff pzq_zxqve(Pbzznaq):
 
588
'''.splitlines(True)
 
589
, [(0,0,1), (1, 4, 2), (9, 19, 1), (12, 23, 3)])
 
590
 
 
591
    def test_patience_unified_diff(self):
 
592
        txt_a = ['hello there\n',
 
593
                 'world\n',
 
594
                 'how are you today?\n']
 
595
        txt_b = ['hello there\n',
 
596
                 'how are you today?\n']
 
597
        unified_diff = bzrlib.patiencediff.unified_diff
 
598
        psm = bzrlib.patiencediff.PatienceSequenceMatcher
 
599
        self.assertEquals([ '---  \n',
 
600
                           '+++  \n',
 
601
                           '@@ -1,3 +1,2 @@\n',
 
602
                           ' hello there\n',
 
603
                           '-world\n',
 
604
                           ' how are you today?\n'
 
605
                          ]
 
606
                          , list(unified_diff(txt_a, txt_b,
 
607
                                 sequencematcher=psm)))
 
608
        txt_a = map(lambda x: x+'\n', 'abcdefghijklmnop')
 
609
        txt_b = map(lambda x: x+'\n', 'abcdefxydefghijklmnop')
 
610
        # This is the result with LongestCommonSubstring matching
 
611
        self.assertEquals(['---  \n',
 
612
                           '+++  \n',
 
613
                           '@@ -1,6 +1,11 @@\n',
 
614
                           ' a\n',
 
615
                           ' b\n',
 
616
                           ' c\n',
 
617
                           '+d\n',
 
618
                           '+e\n',
 
619
                           '+f\n',
 
620
                           '+x\n',
 
621
                           '+y\n',
 
622
                           ' d\n',
 
623
                           ' e\n',
 
624
                           ' f\n']
 
625
                          , list(unified_diff(txt_a, txt_b)))
 
626
        # And the patience diff
 
627
        self.assertEquals(['---  \n',
 
628
                           '+++  \n',
 
629
                           '@@ -4,6 +4,11 @@\n',
 
630
                           ' d\n',
 
631
                           ' e\n',
 
632
                           ' f\n',
 
633
                           '+x\n',
 
634
                           '+y\n',
 
635
                           '+d\n',
 
636
                           '+e\n',
 
637
                           '+f\n',
 
638
                           ' g\n',
 
639
                           ' h\n',
 
640
                           ' i\n',
 
641
                          ]
 
642
                          , list(unified_diff(txt_a, txt_b,
 
643
                                 sequencematcher=psm)))
 
644
 
 
645
 
 
646
class TestPatienceDiffLibFiles(TestCaseInTempDir):
 
647
 
 
648
    def test_patience_unified_diff_files(self):
 
649
        txt_a = ['hello there\n',
 
650
                 'world\n',
 
651
                 'how are you today?\n']
 
652
        txt_b = ['hello there\n',
 
653
                 'how are you today?\n']
 
654
        open('a1', 'wb').writelines(txt_a)
 
655
        open('b1', 'wb').writelines(txt_b)
 
656
 
 
657
        unified_diff_files = bzrlib.patiencediff.unified_diff_files
 
658
        psm = bzrlib.patiencediff.PatienceSequenceMatcher
 
659
        self.assertEquals(['--- a1 \n',
 
660
                           '+++ b1 \n',
 
661
                           '@@ -1,3 +1,2 @@\n',
 
662
                           ' hello there\n',
 
663
                           '-world\n',
 
664
                           ' how are you today?\n',
 
665
                          ]
 
666
                          , list(unified_diff_files('a1', 'b1',
 
667
                                 sequencematcher=psm)))
 
668
 
 
669
        txt_a = map(lambda x: x+'\n', 'abcdefghijklmnop')
 
670
        txt_b = map(lambda x: x+'\n', 'abcdefxydefghijklmnop')
 
671
        open('a2', 'wb').writelines(txt_a)
 
672
        open('b2', 'wb').writelines(txt_b)
 
673
 
 
674
        # This is the result with LongestCommonSubstring matching
 
675
        self.assertEquals(['--- a2 \n',
 
676
                           '+++ b2 \n',
 
677
                           '@@ -1,6 +1,11 @@\n',
 
678
                           ' a\n',
 
679
                           ' b\n',
 
680
                           ' c\n',
 
681
                           '+d\n',
 
682
                           '+e\n',
 
683
                           '+f\n',
 
684
                           '+x\n',
 
685
                           '+y\n',
 
686
                           ' d\n',
 
687
                           ' e\n',
 
688
                           ' f\n']
 
689
                          , list(unified_diff_files('a2', 'b2')))
 
690
 
 
691
        # And the patience diff
 
692
        self.assertEquals(['--- a2 \n',
 
693
                           '+++ b2 \n',
 
694
                           '@@ -4,6 +4,11 @@\n',
 
695
                           ' d\n',
 
696
                           ' e\n',
 
697
                           ' f\n',
 
698
                           '+x\n',
 
699
                           '+y\n',
 
700
                           '+d\n',
 
701
                           '+e\n',
 
702
                           '+f\n',
 
703
                           ' g\n',
 
704
                           ' h\n',
 
705
                           ' i\n',
 
706
                          ]
 
707
                          , list(unified_diff_files('a2', 'b2',
 
708
                                 sequencematcher=psm)))