docs(backend_detect): 中文注释后端检测逻辑与 ashpd 规避原因
This commit is contained in:
@@ -1,3 +1,45 @@
|
|||||||
|
//! # Wayland 截屏后端自动检测(`src/backend_detect.rs`)
|
||||||
|
//!
|
||||||
|
//! 本文件负责检测当前 Wayland 桌面支持哪种屏幕捕获后端,由 [`detect_backend`]
|
||||||
|
//! 返回 [`CaptureBackend::WlrScreencopy`](wlroots 合成器:Sway/Hyprland 等,
|
||||||
|
//! 通过 `zwlr_screencopy_manager_v1` 协议直接交付 dmabuf,性能最好)或
|
||||||
|
//! [`CaptureBackend::PortalPipeWire`](XDG Portal + PipeWire:KDE/GNOME 等,
|
||||||
|
//! 通过 D-Bus 调用 `org.freedesktop.portal.ScreenCast` 接口)。
|
||||||
|
//!
|
||||||
|
//! ## 检测优先级(见 [`detect_backend`])
|
||||||
|
//!
|
||||||
|
//! 1. 用户显式 `--backend portal|screencopy` 命令行参数覆盖;
|
||||||
|
//! 2. 自动检测:wlr-screencopy 优先(通过 Wayland globals 列表),否则回退到 Portal
|
||||||
|
//! (通过 D-Bus 查询 ScreenCast 接口的 `version` 属性 >=1 即视为可用)。
|
||||||
|
//!
|
||||||
|
//! ## 为什么用 raw `zbus` 而不是 `ashpd`(**AGENTS.md 强约束**)
|
||||||
|
//!
|
||||||
|
//! AGENTS.md 明确禁止在此文件使用 `ashpd` crate,原因是:
|
||||||
|
//! `ashpd` 内部把 `zbus::Connection` 缓存在一个全局 `OnceLock`。
|
||||||
|
//! 如果拥有该 connection 的 Tokio runtime 被 drop(例如本文件
|
||||||
|
//! [`check_portal_available`] 自建的临时 runtime 在函数返回时被 drop),
|
||||||
|
//! 缓存的 connection 会变成"僵尸"——后续 `setup_portal()` 复用时会永远 hang,
|
||||||
|
//! 因为底层 `tokio::mpsc` 通道对端已死、但缓存仍报告"已初始化"。
|
||||||
|
//!
|
||||||
|
//! 因此本文件用 `zbus::connection::Builder::session()...build().await` 直接构造
|
||||||
|
//! 一条全新的、生命周期受当前 runtime 控制的连接,每次检测都重建。
|
||||||
|
//!
|
||||||
|
//! ## Go ↔ Rust 概念对照
|
||||||
|
//!
|
||||||
|
//! - `async fn` + `.await`:Rust async 是**惰性的**(async fn 返回 `impl Future`,
|
||||||
|
//! 必须被 `.await` 或 `block_on` 才会真正执行),不同于 Go 的 `go f()` 立即并发。
|
||||||
|
//! - `tokio::runtime::Runtime::new()` + `rt.block_on(fut)`:从同步代码驱动 async,
|
||||||
|
//! 类比 Go `runtime.GOMAXPROCS(1)` + `select { case <-done: }`。
|
||||||
|
//! - `tokio::time::timeout(d, fut).await` ≈ Go `context.WithTimeout(ctx, d)`,
|
||||||
|
//! 返回 `Result<T, Elapsed>`,超时返回 `Err(Elapsed)`。
|
||||||
|
//! - `Result<T, E>` + `?` 操作符 ≈ Go `if err != nil { return err }` 的语法糖。
|
||||||
|
//! - `Option<T>` ≈ Go `*T`(指针可空),但 Rust 强制 `match`/`if let` 才能解引用。
|
||||||
|
//! - `tracing::info!("...{e}")` ≈ Go `log.Printf`,支持 Rust 1.58+ 的内联捕获格式化。
|
||||||
|
//! - `match { ... }` ≈ Go `switch`,但 Rust 强制穷尽所有分支(编译期检查)。
|
||||||
|
//! - `&mut T`(可变引用)≈ Go `*T`,但 Rust 编译期保证无别名(只有一个 mut 引用)。
|
||||||
|
//! - `move || { ... }` 闭包用 `move` 关键字显式捕获变量所有权(按值转移)。
|
||||||
|
//! - `'static` 生命周期约束 ≈ Go"对象不能持有栈指针"的隐式约定,但 Rust 编译期检查。
|
||||||
|
|
||||||
use std::time::Duration;
|
use std::time::Duration;
|
||||||
|
|
||||||
use anyhow::Result;
|
use anyhow::Result;
|
||||||
@@ -26,8 +68,15 @@ pub enum CaptureBackend {
|
|||||||
/// 用于后端检测期间列举 Wayland 全局对象的最小化分发类型(无需实际处理事件)
|
/// 用于后端检测期间列举 Wayland 全局对象的最小化分发类型(无需实际处理事件)
|
||||||
struct RegistryLs;
|
struct RegistryLs;
|
||||||
|
|
||||||
|
// trait 分发:`Dispatch<WlRegistry, GlobalListContents> for RegistryLs` 表示
|
||||||
|
// "用 RegistryLs 作为状态对象、GlobalListContents 作为上下文数据来处理 WlRegistry 事件"。
|
||||||
|
// 类比 Go interface 的隐式满足,但 Rust trait 在编译期静态分发(generic 单态化),
|
||||||
|
// 即编译器为每个 (State, Event) 组合生成一份专属代码——零运行时开销。
|
||||||
// 为 RegistryLs 实现 Wayland 注册表事件分发(空实现,仅需类型满足 trait 约束)
|
// 为 RegistryLs 实现 Wayland 注册表事件分发(空实现,仅需类型满足 trait 约束)
|
||||||
impl Dispatch<WlRegistry, GlobalListContents> for RegistryLs {
|
impl Dispatch<WlRegistry, GlobalListContents> for RegistryLs {
|
||||||
|
// `fn event` 是 Dispatch trait 必须实现的方法:每收到一个 Wayland 事件触发一次。
|
||||||
|
// 下划线前缀参数(`_state`、`_registry` 等):Rust 编译器允许声明但不使用,
|
||||||
|
// 类比 Go 中 `_ = ctx` 显式忽略变量;这里我们只关心类型满足 trait、不处理事件。
|
||||||
fn event(
|
fn event(
|
||||||
_state: &mut Self,
|
_state: &mut Self,
|
||||||
_registry: &WlRegistry,
|
_registry: &WlRegistry,
|
||||||
@@ -47,6 +96,10 @@ impl Dispatch<WlRegistry, GlobalListContents> for RegistryLs {
|
|||||||
/// Portal 后端检测期间每个 D-Bus 操作的超时时间。
|
/// Portal 后端检测期间每个 D-Bus 操作的超时时间。
|
||||||
const PORTAL_DBUS_TIMEOUT: Duration = Duration::from_secs(5);
|
const PORTAL_DBUS_TIMEOUT: Duration = Duration::from_secs(5);
|
||||||
|
|
||||||
|
/// 当 Portal 在超时时间内无响应时,记录详细的错误日志(含 systemctl 重启建议)。
|
||||||
|
///
|
||||||
|
/// 这是一个辅助函数——调用方已经在超时路径上返回了 `false`,本函数仅负责打印提示。
|
||||||
|
/// 不返回 `Result`:日志写入失败本身不应该影响后端检测逻辑。
|
||||||
fn log_portal_unresponsive(operation: &str) {
|
fn log_portal_unresponsive(operation: &str) {
|
||||||
tracing::error!(
|
tracing::error!(
|
||||||
"Portal service did not respond within 5s while {operation}. \
|
"Portal service did not respond within 5s while {operation}. \
|
||||||
@@ -56,20 +109,54 @@ fn log_portal_unresponsive(operation: &str) {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// 通过 D-Bus 检测 XDG Portal ScreenCast 接口是否可用。
|
||||||
|
///
|
||||||
|
/// 检测流程(每一步都有 5 秒超时保护,见 [`PORTAL_DBUS_TIMEOUT`]):
|
||||||
|
/// 1. 连接到 D-Bus session bus;
|
||||||
|
/// 2. 构造 `org.freedesktop.portal.Desktop` 的 ScreenCast proxy;
|
||||||
|
/// 3. 查询 ScreenCast 接口的 `version` 属性(>=1 即视为可用)。
|
||||||
|
///
|
||||||
|
/// 任何一步超时或失败都返回 `false`——上层 [`detect_backend`] 据此决定回退策略。
|
||||||
|
///
|
||||||
|
/// # 同步外壳 + 异步内核
|
||||||
|
///
|
||||||
|
/// `check_portal_available` 本身是同步 `fn`(被同步的 [`detect_backend`] 调用),
|
||||||
|
/// 但内部通过 `tokio::runtime::Runtime::new()` + `rt.block_on(async { ... })`
|
||||||
|
/// 桥接到 async `zbus` API。类比 Go:`func check() bool { rt := NewRuntime(); defer rt.Close(); return rt.BlockOn(asyncFn()) }`。
|
||||||
fn check_portal_available() -> bool {
|
fn check_portal_available() -> bool {
|
||||||
|
// 创建独立的 Tokio runtime:外层 `detect_backend` 是同步 `fn`,没有 async runtime
|
||||||
|
// 上下文,需要自建一个来驱动 `.await`。
|
||||||
|
// 类比 Go:每次调用 `runtime.GOMAXPROCS(1)` 启动一个临时调度器。
|
||||||
|
// **关键**:这个 runtime 在函数结束时 drop——这也是为什么不能用 ashpd
|
||||||
|
// (ashpd 缓存 connection 到全局,runtime drop 后 connection 变僵尸,见文件头注释)。
|
||||||
let rt = match tokio::runtime::Runtime::new() {
|
let rt = match tokio::runtime::Runtime::new() {
|
||||||
Ok(rt) => rt,
|
Ok(rt) => rt,
|
||||||
Err(e) => {
|
Err(e) => {
|
||||||
|
// `tracing::warn!` 宏:结构化日志,类比 Go `log.Printf`,
|
||||||
|
// 但支持 Rust 1.58+ 的 `{e}` 内联捕获格式化(变量名直接作占位符)。
|
||||||
tracing::warn!("Failed to create tokio runtime for portal check: {e}");
|
tracing::warn!("Failed to create tokio runtime for portal check: {e}");
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
|
// `rt.block_on(future)`:在当前同步线程上驱动 future 到完成。
|
||||||
|
// 类比 Go:`select { case <-done: }` 阻塞等待 goroutine 结束。
|
||||||
|
// 但 Rust 的 `block_on` 是单线程内 cooperatively 调度 future(除非 runtime 配 multi-thread)。
|
||||||
rt.block_on(async {
|
rt.block_on(async {
|
||||||
|
// `async { ... }` 块构造一个匿名 Future(类比 Go `func() {}` 闭包)。
|
||||||
|
// 注意:async 块是惰性的——只有 `.await` 或 `block_on` 才会真正执行体内代码。
|
||||||
// Set method_timeout on the connection (bounds method replies) and wrap
|
// Set method_timeout on the connection (bounds method replies) and wrap
|
||||||
// the build itself in tokio::time::timeout (bounds connection setup).
|
// the build itself in tokio::time::timeout (bounds connection setup).
|
||||||
// 同时设置 method_timeout 与外层 tokio::time::timeout 双重保护。
|
// 同时设置 method_timeout 与外层 tokio::time::timeout 双重保护。
|
||||||
|
// `tokio::time::timeout(d, fut)` ≈ Go `context.WithTimeout(ctx, d)`,
|
||||||
|
// 返回 `Result<T, Elapsed>`——超时返回 `Err(Elapsed)`。
|
||||||
let conn = match tokio::time::timeout(PORTAL_DBUS_TIMEOUT, async {
|
let conn = match tokio::time::timeout(PORTAL_DBUS_TIMEOUT, async {
|
||||||
|
// `zbus::connection::Builder::session()` 是 Builder 模式:
|
||||||
|
// 类比 Go `&http.Client{Timeout: ...}` 用链式方法配置参数。
|
||||||
|
// `.expect("...")`:失败时 panic(类比 Go `log.Panic`),
|
||||||
|
// 只用于"不可能失败"的构造——这里 session bus builder 几乎不会失败。
|
||||||
|
// `.method_timeout(...)` 设置单个 D-Bus 方法调用的超时上限。
|
||||||
|
// `.build().await` 异步构造 Connection(涉及 D-Bus 握手)。
|
||||||
zbus::connection::Builder::session()
|
zbus::connection::Builder::session()
|
||||||
.expect("D-Bus session bus builder failed")
|
.expect("D-Bus session bus builder failed")
|
||||||
.method_timeout(PORTAL_DBUS_TIMEOUT)
|
.method_timeout(PORTAL_DBUS_TIMEOUT)
|
||||||
@@ -78,17 +165,29 @@ fn check_portal_available() -> bool {
|
|||||||
})
|
})
|
||||||
.await
|
.await
|
||||||
{
|
{
|
||||||
|
// 嵌套 Result 解构:外层 `Result<Connection, Elapsed>`(来自 timeout),
|
||||||
|
// 内层 `Result<Connection, zbus::Error>`(来自 build)。
|
||||||
|
// `Ok(Ok(c)) => c` 是模式匹配的多层解构(destructuring)——
|
||||||
|
// 类比 Go `if err == nil && inner_err == nil { c := value }`。
|
||||||
Ok(Ok(c)) => c,
|
Ok(Ok(c)) => c,
|
||||||
Ok(Err(e)) => {
|
Ok(Err(e)) => {
|
||||||
tracing::info!("D-Bus session bus unavailable: {e}");
|
tracing::info!("D-Bus session bus unavailable: {e}");
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
Err(_) => {
|
Err(_) => {
|
||||||
|
// `Err(_)` 中的 `_` 是通配符模式:匹配任意值并丢弃。
|
||||||
|
// 这里我们关心的是"超时了",不关心 `Elapsed` 的具体值。
|
||||||
log_portal_unresponsive("connecting to D-Bus session bus");
|
log_portal_unresponsive("connecting to D-Bus session bus");
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
|
// `zbus::Proxy`:D-Bus proxy 是远程对象的强类型句柄,封装 destination+path+interface。
|
||||||
|
// 类比 Go 中的 `dbus.ObjectProxy`:调用 `proxy.get_property(...)` 时
|
||||||
|
// 自动 marshal 成 D-Bus 消息发到目标对象。
|
||||||
|
// `Builder::new(&conn).destination(...).and_then(|b| b.path(...))` 链式构造:
|
||||||
|
// `and_then` 来自 `Result`,把 `Result<Builder, E>` 解开再继续链——
|
||||||
|
// 类比 Go `if b, err := b.X(); err != nil { return err } else { b.Y() }`。
|
||||||
let inner: zbus::Proxy = match zbus::proxy::Builder::new(&conn)
|
let inner: zbus::Proxy = match zbus::proxy::Builder::new(&conn)
|
||||||
.destination("org.freedesktop.portal.Desktop")
|
.destination("org.freedesktop.portal.Desktop")
|
||||||
.and_then(|b| b.path("/org/freedesktop/portal/desktop"))
|
.and_then(|b| b.path("/org/freedesktop/portal/desktop"))
|
||||||
@@ -111,6 +210,10 @@ fn check_portal_available() -> bool {
|
|||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
|
// 查询 ScreenCast 接口的 `version` 属性——这是最可能卡住的操作,
|
||||||
|
// 因为前两步只是本地构造 proxy,而 get_property 需要 Portal 端实际处理请求。
|
||||||
|
// `.get_property::<u32>("version")`:泛型方法,turbofish `::<u32>` 指定返回类型,
|
||||||
|
// 类比 Go `GetVersion() (uint32, error)`——但 Rust 用泛型 + 编译期单态化。
|
||||||
// The most likely operation to hang — requires actual Portal-side work.
|
// The most likely operation to hang — requires actual Portal-side work.
|
||||||
// 最可能卡住的操作,需要 Portal 端实际处理。
|
// 最可能卡住的操作,需要 Portal 端实际处理。
|
||||||
let version = match tokio::time::timeout(
|
let version = match tokio::time::timeout(
|
||||||
@@ -137,10 +240,26 @@ fn check_portal_available() -> bool {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// 通过 Wayland globals 检测 wlr-screencopy 协议是否可用
|
// 通过 Wayland globals 检测 wlr-screencopy 协议是否可用
|
||||||
|
//
|
||||||
|
// Wayland globals 是合成器在连接建立时广播的"已支持协议"列表——
|
||||||
|
// 类比 Go 中的 HTTP OPTIONS:客户端连上服务器后先查询能力,再决定怎么说话。
|
||||||
|
// 我们只需检查列表里是否有 `zwlr_screencopy_manager_v1` 这个接口名即可。
|
||||||
fn check_screencopy_available() -> Result<bool> {
|
fn check_screencopy_available() -> Result<bool> {
|
||||||
|
// `Connection::connect_to_env()?`:从 WAYLAND_DISPLAY 环境变量读取 socket 路径并连接。
|
||||||
|
// `?` 操作符:如果 `connect_to_env` 返回 `Err(e)`,立即把 `e` 转换为函数返回类型
|
||||||
|
// (`anyhow::Result`),并 return 之。类比 Go `if err != nil { return err }`。
|
||||||
let conn = Connection::connect_to_env()?;
|
let conn = Connection::connect_to_env()?;
|
||||||
|
// `registry_queue_init::<RegistryLs>(&conn)?`:turbofish `::<RegistryLs>` 指定
|
||||||
|
// 用我们刚定义的空 Dispatch 实现来接收 registry 事件。函数内部会 roundtrip
|
||||||
|
// 一次拿到所有 globals,返回 `(GlobalList, Queue)` 元组。
|
||||||
|
// `let (globals, _queue) = ...`:元组解构(tuple destructuring),
|
||||||
|
// 类比 Go `globals, queue := ...`,但 Rust 用 `_queue` 表示"我接收但不会用到"。
|
||||||
let (globals, _queue) = registry_queue_init::<RegistryLs>(&conn)?;
|
let (globals, _queue) = registry_queue_init::<RegistryLs>(&conn)?;
|
||||||
|
|
||||||
|
// 迭代器链式调用(zero-cost,编译期单态化):
|
||||||
|
// `.contents()` → `GlobalList`;`.clone_list()` → `Vec<Global>`;
|
||||||
|
// `.iter()` → `Iterator<&Global>`;`.any(|g| ...)` → `bool`(短路求值)。
|
||||||
|
// `|g| g.interface == "..."` 是闭包(closure),类比 Go `func(g Global) bool { ... }`。
|
||||||
let has_screencopy = globals
|
let has_screencopy = globals
|
||||||
.contents()
|
.contents()
|
||||||
.clone_list()
|
.clone_list()
|
||||||
@@ -171,7 +290,14 @@ fn check_screencopy_available() -> Result<bool> {
|
|||||||
pub fn detect_backend(args: &Args) -> Result<CaptureBackend> {
|
pub fn detect_backend(args: &Args) -> Result<CaptureBackend> {
|
||||||
// 1. Check explicit override
|
// 1. Check explicit override
|
||||||
// 步骤 1:检查用户是否通过命令行参数显式指定了后端
|
// 步骤 1:检查用户是否通过命令行参数显式指定了后端
|
||||||
|
// `if let Some(ref backend) = args.backend`:模式匹配 + `ref` 关键字。
|
||||||
|
// `args.backend` 类型是 `Option<String>`,`Some(ref backend)` 表示
|
||||||
|
// "如果是 Some,则把内部 String 的**引用**绑定到 backend"(不获取所有权)。
|
||||||
|
// 类比 Go `if args.Backend != nil { backend := args.Backend }`。
|
||||||
if let Some(ref backend) = args.backend {
|
if let Some(ref backend) = args.backend {
|
||||||
|
// `backend.as_str()`:把 `&String` 转 `&str`(类比 Go string → []byte view)。
|
||||||
|
// `match backend.as_str() { ... }`:Rust 的 match 对 `&str` 强制穷尽所有分支,
|
||||||
|
// 类比 Go `switch backend { case "portal": ...; default: ... }`,但没有隐式 fallthrough。
|
||||||
return match backend.as_str() {
|
return match backend.as_str() {
|
||||||
"portal" => {
|
"portal" => {
|
||||||
tracing::info!("Backend override: Portal/PipeWire");
|
tracing::info!("Backend override: Portal/PipeWire");
|
||||||
@@ -182,7 +308,10 @@ pub fn detect_backend(args: &Args) -> Result<CaptureBackend> {
|
|||||||
Ok(CaptureBackend::WlrScreencopy)
|
Ok(CaptureBackend::WlrScreencopy)
|
||||||
}
|
}
|
||||||
other => {
|
other => {
|
||||||
|
// `other` 是匹配模式变量:绑定未被前面 arm 命中的任意值(类比 Go `default`)。
|
||||||
// 未知后端名称,返回错误
|
// 未知后端名称,返回错误
|
||||||
|
// `anyhow::bail!("...", args)` 是宏(注意 `!`):立即构造 `anyhow::Error`
|
||||||
|
// 并从当前函数 return `Err`。类比 Go `return fmt.Errorf("...", ...)`。
|
||||||
anyhow::bail!("Unknown backend '{}'. Use 'screencopy' or 'portal'.", other);
|
anyhow::bail!("Unknown backend '{}'. Use 'screencopy' or 'portal'.", other);
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
@@ -193,11 +322,18 @@ pub fn detect_backend(args: &Args) -> Result<CaptureBackend> {
|
|||||||
tracing::info!("Auto-detecting capture backend...");
|
tracing::info!("Auto-detecting capture backend...");
|
||||||
|
|
||||||
// 检测 wlr-screencopy(通过 Wayland globals)
|
// 检测 wlr-screencopy(通过 Wayland globals)
|
||||||
|
// `check_screencopy_available()?` 末尾的 `?`:把 `Result<bool>` 解开——
|
||||||
|
// 成功取 bool,失败则立即 return `Err`(错误向上传播)。
|
||||||
let has_screencopy = check_screencopy_available()?;
|
let has_screencopy = check_screencopy_available()?;
|
||||||
// 检测 Portal(通过 D-Bus)
|
// 检测 Portal(通过 D-Bus)
|
||||||
|
// `check_portal_available()` 无 `?`:因为它返回的是 `bool` 而不是 `Result`,
|
||||||
|
// 内部已经把所有错误吞掉并转为 `false`。
|
||||||
let has_portal = check_portal_available();
|
let has_portal = check_portal_available();
|
||||||
|
|
||||||
// 根据检测结果选择后端,screencopy 优先(性能更好、延迟更低)
|
// 根据检测结果选择后端,screencopy 优先(性能更好、延迟更低)
|
||||||
|
// `match (has_screencopy, has_portal) { ... }`:元组匹配——同时匹配两个 bool。
|
||||||
|
// `(true, _)` 中的 `_` 是通配符:表示"任意值都匹配"。类比 Go `switch { case hasSC: ... }`。
|
||||||
|
// Rust 强制穷尽所有 (bool, bool) 组合,编译期检查,不能漏掉一个分支。
|
||||||
match (has_screencopy, has_portal) {
|
match (has_screencopy, has_portal) {
|
||||||
(true, _) => {
|
(true, _) => {
|
||||||
tracing::info!("Detected wlr-screencopy support → using WlrScreencopy backend");
|
tracing::info!("Detected wlr-screencopy support → using WlrScreencopy backend");
|
||||||
@@ -217,11 +353,18 @@ pub fn detect_backend(args: &Args) -> Result<CaptureBackend> {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// `#[cfg(test)]` 属性:条件编译——`cargo build` 时这个 mod 不会被编译进二进制,
|
||||||
|
// 只有 `cargo test` 时才参与编译。这样发布产物零运行时开销。
|
||||||
|
// 类比 Go 中 `_test.go` 后缀的约定:测试代码与生产代码物理分离。
|
||||||
#[cfg(test)]
|
#[cfg(test)]
|
||||||
mod tests {
|
mod tests {
|
||||||
|
// `use super::*;`:glob 导入(wildcard import),把父模块的所有 pub item 引入当前作用域。
|
||||||
|
// 类比 Go 中的 dot-import(`. "pkg"`),但 Rust 限定在 `super::` 即父模块内。
|
||||||
|
// 这里用来在测试中直接访问 `detect_backend`、`CaptureBackend` 等。
|
||||||
use super::*;
|
use super::*;
|
||||||
|
|
||||||
// 测试辅助函数:构造指定后端参数的 Args 实例
|
// 测试辅助函数:构造指定后端参数的 Args 实例
|
||||||
|
// 注意:辅助函数不需要 `#[test]` 属性——它只是被测试函数调用的普通函数。
|
||||||
fn make_args(backend: Option<&str>) -> Args {
|
fn make_args(backend: Option<&str>) -> Args {
|
||||||
Args {
|
Args {
|
||||||
output: Some("test.mp4".to_string()),
|
output: Some("test.mp4".to_string()),
|
||||||
@@ -242,11 +385,17 @@ mod tests {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// 测试:显式指定 portal 后端
|
// 测试:显式指定 portal 后端
|
||||||
|
// `#[test]` 属性:标记此函数为测试用例,`cargo test` 自动发现并执行。
|
||||||
|
// 测试函数约定:`fn name() {}` 无参数无返回值;panic 即测试失败。
|
||||||
#[test]
|
#[test]
|
||||||
fn explicit_portal_backend() {
|
fn explicit_portal_backend() {
|
||||||
let args = make_args(Some("portal"));
|
let args = make_args(Some("portal"));
|
||||||
let result = detect_backend(&args);
|
let result = detect_backend(&args);
|
||||||
|
// `assert!(cond)` 宏:条件为 false 时 panic,类比 Go `if !cond { t.Fatal() }`。
|
||||||
assert!(result.is_ok());
|
assert!(result.is_ok());
|
||||||
|
// `assert_eq!(a, b)` 宏:断言相等,失败时打印两边内容,类比 Go `if a != b { t.Errorf() }`。
|
||||||
|
// `.unwrap()`:解开 Result——成功取内部值,失败 panic。
|
||||||
|
// 测试代码中常用 `unwrap()` 简化错误处理;生产代码应避免(用 `?` 替代)。
|
||||||
assert_eq!(result.unwrap(), CaptureBackend::PortalPipeWire);
|
assert_eq!(result.unwrap(), CaptureBackend::PortalPipeWire);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -265,6 +414,8 @@ mod tests {
|
|||||||
let args = make_args(Some("magic"));
|
let args = make_args(Some("magic"));
|
||||||
let result = detect_backend(&args);
|
let result = detect_backend(&args);
|
||||||
assert!(result.is_err());
|
assert!(result.is_err());
|
||||||
|
// `.unwrap_err()`:与 `unwrap()` 相反——解开 Err 中的错误值(如果 Ok 则 panic)。
|
||||||
|
// `.to_string()`:把 `anyhow::Error` 转为 `String`(用 Display 格式化)。
|
||||||
let err = result.unwrap_err().to_string();
|
let err = result.unwrap_err().to_string();
|
||||||
assert!(
|
assert!(
|
||||||
err.contains("Unknown backend 'magic'"),
|
err.contains("Unknown backend 'magic'"),
|
||||||
|
|||||||
Reference in New Issue
Block a user