• Home
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download
1import difflib
2from test.test_support import run_unittest, findfile
3import unittest
4import doctest
5import sys
6
7
8class TestWithAscii(unittest.TestCase):
9    def test_one_insert(self):
10        sm = difflib.SequenceMatcher(None, 'b' * 100, 'a' + 'b' * 100)
11        self.assertAlmostEqual(sm.ratio(), 0.995, places=3)
12        self.assertEqual(list(sm.get_opcodes()),
13            [   ('insert', 0, 0, 0, 1),
14                ('equal', 0, 100, 1, 101)])
15        sm = difflib.SequenceMatcher(None, 'b' * 100, 'b' * 50 + 'a' + 'b' * 50)
16        self.assertAlmostEqual(sm.ratio(), 0.995, places=3)
17        self.assertEqual(list(sm.get_opcodes()),
18            [   ('equal', 0, 50, 0, 50),
19                ('insert', 50, 50, 50, 51),
20                ('equal', 50, 100, 51, 101)])
21
22    def test_one_delete(self):
23        sm = difflib.SequenceMatcher(None, 'a' * 40 + 'c' + 'b' * 40, 'a' * 40 + 'b' * 40)
24        self.assertAlmostEqual(sm.ratio(), 0.994, places=3)
25        self.assertEqual(list(sm.get_opcodes()),
26            [   ('equal', 0, 40, 0, 40),
27                ('delete', 40, 41, 40, 40),
28                ('equal', 41, 81, 40, 80)])
29
30
31class TestAutojunk(unittest.TestCase):
32    """Tests for the autojunk parameter added in 2.7"""
33    def test_one_insert_homogenous_sequence(self):
34        # By default autojunk=True and the heuristic kicks in for a sequence
35        # of length 200+
36        seq1 = 'b' * 200
37        seq2 = 'a' + 'b' * 200
38
39        sm = difflib.SequenceMatcher(None, seq1, seq2)
40        self.assertAlmostEqual(sm.ratio(), 0, places=3)
41
42        # Now turn the heuristic off
43        sm = difflib.SequenceMatcher(None, seq1, seq2, autojunk=False)
44        self.assertAlmostEqual(sm.ratio(), 0.9975, places=3)
45
46
47class TestSFbugs(unittest.TestCase):
48    def test_ratio_for_null_seqn(self):
49        # Check clearing of SF bug 763023
50        s = difflib.SequenceMatcher(None, [], [])
51        self.assertEqual(s.ratio(), 1)
52        self.assertEqual(s.quick_ratio(), 1)
53        self.assertEqual(s.real_quick_ratio(), 1)
54
55    def test_comparing_empty_lists(self):
56        # Check fix for bug #979794
57        group_gen = difflib.SequenceMatcher(None, [], []).get_grouped_opcodes()
58        self.assertRaises(StopIteration, group_gen.next)
59        diff_gen = difflib.unified_diff([], [])
60        self.assertRaises(StopIteration, diff_gen.next)
61
62    def test_matching_blocks_cache(self):
63        # Issue #21635
64        s = difflib.SequenceMatcher(None, "abxcd", "abcd")
65        first = s.get_matching_blocks()
66        second = s.get_matching_blocks()
67        self.assertEqual(second[0].size, 2)
68        self.assertEqual(second[1].size, 2)
69        self.assertEqual(second[2].size, 0)
70
71    def test_added_tab_hint(self):
72        # Check fix for bug #1488943
73        diff = list(difflib.Differ().compare(["\tI am a buggy"],["\t\tI am a bug"]))
74        self.assertEqual("- \tI am a buggy", diff[0])
75        self.assertEqual("?            --\n", diff[1])
76        self.assertEqual("+ \t\tI am a bug", diff[2])
77        self.assertEqual("? +\n", diff[3])
78
79patch914575_from1 = """
80   1. Beautiful is beTTer than ugly.
81   2. Explicit is better than implicit.
82   3. Simple is better than complex.
83   4. Complex is better than complicated.
84"""
85
86patch914575_to1 = """
87   1. Beautiful is better than ugly.
88   3.   Simple is better than complex.
89   4. Complicated is better than complex.
90   5. Flat is better than nested.
91"""
92
93patch914575_from2 = """
94\t\tLine 1: preceeded by from:[tt] to:[ssss]
95  \t\tLine 2: preceeded by from:[sstt] to:[sssst]
96  \t \tLine 3: preceeded by from:[sstst] to:[ssssss]
97Line 4:  \thas from:[sst] to:[sss] after :
98Line 5: has from:[t] to:[ss] at end\t
99"""
100
101patch914575_to2 = """
102    Line 1: preceeded by from:[tt] to:[ssss]
103    \tLine 2: preceeded by from:[sstt] to:[sssst]
104      Line 3: preceeded by from:[sstst] to:[ssssss]
105Line 4:   has from:[sst] to:[sss] after :
106Line 5: has from:[t] to:[ss] at end
107"""
108
109patch914575_from3 = """line 0
1101234567890123456789012345689012345
111line 1
112line 2
113line 3
114line 4   changed
115line 5   changed
116line 6   changed
117line 7
118line 8  subtracted
119line 9
1201234567890123456789012345689012345
121short line
122just fits in!!
123just fits in two lines yup!!
124the end"""
125
126patch914575_to3 = """line 0
1271234567890123456789012345689012345
128line 1
129line 2    added
130line 3
131line 4   chanGEd
132line 5a  chanGed
133line 6a  changEd
134line 7
135line 8
136line 9
1371234567890
138another long line that needs to be wrapped
139just fitS in!!
140just fits in two lineS yup!!
141the end"""
142
143class TestSFpatches(unittest.TestCase):
144
145    def test_html_diff(self):
146        # Check SF patch 914575 for generating HTML differences
147        f1a = ((patch914575_from1 + '123\n'*10)*3)
148        t1a = (patch914575_to1 + '123\n'*10)*3
149        f1b = '456\n'*10 + f1a
150        t1b = '456\n'*10 + t1a
151        f1a = f1a.splitlines()
152        t1a = t1a.splitlines()
153        f1b = f1b.splitlines()
154        t1b = t1b.splitlines()
155        f2 = patch914575_from2.splitlines()
156        t2 = patch914575_to2.splitlines()
157        f3 = patch914575_from3
158        t3 = patch914575_to3
159        i = difflib.HtmlDiff()
160        j = difflib.HtmlDiff(tabsize=2)
161        k = difflib.HtmlDiff(wrapcolumn=14)
162
163        full = i.make_file(f1a,t1a,'from','to',context=False,numlines=5)
164        tables = '\n'.join(
165            [
166             '<h2>Context (first diff within numlines=5(default))</h2>',
167             i.make_table(f1a,t1a,'from','to',context=True),
168             '<h2>Context (first diff after numlines=5(default))</h2>',
169             i.make_table(f1b,t1b,'from','to',context=True),
170             '<h2>Context (numlines=6)</h2>',
171             i.make_table(f1a,t1a,'from','to',context=True,numlines=6),
172             '<h2>Context (numlines=0)</h2>',
173             i.make_table(f1a,t1a,'from','to',context=True,numlines=0),
174             '<h2>Same Context</h2>',
175             i.make_table(f1a,f1a,'from','to',context=True),
176             '<h2>Same Full</h2>',
177             i.make_table(f1a,f1a,'from','to',context=False),
178             '<h2>Empty Context</h2>',
179             i.make_table([],[],'from','to',context=True),
180             '<h2>Empty Full</h2>',
181             i.make_table([],[],'from','to',context=False),
182             '<h2>tabsize=2</h2>',
183             j.make_table(f2,t2),
184             '<h2>tabsize=default</h2>',
185             i.make_table(f2,t2),
186             '<h2>Context (wrapcolumn=14,numlines=0)</h2>',
187             k.make_table(f3.splitlines(),t3.splitlines(),context=True,numlines=0),
188             '<h2>wrapcolumn=14,splitlines()</h2>',
189             k.make_table(f3.splitlines(),t3.splitlines()),
190             '<h2>wrapcolumn=14,splitlines(True)</h2>',
191             k.make_table(f3.splitlines(True),t3.splitlines(True)),
192             ])
193        actual = full.replace('</body>','\n%s\n</body>' % tables)
194
195        # temporarily uncomment next two lines to baseline this test
196        #with open('test_difflib_expect.html','w') as fp:
197        #    fp.write(actual)
198
199        with open(findfile('test_difflib_expect.html')) as fp:
200            self.assertEqual(actual, fp.read())
201
202    def test_recursion_limit(self):
203        # Check if the problem described in patch #1413711 exists.
204        limit = sys.getrecursionlimit()
205        old = [(i%2 and "K:%d" or "V:A:%d") % i for i in range(limit*2)]
206        new = [(i%2 and "K:%d" or "V:B:%d") % i for i in range(limit*2)]
207        difflib.SequenceMatcher(None, old, new).get_opcodes()
208
209
210class TestOutputFormat(unittest.TestCase):
211    def test_tab_delimiter(self):
212        args = ['one', 'two', 'Original', 'Current',
213            '2005-01-26 23:30:50', '2010-04-02 10:20:52']
214        ud = difflib.unified_diff(*args, lineterm='')
215        self.assertEqual(list(ud)[0:2], [
216                           "--- Original\t2005-01-26 23:30:50",
217                           "+++ Current\t2010-04-02 10:20:52"])
218        cd = difflib.context_diff(*args, lineterm='')
219        self.assertEqual(list(cd)[0:2], [
220                           "*** Original\t2005-01-26 23:30:50",
221                           "--- Current\t2010-04-02 10:20:52"])
222
223    def test_no_trailing_tab_on_empty_filedate(self):
224        args = ['one', 'two', 'Original', 'Current']
225        ud = difflib.unified_diff(*args, lineterm='')
226        self.assertEqual(list(ud)[0:2], ["--- Original", "+++ Current"])
227
228        cd = difflib.context_diff(*args, lineterm='')
229        self.assertEqual(list(cd)[0:2], ["*** Original", "--- Current"])
230
231    def test_range_format_unified(self):
232        # Per the diff spec at http://www.unix.org/single_unix_specification/
233        spec = '''\
234           Each <range> field shall be of the form:
235             %1d", <beginning line number>  if the range contains exactly one line,
236           and:
237            "%1d,%1d", <beginning line number>, <number of lines> otherwise.
238           If a range is empty, its beginning line number shall be the number of
239           the line just before the range, or 0 if the empty range starts the file.
240        '''
241        fmt = difflib._format_range_unified
242        self.assertEqual(fmt(3,3), '3,0')
243        self.assertEqual(fmt(3,4), '4')
244        self.assertEqual(fmt(3,5), '4,2')
245        self.assertEqual(fmt(3,6), '4,3')
246        self.assertEqual(fmt(0,0), '0,0')
247
248    def test_range_format_context(self):
249        # Per the diff spec at http://www.unix.org/single_unix_specification/
250        spec = '''\
251           The range of lines in file1 shall be written in the following format
252           if the range contains two or more lines:
253               "*** %d,%d ****\n", <beginning line number>, <ending line number>
254           and the following format otherwise:
255               "*** %d ****\n", <ending line number>
256           The ending line number of an empty range shall be the number of the preceding line,
257           or 0 if the range is at the start of the file.
258
259           Next, the range of lines in file2 shall be written in the following format
260           if the range contains two or more lines:
261               "--- %d,%d ----\n", <beginning line number>, <ending line number>
262           and the following format otherwise:
263               "--- %d ----\n", <ending line number>
264        '''
265        fmt = difflib._format_range_context
266        self.assertEqual(fmt(3,3), '3')
267        self.assertEqual(fmt(3,4), '4')
268        self.assertEqual(fmt(3,5), '4,5')
269        self.assertEqual(fmt(3,6), '4,6')
270        self.assertEqual(fmt(0,0), '0')
271
272class TestJunkAPIs(unittest.TestCase):
273    def test_is_line_junk_true(self):
274        for line in ['#', '  ', ' #', '# ', ' # ', '']:
275            self.assertTrue(difflib.IS_LINE_JUNK(line), repr(line))
276
277    def test_is_line_junk_false(self):
278        for line in ['##', ' ##', '## ', 'abc ', 'abc #', 'Mr. Moose is up!']:
279            self.assertFalse(difflib.IS_LINE_JUNK(line), repr(line))
280
281    def test_is_line_junk_REDOS(self):
282        evil_input = ('\t' * 1000000) + '##'
283        self.assertFalse(difflib.IS_LINE_JUNK(evil_input))
284
285    def test_is_character_junk_true(self):
286        for char in [' ', '\t']:
287            self.assertTrue(difflib.IS_CHARACTER_JUNK(char), repr(char))
288
289    def test_is_character_junk_false(self):
290        for char in ['a', '#', '\n', '\f', '\r', '\v']:
291            self.assertFalse(difflib.IS_CHARACTER_JUNK(char), repr(char))
292
293def test_main():
294    difflib.HtmlDiff._default_prefix = 0
295    Doctests = doctest.DocTestSuite(difflib)
296    run_unittest(
297        TestWithAscii, TestAutojunk, TestSFpatches, TestSFbugs,
298        TestOutputFormat, TestJunkAPIs)
299
300if __name__ == '__main__':
301    test_main()
302