diff --git a/src/clusterfuzz/_internal/base/feature_flags.py b/src/clusterfuzz/_internal/base/feature_flags.py index 5dfeccae1c..9660745990 100644 --- a/src/clusterfuzz/_internal/base/feature_flags.py +++ b/src/clusterfuzz/_internal/base/feature_flags.py @@ -44,6 +44,7 @@ class FeatureFlags(Enum): ENABLE_FUZZ_FOR_BOTS = 'enable_fuzz_for_bots' STORAGE_THREADED_OPS_FUZZ_TARGETS = 'storage_threaded_ops_fuzz_targets' + CALL_ANDROID_API = 'call_android_api' @property def flag(self): diff --git a/src/clusterfuzz/_internal/platforms/android/fetch_artifact.py b/src/clusterfuzz/_internal/platforms/android/fetch_artifact.py index a3fa414ecc..961e012bc5 100644 --- a/src/clusterfuzz/_internal/platforms/android/fetch_artifact.py +++ b/src/clusterfuzz/_internal/platforms/android/fetch_artifact.py @@ -26,6 +26,7 @@ from google.oauth2 import service_account from oauth2client.service_account import ServiceAccountCredentials +from clusterfuzz._internal.base import feature_flags from clusterfuzz._internal.config import db_config from clusterfuzz._internal.google_cloud_utils import storage from clusterfuzz._internal.metrics import logs @@ -33,8 +34,6 @@ AndroidBuildV4Api from clusterfuzz._internal.system import environment -from . import adb - # 20 MB default chunk size. DEFAULT_CHUNK_SIZE = 20 * 1024 * 1024 @@ -72,6 +71,20 @@ def _use_v4(): return False +def _call_android_api_enabled(): + """Return True if we should call the Android Build API, enabled by default, + Disabled always if invoked in a uworker + """ + if environment.is_uworker(): + logs.info('AndroidBuildAPI access disabled for uworker.') + return False + + flag = feature_flags.FeatureFlags.CALL_ANDROID_API.flag + if flag is None: + return True + return flag.enabled + + def _execute_request_with_retries(request): """Executes request and retries on failure.""" result = None @@ -282,7 +295,6 @@ def _get_artifacts_for_build(client, if not artifacts: logs.error(f'No artifact found for target {target}, build id {bid}.\n' f'results {results}') - adb.bad_state_reached() return artifacts @@ -343,6 +355,11 @@ def _get_stable_build_info(): def get_latest_artifact_info(branch, target, signed=False, stable_build=False): """Return latest artifact for a branch and target.""" + if not _call_android_api_enabled(): + logs.warning( + 'Android build API is disabled by feature flag call_android_api.') + return None + client = _get_client() if not client: return None @@ -409,6 +426,11 @@ def get_latest_artifact_info(branch, target, signed=False, stable_build=False): def get(bid, target, regex, output_directory, output_filename=None): """Return artifact for a given build id, target and file regex.""" + if not _call_android_api_enabled(): + logs.warning( + 'Android build API is disabled by feature flag call_android_api.') + return None + client = _get_client() if not client: return None diff --git a/src/clusterfuzz/_internal/platforms/android/flash.py b/src/clusterfuzz/_internal/platforms/android/flash.py index b292b1491e..283a80120d 100644 --- a/src/clusterfuzz/_internal/platforms/android/flash.py +++ b/src/clusterfuzz/_internal/platforms/android/flash.py @@ -85,7 +85,7 @@ def download_latest_build(build_info, image_regexes, image_directory): logs.error('Failed to download artifact %s for ' 'branch %s and target %s.' % (image_file_paths, build_info['branch'], target)) - return + adb.bad_state_reached() for file_path in image_file_paths: if file_path.endswith('.zip') or file_path.endswith('.tar.gz'): @@ -97,7 +97,10 @@ def boot_stable_build_cuttlefish(branch, target, image_directory): """Boot cuttlefish instance using stable build id fetched from gcs.""" build_info = fetch_artifact.get_latest_artifact_info( branch, target, stable_build=True) - download_latest_build(build_info, FLASH_CUTTLEFISH_REGEXES, image_directory) + if not build_info: + logs.error('Unable to fetch stable build info for cuttlefish.') + else: + download_latest_build(build_info, FLASH_CUTTLEFISH_REGEXES, image_directory) adb.recreate_cuttlefish_device() adb.connect_to_cuttlefish_device() diff --git a/src/clusterfuzz/_internal/platforms/android/symbols_downloader.py b/src/clusterfuzz/_internal/platforms/android/symbols_downloader.py index 112d86fc21..55a906b549 100644 --- a/src/clusterfuzz/_internal/platforms/android/symbols_downloader.py +++ b/src/clusterfuzz/_internal/platforms/android/symbols_downloader.py @@ -196,7 +196,12 @@ def download_trusty_symbols_if_needed(symbols_directory, app_name, bid): branch = 'polygon-trusty-whitechapel-master' if not bid: - bid = fetch_artifact.get_latest_artifact_info(branch, ab_target)['bid'] + build_info = fetch_artifact.get_latest_artifact_info(branch, ab_target) + if not build_info: + logs.error(f'Unable to fetch build info for branch {branch} ' + f'and target {ab_target}.') + return + bid = build_info['bid'] artifact_filename = f'{ab_target}-{bid}.syms.zip' symbols_archive_path = os.path.join(symbols_directory, artifact_filename) @@ -204,6 +209,10 @@ def download_trusty_symbols_if_needed(symbols_directory, app_name, bid): download_artifact_if_needed(bid, symbols_directory, symbols_archive_path, [ab_target], artifact_filename, None) + if not os.path.exists(symbols_archive_path): + logs.error(f'Unable to locate symbols archive {symbols_archive_path}.') + return + with zipfile.ZipFile(symbols_archive_path, 'r') as symbols_zipfile: for filepath in symbols_zipfile.namelist(): if f'{app_name}.syms.elf' in filepath: diff --git a/src/clusterfuzz/_internal/tests/core/platforms/android/fetch_artifact_test.py b/src/clusterfuzz/_internal/tests/core/platforms/android/fetch_artifact_test.py index e01de1ba82..5417a64e22 100644 --- a/src/clusterfuzz/_internal/tests/core/platforms/android/fetch_artifact_test.py +++ b/src/clusterfuzz/_internal/tests/core/platforms/android/fetch_artifact_test.py @@ -18,6 +18,7 @@ import unittest from unittest import mock +from clusterfuzz._internal.base import feature_flags from clusterfuzz._internal.platforms.android import fetch_artifact from clusterfuzz._internal.tests.test_libs import helpers @@ -29,10 +30,12 @@ def setUp(self): helpers.patch(self, [ 'clusterfuzz._internal.platforms.android.fetch_artifact._get_client', 'clusterfuzz._internal.platforms.android.fetch_artifact._use_v4', + 'clusterfuzz._internal.platforms.android.fetch_artifact._call_android_api_enabled', 'clusterfuzz._internal.platforms.android.fetch_artifact._execute_request_with_retries', ]) self.mock_client = mock.MagicMock() self.mock._get_client.return_value = self.mock_client + self.mock._call_android_api_enabled.return_value = True def test_get_latest_artifact_v4_success(self): """Tests get_latest_artifact_info (V4). Expects extraction of {bid, branch, target} when list_builds returns data.""" @@ -116,6 +119,13 @@ def test_get_latest_artifact_client_auth_failure(self): result = fetch_artifact.get_latest_artifact_info('branch1', 'target1') self.assertIsNone(result) + def test_get_latest_artifact_disabled_by_feature_flag(self): + """Tests get_latest_artifact_info exits early and returns None when disabled by feature flag.""" + self.mock._call_android_api_enabled.return_value = False + + result = fetch_artifact.get_latest_artifact_info('branch1', 'target1') + self.assertIsNone(result) + def test_get_artifacts_for_build_empty_regexp(self): """Tests _get_artifacts_for_build returns [] returning early when regexp is empty, bypassing API calls.""" result = fetch_artifact._get_artifacts_for_build( @@ -123,3 +133,42 @@ def test_get_artifacts_for_build_empty_regexp(self): self.assertEqual(result, []) self.mock_client.list_artifacts.assert_not_called() self.mock_client.buildartifact().list.assert_not_called() + + +class CallAndroidApiEnabledTest(unittest.TestCase): + """Tests for _call_android_api_enabled.""" + + def setUp(self): + helpers.patch(self, [ + 'clusterfuzz._internal.system.environment.is_uworker', + ]) + self.mock.is_uworker.return_value = False + + def test_is_uworker_returns_false(self): + self.mock.is_uworker.return_value = True + self.assertFalse(fetch_artifact._call_android_api_enabled()) + + def test_flag_none_returns_true(self): + with mock.patch.object( + feature_flags.FeatureFlags, 'flag', + new_callable=mock.PropertyMock) as mock_flag: + mock_flag.return_value = None + self.assertTrue(fetch_artifact._call_android_api_enabled()) + + def test_flag_enabled_returns_true(self): + mock_flag_obj = mock.MagicMock() + mock_flag_obj.enabled = True + with mock.patch.object( + feature_flags.FeatureFlags, 'flag', + new_callable=mock.PropertyMock) as mock_flag: + mock_flag.return_value = mock_flag_obj + self.assertTrue(fetch_artifact._call_android_api_enabled()) + + def test_flag_disabled_returns_false(self): + mock_flag_obj = mock.MagicMock() + mock_flag_obj.enabled = False + with mock.patch.object( + feature_flags.FeatureFlags, 'flag', + new_callable=mock.PropertyMock) as mock_flag: + mock_flag.return_value = mock_flag_obj + self.assertFalse(fetch_artifact._call_android_api_enabled())