mirror of
https://github.com/rui314/mold.git
synced 2026-11-07 09:16:04 -10:00
Merge the ELF and Mach-O test runners into one crate
tests/elf and tests/macho each had a crate of their own for running their shell scripts, which did much the same thing. A single mold-tests crate directly under tests/ now runs both: lib.rs discovers, schedules, times out and reports the tests, and elf.rs and macho.rs only pick the targets, the latter booting a simulator device for each simulator target in turn. The two runners' options and conventions are made one. Both take --native, --all, --triple and --cpu, the Mach-O tests' --native now running the targets the Mac runs directly, its own architecture and x86_64 under Rosetta, as --host did. The work directory links mold under every name the scripts use, a log line ending in "skipped" marks a skipped test, and unavailable targets are reported the same way. Co-Authored-By: Claude Opus 5.5 <[email protected]>
This commit is contained in:
1 parent
eb21f960fd
commit
d66a911074
13 files changed
+573
-1015
No files matched your search
@@ -36,7 +36,7 @@ jobs:
|
||||
! grep -q '^unavailable:' "$RUNNER_TEMP/test-inventory.txt"
|
||||
# All workspace crates were built above. Avoid linking empty test binaries
|
||||
# for the target crates and command.
|
||||
- run: cargo test --locked -p mold-common -p mold-elf -p mold-elf-tests -p mold --lib --test integration
|
||||
- run: cargo test --locked -p mold-common -p mold-elf -p mold-tests -p mold --lib --test integration
|
||||
- name: archive test results
|
||||
uses: actions/upload-artifact@v7
|
||||
if: failure()
|
||||
|
||||
@@ -40,7 +40,7 @@ jobs:
|
||||
# session alone takes ten seconds on a Mac, and the first of which
|
||||
# ran past a minute on the hosted runners. The executable is built
|
||||
# without the ELF targets, which these tests don't use.
|
||||
- run: cargo test --locked -p mold-macho -p mold-macho-tests --lib
|
||||
- run: cargo test --locked -p mold-macho -p mold-tests --lib
|
||||
- run: |
|
||||
cargo test --locked -p mold --no-default-features --features macho-arm64,macho-x86_64 \
|
||||
--test integration -- --timeout 120
|
||||
|
||||
Generated
+9
-17
@@ -283,13 +283,12 @@ dependencies = [
|
||||
"mold-elf-sh4",
|
||||
"mold-elf-sh4be",
|
||||
"mold-elf-sparc64",
|
||||
"mold-elf-tests",
|
||||
"mold-elf-x86_64",
|
||||
"mold-macho",
|
||||
"mold-macho-arm64",
|
||||
"mold-macho-tests",
|
||||
"mold-macho-x86_64",
|
||||
"mold-mimalloc",
|
||||
"mold-tests",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
@@ -463,13 +462,6 @@ dependencies = [
|
||||
"mold-elf",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "mold-elf-tests"
|
||||
version = "3.0.0"
|
||||
dependencies = [
|
||||
"libc",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "mold-elf-x86_64"
|
||||
version = "3.0.0"
|
||||
@@ -502,14 +494,6 @@ dependencies = [
|
||||
"mold-macho",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "mold-macho-tests"
|
||||
version = "3.0.0"
|
||||
dependencies = [
|
||||
"libc",
|
||||
"serde_json",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "mold-macho-x86_64"
|
||||
version = "3.0.0"
|
||||
@@ -535,6 +519,14 @@ dependencies = [
|
||||
"cc",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "mold-tests"
|
||||
version = "3.0.0"
|
||||
dependencies = [
|
||||
"libc",
|
||||
"serde_json",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "pkg-config"
|
||||
version = "0.3.34"
|
||||
|
||||
+1
-1
@@ -4,7 +4,7 @@
|
||||
# arch/, and the executable in cli/ dispatches to them.
|
||||
[workspace]
|
||||
resolver = "3"
|
||||
members = ["cli", "common", "elf", "macho", "arch/*", "tests/elf", "tests/macho"]
|
||||
members = ["cli", "common", "elf", "macho", "arch/*", "tests"]
|
||||
|
||||
[workspace.package]
|
||||
version = "3.0.0"
|
||||
|
||||
+1
-2
@@ -47,8 +47,7 @@ mold-macho-arm64 = { workspace = true, optional = true }
|
||||
mold-macho-x86_64 = { workspace = true, optional = true }
|
||||
|
||||
[dev-dependencies]
|
||||
mold-elf-tests = { path = "../tests/elf" }
|
||||
mold-macho-tests = { path = "../tests/macho" }
|
||||
mold-tests = { path = "../tests" }
|
||||
|
||||
# One feature per target. All are built by default; a build for one
|
||||
# target alone (`--no-default-features --features x86_64`) is much faster.
|
||||
|
||||
+3
-3
@@ -7,10 +7,10 @@ use std::process::ExitCode;
|
||||
|
||||
fn main() -> ExitCode {
|
||||
#[cfg(target_os = "macos")]
|
||||
let (cases, run) = ("tests/macho", mold_macho_tests::run);
|
||||
let (cases, run) = ("tests/macho", mold_tests::macho::run);
|
||||
#[cfg(not(target_os = "macos"))]
|
||||
let (cases, run) = ("tests/elf", mold_elf_tests::run);
|
||||
let (cases, run) = ("tests/elf", mold_tests::elf::run);
|
||||
|
||||
let root = Path::new(env!("CARGO_MANIFEST_DIR")).parent().unwrap();
|
||||
run(&[root.join(cases)], Path::new(env!("CARGO_BIN_EXE_mold")))
|
||||
run(&root.join(cases), Path::new(env!("CARGO_BIN_EXE_mold")))
|
||||
}
|
||||
@@ -1,5 +1,5 @@
|
||||
[package]
|
||||
name = "mold-macho-tests"
|
||||
name = "mold-tests"
|
||||
version.workspace = true
|
||||
edition.workspace = true
|
||||
rust-version.workspace = true
|
||||
+201
@@ -0,0 +1,201 @@
|
||||
//! The ELF tests' targets: the host's, and each cross target whose cross
|
||||
//! compiler and QEMU are installed, whose programs run under QEMU.
|
||||
|
||||
use std::env;
|
||||
use std::fs;
|
||||
use std::path::Path;
|
||||
use std::process::{Command, ExitCode, Stdio};
|
||||
|
||||
use crate::{
|
||||
Mode, Options, Target, fail, make_jobs, parse_options, prepare_work_dir, print_inventory,
|
||||
print_summary, run_jobs, selected_scripts,
|
||||
};
|
||||
|
||||
struct TargetSpec {
|
||||
machine: &'static str,
|
||||
triple: &'static str,
|
||||
qemu: &'static str,
|
||||
}
|
||||
|
||||
// The targets tested by default. A target other than the host's is
|
||||
// tested only if its cross compiler and QEMU are installed.
|
||||
const TARGETS: &[TargetSpec] = &[
|
||||
TargetSpec { machine: "x86_64", triple: "x86_64-linux-gnu", qemu: "qemu-x86_64" },
|
||||
TargetSpec { machine: "i686", triple: "i686-linux-gnu", qemu: "qemu-i386" },
|
||||
TargetSpec { machine: "aarch64", triple: "aarch64-linux-gnu", qemu: "qemu-aarch64" },
|
||||
TargetSpec { machine: "aarch64_be", triple: "aarch64_be-linux-gnu", qemu: "qemu-aarch64_be" },
|
||||
TargetSpec { machine: "arm", triple: "arm-linux-gnueabihf", qemu: "qemu-arm" },
|
||||
TargetSpec { machine: "armeb", triple: "armeb-linux-gnueabihf", qemu: "qemu-armeb" },
|
||||
TargetSpec { machine: "riscv64", triple: "riscv64-linux-gnu", qemu: "qemu-riscv64" },
|
||||
TargetSpec { machine: "riscv32", triple: "riscv32-linux-gnu", qemu: "qemu-riscv32" },
|
||||
TargetSpec { machine: "ppc", triple: "powerpc-linux-gnu", qemu: "qemu-ppc" },
|
||||
TargetSpec { machine: "ppc64", triple: "powerpc64-linux-gnu", qemu: "qemu-ppc64" },
|
||||
TargetSpec { machine: "ppc64le", triple: "powerpc64le-linux-gnu", qemu: "qemu-ppc64le" },
|
||||
TargetSpec { machine: "sparc64", triple: "sparc64-linux-gnu", qemu: "qemu-sparc64" },
|
||||
TargetSpec { machine: "s390x", triple: "s390x-linux-gnu", qemu: "qemu-s390x" },
|
||||
TargetSpec { machine: "sh4", triple: "sh4-linux-gnu", qemu: "qemu-sh4" },
|
||||
TargetSpec { machine: "sh4aeb", triple: "sh4aeb-linux-gnu", qemu: "qemu-sh4eb" },
|
||||
TargetSpec { machine: "m68k", triple: "m68k-linux-gnu", qemu: "qemu-m68k" },
|
||||
TargetSpec {
|
||||
machine: "loongarch64",
|
||||
triple: "loongarch64-linux-gnu",
|
||||
qemu: "qemu-loongarch64",
|
||||
},
|
||||
];
|
||||
|
||||
/// A target, its programs run natively without a triple, under QEMU
|
||||
/// with one.
|
||||
fn target(machine: &str, triple: Option<&str>, cpu: Option<&str>) -> Target {
|
||||
let label = match cpu {
|
||||
Some(cpu) => format!("{machine}-{cpu}"),
|
||||
None => machine.to_owned(),
|
||||
};
|
||||
Target {
|
||||
label,
|
||||
arch: Some(machine.to_owned()),
|
||||
env: vec![
|
||||
("MACHINE", Some(machine.to_owned())),
|
||||
("TRIPLE", triple.map(str::to_owned)),
|
||||
("CPU", cpu.map(str::to_owned)),
|
||||
],
|
||||
}
|
||||
}
|
||||
|
||||
fn canonical_machine(machine: &str) -> String {
|
||||
let machine = machine.trim();
|
||||
if machine == "amd64" {
|
||||
return "x86_64".to_owned();
|
||||
}
|
||||
if machine.len() == 4 && machine.starts_with('i') && machine.ends_with("86") {
|
||||
return "i686".to_owned();
|
||||
}
|
||||
if machine.starts_with("armeb") {
|
||||
return "armeb".to_owned();
|
||||
}
|
||||
if machine.starts_with("arm") {
|
||||
return "arm".to_owned();
|
||||
}
|
||||
match machine {
|
||||
"powerpc" => "ppc".to_owned(),
|
||||
"powerpc64" => "ppc64".to_owned(),
|
||||
"powerpc64le" => "ppc64le".to_owned(),
|
||||
_ => machine.to_owned(),
|
||||
}
|
||||
}
|
||||
|
||||
fn machine_from_triple(triple: &str) -> String {
|
||||
canonical_machine(triple.split('-').next().unwrap_or(triple))
|
||||
}
|
||||
|
||||
fn native_machine() -> String {
|
||||
if let Some(machine) = env::var_os("MACHINE").filter(|s| !s.is_empty()) {
|
||||
return canonical_machine(&machine.to_string_lossy());
|
||||
}
|
||||
|
||||
if let Ok(output) = Command::new("cc").arg("-dumpmachine").output()
|
||||
&& output.status.success()
|
||||
{
|
||||
let triple = String::from_utf8_lossy(&output.stdout);
|
||||
if !triple.trim().is_empty() {
|
||||
return machine_from_triple(&triple);
|
||||
}
|
||||
}
|
||||
canonical_machine(env::consts::ARCH)
|
||||
}
|
||||
|
||||
fn command_exists(command: &str) -> bool {
|
||||
let path = Path::new(command);
|
||||
if path.components().count() > 1 {
|
||||
return path.is_file();
|
||||
}
|
||||
env::var_os("PATH")
|
||||
.is_some_and(|paths| env::split_paths(&paths).any(|dir| dir.join(command).is_file()))
|
||||
}
|
||||
|
||||
fn supports_power10() -> bool {
|
||||
if !command_exists("powerpc64le-linux-gnu-gcc") || !command_exists("qemu-ppc64le") {
|
||||
return false;
|
||||
}
|
||||
|
||||
let compiler = Command::new("powerpc64le-linux-gnu-gcc")
|
||||
.args(["-mcpu=power10", "-E", "-x", "c", "-"])
|
||||
.stdin(Stdio::null())
|
||||
.stdout(Stdio::null())
|
||||
.stderr(Stdio::null())
|
||||
.status()
|
||||
.is_ok_and(|status| status.success());
|
||||
let qemu = Command::new("qemu-ppc64le")
|
||||
.args(["-cpu", "help"])
|
||||
.output()
|
||||
.is_ok_and(|output| String::from_utf8_lossy(&output.stdout).contains("power10_v2.0"));
|
||||
compiler && qemu
|
||||
}
|
||||
|
||||
fn all_targets(native: &str) -> (Vec<Target>, Vec<String>) {
|
||||
let mut targets = Vec::new();
|
||||
let mut unavailable = Vec::new();
|
||||
|
||||
for spec in TARGETS {
|
||||
if spec.machine == native {
|
||||
targets.push(target(native, None, None));
|
||||
} else if command_exists(spec.qemu) && command_exists(&format!("{}-gcc", spec.triple)) {
|
||||
targets.push(target(spec.machine, Some(spec.triple), None));
|
||||
} else {
|
||||
unavailable.push(spec.machine.to_owned());
|
||||
}
|
||||
}
|
||||
|
||||
if native == "ppc64le"
|
||||
&& fs::read_to_string("/proc/cpuinfo").is_ok_and(|s| s.contains("POWER10"))
|
||||
{
|
||||
targets.push(target(native, None, Some("power10")));
|
||||
} else if supports_power10() {
|
||||
targets.push(target("ppc64le", Some("powerpc64le-linux-gnu"), Some("power10")));
|
||||
}
|
||||
|
||||
(targets, unavailable)
|
||||
}
|
||||
|
||||
fn selected_targets(options: &Options) -> (Vec<Target>, Vec<String>) {
|
||||
let native = native_machine();
|
||||
match &options.mode {
|
||||
Mode::Native => (vec![target(&native, None, None)], Vec::new()),
|
||||
Mode::Default | Mode::All => all_targets(&native),
|
||||
Mode::Triple(triple) => {
|
||||
let machine = machine_from_triple(triple);
|
||||
(vec![target(&machine, Some(triple), options.cpu.as_deref())], Vec::new())
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Runs the ELF test scripts in `cases_dir` with the linker at `mold`.
|
||||
/// Command line arguments are substring patterns selecting a subset of
|
||||
/// tests.
|
||||
pub fn run(cases_dir: &Path, mold: &Path) -> ExitCode {
|
||||
let options = parse_options();
|
||||
let work_dir = prepare_work_dir(mold).unwrap_or_else(|err| fail(err));
|
||||
let (targets, unavailable) = selected_targets(&options);
|
||||
let scripts = selected_scripts(cases_dir, &options.patterns).unwrap_or_else(|err| fail(err));
|
||||
if options.list {
|
||||
return print_inventory(&scripts, &work_dir, targets, &unavailable);
|
||||
}
|
||||
if !unavailable.is_empty() {
|
||||
eprintln!("skipping targets without both compiler and QEMU: {}", unavailable.join(", "));
|
||||
}
|
||||
let jobs = make_jobs(&scripts, &work_dir, targets, true).unwrap_or_else(|err| fail(err));
|
||||
print_summary(&run_jobs(&work_dir, jobs, &options))
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
|
||||
#[test]
|
||||
fn canonicalizes_machine_names() {
|
||||
assert_eq!(canonical_machine("amd64"), "x86_64");
|
||||
assert_eq!(canonical_machine("i386"), "i686");
|
||||
assert_eq!(canonical_machine("armv7l"), "arm");
|
||||
assert_eq!(canonical_machine("aarch64"), "aarch64");
|
||||
assert_eq!(canonical_machine("powerpc64le"), "ppc64le");
|
||||
}
|
||||
}
|
||||
@@ -1,14 +0,0 @@
|
||||
[package]
|
||||
name = "mold-elf-tests"
|
||||
version.workspace = true
|
||||
edition.workspace = true
|
||||
rust-version.workspace = true
|
||||
description = "Test support for mold's shell test suite"
|
||||
license.workspace = true
|
||||
publish = false
|
||||
|
||||
[lib]
|
||||
path = "lib.rs"
|
||||
|
||||
[dependencies]
|
||||
libc.workspace = true
|
||||
+107
-277
@@ -1,8 +1,18 @@
|
||||
//! Runs mold's shell tests in parallel for Cargo's test harness.
|
||||
//!
|
||||
//! The tests themselves are shell scripts so that they exercise mold with
|
||||
//! the real compilers and binutils of each target. The runner owns test
|
||||
//! discovery, target selection, scheduling, timeouts and reporting.
|
||||
//! The tests are shell scripts, in elf/ and macho/, so that they exercise
|
||||
//! mold with the real compilers, binutils and loaders of each target. The
|
||||
//! runner owns test discovery, target selection, scheduling, timeouts and
|
||||
//! reporting. The ELF tests run for the host and, under QEMU, for each
|
||||
//! cross target whose toolchain is installed (see elf.rs). The Mach-O
|
||||
//! tests, which drive Apple's toolchain, run on macOS for the host, for
|
||||
//! x86_64 under Rosetta and on simulator devices (see macho.rs).
|
||||
|
||||
pub mod elf;
|
||||
#[cfg(target_os = "macos")]
|
||||
pub mod macho;
|
||||
#[cfg(target_os = "macos")]
|
||||
mod simulator;
|
||||
|
||||
use std::collections::BTreeMap;
|
||||
use std::env;
|
||||
@@ -21,61 +31,25 @@ use std::os::unix::process::CommandExt;
|
||||
|
||||
const DEFAULT_TIMEOUT: Duration = Duration::from_secs(60);
|
||||
|
||||
#[derive(Clone, Copy)]
|
||||
struct TargetSpec {
|
||||
machine: &'static str,
|
||||
triple: &'static str,
|
||||
qemu: &'static str,
|
||||
}
|
||||
|
||||
// The targets tested by --all, the default mode. A target other than the
|
||||
// host's is tested only if its cross compiler and QEMU are installed.
|
||||
const TARGETS: &[TargetSpec] = &[
|
||||
TargetSpec { machine: "x86_64", triple: "x86_64-linux-gnu", qemu: "qemu-x86_64" },
|
||||
TargetSpec { machine: "i686", triple: "i686-linux-gnu", qemu: "qemu-i386" },
|
||||
TargetSpec { machine: "aarch64", triple: "aarch64-linux-gnu", qemu: "qemu-aarch64" },
|
||||
TargetSpec { machine: "aarch64_be", triple: "aarch64_be-linux-gnu", qemu: "qemu-aarch64_be" },
|
||||
TargetSpec { machine: "arm", triple: "arm-linux-gnueabihf", qemu: "qemu-arm" },
|
||||
TargetSpec { machine: "armeb", triple: "armeb-linux-gnueabihf", qemu: "qemu-armeb" },
|
||||
TargetSpec { machine: "riscv64", triple: "riscv64-linux-gnu", qemu: "qemu-riscv64" },
|
||||
TargetSpec { machine: "riscv32", triple: "riscv32-linux-gnu", qemu: "qemu-riscv32" },
|
||||
TargetSpec { machine: "ppc", triple: "powerpc-linux-gnu", qemu: "qemu-ppc" },
|
||||
TargetSpec { machine: "ppc64", triple: "powerpc64-linux-gnu", qemu: "qemu-ppc64" },
|
||||
TargetSpec { machine: "ppc64le", triple: "powerpc64le-linux-gnu", qemu: "qemu-ppc64le" },
|
||||
TargetSpec { machine: "sparc64", triple: "sparc64-linux-gnu", qemu: "qemu-sparc64" },
|
||||
TargetSpec { machine: "s390x", triple: "s390x-linux-gnu", qemu: "qemu-s390x" },
|
||||
TargetSpec { machine: "sh4", triple: "sh4-linux-gnu", qemu: "qemu-sh4" },
|
||||
TargetSpec { machine: "sh4aeb", triple: "sh4aeb-linux-gnu", qemu: "qemu-sh4eb" },
|
||||
TargetSpec { machine: "m68k", triple: "m68k-linux-gnu", qemu: "qemu-m68k" },
|
||||
TargetSpec {
|
||||
machine: "loongarch64",
|
||||
triple: "loongarch64-linux-gnu",
|
||||
qemu: "qemu-loongarch64",
|
||||
},
|
||||
];
|
||||
|
||||
#[derive(Clone, Debug)]
|
||||
/// A configuration the scripts run in.
|
||||
struct Target {
|
||||
machine: String,
|
||||
triple: Option<String>,
|
||||
cpu: Option<String>,
|
||||
/// The name its results go under.
|
||||
label: String,
|
||||
}
|
||||
|
||||
impl Target {
|
||||
fn native(machine: String) -> Self {
|
||||
Self { label: machine.clone(), machine, triple: None, cpu: None }
|
||||
}
|
||||
|
||||
fn cross(machine: String, triple: String) -> Self {
|
||||
Self { label: machine.clone(), machine, triple: Some(triple), cpu: None }
|
||||
}
|
||||
/// If set, of the scripts named arch-*, only arch-<arch>-* run.
|
||||
arch: Option<String>,
|
||||
/// The environment variables the scripts get, each set or removed.
|
||||
env: Vec<(&'static str, Option<String>)>,
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug, PartialEq, Eq)]
|
||||
enum Mode {
|
||||
/// The targets the host runs directly, without QEMU or a simulator.
|
||||
Native,
|
||||
/// The usual targets.
|
||||
Default,
|
||||
/// Every target there is a toolchain for.
|
||||
All,
|
||||
/// One cross or simulator target.
|
||||
Triple(String),
|
||||
}
|
||||
|
||||
@@ -147,19 +121,29 @@ impl Counts {
|
||||
fn usage() -> ! {
|
||||
eprintln!(
|
||||
"Usage: cargo test [pattern] [-- [--test-threads N] \
|
||||
[--native | --all | --triple TRIPLE] [--cpu CPU] \
|
||||
[--timeout SECONDS] [--list]]"
|
||||
[--native | --all | --triple TRIPLE] [--cpu CPU] [--timeout SECONDS] [--list]]\n\
|
||||
By default the ELF tests run for every target whose cross compiler and QEMU are \
|
||||
installed, and the Mach-O tests for macOS (arm64, and x86_64 under Rosetta) and the \
|
||||
arm64 iOS simulator; --all adds the other simulators. --native runs the targets the \
|
||||
host runs directly, --triple one cross or simulator target (e.g. aarch64-linux-gnu \
|
||||
or arm64-apple-tvos-simulator)."
|
||||
);
|
||||
std::process::exit(2);
|
||||
}
|
||||
|
||||
/// Reports an error of the runner itself and exits.
|
||||
fn fail(err: impl std::fmt::Display) -> ! {
|
||||
eprintln!("mold-tests: {err}");
|
||||
std::process::exit(1);
|
||||
}
|
||||
|
||||
fn parse_usize(value: Option<String>) -> usize {
|
||||
value.and_then(|s| s.parse().ok()).filter(|&n| n != 0).unwrap_or_else(|| usage())
|
||||
}
|
||||
|
||||
fn parse_options() -> Options {
|
||||
let mut jobs = thread::available_parallelism().map_or(1, usize::from);
|
||||
let mut mode = Mode::All;
|
||||
let mut mode = Mode::Default;
|
||||
let mut mode_was_set = false;
|
||||
let mut cpu = None;
|
||||
let mut patterns = Vec::new();
|
||||
@@ -182,12 +166,7 @@ fn parse_options() -> Options {
|
||||
mode = Mode::Triple(args.next().unwrap_or_else(|| usage()));
|
||||
mode_was_set = true;
|
||||
}
|
||||
"--cpu" => {
|
||||
cpu = args.next();
|
||||
if cpu.is_none() {
|
||||
usage();
|
||||
}
|
||||
}
|
||||
"--cpu" => cpu = Some(args.next().unwrap_or_else(|| usage())),
|
||||
"--timeout" => timeout = Duration::from_secs(parse_usize(args.next()) as u64),
|
||||
"--list" => list = true,
|
||||
"--nocapture" | "--show-output" => {}
|
||||
@@ -203,158 +182,43 @@ fn parse_options() -> Options {
|
||||
}
|
||||
}
|
||||
|
||||
// Preserve the old runner's TRIPLE/CPU interface for callers that set
|
||||
// the cross target in the environment.
|
||||
if !mode_was_set {
|
||||
if let Some(value) = env::var_os("TRIPLE").filter(|s| !s.is_empty()) {
|
||||
mode = Mode::Triple(value.to_string_lossy().into_owned());
|
||||
}
|
||||
if cpu.is_none() {
|
||||
cpu = env::var_os("CPU")
|
||||
.filter(|s| !s.is_empty())
|
||||
.map(|s| s.to_string_lossy().into_owned());
|
||||
}
|
||||
// TRIPLE and CPU in the environment select a target too.
|
||||
let var = |name| env::var_os(name).filter(|s| !s.is_empty());
|
||||
if !mode_was_set && let Some(triple) = var("TRIPLE") {
|
||||
mode = Mode::Triple(triple.to_string_lossy().into_owned());
|
||||
}
|
||||
if cpu.is_none() {
|
||||
cpu = var("CPU").map(|s| s.to_string_lossy().into_owned());
|
||||
}
|
||||
|
||||
Options { jobs, mode, cpu, patterns, timeout, list }
|
||||
}
|
||||
|
||||
fn canonical_machine(machine: &str) -> String {
|
||||
let machine = machine.trim();
|
||||
if machine == "amd64" {
|
||||
return "x86_64".to_owned();
|
||||
fn matches_target(name: &str, target: &Target) -> bool {
|
||||
match &target.arch {
|
||||
Some(arch) => !name.starts_with("arch-") || name.starts_with(&format!("arch-{arch}-")),
|
||||
None => true,
|
||||
}
|
||||
if machine.len() == 4 && machine.starts_with('i') && machine.ends_with("86") {
|
||||
return "i686".to_owned();
|
||||
}
|
||||
if machine.starts_with("armeb") {
|
||||
return "armeb".to_owned();
|
||||
}
|
||||
if machine.starts_with("arm") {
|
||||
return "arm".to_owned();
|
||||
}
|
||||
match machine {
|
||||
"powerpc" => "ppc".to_owned(),
|
||||
"powerpc64" => "ppc64".to_owned(),
|
||||
"powerpc64le" => "ppc64le".to_owned(),
|
||||
_ => machine.to_owned(),
|
||||
}
|
||||
}
|
||||
|
||||
fn machine_from_triple(triple: &str) -> String {
|
||||
canonical_machine(triple.split('-').next().unwrap_or(triple))
|
||||
}
|
||||
|
||||
fn native_machine() -> String {
|
||||
if let Some(machine) = env::var_os("MACHINE").filter(|s| !s.is_empty()) {
|
||||
return canonical_machine(&machine.to_string_lossy());
|
||||
}
|
||||
|
||||
if let Ok(output) = Command::new("cc").arg("-dumpmachine").output()
|
||||
&& output.status.success()
|
||||
{
|
||||
let triple = String::from_utf8_lossy(&output.stdout);
|
||||
if !triple.trim().is_empty() {
|
||||
return machine_from_triple(&triple);
|
||||
}
|
||||
}
|
||||
canonical_machine(env::consts::ARCH)
|
||||
}
|
||||
|
||||
fn command_exists(command: &str) -> bool {
|
||||
let path = Path::new(command);
|
||||
if path.components().count() > 1 {
|
||||
return path.is_file();
|
||||
}
|
||||
env::var_os("PATH")
|
||||
.is_some_and(|paths| env::split_paths(&paths).any(|dir| dir.join(command).is_file()))
|
||||
}
|
||||
|
||||
fn supports_power10() -> bool {
|
||||
if !command_exists("powerpc64le-linux-gnu-gcc") || !command_exists("qemu-ppc64le") {
|
||||
return false;
|
||||
}
|
||||
|
||||
let compiler = Command::new("powerpc64le-linux-gnu-gcc")
|
||||
.args(["-mcpu=power10", "-E", "-x", "c", "-"])
|
||||
.stdin(Stdio::null())
|
||||
.stdout(Stdio::null())
|
||||
.stderr(Stdio::null())
|
||||
.status()
|
||||
.is_ok_and(|status| status.success());
|
||||
let qemu = Command::new("qemu-ppc64le")
|
||||
.args(["-cpu", "help"])
|
||||
.output()
|
||||
.is_ok_and(|output| String::from_utf8_lossy(&output.stdout).contains("power10_v2.0"));
|
||||
compiler && qemu
|
||||
}
|
||||
|
||||
fn all_targets(native: &str) -> (Vec<Target>, Vec<&'static TargetSpec>) {
|
||||
let mut targets = Vec::new();
|
||||
let mut unavailable = Vec::new();
|
||||
|
||||
for spec in TARGETS {
|
||||
if spec.machine == native {
|
||||
targets.push(Target::native(native.to_owned()));
|
||||
} else if command_exists(spec.qemu) && command_exists(&format!("{}-gcc", spec.triple)) {
|
||||
targets.push(Target::cross(spec.machine.to_owned(), spec.triple.to_owned()));
|
||||
} else {
|
||||
unavailable.push(spec);
|
||||
}
|
||||
}
|
||||
|
||||
if native == "ppc64le"
|
||||
&& fs::read_to_string("/proc/cpuinfo").is_ok_and(|s| s.contains("POWER10"))
|
||||
{
|
||||
let mut target = Target::native(native.to_owned());
|
||||
target.cpu = Some("power10".to_owned());
|
||||
target.label = "ppc64le-power10".to_owned();
|
||||
targets.push(target);
|
||||
} else if supports_power10() {
|
||||
let mut target = Target::cross("ppc64le".to_owned(), "powerpc64le-linux-gnu".to_owned());
|
||||
target.cpu = Some("power10".to_owned());
|
||||
target.label = "ppc64le-power10".to_owned();
|
||||
targets.push(target);
|
||||
}
|
||||
|
||||
(targets, unavailable)
|
||||
}
|
||||
|
||||
fn selected_targets(options: &Options) -> (Vec<Target>, Vec<&'static TargetSpec>) {
|
||||
let native = native_machine();
|
||||
match &options.mode {
|
||||
Mode::Native => (vec![Target::native(native)], Vec::new()),
|
||||
Mode::All => all_targets(&native),
|
||||
Mode::Triple(triple) => {
|
||||
let machine = machine_from_triple(triple);
|
||||
let mut target = Target::cross(machine, triple.clone());
|
||||
target.cpu.clone_from(&options.cpu);
|
||||
if let Some(cpu) = &target.cpu {
|
||||
target.label = format!("{}-{cpu}", target.machine);
|
||||
}
|
||||
(vec![target], Vec::new())
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn matches_target(name: &str, machine: &str) -> bool {
|
||||
!name.starts_with("arch-") || name.starts_with(&format!("arch-{machine}-"))
|
||||
}
|
||||
|
||||
fn matches_patterns(name: &str, patterns: &[String]) -> bool {
|
||||
patterns.is_empty() || patterns.iter().any(|pattern| name.contains(pattern))
|
||||
}
|
||||
|
||||
fn discover_scripts(test_dir: &Path) -> io::Result<Vec<(String, PathBuf)>> {
|
||||
/// The scripts in `dir` that the patterns select, by name.
|
||||
fn selected_scripts(dir: &Path, patterns: &[String]) -> io::Result<Vec<(String, PathBuf)>> {
|
||||
let mut scripts = Vec::new();
|
||||
for entry in fs::read_dir(test_dir)? {
|
||||
for entry in fs::read_dir(dir)? {
|
||||
let path = entry?.path();
|
||||
if path.extension() != Some(OsStr::new("sh")) {
|
||||
continue;
|
||||
}
|
||||
let name = path.file_stem().unwrap().to_string_lossy().into_owned();
|
||||
scripts.push((name, path));
|
||||
if matches_patterns(&name, patterns) {
|
||||
scripts.push((name, path));
|
||||
}
|
||||
}
|
||||
scripts.sort();
|
||||
Ok(scripts)
|
||||
}
|
||||
|
||||
@@ -392,6 +256,13 @@ fn replace_file_link(source: &Path, destination: &Path) -> io::Result<()> {
|
||||
}
|
||||
}
|
||||
|
||||
/// Tests run from a directory beside the linker under test and write
|
||||
/// their outputs under out/test there, so nothing generated lands in the
|
||||
/// source tree. The directory holds the linker under the names the
|
||||
/// scripts invoke it by: mold, ld and ld.lld, which the ELF scripts find
|
||||
/// with ./mold and the compiler's -B. option (upstream LLVM defaults to
|
||||
/// ld.lld on FreeBSD, so overriding only ld is not enough), and
|
||||
/// ld64.mold, the name under which mold acts as the Mach-O linker.
|
||||
fn prepare_work_dir(mold: &Path) -> io::Result<PathBuf> {
|
||||
let mold = mold.canonicalize()?;
|
||||
let profile_dir = mold.parent().ok_or_else(|| {
|
||||
@@ -400,44 +271,29 @@ fn prepare_work_dir(mold: &Path) -> io::Result<PathBuf> {
|
||||
format!("{} has no parent directory", mold.display()),
|
||||
)
|
||||
})?;
|
||||
|
||||
// The test scripts run in a directory containing mold and ld, which
|
||||
// they find with ./mold and the compiler's -B. option, and write their
|
||||
// outputs under out/test there. Create that directory next to the mold
|
||||
// binary so that generated files stay out of the source tree.
|
||||
let work_dir = profile_dir.join("mold-test");
|
||||
fs::create_dir_all(&work_dir)?;
|
||||
|
||||
// Upstream LLVM defaults to ld.lld on FreeBSD, so overriding only ld
|
||||
// with -B is not enough.
|
||||
for name in ["mold", "ld", "ld.lld"] {
|
||||
for name in ["mold", "ld", "ld.lld", "ld64.mold"] {
|
||||
replace_file_link(&mold, &work_dir.join(name))?;
|
||||
}
|
||||
Ok(work_dir)
|
||||
}
|
||||
|
||||
fn make_jobs(
|
||||
cases_dirs: &[PathBuf],
|
||||
scripts: &[(String, PathBuf)],
|
||||
work_dir: &Path,
|
||||
targets: Vec<Target>,
|
||||
patterns: &[String],
|
||||
clean: bool,
|
||||
) -> io::Result<Vec<TestJob>> {
|
||||
let mut scripts = Vec::new();
|
||||
for dir in cases_dirs {
|
||||
scripts.extend(discover_scripts(dir)?);
|
||||
}
|
||||
scripts.sort_by(|a, b| a.0.cmp(&b.0));
|
||||
let mut jobs = Vec::new();
|
||||
|
||||
for target in targets {
|
||||
let target = Arc::new(target);
|
||||
let result_dir = work_dir.join("out/test/results").join(&target.label);
|
||||
if clean {
|
||||
clear_results(&result_dir)?;
|
||||
}
|
||||
for (name, script) in &scripts {
|
||||
if matches_target(name, &target.machine) && matches_patterns(name, patterns) {
|
||||
for (name, script) in scripts {
|
||||
if matches_target(name, &target) {
|
||||
jobs.push(TestJob {
|
||||
target: Arc::clone(&target),
|
||||
script: script.clone(),
|
||||
@@ -451,11 +307,12 @@ fn make_jobs(
|
||||
Ok(jobs)
|
||||
}
|
||||
|
||||
/// A script that skips itself ends a line of its log with "skipped".
|
||||
fn log_says_skipped(path: &Path) -> bool {
|
||||
fs::read(path).is_ok_and(|bytes| {
|
||||
bytes
|
||||
.split(|&byte| byte == b'\n')
|
||||
.any(|line| line.strip_suffix(b"\r").unwrap_or(line) == b"skipped")
|
||||
.any(|line| line.strip_suffix(b"\r").unwrap_or(line).ends_with(b"skipped"))
|
||||
})
|
||||
}
|
||||
|
||||
@@ -465,23 +322,21 @@ fn run_process(root: &Path, job: &TestJob, timeout: Duration) -> Result<Outcome,
|
||||
let stderr =
|
||||
log.try_clone().map_err(|err| format!("cannot clone {}: {err}", job.log.display()))?;
|
||||
let mut command = Command::new(&job.script);
|
||||
// A script that read the terminal would stop in its background process
|
||||
// group until the timeout; give it end-of-file instead.
|
||||
command
|
||||
.current_dir(root)
|
||||
.stdin(Stdio::null())
|
||||
.env("MACHINE", &job.target.machine)
|
||||
.stdout(Stdio::from(log))
|
||||
.stderr(Stdio::from(stderr));
|
||||
for (name, value) in [("TRIPLE", &job.target.triple), ("CPU", &job.target.cpu)] {
|
||||
if let Some(value) = value {
|
||||
command.env(name, value);
|
||||
} else {
|
||||
command.env_remove(name);
|
||||
}
|
||||
command.current_dir(root).stdout(Stdio::from(log)).stderr(Stdio::from(stderr));
|
||||
for (name, value) in &job.target.env {
|
||||
match value {
|
||||
Some(value) => command.env(name, value),
|
||||
None => command.env_remove(name),
|
||||
};
|
||||
}
|
||||
|
||||
// A timeout must also kill compiler and QEMU children.
|
||||
// A timeout must also kill compiler and QEMU children, so a test runs
|
||||
// in its own process group. That is a background group of the
|
||||
// terminal cargo was started from, so the test must not inherit the
|
||||
// terminal as stdin: a program that reads it or restores its settings
|
||||
// on exit (lldb does, even in batch mode) is stopped by SIGTTIN or
|
||||
// SIGTTOU and hangs until the timeout.
|
||||
command.stdin(Stdio::null());
|
||||
#[cfg(unix)]
|
||||
command.process_group(0);
|
||||
let mut child =
|
||||
@@ -500,8 +355,7 @@ fn run_process(root: &Path, job: &TestJob, timeout: Duration) -> Result<Outcome,
|
||||
}
|
||||
None if start.elapsed() < timeout => thread::sleep(Duration::from_millis(20)),
|
||||
None => {
|
||||
// SAFETY: kill has no memory-safety preconditions; the group
|
||||
// id is the pid of the still-unreaped child.
|
||||
// SAFETY: kill only signals the test's own process group.
|
||||
#[cfg(unix)]
|
||||
unsafe {
|
||||
libc::kill(-(child.id() as i32), libc::SIGKILL);
|
||||
@@ -593,9 +447,17 @@ fn run_jobs(root: &Path, jobs: Vec<TestJob>, options: &Options) -> Vec<TestResul
|
||||
})
|
||||
}
|
||||
|
||||
fn print_inventory(jobs: &[TestJob], unavailable: &[&TargetSpec]) {
|
||||
/// Lists the tests the options select, by target, instead of running
|
||||
/// them, and the targets left out for want of a toolchain.
|
||||
fn print_inventory(
|
||||
scripts: &[(String, PathBuf)],
|
||||
work_dir: &Path,
|
||||
targets: Vec<Target>,
|
||||
unavailable: &[String],
|
||||
) -> ExitCode {
|
||||
let jobs = make_jobs(scripts, work_dir, targets, false).unwrap_or_else(|err| fail(err));
|
||||
let mut counts = BTreeMap::new();
|
||||
for job in jobs {
|
||||
for job in &jobs {
|
||||
*counts.entry(job.target.label.as_str()).or_insert(0usize) += 1;
|
||||
}
|
||||
for (target, count) in counts {
|
||||
@@ -603,13 +465,12 @@ fn print_inventory(jobs: &[TestJob], unavailable: &[&TargetSpec]) {
|
||||
}
|
||||
println!("total: tests={}", jobs.len());
|
||||
if !unavailable.is_empty() {
|
||||
let targets =
|
||||
unavailable.iter().map(|target| target.machine).collect::<Vec<_>>().join(", ");
|
||||
println!("unavailable: {targets}");
|
||||
println!("unavailable: {}", unavailable.join(", "));
|
||||
}
|
||||
ExitCode::SUCCESS
|
||||
}
|
||||
|
||||
fn print_summary(results: &[TestResult]) -> bool {
|
||||
fn print_summary(results: &[TestResult]) -> ExitCode {
|
||||
let mut by_target: BTreeMap<&str, Counts> = BTreeMap::new();
|
||||
for result in results {
|
||||
by_target.entry(&result.target.label).or_default().add(result.outcome);
|
||||
@@ -625,39 +486,7 @@ fn print_summary(results: &[TestResult]) -> bool {
|
||||
} else {
|
||||
println!("pass={} skip={} fail={}", total.pass, total.skip, total.fail);
|
||||
}
|
||||
total.fail == 0
|
||||
}
|
||||
|
||||
pub fn run(cases_dirs: &[PathBuf], mold: &Path) -> ExitCode {
|
||||
let options = parse_options();
|
||||
let work_dir = match prepare_work_dir(mold) {
|
||||
Ok(dir) => dir,
|
||||
Err(err) => {
|
||||
eprintln!("mold-elf-tests: {err}");
|
||||
return ExitCode::FAILURE;
|
||||
}
|
||||
};
|
||||
let (targets, unavailable) = selected_targets(&options);
|
||||
let jobs = match make_jobs(cases_dirs, &work_dir, targets, &options.patterns, !options.list) {
|
||||
Ok(jobs) => jobs,
|
||||
Err(err) => {
|
||||
eprintln!("mold-elf-tests: {err}");
|
||||
return ExitCode::FAILURE;
|
||||
}
|
||||
};
|
||||
|
||||
if options.list {
|
||||
print_inventory(&jobs, &unavailable);
|
||||
return ExitCode::SUCCESS;
|
||||
}
|
||||
if options.mode == Mode::All && !unavailable.is_empty() {
|
||||
let targets =
|
||||
unavailable.iter().map(|target| target.machine).collect::<Vec<_>>().join(", ");
|
||||
eprintln!("skipping targets without both compiler and QEMU: {targets}");
|
||||
}
|
||||
|
||||
let results = run_jobs(&work_dir, jobs, &options);
|
||||
if print_summary(&results) { ExitCode::SUCCESS } else { ExitCode::FAILURE }
|
||||
if total.fail == 0 { ExitCode::SUCCESS } else { ExitCode::FAILURE }
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
@@ -665,18 +494,19 @@ mod tests {
|
||||
use super::*;
|
||||
|
||||
#[test]
|
||||
fn canonicalizes_machine_names() {
|
||||
assert_eq!(canonical_machine("amd64"), "x86_64");
|
||||
assert_eq!(canonical_machine("i386"), "i686");
|
||||
assert_eq!(canonical_machine("armv7l"), "arm");
|
||||
assert_eq!(canonical_machine("aarch64"), "aarch64");
|
||||
assert_eq!(canonical_machine("powerpc64le"), "ppc64le");
|
||||
fn selects_tests_by_substring() {
|
||||
assert!(matches_patterns("dead-strip", &[]));
|
||||
assert!(matches_patterns("dead-strip", &["strip".to_owned()]));
|
||||
assert!(!matches_patterns("hello", &["strip".to_owned()]));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn selects_generic_and_target_tests() {
|
||||
assert!(matches_target("gc-sections", "aarch64"));
|
||||
assert!(matches_target("arch-aarch64-reloc", "aarch64"));
|
||||
assert!(!matches_target("arch-x86_64-reloc", "aarch64"));
|
||||
let target = Target { label: String::new(), arch: Some("aarch64".to_owned()), env: vec![] };
|
||||
assert!(matches_target("gc-sections", &target));
|
||||
assert!(matches_target("arch-aarch64-reloc", &target));
|
||||
assert!(!matches_target("arch-x86_64-reloc", &target));
|
||||
let target = Target { arch: None, ..target };
|
||||
assert!(matches_target("arch-x86_64-reloc", &target));
|
||||
}
|
||||
}
|
||||
+246
@@ -0,0 +1,246 @@
|
||||
//! The Mach-O tests' targets. Tests run natively on the host, on an arm64
|
||||
//! host also for x86_64 under Rosetta, and for a simulator triple on a
|
||||
//! simulator device, which runs the programs as QEMU runs a cross
|
||||
//! target's in the ELF tests.
|
||||
|
||||
use std::path::Path;
|
||||
use std::process::{Command, ExitCode, Stdio};
|
||||
|
||||
use crate::simulator::{self, Device, Runtime, Simulator};
|
||||
use crate::{
|
||||
Mode, Options, Target, fail, make_jobs, parse_options, prepare_work_dir, print_inventory,
|
||||
print_summary, run_jobs, selected_scripts, usage,
|
||||
};
|
||||
|
||||
/// The simulator triples a run adds to the host's targets by default, and
|
||||
/// those --all adds too, each when a runtime runs programs of its
|
||||
/// architecture (an x86_64 one also needs Rosetta). Only an older runtime
|
||||
/// runs x86_64 programs, iOS 17's.
|
||||
const DEFAULT_SIMULATORS: &[&str] = &["arm64-apple-ios-simulator"];
|
||||
const MORE_SIMULATORS: &[&str] =
|
||||
&["x86_64-apple-ios-simulator", "arm64-apple-tvos-simulator", "arm64-apple-xros-simulator"];
|
||||
|
||||
/// A target, with the simulator runtime that runs its programs if it
|
||||
/// isn't the host's. A host target builds and runs macOS programs; a
|
||||
/// simulator target builds for the triple (whose OS version is that of
|
||||
/// the newest runtime, unless the triple names one) and runs the programs
|
||||
/// on a device of the runtime.
|
||||
struct Config {
|
||||
target: Target,
|
||||
runtime: Option<Runtime>,
|
||||
}
|
||||
|
||||
fn host(arch: &str, linker: &Path) -> Config {
|
||||
let target = Target {
|
||||
label: arch.to_owned(),
|
||||
arch: None,
|
||||
env: vec![
|
||||
("mold", Some(linker.display().to_string())),
|
||||
("ARCH", Some(arch.to_owned())),
|
||||
("TRIPLE", None),
|
||||
("SIMULATOR", None),
|
||||
],
|
||||
};
|
||||
Config { target, runtime: None }
|
||||
}
|
||||
|
||||
fn simulator(simulator: &Simulator, runtime: Runtime, linker: &Path) -> Config {
|
||||
let triple = format!("{}-apple-{}{}-simulator", simulator.arch, simulator.os, runtime.version);
|
||||
let target = Target {
|
||||
label: simulator.label(),
|
||||
arch: None,
|
||||
env: vec![
|
||||
("mold", Some(linker.display().to_string())),
|
||||
("ARCH", Some(simulator.arch.clone())),
|
||||
("TRIPLE", Some(triple)),
|
||||
],
|
||||
};
|
||||
Config { target, runtime: Some(runtime) }
|
||||
}
|
||||
|
||||
fn native_arch() -> &'static str {
|
||||
if cfg!(target_arch = "aarch64") { "arm64" } else { "x86_64" }
|
||||
}
|
||||
|
||||
/// Whether x86_64 binaries run on this arm64 host.
|
||||
fn rosetta_available() -> bool {
|
||||
Command::new("arch")
|
||||
.args(["-x86_64", "/usr/bin/true"])
|
||||
.stdout(Stdio::null())
|
||||
.stderr(Stdio::null())
|
||||
.status()
|
||||
.is_ok_and(|status| status.success())
|
||||
}
|
||||
|
||||
/// Returns the targets to run and the simulator triples left out for want
|
||||
/// of a runtime (as on a machine without Xcode's simulators).
|
||||
fn selected_targets(options: &Options, linker: &Path) -> (Vec<Config>, Vec<String>) {
|
||||
let native = native_arch();
|
||||
let mut targets = vec![host(native, linker)];
|
||||
let mut unavailable = Vec::new();
|
||||
match &options.mode {
|
||||
Mode::Native | Mode::Default | Mode::All => {
|
||||
let rosetta = native == "arm64" && rosetta_available();
|
||||
if rosetta {
|
||||
targets.push(host("x86_64", linker));
|
||||
}
|
||||
let simulators: &[&[&str]] = match options.mode {
|
||||
Mode::Default => &[DEFAULT_SIMULATORS],
|
||||
Mode::All => &[DEFAULT_SIMULATORS, MORE_SIMULATORS],
|
||||
_ => &[],
|
||||
};
|
||||
for triple in simulators.concat() {
|
||||
let sim = Simulator::parse(triple).unwrap();
|
||||
match simulator::find_runtime(&sim) {
|
||||
Some(runtime) if sim.arch == native || rosetta => {
|
||||
targets.push(simulator(&sim, runtime, linker))
|
||||
}
|
||||
_ => unavailable.push(triple.to_owned()),
|
||||
}
|
||||
}
|
||||
}
|
||||
Mode::Triple(triple) => {
|
||||
let Some(sim) = Simulator::parse(triple) else {
|
||||
eprintln!("mold-tests: {triple}: not a simulator triple");
|
||||
usage();
|
||||
};
|
||||
let Some(runtime) = simulator::find_runtime(&sim) else {
|
||||
fail(format!("no simulator runtime for {triple}"));
|
||||
};
|
||||
targets = vec![simulator(&sim, runtime, linker)];
|
||||
}
|
||||
}
|
||||
(targets, unavailable)
|
||||
}
|
||||
|
||||
/// Boots a device for each simulator target, which its scripts run their
|
||||
/// programs on. Dropping a device shuts it down.
|
||||
fn boot_devices(configs: Vec<Config>) -> Result<(Vec<Target>, Vec<Device>), String> {
|
||||
let mut targets = Vec::new();
|
||||
let mut devices = Vec::new();
|
||||
for Config { mut target, runtime } in configs {
|
||||
if let Some(runtime) = &runtime {
|
||||
let device = Device::boot(runtime)?;
|
||||
target.env.push(("SIMULATOR", Some(device.udid.clone())));
|
||||
devices.push(device);
|
||||
}
|
||||
targets.push(target);
|
||||
}
|
||||
Ok((targets, devices))
|
||||
}
|
||||
|
||||
/// Runs the Mach-O test scripts in `cases_dir` with the linker at `mold`.
|
||||
/// Command line arguments are substring patterns selecting a subset of
|
||||
/// tests.
|
||||
pub fn run(cases_dir: &Path, mold: &Path) -> ExitCode {
|
||||
let options = parse_options();
|
||||
let work_dir = prepare_work_dir(mold).unwrap_or_else(|err| fail(err));
|
||||
let linker = work_dir.join("ld64.mold");
|
||||
let (configs, unavailable) = selected_targets(&options, &linker);
|
||||
let scripts = selected_scripts(cases_dir, &options.patterns).unwrap_or_else(|err| fail(err));
|
||||
if options.list {
|
||||
let targets = configs.into_iter().map(|config| config.target).collect();
|
||||
return print_inventory(&scripts, &work_dir, targets, &unavailable);
|
||||
}
|
||||
if !unavailable.is_empty() {
|
||||
eprintln!("skipping simulators without a runtime: {}", unavailable.join(", "));
|
||||
}
|
||||
|
||||
// The host's targets run together, then each simulator's on its own,
|
||||
// its device booted for it alone and shut down once it is done: a
|
||||
// booted simulator is a whole OS, and one at a time is load enough.
|
||||
// Errors return rather than exit, so that a device is shut down.
|
||||
let (hosts, simulators): (Vec<_>, Vec<_>) =
|
||||
configs.into_iter().partition(|config| config.runtime.is_none());
|
||||
let mut results = Vec::new();
|
||||
for phase in std::iter::once(hosts).chain(simulators.into_iter().map(|config| vec![config])) {
|
||||
if phase.is_empty() || scripts.is_empty() {
|
||||
continue;
|
||||
}
|
||||
let (targets, devices) = match boot_devices(phase) {
|
||||
Ok(booted) => booted,
|
||||
Err(err) => {
|
||||
eprintln!("mold-tests: {err}");
|
||||
return ExitCode::FAILURE;
|
||||
}
|
||||
};
|
||||
let jobs = match make_jobs(&scripts, &work_dir, targets, true) {
|
||||
Ok(jobs) => jobs,
|
||||
Err(err) => {
|
||||
eprintln!("mold-tests: {err}");
|
||||
return ExitCode::FAILURE;
|
||||
}
|
||||
};
|
||||
results.extend(run_jobs(&work_dir, jobs, &options));
|
||||
drop(devices);
|
||||
}
|
||||
print_summary(&results)
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use std::env;
|
||||
use std::fs;
|
||||
use std::process::Output;
|
||||
use std::sync::atomic::{AtomicUsize, Ordering};
|
||||
|
||||
use super::*;
|
||||
|
||||
fn run_script(body: &str) -> Output {
|
||||
static NEXT: AtomicUsize = AtomicUsize::new(0);
|
||||
let dir = env::temp_dir().join(format!(
|
||||
"mold-harness-{}-{}",
|
||||
std::process::id(),
|
||||
NEXT.fetch_add(1, Ordering::Relaxed)
|
||||
));
|
||||
fs::create_dir(&dir).unwrap();
|
||||
let common = Path::new(env!("CARGO_MANIFEST_DIR")).join("macho/common.inc");
|
||||
let output = Command::new("bash")
|
||||
.args(["-c", &format!("source \"$1\"\n{body}"), "harness-test"])
|
||||
.arg(common)
|
||||
.env("mold", "unused")
|
||||
.current_dir(&dir)
|
||||
.output()
|
||||
.unwrap();
|
||||
fs::remove_dir_all(dir).unwrap();
|
||||
output
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn negated_failure_at_exit_is_not_success() {
|
||||
let output = run_script("! true");
|
||||
assert!(!output.status.success());
|
||||
assert!(!String::from_utf8_lossy(&output.stdout).contains("OK"));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn negative_assertion_stops_before_later_commands() {
|
||||
let output = run_script("not true\necho reached");
|
||||
assert!(!output.status.success());
|
||||
let stdout = String::from_utf8_lossy(&output.stdout);
|
||||
assert!(!stdout.contains("reached"));
|
||||
assert!(!stdout.contains("OK"));
|
||||
assert!(String::from_utf8_lossy(&output.stderr).contains("unexpectedly succeeded"));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn explicit_exit_status_is_preserved() {
|
||||
assert_eq!(run_script("exit 7").status.code(), Some(7));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn expected_failure_and_success_pass() {
|
||||
let output = run_script("not false\ntrue");
|
||||
assert!(output.status.success());
|
||||
assert!(String::from_utf8_lossy(&output.stdout).contains("OK"));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn skip_remains_successful() {
|
||||
let output = run_script("skip");
|
||||
assert!(output.status.success());
|
||||
let stdout = String::from_utf8_lossy(&output.stdout);
|
||||
assert!(stdout.contains("skipped"));
|
||||
assert!(!stdout.contains("OK"));
|
||||
}
|
||||
}
|
||||
@@ -1,696 +0,0 @@
|
||||
//! Runs mold's shell tests in parallel for Cargo's test harness.
|
||||
//!
|
||||
//! The tests themselves deliberately remain shell scripts so that this port
|
||||
//! exercises exactly the same inputs and toolchains as the system linker.
|
||||
//! The runner owns test discovery, target selection, scheduling, timeouts
|
||||
//! and reporting. Tests run natively on the host, on an arm64 host also
|
||||
//! for x86_64 under Rosetta, and for a simulator triple on a simulator
|
||||
//! device, which runs the programs as QEMU runs a cross target's in mold's
|
||||
//! ELF suite.
|
||||
|
||||
// The scripts drive Apple's toolchain, so they run only on macOS.
|
||||
// Elsewhere this crate is empty, which lets the workspace build there.
|
||||
#![cfg(target_os = "macos")]
|
||||
|
||||
mod simulator;
|
||||
|
||||
use simulator::{Device, Runtime, Simulator};
|
||||
use std::collections::BTreeMap;
|
||||
use std::env;
|
||||
use std::ffi::OsStr;
|
||||
use std::fs::{self, File};
|
||||
use std::io;
|
||||
use std::iter;
|
||||
use std::os::unix::process::CommandExt;
|
||||
use std::path::{Path, PathBuf};
|
||||
use std::process::{Command, ExitCode, Stdio};
|
||||
use std::sync::atomic::{AtomicUsize, Ordering};
|
||||
use std::sync::{Arc, mpsc};
|
||||
use std::thread;
|
||||
use std::time::{Duration, Instant};
|
||||
|
||||
const DEFAULT_TIMEOUT: Duration = Duration::from_secs(60);
|
||||
|
||||
/// The simulator configurations a run adds to the host's by default,
|
||||
/// and those --all adds too, each when a runtime runs programs of its
|
||||
/// architecture (an x86_64 one also needs Rosetta). Only an older
|
||||
/// runtime runs x86_64 programs, iOS 17's.
|
||||
const DEFAULT_SIMULATORS: &[&str] = &["arm64-apple-ios-simulator"];
|
||||
const MORE_SIMULATORS: &[&str] =
|
||||
&["x86_64-apple-ios-simulator", "arm64-apple-tvos-simulator", "arm64-apple-xros-simulator"];
|
||||
|
||||
/// A configuration the scripts run in. A host configuration builds and
|
||||
/// runs macOS programs; a simulator configuration builds for the target
|
||||
/// triple (whose OS version is that of the newest runtime, unless the
|
||||
/// triple names one) and runs the programs on a device of the runtime.
|
||||
struct Target {
|
||||
arch: String,
|
||||
triple: Option<String>,
|
||||
runtime: Option<Runtime>,
|
||||
/// The UDID of the booted device that runs the programs.
|
||||
device: Option<String>,
|
||||
label: String,
|
||||
}
|
||||
|
||||
impl Target {
|
||||
fn host(arch: &str) -> Self {
|
||||
Self {
|
||||
arch: arch.to_owned(),
|
||||
triple: None,
|
||||
runtime: None,
|
||||
device: None,
|
||||
label: arch.to_owned(),
|
||||
}
|
||||
}
|
||||
|
||||
fn simulator(simulator: &Simulator, runtime: Runtime) -> Self {
|
||||
let triple =
|
||||
format!("{}-apple-{}{}-simulator", simulator.arch, simulator.os, runtime.version);
|
||||
Self {
|
||||
arch: simulator.arch.clone(),
|
||||
triple: Some(triple),
|
||||
runtime: Some(runtime),
|
||||
device: None,
|
||||
label: simulator.label(),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug, PartialEq, Eq)]
|
||||
enum Mode {
|
||||
/// The host architecture only.
|
||||
Native,
|
||||
/// The host architecture, plus x86_64 under Rosetta when available.
|
||||
Host,
|
||||
/// The host's configurations plus DEFAULT_SIMULATORS.
|
||||
Default,
|
||||
/// The host's configurations plus every simulator's.
|
||||
All,
|
||||
/// One simulator configuration.
|
||||
Triple(String),
|
||||
}
|
||||
|
||||
struct Options {
|
||||
jobs: usize,
|
||||
mode: Mode,
|
||||
patterns: Vec<String>,
|
||||
timeout: Duration,
|
||||
list: bool,
|
||||
}
|
||||
|
||||
struct TestJob {
|
||||
target: Arc<Target>,
|
||||
script: PathBuf,
|
||||
name: String,
|
||||
log: PathBuf,
|
||||
status_file: PathBuf,
|
||||
}
|
||||
|
||||
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
|
||||
enum Outcome {
|
||||
Pass,
|
||||
Skip,
|
||||
Fail,
|
||||
Timeout,
|
||||
}
|
||||
|
||||
impl Outcome {
|
||||
fn status(self) -> &'static str {
|
||||
match self {
|
||||
Self::Pass => "pass",
|
||||
Self::Skip => "skip",
|
||||
Self::Fail | Self::Timeout => "fail",
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
struct TestResult {
|
||||
target: Arc<Target>,
|
||||
name: String,
|
||||
log: PathBuf,
|
||||
outcome: Outcome,
|
||||
}
|
||||
|
||||
#[derive(Default)]
|
||||
struct Counts {
|
||||
pass: usize,
|
||||
skip: usize,
|
||||
fail: usize,
|
||||
}
|
||||
|
||||
impl Counts {
|
||||
fn add(&mut self, outcome: Outcome) {
|
||||
match outcome {
|
||||
Outcome::Pass => self.pass += 1,
|
||||
Outcome::Skip => self.skip += 1,
|
||||
Outcome::Fail | Outcome::Timeout => self.fail += 1,
|
||||
}
|
||||
}
|
||||
|
||||
fn merge(&mut self, other: &Self) {
|
||||
self.pass += other.pass;
|
||||
self.skip += other.skip;
|
||||
self.fail += other.fail;
|
||||
}
|
||||
}
|
||||
|
||||
fn usage() -> ! {
|
||||
eprintln!(
|
||||
"Usage: cargo test [pattern] [-- [--test-threads N] \
|
||||
[--native | --host | --all | --triple TRIPLE] [--timeout SECONDS] [--list]]\n\
|
||||
By default the tests run for macOS (arm64, and x86_64 under Rosetta) and the arm64 \
|
||||
iOS simulator. --all adds the x86_64 iOS, tvOS and visionOS simulators; --host runs \
|
||||
macOS's alone, --native the host architecture's, --triple one simulator's (e.g. \
|
||||
arm64-apple-tvos-simulator)."
|
||||
);
|
||||
std::process::exit(2);
|
||||
}
|
||||
|
||||
fn parse_usize(value: Option<String>) -> usize {
|
||||
value.and_then(|s| s.parse().ok()).filter(|&n| n != 0).unwrap_or_else(|| usage())
|
||||
}
|
||||
|
||||
fn parse_options() -> Options {
|
||||
let mut jobs = thread::available_parallelism().map_or(1, usize::from);
|
||||
let mut mode = Mode::Default;
|
||||
let mut mode_was_set = false;
|
||||
let mut patterns = Vec::new();
|
||||
let mut timeout = DEFAULT_TIMEOUT;
|
||||
let mut list = false;
|
||||
|
||||
let mut args = env::args().skip(1);
|
||||
while let Some(arg) = args.next() {
|
||||
match arg.as_str() {
|
||||
"-j" | "--jobs" | "--test-threads" => jobs = parse_usize(args.next()),
|
||||
"--native" => {
|
||||
mode = Mode::Native;
|
||||
mode_was_set = true;
|
||||
}
|
||||
"--host" => {
|
||||
mode = Mode::Host;
|
||||
mode_was_set = true;
|
||||
}
|
||||
"--all" => {
|
||||
mode = Mode::All;
|
||||
mode_was_set = true;
|
||||
}
|
||||
"--triple" => {
|
||||
mode = Mode::Triple(args.next().unwrap_or_else(|| usage()));
|
||||
mode_was_set = true;
|
||||
}
|
||||
"--timeout" => timeout = Duration::from_secs(parse_usize(args.next()) as u64),
|
||||
"--list" => list = true,
|
||||
"--nocapture" | "--show-output" => {}
|
||||
"-h" | "--help" => usage(),
|
||||
_ if arg.starts_with("-j") && arg.len() > 2 => {
|
||||
jobs = parse_usize(Some(arg[2..].to_owned()))
|
||||
}
|
||||
_ if arg.starts_with("--test-threads=") => {
|
||||
jobs = parse_usize(arg.split_once('=').map(|(_, value)| value.to_owned()))
|
||||
}
|
||||
_ if arg.starts_with('-') => usage(),
|
||||
_ => patterns.push(arg),
|
||||
}
|
||||
}
|
||||
|
||||
// As in mold's ELF suite, TRIPLE in the environment selects the
|
||||
// configuration too.
|
||||
if !mode_was_set && let Some(triple) = env::var_os("TRIPLE").filter(|s| !s.is_empty()) {
|
||||
mode = Mode::Triple(triple.to_string_lossy().into_owned());
|
||||
}
|
||||
|
||||
Options { jobs, mode, patterns, timeout, list }
|
||||
}
|
||||
|
||||
fn native_arch() -> &'static str {
|
||||
if cfg!(target_arch = "aarch64") { "arm64" } else { "x86_64" }
|
||||
}
|
||||
|
||||
/// Whether x86_64 binaries run on this arm64 host.
|
||||
fn rosetta_available() -> bool {
|
||||
Command::new("arch")
|
||||
.args(["-x86_64", "/usr/bin/true"])
|
||||
.stdout(Stdio::null())
|
||||
.stderr(Stdio::null())
|
||||
.status()
|
||||
.is_ok_and(|status| status.success())
|
||||
}
|
||||
|
||||
/// Returns the configurations to run and the simulator triples that a
|
||||
/// run skips, silently, for want of a runtime (as on a machine without
|
||||
/// Xcode's simulators).
|
||||
fn selected_targets(options: &Options) -> (Vec<Target>, Vec<&'static str>) {
|
||||
let native = native_arch();
|
||||
let mut targets = vec![Target::host(native)];
|
||||
let mut unavailable = Vec::new();
|
||||
match &options.mode {
|
||||
Mode::Native => {}
|
||||
Mode::Host | Mode::Default | Mode::All => {
|
||||
let rosetta = native == "arm64" && rosetta_available();
|
||||
if rosetta {
|
||||
targets.push(Target::host("x86_64"));
|
||||
}
|
||||
let simulators: &[&[&str]] = match options.mode {
|
||||
Mode::Default => &[DEFAULT_SIMULATORS],
|
||||
Mode::All => &[DEFAULT_SIMULATORS, MORE_SIMULATORS],
|
||||
_ => &[],
|
||||
};
|
||||
for triple in simulators.concat() {
|
||||
let simulator = Simulator::parse(triple).unwrap();
|
||||
match simulator::find_runtime(&simulator) {
|
||||
Some(runtime) if simulator.arch == native || rosetta => {
|
||||
targets.push(Target::simulator(&simulator, runtime))
|
||||
}
|
||||
_ => unavailable.push(triple),
|
||||
}
|
||||
}
|
||||
}
|
||||
Mode::Triple(triple) => {
|
||||
let Some(simulator) = Simulator::parse(triple) else {
|
||||
eprintln!("mold-macho-tests: {triple}: not a simulator triple");
|
||||
usage();
|
||||
};
|
||||
let Some(runtime) = simulator::find_runtime(&simulator) else {
|
||||
eprintln!("mold-macho-tests: no simulator runtime for {triple}");
|
||||
std::process::exit(1);
|
||||
};
|
||||
targets = vec![Target::simulator(&simulator, runtime)];
|
||||
}
|
||||
}
|
||||
(targets, unavailable)
|
||||
}
|
||||
|
||||
/// Boots a device for each simulator configuration. Dropping a device
|
||||
/// shuts it down.
|
||||
fn boot_devices(targets: &mut [Target]) -> Result<Vec<Device>, String> {
|
||||
let mut devices = Vec::new();
|
||||
for target in targets {
|
||||
if let Some(runtime) = &target.runtime {
|
||||
let device = Device::boot(runtime)?;
|
||||
target.device = Some(device.udid.clone());
|
||||
devices.push(device);
|
||||
}
|
||||
}
|
||||
Ok(devices)
|
||||
}
|
||||
|
||||
fn matches_patterns(name: &str, patterns: &[String]) -> bool {
|
||||
patterns.is_empty() || patterns.iter().any(|pattern| name.contains(pattern))
|
||||
}
|
||||
|
||||
fn discover_scripts(test_dir: &Path) -> io::Result<Vec<(String, PathBuf)>> {
|
||||
let mut scripts = Vec::new();
|
||||
for entry in fs::read_dir(test_dir)? {
|
||||
let path = entry?.path();
|
||||
if path.extension() != Some(OsStr::new("sh")) {
|
||||
continue;
|
||||
}
|
||||
let name = path.file_stem().unwrap().to_string_lossy().into_owned();
|
||||
scripts.push((name, path));
|
||||
}
|
||||
scripts.sort_by(|a, b| a.0.cmp(&b.0));
|
||||
Ok(scripts)
|
||||
}
|
||||
|
||||
fn clear_results(dir: &Path) -> io::Result<()> {
|
||||
fs::create_dir_all(dir)?;
|
||||
for entry in fs::read_dir(dir)? {
|
||||
let path = entry?.path();
|
||||
if matches!(path.extension().and_then(OsStr::to_str), Some("log" | "status")) {
|
||||
fs::remove_file(path)?;
|
||||
}
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Tests run from a directory beside the linker under test and write
|
||||
/// their outputs under out/test there, so nothing generated lands in the
|
||||
/// source tree. The directory also holds a symlink to the linker named
|
||||
/// ld64.mold, the name under which mold acts as the Mach-O linker.
|
||||
fn prepare_work_dir(mold: &Path) -> io::Result<PathBuf> {
|
||||
let mold = mold.canonicalize()?;
|
||||
let profile_dir = mold.parent().ok_or_else(|| {
|
||||
io::Error::new(
|
||||
io::ErrorKind::InvalidInput,
|
||||
format!("{} has no parent directory", mold.display()),
|
||||
)
|
||||
})?;
|
||||
let work_dir = profile_dir.join("mold-test");
|
||||
fs::create_dir_all(&work_dir)?;
|
||||
|
||||
let link = work_dir.join("ld64.mold");
|
||||
match fs::remove_file(&link) {
|
||||
Ok(()) => {}
|
||||
Err(err) if err.kind() == io::ErrorKind::NotFound => {}
|
||||
Err(err) => return Err(err),
|
||||
}
|
||||
std::os::unix::fs::symlink(&mold, &link)?;
|
||||
Ok(work_dir)
|
||||
}
|
||||
|
||||
/// The scripts in `cases_dirs` that the patterns select, by name.
|
||||
fn selected_scripts(
|
||||
cases_dirs: &[PathBuf],
|
||||
patterns: &[String],
|
||||
) -> io::Result<Vec<(String, PathBuf)>> {
|
||||
let mut scripts = Vec::new();
|
||||
for dir in cases_dirs {
|
||||
scripts.extend(discover_scripts(dir)?);
|
||||
}
|
||||
scripts.retain(|(name, _)| matches_patterns(name, patterns));
|
||||
scripts.sort_by(|a, b| a.0.cmp(&b.0));
|
||||
Ok(scripts)
|
||||
}
|
||||
|
||||
fn make_jobs(
|
||||
scripts: &[(String, PathBuf)],
|
||||
work_dir: &Path,
|
||||
targets: Vec<Target>,
|
||||
clean: bool,
|
||||
) -> io::Result<Vec<TestJob>> {
|
||||
let mut jobs = Vec::new();
|
||||
for target in targets {
|
||||
let target = Arc::new(target);
|
||||
let result_dir = work_dir.join("out/test/results").join(&target.label);
|
||||
if clean {
|
||||
clear_results(&result_dir)?;
|
||||
}
|
||||
for (name, script) in scripts {
|
||||
jobs.push(TestJob {
|
||||
target: Arc::clone(&target),
|
||||
script: script.clone(),
|
||||
name: name.clone(),
|
||||
log: result_dir.join(format!("{name}.log")),
|
||||
status_file: result_dir.join(format!("{name}.status")),
|
||||
});
|
||||
}
|
||||
}
|
||||
Ok(jobs)
|
||||
}
|
||||
|
||||
/// A script that skips itself ends its startup line with "skipped".
|
||||
fn log_says_skipped(path: &Path) -> bool {
|
||||
fs::read(path).is_ok_and(|bytes| {
|
||||
bytes
|
||||
.split(|&byte| byte == b'\n')
|
||||
.any(|line| line.strip_suffix(b"\r").unwrap_or(line).ends_with(b"skipped"))
|
||||
})
|
||||
}
|
||||
|
||||
fn run_process(
|
||||
root: &Path,
|
||||
job: &TestJob,
|
||||
linker: &Path,
|
||||
timeout: Duration,
|
||||
) -> Result<Outcome, String> {
|
||||
let log = File::create(&job.log)
|
||||
.map_err(|err| format!("cannot create {}: {err}", job.log.display()))?;
|
||||
let stderr =
|
||||
log.try_clone().map_err(|err| format!("cannot clone {}: {err}", job.log.display()))?;
|
||||
let mut command = Command::new(&job.script);
|
||||
command
|
||||
.current_dir(root)
|
||||
.env("mold", linker)
|
||||
.env("ARCH", &job.target.arch)
|
||||
.stdin(Stdio::null())
|
||||
.stdout(Stdio::from(log))
|
||||
.stderr(Stdio::from(stderr));
|
||||
for (name, value) in [("TRIPLE", &job.target.triple), ("SIMULATOR", &job.target.device)] {
|
||||
if let Some(value) = value {
|
||||
command.env(name, value);
|
||||
} else {
|
||||
command.env_remove(name);
|
||||
}
|
||||
}
|
||||
|
||||
// A timeout must also kill compiler children. The test runs in
|
||||
// its own process group, which is a background group of the
|
||||
// terminal cargo was started from, so it must not inherit that
|
||||
// terminal as stdin: a program that reads it or restores its
|
||||
// settings on exit (lldb does, even in batch mode) is stopped by
|
||||
// SIGTTIN or SIGTTOU and hangs until the timeout.
|
||||
command.process_group(0);
|
||||
let mut child =
|
||||
command.spawn().map_err(|err| format!("cannot run {}: {err}", job.script.display()))?;
|
||||
let start = Instant::now();
|
||||
loop {
|
||||
match child.try_wait().map_err(|err| format!("cannot wait for test: {err}"))? {
|
||||
Some(status) => {
|
||||
return Ok(if !status.success() {
|
||||
Outcome::Fail
|
||||
} else if log_says_skipped(&job.log) {
|
||||
Outcome::Skip
|
||||
} else {
|
||||
Outcome::Pass
|
||||
});
|
||||
}
|
||||
None if start.elapsed() < timeout => thread::sleep(Duration::from_millis(20)),
|
||||
None => {
|
||||
// SAFETY: kill only signals the test's own process group.
|
||||
unsafe {
|
||||
libc::kill(-(child.id() as i32), libc::SIGKILL);
|
||||
}
|
||||
let _ = child.wait();
|
||||
return Ok(Outcome::Timeout);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn run_job(root: &Path, job: &TestJob, linker: &Path, timeout: Duration) -> TestResult {
|
||||
let mut outcome = run_process(root, job, linker, timeout).unwrap_or_else(|err| {
|
||||
eprintln!("{}: {err}", job.name);
|
||||
Outcome::Fail
|
||||
});
|
||||
|
||||
// Keep failed test directories for diagnosis, but do not retain the
|
||||
// successful tests' potentially large temporary files.
|
||||
if matches!(outcome, Outcome::Pass | Outcome::Skip) {
|
||||
let dir = root.join("out/test").join(&job.target.label).join(&job.name);
|
||||
match fs::remove_dir_all(&dir) {
|
||||
Ok(()) => {}
|
||||
Err(err) if err.kind() == io::ErrorKind::NotFound => {}
|
||||
Err(err) => {
|
||||
eprintln!("{}: cannot remove {}: {err}", job.name, dir.display());
|
||||
outcome = Outcome::Fail;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if let Err(err) = fs::write(&job.status_file, format!("{}\n", outcome.status())) {
|
||||
eprintln!("{}: cannot write {}: {err}", job.name, job.status_file.display());
|
||||
}
|
||||
TestResult {
|
||||
target: Arc::clone(&job.target),
|
||||
name: job.name.clone(),
|
||||
log: job.log.clone(),
|
||||
outcome,
|
||||
}
|
||||
}
|
||||
|
||||
fn run_jobs(root: &Path, jobs: Vec<TestJob>, linker: &Path, options: &Options) -> Vec<TestResult> {
|
||||
if jobs.is_empty() {
|
||||
return Vec::new();
|
||||
}
|
||||
let next = AtomicUsize::new(0);
|
||||
let (sender, receiver) = mpsc::channel();
|
||||
let workers = options.jobs.min(jobs.len());
|
||||
|
||||
thread::scope(|scope| {
|
||||
for _ in 0..workers {
|
||||
let jobs = &jobs;
|
||||
let next = &next;
|
||||
let sender = sender.clone();
|
||||
scope.spawn(move || {
|
||||
loop {
|
||||
let index = next.fetch_add(1, Ordering::Relaxed);
|
||||
let Some(job) = jobs.get(index) else {
|
||||
break;
|
||||
};
|
||||
if sender.send(run_job(root, job, linker, options.timeout)).is_err() {
|
||||
break;
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
drop(sender);
|
||||
|
||||
let mut results = Vec::with_capacity(jobs.len());
|
||||
while let Ok(result) = receiver.recv() {
|
||||
if matches!(result.outcome, Outcome::Fail | Outcome::Timeout) {
|
||||
eprintln!(
|
||||
"FAIL {}:{}{} ({})",
|
||||
result.target.label,
|
||||
result.name,
|
||||
if result.outcome == Outcome::Timeout { " [timeout]" } else { "" },
|
||||
result.log.display()
|
||||
);
|
||||
}
|
||||
results.push(result);
|
||||
if results.len().is_multiple_of(100) || results.len() == jobs.len() {
|
||||
eprintln!("completed {}/{}", results.len(), jobs.len());
|
||||
}
|
||||
}
|
||||
results
|
||||
})
|
||||
}
|
||||
|
||||
fn print_inventory(jobs: &[TestJob], unavailable: &[&str]) {
|
||||
let mut counts = BTreeMap::new();
|
||||
for job in jobs {
|
||||
*counts.entry(job.target.label.as_str()).or_insert(0usize) += 1;
|
||||
}
|
||||
for (target, count) in counts {
|
||||
println!("{target}: tests={count}");
|
||||
}
|
||||
println!("total: tests={}", jobs.len());
|
||||
if !unavailable.is_empty() {
|
||||
println!("unavailable: {}", unavailable.join(", "));
|
||||
}
|
||||
}
|
||||
|
||||
fn print_summary(results: &[TestResult]) -> bool {
|
||||
let mut by_target: BTreeMap<&str, Counts> = BTreeMap::new();
|
||||
for result in results {
|
||||
by_target.entry(&result.target.label).or_default().add(result.outcome);
|
||||
}
|
||||
|
||||
let mut total = Counts::default();
|
||||
for (target, counts) in &by_target {
|
||||
println!("{target}: pass={} skip={} fail={}", counts.pass, counts.skip, counts.fail);
|
||||
total.merge(counts);
|
||||
}
|
||||
if by_target.len() > 1 {
|
||||
println!("total: pass={} skip={} fail={}", total.pass, total.skip, total.fail);
|
||||
} else {
|
||||
println!("pass={} skip={} fail={}", total.pass, total.skip, total.fail);
|
||||
}
|
||||
total.fail == 0
|
||||
}
|
||||
|
||||
/// Discovers and runs the test scripts in `cases_dirs`, using the linker
|
||||
/// at `mold`. Command line arguments are substring patterns selecting a
|
||||
/// subset of tests.
|
||||
pub fn run(cases_dirs: &[PathBuf], mold: &Path) -> ExitCode {
|
||||
let options = parse_options();
|
||||
let work_dir = prepare_work_dir(mold).unwrap_or_else(|err| {
|
||||
eprintln!("mold-macho-tests: {err}");
|
||||
std::process::exit(1);
|
||||
});
|
||||
let linker = work_dir.join("ld64.mold");
|
||||
let (targets, unavailable) = selected_targets(&options);
|
||||
let scripts = selected_scripts(cases_dirs, &options.patterns).unwrap_or_else(|err| {
|
||||
eprintln!("mold-macho-tests: {err}");
|
||||
std::process::exit(1);
|
||||
});
|
||||
if options.list {
|
||||
let jobs = make_jobs(&scripts, &work_dir, targets, false).unwrap_or_else(|err| {
|
||||
eprintln!("mold-macho-tests: {err}");
|
||||
std::process::exit(1);
|
||||
});
|
||||
print_inventory(&jobs, &unavailable);
|
||||
return ExitCode::SUCCESS;
|
||||
}
|
||||
|
||||
// The host's configurations run together, then each simulator's on
|
||||
// its own, its device booted for it alone and shut down once it is
|
||||
// done: a booted simulator is a whole OS, and one at a time is load
|
||||
// enough. Errors return rather than exit, so that a device is shut
|
||||
// down.
|
||||
let (hosts, simulators): (Vec<_>, Vec<_>) =
|
||||
targets.into_iter().partition(|target| target.runtime.is_none());
|
||||
let mut results = Vec::new();
|
||||
for mut phase in iter::once(hosts).chain(simulators.into_iter().map(|target| vec![target])) {
|
||||
if phase.is_empty() || scripts.is_empty() {
|
||||
continue;
|
||||
}
|
||||
let devices = match boot_devices(&mut phase) {
|
||||
Ok(devices) => devices,
|
||||
Err(err) => {
|
||||
eprintln!("mold-macho-tests: {err}");
|
||||
return ExitCode::FAILURE;
|
||||
}
|
||||
};
|
||||
let jobs = match make_jobs(&scripts, &work_dir, phase, true) {
|
||||
Ok(jobs) => jobs,
|
||||
Err(err) => {
|
||||
eprintln!("mold-macho-tests: {err}");
|
||||
return ExitCode::FAILURE;
|
||||
}
|
||||
};
|
||||
results.extend(run_jobs(&work_dir, jobs, &linker, &options));
|
||||
drop(devices);
|
||||
}
|
||||
if print_summary(&results) { ExitCode::SUCCESS } else { ExitCode::FAILURE }
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
use std::process::Output;
|
||||
|
||||
fn run_script(body: &str) -> Output {
|
||||
static NEXT: AtomicUsize = AtomicUsize::new(0);
|
||||
let dir = env::temp_dir().join(format!(
|
||||
"mold-harness-{}-{}",
|
||||
std::process::id(),
|
||||
NEXT.fetch_add(1, Ordering::Relaxed)
|
||||
));
|
||||
fs::create_dir(&dir).unwrap();
|
||||
let common = Path::new(env!("CARGO_MANIFEST_DIR")).join("common.inc");
|
||||
let output = Command::new("bash")
|
||||
.args(["-c", &format!("source \"$1\"\n{body}"), "harness-test"])
|
||||
.arg(common)
|
||||
.env("mold", "unused")
|
||||
.current_dir(&dir)
|
||||
.output()
|
||||
.unwrap();
|
||||
fs::remove_dir_all(dir).unwrap();
|
||||
output
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn negated_failure_at_exit_is_not_success() {
|
||||
let output = run_script("! true");
|
||||
assert!(!output.status.success());
|
||||
assert!(!String::from_utf8_lossy(&output.stdout).contains("OK"));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn negative_assertion_stops_before_later_commands() {
|
||||
let output = run_script("not true\necho reached");
|
||||
assert!(!output.status.success());
|
||||
let stdout = String::from_utf8_lossy(&output.stdout);
|
||||
assert!(!stdout.contains("reached"));
|
||||
assert!(!stdout.contains("OK"));
|
||||
assert!(String::from_utf8_lossy(&output.stderr).contains("unexpectedly succeeded"));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn explicit_exit_status_is_preserved() {
|
||||
assert_eq!(run_script("exit 7").status.code(), Some(7));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn expected_failure_and_success_pass() {
|
||||
let output = run_script("not false\ntrue");
|
||||
assert!(output.status.success());
|
||||
assert!(String::from_utf8_lossy(&output.stdout).contains("OK"));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn skip_remains_successful() {
|
||||
let output = run_script("skip");
|
||||
assert!(output.status.success());
|
||||
let stdout = String::from_utf8_lossy(&output.stdout);
|
||||
assert!(stdout.contains("skipped"));
|
||||
assert!(!stdout.contains("OK"));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn selects_tests_by_substring() {
|
||||
assert!(matches_patterns("dead-strip", &[]));
|
||||
assert!(matches_patterns("dead-strip", &["strip".to_owned()]));
|
||||
assert!(!matches_patterns("hello", &["strip".to_owned()]));
|
||||
}
|
||||
}
|
||||
@@ -171,7 +171,7 @@ impl Slot {
|
||||
thread::sleep(Duration::from_secs(1));
|
||||
waited += 1;
|
||||
if waited % 60 == 0 {
|
||||
eprintln!("mold-macho-tests: waiting for the simulator slot ({waited}s)");
|
||||
eprintln!("mold-tests: waiting for the simulator slot ({waited}s)");
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -227,7 +227,7 @@ impl Device {
|
||||
impl Drop for Device {
|
||||
fn drop(&mut self) {
|
||||
if let Err(err) = simctl(&["shutdown", &self.udid]) {
|
||||
eprintln!("mold-macho-tests: {err}");
|
||||
eprintln!("mold-tests: {err}");
|
||||
}
|
||||
}
|
||||
}
|
||||
Reference in new issue
Block a user