| #!/usr/bin/env -S python3.8 -B |
| # Copyright 2020 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. |
| """ |
| Runs the GN SDK tests |
| usage: run.py [-h] [--proj_dir PROJ_DIR] [--out_dir OUT_DIR] |
| |
| optional arguments: |
| -h, --help show this help message and exit |
| --proj_dir PROJ_DIR Path to the test project directory |
| --out_dir OUT_DIR Path to the out directory |
| |
| PROJ_DIR defaults to the same directory run.py is contained in |
| OUT_DIR defaults to ./out/default relative to the run.py |
| """ |
| |
| from __future__ import print_function |
| |
| import argparse |
| import imp |
| import os |
| import platform |
| from subprocess import Popen, PIPE |
| import re |
| import sys |
| import unittest |
| |
| ARCHES = [ |
| 'arm64', |
| 'x64', |
| ] |
| |
| SCRIPT_DIR = os.path.dirname(os.path.abspath(__file__)) |
| DEFAULT_OUT_DIR = os.path.join(SCRIPT_DIR, 'out') |
| |
| FUCHSIA_ROOT = '${data.fuchsia_root}' |
| |
| # Import test_generate |
| TEST_GEN_PATH = os.path.join( |
| FUCHSIA_ROOT, 'scripts', 'sdk', 'gn', 'test_generate.py') |
| test_generate = imp.load_source('test_generate', TEST_GEN_PATH) |
| |
| # gn and ninja constants |
| DEFAULT_HOST = 'linux-x64' |
| PREBUILT_DIR = os.path.join(FUCHSIA_ROOT, 'prebuilt') |
| THIRD_PARTY_DIR = os.path.join(PREBUILT_DIR, 'third_party') |
| DEFAULT_GN_BIN = os.path.join(THIRD_PARTY_DIR, 'gn', DEFAULT_HOST, 'gn') |
| DEFAULT_NINJA_BIN = os.path.join( |
| THIRD_PARTY_DIR, 'ninja', DEFAULT_HOST, 'ninja') |
| DEFAULT_CLANG_DIR = os.path.join(THIRD_PARTY_DIR, 'clang', DEFAULT_HOST) |
| DEFAULT_BUILDIDTOOL_BIN = os.path.join(PREBUILT_DIR, 'tools', 'buildidtool', 'linux-x64', 'buildidtool') |
| |
| # host test constants |
| HOST_TEST_PATH = os.path.join(SCRIPT_DIR, 'tests', 'run-host-tests.sh') |
| |
| class GnTester(object): |
| """Class for GN SDK test setup, execution, and cleanup.""" |
| |
| def __init__(self, gn, ninja, clang, buildidtool, proj_dir, out_dir): |
| self._test_failed = False |
| # Paths for building sample project |
| self.gn = gn |
| self.ninja = ninja |
| self.clang = clang |
| self.buildidtool = buildidtool |
| self.proj_dir = proj_dir |
| self.out_dir = out_dir |
| self.continue_on_error = True |
| # Import gen_fidl_response_file_unittest |
| GEN_FIDL_RESP_FILE_TEST_PATH = os.path.join( |
| proj_dir, 'tests', 'gen_fidl_response_file_unittest.py') |
| self.gen_fidl_response_file_unittest = imp.load_source( |
| 'gen_fidl_response_file_unittest', GEN_FIDL_RESP_FILE_TEST_PATH) |
| |
| def _run_unit_test(self, test_module): |
| loader = unittest.TestLoader() |
| tests = loader.loadTestsFromModule(test_module) |
| suite = unittest.TestSuite() |
| suite.addTests(tests) |
| runner = unittest.TextTestRunner() |
| result = runner.run(suite) |
| if result.failures or result.errors: |
| raise AssertionError('Unit test failed.') |
| |
| def _generate_test(self): |
| """Test generate.py via python test class.""" |
| self._run_unit_test(test_generate) |
| print("Generate tests passed.") |
| |
| def _gen_fild_resp_file_unittest(self): |
| self._run_unit_test(self.gen_fidl_response_file_unittest) |
| print("FIDL response file unit test passed.") |
| |
| def _run_cmd(self, args, cwd=None): |
| job = Popen(args, cwd=cwd, stdout=PIPE, stderr=PIPE, text=True) |
| (stdoutdata, stderrdata) = job.communicate() |
| print(stdoutdata) |
| if job.returncode: |
| print(stderrdata) |
| msg = 'Command returned non-zero exit code: %s' % job.returncode |
| raise AssertionError(msg) |
| return (stdoutdata, stderrdata) |
| |
| def _run_test(self, test, *args): |
| try: |
| getattr(self, test)(*args) |
| except Exception as err: |
| print("ERROR Running test %s: %s" % (test, err)) |
| self._test_failed = True |
| if not self.continue_on_error: |
| raise err |
| |
| def _build_test_project(self, arch): |
| """Builds the test project for given architecture.""" |
| self._invoke_gn(arch) |
| self._invoke_ninja(arch, explain=False) |
| print("Test project for %s built successfully" % arch) |
| |
| def _invoke_gn(self, arch, additional_args=""): |
| """Invokes GN targeting the given architecture. |
| |
| Example invocation: |
| "gn" gen out --args='target_os="fuchsia" target_cpu="arm64"' |
| """ |
| # Invoke the gn binary and "gen" command e.g. `gn gen` |
| invocation = [ |
| self.gn, "gen", |
| os.path.join(self.out_dir, arch), |
| "--args=target_cpu=\"{cpu}\" target_os=\"fuchsia\" clang_base_path=\"{clang}\" buildidtool=\"{buildidtool}\" fuchsia_sdk_readelf_exec=\"{clang}/bin/llvm-readelf\" {additional_args}" |
| .format( |
| cpu=arch, clang=self.clang, buildidtool=self.buildidtool, additional_args=additional_args) |
| ] |
| # invoke command |
| print('Running gn gen: "%s"' % ' '.join(invocation)) |
| self._run_cmd(invocation, cwd=self.proj_dir) |
| |
| def _invoke_ninja(self, arch, explain=True, targets=["default", "tests"]): |
| """Invokes Ninja to build default and tests targets. |
| |
| Args: |
| arch: The target architecture to build. |
| |
| Returns: |
| tuple of (stdout, stderr) |
| """ |
| invocation = [ |
| # Invoke the ninja binary |
| self.ninja, |
| # Tell ninja to keep depfiles around as some tests want to inspect |
| # them. |
| "-d", |
| "keepdepfile", |
| ] |
| if explain: |
| invocation += [ |
| "-d", |
| "explain", |
| ] |
| invocation += [ |
| # Add Ninja flag to command e.g. `-C` |
| "-C", |
| # Add output directory to command |
| os.path.join(self.out_dir, arch) |
| ] + targets |
| print('Running ninja: "%s"' % ' '.join(invocation)) |
| return self._run_cmd(invocation, cwd=self.proj_dir) |
| |
| def _verify_package_depfile(self, arch): |
| print('Running package dep file verification test') |
| # Build test project |
| self._build_test_project(arch) |
| |
| # Verify package dep file for built project |
| expected = { |
| '../../tests/package/file with spaces.txt', |
| 'gen/tests/package/package/test_component_renamed.cm', |
| 'gen/tests/package/package/original.cm', 'lib/libfdio.so', |
| 'lib/libunwind.so.1', 'lib/libc++abi.so.1', 'hello_bin', |
| 'lib/ld.so.1', 'lib/libc++.so.2' |
| } |
| |
| dep_filepath = os.path.join( |
| self.out_dir, arch, "gen", "tests", "package", "package_stamp.d") |
| with open(dep_filepath, 'r') as dep_file: |
| dep_file_contents = dep_file.read() |
| parts = dep_file_contents.split(':') |
| if len(parts) != 2: |
| raise AssertionError( |
| 'Expected file: deps, but got %s' % dep_file_contents) |
| split_parts = (p.replace('\\ ', ' ') for p in re.split(r'(?<!\\) ', parts[1])) |
| actual = {p.strip() for p in split_parts if p.strip()} |
| missing = expected - actual |
| unexpected = actual - expected |
| |
| if len(missing) > 0: |
| raise AssertionError( |
| 'missing dependencies: %s in actual %s' % (missing, actual)) |
| if len(unexpected) > 0: |
| raise AssertionError( |
| 'unexpected dependencies: %s from expected %s' % |
| (unexpected, expected)) |
| |
| def _verify_rebuild_noop(self, arch): |
| """Builds each architecture twice, confirming the second is a noop.""" |
| # Build test project initially |
| self._build_test_project(arch) |
| # Build test project a second time |
| self._invoke_gn(arch) |
| (stdout, stderr) = self._invoke_ninja(arch) |
| # Verify that the second test project build is a noop for ninja |
| ninja_no_work_string = 'ninja: no work to do.' |
| if not (ninja_no_work_string in stdout or ninja_no_work_string in stderr): |
| msg = 'Rebuild of test project did not result in noop.\n' |
| msg += 'Expected std out to contain "%s" but got:\n\n' % ninja_no_work_string |
| msg += '"stdout:\n%s\nstderr:\n%s"' % (stdout, stderr) |
| raise AssertionError(msg) |
| print("Test project rebuilt successfully") |
| |
| def _verify_component_override(self, arch): |
| """Checks that changing BUILD.gn properties of a component template triggers rebuild.""" |
| # Build test project initially |
| self._build_test_project(arch) |
| self._host_tests(arch) |
| # Build test project a second time but with the rename flag. |
| self._invoke_gn(arch, additional_args="do_rename_test=true") |
| (stdout, stderr) = self._invoke_ninja(arch, explain=False) |
| self._host_tests(arch) |
| |
| def _verify_cml_touch(self, arch): |
| """Touches the original.cml file and confirms it is detected as dirty.""" |
| # Build test project initially |
| self._build_test_project(arch) |
| |
| # Touch the file |
| fname = os.path.join( |
| SCRIPT_DIR, "tests", "package", "meta", "original.cml") |
| if os.path.exists(fname): |
| self._run_cmd(['touch', fname]) |
| else: |
| raise AssertionError("File not found: %s" % fname) |
| |
| # Build test project a second time |
| (stdout, stderr) = self._invoke_ninja(arch) |
| # Verify that the manifest is copied. |
| ninja_copy_string = 'older than most recent input ../../tests/package/meta/original.cml' |
| if not ninja_copy_string in stderr: |
| msg = 'Touching //tests/package/meta/original.cml did not trigger rebuild.\n' |
| msg += 'Expected std out to contain "%s" but got:\n\n' % ninja_copy_string |
| msg += '"%s"' % stdout |
| raise AssertionError(msg) |
| |
| # Verify touching the manifest source *and* overriding the name |
| self._invoke_gn(arch, additional_args="do_rename_test=true") |
| self._invoke_ninja(arch, explain=False) |
| self._host_tests(arch) |
| |
| if os.path.exists(fname): |
| self._run_cmd(['touch', fname]) |
| else: |
| raise AssertionError("File not found: %s" % fname) |
| |
| # Build test project a second time |
| (stdout, stderr) = self._invoke_ninja(arch) |
| # Verify that the manifest is copied. |
| ninja_copy_string = 'older than most recent input ../../tests/package/meta/original.cml' |
| if not ninja_copy_string in stderr: |
| msg = 'Touching //tests/package/meta/original.cml did not trigger rebuild.\n' |
| msg += 'Expected std out to contain "%s" but got:\n\n' % ninja_copy_string |
| msg += '"%s"' % stdout |
| raise AssertionError(msg) |
| |
| def _verify_invalid_cml(self, arch): |
| """Run gn gen and then run ninja and expect it to fail.""" |
| self._invoke_gn(arch) |
| try: |
| (stdout, stderr) = self._invoke_ninja( |
| arch, explain=False, targets=['invalid_cml_test']) |
| except AssertionError: |
| return |
| raise AssertionError( |
| "Expected build error on cml validation, but no exception caught") |
| |
| def _run_build_tests(self): |
| """Run the build tests, once per architecture.""" |
| for arch in ARCHES: |
| self._run_test("_verify_invalid_cml", arch) |
| self._run_test("_build_test_project", arch) |
| self._run_test("_host_tests", arch) |
| self._run_test("_verify_package_depfile", arch) |
| self._run_test("_verify_rebuild_noop", arch) |
| self._run_test("_verify_component_override", arch) |
| self._run_test("_verify_cml_touch", arch) |
| |
| def _host_tests(self, arch): |
| invocation = [ |
| HOST_TEST_PATH, |
| os.path.join("out", arch, 'all_host_tests.txt') |
| ] |
| print('Running run-host-tests.sh: "%s"' % ' '.join(invocation)) |
| self._run_cmd(invocation, cwd=self.proj_dir) |
| |
| def run(self): |
| self._run_test("_generate_test") |
| self._run_test("_gen_fild_resp_file_unittest") |
| if platform.system() == 'Darwin': |
| print("The GN SDK does not support building on macOS.") |
| else: |
| self._run_build_tests() |
| return self._test_failed |
| |
| |
| def main(): |
| parser = argparse.ArgumentParser(description='Runs the GN SDK tests') |
| parser.add_argument( |
| '--gn', |
| help='Path to the GN tool; defaults to %s' % DEFAULT_GN_BIN, |
| default=DEFAULT_GN_BIN) |
| parser.add_argument( |
| '--ninja', |
| help='Path to the Ninja tool; otherwise found in PATH', |
| default=DEFAULT_NINJA_BIN) |
| parser.add_argument( |
| '--clang', |
| help='Path to Clang base path, defaults to %s' % DEFAULT_CLANG_DIR, |
| default=os.path.join(SCRIPT_DIR, DEFAULT_CLANG_DIR)) |
| parser.add_argument( |
| '--buildidtool', |
| help='Path to buildidtool, defaults to %s' % DEFAULT_BUILDIDTOOL_BIN, |
| default=DEFAULT_BUILDIDTOOL_BIN) |
| parser.add_argument( |
| '--proj-dir', |
| help='Path to the test project directory, defaults to %s' % SCRIPT_DIR, |
| default=SCRIPT_DIR) |
| parser.add_argument( |
| '--out-dir', |
| help='Path to the out directory, defaults to %s' % DEFAULT_OUT_DIR, |
| default=DEFAULT_OUT_DIR) |
| parser.add_argument( |
| '--quit-on-error', help='Quit when test fails', action='store_true') |
| args = parser.parse_args() |
| tester = GnTester( |
| gn=args.gn, |
| ninja=args.ninja, |
| clang=args.clang, |
| buildidtool=args.buildidtool, |
| proj_dir=args.proj_dir, |
| out_dir=args.out_dir, |
| ) |
| |
| tester.continue_on_error = not args.quit_on_error |
| if tester.run(): |
| print("ERROR: One or more tests failed.") |
| return 1 |
| print("SUCCESS: All tests passed.") |
| return 0 |
| |
| |
| if __name__ == '__main__': |
| sys.exit(main()) |