blob: 7043cee22171adb0751a7ab82ddb62f782530874 [file] [log] [blame]
#!/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())