Skip to content

[Tracing] Emiting TestcaseRejectionEvent during triage #4827

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Open
wants to merge 2 commits into
base: master
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
21 changes: 21 additions & 0 deletions src/clusterfuzz/_internal/cron/triage.py
Original file line number Diff line number Diff line change
Expand Up @@ -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

Expand Down Expand Up @@ -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(
Copy link
Collaborator

@vitorguidi vitorguidi Jun 13, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Now that I think of it, this event will be emited multiple times until the testcase itself is marked as closed, or something else that prevents it from entering this triage loop indefini.

It is not as simple as it looked from the start, we need to check the cleanup code to understand if that might be correct place to emit

Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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
Expand All @@ -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
Expand All @@ -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
Expand Down
1 change: 1 addition & 0 deletions src/clusterfuzz/_internal/metrics/events.py
Original file line number Diff line number Diff line change
Expand Up @@ -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)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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
Expand All @@ -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
Expand All @@ -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
Expand All @@ -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
Expand All @@ -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)
Expand All @@ -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
Expand All @@ -378,16 +407,43 @@ 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(
'Skipping filing a bug since similar testcase (2) in issue (1) '
'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."""
Expand All @@ -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(
Expand Down
Loading