~bzr-pqm/bzr/bzr.dev

« back to all changes in this revision

Viewing changes to bzrlib/tests/test_diff.py

  • Committer: Martin Pool
  • Date: 2005-07-29 19:53:21 UTC
  • Revision ID: mbp@sourcefrog.net-20050729195321-6d9eef1a43dd6c81
- notes from discussion on splitting and joining files

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