Skip to content

Commit 9f0c774

Browse files
tbantikyanletitz
andauthored
Add old state and diff logging on fuzzer update (google#5184)
Logs currently include only the fuzzer name. Previous state and diff information can help identify issues with any updates to fuzzers. --------- Co-authored-by: Titouan Rigoudy <titouan.rigoudy@gmail.com>
1 parent 35b8067 commit 9f0c774

4 files changed

Lines changed: 115 additions & 1 deletion

File tree

src/appengine/handlers/fuzzers.py

Lines changed: 18 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -34,6 +34,10 @@
3434
from libs import helpers
3535

3636
ARCHIVE_READ_SIZE_LIMIT = 16 * 1024 * 1024
37+
FUZZER_FIELDS_EXCLUDED_FROM_LOG = [
38+
'result', 'result_timestamp', 'console_output', 'return_code',
39+
'sample_testcase', 'stats_columns', 'stats_column_descriptions'
40+
]
3741

3842

3943
class Handler(base_handler.Handler):
@@ -139,6 +143,10 @@ def _get_integer_value(self, key):
139143

140144
return value
141145

146+
def _get_fuzzer_state_str(self, fuzzer: data_types.Fuzzer) -> str:
147+
fuzzer_dict = fuzzer.to_dict(exclude=FUZZER_FIELDS_EXCLUDED_FROM_LOG)
148+
return '\n'.join(f"{key}: {val}" for key, val in fuzzer_dict.items())
149+
142150
def apply_fuzzer_changes(self, fuzzer, upload_info):
143151
"""Apply changes to a fuzzer."""
144152
if upload_info and not archive.is_archive(upload_info.filename):
@@ -160,6 +168,8 @@ def apply_fuzzer_changes(self, fuzzer, upload_info):
160168
'uploaded is less than 16MB, ensure that the executable file has '
161169
'"run" in its name.', 400)
162170

171+
existing_fuzzer_info = self._get_fuzzer_state_str(fuzzer)
172+
163173
jobs = request.get('jobs', [])
164174
timeout = self._get_integer_value('timeout')
165175
max_testcases = self._get_integer_value('max_testcases')
@@ -201,7 +211,14 @@ def apply_fuzzer_changes(self, fuzzer, upload_info):
201211

202212
fuzzer_selection.update_mappings_for_fuzzer(fuzzer)
203213

204-
helpers.log('Uploaded fuzzer %s.' % fuzzer.name, helpers.MODIFY_OPERATION)
214+
new_fuzzer_info = self._get_fuzzer_state_str(fuzzer)
215+
fuzzer_diff = helpers.diff(existing_fuzzer_info, new_fuzzer_info)
216+
fuzzer_update_message = (f"\n--- Updated fuzzer {fuzzer.name} ---\n"
217+
f"{new_fuzzer_info}\n"
218+
f"--- Changes (Diff) ---\n"
219+
f"{fuzzer_diff}")
220+
helpers.log(fuzzer_update_message, helpers.MODIFY_OPERATION)
221+
205222
return self.redirect('/fuzzers')
206223

207224

src/appengine/libs/helpers.py

Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,7 @@
1414
"""helper.py is a kitchen sink. It contains static methods that are used by
1515
multiple handlers."""
1616

17+
import difflib
1718
import logging
1819
import sys
1920
import traceback
@@ -165,3 +166,17 @@ def log(message, operation_type):
165166
"""Logs operation being carried by current logged-in user."""
166167
logging.info('ClusterFuzz: %s (%s): %s.', operation_type, get_user_email(),
167168
message)
169+
170+
171+
def diff(old_str: str, new_str: str) -> str:
172+
"""Generates the diff between the two provided strings."""
173+
old_lines = old_str.splitlines(keepends=True)
174+
new_lines = new_str.splitlines(keepends=True)
175+
176+
diff_generator = difflib.ndiff(old_lines, new_lines)
177+
clean_diff = [
178+
line for line in diff_generator
179+
if line.startswith('- ') or line.startswith('+ ')
180+
]
181+
182+
return "".join(clean_diff)
Lines changed: 58 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,58 @@
1+
# Copyright 2026 Google LLC
2+
#
3+
# Licensed under the Apache License, Version 2.0 (the "License");
4+
# you may not use this file except in compliance with the License.
5+
# You may obtain a copy of the License at
6+
#
7+
# http://www.apache.org/licenses/LICENSE-2.0
8+
#
9+
# Unless required by applicable law or agreed to in writing, software
10+
# distributed under the License is distributed on an "AS IS" BASIS,
11+
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12+
# See the License for the specific language governing permissions and
13+
# limitations under the License.
14+
"""Tests for fuzzers handler."""
15+
# pylint: disable=protected-access
16+
17+
import datetime
18+
import unittest
19+
20+
from clusterfuzz._internal.datastore import data_types
21+
from handlers import fuzzers
22+
23+
24+
class BaseEditHandlerTest(unittest.TestCase):
25+
"""Test BaseEditHandler."""
26+
27+
def setUp(self):
28+
self.handler = fuzzers.BaseEditHandler()
29+
30+
def test_get_fuzzer_state_str(self):
31+
"""Test that fuzzer state str excludes specific fields."""
32+
fuzzer = data_types.Fuzzer(
33+
name='test_fuzzer',
34+
revision=1,
35+
timeout=10,
36+
result='bad',
37+
console_output='some output',
38+
result_timestamp=datetime.datetime(2021, 1, 1),
39+
return_code=1,
40+
sample_testcase='testcase',
41+
stats_columns='cols',
42+
stats_column_descriptions='desc',
43+
)
44+
45+
state_str = self.handler._get_fuzzer_state_str(fuzzer)
46+
47+
self.assertIn('name: test_fuzzer', state_str)
48+
self.assertIn('revision: 1', state_str)
49+
self.assertIn('timeout: 10', state_str)
50+
51+
# Explicitly excluded fields
52+
self.assertNotIn('result:', state_str)
53+
self.assertNotIn('result_timestamp', state_str)
54+
self.assertNotIn('console_output:', state_str)
55+
self.assertNotIn('return_code:', state_str)
56+
self.assertNotIn('sample_testcase:', state_str)
57+
self.assertNotIn('stats_columns:', state_str)
58+
self.assertNotIn('stats_column_descriptions:', state_str)

src/clusterfuzz/_internal/tests/appengine/libs/helpers_test.py

Lines changed: 24 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -159,3 +159,27 @@ def test_view(self):
159159
helpers.log('message', helpers.VIEW_OPERATION)
160160
self.mock.info.assert_called_once_with(
161161
'ClusterFuzz: %s (%s): %s.', helpers.VIEW_OPERATION, 'email', 'message')
162+
163+
164+
class DiffTest(unittest.TestCase):
165+
"""Test diff."""
166+
167+
def test_diff_empty(self):
168+
"""Test diff with empty strings."""
169+
self.assertEqual(helpers.diff('', ''), '')
170+
171+
def test_diff_no_change(self):
172+
"""Test diff with no changes."""
173+
self.assertEqual(helpers.diff('a\nb\n', 'a\nb\n'), '')
174+
175+
def test_diff_addition(self):
176+
"""Test diff with addition."""
177+
self.assertEqual(helpers.diff('a\nb\n', 'a\nb\nc\n'), '+ c\n')
178+
179+
def test_diff_deletion(self):
180+
"""Test diff with deletion."""
181+
self.assertEqual(helpers.diff('a\nb\nc\n', 'a\nc\n'), '- b\n')
182+
183+
def test_diff_modification(self):
184+
"""Test diff with modification."""
185+
self.assertEqual(helpers.diff('a\nb\nc\n', 'a\nd\nc\n'), '- b\n+ d\n')

0 commit comments

Comments
 (0)