Connector framework — verification

Test cases verifying the connector framework requirements. Each test directive :verifies: one or more requirements from Connector framework (or building blocks from Connector framework — architecture (arc42)). The four-layer test pyramid from the architecture’s quality strategy is reflected by the section grouping below: unit, codec, transport integration, MQTT integration, workspace end-to-end, and loom concurrency.

Implementation tests (Rust #[test]) and the verification artefacts on this page trace 1:1 — once the implementation lands, each test body cites the Rust test path that runs it.


Unit tests

Per-crate, no IPC, parallel-safe.

Test Case: ExponentialBackoff invariants TEST_0100
status: open
verifies: REQ_0233

Property test (proptest) on ExponentialBackoff confirming: delays are monotonically non-decreasing until the cap is reached, delays never exceed the configured maximum, reset() returns the policy to the initial delay, and jitter stays within the configured ratio. Lives under taktora-connector-core/tests/.

Test Case: ConnectorHealth state-machine transitions TEST_0101
status: open
verifies: REQ_0230, REQ_0234

Unit test asserting that every valid transition between ConnectorHealth variants (per Health and reconnect lifecycle (ARCH_0012)) emits exactly one HealthEvent on the connector’s health channel, and that illegal transitions panic in debug builds.

Test Case: MqttRouting wildcard demux predicate TEST_0102
status: open
verifies: REQ_0254

Unit-level coverage of the topic-match predicate independent of any broker or iceoryx2 service: every (subscription pattern, incoming topic) pair is asserted against the MQTT 3.1.1 wildcard semantics (single-level +, multi-level #).

Test Case: ChannelDescriptor validation TEST_0103
status: open
verifies: REQ_0201, REQ_0221

Asserts that constructing a ChannelDescriptor with an empty name fails, and that the const-generic N propagates correctly through create_writer / create_reader (compile-fail tests ensure mismatched N values do not type-check).


Codec tests

Test Case: JsonCodec round-trip property test TEST_0110
status: open
verifies: REQ_0210, REQ_0212

proptest-driven round-trip for a representative struct: encode(value, &mut buf) followed by decode(&buf[..len]) yields a value equal to the original under every shrunken input. Runs against JsonCodec; will be parameterised over MsgPackCodec and ProtoCodec once those land.

Test Case: Codec encode error on undersized buffer TEST_0111
status: open
verifies: REQ_0213

Encoding a value larger than the provided buffer returns ConnectorError::PayloadOverflow { actual, max } so the buffer-exhaustion path is distinguishable from genuine serializer faults at the codec layer. Other serializer failures (NaN floats with strict configuration, non-string map keys, etc.) surface as ConnectorError::Codec carrying the codec’s static format_name() and the underlying serializer error chain. Routing buffer-overflow to PayloadOverflow keeps the codec layer consistent with Outbound bridge saturation ... (REQ_0323) and Payload-overflow rejection (TEST_0125) — buffer exhaustion is always the same variant regardless of which layer detects it.

Test Case: Codec decode error propagation TEST_0112
status: open
verifies: REQ_0214

Receiving a payload that fails decode<T> (e.g. truncated JSON, wrong shape) surfaces as ConnectorError::Codec from ChannelReader::try_recv rather than silently dropping the envelope.


Transport integration tests

Iceoryx2 services are real; tests run with --test-threads=1; each test scopes its own Node name.

Test Case: ChannelWriter → ChannelReader round-trip TEST_0120
status: open
verifies: REQ_0205, REQ_0223

End-to-end zero-copy round-trip through a real iceoryx2 service: writer.send(&value) followed by reader.try_recv() yields the same value. Verifies that Publisher::loan is used (no intermediate copies) by asserting on a header field set in-place.

Test Case: Sequence-number monotonicity TEST_0121
status: open
verifies: REQ_0202

Sending N envelopes through a single ChannelWriter and reading them on the corresponding ChannelReader asserts strictly increasing sequence_number values starting at zero.

Test Case: Timestamp populated at send TEST_0122
status: open
verifies: REQ_0203

Captures wall-clock time before and after writer.send; the received envelope’s timestamp_ns falls within the bracket.

Test Case: Correlation ID round-trip TEST_0123
status: open
verifies: REQ_0204

writer.send_with_correlation(&value, id) followed by reader.try_recv() yields a header whose correlation_id bytes equal id. Confirms the framework does not interpret the field — random bytes round-trip unchanged.

Test Case: Per-channel size — 4 KB, 64 KB, 1 MB TEST_0124
status: open
verifies: REQ_0201, BB_0010

Three round-trip tests with channels parameterised at distinct N (4 096, 65 536, 1 048 576). All three succeed; iceoryx2 services have non-overlapping pool sizes per channel.

Test Case: Payload-overflow rejection TEST_0125
status: open
verifies: REQ_0201

writer.send(&value) for a value whose encoded form exceeds the channel’s N returns ConnectorError::PayloadOverflow { actual, max } and emits no envelope on the wire.

Test Case: Service naming derived from descriptor TEST_0126
status: open
verifies: REQ_0206, BB_0011

Two ChannelDescriptor values with identical name produce identical iceoryx2 service names; differing name values produce different service names. Names follow the convention documented in ServiceFactory (sub-block o... (BB_0011).


MQTT integration tests

Embedded rumqttd per-test fixture on an ephemeral port; iceoryx2 services per test as before; one tokio runtime per test.

Test Case: QoS 0 round-trip TEST_0130
status: open
verifies: REQ_0252

Plugin → gateway → broker → gateway → plugin round-trip with MqttRouting { qos: AtMostOnce, retained: false }. Asserts the payload bytes are preserved end-to-end.

Test Case: QoS 1 round-trip TEST_0131
status: open
verifies: REQ_0252

Same as TEST_0130 but with qos: AtLeastOnce. Additionally asserts a PUBACK is observed on the gateway side before reporting success.

Test Case: Retained-message publish + subscribe TEST_0132
status: open
verifies: REQ_0253

Publish with retained: true; a subsequent subscribe receives the retained payload as the first message. Publish a second payload with retained: false and verify the retained value is not overwritten by an unset retained.

Test Case: Wildcard subscription with `+` TEST_0133
status: open
verifies: REQ_0254

Subscribe with plant/+/temperature; publish to plant/A/temperature and plant/B/temperature and plant/A/B/temperature. Reader receives the first two; not the third.

Test Case: Wildcard subscription with `#` TEST_0134
status: open
verifies: REQ_0254

Subscribe with plant/#; publish to plant/A, plant/A/B, plant/A/B/C. Reader receives all three.

Test Case: Username/password authentication TEST_0135
status: open
verifies: REQ_0255

MqttConnectorOptions configured with username + password; rumqttd fixture configured to require credentials. CONNECT succeeds; a wrong-credential variant of the same test fails with ConnectorHealth::Down { reason: "auth" }.

Test Case: TLS connection (developer-machine only) TEST_0136
status: open
verifies: REQ_0256

rumqttd fixture configured with a self-signed cert; the tls cargo feature is enabled; MqttConnectorOptions points at the test cert. CONNECT succeeds. Not run in CI — gated behind cfg(feature = "tls") and a CONNECTOR_MQTT_TLS_TESTS env var so the repo carries no embedded test certs.

Test Case: Reconnect after broker bounce TEST_0137
status: open
verifies: REQ_0232, REQ_0233

While the connector is Up, kill the rumqttd fixture; observe transition to Down then Connecting; restart the broker; observe transition back to Up within ExponentialBackoff::max_delay seconds. Counts of HealthEvent transitions are asserted.

Test Case: HealthEvent emitted on every transition TEST_0138
status: open
verifies: REQ_0234

Drives the connector through every legal transition in Health and reconnect lifecycle (ARCH_0012) and asserts a HealthEvent arrives on subscribe_health() for each one, in the order driven.

Test Case: Outbound bridge saturation → BackPressure TEST_0139
status: open
verifies: REQ_0260

Configure MqttConnectorOptions with a tiny outbound-bridge capacity (e.g. 2). Stop draining the gateway by holding the tokio task busy. Send N > 2 messages; the (N-1)th or Nth send returns Err(ConnectorError::BackPressure) and the connector transitions to ConnectorHealth::Degraded.

Test Case: Inbound bridge saturation → DroppedInbound TEST_0140
status: open
verifies: REQ_0261

Configure a tiny inbound-bridge capacity. Block the inbound gateway item from draining (e.g. by holding ChannelReader). Publish a flood of QoS 0 messages from the broker fixture; the gateway emits HealthEvent::DroppedInbound { count } with count > 0. For QoS 1 traffic, PUBACK is observably delayed until the bridge drains.


EtherCAT integration tests

EtherCAT tests come in two flavours: software tests (unit + raw-frame mock) parallel-safe on any host, and hardware-in-the-loop tests marked [bench] that require an EK1100 + EL-series fixture on the CI test bench. The mock implementation lives in taktora-connector-ethercat/tests/mock/ and replays canned EtherCAT frame responses so bring-up, PDO mapping, and WKC scenarios can be exercised without hardware. Bench tests run only when invoked as cargo test --features ethercat-bench.

Test Case: EthercatConnector trait surface TEST_0200
status: open
verifies: REQ_0310

Compile-time test confirming that EthercatConnector<JsonCodec> implements Connector with type Routing = EthercatRouting. A trybuild compile-fail companion test asserts that swapping Routing to a foreign marker fails to compile.

Test Case: EthercatRouting field round-trip TEST_0201
status: open
verifies: REQ_0311

Unit test constructing EthercatRouting values with the four fields (SubDevice configured address, PDO direction, bit offset, bit length); asserts the values round-trip through serialization and that EthercatRouting: Routing + Clone + Send + Sync + Debug + 'static holds at compile time.

Test Case: Single MainDevice per gateway instance TEST_0202
status: open
verifies: REQ_0312

Construct an EthercatGateway builder and confirm that the builder accepts exactly one network interface name and produces exactly one MainDevice. A second call to .with_interface replaces the previous value rather than producing a second device.

Test Case: Bus reaches OP before traffic accepted TEST_0203
status: open
verifies: REQ_0313

Mock-frame test: start an EthercatGateway against the raw-frame mock; before the mock acknowledges the SAFE-OP → OP transition, ChannelWriter::send from the plugin side returns ConnectorError::NotReady. After the mock signals OP, the same send completes successfully.

Test Case: Static PDO map accepted from options TEST_0204
status: open
verifies: REQ_0314

Unit test that EthercatConnectorOptions::with_pdo_mapping accepts a static description per SubDevice (RxPDO / TxPDO entries) and that the in-memory representation preserves entry order and bit-offset values. Mismatched declarations (bit length exceeds SubDevice capacity) are rejected at builder time.

Test Case: PDO mapping applied during PRE-OP to SAFE-OP TEST_0205
status: open
verifies: REQ_0315

Mock-frame test observing the SDO write sequence during bring-up: the gateway emits writes to 0x1C12 (RxPDO) and 0x1C13 (TxPDO) before the SAFE-OP transition is requested. The exact sub-index sequence matches the configured PDO mapping.

Test Case: Cycle time configurable TEST_0206
status: open
verifies: REQ_0316

Unit test that EthercatConnectorOptions::cycle_time accepts Duration values from 1 ms upward; the default is 2 ms; values below 1 ms are rejected at builder time. The configured value is observable on the gateway’s metadata accessor.

Test Case: Missed ticks are skipped not queued TEST_0207
status: open
verifies: REQ_0317

Mock-frame test: stall the gateway’s tokio sidecar for 5 cycles by holding a mutex on the bridge. After release, exactly one tx_rx cycle runs (not five) — the skipped ticks are dropped, not queued for catch-up. The asserted behaviour matches tokio::time::MissedTickBehavior::Skip.

Test Case: Distributed Clocks bring-up is opt-in TEST_0208
status: open
verifies: REQ_0318

Two mock-frame scenarios. (1) Default options: the gateway completes bring-up without emitting any BWR to 0x0900, FRMW to 0x0910, or write to 0x0920. (2) distributed_clocks: true: the DC register sequence appears between PRE-OP and SAFE-OP exactly once per bring-up.

Test Case: Up requires OP and matching working counter TEST_0209
status: open
verifies: REQ_0319

Mock-frame test: drive the gateway to OP with the mock reporting the expected WKC on every cycle for 10 consecutive cycles; Connector::health() returns ConnectorHealth::Up. Inject a single low WKC cycle and immediately query health; Up is no longer reported.

Test Case: Working-counter mismatch transitions to Degraded TEST_0210
status: open
verifies: REQ_0320

Mock-frame test: configure a degradation threshold of N=3 cycles; inject N consecutive cycles with WKC below the expected value; the gateway transitions to ConnectorHealth::Degraded and emits exactly one HealthEvent::Degraded with a reason naming the offending cycle count.

Test Case: Tokio sidecar contained inside connector crate TEST_0211
status: open
verifies: REQ_0321

Structural test using cargo tree: assert that taktora-executor does not appear with tokio in its transitive dependency closure, and that tokio appears only under taktora-connector-ethercat (and other connector crates). A second assertion: the published EthercatConnector plugin surface contains no tokio:: types.

Test Case: Bridge channels are bounded with configurable capacity TEST_0212
status: open
verifies: REQ_0322

Unit test that EthercatConnectorOptions::outbound_capacity and inbound_capacity produce bridges with exactly the configured number of slots. After filling the channel, further non-blocking sends return Full rather than allocating additional capacity.

Test Case: Outbound bridge saturation surfaces as BackPressure TEST_0213
status: open
verifies: REQ_0323

Mock-frame test: configure a tiny outbound-bridge capacity. Stall the tokio sidecar from draining. The plugin’s ChannelWriter::send returns ConnectorError::BackPressure and the gateway reports ConnectorHealth::Degraded until the bridge drains.

Test Case: Inbound bridge saturation surfaces as DroppedInbound TEST_0214
status: open
verifies: REQ_0324

Mock-frame test: configure a tiny inbound-bridge capacity. Block the inbound gateway item from draining. Drive a flood of inbound process-image updates through the mock; the gateway emits one or more HealthEvent::DroppedInbound { count } and the inbound image for affected cycles is dropped (not buffered).

Test Case: Gateway opens raw socket on Linux with CAP_NET_RAW TEST_0215
status: open
verifies: REQ_0325

Hardware-bench test [bench]: on a Linux CI host with CAP_NET_RAW granted to the test binary, the gateway opens the configured NIC via ethercrab::std::tx_rx_task and reports Up. Companion negative test (also Linux): when CAP_NET_RAW is absent, gateway startup fails with a permission error before any EtherCAT frame is sent.

Test Case: PDI bit-slice byte-aligned round-trip TEST_0216
status: open
verifies: REQ_0326, REQ_0327

Pure-logic test of the pdi module. For a representative set of (bit_offset, bit_length) pairs where both endpoints are byte-aligned (bit_offset % 8 == 0, bit_length % 8 == 0), the round-trip write_routing(buf, routing, value); read_routing(buf, routing) shall yield the original value byte-for-byte, with no modification to PDI bytes outside the slice. Property test via proptest over slice positions, lengths, and pre-existing buffer contents.

Test Case: PDI bit-slice unaligned round-trip TEST_0217
status: open
verifies: REQ_0326, REQ_0327

Property test for the same round-trip as PDI bit-slice byte-aligned ... (TEST_0216) but covering bit_offset and bit_length values that are not multiples of 8. Verifies that read-modify-write on partial leading / trailing bytes preserves the unaffected bits exactly (no spillover into adjacent slices).

Test Case: Adjacent PDI bit slices do not interfere TEST_0218
status: open
verifies: REQ_0326

Construct two EthercatRouting declarations whose bit slices are adjacent (e.g. slice A = bits 0..12, slice B = bits 12..24 on the same SubDevice / direction). Write distinct values to A and B in arbitrary order; read both back. Both reads shall return the original written values; neither write shall corrupt the other slice.

Test Case: Per-channel routing registry has stable iteration order TEST_0219
status: open
verifies: REQ_0328

When the application registers N channel descriptors in order D_1 D_N via create_writer / create_reader, the gateway’s cycle-loop iteration over the registry shall visit them in the same order on every cycle. Property test confirms the order is stable across 1 000 cycles and zero per-cycle allocations are observed via CountingAllocator.

Test Case: Outbound end-to-end (plugin send → PDI slice via mock) TEST_0220
status: open
verifies: REQ_0326, REQ_0328

With a MockBusDriver configured for a single SubDevice at address 0x0001 with an outputs buffer of N bytes, a plugin that constructs an EthercatConnector and calls create_writer for a ChannelDescriptor whose EthercatRouting selects bits [bit_offset, bit_offset + bit_length) of that SubDevice’s outputs, then invokes ChannelWriter::send(value): after the next cycle, the mock’s outputs buffer at the routing’s bit slice shall equal the codec-encoded representation of value. PDI bytes outside the routing’s slice shall remain unchanged.

Test Case: Inbound end-to-end (PDI slice via mock → plugin recv) TEST_0221
status: open
verifies: REQ_0327, REQ_0328

With a MockBusDriver preloaded with inputs bytes at a known bit slice via with_subdevice_inputs, a plugin that constructs an EthercatConnector and calls create_reader for a routing pointing at that slice: after one cycle, ChannelReader::try_recv() shall return an envelope whose decoded payload equals the value the mock’s preloaded bytes represent under the channel’s codec.

Test Case: Loopback round-trip (plugin → mock → plugin) TEST_0222
status: open
verifies: REQ_0326, REQ_0327

Compose Outbound end-to-end (plugin... (TEST_0220) and Inbound end-to-end (PDI sli... (TEST_0221) via a MockBusDriver variant that, on every cycle, copies the SubDevice’s outputs buffer over to its inputs buffer (synthetic loopback). The plugin registers paired Rx and Tx channels pointing at the same bit slice and a fresh routing pair; after one cycle of ChannelWriter::send(v) + ChannelReader::try_recv(), the received value equals v byte-for-byte. Verifies end-to-end iceoryx2 ↔ PDI ↔ iceoryx2 plumbing without hardware.


Workspace end-to-end tests

Full stack exercised via taktora-connector-host examples or assert_cmd-driven binary smoke tests.

Test Case: In-process gateway smoke TEST_0150
status: open
verifies: REQ_0241, ARCH_0020

Single-binary integration: ConnectorHost launches the plugin executor and an in-process tokio task hosting MqttGateway against a rumqttd fixture. End-to-end pub/sub round-trip succeeds; process exits cleanly on programmatic stop.

Test Case: Separate-process gateway smoke TEST_0151
status: open
verifies: REQ_0242, ARCH_0021

Two binaries: a plugin process running ConnectorHost and a gateway process running ConnectorGateway against rumqttd. SHM transport carries envelopes between them. A round-trip succeeds; both processes exit cleanly.

Test Case: SIGINT clean exit within 5-second budget TEST_0152
status: open
verifies: REQ_0243, ARCH_0013

While the connector is mid-traffic, send SIGINT; the host returns from run() within 5 seconds; tokio runtime drains; all iceoryx2 services release; exit code is 0.

Test Case: No control-plane envelopes flow TEST_0153
status: open
verifies: REQ_0244, REQ_0291

With one channel configured, observe the iceoryx2 service for the duration of a normal session: the only envelopes that flow are user-payload envelopes (no “ping”, “version”, or “shutdown handshake”). Asserts the framework’s no-control-plane invariant.


Zenoh reference connector

Layer-1 (pure-logic) tests run in default CI against MockZenohSession and require no real zenoh crate. Layer-2 (zenoh-integration feature gate) and Layer-3 (ZENOH_TEST_ROUTER env-gated client mode) tests run on dedicated CI jobs; they :status: draft until those jobs land.

Test Case: ZenohRouting field validation TEST_0300
status: open
verifies: REQ_0401

Unit test asserting that constructing a ZenohRouting with an invalid key_expr (empty, leading slash, illegal wildcard combination) fails with ConnectorError::Configuration before any iceoryx2 service is created. Asserts that valid congestion-control / priority / reliability / express combinations round-trip through ChannelDescriptor without loss.

Test Case: ZenohConnector implements Connector with ZenohRouting TEST_0301
status: open
verifies: REQ_0400

Compile-fail test ensuring ZenohConnector<JsonCodec> is accepted in any position requiring Connector<Routing = ZenohRouting>. Asserts create_writer / create_reader return the expected ChannelWriter<T, JsonCodec, N> / ChannelReader<T, JsonCodec, N> concrete types.

Test Case: Pub/sub end-to-end against MockZenohSession TEST_0302
status: open
links incoming: REQ_0445

Drive a ChannelWriter::send(value) through MockZenohSession and observe ChannelReader::try_recv receive the same value. Asserts sequence number monotonicity (Sequence number monotonical... (REQ_0202)), timestamp non-zero, and that the gateway publishes raw bytes on the inbound service (codec runs on the plugin side, per Zenoh gateway is byte-only ... (REQ_0408)).

Test Case: Query round-trip against MockZenohSession TEST_0303

End-to-end query test: plugin A calls ZenohQuerier::send(q); plugin B’s ZenohQueryable::try_recv surfaces (QueryId, Q); plugin B calls reply(id, r) three times then terminate(id); plugin A’s ZenohQuerier::try_recv observes the three replies in order followed by a 0x02 end-of-stream envelope. Asserts QueryId round-trips through the envelope’s correlation_id unchanged and that the queryable’s gateway map entry is freed after terminate.

Test Case: Codec failure paths for queries TEST_0304
status: open
verifies: REQ_0427

Encoding a value larger than the envelope’s payload returns ConnectorError::Codec from ZenohQuerier::send and from ZenohQueryable::reply; decoding malformed bytes returns ConnectorError::Codec from the matching try_recv. The envelope is not silently dropped (re-affirming Codec decode error variant (REQ_0214)).

Test Case: Outbound bridge saturation surfaces as BackPressure TEST_0305
status: open
verifies: REQ_0404, REQ_0405

With outbound_bridge_capacity = 1 and a deliberately stalled MockZenohSession, the second ChannelWriter::send (and the second ZenohQuerier::send) returns ConnectorError::BackPressure and the connector’s health() snapshot transitions to Degraded.

Test Case: Inbound bridge saturation surfaces as DroppedInbound TEST_0306
status: open
verifies: REQ_0406, REQ_0428

With inbound_bridge_capacity = 1 and a deliberately stalled plugin reader, the gateway emits HealthEvent::DroppedInbound { count = N } reflecting the number of pub/sub samples and reply chunks discarded. The in-flight QueryId is observable as a reply stream with fewer chunks than the upstream peer sent.

Test Case: Query timeout emits 0x03 terminator TEST_0307
status: open
verifies: REQ_0425

With query_timeout = 50 ms and a queryable that never replies, ZenohQuerier::try_recv observes a single envelope with payload[0] == 0x03 for the in-flight QueryId; subsequent try_recv calls for that QueryId return None. Gateway map entry for that QueryId is freed.

Test Case: Health state machine on MockZenohSession lifecycle TEST_0308
status: implemented
verifies: REQ_0440, REQ_0442
links incoming: REQ_0442

Walk the mock session through Connecting Up Degraded Up Down and assert one HealthEvent per transition on the connector’s health channel. Asserts each variant carries the documented payload (since timestamp, reason string). Realised across two test files: the Connecting Up Down legs by crates/taktora-connector-zenoh/tests/health_transitions.rs, and the Up Degraded Up legs (driven by min_peers threshold crossings) by crates/taktora-connector-zenoh/tests/min_peers_degraded.rs.

Test Case: REQ_0441 anti-req — no ReconnectPolicy on session loss TEST_0309
status: implemented
verifies: REQ_0441

Regression-guard for the explicit anti-requirement NO ReconnectPolicy on Zenoh... (REQ_0441) (which is status:rejected because the project deliberately excludes ReconnectPolicy from the Zenoh connector — see the “Anti-goals” preamble above the rejected req cluster in spec/requirements/connector.rst). The :verifies: link asserts that the excluded behaviour remains absent; rejected parent + implemented verifier is the project’s documented convention for anti-req checks.

Static check that ZenohGateway exposes no ReconnectPolicy-typed field and the ZenohConnectorOptions struct does not declare a reconnect_policy setting. Realised as crates/taktora-connector-zenoh/tests/no_reconnect_policy.rs, which shells out to cargo public-api and asserts the public surface contains no ReconnectPolicy / reconnect_policy identifier.

Test Case: zenoh-integration feature gates the real zenoh dep TEST_0310
status: implemented
verifies: REQ_0444, REQ_0445
links incoming: REQ_0444, REQ_0445

Build the crate twice — once with default features, once with --features zenoh-integration — and assert that the default build does not link zenoh (via cargo tree introspection) while the feature build does. Both builds expose MockZenohSession. Realised as scripts/check_dep_gating.sh invoked from the dep-gating job in .github/workflows/ci-zenoh.yml.

Test Case: Cross-platform support TEST_0311
status: implemented
verifies: REQ_0446
links incoming: REQ_0446

CI matrix builds the crate on Linux, macOS, and Windows (default features) and runs cargo test on all three. No platform-specific compile errors; no platform-gated #[cfg] paths break. Realised as the build-test-default matrix job in .github/workflows/ci-zenoh.yml.

Test Case: Two-peer real session pub/sub TEST_0312
status: draft
verifies: REQ_0440, REQ_0443

Layer-2 integration test (zenoh-integration feature): spawn two ZenohConnector instances in peer mode in the same process with disjoint locator listings, exchange envelopes through a real zenoh::Session pair, and assert payloads / sequence numbers round-trip correctly. Status remains deferred until the dedicated CI job lands.

Test Case: Client-mode router smoke TEST_0313
status: draft
verifies: REQ_0440, REQ_0443

Layer-3 env-gated test: when ZENOH_TEST_ROUTER names a reachable zenohd, open a client-mode session and assert ConnectorHealth reaches Up. Status remains deferred until the client-mode CI job lands.

Test Case: Tokio sidecar contained inside taktora-connector-zenoh TEST_0314
status: implemented
verifies: REQ_0403
links incoming: REQ_0403

Static check that the zenoh::Session and any tokio runtime handle live entirely inside the taktora-connector-zenoh crate. No public type exported by taktora-connector-zenoh shall name a tokio::* type in its signature (compile-time API surface scan). Realised as crates/taktora-connector-zenoh/tests/tokio_containment.rs, which shells out to cargo public-api and asserts the public surface contains no tokio:: identifier. The runtime piece — an executor unit test asserting that the WaitSet thread does not contain any tokio task handle attributable to the gateway sidecar (mirrors Tokio sidecar contained ins... (TEST_0211) under Tokio sidecar contained ins... (REQ_0321)) — is deferred to a Z6+ stage that lands taktora-executor task introspection.


CAN (SocketCAN) reference connector

Verification artefacts for the CAN reference connector. Layer-1 (pure-logic) cases use MockCanInterface; layer-2 cases gated on socketcan-integration require a Linux host with the vcan kernel module loaded (modprobe vcan && ip link add dev vcan0 type vcan && ip link set up vcan0).

Test Case: CanConnector trait surface TEST_0500
status: open
verifies: REQ_0600

Compile-time API surface check that CanConnector<C> implements the framework Connector trait with type Routing = CanRouting and type Codec = C. Asserts create_writer<T> and create_reader<T> return the framework’s concrete ChannelWriter<T, C, N> / ChannelReader<T, C, N> (not boxed trait objects, mirroring create_writer / create_read... (REQ_0223)). Realised as crates/taktora-connector-can/tests/connector_surface.rs.

Test Case: CanRouting field round-trip TEST_0501
status: open
verifies: REQ_0601, REQ_0615

Property test (proptest) generating arbitrary CanRouting { iface, can_id, mask, kind, fd_flags } and asserting clone / equality / debug round-trip. Asserts that CanId::standard(0x123) and CanId::extended(0x123) are distinct values (the extended flag is part of identity) and that 11-bit standard IDs reject construction above 0x7FF, 29-bit extended IDs reject construction above 0x1FFF_FFFF.

Test Case: Classical CAN round-trip via MockCanInterface TEST_0502
status: open

Layer-1 end-to-end test: CanConnector over MockCanInterface with one outbound and one inbound channel on a single mock iface, both CanFrameKind::Classical. Send 100 envelopes carrying 1–8 byte payloads with both standard and extended IDs; assert each is delivered byte-for-byte to the inbound reader and that CanFrame DLC matches the encoded payload length.

Test Case: CAN-FD round-trip via MockCanInterface TEST_0503
status: open
verifies: REQ_0611, REQ_0613

As Classical CAN round-trip vi... (TEST_0502) but with CanFrameKind::Fd channels carrying payloads at the FD DLC steps (8, 12, 16, 20, 24, 32, 48, 64 bytes). Asserts BRS and ESI flags from CanRouting::fd_flags are preserved on the mock interface and surface on the receiving side.

Test Case: Per-iface filter union TEST_0504
status: open
verifies: REQ_0622, REQ_0623

Open three inbound readers on the same mock iface with distinct (can_id, mask) pairs; assert that BB_0074’s filter compiler emits exactly three can_filter entries whose union covers all three readers. Drop one reader and assert the filter is recomputed and re-applied without reopening the underlying interface (mock interface records apply_filter calls).

Test Case: Multi-iface inbound demux TEST_0505
status: open

Gateway owns two mock ifaces (vcan0, vcan1). Open one reader on vcan0 for can_id = 0x100 and two readers on vcan1 for (0x200, mask=0x7F0) (overlapping). Inject frames on each iface and assert: vcan0 reader sees only its own frame; both vcan1 readers see their matching frames; readers never see frames from the other iface; the two overlapping vcan1 readers each receive their own envelope copy when an ID lands in their intersection.

Test Case: Bus-off → Down → ReconnectPolicy reopen TEST_0506
status: open
verifies: REQ_0633, REQ_0634

Drive MockCanInterface through Connecting Up then inject a bus-off error frame. Assert: the affected iface transitions to Down, its socket is closed, ReconnectPolicy::next_delay() is consulted, a reopen attempt is scheduled, the filter is re-applied (Filter recomputed on channe... (REQ_0623)), and the sub-state walks back through Connecting Up. Uses a deterministic stub ReconnectPolicy returning fixed 1 ms delays so the test runs without sleeping.

Test Case: error-passive → Degraded → recovery TEST_0507
status: open

Two-iface gateway. Inject an error-passive error frame on vcan0 while vcan1 remains Up. Assert: vcan0’s sub-state transitions to Degraded; the connector’s aggregated ConnectorHealth surfaces as Degraded per ConnectorHealth aggregates ... (REQ_0630); one HealthEvent is emitted carrying DegradedReason::ErrorPassive { iface: "vcan0" }. After the configured recovery_window elapses with no further error frames, the sub-state returns to Up and a second HealthEvent is emitted.

Test Case: Tokio sidecar contained inside taktora-connector-can TEST_0508
status: open
verifies: REQ_0605

Static check that the SocketCAN sockets and any tokio runtime handle live entirely inside the taktora-connector-can crate. No public type exported by taktora-connector-can shall name a tokio::* or socketcan::* type in its signature (compile-time API surface scan). Realised as crates/taktora-connector-can/tests/tokio_containment.rs, shelling out to cargo public-api and asserting absence of tokio:: and (when socketcan-integration is enabled) socketcan:: identifiers in the public surface. Mirrors Tokio sidecar contained ins... (TEST_0314).

Test Case: Outbound bridge saturation surfaces as BackPressure TEST_0509
status: open
verifies: REQ_0606, REQ_0607

With outbound_bridge_capacity = 1 and a deliberately stalled MockCanInterface (TX never drains), the second ChannelWriter::send returns ConnectorError::BackPressure and the connector’s health() snapshot transitions to Degraded.

Test Case: Inbound bridge saturation surfaces as DroppedInbound TEST_0510
status: open
verifies: REQ_0606, REQ_0608

With inbound_bridge_capacity = 1 and a deliberately stalled plugin reader, the gateway emits HealthEvent::DroppedInbound { count = N } reflecting the number of CAN frames discarded. Subsequent reader drains resume normally.

Test Case: socketcan-integration feature gates the real socketcan dep TEST_0511
status: implemented
verifies: REQ_0603, REQ_0604
links outgoing: BB_0070, IMPL_0080

Build the crate twice — once with default features, once with --features socketcan-integration — and assert that the default build does not link socketcan (via cargo tree introspection) while the feature build does. Both builds expose MockCanInterface. Realised as scripts/check_dep_gating_can.sh invoked from the dep-gating job in .github/workflows/ci-can.yml. The feature-build assertion runs on Linux only — the socketcan dep is declared under a cfg(target_os = "linux") target table per quick-xml + serde backend (REQ_0502), so non-Linux hosts legitimately omit it; the script reports a skip notice in that case.

Test Case: Linux raw-socket smoke against vcan0 TEST_0512
status: implemented
links outgoing: BB_0070, IMPL_0080

Layer-2 integration test (socketcan-integration feature, Linux only): require vcan0 present (modprobe vcan && ip link add dev vcan0 type vcan && ip link set up vcan0). Open two RealCanInterface instances bound to vcan0 (one sending, one receiving, exploiting PF_CAN raw-socket broadcast semantics), apply an accept-all filter on the rx side, send 10 classical frames carrying small distinct payloads, and assert each round-trips byte-for-byte with the correct CAN identifier and extended flag. Realised as crates/taktora-connector-can/tests/vcan_smoke.rs, gated #[ignore] so plain cargo test skips it; the vcan-smoke job in .github/workflows/ci-can.yml runs it with --include-ignored after bringing up vcan0.

Test Case: Error frames not exposed to plugin TEST_0513
status: open

Regression-guard for the explicit anti-requirement NO plugin-visible error-fra... (REQ_0643). Inject error frames of every classified kind (error-warning, error-passive, bus-off) via MockCanInterface and assert no ChannelReader<T> on the plugin side observes a Received<T> for any of them. Health-channel observation is the only surface.

Test Case: Per-iface routing registry has stable iteration order TEST_0514
status: open
verifies: REQ_0625

Add 8 channels to the same mock iface in a known order; assert the RX dispatch loop and the TX drain loop iterate them in that insertion order on every cycle. Assert no per-cycle heap allocation: instrument a CountingAllocator and assert zero allocations from the RX/TX hot path over 1000 iterations (mirrors Per-channel routing registr... (TEST_0219)).


Loom concurrency tests

Run with cargo test --features loom under cfg(loom).

Test Case: Bridge handoff under arbitrary interleaving TEST_0160
status: open
verifies: REQ_0259, BB_0022

Loom model of OutboundGatewayItem.execute racing with the tokio task draining the bridge: every produced frame is observed exactly once by the consumer; no deadlock.

Test Case: Health state-machine under concurrent updates TEST_0161
status: open
verifies: REQ_0230, REQ_0234

Loom model with multiple threads attempting transitions simultaneously (e.g. the tokio task reporting Down while the reconnect timer fires Connecting): the state machine never enters an invalid state and no event is dropped.


Cross-cutting traceability

Used filter: types(test)

ID

Title

Status

Verifies

TEST_0100

ExponentialBackoff invariants

open

REQ_0233

TEST_0101

ConnectorHealth state-machine transitions

open

REQ_0230; REQ_0234

TEST_0102

MqttRouting wildcard demux predicate

open

REQ_0254

TEST_0103

ChannelDescriptor validation

open

REQ_0201; REQ_0221

TEST_0110

JsonCodec round-trip property test

open

REQ_0210; REQ_0212

TEST_0111

Codec encode error on undersized buffer

open

REQ_0213

TEST_0112

Codec decode error propagation

open

REQ_0214

TEST_0120

ChannelWriter → ChannelReader round-trip

open

REQ_0205; REQ_0223

TEST_0121

Sequence-number monotonicity

open

REQ_0202

TEST_0122

Timestamp populated at send

open

REQ_0203

TEST_0123

Correlation ID round-trip

open

REQ_0204

TEST_0124

Per-channel size — 4 KB, 64 KB, 1 MB

open

REQ_0201; BB_0010

TEST_0125

Payload-overflow rejection

open

REQ_0201

TEST_0126

Service naming derived from descriptor

open

REQ_0206; BB_0011

TEST_0130

QoS 0 round-trip

open

REQ_0252

TEST_0131

QoS 1 round-trip

open

REQ_0252

TEST_0132

Retained-message publish + subscribe

open

REQ_0253

TEST_0133

Wildcard subscription with `+`

open

REQ_0254

TEST_0134

Wildcard subscription with `#`

open

REQ_0254

TEST_0135

Username/password authentication

open

REQ_0255

TEST_0136

TLS connection (developer-machine only)

open

REQ_0256

TEST_0137

Reconnect after broker bounce

open

REQ_0232; REQ_0233

TEST_0138

HealthEvent emitted on every transition

open

REQ_0234

TEST_0139

Outbound bridge saturation → BackPressure

open

REQ_0260

TEST_0140

Inbound bridge saturation → DroppedInbound

open

REQ_0261

TEST_0150

In-process gateway smoke

open

REQ_0241; ARCH_0020

TEST_0151

Separate-process gateway smoke

open

REQ_0242; ARCH_0021

TEST_0152

SIGINT clean exit within 5-second budget

open

REQ_0243; ARCH_0013

TEST_0153

No control-plane envelopes flow

open

REQ_0244; REQ_0291

TEST_0160

Bridge handoff under arbitrary interleaving

open

REQ_0259; BB_0022

TEST_0161

Health state-machine under concurrent updates

open

REQ_0230; REQ_0234

TEST_0170

Zero allocations in steady-state dispatch

open

REQ_0060

TEST_0180

Cap exhaustion and oversize alloc both fail-closed

open

REQ_0300; REQ_0301

TEST_0181

Steady-state cap behaviour under burst

open

REQ_0300

TEST_0182

lock() then alloc panics

open

REQ_0302

TEST_0183

Counter accuracy

open

REQ_0303

TEST_0184

Concurrent alloc/dealloc safety smoke

open

REQ_0304

TEST_0190

Histogram percentile accuracy

open

REQ_0100

TEST_0191

Per-task max jitter under synthetic period violation

open

REQ_0101

TEST_0192

Overrun counter increments exactly per overrun cycle

open

REQ_0102

TEST_0193

Push and pull stat paths agree

open

REQ_0103

TEST_0194

Allocation-free telemetry update

open

REQ_0104

TEST_0200

EthercatConnector trait surface

open

REQ_0310

TEST_0201

EthercatRouting field round-trip

open

REQ_0311

TEST_0202

Single MainDevice per gateway instance

open

REQ_0312

TEST_0203

Bus reaches OP before traffic accepted

open

REQ_0313

TEST_0204

Static PDO map accepted from options

open

REQ_0314

TEST_0205

PDO mapping applied during PRE-OP to SAFE-OP

open

REQ_0315

TEST_0206

Cycle time configurable

open

REQ_0316

TEST_0207

Missed ticks are skipped not queued

open

REQ_0317

TEST_0208

Distributed Clocks bring-up is opt-in

open

REQ_0318

TEST_0209

Up requires OP and matching working counter

open

REQ_0319

TEST_0210

Working-counter mismatch transitions to Degraded

open

REQ_0320

TEST_0211

Tokio sidecar contained inside connector crate

open

REQ_0321

TEST_0212

Bridge channels are bounded with configurable capacity

open

REQ_0322

TEST_0213

Outbound bridge saturation surfaces as BackPressure

open

REQ_0323

TEST_0214

Inbound bridge saturation surfaces as DroppedInbound

open

REQ_0324

TEST_0215

Gateway opens raw socket on Linux with CAP_NET_RAW

open

REQ_0325

TEST_0216

PDI bit-slice byte-aligned round-trip

open

REQ_0326; REQ_0327

TEST_0217

PDI bit-slice unaligned round-trip

open

REQ_0326; REQ_0327

TEST_0218

Adjacent PDI bit slices do not interfere

open

REQ_0326

TEST_0219

Per-channel routing registry has stable iteration order

open

REQ_0328

TEST_0220

Outbound end-to-end (plugin send → PDI slice via mock)

open

REQ_0326; REQ_0328

TEST_0221

Inbound end-to-end (PDI slice via mock → plugin recv)

open

REQ_0327; REQ_0328

TEST_0222

Loopback round-trip (plugin → mock → plugin)

open

REQ_0326; REQ_0327

TEST_0240

Harness builds and runs on Linux non-RT

open

REQ_0111

TEST_0241

NDJSON schema validation

open

REQ_0111

TEST_0242

Harness telemetry agrees with stats_snapshot

open

REQ_0113

TEST_0300

ZenohRouting field validation

open

REQ_0401

TEST_0301

ZenohConnector implements Connector with ZenohRouting

open

REQ_0400

TEST_0302

Pub/sub end-to-end against MockZenohSession

open

REQ_0402; REQ_0407; REQ_0408; REQ_0445

TEST_0303

Query round-trip against MockZenohSession

open

REQ_0420; REQ_0421; REQ_0422; REQ_0423; REQ_0424; REQ_0426; REQ_0427

TEST_0304

Codec failure paths for queries

open

REQ_0427

TEST_0305

Outbound bridge saturation surfaces as BackPressure

open

REQ_0404; REQ_0405

TEST_0306

Inbound bridge saturation surfaces as DroppedInbound

open

REQ_0406; REQ_0428

TEST_0307

Query timeout emits 0x03 terminator

open

REQ_0425

TEST_0308

Health state machine on MockZenohSession lifecycle

implemented

REQ_0440; REQ_0442

TEST_0309

REQ_0441 anti-req — no ReconnectPolicy on session loss

implemented

REQ_0441

TEST_0310

zenoh-integration feature gates the real zenoh dep

implemented

REQ_0444; REQ_0445

TEST_0311

Cross-platform support

implemented

REQ_0446

TEST_0312

Two-peer real session pub/sub

draft

REQ_0440; REQ_0443

TEST_0313

Client-mode router smoke

draft

REQ_0440; REQ_0443

TEST_0314

Tokio sidecar contained inside taktora-connector-zenoh

implemented

REQ_0403

TEST_0400

parse() accepts a representative Beckhoff EL3001 ESI

open

REQ_0500; REQ_0504

TEST_0401

Parser compiles under no_std + alloc

open

REQ_0501

TEST_0402

Parser is independent of ethercrab

open

REQ_0503

TEST_0403

Vendor-specific elements survive as RawXml

open

REQ_0505

TEST_0404

Parse errors carry line and column

open

REQ_0506

TEST_0410

Name sanitisation handles ESI naming edge cases

open

REQ_0511

TEST_0411

Revision collision produces distinct idents

open

REQ_0512

TEST_0412

PDO entry dedup collapses structurally identical layouts

open

REQ_0513

TEST_0413

TokenStream emission, not string formatting

open

REQ_0514

TEST_0420

EL3001 backend output snapshot

open

REQ_0521; REQ_0522; REQ_0523; REQ_0524

TEST_0421

Generated registry covers every emitted device

open

REQ_0525

TEST_0422

Generated module compiles under no_std + alloc

open

REQ_0526

TEST_0423

Backend is the sole ethercrab consumer in the toolchain

open

REQ_0520

TEST_0424

Object-dictionary emission gated by feature flag

open

REQ_0533

TEST_0430

EsiDevice trait shape compiles for a hand-written device

open

REQ_0530

TEST_0431

EsiConfigurable async trait shape compiles

open

REQ_0531

TEST_0432

ethercat-esi-rt is the trait home, not taktora-internal

open

REQ_0532

TEST_0440

Builder writes a parseable Rust file to OUT_DIR

open

REQ_0540; REQ_0541

TEST_0441

cargo rerun-if-changed emitted per ESI input

open

REQ_0542

TEST_0442

Output passes prettyplease formatting

open

REQ_0543

TEST_0450

cargo esi expand emits a single device's code

open

REQ_0550

TEST_0451

cargo esi list enumerates devices

open

REQ_0551

TEST_0452

CLI output matches build helper output byte-for-byte

open

REQ_0552

TEST_0460

Verifier passes on matching ESI + SII pair

open

REQ_0560

TEST_0461

Verifier reports the differing field

open

REQ_0561

TEST_0462

Verifier reuses ethercat-esi parser

open

REQ_0562

TEST_0463

Verifier exit codes follow the documented matrix

open

REQ_0563

TEST_0470

Repeated codegen runs produce byte-identical output

open

QG_0010; REQ_0543

TEST_0471

Input-file ordering does not affect output

open

QG_0010; REQ_0512; REQ_0513

TEST_0472

Layering integrity check (Cargo.toml audit)

open

QG_0011; REQ_0503; REQ_0520