blob: a77901465c48f69da84ee6b8952a73ca881fd957 [file] [log] [blame]
Copybara854996b2021-09-07 19:36:02 +00001# Copyright 2016 The Chromium Authors. All rights reserved.
2# Use of this source code is governed by a BSD-style
3# license that can be found in the LICENSE file or at
4# https://developers.google.com/open-source/licenses/bsd
5
6"""Unittest for the autolink feature."""
7from __future__ import print_function
8from __future__ import division
9from __future__ import absolute_import
10
11import re
12import unittest
13
14from features import autolink
15from features import autolink_constants
16from framework import template_helpers
17from proto import tracker_pb2
18from testing import fake
19from testing import testing_helpers
20
21
22SIMPLE_EMAIL_RE = re.compile(r'([a-z]+)@([a-z]+)\.com')
23OVER_AMBITIOUS_DOMAIN_RE = re.compile(r'([a-z]+)\.(com|net|org)')
24
25
26class AutolinkTest(unittest.TestCase):
27
28 def RegisterEmailCallbacks(self, aa):
29
30 def LookupUsers(_mr, all_addresses):
31 """Return user objects for only users who are at trusted domains."""
32 return [addr for addr in all_addresses
33 if addr.endswith('@example.com')]
34
35 def Match2Addresses(_mr, match):
36 return [match.group(0)]
37
38 def MakeMailtoLink(_mr, match, comp_ref_artifacts):
39 email = match.group(0)
40 if comp_ref_artifacts and email in comp_ref_artifacts:
41 return [template_helpers.TextRun(
42 tag='a', href='mailto:%s' % email, content=email)]
43 else:
44 return [template_helpers.TextRun('%s AT %s.com' % match.group(1, 2))]
45
46 aa.RegisterComponent('testcomp',
47 LookupUsers,
48 Match2Addresses,
49 {SIMPLE_EMAIL_RE: MakeMailtoLink})
50
51 def RegisterDomainCallbacks(self, aa):
52
53 def LookupDomains(_mr, _all_refs):
54 """Return business objects for only real domains. Always just True."""
55 return True # We don't have domain business objects, accept anything.
56
57 def Match2Domains(_mr, match):
58 return [match.group(0)]
59
60 def MakeHyperLink(_mr, match, _comp_ref_artifacts):
61 domain = match.group(0)
62 return [template_helpers.TextRun(tag='a', href=domain, content=domain)]
63
64 aa.RegisterComponent('testcomp2',
65 LookupDomains,
66 Match2Domains,
67 {OVER_AMBITIOUS_DOMAIN_RE: MakeHyperLink})
68
69 def setUp(self):
70 self.aa = autolink.Autolink()
71 self.RegisterEmailCallbacks(self.aa)
72 self.comment1 = ('Feel free to contact me at a@other.com, '
73 'or b@example.com, or c@example.org.')
74 self.comment2 = 'no matches in this comment'
75 self.comment3 = 'just matches with no ref: a@other.com, c@example.org'
76 self.comments = [self.comment1, self.comment2, self.comment3]
77
78 def testRegisterComponent(self):
79 self.assertIn('testcomp', self.aa.registry)
80
81 def testGetAllReferencedArtifacts(self):
82 all_ref_artifacts = self.aa.GetAllReferencedArtifacts(
83 None, self.comments)
84
85 self.assertIn('testcomp', all_ref_artifacts)
86 comp_refs = all_ref_artifacts['testcomp']
87 self.assertIn('b@example.com', comp_refs)
88 self.assertTrue(len(comp_refs) == 1)
89
90 def testGetAllReferencedArtifacts_TooBig(self):
91 all_ref_artifacts = self.aa.GetAllReferencedArtifacts(
92 None, self.comments, max_total_length=10)
93
94 self.assertEqual(autolink.SKIP_LOOKUPS, all_ref_artifacts)
95
96 def testMarkupAutolinks(self):
97 all_ref_artifacts = self.aa.GetAllReferencedArtifacts(None, self.comments)
98 result = self.aa.MarkupAutolinks(
99 None, [template_helpers.TextRun(self.comment1)], all_ref_artifacts)
100 self.assertEqual('Feel free to contact me at ', result[0].content)
101 self.assertEqual('a AT other.com', result[1].content)
102 self.assertEqual(', or ', result[2].content)
103 self.assertEqual('b@example.com', result[3].content)
104 self.assertEqual('mailto:b@example.com', result[3].href)
105 self.assertEqual(', or c@example.org.', result[4].content)
106
107 result = self.aa.MarkupAutolinks(
108 None, [template_helpers.TextRun(self.comment2)], all_ref_artifacts)
109 self.assertEqual('no matches in this comment', result[0].content)
110
111 result = self.aa.MarkupAutolinks(
112 None, [template_helpers.TextRun(self.comment3)], all_ref_artifacts)
113 self.assertEqual('just matches with no ref: ', result[0].content)
114 self.assertEqual('a AT other.com', result[1].content)
115 self.assertEqual(', c@example.org', result[2].content)
116
117 def testNonnestedAutolinks(self):
118 """Test that when a substitution yields plain text, others are applied."""
119 self.RegisterDomainCallbacks(self.aa)
120 all_ref_artifacts = self.aa.GetAllReferencedArtifacts(None, self.comments)
121 result = self.aa.MarkupAutolinks(
122 None, [template_helpers.TextRun(self.comment1)], all_ref_artifacts)
123 self.assertEqual('Feel free to contact me at ', result[0].content)
124 self.assertEqual('a AT ', result[1].content)
125 self.assertEqual('other.com', result[2].content)
126 self.assertEqual('other.com', result[2].href)
127 self.assertEqual(', or ', result[3].content)
128 self.assertEqual('b@example.com', result[4].content)
129 self.assertEqual('mailto:b@example.com', result[4].href)
130 self.assertEqual(', or c@', result[5].content)
131 self.assertEqual('example.org', result[6].content)
132 self.assertEqual('example.org', result[6].href)
133 self.assertEqual('.', result[7].content)
134
135 result = self.aa.MarkupAutolinks(
136 None, [template_helpers.TextRun(self.comment2)], all_ref_artifacts)
137 self.assertEqual('no matches in this comment', result[0].content)
138 result = self.aa.MarkupAutolinks(
139 None, [template_helpers.TextRun(self.comment3)], all_ref_artifacts)
140 self.assertEqual('just matches with no ref: ', result[0].content)
141 self.assertEqual('a AT ', result[1].content)
142 self.assertEqual('other.com', result[2].content)
143 self.assertEqual('other.com', result[2].href)
144 self.assertEqual(', c@', result[3].content)
145 self.assertEqual('example.org', result[4].content)
146 self.assertEqual('example.org', result[4].href)
147
148 def testMarkupAutolinks_TooBig(self):
149 """If the issue has too much text, we just do regex-based autolinking."""
150 all_ref_artifacts = self.aa.GetAllReferencedArtifacts(
151 None, self.comments, max_total_length=10)
152 result = self.aa.MarkupAutolinks(
153 None, [template_helpers.TextRun(self.comment1)], all_ref_artifacts)
154 self.assertEqual(5, len(result))
155 self.assertEqual('Feel free to contact me at ', result[0].content)
156 # The test autolink handlers in this file do not link email addresses.
157 self.assertEqual('a AT other.com', result[1].content)
158 self.assertIsNone(result[1].href)
159
160class EmailAutolinkTest(unittest.TestCase):
161
162 def setUp(self):
163 self.user_1 = 'fake user' # Note: no User fields are accessed.
164
165 def DoLinkify(
166 self, content, filter_re=autolink_constants.IS_IMPLIED_EMAIL_RE):
167 """Calls the LinkifyEmail method and returns the result.
168
169 Args:
170 content: string with a hyperlink.
171
172 Returns:
173 A list of TextRuns with some runs having the embedded email hyperlinked.
174 Or, None if no link was detected.
175 """
176 match = filter_re.search(content)
177 if not match:
178 return None
179
180 return autolink.LinkifyEmail(None, match, {'one@example.com': self.user_1})
181
182 def testLinkifyEmail(self):
183 """Test that an address is autolinked when put in the given context."""
184 test = 'one@ or @one'
185 result = self.DoLinkify('Have you met %s' % test)
186 self.assertEqual(None, result)
187
188 test = 'one@example.com'
189 result = self.DoLinkify('Have you met %s' % test)
190 self.assertEqual('/u/' + test, result[0].href)
191 self.assertEqual(test, result[0].content)
192
193 test = 'alias@example.com'
194 result = self.DoLinkify('Please also CC %s' % test)
195 self.assertEqual('mailto:' + test, result[0].href)
196 self.assertEqual(test, result[0].content)
197
198 result = self.DoLinkify('Reviewed-By: Test Person <%s>' % test)
199 self.assertEqual('mailto:' + test, result[0].href)
200 self.assertEqual(test, result[0].content)
201
202
203class URLAutolinkTest(unittest.TestCase):
204
205 def DoLinkify(self, content, filter_re=autolink_constants.IS_A_LINK_RE):
206 """Calls the linkify method and returns the result.
207
208 Args:
209 content: string with a hyperlink.
210
211 Returns:
212 A list of TextRuns with some runs will have the embedded URL hyperlinked.
213 Or, None if no link was detected.
214 """
215 match = filter_re.search(content)
216 if not match:
217 return None
218
219 return autolink.Linkify(None, match, None)
220
221 def testLinkify(self):
222 """Test that given url is autolinked when put in the given context."""
223 # Disallow the linking of URLs with user names and passwords.
224 test = 'http://user:pass@www.yahoo.com'
225 result = self.DoLinkify('What about %s' % test)
226 self.assertEqual(None, result[0].tag)
227 self.assertEqual(None, result[0].href)
228 self.assertEqual(test, result[0].content)
229
230 # Disallow the linking of non-HTTP(S) links
231 test = 'nntp://news.google.com'
232 result = self.DoLinkify('%s' % test)
233 self.assertEqual(None, result)
234
235 # Disallow the linking of file links
236 test = 'file://C:/Windows/System32/cmd.exe'
237 result = self.DoLinkify('%s' % test)
238 self.assertEqual(None, result)
239
240 # Test some known URLs
241 test = 'http://www.example.com'
242 result = self.DoLinkify('What about %s' % test)
243 self.assertEqual(test, result[0].href)
244 self.assertEqual(test, result[0].content)
245
246 def testLinkify_FTP(self):
247 """Test that FTP urls are linked."""
248 # Check for a standard ftp link
249 test = 'ftp://ftp.example.com'
250 result = self.DoLinkify('%s' % test)
251 self.assertEqual(test, result[0].href)
252 self.assertEqual(test, result[0].content)
253
254 def testLinkify_Email(self):
255 """Test that mailto: urls are linked."""
256 test = 'mailto:user@example.com'
257 result = self.DoLinkify('%s' % test)
258 self.assertEqual(test, result[0].href)
259 self.assertEqual(test, result[0].content)
260
261 def testLinkify_ShortLink(self):
262 """Test that shortlinks are linked."""
263 test = 'http://go/monorail'
264 result = self.DoLinkify(
265 '%s' % test, filter_re=autolink_constants.IS_A_SHORT_LINK_RE)
266 self.assertEqual(test, result[0].href)
267 self.assertEqual(test, result[0].content)
268
269 test = 'go/monorail'
270 result = self.DoLinkify(
271 '%s' % test, filter_re=autolink_constants.IS_A_SHORT_LINK_RE)
272 self.assertEqual('http://' + test, result[0].href)
273 self.assertEqual(test, result[0].content)
274
275 test = 'b/12345'
276 result = self.DoLinkify(
277 '%s' % test, filter_re=autolink_constants.IS_A_NUMERIC_SHORT_LINK_RE)
278 self.assertEqual('http://' + test, result[0].href)
279 self.assertEqual(test, result[0].content)
280
281 test = 'http://b/12345'
282 result = self.DoLinkify(
283 '%s' % test, filter_re=autolink_constants.IS_A_NUMERIC_SHORT_LINK_RE)
284 self.assertEqual(test, result[0].href)
285 self.assertEqual(test, result[0].content)
286
287 test = '/b/12345'
288 result = self.DoLinkify(
289 '%s' % test, filter_re=autolink_constants.IS_A_SHORT_LINK_RE)
290 self.assertIsNone(result)
291
292 test = '/b/12345'
293 result = self.DoLinkify(
294 '%s' % test, filter_re=autolink_constants.IS_A_NUMERIC_SHORT_LINK_RE)
295 self.assertIsNone(result)
296
297 test = 'b/secondFileInDiff'
298 result = self.DoLinkify(
299 '%s' % test, filter_re=autolink_constants.IS_A_NUMERIC_SHORT_LINK_RE)
300 self.assertIsNone(result)
301
302 def testLinkify_ImpliedLink(self):
303 """Test that text with .com, .org, .net, and .edu are linked."""
304 test = 'google.org'
305 result = self.DoLinkify(
306 '%s' % test, filter_re=autolink_constants.IS_IMPLIED_LINK_RE)
307 self.assertEqual('http://' + test, result[0].href)
308 self.assertEqual(test, result[0].content)
309
310 test = 'code.google.com/p/chromium'
311 result = self.DoLinkify(
312 '%s' % test, filter_re=autolink_constants.IS_IMPLIED_LINK_RE)
313 self.assertEqual('http://' + test, result[0].href)
314 self.assertEqual(test, result[0].content)
315
316 # This is not a domain, it is a directory or something.
317 test = 'build.out/p/chromium'
318 result = self.DoLinkify(
319 '%s' % test, filter_re=autolink_constants.IS_IMPLIED_LINK_RE)
320 self.assertEqual(None, result)
321
322 # We do not link the NNTP scheme, and the domain name part of it will not
323 # be linked as an HTTP link because it is preceeded by "/".
324 test = 'nntp://news.google.com'
325 result = self.DoLinkify(
326 '%s' % test, filter_re=autolink_constants.IS_IMPLIED_LINK_RE)
327 self.assertIsNone(result)
328
329 def testLinkify_Context(self):
330 """Test that surrounding syntax is not considered part of the url."""
331 test = 'http://www.example.com'
332
333 # Check for a link followed by a comma at end of English phrase.
334 result = self.DoLinkify('The URL %s, points to a great website.' % test)
335 self.assertEqual(test, result[0].href)
336 self.assertEqual(test, result[0].content)
337 self.assertEqual(',', result[1].content)
338
339 # Check for a link followed by a period at end of English sentence.
340 result = self.DoLinkify('The best site ever, %s.' % test)
341 self.assertEqual(test, result[0].href)
342 self.assertEqual(test, result[0].content)
343 self.assertEqual('.', result[1].content)
344
345 # Check for a link in paranthesis (), [], or {}
346 result = self.DoLinkify('My fav site (%s).' % test)
347 self.assertEqual(test, result[0].href)
348 self.assertEqual(test, result[0].content)
349 self.assertEqual(').', result[1].content)
350
351 result = self.DoLinkify('My fav site [%s].' % test)
352 self.assertEqual(test, result[0].href)
353 self.assertEqual(test, result[0].content)
354 self.assertEqual('].', result[1].content)
355
356 result = self.DoLinkify('My fav site {%s}.' % test)
357 self.assertEqual(test, result[0].href)
358 self.assertEqual(test, result[0].content)
359 self.assertEqual('}.', result[1].content)
360
361 # Check for a link with trailing colon
362 result = self.DoLinkify('Hit %s: you will love it.' % test)
363 self.assertEqual(test, result[0].href)
364 self.assertEqual(test, result[0].content)
365 self.assertEqual(':', result[1].content)
366
367 # Check link with commas in query string, but don't include trailing comma.
368 test = 'http://www.example.com/?v=1,2,3'
369 result = self.DoLinkify('Try %s, ok?' % test)
370 self.assertEqual(test, result[0].href)
371 self.assertEqual(test, result[0].content)
372
373 # Check link surrounded by angle-brackets.
374 result = self.DoLinkify('<%s>' % test)
375 self.assertEqual(test, result[0].href)
376 self.assertEqual(test, result[0].content)
377 self.assertEqual('>', result[1].content)
378
379 # Check link surrounded by double-quotes.
380 result = self.DoLinkify('"%s"' % test)
381 self.assertEqual(test, result[0].href)
382 self.assertEqual(test, result[0].content)
383 self.assertEqual('"', result[1].content)
384
385 # Check link with embedded double-quotes.
386 test = 'http://www.example.com/?q="a+b+c"'
387 result = self.DoLinkify('Try %s, ok?' % test)
388 self.assertEqual(test, result[0].href)
389 self.assertEqual(test, result[0].content)
390 self.assertEqual(',', result[1].content)
391
392 # Check link surrounded by single-quotes.
393 result = self.DoLinkify("'%s'" % test)
394 self.assertEqual(test, result[0].href)
395 self.assertEqual(test, result[0].content)
396 self.assertEqual("'", result[1].content)
397
398 # Check link with embedded single-quotes.
399 test = "http://www.example.com/?q='a+b+c'"
400 result = self.DoLinkify('Try %s, ok?' % test)
401 self.assertEqual(test, result[0].href)
402 self.assertEqual(test, result[0].content)
403 self.assertEqual(',', result[1].content)
404
405 # Check link with embedded parens.
406 test = 'http://www.example.com/funky(foo)and(bar).asp'
407 result = self.DoLinkify('Try %s, ok?' % test)
408 self.assertEqual(test, result[0].href)
409 self.assertEqual(test, result[0].content)
410 self.assertEqual(',', result[1].content)
411
412 test = 'http://www.example.com/funky(foo)and(bar).asp'
413 result = self.DoLinkify('My fav site <%s>' % test)
414 self.assertEqual(test, result[0].href)
415 self.assertEqual(test, result[0].content)
416 self.assertEqual('>', result[1].content)
417
418 # Check link with embedded brackets and braces.
419 test = 'http://www.example.com/funky[foo]and{bar}.asp'
420 result = self.DoLinkify('My fav site <%s>' % test)
421 self.assertEqual(test, result[0].href)
422 self.assertEqual(test, result[0].content)
423 self.assertEqual('>', result[1].content)
424
425 # Check link with mismatched delimeters inside it or outside it.
426 test = 'http://www.example.com/funky"(foo]and>bar}.asp'
427 result = self.DoLinkify('My fav site <%s>' % test)
428 self.assertEqual(test, result[0].href)
429 self.assertEqual(test, result[0].content)
430 self.assertEqual('>', result[1].content)
431
432 test = 'http://www.example.com/funky"(foo]and>bar}.asp'
433 result = self.DoLinkify('My fav site {%s' % test)
434 self.assertEqual(test, result[0].href)
435 self.assertEqual(test, result[0].content)
436
437 test = 'http://www.example.com/funky"(foo]and>bar}.asp'
438 result = self.DoLinkify('My fav site %s}' % test)
439 self.assertEqual(test, result[0].href)
440 self.assertEqual(test, result[0].content)
441 self.assertEqual('}', result[1].content)
442
443 # Link as part of an HTML example.
444 test = 'http://www.example.com/'
445 result = self.DoLinkify('<a href="%s">' % test)
446 self.assertEqual(test, result[0].href)
447 self.assertEqual(test, result[0].content)
448 self.assertEqual('">', result[1].content)
449
450 # Link nested in an HTML tag.
451 result = self.DoLinkify('<span>%s</span>' % test)
452 self.assertEqual(test, result[0].href)
453 self.assertEqual(test, result[0].content)
454
455 # Link followed by HTML tag - same bug as above.
456 result = self.DoLinkify('%s<span>foo</span>' % test)
457 self.assertEqual(test, result[0].href)
458 self.assertEqual(test, result[0].content)
459
460 # Link followed by unescaped HTML tag.
461 result = self.DoLinkify('%s<span>foo</span>' % test)
462 self.assertEqual(test, result[0].href)
463 self.assertEqual(test, result[0].content)
464
465 # Link surrounded by multiple delimiters.
466 result = self.DoLinkify('(e.g. <%s>)' % test)
467 self.assertEqual(test, result[0].href)
468 self.assertEqual(test, result[0].content)
469 result = self.DoLinkify('(e.g. <%s>),' % test)
470 self.assertEqual(test, result[0].href)
471 self.assertEqual(test, result[0].content)
472
473 def testLinkify_ContextOnBadLink(self):
474 """Test that surrounding text retained in cases where we don't link url."""
475 test = 'http://bad=example'
476 result = self.DoLinkify('<a href="%s">' % test)
477 self.assertEqual(None, result[0].href)
478 self.assertEqual(test + '">', result[0].content)
479 self.assertEqual(1, len(result))
480
481 def testLinkify_UnicodeContext(self):
482 """Test that unicode context does not mess up the link."""
483 test = 'http://www.example.com'
484
485 # This string has a non-breaking space \xa0.
486 result = self.DoLinkify(u'The correct RFC link is\xa0%s' % test)
487 self.assertEqual(test, result[0].content)
488 self.assertEqual(test, result[0].href)
489
490 def testLinkify_UnicodeLink(self):
491 """Test that unicode in a link is OK."""
492 test = u'http://www.example.com?q=division\xc3\xb7sign'
493
494 # This string has a non-breaking space \xa0.
495 result = self.DoLinkify(u'The unicode link is %s' % test)
496 self.assertEqual(test, result[0].content)
497 self.assertEqual(test, result[0].href)
498
499 def testLinkify_LinkTextEscapingDisabled(self):
500 """Test that url-like things that miss validation aren't linked."""
501 # Link matched by the regex but not accepted by the validator.
502 test = 'http://bad_domain/reportdetail?reportid=35aa03e04772358b'
503 result = self.DoLinkify('<span>%s</span>' % test)
504 self.assertEqual(None, result[0].href)
505 self.assertEqual(test, result[0].content)
506
507
508def _Issue(project_name, local_id, summary, status):
509 issue = tracker_pb2.Issue()
510 issue.project_name = project_name
511 issue.local_id = local_id
512 issue.summary = summary
513 issue.status = status
514 return issue
515
516
517class TrackerAutolinkTest(unittest.TestCase):
518
519 COMMENT_TEXT = (
520 'This relates to issue 1, issue #2, and issue3 \n'
521 'as well as bug 4, bug #5, and bug6 \n'
522 'with issue other-project:12 and issue other-project#13. \n'
523 'Watch out for issues 21, 22, and 23 with oxford comma. \n'
524 'And also bugs 31, 32 and 33 with no oxford comma.\n'
525 'Here comes crbug.com/123 and crbug.com/monorail/456.\n'
526 'We do not match when an issue\n'
527 '999. Is split across lines.'
528 )
529
530 def testExtractProjectAndIssueIdNormal(self):
531 mr = testing_helpers.MakeMonorailRequest(
532 path='/p/proj/issues/detail?id=1')
533 ref_batches = []
534 for match in autolink._ISSUE_REF_RE.finditer(self.COMMENT_TEXT):
535 new_refs = autolink.ExtractProjectAndIssueIdsNormal(mr, match)
536 ref_batches.append(new_refs)
537
538 self.assertEqual(
539 ref_batches, [
540 [(None, 1)],
541 [(None, 2)],
542 [(None, 3)],
543 [(None, 4)],
544 [(None, 5)],
545 [(None, 6)],
546 [('other-project', 12)],
547 [('other-project', 13)],
548 [(None, 21), (None, 22), (None, 23)],
549 [(None, 31), (None, 32), (None, 33)],
550 ])
551
552
553 def testExtractProjectAndIssueIdCrbug(self):
554 mr = testing_helpers.MakeMonorailRequest(
555 path='/p/proj/issues/detail?id=1')
556 ref_batches = []
557 for match in autolink._CRBUG_REF_RE.finditer(self.COMMENT_TEXT):
558 new_refs = autolink.ExtractProjectAndIssueIdsCrBug(mr, match)
559 ref_batches.append(new_refs)
560
561 self.assertEqual(ref_batches, [
562 [('chromium', 123)],
563 [('monorail', 456)],
564 ])
565
566 def DoReplaceIssueRef(
567 self, content, regex=autolink._ISSUE_REF_RE,
568 single_issue_regex=autolink._SINGLE_ISSUE_REF_RE,
569 default_project_name=None):
570 """Calls the ReplaceIssueRef method and returns the result.
571
572 Args:
573 content: string that may have a textual reference to an issue.
574 regex: optional regex to use instead of _ISSUE_REF_RE.
575
576 Returns:
577 A list of TextRuns with some runs will have the reference hyperlinked.
578 Or, None if no reference detected.
579 """
580 match = regex.search(content)
581 if not match:
582 return None
583
584 open_dict = {'proj:1': _Issue('proj', 1, 'summary-PROJ-1', 'New'),
585 # Assume there is no issue 3 in PROJ
586 'proj:4': _Issue('proj', 4, 'summary-PROJ-4', 'New'),
587 'proj:6': _Issue('proj', 6, 'summary-PROJ-6', 'New'),
588 'other-project:12': _Issue('other-project', 12,
589 'summary-OP-12', 'Accepted'),
590 }
591 closed_dict = {'proj:2': _Issue('proj', 2, 'summary-PROJ-2', 'Fixed'),
592 'proj:5': _Issue('proj', 5, 'summary-PROJ-5', 'Fixed'),
593 'other-project:13': _Issue('other-project', 13,
594 'summary-OP-12', 'Invalid'),
595 'chromium:13': _Issue('chromium', 13,
596 'summary-Cr-13', 'Invalid'),
597 }
598 comp_ref_artifacts = (open_dict, closed_dict,)
599
600 replacement_runs = autolink._ReplaceIssueRef(
601 match, comp_ref_artifacts, single_issue_regex, default_project_name)
602 return replacement_runs
603
604 def testReplaceIssueRef_NoMatch(self):
605 result = self.DoReplaceIssueRef('What is this all about?')
606 self.assertIsNone(result)
607
608 def testReplaceIssueRef_Normal(self):
609 result = self.DoReplaceIssueRef(
610 'This relates to issue 1', default_project_name='proj')
611 self.assertEqual('/p/proj/issues/detail?id=1', result[0].href)
612 self.assertEqual('issue 1', result[0].content)
613 self.assertEqual(None, result[0].css_class)
614 self.assertEqual('summary-PROJ-1', result[0].title)
615 self.assertEqual('a', result[0].tag)
616
617 result = self.DoReplaceIssueRef(
618 ', issue #2', default_project_name='proj')
619 self.assertEqual('/p/proj/issues/detail?id=2', result[0].href)
620 self.assertEqual(' issue #2 ', result[0].content)
621 self.assertEqual('closed_ref', result[0].css_class)
622 self.assertEqual('summary-PROJ-2', result[0].title)
623 self.assertEqual('a', result[0].tag)
624
625 result = self.DoReplaceIssueRef(
626 ', and issue3 ', default_project_name='proj')
627 self.assertEqual(None, result[0].href) # There is no issue 3
628 self.assertEqual('issue3', result[0].content)
629
630 result = self.DoReplaceIssueRef(
631 'as well as bug 4', default_project_name='proj')
632 self.assertEqual('/p/proj/issues/detail?id=4', result[0].href)
633 self.assertEqual('bug 4', result[0].content)
634
635 result = self.DoReplaceIssueRef(
636 ', bug #5, ', default_project_name='proj')
637 self.assertEqual('/p/proj/issues/detail?id=5', result[0].href)
638 self.assertEqual(' bug #5 ', result[0].content)
639
640 result = self.DoReplaceIssueRef(
641 'and bug6', default_project_name='proj')
642 self.assertEqual('/p/proj/issues/detail?id=6', result[0].href)
643 self.assertEqual('bug6', result[0].content)
644
645 result = self.DoReplaceIssueRef(
646 'with issue other-project:12', default_project_name='proj')
647 self.assertEqual('/p/other-project/issues/detail?id=12', result[0].href)
648 self.assertEqual('issue other-project:12', result[0].content)
649
650 result = self.DoReplaceIssueRef(
651 'and issue other-project#13', default_project_name='proj')
652 self.assertEqual('/p/other-project/issues/detail?id=13', result[0].href)
653 self.assertEqual(' issue other-project#13 ', result[0].content)
654
655 def testReplaceIssueRef_CrBug(self):
656 result = self.DoReplaceIssueRef(
657 'and crbug.com/other-project/13', regex=autolink._CRBUG_REF_RE,
658 single_issue_regex=autolink._CRBUG_REF_RE,
659 default_project_name='chromium')
660 self.assertEqual('/p/other-project/issues/detail?id=13', result[0].href)
661 self.assertEqual(' crbug.com/other-project/13 ', result[0].content)
662
663 result = self.DoReplaceIssueRef(
664 'and http://crbug.com/13', regex=autolink._CRBUG_REF_RE,
665 single_issue_regex=autolink._CRBUG_REF_RE,
666 default_project_name='chromium')
667 self.assertEqual('/p/chromium/issues/detail?id=13', result[0].href)
668 self.assertEqual(' http://crbug.com/13 ', result[0].content)
669
670 result = self.DoReplaceIssueRef(
671 'and http://crbug.com/13#c17', regex=autolink._CRBUG_REF_RE,
672 single_issue_regex=autolink._CRBUG_REF_RE,
673 default_project_name='chromium')
674 self.assertEqual('/p/chromium/issues/detail?id=13#c17', result[0].href)
675 self.assertEqual(' http://crbug.com/13#c17 ', result[0].content)
676
677 def testParseProjectNameMatch(self):
678 golden = 'project-name'
679 variations = ['%s', ' %s', '%s ', '%s:', '%s#', '%s#:', '%s:#', '%s :#',
680 '\t%s', '%s\t', '\t%s\t', '\t\t%s\t\t', '\n%s', '%s\n',
681 '\n%s\n', '\n\n%s\n\n', '\t\n%s', '\n\t%s', '%s\t\n',
682 '%s\n\t', '\t\n%s#', '\n\t%s#', '%s\t\n#', '%s\n\t#',
683 '\t\n%s:', '\n\t%s:', '%s\t\n:', '%s\n\t:'
684 ]
685
686 # First pass checks all valid project name results
687 for pattern in variations:
688 self.assertEqual(
689 golden, autolink._ParseProjectNameMatch(pattern % golden))
690
691 # Second pass tests all inputs that should result in None
692 for pattern in variations:
693 self.assertTrue(
694 autolink._ParseProjectNameMatch(pattern % '') in [None, ''])
695
696
697class VCAutolinkTest(unittest.TestCase):
698
699 GIT_HASH_1 = '1' * 40
700 GIT_HASH_2 = '2' * 40
701 GIT_HASH_3 = 'a1' * 20
702 GIT_COMMENT_TEXT = (
703 'This is a fix for r%s and R%s, by r2d2, who also authored revision %s, '
704 'revision #%s, revision %s, and revision %s' % (
705 GIT_HASH_1, GIT_HASH_2, GIT_HASH_3,
706 GIT_HASH_1.upper(), GIT_HASH_2.upper(), GIT_HASH_3.upper()))
707 SVN_COMMENT_TEXT = (
708 'This is a fix for r12 and R3400, by r2d2, who also authored '
709 'revision r4, '
710 'revision #1234567, revision 789, and revision 9025. If you have '
711 'questions, call me at 18005551212')
712
713 def testGetReferencedRevisions(self):
714 refs = ['1', '2', '3']
715 # For now, we do not look up revision objects, result is always None
716 self.assertIsNone(autolink.GetReferencedRevisions(None, refs))
717
718 def testExtractGitHashes(self):
719 refs = []
720 for match in autolink._GIT_HASH_RE.finditer(self.GIT_COMMENT_TEXT):
721 new_refs = autolink.ExtractRevNums(None, match)
722 refs.extend(new_refs)
723
724 self.assertEqual(
725 refs, [
726 self.GIT_HASH_1, self.GIT_HASH_2, self.GIT_HASH_3,
727 self.GIT_HASH_1.upper(),
728 self.GIT_HASH_2.upper(),
729 self.GIT_HASH_3.upper()
730 ])
731
732 def testExtractRevNums(self):
733 refs = []
734 for match in autolink._SVN_REF_RE.finditer(self.SVN_COMMENT_TEXT):
735 new_refs = autolink.ExtractRevNums(None, match)
736 refs.extend(new_refs)
737
738 # Note that we only autolink rNNNN with at least 4 digits.
739 self.assertEqual(refs, ['3400', '1234567', '9025'])
740
741
742 def DoReplaceRevisionRef(self, content, project=None):
743 """Calls the ReplaceRevisionRef method and returns the result.
744
745 Args:
746 content: string with a hyperlink.
747 project: optional project.
748
749 Returns:
750 A list of TextRuns with some runs will have the embedded URL hyperlinked.
751 Or, None if no link was detected.
752 """
753 match = autolink._GIT_HASH_RE.search(content)
754 if not match:
755 return None
756
757 mr = testing_helpers.MakeMonorailRequest(
758 path='/p/proj/source/detail?r=1', project=project)
759 replacement_runs = autolink.ReplaceRevisionRef(mr, match, None)
760 return replacement_runs
761
762 def testReplaceRevisionRef(self):
763 result = self.DoReplaceRevisionRef(
764 'This is a fix for r%s' % self.GIT_HASH_1)
765 self.assertEqual('https://crrev.com/%s' % self.GIT_HASH_1, result[0].href)
766 self.assertEqual('r%s' % self.GIT_HASH_1, result[0].content)
767
768 result = self.DoReplaceRevisionRef(
769 'and R%s, by r2d2, who ' % self.GIT_HASH_2)
770 self.assertEqual('https://crrev.com/%s' % self.GIT_HASH_2, result[0].href)
771 self.assertEqual('R%s' % self.GIT_HASH_2, result[0].content)
772
773 result = self.DoReplaceRevisionRef('by r2d2, who ')
774 self.assertEqual(None, result)
775
776 result = self.DoReplaceRevisionRef(
777 'also authored revision %s, ' % self.GIT_HASH_3)
778 self.assertEqual('https://crrev.com/%s' % self.GIT_HASH_3, result[0].href)
779 self.assertEqual('revision %s' % self.GIT_HASH_3, result[0].content)
780
781 result = self.DoReplaceRevisionRef(
782 'revision #%s, ' % self.GIT_HASH_1.upper())
783 self.assertEqual(
784 'https://crrev.com/%s' % self.GIT_HASH_1.upper(), result[0].href)
785 self.assertEqual(
786 'revision #%s' % self.GIT_HASH_1.upper(), result[0].content)
787
788 result = self.DoReplaceRevisionRef(
789 'revision %s, ' % self.GIT_HASH_2.upper())
790 self.assertEqual(
791 'https://crrev.com/%s' % self.GIT_HASH_2.upper(), result[0].href)
792 self.assertEqual('revision %s' % self.GIT_HASH_2.upper(), result[0].content)
793
794 result = self.DoReplaceRevisionRef(
795 'and revision %s' % self.GIT_HASH_3.upper())
796 self.assertEqual(
797 'https://crrev.com/%s' % self.GIT_HASH_3.upper(), result[0].href)
798 self.assertEqual('revision %s' % self.GIT_HASH_3.upper(), result[0].content)
799
800 def testReplaceRevisionRef_CustomURL(self):
801 """A project can override the URL used for revision links."""
802 project = fake.Project()
803 project.revision_url_format = 'http://example.com/+/{revnum}'
804 result = self.DoReplaceRevisionRef(
805 'This is a fix for r%s' % self.GIT_HASH_1, project=project)
806 self.assertEqual(
807 'http://example.com/+/%s' % self.GIT_HASH_1, result[0].href)
808 self.assertEqual('r%s' % self.GIT_HASH_1, result[0].content)