mirror of
https://github.com/rust-lang/rust.git
synced 2026-04-27 18:57:42 +03:00
Adding Fuchsia compiler testing script, docs
This commit is contained in:
@@ -0,0 +1,1041 @@
|
||||
#!/usr/bin/env python3
|
||||
|
||||
"""
|
||||
The Rust toolchain test runner for Fuchsia.
|
||||
|
||||
For instructions on running the compiler test suite, see
|
||||
https://doc.rust-lang.org/stable/rustc/platform-support/fuchsia.html#aarch64-fuchsia-and-x86_64-fuchsia
|
||||
"""
|
||||
|
||||
import argparse
|
||||
from dataclasses import dataclass
|
||||
import glob
|
||||
import hashlib
|
||||
import json
|
||||
import os
|
||||
import platform
|
||||
import re
|
||||
import shutil
|
||||
import signal
|
||||
import subprocess
|
||||
import sys
|
||||
from typing import ClassVar, List
|
||||
|
||||
|
||||
@dataclass
|
||||
class TestEnvironment:
|
||||
rust_dir: str
|
||||
sdk_dir: str
|
||||
target_arch: str
|
||||
package_server_pid: int = None
|
||||
emu_addr: str = None
|
||||
libstd_name: str = None
|
||||
libtest_name: str = None
|
||||
verbose: bool = False
|
||||
|
||||
@staticmethod
|
||||
def tmp_dir():
|
||||
tmp_dir = os.environ.get("TEST_TOOLCHAIN_TMP_DIR")
|
||||
if tmp_dir is not None:
|
||||
return os.path.abspath(tmp_dir)
|
||||
return os.path.join(os.path.dirname(__file__), "tmp~")
|
||||
|
||||
@classmethod
|
||||
def env_file_path(cls):
|
||||
return os.path.join(cls.tmp_dir(), "test_env.json")
|
||||
|
||||
@classmethod
|
||||
def from_args(cls, args):
|
||||
return cls(
|
||||
os.path.abspath(args.rust),
|
||||
os.path.abspath(args.sdk),
|
||||
args.target_arch,
|
||||
verbose=args.verbose,
|
||||
)
|
||||
|
||||
@classmethod
|
||||
def read_from_file(cls):
|
||||
with open(cls.env_file_path(), encoding="utf-8") as f:
|
||||
test_env = json.loads(f.read())
|
||||
return cls(
|
||||
test_env["rust_dir"],
|
||||
test_env["sdk_dir"],
|
||||
test_env["target_arch"],
|
||||
libstd_name=test_env["libstd_name"],
|
||||
libtest_name=test_env["libtest_name"],
|
||||
emu_addr=test_env["emu_addr"],
|
||||
package_server_pid=test_env["package_server_pid"],
|
||||
verbose=test_env["verbose"],
|
||||
)
|
||||
|
||||
def image_name(self):
|
||||
if self.target_arch == "x64":
|
||||
return "qemu-x64"
|
||||
if self.target_arch == "arm64":
|
||||
return "qemu-arm64"
|
||||
raise Exception(f"Unrecognized target architecture {self.target_arch}")
|
||||
|
||||
def write_to_file(self):
|
||||
with open(self.env_file_path(), "w", encoding="utf-8") as f:
|
||||
f.write(json.dumps(self.__dict__))
|
||||
|
||||
def ssh_dir(self):
|
||||
return os.path.join(self.tmp_dir(), "ssh")
|
||||
|
||||
def ssh_keyfile_path(self):
|
||||
return os.path.join(self.ssh_dir(), "fuchsia_ed25519")
|
||||
|
||||
def ssh_authfile_path(self):
|
||||
return os.path.join(self.ssh_dir(), "fuchsia_authorized_keys")
|
||||
|
||||
def vdl_output_path(self):
|
||||
return os.path.join(self.tmp_dir(), "vdl_output")
|
||||
|
||||
def package_server_log_path(self):
|
||||
return os.path.join(self.tmp_dir(), "package_server_log")
|
||||
|
||||
def emulator_log_path(self):
|
||||
return os.path.join(self.tmp_dir(), "emulator_log")
|
||||
|
||||
def packages_dir(self):
|
||||
return os.path.join(self.tmp_dir(), "packages")
|
||||
|
||||
def output_dir(self):
|
||||
return os.path.join(self.tmp_dir(), "output")
|
||||
|
||||
TEST_REPO_NAME: ClassVar[str] = "rust-testing"
|
||||
|
||||
def repo_dir(self):
|
||||
return os.path.join(self.tmp_dir(), self.TEST_REPO_NAME)
|
||||
|
||||
def rustlib_dir(self):
|
||||
if self.target_arch == "x64":
|
||||
return "x86_64-fuchsia"
|
||||
if self.target_arch == "arm64":
|
||||
return "aarch64-fuchsia"
|
||||
raise Exception(f"Unrecognized target architecture {self.target_arch}")
|
||||
|
||||
def libs_dir(self):
|
||||
return os.path.join(
|
||||
self.rust_dir,
|
||||
"lib",
|
||||
)
|
||||
|
||||
def rustlibs_dir(self):
|
||||
return os.path.join(
|
||||
self.libs_dir(),
|
||||
"rustlib",
|
||||
self.rustlib_dir(),
|
||||
"lib",
|
||||
)
|
||||
|
||||
def sdk_arch(self):
|
||||
machine = platform.machine()
|
||||
if machine == "x86_64":
|
||||
return "x64"
|
||||
if machine == "arm":
|
||||
return "a64"
|
||||
raise Exception(f"Unrecognized host architecture {machine}")
|
||||
|
||||
def tool_path(self, tool):
|
||||
return os.path.join(self.sdk_dir, "tools", self.sdk_arch(), tool)
|
||||
|
||||
def host_arch_triple(self):
|
||||
machine = platform.machine()
|
||||
if machine == "x86_64":
|
||||
return "x86_64-unknown-linux-gnu"
|
||||
if machine == "arm":
|
||||
return "aarch64-unknown-linux-gnu"
|
||||
raise Exception(f"Unrecognized host architecture {machine}")
|
||||
|
||||
def zxdb_script_path(self):
|
||||
return os.path.join(self.tmp_dir(), "zxdb_script")
|
||||
|
||||
def log_info(self, msg):
|
||||
print(msg)
|
||||
|
||||
def log_debug(self, msg):
|
||||
if self.verbose:
|
||||
print(msg)
|
||||
|
||||
def subprocess_output(self):
|
||||
if self.verbose:
|
||||
return sys.stdout
|
||||
return subprocess.DEVNULL
|
||||
|
||||
def ffx_daemon_log_path(self):
|
||||
return os.path.join(self.tmp_dir(), "ffx_daemon_log")
|
||||
|
||||
def ffx_isolate_dir(self):
|
||||
return os.path.join(self.tmp_dir(), "ffx_isolate")
|
||||
|
||||
def ffx_home_dir(self):
|
||||
return os.path.join(self.ffx_isolate_dir(), "user-home")
|
||||
|
||||
def ffx_tmp_dir(self):
|
||||
return os.path.join(self.ffx_isolate_dir(), "tmp")
|
||||
|
||||
def ffx_log_dir(self):
|
||||
return os.path.join(self.ffx_isolate_dir(), "log")
|
||||
|
||||
def ffx_user_config_dir(self):
|
||||
return os.path.join(self.ffx_xdg_config_home(), "Fuchsia", "ffx", "config")
|
||||
|
||||
def ffx_user_config_path(self):
|
||||
return os.path.join(self.ffx_user_config_dir(), "config.json")
|
||||
|
||||
def ffx_xdg_config_home(self):
|
||||
if platform.system() == "Darwin":
|
||||
return os.path.join(self.ffx_home_dir(), "Library", "Preferences")
|
||||
return os.path.join(self.ffx_home_dir(), ".local", "share")
|
||||
|
||||
def ffx_ascendd_path(self):
|
||||
return os.path.join(self.ffx_tmp_dir(), "ascendd")
|
||||
|
||||
def start_ffx_isolation(self):
|
||||
# Most of this is translated directly from ffx's isolate library
|
||||
os.mkdir(self.ffx_isolate_dir())
|
||||
os.mkdir(self.ffx_home_dir())
|
||||
os.mkdir(self.ffx_tmp_dir())
|
||||
os.mkdir(self.ffx_log_dir())
|
||||
|
||||
fuchsia_dir = os.path.join(self.ffx_home_dir(), ".fuchsia")
|
||||
os.mkdir(fuchsia_dir)
|
||||
|
||||
fuchsia_debug_dir = os.path.join(fuchsia_dir, "debug")
|
||||
os.mkdir(fuchsia_debug_dir)
|
||||
|
||||
metrics_dir = os.path.join(fuchsia_dir, "metrics")
|
||||
os.mkdir(metrics_dir)
|
||||
|
||||
analytics_path = os.path.join(metrics_dir, "analytics-status")
|
||||
with open(analytics_path, "w", encoding="utf-8") as analytics_file:
|
||||
print("0", file=analytics_file)
|
||||
|
||||
ffx_path = os.path.join(metrics_dir, "ffx")
|
||||
with open(ffx_path, "w", encoding="utf-8") as ffx_file:
|
||||
print("1", file=ffx_file)
|
||||
|
||||
os.makedirs(self.ffx_user_config_dir())
|
||||
|
||||
with open(
|
||||
self.ffx_user_config_path(), "w", encoding="utf-8"
|
||||
) as config_json_file:
|
||||
user_config_for_test = {
|
||||
"log": {
|
||||
"enabled": True,
|
||||
"dir": self.ffx_log_dir(),
|
||||
},
|
||||
"overnet": {
|
||||
"socket": self.ffx_ascendd_path(),
|
||||
},
|
||||
"ssh": {
|
||||
"pub": self.ssh_authfile_path(),
|
||||
"priv": self.ssh_keyfile_path(),
|
||||
},
|
||||
"test": {
|
||||
"is_isolated": True,
|
||||
"experimental_structured_output": True,
|
||||
},
|
||||
}
|
||||
print(json.dumps(user_config_for_test), file=config_json_file)
|
||||
|
||||
ffx_env_path = os.path.join(self.ffx_user_config_dir(), ".ffx_env")
|
||||
with open(ffx_env_path, "w", encoding="utf-8") as ffx_env_file:
|
||||
ffx_env_config_for_test = {
|
||||
"user": self.ffx_user_config_path(),
|
||||
"build": None,
|
||||
"global": None,
|
||||
}
|
||||
print(json.dumps(ffx_env_config_for_test), file=ffx_env_file)
|
||||
|
||||
# Start ffx daemon
|
||||
# We want this to be a long-running process that persists after the script finishes
|
||||
# pylint: disable=consider-using-with
|
||||
with open(
|
||||
self.ffx_daemon_log_path(), "w", encoding="utf-8"
|
||||
) as ffx_daemon_log_file:
|
||||
subprocess.Popen(
|
||||
[
|
||||
self.tool_path("ffx"),
|
||||
"--config",
|
||||
self.ffx_user_config_path(),
|
||||
"daemon",
|
||||
"start",
|
||||
],
|
||||
env=self.ffx_cmd_env(),
|
||||
stdout=ffx_daemon_log_file,
|
||||
stderr=ffx_daemon_log_file,
|
||||
)
|
||||
|
||||
def ffx_cmd_env(self):
|
||||
result = {
|
||||
"HOME": self.ffx_home_dir(),
|
||||
"XDG_CONFIG_HOME": self.ffx_xdg_config_home(),
|
||||
"ASCENDD": self.ffx_ascendd_path(),
|
||||
"FUCHSIA_SSH_KEY": self.ssh_keyfile_path(),
|
||||
# We want to use our own specified temp directory
|
||||
"TMP": self.tmp_dir(),
|
||||
"TEMP": self.tmp_dir(),
|
||||
"TMPDIR": self.tmp_dir(),
|
||||
"TEMPDIR": self.tmp_dir(),
|
||||
}
|
||||
|
||||
return result
|
||||
|
||||
def stop_ffx_isolation(self):
|
||||
subprocess.check_call(
|
||||
[
|
||||
self.tool_path("ffx"),
|
||||
"--config",
|
||||
self.ffx_user_config_path(),
|
||||
"daemon",
|
||||
"stop",
|
||||
],
|
||||
env=self.ffx_cmd_env(),
|
||||
stdout=self.subprocess_output(),
|
||||
stderr=self.subprocess_output(),
|
||||
)
|
||||
|
||||
def start(self):
|
||||
"""Sets up the testing environment and prepares to run tests.
|
||||
|
||||
Args:
|
||||
args: The command-line arguments to this command.
|
||||
|
||||
During setup, this function will:
|
||||
- Locate necessary shared libraries
|
||||
- Create a new temp directory (this is where all temporary files are stored)
|
||||
- Start an emulator
|
||||
- Start an update server
|
||||
- Create a new package repo and register it with the emulator
|
||||
- Write test environment settings to a temporary file
|
||||
"""
|
||||
|
||||
# Initialize temp directory
|
||||
if not os.path.exists(self.tmp_dir()):
|
||||
os.mkdir(self.tmp_dir())
|
||||
elif len(os.listdir(self.tmp_dir())) != 0:
|
||||
raise Exception(f"Temp directory is not clean (in {self.tmp_dir()})")
|
||||
|
||||
os.mkdir(self.ssh_dir())
|
||||
os.mkdir(self.output_dir())
|
||||
|
||||
# Find libstd and libtest
|
||||
libstd_paths = glob.glob(os.path.join(self.rustlibs_dir(), "libstd-*.so"))
|
||||
libtest_paths = glob.glob(os.path.join(self.rustlibs_dir(), "libtest-*.so"))
|
||||
|
||||
if not libstd_paths:
|
||||
raise Exception(f"Failed to locate libstd (in {self.rustlibs_dir()})")
|
||||
|
||||
if not libtest_paths:
|
||||
raise Exception(f"Failed to locate libtest (in {self.rustlibs_dir()})")
|
||||
|
||||
self.libstd_name = os.path.basename(libstd_paths[0])
|
||||
self.libtest_name = os.path.basename(libtest_paths[0])
|
||||
|
||||
# Generate SSH keys for the emulator to use
|
||||
self.log_info("Generating SSH keys...")
|
||||
subprocess.check_call(
|
||||
[
|
||||
"ssh-keygen",
|
||||
"-N",
|
||||
"",
|
||||
"-t",
|
||||
"ed25519",
|
||||
"-f",
|
||||
self.ssh_keyfile_path(),
|
||||
"-C",
|
||||
"Generated by test_toolchain.py",
|
||||
],
|
||||
stdout=self.subprocess_output(),
|
||||
stderr=self.subprocess_output(),
|
||||
)
|
||||
authfile_contents = subprocess.check_output(
|
||||
[
|
||||
"ssh-keygen",
|
||||
"-y",
|
||||
"-f",
|
||||
self.ssh_keyfile_path(),
|
||||
],
|
||||
stderr=self.subprocess_output(),
|
||||
)
|
||||
with open(self.ssh_authfile_path(), "wb") as authfile:
|
||||
authfile.write(authfile_contents)
|
||||
|
||||
# Start ffx isolation
|
||||
self.log_info("Starting ffx isolation...")
|
||||
self.start_ffx_isolation()
|
||||
|
||||
# Start emulator (this will generate the vdl output)
|
||||
self.log_info("Starting emulator...")
|
||||
subprocess.check_call(
|
||||
[
|
||||
self.tool_path("fvdl"),
|
||||
"--sdk",
|
||||
"start",
|
||||
"--tuntap",
|
||||
"--headless",
|
||||
"--nointeractive",
|
||||
"--ssh",
|
||||
self.ssh_dir(),
|
||||
"--vdl-output",
|
||||
self.vdl_output_path(),
|
||||
"--emulator-log",
|
||||
self.emulator_log_path(),
|
||||
"--image-name",
|
||||
self.image_name(),
|
||||
],
|
||||
stdout=self.subprocess_output(),
|
||||
stderr=self.subprocess_output(),
|
||||
)
|
||||
|
||||
# Parse vdl output for relevant information
|
||||
with open(self.vdl_output_path(), encoding="utf-8") as f:
|
||||
vdl_content = f.read()
|
||||
matches = re.search(
|
||||
r'network_address:\s+"\[([0-9a-f]{1,4}:(:[0-9a-f]{1,4}){4}%qemu)\]"',
|
||||
vdl_content,
|
||||
)
|
||||
self.emu_addr = matches.group(1)
|
||||
|
||||
# Create new package repo
|
||||
self.log_info("Creating package repo...")
|
||||
subprocess.check_call(
|
||||
[
|
||||
self.tool_path("pm"),
|
||||
"newrepo",
|
||||
"-repo",
|
||||
self.repo_dir(),
|
||||
],
|
||||
stdout=self.subprocess_output(),
|
||||
stderr=self.subprocess_output(),
|
||||
)
|
||||
|
||||
# Start package server
|
||||
self.log_info("Starting package server...")
|
||||
with open(
|
||||
self.package_server_log_path(), "w", encoding="utf-8"
|
||||
) as package_server_log:
|
||||
# We want this to be a long-running process that persists after the script finishes
|
||||
# pylint: disable=consider-using-with
|
||||
self.package_server_pid = subprocess.Popen(
|
||||
[
|
||||
self.tool_path("pm"),
|
||||
"serve",
|
||||
"-vt",
|
||||
"-repo",
|
||||
self.repo_dir(),
|
||||
"-l",
|
||||
":8084",
|
||||
],
|
||||
stdout=package_server_log,
|
||||
stderr=package_server_log,
|
||||
).pid
|
||||
|
||||
# Register package server with emulator
|
||||
self.log_info("Registering package server...")
|
||||
ssh_client = subprocess.check_output(
|
||||
[
|
||||
"ssh",
|
||||
"-i",
|
||||
self.ssh_keyfile_path(),
|
||||
"-o",
|
||||
"StrictHostKeyChecking=accept-new",
|
||||
self.emu_addr,
|
||||
"-f",
|
||||
"echo $SSH_CLIENT",
|
||||
],
|
||||
text=True,
|
||||
)
|
||||
repo_addr = ssh_client.split()[0].replace("%", "%25")
|
||||
repo_url = f"http://[{repo_addr}]:8084/config.json"
|
||||
subprocess.check_call(
|
||||
[
|
||||
"ssh",
|
||||
"-i",
|
||||
self.ssh_keyfile_path(),
|
||||
"-o",
|
||||
"StrictHostKeyChecking=accept-new",
|
||||
self.emu_addr,
|
||||
"-f",
|
||||
f"pkgctl repo add url -f 1 -n {self.TEST_REPO_NAME} {repo_url}",
|
||||
],
|
||||
stdout=self.subprocess_output(),
|
||||
stderr=self.subprocess_output(),
|
||||
)
|
||||
|
||||
# Write to file
|
||||
self.write_to_file()
|
||||
|
||||
self.log_info("Success! Your environment is ready to run tests.")
|
||||
|
||||
# FIXME: shardify this
|
||||
# `facet` statement required for TCP testing via
|
||||
# protocol `fuchsia.posix.socket.Provider`. See
|
||||
# https://fuchsia.dev/fuchsia-src/development/testing/components/test_runner_framework?hl=en#legacy_non-hermetic_tests
|
||||
CML_TEMPLATE: ClassVar[
|
||||
str
|
||||
] = """
|
||||
{{
|
||||
program: {{
|
||||
runner: "elf_test_runner",
|
||||
binary: "bin/{exe_name}",
|
||||
forward_stderr_to: "log",
|
||||
forward_stdout_to: "log",
|
||||
environ: [{env_vars}
|
||||
]
|
||||
}},
|
||||
capabilities: [
|
||||
{{ protocol: "fuchsia.test.Suite" }},
|
||||
],
|
||||
expose: [
|
||||
{{
|
||||
protocol: "fuchsia.test.Suite",
|
||||
from: "self",
|
||||
}},
|
||||
],
|
||||
use: [
|
||||
{{ storage: "data", path: "/data" }},
|
||||
{{ protocol: [ "fuchsia.process.Launcher" ] }},
|
||||
{{ protocol: [ "fuchsia.posix.socket.Provider" ] }}
|
||||
],
|
||||
facets: {{
|
||||
"fuchsia.test": {{ type: "system" }},
|
||||
}},
|
||||
}}
|
||||
"""
|
||||
|
||||
MANIFEST_TEMPLATE = """
|
||||
meta/package={package_dir}/meta/package
|
||||
meta/{package_name}.cm={package_dir}/meta/{package_name}.cm
|
||||
bin/{exe_name}={bin_path}
|
||||
lib/{libstd_name}={rust_dir}/lib/rustlib/{rustlib_dir}/lib/{libstd_name}
|
||||
lib/{libtest_name}={rust_dir}/lib/rustlib/{rustlib_dir}/lib/{libtest_name}
|
||||
lib/ld.so.1={sdk_dir}/arch/{target_arch}/sysroot/lib/libc.so
|
||||
lib/libzircon.so={sdk_dir}/arch/{target_arch}/sysroot/lib/libzircon.so
|
||||
lib/libfdio.so={sdk_dir}/arch/{target_arch}/lib/libfdio.so
|
||||
"""
|
||||
|
||||
TEST_ENV_VARS: ClassVar[List[str]] = [
|
||||
"TEST_EXEC_ENV",
|
||||
"RUST_MIN_STACK",
|
||||
"RUST_BACKTRACE",
|
||||
"RUST_NEWRT",
|
||||
"RUST_LOG",
|
||||
"RUST_TEST_THREADS",
|
||||
]
|
||||
|
||||
def run(self, args):
|
||||
"""Runs the requested test in the testing environment.
|
||||
|
||||
Args:
|
||||
args: The command-line arguments to this command.
|
||||
Returns:
|
||||
The return code of the test (0 for success, else failure).
|
||||
|
||||
To run a test, this function will:
|
||||
- Create, compile, archive, and publish a test package
|
||||
- Run the test package on the emulator
|
||||
- Forward the test's stdout and stderr as this script's stdout and stderr
|
||||
"""
|
||||
|
||||
bin_path = os.path.abspath(args.bin_path)
|
||||
|
||||
# Build a unique, deterministic name for the test using the name of the
|
||||
# binary and the last 6 hex digits of the hash of the full path
|
||||
def path_checksum(path):
|
||||
m = hashlib.sha256()
|
||||
m.update(path.encode("utf-8"))
|
||||
return m.hexdigest()[0:6]
|
||||
|
||||
base_name = os.path.basename(os.path.dirname(args.bin_path))
|
||||
exe_name = base_name.lower().replace(".", "_")
|
||||
package_name = f"{exe_name}_{path_checksum(bin_path)}"
|
||||
|
||||
package_dir = os.path.join(self.packages_dir(), package_name)
|
||||
cml_path = os.path.join(package_dir, "meta", f"{package_name}.cml")
|
||||
cm_path = os.path.join(package_dir, "meta", f"{package_name}.cm")
|
||||
manifest_path = os.path.join(package_dir, f"{package_name}.manifest")
|
||||
far_path = os.path.join(package_dir, f"{package_name}-0.far")
|
||||
|
||||
shared_libs = args.shared_libs[: args.n]
|
||||
arguments = args.shared_libs[args.n :]
|
||||
|
||||
test_output_dir = os.path.join(self.output_dir(), package_name)
|
||||
|
||||
# Clean and create temporary output directory
|
||||
if os.path.exists(test_output_dir):
|
||||
shutil.rmtree(test_output_dir)
|
||||
|
||||
os.mkdir(test_output_dir)
|
||||
|
||||
# Open log file
|
||||
log_path = os.path.join(test_output_dir, "log")
|
||||
with open(log_path, "w", encoding="utf-8") as log_file:
|
||||
|
||||
def log(msg):
|
||||
print(msg, file=log_file)
|
||||
log_file.flush()
|
||||
|
||||
log(f"Bin path: {bin_path}")
|
||||
|
||||
log("Setting up package...")
|
||||
|
||||
# Set up package
|
||||
subprocess.check_call(
|
||||
[
|
||||
self.tool_path("pm"),
|
||||
"-o",
|
||||
package_dir,
|
||||
"-n",
|
||||
package_name,
|
||||
"init",
|
||||
],
|
||||
stdout=log_file,
|
||||
stderr=log_file,
|
||||
)
|
||||
|
||||
log("Writing CML...")
|
||||
|
||||
# Write and compile CML
|
||||
with open(cml_path, "w", encoding="utf-8") as cml:
|
||||
# Collect environment variables
|
||||
env_vars = ""
|
||||
for var_name in self.TEST_ENV_VARS:
|
||||
var_value = os.getenv(var_name)
|
||||
if var_value is not None:
|
||||
env_vars += f'\n "{var_name}={var_value}",'
|
||||
|
||||
# Default to no backtrace for test suite
|
||||
if os.getenv("RUST_BACKTRACE") == None:
|
||||
env_vars += f'\n "RUST_BACKTRACE=0",'
|
||||
|
||||
cml.write(
|
||||
self.CML_TEMPLATE.format(env_vars=env_vars, exe_name=exe_name)
|
||||
)
|
||||
|
||||
log("Compiling CML...")
|
||||
|
||||
subprocess.check_call(
|
||||
[
|
||||
self.tool_path("cmc"),
|
||||
"compile",
|
||||
cml_path,
|
||||
"--includepath",
|
||||
".",
|
||||
"--output",
|
||||
cm_path,
|
||||
],
|
||||
stdout=log_file,
|
||||
stderr=log_file,
|
||||
)
|
||||
|
||||
log("Writing manifest...")
|
||||
|
||||
# Write, build, and archive manifest
|
||||
with open(manifest_path, "w", encoding="utf-8") as manifest:
|
||||
manifest.write(
|
||||
self.MANIFEST_TEMPLATE.format(
|
||||
bin_path=bin_path,
|
||||
exe_name=exe_name,
|
||||
package_dir=package_dir,
|
||||
package_name=package_name,
|
||||
rust_dir=self.rust_dir,
|
||||
rustlib_dir=self.rustlib_dir(),
|
||||
sdk_dir=self.sdk_dir,
|
||||
libstd_name=self.libstd_name,
|
||||
libtest_name=self.libtest_name,
|
||||
target_arch=self.target_arch,
|
||||
)
|
||||
)
|
||||
for shared_lib in shared_libs:
|
||||
manifest.write(f"lib/{os.path.basename(shared_lib)}={shared_lib}\n")
|
||||
|
||||
log("Compiling and archiving manifest...")
|
||||
|
||||
subprocess.check_call(
|
||||
[
|
||||
self.tool_path("pm"),
|
||||
"-o",
|
||||
package_dir,
|
||||
"-m",
|
||||
manifest_path,
|
||||
"build",
|
||||
],
|
||||
stdout=log_file,
|
||||
stderr=log_file,
|
||||
)
|
||||
subprocess.check_call(
|
||||
[
|
||||
self.tool_path("pm"),
|
||||
"-o",
|
||||
package_dir,
|
||||
"-m",
|
||||
manifest_path,
|
||||
"archive",
|
||||
],
|
||||
stdout=log_file,
|
||||
stderr=log_file,
|
||||
)
|
||||
|
||||
log("Publishing package to repo...")
|
||||
|
||||
# Publish package to repo
|
||||
subprocess.check_call(
|
||||
[
|
||||
self.tool_path("pm"),
|
||||
"publish",
|
||||
"-a",
|
||||
"-repo",
|
||||
self.repo_dir(),
|
||||
"-f",
|
||||
far_path,
|
||||
],
|
||||
stdout=log_file,
|
||||
stderr=log_file,
|
||||
)
|
||||
|
||||
log("Running ffx test...")
|
||||
|
||||
# Run test on emulator
|
||||
subprocess.run(
|
||||
[
|
||||
self.tool_path("ffx"),
|
||||
"--config",
|
||||
self.ffx_user_config_path(),
|
||||
"test",
|
||||
"run",
|
||||
f"fuchsia-pkg://{self.TEST_REPO_NAME}/{package_name}#meta/{package_name}.cm",
|
||||
"--min-severity-logs",
|
||||
"TRACE",
|
||||
"--output-directory",
|
||||
test_output_dir,
|
||||
"--",
|
||||
]
|
||||
+ arguments,
|
||||
env=self.ffx_cmd_env(),
|
||||
check=False,
|
||||
stdout=log_file,
|
||||
stderr=log_file,
|
||||
)
|
||||
|
||||
log("Reporting test suite output...")
|
||||
|
||||
# Read test suite output
|
||||
run_summary_path = os.path.join(test_output_dir, "run_summary.json")
|
||||
if os.path.exists(run_summary_path):
|
||||
with open(run_summary_path, encoding="utf-8") as f:
|
||||
run_summary = json.loads(f.read())
|
||||
|
||||
suite = run_summary["data"]["suites"][0]
|
||||
case = suite["cases"][0]
|
||||
|
||||
return_code = 0 if case["outcome"] == "PASSED" else 1
|
||||
|
||||
artifacts = case["artifacts"]
|
||||
artifact_dir = case["artifact_dir"]
|
||||
stdout_path = None
|
||||
stderr_path = None
|
||||
|
||||
for path, artifact in artifacts.items():
|
||||
artifact_path = os.path.join(test_output_dir, artifact_dir, path)
|
||||
artifact_type = artifact["artifact_type"]
|
||||
|
||||
if artifact_type == "STDERR":
|
||||
stderr_path = artifact_path
|
||||
elif artifact_type == "STDOUT":
|
||||
stdout_path = artifact_path
|
||||
|
||||
if stdout_path is not None and os.path.exists(stdout_path):
|
||||
with open(stdout_path, encoding="utf-8") as f:
|
||||
print(f.read(), file=sys.stdout, end="")
|
||||
|
||||
if stderr_path is not None and os.path.exists(stderr_path):
|
||||
with open(stderr_path, encoding="utf-8") as f:
|
||||
print(f.read(), file=sys.stderr, end="")
|
||||
else:
|
||||
log("Failed to open test run summary")
|
||||
return_code = 254
|
||||
|
||||
log("Done!")
|
||||
|
||||
return return_code
|
||||
|
||||
def stop(self):
|
||||
"""Shuts down and cleans up the testing environment.
|
||||
|
||||
Args:
|
||||
args: The command-line arguments to this command.
|
||||
Returns:
|
||||
The return code of the test (0 for success, else failure).
|
||||
|
||||
During cleanup, this function will stop the emulator, package server, and
|
||||
update server, then delete all temporary files. If an error is encountered
|
||||
while stopping any running processes, the temporary files will not be deleted.
|
||||
Passing --delete-tmp will force the process to delete the files anyway.
|
||||
"""
|
||||
|
||||
self.log_debug("Reporting logs...")
|
||||
|
||||
# Print test log files
|
||||
for test_dir in os.listdir(self.output_dir()):
|
||||
log_path = os.path.join(self.output_dir(), test_dir, "log")
|
||||
self.log_debug(f"\n---- Logs for test '{test_dir}' ----\n")
|
||||
if os.path.exists(log_path):
|
||||
with open(log_path, encoding="utf-8") as log:
|
||||
self.log_debug(log.read())
|
||||
else:
|
||||
self.log_debug("No logs found")
|
||||
|
||||
# Print the emulator log
|
||||
self.log_debug("\n---- Emulator logs ----\n")
|
||||
if os.path.exists(self.emulator_log_path()):
|
||||
with open(self.emulator_log_path(), encoding="utf-8") as log:
|
||||
self.log_debug(log.read())
|
||||
else:
|
||||
self.log_debug("No emulator logs found")
|
||||
|
||||
# Print the package server log
|
||||
self.log_debug("\n---- Package server log ----\n")
|
||||
if os.path.exists(self.package_server_log_path()):
|
||||
with open(self.package_server_log_path(), encoding="utf-8") as log:
|
||||
self.log_debug(log.read())
|
||||
else:
|
||||
self.log_debug("No package server log found")
|
||||
|
||||
# Print the ffx daemon log
|
||||
self.log_debug("\n---- ffx daemon log ----\n")
|
||||
if os.path.exists(self.ffx_daemon_log_path()):
|
||||
with open(self.ffx_daemon_log_path(), encoding="utf-8") as log:
|
||||
self.log_debug(log.read())
|
||||
else:
|
||||
self.log_debug("No ffx daemon log found")
|
||||
|
||||
# Stop package server
|
||||
self.log_info("Stopping package server...")
|
||||
os.kill(self.package_server_pid, signal.SIGTERM)
|
||||
|
||||
# Shut down the emulator
|
||||
self.log_info("Stopping emulator...")
|
||||
subprocess.check_call(
|
||||
[
|
||||
self.tool_path("fvdl"),
|
||||
"--sdk",
|
||||
"kill",
|
||||
"--launched-proto",
|
||||
self.vdl_output_path(),
|
||||
],
|
||||
stdout=self.subprocess_output(),
|
||||
stderr=self.subprocess_output(),
|
||||
)
|
||||
|
||||
# Stop ffx isolation
|
||||
self.log_info("Stopping ffx isolation...")
|
||||
self.stop_ffx_isolation()
|
||||
|
||||
def delete_tmp(self):
|
||||
# Remove temporary files
|
||||
self.log_info("Deleting temporary files...")
|
||||
shutil.rmtree(self.tmp_dir(), ignore_errors=True)
|
||||
|
||||
def debug(self, args):
|
||||
command = [
|
||||
self.tool_path("ffx"),
|
||||
"--config",
|
||||
self.ffx_user_config_path(),
|
||||
"debug",
|
||||
"connect",
|
||||
"--",
|
||||
"--build-id-dir",
|
||||
os.path.join(self.sdk_dir, ".build-id"),
|
||||
"--build-id-dir",
|
||||
os.path.join(self.libs_dir(), ".build-id"),
|
||||
]
|
||||
|
||||
# Add rust source if it's available
|
||||
if args.rust_src is not None:
|
||||
command += [
|
||||
"--build-dir",
|
||||
args.rust_src,
|
||||
]
|
||||
|
||||
# Add fuchsia source if it's available
|
||||
if args.fuchsia_src is not None:
|
||||
command += [
|
||||
"--build-dir",
|
||||
os.path.join(args.fuchsia_src, "out", "default"),
|
||||
]
|
||||
|
||||
# Load debug symbols for the test binary and automatically attach
|
||||
if args.test is not None:
|
||||
if args.rust_src is None:
|
||||
raise Exception(
|
||||
"A Rust source path is required with the `test` argument"
|
||||
)
|
||||
|
||||
test_name = os.path.splitext(os.path.basename(args.test))[0]
|
||||
|
||||
build_dir = os.path.join(
|
||||
args.rust_src,
|
||||
"fuchsia-build",
|
||||
self.host_arch_triple(),
|
||||
)
|
||||
test_dir = os.path.join(
|
||||
build_dir,
|
||||
"test",
|
||||
os.path.dirname(args.test),
|
||||
test_name,
|
||||
)
|
||||
|
||||
with open(self.zxdb_script_path(), mode="w", encoding="utf-8") as f:
|
||||
print(f"attach {test_name[:31]}", file=f)
|
||||
|
||||
command += [
|
||||
"--symbol-path",
|
||||
test_dir,
|
||||
"-S",
|
||||
self.zxdb_script_path(),
|
||||
]
|
||||
|
||||
# Add any other zxdb arguments the user passed
|
||||
if args.zxdb_args is not None:
|
||||
command += args.zxdb_args
|
||||
|
||||
# Connect to the running emulator with zxdb
|
||||
subprocess.run(command, env=self.ffx_cmd_env(), check=False)
|
||||
|
||||
|
||||
def start(args):
|
||||
test_env = TestEnvironment.from_args(args)
|
||||
test_env.start()
|
||||
return 0
|
||||
|
||||
|
||||
def run(args):
|
||||
test_env = TestEnvironment.read_from_file()
|
||||
return test_env.run(args)
|
||||
|
||||
|
||||
def stop(args):
|
||||
test_env = TestEnvironment.read_from_file()
|
||||
test_env.stop()
|
||||
if not args.no_delete:
|
||||
test_env.delete_tmp()
|
||||
return 0
|
||||
|
||||
|
||||
def delete_tmp(args):
|
||||
del args
|
||||
test_env = TestEnvironment.read_from_file()
|
||||
test_env.delete_tmp()
|
||||
return 0
|
||||
|
||||
|
||||
def debug(args):
|
||||
test_env = TestEnvironment.read_from_file()
|
||||
test_env.debug(args)
|
||||
return 0
|
||||
|
||||
|
||||
def main():
|
||||
parser = argparse.ArgumentParser()
|
||||
|
||||
def print_help(args):
|
||||
del args
|
||||
parser.print_help()
|
||||
return 0
|
||||
|
||||
parser.set_defaults(func=print_help)
|
||||
|
||||
subparsers = parser.add_subparsers(help="valid sub-commands")
|
||||
|
||||
start_parser = subparsers.add_parser(
|
||||
"start", help="initializes the testing environment"
|
||||
)
|
||||
start_parser.add_argument(
|
||||
"--rust",
|
||||
help="the directory of the installed Rust compiler for Fuchsia",
|
||||
required=True,
|
||||
)
|
||||
start_parser.add_argument(
|
||||
"--sdk",
|
||||
help="the directory of the fuchsia SDK",
|
||||
required=True,
|
||||
)
|
||||
start_parser.add_argument(
|
||||
"--verbose",
|
||||
help="prints more output from executed processes",
|
||||
action="store_true",
|
||||
)
|
||||
start_parser.add_argument(
|
||||
"--target-arch",
|
||||
help="the architecture of the image to test",
|
||||
required=True,
|
||||
)
|
||||
start_parser.set_defaults(func=start)
|
||||
|
||||
run_parser = subparsers.add_parser(
|
||||
"run", help="run a test in the testing environment"
|
||||
)
|
||||
run_parser.add_argument(
|
||||
"n", help="the number of shared libs passed along with the executable", type=int
|
||||
)
|
||||
run_parser.add_argument("bin_path", help="path to the binary to run")
|
||||
run_parser.add_argument(
|
||||
"shared_libs",
|
||||
help="the shared libs passed along with the binary",
|
||||
nargs=argparse.REMAINDER,
|
||||
)
|
||||
run_parser.set_defaults(func=run)
|
||||
|
||||
stop_parser = subparsers.add_parser(
|
||||
"stop", help="shuts down and cleans up the testing environment"
|
||||
)
|
||||
stop_parser.add_argument(
|
||||
"--no-delete",
|
||||
default=False,
|
||||
action="store_true",
|
||||
help="don't delete temporary files after stopping",
|
||||
)
|
||||
stop_parser.set_defaults(func=stop)
|
||||
|
||||
delete_parser = subparsers.add_parser(
|
||||
"delete-tmp",
|
||||
help="deletes temporary files after the testing environment has been manually cleaned up",
|
||||
)
|
||||
delete_parser.set_defaults(func=delete_tmp)
|
||||
|
||||
debug_parser = subparsers.add_parser(
|
||||
"debug",
|
||||
help="connect to the active testing environment with zxdb",
|
||||
)
|
||||
debug_parser.add_argument(
|
||||
"--rust-src",
|
||||
default=None,
|
||||
help="the path to the Rust source being tested",
|
||||
)
|
||||
debug_parser.add_argument(
|
||||
"--fuchsia-src",
|
||||
default=None,
|
||||
help="the path to the Fuchsia source",
|
||||
)
|
||||
debug_parser.add_argument(
|
||||
"--test",
|
||||
default=None,
|
||||
help="the path to the test to debug (e.g. ui/box/new.rs)",
|
||||
)
|
||||
debug_parser.add_argument(
|
||||
"zxdb_args",
|
||||
default=None,
|
||||
nargs=argparse.REMAINDER,
|
||||
help="any additional arguments to pass to zxdb",
|
||||
)
|
||||
debug_parser.set_defaults(func=debug)
|
||||
|
||||
args = parser.parse_args()
|
||||
return args.func(args)
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
sys.exit(main())
|
||||
@@ -641,8 +641,60 @@ available on the [Fuchsia devsite].
|
||||
|
||||
### Running the compiler test suite
|
||||
|
||||
Running the Rust test suite on Fuchsia is [not currently supported], but work is
|
||||
underway to enable it.
|
||||
Pre-requisites for running the Rust test suite on Fuchsia are:
|
||||
1. Checkout of Rust source.
|
||||
1. Setup of `config-env.sh` and `config.toml` from "[Targeting Fuchsia with a compiler built from source](#targeting-fuchsia-with-a-compiler-built-from-source)".
|
||||
1. Download of the Fuchsia SDK. Minimum supported SDK version is [9.20220726.1.1](https://chrome-infra-packages.appspot.com/p/fuchsia/sdk/core/linux-amd64/+/version:9.20220726.1.1)
|
||||
|
||||
Interfacing with the Fuchsia emulator is handled by our test runner script located
|
||||
at `${RUST_SRC_PATH}/src/ci/docker/scripts/fuchsia-test-runner.py`.
|
||||
|
||||
We start by activating our Fuchsia test environment. From a terminal:
|
||||
|
||||
**Issue command from ${RUST_SRC_PATH}**
|
||||
```sh
|
||||
src/ci/docker/scripts/fuchsia-test-runner.py start
|
||||
--rust .
|
||||
--sdk ${SDK_PATH}
|
||||
--target-arch {x64,arm64}
|
||||
```
|
||||
|
||||
Next, for ease of commands, we copy `config-env.sh` and `config.toml` into our Rust source
|
||||
code path, `${RUST_SRC_PATH}`.
|
||||
|
||||
From there, we utilize `x.py` to run our tests, using the test runner script to
|
||||
run the tests on our emulator. To run the full `src/test/ui` test suite:
|
||||
|
||||
**Run from ${RUST_SRC_PATH}**
|
||||
```sh
|
||||
( \
|
||||
source config-env.sh && \
|
||||
./x.py \
|
||||
--config config.toml \
|
||||
--stage=2 \
|
||||
test src/test/ui \
|
||||
--target x86_64-fuchsia \
|
||||
--run=always --jobs 1 \
|
||||
--test-args --target-rustcflags -L \
|
||||
--test-args --target-rustcflags ${SDK_PATH}/arch/{x64|arm64}/sysroot/lib \
|
||||
--test-args --target-rustcflags -L \
|
||||
--test-args --target-rustcflags ${SDK_PATH}/arch/{x64|arm64}/lib \
|
||||
--test-args --target-rustcflags -Cpanic=abort \
|
||||
--test-args --target-rustcflags -Zpanic_abort_tests \
|
||||
--test-args --remote-test-client \
|
||||
--test-args src/ci/docker/scripts/fuchsia-test-runner.py \
|
||||
)
|
||||
```
|
||||
|
||||
*Note: The test suite cannot be run in parallel at the moment, so `x.py`
|
||||
must be run with `--jobs 1` to ensure only one test runs at a time.*
|
||||
|
||||
When finished, stop the test environment:
|
||||
|
||||
**Issue command from ${RUST_SRC_PATH}**
|
||||
```sh
|
||||
src/ci/docker/scripts/fuchsia-test-runner.py stop
|
||||
```
|
||||
|
||||
## Debugging
|
||||
|
||||
|
||||
Reference in New Issue
Block a user