bench-scenarios/src/served.rs
//! Served-over-HTTP driver — the **honest** comparison foundation.
//!
//! ## Why this module exists (årsstädning 2026-06-28 fairness fix, part 2)
//!
//! The Nexus / Artifactory comparison scenarios used to measure Holger
//! **in-process** (`repo.get_file()` — no syscall, no HTTP framing, no TLS) while
//! the JVM competitors were measured **over HTTP**. That is not a comparison. The
//! rule from the audit: *when you measure Holger's package delivery you MUST go
//! via the package protocol* — Holger's real **native HTTP door**
//! ([`RepositoryBackendTrait::handle_http2_request`], the very path stock
//! cargo/pip/npm hit, served by [`start_http_gateway`]).
//!
//! This module stands up a **real Holger HTTP server** (the same
//! [`start_http_gateway`] the production `holger serve` / `dev-pair` path uses),
//! then drives a **pure-Rust, gatling-style parallel GET pool**
//! ([`bombard_http_get`]) against it — the exact same over-the-wire workload the
//! competitors get. The result (`holger_served_mbs`) is the *like-for-like*
//! head-to-head number; the old in-process figure is kept only as an internal
//! "Holger-vs-Holger regression ceiling" ([`inprocess_read_ceiling`]), never
//! placed beside the competitor.
//!
//! ## Reuse note — this is the `holger-ds` foundation
//!
//! The publish + fetch-over-protocol pieces here are deliberately **standalone,
//! callable functions** (not buried in one scenario): the upcoming `holger-ds`
//! tool drives Holger's served protocol the same way. Keep
//! [`start_holger_gateway`] / [`bombard_http_get`] / [`crate_download_urls`]
//! generic and reusable.
use std::net::SocketAddr;
use std::path::Path;
use std::sync::atomic::{AtomicU64, Ordering};
use std::sync::Arc;
use std::time::{Duration, Instant};
use anyhow::{Context, Result};
use repository_znippy_python::PipRepoZnippy;
use repository_znippy_rust::RustRepoZnippy;
use traits::RepositoryBackendTrait;
#[cfg(any(feature = "nexus", feature = "artifactory"))]
use testcontainers::{ContainerAsync, GenericImage};
#[cfg(any(feature = "nexus", feature = "artifactory"))]
use tokio::io::{AsyncBufReadExt, AsyncWriteExt, BufReader};
/// **Perfect instrumentation** for the JVM competitor containers (Nexus,
/// Artifactory): stream the container's combined stdout+stderr to a host log file
/// in real time, so startup/crash detail (e.g. Artifactory's master.key / join.key
/// / Access bootstrap) is fully visible instead of an opaque "skipped". Best-effort
/// — capture failures never affect the bench. Writes
/// `$HOLGER_BENCH_DIR/<label>-container.log` (falls back to `/tmp`). Call right
/// after `image.start()`.
#[cfg(any(feature = "nexus", feature = "artifactory"))]
pub fn capture_container_logs(container: &ContainerAsync<GenericImage>, label: &str) {
let mut out = BufReader::new(container.stdout(true)).lines();
let mut err = BufReader::new(container.stderr(true)).lines();
let dir = std::env::var("HOLGER_BENCH_DIR").unwrap_or_else(|_| "/tmp".into());
let path = format!("{dir}/{label}-container.log");
tokio::spawn(async move {
let Ok(mut f) = tokio::fs::File::create(&path).await else {
return;
};
loop {
tokio::select! {
Ok(Some(l)) = out.next_line() => { let _ = f.write_all(format!("{l}\n").as_bytes()).await; }
Ok(Some(l)) = err.next_line() => { let _ = f.write_all(format!("ERR {l}\n").as_bytes()).await; }
else => break,
}
let _ = f.flush().await;
}
});
}
use server_lib::audit::NoopAuditLog;
use server_lib::auth::AuthConfig;
use server_lib::exposed::fast_routes::FastRoutes;
use server_lib::exposed::http::{start_http_gateway, GatewaySettings};
/// Aggregate outcome of one over-the-wire GET bombardment.
#[derive(Debug, Clone, Default)]
pub struct HttpFetchOutcome {
pub ops: u64,
pub bytes: u64,
pub mbs: f64,
pub ops_per_sec: f64,
/// Of `ops`, how many returned a 2xx. A run where `ok_ops == 0` (e.g. every
/// request 404'd because the target lacks the synthetic corpus) is a failed
/// benchmark, not a slow one — callers should reject it instead of printing
/// a misleading ~0 MB/s.
pub ok_ops: u64,
/// Per-request latency percentiles (milliseconds), computed over EVERY GET's
/// wall time in the same bombardment. Measured identically for Holger, Nexus
/// and Artifactory (same pure-Rust client pool, same loopback transport), so
/// they are the honest like-for-like latency head-to-head.
pub p50_ms: f64,
pub p99_ms: f64,
pub p999_ms: f64,
pub mean_ms: f64,
}
/// Grab an ephemeral loopback port by binding+dropping a std listener, so we can
/// hand a concrete `addr` to [`start_http_gateway`] (which binds internally and
/// does not report the chosen port back). Loopback-only bench, so the small
/// bind→drop→rebind race is benign.
fn free_loopback_addr() -> Result<SocketAddr> {
let listener =
std::net::TcpListener::bind("127.0.0.1:0").context("bind ephemeral loopback port")?;
let addr = listener.local_addr().context("local_addr")?;
drop(listener);
Ok(addr)
}
/// Stand up a **real Holger native-HTTP server** for `repos` and return the bound
/// loopback address (e.g. `127.0.0.1:54321`). This is the same
/// [`start_http_gateway`] the production serve / `dev-pair` path uses — cleartext
/// loopback (the competitors also run cleartext HTTP on loopback, so the
/// transport is symmetric), open auth, no-op audit, 1 GiB body cap.
///
/// Must be called from within a Tokio runtime (the gateway `tokio::spawn`s its
/// accept loop). Polls `GET /healthz` until the listener answers (or a short
/// timeout elapses) so callers can fetch immediately on return.
///
/// `repos` is `(route_name, backend)`: the gateway routes `/{route_name}/…` to
/// that backend's `handle_http2_request`, exactly as cargo/pip/npm reach it.
///
/// IMPORTANT: drive this from a **multi-thread** Tokio runtime (e.g.
/// `Builder::new_multi_thread`). The companion [`bombard_http_get`] pool blocks
/// OS threads; on a single-thread runtime that blocking would starve this
/// gateway's accept loop and no request would ever be served.
pub async fn start_holger_gateway(
repos: Vec<(String, Arc<dyn RepositoryBackendTrait>)>,
) -> Result<SocketAddr> {
let addr = free_loopback_addr()?;
let settings = Arc::new(GatewaySettings {
gate: Default::default(),
routes: FastRoutes::new(repos),
auth_config: Arc::new(AuthConfig::default()),
// No repository here declares `ron_require_auth`, so the scope is empty
// and the read gate is exactly the global one.
read_gate: Arc::new(server_lib::auth::ReadGate::GLOBAL_ONLY),
tls: None,
max_body_bytes: 1024 * 1024 * 1024,
audit: Arc::new(NoopAuditLog),
operation_mode: Default::default(),
twin: Default::default(),
grow: Default::default(),
security_profile: Default::default(),
// The bench measures the door itself: every request admitted Full.
admission: server_lib::admission::DoorAdmission::pass_through(),
// The bench has no intake chain: nothing to probe.
scan_probe: None,
ready: None,
sboms: None,
attest: None,
evidence_key: None,
repo_key: None,
// The bench serves artifacts, not policy: no reserved keys, no promotion
// requirement. Both default to "nothing is reserved", which leaves the
// property door exactly as permissive as the bench needs.
reserved_properties: Default::default(),
promotion_required_property: None,
properties: None,
intake: None,
imports: None,
admit_upload: None,
// The bench serves a door and measures it; nothing here counts visitors
// or knows a peer.
trusted_proxies: None,
door: "bench".to_string(),
visitors: Arc::new(server_lib::visitors::Visitors::empty()),
console: Default::default(),
peers: Default::default(),
board: None,
flow: None,
// The bench serves artifacts, never a login: no passkey store, so the
// `/-/auth/passkey/...` surface answers 404 the way it does on any
// server with no `Passkey` method configured. The field is pinned by
// this crate's explicit `auth-passkey` on the server-lib dep.
passkeys: None,
});
start_http_gateway(addr, settings).context("start Holger HTTP gateway")?;
// Wait for readiness (the accept loop binds inside the spawned task).
let client = tls_roots(reqwest::Client::builder())
.timeout(Duration::from_secs(2))
.build()
.context("build readiness client")?;
let health = format!("http://{addr}/healthz");
let deadline = Instant::now() + Duration::from_secs(10);
while Instant::now() < deadline {
if let Ok(resp) = client.get(&health).send().await {
if resp.status().is_success() {
return Ok(addr);
}
}
tokio::time::sleep(Duration::from_millis(50)).await;
}
anyhow::bail!("Holger HTTP gateway at {addr} did not become ready in time");
}
/// Build the cargo crate-download URLs (`/{repo}/crates/{name}/{ver}/download`,
/// the real sparse-protocol fetch path) for every `{name}-{ver}.crate` file in
/// `files`. Files that don't parse as `name-version.crate` are skipped.
pub fn crate_download_urls(base: &str, repo: &str, files: &[String]) -> Vec<String> {
files
.iter()
.filter_map(|f| {
let stem = f.rsplit('/').next().unwrap_or(f).strip_suffix(".crate")?;
let (name, version) = stem.rsplit_once('-')?;
if name.is_empty() || version.is_empty() {
return None;
}
Some(format!("{base}/{repo}/crates/{name}/{version}/download"))
})
.collect()
}
/// Build the wheel/sdist download URLs (`/{repo}/packages/{name}/{filename}`, the
/// real PEP 503 fetch path pip/uv hit after resolving the index) for every
/// `packages/{name}/{filename}` entry in `files`. Non-`packages/` entries are
/// skipped. Mirrors [`crate_download_urls`] for the Python journey.
pub fn pip_download_urls(base: &str, repo: &str, files: &[String]) -> Vec<String> {
files
.iter()
.filter(|f| f.starts_with("packages/") && !f.trim_end_matches('/').ends_with('/'))
.map(|f| format!("{base}/{repo}/{f}"))
.collect()
}
/// Build the PEP 503 simple **detail-page** URLs (`/{repo}/simple/{name}/`, one per
/// unique package) from `packages/{name}/…` entries — the index-resolve step
/// pip/uv do before downloading. Deduplicated, insertion order irrelevant.
pub fn pip_index_urls(base: &str, repo: &str, files: &[String]) -> Vec<String> {
let mut seen = std::collections::BTreeSet::new();
for f in files {
if let Some(rest) = f.strip_prefix("packages/") {
if let Some((name, _)) = rest.split_once('/') {
if !name.is_empty() {
seen.insert(name.to_string());
}
}
}
}
seen.into_iter()
.map(|name| format!("{base}/{repo}/simple/{name}/"))
.collect()
}
/// **Pure-Rust, gatling-style** parallel GET pool (no curl/wrk shelling): spawn
/// `concurrency` worker threads, each holding a keep-alive `reqwest::blocking`
/// client and looping random GETs over `urls` until `dur` elapses, summing bytes
/// actually read off the wire. This is the symmetric over-the-wire workload used
/// against **all three** systems (Holger, Nexus, Artifactory).
///
/// Unauthenticated variant — for targets (Holger open-auth loopback, Nexus
/// anonymous-read raw repos) that serve GETs without credentials. When a target
/// *requires* auth on read (Artifactory OSS returns 401 to anonymous GETs), use
/// [`bombard_http_get_auth`] so the head-to-head transport actually reaches the
/// bytes instead of measuring a wall of 401s as ~0 MB/s.
pub fn bombard_http_get(urls: &[String], concurrency: usize, dur: Duration) -> HttpFetchOutcome {
bombard_http_get_auth(urls, None, concurrency, dur)
}
/// As [`bombard_http_get`], but each GET carries HTTP Basic credentials when
/// `auth` is `Some((user, pass))`. Artifactory OSS rejects anonymous reads (401),
/// so its leg of the head-to-head must authenticate; Holger (open auth on the
/// loopback bench) and Nexus (anonymous raw read) pass `None`. Basic auth is a
/// constant per-request header — the like-for-like *transport* (concurrent HTTP
/// GET over loopback) is unchanged.
pub fn bombard_http_get_auth(
urls: &[String],
auth: Option<(&str, &str)>,
concurrency: usize,
dur: Duration,
) -> HttpFetchOutcome {
if urls.is_empty() {
return HttpFetchOutcome::default();
}
let start = Instant::now();
// Each worker returns (ops, ok, bytes, latencies_ms) so we can compute the
// like-for-like latency percentiles over EVERY successful GET's wall time.
let per_thread: Vec<(u64, u64, u64, Vec<f32>)> = std::thread::scope(|s| {
let handles: Vec<_> = (0..concurrency.max(1))
.map(|tid| {
let urls = &urls;
s.spawn(move || {
let client = match tls_roots_blocking(reqwest::blocking::Client::builder())
.tcp_keepalive(Duration::from_secs(30))
.build()
{
Ok(c) => c,
Err(_) => return (0u64, 0u64, 0u64, Vec::new()),
};
let mut ops = 0u64;
let mut ok = 0u64;
let mut bytes = 0u64;
let mut lat: Vec<f32> = Vec::new();
while start.elapsed() < dur {
let idx = ((ops as usize) * 7 + tid * 1337) % urls.len();
let mut req = client.get(&urls[idx]);
if let Some((u, p)) = auth {
req = req.basic_auth(u, Some(p));
}
let t0 = Instant::now();
if let Ok(resp) = req.send() {
// Only a 2xx counts as a real fetch: a 404 error page
// must not be summed as throughput (that's what made an
// external target with no matching corpus read as ~0 MB/s
// instead of an obvious failure).
let success = resp.status().is_success();
if let Ok(body) = resp.bytes() {
if success {
bytes += body.len() as u64;
ok += 1;
// Sample latency only for real (2xx) fetches, so
// the percentiles describe served requests — the
// exact same rule Nexus/Artifactory are judged by.
lat.push(t0.elapsed().as_secs_f64() as f32 * 1000.0);
}
}
}
ops += 1;
}
(ops, ok, bytes, lat)
})
})
.collect();
handles
.into_iter()
.map(|h| h.join().unwrap_or((0, 0, 0, Vec::new())))
.collect()
});
let elapsed = start.elapsed().as_secs_f64().max(1e-6);
let mut ops = 0u64;
let mut ok_ops = 0u64;
let mut bytes = 0u64;
let mut lat: Vec<f32> = Vec::new();
for (o, k, b, mut l) in per_thread {
ops += o;
ok_ops += k;
bytes += b;
lat.append(&mut l);
}
let (p50_ms, p99_ms, p999_ms, mean_ms) = latency_percentiles(&mut lat);
HttpFetchOutcome {
ops,
bytes,
mbs: (bytes as f64 / 1_048_576.0) / elapsed,
ops_per_sec: ops as f64 / elapsed,
ok_ops,
p50_ms,
p99_ms,
p999_ms,
mean_ms,
}
}
/// Compute (p50, p99, p999, mean) latency in ms from an unsorted sample vec.
/// Sorts in place. Empty input → all zeros (a skipped/empty leg reads as 0, not a
/// misleading spike).
pub(crate) fn latency_percentiles(lat: &mut [f32]) -> (f64, f64, f64, f64) {
if lat.is_empty() {
return (0.0, 0.0, 0.0, 0.0);
}
lat.sort_unstable_by(|a, b| a.partial_cmp(b).unwrap_or(std::cmp::Ordering::Equal));
let pick = |p: f64| -> f64 {
let idx = (((lat.len() as f64) * p).floor() as usize).min(lat.len() - 1);
lat[idx] as f64
};
let mean = lat.iter().map(|v| *v as f64).sum::<f64>() / lat.len() as f64;
(pick(0.50), pick(0.99), pick(0.999), mean)
}
/// The `holger-ds` artifact-delivery run: open a synthetic **crate** `archive`,
/// expose it over Holger's native HTTP door under route `repo` (or, when `target`
/// is `Some(base_url)`, drive an already-running Holger at that base instead of
/// standing one up), then bombard the crate-download path with the gatling pool —
/// measured **over the package protocol**, never in-process. Returns
/// `(file_count, outcome)`.
///
/// Pure reuse: [`start_holger_gateway`] + [`crate_download_urls`] +
/// [`bombard_http_get`] — the exact same over-the-wire path as the competitor
/// head-to-head. A `target` is expected to serve the matching `/{repo}` corpus.
///
/// Must run inside a **multi-thread** Tokio runtime (see [`start_holger_gateway`]).
pub async fn run_crate_fetch(
archive: &Path,
repo: &str,
target: Option<&str>,
concurrency: usize,
dur: Duration,
) -> Result<(usize, HttpFetchOutcome)> {
let backend_repo = Arc::new(
RustRepoZnippy::with_archive(repo.to_string(), archive.to_path_buf())
.map_err(|e| anyhow::anyhow!("open crate archive: {e:?}"))?,
);
let files = backend_repo.list_files();
anyhow::ensure!(!files.is_empty(), "no crate files in corpus");
let base = match target {
Some(t) => t.trim_end_matches('/').to_string(),
None => {
let backend: Arc<dyn RepositoryBackendTrait> = Arc::clone(&backend_repo) as _;
let addr = start_holger_gateway(vec![(repo.to_string(), backend)]).await?;
format!("http://{addr}")
}
};
let urls = crate_download_urls(&base, repo, &files);
anyhow::ensure!(!urls.is_empty(), "no crate-download URLs");
let outcome = bombard_http_get(&urls, concurrency, dur);
// When driving an EXTERNAL target, the synthetic corpus's crate names won't
// exist there, so every GET 404s — fail loudly instead of reporting ~0 MB/s
// as if the server were merely slow. (Self-host serves its own corpus, so a
// zero there is a real regression and is left to the caller.)
if target.is_some() && outcome.ok_ops == 0 {
anyhow::bail!(
"external target {base} returned no successful fetches ({} requests, all non-2xx) — \
the synthetic holger-ds corpus does not exist on an external server; benchmark a \
self-hosted holger (omit --target) or point at a server seeded with this corpus",
outcome.ops
);
}
Ok((files.len(), outcome))
}
/// Aggregate outcome of the `holger-ds` **Python-index** journey: the wheel
/// download head-to-head (headline MB/s + ops/s), the PEP 503 index-resolve arm
/// (ops/s — the `/simple/{name}/` step, which also computes+caches the SHA-256
/// pip/uv verify), and the internal in-process read ceiling.
#[derive(Debug, Clone, Default)]
pub struct PipServeOutcome {
pub files: usize,
pub packages: usize,
/// `/packages/{name}/{file}` wheel download bombardment (the served head-to-head).
pub wheel: HttpFetchOutcome,
/// `/simple/{name}/` PEP 503 detail-page resolve bombardment.
pub index: HttpFetchOutcome,
/// Holger-vs-Holger in-process ceiling (NOT a competitor column).
pub inprocess_ceiling_mbs: f64,
}
/// The `holger-ds` **Python journey** run: expose a synthetic **wheel** corpus over
/// Holger's PEP 503 / PEP 691 simple index (the very door `uv`/`pip` hit), then
/// bombard both the wheel-download path and the index-resolve path with the
/// gatling pool — measured **over the package protocol**, never in-process. The
/// Python sibling of [`run_crate_fetch`]: same [`start_holger_gateway`] +
/// [`bombard_http_get`], a [`PipRepoZnippy`] backend instead of a crate one.
///
/// When `target` is `Some(base_url)` an already-running Holger is driven instead of
/// standing one up (it must serve the matching `/{repo}` corpus). Must run inside a
/// **multi-thread** Tokio runtime (see [`start_holger_gateway`]).
pub async fn run_pip_index_fetch(
archive: &Path,
repo: &str,
target: Option<&str>,
concurrency: usize,
dur: Duration,
) -> Result<PipServeOutcome> {
let backend_repo = Arc::new(
PipRepoZnippy::with_archive(repo.to_string(), archive.to_path_buf())
.map_err(|e| anyhow::anyhow!("open pip archive: {e:?}"))?,
);
let files = backend_repo.list_files();
anyhow::ensure!(!files.is_empty(), "no wheel files in corpus");
let base = match target {
Some(t) => t.trim_end_matches('/').to_string(),
None => {
let backend: Arc<dyn RepositoryBackendTrait> = Arc::clone(&backend_repo) as _;
let addr = start_holger_gateway(vec![(repo.to_string(), backend)]).await?;
format!("http://{addr}")
}
};
let wheel_urls = pip_download_urls(&base, repo, &files);
let index_urls = pip_index_urls(&base, repo, &files);
anyhow::ensure!(!wheel_urls.is_empty(), "no wheel-download URLs");
// Headline: the wheel download bombardment (served bytes, like the competitor
// head-to-head). Then the index-resolve arm (ops/s over `/simple/{name}/`).
let wheel = bombard_http_get(&wheel_urls, concurrency, dur);
let index = bombard_http_get(&index_urls, concurrency, dur);
if target.is_some() && wheel.ok_ops == 0 {
anyhow::bail!(
"external target {base} returned no successful wheel fetches ({} requests, all non-2xx) — \
the synthetic holger-ds wheel corpus does not exist on an external server; benchmark a \
self-hosted holger (omit --target) or seed the server with this corpus",
wheel.ops
);
}
// In-process ceiling (Holger-vs-Holger): raw `get_file` over the wheel paths.
let wheel_files: Vec<String> = files
.iter()
.filter(|f| f.starts_with("packages/"))
.cloned()
.collect();
let ceiling_backend = Arc::clone(&backend_repo);
let n = wheel_files.len().max(1);
let inprocess_ceiling_mbs = inprocess_read_ceiling(concurrency, dur, move |i, tid| {
let idx = ((i as usize) * 7 + tid * 1337) % n;
ceiling_backend
.get_file(&wheel_files[idx])
.map(|d| d.len())
.unwrap_or(0)
});
Ok(PipServeOutcome {
files: files.len(),
packages: index_urls.len(),
wheel,
index,
inprocess_ceiling_mbs,
})
}
/// **Internal "Holger-vs-Holger regression ceiling" only** — NOT a competitor
/// comparison. Concurrent **in-process** `get_file()` over `files`, no HTTP/TLS,
/// returning MB/s. This is the theoretical read ceiling of the znippy archive
/// path; it is reported as `holger_inprocess_read_ceiling_mbs`, kept apart from
/// the over-the-wire `holger_served_mbs` head-to-head so a demo can never show it
/// beside the competitor numbers.
pub fn inprocess_read_ceiling<F>(concurrency: usize, dur: Duration, get: F) -> f64
where
F: Fn(u64, usize) -> usize + Send + Sync,
{
let get = &get;
let total_bytes = AtomicU64::new(0);
let start = Instant::now();
std::thread::scope(|s| {
for tid in 0..concurrency.max(1) {
let total_bytes = &total_bytes;
s.spawn(move || {
let mut n = 0u64;
let mut b = 0u64;
while start.elapsed() < dur {
b += get(n, tid) as u64;
n += 1;
}
total_bytes.fetch_add(b, Ordering::Relaxed);
});
}
});
let elapsed = start.elapsed().as_secs_f64().max(1e-6);
(total_bytes.load(Ordering::Relaxed) as f64 / 1_048_576.0) / elapsed
}
#[cfg(test)]
mod ds_smoke {
use super::*;
/// End-to-end `holger-ds` smoke: build a tiny synthetic crate corpus, stand up
/// the real Holger HTTP gateway over it, and bombard it briefly — asserting we
/// actually fetched bytes over the package protocol.
#[tokio::test(flavor = "multi_thread", worker_threads = 4)]
async fn corpus_served_over_protocol() {
let dir = std::env::temp_dir().join(format!("holger-ds-smoke-{}", std::process::id()));
let _ = std::fs::remove_dir_all(&dir);
let (archive, entries) =
crate::build_crate_corpus(&dir, 1024 * 1024).expect("build tiny corpus");
assert!(entries > 0, "corpus should contain crates");
let (files, outcome) =
run_crate_fetch(&archive, "rust", None, 2, Duration::from_millis(400))
.await
.expect("served crate fetch");
assert!(files > 0, "gateway should expose crate files");
assert!(outcome.ops > 0, "should complete GETs over the protocol");
assert!(outcome.bytes > 0, "should read bytes off the wire");
let _ = std::fs::remove_dir_all(&dir);
}
/// End-to-end `holger-ds` **Python** smoke: build a tiny synthetic wheel corpus,
/// serve it over the real Holger HTTP gateway as a PEP 503 index, and bombard
/// both the wheel-download and index-resolve paths over the protocol.
#[tokio::test(flavor = "multi_thread", worker_threads = 4)]
async fn pip_index_served_over_protocol() {
let dir = std::env::temp_dir().join(format!("holger-ds-pip-smoke-{}", std::process::id()));
let _ = std::fs::remove_dir_all(&dir);
let (archive, packages) =
crate::build_pip_corpus(&dir, 1024 * 1024).expect("build tiny wheel corpus");
assert!(packages > 0, "corpus should contain wheels");
let out = run_pip_index_fetch(&archive, "pip", None, 2, Duration::from_millis(400))
.await
.expect("served pip index fetch");
assert!(out.files > 0, "gateway should expose wheel files");
assert!(out.packages > 0, "index should list packages");
assert!(
out.wheel.ops > 0 && out.wheel.bytes > 0,
"wheel GETs over the protocol"
);
assert!(out.wheel.ok_ops > 0, "wheel fetches returned 2xx");
assert!(
out.index.ops > 0 && out.index.ok_ops > 0,
"index resolves returned 2xx"
);
let _ = std::fs::remove_dir_all(&dir);
}
}
/// reqwest 0.13 with reqwest 0.12 `rustls-tls` behaviour (2026-10-02): the ring
/// provider and the bundled Mozilla roots (webpki), not 0.13's aws-lc-rs + OS
/// trust store. Roots added later on the builder are merged in, as in 0.12.
#[allow(dead_code)]
fn tls_roots(b: reqwest::ClientBuilder) -> reqwest::ClientBuilder {
let _ = rustls::crypto::ring::default_provider().install_default();
b.tls_certs_only(
webpki_root_certs::TLS_SERVER_ROOT_CERTS
.iter()
.filter_map(|c| reqwest::Certificate::from_der(c).ok()),
)
}
/// reqwest 0.13 with reqwest 0.12 `rustls-tls` behaviour (2026-10-02): the ring
/// provider and the bundled Mozilla roots (webpki), not 0.13's aws-lc-rs + OS
/// trust store. Roots added later on the builder are merged in, as in 0.12.
#[allow(dead_code)]
fn tls_roots_blocking(b: reqwest::blocking::ClientBuilder) -> reqwest::blocking::ClientBuilder {
let _ = rustls::crypto::ring::default_provider().install_default();
b.tls_certs_only(
webpki_root_certs::TLS_SERVER_ROOT_CERTS
.iter()
.filter_map(|c| reqwest::Certificate::from_der(c).ok()),
)
}