ANDROID:Introduce find_build_breakage.sh to identify Android build regressions.
Test: ./find_build_breakage.sh -pb "ab://git_main/aosp_cf_x86_64_only_phone-trunk_staging-userdebug/13609606-13610298" \
-t "CtsHibernationTestCases android.hibernation.cts.AutoRevokeTest#testUnusedApp_uninstallApp" \
-td ~/Downloads/android-cts --skip-build -tr 2 -sr 2
Bug: 433196326
Change-Id: I0014901e9d662b946ee0819bc88bd88ee5bd1629
Signed-off-by: Darren Chang <chihsheng@google.com>
diff --git a/tools/find_build_breakage.sh b/tools/find_build_breakage.sh
new file mode 100755
index 0000000..b434746
--- /dev/null
+++ b/tools/find_build_breakage.sh
@@ -0,0 +1,966 @@
+#!/usr/bin/env bash
+# SPDX-License-Identifier: GPL-2.0
+
+#
+# A build-level bisect testing tool to find breaking changes in Android builds.
+#
+
+# --- Configuration Constants ---
+readonly DEFAULT_BISECT_BUILDS_FILENAME="bisect_builds.xml"
+readonly DEFAULT_OUTPUT_DIR="out/$(date +%Y%m%d_%H%M%S)"
+readonly DEFAULT_TEST_RETRY=3
+readonly DEFAULT_SETUP_RETRY=3
+
+# --- Global Variables ---
+ACLOUD_OUTPUT_FILE="/tmp/acloud_output.tmp"
+BUILD_TYPE=""
+DEVICE_TYPE=""
+PLATFORM_BUILD=""
+KERNEL_BUILD=""
+VENDOR_KERNEL_BUILD=""
+SERIAL_NUMBER=""
+TEST_NAME=()
+TEST_DIR=""
+TEST_RETRY=$DEFAULT_TEST_RETRY
+SETUP_RETRY=$DEFAULT_SETUP_RETRY
+OUTPUT_DIR=""
+INPUT_FILE=""
+BISECT_FILE=""
+SKIP_BUILD=false
+TEMP_FILES=("$ACLOUD_OUTPUT_FILE")
+CURRENT_INPUT_SERIAL=""
+CURRENT_FASTBOOT_SERIAL=""
+CURRENT_ADB_SERIAL=""
+CURRENT_DEVICE_SERIAL=""
+CURRENT_MODE_TYPE=""
+CURRENT_DEVICE_TYPE=""
+
+# Bisect state variables
+GOOD_INDEX=-1
+BAD_INDEX=-1
+BISECT_STATUS=""
+BUILDS_TO_TEST=()
+BISECT_BRANCH=""
+BISECT_TARGET=""
+
+# --- Library Import ---
+SCRIPT_PATH="$(realpath "${BASH_SOURCE[0]}")"
+SCRIPT_DIR="$(dirname "${SCRIPT_PATH}")"
+LIB_PATH="${SCRIPT_DIR}/common_lib.sh"
+
+if [[ ! -f "$LIB_PATH" ]]; then
+ echo "FATAL ERROR: Cannot find required library '$LIB_PATH'" >&2
+ exit 1
+fi
+if ! . "$LIB_PATH"; then
+ echo "FATAL ERROR: Failed to source library '$LIB_PATH'. Check common_lib.sh dependencies." >&2
+ exit 1
+fi
+
+# --- Scripts ---
+readonly LAUNCH_CVD_SCRIPT="${SCRIPT_DIR}/launch_cvd.sh"
+readonly FLASH_DEVICE_SCRIPT="${SCRIPT_DIR}/flash_device.sh"
+readonly RUN_TEST_SCRIPT="${SCRIPT_DIR}/run_test_only.sh"
+readonly QUERY_BUILD_SCRIPT="${SCRIPT_DIR}/query_build.sh"
+
+
+# --- Functions ---
+function print_help() {
+ echo "Usage: $0 [OPTIONS]"
+ echo ""
+ echo "A tool to perform build-level bisection to find the first failing Android build."
+ echo ""
+ echo "To start a new bisection, you must specify a build range for one, and only one, of the build types."
+ echo "The build argument with a range defines the target for bisection."
+ echo ""
+ echo "Modes:"
+ echo " New Bisection: Provide a build range via -pb, -kb, or -vkb, along with -t, and -td."
+ echo " Resume Bisection: Provide -i to resume a previously started bisection."
+ echo ""
+ echo "Options:"
+ echo " -pb, --platform-build <url|path>"
+ echo " A platform build. To bisect, use a range format."
+ echo " Format: ab://<branch>/<target>/<id1>-<id2> OR ab://.../<id1,id2,...>"
+ echo " -kb, --kernel-build <url|path>"
+ echo " A kernel build. To bisect, use a range format."
+ echo " -vkb, --vendor-kernel-build <url|path>"
+ echo " A vendor kernel build. To bisect, use a range format."
+ echo " -s, --serial-number <serial> The physical device serial. If omitted, uses a Cuttlefish virtual device."
+ echo " -t, --test <name> [Required] The test name(s) to run. Can be repeated. (e.g., 'CtsMyModuleTest')"
+ echo " -td, --test-dir <path> [Required] The path to the test artifacts (e.g., android-cts.zip)."
+ echo " -tr, --test-retry <count> Retry count for a failed test. Default: ${DEFAULT_TEST_RETRY}."
+ echo " -sr, --setup-retry <count> Retry count for failed device setup. Default: ${DEFAULT_SETUP_RETRY}."
+ echo " --skip-build [Optional] If set, pass '--skip-build' to underlying flash/launch scripts."
+ echo " -od, --output-dir <path> Path of Directory to store the bisection state XML file. Default: ${DEFAULT_OUTPUT_DIR}/${DEFAULT_BISECT_FILE}."
+ echo " -o, --output-file <path> Path to store the bisection state XML file. Default: ${DEFAULT_BISECT_FILE}."
+ echo " -i, --input-file <path> Resume bisection from the given state XML file."
+ echo " -h, --help Display this help message."
+ echo ""
+ echo "Examples:"
+ echo " # Start a new bisection for a platform build regression on a Cuttlefish device"
+ echo " $0 -pb ab://git_main/oriole-userdebug/120000-130000 \\"
+ echo " -t CtsMyModuleTest -td /path/to/android-cts.zip"
+ echo ""
+ echo " # Start bisection using a specific list of kernel builds on a physical device"
+ echo " $0 -kb ab://git_main/oriole-userdebug/120000,120005,120010 -pb ab://.../130000 \\"
+ echo " -s 1A2B3C4D -t CtsMyModuleTest -td /path/to/android-cts.zip"
+ echo ""
+ echo " # Resume an interrupted bisection"
+ echo " $0 -i bisect_builds.xml"
+}
+
+function parse_args() {
+ local has_input_file=false
+ local has_new_bisect_args=false
+
+ while test $# -gt 0; do
+ case "$1" in
+ -h|--help)
+ print_help
+ exit 0
+ ;;
+ -i|--input-file)
+ shift
+ INPUT_FILE="$1"
+ has_input_file=true
+ shift
+ ;;
+ -od|--output-dir)
+ shift
+ OUTPUT_DIR="$1"
+ shift
+ ;;
+ -pb|--platform-build)
+ shift
+ PLATFORM_BUILD="$1"
+ has_new_bisect_args=true
+ shift
+ ;;
+ -kb|--kernel-build)
+ shift
+ KERNEL_BUILD="$1"
+ has_new_bisect_args=true
+ shift
+ ;;
+ -vkb|--vendor-kernel-build)
+ shift
+ VENDOR_KERNEL_BUILD="$1"
+ has_new_bisect_args=true
+ shift
+ ;;
+ -s|--serial-number)
+ shift
+ SERIAL_NUMBER="$1"
+ shift
+ ;;
+ -t|--test)
+ shift
+ TEST_NAME+=("$1")
+ shift
+ ;;
+ -td|--test-dir)
+ shift
+ TEST_DIR="$1"
+ shift
+ ;;
+ -tr|--test-retry)
+ shift
+ TEST_RETRY="$1"
+ shift
+ ;;
+ -sr|--setup-retry)
+ shift
+ SETUP_RETRY="$1"
+ shift
+ ;;
+ --skip-build)
+ SKIP_BUILD=true
+ shift
+ ;;
+ *)
+ log_error "Unsupported flag: $1"
+ print_help
+ exit 1
+ ;;
+ esac
+ done
+
+ if "$has_input_file" && "$has_new_bisect_args"; then
+ fail_error "Cannot specify new bisection options (-pb, -kb, etc.) when resuming with -i."
+ fi
+
+ # Specify the default output directory in configuration file.
+ if [[ "$has_input_file" == true && -z "${OUTPUT_DIR}" ]]; then
+ OUTPUT_DIR=$(xmlstarlet sel -t -v "/bisect/parameters/@output_dir" "$INPUT_FILE" 2> /dev/null)
+ fi
+
+ if ! "$has_input_file"; then
+ if [[ -z "$TEST_DIR" || ${#TEST_NAME[@]} -eq 0 ]]; then
+ fail_error "For a new bisection, both --test (-t) and --test-dir (-td) must be specified."
+ fi
+ fi
+}
+
+function parse_build_string() {
+ local build_str="$1"
+ local -n branch_ref="$2"
+ local -n target_ref="$3"
+ local -n ids_ref="$4"
+ local -n type_ref="$5" # Will be 'range', 'list', 'single', or 'local'
+
+ if [[ "$build_str" != ab://* ]]; then
+ if [[ -d "$build_str" ]]; then
+ type_ref="local"
+ ids_ref=("$build_str")
+ return 0
+ else
+ fail_error "Build string is not a valid 'ab://' URL or a local directory: $build_str"
+ fi
+ fi
+
+ local path_part="${build_str#ab://}"
+ local -a parts=()
+ local IFS='/'
+ read -r -a parts <<< "$path_part"
+
+ if (( ${#parts[@]} < 3 )) || [[ -z "${parts[0]}" || -z "${parts[1]}" || -z "${parts[2]}" ]]; then
+ fail_error "Malformed ab URL. Expected format: ab://<branch>/<target>/<ids>. Got: $build_str"
+ fi
+
+ branch_ref="${parts[0]}"
+ target_ref="${parts[1]}"
+ local id_part
+ id_part=$(echo "${parts[2]}" | tr -d '[:space:]') # Remove all whitespace
+
+ if [[ "$id_part" == *","* ]]; then
+ type_ref="list"
+ local old_ifs=$IFS; IFS=','
+ read -r -a ids_ref <<< "$id_part"
+ IFS=$old_ifs
+ # Validate that all elements are numeric
+ for id in "${ids_ref[@]}"; do
+ if ! [[ "$id" =~ ^[0-9]+$ ]]; then
+ fail_error "Invalid build ID in list. All IDs must be numeric: $id"
+ fi
+ done
+ mapfile -t sorted_ids < <(printf "%s\n" "${ids_ref[@]}" | sort -n)
+ ids_ref=("${sorted_ids[@]}")
+ elif [[ "$id_part" == *"-"* ]]; then
+ type_ref="range"
+ local id1 id2
+ id1=$(echo "$id_part" | cut -d'-' -f1)
+ id2=$(echo "$id_part" | cut -d'-' -f2)
+ if ! [[ "$id1" =~ ^[0-9]+$ && "$id2" =~ ^[0-9]+$ ]]; then
+ fail_error "Invalid range format. IDs must be numeric: $id_part"
+ fi
+ if (( id1 >= id2 )); then
+ fail_error "Invalid range: start ID ($id1) must be less than end ID ($id2)."
+ fi
+ ids_ref=("$id1" "$id2")
+ else
+ type_ref="single"
+ if ! [[ "$id_part" =~ ^[0-9]+$ ]]; then
+ fail_error "Invalid build ID. Must be numeric for a single build: $id_part"
+ fi
+ ids_ref=("$id_part")
+ fi
+ return 0
+}
+
+function validate_args() {
+ OUTPUT_DIR="${OUTPUT_DIR:-$DEFAULT_OUTPUT_DIR}"
+ if [[ ! -d "$OUTPUT_DIR" ]]; then
+ mkdir -p "$OUTPUT_DIR"
+ fi
+ OUTPUT_DIR=$(realpath "$OUTPUT_DIR")
+ BISECT_FILE="${OUTPUT_DIR}/${DEFAULT_BISECT_BUILDS_FILENAME}"
+
+ if [[ -n "$INPUT_FILE" ]]; then
+ if [[ ! -f "$INPUT_FILE" ]]; then
+ fail_error "Input file not found: $INPUT_FILE"
+ fi
+ return 0
+ fi
+
+ # --- New Bisection Validations ---
+ local bisect_arg_found=false
+ local -A build_type_map=( ["pb"]="PLATFORM_BUILD" ["kb"]="KERNEL_BUILD" ["vkb"]="VENDOR_KERNEL_BUILD" )
+ for type_code in "${!build_type_map[@]}"; do
+ local var_name="${build_type_map[$type_code]}"
+ local build_value="${!var_name}"
+ if [[ -z "$build_value" ]]; then
+ continue
+ fi
+
+ local branch target
+ local -a ids=()
+ local id_type=""
+ parse_build_string "$build_value" branch target ids id_type
+
+ if [[ "$id_type" == "range" || "$id_type" == "list" ]]; then
+ if "$bisect_arg_found"; then
+ fail_error "Only one build argument (-pb, -kb, or -vkb) can contain a bisection range."
+ fi
+ bisect_arg_found=true
+ BUILD_TYPE="$type_code"
+ BISECT_BRANCH="$branch"
+ BISECT_TARGET="$target"
+
+ if [[ "$id_type" == "range" ]]; then
+ get_build_ids "${ids[0]}" "${ids[1]}"
+ else # list
+ BUILDS_TO_TEST=("${ids[@]}")
+ fi
+
+ if (( ${#BUILDS_TO_TEST[@]} < 2 )); then
+ fail_error "Bisection requires at least two builds to test. Found ${#BUILDS_TO_TEST[@]}."
+ fi
+ fi
+ done
+
+ if ! "$bisect_arg_found"; then
+ fail_error "New bisection requires one build argument (-pb, -kb, -vkb) to have a range (e.g., 1-2) or list (e.g., 1,2,3)."
+ fi
+
+ local fixed_builds_are_all_remote=true
+ # Validate that other fixed builds are valid single builds or local paths.
+ for type_code in "${!build_type_map[@]}"; do
+ # Skip the one we are bisecting
+ if [[ "$type_code" == "$BUILD_TYPE" ]]; then
+ continue
+ fi
+
+ local var_name="${build_type_map[$type_code]}"
+ local build_value="${!var_name}"
+ if [[ -n "$build_value" ]]; then
+ if [[ "$build_value" != ab://* ]]; then
+ fixed_builds_are_all_remote=false
+ # It's a local path, validation already done in parse_build_string
+ else
+ # It must be a single remote build
+ local branch target; local -a ids; local id_type
+ parse_build_string "$build_value" branch target ids id_type
+ if [[ "$id_type" != "single" ]]; then
+ fail_error "Fixed build --${type_code}-build must be a single build ID or local path, not a range or list."
+ fi
+ fi
+ fi
+ done
+
+ # Warn if --skip-build is used pointlessly.
+ if "$SKIP_BUILD" && "$fixed_builds_are_all_remote"; then
+ log_warn "--skip-build is specified, but all provided builds are remote 'ab://' URLs. No local building would occur anyway."
+ fi
+}
+
+function fail_error() {
+ local message="$1"
+ local exit_code="${2:-1}"
+ # Pass frame offset 2 to log_error to point to the caller of fail_error
+ log_error "$message" "$exit_code" 2
+ exit "$exit_code"
+}
+
+function extract_build_ids_from_file() {
+ local filename="$1"
+ if [[ ! -f "$filename" ]]; then
+ fail_error "Error: File '${filename}' not found."
+ fi
+
+ awk -F',' '
+ NR > 1 {
+ gsub(/"/, "", $1);
+ print $1;
+ }
+ ' "$filename"
+}
+
+function get_build_ids() {
+ local start_id="$1"
+ local end_id="$2"
+ log_info "Fetching all build IDs from $start_id to $end_id..."
+
+ if ! "$QUERY_BUILD_SCRIPT" -br "${BISECT_BRANCH}" -bt "${BISECT_TARGET}" -sbid "${start_id}" -ebid "${end_id}"; then
+ fail_error "Error running query_build.sh. branch: ${BISECT_BRANCH}, build_target: ${BISECT_TARGET}"
+ fi
+
+ local queryfile="/tmp/build_query_output.csv"
+ local -a unsorted_build_ids_array
+ mapfile -t unsorted_build_ids_array < <(extract_build_ids_from_file "$queryfile")
+ mapfile -t BUILDS_TO_TEST < <(printf "%s\n" "${unsorted_build_ids_array[@]}" | sort -n)
+
+ if (( ${#BUILDS_TO_TEST[@]} == 0 )); then
+ fail_error "Could not find any builds between $start_id and $end_id for target $BISECT_TARGET on branch $BISECT_BRANCH."
+ fi
+ log_info "Found ${#BUILDS_TO_TEST[@]} builds to test."
+}
+
+function xml::add_node() {
+ local -n cmd_array_ref=$1
+ local parent_xpath=$2
+ local element_name=$3
+ cmd_array_ref+=(-s "$parent_xpath" -t elem -n "$element_name")
+}
+
+function xml::add_attribute() {
+ local -n cmd_array_ref=$1
+ local parent_xpath=$2
+ local attr_name=$3
+ local attr_value=$4
+ cmd_array_ref+=(-i "$parent_xpath" -t attr -n "$attr_name" -v "$attr_value")
+}
+
+function xml::add_element() {
+ local -n cmd_array_ref=$1
+ local parent_xpath=$2
+ local element_name=$3
+ local element_value=$4
+ cmd_array_ref+=(-s "$parent_xpath" -t elem -n "$element_name" -v "$element_value")
+}
+function init_bisect_file() {
+ log_info "Initializing new bisection state file: $BISECT_FILE"
+
+ local good_build_index=0
+ local bad_build_index=$(( ${#BUILDS_TO_TEST[@]} - 1 ))
+ local -A build_type_map=( ["pb"]="PLATFORM_BUILD" ["kb"]="KERNEL_BUILD" ["vkb"]="VENDOR_KERNEL_BUILD" )
+
+ # Create base XML structure
+ echo '<?xml version="1.0" encoding="UTF-8"?><bisect/>' > "$BISECT_FILE"
+ local -a xml_edit_cmd=("xmlstarlet" "ed" "-L")
+
+ # State node
+ xml::add_node xml_edit_cmd "/bisect" "state"
+ xml::add_attribute xml_edit_cmd "/bisect/state" "build_type" "$BUILD_TYPE"
+ xml::add_attribute xml_edit_cmd "/bisect/state" "good_index" "$good_build_index"
+ xml::add_attribute xml_edit_cmd "/bisect/state" "bad_index" "$bad_build_index"
+ xml::add_attribute xml_edit_cmd "/bisect/state" "status" "new"
+
+ # Parameters node
+ xml::add_node xml_edit_cmd "/bisect" "parameters"
+ xml::add_attribute xml_edit_cmd "/bisect/parameters" "test_dir" "$TEST_DIR"
+ xml::add_attribute xml_edit_cmd "/bisect/parameters" "output_dir" "$OUTPUT_DIR"
+ xml::add_attribute xml_edit_cmd "/bisect/parameters" "test_retry" "$TEST_RETRY"
+ xml::add_attribute xml_edit_cmd "/bisect/parameters" "setup_retry" "$SETUP_RETRY"
+ xml::add_attribute xml_edit_cmd "/bisect/parameters" "serial_number" "$SERIAL_NUMBER"
+ xml::add_attribute xml_edit_cmd "/bisect/parameters" "skip_build" "$SKIP_BUILD"
+ for test in "${TEST_NAME[@]}"; do
+ xml::add_element xml_edit_cmd "/bisect/parameters" "test" "$test"
+ done
+
+ # Bisected build list
+ local build_node_name
+ case "$BUILD_TYPE" in
+ pb) build_node_name="platform_builds" ;;
+ kb) build_node_name="kernel_builds" ;;
+ vkb) build_node_name="vendor_kernel_builds" ;;
+ esac
+ xml::add_node xml_edit_cmd "/bisect" "$build_node_name"
+ xml::add_attribute xml_edit_cmd "/bisect/$build_node_name" "branch" "$BISECT_BRANCH"
+ xml::add_attribute xml_edit_cmd "/bisect/$build_node_name" "target" "$BISECT_TARGET"
+ for build_id in "${BUILDS_TO_TEST[@]}"; do
+ xml::add_element xml_edit_cmd "/bisect/$build_node_name" "build" "$build_id"
+ done
+
+ # Fixed builds
+ for type in "platform" "kernel" "vendor_kernel"; do
+ local var_name="${type^^}_BUILD"
+ local build_value="${!var_name}"
+ if [[ -n "$build_value" && "${build_type_map[$BUILD_TYPE]}" != "$var_name" ]]; then
+ xml::add_element xml_edit_cmd "/bisect" "${type}_build" "$build_value"
+ fi
+ done
+
+ # Execute the command
+ "${xml_edit_cmd[@]}" "$BISECT_FILE"
+}
+
+function xml::read_value() {
+ local xpath=$1
+ xmlstarlet sel -t -v "$xpath" "$BISECT_FILE" 2>/dev/null
+}
+
+function xml::read_values_to_array() {
+ local xpath=$1
+ local -n array_ref=$2
+ mapfile -t array_ref < <(xmlstarlet sel -t -v "$xpath" -n "$BISECT_FILE" 2>/dev/null)
+}
+function load_state_from_xml() {
+ log_info "Loading state from $BISECT_FILE..."
+ # State
+ BUILD_TYPE=$(xml::read_value "/bisect/state/@build_type")
+ GOOD_INDEX=$(xml::read_value "/bisect/state/@good_index")
+ BAD_INDEX=$(xml::read_value "/bisect/state/@bad_index")
+ BISECT_STATUS=$(xml::read_value "/bisect/state/@status")
+
+ # Parameters
+ TEST_DIR=$(xml::read_value "/bisect/parameters/@test_dir")
+ OUTPUT_DIR=$(xml::read_value "/bisect/parameters/@output_dir")
+ TEST_RETRY=$(xml::read_value "/bisect/parameters/@test_retry")
+ SETUP_RETRY=$(xml::read_value "/bisect/parameters/@setup_retry")
+ SERIAL_NUMBER=$(xml::read_value "/bisect/parameters/@serial_number")
+ SKIP_BUILD=$(xml::read_value "/bisect/parameters/@skip_build")
+ xml::read_values_to_array "/bisect/parameters/test" TEST_NAME
+
+ # Bisected Builds
+ local bisect_node_name
+ case "$BUILD_TYPE" in
+ pb) bisect_node_name="platform_builds" ;;
+ kb) bisect_node_name="kernel_builds" ;;
+ vkb) bisect_node_name="vendor_kernel_builds" ;;
+ esac
+ BISECT_BRANCH=$(xml::read_value "/bisect/$bisect_node_name/@branch")
+ BISECT_TARGET=$(xml::read_value "/bisect/$bisect_node_name/@target")
+ xml::read_values_to_array "/bisect/$bisect_node_name/build" BUILDS_TO_TEST
+
+ # Fixed Builds
+ PLATFORM_BUILD=$(xml::read_value "/bisect/platform_build")
+ KERNEL_BUILD=$(xml::read_value "/bisect/kernel_build")
+ VENDOR_KERNEL_BUILD=$(xml::read_value "/bisect/vendor_kernel_build")
+
+ log_info "State loaded successfully. Good: ${BUILDS_TO_TEST[$GOOD_INDEX]}, Bad: ${BUILDS_TO_TEST[$BAD_INDEX]}. Status: $BISECT_STATUS"
+}
+
+function xml::update_xml_node() {
+ local xpath_expr="$1"
+ local value="$2"
+ log_info "Updating XML state in $BISECT_FILE: $xpath_expr -> $value"
+ xmlstarlet ed -L -u "$xpath_expr" -v "$value" "$BISECT_FILE"
+}
+
+function setup_and_test_build() {
+ local build_id_to_test="$1"
+ local -a setup_cmd_array=()
+ local -a test_cmd=()
+ local current_pb=""
+ local current_kb=""
+ local current_vkb=""
+ local input_serial_to_use="$SERIAL_NUMBER"
+
+ # --- Construct Build Arguments ---
+ case "$BUILD_TYPE" in
+ pb) current_pb="ab://${BISECT_BRANCH}/${BISECT_TARGET}/${build_id_to_test}" ;;
+ kb) current_kb="ab://${BISECT_BRANCH}/${BISECT_TARGET}/${build_id_to_test}" ;;
+ vkb) current_vkb="ab://${BISECT_BRANCH}/${BISECT_TARGET}/${build_id_to_test}" ;;
+ esac
+ # Override with fixed builds if they are provided
+ [[ -n "$PLATFORM_BUILD" ]] && current_pb="$PLATFORM_BUILD"
+ [[ -n "$KERNEL_BUILD" ]] && current_kb="$KERNEL_BUILD"
+ [[ -n "$VENDOR_KERNEL_BUILD" ]] && current_vkb="$VENDOR_KERNEL_BUILD"
+
+ log_info "Preparing to test build ID: $build_id_to_test"
+ log_info "Platform: ${current_pb:-<not set>}"
+ log_info "Kernel: ${current_kb:-<not set>}"
+ log_info "Vendor Kernel: ${current_vkb:-<not set>}"
+
+ # --- Construct Setup Command Array ---
+ if [[ "$DEVICE_TYPE" == "PHYSICAL" ]]; then
+ setup_cmd_array=("$FLASH_DEVICE_SCRIPT" "-s" "$SERIAL_NUMBER")
+ [[ -n "$current_pb" ]] && setup_cmd_array+=("-pb" "$current_pb")
+ [[ -n "$current_kb" ]] && setup_cmd_array+=("-kb" "$current_kb")
+ [[ -n "$current_vkb" ]] && setup_cmd_array+=("-vkb" "$current_vkb")
+ elif [[ "$DEVICE_TYPE" == "VIRTUAL" ]]; then
+ setup_cmd_array=("$LAUNCH_CVD_SCRIPT")
+ [[ -n "$current_pb" ]] && setup_cmd_array+=("-pb" "$current_pb")
+ [[ -n "$current_kb" ]] && setup_cmd_array+=("-kb" "$current_kb")
+ # launch_cvd does not support vkb
+ else
+ fail_error "The Device Type Option not supported: ${DEVICE_TYPE}"
+ fi
+ if "$SKIP_BUILD"; then
+ setup_cmd_array+=("--skip-build")
+ fi
+
+ # --- Execute Setup with Retry ---
+ log_info "Executing setup command: ${setup_cmd_array[*]}"
+ local setup_success=false
+
+ for i in $(seq 1 "$SETUP_RETRY"); do
+ local setup_status=1
+ if [[ "$DEVICE_TYPE" == "VIRTUAL" ]]; then
+ # Special handling for teeing output
+ unbuffer "${setup_cmd_array[@]}" | tee "$ACLOUD_OUTPUT_FILE"
+ setup_status=${PIPESTATUS[0]}
+ else
+ "${setup_cmd_array[@]}"
+ setup_status=$?
+ fi
+
+ if (( setup_status == 0 )); then
+ setup_success=true
+ break
+ fi
+ log_warn "Setup failed (Attempt $i/$SETUP_RETRY). Retrying in 10 seconds..."
+ sleep 10
+ done
+
+ if ! "$setup_success"; then
+ fail_error "Device setup failed after $SETUP_RETRY attempts. Aborting bisection."
+ fi
+
+ log_info "Device setup successful."
+
+ # If CVD, find the new serial number
+ if [[ "$DEVICE_TYPE" == "VIRTUAL" ]]; then
+ log_info "Waiting for Cuttlefish device to come online..."
+
+ if ! [[ -f "$ACLOUD_OUTPUT_FILE" ]]; then
+ fail_error "Could not find serial for Cuttlefish device after launch."
+ fi
+
+ input_serial_to_use=$(grep -oP "ANDROID_SERIAL=\K[\.0-9:]+" "$ACLOUD_OUTPUT_FILE")
+ if [[ -z "$input_serial_to_use" ]]; then
+ fail_error "Could not find ANDROID_SERIAL in launch_cvd output. Cannot proceed."
+ fi
+
+ log_info "Found Cuttlefish device serial: $input_serial_to_use. Waiting for boot to complete..."
+ adb -s "$input_serial_to_use" wait-for-device
+ while ! adb -s "$input_serial_to_use" shell pm path com.android.settings > /dev/null 2>&1; do
+ log_info "Waiting for package manager on $input_serial_to_use..."
+ sleep 5
+ done
+ log_info "Device $input_serial_to_use is fully online."
+ elif [[ "$DEVICE_TYPE" == "PHYSICAL" ]]; then
+ device::set_current "$input_serial_to_use"
+ unlock_screen "$CURRENT_ADB_SERIAL"
+ skip_setup_wizard "$CURRENT_ADB_SERIAL"
+ fi
+
+ device::set_current "$input_serial_to_use"
+
+ # --- Construct and Execute Test Command Array ---
+ test_cmd=("$RUN_TEST_SCRIPT")
+ [[ "$CURRENT_MODE_TYPE" == "FASTBOOT" ]] && test_cmd+=("-s" "$CURRENT_FASTBOOT_SERIAL")
+ [[ "$CURRENT_MODE_TYPE" == "ADB" ]] && test_cmd+=("-s" "$CURRENT_ADB_SERIAL")
+ test_cmd+=("-td" "$TEST_DIR")
+ test_cmd+=("-tl" "$OUTPUT_DIR/test_logs")
+ for test in "${TEST_NAME[@]}"; do
+ test_cmd+=("-t" "$test")
+ done
+
+ log_info "Executing test command: ${test_cmd[*]}"
+
+ local test_success=false
+ for i in $(seq 1 "$TEST_RETRY"); do
+ "${test_cmd[@]}"
+ if (( $? == 0 )); then
+ test_success=true
+ break
+ fi
+ log_warn "Test failed (Attempt $i/$TEST_RETRY). Retrying..."
+ done
+
+ if "$test_success"; then
+ log_info "Test SUCCEEDED for build $build_id_to_test."
+ return 0 # GOOD
+ else
+ log_warn "Test FAILED for build $build_id_to_test after $TEST_RETRY attempts."
+ return 1 # BAD
+ fi
+}
+
+
+function validate_initial_bounds() {
+ log_info "--- Validating Good Build (${BUILDS_TO_TEST[0]}) ---"
+ setup_and_test_build "${BUILDS_TO_TEST[0]}"
+ if (( $? != 0 )); then
+ fail_error "Validation failed: The first build in the range is FAILING the test. Please check your inputs and test stability."
+ fi
+ log_info "Good Build validation successful."
+
+ local last_build_index=$(( ${#BUILDS_TO_TEST[@]} - 1 ))
+ log_info "--- Validating Bad Build (${BUILDS_TO_TEST[$last_build_index]}) ---"
+ setup_and_test_build "${BUILDS_TO_TEST[$last_build_index]}"
+ if (( $? == 0 )); then
+ fail_error "Validation failed: The last build in the range is PASSING the test. Please check your inputs."
+ fi
+ log_info "Bad Build validation successful."
+}
+
+function skip_setup_wizard() {
+ local android_serial="$1"
+ log_info "Device ${android_serial} is online. Waiting for package manager to be ready..."
+ # Loop until the package manager is queryable, which is a good sign that the core system is up.
+ while ! adb -s "${android_serial}" shell pm path com.android.settings > /dev/null 2>&1; do
+ sleep 2
+ done
+
+ log_info "Core system is up. Disabling Setup Wizard..."
+ # --- Commands to Disable Setup Wizard ---
+ adb -s "${android_serial}" shell settings put global device_provisioned 1
+ adb -s "${android_serial}" shell settings put secure user_setup_complete 1
+
+ # --- Disable the wizard package itself as a fallback ---
+ # Find the package name (it can vary)
+ local SETUP_WIZARD_PKG
+ SETUP_WIZARD_PKG=$(adb -s "${android_serial}" shell pm list packages | grep -i 'setupwizard' | head -n 1 | cut -d':' -f2)
+ if [ -n "$SETUP_WIZARD_PKG" ]; then
+ log_info "Found setup wizard package: $SETUP_WIZARD_PKG. Disabling..."
+ adb -s "${android_serial}" shell pm disable-user --user 0 "$SETUP_WIZARD_PKG"
+ else
+ log_warn "Could not find setup wizard package automatically."
+ fi
+
+ log_info "Setup Wizard skipped. Device should now be at the home screen."
+ echo "------------------------------------------------------------------"
+}
+
+function unlock_screen() {
+ local android_serial="$1"
+ local timeout_seconds=60
+
+ log_info "Waiting for device to come online after reboot..."
+ timeout $timeout_seconds adb -s "$android_serial" wait-for-device
+ if (( $? == 124 )); then
+ fail_error "Timeout reached, the device has no response."
+ fi
+
+ while [ "$(adb -s "${android_serial}" shell getprop sys.boot_completed | tr -d '\r')" != "1" ]; do
+ sleep 5
+ done
+
+ log_info "The device boot complete..."
+
+ local device_idle_status
+ device_idle_status="$(adb -s "${android_serial}" shell dumpsys deviceidle)"
+
+ local is_screen_on
+ is_screen_on=$(echo "${device_idle_status}" | grep "mScreenOn" | cut -d'=' -f2)
+
+ if [[ "${is_screen_on}" == "false" ]]; then
+ log_info "Screen is off. Turning it on..."
+ adb -s "$android_serial" shell input keyevent 26 || fail_error "Failed to turn on the screen."
+ sleep 1
+ # Refresh the status after the action.
+ device_idle_status="$(adb -s "${android_serial}" shell dumpsys deviceidle)"
+ is_screen_on=$(echo "${device_idle_status}" | grep "mScreenOn" | cut -d'=' -f2)
+ fi
+
+ # If the screen is now on, check if it's locked.
+ if [[ "${is_screen_on}" == "true" ]]; then
+ local is_screen_locked
+ is_screen_locked=$(echo "${device_idle_status}" | grep "mScreenLocked" | cut -d'=' -f2)
+
+ if [[ "${is_screen_locked}" == "true" ]]; then
+ log_info "Screen is on and locked. Unlocking..."
+ adb -s "$android_serial" shell input keyevent 82 || fail_error "Failed to unlock the screen."
+ log_info "Screen unlocked successfully."
+ else
+ log_info "Screen is already on and unlocked."
+ fi
+ else
+ fail_error "Can't turn on the screen. Please check the device."
+ fi
+}
+
+
+function determine_default_device_type() {
+ if [[ -n "$SERIAL_NUMBER" ]]; then
+ DEVICE_TYPE="PHYSICAL"
+ else
+ DEVICE_TYPE="VIRTUAL"
+ fi
+ log_info "Determined the Default Device Type: $DEVICE_TYPE"
+}
+
+function cleanup() {
+ log_info "Cleaning up temporary files..."
+ if (( ${#TEMP_FILES[@]} > 0 )); then
+ rm -f "${TEMP_FILES[@]}"
+ fi
+}
+
+function device::set_current() {
+ local serial="$1"
+ local found_device=false
+
+ CURRENT_INPUT_SERIAL=""
+ CURRENT_FASTBOOT_SERIAL=""
+ CURRENT_ADB_SERIAL=""
+ CURRENT_DEVICE_SERIAL=""
+ CURRENT_MODE_TYPE=""
+ CURRENT_DEVICE_TYPE=""
+
+ if adb devices | grep -q "$serial"; then
+ CURRENT_MODE_TYPE="ADB"
+ CURRENT_ADB_SERIAL="$serial"
+ CURRENT_DEVICE_SERIAL=$(adb -s "$CURRENT_ADB_SERIAL" shell getprop ro.serialno)
+ found_device=true
+ fi
+
+ if fastboot devices | grep -q "$serial"; then
+ CURRENT_MODE_TYPE="FASTBOOT"
+ CURRENT_FASTBOOT_SERIAL="$serial"
+ local serial_info
+ serial_info=$(fastboot -s "$CURRENT_FASTBOOT_SERIAL" getvar serialno 2>&1)
+ CURRENT_DEVICE_SERIAL=$(echo "$serial_info" | grep -Po "serialno: \K[A-Z0-9]+")
+ found_device=true
+ fi
+
+ if [[ -x "$(command -v pontis)" && "$found_device" == false ]]; then
+ local pontis_info
+ pontis_info=$(pontis devices | grep "$serial")
+ if [[ "$pontis_info" == *Fastboot* ]]; then
+ CURRENT_MODE_TYPE="FASTBOOT"
+ CURRENT_DEVICE_SERIAL="$serial"
+ found_device=true
+ log_info "Device $serial is connected through pontis in fastboot"
+ device::find_fastboot_serial_number "$CURRENT_DEVICE_SERIAL"
+ elif [[ "$pontis_info" == *ADB* ]]; then
+ CURRENT_MODE_TYPE="ADB"
+ CURRENT_DEVICE_SERIAL="$serial"
+ found_device=true
+ log_info "Device $serial is connected through pontis in adb"
+ device::find_adb_serial_number "$CURRENT_DEVICE_SERIAL"
+ fi
+ fi
+
+ if ! $found_device; then
+ log_warn "Cannot find out the device ${serial}. Please check the device connection."
+ return 1
+ fi
+
+ if [[ "$CURRENT_MODE_TYPE" == "ADB" ]]; then
+ local product
+ product=$(adb -s "$CURRENT_ADB_SERIAL" shell getprop ro.product.board)
+ if [[ "$product" == "cutf" ]]; then
+ CURRENT_DEVICE_TYPE="VIRTUAL"
+ else
+ CURRENT_DEVICE_TYPE="PHYSICAL"
+ fi
+ else
+ CURRENT_DEVICE_TYPE="PHYSICAL"
+ fi
+
+ [[ "$CURRENT_DEVICE_TYPE" == "$DEVICE_TYPE" ]] || log_error "The Current Device Type \
+ ${CURRENT_DEVICE_TYPE} is inconsistent with Default Type ${DEVICE_TYPE}"
+
+ CURRENT_INPUT_SERIAL="$serial"
+ return 0
+}
+
+function device::find_fastboot_serial_number() {
+ #print_info "Try to find device $DEVICE_SERIAL_NUMBER serial id in fastboot devices" "$LINENO"
+ local device_serial="$1"
+ local device_ids
+ device_ids=$(fastboot devices | awk '{print $1}')
+ while IFS= read -r device_id; do
+ # Use fastboot getvar to retrieve serial number
+ local _output
+ _output=$(fastboot -s "$device_id" getvar serialno 2>&1)
+ local target_device_serial
+ target_device_serial=$(echo "$_output" | grep -Po "serialno: \K[A-Z0-9]+")
+ if [[ "$target_device_serial" == "$device_serial" ]]; then
+ CURRENT_FASTBOOT_SERIAL="$device_id"
+ log_info "Device $device_serial shows up as $CURRENT_FASTBOOT_SERIAL in fastboot"
+ return 0
+ fi
+ done <<< "$device_ids"
+ fail_error "Can not find device in fastboot has device serial number $device_serial"
+}
+
+function device::find_adb_serial_number() {
+ local device_serial="$1"
+ log_info "Try to find device $device_serial serial id in adb devices"
+ local _device_ids
+ _device_ids=$(adb devices | awk '$2 == "device" {print $1}')
+ local devices=()
+ while IFS= read -r device_id; do
+ devices+=("$device_id")
+ done <<< "$_device_ids"
+
+ for device_id in "${devices[@]}"; do
+ local target_device_serial
+ target_device_serial=$(adb -s "$device_id" shell getprop ro.serialno)
+ if [[ "$target_device_serial" == "$device_serial" ]]; then
+ CURRENT_ADB_SERIAL="$device_id"
+ log_info "Device $device_serial shows up as $CURRENT_ADB_SERIAL in adb"
+ return 0
+ fi
+ done
+ fail_error "Can not find device in adb has device serial number $device_serial. \
+ Check if the device is connected with adb authentication"
+}
+
+function bisect_builds() {
+ log_info "========================================="
+ log_info "Starting Bisection Loop"
+ log_info "Device Type: $DEVICE_TYPE"
+ log_info "Range: Index $GOOD_INDEX (Build ${BUILDS_TO_TEST[$GOOD_INDEX]}) to $BAD_INDEX (Build ${BUILDS_TO_TEST[$BAD_INDEX]})"
+ log_info "========================================="
+
+ while (( GOOD_INDEX + 1 < BAD_INDEX )); do
+ local mid_index=$(( GOOD_INDEX + (BAD_INDEX - GOOD_INDEX) / 2 ))
+ local mid_build_id=${BUILDS_TO_TEST[$mid_index]}
+
+ log_info "--- Testing build at index: $mid_index (ID: $mid_build_id) ---"
+
+ setup_and_test_build "$mid_build_id"
+ local test_status=$?
+
+ if (( test_status == 0 )); then
+ log_info "RESULT: Build $mid_build_id is GOOD."
+ GOOD_INDEX=$mid_index
+ xml::update_xml_node "/bisect/state/@good_index" "$GOOD_INDEX"
+ else
+ log_info "RESULT: Build $mid_build_id is BAD."
+ BAD_INDEX=$mid_index
+ xml::update_xml_node "/bisect/state/@bad_index" "$BAD_INDEX"
+ fi
+ log_info "New Range: Index $GOOD_INDEX (Good) to $BAD_INDEX (Bad)"
+ done
+
+ log_info "========================================="
+ log_info "Bisection Complete!"
+ log_info "========================================="
+ xml::update_xml_node "/bisect/state/@status" "complete"
+
+ local first_bad_id=${BUILDS_TO_TEST[$BAD_INDEX]}
+ local last_good_id=${BUILDS_TO_TEST[$GOOD_INDEX]}
+
+ local first_bad_url="ab://${BISECT_BRANCH}/${BISECT_TARGET}/${first_bad_id}"
+ local last_good_url="ab://${BISECT_BRANCH}/${BISECT_TARGET}/${last_good_id}"
+
+ log_info "Last known good build: $last_good_url"
+ log_info "${RED}First known bad build: $first_bad_url${END}"
+}
+
+# --- Main Script Logic ---
+function main() {
+ trap cleanup EXIT
+
+ check_commands_available "xmlstarlet" || fail_error "xmlstarlet is required. Please install it."
+
+ parse_args "$@"
+ validate_args
+
+ if [[ -n "$INPUT_FILE" ]]; then
+ log_info "Resuming bisection from $INPUT_FILE"
+ if [[ "$INPUT_FILE" != "$BISECT_FILE" ]]; then
+ cp "$INPUT_FILE" "$BISECT_FILE"
+ xml::update_xml_node "/bisect/parameters/@output_dir" "$OUTPUT_DIR"
+ fi
+ else
+ log_info "Starting new bisection..."
+ init_bisect_file
+ fi
+ load_state_from_xml
+ determine_default_device_type
+
+ if [[ "$BISECT_STATUS" == "new" ]]; then
+ log_info "--- New bisection: Validating initial good and bad build boundaries ---"
+ validate_initial_bounds
+ log_info "--- Initial boundaries validated successfully ---"
+ xml::update_xml_node "/bisect/state/@status" "in_progress"
+ elif [[ "$BISECT_STATUS" == "complete" ]]; then
+ log_info "Bisection is already complete according to state file. Nothing to do."
+ bisect_builds # Show the final result again
+ exit 0
+ else
+ log_info "--- Resuming bisection. Skipping initial bounds validation. ---"
+ fi
+
+ bisect_builds
+}
+
+# Execute main
+main "$@"
\ No newline at end of file