blackbull.testing.native¶
blackbull.testing.native
¶
Native dispatch and full-stack loopback test instruments.
request() bypasses sockets; NativeTestServer includes protocol parsing. Synchronous wrappers own a background loop for their whole lifetime.
NativeClient
¶
Synchronous Tier 1 client, for tests written as plain def.
Owns one background event loop for the whole session — not one per
request — and drives the ASGI lifespan protocol around it, so startup
hooks have run before the first request the way they have in production::
with NativeClient(app) as client:
resp = client.get('/hello')
assert resp.status == 200
Prefer the module-level coroutines from an async def test: they call
the app on the test's own loop with no thread hand-off at all.
__enter__()
¶
Start the background loop and run the application's lifespan startup.
__init__(app)
¶
Prepare a client for app; the loop thread starts on __enter__.
delete(path, **kwargs)
¶
DELETE path on the app's own dispatch, no socket involved.
get(path, **kwargs)
¶
GET path on the app's own dispatch, no socket involved.
head(path, **kwargs)
¶
HEAD path on the app's own dispatch, no socket involved.
options(path, **kwargs)
¶
OPTIONS path on the app's own dispatch, no socket involved.
patch(path, **kwargs)
¶
PATCH path on the app's own dispatch, no socket involved.
post(path, **kwargs)
¶
POST path on the app's own dispatch, no socket involved.
put(path, **kwargs)
¶
PUT path on the app's own dispatch, no socket involved.
NativeTestResponse
dataclass
¶
What Tier 1 collected from the app's send channel.
body is the concatenation of every http.response.body chunk, so a
streaming handler and a single-shot one are compared the same way.
events keeps the raw ASGI events for tests that assert on the emission
sequence itself (chunk boundaries, more_body flags, trailers).
NativeTestServer
¶
BlackBull's own server on a loopback port, for full-stack tests.
Every layer runs: TCP accept, ConnectionActor, HTTP1Actor parsing,
the native Connection, dispatch, and the bytes the sender puts on the
wire. Anything whose answer depends on the wire — keep-alive reuse, the
HEAD body strip, chunked framing, connection close semantics — is only
observable here.
Async form (preferred: the server shares the test's event loop, as it shares the process loop in production)::
async with NativeTestServer(app) as server:
resp = await server.client.get('/hello')
Synchronous form, for plain def tests — the server runs on one
background loop for the session::
with NativeTestServer(app) as server:
resp = server.client.get('/hello')
The listener binds 127.0.0.1 only, so a test never exposes a port
beyond the machine. Plaintext HTTP/1.1 and WebSocket; TLS and HTTP/2 are
out of scope for this tier — reach for blackbull.fault_injection or the
conformance suites there.
build_connection(method, path, *, headers=None, body=b'', http_version='1.1', scheme='http', root_path='', client=_TEST_CLIENT_ADDR, server=_TEST_SERVER_ADDR)
¶
Build a Connection without protocol parsing.
Split query from path; retain undecoded raw_path and percent-decode path. Supply missing Host and Content-Length; an explicit Content-Length wins so tests can synthesize inconsistent framing.
delete(app, path, **kwargs)
async
¶
Drive app with a DELETE through the native dispatch path.
get(app, path, **kwargs)
async
¶
Drive app with a GET through the native dispatch path.
head(app, path, **kwargs)
async
¶
Drive app with a HEAD through the native dispatch path.
The handler sees HEAD: rewriting it to GET and stripping the body
is the H/1.1 actor's job (RFC 9110 §9.3.2), which is below this tier. Use
NativeTestServer to assert HEAD's wire behaviour.
options(app, path, **kwargs)
async
¶
Drive app with an OPTIONS through the native dispatch path.
patch(app, path, **kwargs)
async
¶
Drive app with a PATCH through the native dispatch path.
post(app, path, **kwargs)
async
¶
Drive app with a POST through the native dispatch path.
put(app, path, **kwargs)
async
¶
Drive app with a PUT through the native dispatch path.
request(app, conn, *, body=b'')
async
¶
Call app(conn, receive, send) and collect the response.
The full-control form: build (or mutate) a Connection yourself and
drive the app with it. The verb helpers below are thin wrappers over this.
The receive channel delivers body as one http.request event and then
reports http.disconnect — the same terminal signal a real recipient
gives once the body is drained, so a handler that over-reads gets the
production answer rather than hanging.