From d5936d5d182df752136c1cd8b0d4ac753d5348c6 Mon Sep 17 00:00:00 2001 From: Diego Jardon Date: Fri, 15 May 2026 20:45:06 +0000 Subject: [PATCH 01/14] Optimize Android emulator initialization by batching reboots This change reduces the number of reboots during Android device setup by: - Adding a wait_for_reboot parameter to adb.write_data_to_file. - Tracking reboot status in initialize_device to skip the final reboot if one already occurred. - Disabling reboots when setting sanitizer options since the app restart is sufficient. These optimizations improve bot startup efficiency and overall fuzzing throughput. --- .../_internal/platforms/android/adb.py | 4 ++-- .../_internal/platforms/android/device.py | 17 +++++++++++------ .../_internal/platforms/android/sanitizer.py | 14 +++++++++----- 3 files changed, 22 insertions(+), 13 deletions(-) diff --git a/src/clusterfuzz/_internal/platforms/android/adb.py b/src/clusterfuzz/_internal/platforms/android/adb.py index 47c59b31c72..1f5eefcd861 100755 --- a/src/clusterfuzz/_internal/platforms/android/adb.py +++ b/src/clusterfuzz/_internal/platforms/android/adb.py @@ -880,7 +880,7 @@ def write_command_line_file(command_line, app_path): write_data_to_file(command_line_file_contents, command_line_path) -def write_data_to_file(contents, file_path): +def write_data_to_file(contents, file_path, wait_for_reboot=True): """Writes content to file.""" # If this is a file in /system, we need to remount /system as read-write and # after file is written, revert it back to read-only. @@ -895,6 +895,6 @@ def write_data_to_file(contents, file_path): # Make command line file is readable. run_shell_command('chmod 0644 %s' % file_path, root=True) - if is_system_file: + if is_system_file and wait_for_reboot: reboot() wait_until_fully_booted() diff --git a/src/clusterfuzz/_internal/platforms/android/device.py b/src/clusterfuzz/_internal/platforms/android/device.py index 0c8d81ca686..e13ddf6dca4 100755 --- a/src/clusterfuzz/_internal/platforms/android/device.py +++ b/src/clusterfuzz/_internal/platforms/android/device.py @@ -195,9 +195,9 @@ def configure_system_build_properties(): current_md5 = adb.get_file_checksum(BUILD_PROP_PATH) if current_md5 is None: logs.error('Unable to find %s on device.' % BUILD_PROP_PATH) - return + return False if old_md5 == current_md5: - return + return False # Pull to tmp file. bot_tmp_directory = environment.get_value('BOT_TMPDIR') @@ -205,7 +205,7 @@ def configure_system_build_properties(): adb.run_command(['pull', BUILD_PROP_PATH, old_build_prop_path]) if not os.path.exists(old_build_prop_path): logs.error('Unable to fetch %s from device.' % BUILD_PROP_PATH) - return + return False # Write new build.prop. new_build_prop_path = os.path.join(bot_tmp_directory, 'new.prop') @@ -228,11 +228,13 @@ def configure_system_build_properties(): # Keep verified boot disabled for M and higher releases. This makes it easy # to modify system's app_process to load asan libraries. + reboot_done = False build_version = settings.get_build_version() if is_build_at_least(build_version, 'M'): adb.run_as_root() adb.run_command('disable-verity') reboot() + reboot_done = True # Make /system writable. adb.run_as_root() @@ -255,6 +257,8 @@ def configure_system_build_properties(): current_md5 = adb.get_file_checksum(BUILD_PROP_PATH) persistent_cache.set_value(constants.BUILD_PROP_MD5_KEY, current_md5) + return reboot_done + def get_debug_props_and_values(): """Return debug property names and values based on |ENABLE_DEBUG_CHECKS| @@ -306,16 +310,17 @@ def initialize_device(): adb.setup_adb() # General device configuration settings. - configure_system_build_properties() + reboot_done = configure_system_build_properties() configure_device_settings() add_test_accounts_if_needed() # Setup AddressSanitizer if needed. - sanitizer.setup_asan_if_needed() + reboot_done = sanitizer.setup_asan_if_needed() or reboot_done # Reboot device as above steps would need it and also it brings device in a # good state. - reboot() + if not reboot_done: + reboot() # Make sure we are running as root after restart. adb.run_as_root() diff --git a/src/clusterfuzz/_internal/platforms/android/sanitizer.py b/src/clusterfuzz/_internal/platforms/android/sanitizer.py index 832f6a4a577..f5d954e0a2c 100644 --- a/src/clusterfuzz/_internal/platforms/android/sanitizer.py +++ b/src/clusterfuzz/_internal/platforms/android/sanitizer.py @@ -69,7 +69,9 @@ def set_options(sanitizer_tool_name, sanitizer_options): if not sanitizer_options_file_path: return - adb.write_data_to_file(sanitizer_options, sanitizer_options_file_path) + # Skip reboot as the app will pick up the options file on restart. + adb.write_data_to_file( + sanitizer_options, sanitizer_options_file_path, wait_for_reboot=False) def setup_asan_if_needed(): @@ -78,16 +80,16 @@ def setup_asan_if_needed(): # Only do this step if explicitly enabled in the job type. This cannot be # determined from libraries in application directory since they can go # missing in a bad build, so we want to catch that. - return + return False if settings.get_sanitizer_tool_name(): # If this is a sanitizer build, no need to setup ASAN (incompatible). - return + return False app_directory = environment.get_value('APP_DIR') if not app_directory: # No app directory -> No ASAN runtime library. No work to do, bail out. - return + return False # Initialize variables. android_directory = environment.get_platform_resources_directory() @@ -108,7 +110,7 @@ def setup_asan_if_needed(): result = process.run_and_wait() if result.return_code: logs.error('Failed to setup ASan on device.', output=result.output) - return + return False logs.info( 'ASan device setup script successfully finished, waiting for boot.', @@ -117,3 +119,5 @@ def setup_asan_if_needed(): # Wait until fully booted as otherwise shell restart followed by a quick # reboot can trigger data corruption in /data/data. adb.wait_until_fully_booted() + + return True From 9b8833536b38dc9dd6f072eef8e7e4278553db40 Mon Sep 17 00:00:00 2001 From: Diego Jardon Date: Sat, 16 May 2026 16:57:40 +0000 Subject: [PATCH 02/14] Fix reboot_done tracking to ensure device settings are applied --- src/clusterfuzz/_internal/platforms/android/device.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/clusterfuzz/_internal/platforms/android/device.py b/src/clusterfuzz/_internal/platforms/android/device.py index e13ddf6dca4..da186f4060f 100755 --- a/src/clusterfuzz/_internal/platforms/android/device.py +++ b/src/clusterfuzz/_internal/platforms/android/device.py @@ -310,12 +310,12 @@ def initialize_device(): adb.setup_adb() # General device configuration settings. - reboot_done = configure_system_build_properties() + configure_system_build_properties() configure_device_settings() add_test_accounts_if_needed() # Setup AddressSanitizer if needed. - reboot_done = sanitizer.setup_asan_if_needed() or reboot_done + reboot_done = sanitizer.setup_asan_if_needed() # Reboot device as above steps would need it and also it brings device in a # good state. From 1aecb3abce4397cd145c8d46f0e78a591987e3f9 Mon Sep 17 00:00:00 2001 From: Diego Jardon Date: Sat, 16 May 2026 17:05:15 +0000 Subject: [PATCH 03/14] Revert previous fix and use accumulated state for reboot_done --- src/clusterfuzz/_internal/platforms/android/device.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/clusterfuzz/_internal/platforms/android/device.py b/src/clusterfuzz/_internal/platforms/android/device.py index da186f4060f..e13ddf6dca4 100755 --- a/src/clusterfuzz/_internal/platforms/android/device.py +++ b/src/clusterfuzz/_internal/platforms/android/device.py @@ -310,12 +310,12 @@ def initialize_device(): adb.setup_adb() # General device configuration settings. - configure_system_build_properties() + reboot_done = configure_system_build_properties() configure_device_settings() add_test_accounts_if_needed() # Setup AddressSanitizer if needed. - reboot_done = sanitizer.setup_asan_if_needed() + reboot_done = sanitizer.setup_asan_if_needed() or reboot_done # Reboot device as above steps would need it and also it brings device in a # good state. From 1482ba7d6ee89ac1925f48aa72291107f6e809c9 Mon Sep 17 00:00:00 2001 From: Diego Jardon Date: Sat, 16 May 2026 17:12:12 +0000 Subject: [PATCH 04/14] Invert logic to needs_reboot and use bitwise OR operator to fix state tracking and short-circuit issues --- src/clusterfuzz/_internal/platforms/android/device.py | 10 ++++------ 1 file changed, 4 insertions(+), 6 deletions(-) diff --git a/src/clusterfuzz/_internal/platforms/android/device.py b/src/clusterfuzz/_internal/platforms/android/device.py index e13ddf6dca4..570edbde43e 100755 --- a/src/clusterfuzz/_internal/platforms/android/device.py +++ b/src/clusterfuzz/_internal/platforms/android/device.py @@ -228,13 +228,11 @@ def configure_system_build_properties(): # Keep verified boot disabled for M and higher releases. This makes it easy # to modify system's app_process to load asan libraries. - reboot_done = False build_version = settings.get_build_version() if is_build_at_least(build_version, 'M'): adb.run_as_root() adb.run_command('disable-verity') reboot() - reboot_done = True # Make /system writable. adb.run_as_root() @@ -257,7 +255,7 @@ def configure_system_build_properties(): current_md5 = adb.get_file_checksum(BUILD_PROP_PATH) persistent_cache.set_value(constants.BUILD_PROP_MD5_KEY, current_md5) - return reboot_done + return True def get_debug_props_and_values(): @@ -310,16 +308,16 @@ def initialize_device(): adb.setup_adb() # General device configuration settings. - reboot_done = configure_system_build_properties() + needs_reboot = configure_system_build_properties() configure_device_settings() add_test_accounts_if_needed() # Setup AddressSanitizer if needed. - reboot_done = sanitizer.setup_asan_if_needed() or reboot_done + needs_reboot |= sanitizer.setup_asan_if_needed() # Reboot device as above steps would need it and also it brings device in a # good state. - if not reboot_done: + if needs_reboot: reboot() # Make sure we are running as root after restart. From f960945ae24a418fc6dc8dd34ceabf540608da4e Mon Sep 17 00:00:00 2001 From: Diego Jardon Date: Sat, 16 May 2026 17:14:42 +0000 Subject: [PATCH 05/14] Fix needs_reboot logic so ASan setup clears pending reboots --- src/clusterfuzz/_internal/platforms/android/device.py | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/src/clusterfuzz/_internal/platforms/android/device.py b/src/clusterfuzz/_internal/platforms/android/device.py index 570edbde43e..a80826ba465 100755 --- a/src/clusterfuzz/_internal/platforms/android/device.py +++ b/src/clusterfuzz/_internal/platforms/android/device.py @@ -313,7 +313,10 @@ def initialize_device(): add_test_accounts_if_needed() # Setup AddressSanitizer if needed. - needs_reboot |= sanitizer.setup_asan_if_needed() + if sanitizer.setup_asan_if_needed(): + # ASan setup script performs a shell restart and waits for boot. + # This satisfies any pending reboot requirements from earlier steps. + needs_reboot = False # Reboot device as above steps would need it and also it brings device in a # good state. From def65fa1ae506d18eeae606701e613442a1f220b Mon Sep 17 00:00:00 2001 From: Diego Jardon Date: Sat, 16 May 2026 17:17:37 +0000 Subject: [PATCH 06/14] Fix initialization reboot logic to correctly preserve base requirements --- src/clusterfuzz/_internal/platforms/android/device.py | 9 +++------ 1 file changed, 3 insertions(+), 6 deletions(-) diff --git a/src/clusterfuzz/_internal/platforms/android/device.py b/src/clusterfuzz/_internal/platforms/android/device.py index a80826ba465..0cc1512c2ff 100755 --- a/src/clusterfuzz/_internal/platforms/android/device.py +++ b/src/clusterfuzz/_internal/platforms/android/device.py @@ -308,19 +308,16 @@ def initialize_device(): adb.setup_adb() # General device configuration settings. - needs_reboot = configure_system_build_properties() + needs_full_reboot = configure_system_build_properties() configure_device_settings() add_test_accounts_if_needed() # Setup AddressSanitizer if needed. - if sanitizer.setup_asan_if_needed(): - # ASan setup script performs a shell restart and waits for boot. - # This satisfies any pending reboot requirements from earlier steps. - needs_reboot = False + asan_reboot_done = sanitizer.setup_asan_if_needed() # Reboot device as above steps would need it and also it brings device in a # good state. - if needs_reboot: + if needs_full_reboot or not asan_reboot_done: reboot() # Make sure we are running as root after restart. From ed141f17ed40855ae05cf31317fa9a02f0145817 Mon Sep 17 00:00:00 2001 From: Diego Jardon Date: Sat, 16 May 2026 17:21:36 +0000 Subject: [PATCH 07/14] Add unit tests for android device reboot logic --- .../core/platforms/android/device_test.py | 60 +++++++++++++++++++ 1 file changed, 60 insertions(+) diff --git a/src/clusterfuzz/_internal/tests/core/platforms/android/device_test.py b/src/clusterfuzz/_internal/tests/core/platforms/android/device_test.py index e10051fb7ef..726739dcad7 100644 --- a/src/clusterfuzz/_internal/tests/core/platforms/android/device_test.py +++ b/src/clusterfuzz/_internal/tests/core/platforms/android/device_test.py @@ -13,6 +13,9 @@ # limitations under the License. """Tests for device functions.""" +import unittest +from unittest import mock + from clusterfuzz._internal.platforms.android import device from clusterfuzz._internal.tests.test_libs import android_helpers @@ -23,3 +26,60 @@ class InitializeEnvironmentTest(android_helpers.AndroidTest): def test(self): """Ensure that initialize_environment throws no exceptions.""" device.initialize_environment() + + +class InitializeDeviceRebootLogicTest(unittest.TestCase): + """Tests the reboot batching logic in initialize_device.""" + + def setUp(self): + # Mock environment to bypass the engine fuzzer check + mock.patch('clusterfuzz._internal.system.environment.is_engine_fuzzer_job', + return_value=False).start() + + # Mock all the setup steps so we don't actually run ADB commands + self.mock_setup_adb = mock.patch('clusterfuzz._internal.platforms.android.adb.setup_adb').start() + self.mock_run_as_root = mock.patch('clusterfuzz._internal.platforms.android.adb.run_as_root').start() + self.mock_config_props = mock.patch('clusterfuzz._internal.platforms.android.device.configure_system_build_properties').start() + self.mock_config_settings = mock.patch('clusterfuzz._internal.platforms.android.device.configure_device_settings').start() + self.mock_add_accounts = mock.patch('clusterfuzz._internal.platforms.android.device.add_test_accounts_if_needed').start() + self.mock_setup_asan = mock.patch('clusterfuzz._internal.platforms.android.sanitizer.setup_asan_if_needed').start() + + # Mock the reboot function we are trying to track + self.mock_reboot = mock.patch('clusterfuzz._internal.platforms.android.device.reboot').start() + + # Mock the post-reboot steps + mock.patch('clusterfuzz._internal.platforms.android.wifi.configure').start() + mock.patch('clusterfuzz._internal.platforms.android.device.setup_host_and_device_forwarder_if_needed').start() + mock.patch('clusterfuzz._internal.platforms.android.settings.change_se_linux_to_permissive_mode').start() + mock.patch('clusterfuzz._internal.platforms.android.app.wait_until_optimization_complete').start() + mock.patch('clusterfuzz._internal.platforms.android.ui.clear_notifications').start() + mock.patch('clusterfuzz._internal.platforms.android.ui.unlock_screen').start() + + def tearDown(self): + mock.patch.stopall() + + def test_reboot_if_props_changed(self): + """Test that it reboots if build.prop changed, even if ASan didn't run.""" + self.mock_config_props.return_value = True + self.mock_setup_asan.return_value = False + + device.initialize_device() + self.mock_reboot.assert_called_once() + + def test_no_reboot_if_asan_ran(self): + """Test that the final reboot is skipped if ASan setup performed a shell restart.""" + # If build.prop didn't change, the ASan restart handles the clean state. + self.mock_config_props.return_value = False + self.mock_setup_asan.return_value = True + + device.initialize_device() + self.mock_reboot.assert_not_called() + + def test_reboot_if_clean_slate_needed(self): + """Test that it still reboots to ensure a clean state if no other steps restarted it.""" + self.mock_config_props.return_value = False + self.mock_setup_asan.return_value = False + + device.initialize_device() + self.mock_reboot.assert_called_once() + From a0d418e4081e78a6b71c28112a72813f9f1cb0f3 Mon Sep 17 00:00:00 2001 From: Diego Jardon Date: Mon, 18 May 2026 19:23:24 +0000 Subject: [PATCH 08/14] Fix formatting issues from linter in device_test.py --- .../core/platforms/android/device_test.py | 63 ++++++++++++------- 1 file changed, 41 insertions(+), 22 deletions(-) diff --git a/src/clusterfuzz/_internal/tests/core/platforms/android/device_test.py b/src/clusterfuzz/_internal/tests/core/platforms/android/device_test.py index 726739dcad7..7a5f792f545 100644 --- a/src/clusterfuzz/_internal/tests/core/platforms/android/device_test.py +++ b/src/clusterfuzz/_internal/tests/core/platforms/android/device_test.py @@ -33,27 +33,47 @@ class InitializeDeviceRebootLogicTest(unittest.TestCase): def setUp(self): # Mock environment to bypass the engine fuzzer check - mock.patch('clusterfuzz._internal.system.environment.is_engine_fuzzer_job', - return_value=False).start() - + mock.patch( + 'clusterfuzz._internal.system.environment.is_engine_fuzzer_job', + return_value=False).start() + # Mock all the setup steps so we don't actually run ADB commands - self.mock_setup_adb = mock.patch('clusterfuzz._internal.platforms.android.adb.setup_adb').start() - self.mock_run_as_root = mock.patch('clusterfuzz._internal.platforms.android.adb.run_as_root').start() - self.mock_config_props = mock.patch('clusterfuzz._internal.platforms.android.device.configure_system_build_properties').start() - self.mock_config_settings = mock.patch('clusterfuzz._internal.platforms.android.device.configure_device_settings').start() - self.mock_add_accounts = mock.patch('clusterfuzz._internal.platforms.android.device.add_test_accounts_if_needed').start() - self.mock_setup_asan = mock.patch('clusterfuzz._internal.platforms.android.sanitizer.setup_asan_if_needed').start() - + self.mock_setup_adb = mock.patch( + 'clusterfuzz._internal.platforms.android.adb.setup_adb').start() + self.mock_run_as_root = mock.patch( + 'clusterfuzz._internal.platforms.android.adb.run_as_root').start() + self.mock_config_props = mock.patch( + 'clusterfuzz._internal.platforms.android.device.configure_system_build_properties' + ).start() + self.mock_config_settings = mock.patch( + 'clusterfuzz._internal.platforms.android.device.configure_device_settings' + ).start() + self.mock_add_accounts = mock.patch( + 'clusterfuzz._internal.platforms.android.device.add_test_accounts_if_needed' + ).start() + self.mock_setup_asan = mock.patch( + 'clusterfuzz._internal.platforms.android.sanitizer.setup_asan_if_needed' + ).start() + # Mock the reboot function we are trying to track - self.mock_reboot = mock.patch('clusterfuzz._internal.platforms.android.device.reboot').start() - + self.mock_reboot = mock.patch( + 'clusterfuzz._internal.platforms.android.device.reboot').start() + # Mock the post-reboot steps mock.patch('clusterfuzz._internal.platforms.android.wifi.configure').start() - mock.patch('clusterfuzz._internal.platforms.android.device.setup_host_and_device_forwarder_if_needed').start() - mock.patch('clusterfuzz._internal.platforms.android.settings.change_se_linux_to_permissive_mode').start() - mock.patch('clusterfuzz._internal.platforms.android.app.wait_until_optimization_complete').start() - mock.patch('clusterfuzz._internal.platforms.android.ui.clear_notifications').start() - mock.patch('clusterfuzz._internal.platforms.android.ui.unlock_screen').start() + mock.patch( + 'clusterfuzz._internal.platforms.android.device.setup_host_and_device_forwarder_if_needed' + ).start() + mock.patch( + 'clusterfuzz._internal.platforms.android.settings.change_se_linux_to_permissive_mode' + ).start() + mock.patch( + 'clusterfuzz._internal.platforms.android.app.wait_until_optimization_complete' + ).start() + mock.patch('clusterfuzz._internal.platforms.android.ui.clear_notifications' + ).start() + mock.patch( + 'clusterfuzz._internal.platforms.android.ui.unlock_screen').start() def tearDown(self): mock.patch.stopall() @@ -62,16 +82,16 @@ def test_reboot_if_props_changed(self): """Test that it reboots if build.prop changed, even if ASan didn't run.""" self.mock_config_props.return_value = True self.mock_setup_asan.return_value = False - + device.initialize_device() self.mock_reboot.assert_called_once() def test_no_reboot_if_asan_ran(self): """Test that the final reboot is skipped if ASan setup performed a shell restart.""" # If build.prop didn't change, the ASan restart handles the clean state. - self.mock_config_props.return_value = False + self.mock_config_props.return_value = False self.mock_setup_asan.return_value = True - + device.initialize_device() self.mock_reboot.assert_not_called() @@ -79,7 +99,6 @@ def test_reboot_if_clean_slate_needed(self): """Test that it still reboots to ensure a clean state if no other steps restarted it.""" self.mock_config_props.return_value = False self.mock_setup_asan.return_value = False - + device.initialize_device() self.mock_reboot.assert_called_once() - From 53bdbc80c295fafeb571331370d345dc50c1f976 Mon Sep 17 00:00:00 2001 From: Diego Jardon Date: Mon, 18 May 2026 23:07:33 +0000 Subject: [PATCH 09/14] Address PR 5280 comments: Add return types and docstrings Adds `-> bool` return type hints and explanatory docstrings to: - `configure_system_build_properties` - `setup_asan_if_needed` These updates clarify that the returned boolean indicates whether a device reboot occurred during the setup step. --- src/clusterfuzz/_internal/platforms/android/device.py | 8 ++++++-- src/clusterfuzz/_internal/platforms/android/sanitizer.py | 8 ++++++-- 2 files changed, 12 insertions(+), 4 deletions(-) diff --git a/src/clusterfuzz/_internal/platforms/android/device.py b/src/clusterfuzz/_internal/platforms/android/device.py index 0cc1512c2ff..5a4461a84a5 100755 --- a/src/clusterfuzz/_internal/platforms/android/device.py +++ b/src/clusterfuzz/_internal/platforms/android/device.py @@ -183,9 +183,13 @@ def configure_device_settings(): adb.write_data_to_file(local_properties_file_contents, LOCAL_PROP_PATH) -def configure_system_build_properties(): +def configure_system_build_properties() -> bool: """Modifies system build properties in /system/build.prop for better boot - speed and power use.""" + speed and power use. + + Returns: + True if the device was rebooted during configuration, False otherwise. + """ adb.run_as_root() # Check md5 checksum of build.prop to see if already updated, diff --git a/src/clusterfuzz/_internal/platforms/android/sanitizer.py b/src/clusterfuzz/_internal/platforms/android/sanitizer.py index f5d954e0a2c..de51bd4207b 100644 --- a/src/clusterfuzz/_internal/platforms/android/sanitizer.py +++ b/src/clusterfuzz/_internal/platforms/android/sanitizer.py @@ -74,8 +74,12 @@ def set_options(sanitizer_tool_name, sanitizer_options): sanitizer_options, sanitizer_options_file_path, wait_for_reboot=False) -def setup_asan_if_needed(): - """Set up asan on device.""" +def setup_asan_if_needed() -> bool: + """Set up asan on device. + + Returns: + True if the device was rebooted or restarted during setup, False otherwise. + """ if not environment.get_value('ASAN_DEVICE_SETUP'): # Only do this step if explicitly enabled in the job type. This cannot be # determined from libraries in application directory since they can go From fd28f7b73bdbbd4b6399ed4034a90e4f57a3bb30 Mon Sep 17 00:00:00 2001 From: Diego Jardon Date: Wed, 20 May 2026 04:48:00 +0000 Subject: [PATCH 10/14] Rename wait_for_reboot to should_reboot --- src/clusterfuzz/_internal/platforms/android/adb.py | 4 ++-- src/clusterfuzz/_internal/platforms/android/sanitizer.py | 2 +- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/src/clusterfuzz/_internal/platforms/android/adb.py b/src/clusterfuzz/_internal/platforms/android/adb.py index 1f5eefcd861..071922e084d 100755 --- a/src/clusterfuzz/_internal/platforms/android/adb.py +++ b/src/clusterfuzz/_internal/platforms/android/adb.py @@ -880,7 +880,7 @@ def write_command_line_file(command_line, app_path): write_data_to_file(command_line_file_contents, command_line_path) -def write_data_to_file(contents, file_path, wait_for_reboot=True): +def write_data_to_file(contents, file_path, should_reboot=True): """Writes content to file.""" # If this is a file in /system, we need to remount /system as read-write and # after file is written, revert it back to read-only. @@ -895,6 +895,6 @@ def write_data_to_file(contents, file_path, wait_for_reboot=True): # Make command line file is readable. run_shell_command('chmod 0644 %s' % file_path, root=True) - if is_system_file and wait_for_reboot: + if is_system_file and should_reboot: reboot() wait_until_fully_booted() diff --git a/src/clusterfuzz/_internal/platforms/android/sanitizer.py b/src/clusterfuzz/_internal/platforms/android/sanitizer.py index de51bd4207b..c602df87221 100644 --- a/src/clusterfuzz/_internal/platforms/android/sanitizer.py +++ b/src/clusterfuzz/_internal/platforms/android/sanitizer.py @@ -71,7 +71,7 @@ def set_options(sanitizer_tool_name, sanitizer_options): # Skip reboot as the app will pick up the options file on restart. adb.write_data_to_file( - sanitizer_options, sanitizer_options_file_path, wait_for_reboot=False) + sanitizer_options, sanitizer_options_file_path, should_reboot=False) def setup_asan_if_needed() -> bool: From 169b8ae7ef0cfcbaa7cc700309c767ff83b0f346 Mon Sep 17 00:00:00 2001 From: Diego Jardon Date: Wed, 20 May 2026 04:55:02 +0000 Subject: [PATCH 11/14] Update docstrings for test cases --- .../tests/core/platforms/android/device_test.py | 10 +++++++--- 1 file changed, 7 insertions(+), 3 deletions(-) diff --git a/src/clusterfuzz/_internal/tests/core/platforms/android/device_test.py b/src/clusterfuzz/_internal/tests/core/platforms/android/device_test.py index 7a5f792f545..00811c5312a 100644 --- a/src/clusterfuzz/_internal/tests/core/platforms/android/device_test.py +++ b/src/clusterfuzz/_internal/tests/core/platforms/android/device_test.py @@ -79,7 +79,8 @@ def tearDown(self): mock.patch.stopall() def test_reboot_if_props_changed(self): - """Test that it reboots if build.prop changed, even if ASan didn't run.""" + """Test that `initialize_device()` reboots the device if + `configure_system_build_properties()` did, and the ASan setup script did not.""" self.mock_config_props.return_value = True self.mock_setup_asan.return_value = False @@ -87,7 +88,9 @@ def test_reboot_if_props_changed(self): self.mock_reboot.assert_called_once() def test_no_reboot_if_asan_ran(self): - """Test that the final reboot is skipped if ASan setup performed a shell restart.""" + """Test that `initialize_device()` skips calling `reboot()` if + `configure_system_build_properties()` did not cause a reboot but the ASan + setup script did.""" # If build.prop didn't change, the ASan restart handles the clean state. self.mock_config_props.return_value = False self.mock_setup_asan.return_value = True @@ -96,7 +99,8 @@ def test_no_reboot_if_asan_ran(self): self.mock_reboot.assert_not_called() def test_reboot_if_clean_slate_needed(self): - """Test that it still reboots to ensure a clean state if no other steps restarted it.""" + """Test that `initialize_device()` calls `reboot()` if neither + `configure_system_build_properties()` nor the ASan setup script did.""" self.mock_config_props.return_value = False self.mock_setup_asan.return_value = False From 01dc2a9e21ef7eabd27efc662d176d27d6fa9951 Mon Sep 17 00:00:00 2001 From: Diego Jardon Date: Wed, 20 May 2026 05:05:08 +0000 Subject: [PATCH 12/14] Simplify initialize_device logic and refactor tests - Revert configure_system_build_properties to not return a bool. - Simplify initialize_device to only reboot if ASan setup did not reboot the device. This fixes the bug where it would reboot even if no properties changed. - Refactor device_test.py to use helpers.patch instead of manual mock setups, matching the codebase style. --- .../_internal/platforms/android/device.py | 20 ++-- .../core/platforms/android/device_test.py | 93 ++++++------------- 2 files changed, 33 insertions(+), 80 deletions(-) diff --git a/src/clusterfuzz/_internal/platforms/android/device.py b/src/clusterfuzz/_internal/platforms/android/device.py index 5a4461a84a5..6cf783e5a57 100755 --- a/src/clusterfuzz/_internal/platforms/android/device.py +++ b/src/clusterfuzz/_internal/platforms/android/device.py @@ -183,13 +183,9 @@ def configure_device_settings(): adb.write_data_to_file(local_properties_file_contents, LOCAL_PROP_PATH) -def configure_system_build_properties() -> bool: +def configure_system_build_properties(): """Modifies system build properties in /system/build.prop for better boot - speed and power use. - - Returns: - True if the device was rebooted during configuration, False otherwise. - """ + speed and power use.""" adb.run_as_root() # Check md5 checksum of build.prop to see if already updated, @@ -199,9 +195,9 @@ def configure_system_build_properties() -> bool: current_md5 = adb.get_file_checksum(BUILD_PROP_PATH) if current_md5 is None: logs.error('Unable to find %s on device.' % BUILD_PROP_PATH) - return False + return if old_md5 == current_md5: - return False + return # Pull to tmp file. bot_tmp_directory = environment.get_value('BOT_TMPDIR') @@ -209,7 +205,7 @@ def configure_system_build_properties() -> bool: adb.run_command(['pull', BUILD_PROP_PATH, old_build_prop_path]) if not os.path.exists(old_build_prop_path): logs.error('Unable to fetch %s from device.' % BUILD_PROP_PATH) - return False + return # Write new build.prop. new_build_prop_path = os.path.join(bot_tmp_directory, 'new.prop') @@ -259,8 +255,6 @@ def configure_system_build_properties() -> bool: current_md5 = adb.get_file_checksum(BUILD_PROP_PATH) persistent_cache.set_value(constants.BUILD_PROP_MD5_KEY, current_md5) - return True - def get_debug_props_and_values(): """Return debug property names and values based on |ENABLE_DEBUG_CHECKS| @@ -312,7 +306,7 @@ def initialize_device(): adb.setup_adb() # General device configuration settings. - needs_full_reboot = configure_system_build_properties() + configure_system_build_properties() configure_device_settings() add_test_accounts_if_needed() @@ -321,7 +315,7 @@ def initialize_device(): # Reboot device as above steps would need it and also it brings device in a # good state. - if needs_full_reboot or not asan_reboot_done: + if not asan_reboot_done: reboot() # Make sure we are running as root after restart. diff --git a/src/clusterfuzz/_internal/tests/core/platforms/android/device_test.py b/src/clusterfuzz/_internal/tests/core/platforms/android/device_test.py index 00811c5312a..2c0b8fb9450 100644 --- a/src/clusterfuzz/_internal/tests/core/platforms/android/device_test.py +++ b/src/clusterfuzz/_internal/tests/core/platforms/android/device_test.py @@ -14,7 +14,6 @@ """Tests for device functions.""" import unittest -from unittest import mock from clusterfuzz._internal.platforms.android import device from clusterfuzz._internal.tests.test_libs import android_helpers @@ -32,77 +31,37 @@ class InitializeDeviceRebootLogicTest(unittest.TestCase): """Tests the reboot batching logic in initialize_device.""" def setUp(self): - # Mock environment to bypass the engine fuzzer check - mock.patch( + from clusterfuzz._internal.tests.test_libs import helpers + helpers.patch(self, [ 'clusterfuzz._internal.system.environment.is_engine_fuzzer_job', - return_value=False).start() - - # Mock all the setup steps so we don't actually run ADB commands - self.mock_setup_adb = mock.patch( - 'clusterfuzz._internal.platforms.android.adb.setup_adb').start() - self.mock_run_as_root = mock.patch( - 'clusterfuzz._internal.platforms.android.adb.run_as_root').start() - self.mock_config_props = mock.patch( - 'clusterfuzz._internal.platforms.android.device.configure_system_build_properties' - ).start() - self.mock_config_settings = mock.patch( - 'clusterfuzz._internal.platforms.android.device.configure_device_settings' - ).start() - self.mock_add_accounts = mock.patch( - 'clusterfuzz._internal.platforms.android.device.add_test_accounts_if_needed' - ).start() - self.mock_setup_asan = mock.patch( - 'clusterfuzz._internal.platforms.android.sanitizer.setup_asan_if_needed' - ).start() - - # Mock the reboot function we are trying to track - self.mock_reboot = mock.patch( - 'clusterfuzz._internal.platforms.android.device.reboot').start() - - # Mock the post-reboot steps - mock.patch('clusterfuzz._internal.platforms.android.wifi.configure').start() - mock.patch( - 'clusterfuzz._internal.platforms.android.device.setup_host_and_device_forwarder_if_needed' - ).start() - mock.patch( - 'clusterfuzz._internal.platforms.android.settings.change_se_linux_to_permissive_mode' - ).start() - mock.patch( - 'clusterfuzz._internal.platforms.android.app.wait_until_optimization_complete' - ).start() - mock.patch('clusterfuzz._internal.platforms.android.ui.clear_notifications' - ).start() - mock.patch( - 'clusterfuzz._internal.platforms.android.ui.unlock_screen').start() - - def tearDown(self): - mock.patch.stopall() - - def test_reboot_if_props_changed(self): - """Test that `initialize_device()` reboots the device if - `configure_system_build_properties()` did, and the ASan setup script did not.""" - self.mock_config_props.return_value = True - self.mock_setup_asan.return_value = False + 'clusterfuzz._internal.platforms.android.adb.setup_adb', + 'clusterfuzz._internal.platforms.android.adb.run_as_root', + 'clusterfuzz._internal.platforms.android.device.configure_system_build_properties', + 'clusterfuzz._internal.platforms.android.device.configure_device_settings', + 'clusterfuzz._internal.platforms.android.device.add_test_accounts_if_needed', + 'clusterfuzz._internal.platforms.android.sanitizer.setup_asan_if_needed', + 'clusterfuzz._internal.platforms.android.device.reboot', + 'clusterfuzz._internal.platforms.android.wifi.configure', + 'clusterfuzz._internal.platforms.android.device.setup_host_and_device_forwarder_if_needed', + 'clusterfuzz._internal.platforms.android.settings.change_se_linux_to_permissive_mode', + 'clusterfuzz._internal.platforms.android.app.wait_until_optimization_complete', + 'clusterfuzz._internal.platforms.android.ui.clear_notifications', + 'clusterfuzz._internal.platforms.android.ui.unlock_screen', + ]) + self.mock.is_engine_fuzzer_job.return_value = False + + def test_reboot_if_asan_did_not_run(self): + """Test that `initialize_device()` calls `reboot()` if the ASan setup + script did not.""" + self.mock.setup_asan_if_needed.return_value = False device.initialize_device() - self.mock_reboot.assert_called_once() + self.mock.reboot.assert_called_once() def test_no_reboot_if_asan_ran(self): - """Test that `initialize_device()` skips calling `reboot()` if - `configure_system_build_properties()` did not cause a reboot but the ASan + """Test that `initialize_device()` skips calling `reboot()` if the ASan setup script did.""" - # If build.prop didn't change, the ASan restart handles the clean state. - self.mock_config_props.return_value = False - self.mock_setup_asan.return_value = True - - device.initialize_device() - self.mock_reboot.assert_not_called() - - def test_reboot_if_clean_slate_needed(self): - """Test that `initialize_device()` calls `reboot()` if neither - `configure_system_build_properties()` nor the ASan setup script did.""" - self.mock_config_props.return_value = False - self.mock_setup_asan.return_value = False + self.mock.setup_asan_if_needed.return_value = True device.initialize_device() - self.mock_reboot.assert_called_once() + self.mock.reboot.assert_not_called() From 450b66e9d04c4501517714837c57880c24bd9721 Mon Sep 17 00:00:00 2001 From: Diego Jardon Date: Wed, 20 May 2026 05:18:02 +0000 Subject: [PATCH 13/14] Add descriptive docstring for InitializeEnvironmentTest --- .../_internal/tests/core/platforms/android/device_test.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/clusterfuzz/_internal/tests/core/platforms/android/device_test.py b/src/clusterfuzz/_internal/tests/core/platforms/android/device_test.py index 2c0b8fb9450..ebb6ba11a26 100644 --- a/src/clusterfuzz/_internal/tests/core/platforms/android/device_test.py +++ b/src/clusterfuzz/_internal/tests/core/platforms/android/device_test.py @@ -20,7 +20,7 @@ class InitializeEnvironmentTest(android_helpers.AndroidTest): - """Tests for """ + """Tests for the device environment initialization process (`initialize_environment`).""" def test(self): """Ensure that initialize_environment throws no exceptions.""" From 2888eab0d76b96c94de826048d242a058745899c Mon Sep 17 00:00:00 2001 From: Diego Jardon Date: Wed, 20 May 2026 05:37:11 +0000 Subject: [PATCH 14/14] Add docstring to write_data_to_file --- src/clusterfuzz/_internal/platforms/android/adb.py | 9 ++++++++- 1 file changed, 8 insertions(+), 1 deletion(-) diff --git a/src/clusterfuzz/_internal/platforms/android/adb.py b/src/clusterfuzz/_internal/platforms/android/adb.py index 071922e084d..0776ecfeb26 100755 --- a/src/clusterfuzz/_internal/platforms/android/adb.py +++ b/src/clusterfuzz/_internal/platforms/android/adb.py @@ -881,7 +881,14 @@ def write_command_line_file(command_line, app_path): def write_data_to_file(contents, file_path, should_reboot=True): - """Writes content to file.""" + """Writes content to file. + + Args: + contents: The string content to write. + file_path: The path to the file on the Android device. + should_reboot: Whether to reboot the device if a system file is modified. + Defaults to True. + """ # If this is a file in /system, we need to remount /system as read-write and # after file is written, revert it back to read-only. is_system_file = file_path.startswith('/system')