Files
Unlucky-Launcher/crates/core/tests/launch/process_smoke.rs
T
AskaEth bc41dfcf7d
CI / fmt (rustfmt --check) (push) Canceled after 0s
CI / clippy (ubuntu-latest) (push) Canceled after 0s
CI / clippy (windows-latest) (push) Canceled after 0s
CI / test (ubuntu-latest) (push) Canceled after 0s
CI / test (windows-latest) (push) Canceled after 0s
CI / build release (ubuntu-latest) (push) Canceled after 0s
CI / build release (windows-latest) (push) Canceled after 0s
Guards / core 零 UI 守门 (push) Canceled after 0s
Guards / 文件行数 ≤400 守门 (push) Canceled after 0s
Guards / 依赖许可与来源(cargo-deny) (push) Canceled after 0s
Unlucky Launcher v0.0.1-alpha
Rust 原生单文件 Minecraft 启动器:CLI 完整可用(install/launch/accounts/java/diagnose/agent),
MCP server 接入外部 Agent 工具,GUI 开发中。
2026-10-31 21:54:10 +08:00

128 lines
3.9 KiB
Rust

//! 进程冒烟测试:真实 spawn 一个 shell,验证泵、退出码、进程树 kill。
//!
//! 这些用例跑的是真进程(不是 mock),是“事件流 + 退出分析 + 树 kill”端到端的最小证据。
use std::path::PathBuf;
use std::sync::Arc;
use std::time::Duration;
use crate::prelude::*;
use unlucky_core::launch::process::{self, ProcessConfig, RunningProcess};
use unlucky_core::launch::pump::{LineSink, NullSink};
fn shell_command(script: &str) -> AssembledCommand {
#[cfg(windows)]
let (program, args) = (
"cmd".to_string(),
vec!["/C".to_string(), script.to_string()],
);
#[cfg(not(windows))]
let (program, args) = (
"/bin/sh".to_string(),
vec!["-c".to_string(), script.to_string()],
);
AssembledCommand {
program: PathBuf::from(program),
args,
classpath: Vec::new(),
encoding: "UTF-8".to_string(),
memory: MemoryOptions::default(),
env: Vec::new(),
warnings: Vec::new(),
main_class: String::new(),
}
}
fn config() -> ProcessConfig {
ProcessConfig {
graceful_stop: Duration::from_millis(200),
..ProcessConfig::default()
}
}
#[test]
fn spawn_collects_both_streams_and_exit_code() {
let root = temp_sessions_root("smoke");
let meta = SessionMeta::new("inst-1", "smoke", "1.20.1");
let writer =
SessionWriter::create_with_id(&root, "smoke-1", meta, Redactor::new()).expect("创建会话");
let path = writer.path().to_path_buf();
let sink = Arc::new(SessionSink::new(writer));
let mut child = RunningProcess::spawn(
&shell_command("echo hello-from-stdout & echo hello-from-stderr 1>&2"),
None,
sink.clone(),
config(),
)
.expect("spawn 应成功");
let status = child.wait().expect("wait 应成功");
assert_eq!(process::exit_code(&status), 0);
assert!(child.pid() > 0);
let verdict = child.supervise();
assert!(verdict.is_complete(), "两条流都应在预算内结束");
let texts: Vec<String> = child.lines().into_iter().map(|line| line.text).collect();
assert!(
texts.iter().any(|text| text.contains("hello-from-stdout")),
"stdout 未收集到:{texts:?}"
);
assert!(
texts.iter().any(|text| text.contains("hello-from-stderr")),
"stderr 未收集到:{texts:?}"
);
// 事件流里应有 stdout/stderr 行事件。
let events = read_events(&path);
let stdout_lines = events
.iter()
.filter(|event| event["kind"] == "stdout")
.count();
let stderr_lines = events
.iter()
.filter(|event| event["kind"] == "stderr")
.count();
assert!(stdout_lines >= 1, "应有 stdout 事件");
assert!(stderr_lines >= 1, "应有 stderr 事件");
}
#[test]
fn stop_kills_process_tree() {
#[cfg(windows)]
let script = "ping -n 30 127.0.0.1 >nul";
#[cfg(not(windows))]
let script = "sleep 30";
let mut child =
RunningProcess::spawn(&shell_command(script), None, Arc::new(NullSink), config())
.expect("spawn 应成功");
assert!(child.is_running());
child.stop().expect("停止应成功");
let status = child.wait().expect("wait 应成功");
assert_ne!(
process::exit_code(&status),
0,
"被结束的进程不应返回退出码 0"
);
assert!(!child.is_running());
}
#[test]
fn spawn_reports_missing_program() {
let mut command = shell_command("echo hi");
command.program = PathBuf::from("definitely-not-a-real-binary-ull");
let error = RunningProcess::spawn(&command, None, Arc::new(NullSink), config())
.expect_err("程序不存在必须报错");
assert!(matches!(error, LaunchError::Spawn { .. }));
}
#[test]
fn line_sink_trait_object_is_usable() {
let sink: Arc<dyn LineSink> = Arc::new(NullSink);
sink.on_line(&LogLine::new(Stream::Stdout, "ignored"));
}