Files
OxideJS-New/crates/oxide_test262/src/config.rs
T

346 lines
18 KiB
Rust
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
//! test262 runner 运行配置:`RunConfig` 结构、CLI 解析(`parse`)与用法输出(`usage`)。
//! 8 字段与 3 关联函数放宽为 `pub(crate)`;`usage` 全文即 `--help` 用户可见输出,逐字保留。
use std::net::SocketAddr;
use std::path::PathBuf;
/// 运行配置:test262 根目录、路径过滤器及各选项开关。
#[derive(Debug, Default)]
pub(crate) struct RunConfig {
pub(crate) test262_root: Option<PathBuf>,
pub(crate) filter: Option<String>,
pub(crate) no_skip: bool,
pub(crate) supervise: bool,
/// 远程分片 worker 模式:连监督者端点收窗口指派帧,in-process 执行并发帧。
pub(crate) worker: bool,
/// 远程监督模式:(端点文本, worker 数)。监督者 bind 端点、每窗口 accept 一条
/// worker 连接,经 Tcp 传输执行窗口。
pub(crate) remote_supervise: Option<(String, usize)>,
/// 关闭 liveness/精确 DCE/RegAlloc 链(开关对比:关优化链 vs 开优化链各跑一遍)。
pub(crate) no_regalloc: bool,
/// 逐测试打印 PASS/FAIL/SKIP(开关对比时对比两轮结果集合用)。
pub(crate) verbose: bool,
/// 汇总尾部不打印 FAIL 清单。
pub(crate) no_fail_list: bool,
}
impl RunConfig {
/// 默认运行配置。
pub(crate) fn new() -> Self {
Self {
test262_root: None,
filter: None,
no_skip: false,
supervise: false,
worker: false,
remote_supervise: None,
no_regalloc: false,
verbose: false,
no_fail_list: false,
}
}
/// 监督模式判定:`--supervise` 或 `--remote-supervise` 且非监督派生的子进程
/// (子进程不重入监督)。路径 filter 不参与判定——filter 在子进程内逐测试
/// 应用,不构成监督屏障。
pub(crate) fn supervised(&self, is_chunk_child: bool) -> bool {
(self.supervise || self.remote_supervise.is_some()) && !is_chunk_child
}
/// 解析 `--remote-supervise` 端点:缺省补 loopback、解析期拒绝非 loopback。
///
/// # 步骤
/// 1. `:port` 形态补 `127.0.0.1` 前缀,裸端口(无冒号)补 `127.0.0.1:` 前缀。
/// 2. 解析为 `SocketAddr`;失败(含主机名形态)返回错误。
/// 3. 非 loopback 地址(0.0.0.0、局域网 IP)返回错误。
///
/// # 边界与前提
/// - 端点只接受 `[IP]:port` 形态;主机名不解析(无法在解析期验证 loopback)。
/// - 比 server 的「0.0.0.0 可配置但不受支持」更严:worker 执行不可信测试
/// 内容,非 loopback 绑定会把窗口指派面(含本地路径执行)暴露到网络。
///
/// # 副作用
/// - 无(纯函数,返回归一化后的端点文本)。
fn parse_remote_supervise_endpoint(raw: &str) -> Result<String, String> {
let normalized = if raw.starts_with(':') {
format!("127.0.0.1{raw}")
} else if !raw.contains(':') {
format!("127.0.0.1:{raw}")
} else {
raw.to_string()
};
let addr: SocketAddr = normalized
.parse()
.map_err(|_| format!("invalid remote-supervise endpoint: {raw} (expected [IP]:port, loopback only)"))?;
if !addr.ip().is_loopback() {
return Err(format!(
"remote-supervise endpoint must bind to loopback only: {raw} (workers execute untrusted test content; non-loopback binding exposes the window assignment surface to the network)"
));
}
Ok(normalized)
}
/// 解析命令行参数为运行配置;未知选项、参数过多或 `--remote-supervise`
/// 缺实参 / 端点非 loopback / worker 数非法返回错误。
pub(crate) fn parse(args: &[String]) -> Result<Self, String> {
let mut config = Self::new();
let mut positional = Vec::new();
let mut i = 1;
while i < args.len() {
let arg = &args[i];
match arg.as_str() {
"--no-skip" => config.no_skip = true,
"--no-regalloc" => config.no_regalloc = true,
"--verbose" => config.verbose = true,
"--supervise" => config.supervise = true,
"--worker" => config.worker = true,
"--no-fail-list" => config.no_fail_list = true,
// --remote-supervise 消费后两个实参:端点文本与 worker 数。
// 端点解析期归一化:缺省补 loopback,非 loopback 拒绝。
"--remote-supervise" => {
let endpoint_raw = args
.get(i + 1)
.ok_or_else(|| format!("--remote-supervise requires an endpoint\n\n{}", Self::usage()))?;
let endpoint = Self::parse_remote_supervise_endpoint(endpoint_raw)
.map_err(|e| format!("{e}\n\n{}", Self::usage()))?;
let workers_str = args
.get(i + 2)
.ok_or_else(|| format!("--remote-supervise requires a worker count\n\n{}", Self::usage()))?;
let workers = workers_str
.parse::<usize>()
.map_err(|_| format!("invalid worker count: {workers_str}\n\n{}", Self::usage()))?;
config.remote_supervise = Some((endpoint, workers));
i += 2;
}
"--help" | "-h" => return Err(Self::usage()),
_ if arg.starts_with("--") => return Err(format!("unknown option: {arg}\n\n{}", Self::usage())),
_ => positional.push(arg.clone()),
}
i += 1;
}
// worker 与 supervise 互斥:前者是远程分片的执行半区(连监督者端点),
// 后者是本地子进程监督,两者不共存。
if config.worker && config.supervise {
return Err(format!("--worker and --supervise are mutually exclusive\n\n{}", Self::usage()));
}
// remote-supervise 是远程监督半区,与 worker(执行半区)及本地 supervise 互斥。
if config.worker && config.remote_supervise.is_some() {
return Err(format!("--worker and --remote-supervise are mutually exclusive\n\n{}", Self::usage()));
}
if config.supervise && config.remote_supervise.is_some() {
return Err(format!("--supervise and --remote-supervise are mutually exclusive\n\n{}", Self::usage()));
}
if let Some(root) = positional.first() {
config.test262_root = Some(PathBuf::from(root));
}
if let Some(filter) = positional.get(1) {
config.filter = Some(filter.clone());
}
if positional.len() > 2 {
return Err(format!("too many positional arguments\n\n{}", Self::usage()));
}
Ok(config)
}
/// 打印用法说明。
pub(crate) fn usage() -> String {
"usage: test262-runner [--no-skip] [--no-regalloc] [--verbose] [--supervise] [--worker] [--remote-supervise <endpoint> <workers>] [--version] [test262-root] [path-filter]\n\
\n\
--no-skip Run capability-excluded tests and count unsupported compile/runtime results as failures.\n\
--no-regalloc Disable the liveness/precise-DCE/RegAlloc compiler chain (vregs stay as physical numbers).\n\
--verbose Print one PASS/FAIL/SKIP line per test (for on/off result-set comparison).\n\
--no-fail-list Do not print the per-path FAIL list at the end of the run.\n\
--version Print the binary version with the build-time git short hash (freshness self-check), then exit.\n\
--supervise Run the suite as single-worker child-process windows with a hard per-test timeout and\n\
\x20 automatic resume past any hanging/crashing test. A hang or crash is reported by path.\n\
\x20 Combines with [path-filter]: the filter is applied per-test inside each child window.\n\
--worker Run as a remote shard worker: connect to the supervisor endpoint and execute assigned\n\
\x20 test windows in-process, sending heartbeat/fail/scene/DONE frames over the connection.\n\
\x20 Authenticates with the token from OXIDE_TEST262_WORKER_TOKEN (out-of-band from the\n\
\x20 supervisor's foreground print). Mutually exclusive with --supervise.\n\
--remote-supervise Run as a remote supervisor: bind <endpoint> (loopback [IP]:port; a bare port\n\
\x20 defaults to 127.0.0.1, non-loopback addresses are rejected) and execute test\n\
\x20 windows on <workers> remote workers, sending assign frames and receiving\n\
\x20 heartbeat/fail/scene/DONE frames over the accepted connections. A worker\n\
\x20 disconnect is recovered as a child-process death. Mutually exclusive with\n\
\x20 --supervise and --worker.\n\
\n\
env tunables (all optional):\n\
\x20 OXIDE_LOG per-subsystem log levels (default off)\n\
\x20 OXIDE_TEST262_WORKERS worker threads in parallel mode (default = available parallelism, 4 under --no-skip)\n\
\x20 OXIDE_TEST262_WINDOW supervised tests per child window (default 5000)\n\
\x20 OXIDE_TEST262_TIMEOUT_SECS per-test wall-clock timeout in supervised mode (default 10)\n\
\x20 OXIDE_TEST262_STARTUP_GRACE_SECS grace for a child's first heartbeat (default 60)\n\
\x20 OXIDE_TEST262_SUPERVISORS concurrent supervised windows (default 16)\n\
\x20 OXIDE_TEST262_RUNNING_LOG log every test as it starts (any value)\n\
\x20 OXIDE_TEST262_HEARTBEAT heartbeat file path (supervised/chunked)\n\
\x20 OXIDE_TEST262_PC_WATCH last-pc scene file path (supervised child appends pc/opcode/frames every 65536 instructions)\n\
\x20 OXIDE_TEST262_CHUNK_SIZE tests per child in chunked mode\n\
\x20 OXIDE_TEST262_CHILD_CHUNK marks a spawned child as chunk worker (any value)\n\
\x20 OXIDE_TEST262_ALLOW_FAIL_EXIT child may exit 1 on fail (any value)\n\
\x20 OXIDE_TEST262_WORKER_ENDPOINT supervisor endpoint for --worker mode (host:port)\n\
\x20 OXIDE_TEST262_WORKER_TOKEN worker auth token for --worker mode (out-of-band from the supervisor's foreground print)\n\
\x20 OXIDE_TEST262_KERNEL_BATCH kernel rebuilds every N tests (default 256, clamped to the 512 realm bound)\n\
\x20 OXIDE_TEST262_LOG_LEVEL runner log level (default info)\n\
\x20 OXIDE_SKIP_UNTIL skip test indexes below N (chunking/supervise resume)\n\
\x20 OXIDE_MAX_TESTS run at most N tests after skip point"
.into()
}
}
#[cfg(test)]
mod tests {
use super::*;
/// 构造解析用参数字列(程序名 + 实参)。
fn args(args: &[&str]) -> Vec<String> {
let mut v = vec!["test262-runner".to_string()];
v.extend(args.iter().map(|s| s.to_string()));
v
}
/// 五旗标 + 2 位置参数一次 parse:五裸旗各置位、
/// 位置参数落 root/filter,Ok 路径全断言。
#[test]
fn parse_all_flags_with_two_positionals() {
let cfg = RunConfig::parse(&args(&[
"--no-skip",
"--no-regalloc",
"--verbose",
"--supervise",
"--no-fail-list",
"tests/test262",
"language",
]))
.expect("full-flag parse should succeed");
assert!(cfg.no_skip);
assert!(cfg.no_regalloc);
assert!(cfg.verbose);
assert!(cfg.supervise);
assert!(cfg.no_fail_list);
assert_eq!(cfg.test262_root, Some(PathBuf::from("tests/test262")));
assert_eq!(cfg.filter.as_deref(), Some("language"));
}
/// --worker 旗标置位;与 --supervise 同时给返回互斥错误。
#[test]
fn parse_worker_flag_and_supervise_exclusive() {
let cfg = RunConfig::parse(&args(&["--worker"])).expect("worker flag should parse");
assert!(cfg.worker, "worker 旗标应置位");
assert!(!cfg.supervise, "supervise 旗标不应置位");
let err =
RunConfig::parse(&args(&["--worker", "--supervise"])).expect_err("worker+supervise must be exclusive");
assert!(err.contains("mutually exclusive"), "应提示互斥:{err}");
}
/// --remote-supervise 正常解析端点与 worker 数;缺实参、worker 数非法返回错误。
#[test]
fn parse_remote_supervise_flag() {
let cfg = RunConfig::parse(&args(&["--remote-supervise", "127.0.0.1:8080", "4"]))
.expect("remote-supervise 应解析成功");
assert_eq!(cfg.remote_supervise, Some(("127.0.0.1:8080".to_string(), 4)));
assert!(!cfg.supervise, "remote-supervise 不应置位 supervise");
assert!(!cfg.worker, "remote-supervise 不应置位 worker");
let err = RunConfig::parse(&args(&["--remote-supervise"])).expect_err("缺端点应报错");
assert!(err.contains("--remote-supervise requires an endpoint"), "应提示缺端点:{err}");
let err = RunConfig::parse(&args(&["--remote-supervise", "127.0.0.1:8080"])).expect_err("缺 worker 数应报错");
assert!(err.contains("--remote-supervise requires a worker count"), "应提示缺 worker 数:{err}");
let err =
RunConfig::parse(&args(&["--remote-supervise", "127.0.0.1:8080", "abc"])).expect_err("worker 数非法应报错");
assert!(err.contains("invalid worker count"), "应提示 worker 数非法:{err}");
}
/// --remote-supervise 端点缺省补 loopback:裸端口与 `:port` 形态归一化为
/// 127.0.0.1 前缀,显式 loopback 地址原样保留。
#[test]
fn parse_remote_supervise_endpoint_defaults_loopback() {
let cfg = RunConfig::parse(&args(&["--remote-supervise", ":4243", "2"])).expect(":port 形态应补 loopback");
assert_eq!(cfg.remote_supervise, Some(("127.0.0.1:4243".to_string(), 2)));
let cfg = RunConfig::parse(&args(&["--remote-supervise", "4243", "2"])).expect("裸端口应补 loopback");
assert_eq!(cfg.remote_supervise, Some(("127.0.0.1:4243".to_string(), 2)));
let cfg =
RunConfig::parse(&args(&["--remote-supervise", "127.0.0.1:4243", "2"])).expect("显式 loopback 应保留");
assert_eq!(cfg.remote_supervise, Some(("127.0.0.1:4243".to_string(), 2)));
}
/// --remote-supervise 端点解析期拒绝非 loopback(0.0.0.0、局域网 IP)与
/// 非法形态(主机名、非端口文本)。
#[test]
fn parse_remote_supervise_endpoint_rejects_non_loopback() {
for raw in ["0.0.0.0:4243", "192.168.1.5:4243", "localhost:4243", "notanendpoint"] {
let err = RunConfig::parse(&args(&["--remote-supervise", raw, "2"])).expect_err("应拒绝:{raw}");
assert!(
err.contains("loopback") || err.contains("invalid remote-supervise endpoint"),
"拒绝文案应说明 loopback 口径或端点非法:{err}"
);
}
}
/// --remote-supervise 与 --worker、--supervise 互斥。
#[test]
fn parse_remote_supervise_exclusive() {
let err = RunConfig::parse(&args(&["--remote-supervise", "127.0.0.1:8080", "4", "--worker"]))
.expect_err("worker+remote-supervise 应互斥");
assert!(err.contains("mutually exclusive"), "应提示互斥:{err}");
let err = RunConfig::parse(&args(&["--remote-supervise", "127.0.0.1:8080", "4", "--supervise"]))
.expect_err("supervise+remote-supervise 应互斥");
assert!(err.contains("mutually exclusive"), "应提示互斥:{err}");
}
/// remote_supervise 置位也触发 supervised()(非子进程);子进程不重入。
#[test]
fn supervised_predicate_with_remote_supervise() {
let mut cfg = RunConfig::new();
cfg.remote_supervise = Some(("127.0.0.1:8080".to_string(), 4));
assert!(cfg.supervised(false), "remote_supervise 应触发 supervised");
assert!(!cfg.supervised(true), "子进程不应重入 supervised");
}
/// 未知选项返回含 "unknown option" 的错误;--help 的错误文案与 usage 逐字相等。
#[test]
fn parse_unknown_option_and_help_err_text() {
let err = RunConfig::parse(&args(&["--bogus"])).expect_err("unknown option must error");
assert!(err.contains("unknown option: --bogus"));
let help = RunConfig::parse(&args(&["--help"])).expect_err("--help must error");
assert_eq!(help, RunConfig::usage());
}
/// 监督判定三态:开启时 filter 不否决、子进程不重入、未开启恒 false。
#[test]
fn supervised_predicate_ignores_filter() {
let mut cfg = RunConfig::new();
cfg.supervise = true;
cfg.filter = Some("language".to_string());
assert!(cfg.supervised(false), "filter 不得否决监督");
assert!(!cfg.supervised(true), "子进程不得重入监督");
let off = RunConfig::new();
assert!(!off.supervised(false));
}
/// leak-check 旗标不在已知选项集内,落未知选项分支返回错误(回归钉);
/// 位置参数超过 2 个返回错误。
#[test]
fn parse_leak_check_flags_unknown_and_positional_limit() {
let err = RunConfig::parse(&args(&["--leak-check"])).expect_err("removed flag must be rejected");
assert!(err.contains("unknown option: --leak-check"));
let err = RunConfig::parse(&args(&["--leak-check-interval=abc"]))
.expect_err("removed interval flag must be rejected");
assert!(err.contains("unknown option: --leak-check-interval=abc"));
let err = RunConfig::parse(&args(&["a", "b", "c"])).expect_err("three positionals must error");
assert!(err.contains("too many positional arguments"));
}
}