Skip to content

blackbull.client.scenario

blackbull.client.scenario

Scenario data model for the HTTP/1.1 test instrument.

A :class:Scenario is a sequence of typed steps that the :meth:blackbull.client.HTTP1Client.execute_scenario executor walks in order against a live socket. The same data model is consumed by:

  • the differential test (:mod:tests.conformance.http1.test_http1_differential), which generates scenarios via Hypothesis and compares BlackBull's response to nginx's.
  • the atheris fuzz harness (:mod:tests.conformance.http1.fuzz.fuzz_http1), which decodes coverage-guided mutated bytes into scenarios via :meth:Scenario.from_bytes.

Two serialisations are supported:

  • :meth:Scenario.to_json / :meth:Scenario.from_json — JSON Lines, one step per line, for the on-disk corpus under tests/conformance/http1/fuzz/corpus/*.jsonl. Diff-friendly in git; readable when failures are pasted into reports.
  • :meth:Scenario.from_bytes — a total opcode-tagged decoder. Every byte string maps to a valid scenario, so atheris's byte-level mutations never crash on input parsing — each mutation produces a distinct execution path against the server.

Abort dataclass

Hard-close the connection (transport.abort → RST on Linux).

Distinct from the graceful writer.close() / wait_closed() in HTTP1Client.__aexit__. Subsequent steps short-circuit; the executor stops walking the scenario after an Abort.

ReadResponse dataclass

Read one HTTP/1.1 response from the connection.

timeout bounds the entire status-line + headers + body read. On timeout the executor records the outcome on the :class:ScenarioResult and does not raise — the caller decides whether to treat that as a transport-fail or normal outcome.

Scenario dataclass

A sequence of steps the executor walks against one connection.

from_bytes(raw) classmethod

Decode arbitrary bytes into a scenario.

Total function: every byte string yields a valid scenario, including the empty string (→ empty scenario). Designed so atheris's coverage-guided byte mutations always produce runnable input — the fuzzer never spends cycles on parser errors.

Encoding:

  • The decoder walks raw left-to-right. At each position the next byte selects an opcode via % 4 (every byte value is therefore a legal opcode tag).
  • Each opcode then consumes a small payload from the following bytes. If the payload is short (end of input), decoding stops cleanly and the partial scenario is returned.

Opcode layout::

byte % 4 == 0  → SEND
    next 2 bytes (big-endian uint16) = length;
    next ``length`` bytes = data;
    next 1 byte (% len(_BYTE_INTERVAL_TABLE))
      → byte_interval.
byte % 4 == 1  → SLEEP
    next 1 byte (% len(_SLEEP_TABLE)) → duration.
byte % 4 == 2  → READ
    next 1 byte (% len(_TIMEOUT_TABLE)) → timeout.
byte % 4 == 3  → ABORT
    no payload.  Remaining bytes are discarded — an
    Abort short-circuits execution anyway, so it's the
    natural terminator.

Bounded payload sizes (uint16 length) keep individual scenarios well under 64 KiB, which is what we want for per-iteration fuzz throughput.

from_json(src) classmethod

Parse JSON Lines back to a :class:Scenario.

Skips blank lines so files that end with a trailing newline (the conventional git-friendly shape) parse cleanly.

to_json()

Serialise to JSON Lines: one {"op": ..., ...} per line.

Bytes payloads are base64-encoded so the result round-trips through stdout / git / json.loads without escape ambiguity. Round-tripped by :meth:from_json.

well_formed(raw_request, *, response_timeout=5.0) classmethod

Wrap a complete raw HTTP/1.1 request as a one-shot scenario.

Equivalent to "send these bytes, then read one response". Used by the legacy diff_*.txt corpus loader and by the Hypothesis well_formed_scenario_strategy.

ScenarioResult dataclass

Outcome of one :meth:HTTP1Client.execute_scenario call.

Exactly one of response / exception / timed_out / aborted is the meaningful field; the others are None / False. The executor never raises, so callers (differential test, fuzz harness) categorise on this object instead of writing try/except boilerplate per scenario.

SendBytes dataclass

Push raw bytes onto the connection.

byte_interval > 0 transmits one byte at a time with that delay between bytes — the primitive slowloris-style stall that lets scenarios express trickled headers or trickled bodies without dropping to a raw asyncio socket.

Sleep dataclass

Idle for duration seconds without sending or reading.

Useful for post-headers idle, mid-keep-alive idle, and pre-response stall scenarios where the server is expected to time out and close.

StepOp

Bases: str, Enum

Tag for serialising / decoding a step. str mixin so values drop straight into JSON without a custom encoder.