blob: 1818dfddb32d40f7b3320b884e5d0f53c23e2e0b [file] [log] [blame]
#!/bin/bash
# Copyright 2025 The Fuchsia Authors. All rights reserved.
# Use of this source code is governed by a BSD-style license that can be
# found in the LICENSE file.
source "$(cd "$(dirname "${BASH_SOURCE[0]}")" >/dev/null 2>&1 && pwd)"/../../lib/platform.sh || exit $?
BT_FILE_DEPS=(
"prebuilt/third_party/jq/${HOST_PLATFORM}/bin/jq"
"scripts/fx"
"scripts/fuchsia-vendored-python3.11"
"scripts/hermetic-env"
"build/scripts/main_build.py"
"build/force_clean/force_clean_if_needed.py"
"build/scripts/top_build_wrap.sh"
"tools/devshell/lib/vars.sh"
"tools/devshell/build"
"tools/devshell/jq.fx"
"tools/devshell/lib/build_api_client.sh"
"tools/devshell/lib/fx-cmd-locator.sh"
"tools/devshell/lib/fx-optional-features.sh"
"tools/devshell/lib/platform.sh"
"tools/devshell/lib/generate-ssh-config.sh"
"tools/devshell/lib/bazel_utils.sh"
"tools/devshell/contrib/lib/count-ninja-actions.py"
)
declare fx
BT_MKDIR_DEPS=(
"out/default"
)
MOCK_PYTHON3_DIR="prebuilt/third_party/python3/test"
MOCK_PYTHON3="${MOCK_PYTHON3_DIR}/bin/python3"
BT_MOCKED_TOOLS=(
"build/api/client"
"build/regenerator"
"scripts/fuchsia-vendored-python"
"tools/devshell/lib/bazel_utils.sh"
"tools/devshell/rbe"
"tools/devshell/lib/platform.sh"
"tools/integration/bootstrap.sh"
"build/rbe/fuchsia-reproxy-wrap.sh"
"build/profile/profile_wrap.sh"
"build/resultstore/fuchsia-rsproxy-wrap.sh"
"${MOCK_PYTHON3}"
)
BT_SET_UP() {
source "${BT_TEMP_DIR}/tools/devshell/tests/lib/fuchsia-mock.sh"
fx="$(btf::setup_fx)"
TEST_BUILD_DIR="${BT_TEMP_DIR}/out/default"
}
# Helper to create a pass-through mock wrapper that shifts until '--'.
# This is useful for testing orchestration of wrapper scripts without
# exposing their internal requirements.
btf::make_pass_through_mock() {
local mock_path="$1"
cat > "${BT_TEMP_DIR}/${mock_path}.mock_side_effects" <<EOF
while [[ "\$#" -gt 0 && "\$1" != "--" ]]; do shift; done
if [[ "\$1" == "--" ]]; then shift; fi
exec "\$@"
EOF
}
# This function must be called by any test that checks the
# behavior of the validate_build_args() function.
setup_parsing_validation_test() {
export FX_BUILD_DEBUG_VALIDATION=1
}
# This function must be called by regular build tests.
setup_build_test() {
# Set up mocked replacements for system utils.
# IMPORTANT: The test suite invokes the current script through "/usr/bin/env -i"
# which appends ":." at the end of PATH, which will result in an error when
# calling fx-run-build-command, so remove this here explicitly.
export PATH="${BT_TEMP_DIR}/bin:${PATH%:.}"
# Misc mock binaries invoked by tools/devshell/build
ninja="$(btf::make_mock_binary bin/ninja)"
gn_bin="$(btf::make_mock_binary bin/gn)"
build_api_client="${BT_TEMP_DIR}/build/api/client"
bazel="$(btf::make_mock_binary bin/bazel)"
cat > "${BT_TEMP_DIR}/tools/devshell/lib/bazel_utils.sh.mock_side_effects" <<EOF
function fx-get-bazel {
echo "${bazel}"
}
function fx-update-bazel-workspace { return 0; }
EOF
cat > "${BT_TEMP_DIR}/tools/devshell/lib/platform.sh.mock_side_effects" <<EOF
readonly PREBUILT_ALL_PATHS="${BT_TEMP_DIR}"
readonly PREBUILT_JQ="${BT_TEMP_DIR}/prebuilt/third_party/jq/linux-x64/bin/jq"
readonly PREBUILT_PYTHON3="$(command -v python3)"
readonly PREBUILT_PYTHON3_DIR="$(dirname "${PREBUILT_PYTHON3}")"
readonly PREBUILT_NINJA="${ninja}"
readonly PREBUILT_GN="${gn_bin}"
EOF
if [[ "$1" != "enable_landmines" ]]; then
# Ensure the landmine checks pass by updating the timestamp of various files
# in the test directory.
# LINT.IfChange(landmine_files)
touch "${TEST_BUILD_DIR}/args.gn"
touch "${BT_TEMP_DIR}/tools/devshell/build"
touch "${BT_TEMP_DIR}/tools/devshell/lib/bazel_utils.sh"
touch "${BT_TEMP_DIR}/tools/devshell/lib/vars.sh"
touch "${TEST_BUILD_DIR}/build.ninja" # Must appear last
# LINT.ThenChange(//tools/devshell/build:landmine_files)
fi
# Use pass-through mocks for all build wrappers.
# This allows testing the orchestration layer (top_build_wrap.sh)
# without satisfying the wrappers' own complex pre-flight requirements.
btf::make_pass_through_mock "build/rbe/fuchsia-reproxy-wrap.sh"
btf::make_pass_through_mock "build/profile/profile_wrap.sh"
btf::make_pass_through_mock "build/resultstore/fuchsia-rsproxy-wrap.sh"
# Ensure RBE appears enabled to test that orchestration works.
echo '{"local_only": false}' > "${TEST_BUILD_DIR}/rbe_settings.json"
export FX_REMOTE_BUILD_METRICS=0
# fuchsia-vendored-python is used to run helper scripts like count-ninja-actions.py.
# Make it a pass-through to the real host python in the test environment.
cat > "${BT_TEMP_DIR}/scripts/fuchsia-vendored-python.mock_side_effects" <<EOF
exec "$(command -v python3)" "\$@"
EOF
# args.json is parsed to look for enable_jobserver in Ninja build mode.
# Ensure this is disabled by default.
touch "${TEST_BUILD_DIR}/args.json"
# Force concurrency, see fx-cpu-count.
mock_getconf=$(btf::make_mock_binary getconf)
echo "8" > "${mock_getconf}.mock_stdout"
btf::add_binary_to_path "${mock_getconf}"
}
# In Ninja build mode, build/api/client is called twice. The first time to
# convert GN labels and Ninja output paths to GN labels (if possible, since
# some GN convenience aliases have no GN labels), the second time to convert
# the labels (and remaining aliases) to Ninja paths.
#
# These two steps are used to print different set of messages in case of
# errors, and could be grouped into a single build/api/client command, but
# for now, both need to be supported.
# $1: The output of the first build/api/client (should be a list of labels)
# $2: The output of the second build/api/client (should be a list of Ninja paths)
setup_build_api_results() {
echo "$1" > "${build_api_client}.mock_stdout.1"
echo "$2" > "${build_api_client}.mock_stdout.2"
}
setup_fint_build() {
# tools/integration/bootstrap.sh is already mocked, populated
# the mock FX_CACHE_DIR with a symlink to a mock fint tool.
fint_bin=$(btf::make_mock_binary bin/fint)
# bootstrap.sh -o <path>
cat > "${BT_TEMP_DIR}/tools/integration/bootstrap.sh.mock_side_effects" <<EOF
if [[ "\$1" == "-o" ]]; then
mkdir -p "\$(dirname "\$2")"
# Use a real script that records its own execution in the EXPECTED path
cat > "\$2" <<INNER
#!/bin/bash
# Records arguments in a way that can be sourced
echo "BT_MOCK_ARGS=(\"\\\$0\" \"\\\$@\")" > "${fint_bin}.mock_state"
INNER
chmod +x "\$2"
fi
EOF
}
# --- Validation Tests ---
TEST_fx-build-validate-gn-labels() {
setup_parsing_validation_test
BT_EXPECT "${fx}" build //src:foo ">" stdout.txt
BT_EXPECT_FILE_CONTAINS stdout.txt "FX_BUILD_MODE gn"
}
TEST_fx-build-validate-gn-toolchain() {
setup_parsing_validation_test
BT_EXPECT "${fx}" build --toolchain=//toolchain:bar //src:foo ">" stdout.txt
BT_EXPECT_FILE_CONTAINS stdout.txt "FX_BUILD_MODE gn"
}
TEST_fx-build-validate-gn-alias-default() {
setup_parsing_validation_test
BT_EXPECT "${fx}" build --default //src:foo ">" stdout.txt
BT_EXPECT_FILE_CONTAINS stdout.txt "FX_BUILD_MODE gn"
}
TEST_fx-build-validate-gn-alias-fuchsia() {
setup_parsing_validation_test
BT_EXPECT "${fx}" build --fuchsia //src:foo ">" stdout.txt
BT_EXPECT_FILE_CONTAINS stdout.txt "FX_BUILD_MODE gn"
}
TEST_fx-build-validate-gn-alias-fidl() {
setup_parsing_validation_test
BT_EXPECT "${fx}" build --fidl //src:foo ">" stdout.txt
BT_EXPECT_FILE_CONTAINS stdout.txt "FX_BUILD_MODE gn"
}
TEST_fx-build-validate-gn-alias-host() {
setup_parsing_validation_test
BT_EXPECT "${fx}" build --host //src:foo ">" stdout.txt
BT_EXPECT_FILE_CONTAINS stdout.txt "FX_BUILD_MODE gn"
}
TEST_fx-build-reject-gn-toolchain-as-last-argument() {
setup_parsing_validation_test
BT_EXPECT_FAIL "${fx}" build //src:foo --host "2>" stderr.txt
BT_EXPECT_FILE_CONTAINS stderr.txt "ERROR: Toolchain option --host must be followed by at least one GN label"
}
TEST_fx-build-validate-bazel-single-label() {
setup_parsing_validation_test
BT_EXPECT "${fx}" build @//src:foo ">" stdout.txt
BT_EXPECT_FILE_CONTAINS stdout.txt "FX_BUILD_MODE bazel"
}
TEST_fx-build-reject-bazel-and-gn-toolchain() {
setup_parsing_validation_test
BT_EXPECT_FAIL "${fx}" build @//src:foo --toolchain=//toolchain:bar "2>" stderr.txt
BT_EXPECT_FILE_CONTAINS stderr.txt "ERROR: GN option --toolchain=//toolchain:bar cannot be used with Bazel targets: @//src:foo"
}
TEST_fx-build-reject-bazel-and-gn-alias-default() {
setup_parsing_validation_test
BT_EXPECT_FAIL "${fx}" build @//src:foo --default "2>" stderr.txt
BT_EXPECT_FILE_CONTAINS stderr.txt "ERROR: GN option --default cannot be used with Bazel targets: @//src:foo"
}
TEST_fx-build-reject-bazel-and-gn-alias-fuchsia() {
setup_parsing_validation_test
BT_EXPECT_FAIL "${fx}" build @//src:foo --fuchsia "2>" stderr.txt
BT_EXPECT_FILE_CONTAINS stderr.txt "ERROR: GN option --fuchsia cannot be used with Bazel targets: @//src:foo"
}
TEST_fx-build-reject-bazel-and-gn-alias-fidl() {
setup_parsing_validation_test
BT_EXPECT_FAIL "${fx}" build @//src:foo --fidl "2>" stderr.txt
BT_EXPECT_FILE_CONTAINS stderr.txt "ERROR: GN option --fidl cannot be used with Bazel targets: @//src:foo"
}
TEST_fx-build-validate-bazel-with-host-alias() {
setup_parsing_validation_test
BT_EXPECT "${fx}" build @//src:foo --host ">" stdout.txt
BT_EXPECT_FILE_CONTAINS stdout.txt "FX_BUILD_MODE bazel"
}
TEST_fx-build-reject-bazel-and-gn-labels() {
setup_parsing_validation_test
BT_EXPECT_FAIL "${fx}" build @//src:foo //src:bar "2>" stderr.txt
BT_EXPECT_FILE_CONTAINS stderr.txt "ERROR: GN label //src:bar cannot be used with Bazel targets: @//src:foo"
}
TEST_fx-build-reject-fint-params-and-bazel-label() {
setup_parsing_validation_test
BT_EXPECT_FAIL "${fx}" build --fint-params-path foo @//src:foo "2>" stderr.txt
BT_EXPECT_FILE_CONTAINS stderr.txt "ERROR: It's invalid to specify Bazel or GN arguments along with --fint-params-path."
}
TEST_fx-build-reject-fint-params-and-gn-label() {
setup_parsing_validation_test
BT_EXPECT_FAIL "${fx}" build --fint-params-path foo //src:foo "2>" stderr.txt
BT_EXPECT_FILE_CONTAINS stderr.txt "ERROR: It's invalid to specify Bazel or GN arguments along with --fint-params-path."
}
TEST_fx-build-reject-fint-params-and-extra-args() {
setup_parsing_validation_test
BT_EXPECT_FAIL "${fx}" build --fint-params-path FILE -- -j20 "2>" stderr.txt
BT_EXPECT_FILE_CONTAINS stderr.txt "ERROR: It's invalid to specify extra Ninja flags along with --fint-params-path."
}
TEST_fx-build-reject-fint-params-missing-arg() {
setup_parsing_validation_test
BT_EXPECT_FAIL "${fx}" build --fint-params-path "2>" stderr.txt
BT_EXPECT_FILE_CONTAINS stderr.txt "ERROR: Missing --fint-params-path argument, see --help."
}
TEST_fx-build-validate-fint-params() {
setup_parsing_validation_test
BT_EXPECT "${fx}" build --fint-params-path FOO ">" stdout.txt
BT_EXPECT_FILE_CONTAINS stdout.txt "FX_BUILD_MODE fint"
}
TEST_fx-build-validate-fint-params-with-single-arg() {
setup_parsing_validation_test
BT_EXPECT "${fx}" build --fint-params-path=FOO ">" stdout.txt
BT_EXPECT_FILE_CONTAINS stdout.txt "FX_BUILD_MODE fint"
}
TEST_fx-build-reject-log-missing-arg() {
setup_parsing_validation_test
BT_EXPECT_FAIL "${fx}" build --log "2>" stderr.txt
BT_EXPECT_FILE_CONTAINS stderr.txt "ERROR: Missing --log argument, see --help."
}
# --- Integration Tests ---
TEST_fx-build-with-fint() {
setup_build_test
setup_fint_build
BT_EXPECT "${fx}" build --fint-params-path=path/to/build_config.proto
BT_EXPECT_FILE_EXISTS "${fint_bin}.mock_state"
source "${fint_bin}.mock_state"
local actual_args="${BT_MOCK_ARGS[*]}"
# The last item in BT_MOCK_ARGS will be -context=/path/to/tmp.textproto
# Sanitize dynamic paths (like /dev/fd/N or /tmp/tmp.textproto) to allow for stable comparisons.
actual_args=$(echo "${actual_args}" | sed -e 's|/.*/fint|/dev/ANY/fint|' -e 's|-context=[^ ]*|-context=/dev/fd/ANY|')
local expected_args="/dev/ANY/fint -log-level=error build -static=path/to/build_config.proto -context=/dev/fd/ANY"
BT_EXPECT_EQ "${actual_args}" "${expected_args}"
}
TEST_fx-build-with-gn-label() {
setup_build_test
setup_build_api_results "//src:foo" "obj/src/foo"
# Use dry run to verify the entire stack without actually running Ninja.
# This flows through the real top_build_wrap.sh but uses pass-through mocks
# for intermediate wrappers (reproxy, profile, rsproxy).
export FX_BUILD_DRY_RUN=1
BT_EXPECT "${fx}" build //src:foo ">" stdout.txt
# Verify that the converted Ninja target reached the end of the stack.
BT_EXPECT grep -q "obj/src/foo" stdout.txt
}
TEST_fx-build-with-gn-label-and-j32() {
setup_build_test
setup_build_api_results "//src:foo" "obj/src/foo"
export FX_BUILD_DRY_RUN=1
BT_EXPECT "${fx}" build -j32 //src:foo ">" stdout.txt
# Verify both the flag and the target reached the end of the stack.
# Note: main_build.py normalizes -j32 to -j 32
BT_EXPECT "grep -q -e '-j 32' stdout.txt"
BT_EXPECT grep -q "obj/src/foo" stdout.txt
}
TEST_fx-build-with-gn-label-and-enable_jobserver() {
setup_build_test
# Verify that enable_jobserver=true in args.gn sets the Ninja --jobserver flag.
echo "{ \"enable_jobserver\": true }" > "${TEST_BUILD_DIR}/args.json"
setup_build_api_results "//src:foo" "obj/src/foo"
export FX_BUILD_DRY_RUN=1
BT_EXPECT "${fx}" build //src:foo ">" stdout.txt
BT_EXPECT grep -q -e "--jobserver" stdout.txt
BT_EXPECT grep -q "obj/src/foo" stdout.txt
}
TEST_fx-build-with-gn-label-and-j-32() {
setup_build_test
setup_build_api_results "//src:foo" "obj/src/foo"
export FX_BUILD_DRY_RUN=1
BT_EXPECT "${fx}" build -j 32 //src:foo ">" stdout.txt
# Verify both the flag and the target reached the end of the stack.
BT_EXPECT "grep -q -e '-j 32' stdout.txt"
BT_EXPECT grep -q "obj/src/foo" stdout.txt
}
TEST_fx-build-with-ninja-path() {
setup_build_test
# When a raw Ninja path is used, build/api/client is called to convert it to GN labels (if any).
# If it's already a Ninja path, it might return it as-is.
setup_build_api_results "obj/src/foo" "obj/src/foo"
export FX_BUILD_DRY_RUN=1
BT_EXPECT "${fx}" build obj/src/foo ">" stdout.txt
BT_EXPECT grep -q "obj/src/foo" stdout.txt
}
TEST_fx-build-with-bazel-non-host-label() {
setup_build_test
BT_EXPECT_FAIL "${fx}" build @//src/foo "2>" stderr.txt
BT_EXPECT_FILE_CONTAINS stderr.txt "ERROR: Only host Bazel target labels are supported for now, please use --host flag."
}
TEST_fx-build-with-bazel-host-label() {
setup_build_test
export FX_BUILD_DRY_RUN=1
BT_EXPECT "${fx}" build --host @//src/foo ">" stdout.txt
# Verify that the command reached the end of the stack with correct flags.
BT_EXPECT grep -q "bazel" stdout.txt
BT_EXPECT grep -q "build" stdout.txt
BT_EXPECT grep -q -e "--config=host" stdout.txt
BT_EXPECT grep -q -e "@//src/foo" stdout.txt
}
TEST_fx-build-with-bazel-host-label-and-j32() {
setup_build_test
export FX_BUILD_DRY_RUN=1
BT_EXPECT "${fx}" build --host -j32 @//src/foo ">" stdout.txt
BT_EXPECT grep -q "bazel" stdout.txt
BT_EXPECT grep -q "build" stdout.txt
BT_EXPECT grep -q -e "--config=host" stdout.txt
BT_EXPECT grep -q -e "-j32" stdout.txt
BT_EXPECT grep -q -e "@//src/foo" stdout.txt
}
TEST_fx-build-with-bazel-label-with-trailing-host() {
setup_build_test
export FX_BUILD_DRY_RUN=1
# Unlike with GN build mode, --host can appear after the Bazel label
BT_EXPECT "${fx}" build @//src/foo --host ">" stdout.txt
BT_EXPECT grep -q "bazel" stdout.txt
BT_EXPECT grep -q "build" stdout.txt
BT_EXPECT grep -q -e "--config=host" stdout.txt
BT_EXPECT grep -q -e "@//src/foo" stdout.txt
}
TEST_fx-build-with-bazel-label-with-config-host() {
setup_build_test
export FX_BUILD_DRY_RUN=1
# Unlike GN --toolchain options, --config can appear after the Bazel label.
BT_EXPECT "${fx}" build @//src/foo --config=host ">" stdout.txt
BT_EXPECT grep -q "bazel" stdout.txt
BT_EXPECT grep -q "build" stdout.txt
BT_EXPECT grep -q -e "--config=host" stdout.txt
BT_EXPECT grep -q -e "@//src/foo" stdout.txt
}
TEST_fx-build-produces-build-tracking-events() {
setup_build_test
track_build_event=$(btf::make_mock_binary track-build-event)
cat > "${BT_TEMP_DIR}/tools/devshell/lib/metrics.sh.mock_side_effects" <<EOF
function metrics-init {
echo ""
}
function track-command-execution {
echo ""
}
function track-feature-status {
echo ""
}
function track-command-finished {
echo ""
}
function track-build-event {
"${track_build_event}" "\$@"
}
EOF
setup_build_api_results "//src:foo" "obj/src/foo"
export FX_BUILD_DRY_RUN=1
BT_EXPECT "${fx}" build "-j4" //src:foo ">" stdout.txt
# Verify that the target reached the end of the stack.
BT_EXPECT grep -q "obj/src/foo" stdout.txt
source "${track_build_event}.mock_state"
BT_EXPECT_EQ "${#BT_MOCK_ARGS[@]}" "10"
BT_EXPECT_EQ "${BT_MOCK_ARGS[0]}" "${track_build_event}"
# [1] is start time (time-dependent)
# [2] is end time (time-dependent)
BT_EXPECT_EQ "${BT_MOCK_ARGS[3]}" "0" # status
BT_EXPECT_EQ "${BT_MOCK_ARGS[4]}" "-j4" # extra switches
BT_EXPECT_EQ "${BT_MOCK_ARGS[5]}" "obj/src/foo" # ninja targets
BT_EXPECT_EQ "${BT_MOCK_ARGS[6]}" "${TEST_BUILD_DIR}" # fuchsia build dir
# [7] is_no_op (timestamp-dependent)
# [8] is_clean_build (timestamp-dependent)
BT_EXPECT_EQ "${BT_MOCK_ARGS[9]}" "" # quiet
}
BT_RUN_TESTS "$@"