blob: 129f45717e5c10b168780ff50b91a3a34ebd4298 [file] [log] [blame]
#!/usr/bin/env python3
# Copyright 2022 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.
"""Run misc. tests to ensure Bazel support works as expected!.
By default, this will invoker prepare-fuchsia-checkout.py and do an
`fx clean` operation before anything else. Use --skip-prepare or
--skip-clean to skip these steps, which is useful when adding new
tests.
By default, command outputs is sent to a log file in your TMPDIR, whose
name is printed when this script starts. Use --log-file FILE to send them
to a specific file instead, --verbose to print everything on the current
terminal. Finally --quiet will remove normal outputs (but will not
disable logging, use `--log-file /dev/null` if this is really needed).
This script can be run on CQ, but keep in mind that this requires accessing
the network to download various prebuilts. This happens both during the
prepare-fuchsia-checkout.py step, as well as during the Bazel build
itself, due to the current state of the @fuchsia_workspace repository rules
being used (to be fixed in the future, of course).
"""
import argparse
import os
import shlex
import shutil
import stat
import subprocess
import sys
import tempfile
import json
_SCRIPT_DIR = os.path.abspath(os.path.dirname(__file__))
def get_fx_build_dir(fuchsia_dir):
"""Return the path to the Ninja build directory."""
fx_build_dir_path = os.path.join(fuchsia_dir, ".fx-build-dir")
build_dir = None
if os.path.exists(fx_build_dir_path):
with open(fx_build_dir_path) as f:
build_dir = f.read().strip()
if not build_dir:
build_dir = "out/default"
return os.path.join(fuchsia_dir, build_dir)
# The list of known product names that are supported from //build/assembly/BUILD.gn
# If the value returned by get_product_name() is not one of them, then fallback
# to _FALLBACK_PRODUCT_NAME
_KNOWN_PRODUCT_NAMES = ("bringup", "minimal", "core", "workstation_eng")
_FALLBACK_PRODUCT_NAME = "minimal"
def remove_directory(directory):
"""Safely remove a directory, even if it contains read-only files."""
# Error handler for rmtree call below, to deal with read-only files
# that need to be removed explicitly.
def on_error(action, path, excinfo):
# Make file writable, taking care of parent directories as well.
file_path = path
while True:
file_mode = os.stat(file_path).st_mode
if file_mode & stat.S_IWRITE != 0:
break
os.chmod(file_path, file_mode | stat.S_IWRITE)
file_path = os.path.dirname(file_path)
if file_path == "/":
break
os.remove(path)
shutil.rmtree(directory, onerror=on_error)
def get_product_name(fuchsia_build_dir):
with open(os.path.join(fuchsia_build_dir, "args.json")) as f:
args = json.load(f)
product_name = args["build_info_product"]
if product_name not in _KNOWN_PRODUCT_NAMES:
product_name = _FALLBACK_PRODUCT_NAME
return product_name
def get_bazel_relative_topdir(fuchsia_dir, workspace_name):
"""Return Bazel topdir for a given workspace, relative to Ninja output dir."""
input_file = os.path.join(
fuchsia_dir,
"build",
"bazel",
"config",
f"{workspace_name}_workspace_top_dir",
)
assert os.path.exists(input_file), "Missing input file: " + input_file
with open(input_file) as f:
return f.read().strip()
def get_host_bazel_platform():
"""Return host --platform name for the current build machine."""
sysname = os.uname().sysname
machine = os.uname().machine
host_os = {
"Linux": "linux",
"Darwin": "mac",
"Windows": "win",
}.get(sysname)
host_cpu = {
"aarch64": "arm64",
"x86_64": "x64",
}.get(machine, machine)
return f"//build/bazel/platforms:{host_os}_{host_cpu}"
class CommandLauncher(object):
"""Helper class used to launch external commands."""
def __init__(self, cwd, log_file, prefix_args=[]):
self._cwd = cwd
self._log_file = log_file
self._prefix_args = prefix_args
def run(self, args):
"""Run command, write output to logfile. Raise exception on error."""
subprocess.check_call(
self._prefix_args + args,
stdout=self._log_file,
stderr=subprocess.STDOUT,
cwd=self._cwd,
text=True,
)
def get_output(self, args):
"""Run command, return output as string, Raise exception on error."""
args = self._prefix_args + args
ret = subprocess.run(
args, capture_output=True, cwd=self._cwd, text=True
)
if ret.returncode != 0:
print(
"ERROR: Received returncode=%d when trying to run command\n %s\nError output:\n%s"
% (
ret.returncode,
" ".join(shlex.quote(arg) for arg in args),
ret.stderr,
),
file=sys.stderr,
)
ret.check_returncode()
return ret.stdout
def check_update_workspace_script(
fuchsia_dir, update_workspace_cmd, cmd_launcher
):
"""Check the behavior of the update_workspace.py script!"""
def get_update_output():
return cmd_launcher.get_output(update_workspace_cmd)
# Calling the update script a second time should not trigger an update.
out = get_update_output()
assert not out, "Unexpected workspace update!"
# Adding a top-level file entry to FUCHSIA_DIR should trigger an update.
touched_file = os.path.join(
fuchsia_dir, "touched-file-for-tests-please-remove"
)
with open(touched_file, "w") as f:
f.write("0")
try:
out = get_update_output()
assert out, "Expected workspace update after adding FUCHSIA_DIR file!"
# Then calling the script again should do nothing.
out = get_update_output()
assert (
not out
), "Unexpected workspace update after FUCHSIA_DIR addition!"
finally:
os.remove(touched_file)
# Removing a top-level file entry from FUCHSIA_DIR should trigger an update.
out = get_update_output()
assert out, "Expected workspace update after removing FUCHSIA_DIR file!"
# But not calling the script again after that.
out = get_update_output()
assert not out, "Unexpected workspace update after FUCHSIA_DIR removal!"
# Touching a BUILD.gn file should trigger an update (of the Ninja build plan).
os.utime(os.path.join(fuchsia_dir, "build", "bazel", "BUILD.gn"))
out = get_update_output()
assert out, "Expected workspace update after touching BUILD.gn file!"
def main():
parser = argparse.ArgumentParser(
description=__doc__,
formatter_class=argparse.RawDescriptionHelpFormatter,
)
parser.add_argument(
"--fuchsia-dir", help="Specify top-level Fuchsia directory."
)
parser.add_argument(
"--fuchsia-build-dir",
help="Specify build output directory (auto-detected by default).",
)
parser.add_argument(
"--verbose",
action="store_true",
help="Print everything to current terminal instead of logging to file.",
)
parser.add_argument("--log-file", help="Specify log file.")
parser.add_argument(
"--skip-prepare",
action="store_true",
help="Skip the inital checkout preparation step.",
)
parser.add_argument(
"--skip-clean",
action="store_true",
help="Skip the output directory cleanup step, implies --skip-prepare.",
)
parser.add_argument(
"--quiet", action="store_true", help="Reduce verbosity."
)
args = parser.parse_args()
if args.skip_clean:
args.skip_prepare = True
if args.verbose:
log_file = None
elif args.log_file:
log_file_path = args.log_file
log_file = open(log_file_path, "w")
else:
log_file = tempfile.NamedTemporaryFile(mode="w", delete=False)
log_file_path = log_file.name
if not args.fuchsia_dir:
# Assumes this is under //build/bazel/scripts/
args.fuchsia_dir = os.path.join(
os.path.dirname(__file__), "..", "..", ".."
)
fuchsia_dir = os.path.abspath(args.fuchsia_dir)
def log(message):
message = "[test-all] " + message
if log_file:
print(message, file=log_file, flush=True)
if not args.quiet:
print(message, flush=True)
if log_file:
log("Logging enabled, use: tail -f " + log_file_path)
log("Using Fuchsia root directory: " + fuchsia_dir)
build_dir = args.fuchsia_build_dir
if not build_dir:
build_dir = get_fx_build_dir(fuchsia_dir)
build_dir = os.path.abspath(build_dir)
log("Using build directory: " + build_dir)
if not os.path.exists(build_dir):
print(
"ERROR: Missing build directory, did you call `fx set`?: "
+ build_dir,
file=sys.stderr,
)
return 1
command_launcher = CommandLauncher(fuchsia_dir, log_file)
# Path to 'fx' script, relative to fuchsia_dir.
fx_script = "scripts/fx"
def run_fx_command(args):
command_launcher.run([fx_script] + args)
def run_bazel_command(args):
run_fx_command(["bazel"] + args)
def get_fx_command_output(args):
return command_launcher.get_output([fx_script] + args)
def get_bazel_command_output(args):
return get_fx_command_output(["bazel"] + args)
# Preparation step
if args.skip_prepare:
log("Skipping preparation step due to --skip-prepare.")
else:
log("Preparing Fuchsia checkout at: " + fuchsia_dir)
command_launcher.run(
[
os.path.join(_SCRIPT_DIR, "prepare-fuchsia-checkout.py"),
"--fuchsia-dir",
fuchsia_dir,
]
)
# Clean step
if args.skip_clean:
log("Skipping cleanup step due to --skip-clean.")
else:
log("Cleaning current output directory.")
run_fx_command(["clean"])
log("Regenerating GN outputs.")
run_fx_command(["gen"])
log("Generating bazel workspace and repositories.")
update_workspace_cmd = [
os.path.join(_SCRIPT_DIR, "update_workspace.py"),
"--fuchsia-dir",
fuchsia_dir,
]
command_launcher.run(update_workspace_cmd)
# Prepare bazel wrapper script invocations.
bazel_main_top_dir = os.path.join(
build_dir, get_bazel_relative_topdir(fuchsia_dir, "main")
)
bazel_script = os.path.join(bazel_main_top_dir, "bazel")
assert os.path.exists(bazel_script), (
"Bazel script does not exist: " + bazel_script
)
# Verify that adding or removinf files from FUCHSIA_DIR invokes a regeneration.
log("Checking behavior of update_workspace.py script.")
check_update_workspace_script(
fuchsia_dir, update_workspace_cmd, command_launcher
)
def check_test_query(name, path):
"""Run a Bazel query and verify its output.
Args:
name: Name of query check for the log.
path: Directory path, this must contain two files named
'test-query.patterns' and 'test-query.expected_output'.
The first one contains a list of Bazel query patterns
(one per line). The second one corresponds to the
expected output for the query.
Returns:
True on success, False on error (after displaying an error
message that shows the mismatched actual and expected outputs.
"""
with open(os.path.join(path, "test-query.patterns")) as f:
query_patterns = f.read().splitlines()
with open(os.path.join(path, "test-query.expected_output")) as f:
expected_output = f.read()
output = get_bazel_command_output(["query"] + query_patterns)
try:
output = get_bazel_command_output(["query"] + query_patterns)
except subprocess.CalledProcessError:
return False
if output != expected_output:
log("ERROR: Unexpectedoutput for %s query:" % name)
log(
"ACTUAL [[[\n%s\n]]] EXPECTED [[[\n%s\n]]]"
% (output, expected_output)
)
return False
return True
# NOTE: this requires Ninja outputs to be properly generated.
# See note in build/bazel/tests/bazel_inputs_resource_directory/BUILD.bazel
log("Generating @legacy_ninja_build_outputs repository")
run_fx_command(
["build", "-d", "explain", "build/bazel:legacy_ninja_build_outputs"]
)
log("bazel_inputs_resource_directory() check.")
if not check_test_query(
"bazel_input_resource_directory",
os.path.join(
args.fuchsia_dir, "build/bazel/tests/bazel_input_resource_directory"
),
):
return 1
log("Checking @com_google_googletest repository creation")
out = get_bazel_command_output(
["query", "@com_google_googletest//:BUILD.bazel"]
)
expected = "@com_google_googletest//:BUILD.bazel\n"
if out != expected:
print(
"ERROR: @com_google_googletest repository creation failed! got [%s] expected [%s]"
% (out, expected),
file=sys.stderr,
)
return 1
# This builds and runs a simple hello_world host binary using the
# @prebuilt_clang toolchain (which differs from sdk-integration's
# @fuchsia_clang that can only generate Fuchsia binaries so far.
log(
"Checking `fx bazel run --platforms=//build/bazel/platforms:host //build/bazel/examples/hello_world`."
)
run_bazel_command(
[
"run",
"--platforms=//build/bazel/platforms:host",
"//build/bazel/examples/hello_world",
]
)
# This creates a py_binary launcher script + runfiles directory, then calls it.
log(f"Checking `fx bazel run //build/bazel/examples/hello_python:bin`.")
run_bazel_command(["run", "//build/bazel/examples/hello_python:bin"])
# Run a few simple Starlark unit tests.
log("@prebuilt_clang test suite")
run_bazel_command(["test", "@prebuilt_clang//:test_suite"])
# Run the bazel_action() checks.
log("bazel_action() checks.")
command_launcher.run(["build/bazel/tests/build_action/run_tests.py"])
log("//build/bazel:generate_fuchsia_sdk_repository check")
output_base_dir = os.path.join(bazel_main_top_dir, "output_base")
remove_directory(output_base_dir)
fuchsia_sdk_symlink = os.path.join(bazel_main_top_dir, "fuchsia_sdk")
if os.path.exists(fuchsia_sdk_symlink):
os.unlink(fuchsia_sdk_symlink)
run_fx_command(["build", "build/bazel:generate_fuchsia_sdk_repository"])
if not os.path.exists(fuchsia_sdk_symlink):
print(
"ERROR: Missing symlink to @fuchsia_sdk repository: "
+ fuchsia_sdk_symlink,
file=sys.stderr,
)
return 1
if not os.path.islink(fuchsia_sdk_symlink):
print("ERROR: Not a symlink: " + fuchsia_sdk_symlink, file=sys.stderr)
return 1
api_version_path = os.path.join(fuchsia_sdk_symlink, "api_version.bzl")
if not os.path.exists(api_version_path):
print("ERROR: Missing @fuchsia_sdk file: " + api_version_path)
return 1
product_name = get_product_name(build_dir)
log(f"bazel platform assembly check for {product_name}.")
run_fx_command(
[
"bazel",
"build",
"--spawn_strategy=local",
f"//build/bazel/assembly:{product_name}",
]
)
log("Done!")
return 0
if __name__ == "__main__":
sys.exit(main())