Skip to content
Open
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
23 changes: 23 additions & 0 deletions .github/workflows/ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -77,6 +77,25 @@ jobs:
version: 1.0
- run: cargo test --workspace

test-blitz-net-features:
name: "Test blitz-net [cookies,cache,multipart]"
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- uses: dtolnay/rust-toolchain@stable
- uses: Swatinem/rust-cache@v2
- run: cargo test -p blitz-net-tests --features cookies,cache
- run: cargo test -p blitz-net-tests --features cookies,multipart

test-blitz-net-windows:
name: "Test blitz-net (Windows)"
runs-on: windows-latest
steps:
- uses: actions/checkout@v4
- uses: dtolnay/rust-toolchain@stable
- uses: Swatinem/rust-cache@v2
- run: cargo test -p blitz-net-tests

build-counter:
name: "Build counter example"
runs-on: ubuntu-latest
Expand Down Expand Up @@ -135,6 +154,10 @@ jobs:
packages: libgtk-3-dev libxdo-dev
version: 1.0
- run: cargo clippy --workspace -- -D warnings
# Lint blitz-net-tests' feature-gated modules — cache and multipart are
# mutually incompatible in blitz-net, so they need separate passes.
- run: cargo clippy -p blitz-net-tests --tests --features cookies,cache -- -D warnings
- run: cargo clippy -p blitz-net-tests --tests --features cookies,multipart -- -D warnings

doc:
name: Documentation
Expand Down
67 changes: 67 additions & 0 deletions Cargo.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

1 change: 1 addition & 0 deletions Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,7 @@ members = [
"apps/readme",
"apps/bump",
"wpt/runner",
"tests/blitz-net",
"examples/counter",
"examples/seven_guis",
"examples/todomvc",
Expand Down
38 changes: 32 additions & 6 deletions packages/blitz-net/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -70,15 +70,21 @@ pub struct Provider {
}
impl Provider {
pub fn new(waker: Option<Arc<dyn NetWaker>>) -> Self {
#[cfg(feature = "cache")]
return Self::with_cache_dir(waker, get_cache_path());

#[cfg(not(feature = "cache"))]
Self::build(waker)
}

#[cfg(feature = "cache")]
pub fn with_cache_dir(waker: Option<Arc<dyn NetWaker>>, cache_dir: std::path::PathBuf) -> Self {
let builder = reqwest::Client::builder();
#[cfg(feature = "cookies")]
let builder = builder.cookie_store(true);
let client = builder.build().unwrap();

#[cfg(feature = "cache")]
let cache_manager = CACacheManager::new(get_cache_path(), true);

#[cfg(feature = "cache")]
let cache_manager = CACacheManager::new(cache_dir, true);
let client = reqwest_middleware::ClientBuilder::new(client)
.with(Cache(HttpCache {
mode: CacheMode::Default,
Expand All @@ -92,10 +98,24 @@ impl Provider {
client,
waker,
per_host_limits: Arc::new(Mutex::new(HashMap::new())),
#[cfg(feature = "cache")]
cache_manager,
}
}

#[cfg(not(feature = "cache"))]
fn build(waker: Option<Arc<dyn NetWaker>>) -> Self {
let builder = reqwest::Client::builder();
#[cfg(feature = "cookies")]
let builder = builder.cookie_store(true);
let client = builder.build().unwrap();

let waker = waker.unwrap_or(Arc::new(DummyNetWaker));
Self {
client,
waker,
per_host_limits: Arc::new(Mutex::new(HashMap::new())),
}
}
pub fn shared(waker: Option<Arc<dyn NetWaker>>) -> Arc<dyn NetProvider> {
Arc::new(Self::new(waker))
}
Expand Down Expand Up @@ -129,7 +149,13 @@ impl Provider {
Ok((request.url.to_string(), Bytes::from(decoded.0)))
}
"file" => {
let file_content = std::fs::read(request.url.path())?;
let path = request.url.to_file_path().map_err(|_| {
std::io::Error::new(
std::io::ErrorKind::InvalidInput,
format!("invalid file URL: {}", request.url),
)
})?;
let file_content = std::fs::read(path)?;
Ok((request.url.to_string(), Bytes::from(file_content)))
}
_ => Self::fetch_http(client, request, per_host_limits).await,
Expand Down
39 changes: 39 additions & 0 deletions tests/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,39 @@
# Integration tests

This directory hosts workspace-member test crates that exercise individual Blitz packages from the outside.

## Layout

- `blitz-net/` — public-API regression tests for `packages/blitz-net`. Uses `wiremock` to stand up in-process HTTP servers; asserts on `Provider` behavior, scheme dispatch, body encoding, abort signals, per-host concurrency limits, and feature-gated paths (`cookies`, `cache`, `multipart`).
- `stylo_usage.rs` — historical example file; not wired into a crate.

`cargo test --workspace` runs the default-feature tests. Feature-gated tests need explicit flags (see the crate's `Cargo.toml`).

## Why these tests exist alongside WPT

Blitz already has a large test surface in `wpt/runner/` — the [web-platform-tests](https://github.com/web-platform-tests/wpt) conformance suite. The crates here cover what WPT can't, for two reasons:

**WPT doesn't use `blitz-net::Provider`.** The WPT runner ships its own `NetProvider` impl (`wpt/runner/src/net_provider.rs`) that resolves URLs against a local checkout of the WPT git repo. No HTTP, no `reqwest`, no semaphore, no cookies. Every behavior worth regressing on in `blitz-net` — per-host limiting, User-Agent injection, status-code mapping, cache middleware, multipart bodies, abort signals — is bypassed by the runner. Wiring `blitz-net::Provider` into WPT would require running a `wptserve` instance and adapting the runner's loader, which is a much larger project than testing the crate directly.

**WPT's assertion model is shaped for renderer conformance, not API regression.** WPT compares rendered bitmaps against reference images via `dify` pixel-diffs. The blitz-net tests need structural assertions on Rust values — `matches!(err, ProviderError::HttpStatus { status, .. })`, `provider.count() == 1`, `received_requests().len() == 6` — that have no natural expression in a visual-diff framework.

The two suites are complementary: WPT answers "does Blitz render this HTML/CSS correctly per spec?"; the crates here answer "does this package's public API still behave as documented?"

## Running the tests

```sh
# Default-feature tests (also covered by `cargo test --workspace`)
cargo test -p blitz-net-tests

# Feature-gated tests — `cookies,cache` and `cookies,multipart` are run separately
# because the `cache` and `multipart` features are currently incompatible in blitz-net.
cargo test -p blitz-net-tests --features cookies,cache
cargo test -p blitz-net-tests --features cookies,multipart

# Single test
cargo test -p blitz-net-tests injects_user_agent_header

# Single test file
cargo test -p blitz-net-tests --test concurrency
```

22 changes: 22 additions & 0 deletions tests/blitz-net/Cargo.toml
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
[package]
name = "blitz-net-tests"
version = "0.0.0"
edition = "2024"
publish = false

[features]
default = []
cookies = ["blitz-net/cookies"]
cache = ["blitz-net/cache"]
multipart = ["blitz-net/multipart"]

[dev-dependencies]
blitz-net = { path = "../../packages/blitz-net" }
blitz-traits = { workspace = true }
tokio = { workspace = true, features = ["macros", "rt-multi-thread", "sync", "time"] }
wiremock = "0.6"
tempfile = "3"
futures-util = { workspace = true }
url = { workspace = true }
http = { workspace = true }
bytes = { workspace = true }
101 changes: 101 additions & 0 deletions tests/blitz-net/tests/common/mod.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,101 @@
// Each helper is used by at least one test binary but not by all of them, so per-item
// `#[allow(dead_code)]` is required — without it, every binary's compile would warn on
// the helpers it doesn't reference. Prefer the per-item form over a blanket file-level
// allow so genuinely unreferenced helpers still surface in review.
use blitz_traits::net::{Bytes, NetHandler, NetWaker};
use std::io::Write as _;
use std::sync::{Arc, Mutex};
use std::time::{Duration, Instant};
use wiremock::matchers::method;
use wiremock::{Mock, MockServer, ResponseTemplate};

// 300ms is wide enough to keep mid-flight observation windows deterministic on slow CI.
#[allow(dead_code)]
pub const RESPONSE_DELAY: Duration = Duration::from_millis(300);

#[allow(dead_code)]
pub struct CaptureHandler(pub tokio::sync::oneshot::Sender<(String, Bytes)>);

impl NetHandler for CaptureHandler {
fn bytes(self: Box<Self>, url: String, b: Bytes) {
let _ = self.0.send((url, b));
}
}

#[allow(dead_code)]
#[derive(Default, Clone)]
pub struct CaptureWaker(pub Arc<Mutex<Vec<usize>>>);

impl NetWaker for CaptureWaker {
fn wake(&self, id: usize) {
self.0.lock().unwrap().push(id);
}
}

#[allow(dead_code)]
pub fn make_url(s: &str) -> url::Url {
url::Url::parse(s).expect("valid url")
}

#[allow(dead_code)]
pub fn write_tempfile(contents: &[u8]) -> tempfile::NamedTempFile {
let mut tmp = tempfile::NamedTempFile::new().unwrap();
tmp.write_all(contents).unwrap();
tmp
}

#[allow(dead_code)]
pub async fn mount_get_ok(server: &MockServer) {
Mock::given(method("GET"))
.respond_with(ResponseTemplate::new(200))
.mount(server)
.await;
}

#[allow(dead_code)]
pub async fn mount_get_body(server: &MockServer, body: &'static str) {
Mock::given(method("GET"))
.respond_with(ResponseTemplate::new(200).set_body_string(body))
.mount(server)
.await;
}

#[allow(dead_code)]
pub async fn mount_get_status(server: &MockServer, status: u16) {
Mock::given(method("GET"))
.respond_with(ResponseTemplate::new(status))
.mount(server)
.await;
}

#[allow(dead_code)]
pub async fn mount_post_ok(server: &MockServer) {
Mock::given(method("POST"))
.respond_with(ResponseTemplate::new(200))
.mount(server)
.await;
}

#[allow(dead_code)]
pub async fn wait_until<F: FnMut() -> bool>(timeout: Duration, mut condition: F) -> bool {
let deadline = Instant::now() + timeout;
while Instant::now() < deadline {
if condition() {
return true;
}
tokio::time::sleep(Duration::from_millis(10)).await;
}
condition()
}

#[allow(dead_code)]
pub async fn wait_for_received(server: &MockServer, target: usize, timeout: Duration) -> usize {
let deadline = Instant::now() + timeout;
loop {
let n = server.received_requests().await.unwrap().len();
if n >= target || Instant::now() >= deadline {
return n;
}
tokio::time::sleep(Duration::from_millis(10)).await;
}
}
Loading
Loading