Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 2 additions & 0 deletions docs/testing.md
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,8 @@ Skill Deck 需要在 Windows、macOS 和 Linux 上处理文件、启动进程并

测试名称应与实际运行方式一致。Tauri 模拟运行时(`MockRuntime`)、前端模拟实现(mock)和本地测试服务器不会启动原生 WebView,使用这些能力的测试不属于桌面应用验收。具体限制见 [Tauri 测试说明](https://v2.tauri.app/develop/tests/)。

Rust 单元测试使用 `MockRuntime` 构建 Tauri 应用时,`generate_context!` 必须启用 `test = true`。测试上下文继续读取应用配置,但不生成 Info.plist 等只属于真实应用运行时的内容,避免 macOS 测试二进制与应用入口重复嵌入同名符号。

## 选择和编写测试

1. 先确定需要防止的错误、预期结果和需要区分的失败类型,再选择测试类型。
Expand Down
39 changes: 39 additions & 0 deletions scripts/__tests__/rust-test-policy.test.mjs
Original file line number Diff line number Diff line change
@@ -0,0 +1,39 @@
import assert from "node:assert/strict";
import { readdir, readFile } from "node:fs/promises";
import path from "node:path";
import { fileURLToPath } from "node:url";
import test from "node:test";

const rustSourceRoot = fileURLToPath(new URL("../../src-tauri/src/", import.meta.url));
const applicationEntryUrl = new URL("../../src-tauri/src/lib.rs", import.meta.url);

async function rustSourceFiles(root) {
const entries = await readdir(root, { withFileTypes: true });
const files = await Promise.all(entries.map(async (entry) => {
const entryPath = path.join(root, entry.name);
if (entry.isDirectory()) return rustSourceFiles(entryPath);
return entry.isFile() && entry.name.endsWith(".rs") ? [entryPath] : [];
}));
return files.flat();
}

test("Tauri contexts distinguish test fixtures from the application runtime", async () => {
const sourceFiles = await rustSourceFiles(rustSourceRoot);
const ordinaryContexts = [];
let contextCalls = 0;
let testContexts = 0;
for (const sourceFile of sourceFiles) {
const source = await readFile(sourceFile, "utf8");
contextCalls += source.match(/tauri::generate_context!\(/g)?.length ?? 0;
if (source.includes("tauri::generate_context!()")) {
ordinaryContexts.push(path.relative(rustSourceRoot, sourceFile));
}
testContexts += source.match(/tauri::generate_context!\(test\s*=\s*true\)/g)?.length ?? 0;
}

assert.deepEqual(ordinaryContexts, ["lib.rs"]);
assert.ok(testContexts > 0);
assert.equal(contextCalls, ordinaryContexts.length + testContexts);
const applicationEntry = await readFile(applicationEntryUrl, "utf8");
assert.match(applicationEntry, /\.run\(tauri::generate_context!\(\)\)/);
});
3 changes: 2 additions & 1 deletion src-tauri/src/runtime/application_updater.rs
Original file line number Diff line number Diff line change
Expand Up @@ -186,7 +186,8 @@ mod tests {
use crate::models::{NetworkProxySettings, ProxyMode};

fn test_app(manifest_urls: &[url::Url]) -> tauri::App<MockRuntime> {
let mut context = tauri::generate_context!();
// 测试上下文不嵌入 macOS Info.plist,避免与应用入口重复定义符号。
let mut context = tauri::generate_context!(test = true);
let updater_config = context
.config_mut()
.plugins
Expand Down
80 changes: 65 additions & 15 deletions src-tauri/src/runtime/http_transport.rs
Original file line number Diff line number Diff line change
Expand Up @@ -247,9 +247,13 @@ impl HttpTransport {
response
.chunk()
.await
.map_err(|_| HttpTransportError::Request {
.map_err(|error| HttpTransportError::Request {
stage: "response_body",
reason: "transport",
reason: if error.is_timeout() {
"timeout"
} else {
"transport"
},
})?
{
if body.len().saturating_add(chunk.len()) > max_body_bytes {
Expand Down Expand Up @@ -321,7 +325,7 @@ fn request_timeout() -> HttpTransportError {

#[cfg(test)]
mod tests {
use std::io::Write;
use std::io::{BufRead, BufReader, Write};
use std::net::TcpListener;
use std::sync::atomic::{AtomicBool, Ordering};
use std::sync::{Arc, Mutex, Once};
Expand Down Expand Up @@ -559,34 +563,80 @@ mod tests {
#[tokio::test]
async fn total_timeout_includes_response_body_reading() {
let listener = TcpListener::bind("127.0.0.1:0").expect("origin listener");
listener
.set_nonblocking(true)
.expect("configure origin listener");
let target = format!(
"http://{}/slow",
listener.local_addr().expect("origin addr")
);
let worker = thread::spawn(move || {
let (mut stream, _) = listener.accept().expect("origin request");
let accept_deadline = std::time::Instant::now() + Duration::from_secs(2);
let (mut stream, _) = loop {
match listener.accept() {
Ok(connection) => break connection,
Err(error) if error.kind() == std::io::ErrorKind::WouldBlock => {
assert!(
std::time::Instant::now() < accept_deadline,
"origin request was not received before the deadline"
);
thread::sleep(Duration::from_millis(5));
}
Err(error) => panic!("accept origin request: {error}"),
}
};
stream
.set_read_timeout(Some(Duration::from_secs(2)))
.expect("configure origin request timeout");
let mut request = BufReader::new(stream.try_clone().expect("clone origin stream"));
loop {
let mut line = String::new();
let bytes_read = request.read_line(&mut line).expect("read origin request");
assert_ne!(bytes_read, 0, "origin request ended before headers");
if line == "\r\n" {
break;
}
}
stream
.write_all(b"HTTP/1.1 200 OK\r\nContent-Length: 4\r\n\r\no")
.expect("partial response");
thread::sleep(Duration::from_millis(250));
let _ = stream.write_all(b"kay");
stream.flush().expect("flush partial response");
thread::sleep(Duration::from_millis(500));
if stream.write_all(b"kay").is_ok() {
let _ = stream.flush();
let _ = stream.shutdown(std::net::Shutdown::Write);
}
});
let started = std::time::Instant::now();

let result = direct_client()
.get(HttpGetRequest::new(target, Duration::from_millis(40), 1024))
.get(HttpGetRequest::new(
target,
Duration::from_millis(200),
1024,
))
.await;
let elapsed = started.elapsed();

worker.join().expect("origin worker");
assert!(elapsed < Duration::from_millis(200));
assert!(matches!(
result,
Err(super::HttpTransportError::Request {
reason: "timeout",
..
})
));
assert!(elapsed < Duration::from_millis(400));
let error = match result {
Ok(response) => panic!(
"request unexpectedly succeeded with status {}",
response.status
),
Err(error) => error,
};
assert!(
matches!(
error,
super::HttpTransportError::Request {
reason: "timeout",
..
}
),
"unexpected timeout error: {error:?}"
);
}

#[tokio::test]
Expand Down
Loading