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

330 lines
16 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.
//! 监督窗口传输抽象:把 supervise 子进程窗口与父进程之间的文件协议(心跳文件、
//! 旁路失败行、last-pc 现场、sidecar 日志、子进程派生与杀死)收口成六操作传输
//! 接口;LocalFile 实现即现状文件协议逐位不变,远程模式将来加 Tcp 实现。
//!
//! 传输只负责「怎么把窗口跑起来、怎么读回状态」;超时判定、重启循环、记账等
//! 监督策略留在 supervise 侧。传输实例是窗口级状态:跨子进程重启存活,
//! `assign` 只重置子进程句柄,不重置已消费偏移。
use crate::heartbeat::{read_heartbeat, Heartbeat};
use crate::report::parse_fail_log;
use crate::stats::RunStats;
use std::path::{Path, PathBuf};
use std::process::{Child, Command};
/// 派生窗口子进程失败:spawn 的 io 错误。
#[derive(Debug)]
pub(crate) struct SpawnError(pub(crate) std::io::Error);
impl std::fmt::Display for SpawnError {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
self.0.fmt(f)
}
}
/// 子进程等待三态:死亡(带退出码)/ 存活 / 等待错误。
/// 三臂在监督策略里处理不同(死亡走心跳裁决、存活继续轮询、错误走 kill 加
/// 记账),不得压扁。退出码仅用于打印(`ExitStatus` 无公开构造器,远程断连
/// 合成码 1 走同一打印路径,不参与逻辑分支)。
pub(crate) enum PollResult {
Exited(usize),
Alive,
WaitError(std::io::Error),
}
/// 监督窗口传输:六操作(assign/heartbeat/merge_fail/scene/kill/poll)加
/// 生命周期与日志辅助。LocalFile 臂是现状文件协议,远程臂将来是窗口指派/
/// 心跳/失败记录/现场/kill/DONE 帧。
pub(crate) trait WindowTransport {
/// 派生一个跑 `[wstart, wend)` 的子进程(LocalFile:九项环境变量加
/// sidecar 双写,打开失败回退丢弃不中断)。
fn assign(&mut self, wstart: usize, wend: usize) -> Result<(), SpawnError>;
/// 读最新心跳(缺失/畸形返回 None,调用方下一拍重试)。
fn heartbeat(&self) -> Option<Heartbeat>;
/// 把旁路失败行新增段并入统计(已消费偏移跨重启保留,防双计)。
fn merge_fail(&mut self, stats: &mut RunStats);
/// 读子进程被杀前留下的 last-pc 现场行(缺文件/残行返回 None)。
fn scene(&self) -> Option<String>;
/// 杀死子进程并等待回收。
fn kill(&mut self);
/// 非阻塞查询子进程状态(三态)。
fn poll(&mut self) -> PollResult;
/// 读 sidecar 日志末 ~20 行(超时/崩溃后供现场重建)。
fn log_tail(&self) -> Option<String>;
/// 每轮重启前清理:删心跳文件、其 .tmp 与 last-pc 现场文件;旁路失败行
/// 与 sidecar 日志跨重启追加保留。
fn reset_round(&mut self);
/// 窗口收尾:删心跳与 .tmp 与 last-pc 文件,旁路失败行与 sidecar 日志
/// 归档为 .done 而非删除(收尾逐文件差分的终态依据)。
fn archive_window(&mut self);
}
/// 本地文件传输:现状文件协议实现(心跳文件 + 旁路失败行 + last-pc 现场 +
/// sidecar 日志 + 子进程句柄)。
pub(crate) struct LocalFileWindowTransport {
exe: PathBuf,
args: Vec<String>,
paths_file: PathBuf,
hb_path: PathBuf,
log_path: PathBuf,
pc_path: PathBuf,
/// 旁路失败行已消费字节偏移:窗口级状态,跨子进程重启保留(防双计)。
fails_consumed: usize,
child: Option<Child>,
}
impl LocalFileWindowTransport {
/// 构造传输:按 pid 加 window_id 命名规则算好三个文件路径(与现状一致)。
pub(crate) fn new(exe: &Path, args: &[String], paths_file: &Path, window_id: usize) -> Self {
let hb_path = std::env::temp_dir().join(format!("oxide_t262_hb_{}_{}.txt", std::process::id(), window_id));
let log_path = hb_path.with_extension("log");
let pc_path = std::env::temp_dir().join(format!("oxide_t262_pc_{}_{}.txt", std::process::id(), window_id));
Self {
exe: exe.to_path_buf(),
args: args.to_vec(),
paths_file: paths_file.to_path_buf(),
hb_path,
log_path,
pc_path,
fails_consumed: 0,
child: None,
}
}
}
impl WindowTransport for LocalFileWindowTransport {
fn assign(&mut self, wstart: usize, wend: usize) -> Result<(), SpawnError> {
let max_tests = wend - wstart;
// sidecar 日志经手:子进程 stdout/stderr 双写此文件(直接文件写,无
// pipe 缓冲死锁面);打开失败回退丢弃,不中断运行。
let (stdout, stderr) = match std::fs::OpenOptions::new().create(true).append(true).open(&self.log_path) {
Ok(file) => {
// 文件经手克隆:底层 fd 复制,stdout/stderr 各持一份。
let err_file = file.try_clone().expect("sidecar 日志经手克隆失败");
let out = std::process::Stdio::from(file);
let err = std::process::Stdio::from(err_file);
(out, err)
}
Err(_) => (std::process::Stdio::null(), std::process::Stdio::null()),
};
let child = Command::new(&self.exe)
.args(self.args.iter().skip(1))
.env("OXIDE_SKIP_UNTIL", wstart.to_string())
.env("OXIDE_MAX_TESTS", max_tests.to_string())
.env("OXIDE_TEST262_WORKERS", "1")
.env("OXIDE_TEST262_HEARTBEAT", &self.hb_path)
.env("OXIDE_TEST262_PC_WATCH", &self.pc_path)
.env("OXIDE_TEST262_PATHS_FILE", &self.paths_file)
.env("OXIDE_TEST262_CHILD_CHUNK", "1")
.env("OXIDE_TEST262_ALLOW_FAIL_EXIT", "1")
.env_remove("OXIDE_TEST262_CHUNK_SIZE")
.stdout(stdout)
.stderr(stderr)
.spawn()
.map_err(SpawnError)?;
self.child = Some(child);
Ok(())
}
fn heartbeat(&self) -> Option<Heartbeat> {
read_heartbeat(&self.hb_path)
}
fn merge_fail(&mut self, stats: &mut RunStats) {
merge_fail_log(stats, &self.hb_path, &mut self.fails_consumed);
}
fn scene(&self) -> Option<String> {
read_pc_scene(&self.pc_path)
}
fn kill(&mut self) {
if let Some(child) = self.child.as_mut() {
let _ = child.kill();
let _ = child.wait();
}
self.child = None;
}
fn poll(&mut self) -> PollResult {
let Some(child) = self.child.as_mut() else {
return PollResult::Alive;
};
match child.try_wait() {
// 退出码仅用于打印:正常退出取 code,信号杀死取缺省 1。
Ok(Some(status)) => PollResult::Exited(status.code().unwrap_or(1) as usize),
Ok(None) => PollResult::Alive,
Err(err) => PollResult::WaitError(err),
}
}
fn log_tail(&self) -> Option<String> {
tail_lines(&self.log_path, 20)
}
fn reset_round(&mut self) {
let _ = std::fs::remove_file(&self.hb_path);
let _ = std::fs::remove_file(format!("{}.tmp", self.hb_path.display()));
let _ = std::fs::remove_file(&self.pc_path);
}
fn archive_window(&mut self) {
let _ = std::fs::remove_file(&self.hb_path);
let _ = std::fs::remove_file(format!("{}.tmp", self.hb_path.display()));
let _ = std::fs::remove_file(&self.pc_path);
// 旁路失败行归档而非删除:统计已由心跳合并入父进程,此文件是收尾逐文件
// 差分的终态依据(删除即永久丢记录)。
let _ = std::fs::rename(self.hb_path.with_extension("fails"), self.hb_path.with_extension("fails.done"));
// sidecar 日志同式归档:整窗子进程输出是现场重建的终态依据。
let _ = std::fs::rename(&self.log_path, self.log_path.with_extension("log.done"));
}
}
/// 把 supervise 旁路失败行文件并入统计:逐行重建 FailRecord(类别经本表 id
/// 重映射 + push_fail_record 的 cap 逻辑)。
///
/// # 边界与前提
/// - 失败计数(fail_categories)已由对应心跳合并覆盖,此处只补 fail_records,
/// 不得再累计计数,避免双计。worker 循环先写心跳后追加旁路,故"无心跳退出"
/// 分支旁路必为空(防御性 no-op)。
/// - 旁路文件跨子进程重启追加保留,本函数只解析自上次消费以来的新增段
/// (`consumed` 由传输实例按窗口持有);文件缩至已消费偏移之下(删除/重建)
/// 回绕到文件头重读,避免丢记录。
fn merge_fail_log(stats: &mut RunStats, hb_path: &Path, consumed: &mut usize) {
let content = match std::fs::read_to_string(hb_path.with_extension("fails")) {
Ok(c) => c,
Err(_) => return,
};
if content.len() < *consumed {
*consumed = 0;
}
let fresh = &content[*consumed..];
*consumed = content.len();
for (index, cat, subkey, message) in parse_fail_log(fresh) {
stats.push_fail_record(index, cat, subkey, message);
}
}
/// 读子进程被杀前留下的 last-pc 现场行:取文件末行,须含 pc/op/frames 三字段
/// 才接受(缺文件 / 空文件 / 残行均返回 None)。
fn read_pc_scene(path: &Path) -> Option<String> {
let content = std::fs::read_to_string(path).ok()?;
let last = content.lines().last()?.trim();
if last.starts_with("pc=") && last.contains(" op=") && last.contains(" frames=") {
Some(last.to_string())
} else {
None
}
}
/// 读 sidecar 日志的末 `n` 行:文件缺失/空返回 None,有内容时返回整文件或
/// 末 N 行(超时/崩溃后供现场重建)。
fn tail_lines(path: &Path, n: usize) -> Option<String> {
let content = std::fs::read_to_string(path).ok()?;
let lines: Vec<&str> = content.lines().collect();
let start = lines.len().saturating_sub(n);
if start == lines.len() {
return None;
}
Some(lines[start..].join("\n"))
}
#[cfg(test)]
mod tests {
use super::*;
use crate::report::append_fail_log;
/// read_pc_scene 防御解析:缺文件 / 空文件 / 残行(缺 frames 字段)均 None,
/// 末行完整时接受(忽略中间行)。
#[test]
fn read_pc_scene_defensive_parse() {
let dir = std::env::temp_dir();
let path = dir.join(format!("oxide_t262_pc_test_{}.txt", std::process::id()));
assert_eq!(read_pc_scene(&path), None, "缺文件返回 None");
std::fs::write(&path, "").expect("写失败");
assert_eq!(read_pc_scene(&path), None, "空文件返回 None");
std::fs::write(&path, "pc=1 op=ADD flat_id=0 frames=1\npc=2 op=ADD flat_id=0 frames=1\n").expect("写失败");
assert_eq!(read_pc_scene(&path), Some("pc=2 op=ADD flat_id=0 frames=1".to_string()));
// 残行:末行缺 frames 字段,返回 None。
std::fs::write(&path, "pc=1 op=ADD flat_id=0 frames=1\npc=2 op=AD").expect("写失败");
assert_eq!(read_pc_scene(&path), None, "残行返回 None");
let _ = std::fs::remove_file(&path);
}
/// tail_lines 防御解析:缺文件 / 空文件返回 None,不足 N 行返回整文件,
/// 超出 N 行只取末 N 行。
#[test]
fn tail_lines_defensive_parse() {
let dir = std::env::temp_dir();
let path = dir.join(format!("oxide_t262_log_test_{}.log", std::process::id()));
assert_eq!(tail_lines(&path, 20), None, "缺文件返回 None");
std::fs::write(&path, "").expect("写失败");
assert_eq!(tail_lines(&path, 20), None, "空文件返回 None");
std::fs::write(&path, "a\nb\n").expect("写失败");
assert_eq!(tail_lines(&path, 20), Some("a\nb".to_string()), "不足 N 行返回整文件");
std::fs::write(&path, "1\n2\n3\n4\n").expect("写失败");
assert_eq!(tail_lines(&path, 2), Some("3\n4".to_string()), "超出 N 行取末 N 行");
let _ = std::fs::remove_file(&path);
}
/// merge_fail_log 重建 fail_records:类别经 id 表重映射、消息 cap;
/// fail_categories 保持空(计数不双计契约)。
#[test]
fn merge_fail_log_reconstructs_fail_records() {
let dir = std::env::temp_dir();
let hb_path = dir.join(format!("oxide_t262_hb_test_mrg_{}.txt", std::process::id()));
let sidecar = hb_path.with_extension("fails");
append_fail_log(&sidecar, 2, "vm: not callable", "", "x is not callable").expect("追加失败");
append_fail_log(&sidecar, 7, "compile: unsupported", "", "y").expect("追加失败");
let mut stats = RunStats::default();
let mut fails_consumed = 0usize;
merge_fail_log(&mut stats, &hb_path, &mut fails_consumed);
assert_eq!(stats.fail_records.len(), 2);
// 无新增内容的二次合并不产生重复记录(已消费偏移)。
merge_fail_log(&mut stats, &hb_path, &mut fails_consumed);
assert_eq!(stats.fail_records.len(), 2);
assert_eq!(stats.fail_records[0].index, 2);
assert_eq!(stats.categories[stats.fail_records[0].category_id as usize], "vm: not callable");
assert_eq!(stats.fail_records[0].message, "x is not callable");
assert_eq!(stats.fail_records[1].index, 7);
assert_eq!(stats.categories[stats.fail_records[1].category_id as usize], "compile: unsupported");
assert!(stats.fail_categories.is_empty(), "计数不双计:fail_categories 必须保持空");
let _ = std::fs::remove_file(&sidecar);
let _ = std::fs::remove_file(&hb_path);
}
/// 跨重启合并序列:旁路文件两次"子进程死亡"间追加保留,第二次合并只并入
/// 新增段(不重复并入前段行);重跑测试的新行是真实新事件;文件重建得比
/// 已消费偏移更短时回绕重读。
#[test]
fn merge_fail_log_restart_sequence_no_reread_duplicates() {
let dir = std::env::temp_dir();
let hb_path = dir.join(format!("oxide_t262_hb_test_rst_{}.txt", std::process::id()));
let sidecar = hb_path.with_extension("fails");
// 段 1:首个子进程死亡前的失败行。
append_fail_log(&sidecar, 10, "vm: not callable", "", "a").expect("追加失败");
append_fail_log(&sidecar, 11, "vm: not callable", "", "b").expect("追加失败");
let mut stats = RunStats::default();
let mut fails_consumed = 0usize;
merge_fail_log(&mut stats, &hb_path, &mut fails_consumed);
assert_eq!(stats.fail_records.len(), 2);
// 段 2:第二个子进程追加(重跑的测试 11 至多重录一条 + 新测试 12)。
append_fail_log(&sidecar, 11, "vm: not callable", "", "b again").expect("追加失败");
append_fail_log(&sidecar, 12, "compile: unsupported", "", "c").expect("追加失败");
merge_fail_log(&mut stats, &hb_path, &mut fails_consumed);
assert_eq!(stats.fail_records.len(), 4, "只应并入新增段: {:?}", stats.fail_records);
assert_eq!(stats.fail_records.iter().map(|r| r.index).collect::<Vec<_>>(), vec![10, 11, 11, 12]);
// 段 3:无新增内容的合并幂等。
merge_fail_log(&mut stats, &hb_path, &mut fails_consumed);
assert_eq!(stats.fail_records.len(), 4);
// 文件重建得比已消费偏移短:回绕到文件头重读。
std::fs::write(&sidecar, "20\tvm: not callable\t\td\n").expect("重写失败");
merge_fail_log(&mut stats, &hb_path, &mut fails_consumed);
assert_eq!(stats.fail_records.len(), 5);
assert_eq!(stats.fail_records[4].index, 20);
let _ = std::fs::remove_file(&sidecar);
let _ = std::fs::remove_file(&hb_path);
}
}