修复进程会话测试失败时的残留进程清理
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"
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -61,6 +61,8 @@ AGC 预览快捷操作的界面测试按独立命令或有状态短流程注册
|
||||
|
||||
Rust 分片失败日志保留有界的失败详情,包括 panic 位置、断言和最终通过/失败数量;分片选中数量标为 selected,避免误读为失败数量。修改分片日志时运行 `node --test apps/ai-game-creator-shell/scripts/run-rust-shell-test-shards.test.mjs`,用最小 Rust fixture 验证失败详情和成功摘要。
|
||||
|
||||
Linux process-session 的 owner SIGKILL 测试在启动 owner 后立即建立清理 guard,正常结束和 panic 展开都必须终止并回收 owner,再按独立临时项目目录清理残留进程。先完成「杀掉 owner 后子进程自行退出」的原有断言,guard 只在退出测试作用域时兜底,不得提前清理子树使生命周期回归假绿;清理本身不得 panic 或无限等待。
|
||||
|
||||
AGC 运行时配置默认值调整时,同步核对 Rust 默认值、分发配置模板、设置弹窗默认草稿和 `runtime-settings.suite.ts` 的恢复默认断言;显式传入旧值的配置读取用例仍验证原值保留,不批量替换测试数据。
|
||||
|
||||
AGC 测试构造单 HTML 项目时,必须在初始化之前写入 HTML,避免自动建立 npm 工程;npm 预览和导出测试应提供 dist 产物。已有图片生成 pending/operation 属于持久化恢复合同,修改工具默认参数后仍须验证旧动作恢复不重复提交、不因默认值变化被误判为新意图。
|
||||
|
||||
@@ -288,6 +288,8 @@ npm run check
|
||||
|
||||
### Gitea Actions PR 门禁
|
||||
|
||||
Linux process-session 的 owner SIGKILL 用例必须在启动 owner 后立即建立测试清理 guard:正常退出或断言 panic 时终止、回收 owner,并在有界时间内清理其独立临时项目目录中的残留进程。原有「owner 退出后子进程自行消失」断言在兜底清理之前执行,不能由 guard 代替生产生命周期验证。清理覆盖 panic 路径及临时项目间隔离,且不得因清理失败再次 panic。
|
||||
|
||||
仓库级 Gitea Actions 工作流固定为 `.gitea/workflows/project-ci.yml`,在向 `master` 推送、创建或更新 PR,以及手工触发时运行。工作流拆成九个必须通过的 job。job 声明顺序就是 runner 领取顺序,因此把两条 AGC 壳 Rust lane 排在最前:并发槽位不足时它们必须最先开始,AGC 侧的关键路径才由自己而不是由排队决定。
|
||||
|
||||
所有 CI job 和 Jenkins Web Build 在根 workspace 安装前都必须确认 `npm --version` 为 `10.9.7`。Gitea job 使用预构建镜像内的固定版本;Jenkins Web Build 在每个独立 `bash -lc` 中 source `scripts/jenkins-prepare-npm-env.sh`,首次为 Jenkins 运行用户的版本隔离目录引导同版 npm,后续复用并把该 `bin` 放到 `PATH` 首位。旧固定镜像缺少版本元数据时只能报告 `npm_version=partial` 并由当前 job 的根 `npm ci` 继续校验 lock,不能把过渡状态当作工具链已闭合。
|
||||
|
||||
Reference in New Issue
Block a user