Skip to content
19 changes: 18 additions & 1 deletion src/appengine/handlers/fuzzers.py
Original file line number Diff line number Diff line change
Expand Up @@ -34,6 +34,10 @@
from libs import helpers

ARCHIVE_READ_SIZE_LIMIT = 16 * 1024 * 1024
FUZZER_FIELDS_EXCLUDED_FROM_LOG = [
'result', 'result_timestamp', 'console_output', 'return_code',
'sample_testcase', 'stats_columns', 'stats_column_descriptions'
]


class Handler(base_handler.Handler):
Expand Down Expand Up @@ -139,6 +143,10 @@ def _get_integer_value(self, key):

return value

def _get_fuzzer_state_str(self, fuzzer: data_types.Fuzzer) -> str:
fuzzer_dict = fuzzer.to_dict(exclude=FUZZER_FIELDS_EXCLUDED_FROM_LOG)
return '\n'.join(f"{key}: {val}" for key, val in fuzzer_dict.items())

def apply_fuzzer_changes(self, fuzzer, upload_info):
"""Apply changes to a fuzzer."""
if upload_info and not archive.is_archive(upload_info.filename):
Expand All @@ -160,6 +168,8 @@ def apply_fuzzer_changes(self, fuzzer, upload_info):
'uploaded is less than 16MB, ensure that the executable file has '
'"run" in its name.', 400)

existing_fuzzer_info = self._get_fuzzer_state_str(fuzzer)

jobs = request.get('jobs', [])
timeout = self._get_integer_value('timeout')
max_testcases = self._get_integer_value('max_testcases')
Expand Down Expand Up @@ -201,7 +211,14 @@ def apply_fuzzer_changes(self, fuzzer, upload_info):

fuzzer_selection.update_mappings_for_fuzzer(fuzzer)

helpers.log('Uploaded fuzzer %s.' % fuzzer.name, helpers.MODIFY_OPERATION)
new_fuzzer_info = self._get_fuzzer_state_str(fuzzer)
fuzzer_diff = helpers.diff(existing_fuzzer_info, new_fuzzer_info)
fuzzer_update_message = (f"\n--- Updated fuzzer {fuzzer.name} ---\n"
f"{new_fuzzer_info}\n"
f"--- Changes (Diff) ---\n"
f"{fuzzer_diff}")
helpers.log(fuzzer_update_message, helpers.MODIFY_OPERATION)

return self.redirect('/fuzzers')


Expand Down
15 changes: 15 additions & 0 deletions src/appengine/libs/helpers.py
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@
"""helper.py is a kitchen sink. It contains static methods that are used by
multiple handlers."""

import difflib
import logging
import sys
import traceback
Expand Down Expand Up @@ -165,3 +166,17 @@ def log(message, operation_type):
"""Logs operation being carried by current logged-in user."""
logging.info('ClusterFuzz: %s (%s): %s.', operation_type, get_user_email(),
message)


def diff(old_str: str, new_str: str) -> str:
"""Generates the diff between the two provided strings."""
old_lines = old_str.splitlines(keepends=True)
Comment thread
tbantikyan marked this conversation as resolved.
new_lines = new_str.splitlines(keepends=True)

diff_generator = difflib.ndiff(old_lines, new_lines)
clean_diff = [
line for line in diff_generator
if line.startswith('- ') or line.startswith('+ ')
]

return "".join(clean_diff)
Original file line number Diff line number Diff line change
@@ -0,0 +1,58 @@
# Copyright 2026 Google LLC
#
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.
"""Tests for fuzzers handler."""
# pylint: disable=protected-access

import datetime
import unittest

from clusterfuzz._internal.datastore import data_types
from handlers import fuzzers


class BaseEditHandlerTest(unittest.TestCase):
"""Test BaseEditHandler."""

def setUp(self):
self.handler = fuzzers.BaseEditHandler()

def test_get_fuzzer_state_str(self):
"""Test that fuzzer state str excludes specific fields."""
fuzzer = data_types.Fuzzer(
name='test_fuzzer',
revision=1,
timeout=10,
result='bad',
console_output='some output',
result_timestamp=datetime.datetime(2021, 1, 1),
return_code=1,
sample_testcase='testcase',
stats_columns='cols',
stats_column_descriptions='desc',
)

state_str = self.handler._get_fuzzer_state_str(fuzzer)

self.assertIn('name: test_fuzzer', state_str)
self.assertIn('revision: 1', state_str)
self.assertIn('timeout: 10', state_str)

# Explicitly excluded fields
self.assertNotIn('result:', state_str)
self.assertNotIn('result_timestamp', state_str)
self.assertNotIn('console_output:', state_str)
self.assertNotIn('return_code:', state_str)
self.assertNotIn('sample_testcase:', state_str)
self.assertNotIn('stats_columns:', state_str)
self.assertNotIn('stats_column_descriptions:', state_str)
24 changes: 24 additions & 0 deletions src/clusterfuzz/_internal/tests/appengine/libs/helpers_test.py
Original file line number Diff line number Diff line change
Expand Up @@ -159,3 +159,27 @@ def test_view(self):
helpers.log('message', helpers.VIEW_OPERATION)
self.mock.info.assert_called_once_with(
'ClusterFuzz: %s (%s): %s.', helpers.VIEW_OPERATION, 'email', 'message')


class DiffTest(unittest.TestCase):
"""Test diff."""

def test_diff_empty(self):
"""Test diff with empty strings."""
self.assertEqual(helpers.diff('', ''), '')

def test_diff_no_change(self):
"""Test diff with no changes."""
self.assertEqual(helpers.diff('a\nb\n', 'a\nb\n'), '')

def test_diff_addition(self):
"""Test diff with addition."""
self.assertEqual(helpers.diff('a\nb\n', 'a\nb\nc\n'), '+ c\n')

def test_diff_deletion(self):
"""Test diff with deletion."""
self.assertEqual(helpers.diff('a\nb\nc\n', 'a\nc\n'), '- b\n')

def test_diff_modification(self):
"""Test diff with modification."""
self.assertEqual(helpers.diff('a\nb\nc\n', 'a\nd\nc\n'), '- b\n+ d\n')
Loading