Repository navigation
Testing
Three tiers. Each gates something specific, and the higher ones are deliberately rare so that day-to-day work stays fast.
| Tier | What it is | Gates |
|---|---|---|
| 1. Unit |
cargo test — config parsing, payload body, and the engine contract driven over a real socket; shellcheck over the coturn installer |
Every push, every image build |
| 2. e2e | Playwright drives the real engine in Chromium and Firefox against the real backend; a loopback throughput floor; a history suite; a packet-loss run against real coturn; the built image is booted and probed | Backend routes, front-end engine wiring, measurement profiles |
| 3. On hardware | A full run from a LAN browser against a real deployment, and a relay verified with Trickle-ICE | Before shipping a change to the relay, TLS, or anything in the data path |
CI runs every tier-1 and tier-2 job on every push: Backend, Shell, e2e, History, Packet loss via TURN, and Docker image.
# Tier 1 — backend, plus the throughput floor
cargo test --manifest-path backend/Cargo.toml --release -- --nocapture
# Tier 1 — front end
npm --prefix frontend run typecheck
npm --prefix frontend run test:unit
# Tier 1 — the relay installer
shellcheck provisioning/coturn/install-coturn.sh
# Tier 2 (needs both built first)
npm --prefix frontend run build
cargo build --release --manifest-path backend/Cargo.toml
(cd frontend && npx playwright install --with-deps chromium firefox)
npm --prefix frontend run test:e2e
# Tier 2, packet loss (needs coturn)
docker compose -f docker-compose.e2e.yml up -d
# start the backend with SPEEDTEST_PROFILE=e2e-packetloss and the TURN vars set,
# then:
SPEEDTEST_E2E_NO_SERVER=1 SPEEDTEST_E2E_PACKETLOSS=1 npx playwright test packetloss # from frontend/The packet-loss and history suites skip themselves when their environment variable is unset, rather than failing on an absent relay or an absent database.
Most of them exist because the corresponding failure is silent — the engine keeps working and reports a wrong number.
-
logAimApiUrlis null. Otherwise every completed result is POSTed to Cloudflare. Asserted in the contract tests, re-asserted in the browser, and backed by an end-to-end check that no request leaves the origin for the whole run. -
server-timingmatches the engine's parser. The test transcribes the engine's own regex and also asserts that the obvious wrong spelling would be rejected, so the test cannot rot into tautology. -
/__upwithholds its response until the body is drained. Fed a slow body, the response must not arrive early. Upload speed is time-to-first-byte alone, so answering early makes uploads look instantaneous. - Measurement responses forbid caching. Otherwise the browser serves repeat requests from cache and the engine measures the cache.
-
Profiles keep loaded latency measurable. Checked against each profile's
nominal link speed; a profile whose smallest transfer is faster than its own
loaded_request_min_durationfails the build. This one has already caught a real mistake. - The packet-loss stage is dropped when no relay is configured, and both TURN credentials appear when one is.
-
/metricsrefuses rather than falling through. With metrics off the route must return 404 from its own handler; a test that only checked the status code once passed for the wrong reason, because itsstatic_dirpointed at a directory that did not exist and so the app-shell fallback failed too. It now asserts the fallback is genuinely answering first.
backend/tests/throughput.rs pulls several parallel streams over loopback and
asserts a floor, taking the best of three passes because shared runners are
noisy. It prints what it achieved so the number the floor was chosen against
stays visible in the log.
It is not a prediction of browser-reported speed. The engine is
single-stream and decodes every body through r.text(), so it always reads
lower. This test asserts one thing: the backend is not the bottleneck. It
catches regressions of kind — per-request allocation, an accidental full-body
buffer — which cost an order of magnitude.
A 10 GbE saturation check against real hardware was planned and waived. The claim it would have supported — that the backend can saturate a 10 GbE link — is therefore untested at that rate, and the throughput floor over loopback is what stands in for it.
The other thing that check would have proven, that no traffic leaves the network, is instead proven by the end-to-end suite on every push: a real browser drives a complete run and the test fails if a single request leaves the origin. That is a stronger guarantee than a one-off packet capture, because it runs again every time anything changes.
See also: Development · Engine Contract · TURN and Packet Loss · Troubleshooting
Getting it running
Using it
Working on it