Repository: google/clusterfuzz

Issue:
Testcase can be untriaged indefinitely if fixed == 'NA' and a bug is attached.
A testcase can have its `fixed` status marked as 'NA' and `open = False` in some cases.

e.g. https://github.com/google/clusterfuzz/blob/537b73cdc5aec7320af39562badebfe7c9b470c2/src/python/bot/tasks/progression_task.py#L399

If a bug is attached, that will never be closed in our current cleanup task logic: https://github.com/google/clusterfuzz/blob/8aa74e1b932b870792e095136ce14a20f2b8fee4/src/appengine/handlers/cron/cleanup.py#L435

`triaged` will also be False indefinitely as the bug remains open. 


Discussion:

Resolving patch:
diff --git a/src/appengine/handlers/cron/cleanup.py b/src/appengine/handlers/cron/cleanup.py
--- a/src/appengine/handlers/cron/cleanup.py
+++ b/src/appengine/handlers/cron/cleanup.py
@@ -147,6 +147,7 @@ def cleanup_testcases_and_issues():
       mark_unreproducible_testcase_as_fixed_if_issue_is_closed(testcase, issue)
       mark_unreproducible_testcase_and_issue_as_closed_after_deadline(
           policy, testcase, issue)
+      mark_na_testcase_issues_as_wontfix(policy, testcase, issue)
 
       # Notification, to be done at end after testcase state is updated from
       # previous rules.
@@ -597,6 +598,43 @@ def mark_unreproducible_testcase_and_issue_as_closed_after_deadline(
            testcase.key.id())
 
 
+def mark_na_testcase_issues_as_wontfix(policy, testcase, issue):
+  """Mark issues for testcases with fixed == 'NA' as fixed."""
+  # Check for for closed, NA testcases.
+  if testcase.open or testcase.fixed != 'NA':
+    return
+
+  # Nothing to be done if no issue is attached, or if issue is already closed.
+  if not issue or not issue.is_open:
+    return
+
+  # Make sure that no other testcases associated with this issue are open.
+  similar_testcase = data_types.Testcase.query(
+      data_types.Testcase.bug_information == testcase.bug_information,
+      ndb_utils.is_true(data_types.Testcase.open),
+      ndb_utils.is_false(data_types.Testcase.one_time_crasher_flag)).get()
+  if similar_testcase:
+    return
+
+  # Make that there is no crash seen in the deadline period.
+  if get_crash_occurrence_platforms(
+      testcase, data_types.UNREPRODUCIBLE_TESTCASE_WITH_BUG_DEADLINE):
+    return
+
+  # As a last check, do the expensive call of actually checking all issue
+  # comments to make sure we we didn't get called out on issue mistriage.
+  if issue_tracker_utils.was_label_added(issue, policy.label('wrong')):
+    return
+
+  comment = (f'ClusterFuzz testcase {testcase.key.id()} is closed as invalid, '
+             'so closing issue.')
+
+  issue.status = policy.status('wontfix')
+  issue.save(new_comment=comment, notify=True)
+  logs.log(
+      f'Closing issue {issue.id} for invalid testcase {testcase.key.id()}.')
+
+
 def mark_testcase_as_triaged_if_needed(testcase, issue):
   """Mark testcase as triage complete if both testcase and associated issue
   are closed."""


Test patch:
diff --git a/src/python/tests/appengine/handlers/cron/cleanup_test.py b/src/python/tests/appengine/handlers/cron/cleanup_test.py
--- a/src/python/tests/appengine/handlers/cron/cleanup_test.py
+++ b/src/python/tests/appengine/handlers/cron/cleanup_test.py
@@ -855,6 +855,89 @@ def test_notify_closed_issue_if_testcase_is_open_9(self):
         policy=self.policy, testcase=testcase, issue=self.issue)
     self.assertIn(ISSUE_NEEDS_FEEDBACK_LABEL, self.issue.labels)
 
+  def test_mark_na_testcase_issues_as_wontfix(self):
+    """Test that issue for fixed == 'NA' testcases are closed."""
+    self.mock.get_crash_occurrence_platforms.return_value = []
+    testcase = test_utils.create_generic_testcase()
+    testcase.status = 'Processed'
+    testcase.open = False
+    testcase.fixed = 'NA'
+    testcase.bug_information = str(self.issue.id)
+    testcase.put()
+
+    cleanup.mark_na_testcase_issues_as_wontfix(
+        policy=self.policy, testcase=testcase, issue=self.issue)
+    self.assertIn(
+        'ClusterFuzz testcase 1 is closed as invalid, so closing issue.',
+        self.issue._monorail_issue.comment)
+    self.assertEqual('WontFix', self.issue.status)
+
+  def test_mark_na_testcase_issues_as_wontfix_testcase_open(self):
+    """Test that valid open testcases don't get their issues closed."""
+    self.mock.get_crash_occurrence_platforms.return_value = []
+    testcase = test_utils.create_generic_testcase()
+    testcase.status = 'Processed'
+    testcase.bug_information = str(self.issue.id)
+    testcase.put()
+
+    cleanup.mark_na_testcase_issues_as_wontfix(
+        policy=self.policy, testcase=testcase, issue=self.issue)
+    self.assertEqual('Assigned', self.issue.status)
+
+  def test_mark_na_testcase_issues_as_wontfix_still_occurring(self):
+    """Test that issue for fixed == 'NA' testcases are not closed if the crash
+    is still occurring."""
+    self.mock.get_crash_occurrence_platforms.return_value = ['Linux']
+    testcase = test_utils.create_generic_testcase()
+    testcase.status = 'Processed'
+    testcase.open = False
+    testcase.fixed = 'NA'
+    testcase.bug_information = str(self.issue.id)
+    testcase.put()
+
+    cleanup.mark_na_testcase_issues_as_wontfix(
+        policy=self.policy, testcase=testcase, issue=self.issue)
+    self.assertEqual('Assigned', self.issue.status)
+
+  def test_mark_na_testcase_issues_as_wontfix_similar_testcase(self):
+    """Test that issue for fixed == 'NA' testcases are not closed if there is a
+    similar testcase attached to the same issue."""
+    self.mock.get_crash_occurrence_platforms.return_value = []
+    testcase = test_utils.create_generic_testcase()
+    testcase.status = 'Processed'
+    testcase.open = False
+    testcase.fixed = 'NA'
+    testcase.bug_information = str(self.issue.id)
+    testcase.put()
+
+    similar_testcase = test_utils.create_generic_testcase()
+    similar_testcase.bug_information = str(self.issue.id)
+    similar_testcase.one_time_crasher_flag = False
+    similar_testcase.put()
+
+    cleanup.mark_na_testcase_issues_as_wontfix(
+        policy=self.policy, testcase=testcase, issue=self.issue)
+    self.assertEqual('Assigned', self.issue.status)
+
+  def test_mark_na_testcase_issues_as_wontfix_mistriaged(self):
+    """Test that issue for fixed == 'NA' testcases are not closed if the issue
+    was marked as being mistriaged."""
+    self.mock.get_crash_occurrence_platforms.return_value = []
+    self.issue._monorail_issue.comments += [
+        appengine_test_utils.create_generic_issue_comment(
+            labels=[ISSUE_MISTRIAGED_LABEL])
+    ]
+    testcase = test_utils.create_generic_testcase()
+    testcase.status = 'Processed'
+    testcase.open = False
+    testcase.fixed = 'NA'
+    testcase.bug_information = str(self.issue.id)
+    testcase.put()
+
+    cleanup.mark_na_testcase_issues_as_wontfix(
+        policy=self.policy, testcase=testcase, issue=self.issue)
+    self.assertEqual('Assigned', self.issue.status)
+
 
 @test_utils.with_cloud_emulators('datastore')
 class UpdateOsLabelsTest(unittest.TestCase):