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