Skip to content
Merged
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 应用时,测试夹具只提供被测行为需要的配置和资源。优先使用 Tauri 的测试上下文;确需通过 `generate_context!` 读取应用配置时,使用测试模式,避免测试二进制生成只属于真实应用运行时的资源。

## 选择和编写测试

1. 先确定需要防止的错误、预期结果和需要区分的失败类型,再选择测试类型。
Expand Down
12 changes: 6 additions & 6 deletions src-tauri/src/environment/wsl/operations/source_acquisition.rs
Original file line number Diff line number Diff line change
Expand Up @@ -348,12 +348,12 @@ mod tests {
use std::collections::BTreeMap;
#[cfg(unix)]
use std::fs;
#[cfg(unix)]
#[cfg(target_os = "linux")]
use std::os::unix::fs::PermissionsExt;
#[cfg(unix)]
use std::process::Command;

#[cfg(unix)]
#[cfg(target_os = "linux")]
fn git(cwd: &std::path::Path, args: &[&str]) -> String {
let output = Command::new("git")
.current_dir(cwd)
Expand Down Expand Up @@ -409,7 +409,7 @@ mod tests {
assert_eq!(workspace.deferred_source_cleanup_count(), 1);
}

#[cfg(unix)]
#[cfg(target_os = "linux")]
#[test]
fn git_acquisition_reports_cloned_head_even_if_source_advances_after_clone() {
let temp = tempfile::tempdir().expect("temp");
Expand Down Expand Up @@ -718,7 +718,7 @@ mod tests {
assert!(destination.join("keep").is_file());
}

#[cfg(unix)]
#[cfg(target_os = "linux")]
#[test]
fn failed_git_acquisition_removes_its_managed_temporary_root() {
let managed_root = std::path::PathBuf::from(format!(
Expand All @@ -744,7 +744,7 @@ mod tests {
assert!(!managed_root.exists());
}

#[cfg(unix)]
#[cfg(target_os = "linux")]
#[test]
fn git_timeout_starts_when_the_clone_process_starts() {
let temp = tempfile::tempdir().expect("fake Git temp dir");
Expand Down Expand Up @@ -788,7 +788,7 @@ mod tests {
assert!(!managed_root.exists());
}

#[cfg(unix)]
#[cfg(target_os = "linux")]
#[test]
fn git_probe_uses_a_stable_diagnostic_locale() {
let temp = tempfile::tempdir().expect("fake Git temp dir");
Expand Down
22 changes: 12 additions & 10 deletions src-tauri/src/runtime/application_updater.rs
Original file line number Diff line number Diff line change
Expand Up @@ -180,21 +180,23 @@ mod tests {
use std::sync::Mutex;
use std::thread;

use tauri::test::{mock_builder, MockRuntime};
use tauri::test::{mock_builder, mock_context, noop_assets, MockRuntime};

use super::*;
use crate::models::{NetworkProxySettings, ProxyMode};

const TEST_UPDATER_PUBKEY: &str = "dW50cnVzdGVkIGNvbW1lbnQ6IG1pbmlzaWduIHB1YmxpYyBrZXk6IEQ1NkM0MjI5RDUzMzU2MkMKUldRc1ZqUFZLVUpzMVRUYmF2dW0rcjJYbm5pWFp6cEplQWNDc09xb1pnb0ozejJmQUtqd2JjNTUK";

fn test_app(manifest_urls: &[url::Url]) -> tauri::App<MockRuntime> {
let mut context = tauri::generate_context!();
let updater_config = context
.config_mut()
.plugins
.0
.get_mut("updater")
.expect("updater plugin config");
updater_config["endpoints"] = serde_json::json!(manifest_urls);
updater_config["dangerousInsecureTransportProtocol"] = serde_json::json!(true);
let mut context = mock_context(noop_assets());
context.config_mut().plugins.0.insert(
"updater".to_string(),
serde_json::json!({
"pubkey": TEST_UPDATER_PUBKEY,
"endpoints": manifest_urls,
"dangerousInsecureTransportProtocol": true
}),
);
mock_builder()
.plugin(tauri_plugin_updater::Builder::new().build())
.build(context)
Expand Down
142 changes: 113 additions & 29 deletions src-tauri/src/runtime/http_transport.rs
Original file line number Diff line number Diff line change
Expand Up @@ -321,8 +321,8 @@ fn request_timeout() -> HttpTransportError {

#[cfg(test)]
mod tests {
use std::io::Write;
use std::net::TcpListener;
use std::io::{BufRead, BufReader, Write};
use std::net::{SocketAddr, TcpListener, TcpStream};
use std::sync::atomic::{AtomicBool, Ordering};
use std::sync::{Arc, Mutex, Once};
use std::thread;
Expand All @@ -341,6 +341,26 @@ mod tests {

struct PreCancelLogger;

struct StalledResponse {
target: String,
address: SocketAddr,
headers_sent: tokio::sync::oneshot::Receiver<()>,
release: std::sync::mpsc::Sender<()>,
worker: thread::JoinHandle<()>,
}

impl StalledResponse {
fn finish(self) {
let _ = self.release.send(());
if let Ok(mut stream) =
TcpStream::connect_timeout(&self.address, Duration::from_secs(1))
{
let _ = stream.write_all(b"GET /cleanup HTTP/1.1\r\nHost: localhost\r\n\r\n");
}
self.worker.join().expect("origin worker");
}
}

impl log::Log for PreCancelLogger {
fn enabled(&self, metadata: &log::Metadata<'_>) -> bool {
metadata.level() <= log::Level::Warn
Expand Down Expand Up @@ -381,6 +401,42 @@ mod tests {
)))
}

fn stalled_response() -> StalledResponse {
let listener = TcpListener::bind("127.0.0.1:0").expect("origin listener");
let address = listener.local_addr().expect("origin addr");
let target = format!("http://{address}/stalled");
let (headers_sent_tx, headers_sent_rx) = tokio::sync::oneshot::channel();
let (release_tx, release_rx) = std::sync::mpsc::channel();
let worker = thread::spawn(move || {
let (mut stream, _) = listener.accept().expect("origin request");
stream
.set_read_timeout(Some(Duration::from_secs(2)))
.expect("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: 1\r\n\r\n")
.expect("response headers");
stream.flush().expect("flush response headers");
let _ = headers_sent_tx.send(());
let _ = release_rx.recv_timeout(Duration::from_secs(2));
});
StalledResponse {
target,
address,
headers_sent: headers_sent_rx,
release: release_tx,
worker,
}
}

#[test]
fn requests_receive_unique_operation_ids_and_allow_workflow_correlation() {
let first = HttpGetRequest::new("https://example.com/first", Duration::from_secs(1), 1024);
Expand Down Expand Up @@ -558,35 +614,63 @@ mod tests {

#[tokio::test]
async fn total_timeout_includes_response_body_reading() {
let listener = TcpListener::bind("127.0.0.1:0").expect("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");
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");
let mut stalled = stalled_response();
let client = direct_client();
let target = stalled.target.clone();
let request = tokio::spawn(async move {
client
.get(HttpGetRequest::new(
target,
Duration::from_millis(500),
1024,
))
.await
});
let started = std::time::Instant::now();

let result = direct_client()
.get(HttpGetRequest::new(target, Duration::from_millis(40), 1024))
.await;
let elapsed = started.elapsed();
let headers_sent =
tokio::time::timeout(Duration::from_secs(2), &mut stalled.headers_sent).await;
let request_result = tokio::time::timeout(Duration::from_secs(2), request).await;
stalled.finish();
headers_sent
.expect("response headers sent before deadline")
.expect("response headers signal");
let result = request_result
.expect("request completed before test deadline")
.expect("request task");
assert!(
matches!(
result,
Err(super::HttpTransportError::Request {
reason: "timeout",
..
})
),
"request should time out while reading the response body"
);
}

worker.join().expect("origin worker");
assert!(elapsed < Duration::from_millis(200));
assert!(matches!(
result,
Err(super::HttpTransportError::Request {
reason: "timeout",
..
})
));
#[tokio::test]
async fn status_only_returns_after_headers_without_reading_the_body() {
let mut stalled = stalled_response();

let headers_sent = tokio::time::timeout(Duration::from_secs(2), &mut stalled.headers_sent);
let result = tokio::time::timeout(
Duration::from_secs(2),
direct_client().get(HttpGetRequest::status_only(
stalled.target.clone(),
Duration::from_secs(10),
)),
)
.await;
let headers_sent = headers_sent.await;
stalled.finish();
headers_sent
.expect("response headers sent before deadline")
.expect("response headers signal");
let response = result
.expect("status-only request returned after response headers")
.expect("status-only response");
assert_eq!(response.status, reqwest::StatusCode::OK);
assert!(response.body.is_empty());
}

#[tokio::test]
Expand Down
25 changes: 0 additions & 25 deletions src-tauri/src/runtime/network_connection.rs
Original file line number Diff line number Diff line change
Expand Up @@ -306,31 +306,6 @@ mod tests {
assert_eq!(result.reason_code.as_deref(), Some("request_failed"));
}

#[tokio::test]
async fn http_probe_succeeds_without_buffering_a_large_response_body() {
let proxy = tiny_http::Server::http("127.0.0.1:0").expect("proxy server");
let proxy_url = format!("http://{}", proxy.server_addr());
let server_thread = thread::spawn(move || {
proxy
.recv_timeout(Duration::from_secs(2))
.expect("proxy receive")
.expect("proxy request before deadline")
.respond(tiny_http::Response::from_data(vec![b'x'; 2 * 1024 * 1024]))
.expect("proxy response");
});
let settings = Arc::new(ProxySettingsStore::new(NetworkProxySettings {
mode: ProxyMode::Custom,
custom_proxy_url: Some(proxy_url),
..NetworkProxySettings::default()
}));

let result = test_http_target(&HttpTransport::new(settings), "http://skills.sh/").await;

server_thread.join().expect("proxy server thread");
assert_eq!(result.status, ProxyConnectionStatus::Succeeded);
assert_eq!(result.reason_code, None);
}

#[tokio::test]
async fn wsl_adapter_maps_an_unavailable_distribution_without_running_wsl() {
let wsl = WslRuntime::new_with_support(false, false);
Expand Down
Loading