postjevsql.git / tests / README.md

Chapter 13: tests, a real Postgres and a fake Jev

Every test in this folder starts a real Postgres of its own, a throwaway one in a temporary directory, with the freshly built extension served in place. And every test points that Postgres at a fake Jev: jev-mock (from jevcrates), a real HTTP/2 server over real TLS on loopback, which answers however the test tells it to and remembers every request it was sent.

flowchart LR
  T["a test (tokio-postgres)"] -->|"SQL over a Unix socket"| P[("a throwaway Postgres<br/>with the built extension")]
  P -->|"HTTP/2 + TLS, trusted through jev.ca_file"| M["jev-mock on loopback"]
  T -.->|"what arrived, byte for byte"| M

That last arrow is the point. A test does not only check that the query returned the right number. It goes to the mock and checks what was actually sent: how many requests, on how many connections, with which headers, how many at once, which streams were reset. "The query came back" can hide a dozen sins; the wire cannot.

Aside. Why not wall-clock time? Because the machine that runs the suite is shared with other builds, and "this took under two seconds" failed there while passing alone. So timing is measured where it means something: the mock counts the peak number of requests in flight for concurrency, the connections it accepted for redials, the retry header for retries. A test may say a backoff waited at least its delay; it never says at most.

Try it. nix develop -c buck2 test //tests:happy_path runs the smallest one: one row, one Noul, and an assertion on every byte of the one request it sends. Every test also has a twin for Postgres 17, //tests:happy_path-pg17, and nix develop -c buck2 test //... runs the whole suite on both majors.

The tests

FileWhat it holds the extension to
happy_path.rsjev_prob end to end, asserting the exact request.
batching.rsThe batch scan: concurrency on one connection, filtered rows never sent, LIMIT, shared requests, rescans, aggregates, windows, CTEs, subqueries, partitions, reach, refusal before any request, EXPLAIN.
dml.rsUPDATE, DELETE, INSERT … SELECT and SELECT … FOR UPDATE batched, and an EvalPlanQual recheck that never sends.
threshold.rsjev(row, q [, threshold]): argument, then jev.threshold, then 0.5, sharing judgments with jev_prob.
score.rsjev_score and jev_score_norm: 2–10 levels in the caller's order, one judgment between them.
choice.rsjev_choice: options in the caller's order, 2–255 sent, one answered without asking.
confidence.rsjev_confidence: the vendor's figure, sharing the Choice's or Score's judgment; 'noul' refused before sending.
eval.rsjev_eval and the *_full functions: typed composites from the same judgment and cache row.
label_tree.rsjev_choice_tree: one Choice per node, a round per request, each node cached.
shortlist.rsjev_choice_shortlist: chunks in one request, finalists in a second, each cached.
cache.rsThe durable cache, and equal judgments sharing one request in a statement, including a call that asks again when a rescan drops the one it waited on.
canonical.rsThe row sent in a form that does not depend on the session and loses no digit.
budget.rsjev.max_rows and jev.max_cost: refused before the first request, and stopped mid-statement.
ratelimit.rsThe cluster-wide rate limit, timed at the mock: two backends sharing one ceiling, a 429 halving the rate, a cancel ending the wait.
retries.rs408, 429 and 5xx retried at most twice, a server delay honoured, the retry count sent; any other status fails at once.
on_error.rsjev.on_error: a 500 skips its row under unsure and fails the statement under error; a refused key raises under both.
cancel.rsA timed-out or cancelled request resets its HTTP/2 stream, proven at the server.
keepalive.rsPings while a scan runs and only then, and an unanswered one replacing the connection.
dns.rsHost names resolved through hickory on the executor, against jev.dns_servers.
latency.rsThe extension's own overhead per call, against a mock that answers instantly.
stats.rsjev_stats() counts this session's work, and every retry, redial and answer is logged at DEBUG1 with its request id.
api_key.rsThe key from exactly one place: TYPESAFE_API_KEY or jev.api_key_file, never both.
privileges.rsSpending takes an explicit GRANT, and no function is executable by PUBLIC.
settings.rsSettings checked when they are set, not only when used.
replay.rsFixture replay by exact request bytes, and a request the fixture lacks failing with the request named.
gate_layout_parity.rs, gate_unsure_band.rs, gate_label_tree.rs, gate_ranking.rsEach release gate's recorded live answers replayed through the extension, so a change to what it sends fails naming the request (chapter 15).
sidecar.rsThe scan over a postgres_fdw loopback foreign table, and a server listing this extension in extensions refused at plan time.
sidecar_cli.rsThe sidecar CLI against a throwaway target and sidecar (chapter 12).

For the people who maintain it

PathWhat
support/The library every test shares: the throwaway Postgres, the instance wired to the mock, the gates' rows and SQL. Chapter 14.
fixtures/The release gates' recorded live answers. Chapter 15.
BUCKOne first_party_test per file, each given the built extension, the server of its major and the fixtures through the environment.
run-check.shThe repo's whole check, nix develop -c buck2 test //..., queued on a lock so two checks in one checkout do not race the same buck daemon. Mefi Studio runs it to verify finished agent work.

← Previous: Chapter 12, cli/ · Up: postjevsql · Next: Chapter 14, support/ →