Skip to content

Commit 31ae311

Browse files
[Tracing] Create testcase grouping event (#4877)
This creates the grouping testcase event (`TestcaseGroupingEvent`) and its unit tests. The emit calls will be added in a further PR. The rationale for this event and its fields explained at: b/394051778
1 parent 87451a7 commit 31ae311

File tree

3 files changed

+168
-1
lines changed

3 files changed

+168
-1
lines changed

src/clusterfuzz/_internal/datastore/data_types.py

Lines changed: 17 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1660,7 +1660,7 @@ class TestcaseLifecycleEvent(Model):
16601660
# If testcase is manually uploaded, the user email.
16611661
uploader = ndb.StringProperty()
16621662

1663-
### Testcase Rejection
1663+
### Testcase Rejection.
16641664
# Explanation for the testcase rejection.
16651665
rejection_reason = ndb.StringProperty()
16661666

@@ -1681,6 +1681,22 @@ class TestcaseLifecycleEvent(Model):
16811681
# Reason for closing the issue (e.g., testcase fixed).
16821682
closing_reason = ndb.StringProperty()
16831683

1684+
### Grouping.
1685+
# Group ID that the testcase is currently being moved to.
1686+
group_id = ndb.IntegerProperty()
1687+
1688+
# Previous group ID, If testcase was in a previous group.
1689+
previous_group_id = ndb.IntegerProperty()
1690+
1691+
# Similar testcase that caused the grouping.
1692+
similar_testcase_id = ndb.IntegerProperty()
1693+
1694+
# Reason for grouping.
1695+
grouping_reason = ndb.StringProperty()
1696+
1697+
# If testcase's group is being merged, the reason that caused the grouping.
1698+
group_merge_reason = ndb.StringProperty()
1699+
16841700
### Task execution.
16851701
# Task stage, i.e., preprocess, main or postprocess.
16861702
task_stage = ndb.StringProperty()

src/clusterfuzz/_internal/metrics/events.py

Lines changed: 26 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -44,6 +44,7 @@ class EventTypes:
4444
ISSUE_FILING = 'issue_filing'
4545
TASK_EXECUTION = 'task_execution'
4646
ISSUE_CLOSING = 'issue_closing'
47+
TESTCASE_GROUPING = 'testcase_grouping'
4748

4849

4950
class TestcaseOrigin:
@@ -99,6 +100,14 @@ class ClosingReason:
99100
TESTCASE_INVALID = 'testcase_invalid'
100101

101102

103+
class GroupingReason:
104+
"""Reason for grouping testcases."""
105+
SIMILAR_CRASH = 'similar_crash'
106+
SAME_ISSUE = 'same_issue'
107+
IDENTICAL_VARIANT = 'identical_variant'
108+
GROUP_MERGE = 'group_merge'
109+
110+
102111
@dataclass(kw_only=True)
103112
class Event:
104113
"""Base class for ClusterFuzz events."""
@@ -243,6 +252,22 @@ class IssueClosingEvent(BaseTestcaseEvent, BaseTaskEvent):
243252
closing_reason: str | None = None
244253

245254

255+
@dataclass(kw_only=True)
256+
class TestcaseGroupingEvent(BaseTestcaseEvent, BaseTaskEvent):
257+
"""Testcase grouping event."""
258+
event_type: str = field(default=EventTypes.TESTCASE_GROUPING, init=False)
259+
# Group ID that the testcase is currently being moved to.
260+
group_id: int | None = None
261+
# Previous group ID, If testcase was in a previous group.
262+
previous_group_id: int | None = None
263+
# Similar testcase that caused the grouping.
264+
similar_testcase_id: int | None = None
265+
# Reason for grouping.
266+
grouping_reason: str | None = None
267+
# If testcase's group is being merged, the reason that caused the grouping.
268+
group_merge_reason: str | None = None
269+
270+
246271
# Mapping of specific event types to their data classes.
247272
_EVENT_TYPE_CLASSES = {
248273
EventTypes.TESTCASE_CREATION: TestcaseCreationEvent,
@@ -251,6 +276,7 @@ class IssueClosingEvent(BaseTestcaseEvent, BaseTaskEvent):
251276
EventTypes.ISSUE_FILING: IssueFilingEvent,
252277
EventTypes.TASK_EXECUTION: TaskExecutionEvent,
253278
EventTypes.ISSUE_CLOSING: IssueClosingEvent,
279+
EventTypes.TESTCASE_GROUPING: TestcaseGroupingEvent,
254280
}
255281

256282

src/clusterfuzz/_internal/tests/core/metrics/events_test.py

Lines changed: 125 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -204,6 +204,51 @@ def test_issue_closing_event(self):
204204
self.assertEqual(event_closing.closing_reason,
205205
events.ClosingReason.TESTCASE_FIXED)
206206

207+
def test_testcase_grouping_event(self):
208+
"""Test testcase grouping event class."""
209+
event_type = events.EventTypes.TESTCASE_GROUPING
210+
source = 'events_test'
211+
testcase = test_utils.create_generic_testcase()
212+
similar_testcase_id = 2
213+
214+
# Testcase is similar to another one.
215+
event_grouping = events.TestcaseGroupingEvent(
216+
source=source,
217+
testcase=testcase,
218+
group_id=10,
219+
previous_group_id=0,
220+
similar_testcase_id=similar_testcase_id,
221+
grouping_reason=events.GroupingReason.SIMILAR_CRASH)
222+
self._assert_event_common_fields(event_grouping, event_type, source)
223+
self._assert_testcase_fields(event_grouping, testcase)
224+
self._assert_task_fields(event_grouping)
225+
self.assertEqual(event_grouping.group_id, 10)
226+
self.assertEqual(event_grouping.previous_group_id, 0)
227+
self.assertEqual(event_grouping.similar_testcase_id, similar_testcase_id)
228+
self.assertEqual(event_grouping.grouping_reason,
229+
events.GroupingReason.SIMILAR_CRASH)
230+
self.assertEqual(event_grouping.group_merge_reason, None)
231+
232+
# Testcase's group is being merged.
233+
event_grouping = events.TestcaseGroupingEvent(
234+
source=source,
235+
testcase=testcase,
236+
group_id=10,
237+
previous_group_id=5,
238+
similar_testcase_id=similar_testcase_id,
239+
grouping_reason=events.GroupingReason.GROUP_MERGE,
240+
group_merge_reason=events.GroupingReason.SAME_ISSUE)
241+
self._assert_event_common_fields(event_grouping, event_type, source)
242+
self._assert_testcase_fields(event_grouping, testcase)
243+
self._assert_task_fields(event_grouping)
244+
self.assertEqual(event_grouping.group_id, 10)
245+
self.assertEqual(event_grouping.previous_group_id, 5)
246+
self.assertEqual(event_grouping.similar_testcase_id, similar_testcase_id)
247+
self.assertEqual(event_grouping.grouping_reason,
248+
events.GroupingReason.GROUP_MERGE)
249+
self.assertEqual(event_grouping.group_merge_reason,
250+
events.GroupingReason.SAME_ISSUE)
251+
207252
def test_task_execution_event(self):
208253
"""Test task execution events."""
209254
job_type = 'job_test'
@@ -465,6 +510,40 @@ def test_serialize_issue_closing_event(self):
465510
self.assertEqual(event_entity.closing_reason,
466511
events.ClosingReason.TESTCASE_FIXED)
467512

513+
def test_serialize_testcase_grouping_event(self):
514+
"""Test serializing a testcase grouping event."""
515+
testcase = test_utils.create_generic_testcase()
516+
event = events.TestcaseGroupingEvent(
517+
source='events_test',
518+
testcase=testcase,
519+
group_id=10,
520+
previous_group_id=5,
521+
similar_testcase_id=2,
522+
grouping_reason=events.GroupingReason.GROUP_MERGE,
523+
group_merge_reason=events.GroupingReason.IDENTICAL_VARIANT)
524+
event_type = event.event_type
525+
timestamp = event.timestamp
526+
527+
event_entity = self.repository._serialize_event(event) # pylint: disable=protected-access
528+
529+
# BaseTestcaseEvent and BaseTaskEvent general assertions
530+
self.assertIsNotNone(event_entity)
531+
self.assertIsInstance(event_entity, data_types.TestcaseLifecycleEvent)
532+
self.assertEqual(event_entity.event_type, event_type)
533+
self.assertEqual(event_entity.timestamp, timestamp)
534+
self._assert_common_event_fields(event_entity)
535+
self._assert_testcase_fields(event_entity, testcase.key.id())
536+
self._assert_task_fields(event_entity)
537+
538+
# TestcaseGroupingEvent specific assertions
539+
self.assertEqual(event_entity.group_id, 10)
540+
self.assertEqual(event_entity.previous_group_id, 5)
541+
self.assertEqual(event_entity.similar_testcase_id, 2)
542+
self.assertEqual(event_entity.grouping_reason,
543+
events.GroupingReason.GROUP_MERGE)
544+
self.assertEqual(event_entity.group_merge_reason,
545+
events.GroupingReason.IDENTICAL_VARIANT)
546+
468547
def test_serialize_task_execution_event(self):
469548
"""Test serializing a task execution event into a datastore entity."""
470549
source = 'events_test'
@@ -712,6 +791,52 @@ def test_deserialize_issue_closing_event(self):
712791
self.assertEqual(event.issue_id, '13579')
713792
self.assertEqual(event.closing_reason, events.ClosingReason.TESTCASE_FIXED)
714793

794+
def test_deserialize_testcase_grouping_event(self):
795+
"""Test deserializing a testcase grouping event."""
796+
event_type = events.EventTypes.TESTCASE_GROUPING
797+
date_now = datetime.datetime(2025, 1, 1, 10, 30, 15)
798+
799+
event_entity = data_types.TestcaseLifecycleEvent(event_type=event_type)
800+
event_entity.timestamp = date_now
801+
event_entity.source = 'events_test'
802+
self._set_common_event_fields(event_entity)
803+
event_entity.task_id = 'f61826c3-ca9a-4b97-9c1e-9e6f4e4f8868'
804+
event_entity.task_name = 'triage'
805+
event_entity.testcase_id = 1
806+
event_entity.fuzzer = 'fuzzer1'
807+
event_entity.job = 'test_job'
808+
event_entity.crash_revision = 2
809+
event_entity.group_id = 10
810+
event_entity.previous_group_id = 5
811+
event_entity.similar_testcase_id = 2
812+
event_entity.grouping_reason = events.GroupingReason.GROUP_MERGE
813+
event_entity.group_merge_reason = events.GroupingReason.SIMILAR_CRASH
814+
event_entity.put()
815+
816+
event = self.repository._deserialize_event(event_entity) # pylint: disable=protected-access
817+
self.assertIsNotNone(event)
818+
self.assertIsInstance(event, events.TestcaseGroupingEvent)
819+
820+
# BaseTestcaseEvent and BaseTaskEvent general assertions.
821+
self.assertEqual(event.event_type, event_type)
822+
self.assertEqual(event.source, 'events_test')
823+
self.assertEqual(event.timestamp, date_now)
824+
self._assert_common_event_fields(event)
825+
self.assertEqual(event.task_id, 'f61826c3-ca9a-4b97-9c1e-9e6f4e4f8868')
826+
self.assertEqual(event.task_name, 'triage')
827+
self.assertEqual(event.testcase_id, 1)
828+
self.assertEqual(event.fuzzer, 'fuzzer1')
829+
self.assertEqual(event.job, 'test_job')
830+
self.assertEqual(event.crash_revision, 2)
831+
832+
# TestcaseGroupingEvent specific assertions
833+
self.assertEqual(event.group_id, 10)
834+
self.assertEqual(event.previous_group_id, 5)
835+
self.assertEqual(event.similar_testcase_id, 2)
836+
self.assertEqual(event.grouping_reason, events.GroupingReason.GROUP_MERGE)
837+
self.assertEqual(event.group_merge_reason,
838+
events.GroupingReason.SIMILAR_CRASH)
839+
715840
def test_deserialize_task_execution_event(self):
716841
"""Test deserializing a datastore event into a task execution event."""
717842
event_type = events.EventTypes.TASK_EXECUTION

0 commit comments

Comments
 (0)