[warnings] Add support for Google Issue tracker bugs.

Chromium at least is now on issues.chromium.org.

R=fancl, mohrr

Change-Id: Ic8bf106caabfceb3020b5d28d9470fff960ffda4
Reviewed-on: https://chromium-review.googlesource.com/c/infra/luci/recipes-py/+/5421529
Auto-Submit: Robbie Iannucci <iannucci@chromium.org>
Reviewed-by: Rob Mohr <mohrr@google.com>
Commit-Queue: Rob Mohr <mohrr@google.com>
diff --git a/doc/user_guide.md b/doc/user_guide.md
index 9010ae1..5708aa0 100644
--- a/doc/user_guide.md
+++ b/doc/user_guide.md
@@ -713,15 +713,14 @@
 repo. `recipes.warnings` is a text proto formatted file of
 `DefinitionCollection` Message in [warning.proto]. Example as follows:
 
-    monorail_bug_default {
-      host: "bugs.chromium.org"
-      project: "chromium"
+    google_issue_default {
+      host: "issues.chromium.org"
     }
     warning {
       name: "MYMODULE_SWIZZLE_BADARG_USAGE"
       description: "The `badarg` argument on mymodule.swizzle is deprecated and replaced with swizmod."
       deadline: "2020-01-01"
-      monorail_bug {
+      google_issue {
         id: 123456
       }
     }
@@ -733,10 +732,10 @@
       description: "" # blank line
       description: "MyModule contains infra specific logic."
       deadline: "2020-12-31"
-      monorail_bug {
+      google_issue {
         id: 987654
       }
-      monorail_bug {
+      google_issue {
         project: "chrome-operations"
         id: 654321
       }
diff --git a/recipe.warnings b/recipe.warnings
index 55716ac..32313fd 100644
--- a/recipe.warnings
+++ b/recipe.warnings
@@ -1,6 +1,5 @@
-monorail_bug_default {
-  host: "bugs.chromium.org"
-  project: "chromium"
+google_issue_default {
+  host: "issues.chromium.org"
 }
 
 warning {
diff --git a/recipe_engine/internal/commands/test/report.py b/recipe_engine/internal/commands/test/report.py
index d6b1dfd..69e7cda 100644
--- a/recipe_engine/internal/commands/test/report.py
+++ b/recipe_engine/internal/commands/test/report.py
@@ -350,19 +350,23 @@
 
 def _print_warnings(warning_result, recipe_deps):
   def print_bug_links(definition):
-    def construct_monorail_link(bug):
-      return 'https://%s/p/%s/issues/detail?id=%d' % (
-          bug.host, bug.project, bug.id)
+    bug_links = [
+      f'https://{bug.host}/p/{bug.project}/issues/detail?id={bug.id}'
+      for bug in definition.monorail_bug
+    ] + [
+      f'https://{iss.host}/{iss.id}'
+      for iss in definition.google_issue
+    ]
 
-    if definition.monorail_bug:
+    if bug_links:
       print()
-      if len(definition.monorail_bug) == 1:
-        print('Bug Link: %s' % (
-            construct_monorail_link(definition.monorail_bug[0]),))
+      if len(bug_links) == 1:
+        print(f'Bug Link: {bug_links[0]}')
       else:
         print('Bug Links:')
-        for bug in definition.monorail_bug:
-          print('  %s' % construct_monorail_link(bug))
+        for link in bug_links:
+          print(f'  {link}')
+
 
   def print_call_sites(call_sites):
     def stringify_frame(frame):
diff --git a/recipe_engine/internal/warn/definition.py b/recipe_engine/internal/warn/definition.py
index 8e50d14..74c923d 100644
--- a/recipe_engine/internal/warn/definition.py
+++ b/recipe_engine/internal/warn/definition.py
@@ -14,6 +14,7 @@
 # path (where the "recipes" and/or "recipe_modules" directories sit)
 RECIPE_WARNING_DEFINITIONS_REL = 'recipe.warnings'
 
+
 def parse_warning_definitions(file_path):
   """Parse the warning definition file at the given absolute path. The file
   content is expected to be in text proto format of warning.DefinitionCollection
@@ -40,8 +41,9 @@
   definitions = list(definition_collection.warning)
 
   if definition_collection.HasField('monorail_bug_default'):
-    _populate_monorail_bug_default_fields(
-      definitions, definition_collection.monorail_bug_default)
+    _populate_bug_issue_fields(definitions,
+                               definition_collection.monorail_bug_default,
+                               definition_collection.google_issue_default)
 
   ret = {}
   for definition in definitions:
@@ -52,10 +54,12 @@
     ret[definition.name] = definition
   return ret
 
-def _populate_monorail_bug_default_fields(definitions, monorail_bug_default):
-  """If default field value has been declared for monorail bug, run through all
-  monorail bugs declared in all warning definitions and assign default
-  value to fields which are empty
+
+def _populate_bug_issue_fields(definitions, monorail_bug_default,
+                               google_issue_default):
+  """If default field value has been declared for bugs/issues, run through all
+  bugs/issues declared in all warning definitions and assign default value to
+  fields which are unset.
 
   Args:
     * definitions (list of warning.Definition)
@@ -68,6 +72,10 @@
       bug.host = bug.host or monorail_bug_default.host
       bug.project = bug.project or monorail_bug_default.project
 
+    for iss in definition.google_issue:
+      iss.host = iss.host or google_issue_default.host
+
+
 def _validate(definition):
   """Ensure the given warning definition is valid. ValueError will be
   raised otherwise. All conditions are documented in warning.proto.
@@ -94,8 +102,14 @@
     _require_non_zero_value(bug.project, err_msg_template % 'project')
     _require_non_zero_value(bug.id, err_msg_template % 'id')
 
+  for iss in definition.google_issue:
+    err_msg_template = 'Field: %s is required; Got empty value'
+    _require_non_zero_value(iss.host, err_msg_template % 'host')
+    _require_non_zero_value(iss.id, err_msg_template % 'id')
+
+
 def _require_non_zero_value(value, message):
   """Raise ValueError with message if the supplied value is a zero value
   """
   if not value:
-    raise ValueError(message)
\ No newline at end of file
+    raise ValueError(message)
diff --git a/recipe_engine/warning.proto b/recipe_engine/warning.proto
index bc6bb38..408f1e0 100644
--- a/recipe_engine/warning.proto
+++ b/recipe_engine/warning.proto
@@ -13,8 +13,12 @@
   repeated Definition warning = 1;
 
   // (optional) contains the default values for fields in all MonorailBug
-  // message proto instances enclosed in this DefinitionCollection
+  // message proto instances enclosed in this DefinitionCollection.
   MonorailBugDefault monorail_bug_default = 2;
+
+  // (optional) contains the default values for fields in all GoogleIssue
+  // message proto instances enclosed in this DefinitionCollection.
+  GoogleIssueDefault google_issue_default = 3;
 }
 
 message Definition {
@@ -36,14 +40,16 @@
   // recipe test execution to error out once the deadline is passed
   string deadline = 3;
 
-  // (optional) a list of monorail bugs associated with the current warning
-  // TODO: (yiwzhang) The bugs are currently for informal purpose only as well.
-  // No automation has been built upon that.
+  // (optional) a list of monorail bugs associated with the current warning.
   repeated MonorailBug monorail_bug = 4;
+
+  // (optional) a list of Google Issue tracker bugs associated with the current
+  // warning.
+  repeated GoogleIssue google_issue = 5;
 }
 
-// The proto message that models a bug in the Monorail. The existence
-// of supplied bug won't be validated
+// The proto message that models a bug in the Monorail issue tracker. The
+// existence of the supplied bug won't be validated.
 message MonorailBug {
   // (required if host is not provided in MonorailBugDefault) host name of
   // monorail. E.g. bugs.chromium.org
@@ -55,6 +61,21 @@
   uint32 id = 3;
 }
 
+// The proto message that models a bug in the Google issue tracker. The
+// existence of the supplied bug won't be validated.
+message GoogleIssue {
+  // (required if host is not provided in GoogleIssueDefault) host name of
+  // google issue tracker. E.g. issues.chromium.org
+  string host = 1;
+  // (required) bug numeric id
+  uint32 id = 2;
+}
+
+message GoogleIssueDefault {
+  // (optional) host name of google issue tracker, e.g. issues.chromium.org
+  string host = 1;
+}
+
 // The proto message that contains the default value for the MonorailBug proto
 // message instance
 message MonorailBugDefault {
diff --git a/unittests/warn_test.py b/unittests/warn_test.py
index 31f143e..e882d29 100755
--- a/unittests/warn_test.py
+++ b/unittests/warn_test.py
@@ -6,80 +6,103 @@
 import contextlib
 import inspect
 import os
-import re
 import textwrap
 
 from unittest import mock
 
 import test_env
 
-from recipe_engine.internal.recipe_deps import (
-  Recipe,
-  RecipeDeps,
-  RecipeModule,
-  RecipeRepo)
+from recipe_engine.internal.recipe_deps import (Recipe, RecipeDeps,
+                                                RecipeModule)
 from recipe_engine.internal.warn import escape, record
 from recipe_engine.internal.warn.definition import (
-  RECIPE_WARNING_DEFINITIONS_REL,
-  _populate_monorail_bug_default_fields,
-  _validate,
+    RECIPE_WARNING_DEFINITIONS_REL,
+    _populate_bug_issue_fields,
+    _validate,
 )
 
 import PB.recipe_engine.warning as warning_pb
 
-def create_definition(
-  name, description=None, deadline=None, monorail_bug=None):
+def create_definition(name,
+                      description=None,
+                      deadline=None,
+                      monorail_bug=None,
+                      google_issue=None):
   """Shorthand to create a warning definition proto message based on the
   given input"""
   return warning_pb.Definition(
-    name = name,
-    description = description,
-    deadline = deadline,
-    monorail_bug = [monorail_bug] if monorail_bug else None,
+      name=name,
+      description=description,
+      deadline=deadline,
+      monorail_bug=[monorail_bug] if monorail_bug else None,
+      google_issue=[google_issue] if google_issue else None,
   )
 
+
 class TestWarningDefinition(test_env.RecipeEngineUnitTest):
-  def test_populate_monorail_bug_default_fields(self):
+
+  def test_populate_google_issue_default_fields(self):
     # No Default fields specified
     definition = create_definition(
-      'WARNING_NAME', monorail_bug=warning_pb.MonorailBug(id=123))
+        'WARNING_NAME',
+        monorail_bug=warning_pb.MonorailBug(id=123),
+        google_issue=warning_pb.GoogleIssue(id=123),
+    )
     expected_definition = warning_pb.Definition()
     expected_definition.CopyFrom(definition)
-    _populate_monorail_bug_default_fields(
-      [definition], warning_pb.MonorailBugDefault())
+    _populate_bug_issue_fields([definition], warning_pb.MonorailBugDefault(),
+                               warning_pb.GoogleIssueDefault())
     self.assertEqual(expected_definition, definition)
 
     # All Default fields specified
     definition = create_definition(
-      'WARNING_NAME',
-      monorail_bug=warning_pb.MonorailBug(project= 'two', id=123))
-    _populate_monorail_bug_default_fields(
-      [definition], warning_pb.MonorailBugDefault(host='a.com', project='one'))
+        'WARNING_NAME',
+        monorail_bug=warning_pb.MonorailBug(project='two', id=123),
+        google_issue=warning_pb.GoogleIssue(id=123))
+    _populate_bug_issue_fields(
+        [definition],
+        warning_pb.MonorailBugDefault(host='m.com', project='one'),
+        warning_pb.GoogleIssueDefault(host='g.com'),
+    )
     expected_definition = create_definition(
-      'WARNING_NAME',
-      # default project should not override the existing one
-      monorail_bug=warning_pb.MonorailBug(host='a.com', project='two', id=123))
+        'WARNING_NAME',
+        # default project should not override the existing one
+        monorail_bug=warning_pb.MonorailBug(
+            host='m.com', project='two', id=123),
+        google_issue=warning_pb.GoogleIssue(host='g.com', id=123))
     self.assertEqual(expected_definition, definition)
 
     # Partial fields specified
     definition = create_definition(
-      'WARNING_NAME', monorail_bug=warning_pb.MonorailBug(id=123))
-    _populate_monorail_bug_default_fields(
-      [definition], warning_pb.MonorailBugDefault(host='a.com'))
+        'WARNING_NAME',
+        monorail_bug=warning_pb.MonorailBug(id=123),
+        google_issue=warning_pb.GoogleIssue(id=123),
+    )
+    _populate_bug_issue_fields(
+        [definition],
+        warning_pb.MonorailBugDefault(host='m.com'),
+        warning_pb.GoogleIssueDefault(host='g.com'),
+    )
     expected_definition = create_definition(
-      'WARNING_NAME',
-      monorail_bug=warning_pb.MonorailBug(host='a.com', id=123))
+        'WARNING_NAME',
+        monorail_bug=warning_pb.MonorailBug(host='m.com', id=123),
+        google_issue=warning_pb.GoogleIssue(host='g.com', id=123),
+    )
     self.assertEqual(expected_definition, definition)
 
   def test_valid_definitions(self):
     simple_definition = create_definition('SIMPLE_WARNING_NAME')
     _validate(simple_definition)
     full_definition = create_definition(
-      'FULL_WARNING_NAME',
-      description = ['this is a description',],
-      deadline = '2020-12-31',
-      monorail_bug = warning_pb.MonorailBug(
-        host='bugs.chromium.org', project= 'chromium', id=123456),
+        'FULL_WARNING_NAME',
+        description=[
+            'this is a description',
+        ],
+        deadline='2020-12-31',
+        monorail_bug=warning_pb.MonorailBug(
+            host='bugs.chromium.org', project='chromium', id=123456),
+        google_issue=warning_pb.GoogleIssue(
+            host='issues.chromium.org', id=123456),
     )
     _validate(full_definition)
 
@@ -97,18 +120,35 @@
       _validate(definition)
     # No project specified
     definition = create_definition(
+        'WARNING_NAME',
+        monorail_bug=warning_pb.MonorailBug(
+            host='bugs.chromium.org', id=123456),
+    )
+    with self.assertRaises(ValueError):
+      _validate(definition)
+    # No id specified
+    definition = create_definition(
+        'WARNING_NAME',
+        monorail_bug=warning_pb.MonorailBug(
+            host='bugs.chromium.org', project='chromium'),
+    )
+    with self.assertRaises(ValueError):
+      _validate(definition)
+
+  def test_invalid_google_issue(self):
+    # No host specified
+    definition = create_definition(
       'WARNING_NAME',
-      monorail_bug = warning_pb.MonorailBug(
-        host='bugs.chromium.org', id=123456),
+      google_issue = warning_pb.GoogleIssue(id=123456),
       )
     with self.assertRaises(ValueError):
       _validate(definition)
     # No id specified
     definition = create_definition(
-      'WARNING_NAME',
-      monorail_bug = warning_pb.MonorailBug(
-        host='bugs.chromium.org', project='chromium'),
-      )
+        'WARNING_NAME',
+        google_issue=warning_pb.GoogleIssue(
+            host='issues.chromium.org'),
+    )
     with self.assertRaises(ValueError):
       _validate(definition)
 
@@ -391,6 +431,9 @@
     self.deps = self.FakeRecipeDeps()
     with self.deps.main_repo.write_file(RECIPE_WARNING_DEFINITIONS_REL) as d:
       d.write('''
+      google_issue_default {
+        host: "issues.chromium.org"
+      }
       monorail_bug_default {
         host: "bugs.chromium.org"
         project: "chromium"
@@ -402,6 +445,9 @@
         monorail_bug {
           id: 123456
         }
+        google_issue {
+          id: 123456
+        }
       }
       warning {
         name: "MYMODULE_DEPRECATION"
@@ -409,6 +455,9 @@
         # Comment goes here
         description: "Use other_mod instead."
         deadline: "2020-12-31"
+        google_issue {
+          id: 654321
+        }
         monorail_bug {
           project: "chrome-operations"
           id: 654321
@@ -468,7 +517,9 @@
       The `badarg` argument on my_mod\.swizzle is deprecated\.
     Deadline: 2020-01-01
 
-    Bug Link: https://bugs\.chromium\.org/p/chromium/issues/detail\?id=123456
+    Bug Links:
+      https://bugs\.chromium\.org/p/chromium/issues/detail\?id=123456
+      https://issues\.chromium\.org/123456
     Call Sites:
     .+/recipe_modules/cool_mod/api\.py:\d+
     .+/recipe_modules/my_mod/tests/bad\.py:3
@@ -510,7 +561,9 @@
       Use other_mod instead\.
     Deadline: 2020-12-31
 
-    Bug Link: https://bugs\.chromium\.org/p/chrome\-operations/issues/detail\?id=654321
+    Bug Links:
+      https://bugs\.chromium\.org/p/chrome\-operations/issues/detail\?id=654321
+      https://issues\.chromium\.org/654321
     Import Sites:
     .+/recipe_modules/my_mod/tests/full\.py
     .+/recipe_modules/cool_mod/__init__\.py
@@ -546,7 +599,9 @@
       Use other_mod instead\.
     Deadline: 2020-12-31
 
-    Bug Link: https://bugs\.chromium\.org/p/chrome\-operations/issues/detail\?id=654321
+    Bug Links:
+      https://bugs\.chromium\.org/p/chrome\-operations/issues/detail\?id=654321
+      https://issues\.chromium\.org/654321
     Call Sites:
     .+/recipe_modules/my_mod/tests/full\.py:3
     Import Sites:
@@ -674,6 +729,9 @@
     upstream = self.deps.add_repo('upstream')
     with upstream.write_file(RECIPE_WARNING_DEFINITIONS_REL) as d:
       d.write('''
+      google_issue_default {
+        host: "issues.chromium.org"
+      }
       monorail_bug_default {
         host: "bugs.chromium.org"
         project: "chromium"
@@ -682,7 +740,7 @@
         name: "MYMODULE_SWIZZLE_BADARG_USAGE"
         description: "The `badarg` argument on my_mod.swizzle is deprecated."
         deadline: "2020-01-01"
-        monorail_bug {
+        google_issue {
           id: 123456
         }
       }
@@ -752,7 +810,7 @@
       The `badarg` argument on my_mod\.swizzle is deprecated\.
     Deadline: 2020-01-01
 
-    Bug Link: https://bugs\.chromium\.org/p/chromium/issues/detail\?id=123456
+    Bug Link: https://issues\.chromium\.org/123456
     Call Sites:
     .+/main/recipes/bad\.py:3
     '''.strip('\n'))