blob: f748f75c9d3b253ae19cad18544859fe492369b7 [file]
# Copyright 2026 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.
"""Unit tests for bazel_build_args.py."""
import io
import os
import sys
import tempfile
import unittest
from pathlib import Path
_SCRIPT_DIR = os.path.dirname(__file__)
sys.path.insert(0, _SCRIPT_DIR)
import bazel_build_args
import build_utils
from bazel_build_args import (
ParsedAction,
ResolvedBuildArgsFlags,
ResolvedBuildArgsMap,
ResponseFileMap,
ResponseFileTarget,
)
class PathToLabelTest(unittest.TestCase):
def test_valid_path_parsing(self) -> None:
TEST_CASES = [
(
"bazel-out/k8-fastbuild/bin/build/bazel/examples/hello_world/main.cpp_compile.build_flags",
ResponseFileTarget(
"@@//build/bazel/examples/hello_world:main.final_build_flags",
"cpp_compile",
),
),
(
"bazel-out/fuchsia_platform_arm64-fastbuild/bin/src/my_pkg/tool.cpp_link.build_flags",
ResponseFileTarget(
"@@//src/my_pkg:tool.final_build_flags", "cpp_link"
),
),
(
"execroot/_main/bazel-out/linux_x64-opt/bin/external/rules_rust/tool.rust_compile.build_flags",
ResponseFileTarget(
"@@rules_rust//:tool.final_build_flags", "rust_compile"
),
),
(
"${pwd}/bazel-out/k8-fastbuild/bin/build/target.rust_compile.build_flags",
ResponseFileTarget(
"@@//build:target.final_build_flags", "rust_compile"
),
),
(
"${pwd}/bazel-out/k8-fastbuild/bin/build/target.rustc_env_file.build_flags",
ResponseFileTarget(
"@@//build:target.final_build_flags", "rust_compile"
),
),
]
for path, expected in TEST_CASES:
result = ResponseFileTarget.from_execroot_path(path)
self.assertEqual(result, expected, msg=f"For path [{path}]")
def test_invalid_path_returns_none(self) -> None:
TEST_CASES = [
"src/my_pkg/tool.cpp_compile.build_flags", # no bazel-out/../bin/
"bazel-out/k8-fastbuild/bin/build/target.unknown_suffix",
"bazel-out/bin/too_short.cpp_compile.build_flags", # config dir missing
"",
]
for path in TEST_CASES:
self.assertIsNone(
ResponseFileTarget.from_execroot_path(path),
msg=f"Expected None for [{path}]",
)
class ParseAqueryOutputTest(unittest.TestCase):
def test_parse_aquery_no_filter(self) -> None:
aquery_output = """
{
"targets": [
{ "id": 1, "label": "//src:main" },
{ "id": 2, "label": "//src:lib" }
],
"configuration": [
{ "id": 1, "mnemonic": "k8-fastbuild" }
],
"actions": [
{
"targetId": 1,
"mnemonic": "CppCompile",
"configurationId": 1,
"arguments": ["exec", "clang", "-c", "src/main.cc", "@bazel-out/k8-fastbuild/bin/src/main.cpp_compile.build_flags", "-o", "main.o"],
"environmentVariables": [
{ "key": "PATH", "value": "/bin" }
]
},
{
"targetId": 1,
"mnemonic": "CppLink",
"configurationId": 1,
"arguments": ["exec", "clang", "main.o", "-o", "bin", "@bazel-out/k8-fastbuild/bin/main.cpp_link.build_flags"]
}
]
}
"""
result = bazel_build_args.parse_aquery_output(aquery_output)
self.assertEqual(len(result.actions), 2)
self.assertEqual(result.actions[0].mnemonic, "CppCompile")
self.assertEqual(result.actions[0].target, "//src:main")
self.assertEqual(result.actions[0].env_vars, {"PATH": "/bin"})
self.assertListEqual(
result.actions[0].args,
[
"exec",
"clang",
"-c",
"src/main.cc",
"@bazel-out/k8-fastbuild/bin/src/main.cpp_compile.build_flags",
"-o",
"main.o",
],
)
self.assertEqual(result.actions[1].mnemonic, "CppLink")
self.assertListEqual(
result.actions[1].args,
[
"exec",
"clang",
"main.o",
"-o",
"bin",
"@bazel-out/k8-fastbuild/bin/main.cpp_link.build_flags",
],
)
self.assertDictEqual(
result.response_files_map,
{
"bazel-out/k8-fastbuild/bin/src/main.cpp_compile.build_flags": (
ResponseFileTarget(
"@@//src:main.final_build_flags", "cpp_compile"
)
),
"bazel-out/k8-fastbuild/bin/main.cpp_link.build_flags": (
ResponseFileTarget(
"@@//:main.final_build_flags", "cpp_link"
)
),
},
)
def test_parse_aquery_with_mnemonic_filter(self) -> None:
aquery_output = """
{
"targets": [
{ "id": 1, "label": "//src:main" },
{ "id": 2, "label": "//src:lib" }
],
"configuration": [
{ "id": 1, "mnemonic": "k8-fastbuild" }
],
"actions": [
{
"targetId": 1,
"mnemonic": "CppCompile",
"configurationId": 1,
"arguments": ["exec", "clang", "-c", "src/main.cc", "@bazel-out/k8-fastbuild/bin/src/main.cpp_compile.build_flags"]
},
{
"targetId": 2,
"mnemonic": "CppLink",
"configurationId": 1,
"arguments": ["exec", "clang", "main.o", "-o", "bin", "@bazel-out/k8-fastbuild/bin/main.cpp_link.build_flags"]
}
]
}
"""
result = bazel_build_args.parse_aquery_output(
aquery_output,
filter_mnemonics=["CppCompile"],
)
self.assertEqual(len(result.actions), 1)
self.assertEqual(result.actions[0].mnemonic, "CppCompile")
self.assertDictEqual(
result.response_files_map,
{
"bazel-out/k8-fastbuild/bin/src/main.cpp_compile.build_flags": (
ResponseFileTarget(
"@@//src:main.final_build_flags", "cpp_compile"
)
),
},
)
def test_parse_aquery_malformed_json_returns_empty(self) -> None:
suppressed_stderr = io.StringIO()
orig_stderr = sys.stderr
sys.stderr = suppressed_stderr
try:
result = bazel_build_args.parse_aquery_output("malformed { json }")
finally:
sys.stderr = orig_stderr
self.assertEqual(len(result.actions), 0)
self.assertEqual(len(result.response_files_map), 0)
class ExpandArgsFromDiskTest(unittest.TestCase):
def test_recursive_disk_expansion(self) -> None:
with tempfile.TemporaryDirectory() as tmp_dir:
execroot = Path(tmp_dir)
# Create a standard .params file
params_file = execroot / "target.params"
params_file.write_text("-DCOMPILE_FLAG\n@nested.flags\n")
# Create a nested custom flags file
nested_file = execroot / "nested.flags"
nested_file.write_text("-DNESTED_FLAG\n--env-file\nenv.vars\n")
# Create an env file
env_file = execroot / "env.vars"
env_file.write_text("KEY=VAL\n")
raw_args = [
"exec",
"clang",
"@target.params",
"-o",
"output",
]
result = bazel_build_args.expand_args_from_disk(
raw_args, {}, str(execroot)
)
self.assertListEqual(
result.expanded_args,
[
"exec",
"clang",
"-DCOMPILE_FLAG",
"-DNESTED_FLAG",
"-o",
"output",
],
)
self.assertListEqual(result.env_vars, ["KEY=VAL"])
self.assertEqual(len(result.warnings), 0)
def test_cycle_detection(self) -> None:
with tempfile.TemporaryDirectory() as tmp_dir:
execroot = Path(tmp_dir)
# Create a cycling params file
params_file = execroot / "cycle.params"
params_file.write_text("@cycle.params\n")
raw_args = ["@cycle.params"]
result = bazel_build_args.expand_args_from_disk(
raw_args, {}, str(execroot)
)
self.assertListEqual(result.expanded_args, ["@cycle.params"])
self.assertListEqual(result.env_vars, [])
self.assertEqual(len(result.warnings), 1)
self.assertEqual(
result.warnings[0],
"Cycle detected in response file @cycle.params: cycle.params -> cycle.params",
)
def test_missing_file_reports_warning(self) -> None:
result = bazel_build_args.expand_args_from_disk(
["@missing.params"], {}, "/non_existent_root"
)
self.assertListEqual(result.expanded_args, ["@missing.params"])
self.assertListEqual(result.env_vars, [])
self.assertEqual(len(result.warnings), 1)
self.assertIn("was not found on disk", result.warnings[0])
def test_safe_path_normalization_prevents_false_positives(self) -> None:
with tempfile.TemporaryDirectory() as tmp_dir:
execroot = Path(tmp_dir)
pwd_sub_dir = execroot / "mypwd"
pwd_sub_dir.mkdir()
nested_file = pwd_sub_dir / "target.params"
nested_file.write_text("-DCOMPILE_FLAG\n")
raw_args = ["@mypwd/target.params"]
result = bazel_build_args.expand_args_from_disk(
raw_args, {}, str(execroot)
)
self.assertListEqual(result.expanded_args, ["-DCOMPILE_FLAG"])
self.assertListEqual(result.env_vars, [])
self.assertEqual(len(result.warnings), 0)
def test_normalize_path_strips_env_pwd_but_keeps_folder_pwd(self) -> None:
with tempfile.TemporaryDirectory() as tmp_dir:
execroot = Path(tmp_dir)
# Directory named "pwd" should be kept
pwd_dir = execroot / "pwd"
pwd_dir.mkdir()
file_in_pwd = pwd_dir / "target.params"
file_in_pwd.write_text("-DPWD_FOLDER_FLAG\n")
# File in root to be found via stripped ${pwd}/ prefix
file_in_root = execroot / "target.params"
file_in_root.write_text("-DROOT_FLAG\n")
# Test that ${pwd}/ prefix gets stripped and targets file in root
result_stripped = bazel_build_args.expand_args_from_disk(
["@${pwd}/target.params"], {}, str(execroot)
)
self.assertListEqual(result_stripped.expanded_args, ["-DROOT_FLAG"])
# Test that pwd/ prefix is NOT stripped and targets file in pwd/ directory
result_not_stripped = bazel_build_args.expand_args_from_disk(
["@pwd/target.params"], {}, str(execroot)
)
self.assertListEqual(
result_not_stripped.expanded_args, ["-DPWD_FOLDER_FLAG"]
)
class ExpandArgsStaticTest(unittest.TestCase):
def test_static_expansion(self) -> None:
raw_args = [
"exec",
"clang",
"@bazel-out/k8-fastbuild/bin/src/main.cpp_compile.build_flags",
"@bazel-out/k8-fastbuild/bin/target.params",
"--env-file",
"bazel-out/k8-fastbuild/bin/env.rustc_env_file.build_flags",
]
response_files_map = ResponseFileMap(
{
"bazel-out/k8-fastbuild/bin/src/main.cpp_compile.build_flags": (
ResponseFileTarget(
"@@//src:main.final_build_flags", "cpp_compile"
)
),
"bazel-out/k8-fastbuild/bin/env.rustc_env_file.build_flags": (
ResponseFileTarget(
"@@//src:main.final_build_flags", "rust_compile"
)
),
}
)
build_args_map = ResolvedBuildArgsMap(
{
"@@//src:main.final_build_flags": ResolvedBuildArgsFlags(
label="@@//src:main.final_build_flags",
cflags=["-DCOMPILE_FLAG"],
cflags_c=[],
cflags_cc=["-DCC_FLAG"],
defines=[],
include_dirs=[],
ldflags=[],
lib_dirs=[],
rustflags=[],
rustenv=["KEY=VAL"],
)
}
)
result = bazel_build_args.expand_args_with_build_args_map(
raw_args, {}, response_files_map, build_args_map
)
self.assertListEqual(
result.expanded_args,
[
"exec",
"clang",
"-DCOMPILE_FLAG",
"-DCC_FLAG",
"@bazel-out/k8-fastbuild/bin/target.params",
],
)
self.assertListEqual(result.env_vars, ["KEY=VAL"])
# Verify that the standard .params file registers a warning and is kept verbatim
self.assertEqual(len(result.warnings), 1)
self.assertIn("cannot be expanded with queries", result.warnings[0])
class GetBazelExpandedActionsTest(unittest.TestCase):
def test_get_bazel_expanded_actions_static(self) -> None:
# Setup mocks
mock_launcher = build_utils.MockBazelLauncher()
mock_launcher.push_expected_outputs(['{"some": "json"}'])
# Mock aquery result
actions = [
ParsedAction(
name="action1",
target="//src:main",
config="config",
mnemonic="CppCompile",
args=[
"exec",
"clang",
"@bazel-out/bin/main.cpp_compile.build_flags",
],
env_vars={"FOO": "foo"},
)
]
response_files_map = ResponseFileMap(
{
"bazel-out/bin/main.cpp_compile.build_flags": ResponseFileTarget(
"@@//src:main.final_build_flags", "cpp_compile"
)
}
)
# Monkeypatch
orig_parse_aquery = bazel_build_args.parse_aquery_output
orig_query_flags = bazel_build_args.query_build_flags_from_bazel
bazel_build_args.parse_aquery_output = (
lambda aquery_output, filter_mnemonics=None: (
bazel_build_args.ParsedAqueryResult(
actions=actions,
response_files_map=response_files_map,
uses_bazel_params_file=False,
)
)
)
bazel_build_args.query_build_flags_from_bazel = lambda final_build_flags_labels, bazel_launcher, config_args: (
ResolvedBuildArgsMap(
{
"@@//src:main.final_build_flags": ResolvedBuildArgsFlags(
label="@@//src:main.final_build_flags",
cflags=["-DSTATIC_FLAG"],
cflags_c=[],
cflags_cc=[],
defines=[],
include_dirs=[],
ldflags=[],
lib_dirs=[],
rustflags=[],
rustenv=[],
)
}
)
)
try:
expanded = bazel_build_args.get_bazel_expanded_actions(
bazel_launcher=mock_launcher,
bazel_execroot="/dummy_root",
bazel_target="//src:main",
config_args=["--config=fuchsia"],
read_response_files=False,
)
self.assertEqual(len(expanded), 1)
self.assertEqual(expanded[0].action, "action1")
self.assertEqual(expanded[0].target, "//src:main")
self.assertListEqual(
expanded[0].args, ["exec", "clang", "-DSTATIC_FLAG"]
)
self.assertListEqual(expanded[0].env_vars, ["FOO=foo"])
self.assertEqual(len(expanded[0].warnings), 0)
finally:
# Restore original functions
bazel_build_args.parse_aquery_output = orig_parse_aquery
bazel_build_args.query_build_flags_from_bazel = orig_query_flags
def test_get_bazel_expanded_actions_dynamic(self) -> None:
with tempfile.TemporaryDirectory() as tmp_dir:
execroot = Path(tmp_dir)
flags_file = execroot / "bazel-out/bin/main.cpp_compile.build_flags"
flags_file.parent.mkdir(parents=True, exist_ok=True)
flags_file.write_text("-DDYNAMIC_FLAG\n")
mock_launcher = build_utils.MockBazelLauncher()
mock_launcher.push_expected_outputs(["{}"])
actions = [
ParsedAction(
name="action1",
target="//src:main",
config="config",
mnemonic="CppCompile",
args=[
"exec",
"clang",
f"@bazel-out/bin/main.cpp_compile.build_flags",
],
env_vars={"FOO": "foo"},
)
]
# Monkeypatch
orig_parse_aquery = bazel_build_args.parse_aquery_output
bazel_build_args.parse_aquery_output = (
lambda aquery_output, filter_mnemonics=None: (
bazel_build_args.ParsedAqueryResult(
actions=actions,
response_files_map=ResponseFileMap({}),
uses_bazel_params_file=False,
)
)
)
try:
expanded = bazel_build_args.get_bazel_expanded_actions(
bazel_launcher=mock_launcher,
bazel_execroot=str(execroot),
bazel_target="//src:main",
config_args=[],
read_response_files=True,
)
self.assertEqual(len(expanded), 1)
self.assertEqual(expanded[0].action, "action1")
self.assertListEqual(
expanded[0].args, ["exec", "clang", "-DDYNAMIC_FLAG"]
)
self.assertListEqual(expanded[0].env_vars, ["FOO=foo"])
finally:
# Restore
bazel_build_args.parse_aquery_output = orig_parse_aquery
if __name__ == "__main__":
unittest.main()