diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index ac8a1b9..f9b59a4 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -25,6 +25,9 @@ jobs: toolchain: ${{ env.RUST_VERSION }} components: clippy,rustfmt + - name: Cache Rust build + uses: Swatinem/rust-cache@v2 + - name: Rust format run: cargo fmt --check @@ -51,6 +54,9 @@ jobs: with: toolchain: ${{ env.RUST_VERSION }} + - name: Cache Rust build + uses: Swatinem/rust-cache@v2 + - name: Install protoc (Linux) if: runner.os == 'Linux' run: sudo apt-get update && sudo apt-get install -y protobuf-compiler @@ -64,4 +70,4 @@ jobs: run: choco install protoc -y - name: Rust integration tests against Rust binary - run: cargo test --locked --all-features --test cli --test formatting --test grpc --test http --test install --test network --test terminal --test update --test websocket -- --test-threads=1 + run: cargo test --locked --all-features --test cli --test formatting --test grpc --test har --test http --test install --test network --test terminal --test update --test websocket -- --test-threads=2 diff --git a/AGENTS.md b/AGENTS.md index b00821c..8a30113 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -30,14 +30,15 @@ Full CI-equivalent before PRs, shared transport/request/response changes, or unc cargo fmt --check cargo clippy --locked --all-targets --all-features -- -D warnings cargo test --locked --all-features --lib --bins -cargo test --locked --all-features --test cli --test formatting --test grpc --test http --test install --test network --test terminal --test update --test websocket +cargo test --locked --all-features --test cli --test formatting --test grpc --test har --test http --test install --test network --test terminal --test update --test websocket -- --test-threads=2 ``` The integration test suite uses `TcpListener::bind("127.0.0.1:0")` and -`UdpSocket::bind("127.0.0.1:0")` for all test servers, so tests are safe to run in -parallel. If transient "Connection refused" errors occur from port reuse races, add -`-- --test-threads=1` to force sequential execution. The `TestServer` and -`H3TestServer` use `mpsc` channels instead of polling for request notification. +`UdpSocket::bind("127.0.0.1:0")` for all test servers. CI uses two test threads to +speed up the suite without overloading timing-sensitive socket tests. If transient +"Connection refused" or connection-reset errors occur from port reuse races, use +`-- --test-threads=1` to diagnose sequentially. The `TestServer` and `H3TestServer` +use `mpsc` channels instead of polling for request notification. Docs-only changes: skip Cargo unless examples/generated CLI output changed; format changed docs only: diff --git a/Cargo.lock b/Cargo.lock index 5e8f952..89c2895 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -785,6 +785,7 @@ dependencies = [ "tokio-util", "tower-service", "url", + "wait-timeout", "webpki-roots 1.0.9", "windows-sys 0.61.2", "zip", diff --git a/Cargo.toml b/Cargo.toml index b635a41..1dde9b8 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -86,3 +86,4 @@ predicates = "=3.1.4" rcgen = { version = "=0.14.8", features = ["aws_lc_rs"] } sha1 = "=0.11.0" tempfile = "=3.27.0" +wait-timeout = "=0.2.1" diff --git a/tests/support/common.rs b/tests/support/common.rs index 935e495..9db1a23 100644 --- a/tests/support/common.rs +++ b/tests/support/common.rs @@ -10,6 +10,7 @@ use std::time::{Duration, Instant}; use tempfile::TempDir; use url::Url; +use wait_timeout::ChildExt; #[cfg(unix)] use std::os::fd::{FromRawFd, RawFd}; @@ -113,30 +114,41 @@ pub(crate) fn run_fetch_once(opts: FetchOpts, args: &[&str]) -> FetchOutput { let mut stdin = child.stdin.take().expect("child stdin"); stdin.write_all(input.as_bytes()).expect("write stdin"); } - let start = Instant::now(); - loop { - if child.try_wait().expect("poll fetch").is_some() { - break; - } - if start.elapsed() > Duration::from_secs(15) { - let _ = child.kill(); - let out = child.wait_with_output().expect("wait killed fetch"); - return FetchOutput { - status: out.status, - stdout: String::from_utf8_lossy(&out.stdout).into_owned(), - stderr: format!( - "{}\nfetch test harness timeout after 30s", - String::from_utf8_lossy(&out.stderr) - ), - }; - } - thread::sleep(Duration::from_millis(10)); + + // Drain both pipes while the child runs so a verbose command cannot block on + // a full pipe. wait_timeout avoids adding up to 10ms of polling latency to + // every short-lived CLI invocation in the integration suite. + let mut stdout = child.stdout.take().expect("child stdout"); + let stdout_reader = thread::spawn(move || { + let mut output = Vec::new(); + stdout.read_to_end(&mut output).expect("read fetch stdout"); + output + }); + let mut stderr = child.stderr.take().expect("child stderr"); + let stderr_reader = thread::spawn(move || { + let mut output = Vec::new(); + stderr.read_to_end(&mut output).expect("read fetch stderr"); + output + }); + + let timed_out = child + .wait_timeout(Duration::from_secs(15)) + .expect("wait for fetch") + .is_none(); + if timed_out { + let _ = child.kill(); + } + let status = child.wait().expect("wait fetch"); + let stdout = stdout_reader.join().expect("join fetch stdout reader"); + let stderr = stderr_reader.join().expect("join fetch stderr reader"); + let mut stderr = String::from_utf8_lossy(&stderr).into_owned(); + if timed_out { + stderr.push_str("\nfetch test harness timeout after 15s"); } - let out = child.wait_with_output().expect("wait fetch"); FetchOutput { - status: out.status, - stdout: String::from_utf8_lossy(&out.stdout).into_owned(), - stderr: String::from_utf8_lossy(&out.stderr).into_owned(), + status, + stdout: String::from_utf8_lossy(&stdout).into_owned(), + stderr, } } @@ -286,17 +298,10 @@ pub(crate) fn wait_child( child: &mut std::process::Child, timeout: Duration, ) -> Option> { - let start = Instant::now(); - loop { - match child.try_wait() { - Ok(Some(status)) => return Some(Ok(status)), - Ok(None) => {} - Err(err) => return Some(Err(err)), - } - if start.elapsed() > timeout { - return None; - } - thread::sleep(Duration::from_millis(10)); + match child.wait_timeout(timeout) { + Ok(Some(status)) => Some(Ok(status)), + Ok(None) => None, + Err(err) => Some(Err(err)), } }