blackbull.client.scenario_oracle¶
blackbull.client.scenario_oracle
¶
Differential oracle shared by the conformance test and atheris fuzz.
Sprint 18 Phase 3 — the categoriser, per-side outcome dataclass, and
scenario runner used to live inside
tests/conformance/http1/test_http1_differential.py. That kept
them out of the production package (correct for test-only code), but
it also meant the atheris fuzz harness at
tests/conformance/http1/fuzz/fuzz_http1.py couldn't reuse them
without importing the pytest module (which side-effects on import via
pytest.importorskip and pytest.skip).
This module is the smallest extracted surface that lets atheris drive the same differential check as the Hypothesis test:
- :class:
Category— the 9-way enum that buckets each example. - :data:
ACCEPTED_CATEGORIES— pass-through set (OK,BOTH_REJECTED); divergences sit outside. - :class:
SideOutcome— what one server returned (response, or exception / timeout) in normalised form. - :func:
normalize_response, :func:categorize— the pure functions consumed by both callers. - :func:
run_scenario— the async wrapper that drives a :class:~blackbull.client.Scenarioagainst(host, port)and returns(SideOutcome, wire_bytes). - :data:
PER_REQUEST_TIMEOUT_S— wall budget cap per side.
The pytest module continues to own everything test-specific —
fixtures, Hypothesis strategies, corpus capture, DiffContext —
since none of that is meaningful in the atheris context.
Category
¶
Bases: str, Enum
Why a differential example was (not) accepted.
Subclassing str makes the values JSON-serialisable directly
and keeps assert ctx.category == 'OK'-style sites readable.
SideOutcome
dataclass
¶
One side's response in a differential pair.
Exactly one of (response, exception) is populated. timed_out
is True if the failure was an :class:asyncio.TimeoutError from
the per-example wait_for; we record it separately because timeouts
are semantically distinct from other transport errors.
categorize(ng, bb)
¶
Bucket a differential example into a :class:Category.
Order of the checks matters: both-rejected wins over individual transport failures so we don't flag inputs that nginx also refused.
normalize_response(resp)
¶
Drop volatile / framing-only headers; preserve status + body.
run_scenario(host, port, scenario)
async
¶
Execute scenario against (host, port) and return (outcome, wire_bytes).
Drives the scenario through :meth:HTTP1Client.execute_scenario,
which itself never raises; this wrapper only adds an outer
asyncio.wait_for so a runaway scenario (e.g. trickled bytes
plus a long read timeout) can't blow the per-side budget.
Returns the captured wire bytes (both servers receive identical bytes, so a single capture is enough for failure diagnostics).