修复进程会话测试失败时的残留进程清理
Project CI / AI game creator shell Rust smoke (pull_request) Has been cancelled
Project CI / AI game creator shell Rust crates (pull_request) Has been cancelled
Project CI / Backend tests (pull_request) Has been cancelled
Project CI / Native shell tests (pull_request) Has been cancelled
Project CI / Frontend tests (pull_request) Has been cancelled
Project CI / Repository checks (pull_request) Has been cancelled
Project CI / AI game creator shell web tests (pull_request) Has been cancelled
Project CI / AI game creator shell Rust lane 1/2 (pull_request) Has been cancelled
Project CI / AI game creator shell Rust lane 2/2 (pull_request) Has been cancelled
Project CI / AI game creator shell Rust smoke (pull_request) Has been cancelled
Project CI / AI game creator shell Rust crates (pull_request) Has been cancelled
Project CI / Backend tests (pull_request) Has been cancelled
Project CI / Native shell tests (pull_request) Has been cancelled
Project CI / Frontend tests (pull_request) Has been cancelled
Project CI / Repository checks (pull_request) Has been cancelled
Project CI / AI game creator shell web tests (pull_request) Has been cancelled
Project CI / AI game creator shell Rust lane 1/2 (pull_request) Has been cancelled
Project CI / AI game creator shell Rust lane 2/2 (pull_request) Has been cancelled
为 owner 测试添加作用域清理,确保正常退出和 panic 时回收进程 保留子进程自行退出断言,并限定残留清理到独立临时项目 增加 panic 清理与跨项目隔离回归测试 同步更新开发运维文档和共享测试约定
This commit is contained in:
@@ -2,6 +2,11 @@ use super::*;
|
||||
#[cfg(target_os = "linux")]
|
||||
use std::process::Stdio;
|
||||
|
||||
#[cfg(target_os = "linux")]
|
||||
mod owner_fixture_cleanup;
|
||||
#[cfg(target_os = "linux")]
|
||||
use owner_fixture_cleanup::{project_processes, OwnerFixtureCleanup};
|
||||
|
||||
static PROCESS_SESSION_TEST_LOCK: OnceLock<Mutex<()>> = OnceLock::new();
|
||||
|
||||
fn process_session_test_guard() -> std::sync::MutexGuard<'static, ()> {
|
||||
@@ -1743,20 +1748,6 @@ fn process_session_runner_owner_fixture() {
|
||||
#[cfg(target_os = "linux")]
|
||||
#[test]
|
||||
fn process_session_owner_sigkill_leaves_no_child_process() {
|
||||
fn project_processes(root: &Path) -> Vec<i32> {
|
||||
let canonical_root = fs::canonicalize(root).expect("canonical test project");
|
||||
fs::read_dir("/proc")
|
||||
.into_iter()
|
||||
.flatten()
|
||||
.flatten()
|
||||
.filter_map(|entry| {
|
||||
let process_id = entry.file_name().to_string_lossy().parse::<i32>().ok()?;
|
||||
let cwd = fs::read_link(entry.path().join("cwd")).ok()?;
|
||||
(cwd == canonical_root).then_some(process_id)
|
||||
})
|
||||
.collect()
|
||||
}
|
||||
|
||||
let directory = tempfile::tempdir().expect("temp project");
|
||||
let root = directory.path();
|
||||
init_local_game_project_at(root, "owner-process-project", "Owner Process Project")
|
||||
@@ -1787,6 +1778,10 @@ setInterval(() => {}, 1000);
|
||||
.stderr(Stdio::null())
|
||||
.spawn()
|
||||
.expect("spawn owner fixture test process");
|
||||
let cleanup = OwnerFixtureCleanup {
|
||||
owner: &mut owner,
|
||||
root,
|
||||
};
|
||||
let deadline = std::time::Instant::now() + Duration::from_secs(10);
|
||||
while (!root.join("owner-ready").is_file() || project_processes(root).is_empty())
|
||||
&& std::time::Instant::now() < deadline
|
||||
@@ -1799,9 +1794,9 @@ setInterval(() => {}, 1000);
|
||||
"sandbox child should be visible from host /proc"
|
||||
);
|
||||
|
||||
let owner_pid = i32::try_from(owner.id()).expect("owner pid");
|
||||
assert_eq!(unsafe { libc::kill(owner_pid, libc::SIGKILL) }, 0);
|
||||
owner.wait().expect("reap owner fixture");
|
||||
// Linux Child::kill 发送 SIGKILL;先检查真实子树回收,再由 guard 兜底。
|
||||
cleanup.owner.kill().expect("SIGKILL owner fixture");
|
||||
cleanup.owner.wait().expect("reap owner fixture");
|
||||
let deadline = std::time::Instant::now() + Duration::from_secs(5);
|
||||
loop {
|
||||
let remaining = project_processes(root);
|
||||
|
||||
+127
@@ -0,0 +1,127 @@
|
||||
use std::fs;
|
||||
use std::path::Path;
|
||||
use std::process::Child;
|
||||
use std::thread;
|
||||
use std::time::{Duration, Instant};
|
||||
|
||||
pub(super) fn project_processes(root: &Path) -> Vec<i32> {
|
||||
let Ok(canonical_root) = fs::canonicalize(root) else {
|
||||
return Vec::new();
|
||||
};
|
||||
fs::read_dir("/proc")
|
||||
.into_iter()
|
||||
.flatten()
|
||||
.flatten()
|
||||
.filter_map(|entry| {
|
||||
let process_id = entry.file_name().to_string_lossy().parse::<i32>().ok()?;
|
||||
if process_id <= 1 || process_id == std::process::id() as i32 {
|
||||
return None;
|
||||
}
|
||||
let cwd = fs::read_link(entry.path().join("cwd")).ok()?;
|
||||
(cwd == canonical_root).then_some(process_id)
|
||||
})
|
||||
.collect()
|
||||
}
|
||||
|
||||
pub(super) struct OwnerFixtureCleanup<'a> {
|
||||
pub(super) owner: &'a mut Child,
|
||||
pub(super) root: &'a Path,
|
||||
}
|
||||
|
||||
impl Drop for OwnerFixtureCleanup<'_> {
|
||||
fn drop(&mut self) {
|
||||
let _ = self.owner.kill();
|
||||
let _ = self.owner.wait();
|
||||
|
||||
// 正常路径先验证子进程自行退出;这里只兜底作用域退出(包括 panic)后的残留。
|
||||
// 项目目录由每条用例独占,不能按进程名清理其他用例或开发进程。
|
||||
let deadline = Instant::now() + Duration::from_secs(5);
|
||||
loop {
|
||||
let remaining = project_processes(self.root);
|
||||
if remaining.is_empty() {
|
||||
return;
|
||||
}
|
||||
for process_id in &remaining {
|
||||
unsafe {
|
||||
libc::kill(*process_id, libc::SIGKILL);
|
||||
}
|
||||
}
|
||||
if Instant::now() >= deadline {
|
||||
// Drop 可能在 panic 展开期间执行,不能再次 panic。
|
||||
use std::io::Write;
|
||||
let _ = writeln!(
|
||||
std::io::stderr(),
|
||||
"owner fixture cleanup timed out: pids={remaining:?}"
|
||||
);
|
||||
return;
|
||||
}
|
||||
thread::sleep(Duration::from_millis(25));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn owner_fixture_cleanup_reaps_processes_on_panic_without_touching_other_projects() {
|
||||
use std::panic::{catch_unwind, AssertUnwindSafe};
|
||||
use std::process::{Command, Stdio};
|
||||
|
||||
// 回归夹具自己的回收不能依赖被测 guard,否则 guard 回归时测试也会泄漏。
|
||||
struct Sleeper(Child);
|
||||
impl Drop for Sleeper {
|
||||
fn drop(&mut self) {
|
||||
let _ = self.0.kill();
|
||||
let _ = self.0.wait();
|
||||
}
|
||||
}
|
||||
fn sleeper(root: &Path) -> Sleeper {
|
||||
Sleeper(
|
||||
Command::new("sleep")
|
||||
.arg("60")
|
||||
.current_dir(root)
|
||||
.stdin(Stdio::null())
|
||||
.stdout(Stdio::null())
|
||||
.stderr(Stdio::null())
|
||||
.spawn()
|
||||
.expect("spawn cleanup fixture"),
|
||||
)
|
||||
}
|
||||
|
||||
// 同时覆盖 owner 刚启动就失败,以及已有残留进程时失败。
|
||||
for has_residual in [false, true] {
|
||||
let project = tempfile::tempdir().expect("cleanup project");
|
||||
let other_project = tempfile::tempdir().expect("unrelated project");
|
||||
let mut owner = sleeper(project.path());
|
||||
let cleanup = OwnerFixtureCleanup {
|
||||
owner: &mut owner.0,
|
||||
root: project.path(),
|
||||
};
|
||||
// 故意不依赖 owner 退出监测,验证兜底能清理仍留在项目目录的进程。
|
||||
let mut residual = has_residual.then(|| sleeper(project.path()));
|
||||
let mut other = sleeper(other_project.path());
|
||||
let result = catch_unwind(AssertUnwindSafe(move || {
|
||||
let _cleanup = cleanup;
|
||||
panic!("simulate an assertion failure before owner shutdown");
|
||||
}));
|
||||
|
||||
let owner_status = owner.0.try_wait();
|
||||
let residual_status = residual.as_mut().map(|child| child.0.try_wait());
|
||||
let other_status = other.0.try_wait();
|
||||
// 即使 guard 回归,先收口本测试持有的进程再断言,避免回归用例自身泄漏。
|
||||
drop(owner);
|
||||
drop(residual);
|
||||
drop(other);
|
||||
|
||||
assert!(result.is_err());
|
||||
assert!(matches!(owner_status, Ok(Some(_))), "owner must exit");
|
||||
if has_residual {
|
||||
assert!(
|
||||
matches!(residual_status, Some(Ok(Some(_)))),
|
||||
"residual process must exit"
|
||||
);
|
||||
}
|
||||
assert!(
|
||||
matches!(other_status, Ok(None)),
|
||||
"other project must survive"
|
||||
);
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user