Repository: google/clusterfuzz

Issue:
Support fallback components
If a recoverable exception occurs during issue filing, we should support the concept of a fallback component to file into. 


Discussion:
@Navidem This would be another good one to try!

Resolving patch:
diff --git a/src/appengine/libs/issue_management/issue_filer.py b/src/appengine/libs/issue_management/issue_filer.py
--- a/src/appengine/libs/issue_management/issue_filer.py
+++ b/src/appengine/libs/issue_management/issue_filer.py
@@ -418,7 +418,18 @@ def file_issue(testcase,
     issue.components.add(policy.unreproducible_component)
 
   issue.reporter = user_email
-  issue.save()
+
+  try:
+    issue.save()
+  except:
+    if policy.fallback_component:
+      issue.components.clear()
+      issue.components.add(policy.fallback_component)
+      if policy.fallback_policy_message:
+        message = policy.fallback_policy_message.replace(
+            '%COMPONENTS%', ' '.join(metadata_components))
+        issue.body += '\n\n' + message
+      issue.save()
 
   # Update the testcase with this newly created issue.
   testcase.bug_information = str(issue.id)
diff --git a/src/appengine/libs/issue_management/issue_tracker.py b/src/appengine/libs/issue_management/issue_tracker.py
--- a/src/appengine/libs/issue_management/issue_tracker.py
+++ b/src/appengine/libs/issue_management/issue_tracker.py
@@ -75,7 +75,8 @@ def reset_tracking(self):
 
   def clear(self):
     """Clear the store, and reset tracking."""
-    self.reset_tracking()
+    self._added.clear()
+    self._removed.update(self._backing)
     self._backing.clear()
 
   def get_by_prefix(self, prefix):
diff --git a/src/appengine/libs/issue_management/issue_tracker_policy.py b/src/appengine/libs/issue_management/issue_tracker_policy.py
--- a/src/appengine/libs/issue_management/issue_tracker_policy.py
+++ b/src/appengine/libs/issue_management/issue_tracker_policy.py
@@ -97,6 +97,16 @@ def unreproducible_component(self):
     """Get the component for unreproducible bugs, if it exists."""
     return self._data.get('unreproducible_component')
 
+  @property
+  def fallback_component(self):
+    """Get the component for fallback (when save fails) issues, if it exists."""
+    return self._data.get('fallback_component')
+
+  @property
+  def fallback_policy_message(self):
+    """Get the fallback policy message, if it exists."""
+    return self._data.get('fallback_policy_message')
+
   def get_new_issue_properties(self, is_security, is_crash):
     """Get the properties to apply to a new issue."""
     policy = NewIssuePolicy()


Test patch:
diff --git a/src/clusterfuzz/_internal/tests/appengine/libs/issue_filer_test.py b/src/clusterfuzz/_internal/tests/appengine/libs/issue_filer_test.py
--- a/src/clusterfuzz/_internal/tests/appengine/libs/issue_filer_test.py
+++ b/src/clusterfuzz/_internal/tests/appengine/libs/issue_filer_test.py
@@ -76,6 +76,51 @@
     'unreproducible_component': 'Unreproducible>Component'
 })
 
+CHROMIUM_POLICY_FALLBACK = issue_tracker_policy.IssueTrackerPolicy({
+    'status': {
+        'assigned': 'Assigned',
+        'duplicate': 'Duplicate',
+        'verified': 'Verified',
+        'new': 'Untriaged',
+        'wontfix': 'WontFix',
+        'fixed': 'Fixed'
+    },
+    'all': {
+        'status': 'new',
+        'labels': ['ClusterFuzz', 'Stability-%SANITIZER%']
+    },
+    'non_security': {
+        'labels': ['Type-Bug'],
+        'crash_labels': ['Stability-Crash', 'Pri-1'],
+        'non_crash_labels': ['Pri-2']
+    },
+    'labels': {
+        'ignore': 'ClusterFuzz-Ignore',
+        'verified': 'ClusterFuzz-Verified',
+        'security_severity': 'Security_Severity-%SEVERITY%',
+        'needs_feedback': 'Needs-Feedback',
+        'invalid_fuzzer': 'ClusterFuzz-Invalid-Fuzzer',
+        'reported': None,
+        'wrong': 'ClusterFuzz-Wrong',
+        'fuzz_blocker': 'Fuzz-Blocker',
+        'reproducible': 'Reproducible',
+        'auto_cc_from_owners': 'ClusterFuzz-Auto-CC',
+        'os': 'OS-%PLATFORM%',
+        'unreproducible': 'Unreproducible',
+        'restrict_view': 'Restrict-View-SecurityTeam'
+    },
+    'security': {
+        'labels': ['Type-Bug-Security']
+    },
+    'existing': {
+        'labels': ['Stability-%SANITIZER%']
+    },
+    'fallback_component': 'fallback>component',
+    'fallback_policy_message':
+        '**NOTE**: This bug was filed into this component due to permission '
+        'or configuration issues with the specified component(s) %COMPONENTS%'
+})
+
 OSS_FUZZ_POLICY = issue_tracker_policy.IssueTrackerPolicy({
     'status': {
         'assigned': 'Assigned',
@@ -366,6 +411,40 @@ def test_testcase_metadata_invalid(self):
         ['ClusterFuzz', 'Reproducible', 'Pri-1', 'Stability-Crash', 'Type-Bug'],
         issue_tracker._itm.last_issue.labels)
 
+  def test_testcase_save_exception(self):
+    """Tests issue filing when issue.save exception"""
+    self.mock.get.return_value = CHROMIUM_POLICY_FALLBACK
+    original_save = monorail.issue.Issue.save
+    helpers.patch(self, ['libs.issue_management.monorail.issue.Issue.save'])
+
+    def my_save(*args, **kwargs):
+      if getattr(my_save, 'raise_exception', True):
+        setattr(my_save, 'raise_exception', False)
+        raise Exception("Boom!")
+      return original_save(*args, **kwargs)
+
+    self.mock.save.side_effect = my_save
+
+    issue_tracker = monorail.IssueTracker(IssueTrackerManager('chromium'))
+    issue_filer.file_issue(self.testcase5, issue_tracker)
+
+    six.assertCountEqual(self,
+                         ['fallback>component', '-component1', '-component2'],
+                         issue_tracker._itm.last_issue.components)
+    self.assertIn(
+        '**NOTE**: This bug was filed into this component due to permission or '
+        'configuration issues with the specified component(s) component1 component2',
+        issue_tracker._itm.last_issue.body)
+
+    # call without fallback_component in policy
+    # Expected result: no issue is added to itm
+    setattr(my_save, 'raise_exception', True)
+    issue_tracker = monorail.IssueTracker(IssueTrackerManager('chromium'))
+    self.mock.get.return_value = CHROMIUM_POLICY
+    issue_filer.file_issue(self.testcase1, issue_tracker)
+
+    self.assertIsNone(issue_tracker._itm.last_issue)
+
   @parameterized.parameterized.expand([
       ('chromium', CHROMIUM_POLICY),
       ('oss-fuzz', OSS_FUZZ_POLICY),