mctp/server: add mock-transport test suite
Add a comprehensive set of integration and unit tests for the MCTP server
that run without the I2C transport layer. A BufferSender mock replaces
the real I2C sender; no transport hardware or system linker is required.
New files:
- tests/common/mod.rs -- shared fixtures: BufferSender, DroppingBufferSender,
DirectClient, DirectListener, DirectRespChannel,
DirectReqChannel, transfer()
- tests/server_unit.rs -- unit tests for Server methods: EID management,
handle alloc/free, try_recv, inbound routing,
oversized payload, register_recv + timeout
- tests/integration.rs -- multi-fragment reassembly, concurrent listeners,
echo via MctpListener+MctpRespChannel traits,
MctpReqChannel, drop_handle mid-flight,
response EID/tag threading
Updated files:
- tests/echo.rs -- refactored to use common/ (no duplicated fixtures)
- tests/dispatch.rs -- refactored to use common/; added edge-case tests:
malformed request, unknown opcode, Recv with no
message, Unbind
- BUILD.bazel -- four new rust_test targets, one per test file
- README.md -- document new test layout and Bazel invocations
All four Bazel test targets pass:
bazel test //services/mctp/server:mctp_server_{echo,dispatch,unit,integration}_test
diff --git a/.claude/mctp-test-plan.md b/.claude/mctp-test-plan.md
new file mode 100644
index 0000000..d09d9c4
--- /dev/null
+++ b/.claude/mctp-test-plan.md
@@ -0,0 +1,80 @@
+# MCTP Server Test Plan (without I2C transport)
+
+Branch: ocp-emea-demo
+
+## Core Insight
+`Server<S: Sender, const N>` is generic over `mctp_lib::Sender`.
+I2C transport is just one `Sender` impl + a caller of `server.inbound()`.
+No mocking framework, feature flags, or cfg(test) shims needed.
+
+---
+
+## File Layout
+
+```
+services/mctp/server/tests/
+├── common/
+│ └── mod.rs ← shared BufferSender, transfer(), DirectClient
+├── echo.rs ← already exists; refactor to use common/
+├── dispatch.rs ← already exists; add missing cases
+├── server_unit.rs ← NEW: Layer 2 unit tests
+└── integration.rs ← NEW: Layer 4 multi-fragment / concurrency
+```
+
+---
+
+## Layer 1 — Shared Test Fixtures (common/mod.rs)
+
+- [ ] Extract `BufferSender<'_>` from echo.rs and dispatch.rs into `tests/common/mod.rs`
+- [ ] Add `DroppingBufferSender` (discards writes, always returns Ok) for tests that only care about inbound routing
+- [ ] Extract `transfer(from, to)` helper into common
+- [ ] Extract `DirectClient<'a, S, N>` into common (wraps &RefCell<Server> as MctpClient)
+
+---
+
+## Layer 2 — Server Unit Tests (server_unit.rs)
+
+- [ ] `req()` + `unbind()` — handle allocation/deallocation
+- [ ] `listener()` duplicate msg_type — expect AlreadyBound error
+- [ ] `try_recv()` before any `inbound()` — returns None
+- [ ] `inbound(raw_pkt)` + `try_recv()` — full routing path (use mctp_lib::fragment::Fragmenter to build raw pkts)
+- [ ] `register_recv()` + `update(now + timeout)` — timeout fires RecvResult::TimedOut
+- [ ] `set_eid()` / `get_eid()` — EID round-trip
+- [ ] `send()` with payload > MAX_PAYLOAD — expect NoSpace error
+
+---
+
+## Layer 3 — Dispatch Unit Tests (dispatch.rs additions)
+
+- [ ] Malformed wire request → BadArgument
+- [ ] `MctpOp::Send` via response path (no handle, HAS_EID flag, explicit tag)
+- [ ] `MctpOp::Unbind` for never-allocated handle → error
+- [ ] `MctpOp::Recv` when no message ready → TimedOut (gap noted in code comment)
+
+---
+
+## Layer 4 — Integration Tests (integration.rs)
+
+- [ ] Multi-fragment roundtrip: set `get_mtu()=64`, send 200-byte payload, verify reassembly
+- [ ] Multiple concurrent listeners: two msg_type values, cross-deliver, verify no cross-talk
+- [ ] Response-without-handle: verify tag & EID threading through echo
+- [ ] Interleaved requests from two senders: tag collision avoidance
+
+---
+
+## Layer 5 — MctpClient Trait Tests (via DirectClient in echo.rs)
+
+- [ ] `MctpListener::recv()` — called after inbound, returns payload
+- [ ] `MctpReqChannel::send()` + `recv()` — full request-response cycle
+- [ ] `drop_handle` mid-flight — verify outstanding entry is cleared
+
+---
+
+## Out of Scope (belongs in other crates)
+
+| Concern | Owner crate |
+|---|---|
+| MCTP-over-I2C framing/PEC | openprot-mctp-transport-i2c |
+| MctpI2cReceiver::decode | openprot-mctp-transport-i2c |
+| I2cClientBlocking mock | i2c service tests |
+| IpcI2cClient / handle::I2C wiring | target/platform integration |
diff --git a/services/mctp/server/BUILD.bazel b/services/mctp/server/BUILD.bazel
index 7a6098d..027a423 100644
--- a/services/mctp/server/BUILD.bazel
+++ b/services/mctp/server/BUILD.bazel
@@ -26,6 +26,74 @@
crate = ":mctp_server_lib",
)
+# Integration tests — each tests/ file is its own Bazel test target.
+# All share tests/common/mod.rs for fixtures (BufferSender, DirectClient, etc.).
+# No I2C transport dependency; the mock Sender replaces it entirely.
+
+rust_test(
+ name = "mctp_server_echo_test",
+ srcs = [
+ "tests/echo.rs",
+ "tests/common/mod.rs",
+ ],
+ crate_root = "tests/echo.rs",
+ edition = "2024",
+ deps = [
+ ":mctp_server_lib",
+ "//services/mctp/api:mctp_api",
+ "@rust_crates//:mctp",
+ "@rust_crates//:mctp-lib",
+ ],
+)
+
+rust_test(
+ name = "mctp_server_dispatch_test",
+ srcs = [
+ "tests/dispatch.rs",
+ "tests/common/mod.rs",
+ ],
+ crate_root = "tests/dispatch.rs",
+ edition = "2024",
+ deps = [
+ ":mctp_server_lib",
+ "//services/mctp/api:mctp_api",
+ "@rust_crates//:mctp",
+ "@rust_crates//:mctp-lib",
+ ],
+)
+
+rust_test(
+ name = "mctp_server_unit_test",
+ srcs = [
+ "tests/server_unit.rs",
+ "tests/common/mod.rs",
+ ],
+ crate_root = "tests/server_unit.rs",
+ edition = "2024",
+ deps = [
+ ":mctp_server_lib",
+ "//services/mctp/api:mctp_api",
+ "@rust_crates//:mctp",
+ "@rust_crates//:mctp-lib",
+ ],
+)
+
+rust_test(
+ name = "mctp_server_integration_test",
+ srcs = [
+ "tests/integration.rs",
+ "tests/common/mod.rs",
+ ],
+ crate_root = "tests/integration.rs",
+ edition = "2024",
+ deps = [
+ ":mctp_server_lib",
+ "//services/mctp/api:mctp_api",
+ "@rust_crates//:mctp",
+ "@rust_crates//:mctp-lib",
+ ],
+)
+
rust_binary(
name = "mctp_server",
srcs = ["src/main.rs"],
diff --git a/services/mctp/server/README.md b/services/mctp/server/README.md
index c6dac68..2cfed72 100644
--- a/services/mctp/server/README.md
+++ b/services/mctp/server/README.md
@@ -84,47 +84,34 @@
## Tests
-The `tests/` directory contains two integration tests that run on the host (std):
+The `tests/` directory contains integration tests that run on the host (std) with **no I2C transport** — a `BufferSender` mock replaces it entirely.
+
+### Test files
| File | What it tests |
|------|---------------|
-| `tests/echo.rs` | Full MCTP echo round-trip: server A listens, server B sends a request, verifies the echoed response via a mock `BufferSender` transport. |
-| `tests/dispatch.rs` | IPC wire-protocol dispatch: encodes a request with `wire::encode_*`, calls `dispatch_mctp_op`, and verifies the decoded response. |
+| `tests/common/mod.rs` | Shared fixtures: `BufferSender`, `DroppingBufferSender`, `DirectClient`, `DirectListener`, `DirectRespChannel`, `DirectReqChannel` |
+| `tests/echo.rs` | Full MCTP echo round-trip via `MctpClient` trait |
+| `tests/dispatch.rs` | IPC wire-protocol dispatch: encodes requests, calls `dispatch_mctp_op`, verifies responses. Includes edge cases: malformed request, unknown opcode, `Recv` with no message, `Unbind` |
+| `tests/server_unit.rs` | Unit tests for `Server` methods: EID management, handle allocation, `try_recv` before/after `inbound`, oversized payload, timeout via `register_recv` + `update` |
+| `tests/integration.rs` | Multi-fragment reassembly, multiple concurrent listeners (no cross-talk), echo via `MctpListener` + `MctpRespChannel` traits, `MctpReqChannel` trait, `drop_handle` mid-flight, response EID/tag threading |
-### Running with Bazel (primary)
+The echo via `MctpListener` + `MctpRespChannel` test (`echo_via_mctplistener_trait`) is the key one: it exercises the exact interface the real echo application uses, with `BufferSender` as the only transport.
-This is a Bazel project. Use these commands from the workspace root:
+### Running with Bazel
```sh
-# Run both integration tests via the mctp_server_test rule
-bazel test //services/mctp/server:mctp_server_test
-
-# Run alongside the API tests
-bazel test //services/mctp/server:mctp_server_test //services/mctp/api:mctp_api_test
+# Run all test targets
+bazel test //services/mctp/server:mctp_server_echo_test \
+ //services/mctp/server:mctp_server_dispatch_test \
+ //services/mctp/server:mctp_server_unit_test \
+ //services/mctp/server:mctp_server_integration_test
# Show test output
-bazel test //services/mctp/server:mctp_server_test --test_output=all
+bazel test //services/mctp/server:mctp_server_unit_test --test_output=all
```
> **Note:** Do not use the `//services/mctp/...` wildcard — it will also pick up
> the `mctp_server` and `mctp_echo` kernel binaries, which require `kernel_config`
-> (generated only during a full system image build). Target the test rule directly
+> (generated only during a full system image build). Target the test rules directly
> as shown above.
-
-### Running with Cargo (host-only convenience)
-
-Because the integration tests are `std`-only, they can also be run with Cargo for quick iteration without a full Bazel setup:
-
-```sh
-# Run all tests for this crate
-cargo test -p openprot-mctp-server
-
-# Run only the echo test
-cargo test -p openprot-mctp-server --test echo
-
-# Run only the dispatch test
-cargo test -p openprot-mctp-server --test dispatch
-
-# Show stdout from passing tests
-cargo test -p openprot-mctp-server -- --nocapture
-```
diff --git a/services/mctp/server/tests/common/mod.rs b/services/mctp/server/tests/common/mod.rs
new file mode 100644
index 0000000..19ad95e
--- /dev/null
+++ b/services/mctp/server/tests/common/mod.rs
@@ -0,0 +1,358 @@
+// Licensed under the Apache-2.0 license
+
+//! Shared test fixtures for MCTP server integration tests.
+//!
+//! Provides:
+//! - [`BufferSender`] — captures outbound packets into a `Vec` (no I2C)
+//! - [`DroppingBufferSender`] — discards all outbound packets (for inbound-only tests)
+//! - [`transfer`] — drains one server's outbound buffer into another's inbound path
+//! - [`DirectClient`] — implements `MctpClient` directly against a `Server` (no IPC)
+//! - [`DirectListener`] — implements `MctpListener` via a `DirectClient`
+//! - [`DirectRespChannel`] — implements `MctpRespChannel` via a `DirectClient`
+//! - [`DirectReqChannel`] — implements `MctpReqChannel` via a `DirectClient`
+
+// Each integration test file is its own crate in Bazel. Not every file uses
+// every fixture, so suppress dead-code warnings for the shared module.
+#![allow(dead_code)]
+
+use std::cell::RefCell;
+
+use mctp::{Eid, Tag};
+use mctp_lib::fragment::{Fragmenter, SendOutput};
+use mctp_lib::Sender;
+use openprot_mctp_api::{
+ Handle, MctpClient, MctpError, MctpListener, MctpReqChannel, MctpRespChannel, RecvMetadata,
+ ResponseCode,
+};
+use openprot_mctp_server::Server;
+
+// ---------------------------------------------------------------------------
+// BufferSender
+// ---------------------------------------------------------------------------
+
+/// A mock [`Sender`] that captures every outbound MCTP packet into a shared buffer.
+///
+/// Use [`transfer`] to drain the buffer into another server's inbound path.
+pub struct BufferSender<'a> {
+ pub packets: &'a RefCell<Vec<Vec<u8>>>,
+}
+
+impl Sender for BufferSender<'_> {
+ fn send_vectored(
+ &mut self,
+ mut fragmenter: Fragmenter,
+ payload: &[&[u8]],
+ ) -> mctp::Result<Tag> {
+ loop {
+ let mut buf = [0u8; 255];
+ match fragmenter.fragment_vectored(payload, &mut buf) {
+ SendOutput::Packet(p) => {
+ self.packets.borrow_mut().push(p.to_vec());
+ }
+ SendOutput::Complete { tag, .. } => return Ok(tag),
+ SendOutput::Error { err, .. } => return Err(err),
+ }
+ }
+ }
+
+ fn get_mtu(&self) -> usize {
+ 255
+ }
+}
+
+// ---------------------------------------------------------------------------
+// SmallMtuBufferSender
+// ---------------------------------------------------------------------------
+
+/// A [`BufferSender`] variant with a configurable (small) MTU for fragment tests.
+pub struct SmallMtuBufferSender<'a> {
+ pub packets: &'a RefCell<Vec<Vec<u8>>>,
+ pub mtu: usize,
+}
+
+impl Sender for SmallMtuBufferSender<'_> {
+ fn send_vectored(
+ &mut self,
+ mut fragmenter: Fragmenter,
+ payload: &[&[u8]],
+ ) -> mctp::Result<Tag> {
+ loop {
+ let mut buf = [0u8; 255];
+ match fragmenter.fragment_vectored(payload, &mut buf) {
+ SendOutput::Packet(p) => {
+ self.packets.borrow_mut().push(p.to_vec());
+ }
+ SendOutput::Complete { tag, .. } => return Ok(tag),
+ SendOutput::Error { err, .. } => return Err(err),
+ }
+ }
+ }
+
+ fn get_mtu(&self) -> usize {
+ self.mtu
+ }
+}
+
+// ---------------------------------------------------------------------------
+// DroppingBufferSender
+// ---------------------------------------------------------------------------
+
+/// A mock [`Sender`] that silently discards all outbound packets.
+///
+/// Use when a test only cares about the inbound path and does not need to
+/// inspect what the server would have sent out.
+pub struct DroppingBufferSender;
+
+impl Sender for DroppingBufferSender {
+ fn send_vectored(
+ &mut self,
+ mut fragmenter: Fragmenter,
+ payload: &[&[u8]],
+ ) -> mctp::Result<Tag> {
+ loop {
+ let mut buf = [0u8; 255];
+ match fragmenter.fragment_vectored(payload, &mut buf) {
+ SendOutput::Packet(_) => {}
+ SendOutput::Complete { tag, .. } => return Ok(tag),
+ SendOutput::Error { err, .. } => return Err(err),
+ }
+ }
+ }
+
+ fn get_mtu(&self) -> usize {
+ 255
+ }
+}
+
+// ---------------------------------------------------------------------------
+// transfer
+// ---------------------------------------------------------------------------
+
+/// Drain `packets` into `dest` as inbound MCTP packets.
+///
+/// Call this after the sender server has processed a send, to deliver the
+/// packets to the receiver server. The buffer is **not** cleared; call
+/// `packets.borrow_mut().clear()` manually between rounds if needed.
+pub fn transfer<S: Sender, const N: usize>(
+ packets: &RefCell<Vec<Vec<u8>>>,
+ dest: &mut Server<S, N>,
+) {
+ let pkts = packets.borrow();
+ for pkt in pkts.iter() {
+ dest.inbound(pkt).unwrap();
+ }
+}
+
+// ---------------------------------------------------------------------------
+// DirectClient
+// ---------------------------------------------------------------------------
+
+/// Implements [`MctpClient`] by calling [`Server`] methods directly (no IPC).
+///
+/// In production an IPC channel sits between the application and the MCTP
+/// server. `DirectClient` replaces that channel, allowing application code
+/// written against `MctpClient` to be exercised in pure `std` tests without
+/// any transport hardware.
+pub struct DirectClient<'a, S: Sender, const N: usize> {
+ pub server: &'a RefCell<Server<S, N>>,
+}
+
+impl<'a, S: Sender, const N: usize> DirectClient<'a, S, N> {
+ pub fn new(server: &'a RefCell<Server<S, N>>) -> Self {
+ Self { server }
+ }
+}
+
+impl<S: Sender, const N: usize> MctpClient for DirectClient<'_, S, N> {
+ fn req(&self, eid: u8) -> Result<Handle, MctpError> {
+ self.server.borrow_mut().req(eid)
+ }
+
+ fn listener(&self, msg_type: u8) -> Result<Handle, MctpError> {
+ self.server.borrow_mut().listener(msg_type)
+ }
+
+ fn get_eid(&self) -> u8 {
+ self.server.borrow().get_eid()
+ }
+
+ fn set_eid(&self, eid: u8) -> Result<(), MctpError> {
+ self.server.borrow_mut().set_eid(eid)
+ }
+
+ fn recv(
+ &self,
+ handle: Handle,
+ _timeout_millis: u32,
+ buf: &mut [u8],
+ ) -> Result<RecvMetadata, MctpError> {
+ self.server
+ .borrow_mut()
+ .try_recv(handle, buf)
+ .ok_or(MctpError::from_code(ResponseCode::TimedOut))
+ }
+
+ fn send(
+ &self,
+ handle: Option<Handle>,
+ msg_type: u8,
+ eid: Option<u8>,
+ tag: Option<u8>,
+ integrity_check: bool,
+ buf: &[u8],
+ ) -> Result<u8, MctpError> {
+ self.server
+ .borrow_mut()
+ .send(handle, msg_type, eid, tag, integrity_check, buf)
+ }
+
+ fn drop_handle(&self, handle: Handle) {
+ let _ = self.server.borrow_mut().unbind(handle);
+ }
+}
+
+// ---------------------------------------------------------------------------
+// DirectRespChannel
+// ---------------------------------------------------------------------------
+
+/// Implements [`MctpRespChannel`] — sends a reply back through a [`DirectClient`].
+///
+/// Captured metadata from the received request carries the EID and tag needed
+/// to route the response correctly.
+pub struct DirectRespChannel<'a, S: Sender, const N: usize> {
+ client: &'a DirectClient<'a, S, N>,
+ msg_type: u8,
+ remote_eid: u8,
+ tag: u8,
+}
+
+impl<S: Sender, const N: usize> MctpRespChannel for DirectRespChannel<'_, S, N> {
+ fn send(&mut self, buf: &[u8]) -> Result<(), MctpError> {
+ self.client
+ .send(
+ None,
+ self.msg_type,
+ Some(self.remote_eid),
+ Some(self.tag),
+ false,
+ buf,
+ )
+ .map(|_| ())
+ }
+
+ fn remote_eid(&self) -> u8 {
+ self.remote_eid
+ }
+}
+
+// ---------------------------------------------------------------------------
+// DirectListener
+// ---------------------------------------------------------------------------
+
+/// Implements [`MctpListener`] by polling a listener handle via [`DirectClient`].
+///
+/// `recv` will return `TimedOut` if no message is available yet. In tests,
+/// call this after feeding inbound packets with [`transfer`].
+pub struct DirectListener<'a, S: Sender, const N: usize> {
+ pub client: &'a DirectClient<'a, S, N>,
+ pub handle: Handle,
+}
+
+impl<'a, S: Sender, const N: usize> DirectListener<'a, S, N> {
+ pub fn new(client: &'a DirectClient<'a, S, N>, handle: Handle) -> Self {
+ Self { client, handle }
+ }
+}
+
+impl<'a, S: Sender, const N: usize> MctpListener for DirectListener<'a, S, N> {
+ type RespChannel<'r> = DirectRespChannel<'a, S, N> where Self: 'r;
+
+ fn recv<'f>(
+ &mut self,
+ buf: &'f mut [u8],
+ ) -> Result<(RecvMetadata, &'f mut [u8], Self::RespChannel<'_>), MctpError> {
+ let meta = self
+ .client
+ .server
+ .borrow_mut()
+ .try_recv(self.handle, buf)
+ .ok_or(MctpError::from_code(ResponseCode::TimedOut))?;
+
+ let len = meta.payload_size;
+ let resp = DirectRespChannel {
+ client: self.client,
+ msg_type: meta.msg_type,
+ remote_eid: meta.remote_eid,
+ tag: meta.msg_tag,
+ };
+ Ok((meta, &mut buf[..len], resp))
+ }
+}
+
+// ---------------------------------------------------------------------------
+// DirectReqChannel
+// ---------------------------------------------------------------------------
+
+/// Implements [`MctpReqChannel`] — send a request and receive the response
+/// via [`DirectClient`].
+pub struct DirectReqChannel<'a, S: Sender, const N: usize> {
+ client: &'a DirectClient<'a, S, N>,
+ handle: Handle,
+ msg_type: u8,
+ remote_eid: u8,
+}
+
+impl<'a, S: Sender, const N: usize> DirectReqChannel<'a, S, N> {
+ pub fn new(
+ client: &'a DirectClient<'a, S, N>,
+ handle: Handle,
+ msg_type: u8,
+ remote_eid: u8,
+ ) -> Self {
+ Self {
+ client,
+ handle,
+ msg_type,
+ remote_eid,
+ }
+ }
+}
+
+impl<S: Sender, const N: usize> MctpReqChannel for DirectReqChannel<'_, S, N> {
+ fn send(&mut self, msg_type: u8, buf: &[u8]) -> Result<(), MctpError> {
+ self.msg_type = msg_type;
+ self.client
+ .send(Some(self.handle), msg_type, None, None, false, buf)
+ .map(|_| ())
+ }
+
+ fn recv<'f>(
+ &mut self,
+ buf: &'f mut [u8],
+ ) -> Result<(RecvMetadata, &'f mut [u8]), MctpError> {
+ let meta = self
+ .client
+ .server
+ .borrow_mut()
+ .try_recv(self.handle, buf)
+ .ok_or(MctpError::from_code(ResponseCode::TimedOut))?;
+ let len = meta.payload_size;
+ Ok((meta, &mut buf[..len]))
+ }
+
+ fn remote_eid(&self) -> u8 {
+ self.remote_eid
+ }
+}
+
+// ---------------------------------------------------------------------------
+// make_server helper
+// ---------------------------------------------------------------------------
+
+/// Construct a `Server` + its outbound packet buffer, for two-endpoint tests.
+pub fn make_server(
+ eid: u8,
+ packets: &RefCell<Vec<Vec<u8>>>,
+) -> Server<BufferSender<'_>, 16> {
+ Server::new(Eid(eid), 0, BufferSender { packets })
+}
diff --git a/services/mctp/server/tests/dispatch.rs b/services/mctp/server/tests/dispatch.rs
index d94dad8..3fc72ba 100644
--- a/services/mctp/server/tests/dispatch.rs
+++ b/services/mctp/server/tests/dispatch.rs
@@ -7,53 +7,16 @@
//! work together correctly, simulating what happens when `IpcMctpClient`
//! talks to the MCTP server process over a Pigweed IPC channel.
+mod common;
+
use std::cell::RefCell;
-use mctp::{Eid, Tag};
-use mctp_lib::fragment::{Fragmenter, SendOutput};
-use mctp_lib::Sender;
+use mctp::Eid;
use openprot_mctp_api::wire;
use openprot_mctp_server::{dispatch::dispatch_mctp_op, Server};
-// ---------------------------------------------------------------------------
-// Mock transport (same as echo.rs)
-// ---------------------------------------------------------------------------
+use common::{transfer, BufferSender};
-struct BufferSender<'a> {
- packets: &'a RefCell<Vec<Vec<u8>>>,
-}
-
-impl Sender for BufferSender<'_> {
- fn send_vectored(
- &mut self,
- mut fragmenter: Fragmenter,
- payload: &[&[u8]],
- ) -> mctp::Result<Tag> {
- loop {
- let mut buf = [0u8; 255];
- match fragmenter.fragment_vectored(payload, &mut buf) {
- SendOutput::Packet(p) => {
- self.packets.borrow_mut().push(p.to_vec());
- }
- SendOutput::Complete { tag, .. } => return Ok(tag),
- SendOutput::Error { err, .. } => return Err(err),
- }
- }
- }
-
- fn get_mtu(&self) -> usize {
- 255
- }
-}
-
-fn transfer<S: Sender, const N: usize>(
- packets: &RefCell<Vec<Vec<u8>>>,
- dest: &mut Server<S, N>,
-) {
- for pkt in packets.borrow().iter() {
- dest.inbound(pkt).unwrap();
- }
-}
// ---------------------------------------------------------------------------
// Tests
@@ -172,3 +135,129 @@
let echo_payload = wire::get_response_payload(&resp[..resp_len], &header).unwrap();
assert_eq!(echo_payload, payload);
}
+
+// ---------------------------------------------------------------------------
+// Malformed request → BadArgument
+// ---------------------------------------------------------------------------
+
+/// A request buffer shorter than the header size must return `BadArgument`.
+#[test]
+fn dispatch_malformed_request_returns_bad_argument() {
+ use openprot_mctp_api::ResponseCode;
+
+ let buf = RefCell::new(Vec::new());
+ let sender = BufferSender { packets: &buf };
+ let mut server: Server<_, 16> = Server::new(Eid(8), 0, sender);
+
+ let mut resp = [0u8; 64];
+ let mut recv_buf = [0u8; 255];
+
+ // Two bytes — shorter than MctpRequestHeader::SIZE (12)
+ let bad_request = [0u8; 2];
+ let resp_len = dispatch_mctp_op(&bad_request, &mut resp, &mut server, &mut recv_buf);
+
+ let header = wire::decode_response_header(&resp[..resp_len]).unwrap();
+ assert!(!header.is_success());
+ assert_eq!(header.response_code(), ResponseCode::BadArgument);
+}
+
+/// An opcode byte that is not a known `MctpOp` must return `BadArgument`.
+#[test]
+fn dispatch_unknown_opcode_returns_bad_argument() {
+ use openprot_mctp_api::ResponseCode;
+
+ let buf = RefCell::new(Vec::new());
+ let sender = BufferSender { packets: &buf };
+ let mut server: Server<_, 16> = Server::new(Eid(8), 0, sender);
+
+ let mut resp = [0u8; 64];
+ let mut recv_buf = [0u8; 255];
+
+ // 12-byte header with opcode 0xFF (unrecognised)
+ let mut bad_request = [0u8; 12];
+ bad_request[0] = 0xFF;
+ let resp_len = dispatch_mctp_op(&bad_request, &mut resp, &mut server, &mut recv_buf);
+
+ let header = wire::decode_response_header(&resp[..resp_len]).unwrap();
+ assert!(!header.is_success());
+ assert_eq!(header.response_code(), ResponseCode::BadArgument);
+}
+
+// ---------------------------------------------------------------------------
+// MctpOp::Recv when no message is ready → TimedOut
+// ---------------------------------------------------------------------------
+
+/// `Recv` dispatched when no message has arrived must return `TimedOut`.
+#[test]
+fn dispatch_recv_no_message_returns_timed_out() {
+ use openprot_mctp_api::ResponseCode;
+
+ let buf = RefCell::new(Vec::new());
+ let sender = BufferSender { packets: &buf };
+ let mut server: Server<_, 16> = Server::new(Eid(8), 0, sender);
+
+ let mut req = [0u8; 64];
+ let mut resp = [0u8; 64];
+ let mut recv_buf = [0u8; 255];
+
+ // Register a listener
+ let req_len = wire::encode_listener(&mut req, 1).unwrap();
+ let resp_len = dispatch_mctp_op(&req[..req_len], &mut resp, &mut server, &mut recv_buf);
+ let h = wire::decode_response_header(&resp[..resp_len]).unwrap();
+ assert!(h.is_success());
+ let listener_handle = h.handle;
+
+ // Attempt Recv immediately — no inbound packet
+ let req_len = wire::encode_recv(&mut req, listener_handle, 0).unwrap();
+ let resp_len = dispatch_mctp_op(&req[..req_len], &mut resp, &mut server, &mut recv_buf);
+ let header = wire::decode_response_header(&resp[..resp_len]).unwrap();
+ assert!(!header.is_success());
+ assert_eq!(header.response_code(), ResponseCode::TimedOut);
+}
+
+// ---------------------------------------------------------------------------
+// MctpOp::Unbind
+// ---------------------------------------------------------------------------
+
+/// `Unbind` on a valid handle returns success.
+#[test]
+fn dispatch_unbind_valid_handle() {
+ let buf = RefCell::new(Vec::new());
+ let sender = BufferSender { packets: &buf };
+ let mut server: Server<_, 16> = Server::new(Eid(8), 0, sender);
+
+ let mut req = [0u8; 64];
+ let mut resp = [0u8; 64];
+ let mut recv_buf = [0u8; 255];
+
+ // Allocate a listener handle
+ let req_len = wire::encode_listener(&mut req, 1).unwrap();
+ let resp_len = dispatch_mctp_op(&req[..req_len], &mut resp, &mut server, &mut recv_buf);
+ let h = wire::decode_response_header(&resp[..resp_len]).unwrap();
+ assert!(h.is_success());
+ let listener_handle = h.handle;
+
+ // Unbind it
+ let req_len = wire::encode_unbind(&mut req, listener_handle).unwrap();
+ let resp_len = dispatch_mctp_op(&req[..req_len], &mut resp, &mut server, &mut recv_buf);
+ let header = wire::decode_response_header(&resp[..resp_len]).unwrap();
+ assert!(header.is_success());
+}
+
+/// `Unbind` on a handle that was never allocated still returns success.
+/// (The server's `unbind` is idempotent — it ignores unknown handles.)
+#[test]
+fn dispatch_unbind_unknown_handle_is_idempotent() {
+ let buf = RefCell::new(Vec::new());
+ let sender = BufferSender { packets: &buf };
+ let mut server: Server<_, 16> = Server::new(Eid(8), 0, sender);
+
+ let mut req = [0u8; 64];
+ let mut resp = [0u8; 64];
+ let mut recv_buf = [0u8; 255];
+
+ let req_len = wire::encode_unbind(&mut req, 0xDEAD_BEEF).unwrap();
+ let resp_len = dispatch_mctp_op(&req[..req_len], &mut resp, &mut server, &mut recv_buf);
+ let header = wire::decode_response_header(&resp[..resp_len]).unwrap();
+ assert!(header.is_success());
+}
diff --git a/services/mctp/server/tests/echo.rs b/services/mctp/server/tests/echo.rs
index 28e1173..b3a7790 100644
--- a/services/mctp/server/tests/echo.rs
+++ b/services/mctp/server/tests/echo.rs
@@ -13,123 +13,16 @@
//! the `Server` + transport plumbing is the server side. This mirrors the
//! real deployment where the echo task is an IPC client of the MCTP server.
+mod common;
+
use std::cell::RefCell;
-use mctp::{Eid, Tag};
-use mctp_lib::fragment::{Fragmenter, SendOutput};
-use mctp_lib::Sender;
-use openprot_mctp_api::{Handle, MctpClient, MctpError, RecvMetadata, ResponseCode};
+use mctp::Eid;
+use openprot_mctp_api::{Handle, MctpClient};
use openprot_mctp_server::Server;
-// ---------------------------------------------------------------------------
-// Mock transport
-// ---------------------------------------------------------------------------
+use common::{transfer, BufferSender, DirectClient};
-/// A mock sender that captures outbound packets in a buffer.
-struct BufferSender<'a> {
- packets: &'a RefCell<Vec<Vec<u8>>>,
-}
-
-impl Sender for BufferSender<'_> {
- fn send_vectored(
- &mut self,
- mut fragmenter: Fragmenter,
- payload: &[&[u8]],
- ) -> mctp::Result<Tag> {
- loop {
- let mut buf = [0u8; 255];
- match fragmenter.fragment_vectored(payload, &mut buf) {
- SendOutput::Packet(p) => {
- self.packets.borrow_mut().push(p.to_vec());
- }
- SendOutput::Complete { tag, .. } => return Ok(tag),
- SendOutput::Error { err, .. } => return Err(err),
- }
- }
- }
-
- fn get_mtu(&self) -> usize {
- 255
- }
-}
-
-/// Transfer packets from one server's outbound buffer to another server.
-fn transfer<S: Sender, const N: usize>(
- packets: &RefCell<Vec<Vec<u8>>>,
- dest: &mut Server<S, N>,
-) {
- let pkts = packets.borrow();
- for pkt in pkts.iter() {
- dest.inbound(pkt).unwrap();
- }
-}
-
-// ---------------------------------------------------------------------------
-// Client-side wrapper (simulates IPC client)
-// ---------------------------------------------------------------------------
-
-/// A direct (in-process) MCTP client that wraps a `Server` via `RefCell`.
-///
-/// This plays the same role as the Hubris `mctp_api::Stack` — it provides the
-/// `MctpClient` trait interface to application code. In production, IPC would
-/// sit between client and server; here we call `Server` methods directly.
-struct DirectClient<'a, S: Sender, const N: usize> {
- server: &'a RefCell<Server<S, N>>,
-}
-
-impl<'a, S: Sender, const N: usize> DirectClient<'a, S, N> {
- fn new(server: &'a RefCell<Server<S, N>>) -> Self {
- Self { server }
- }
-}
-
-impl<S: Sender, const N: usize> MctpClient for DirectClient<'_, S, N> {
- fn req(&self, eid: u8) -> Result<Handle, MctpError> {
- self.server.borrow_mut().req(eid)
- }
-
- fn listener(&self, msg_type: u8) -> Result<Handle, MctpError> {
- self.server.borrow_mut().listener(msg_type)
- }
-
- fn get_eid(&self) -> u8 {
- self.server.borrow().get_eid()
- }
-
- fn set_eid(&self, eid: u8) -> Result<(), MctpError> {
- self.server.borrow_mut().set_eid(eid)
- }
-
- fn recv(
- &self,
- handle: Handle,
- _timeout_millis: u32,
- buf: &mut [u8],
- ) -> Result<RecvMetadata, MctpError> {
- self.server
- .borrow_mut()
- .try_recv(handle, buf)
- .ok_or(MctpError::from_code(ResponseCode::TimedOut))
- }
-
- fn send(
- &self,
- handle: Option<Handle>,
- msg_type: u8,
- eid: Option<u8>,
- tag: Option<u8>,
- integrity_check: bool,
- buf: &[u8],
- ) -> Result<u8, MctpError> {
- self.server
- .borrow_mut()
- .send(handle, msg_type, eid, tag, integrity_check, buf)
- }
-
- fn drop_handle(&self, handle: Handle) {
- let _ = self.server.borrow_mut().unbind(handle);
- }
-}
// ---------------------------------------------------------------------------
// Echo application logic (client side — mirrors Hubris mctp-echo task)
diff --git a/services/mctp/server/tests/integration.rs b/services/mctp/server/tests/integration.rs
new file mode 100644
index 0000000..3f1b006
--- /dev/null
+++ b/services/mctp/server/tests/integration.rs
@@ -0,0 +1,323 @@
+// Licensed under the Apache-2.0 license
+
+//! MCTP integration tests — multi-fragment, multi-listener, MctpListener trait.
+//!
+//! Tests in this file exercise:
+//! - Multi-fragment reassembly (small MTU sender)
+//! - Multiple concurrent listeners with no cross-talk
+//! - `MctpListener` + `MctpRespChannel` trait path (mirrors real echo application)
+//! - `MctpReqChannel` trait path
+//! - `drop_handle` mid-flight clears the outstanding entry
+//!
+//! No I2C transport is used anywhere in this file.
+
+mod common;
+
+use std::cell::RefCell;
+
+use mctp::Eid;
+use openprot_mctp_api::{MctpClient, MctpListener, MctpReqChannel, MctpRespChannel};
+use openprot_mctp_server::Server;
+
+use common::{transfer, BufferSender, DirectClient, DirectListener, DirectReqChannel, SmallMtuBufferSender};
+
+// ---------------------------------------------------------------------------
+// Multi-fragment roundtrip
+// ---------------------------------------------------------------------------
+
+/// Send a 200-byte payload through a server whose sender MTU is 64 bytes.
+///
+/// The fragmenter must split it into multiple packets. The receiving server
+/// must reassemble them before delivering to the listener.
+#[test]
+fn multi_fragment_roundtrip() {
+ let buf_a = RefCell::new(Vec::new());
+ let buf_b = RefCell::new(Vec::new());
+
+ // Server A: small MTU sender (forces fragmentation)
+ let sender_a = SmallMtuBufferSender {
+ packets: &buf_a,
+ mtu: 64,
+ };
+ let server_a: RefCell<Server<_, 16>> = RefCell::new(Server::new(Eid(8), 0, sender_a));
+
+ // Server B: normal MTU (sends the request)
+ let server_b: RefCell<Server<_, 16>> =
+ RefCell::new(Server::new(Eid(42), 0, BufferSender { packets: &buf_b }));
+
+ let client_a = DirectClient::new(&server_a);
+ let client_b = DirectClient::new(&server_b);
+
+ let listener = client_a.listener(1).unwrap();
+ let req = client_b.req(8).unwrap();
+
+ // 200-byte payload — exceeds a single 64-byte MTU fragment
+ let payload: Vec<u8> = (0u8..200).collect();
+ client_b
+ .send(Some(req), 1, None, None, false, &payload)
+ .unwrap();
+
+ // Transfer B → A (may be multiple packets)
+ transfer(&buf_b, &mut server_a.borrow_mut());
+
+ // A should have reassembled and delivered to the listener
+ let mut recv_buf = [0u8; 512];
+ let meta = client_a
+ .recv(listener, 0, &mut recv_buf)
+ .expect("A should receive the reassembled message");
+
+ assert_eq!(meta.payload_size, payload.len());
+ assert_eq!(&recv_buf[..meta.payload_size], payload.as_slice());
+ assert_eq!(meta.remote_eid, 42);
+}
+
+// ---------------------------------------------------------------------------
+// Multiple concurrent listeners — no cross-talk
+// ---------------------------------------------------------------------------
+
+/// Two listeners on the same server for different msg_types each receive only
+/// their own messages.
+#[test]
+fn multiple_listeners_no_crosstalk() {
+ let buf_a = RefCell::new(Vec::new());
+ let buf_b = RefCell::new(Vec::new());
+
+ let server_a: RefCell<Server<_, 16>> =
+ RefCell::new(Server::new(Eid(8), 0, BufferSender { packets: &buf_a }));
+ let server_b: RefCell<Server<_, 16>> =
+ RefCell::new(Server::new(Eid(42), 0, BufferSender { packets: &buf_b }));
+
+ let client_a = DirectClient::new(&server_a);
+ let client_b = DirectClient::new(&server_b);
+
+ // Two listeners: type 1 and type 2
+ let listener_type1 = client_a.listener(1).unwrap();
+ let listener_type2 = client_a.listener(2).unwrap();
+
+ // B sends type 2
+ let req = client_b.req(8).unwrap();
+ let msg_type2 = b"for type 2";
+ client_b
+ .send(Some(req), 2, None, None, false, msg_type2)
+ .unwrap();
+ transfer(&buf_b, &mut server_a.borrow_mut());
+
+ // Type 1 listener should see nothing
+ let mut buf = [0u8; 255];
+ assert!(
+ client_a.recv(listener_type1, 0, &mut buf).is_err(),
+ "type-1 listener should not receive a type-2 message"
+ );
+
+ // Type 2 listener should see the message
+ let meta = client_a
+ .recv(listener_type2, 0, &mut buf)
+ .expect("type-2 listener should receive the message");
+ assert_eq!(&buf[..meta.payload_size], msg_type2);
+}
+
+// ---------------------------------------------------------------------------
+// MctpListener + MctpRespChannel trait path (real echo application shape)
+// ---------------------------------------------------------------------------
+
+/// Exercises the `MctpListener` / `MctpRespChannel` traits — the same interface
+/// used by the real echo application — with `BufferSender` as the transport.
+///
+/// This is the key test that lets the echo application logic run without I2C:
+/// ```
+/// fn echo_app(listener: &mut impl MctpListener) {
+/// let (meta, msg, mut resp) = listener.recv(&mut buf).unwrap();
+/// resp.send(msg).unwrap();
+/// }
+/// ```
+#[test]
+fn echo_via_mctplistener_trait() {
+ let buf_a = RefCell::new(Vec::new());
+ let buf_b = RefCell::new(Vec::new());
+
+ let server_a: RefCell<Server<_, 16>> =
+ RefCell::new(Server::new(Eid(8), 0, BufferSender { packets: &buf_a }));
+ let server_b: RefCell<Server<_, 16>> =
+ RefCell::new(Server::new(Eid(42), 0, BufferSender { packets: &buf_b }));
+
+ let client_a = DirectClient::new(&server_a);
+ let client_b = DirectClient::new(&server_b);
+
+ let listener_handle = client_a.listener(1).unwrap();
+ let req_handle = client_b.req(8).unwrap();
+
+ // B sends a request
+ let request = b"echo via trait";
+ client_b
+ .send(Some(req_handle), 1, None, None, false, request)
+ .unwrap();
+ transfer(&buf_b, &mut server_a.borrow_mut());
+
+ // A echoes back using the MctpListener + MctpRespChannel trait path
+ // (same shape as the real echo application in hubris/task/mctp-echo)
+ let mut listener = DirectListener::new(&client_a, listener_handle);
+ let mut recv_buf = [0u8; 255];
+ let (meta, payload, mut resp) = listener
+ .recv(&mut recv_buf)
+ .expect("listener should have a message ready");
+
+ assert_eq!(payload, request);
+ assert_eq!(meta.remote_eid, 42);
+
+ resp.send(payload).expect("response send should succeed");
+
+ // Transfer A → B and verify
+ transfer(&buf_a, &mut server_b.borrow_mut());
+
+ let mut resp_buf = [0u8; 255];
+ let resp_meta = client_b
+ .recv(req_handle, 0, &mut resp_buf)
+ .expect("B should receive the echo");
+
+ assert_eq!(&resp_buf[..resp_meta.payload_size], request);
+ assert_eq!(resp_meta.remote_eid, 8);
+ assert_eq!(resp_meta.msg_type, 1);
+}
+
+// ---------------------------------------------------------------------------
+// MctpReqChannel trait path
+// ---------------------------------------------------------------------------
+
+/// Exercises `MctpReqChannel::send` + `MctpReqChannel::recv` trait methods.
+#[test]
+fn req_channel_send_recv() {
+ let buf_a = RefCell::new(Vec::new());
+ let buf_b = RefCell::new(Vec::new());
+
+ let server_a: RefCell<Server<_, 16>> =
+ RefCell::new(Server::new(Eid(8), 0, BufferSender { packets: &buf_a }));
+ let server_b: RefCell<Server<_, 16>> =
+ RefCell::new(Server::new(Eid(42), 0, BufferSender { packets: &buf_b }));
+
+ let client_a = DirectClient::new(&server_a);
+ let client_b = DirectClient::new(&server_b);
+
+ let listener_handle = client_a.listener(1).unwrap();
+ let req_handle = client_b.req(8).unwrap();
+
+ // B sends via MctpReqChannel
+ let mut req_channel = DirectReqChannel::new(&client_b, req_handle, 1, 8);
+ req_channel
+ .send(1, b"req channel test")
+ .expect("req channel send should succeed");
+ assert_eq!(req_channel.remote_eid(), 8);
+
+ transfer(&buf_b, &mut server_a.borrow_mut());
+
+ // A echoes manually (through MctpClient)
+ let mut echo_buf = [0u8; 255];
+ let meta = client_a
+ .recv(listener_handle, 0, &mut echo_buf)
+ .unwrap();
+ client_a
+ .send(
+ None,
+ meta.msg_type,
+ Some(meta.remote_eid),
+ Some(meta.msg_tag),
+ false,
+ &echo_buf[..meta.payload_size],
+ )
+ .unwrap();
+
+ transfer(&buf_a, &mut server_b.borrow_mut());
+
+ // B receives via MctpReqChannel
+ let mut resp_buf = [0u8; 255];
+ let (resp_meta, resp_payload) = req_channel
+ .recv(&mut resp_buf)
+ .expect("req channel recv should succeed");
+
+ assert_eq!(resp_payload, b"req channel test");
+ assert_eq!(resp_meta.remote_eid, 8);
+}
+
+// ---------------------------------------------------------------------------
+// drop_handle mid-flight
+// ---------------------------------------------------------------------------
+
+/// Dropping a listener handle while a recv is outstanding clears the entry.
+/// After `unbind`, `try_recv` no longer panics and the handle is gone.
+#[test]
+fn drop_handle_mid_flight_clears_entry() {
+ let sender = common::DroppingBufferSender;
+ let mut server: Server<_, 16> = Server::new(Eid(8), 0, sender);
+
+ let handle = server.listener(1).unwrap();
+
+ // Register a pending recv
+ server
+ .register_recv(handle, 1000, 0)
+ .expect("register_recv should succeed");
+
+ // Drop the handle before any message or timeout
+ server.unbind(handle).expect("unbind should succeed");
+
+ // update should return nothing for that handle
+ let mut recv_buf = [0u8; 255];
+ let (_, ready) = server.update(500, &mut recv_buf);
+ assert!(
+ ready.iter().all(|(h, _)| *h != handle),
+ "dropped handle should not appear in update results"
+ );
+}
+
+// ---------------------------------------------------------------------------
+// Response-without-handle: tag & EID threading
+// ---------------------------------------------------------------------------
+
+/// A response sent without a handle (the reply path) correctly threads the
+/// remote EID and tag back so the requester receives it.
+#[test]
+fn response_without_handle_eid_tag_threading() {
+ let buf_a = RefCell::new(Vec::new());
+ let buf_b = RefCell::new(Vec::new());
+
+ let server_a: RefCell<Server<_, 16>> =
+ RefCell::new(Server::new(Eid(8), 0, BufferSender { packets: &buf_a }));
+ let server_b: RefCell<Server<_, 16>> =
+ RefCell::new(Server::new(Eid(42), 0, BufferSender { packets: &buf_b }));
+
+ let client_a = DirectClient::new(&server_a);
+ let client_b = DirectClient::new(&server_b);
+
+ let listener = client_a.listener(5).unwrap();
+ let req = client_b.req(8).unwrap();
+
+ // B sends a type-5 request
+ client_b
+ .send(Some(req), 5, None, None, false, b"ping")
+ .unwrap();
+ transfer(&buf_b, &mut server_a.borrow_mut());
+
+ // A receives and replies — no handle, explicit EID + tag
+ let mut buf = [0u8; 255];
+ let meta = client_a.recv(listener, 0, &mut buf).unwrap();
+ client_a
+ .send(
+ None,
+ meta.msg_type,
+ Some(meta.remote_eid),
+ Some(meta.msg_tag),
+ false,
+ b"pong",
+ )
+ .unwrap();
+
+ transfer(&buf_a, &mut server_b.borrow_mut());
+
+ // B receives the response and verifies metadata
+ let mut resp_buf = [0u8; 255];
+ let resp = client_b
+ .recv(req, 0, &mut resp_buf)
+ .expect("B should receive pong");
+
+ assert_eq!(&resp_buf[..resp.payload_size], b"pong");
+ assert_eq!(resp.remote_eid, 8);
+ assert_eq!(resp.msg_type, 5);
+}
diff --git a/services/mctp/server/tests/server_unit.rs b/services/mctp/server/tests/server_unit.rs
new file mode 100644
index 0000000..f468961
--- /dev/null
+++ b/services/mctp/server/tests/server_unit.rs
@@ -0,0 +1,241 @@
+// Licensed under the Apache-2.0 license
+
+//! Server unit tests — exercise `Server` methods directly without I2C.
+//!
+//! Each test constructs a `Server` with `DroppingBufferSender` (or
+//! `BufferSender` when outbound packets are needed) and calls the
+//! server API directly. No transport hardware is involved.
+
+mod common;
+
+use std::cell::RefCell;
+
+use mctp::Eid;
+use openprot_mctp_api::{ResponseCode};
+use openprot_mctp_server::{RecvResult, Server, ServerConfig};
+
+use common::{BufferSender, DroppingBufferSender, transfer};
+
+// ---------------------------------------------------------------------------
+// Helpers
+// ---------------------------------------------------------------------------
+
+/// Deliver a message from a sender EID to a receiver server by routing it
+/// through a real `Server::send()` call, avoiding any direct Fragmenter API.
+///
+/// Creates a temporary sender server (EID `src`) with a `BufferSender`, sends
+/// one message of `msg_type` to `dst_eid`, then feeds the captured packets
+/// into `dest` via `inbound`.
+fn deliver_to<S: mctp_lib::Sender, const N: usize>(
+ src: u8,
+ dst_eid: u8,
+ msg_type: u8,
+ payload: &[u8],
+ dest: &mut Server<S, N>,
+) {
+ let buf = RefCell::new(Vec::new());
+ let mut sender_server: Server<BufferSender<'_>, 16> =
+ Server::new(Eid(src), 0, BufferSender { packets: &buf });
+
+ let req_handle = sender_server.req(dst_eid).unwrap();
+ sender_server
+ .send(Some(req_handle), msg_type, None, None, false, payload)
+ .unwrap();
+
+ transfer(&buf, dest);
+}
+
+// ---------------------------------------------------------------------------
+// EID management
+// ---------------------------------------------------------------------------
+
+/// `get_eid` returns the EID passed to `Server::new`.
+#[test]
+fn eid_initial_value() {
+ let sender = DroppingBufferSender;
+ let server: Server<_, 16> = Server::new(Eid(8), 0, sender);
+ assert_eq!(server.get_eid(), 8);
+}
+
+/// `set_eid` + `get_eid` round-trip.
+#[test]
+fn eid_set_get_roundtrip() {
+ let sender = DroppingBufferSender;
+ let mut server: Server<_, 16> = Server::new(Eid(0), 0, sender);
+ server.set_eid(42).expect("set_eid should succeed");
+ assert_eq!(server.get_eid(), 42);
+}
+
+// ---------------------------------------------------------------------------
+// Handle allocation / deallocation
+// ---------------------------------------------------------------------------
+
+/// `req()` succeeds and `unbind()` releases the handle cleanly.
+#[test]
+fn req_handle_alloc_and_unbind() {
+ let sender = DroppingBufferSender;
+ let mut server: Server<_, 16> = Server::new(Eid(8), 0, sender);
+ let handle = server.req(42).expect("req should succeed");
+ server.unbind(handle).expect("unbind should succeed");
+}
+
+/// `listener()` succeeds and `unbind()` releases the handle cleanly.
+#[test]
+fn listener_handle_alloc_and_unbind() {
+ let sender = DroppingBufferSender;
+ let mut server: Server<_, 16> = Server::new(Eid(8), 0, sender);
+ let handle = server.listener(1).expect("listener should succeed");
+ server.unbind(handle).expect("unbind should succeed");
+}
+
+/// Registering a second listener for the same `msg_type` returns `AddrInUse`.
+#[test]
+fn listener_duplicate_msg_type_returns_addr_in_use() {
+ let sender = DroppingBufferSender;
+ let mut server: Server<_, 16> = Server::new(Eid(8), 0, sender);
+ server.listener(1).expect("first listener should succeed");
+ let err = server.listener(1).expect_err("duplicate listener should fail");
+ assert_eq!(err.code, ResponseCode::AddrInUse);
+}
+
+/// Two listeners for *different* `msg_type` values both succeed.
+#[test]
+fn listener_different_types_both_succeed() {
+ let sender = DroppingBufferSender;
+ let mut server: Server<_, 16> = Server::new(Eid(8), 0, sender);
+ let h1 = server.listener(1).expect("listener type 1 should succeed");
+ let h2 = server.listener(2).expect("listener type 2 should succeed");
+ assert_ne!(h1, h2);
+}
+
+// ---------------------------------------------------------------------------
+// try_recv before inbound
+// ---------------------------------------------------------------------------
+
+/// `try_recv` returns `None` when no message has been fed via `inbound`.
+#[test]
+fn try_recv_before_inbound_returns_none() {
+ let sender = DroppingBufferSender;
+ let mut server: Server<_, 16> = Server::new(Eid(8), 0, sender);
+ let handle = server.listener(1).unwrap();
+ let mut buf = [0u8; 255];
+ assert!(server.try_recv(handle, &mut buf).is_none());
+}
+
+// ---------------------------------------------------------------------------
+// inbound → try_recv routing
+// ---------------------------------------------------------------------------
+
+/// A raw packet fed via `inbound` is delivered to the matching listener.
+#[test]
+fn inbound_then_try_recv_delivers_message() {
+ let buf_out = RefCell::new(Vec::new());
+ let sender = BufferSender { packets: &buf_out };
+ let mut server: Server<_, 16> = Server::new(Eid(8), 0, sender);
+
+ let listener = server.listener(1).unwrap();
+
+ let payload = b"hello";
+ deliver_to(42, 8, 1, payload, &mut server);
+
+ let mut recv_buf = [0u8; 255];
+ let meta = server
+ .try_recv(listener, &mut recv_buf)
+ .expect("message should be available after inbound");
+
+ assert_eq!(meta.msg_type, 1);
+ assert_eq!(meta.remote_eid, 42);
+ assert_eq!(meta.payload_size, payload.len());
+ assert_eq!(&recv_buf[..meta.payload_size], payload);
+}
+
+/// A packet for msg_type 2 is not delivered to a listener for msg_type 1.
+#[test]
+fn inbound_wrong_type_not_delivered() {
+ let sender = DroppingBufferSender;
+ let mut server: Server<_, 16> = Server::new(Eid(8), 0, sender);
+ let listener = server.listener(1).unwrap();
+
+ deliver_to(42, 8, 2, b"wrong type", &mut server);
+
+ let mut buf = [0u8; 255];
+ assert!(server.try_recv(listener, &mut buf).is_none());
+}
+
+// ---------------------------------------------------------------------------
+// send with oversized payload
+// ---------------------------------------------------------------------------
+
+/// `send` with a payload larger than `MAX_PAYLOAD` returns `NoSpace`.
+#[test]
+fn send_oversized_payload_returns_no_space() {
+ let sender = DroppingBufferSender;
+ let mut server: Server<_, 16> = Server::new(Eid(8), 0, sender);
+ let req_handle = server.req(42).unwrap();
+
+ let big_payload = vec![0u8; ServerConfig::MAX_PAYLOAD + 1];
+ let err = server
+ .send(Some(req_handle), 1, None, None, false, &big_payload)
+ .expect_err("oversized send should fail");
+ assert_eq!(err.code, ResponseCode::NoSpace);
+}
+
+// ---------------------------------------------------------------------------
+// register_recv + update timeout
+// ---------------------------------------------------------------------------
+
+/// A registered recv with a timeout fires `RecvResult::TimedOut` after the deadline.
+#[test]
+fn pending_recv_times_out() {
+ let sender = DroppingBufferSender;
+ let mut server: Server<_, 16> = Server::new(Eid(8), 0, sender);
+ let listener = server.listener(1).unwrap();
+
+ // Register a recv with a 100 ms timeout, starting at t=0.
+ server
+ .register_recv(listener, 100, 0)
+ .expect("register_recv should succeed");
+
+ let mut recv_buf = [0u8; 255];
+
+ // At t=50 ms: not yet timed out, no message.
+ let (_, ready) = server.update(50, &mut recv_buf);
+ assert!(ready.is_empty(), "should not fire before deadline");
+
+ // At t=100 ms: deadline reached.
+ let (_, ready) = server.update(100, &mut recv_buf);
+ assert_eq!(ready.len(), 1);
+ assert!(
+ matches!(ready[0], (h, RecvResult::TimedOut) if h == listener),
+ "expected TimedOut for listener handle"
+ );
+}
+
+/// A registered recv that receives a message before the deadline resolves with
+/// `RecvResult::Message`, not a timeout.
+#[test]
+fn pending_recv_fulfilled_before_timeout() {
+ let buf_out = RefCell::new(Vec::new());
+ let sender = BufferSender { packets: &buf_out };
+ let mut server: Server<_, 16> = Server::new(Eid(8), 0, sender);
+ let listener = server.listener(1).unwrap();
+
+ server
+ .register_recv(listener, 1000, 0)
+ .expect("register_recv should succeed");
+
+ deliver_to(42, 8, 1, b"data", &mut server);
+
+ let mut recv_buf = [0u8; 255];
+ let (_, ready) = server.update(50, &mut recv_buf);
+
+ assert_eq!(ready.len(), 1);
+ assert!(
+ matches!(ready[0], (h, RecvResult::Message(_)) if h == listener),
+ "expected Message result"
+ );
+ if let (_, RecvResult::Message(meta)) = ready[0] {
+ assert_eq!(meta.remote_eid, 42);
+ assert_eq!(meta.msg_type, 1);
+ }
+}