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
Rust 原生单文件 Minecraft 启动器:CLI 完整可用(install/launch/accounts/java/diagnose/agent), MCP server 接入外部 Agent 工具,GUI 开发中。
128 lines
3.9 KiB
Rust
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"));
|
|
}
|