176 lines
8.1 KiB
Rust
176 lines
8.1 KiB
Rust
//! 集成测试:通过 shell out 到 `target/release/wl-webrtc` 二进制来验证 CLI 行为。
|
||
//!
|
||
//! 与单元测试(在进程内调用库函数)不同,集成测试把产物当作黑盒,启动子进程
|
||
//! 并检查其 stdout/stderr/exit code。这种模式类似 Go 的 `testing` 包配合
|
||
//! `os/exec.Command(...)` —— Rust 这边对应 `std::process::Command::new(...)`,
|
||
//! 通过 `.arg(...)` 链式追加参数,最后 `.output()` 一次性等待子进程结束并
|
||
//! 拿到 `Output { status, stdout, stderr }`。
|
||
//!
|
||
//! # 运行前必读
|
||
//!
|
||
//! 这些测试**依赖 release 版二进制存在**。`cargo test --test integration_test`
|
||
//! 本身不会触发 release 构建,必须先手动执行:
|
||
//!
|
||
//! ```bash
|
||
//! cargo build --release
|
||
//! ```
|
||
//!
|
||
//! 否则 `Command::new("target/release/wl-webrtc")` 会因为找不到可执行文件而 panic,
|
||
//! 所有 `#[test]` 都将以 "failed to execute" 失败。详见 `AGENTS.md`
|
||
//! "Testing and verification" 章节。
|
||
//!
|
||
//! # 测试发现与断言
|
||
//!
|
||
//! `cargo test` 通过 `#[test]` 属性宏自动发现并运行标记的函数,无需像 Go 那样
|
||
//! 约定 `TestXxx(t *testing.T)` 签名 —— 普通函数加 `#[test]` 即可。
|
||
//! `#[ignore]` 标记的测试默认跳过,需 `cargo test -- --ignored` 显式开启。
|
||
//!
|
||
//! 断言方面,`assert!(cond, "msg")` 类似 Go 的 `if !cond { t.Errorf("msg") }`,
|
||
//! 但 Rust 会在失败时立即 unwind 当前测试函数(而非 Go 那样继续执行后续断言)。
|
||
//! 当测试函数返回 `Result<(), E>` 时,可用 `?` 操作符把 IO/解码错误直接传播
|
||
//! 给测试 harness(失败时打印 `Err` 而非 `panic!`);本文件的测试为了聚焦于
|
||
//! 子进程行为,全部用 `.expect(...)`/`assert!` 风格,不返回 `Result`。
|
||
|
||
use std::process::Command;
|
||
|
||
/// Helper: get the binary path. Uses the release build if available.
|
||
///
|
||
/// 返回被测二进制的路径。集成测试 shell out 到 release 构建产物(debug 构建太慢,
|
||
/// 且无法真实反映发布行为)。返回 `&'static str` 而非 `PathBuf` 是因为这个路径
|
||
/// 是编译期常量,无需在每次调用时分配。
|
||
fn bin_path() -> &'static str {
|
||
"target/release/wl-webrtc"
|
||
}
|
||
|
||
/// 测试 `--help` 子命令:应正常退出(exit 0),且 stdout 至少包含关键字段名。
|
||
///
|
||
/// 验证 README/AGENTS.md 中列出的核心 CLI 参数(output/fps/codec/bitrate/gop-size/drm-device)
|
||
/// 都能在 `--help` 输出中找到 —— 这是一道"防回归"测试:一旦某参数被改名或删除,
|
||
/// 此处 `assert!` 会立刻失败。
|
||
#[test]
|
||
fn test_help_flag() {
|
||
// `Command::new(...)` 类似 Go 的 `exec.Command(...)`:构造一个待运行的
|
||
// 子进程描述符,此时还未真正 fork/exec。链式 `.arg(...)` 把参数逐个追加到
|
||
// 命令行末尾(保持顺序),`.output()` 则 fork、exec、等待子进程退出,并
|
||
// 一次性捕获 stdout/stderr 到 `Output` 结构体。
|
||
//
|
||
// `.expect(...)` 等价于 `match result { Ok(v) => v, Err(_) => panic!(...) }`,
|
||
// 用于在父进程侧(不是被测程序侧)报告"无法启动子进程"这种环境性错误。
|
||
let output = Command::new(bin_path())
|
||
.arg("--help")
|
||
.output()
|
||
.expect("failed to execute wl-webrtc --help");
|
||
|
||
// `String::from_utf8_lossy(...)` 把 `Vec<u8>` 字节流解码成字符串;遇到非法
|
||
// UTF-8 序列时用 U+FFFD 替换而非报错。这里用 `_lossy` 变体而非
|
||
// `String::from_utf8(...)?` 是因为 stdout 理论上可能包含任意字节(如 ANSI
|
||
// color code 失控),不值得为解码失败让整个测试 panic。
|
||
let stdout = String::from_utf8_lossy(&output.stdout);
|
||
// `output.status.success()` 检查子进程退出码是否为 0(Unix 上即 WIFEXITED
|
||
// 且 exit code == 0)。`assert!(cond, "msg")` 失败时打印 `msg` 并 panic
|
||
// 当前测试函数,类似 Go 的 `t.Fatalf` 而非 `t.Errorf`。
|
||
assert!(output.status.success(), "--help should exit 0");
|
||
// 后续 `assert!(stdout.contains(...), "...")` 检查帮助文本是否覆盖每个
|
||
// 文档化参数。任何一个缺失都会让测试失败并打印自定义消息。
|
||
assert!(
|
||
stdout.contains("output"),
|
||
"help output should mention 'output'"
|
||
);
|
||
assert!(stdout.contains("fps"), "help output should mention 'fps'");
|
||
assert!(
|
||
stdout.contains("codec"),
|
||
"help output should mention 'codec'"
|
||
);
|
||
assert!(
|
||
stdout.contains("bitrate"),
|
||
"help output should mention 'bitrate'"
|
||
);
|
||
assert!(
|
||
stdout.contains("gop-size"),
|
||
"help output should mention 'gop-size'"
|
||
);
|
||
assert!(
|
||
stdout.contains("drm-device"),
|
||
"help output should mention 'drm-device'"
|
||
);
|
||
}
|
||
|
||
/// 测试未知参数应被拒绝:非零退出码 + stderr 包含 error/unexpected/unrecognized 之一。
|
||
///
|
||
/// `clap` 默认对未识别的 flag 返回非零退出码并打印错误到 stderr。这里用
|
||
/// `!output.status.success()` 断言"应该失败",再检查 stderr 文本以排除"碰巧
|
||
/// 崩溃退出"的假阳性。
|
||
#[test]
|
||
fn test_rejects_invalid_args() {
|
||
let output = Command::new(bin_path())
|
||
.arg("--nonexistent-flag-xyz")
|
||
.output()
|
||
.expect("failed to execute wl-webrtc with invalid args");
|
||
|
||
// 断言"非零退出码"。注意 `!` 取反 —— 与 Go 的 `t.Errorf` 风格不同,Rust 的
|
||
// `assert!` 直接接受 bool 表达式,没有 `assertFalse` 这种专门函数。
|
||
assert!(!output.status.success(), "should reject unrecognized flag");
|
||
let stderr = String::from_utf8_lossy(&output.stderr);
|
||
// 多个可能的错误措辞用 `||` 连接 —— 不同 clap 版本可能输出 "error: unexpected"
|
||
// 或 "error: unrecognized",任一匹配即可。自定义消息末尾的 `{stderr}` 利用
|
||
// `format!` 占位符在失败时打印实际 stderr 内容,便于排错。
|
||
assert!(
|
||
stderr.to_lowercase().contains("error")
|
||
|| stderr.to_lowercase().contains("unexpected")
|
||
|| stderr.to_lowercase().contains("unrecognized"),
|
||
"stderr should mention the error, got: {stderr}"
|
||
);
|
||
}
|
||
|
||
/// 测试 `--codec hevc` 在 MVP 阶段应被拒绝:MVP 只支持 h264。
|
||
#[test]
|
||
fn test_rejects_hevc_codec() {
|
||
// 多个 `.arg(...)` 链式调用按顺序追加参数,等价于命令行
|
||
// `wl-webrtc --output /dev/null --codec hevc`。
|
||
let output = Command::new(bin_path())
|
||
.arg("--output")
|
||
.arg("/dev/null")
|
||
.arg("--codec")
|
||
.arg("hevc")
|
||
.output()
|
||
.expect("failed to execute wl-webrtc --codec hevc");
|
||
|
||
// MVP only supports h264; hevc should be rejected.
|
||
assert!(!output.status.success(), "should reject hevc codec in MVP");
|
||
}
|
||
|
||
/// Tests requiring a live Wayland compositor and VAAPI hardware.
|
||
/// Run with: cargo test -- --ignored
|
||
///
|
||
/// 该测试需要真实 Wayland 会话 + VAAPI GPU + 可写输出路径,无法在 CI 中运行。
|
||
/// `#[ignore]` 属性告诉 `cargo test` 默认跳过它,只有显式
|
||
/// `cargo test -- --ignored` 时才执行。
|
||
///
|
||
/// 注意:此测试只验证"参数解析不立即报错",并未真正完成捕获 —— 真正的捕获
|
||
/// 需要异步等待几秒再发 SIGINT,这里只做最小烟雾测试。
|
||
#[test]
|
||
#[ignore]
|
||
fn test_capture_starts_with_valid_output() {
|
||
// This test requires:
|
||
// - A running Wayland compositor
|
||
// - VAAPI-compatible GPU hardware
|
||
// - A writable output path
|
||
let output = Command::new(bin_path())
|
||
.arg("--output")
|
||
.arg("/tmp/wl-webrtc-test-capture.mp4")
|
||
.arg("--fps")
|
||
.arg("10")
|
||
.output()
|
||
.expect("failed to start wl-webrtc");
|
||
|
||
// If we get here, the process ran and exited.
|
||
// In a real test, we'd give it time to capture a few frames
|
||
// and then send SIGINT, but for now just check it didn't
|
||
// immediately fail with an argument error.
|
||
let stderr = String::from_utf8_lossy(&output.stderr);
|
||
assert!(
|
||
!stderr.contains("error: unexpected") && !stderr.contains("error: invalid"),
|
||
"should not have argument parsing errors, got: {stderr}"
|
||
);
|
||
}
|