diff --git a/.cargo/mutants.toml b/.cargo/mutants.toml index c2316a8..2fbac63 100644 --- a/.cargo/mutants.toml +++ b/.cargo/mutants.toml @@ -130,12 +130,45 @@ exclude_re = [ # not pay the timeout floor weekly for a verdict already in hand. "otel\\.rs:931:21:.*\\+= with \\*=", "otel\\.rs:634:22:.*== with !=", + # The quicksearch byte scanners' cursor hangs, hand-reproduced test by + # test under a 10s timeout: `i *= 1` leaves the index where it is, so + # the loop re-runs the same byte in the same state forever; `i -= 1` + # (`-= 2` past an escape) steps back onto a byte the preceding branch + # advances over again, so the pair cycles (on other inputs it + # underflows, and those tests fail). Each one hangs at least one test + # in the same binary, which then never exits, so no added test can + # turn it into a kill. + "server\\.rs:2044:15:.*\\+= with -=", + "server\\.rs:2052:15:.*\\+= with (-=|\\*=)", + "server\\.rs:2063:15:.*\\+= with \\*=", + "server\\.rs:2078:15:.*\\+= with (-=|\\*=)", + "server\\.rs:2082:11:.*\\+= with (-=|\\*=)", + "server\\.rs:2099:11:.*\\+= with \\*=", + "server\\.rs:2116:15:.*\\+= with (-=|\\*=)", + "server\\.rs:2132:11:.*\\+= with (-=|\\*=)", + "server\\.rs:2144:11:.*\\+= with \\*=", + # The foreign-host skip `at = pos + 1`: `pos - 1` and `pos * 1` both + # re-find the occurrence just rejected, so the scan never passes it + # (embedded_scan_needs_a_domain_boundary_on_the_left hangs). + "guard\\.rs:942:26:.*\\+ with (-|\\*)", + # The stdio delimiter advance `newline + 1` (DESIGN.md): `- 1` and + # `* 1` leave the newline inside `rest`, so the frame loop finds it + # again forever (the multi-frame drain hangs; `- 1` on a bare newline + # underflows instead, in the same binary). + "stdio\\.rs:196:34:.*\\+ with (-|\\*)", # The glob backtrack advance is unobservable: `p = star_pos` re-enters # the `'*'` arm and lands on the identical (p, resume, s) state one # iteration later (or skips the same `'*'` in the trailing-star loop), # proven by 265k exhaustive + 200k random differential cases with zero # divergences. "policy\\.rs:171:26:.*\\+ with \\*", + # Its three advances frozen by `*= 1` — the `'*'` skip (164), the + # backtrack resume (172) and the trailing-star drain (180) — never + # move p or resume, so every glob test with a `*` hangs; reproduced + # by hand like the otel hangs above. + "policy\\.rs:164:15:.*\\+= with \\*=", + "policy\\.rs:172:20:.*\\+= with \\*=", + "policy\\.rs:180:11:.*\\+= with \\*=", # Typo-shaped ports are refused by exact refusal text the anchor tests # pin; `>=` for `>` is identical (at equal lengths the byte lookup is # None), so that one is documented in DESIGN.md, not tested. diff --git a/.github/workflows/mutants.yml b/.github/workflows/mutants.yml index 0a784c0..e9738bd 100644 --- a/.github/workflows/mutants.yml +++ b/.github/workflows/mutants.yml @@ -87,8 +87,9 @@ jobs: # the tier/variant/tag exclusions, policy.rs the glob-advance one, # client.rs none; the eight small files carry two # documented-not-tested exclusions (config port >=, custom comma - # collapse) while the stdio delimiter hangs stay in scope and pay - # the floor weekly. + # collapse). Hand-reproduced cursor hangs (server, policy, guard, + # stdio) are excluded like otel's, so a verdict already in hand does + # not pay the floor weekly. MUTANTS_SCOPE: crates/bugwarden-core/src/guard.rs crates/bugwarden/src/main.rs crates/bugwarden/src/server.rs crates/bugwarden/src/audit.rs crates/bugwarden/src/otel.rs crates/bugwarden-core/src/client.rs crates/bugwarden-core/src/policy.rs crates/bugwarden-core/src/custom_fields.rs crates/bugwarden/src/config.rs crates/bugwarden/src/http_auth.rs crates/bugwarden/src/stdio.rs crates/bugwarden/src/tracing_fields.rs crates/bugwarden/src/http_session.rs crates/bugwarden/src/panic_hook.rs crates/bugwarden-core/src/quoted.rs steps: - uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 diff --git a/crates/bugwarden-core/src/client.rs b/crates/bugwarden-core/src/client.rs index b2038f8..86bb7c2 100644 --- a/crates/bugwarden-core/src/client.rs +++ b/crates/bugwarden-core/src/client.rs @@ -1047,4 +1047,24 @@ mod tests { "Debug keeps the status and the code: {own}" ); } + + /// Microseconds are summed across requests and reported as whole + /// milliseconds, so two sub-millisecond waits make one together; a `%` + /// or `*` in the scale reports 300 or 1_300_000 for the same pair. + #[test] + fn upstream_stats_sum_microseconds_into_whole_milliseconds() { + let stats = UpstreamStats::default(); + stats.record(Duration::from_micros(600), Some(200)); + stats.record(Duration::from_micros(700), None); + assert_eq!(stats.requests(), 2); + assert_eq!(stats.latency_ms(), 1); + assert_eq!( + stats.status(), + None, + "the last request produced no response" + ); + stats.record(Duration::from_micros(2_345_678), Some(404)); + assert_eq!(stats.latency_ms(), 2_346); + assert_eq!(stats.status(), Some(404)); + } } diff --git a/docs/DESIGN.md b/docs/DESIGN.md index f692957..fe9dcb5 100644 --- a/docs/DESIGN.md +++ b/docs/DESIGN.md @@ -3957,8 +3957,11 @@ wired, `server.rs` and `main.rs` are the reference. Display is the unchanged text; the code is read only inside an `"error": true` envelope (a proxy's non-2xx JSON, a string code and a float code give none); a gateway's HTML under 502 and a 200 non-JSON body are - typed with no code; and neither the type's Debug nor anyhow's alternate - Debug carries the message. + typed with no code; neither the type's Debug nor anyhow's alternate + Debug carries the message; and `UpstreamStats` sums microseconds across + requests into whole milliseconds (two sub-millisecond waits make one, so + neither a `%` nor a `*` scale passes), the last status winning and a + response-less request reading `None`. - Unit tests (#[cfg(test)] in crates/bugwarden/src/server.rs): assemble_bug_info re-classification — a body embargoed after the verdict is refused, a body that now earns only summary is downgraded, a body granting neither read nor