From c66e53248aa3b9d41ec0d84a89f3d1e915048fe8 Mon Sep 17 00:00:00 2001 From: PauloVLB Date: Fri, 10 Oct 2025 19:50:40 +0000 Subject: [PATCH 01/10] refactor: remove uncessary typing, add warning and download minimized keys --- .../tests/core/local/butler/reproduce_test.py | 33 +++++++++++++++++-- src/local/butler/reproduce.py | 24 +++++++++----- 2 files changed, 47 insertions(+), 10 deletions(-) diff --git a/src/clusterfuzz/_internal/tests/core/local/butler/reproduce_test.py b/src/clusterfuzz/_internal/tests/core/local/butler/reproduce_test.py index 2160a7306d..7faeae8283 100644 --- a/src/clusterfuzz/_internal/tests/core/local/butler/reproduce_test.py +++ b/src/clusterfuzz/_internal/tests/core/local/butler/reproduce_test.py @@ -197,8 +197,9 @@ def setUp(self): self.mock_testcase = mock.MagicMock(spec=Testcase) self.mock_testcase.fuzzed_keys = 'testcase_key' + self.mock_testcase.minimized_keys = None - def test_success(self): + def test_success_fuzzed_keys(self): """Test successful local setup of a testcase.""" self.mock._get_testcase_file_and_path.return_value = (mock.ANY, '/tmp/testcase') @@ -213,6 +214,22 @@ def test_success(self): self.mock.prepare_environment_for_testcase.assert_called_once_with( self.mock_testcase) + def test_success_minimized_keys(self): + """Test successful local setup of a testcase with minimized keys.""" + self.mock_testcase.minimized_keys = 'minimized_key' + self.mock._get_testcase_file_and_path.return_value = (mock.ANY, + '/tmp/testcase') + self.mock.read_blob_to_disk.return_value = True + + ok, path = reproduce._setup_testcase_locally(self.mock_testcase) + self.assertTrue(ok) + self.assertEqual(path, '/tmp/testcase') + self.mock.clear_testcase_directories.assert_called_once() + self.mock.read_blob_to_disk.assert_called_once_with('minimized_key', + '/tmp/testcase') + self.mock.prepare_environment_for_testcase.assert_called_once_with( + self.mock_testcase) + def test_clear_directories_fails(self): """Test handling an exception from clear_testcase_directories.""" self.mock.clear_testcase_directories.side_effect = Exception( @@ -223,7 +240,7 @@ def test_clear_directories_fails(self): self.mock_logs['error'].assert_called_with( 'Error clearing testcase directories: mock clear error') - def test_download_fails(self): + def test_download_fails_fuzzed_keys(self): """Test handling a download failure from read_blob_to_disk.""" self.mock._get_testcase_file_and_path.return_value = (mock.ANY, '/tmp/testcase') @@ -234,6 +251,18 @@ def test_download_fails(self): self.mock_logs['error'].assert_called_with( 'Failed to download testcase from blobstore: testcase_key') + def test_download_fails_minimized_keys(self): + """Test handling a download failure from read_blob_to_disk with minimized keys.""" + self.mock_testcase.minimized_keys = 'minimized_key' + self.mock._get_testcase_file_and_path.return_value = (mock.ANY, + '/tmp/testcase') + self.mock.read_blob_to_disk.return_value = False + ok, path = reproduce._setup_testcase_locally(self.mock_testcase) + self.assertFalse(ok) + self.assertIsNone(path) + self.mock_logs['error'].assert_called_with( + 'Failed to download testcase from blobstore: minimized_key') + def test_prepare_env_fails(self): """Test handling an exception from prepare_environment_for_testcase.""" self.mock._get_testcase_file_and_path.return_value = (mock.ANY, diff --git a/src/local/butler/reproduce.py b/src/local/butler/reproduce.py index e79292d687..07adbafa50 100644 --- a/src/local/butler/reproduce.py +++ b/src/local/butler/reproduce.py @@ -50,12 +50,16 @@ def _setup_fuzzer(fuzzer_name: str) -> bool: Returns: True if setup was successful, False otherwise. """ - fuzzer: Optional[Fuzzer] = data_types.Fuzzer.query( + fuzzer = data_types.Fuzzer.query( data_types.Fuzzer.name == fuzzer_name).get() if not fuzzer: logs.error(f'Fuzzer {fuzzer_name} not found.') return False + if fuzzer.untrusted_content: + logs.warning('You are about to run an untrusted fuzzer locally. ' + 'This can be dangerous.') + environment.set_value('UNTRUSTED_CONTENT', fuzzer.untrusted_content) if fuzzer.data_bundle_name: @@ -140,9 +144,12 @@ def _setup_testcase_locally(testcase: Testcase) -> Tuple[bool, Optional[str]]: try: _, testcase_file_path = setup._get_testcase_file_and_path(testcase) - if not blobs.read_blob_to_disk(testcase.fuzzed_keys, testcase_file_path): - logs.error('Failed to download testcase from blobstore: ' - f'{testcase.fuzzed_keys}') + if testcase.minimized_keys and testcase.minimized_keys != 'NA': + blob_key = testcase.minimized_keys + else: + blob_key = testcase.fuzzed_keys + if not blobs.read_blob_to_disk(blob_key, testcase_file_path): + logs.error(f'Failed to download testcase from blobstore: {blob_key}') # Returning None for path when download fails return False, None setup.prepare_environment_for_testcase(testcase) @@ -159,13 +166,13 @@ def _reproduce_testcase(args: argparse.Namespace) -> None: Args: args: Parsed command-line arguments. """ - testcase: Optional[Testcase] = data_handler.get_testcase_by_id( + testcase = data_handler.get_testcase_by_id( args.testcase_id) if not testcase: logs.error(f'Testcase with ID {args.testcase_id} not found.') return - job: Optional[Job] = data_types.Job.query( + job = data_types.Job.query( data_types.Job.name == testcase.job_type).get() if not job: logs.error(f'Job type {testcase.job_type} not found for testcase.') @@ -180,8 +187,9 @@ def _reproduce_testcase(args: argparse.Namespace) -> None: logs.error(f'Failed to setup fuzzer {testcase.fuzzer_name}. Exiting.') return - ok, testcase_file_path = _setup_testcase_locally(testcase) - if not ok or testcase_file_path is None: + testcase_setup_successful, testcase_file_path = _setup_testcase_locally( + testcase) + if not testcase_setup_successful or testcase_file_path is None: logs.error('Could not setup testcase locally. Exiting.') return From 6743445cb8f02a4b3a11b804cf5099f0fd50989b Mon Sep 17 00:00:00 2001 From: PauloVLB Date: Fri, 10 Oct 2025 19:54:31 +0000 Subject: [PATCH 02/10] refactor: remove non-modules imports --- src/local/butler/reproduce.py | 10 ++++------ 1 file changed, 4 insertions(+), 6 deletions(-) diff --git a/src/local/butler/reproduce.py b/src/local/butler/reproduce.py index 07adbafa50..6b3e607333 100644 --- a/src/local/butler/reproduce.py +++ b/src/local/butler/reproduce.py @@ -21,15 +21,13 @@ from clusterfuzz._internal.bot import testcase_manager from clusterfuzz._internal.bot.fuzzers import init from clusterfuzz._internal.bot.tasks import setup -from clusterfuzz._internal.bot.tasks.commands import update_environment_for_job +from clusterfuzz._internal.bot.tasks import commands from clusterfuzz._internal.build_management import build_manager from clusterfuzz._internal.config import local_config from clusterfuzz._internal.datastore import data_handler from clusterfuzz._internal.datastore import data_types from clusterfuzz._internal.datastore import ndb_init -from clusterfuzz._internal.datastore.data_types import Fuzzer -from clusterfuzz._internal.datastore.data_types import Job -from clusterfuzz._internal.datastore.data_types import Testcase +from clusterfuzz._internal.datastore import data_types from clusterfuzz._internal.google_cloud_utils import blobs from clusterfuzz._internal.metrics import logs from clusterfuzz._internal.protos import uworker_msg_pb2 @@ -125,7 +123,7 @@ def _setup_fuzzer(fuzzer_name: str) -> bool: return True -def _setup_testcase_locally(testcase: Testcase) -> Tuple[bool, Optional[str]]: +def _setup_testcase_locally(testcase: data_types.Testcase) -> Tuple[bool, Optional[str]]: """Sets up the testcase file locally. Args: @@ -181,7 +179,7 @@ def _reproduce_testcase(args: argparse.Namespace) -> None: # The job name is not set in update_environment_for_job, # so it was needed to manually set it here. environment.set_value('JOB_NAME', job.name) - update_environment_for_job(job.get_environment_string()) + commands.update_environment_for_job(job.get_environment_string()) if not _setup_fuzzer(testcase.fuzzer_name): logs.error(f'Failed to setup fuzzer {testcase.fuzzer_name}. Exiting.') From 7601d47c8b5bd1743d8be230946176ab84debea9 Mon Sep 17 00:00:00 2001 From: PauloVLB Date: Fri, 10 Oct 2025 20:20:07 +0000 Subject: [PATCH 03/10] refactor: moves setup reproduce logic and create local logging option in butler --- butler.py | 13 +++++++++++-- src/clusterfuzz/_internal/system/environment.py | 6 ++++++ src/local/butler/reproduce.py | 14 +++++++++----- 3 files changed, 26 insertions(+), 7 deletions(-) diff --git a/butler.py b/butler.py index 36d4df72db..2d9181f69b 100644 --- a/butler.py +++ b/butler.py @@ -249,6 +249,11 @@ def main(): """Parse the command-line args and invoke the right command.""" parser = _ArgumentParser( description='Butler is here to help you with command-line tasks.') + parser.add_argument( + '--local-logging', + action='store_true', + default=False, + help='Force logs to be local-only.') subparsers = parser.add_subparsers(dest='command') subparsers.add_parser( @@ -424,12 +429,12 @@ def main(): parser.print_help() return 0 - _setup() + _setup(args) command = importlib.import_module(f'local.butler.{args.command}') return command.execute(args) -def _setup(): +def _setup(args): """Set up configs and import paths.""" os.environ['ROOT_DIR'] = os.path.abspath('.') os.environ['PYTHONIOENCODING'] = 'UTF-8' @@ -438,6 +443,10 @@ def _setup(): from clusterfuzz._internal.base import modules modules.fix_module_search_paths() + if args.local_logging: + from clusterfuzz._internal.system import environment + environment.set_local_log_only() + if __name__ == '__main__': sys.exit(main()) diff --git a/src/clusterfuzz/_internal/system/environment.py b/src/clusterfuzz/_internal/system/environment.py index 6618a01190..66dce9615d 100644 --- a/src/clusterfuzz/_internal/system/environment.py +++ b/src/clusterfuzz/_internal/system/environment.py @@ -1001,6 +1001,12 @@ def set_bot_environment(): return True +def set_local_log_only(): + """Force logs to be local-only.""" + set_value('LOG_TO_GCP', '') + set_value('LOG_TO_CONSOLE', 'True') + + def set_tsan_max_history_size(): """Sets maximum history size for TSAN tool.""" tsan_options = get_value('TSAN_OPTIONS') diff --git a/src/local/butler/reproduce.py b/src/local/butler/reproduce.py index 6b3e607333..29c2eecd22 100644 --- a/src/local/butler/reproduce.py +++ b/src/local/butler/reproduce.py @@ -254,9 +254,8 @@ def _reproduce_testcase(args: argparse.Namespace) -> None: else: logs.info('The testcase does not reliably reproduce.') - -def execute(args: argparse.Namespace) -> None: - """Initializes the environment and reproduces a testcase locally. +def _setup_reproduce(args) -> None: + """Sets up the environment for reproducing a testcase. Args: args: Parsed command-line arguments. @@ -264,10 +263,15 @@ def execute(args: argparse.Namespace) -> None: os.environ['CONFIG_DIR_OVERRIDE'] = os.path.abspath(args.config_dir) local_config.ProjectConfig().set_environment() environment.set_bot_environment() - os.environ['LOG_TO_CONSOLE'] = 'True' - os.environ['LOG_TO_GCP'] = '' logs.configure('run_bot') init.run() +def execute(args: argparse.Namespace) -> None: + """Initializes the environment and reproduces a testcase locally. + + Args: + args: Parsed command-line arguments. + """ + _setup_reproduce(args) with ndb_init.context(): _reproduce_testcase(args) From 3810e853adc4f65fca44d436fb35d73fe1887ab9 Mon Sep 17 00:00:00 2001 From: PauloVLB Date: Mon, 13 Oct 2025 16:38:42 +0000 Subject: [PATCH 04/10] refactor: use create_auto_spec in tests --- .../tests/core/local/butler/reproduce_test.py | 18 +++++++++++------- 1 file changed, 11 insertions(+), 7 deletions(-) diff --git a/src/clusterfuzz/_internal/tests/core/local/butler/reproduce_test.py b/src/clusterfuzz/_internal/tests/core/local/butler/reproduce_test.py index 7faeae8283..4a022f27f5 100644 --- a/src/clusterfuzz/_internal/tests/core/local/butler/reproduce_test.py +++ b/src/clusterfuzz/_internal/tests/core/local/butler/reproduce_test.py @@ -25,6 +25,7 @@ from clusterfuzz._internal.datastore.data_types import Fuzzer from clusterfuzz._internal.datastore.data_types import Job from clusterfuzz._internal.datastore.data_types import Testcase +from clusterfuzz._internal.crash_analysis.crash_result import CrashResult from clusterfuzz._internal.google_cloud_utils import blobs from clusterfuzz._internal.metrics import logs from clusterfuzz._internal.protos import uworker_msg_pb2 @@ -70,11 +71,11 @@ def setUp(self): self.mock.remove_directory.return_value = True self.mock.read_blob_to_disk.return_value = True - self.mock_archive_reader = mock.MagicMock() + self.mock_archive_reader = mock.create_autospec(spec=archive.ArchiveReader, instance=True, spec_set=True) self.mock.open.return_value.__enter__.return_value = self.mock_archive_reader # Common mock fuzzer object - self.mock_fuzzer = mock.MagicMock(spec=Fuzzer) + self.mock_fuzzer = mock.create_autospec(spec=Fuzzer, instance=True, spec_set=True) self.mock_fuzzer.name = 'test_fuzzer' self.mock_fuzzer.builtin = False self.mock_fuzzer.data_bundle_name = 'test_bundle' @@ -195,7 +196,7 @@ def setUp(self): 'clusterfuzz._internal.bot.tasks.setup.prepare_environment_for_testcase' ]) - self.mock_testcase = mock.MagicMock(spec=Testcase) + self.mock_testcase = mock.create_autospec(spec=Testcase, spec_set=True, instance=True) self.mock_testcase.fuzzed_keys = 'testcase_key' self.mock_testcase.minimized_keys = None @@ -304,23 +305,26 @@ def setUp(self): 'clusterfuzz._internal.bot.untrusted_runner.host.stub', ]) - self.mock_testcase = mock.MagicMock(spec=Testcase) + self.mock_testcase = mock.create_autospec(spec=Testcase, instance=True, spec_set=True) self.mock_testcase.job_type = 'test_job' self.mock_testcase.fuzzer_name = 'test_fuzzer' self.mock_testcase.crash_revision = 12345 - self.mock_job = mock.MagicMock(spec=Job) + self.mock_job = mock.create_autospec(spec=Job, instance=True, spec_set=True) self.mock.get_testcase_by_id.return_value = self.mock_testcase self.mock.query.return_value.get.return_value = self.mock_job self.mock._setup_fuzzer.return_value = True self.mock._setup_testcase_locally.return_value = (True, '/tmp/testcase') - mock_build_result = mock.MagicMock(spec=uworker_msg_pb2.BuildData) + mock_build_result = mock.create_autospec( + spec=uworker_msg_pb2.BuildData, instance=True) mock_build_result.is_bad_build = False self.mock.check_for_bad_build.return_value = mock_build_result - self.mock_crash_result = mock.MagicMock() + self.mock_crash_result = mock.create_autospec( + spec=CrashResult, instance=True) self.mock_crash_result.is_crash.return_value = True + self.mock_crash_result.output = 'mock crash output' self.mock.test_for_crash_with_retries.return_value = self.mock_crash_result self.mock.test_for_reproducibility.return_value = True From 7f7866441d190d7df98cd531c3b689f0631c4fba Mon Sep 17 00:00:00 2001 From: PauloVLB Date: Mon, 13 Oct 2025 17:45:50 +0000 Subject: [PATCH 05/10] refactor: use real objects instead of mocks in tests --- .../tests/core/local/butler/reproduce_test.py | 86 +++++++++---------- 1 file changed, 42 insertions(+), 44 deletions(-) diff --git a/src/clusterfuzz/_internal/tests/core/local/butler/reproduce_test.py b/src/clusterfuzz/_internal/tests/core/local/butler/reproduce_test.py index 4a022f27f5..5757f40ebe 100644 --- a/src/clusterfuzz/_internal/tests/core/local/butler/reproduce_test.py +++ b/src/clusterfuzz/_internal/tests/core/local/butler/reproduce_test.py @@ -74,20 +74,20 @@ def setUp(self): self.mock_archive_reader = mock.create_autospec(spec=archive.ArchiveReader, instance=True, spec_set=True) self.mock.open.return_value.__enter__.return_value = self.mock_archive_reader - # Common mock fuzzer object - self.mock_fuzzer = mock.create_autospec(spec=Fuzzer, instance=True, spec_set=True) - self.mock_fuzzer.name = 'test_fuzzer' - self.mock_fuzzer.builtin = False - self.mock_fuzzer.data_bundle_name = 'test_bundle' - self.mock_fuzzer.launcher_script = None - self.mock_fuzzer.filename = 'fuzzer.zip' - self.mock_fuzzer.executable_path = 'fuzzer_exe' - self.mock_fuzzer.blobstore_key = 'some_key' - self.mock_get.return_value = self.mock_fuzzer + # Common fuzzer object + self.fuzzer = Fuzzer( + name='test_fuzzer', + builtin=False, + data_bundle_name='test_bundle', + launcher_script=None, + filename='fuzzer.zip', + executable_path='fuzzer_exe', + blobstore_key='some_key') + self.mock_get.return_value = self.fuzzer def test_setup_fuzzer_builtin_success(self): """Test successful setup of a builtin fuzzer.""" - self.mock_fuzzer.builtin = True + self.fuzzer.builtin = True self.assertTrue(reproduce._setup_fuzzer('builtin_fuzzer')) self.mock.set_value.assert_called_once() self.mock.remove_directory.assert_not_called() @@ -118,7 +118,7 @@ def test_fuzzer_not_found(self): def test_launcher_script_unsupported(self): """Test that fuzzers with launcher scripts are not supported.""" - self.mock_fuzzer.launcher_script = 'launcher.sh' + self.fuzzer.launcher_script = 'launcher.sh' self.assertFalse(reproduce._setup_fuzzer('launcher_fuzzer')) self.mock_logs['error'].assert_called_with( 'Fuzzers with launch script not supported yet.') @@ -196,9 +196,8 @@ def setUp(self): 'clusterfuzz._internal.bot.tasks.setup.prepare_environment_for_testcase' ]) - self.mock_testcase = mock.create_autospec(spec=Testcase, spec_set=True, instance=True) - self.mock_testcase.fuzzed_keys = 'testcase_key' - self.mock_testcase.minimized_keys = None + self.testcase = Testcase( + fuzzed_keys='testcase_key', minimized_keys=None) def test_success_fuzzed_keys(self): """Test successful local setup of a testcase.""" @@ -206,36 +205,36 @@ def test_success_fuzzed_keys(self): '/tmp/testcase') self.mock.read_blob_to_disk.return_value = True - ok, path = reproduce._setup_testcase_locally(self.mock_testcase) + ok, path = reproduce._setup_testcase_locally(self.testcase) self.assertTrue(ok) self.assertEqual(path, '/tmp/testcase') self.mock.clear_testcase_directories.assert_called_once() self.mock.read_blob_to_disk.assert_called_once_with('testcase_key', '/tmp/testcase') self.mock.prepare_environment_for_testcase.assert_called_once_with( - self.mock_testcase) + self.testcase) def test_success_minimized_keys(self): """Test successful local setup of a testcase with minimized keys.""" - self.mock_testcase.minimized_keys = 'minimized_key' + self.testcase.minimized_keys = 'minimized_key' self.mock._get_testcase_file_and_path.return_value = (mock.ANY, '/tmp/testcase') self.mock.read_blob_to_disk.return_value = True - ok, path = reproduce._setup_testcase_locally(self.mock_testcase) + ok, path = reproduce._setup_testcase_locally(self.testcase) self.assertTrue(ok) self.assertEqual(path, '/tmp/testcase') self.mock.clear_testcase_directories.assert_called_once() self.mock.read_blob_to_disk.assert_called_once_with('minimized_key', '/tmp/testcase') self.mock.prepare_environment_for_testcase.assert_called_once_with( - self.mock_testcase) + self.testcase) def test_clear_directories_fails(self): """Test handling an exception from clear_testcase_directories.""" self.mock.clear_testcase_directories.side_effect = Exception( 'mock clear error') - ok, path = reproduce._setup_testcase_locally(self.mock_testcase) + ok, path = reproduce._setup_testcase_locally(self.testcase) self.assertFalse(ok) self.assertIsNone(path) self.mock_logs['error'].assert_called_with( @@ -246,7 +245,7 @@ def test_download_fails_fuzzed_keys(self): self.mock._get_testcase_file_and_path.return_value = (mock.ANY, '/tmp/testcase') self.mock.read_blob_to_disk.return_value = False - ok, path = reproduce._setup_testcase_locally(self.mock_testcase) + ok, path = reproduce._setup_testcase_locally(self.testcase) self.assertFalse(ok) self.assertIsNone(path) self.mock_logs['error'].assert_called_with( @@ -254,11 +253,11 @@ def test_download_fails_fuzzed_keys(self): def test_download_fails_minimized_keys(self): """Test handling a download failure from read_blob_to_disk with minimized keys.""" - self.mock_testcase.minimized_keys = 'minimized_key' + self.testcase.minimized_keys = 'minimized_key' self.mock._get_testcase_file_and_path.return_value = (mock.ANY, '/tmp/testcase') self.mock.read_blob_to_disk.return_value = False - ok, path = reproduce._setup_testcase_locally(self.mock_testcase) + ok, path = reproduce._setup_testcase_locally(self.testcase) self.assertFalse(ok) self.assertIsNone(path) self.mock_logs['error'].assert_called_with( @@ -271,7 +270,7 @@ def test_prepare_env_fails(self): self.mock.read_blob_to_disk.return_value = True self.mock.prepare_environment_for_testcase.side_effect = Exception( 'mock prepare error') - ok, path = reproduce._setup_testcase_locally(self.mock_testcase) + ok, path = reproduce._setup_testcase_locally(self.testcase) self.assertFalse(ok) self.assertIsNone(path) self.mock_logs['error'].assert_called_with( @@ -305,27 +304,26 @@ def setUp(self): 'clusterfuzz._internal.bot.untrusted_runner.host.stub', ]) - self.mock_testcase = mock.create_autospec(spec=Testcase, instance=True, spec_set=True) - self.mock_testcase.job_type = 'test_job' - self.mock_testcase.fuzzer_name = 'test_fuzzer' - self.mock_testcase.crash_revision = 12345 - self.mock_job = mock.create_autospec(spec=Job, instance=True, spec_set=True) - self.mock.get_testcase_by_id.return_value = self.mock_testcase - self.mock.query.return_value.get.return_value = self.mock_job + self.testcase = Testcase( + job_type='test_job', + fuzzer_name='test_fuzzer', + crash_revision=12345) + self.testcase.get_fuzz_target = mock.Mock() + self.job = Job(name='test_job') + self.mock.get_testcase_by_id.return_value = self.testcase + self.mock.query.return_value.get.return_value = self.job self.mock._setup_fuzzer.return_value = True self.mock._setup_testcase_locally.return_value = (True, '/tmp/testcase') - mock_build_result = mock.create_autospec( - spec=uworker_msg_pb2.BuildData, instance=True) - mock_build_result.is_bad_build = False - self.mock.check_for_bad_build.return_value = mock_build_result + build_result = uworker_msg_pb2.BuildData(is_bad_build=False) + self.mock.check_for_bad_build.return_value = build_result - self.mock_crash_result = mock.create_autospec( - spec=CrashResult, instance=True) - self.mock_crash_result.is_crash.return_value = True - self.mock_crash_result.output = 'mock crash output' - self.mock.test_for_crash_with_retries.return_value = self.mock_crash_result + # The crash result needs to be a real object, but we need to control the + # return value of is_crash(). + crash_result = CrashResult(1, 1, 'mock crash output') + crash_result.is_crash = mock.Mock(return_value=True) + self.mock.test_for_crash_with_retries.return_value = crash_result self.mock.test_for_reproducibility.return_value = True @@ -366,7 +364,7 @@ def test_job_not_found(self): self.mock.query.return_value.get.return_value = None reproduce._reproduce_testcase(self.args) self.mock_logs['error'].assert_called_with( - f'Job type {self.mock_testcase.job_type} not found for testcase.') + f'Job type {self.testcase.job_type} not found for testcase.') self.mock._setup_fuzzer.assert_not_called() def test_setup_fuzzer_fails(self): @@ -374,7 +372,7 @@ def test_setup_fuzzer_fails(self): self.mock._setup_fuzzer.return_value = False reproduce._reproduce_testcase(self.args) self.mock_logs['error'].assert_called_with( - f'Failed to setup fuzzer {self.mock_testcase.fuzzer_name}. Exiting.') + f'Failed to setup fuzzer {self.testcase.fuzzer_name}. Exiting.') self.mock._setup_testcase_locally.assert_not_called() def test_setup_testcase_fails(self): @@ -391,7 +389,7 @@ def test_setup_build_fails(self): reproduce._reproduce_testcase(self.args) self.mock_logs['error'].assert_called_with( f'Error setting up build for revision ' - f'{self.mock_testcase.crash_revision}: mock build error') + f'{self.testcase.crash_revision}: mock build error') self.mock.check_for_bad_build.assert_not_called() def test_bad_build(self): From 0724c3d07267c89d00c0e604632cedc87ab0b530 Mon Sep 17 00:00:00 2001 From: PauloVLB Date: Mon, 13 Oct 2025 18:48:01 +0000 Subject: [PATCH 06/10] refactor: change setup_testcase return and tests accordingly --- .../tests/core/local/butler/reproduce_test.py | 33 +++++++------------ src/local/butler/reproduce.py | 15 ++++----- 2 files changed, 19 insertions(+), 29 deletions(-) diff --git a/src/clusterfuzz/_internal/tests/core/local/butler/reproduce_test.py b/src/clusterfuzz/_internal/tests/core/local/butler/reproduce_test.py index 5757f40ebe..add914d884 100644 --- a/src/clusterfuzz/_internal/tests/core/local/butler/reproduce_test.py +++ b/src/clusterfuzz/_internal/tests/core/local/butler/reproduce_test.py @@ -22,9 +22,6 @@ from clusterfuzz._internal.bot.tasks.commands import update_environment_for_job from clusterfuzz._internal.datastore import data_handler from clusterfuzz._internal.datastore import data_types -from clusterfuzz._internal.datastore.data_types import Fuzzer -from clusterfuzz._internal.datastore.data_types import Job -from clusterfuzz._internal.datastore.data_types import Testcase from clusterfuzz._internal.crash_analysis.crash_result import CrashResult from clusterfuzz._internal.google_cloud_utils import blobs from clusterfuzz._internal.metrics import logs @@ -75,7 +72,7 @@ def setUp(self): self.mock.open.return_value.__enter__.return_value = self.mock_archive_reader # Common fuzzer object - self.fuzzer = Fuzzer( + self.fuzzer = data_types.Fuzzer( name='test_fuzzer', builtin=False, data_bundle_name='test_bundle', @@ -196,7 +193,7 @@ def setUp(self): 'clusterfuzz._internal.bot.tasks.setup.prepare_environment_for_testcase' ]) - self.testcase = Testcase( + self.testcase = data_types.Testcase( fuzzed_keys='testcase_key', minimized_keys=None) def test_success_fuzzed_keys(self): @@ -205,8 +202,7 @@ def test_success_fuzzed_keys(self): '/tmp/testcase') self.mock.read_blob_to_disk.return_value = True - ok, path = reproduce._setup_testcase_locally(self.testcase) - self.assertTrue(ok) + path = reproduce._setup_testcase_locally(self.testcase) self.assertEqual(path, '/tmp/testcase') self.mock.clear_testcase_directories.assert_called_once() self.mock.read_blob_to_disk.assert_called_once_with('testcase_key', @@ -221,8 +217,7 @@ def test_success_minimized_keys(self): '/tmp/testcase') self.mock.read_blob_to_disk.return_value = True - ok, path = reproduce._setup_testcase_locally(self.testcase) - self.assertTrue(ok) + path = reproduce._setup_testcase_locally(self.testcase) self.assertEqual(path, '/tmp/testcase') self.mock.clear_testcase_directories.assert_called_once() self.mock.read_blob_to_disk.assert_called_once_with('minimized_key', @@ -234,8 +229,7 @@ def test_clear_directories_fails(self): """Test handling an exception from clear_testcase_directories.""" self.mock.clear_testcase_directories.side_effect = Exception( 'mock clear error') - ok, path = reproduce._setup_testcase_locally(self.testcase) - self.assertFalse(ok) + path = reproduce._setup_testcase_locally(self.testcase) self.assertIsNone(path) self.mock_logs['error'].assert_called_with( 'Error clearing testcase directories: mock clear error') @@ -245,8 +239,7 @@ def test_download_fails_fuzzed_keys(self): self.mock._get_testcase_file_and_path.return_value = (mock.ANY, '/tmp/testcase') self.mock.read_blob_to_disk.return_value = False - ok, path = reproduce._setup_testcase_locally(self.testcase) - self.assertFalse(ok) + path = reproduce._setup_testcase_locally(self.testcase) self.assertIsNone(path) self.mock_logs['error'].assert_called_with( 'Failed to download testcase from blobstore: testcase_key') @@ -257,8 +250,7 @@ def test_download_fails_minimized_keys(self): self.mock._get_testcase_file_and_path.return_value = (mock.ANY, '/tmp/testcase') self.mock.read_blob_to_disk.return_value = False - ok, path = reproduce._setup_testcase_locally(self.testcase) - self.assertFalse(ok) + path = reproduce._setup_testcase_locally(self.testcase) self.assertIsNone(path) self.mock_logs['error'].assert_called_with( 'Failed to download testcase from blobstore: minimized_key') @@ -270,8 +262,7 @@ def test_prepare_env_fails(self): self.mock.read_blob_to_disk.return_value = True self.mock.prepare_environment_for_testcase.side_effect = Exception( 'mock prepare error') - ok, path = reproduce._setup_testcase_locally(self.testcase) - self.assertFalse(ok) + path = reproduce._setup_testcase_locally(self.testcase) self.assertIsNone(path) self.mock_logs['error'].assert_called_with( 'Error setting up testcase locally: mock prepare error') @@ -304,17 +295,17 @@ def setUp(self): 'clusterfuzz._internal.bot.untrusted_runner.host.stub', ]) - self.testcase = Testcase( + self.testcase = data_types.Testcase( job_type='test_job', fuzzer_name='test_fuzzer', crash_revision=12345) self.testcase.get_fuzz_target = mock.Mock() - self.job = Job(name='test_job') + self.job = data_types.Job(name='test_job') self.mock.get_testcase_by_id.return_value = self.testcase self.mock.query.return_value.get.return_value = self.job self.mock._setup_fuzzer.return_value = True - self.mock._setup_testcase_locally.return_value = (True, '/tmp/testcase') + self.mock._setup_testcase_locally.return_value = '/tmp/testcase' build_result = uworker_msg_pb2.BuildData(is_bad_build=False) self.mock.check_for_bad_build.return_value = build_result @@ -377,7 +368,7 @@ def test_setup_fuzzer_fails(self): def test_setup_testcase_fails(self): """Test that it exits when testcase setup fails.""" - self.mock._setup_testcase_locally.return_value = (False, None) + self.mock._setup_testcase_locally.return_value = None reproduce._reproduce_testcase(self.args) self.mock_logs['error'].assert_called_with( 'Could not setup testcase locally. Exiting.') diff --git a/src/local/butler/reproduce.py b/src/local/butler/reproduce.py index 29c2eecd22..b25592ef28 100644 --- a/src/local/butler/reproduce.py +++ b/src/local/butler/reproduce.py @@ -123,7 +123,7 @@ def _setup_fuzzer(fuzzer_name: str) -> bool: return True -def _setup_testcase_locally(testcase: data_types.Testcase) -> Tuple[bool, Optional[str]]: +def _setup_testcase_locally(testcase: data_types.Testcase) -> str | None: """Sets up the testcase file locally. Args: @@ -138,7 +138,7 @@ def _setup_testcase_locally(testcase: data_types.Testcase) -> Tuple[bool, Option shell.clear_testcase_directories() except Exception as e: logs.error(f'Error clearing testcase directories: {e}') - return False, None + return None try: _, testcase_file_path = setup._get_testcase_file_and_path(testcase) @@ -149,13 +149,13 @@ def _setup_testcase_locally(testcase: data_types.Testcase) -> Tuple[bool, Option if not blobs.read_blob_to_disk(blob_key, testcase_file_path): logs.error(f'Failed to download testcase from blobstore: {blob_key}') # Returning None for path when download fails - return False, None + return None setup.prepare_environment_for_testcase(testcase) except Exception as e: logs.error(f'Error setting up testcase locally: {e}') - return False, None + return None - return True, testcase_file_path + return testcase_file_path def _reproduce_testcase(args: argparse.Namespace) -> None: @@ -185,9 +185,8 @@ def _reproduce_testcase(args: argparse.Namespace) -> None: logs.error(f'Failed to setup fuzzer {testcase.fuzzer_name}. Exiting.') return - testcase_setup_successful, testcase_file_path = _setup_testcase_locally( - testcase) - if not testcase_setup_successful or testcase_file_path is None: + testcase_file_path = _setup_testcase_locally(testcase) + if testcase_file_path is None: logs.error('Could not setup testcase locally. Exiting.') return From bf40023d100c29c2d2f3d4de77be6faa45ccfa94 Mon Sep 17 00:00:00 2001 From: PauloVLB Date: Tue, 14 Oct 2025 18:45:19 +0000 Subject: [PATCH 07/10] refactor: add docstrings to setup methods --- src/clusterfuzz/_internal/bot/tasks/setup.py | 2 ++ 1 file changed, 2 insertions(+) diff --git a/src/clusterfuzz/_internal/bot/tasks/setup.py b/src/clusterfuzz/_internal/bot/tasks/setup.py index 0a46472bf2..3800946574 100644 --- a/src/clusterfuzz/_internal/bot/tasks/setup.py +++ b/src/clusterfuzz/_internal/bot/tasks/setup.py @@ -352,10 +352,12 @@ def _get_testcase_key_and_archive_status(testcase): def _is_testcase_minimized(testcase): + """Returns True if the testcase is minimized.""" return testcase.minimized_keys and testcase.minimized_keys != 'NA' def download_testcase(testcase_download_url, dst): + """Downloads a testcase from a signed url""" return storage.download_signed_url_to_file(testcase_download_url, dst) From 0cd6c832c3710f1d06e2d74779cd5d20c08abc12 Mon Sep 17 00:00:00 2001 From: PauloVLB Date: Tue, 14 Oct 2025 19:52:47 +0000 Subject: [PATCH 08/10] refactor: move setup methods from reproduce and tests --- src/clusterfuzz/_internal/bot/tasks/setup.py | 121 +++++++- .../tests/core/bot/tasks/setup_test.py | 236 ++++++++++++++++ .../tests/core/local/butler/reproduce_test.py | 259 +----------------- src/local/butler/reproduce.py | 146 +--------- 4 files changed, 378 insertions(+), 384 deletions(-) diff --git a/src/clusterfuzz/_internal/bot/tasks/setup.py b/src/clusterfuzz/_internal/bot/tasks/setup.py index 3800946574..4d50aa2789 100644 --- a/src/clusterfuzz/_internal/bot/tasks/setup.py +++ b/src/clusterfuzz/_internal/bot/tasks/setup.py @@ -49,7 +49,7 @@ _DATA_BUNDLE_SYNC_INTERVAL_IN_SECONDS = 6 * 60 * 60 _SYNC_FILENAME = '.sync' _TESTCASE_ARCHIVE_EXTENSION = '.zip' - +_EXECUTABLE_PERMISSIONS = 0o750 def _set_timeout_value_from_user_upload(testcase_id, uworker_env): """Get the timeout associated with this testcase.""" @@ -653,7 +653,7 @@ def _update_fuzzer( return False # Make fuzzer executable. - os.chmod(fuzzer_path, 0o750) + os.chmod(fuzzer_path, _EXECUTABLE_PERMISSIONS) # Cleanup unneeded archive. shell.remove_file(archive_path) @@ -892,3 +892,120 @@ def archive_testcase_and_dependencies_in_gcs(resource_list, testcase_path: str, shell.remove_file(zip_path) return archived, absolute_filename, zip_filename + +def setup_local_testcase(testcase: data_types.Testcase) -> str | None: + """Sets up the testcase file locally. + + Args: + testcase: The Testcase object. + + Returns: + A tuple containing: + - bool: True if the testcase was downloaded successfully. + - Optional[str]: The local file path to the testcase, or None on failure. + """ + try: + shell.clear_testcase_directories() + except Exception as e: + logs.error(f'Error clearing testcase directories: {e}') + return None + + try: + _, testcase_file_path = _get_testcase_file_and_path(testcase) + if testcase.minimized_keys and testcase.minimized_keys != 'NA': + blob_key = testcase.minimized_keys + else: + blob_key = testcase.fuzzed_keys + if not blobs.read_blob_to_disk(blob_key, testcase_file_path): + logs.error(f'Failed to download testcase from blobstore: {blob_key}') + # Returning None for path when download fails + return None + prepare_environment_for_testcase(testcase) + except Exception as e: + logs.error(f'Error setting up testcase locally: {e}') + return None + + return testcase_file_path + +def setup_local_fuzzer(fuzzer_name: str) -> bool: + """Sets up the fuzzer binaries and environment. + + Args: + fuzzer_name: The name of the fuzzer to set up. + + Returns: + True if setup was successful, False otherwise. + """ + fuzzer = data_types.Fuzzer.query( + data_types.Fuzzer.name == fuzzer_name).get() + if not fuzzer: + logs.error(f'Fuzzer {fuzzer_name} not found.') + return False + + if fuzzer.untrusted_content: + logs.warning('You are about to run an untrusted fuzzer locally. ' + 'This can be dangerous.') + + environment.set_value('UNTRUSTED_CONTENT', fuzzer.untrusted_content) + + if fuzzer.data_bundle_name: + logs.info( + f'Fuzzer {fuzzer_name} uses data bundle: {fuzzer.data_bundle_name}') + + if fuzzer.launcher_script: + logs.error('Fuzzers with launch script not supported yet.') + return False + + if fuzzer.builtin: + logs.info(f'Fuzzer {fuzzer_name} is builtin, no setup required.') + return True + + fuzzer_directory: str = get_fuzzer_directory(fuzzer.name) + + try: + if not shell.remove_directory(fuzzer_directory, recreate=True): + logs.error(f'Failed to clear fuzzer directory: {fuzzer_directory}') + return False + except Exception as e: + logs.error(f'Error clearing fuzzer directory {fuzzer_directory}: {e}') + return False + + archive_path = os.path.join(fuzzer_directory, fuzzer.filename) + if not blobs.read_blob_to_disk(fuzzer.blobstore_key, archive_path): + logs.error('Failed to download fuzzer archive from blobstore: ' + f'{fuzzer.blobstore_key}') + return False + + try: + with archive.open(archive_path) as reader: + reader.extract_all(fuzzer_directory) + except archive.ArchiveError as e: + logs.error(f'Failed to unpack fuzzer archive {fuzzer.filename}: {e}') + return False + except Exception as e: + logs.error( + f'Unexpected error unpacking fuzzer archive {fuzzer.filename}: {e}') + return False + finally: + if os.path.exists(archive_path): + try: + shell.remove_file(archive_path) + except Exception as e: + logs.warning( + f'Failed to remove temporary archive file {archive_path}: {e}') + + fuzzer_path = os.path.join(fuzzer_directory, fuzzer.executable_path) + if not os.path.exists(fuzzer_path): + logs.error( + f'Fuzzer executable {fuzzer.executable_path} not found in archive. ' + 'Check fuzzer configuration.') + return False + + try: + os.chmod(fuzzer_path, _EXECUTABLE_PERMISSIONS) + except OSError as e: + logs.error( + f'Failed to set permissions on fuzzer executable {fuzzer_path}: {e}') + return False + + return True \ No newline at end of file diff --git a/src/clusterfuzz/_internal/tests/core/bot/tasks/setup_test.py b/src/clusterfuzz/_internal/tests/core/bot/tasks/setup_test.py index b0c0c3c97b..084dd00ef2 100644 --- a/src/clusterfuzz/_internal/tests/core/bot/tasks/setup_test.py +++ b/src/clusterfuzz/_internal/tests/core/bot/tasks/setup_test.py @@ -25,6 +25,8 @@ from clusterfuzz._internal.system import environment from clusterfuzz._internal.tests.test_libs import helpers from clusterfuzz._internal.tests.test_libs import test_utils +from clusterfuzz._internal.system import archive + # pylint: disable=protected-access @@ -235,3 +237,237 @@ def test_data_bundles(self): self.fuzzer_name) setup.update_fuzzer_and_data_bundles(setup_input) self.assertEqual(self.mock.update_data_bundle.call_count, 2) + +class SetupLocalFuzzerTest(unittest.TestCase): + """Tests for the setup_local_fuzzer function.""" + + def setUp(self): + super().setUp() + helpers.patch_environ(self) + self.addCleanup(unittest.mock.patch.stopall) + + self.mock_logs = unittest.mock.patch.multiple( + 'local.butler.reproduce.logs', + error=unittest.mock.DEFAULT, + info=unittest.mock.DEFAULT, + warning=unittest.mock.DEFAULT).start() + + helpers.patch(self, [ + 'clusterfuzz._internal.datastore.data_types.Fuzzer.query', + 'clusterfuzz._internal.system.environment.set_value', + 'clusterfuzz._internal.bot.tasks.setup.get_fuzzer_directory', + 'clusterfuzz._internal.system.shell.remove_directory', + 'clusterfuzz._internal.google_cloud_utils.blobs.read_blob_to_disk', + 'clusterfuzz._internal.system.archive.open', + 'clusterfuzz._internal.system.shell.remove_file', + 'os.path.exists', + 'os.chmod', + ]) + + # Alias for clarity + self.mock_fuzzer_query = self.mock.query + self.mock_get = self.mock_fuzzer_query.return_value.get + + # Default return values for success paths + self.mock.get_fuzzer_directory.return_value = '/tmp/fuzzer_dir' + self.mock.remove_directory.return_value = True + self.mock.read_blob_to_disk.return_value = True + + self.mock_archive_reader = unittest.mock.create_autospec(spec=archive.ArchiveReader, instance=True, spec_set=True) + self.mock.open.return_value.__enter__.return_value = self.mock_archive_reader + + # Common fuzzer object + self.fuzzer = data_types.Fuzzer( + name='test_fuzzer', + builtin=False, + data_bundle_name='test_bundle', + launcher_script=None, + filename='fuzzer.zip', + executable_path='fuzzer_exe', + blobstore_key='some_key') + self.mock_get.return_value = self.fuzzer + + def test_setup_fuzzer_builtin_success(self): + """Test successful setup of a builtin fuzzer.""" + self.fuzzer.builtin = True + self.assertTrue(setup.setup_local_fuzzer('builtin_fuzzer')) + self.mock.set_value.assert_called_once() + self.mock.remove_directory.assert_not_called() + self.mock_logs['info'].assert_any_call( + 'Fuzzer builtin_fuzzer is builtin, no setup required.') + + def test_setup_fuzzer_external_success(self): + """Test successful setup of an external fuzzer.""" + self.mock.exists.side_effect = [True, True] # archive, executable + self.assertTrue(setup.setup_local_fuzzer('external_fuzzer')) + self.mock.remove_directory.assert_called_once_with( + '/tmp/fuzzer_dir', recreate=True) + self.mock.read_blob_to_disk.assert_called_once_with( + 'some_key', '/tmp/fuzzer_dir/fuzzer.zip') + self.mock.open.assert_called_once_with('/tmp/fuzzer_dir/fuzzer.zip') + self.mock_archive_reader.extract_all.assert_called_once_with( + '/tmp/fuzzer_dir') + self.mock.remove_file.assert_called_once_with('/tmp/fuzzer_dir/fuzzer.zip') + self.mock.chmod.assert_called_once_with('/tmp/fuzzer_dir/fuzzer_exe', + setup._EXECUTABLE_PERMISSIONS) + + def test_fuzzer_not_found(self): + """Test when the fuzzer is not found in the database.""" + self.mock_get.return_value = None + self.assertFalse(setup.setup_local_fuzzer('nonexistent_fuzzer')) + self.mock_logs['error'].assert_called_with( + 'Fuzzer nonexistent_fuzzer not found.') + + def test_launcher_script_unsupported(self): + """Test that fuzzers with launcher scripts are not supported.""" + self.fuzzer.launcher_script = 'launcher.sh' + self.assertFalse(setup.setup_local_fuzzer('launcher_fuzzer')) + self.mock_logs['error'].assert_called_with( + 'Fuzzers with launch script not supported yet.') + + def test_remove_directory_fails(self): + """Test failure when clearing the fuzzer directory.""" + self.mock.remove_directory.return_value = False + self.assertFalse(setup.setup_local_fuzzer('external_fuzzer')) + self.mock_logs['error'].assert_called_with( + 'Failed to clear fuzzer directory: /tmp/fuzzer_dir') + + def test_remove_directory_exception(self): + """Test exception when clearing the fuzzer directory.""" + self.mock.remove_directory.side_effect = Exception('mock remove error') + self.assertFalse(setup.setup_local_fuzzer('external_fuzzer')) + self.mock_logs['error'].assert_called_with( + 'Error clearing fuzzer directory /tmp/fuzzer_dir: mock remove error') + + def test_download_archive_fails(self): + """Test failure when downloading the fuzzer archive.""" + self.mock.read_blob_to_disk.return_value = False + self.assertFalse(setup.setup_local_fuzzer('external_fuzzer')) + self.mock_logs['error'].assert_called_with( + 'Failed to download fuzzer archive from blobstore: some_key') + + def test_unpack_archive_fails_archiveerror(self): + """Test failure when unpacking the fuzzer archive with ArchiveError.""" + self.mock.open.side_effect = archive.ArchiveError('mock unpack error') + self.assertFalse(setup.setup_local_fuzzer('external_fuzzer')) + self.mock_logs['error'].assert_called_with( + 'Failed to unpack fuzzer archive fuzzer.zip: mock unpack error') + + def test_unpack_archive_fails_exception(self): + """Test failure when unpacking with a generic exception.""" + self.mock.open.side_effect = Exception('mock generic error') + self.assertFalse(setup.setup_local_fuzzer('external_fuzzer')) + self.mock_logs['error'].assert_called_with( + 'Unexpected error unpacking fuzzer archive fuzzer.zip: mock generic error' + ) + + def test_executable_not_found(self): + """Test when the executable is not found after unpacking.""" + self.mock.exists.side_effect = [True, False] # archive, executable + self.assertFalse(setup.setup_local_fuzzer('external_fuzzer')) + self.mock_logs['error'].assert_called_with( + 'Fuzzer executable fuzzer_exe not found in archive. Check fuzzer configuration.' + ) + + def test_chmod_fails(self): + """Test failure when setting permissions on the executable.""" + self.mock.exists.side_effect = [True, True] + self.mock.chmod.side_effect = OSError('mock chmod error') + self.assertFalse(setup.setup_local_fuzzer('external_fuzzer')) + self.mock_logs['error'].assert_called_with( + 'Failed to set permissions on fuzzer executable /tmp/fuzzer_dir/fuzzer_exe: mock chmod error' + ) + + +class SetupLocalTestcaseTest(unittest.TestCase): + """Tests for the setup_local_testcase function.""" + + def setUp(self): + super().setUp() + self.addCleanup(unittest.mock.patch.stopall) + self.mock_logs = unittest.mock.patch.multiple( + 'local.butler.reproduce.logs', + error=unittest.mock.DEFAULT, + info=unittest.mock.DEFAULT, + warning=unittest.mock.DEFAULT).start() + + helpers.patch(self, [ + 'clusterfuzz._internal.system.shell.clear_testcase_directories', + 'clusterfuzz._internal.bot.tasks.setup._get_testcase_file_and_path', + 'clusterfuzz._internal.google_cloud_utils.blobs.read_blob_to_disk', + 'clusterfuzz._internal.bot.tasks.setup.prepare_environment_for_testcase' + ]) + + self.testcase = data_types.Testcase( + fuzzed_keys='testcase_key', minimized_keys=None) + + def test_success_fuzzed_keys(self): + """Test successful local setup of a testcase.""" + self.mock._get_testcase_file_and_path.return_value = (unittest.mock.ANY, + '/tmp/testcase') + self.mock.read_blob_to_disk.return_value = True + + path = setup.setup_local_testcase(self.testcase) + self.assertEqual(path, '/tmp/testcase') + self.mock.clear_testcase_directories.assert_called_once() + self.mock.read_blob_to_disk.assert_called_once_with('testcase_key', + '/tmp/testcase') + self.mock.prepare_environment_for_testcase.assert_called_once_with( + self.testcase) + + def test_success_minimized_keys(self): + """Test successful local setup of a testcase with minimized keys.""" + self.testcase.minimized_keys = 'minimized_key' + self.mock._get_testcase_file_and_path.return_value = (unittest.mock.ANY, + '/tmp/testcase') + self.mock.read_blob_to_disk.return_value = True + + path = setup.setup_local_testcase(self.testcase) + self.assertEqual(path, '/tmp/testcase') + self.mock.clear_testcase_directories.assert_called_once() + self.mock.read_blob_to_disk.assert_called_once_with('minimized_key', + '/tmp/testcase') + self.mock.prepare_environment_for_testcase.assert_called_once_with( + self.testcase) + + def test_clear_directories_fails(self): + """Test handling an exception from clear_testcase_directories.""" + self.mock.clear_testcase_directories.side_effect = Exception( + 'mock clear error') + path = setup.setup_local_testcase(self.testcase) + self.assertIsNone(path) + self.mock_logs['error'].assert_called_with( + 'Error clearing testcase directories: mock clear error') + + def test_download_fails_fuzzed_keys(self): + """Test handling a download failure from read_blob_to_disk.""" + self.mock._get_testcase_file_and_path.return_value = (unittest.mock.ANY, + '/tmp/testcase') + self.mock.read_blob_to_disk.return_value = False + path = setup.setup_local_testcase(self.testcase) + self.assertIsNone(path) + self.mock_logs['error'].assert_called_with( + 'Failed to download testcase from blobstore: testcase_key') + + def test_download_fails_minimized_keys(self): + """Test handling a download failure from read_blob_to_disk with minimized keys.""" + self.testcase.minimized_keys = 'minimized_key' + self.mock._get_testcase_file_and_path.return_value = (unittest.mock.ANY, + '/tmp/testcase') + self.mock.read_blob_to_disk.return_value = False + path = setup.setup_local_testcase(self.testcase) + self.assertIsNone(path) + self.mock_logs['error'].assert_called_with( + 'Failed to download testcase from blobstore: minimized_key') + + def test_prepare_env_fails(self): + """Test handling an exception from prepare_environment_for_testcase.""" + self.mock._get_testcase_file_and_path.return_value = (unittest.mock.ANY, + '/tmp/testcase') + self.mock.read_blob_to_disk.return_value = True + self.mock.prepare_environment_for_testcase.side_effect = Exception( + 'mock prepare error') + path = setup.setup_local_testcase(self.testcase) + self.assertIsNone(path) + self.mock_logs['error'].assert_called_with( + 'Error setting up testcase locally: mock prepare error') \ No newline at end of file diff --git a/src/clusterfuzz/_internal/tests/core/local/butler/reproduce_test.py b/src/clusterfuzz/_internal/tests/core/local/butler/reproduce_test.py index add914d884..638974583b 100644 --- a/src/clusterfuzz/_internal/tests/core/local/butler/reproduce_test.py +++ b/src/clusterfuzz/_internal/tests/core/local/butler/reproduce_test.py @@ -33,241 +33,6 @@ from local.butler import reproduce -class SetupFuzzerTest(unittest.TestCase): - """Tests for the _setup_fuzzer function.""" - - def setUp(self): - super().setUp() - helpers.patch_environ(self) - self.addCleanup(mock.patch.stopall) - - self.mock_logs = mock.patch.multiple( - 'local.butler.reproduce.logs', - error=mock.DEFAULT, - info=mock.DEFAULT, - warning=mock.DEFAULT).start() - - helpers.patch(self, [ - 'clusterfuzz._internal.datastore.data_types.Fuzzer.query', - 'clusterfuzz._internal.system.environment.set_value', - 'clusterfuzz._internal.bot.tasks.setup.get_fuzzer_directory', - 'clusterfuzz._internal.system.shell.remove_directory', - 'clusterfuzz._internal.google_cloud_utils.blobs.read_blob_to_disk', - 'clusterfuzz._internal.system.archive.open', - 'clusterfuzz._internal.system.shell.remove_file', - 'os.path.exists', - 'os.chmod', - ]) - - # Alias for clarity - self.mock_fuzzer_query = self.mock.query - self.mock_get = self.mock_fuzzer_query.return_value.get - - # Default return values for success paths - self.mock.get_fuzzer_directory.return_value = '/tmp/fuzzer_dir' - self.mock.remove_directory.return_value = True - self.mock.read_blob_to_disk.return_value = True - - self.mock_archive_reader = mock.create_autospec(spec=archive.ArchiveReader, instance=True, spec_set=True) - self.mock.open.return_value.__enter__.return_value = self.mock_archive_reader - - # Common fuzzer object - self.fuzzer = data_types.Fuzzer( - name='test_fuzzer', - builtin=False, - data_bundle_name='test_bundle', - launcher_script=None, - filename='fuzzer.zip', - executable_path='fuzzer_exe', - blobstore_key='some_key') - self.mock_get.return_value = self.fuzzer - - def test_setup_fuzzer_builtin_success(self): - """Test successful setup of a builtin fuzzer.""" - self.fuzzer.builtin = True - self.assertTrue(reproduce._setup_fuzzer('builtin_fuzzer')) - self.mock.set_value.assert_called_once() - self.mock.remove_directory.assert_not_called() - self.mock_logs['info'].assert_any_call( - 'Fuzzer builtin_fuzzer is builtin, no setup required.') - - def test_setup_fuzzer_external_success(self): - """Test successful setup of an external fuzzer.""" - self.mock.exists.side_effect = [True, True] # archive, executable - self.assertTrue(reproduce._setup_fuzzer('external_fuzzer')) - self.mock.remove_directory.assert_called_once_with( - '/tmp/fuzzer_dir', recreate=True) - self.mock.read_blob_to_disk.assert_called_once_with( - 'some_key', '/tmp/fuzzer_dir/fuzzer.zip') - self.mock.open.assert_called_once_with('/tmp/fuzzer_dir/fuzzer.zip') - self.mock_archive_reader.extract_all.assert_called_once_with( - '/tmp/fuzzer_dir') - self.mock.remove_file.assert_called_once_with('/tmp/fuzzer_dir/fuzzer.zip') - self.mock.chmod.assert_called_once_with('/tmp/fuzzer_dir/fuzzer_exe', - reproduce._EXECUTABLE_PERMISSIONS) - - def test_fuzzer_not_found(self): - """Test when the fuzzer is not found in the database.""" - self.mock_get.return_value = None - self.assertFalse(reproduce._setup_fuzzer('nonexistent_fuzzer')) - self.mock_logs['error'].assert_called_with( - 'Fuzzer nonexistent_fuzzer not found.') - - def test_launcher_script_unsupported(self): - """Test that fuzzers with launcher scripts are not supported.""" - self.fuzzer.launcher_script = 'launcher.sh' - self.assertFalse(reproduce._setup_fuzzer('launcher_fuzzer')) - self.mock_logs['error'].assert_called_with( - 'Fuzzers with launch script not supported yet.') - - def test_remove_directory_fails(self): - """Test failure when clearing the fuzzer directory.""" - self.mock.remove_directory.return_value = False - self.assertFalse(reproduce._setup_fuzzer('external_fuzzer')) - self.mock_logs['error'].assert_called_with( - 'Failed to clear fuzzer directory: /tmp/fuzzer_dir') - - def test_remove_directory_exception(self): - """Test exception when clearing the fuzzer directory.""" - self.mock.remove_directory.side_effect = Exception('mock remove error') - self.assertFalse(reproduce._setup_fuzzer('external_fuzzer')) - self.mock_logs['error'].assert_called_with( - 'Error clearing fuzzer directory /tmp/fuzzer_dir: mock remove error') - - def test_download_archive_fails(self): - """Test failure when downloading the fuzzer archive.""" - self.mock.read_blob_to_disk.return_value = False - self.assertFalse(reproduce._setup_fuzzer('external_fuzzer')) - self.mock_logs['error'].assert_called_with( - 'Failed to download fuzzer archive from blobstore: some_key') - - def test_unpack_archive_fails_archiveerror(self): - """Test failure when unpacking the fuzzer archive with ArchiveError.""" - self.mock.open.side_effect = archive.ArchiveError('mock unpack error') - self.assertFalse(reproduce._setup_fuzzer('external_fuzzer')) - self.mock_logs['error'].assert_called_with( - 'Failed to unpack fuzzer archive fuzzer.zip: mock unpack error') - - def test_unpack_archive_fails_exception(self): - """Test failure when unpacking with a generic exception.""" - self.mock.open.side_effect = Exception('mock generic error') - self.assertFalse(reproduce._setup_fuzzer('external_fuzzer')) - self.mock_logs['error'].assert_called_with( - 'Unexpected error unpacking fuzzer archive fuzzer.zip: mock generic error' - ) - - def test_executable_not_found(self): - """Test when the executable is not found after unpacking.""" - self.mock.exists.side_effect = [True, False] # archive, executable - self.assertFalse(reproduce._setup_fuzzer('external_fuzzer')) - self.mock_logs['error'].assert_called_with( - 'Fuzzer executable fuzzer_exe not found in archive. Check fuzzer configuration.' - ) - - def test_chmod_fails(self): - """Test failure when setting permissions on the executable.""" - self.mock.exists.side_effect = [True, True] - self.mock.chmod.side_effect = OSError('mock chmod error') - self.assertFalse(reproduce._setup_fuzzer('external_fuzzer')) - self.mock_logs['error'].assert_called_with( - 'Failed to set permissions on fuzzer executable /tmp/fuzzer_dir/fuzzer_exe: mock chmod error' - ) - - -class SetupTestcaseLocallyTest(unittest.TestCase): - """Tests for the _setup_testcase_locally function.""" - - def setUp(self): - super().setUp() - self.addCleanup(mock.patch.stopall) - self.mock_logs = mock.patch.multiple( - 'local.butler.reproduce.logs', - error=mock.DEFAULT, - info=mock.DEFAULT, - warning=mock.DEFAULT).start() - - helpers.patch(self, [ - 'clusterfuzz._internal.system.shell.clear_testcase_directories', - 'clusterfuzz._internal.bot.tasks.setup._get_testcase_file_and_path', - 'clusterfuzz._internal.google_cloud_utils.blobs.read_blob_to_disk', - 'clusterfuzz._internal.bot.tasks.setup.prepare_environment_for_testcase' - ]) - - self.testcase = data_types.Testcase( - fuzzed_keys='testcase_key', minimized_keys=None) - - def test_success_fuzzed_keys(self): - """Test successful local setup of a testcase.""" - self.mock._get_testcase_file_and_path.return_value = (mock.ANY, - '/tmp/testcase') - self.mock.read_blob_to_disk.return_value = True - - path = reproduce._setup_testcase_locally(self.testcase) - self.assertEqual(path, '/tmp/testcase') - self.mock.clear_testcase_directories.assert_called_once() - self.mock.read_blob_to_disk.assert_called_once_with('testcase_key', - '/tmp/testcase') - self.mock.prepare_environment_for_testcase.assert_called_once_with( - self.testcase) - - def test_success_minimized_keys(self): - """Test successful local setup of a testcase with minimized keys.""" - self.testcase.minimized_keys = 'minimized_key' - self.mock._get_testcase_file_and_path.return_value = (mock.ANY, - '/tmp/testcase') - self.mock.read_blob_to_disk.return_value = True - - path = reproduce._setup_testcase_locally(self.testcase) - self.assertEqual(path, '/tmp/testcase') - self.mock.clear_testcase_directories.assert_called_once() - self.mock.read_blob_to_disk.assert_called_once_with('minimized_key', - '/tmp/testcase') - self.mock.prepare_environment_for_testcase.assert_called_once_with( - self.testcase) - - def test_clear_directories_fails(self): - """Test handling an exception from clear_testcase_directories.""" - self.mock.clear_testcase_directories.side_effect = Exception( - 'mock clear error') - path = reproduce._setup_testcase_locally(self.testcase) - self.assertIsNone(path) - self.mock_logs['error'].assert_called_with( - 'Error clearing testcase directories: mock clear error') - - def test_download_fails_fuzzed_keys(self): - """Test handling a download failure from read_blob_to_disk.""" - self.mock._get_testcase_file_and_path.return_value = (mock.ANY, - '/tmp/testcase') - self.mock.read_blob_to_disk.return_value = False - path = reproduce._setup_testcase_locally(self.testcase) - self.assertIsNone(path) - self.mock_logs['error'].assert_called_with( - 'Failed to download testcase from blobstore: testcase_key') - - def test_download_fails_minimized_keys(self): - """Test handling a download failure from read_blob_to_disk with minimized keys.""" - self.testcase.minimized_keys = 'minimized_key' - self.mock._get_testcase_file_and_path.return_value = (mock.ANY, - '/tmp/testcase') - self.mock.read_blob_to_disk.return_value = False - path = reproduce._setup_testcase_locally(self.testcase) - self.assertIsNone(path) - self.mock_logs['error'].assert_called_with( - 'Failed to download testcase from blobstore: minimized_key') - - def test_prepare_env_fails(self): - """Test handling an exception from prepare_environment_for_testcase.""" - self.mock._get_testcase_file_and_path.return_value = (mock.ANY, - '/tmp/testcase') - self.mock.read_blob_to_disk.return_value = True - self.mock.prepare_environment_for_testcase.side_effect = Exception( - 'mock prepare error') - path = reproduce._setup_testcase_locally(self.testcase) - self.assertIsNone(path) - self.mock_logs['error'].assert_called_with( - 'Error setting up testcase locally: mock prepare error') - - class ReproduceTestcaseTest(unittest.TestCase): """Tests for the _reproduce_testcase function.""" @@ -286,8 +51,8 @@ def setUp(self): 'clusterfuzz._internal.system.environment.set_value', 'clusterfuzz._internal.system.environment.get_value', 'clusterfuzz._internal.bot.tasks.commands.update_environment_for_job', - 'local.butler.reproduce._setup_fuzzer', - 'local.butler.reproduce._setup_testcase_locally', + 'clusterfuzz._internal.bot.tasks.setup.setup_local_fuzzer', + 'clusterfuzz._internal.bot.tasks.setup.setup_local_testcase', 'clusterfuzz._internal.build_management.build_manager.setup_build', 'clusterfuzz._internal.bot.testcase_manager.check_for_bad_build', 'clusterfuzz._internal.bot.testcase_manager.test_for_crash_with_retries', @@ -304,8 +69,8 @@ def setUp(self): self.mock.get_testcase_by_id.return_value = self.testcase self.mock.query.return_value.get.return_value = self.job - self.mock._setup_fuzzer.return_value = True - self.mock._setup_testcase_locally.return_value = '/tmp/testcase' + self.mock.setup_local_fuzzer.return_value = True + self.mock.setup_local_testcase.return_value = '/tmp/testcase' build_result = uworker_msg_pb2.BuildData(is_bad_build=False) self.mock.check_for_bad_build.return_value = build_result @@ -326,8 +91,8 @@ def test_success_crash_reproduces(self): reproduce._reproduce_testcase(self.args) self.mock.get_testcase_by_id.assert_called_once_with(123) - self.mock._setup_fuzzer.assert_called_once() - self.mock._setup_testcase_locally.assert_called_once() + self.mock.setup_local_fuzzer.assert_called_once() + self.mock.setup_local_testcase.assert_called_once() self.mock.setup_build.assert_called_once() self.mock.check_for_bad_build.assert_called_once() self.mock.test_for_crash_with_retries.assert_called_once() @@ -348,7 +113,7 @@ def test_testcase_not_found(self): reproduce._reproduce_testcase(self.args) self.mock_logs['error'].assert_called_with( 'Testcase with ID 123 not found.') - self.mock._setup_fuzzer.assert_not_called() + self.mock.setup_local_fuzzer.assert_not_called() def test_job_not_found(self): """Test that it exits when the job is not found.""" @@ -356,19 +121,19 @@ def test_job_not_found(self): reproduce._reproduce_testcase(self.args) self.mock_logs['error'].assert_called_with( f'Job type {self.testcase.job_type} not found for testcase.') - self.mock._setup_fuzzer.assert_not_called() + self.mock.setup_local_fuzzer.assert_not_called() def test_setup_fuzzer_fails(self): """Test that it exits when fuzzer setup fails.""" - self.mock._setup_fuzzer.return_value = False + self.mock.setup_local_fuzzer.return_value = False reproduce._reproduce_testcase(self.args) self.mock_logs['error'].assert_called_with( f'Failed to setup fuzzer {self.testcase.fuzzer_name}. Exiting.') - self.mock._setup_testcase_locally.assert_not_called() + self.mock.setup_local_testcase.assert_not_called() def test_setup_testcase_fails(self): """Test that it exits when testcase setup fails.""" - self.mock._setup_testcase_locally.return_value = None + self.mock.setup_local_testcase.return_value = None reproduce._reproduce_testcase(self.args) self.mock_logs['error'].assert_called_with( 'Could not setup testcase locally. Exiting.') @@ -410,4 +175,4 @@ def test_invalid_timeout_env(self): if __name__ == '__main__': - unittest.main() + unittest.main() \ No newline at end of file diff --git a/src/local/butler/reproduce.py b/src/local/butler/reproduce.py index b25592ef28..17eb53550e 100644 --- a/src/local/butler/reproduce.py +++ b/src/local/butler/reproduce.py @@ -28,136 +28,24 @@ from clusterfuzz._internal.datastore import data_types from clusterfuzz._internal.datastore import ndb_init from clusterfuzz._internal.datastore import data_types -from clusterfuzz._internal.google_cloud_utils import blobs from clusterfuzz._internal.metrics import logs from clusterfuzz._internal.protos import uworker_msg_pb2 -from clusterfuzz._internal.system import archive from clusterfuzz._internal.system import environment -from clusterfuzz._internal.system import shell _DEFAULT_TEST_TIMEOUT = 60 -_EXECUTABLE_PERMISSIONS = 0o750 - -def _setup_fuzzer(fuzzer_name: str) -> bool: - """Sets up the fuzzer binaries and environment. - - Args: - fuzzer_name: The name of the fuzzer to set up. - - Returns: - True if setup was successful, False otherwise. - """ - fuzzer = data_types.Fuzzer.query( - data_types.Fuzzer.name == fuzzer_name).get() - if not fuzzer: - logs.error(f'Fuzzer {fuzzer_name} not found.') - return False - - if fuzzer.untrusted_content: - logs.warning('You are about to run an untrusted fuzzer locally. ' - 'This can be dangerous.') - - environment.set_value('UNTRUSTED_CONTENT', fuzzer.untrusted_content) - - if fuzzer.data_bundle_name: - logs.info( - f'Fuzzer {fuzzer_name} uses data bundle: {fuzzer.data_bundle_name}') - - if fuzzer.launcher_script: - logs.error('Fuzzers with launch script not supported yet.') - return False - - if fuzzer.builtin: - logs.info(f'Fuzzer {fuzzer_name} is builtin, no setup required.') - return True - - fuzzer_directory: str = setup.get_fuzzer_directory(fuzzer.name) - - try: - if not shell.remove_directory(fuzzer_directory, recreate=True): - logs.error(f'Failed to clear fuzzer directory: {fuzzer_directory}') - return False - except Exception as e: - logs.error(f'Error clearing fuzzer directory {fuzzer_directory}: {e}') - return False - - archive_path = os.path.join(fuzzer_directory, fuzzer.filename) - if not blobs.read_blob_to_disk(fuzzer.blobstore_key, archive_path): - logs.error('Failed to download fuzzer archive from blobstore: ' - f'{fuzzer.blobstore_key}') - return False - - try: - with archive.open(archive_path) as reader: - reader.extract_all(fuzzer_directory) - except archive.ArchiveError as e: - logs.error(f'Failed to unpack fuzzer archive {fuzzer.filename}: {e}') - return False - except Exception as e: - logs.error( - f'Unexpected error unpacking fuzzer archive {fuzzer.filename}: {e}') - return False - finally: - if os.path.exists(archive_path): - try: - shell.remove_file(archive_path) - except Exception as e: - logs.warning( - f'Failed to remove temporary archive file {archive_path}: {e}') - - fuzzer_path = os.path.join(fuzzer_directory, fuzzer.executable_path) - if not os.path.exists(fuzzer_path): - logs.error( - f'Fuzzer executable {fuzzer.executable_path} not found in archive. ' - 'Check fuzzer configuration.') - return False - - try: - os.chmod(fuzzer_path, _EXECUTABLE_PERMISSIONS) - except OSError as e: - logs.error( - f'Failed to set permissions on fuzzer executable {fuzzer_path}: {e}') - return False - - return True - - -def _setup_testcase_locally(testcase: data_types.Testcase) -> str | None: - """Sets up the testcase file locally. +def _setup_reproduce(args) -> None: + """Sets up the environment for reproducing a testcase. Args: - testcase: The Testcase object. - - Returns: - A tuple containing: - - bool: True if the testcase was downloaded successfully. - - Optional[str]: The local file path to the testcase, or None on failure. + args: Parsed command-line arguments. """ - try: - shell.clear_testcase_directories() - except Exception as e: - logs.error(f'Error clearing testcase directories: {e}') - return None - - try: - _, testcase_file_path = setup._get_testcase_file_and_path(testcase) - if testcase.minimized_keys and testcase.minimized_keys != 'NA': - blob_key = testcase.minimized_keys - else: - blob_key = testcase.fuzzed_keys - if not blobs.read_blob_to_disk(blob_key, testcase_file_path): - logs.error(f'Failed to download testcase from blobstore: {blob_key}') - # Returning None for path when download fails - return None - setup.prepare_environment_for_testcase(testcase) - except Exception as e: - logs.error(f'Error setting up testcase locally: {e}') - return None - - return testcase_file_path - - + os.environ['CONFIG_DIR_OVERRIDE'] = os.path.abspath(args.config_dir) + local_config.ProjectConfig().set_environment() + environment.set_bot_environment() + logs.configure('run_bot') + init.run() + def _reproduce_testcase(args: argparse.Namespace) -> None: """Reproduces a testcase locally based on the provided arguments. @@ -181,11 +69,11 @@ def _reproduce_testcase(args: argparse.Namespace) -> None: environment.set_value('JOB_NAME', job.name) commands.update_environment_for_job(job.get_environment_string()) - if not _setup_fuzzer(testcase.fuzzer_name): + if not setup.setup_local_fuzzer(testcase.fuzzer_name): logs.error(f'Failed to setup fuzzer {testcase.fuzzer_name}. Exiting.') return - testcase_file_path = _setup_testcase_locally(testcase) + testcase_file_path = setup.setup_local_testcase(testcase) if testcase_file_path is None: logs.error('Could not setup testcase locally. Exiting.') return @@ -253,18 +141,6 @@ def _reproduce_testcase(args: argparse.Namespace) -> None: else: logs.info('The testcase does not reliably reproduce.') -def _setup_reproduce(args) -> None: - """Sets up the environment for reproducing a testcase. - - Args: - args: Parsed command-line arguments. - """ - os.environ['CONFIG_DIR_OVERRIDE'] = os.path.abspath(args.config_dir) - local_config.ProjectConfig().set_environment() - environment.set_bot_environment() - logs.configure('run_bot') - init.run() - def execute(args: argparse.Namespace) -> None: """Initializes the environment and reproduces a testcase locally. From 6a26ab017a7e864de640a099f0dca7f5962e248a Mon Sep 17 00:00:00 2001 From: PauloVLB Date: Wed, 15 Oct 2025 14:34:18 +0000 Subject: [PATCH 09/10] refactor: linting fix --- butler.py | 11 ++++- src/clusterfuzz/_internal/bot/tasks/setup.py | 18 ++++---- .../tests/core/bot/tasks/setup_test.py | 9 ++-- .../tests/core/local/butler/reproduce_test.py | 45 +++++++------------ src/local/butler/reproduce.py | 18 ++++---- 5 files changed, 49 insertions(+), 52 deletions(-) diff --git a/butler.py b/butler.py index 2d9181f69b..815899b6cc 100644 --- a/butler.py +++ b/butler.py @@ -97,11 +97,13 @@ def _setup_args_for_remote(parser): subparsers.add_parser('reboot', help='Reboot with `sudo reboot`.') + def _add_integration_tests_subparsers(toplevel_subparsers): """Adds a parser for the `integration_tests` command.""" toplevel_subparsers.add_parser( 'integration_tests', help='Run end-to-end integration tests.') - + + def _add_weights_fuzzer_subparser(weights_subparsers): """Adds a parser for the `weights fuzzer` command.""" parser = weights_subparsers.add_parser( @@ -236,6 +238,7 @@ def _add_weights_subparser(toplevel_subparsers): _add_weights_batches_subparser(subparsers) _add_weights_target_subparser(subparsers) + def _add_reproduce_subparser(toplevel_subparsers): """Adds a parser for the `reproduce` command.""" parser = toplevel_subparsers.add_parser( @@ -243,7 +246,11 @@ def _add_reproduce_subparser(toplevel_subparsers): parser.add_argument( '-c', '--config-dir', required=True, help='Path to application config.') parser.add_argument( - '-t', '--testcase-id', required=True, help='The testcase ID to reproduce.') + '-t', + '--testcase-id', + required=True, + help='The testcase ID to reproduce.') + def main(): """Parse the command-line args and invoke the right command.""" diff --git a/src/clusterfuzz/_internal/bot/tasks/setup.py b/src/clusterfuzz/_internal/bot/tasks/setup.py index 4d50aa2789..913251f853 100644 --- a/src/clusterfuzz/_internal/bot/tasks/setup.py +++ b/src/clusterfuzz/_internal/bot/tasks/setup.py @@ -51,6 +51,7 @@ _TESTCASE_ARCHIVE_EXTENSION = '.zip' _EXECUTABLE_PERMISSIONS = 0o750 + def _set_timeout_value_from_user_upload(testcase_id, uworker_env): """Get the timeout associated with this testcase.""" metadata = data_types.TestcaseUploadMetadata.query( @@ -801,8 +802,10 @@ def get_fuzzer_directory(fuzzer_name): return fuzzer_directory -def archive_testcase_and_dependencies_in_gcs(resource_list, testcase_path: str, - upload_url: str): +def archive_testcase_and_dependencies_in_gcs( + resource_list, + testcase_path: str, # pylint: disable=line-too-long + upload_url: str): """Archive testcase and its dependencies, and store in blobstore. Returns whether it is archived, the absolute_filename, and the zip_filename.""" if not os.path.exists(testcase_path): @@ -893,6 +896,7 @@ def archive_testcase_and_dependencies_in_gcs(resource_list, testcase_path: str, return archived, absolute_filename, zip_filename + def setup_local_testcase(testcase: data_types.Testcase) -> str | None: """Sets up the testcase file locally. @@ -900,9 +904,7 @@ def setup_local_testcase(testcase: data_types.Testcase) -> str | None: testcase: The Testcase object. Returns: - A tuple containing: - - bool: True if the testcase was downloaded successfully. - - Optional[str]: The local file path to the testcase, or None on failure. + - str | None : The local file path to the testcase, or None on failure. """ try: shell.clear_testcase_directories() @@ -927,6 +929,7 @@ def setup_local_testcase(testcase: data_types.Testcase) -> str | None: return testcase_file_path + def setup_local_fuzzer(fuzzer_name: str) -> bool: """Sets up the fuzzer binaries and environment. @@ -936,8 +939,7 @@ def setup_local_fuzzer(fuzzer_name: str) -> bool: Returns: True if setup was successful, False otherwise. """ - fuzzer = data_types.Fuzzer.query( - data_types.Fuzzer.name == fuzzer_name).get() + fuzzer = data_types.Fuzzer.query(data_types.Fuzzer.name == fuzzer_name).get() if not fuzzer: logs.error(f'Fuzzer {fuzzer_name} not found.') return False @@ -1008,4 +1010,4 @@ def setup_local_fuzzer(fuzzer_name: str) -> bool: f'Failed to set permissions on fuzzer executable {fuzzer_path}: {e}') return False - return True \ No newline at end of file + return True diff --git a/src/clusterfuzz/_internal/tests/core/bot/tasks/setup_test.py b/src/clusterfuzz/_internal/tests/core/bot/tasks/setup_test.py index 084dd00ef2..6e39dbe6b1 100644 --- a/src/clusterfuzz/_internal/tests/core/bot/tasks/setup_test.py +++ b/src/clusterfuzz/_internal/tests/core/bot/tasks/setup_test.py @@ -22,11 +22,10 @@ from clusterfuzz._internal.bot.tasks.utasks import uworker_io from clusterfuzz._internal.datastore import data_types from clusterfuzz._internal.protos import uworker_msg_pb2 +from clusterfuzz._internal.system import archive from clusterfuzz._internal.system import environment from clusterfuzz._internal.tests.test_libs import helpers from clusterfuzz._internal.tests.test_libs import test_utils -from clusterfuzz._internal.system import archive - # pylint: disable=protected-access @@ -238,6 +237,7 @@ def test_data_bundles(self): setup.update_fuzzer_and_data_bundles(setup_input) self.assertEqual(self.mock.update_data_bundle.call_count, 2) + class SetupLocalFuzzerTest(unittest.TestCase): """Tests for the setup_local_fuzzer function.""" @@ -273,7 +273,8 @@ def setUp(self): self.mock.remove_directory.return_value = True self.mock.read_blob_to_disk.return_value = True - self.mock_archive_reader = unittest.mock.create_autospec(spec=archive.ArchiveReader, instance=True, spec_set=True) + self.mock_archive_reader = unittest.mock.create_autospec( + spec=archive.ArchiveReader, instance=True, spec_set=True) self.mock.open.return_value.__enter__.return_value = self.mock_archive_reader # Common fuzzer object @@ -470,4 +471,4 @@ def test_prepare_env_fails(self): path = setup.setup_local_testcase(self.testcase) self.assertIsNone(path) self.mock_logs['error'].assert_called_with( - 'Error setting up testcase locally: mock prepare error') \ No newline at end of file + 'Error setting up testcase locally: mock prepare error') diff --git a/src/clusterfuzz/_internal/tests/core/local/butler/reproduce_test.py b/src/clusterfuzz/_internal/tests/core/local/butler/reproduce_test.py index 638974583b..a08043a955 100644 --- a/src/clusterfuzz/_internal/tests/core/local/butler/reproduce_test.py +++ b/src/clusterfuzz/_internal/tests/core/local/butler/reproduce_test.py @@ -11,24 +11,15 @@ # 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 the reproduce butler.""" import argparse -import os import unittest from unittest import mock -from clusterfuzz._internal.bot import testcase_manager -from clusterfuzz._internal.bot.tasks import setup -from clusterfuzz._internal.bot.tasks.commands import update_environment_for_job -from clusterfuzz._internal.datastore import data_handler -from clusterfuzz._internal.datastore import data_types from clusterfuzz._internal.crash_analysis.crash_result import CrashResult -from clusterfuzz._internal.google_cloud_utils import blobs -from clusterfuzz._internal.metrics import logs +from clusterfuzz._internal.datastore import data_types from clusterfuzz._internal.protos import uworker_msg_pb2 -from clusterfuzz._internal.system import archive -from clusterfuzz._internal.system import environment -from clusterfuzz._internal.system import shell from clusterfuzz._internal.tests.test_libs import helpers from local.butler import reproduce @@ -61,9 +52,7 @@ def setUp(self): ]) self.testcase = data_types.Testcase( - job_type='test_job', - fuzzer_name='test_fuzzer', - crash_revision=12345) + job_type='test_job', fuzzer_name='test_fuzzer', crash_revision=12345) self.testcase.get_fuzz_target = mock.Mock() self.job = data_types.Job(name='test_job') self.mock.get_testcase_by_id.return_value = self.testcase @@ -83,12 +72,12 @@ def setUp(self): self.mock.test_for_reproducibility.return_value = True - self.mock.get_value.return_value = str(reproduce._DEFAULT_TEST_TIMEOUT) + self.mock.get_value.return_value = str(reproduce._DEFAULT_TEST_TIMEOUT) # pylint: disable=protected-access self.args = argparse.Namespace(testcase_id=123, config_dir='/foo') def test_success_crash_reproduces(self): """Test the full success path where the crash reproduces.""" - reproduce._reproduce_testcase(self.args) + reproduce._reproduce_testcase(self.args) # pylint: disable=protected-access self.mock.get_testcase_by_id.assert_called_once_with(123) self.mock.setup_local_fuzzer.assert_called_once() @@ -102,7 +91,7 @@ def test_success_crash_reproduces(self): def test_success_crash_not_reproduces(self): """Test the success path where the crash does not reproduce.""" self.mock.test_for_reproducibility.return_value = False - reproduce._reproduce_testcase(self.args) + reproduce._reproduce_testcase(self.args) # pylint: disable=protected-access self.mock.test_for_reproducibility.assert_called_once() self.mock_logs['info'].assert_any_call( 'The testcase does not reliably reproduce.') @@ -110,7 +99,7 @@ def test_success_crash_not_reproduces(self): def test_testcase_not_found(self): """Test that it exits when the testcase is not found.""" self.mock.get_testcase_by_id.return_value = None - reproduce._reproduce_testcase(self.args) + reproduce._reproduce_testcase(self.args) # pylint: disable=protected-access self.mock_logs['error'].assert_called_with( 'Testcase with ID 123 not found.') self.mock.setup_local_fuzzer.assert_not_called() @@ -118,7 +107,7 @@ def test_testcase_not_found(self): def test_job_not_found(self): """Test that it exits when the job is not found.""" self.mock.query.return_value.get.return_value = None - reproduce._reproduce_testcase(self.args) + reproduce._reproduce_testcase(self.args) # pylint: disable=protected-access self.mock_logs['error'].assert_called_with( f'Job type {self.testcase.job_type} not found for testcase.') self.mock.setup_local_fuzzer.assert_not_called() @@ -126,7 +115,7 @@ def test_job_not_found(self): def test_setup_fuzzer_fails(self): """Test that it exits when fuzzer setup fails.""" self.mock.setup_local_fuzzer.return_value = False - reproduce._reproduce_testcase(self.args) + reproduce._reproduce_testcase(self.args) # pylint: disable=protected-access self.mock_logs['error'].assert_called_with( f'Failed to setup fuzzer {self.testcase.fuzzer_name}. Exiting.') self.mock.setup_local_testcase.assert_not_called() @@ -134,7 +123,7 @@ def test_setup_fuzzer_fails(self): def test_setup_testcase_fails(self): """Test that it exits when testcase setup fails.""" self.mock.setup_local_testcase.return_value = None - reproduce._reproduce_testcase(self.args) + reproduce._reproduce_testcase(self.args) # pylint: disable=protected-access self.mock_logs['error'].assert_called_with( 'Could not setup testcase locally. Exiting.') self.mock.setup_build.assert_not_called() @@ -142,7 +131,7 @@ def test_setup_testcase_fails(self): def test_setup_build_fails(self): """Test that it exits when build setup fails.""" self.mock.setup_build.side_effect = Exception('mock build error') - reproduce._reproduce_testcase(self.args) + reproduce._reproduce_testcase(self.args) # pylint: disable=protected-access self.mock_logs['error'].assert_called_with( f'Error setting up build for revision ' f'{self.testcase.crash_revision}: mock build error') @@ -151,28 +140,28 @@ def test_setup_build_fails(self): def test_bad_build(self): """Test that it exits when a bad build is detected.""" self.mock.check_for_bad_build.return_value.is_bad_build = True - reproduce._reproduce_testcase(self.args) + reproduce._reproduce_testcase(self.args) # pylint: disable=protected-access self.mock_logs['error'].assert_called_with('Bad build detected. Exiting.') self.mock.test_for_crash_with_retries.assert_not_called() def test_no_crash(self): """Test that it exits when the initial crash does not occur.""" self.mock.test_for_crash_with_retries.return_value.is_crash.return_value = False - reproduce._reproduce_testcase(self.args) + reproduce._reproduce_testcase(self.args) # pylint: disable=protected-access self.mock_logs['info'].assert_any_call('No crash occurred. Exiting.') self.mock.test_for_reproducibility.assert_not_called() def test_invalid_timeout_env(self): """Test when TEST_TIMEOUT environment variable is invalid.""" self.mock.get_value.return_value = 'invalid' - reproduce._reproduce_testcase(self.args) + reproduce._reproduce_testcase(self.args) # pylint: disable=protected-access self.mock_logs['warning'].assert_any_call( f"Invalid TEST_TIMEOUT value: invalid. " - f"Using default: {reproduce._DEFAULT_TEST_TIMEOUT}") + f"Using default: {reproduce._DEFAULT_TEST_TIMEOUT}") # pylint: disable=protected-access # Check that test_for_crash_with_retries was called with default timeout _, kwargs = self.mock.test_for_crash_with_retries.call_args - self.assertEqual(kwargs['test_timeout'], reproduce._DEFAULT_TEST_TIMEOUT) + self.assertEqual(kwargs['test_timeout'], reproduce._DEFAULT_TEST_TIMEOUT) # pylint: disable=protected-access if __name__ == '__main__': - unittest.main() \ No newline at end of file + unittest.main() diff --git a/src/local/butler/reproduce.py b/src/local/butler/reproduce.py index 17eb53550e..37f35ef6ea 100644 --- a/src/local/butler/reproduce.py +++ b/src/local/butler/reproduce.py @@ -15,25 +15,23 @@ import argparse import os -from typing import Optional -from typing import Tuple from clusterfuzz._internal.bot import testcase_manager from clusterfuzz._internal.bot.fuzzers import init -from clusterfuzz._internal.bot.tasks import setup from clusterfuzz._internal.bot.tasks import commands +from clusterfuzz._internal.bot.tasks import setup from clusterfuzz._internal.build_management import build_manager from clusterfuzz._internal.config import local_config from clusterfuzz._internal.datastore import data_handler from clusterfuzz._internal.datastore import data_types from clusterfuzz._internal.datastore import ndb_init -from clusterfuzz._internal.datastore import data_types from clusterfuzz._internal.metrics import logs from clusterfuzz._internal.protos import uworker_msg_pb2 from clusterfuzz._internal.system import environment _DEFAULT_TEST_TIMEOUT = 60 + def _setup_reproduce(args) -> None: """Sets up the environment for reproducing a testcase. @@ -45,21 +43,20 @@ def _setup_reproduce(args) -> None: environment.set_bot_environment() logs.configure('run_bot') init.run() - + + def _reproduce_testcase(args: argparse.Namespace) -> None: """Reproduces a testcase locally based on the provided arguments. Args: args: Parsed command-line arguments. """ - testcase = data_handler.get_testcase_by_id( - args.testcase_id) + testcase = data_handler.get_testcase_by_id(args.testcase_id) if not testcase: logs.error(f'Testcase with ID {args.testcase_id} not found.') return - job = data_types.Job.query( - data_types.Job.name == testcase.job_type).get() + job = data_types.Job.query(data_types.Job.name == testcase.job_type).get() if not job: logs.error(f'Job type {testcase.job_type} not found for testcase.') return @@ -89,7 +86,7 @@ def _reproduce_testcase(args: argparse.Namespace) -> None: f'Error setting up build for revision {testcase.crash_revision}: {e}') return - bad_build_result: uworker_msg_pb2.BuildData = ( + bad_build_result: uworker_msg_pb2.BuildData = ( # pylint: disable=no-member testcase_manager.check_for_bad_build(job.name, testcase.crash_revision)) if bad_build_result.is_bad_build: logs.error('Bad build detected. Exiting.') @@ -141,6 +138,7 @@ def _reproduce_testcase(args: argparse.Namespace) -> None: else: logs.info('The testcase does not reliably reproduce.') + def execute(args: argparse.Namespace) -> None: """Initializes the environment and reproduces a testcase locally. From dc06e761800e33b9a18a40760353221f31b1358e Mon Sep 17 00:00:00 2001 From: PauloVLB Date: Thu, 16 Oct 2025 12:38:34 +0000 Subject: [PATCH 10/10] lint: remove line-too-long disable --- src/clusterfuzz/_internal/bot/tasks/setup.py | 6 ++---- 1 file changed, 2 insertions(+), 4 deletions(-) diff --git a/src/clusterfuzz/_internal/bot/tasks/setup.py b/src/clusterfuzz/_internal/bot/tasks/setup.py index 913251f853..3476060283 100644 --- a/src/clusterfuzz/_internal/bot/tasks/setup.py +++ b/src/clusterfuzz/_internal/bot/tasks/setup.py @@ -802,10 +802,8 @@ def get_fuzzer_directory(fuzzer_name): return fuzzer_directory -def archive_testcase_and_dependencies_in_gcs( - resource_list, - testcase_path: str, # pylint: disable=line-too-long - upload_url: str): +def archive_testcase_and_dependencies_in_gcs(resource_list, testcase_path: str, + upload_url: str): """Archive testcase and its dependencies, and store in blobstore. Returns whether it is archived, the absolute_filename, and the zip_filename.""" if not os.path.exists(testcase_path):