Merge lp:~thumper/launchpad/diff-tales into lp:launchpad
- diff-tales
- Merge into devel
Status: | Merged | ||||||||
---|---|---|---|---|---|---|---|---|---|
Approved by: | Tim Penhey | ||||||||
Approved revision: | no longer in the source branch. | ||||||||
Merged at revision: | 11007 | ||||||||
Proposed branch: | lp:~thumper/launchpad/diff-tales | ||||||||
Merge into: | lp:launchpad | ||||||||
Diff against target: |
504 lines (+241/-207) 3 files modified
lib/canonical/launchpad/webapp/tests/test_tales.py (+0/-205) lib/lp/app/browser/stringformatter.py (+10/-2) lib/lp/app/browser/tests/test_stringformatter.py (+231/-0) |
||||||||
To merge this branch: | bzr merge lp:~thumper/launchpad/diff-tales | ||||||||
Related bugs: |
|
Reviewer | Review Type | Date Requested | Status |
---|---|---|---|
Stuart Bishop (community) | Approve | ||
Review via email: mp+27469@code.launchpad.net |
Commit message
Fix the incorrect identification of the removal of lines starting with -- or the addition of lines starting with ++ in the string diff formatter.
Description of the change
This branch fixes two very related bugs:
bug #553642: Incorrect display of diff lines starting with +++
bug #561162: diff colorization fails with deleted SQL comments
This was due to the simplistic nature of the parser. I've added a very small amount of smarts to the parser so it only looks for the file headers directly after a filename.
I also moved the stringformatter TALES tests into lp.app.
This does however obscure the actual test for this:
TestDiffForma
Preview Diff
1 | === modified file 'lib/canonical/launchpad/webapp/tests/test_tales.py' | |||
2 | --- lib/canonical/launchpad/webapp/tests/test_tales.py 2010-05-21 02:33:14 +0000 | |||
3 | +++ lib/canonical/launchpad/webapp/tests/test_tales.py 2010-06-14 04:41:26 +0000 | |||
4 | @@ -3,19 +3,10 @@ | |||
5 | 3 | 3 | ||
6 | 4 | """tales.py doctests.""" | 4 | """tales.py doctests.""" |
7 | 5 | 5 | ||
8 | 6 | from textwrap import dedent | ||
9 | 7 | import unittest | 6 | import unittest |
10 | 8 | 7 | ||
11 | 9 | from zope.component import getUtility | ||
12 | 10 | from zope.testing.doctestunit import DocTestSuite | 8 | from zope.testing.doctestunit import DocTestSuite |
13 | 11 | 9 | ||
14 | 12 | from canonical.config import config | ||
15 | 13 | from canonical.launchpad.testing.pages import find_tags_by_class | ||
16 | 14 | from canonical.launchpad.webapp.interfaces import ILaunchBag | ||
17 | 15 | from canonical.testing import DatabaseFunctionalLayer | ||
18 | 16 | from lp.app.browser.stringformatter import FormattersAPI | ||
19 | 17 | from lp.testing import TestCase | ||
20 | 18 | |||
21 | 19 | 10 | ||
22 | 20 | def test_requestapi(): | 11 | def test_requestapi(): |
23 | 21 | """ | 12 | """ |
24 | @@ -106,202 +97,6 @@ | |||
25 | 106 | 97 | ||
26 | 107 | """ | 98 | """ |
27 | 108 | 99 | ||
28 | 109 | def test_split_paragraphs(): | ||
29 | 110 | r""" | ||
30 | 111 | The split_paragraphs() method is used to split a block of text | ||
31 | 112 | into paragraphs, which are separated by one or more blank lines. | ||
32 | 113 | Paragraphs are yielded as a list of lines in the paragraph. | ||
33 | 114 | |||
34 | 115 | >>> from lp.app.browser.stringformatter import split_paragraphs | ||
35 | 116 | >>> for paragraph in split_paragraphs('\na\nb\n\nc\nd\n\n\n'): | ||
36 | 117 | ... print paragraph | ||
37 | 118 | ['a', 'b'] | ||
38 | 119 | ['c', 'd'] | ||
39 | 120 | """ | ||
40 | 121 | |||
41 | 122 | def test_re_substitute(): | ||
42 | 123 | """ | ||
43 | 124 | When formatting text, we want to replace portions with links. | ||
44 | 125 | re.sub() works fairly well for this, but doesn't give us much | ||
45 | 126 | control over the non-matched text. The re_substitute() function | ||
46 | 127 | lets us do that. | ||
47 | 128 | |||
48 | 129 | >>> import re | ||
49 | 130 | >>> from lp.app.browser.stringformatter import re_substitute | ||
50 | 131 | |||
51 | 132 | >>> def match_func(match): | ||
52 | 133 | ... return '[%s]' % match.group() | ||
53 | 134 | >>> def nomatch_func(text): | ||
54 | 135 | ... return '{%s}' % text | ||
55 | 136 | |||
56 | 137 | >>> pat = re.compile('a{2,6}') | ||
57 | 138 | >>> print re_substitute(pat, match_func, nomatch_func, | ||
58 | 139 | ... 'bbaaaabbbbaaaaaaa aaaaaaaab') | ||
59 | 140 | {bb}[aaaa]{bbbb}[aaaaaa]{a }[aaaaaa][aa]{b} | ||
60 | 141 | """ | ||
61 | 142 | |||
62 | 143 | def test_add_word_breaks(): | ||
63 | 144 | """ | ||
64 | 145 | Long words can cause page layout problems, so we insert manual | ||
65 | 146 | word breaks into long words. Breaks are added at least once every | ||
66 | 147 | 15 characters, but will break on as little as 7 characters if | ||
67 | 148 | there is a suitable non-alphanumeric character to break after. | ||
68 | 149 | |||
69 | 150 | >>> from lp.app.browser.stringformatter import add_word_breaks | ||
70 | 151 | |||
71 | 152 | >>> print add_word_breaks('abcdefghijklmnop') | ||
72 | 153 | abcdefghijklmno<wbr></wbr>p | ||
73 | 154 | |||
74 | 155 | >>> print add_word_breaks('abcdef/ghijklmnop') | ||
75 | 156 | abcdef/<wbr></wbr>ghijklmnop | ||
76 | 157 | |||
77 | 158 | >>> print add_word_breaks('ab/cdefghijklmnop') | ||
78 | 159 | ab/cdefghijklmn<wbr></wbr>op | ||
79 | 160 | |||
80 | 161 | The string can contain HTML entities, which do not get split: | ||
81 | 162 | |||
82 | 163 | >>> print add_word_breaks('abcdef&anentity;hijklmnop') | ||
83 | 164 | abcdef&anentity;<wbr></wbr>hijklmnop | ||
84 | 165 | """ | ||
85 | 166 | |||
86 | 167 | def test_break_long_words(): | ||
87 | 168 | """ | ||
88 | 169 | If we have a long HTML string, break_long_words() can be used to | ||
89 | 170 | add word breaks to the long words. It will not add breaks inside HTML | ||
90 | 171 | tags. Only words longer than 20 characters will have breaks added. | ||
91 | 172 | |||
92 | 173 | >>> from lp.app.browser.stringformatter import break_long_words | ||
93 | 174 | |||
94 | 175 | >>> print break_long_words('1234567890123456') | ||
95 | 176 | 1234567890123456 | ||
96 | 177 | |||
97 | 178 | >>> print break_long_words('12345678901234567890') | ||
98 | 179 | 123456789012345<wbr></wbr>67890 | ||
99 | 180 | |||
100 | 181 | >>> print break_long_words('<tag a12345678901234567890="foo"></tag>') | ||
101 | 182 | <tag a12345678901234567890="foo"></tag> | ||
102 | 183 | |||
103 | 184 | >>> print break_long_words('12345678901234567890 1234567890.1234567890') | ||
104 | 185 | 123456789012345<wbr></wbr>67890 1234567890.<wbr></wbr>1234567890 | ||
105 | 186 | |||
106 | 187 | >>> print break_long_words('1234567890&abcdefghi;123') | ||
107 | 188 | 1234567890&abcdefghi;123 | ||
108 | 189 | |||
109 | 190 | >>> print break_long_words('<tag>1234567890123456</tag>') | ||
110 | 191 | <tag>1234567890123456</tag> | ||
111 | 192 | """ | ||
112 | 193 | |||
113 | 194 | |||
114 | 195 | class TestDiffFormatter(TestCase): | ||
115 | 196 | """Test the string formtter fmt:diff.""" | ||
116 | 197 | layer = DatabaseFunctionalLayer | ||
117 | 198 | |||
118 | 199 | def test_emptyString(self): | ||
119 | 200 | # An empty string gives an empty string. | ||
120 | 201 | self.assertEqual( | ||
121 | 202 | '', FormattersAPI('').format_diff()) | ||
122 | 203 | |||
123 | 204 | def test_almostEmptyString(self): | ||
124 | 205 | # White space doesn't count as empty, and is formtted. | ||
125 | 206 | self.assertEqual( | ||
126 | 207 | '<table class="diff"><tr><td class="line-no">1</td>' | ||
127 | 208 | '<td class="text"> </td></tr></table>', | ||
128 | 209 | FormattersAPI(' ').format_diff()) | ||
129 | 210 | |||
130 | 211 | def test_format_unicode(self): | ||
131 | 212 | # Sometimes the strings contain unicode, those should work too. | ||
132 | 213 | self.assertEqual( | ||
133 | 214 | u'<table class="diff"><tr><td class="line-no">1</td>' | ||
134 | 215 | u'<td class="text">Unicode \u1010</td></tr></table>', | ||
135 | 216 | FormattersAPI(u'Unicode \u1010').format_diff()) | ||
136 | 217 | |||
137 | 218 | def test_cssClasses(self): | ||
138 | 219 | # Different parts of the diff have different css classes. | ||
139 | 220 | diff = dedent('''\ | ||
140 | 221 | === modified file 'tales.py' | ||
141 | 222 | --- tales.py | ||
142 | 223 | +++ tales.py | ||
143 | 224 | @@ -2435,6 +2435,8 @@ | ||
144 | 225 | def format_diff(self): | ||
145 | 226 | - removed this line | ||
146 | 227 | + added this line | ||
147 | 228 | ######## | ||
148 | 229 | # A merge directive comment. | ||
149 | 230 | ''') | ||
150 | 231 | html = FormattersAPI(diff).format_diff() | ||
151 | 232 | line_numbers = find_tags_by_class(html, 'line-no') | ||
152 | 233 | self.assertEqual( | ||
153 | 234 | ['1','2','3','4','5','6','7','8','9'], | ||
154 | 235 | [tag.renderContents() for tag in line_numbers]) | ||
155 | 236 | text = find_tags_by_class(html, 'text') | ||
156 | 237 | self.assertEqual( | ||
157 | 238 | ['diff-file text', | ||
158 | 239 | 'diff-header text', | ||
159 | 240 | 'diff-header text', | ||
160 | 241 | 'diff-chunk text', | ||
161 | 242 | 'text', | ||
162 | 243 | 'diff-removed text', | ||
163 | 244 | 'diff-added text', | ||
164 | 245 | 'diff-comment text', | ||
165 | 246 | 'diff-comment text'], | ||
166 | 247 | [str(tag['class']) for tag in text]) | ||
167 | 248 | |||
168 | 249 | def test_config_value_limits_line_count(self): | ||
169 | 250 | # The config.diff.max_line_format contains the maximum number of lines | ||
170 | 251 | # to format. | ||
171 | 252 | diff = dedent('''\ | ||
172 | 253 | === modified file 'tales.py' | ||
173 | 254 | --- tales.py | ||
174 | 255 | +++ tales.py | ||
175 | 256 | @@ -2435,6 +2435,8 @@ | ||
176 | 257 | def format_diff(self): | ||
177 | 258 | - removed this line | ||
178 | 259 | + added this line | ||
179 | 260 | ######## | ||
180 | 261 | # A merge directive comment. | ||
181 | 262 | ''') | ||
182 | 263 | self.pushConfig("diff", max_format_lines=3) | ||
183 | 264 | html = FormattersAPI(diff).format_diff() | ||
184 | 265 | line_count = html.count('<td class="line-no">') | ||
185 | 266 | self.assertEqual(3, line_count) | ||
186 | 267 | |||
187 | 268 | |||
188 | 269 | class TestOOPSFormatter(TestCase): | ||
189 | 270 | """A test case for the oops_id() string formatter.""" | ||
190 | 271 | |||
191 | 272 | layer = DatabaseFunctionalLayer | ||
192 | 273 | |||
193 | 274 | def test_doesnt_linkify_for_non_developers(self): | ||
194 | 275 | # OOPS IDs won't be linkified for non-developers. | ||
195 | 276 | oops_id = 'OOPS-12345TEST' | ||
196 | 277 | formatter = FormattersAPI(oops_id) | ||
197 | 278 | formatted_string = formatter.oops_id() | ||
198 | 279 | |||
199 | 280 | self.assertEqual( | ||
200 | 281 | oops_id, formatted_string, | ||
201 | 282 | "Formatted string should be '%s', was '%s'" % ( | ||
202 | 283 | oops_id, formatted_string)) | ||
203 | 284 | |||
204 | 285 | def _setDeveloper(self): | ||
205 | 286 | """Override ILaunchBag.developer for testing purposes.""" | ||
206 | 287 | launch_bag = getUtility(ILaunchBag) | ||
207 | 288 | launch_bag.setDeveloper(True) | ||
208 | 289 | |||
209 | 290 | def test_linkifies_for_developers(self): | ||
210 | 291 | # OOPS IDs will be linkified for Launchpad developers. | ||
211 | 292 | oops_id = 'OOPS-12345TEST' | ||
212 | 293 | formatter = FormattersAPI(oops_id) | ||
213 | 294 | |||
214 | 295 | self._setDeveloper() | ||
215 | 296 | formatted_string = formatter.oops_id() | ||
216 | 297 | |||
217 | 298 | expected_string = '<a href="%s">%s</a>' % ( | ||
218 | 299 | config.launchpad.oops_root_url + oops_id, oops_id) | ||
219 | 300 | |||
220 | 301 | self.assertEqual( | ||
221 | 302 | expected_string, formatted_string, | ||
222 | 303 | "Formatted string should be '%s', was '%s'" % ( | ||
223 | 304 | expected_string, formatted_string)) | ||
224 | 305 | 100 | ||
225 | 306 | 101 | ||
226 | 307 | def test_suite(): | 102 | def test_suite(): |
227 | 308 | 103 | ||
228 | === modified file 'lib/lp/app/browser/stringformatter.py' | |||
229 | --- lib/lp/app/browser/stringformatter.py 2010-05-21 02:33:14 +0000 | |||
230 | +++ lib/lp/app/browser/stringformatter.py 2010-06-14 04:41:26 +0000 | |||
231 | @@ -722,27 +722,35 @@ | |||
232 | 722 | result = ['<table class="diff">'] | 722 | result = ['<table class="diff">'] |
233 | 723 | 723 | ||
234 | 724 | max_format_lines = config.diff.max_format_lines | 724 | max_format_lines = config.diff.max_format_lines |
235 | 725 | header_next = False | ||
236 | 725 | for row, line in enumerate(text.splitlines()[:max_format_lines]): | 726 | for row, line in enumerate(text.splitlines()[:max_format_lines]): |
237 | 726 | result.append('<tr>') | 727 | result.append('<tr>') |
238 | 727 | result.append('<td class="line-no">%s</td>' % (row+1)) | 728 | result.append('<td class="line-no">%s</td>' % (row+1)) |
239 | 728 | if line.startswith('==='): | 729 | if line.startswith('==='): |
240 | 729 | css_class = 'diff-file text' | 730 | css_class = 'diff-file text' |
243 | 730 | elif (line.startswith('+++') or | 731 | header_next = True |
244 | 731 | line.startswith('---')): | 732 | elif (header_next and |
245 | 733 | (line.startswith('+++') or | ||
246 | 734 | line.startswith('---'))): | ||
247 | 732 | css_class = 'diff-header text' | 735 | css_class = 'diff-header text' |
248 | 733 | elif line.startswith('@@'): | 736 | elif line.startswith('@@'): |
249 | 734 | css_class = 'diff-chunk text' | 737 | css_class = 'diff-chunk text' |
250 | 738 | header_next = False | ||
251 | 735 | elif line.startswith('+'): | 739 | elif line.startswith('+'): |
252 | 736 | css_class = 'diff-added text' | 740 | css_class = 'diff-added text' |
253 | 741 | header_next = False | ||
254 | 737 | elif line.startswith('-'): | 742 | elif line.startswith('-'): |
255 | 738 | css_class = 'diff-removed text' | 743 | css_class = 'diff-removed text' |
256 | 744 | header_next = False | ||
257 | 739 | elif line.startswith('#'): | 745 | elif line.startswith('#'): |
258 | 740 | # This doesn't occur in normal unified diffs, but does | 746 | # This doesn't occur in normal unified diffs, but does |
259 | 741 | # appear in merge directives, which use text/x-diff or | 747 | # appear in merge directives, which use text/x-diff or |
260 | 742 | # text/x-patch. | 748 | # text/x-patch. |
261 | 743 | css_class = 'diff-comment text' | 749 | css_class = 'diff-comment text' |
262 | 750 | header_next = False | ||
263 | 744 | else: | 751 | else: |
264 | 745 | css_class = 'text' | 752 | css_class = 'text' |
265 | 753 | header_next = False | ||
266 | 746 | result.append( | 754 | result.append( |
267 | 747 | '<td class="%s">%s</td>' % (css_class, escape(line))) | 755 | '<td class="%s">%s</td>' % (css_class, escape(line))) |
268 | 748 | result.append('</tr>') | 756 | result.append('</tr>') |
269 | 749 | 757 | ||
270 | === added file 'lib/lp/app/browser/tests/test_stringformatter.py' | |||
271 | --- lib/lp/app/browser/tests/test_stringformatter.py 1970-01-01 00:00:00 +0000 | |||
272 | +++ lib/lp/app/browser/tests/test_stringformatter.py 2010-06-14 04:41:26 +0000 | |||
273 | @@ -0,0 +1,231 @@ | |||
274 | 1 | # Copyright 2010 Canonical Ltd. This software is licensed under the | ||
275 | 2 | # GNU Affero General Public License version 3 (see the file LICENSE). | ||
276 | 3 | |||
277 | 4 | """Unit tests for the string TALES formatter.""" | ||
278 | 5 | |||
279 | 6 | __metaclass__ = type | ||
280 | 7 | |||
281 | 8 | from textwrap import dedent | ||
282 | 9 | import unittest | ||
283 | 10 | |||
284 | 11 | from zope.component import getUtility | ||
285 | 12 | from zope.testing.doctestunit import DocTestSuite | ||
286 | 13 | |||
287 | 14 | from canonical.config import config | ||
288 | 15 | from canonical.launchpad.testing.pages import find_tags_by_class | ||
289 | 16 | from canonical.launchpad.webapp.interfaces import ILaunchBag | ||
290 | 17 | from canonical.testing import DatabaseFunctionalLayer | ||
291 | 18 | from lp.app.browser.stringformatter import FormattersAPI | ||
292 | 19 | from lp.testing import TestCase | ||
293 | 20 | |||
294 | 21 | |||
295 | 22 | def test_split_paragraphs(): | ||
296 | 23 | r""" | ||
297 | 24 | The split_paragraphs() method is used to split a block of text | ||
298 | 25 | into paragraphs, which are separated by one or more blank lines. | ||
299 | 26 | Paragraphs are yielded as a list of lines in the paragraph. | ||
300 | 27 | |||
301 | 28 | >>> from lp.app.browser.stringformatter import split_paragraphs | ||
302 | 29 | >>> for paragraph in split_paragraphs('\na\nb\n\nc\nd\n\n\n'): | ||
303 | 30 | ... print paragraph | ||
304 | 31 | ['a', 'b'] | ||
305 | 32 | ['c', 'd'] | ||
306 | 33 | """ | ||
307 | 34 | |||
308 | 35 | |||
309 | 36 | def test_re_substitute(): | ||
310 | 37 | """ | ||
311 | 38 | When formatting text, we want to replace portions with links. | ||
312 | 39 | re.sub() works fairly well for this, but doesn't give us much | ||
313 | 40 | control over the non-matched text. The re_substitute() function | ||
314 | 41 | lets us do that. | ||
315 | 42 | |||
316 | 43 | >>> import re | ||
317 | 44 | >>> from lp.app.browser.stringformatter import re_substitute | ||
318 | 45 | |||
319 | 46 | >>> def match_func(match): | ||
320 | 47 | ... return '[%s]' % match.group() | ||
321 | 48 | >>> def nomatch_func(text): | ||
322 | 49 | ... return '{%s}' % text | ||
323 | 50 | |||
324 | 51 | >>> pat = re.compile('a{2,6}') | ||
325 | 52 | >>> print re_substitute(pat, match_func, nomatch_func, | ||
326 | 53 | ... 'bbaaaabbbbaaaaaaa aaaaaaaab') | ||
327 | 54 | {bb}[aaaa]{bbbb}[aaaaaa]{a }[aaaaaa][aa]{b} | ||
328 | 55 | """ | ||
329 | 56 | |||
330 | 57 | |||
331 | 58 | def test_add_word_breaks(): | ||
332 | 59 | """ | ||
333 | 60 | Long words can cause page layout problems, so we insert manual | ||
334 | 61 | word breaks into long words. Breaks are added at least once every | ||
335 | 62 | 15 characters, but will break on as little as 7 characters if | ||
336 | 63 | there is a suitable non-alphanumeric character to break after. | ||
337 | 64 | |||
338 | 65 | >>> from lp.app.browser.stringformatter import add_word_breaks | ||
339 | 66 | |||
340 | 67 | >>> print add_word_breaks('abcdefghijklmnop') | ||
341 | 68 | abcdefghijklmno<wbr></wbr>p | ||
342 | 69 | |||
343 | 70 | >>> print add_word_breaks('abcdef/ghijklmnop') | ||
344 | 71 | abcdef/<wbr></wbr>ghijklmnop | ||
345 | 72 | |||
346 | 73 | >>> print add_word_breaks('ab/cdefghijklmnop') | ||
347 | 74 | ab/cdefghijklmn<wbr></wbr>op | ||
348 | 75 | |||
349 | 76 | The string can contain HTML entities, which do not get split: | ||
350 | 77 | |||
351 | 78 | >>> print add_word_breaks('abcdef&anentity;hijklmnop') | ||
352 | 79 | abcdef&anentity;<wbr></wbr>hijklmnop | ||
353 | 80 | """ | ||
354 | 81 | |||
355 | 82 | |||
356 | 83 | def test_break_long_words(): | ||
357 | 84 | """ | ||
358 | 85 | If we have a long HTML string, break_long_words() can be used to | ||
359 | 86 | add word breaks to the long words. It will not add breaks inside HTML | ||
360 | 87 | tags. Only words longer than 20 characters will have breaks added. | ||
361 | 88 | |||
362 | 89 | >>> from lp.app.browser.stringformatter import break_long_words | ||
363 | 90 | |||
364 | 91 | >>> print break_long_words('1234567890123456') | ||
365 | 92 | 1234567890123456 | ||
366 | 93 | |||
367 | 94 | >>> print break_long_words('12345678901234567890') | ||
368 | 95 | 123456789012345<wbr></wbr>67890 | ||
369 | 96 | |||
370 | 97 | >>> print break_long_words('<tag a12345678901234567890="foo"></tag>') | ||
371 | 98 | <tag a12345678901234567890="foo"></tag> | ||
372 | 99 | |||
373 | 100 | >>> print break_long_words('12345678901234567890 1234567890.1234567890') | ||
374 | 101 | 123456789012345<wbr></wbr>67890 1234567890.<wbr></wbr>1234567890 | ||
375 | 102 | |||
376 | 103 | >>> print break_long_words('1234567890&abcdefghi;123') | ||
377 | 104 | 1234567890&abcdefghi;123 | ||
378 | 105 | |||
379 | 106 | >>> print break_long_words('<tag>1234567890123456</tag>') | ||
380 | 107 | <tag>1234567890123456</tag> | ||
381 | 108 | """ | ||
382 | 109 | |||
383 | 110 | |||
384 | 111 | class TestDiffFormatter(TestCase): | ||
385 | 112 | """Test the string formtter fmt:diff.""" | ||
386 | 113 | layer = DatabaseFunctionalLayer | ||
387 | 114 | |||
388 | 115 | def test_emptyString(self): | ||
389 | 116 | # An empty string gives an empty string. | ||
390 | 117 | self.assertEqual( | ||
391 | 118 | '', FormattersAPI('').format_diff()) | ||
392 | 119 | |||
393 | 120 | def test_almostEmptyString(self): | ||
394 | 121 | # White space doesn't count as empty, and is formtted. | ||
395 | 122 | self.assertEqual( | ||
396 | 123 | '<table class="diff"><tr><td class="line-no">1</td>' | ||
397 | 124 | '<td class="text"> </td></tr></table>', | ||
398 | 125 | FormattersAPI(' ').format_diff()) | ||
399 | 126 | |||
400 | 127 | def test_format_unicode(self): | ||
401 | 128 | # Sometimes the strings contain unicode, those should work too. | ||
402 | 129 | self.assertEqual( | ||
403 | 130 | u'<table class="diff"><tr><td class="line-no">1</td>' | ||
404 | 131 | u'<td class="text">Unicode \u1010</td></tr></table>', | ||
405 | 132 | FormattersAPI(u'Unicode \u1010').format_diff()) | ||
406 | 133 | |||
407 | 134 | def test_cssClasses(self): | ||
408 | 135 | # Different parts of the diff have different css classes. | ||
409 | 136 | diff = dedent('''\ | ||
410 | 137 | === modified file 'tales.py' | ||
411 | 138 | --- tales.py | ||
412 | 139 | +++ tales.py | ||
413 | 140 | @@ -2435,6 +2435,8 @@ | ||
414 | 141 | def format_diff(self): | ||
415 | 142 | - removed this line | ||
416 | 143 | + added this line | ||
417 | 144 | -------- a sql style comment | ||
418 | 145 | ++++++++ a line of pluses | ||
419 | 146 | ######## | ||
420 | 147 | # A merge directive comment. | ||
421 | 148 | ''') | ||
422 | 149 | html = FormattersAPI(diff).format_diff() | ||
423 | 150 | line_numbers = find_tags_by_class(html, 'line-no') | ||
424 | 151 | self.assertEqual( | ||
425 | 152 | ['1','2','3','4','5','6','7','8','9', '10', '11'], | ||
426 | 153 | [tag.renderContents() for tag in line_numbers]) | ||
427 | 154 | text = find_tags_by_class(html, 'text') | ||
428 | 155 | self.assertEqual( | ||
429 | 156 | ['diff-file text', | ||
430 | 157 | 'diff-header text', | ||
431 | 158 | 'diff-header text', | ||
432 | 159 | 'diff-chunk text', | ||
433 | 160 | 'text', | ||
434 | 161 | 'diff-removed text', | ||
435 | 162 | 'diff-added text', | ||
436 | 163 | 'diff-removed text', | ||
437 | 164 | 'diff-added text', | ||
438 | 165 | 'diff-comment text', | ||
439 | 166 | 'diff-comment text'], | ||
440 | 167 | [str(tag['class']) for tag in text]) | ||
441 | 168 | |||
442 | 169 | def test_config_value_limits_line_count(self): | ||
443 | 170 | # The config.diff.max_line_format contains the maximum number of lines | ||
444 | 171 | # to format. | ||
445 | 172 | diff = dedent('''\ | ||
446 | 173 | === modified file 'tales.py' | ||
447 | 174 | --- tales.py | ||
448 | 175 | +++ tales.py | ||
449 | 176 | @@ -2435,6 +2435,8 @@ | ||
450 | 177 | def format_diff(self): | ||
451 | 178 | - removed this line | ||
452 | 179 | + added this line | ||
453 | 180 | ######## | ||
454 | 181 | # A merge directive comment. | ||
455 | 182 | ''') | ||
456 | 183 | self.pushConfig("diff", max_format_lines=3) | ||
457 | 184 | html = FormattersAPI(diff).format_diff() | ||
458 | 185 | line_count = html.count('<td class="line-no">') | ||
459 | 186 | self.assertEqual(3, line_count) | ||
460 | 187 | |||
461 | 188 | |||
462 | 189 | class TestOOPSFormatter(TestCase): | ||
463 | 190 | """A test case for the oops_id() string formatter.""" | ||
464 | 191 | |||
465 | 192 | layer = DatabaseFunctionalLayer | ||
466 | 193 | |||
467 | 194 | def test_doesnt_linkify_for_non_developers(self): | ||
468 | 195 | # OOPS IDs won't be linkified for non-developers. | ||
469 | 196 | oops_id = 'OOPS-12345TEST' | ||
470 | 197 | formatter = FormattersAPI(oops_id) | ||
471 | 198 | formatted_string = formatter.oops_id() | ||
472 | 199 | |||
473 | 200 | self.assertEqual( | ||
474 | 201 | oops_id, formatted_string, | ||
475 | 202 | "Formatted string should be '%s', was '%s'" % ( | ||
476 | 203 | oops_id, formatted_string)) | ||
477 | 204 | |||
478 | 205 | def _setDeveloper(self): | ||
479 | 206 | """Override ILaunchBag.developer for testing purposes.""" | ||
480 | 207 | launch_bag = getUtility(ILaunchBag) | ||
481 | 208 | launch_bag.setDeveloper(True) | ||
482 | 209 | |||
483 | 210 | def test_linkifies_for_developers(self): | ||
484 | 211 | # OOPS IDs will be linkified for Launchpad developers. | ||
485 | 212 | oops_id = 'OOPS-12345TEST' | ||
486 | 213 | formatter = FormattersAPI(oops_id) | ||
487 | 214 | |||
488 | 215 | self._setDeveloper() | ||
489 | 216 | formatted_string = formatter.oops_id() | ||
490 | 217 | |||
491 | 218 | expected_string = '<a href="%s">%s</a>' % ( | ||
492 | 219 | config.launchpad.oops_root_url + oops_id, oops_id) | ||
493 | 220 | |||
494 | 221 | self.assertEqual( | ||
495 | 222 | expected_string, formatted_string, | ||
496 | 223 | "Formatted string should be '%s', was '%s'" % ( | ||
497 | 224 | expected_string, formatted_string)) | ||
498 | 225 | |||
499 | 226 | |||
500 | 227 | def test_suite(): | ||
501 | 228 | suite = unittest.TestSuite() | ||
502 | 229 | suite.addTests(DocTestSuite()) | ||
503 | 230 | suite.addTests(unittest.TestLoader().loadTestsFromName(__name__)) | ||
504 | 231 | return suite |
All the new code looks good.
I find it worrying we are munging HTML rather than munging strings before we generate the HTML (break_long_words), and that the method name doesn't indicate it accepts HTML input rather than plain text. But this is old code.