blob: cf729a015efacfb7449980557e80053765f6fb25 [file] [log] [blame]
#!/usr/bin/env fuchsia-vendored-python
# Copyright 2024 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.
import json
import os
import subprocess
import sys
import tempfile
import unittest
from pathlib import Path
from typing import Any, List
_SCRIPT_DIR = Path(__file__).parent
_BUILD_API_SCRIPT = _SCRIPT_DIR / "client"
# While these values are also defined in ninja_artifacts.py, redefine
# them here to avoid an import statement, as this regression test
# should not depend on implementation details of the client.py
# script.
_NINJA_BUILD_PLAN_DEPS_FILE = "build.ninja.d"
_NINJA_LAST_BUILD_TARGETS_FILE = "last_ninja_build_targets.txt"
_NINJA_LAST_BUILD_SUCCESS_FILE = "last_ninja_build_success.stamp"
def _write_file(path: Path, content: str):
path.parent.mkdir(parents=True, exist_ok=True)
path.write_text(content)
def _write_json(path: Path, content: Any):
with path.open("w") as f:
json.dump(content, f, sort_keys=True)
class ClientTest(unittest.TestCase):
def setUp(self):
self._temp_dir = tempfile.TemporaryDirectory()
self._top_dir = Path(self._temp_dir.name)
self._build_gn_path = self._top_dir / "BUILD.gn"
self._build_gn_path.write_text("# EMPTY\n")
self._build_dir = self._top_dir / "out" / "build_dir"
self._build_dir.mkdir(parents=True)
self._build_ninja_d_path = self._build_dir / _NINJA_BUILD_PLAN_DEPS_FILE
self._build_ninja_d_path.write_text(
"build.ninja.stamp: ../../BUILD.gn\n"
)
self._last_build_success_path = (
self._build_dir / _NINJA_LAST_BUILD_SUCCESS_FILE
)
self._last_targets_path = (
self._build_dir / _NINJA_LAST_BUILD_TARGETS_FILE
)
self._build_ninja_path = self._build_dir / "build.ninja"
# The build_api_client_info file maps each module name to its .json file.
with (self._build_dir / "build_api_client_info").open("wt") as f:
f.write(
"""args=args.json
build_info=build_info.json
debug_symbols=debug_symbols.json
tests=tests.json
"""
)
# The $BUILD_DIR/args.json file is necessary to extract the
# target cpu value.
self._args_json = json.dumps({"target_cpu": "aRm64"})
# Fake tests.json with a single entry.
self._tests_json = json.dumps(
[
{
"environments": [
{
"dimensions": {
"cpu": "y64",
"os": "Linux",
}
}
],
"test": {
"cpu": "y64",
"label": "//some/test:target(//build/toolchain:host_y64)",
"name": "host_y64/obj/some/test/target_test.sh",
"os": "linux",
"path": "host_y64/obj/some/test/target_test.sh",
"runtime_deps": "host_y64/gen/some/test/target_test.deps.json",
},
},
]
)
# Fake build_info.json
self._build_info_json = json.dumps(
{
"configurations": [
{
"board": "y64",
"product": "core",
},
],
"version": "",
},
)
_write_file(self._build_dir / "args.json", self._args_json)
_write_file(self._build_dir / "tests.json", self._tests_json)
_write_file(self._build_dir / "build_info.json", self._build_info_json)
# Fake Ninja outputs.
self._ninja_outputs = {
"//foo:foo": [
"obj/foo.stamp",
],
"//bar:bar": [
"obj/bar.output",
"obj/bar.stamp",
],
"//src:lib": [
"obj/src/lib.cc.o",
],
"//src:bin": [
"obj/src/main.cc.o",
"obj/src/program",
],
"//tools:hammer(//build/toolchain:host_y64)": [
"host_y64/exe.unstripped/hammer",
"host_y64/hammer",
"host_y64/obj/tools/hammer.cc.o",
],
"//some/test:target(//build/toolchain:host_y64)": [
"host_y64/obj/some/test/target_test.sh",
"host_y64/gen/some/test/target_test.deps.json",
],
}
_write_json(self._build_dir / "ninja_outputs.json", self._ninja_outputs)
# Fake debug symbols
self._debug_symbols_json = json.dumps(
[
{
"cpu": "x64",
"debug": "obj/src/foo/lib_shared/libfoo.so.unstripped",
"elf_build_id": "00000000000000001",
"label": "//src/foo:lib_shared",
"os": "fuchsia",
},
{
"cpu": "x64",
"debug": "../../prebuilt/.build-id/aa/bbbbbbbbbbb.debug",
"label": "//prebuilt/foo:symbol_file",
"os": "fuchsia",
},
{
"cpu": "x64",
"debug": "obj/src/bar/binary.unstripped",
"elf_build_id_file": "obj/src/bar/binary.elf_build_id",
"label": "//src/bar:binary",
"os": "fuchsia",
},
{
"cpu": "x64",
"debug": "obj/src/zoo/binary.unstripped",
"label": "//src/zoo:binary",
"os": "fuchsia",
},
]
)
_write_file(
self._build_dir / "debug_symbols.json", self._debug_symbols_json
)
_write_file(
self._build_dir / "obj/src/bar/binary.elf_build_id",
"build_id_for_bar",
)
def tearDown(self):
self._temp_dir.cleanup()
def run_raw_client(
self, args: List[str | Path]
) -> subprocess.CompletedProcess:
return subprocess.run(
[
_BUILD_API_SCRIPT,
]
+ [str(a) for a in args],
cwd=self._build_dir,
text=True,
capture_output=True,
)
def assert_raw_outputs(
self,
raw_ret: subprocess.CompletedProcess,
expected_out: str,
expected_err: str = "",
expected_status: int = 0,
msg: str = "",
):
if not expected_err and raw_ret.stderr:
print(f"ERROR: {raw_ret.stderr}", file=sys.stderr)
self.assertEqual(expected_err, raw_ret.stderr, msg=msg)
self.assertEqual(expected_out, raw_ret.stdout, msg=msg)
self.assertEqual(expected_status, raw_ret.returncode, msg=msg)
def run_client(self, args: List[str | Path]) -> subprocess.CompletedProcess:
return self.run_raw_client(
[
"--build-dir",
str(self._build_dir),
"--host-tag=linux-y64",
]
+ args
)
def assert_output(
self,
args: List[str | Path],
expected_out: str,
expected_err: str = "",
expected_status: int = 0,
msg: str = "",
):
return self.assert_raw_outputs(
self.run_client(args),
expected_out,
expected_err,
expected_status,
msg="'%s' command" % " ".join(str(a) for a in args),
)
def assert_error(
self,
args: List[str | Path],
expected_err: str,
msg: str = "",
):
self.assert_output(args, "", expected_err, expected_status=1, msg=msg)
def test_list(self):
self.assert_output(["list"], "args\nbuild_info\ndebug_symbols\ntests\n")
def test_print(self):
MODULES = {
"args": self._args_json + "\n",
"tests": self._tests_json + "\n",
"build_info": self._build_info_json + "\n",
}
for module, expected in MODULES.items():
self.assert_output(["print", module], expected)
def test_print_all(self):
expected = {
"args": {
"file": "args.json",
"json": json.loads(self._args_json),
},
"build_info": {
"file": "build_info.json",
"json": json.loads(self._build_info_json),
},
"debug_symbols": {
"file": "debug_symbols.json",
"json": json.loads(self._debug_symbols_json),
},
"tests": {
"file": "tests.json",
"json": json.loads(self._tests_json),
},
}
self.assert_output(["print_all"], json.dumps(expected) + "\n")
self.assert_output(
["print_all", "--pretty"], json.dumps(expected, indent=2) + "\n"
)
def test_print_all(self):
expected = {
"args": {
"file": "args.json",
"json": json.loads(self._args_json),
},
"build_info": {
"file": "build_info.json",
"json": json.loads(self._build_info_json),
},
"debug_symbols": {
"file": "debug_symbols.json",
"json": json.loads(self._debug_symbols_json),
},
"tests": {
"file": "tests.json",
"json": json.loads(self._tests_json),
},
}
self.assert_output(["print_all"], json.dumps(expected) + "\n")
self.assert_output(
["print_all", "--pretty"], json.dumps(expected, indent=2) + "\n"
)
def test_print_debug_symbols(self):
self.maxDiff = None
expected = [
{
"cpu": "x64",
"debug": "obj/src/foo/lib_shared/libfoo.so.unstripped",
"elf_build_id": "00000000000000001",
"label": "//src/foo:lib_shared",
"os": "fuchsia",
},
{
"cpu": "x64",
"debug": "../../prebuilt/.build-id/aa/bbbbbbbbbbb.debug",
"label": "//prebuilt/foo:symbol_file",
"os": "fuchsia",
},
{
"cpu": "x64",
"debug": "obj/src/bar/binary.unstripped",
"elf_build_id_file": "obj/src/bar/binary.elf_build_id",
"label": "//src/bar:binary",
"os": "fuchsia",
},
{
"cpu": "x64",
"debug": "obj/src/zoo/binary.unstripped",
"label": "//src/zoo:binary",
"os": "fuchsia",
},
]
self.assert_output(["print_debug_symbols"], json.dumps(expected) + "\n")
self.assert_output(
["print_debug_symbols", "--pretty"],
json.dumps(expected, indent=2) + "\n",
)
def test_print_debug_symbols_with_build_id_resolution(self):
self.maxDiff = None
expected = [
{
"cpu": "x64",
"debug": "obj/src/foo/lib_shared/libfoo.so.unstripped",
"elf_build_id": "00000000000000001",
"label": "//src/foo:lib_shared",
"os": "fuchsia",
},
{
"cpu": "x64",
"debug": "../../prebuilt/.build-id/aa/bbbbbbbbbbb.debug",
"elf_build_id": "aabbbbbbbbbbb",
"label": "//prebuilt/foo:symbol_file",
"os": "fuchsia",
},
{
"cpu": "x64",
"debug": "obj/src/bar/binary.unstripped",
"elf_build_id": "build_id_for_bar",
"elf_build_id_file": "obj/src/bar/binary.elf_build_id",
"label": "//src/bar:binary",
"os": "fuchsia",
},
# NOTE: Because of the --test-mode flag used below, the build-id value for the
# file obj/src/zoo/binary.unstripped is just its file name. This avoids creating
# a fake ELF file in the test build directory.
{
"cpu": "x64",
"debug": "obj/src/zoo/binary.unstripped",
"elf_build_id": "binary.unstripped",
"label": "//src/zoo:binary",
"os": "fuchsia",
},
]
self.assert_output(
["print_debug_symbols", "--resolve-build-ids", "--test-mode"],
json.dumps(expected) + "\n",
)
self.assert_output(
[
"print_debug_symbols",
"--resolve-build-ids",
"--test-mode",
"--pretty",
],
json.dumps(expected, indent=2) + "\n",
)
def test_ninja_path_to_gn_label(self):
# Test each Ninja path individually.
for label, paths in self._ninja_outputs.items():
for path in paths:
self.assert_output(
["ninja_path_to_gn_label", path], f"{label}\n"
)
# Test each set of Ninja output paths per label.
for label, paths in self._ninja_outputs.items():
self.assert_output(["ninja_path_to_gn_label"] + paths, f"{label}\n")
# Test a single invocation with all Ninja paths, which must return the set of all labels,
# deduplicated.
all_paths = set()
for paths in self._ninja_outputs.values():
all_paths.update(paths)
all_labels = sorted(set(self._ninja_outputs.keys()))
expected = "\n".join(all_labels) + "\n"
self.assert_output(
["ninja_path_to_gn_label"] + sorted(all_paths), expected
)
# Test unknown Ninja path
self.assert_error(
["ninja_path_to_gn_label", "obj/unknown/path"],
"ERROR: Unknown Ninja target path: obj/unknown/path\n",
)
def test_ninja_target_to_gn_labels(self):
# Test each Ninja path basename individually. This works because
# the only two file paths with the same name are produced by the same GN
# label.
for label, paths in self._ninja_outputs.items():
for target_name in list({os.path.basename(p) for p in paths}):
self.assert_output(
["ninja_target_to_gn_labels", target_name], f"{label}\n"
)
# Test unknown Ninja target name
self.assert_output(["ninja_target_to_gn_labels", "unknown_target"], "")
# Test malformed Ninja target name
self.assert_error(
["ninja_target_to_gn_labels", "some/path"],
"ERROR: Malformed Ninja target file name: some/path\n",
)
# Update the ninja_outputs.json file to include a second label
# that generates a target with the name "hammer", then check that
# the command returns a list with two labels.
self._ninja_outputs["//secondary:hammer_target"] = ["other/hammer"]
_write_json(self._build_dir / "ninja_outputs.json", self._ninja_outputs)
self.assert_output(
["ninja_target_to_gn_labels", "hammer"],
"//secondary:hammer_target\n"
+ "//tools:hammer(//build/toolchain:host_y64)\n",
)
def test_gn_labels_to_ninja_paths(self):
# Test each label individually.
for label, paths in self._ninja_outputs.items():
expected = "\n".join(sorted(paths)) + "\n"
self.assert_output(["gn_label_to_ninja_paths", label], expected)
# Test all labels at the same time
all_paths = set()
for paths in self._ninja_outputs.values():
all_paths.update(paths)
expected = "\n".join(sorted(all_paths)) + "\n"
self.assert_output(
["gn_label_to_ninja_paths"] + list(self._ninja_outputs.keys()),
expected,
)
# Test unknown GN label
self.assert_error(
["gn_label_to_ninja_paths", "//unknown:label"],
"ERROR: Unknown GN label (not in the configured graph): //unknown:label\n",
)
# Test unknown GN label
self.assert_output(
[
"gn_label_to_ninja_paths",
"--allow-unknown",
"unknown_path",
"unknown:label",
],
"unknown:label\nunknown_path\n",
)
# Test that labels are properly qualified before looking into the database.
self.assert_output(
[
"gn_label_to_ninja_paths",
"//bar(//build/toolchain/fuchsia:aRm64)",
],
"obj/bar.output\nobj/bar.stamp\n",
)
# Test that --allow_unknown does not pass unknown GN labels or absolute file paths.
self.assert_error(
[
"gn_label_to_ninja_paths",
"--allow-unknown",
"//unknown:label",
],
"ERROR: Unknown GN label (not in the configured graph): //unknown:label\n",
)
self.assert_error(
[
"gn_label_to_ninja_paths",
"--allow-unknown",
"/unknown/path",
],
"ERROR: Absolute path is not a valid GN label or Ninja path: /unknown/path\n",
)
def test_fx_build_args_to_labels(self):
_TEST_CASES = [
(["--args", "//aa"], ["//aa:aa"]),
(
["--args", "--host", "//foo/bar"],
["//foo/bar:bar(//build/toolchain:host_y64)"],
),
(["--args", "--fuchsia", "//:foo"], ["//:foo"]),
(
[
"--args",
"--host",
"//first",
"//second",
"--fuchsia",
"//third",
"//fourth",
"--fidl",
"//fifth",
],
[
"//first:first(//build/toolchain:host_y64)",
"//second:second(//build/toolchain:host_y64)",
"//third:third",
"//fourth:fourth",
"//fifth:fifth(//build/fidl:fidling)",
],
),
(
[
"--args",
"//unknown",
"//other:unknown",
],
["//unknown:unknown", "//other:unknown"],
),
]
for args, expected_list in _TEST_CASES:
expected_out = "\n".join(expected_list) + "\n"
self.assert_output(["fx_build_args_to_labels"] + args, expected_out)
_WARNING_CASES = [
(
[
"--args",
"host_y64/hammer",
],
["//tools:hammer(//build/toolchain:host_y64)"],
"WARNING: Use '--host //tools:hammer' instead of Ninja path 'host_y64/hammer'\n",
),
(
[
"--allow-targets",
"--args",
"hammer",
],
["//tools:hammer(//build/toolchain:host_y64)"],
"WARNING: Use '--host //tools:hammer' instead of Ninja target 'hammer'\n",
),
]
for args, expected_list, expected_err in _WARNING_CASES:
expected_out = "\n".join(expected_list) + "\n"
self.assert_output(
["fx_build_args_to_labels"] + args,
expected_out,
expected_err=expected_err,
expected_status=0,
)
_ERROR_CASES = [
(
[
"--args",
"host_y64/unknown",
],
"ERROR: Unknown Ninja path: host_y64/unknown\n",
),
(
[
"--allow-targets",
"--args",
"first_path",
"second/path",
],
"ERROR: Unknown Ninja target: first_path\n"
+ "ERROR: Unknown Ninja path: second/path\n",
),
]
self.maxDiff = 1000
for args, expected_err in _ERROR_CASES:
self.assert_error(
["fx_build_args_to_labels"] + args,
expected_err=expected_err,
)
def test_last_ninja_artifacts(self):
self._build_ninja_path.write_text(
"""
rule copy
command = cp -f $in $out
build out1: copy input1
build out2: copy out1
build out3: copy out1
build $:default: phony out1
build all: phony out1 out2 out3
"""
)
def assert_last_ninja_artifacts_output(expected):
self.assert_raw_outputs(
self.run_raw_client(
[
"--build-dir",
str(self._build_dir),
"--fuchsia-dir",
str(_SCRIPT_DIR.parent.parent.resolve()),
"last_ninja_artifacts",
]
),
expected,
)
# Verify that if the file doesn't exist, then the result should
# correspond to the :default target.
assert not self._last_targets_path.exists()
assert_last_ninja_artifacts_output("out1\n")
# Change the list of targets.
self._last_targets_path.write_text("all")
assert_last_ninja_artifacts_output("out1\nout2\nout3\n")
def test_export_last_build_debug_symbols(self):
self.maxDiff = None
self._build_ninja_path.write_text(
"""
rule whatever
command = ignored
build obj/src/foo/lib_shared/libfoo.so.unstripped obj/src/bar/binary.unstripped obj/src/zoo/binary.unstripped: whatever ../../prebuilt/.build-id/aa/bbbbbbbbbbb.debug
build $:default: phony obj/src/foo/lib_shared/libfoo.so.unstripped
"""
)
# Create a fake dump_syms tool that simply prints the path of
# the input debug symbol file.
dump_syms = self._top_dir / "dump_syms"
dump_syms.write_text(
f"""#!{sys.executable}
import argparse
parser = argparse.ArgumentParser()
parser.add_argument("-r", action="store_true")
parser.add_argument("-n")
parser.add_argument("-o")
parser.add_argument("debug_symbol_file")
args = parser.parse_args()
print(args.debug_symbol_file)
"""
)
dump_syms.chmod(0o755)
gsymutil = self._top_dir / "gsymutil"
gsymutil.write_text(
f"""#!{sys.executable}
import argparse
import os
parser = argparse.ArgumentParser()
parser.add_argument("--convert", required=True, help="Input debug binary")
parser.add_argument("--out-file", required=True, help="Output file path")
args = parser.parse_args()
os.makedirs(os.path.dirname(args.out_file), exist_ok=True)
with open(args.out_file, "wt") as f:
f.write(args.convert)
f.write("\\n")
"""
)
gsymutil.chmod(0o755)
export_dir = self._top_dir / "exported_debug_symbols"
expected_err = """MISSING build-id FOR {'cpu': 'x64', 'debug': 'obj/src/zoo/binary.unstripped', 'label': '//src/zoo:binary', 'os': 'fuchsia'}
"""
expected_out = f"""Creating {export_dir}/build-ids.json
Creating {export_dir}/build-ids.txt
Creating 3 symlinks in {export_dir}
Generating 3 breakpad symbols in {export_dir}
- Creating .build-id/00/000000000000001.sym FROM obj/src/foo/lib_shared/libfoo.so.unstripped
- Creating .build-id/aa/bbbbbbbbbbb.sym FROM ../../prebuilt/.build-id/aa/bbbbbbbbbbb.debug
- Creating .build-id/bu/ild_id_for_bar.sym FROM obj/src/bar/binary.unstripped
Generating 3 GSYM symbols in {export_dir}
- Creating .build-id/00/000000000000001.gsym FROM obj/src/foo/lib_shared/libfoo.so.unstripped
- Creating .build-id/aa/bbbbbbbbbbb.gsym FROM ../../prebuilt/.build-id/aa/bbbbbbbbbbb.debug
- Creating .build-id/bu/ild_id_for_bar.gsym FROM obj/src/bar/binary.unstripped
Done!
"""
self.assert_raw_outputs(
self.run_raw_client(
[
"--build-dir",
str(self._build_dir),
"--fuchsia-dir",
str(_SCRIPT_DIR.parent.parent.resolve()),
"export_last_build_debug_symbols",
f"--output-dir={export_dir}",
"--with-breakpad-symbols",
f"--dump_syms={dump_syms}",
"--with-gsym-symbols",
f"--gsymutil={gsymutil}",
]
),
expected_out,
expected_err,
)
if __name__ == "__main__":
unittest.main()