From 91319285ee8d2a7e5751d23d45535bdb74c2ad83 Mon Sep 17 00:00:00 2001 From: Dylan Jew Date: Thu, 2 Apr 2026 11:30:10 -0400 Subject: [PATCH 1/3] Add Fuzzer.created_at field We want to start tracking more metrics around blackbox fuzzers. We currently only track the last updated timestamp of a fuzzer. This introduces a craeted_at timestamp which is set when creating a fuzzer. Testing: Ran a local server and creatd a Fuzzer to verify the field is set. --- src/appengine/handlers/fuzzers.py | 2 ++ src/clusterfuzz/_internal/datastore/data_types.py | 4 ++++ 2 files changed, 6 insertions(+) diff --git a/src/appengine/handlers/fuzzers.py b/src/appengine/handlers/fuzzers.py index c5d557aebab..b97534b7c40 100644 --- a/src/appengine/handlers/fuzzers.py +++ b/src/appengine/handlers/fuzzers.py @@ -251,6 +251,8 @@ def post(self): fuzzer = data_types.Fuzzer() fuzzer.name = name fuzzer.revision = 0 + fuzzer.created_at = datetime.datetime.now(tz=datetime.timezone.utc).replace( + tzinfo=None) return self.apply_fuzzer_changes(fuzzer, upload_info) diff --git a/src/clusterfuzz/_internal/datastore/data_types.py b/src/clusterfuzz/_internal/datastore/data_types.py index e15966b61bf..ab0eb20408b 100644 --- a/src/clusterfuzz/_internal/datastore/data_types.py +++ b/src/clusterfuzz/_internal/datastore/data_types.py @@ -261,6 +261,10 @@ class Blacklist(Model): class Fuzzer(Model): """Represents a fuzzer.""" + # Created at timestamp. Not set for fuzzers created before this field was + # added. + created_at = ndb.DateTimeProperty() + # Additionally allows '.' and '@' over NAME_CHECK_REGEX. VALID_NAME_REGEX = re.compile(r'^[a-zA-Z0-9_@.-]+$') From a291c1f8e2715fb66e6376e07efecff83062820a Mon Sep 17 00:00:00 2001 From: Dylan Jew Date: Thu, 2 Apr 2026 13:19:05 -0400 Subject: [PATCH 2/3] Add unit test for create fuzzer --- .../tests/appengine/handlers/fuzzers_test.py | 52 +++++++++++++++++++ 1 file changed, 52 insertions(+) diff --git a/src/clusterfuzz/_internal/tests/appengine/handlers/fuzzers_test.py b/src/clusterfuzz/_internal/tests/appengine/handlers/fuzzers_test.py index 997bf7d2f52..8e6434e6165 100644 --- a/src/clusterfuzz/_internal/tests/appengine/handlers/fuzzers_test.py +++ b/src/clusterfuzz/_internal/tests/appengine/handlers/fuzzers_test.py @@ -17,8 +17,14 @@ import datetime import unittest +import flask +import webtest + from clusterfuzz._internal.datastore import data_types +from clusterfuzz._internal.tests.test_libs import helpers as test_helpers +from clusterfuzz._internal.tests.test_libs import test_utils from handlers import fuzzers +from libs import form class BaseEditHandlerTest(unittest.TestCase): @@ -56,3 +62,49 @@ def test_get_fuzzer_state_str(self): self.assertNotIn('sample_testcase:', state_str) self.assertNotIn('stats_columns:', state_str) self.assertNotIn('stats_column_descriptions:', state_str) + + +@test_utils.with_cloud_emulators('datastore') +class CreateHandlerTest(unittest.TestCase): + """Tests CreateHandler creates a new Fuzzer""" + + def setUp(self): + test_helpers.patch(self, [ + 'libs.access.has_access', + 'libs.auth.get_current_user', + 'libs.helpers.get_user_email', + 'handlers.fuzzers.datetime', + 'handlers.fuzzers.CreateHandler.get_upload', + 'handlers.fuzzers.CreateHandler.apply_fuzzer_changes', + ]) + self.mock.has_access.return_value = True + self.mock.get_current_user().email = 'test@user.com' + self.mock.get_user_email.return_value = 'test@user.com' + + self.mock_time = datetime.datetime(2026, 1, 1, tzinfo=None) + self.mock.datetime.datetime.now.return_value = self.mock_time + self.mock.datetime.timezone = datetime.timezone + + flaskapp = flask.Flask('testflask') + flaskapp.add_url_rule('/', view_func=fuzzers.CreateHandler.as_view('/')) + self.app = webtest.TestApp(flaskapp) + + def test_create_fuzzer(self): + """Test create fuzzer with basic properties.""" + fuzzer_name = 'test_fuzzer' + + resp = self.app.post_json('/', { + 'csrf_token': form.generate_csrf_token(), + 'name': fuzzer_name, + }) + + self.assertEqual(200, resp.status_int) + + self.mock.apply_fuzzer_changes.assert_called_once() + + fuzzer = self.mock.apply_fuzzer_changes.call_args[0][1] + upload_info = self.mock.apply_fuzzer_changes.call_args[0][2] + + self.assertEqual(fuzzer.name, fuzzer_name) + self.assertEqual(fuzzer.revision, 0) + self.assertEqual(fuzzer.created_at, self.mock_time) From 569858cb43c36cc9049ab44076092f6316885b0c Mon Sep 17 00:00:00 2001 From: Dylan Jew Date: Thu, 2 Apr 2026 13:43:47 -0400 Subject: [PATCH 3/3] fix unused var and replace deprecated datetime.utcnow --- src/appengine/handlers/fuzzers.py | 3 ++- .../_internal/tests/appengine/handlers/fuzzers_test.py | 1 - 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/src/appengine/handlers/fuzzers.py b/src/appengine/handlers/fuzzers.py index b97534b7c40..33d93e73f16 100644 --- a/src/appengine/handlers/fuzzers.py +++ b/src/appengine/handlers/fuzzers.py @@ -195,7 +195,8 @@ def apply_fuzzer_changes(self, fuzzer, upload_info): fuzzer.external_contribution = bool(external_contribution) fuzzer.differential = bool(differential) fuzzer.additional_environment_string = environment_string - fuzzer.timestamp = datetime.datetime.utcnow() + fuzzer.timestamp = datetime.datetime.now(tz=datetime.timezone.utc).replace( + tzinfo=None) fuzzer.data_bundle_name = data_bundle_name # Update only if a new archive is provided. diff --git a/src/clusterfuzz/_internal/tests/appengine/handlers/fuzzers_test.py b/src/clusterfuzz/_internal/tests/appengine/handlers/fuzzers_test.py index 8e6434e6165..6ddf0cc13a0 100644 --- a/src/clusterfuzz/_internal/tests/appengine/handlers/fuzzers_test.py +++ b/src/clusterfuzz/_internal/tests/appengine/handlers/fuzzers_test.py @@ -103,7 +103,6 @@ def test_create_fuzzer(self): self.mock.apply_fuzzer_changes.assert_called_once() fuzzer = self.mock.apply_fuzzer_changes.call_args[0][1] - upload_info = self.mock.apply_fuzzer_changes.call_args[0][2] self.assertEqual(fuzzer.name, fuzzer_name) self.assertEqual(fuzzer.revision, 0)