diff --git a/src/clusterfuzz/_internal/cron/triage.py b/src/clusterfuzz/_internal/cron/triage.py index be433bc9f1..43bf04d0a0 100644 --- a/src/clusterfuzz/_internal/cron/triage.py +++ b/src/clusterfuzz/_internal/cron/triage.py @@ -30,6 +30,7 @@ from clusterfuzz._internal.issue_management import issue_tracker_policy from clusterfuzz._internal.issue_management import issue_tracker_utils from clusterfuzz._internal.metrics import crash_stats +from clusterfuzz._internal.metrics import events from clusterfuzz._internal.metrics import logs from clusterfuzz._internal.metrics import monitoring_metrics @@ -262,10 +263,20 @@ def _check_and_update_similar_bug(testcase, issue_tracker): # might be caused by non-availability of latest builds. In that case, # don't file a new bug yet. if similar_testcase.open and not similar_testcase.one_time_crasher_flag: + events.emit( + events.TestcaseRejectionEvent( + testcase=testcase, + rejection_reason=events.RejectionReason.TRIAGE_DUPLICATE_TESTCASE) + ) return True # If the issue is still open, no need to file a duplicate bug. if issue.is_open: + events.emit( + events.TestcaseRejectionEvent( + testcase=testcase, + rejection_reason=events.RejectionReason.TRIAGE_DUPLICATE_TESTCASE) + ) return True # If the issue indicates that this crash needs to be ignored, no need to @@ -281,6 +292,11 @@ def _check_and_update_similar_bug(testcase, issue_tracker): testcase_id=similar_testcase.key.id(), issue_id=issue.id, ignore_label=ignore_label)) + events.emit( + events.TestcaseRejectionEvent( + testcase=testcase, + rejection_reason=events.RejectionReason.TRIAGE_DUPLICATE_TESTCASE) + ) return True # If this testcase is not reproducible, and a previous similar @@ -292,6 +308,11 @@ def _check_and_update_similar_bug(testcase, issue_tracker): testcase, 'Skipping filing unreproducible bug since one was already filed ' f'({similar_testcase.key.id()}).') + events.emit( + events.TestcaseRejectionEvent( + testcase=testcase, + rejection_reason=events.RejectionReason.TRIAGE_DUPLICATE_TESTCASE) + ) return True # If the issue is recently closed, wait certain time period to make sure diff --git a/src/clusterfuzz/_internal/metrics/events.py b/src/clusterfuzz/_internal/metrics/events.py index d98e743627..54e0070314 100644 --- a/src/clusterfuzz/_internal/metrics/events.py +++ b/src/clusterfuzz/_internal/metrics/events.py @@ -45,6 +45,7 @@ class RejectionReason: """Explanation for the testcase rejection values.""" ANALYZE_NO_REPRO = 'analyze_no_repro' ANALYZE_FLAKE_ON_FIRST_ATTEMPT = 'analyze_flake_on_first_attempt' + TRIAGE_DUPLICATE_TESTCASE = 'triage_duplicate_testcase' @dataclass(kw_only=True) diff --git a/src/clusterfuzz/_internal/tests/appengine/handlers/cron/triage_test.py b/src/clusterfuzz/_internal/tests/appengine/handlers/cron/triage_test.py index 25476979d9..3a1389b48b 100644 --- a/src/clusterfuzz/_internal/tests/appengine/handlers/cron/triage_test.py +++ b/src/clusterfuzz/_internal/tests/appengine/handlers/cron/triage_test.py @@ -20,6 +20,7 @@ from clusterfuzz._internal.cron import triage from clusterfuzz._internal.datastore import data_handler from clusterfuzz._internal.datastore import data_types +from clusterfuzz._internal.metrics import events from clusterfuzz._internal.tests.test_libs import appengine_test_utils from clusterfuzz._internal.tests.test_libs import helpers from clusterfuzz._internal.tests.test_libs import test_utils @@ -244,29 +245,50 @@ class CheckAndUpdateSimilarBug(unittest.TestCase): """Tests for _check_and_update_similar_bug.""" def setUp(self): + self.mock_rejection_event = unittest.mock.Mock() + helpers.patch(self, [ 'clusterfuzz._internal.base.utils.utcnow', + 'clusterfuzz._internal.metrics.events.emit', + 'clusterfuzz._internal.metrics.events.TestcaseRejectionEvent', ]) + + # When TestcaseRejectionEvent is created, call our helper to populate + # the mock object and then return it. + self.mock.TestcaseRejectionEvent.side_effect = self.init_rejection_event self.mock.utcnow.return_value = test_utils.CURRENT_TIME self.testcase = test_utils.create_generic_testcase() self.issue = appengine_test_utils.create_generic_issue() self.issue_tracker = self.issue.issue_tracker + def init_rejection_event(self, testcase, rejection_reason): + self.mock_rejection_event.testcase_id = testcase.key.id() + self.mock_rejection_event.rejection_reason = rejection_reason + return self.mock_rejection_event + + def _assert_rejection_event_emitted(self, expected_reason): + """Assert that a rejection event was emitted.""" + self.mock.emit.assert_called_once_with(self.mock_rejection_event) + self.assertEqual(self.testcase.key.id(), + self.mock_rejection_event.testcase_id) + self.assertEqual(expected_reason, + self.mock_rejection_event.rejection_reason) + def test_no_other_testcase(self): """Tests result is false when there is no other similar testcase.""" - self.assertEqual( - False, + self.assertFalse( triage._check_and_update_similar_bug(self.testcase, self.issue_tracker)) + self.mock.emit.assert_not_called() def test_similar_testcase_without_bug_information(self): """Tests result is false when there is a similar testcase but without an associated bug.""" similar_testcase = test_utils.create_generic_testcase() # pylint: disable=unused-variable - self.assertEqual( - False, + self.assertFalse( triage._check_and_update_similar_bug(self.testcase, self.issue_tracker)) + self.mock.emit.assert_not_called() def test_similar_testcase_get_issue_failed(self): """Tests result is false when there is a similar testcase with an associated @@ -275,9 +297,9 @@ def test_similar_testcase_get_issue_failed(self): similar_testcase.bug_information = '2' # Non-existent. similar_testcase.put() - self.assertEqual( - False, + self.assertFalse( triage._check_and_update_similar_bug(self.testcase, self.issue_tracker)) + self.mock.emit.assert_not_called() def test_similar_testcase_is_reproducible_and_open(self): """Tests result is true when there is a similar testcase which is @@ -290,9 +312,10 @@ def test_similar_testcase_is_reproducible_and_open(self): similar_testcase.bug_information = str(self.issue.id) similar_testcase.put() - self.assertEqual( - True, + self.assertTrue( triage._check_and_update_similar_bug(self.testcase, self.issue_tracker)) + self._assert_rejection_event_emitted( + expected_reason=events.RejectionReason.TRIAGE_DUPLICATE_TESTCASE) def test_similar_testcase_reproducible_and_closed_but_issue_open_1(self): """Tests result is true when there is a similar testcase which is @@ -307,20 +330,24 @@ def test_similar_testcase_reproducible_and_closed_but_issue_open_1(self): similar_testcase.bug_information = str(self.issue.id) similar_testcase.put() - self.assertEqual( - True, + self.assertTrue( triage._check_and_update_similar_bug(self.testcase, self.issue_tracker)) + self._assert_rejection_event_emitted( + expected_reason=events.RejectionReason.TRIAGE_DUPLICATE_TESTCASE) + testcase = data_handler.get_testcase_by_id(self.testcase.key.id()) self.assertEqual(None, testcase.bug_information) self.assertEqual('', self.issue._monorail_issue.comment) + self.mock.emit.reset_mock() similar_testcase.set_metadata( 'closed_time', test_utils.CURRENT_TIME - datetime.timedelta(hours=data_types.MIN_ELAPSED_TIME_SINCE_FIXED + 1)) - self.assertEqual( - True, + self.assertTrue( triage._check_and_update_similar_bug(self.testcase, self.issue_tracker)) + self._assert_rejection_event_emitted( + expected_reason=events.RejectionReason.TRIAGE_DUPLICATE_TESTCASE) def test_similar_testcase_reproducible_and_closed_but_issue_open_2(self): """Tests result is true when there is a similar testcase which is @@ -341,9 +368,10 @@ def test_similar_testcase_reproducible_and_closed_but_issue_open_2(self): similar_testcase_2.bug_information = str(self.issue.id) similar_testcase_2.put() - self.assertEqual( - True, + self.assertTrue( triage._check_and_update_similar_bug(self.testcase, self.issue_tracker)) + self._assert_rejection_event_emitted( + expected_reason=events.RejectionReason.TRIAGE_DUPLICATE_TESTCASE) testcase = data_handler.get_testcase_by_id(self.testcase.key.id()) self.assertEqual(None, testcase.bug_information) self.assertEqual('', self.issue._monorail_issue.comment) @@ -360,9 +388,10 @@ def test_similar_testcase_unreproducible_but_issue_open(self): similar_testcase.bug_information = str(self.issue.id) similar_testcase.put() - self.assertEqual( - True, + self.assertTrue( triage._check_and_update_similar_bug(self.testcase, self.issue_tracker)) + self._assert_rejection_event_emitted( + expected_reason=events.RejectionReason.TRIAGE_DUPLICATE_TESTCASE) def test_similar_testcase_with_issue_closed_with_ignore_label(self): """Tests result is true when there is a similar testcase with closed issue @@ -378,9 +407,10 @@ def test_similar_testcase_with_issue_closed_with_ignore_label(self): similar_testcase.bug_information = str(self.issue.id) similar_testcase.put() - self.assertEqual( - True, + self.assertTrue( triage._check_and_update_similar_bug(self.testcase, self.issue_tracker)) + self._assert_rejection_event_emitted( + expected_reason=events.RejectionReason.TRIAGE_DUPLICATE_TESTCASE) testcase = data_handler.get_testcase_by_id(self.testcase.key.id()) self.assertEqual( @@ -388,6 +418,32 @@ def test_similar_testcase_with_issue_closed_with_ignore_label(self): 'is blacklisted with ClusterFuzz-Ignore label.', testcase.get_metadata(triage.TRIAGE_MESSAGE_KEY)) + def test_similar_unreproducible_testcase_already_filed(self): + """Tests result is true when a similar unreproducible bug has been + filed.""" + self.testcase.one_time_crasher_flag = True + self.testcase.put() + + self.issue.status = 'Fixed' + self.issue._monorail_issue.open = False + self.issue.save() + + similar_testcase = test_utils.create_generic_testcase() + similar_testcase.one_time_crasher_flag = True + similar_testcase.bug_information = str(self.issue.id) + similar_testcase.put() + + self.assertTrue( + triage._check_and_update_similar_bug(self.testcase, self.issue_tracker)) + self._assert_rejection_event_emitted( + expected_reason=events.RejectionReason.TRIAGE_DUPLICATE_TESTCASE) + + testcase = data_handler.get_testcase_by_id(self.testcase.key.id()) + self.assertEqual( + 'Skipping filing unreproducible bug since one was already filed ' + f'({similar_testcase.key.id()}).', + testcase.get_metadata(triage.TRIAGE_MESSAGE_KEY)) + def test_similar_testcase_with_issue_recently_closed(self): """Tests result is true when there is a similar testcase with issue closed recently.""" @@ -404,9 +460,9 @@ def test_similar_testcase_with_issue_recently_closed(self): similar_testcase.bug_information = str(self.issue.id) similar_testcase.put() - self.assertEqual( - True, + self.assertTrue( triage._check_and_update_similar_bug(self.testcase, self.issue_tracker)) + self.mock.emit.assert_not_called() testcase = data_handler.get_testcase_by_id(self.testcase.key.id()) self.assertEqual(