diff --git a/src/clusterfuzz/_internal/cron/triage.py b/src/clusterfuzz/_internal/cron/triage.py index be433bc9f1..f2dc3790b1 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 @@ -531,6 +532,11 @@ def _triage_testcase(testcase, excluded_jobs, all_jobs, throttler): _emit_untriaged_testcase_age_metric(testcase, PENDING_FILING) _increment_untriaged_testcase_count(testcase.job_type, PENDING_FILING) logs.info(f'Issue filing failed for testcase id {testcase_id}') + events.emit( + events.IssueFilingEvent( + testcase=testcase, + issue_tracker_project=issue_tracker.project, + issue_created=False)) return _set_testcase_stuck_state(testcase, False) @@ -538,6 +544,12 @@ def _triage_testcase(testcase, excluded_jobs, all_jobs, throttler): _create_filed_bug_metadata(testcase) issue_filer.notify_issue_update(testcase, 'new') + events.emit( + events.IssueFilingEvent( + testcase=testcase, + issue_tracker_project=issue_tracker.project, + issue_id=str(testcase.bug_information), + issue_created=True)) logs.info('Filed new issue %s for testcase %d.' % (testcase.bug_information, testcase_id)) diff --git a/src/clusterfuzz/_internal/datastore/data_types.py b/src/clusterfuzz/_internal/datastore/data_types.py index c5c7fae419..2ba2ef484b 100644 --- a/src/clusterfuzz/_internal/datastore/data_types.py +++ b/src/clusterfuzz/_internal/datastore/data_types.py @@ -1665,8 +1665,8 @@ class TestcaseLifecycleEvent(Model): rejection_reason = ndb.StringProperty() ### Issue Filing. - # Name of the issue tracker (e.g., buganizer). - issue_tracker = ndb.StringProperty() + # Name of the project associated with the issue tracker. + issue_tracker_project = ndb.StringProperty() # ID from issue tracker bug (same as `bug_information` for Testcase). issue_id = ndb.StringProperty() diff --git a/src/clusterfuzz/_internal/metrics/events.py b/src/clusterfuzz/_internal/metrics/events.py index 335f2cb51e..6a8d4e2475 100644 --- a/src/clusterfuzz/_internal/metrics/events.py +++ b/src/clusterfuzz/_internal/metrics/events.py @@ -152,8 +152,8 @@ class TestcaseRejectionEvent(BaseTestcaseEvent, BaseTaskEvent): class IssueFilingEvent(BaseTestcaseEvent, BaseTaskEvent): """Issue filing event.""" event_type: str = field(default=EventTypes.ISSUE_FILING, init=False) - # Either buganizer or some_other_board. - issue_tracker: str | None = None + # Name of the project associate with the issue tracker. + issue_tracker_project: str | None = None # The number of the issue on the issue tracker. issue_id: str | None = None # If the issue filing attempt was successful. 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..fc6bd2a516 100644 --- a/src/clusterfuzz/_internal/tests/appengine/handlers/cron/triage_test.py +++ b/src/clusterfuzz/_internal/tests/appengine/handlers/cron/triage_test.py @@ -16,10 +16,12 @@ import datetime import unittest +from unittest import mock 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 @@ -561,3 +563,80 @@ def test_default_limit(self): throttler._get_project_bugs_filing_max(testcase.job_type)) self.assertEqual(None, throttler._get_job_bugs_filing_max(testcase.job_type)) + + +@test_utils.with_cloud_emulators('datastore') +class IssueFilingEventEmitTest(unittest.TestCase): + """Tests emission of IssueFilingEvent when filing an issue.""" + + def setUp(self): + helpers.patch(self, [ + 'clusterfuzz._internal.metrics.events.emit', + 'clusterfuzz._internal.metrics.events._get_datetime_now', + 'clusterfuzz._internal.cron.triage._file_issue', + 'clusterfuzz._internal.cron.triage._is_bug_filed', + 'clusterfuzz._internal.cron.triage._is_crash_important', + 'clusterfuzz._internal.issue_management.issue_tracker_utils.get_issue_tracker_for_testcase', + 'clusterfuzz._internal.datastore.data_handler.critical_tasks_completed', + 'clusterfuzz._internal.cron.triage._check_and_update_similar_bug', + 'clusterfuzz._internal.cron.triage._create_filed_bug_metadata', + 'clusterfuzz._internal.cron.triage.issue_filer.notify_issue_update', + 'clusterfuzz._internal.cron.triage._set_testcase_stuck_state', + 'clusterfuzz._internal.cron.triage._emit_untriaged_testcase_age_metric', + 'clusterfuzz._internal.cron.triage._increment_untriaged_testcase_count', + ]) + self.mock._get_datetime_now.return_value = datetime.datetime(2025, 1, 1) + self.mock._is_bug_filed.return_value = False + self.mock._is_crash_important.return_value = True + self.mock.critical_tasks_completed.return_value = True + self.mock._check_and_update_similar_bug.return_value = False + self.mock._create_filed_bug_metadata.return_value = None + self.mock.notify_issue_update.return_value = None + self.mock._set_testcase_stuck_state.return_value = None + self.mock._emit_untriaged_testcase_age_metric.return_value = None + self.mock._increment_untriaged_testcase_count.return_value = None + + self.testcase = test_utils.create_generic_testcase() + self.testcase.set_metadata('ran_grouper', True) + + issue_tracker = mock.MagicMock() + issue_tracker.project = 'oss-fuzz' + self.mock.get_issue_tracker_for_testcase.return_value = issue_tracker + + def test_event_emitted(self): + """Tests that the IssueFilingEvent is emitted on a filled testcase.""" + + def file_issue(testcase, issue_tracker_obj, throttler): # pylint: disable=unused-argument + testcase.bug_information = '99' + return True + + self.mock._file_issue.side_effect = file_issue + + triage._triage_testcase( + self.testcase, + excluded_jobs=[], + all_jobs=[self.testcase.job_type], + throttler=triage.Throttler()) + + self.mock.emit.assert_called_once_with( + events.IssueFilingEvent( + testcase=self.testcase, + issue_tracker_project='oss-fuzz', + issue_id='99', + issue_created=True)) + + def test_event_emitted_on_failure(self): + """Tests that the IssueFilingEvent is emitted on a failed filing.""" + self.mock._file_issue.return_value = False + + triage._triage_testcase( + self.testcase, + excluded_jobs=[], + all_jobs=[self.testcase.job_type], + throttler=triage.Throttler()) + + self.mock.emit.assert_called_once_with( + events.IssueFilingEvent( + testcase=self.testcase, + issue_tracker_project='oss-fuzz', + issue_created=False)) diff --git a/src/clusterfuzz/_internal/tests/core/metrics/events_test.py b/src/clusterfuzz/_internal/tests/core/metrics/events_test.py index a95dc11f63..bd7f041431 100644 --- a/src/clusterfuzz/_internal/tests/core/metrics/events_test.py +++ b/src/clusterfuzz/_internal/tests/core/metrics/events_test.py @@ -159,13 +159,13 @@ def test_issue_filing_event(self): event_filing = events.IssueFilingEvent( source=source, testcase=testcase, - issue_tracker='buganizer', + issue_tracker_project='oss-fuzz', issue_id='12345', issue_created=True) self._assert_event_common_fields(event_filing, event_type, source) self._assert_testcase_fields(event_filing, testcase) self._assert_task_fields(event_filing) - self.assertEqual(event_filing.issue_tracker, 'buganizer') + self.assertEqual(event_filing.issue_tracker_project, 'oss-fuzz') self.assertEqual(event_filing.issue_id, '12345') self.assertTrue(event_filing.issue_created) @@ -307,7 +307,7 @@ def test_serialize_issue_filing_event(self): event = events.IssueFilingEvent( source='events_test', testcase=testcase, - issue_tracker='buganizer', + issue_tracker_project='oss-fuzz', issue_id='67890', issue_created=False) event_type = event.event_type @@ -325,7 +325,7 @@ def test_serialize_issue_filing_event(self): self._assert_task_fields(event_entity) # IssueFilingEvent specific assertions - self.assertEqual(event_entity.issue_tracker, 'buganizer') + self.assertEqual(event_entity.issue_tracker_project, 'oss-fuzz') self.assertEqual(event_entity.issue_id, '67890') self.assertFalse(event_entity.issue_created) @@ -437,7 +437,7 @@ def test_deserialize_issue_filing_event(self): event_entity.fuzzer = 'fuzzer1' event_entity.job = 'test_job' event_entity.crash_revision = 2 - event_entity.issue_tracker = 'buganizer' + event_entity.issue_tracker_project = 'oss-fuzz' event_entity.issue_id = '13579' event_entity.issue_created = True event_entity.put() @@ -458,7 +458,7 @@ def test_deserialize_issue_filing_event(self): self.assertEqual(event.crash_revision, 2) # IssueFilingEvent specific assertions - self.assertEqual(event.issue_tracker, 'buganizer') + self.assertEqual(event.issue_tracker_project, 'oss-fuzz') self.assertEqual(event.issue_id, '13579') self.assertTrue(event.issue_created)