| #!/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 @rules_fuchsia repository rules |
| being used (to be fixed in the future, of course). |
| """ |
| |
| import argparse |
| import os |
| import shlex |
| import shutil |
| 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 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_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_script = os.path.join(build_dir, 'gen', 'build', 'bazel', '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']) |
| |
| log('bazel_build_action() checks.') |
| # This verifies that bazel_build_action() works properly, i.e. that |
| # it invokes a Bazel build command that takes inputs from a |
| # @legacy_ninja_build_outputs filegroup(), which generates the |
| # expected output, copied to the appropriate Ninja output directory |
| # location. |
| run_fx_command(['build', 'build/bazel/tests/build_action']) |
| |
| log('//build/bazel:generate_fuchsia_sdk_repository check') |
| output_base_dir = os.path.join( |
| build_dir, 'gen', 'build', 'bazel', 'output_base') |
| shutil.rmtree(output_base_dir) |
| fuchsia_sdk_symlink = os.path.join( |
| build_dir, 'gen', 'build', 'bazel', '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()) |