Conformance¶
The conformance harness proves a broker honours the core contract. It has two entry points, both of which panic with a descriptive message on the first failure:
harness::run_suitechecks the routing surface against your in-processTestClient.harness::lifecyclechecks the lazy-startup contract end to end against a connected broker.
Run both: run_suite for the dispatch guarantees, lifecycle to prove new -> connect ->
subscribe -> publish -> ack -> shutdown works on the real transport.
Enable your crate's own testing feature alongside it, since run_suite drives the TestClient you
ship there.
The routing suite¶
harness::run_suite takes a factory that builds a fresh client per scenario. The factory is
fallible (it returns the TestClient::start result) and is invoked once per scenario, so scenarios
cannot leak state into each other. This is the in-memory reference broker's own suite run,
verbatim; substitute your TestClient::start in the factory:
use ruststream::conformance::harness;
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
async fn memory_broker_passes_conformance_suite() {
harness::run_suite(|| async { Ok::<_, Infallible>(MemoryBroker::new()) }).await;
}
What it checks¶
| Scenario | Asserts |
|---|---|
| ordering | messages are delivered in publish order |
| publish after subscribe | a subscriber receives only messages published after it attached; earlier publishes are not buffered |
| ack consumes delivery | an acked message is not redelivered |
| nack with requeue redelivers | nack(requeue = true) delivers the message again |
| nack without requeue drops | nack(requeue = false) does not redeliver |
| headers propagate | message headers survive the round trip |
| expect_published observes publishes | the test client records published messages |
These are core-routing guarantees, the contract every broker must meet. The harness does not test broker-specific semantics (durable resume, redelivery on timeout, partition assignment); those are not part of the contract and are verified in your own end-to-end suite against a real server.
The lifecycle check¶
run_suite exercises routing through the TestClient; harness::lifecycle exercises the
lazy-startup contract through the real Broker: synchronous construction with no I/O, then
connect, a subscription opened through the broker's own SubscriptionSource, a publish the
subscription receives and acks, and finally shutdown. It takes three factories that keep it
broker-agnostic:
use ruststream::conformance::harness;
use ruststream_nats::{NatsBroker, SubscribeOptions};
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
#[ignore = "needs a running nats-server; set NATS_TEST_URL"]
async fn passes_lifecycle() {
let url = std::env::var("NATS_TEST_URL").unwrap();
harness::lifecycle(
|| NatsBroker::new(url.clone()), // sync construction (no I/O)
|subject| SubscribeOptions::new(subject), // the broker's SubscriptionSource
|broker| broker.publisher(), // a publisher from the connected broker
)
.await;
}
make_brokeris synchronous (Fn() -> B). A broker that can only be built asynchronously cannot satisfy it - which is the point: construct cheaply, connect lazily inBroker::connect.make_sourcebuilds the subscription descriptor for a subject (the macro-subscriber path).make_publisherproduces a publisher from the connected broker.
A broker with no ack semantics (Core NATS) passes by returning AckError::Unsupported from ack;
the check accepts that as well as a successful ack. Because lifecycle performs a real connect,
run it against a live server (gate it behind an env var like NATS_TEST_URL); the in-memory broker
can run it in-process.
Capability suites¶
If your broker implements a capability trait, run the matching suite from
conformance::capabilities to prove the implementation honours the trait contract; brokers
without the capability simply do not call it. Each suite takes the same factory shape as
lifecycle and performs a real connect, so gate it the same way:
| Suite | Requires | Asserts |
|---|---|---|
capabilities::request_reply |
RequestReply |
the request reaches a responder with a usable reply-to header, the correlated reply resolves the request, an unanswered request fails after its timeout |
capabilities::batches |
BatchSubscriber |
every published message arrives in publish order, distributed over non-empty batches |
capabilities::transactions |
TransactionalPublisher |
nothing inside a transaction is visible before commit, a commit publishes the buffer in order, an abort discards it |
use ruststream::conformance::capabilities;
use ruststream_nats::{NatsBroker, SubscribeOptions};
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
#[ignore = "needs a running nats-server; set NATS_TEST_URL"]
async fn passes_request_reply() {
let url = std::env::var("NATS_TEST_URL").unwrap();
capabilities::request_reply(
|| NatsBroker::new(url.clone()),
|subject| SubscribeOptions::new(subject),
|broker| broker.publisher(), // the RequestReply publisher under test
|broker| broker.publisher(), // the plain publisher the responder replies through
)
.await;
}
The in-memory broker implements every capability natively and passes all three suites in-process (see Memory); it is the executable reference for what each suite expects.
Author checklist¶
Before publishing a broker crate:
- [ ]
Broker,Subscribe(or aSubscriptionSource),Subscriber,IncomingMessage, andPublisherare implemented. - [ ]
shutdownperforms all fallible teardown and never blocks or panics. - [ ] Ack consumes
self; nack honours therequeueflag. - [ ] The crate owns its
Config; fields without a sane default do not get aDefault. - [ ] Capability traits are implemented only where the broker genuinely supports them, and each
implemented capability passes its
conformance::capabilitiessuite. - [ ] A
TestClientis shipped under atestingfeature, doing core routing only. - [ ]
harness::run_suitepasses (the routing surface). - [ ]
harness::lifecyclepasses against a real server, gated behind an environment variable (the lazy-startup contract: syncnew, lazyconnect, subscribe, ack,shutdown). - [ ] An end-to-end suite covers broker-specific semantics, also gated behind that variable.
- [ ]
Cargo.tomlmetadata is complete (description,license,repository,keywords,categories), and CI checks--no-default-featuresand--all-features.
See Writing a broker for the trait contract.