introduce mtcp subsystem of openprot
diff --git a/services/mctp/BUILD.bazel b/services/mctp/BUILD.bazel new file mode 100644 index 0000000..688e62e --- /dev/null +++ b/services/mctp/BUILD.bazel
@@ -0,0 +1,27 @@ +# Licensed under the Apache-2.0 license +# SPDX-License-Identifier: Apache-2.0 + +package(default_visibility = ["//visibility:public"]) + +# Embedded-safe production crates only (no rust_test targets). +filegroup( + name = "mctp_embedded_all", + srcs = [ + "//services/mctp/api:mctp_api", + "//services/mctp/echo:mctp_echo", + "//services/mctp/server:mctp_server_lib", + ], +) + +# Host-side tests; run without embedded target config. +test_suite( + name = "mctp_host_tests", + tests = [ + "//services/mctp/api:mctp_api_test", + "//services/mctp/echo:mctp_echo_host_test", + "//services/mctp/server:mctp_server_dispatch_test", + "//services/mctp/server:mctp_server_echo_test", + "//services/mctp/server:mctp_server_integration_test", + "//services/mctp/server:mctp_server_unit_test", + ], +)
diff --git a/services/mctp/README.md b/services/mctp/README.md new file mode 100644 index 0000000..165cb4f --- /dev/null +++ b/services/mctp/README.md
@@ -0,0 +1,28 @@ +# MCTP Service + +This directory contains the MCTP API, echo policy crate, and server implementation. + +## Test Coverage + +This section documents test target coverage (Bazel targets), not line or branch coverage percentages. + +### Host Test Suite + +Run all host-side tests with: + +```bash +bazelisk test //services/mctp:mctp_host_tests --test_output=errors +``` + +`//services/mctp:mctp_host_tests` includes: + +- `//services/mctp/api:mctp_api_test` +- `//services/mctp/echo:mctp_echo_host_test` +- `//services/mctp/server:mctp_server_dispatch_test` +- `//services/mctp/server:mctp_server_echo_test` +- `//services/mctp/server:mctp_server_integration_test` +- `//services/mctp/server:mctp_server_unit_test` + +## Notes + +- Transport-dependent behavior is covered in integration-style host tests using in-memory fixtures.
diff --git a/services/mctp/api/BUILD.bazel b/services/mctp/api/BUILD.bazel index 3f3df45..171f7a3 100644 --- a/services/mctp/api/BUILD.bazel +++ b/services/mctp/api/BUILD.bazel
@@ -9,10 +9,6 @@ crate_name = "openprot_mctp_api", edition = "2024", visibility = ["//visibility:public"], - deps = [ - "@rust_crates//:heapless", - "@rust_crates//:zerocopy", - ], ) rust_test(
diff --git a/services/mctp/api/README.md b/services/mctp/api/README.md index de26023..c2f1726 100644 --- a/services/mctp/api/README.md +++ b/services/mctp/api/README.md
@@ -7,22 +7,23 @@ This crate defines the API contract between MCTP applications and the MCTP server. It provides two layers: -1. **`MctpClient` trait** — low-level interface mirroring the IPC wire operations - (req, listener, recv, send, drop_handle). Platform-specific crates such as - `openprot-mctp-client` implement this trait using the OS transport (e.g. Pigweed IPC). +1. **`MctpClient` trait** — low-level service-transport interface for + `req`, `listener`, `recv`, `send`, `drop_handle`, plus local EID control + (`get_eid` / `set_eid`). Platform-specific clients implement this trait + over a chosen transport (for example Pigweed IPC, sockets, or test doubles). 2. **`Stack` facade** (`stack` module) — high-level entry point that wraps any `MctpClient` and returns typed channel objects (`StackListener`, `StackReqChannel`, `StackRespChannel`) that implement the `MctpListener` / `MctpReqChannel` / `MctpRespChannel` traits. This two-layer design hides both the **concrete MCTP stack implementation** (which lives -inside the server process) and the **OS / IPC transport** from application code. +inside the server process) and the **transport mechanism** from application code. Applications depend only on the high-level traits; swapping the transport or stack requires no application changes. ```text ┌─────────────────────┐ -│ Application │ uses MctpListener / MctpReqChannel / MctpRespChannel traits +│ Application │ uses Stack facade, then channel traits └─────────┬───────────┘ │ ▼ @@ -32,10 +33,10 @@ │ MctpClient trait ▼ ┌─────────────────────┐ -│ IpcMctpClient │ encodes wire protocol, calls OS IPC (e.g. Pigweed channel_transact) -│ (mctp-client crate)│ +│ MctpClient impl │ encodes wire protocol, uses a transport backend +│ (IPC/sockets/test) │ └─────────┬───────────┘ - │ IPC + │ Transport ▼ ┌─────────────────────┐ │ MCTP Server │ owns the concrete MCTP stack (mctp-lib, etc.) @@ -75,11 +76,13 @@ `Stack<C: MctpClient>` applies the **Strategy pattern**: - **Context** → `Stack<C>` holds the strategy and exposes the high-level API -- **Strategy trait** → `MctpClient` defines the IPC operations (req, listener, recv, send, drop_handle) -- **Concrete strategies** → `IpcMctpClient` (Pigweed IPC), test `DirectClient`, future Linux socket client +- **Strategy trait** → `MctpClient` defines the service-transport operations (`req`, `listener`, `recv`, `send`, `drop_handle`) and local EID control (`get_eid`/`set_eid`). +- **Concrete strategies** → transport-specific `MctpClient` implementations (for example IPC clients) and test `DirectClient` -Applications code against `MctpListener` / `MctpReqChannel` / `MctpRespChannel` traits and never -see the strategy. The concrete `MctpClient` implementation is injected via `Stack::new(client)`. +Application code enters through the `Stack` facade and then operates on channels +implementing `MctpListener` / `MctpReqChannel` / `MctpRespChannel`. During +initialization, a concrete `MctpClient` is provided to `Stack::new(client)`; +after that, protocol logic remains transport-agnostic. This gives two independent axes of variation: @@ -91,13 +94,11 @@ -The `wire` module implements binary request/response encoding for IPC communication. -It is used internally by `openprot-mctp-client` and the server; applications do not -use it directly. +The `wire` module implements binary request/response encoding. +It is used internally by transport client implementations and server endpoints; +applications do not use it directly. ## Dependencies -- `zerocopy` — zero-copy serialization -- `heapless` — `no_std` collections - -This crate is `no_std` compatible. +This crate currently has no external Rust crate dependencies. +It is no_std compatible and relies on core plus internal modules. \ No newline at end of file
diff --git a/services/mctp/api/src/stack.rs b/services/mctp/api/src/stack.rs index 3de5470..89adbe5 100644 --- a/services/mctp/api/src/stack.rs +++ b/services/mctp/api/src/stack.rs
@@ -11,7 +11,7 @@ //! ## Usage //! //! ```rust,ignore -//! use openprot_mctp_client::IpcMctpClient; +//! use openprot_mctp_client_ipc::IpcMctpClient; //! use openprot_mctp_api::stack::Stack; //! use openprot_mctp_api::{MctpListener, MctpReqChannel, MctpRespChannel}; //!
diff --git a/services/mctp/echo/BUILD.bazel b/services/mctp/echo/BUILD.bazel new file mode 100644 index 0000000..2a280d1 --- /dev/null +++ b/services/mctp/echo/BUILD.bazel
@@ -0,0 +1,30 @@ +# Licensed under the Apache-2.0 license +# SPDX-License-Identifier: Apache-2.0 + +load("@rules_rust//rust:defs.bzl", "rust_library", "rust_test") + +rust_library( + name = "mctp_echo", + srcs = ["src/lib.rs"], + crate_name = "openprot_mctp_echo", + edition = "2024", + visibility = ["//visibility:public"], + deps = [ + "//services/mctp/api:mctp_api", + "@pigweed//pw_log/rust:pw_log", + ], +) + +rust_test( + name = "mctp_echo_host_test", + srcs = ["tests/echo_host.rs"], + crate_root = "tests/echo_host.rs", + edition = "2024", + deps = [ + ":mctp_echo", + "//services/mctp/api:mctp_api", + "//services/mctp/server:mctp_server_lib", + "@rust_crates//:mctp", + "@rust_crates//:mctp-lib", + ], +)
diff --git a/services/mctp/echo/src/lib.rs b/services/mctp/echo/src/lib.rs new file mode 100644 index 0000000..0f811ed --- /dev/null +++ b/services/mctp/echo/src/lib.rs
@@ -0,0 +1,122 @@ +// Licensed under the Apache-2.0 license +// SPDX-License-Identifier: Apache-2.0 + +//! Reusable MCTP echo loop. +//! +//! This crate keeps the echo application policy separate from target wiring: +//! callers create an `openprot_mctp_api::Stack`, then hand it to the helpers +//! here to configure the local EID, open a listener, and echo received payloads +//! back to the sender. +//! +//! The loop can optionally send periodic test messages to a peer endpoint +//! to bootstrap communication when both endpoints are passive listeners. + +#![no_std] + +use openprot_mctp_api::{MctpClient, MctpError, MctpReqChannel, MctpRespChannel, Stack, StackListener}; + +/// Default MCTP message type used by the echo app. +pub const ECHO_MSG_TYPE: u8 = 1; + +/// Default local EID used by the echo app. +pub const ECHO_EID: u8 = 8; + +/// Prepare a stack for echoing by setting the local EID and opening a listener. +pub fn prepare_listener<C: MctpClient>(stack: &Stack<C>) -> Result<StackListener<'_, C>, MctpError> { + stack.set_eid(ECHO_EID)?; + stack.listener(ECHO_MSG_TYPE, 0) +} + +/// Prepare a stack with a custom EID and timeout for echoing. +pub fn prepare_listener_with_eid_and_timeout<C: MctpClient>( + stack: &Stack<C>, + eid: u8, + timeout_millis: u32, +) -> Result<StackListener<'_, C>, MctpError> { + stack.set_eid(eid)?; + stack.listener(ECHO_MSG_TYPE, timeout_millis) +} + +/// Process one echo receive/send cycle. +/// +/// Returns an error from either receive or response send. +pub fn echo_once<L>(listener: &mut L, buf: &mut [u8]) -> Result<(), MctpError> +where + L: openprot_mctp_api::MctpListener, +{ + let (_meta, msg, mut resp) = listener.recv(buf)?; + resp.send(msg) +} + +/// Run the echo loop forever, echoing received messages. +pub fn run<L>(listener: &mut L) -> ! +where + L: openprot_mctp_api::MctpListener, +{ + let mut buf = [0u8; 255]; + loop { + match echo_once(listener, &mut buf) { + Ok(()) => {} + Err(e) => { + if e.code as u32 != 4 { + // Suppress timeout (code 4) errors; only log other errors + pw_log::error!("echo recv failed: code={}", e.code as u32); + } + } + } + } +} + +/// Run the echo loop with periodic sends to a peer endpoint. +/// +/// This variant sends a test message every `send_interval` receive attempts, +/// allowing two passive listeners to bootstrap communication. +pub fn run_with_peer<C: MctpClient, L: openprot_mctp_api::MctpListener>( + stack: &Stack<C>, + peer_eid: u8, + listener: &mut L, +) -> ! { + run_with_peer_round_trip_limit(stack, peer_eid, listener, u32::MAX) +} + +/// Run the echo loop with periodic sends to a peer endpoint, stopping after +/// `max_round_trips` successful request/response exchanges. +pub fn run_with_peer_round_trip_limit<C: MctpClient, L: openprot_mctp_api::MctpListener>( + stack: &Stack<C>, + peer_eid: u8, + listener: &mut L, + max_round_trips: u32, +) -> ! { + let mut buf = [0u8; 255]; + let mut iteration: u32 = 0; + let mut completed_round_trips: u32 = 0; + let send_interval = 10; // Send every 10 iterations + + loop { + iteration = iteration.wrapping_add(1); + + // Periodically try to send a test message to the peer. + if iteration % send_interval == 0 + && completed_round_trips < max_round_trips + && let Ok(mut req) = stack.req(peer_eid, 100) + { + let test_msg = b"echo_test"; + let _ = req.send(ECHO_MSG_TYPE, test_msg); + // Try to receive response with a short timeout. + if req.recv(&mut buf).is_ok() { + completed_round_trips = completed_round_trips.saturating_add(1); + } + } + + // Listen for incoming messages and echo them back. + match echo_once(listener, &mut buf) { + Ok(()) => {} + Err(e) => { + if e.code as u32 != 4 { + // Suppress timeout (code 4) errors; only log other errors + pw_log::error!("echo recv failed: code={}", e.code as u32); + } + } + } + } +}
diff --git a/services/mctp/echo/tests/echo_host.rs b/services/mctp/echo/tests/echo_host.rs new file mode 100644 index 0000000..b3b939e --- /dev/null +++ b/services/mctp/echo/tests/echo_host.rs
@@ -0,0 +1,150 @@ +// Licensed under the Apache-2.0 license +// SPDX-License-Identifier: Apache-2.0 + +//! Host integration test for the reusable echo crate. +//! +//! Exercises the echo path via Stack + MctpClient against two in-memory +//! server instances. This validates that openprot_mctp_echo helpers can be +//! used in a host-only environment without IPC/I2C transport. + +use core::cell::RefCell; + +use mctp::{Eid, Tag}; +use mctp_lib::fragment::{Fragmenter, SendOutput}; +use mctp_lib::Sender; +use openprot_mctp_api::{Handle, MctpClient, MctpError, MctpReqChannel, RecvMetadata, Stack}; +use openprot_mctp_echo::{echo_once, prepare_listener, ECHO_MSG_TYPE}; +use openprot_mctp_server::Server; + +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>, +) { + let pkts = packets.borrow(); + for pkt in pkts.iter() { + dest.inbound(pkt).expect("inbound should accept packet"); + } +} + +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(openprot_mctp_api::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); + } +} + +#[test] +fn echo_path_roundtrip_via_stack_and_echo_helper() { + let buf_a = RefCell::new(Vec::new()); + let sender_a = BufferSender { packets: &buf_a }; + let server_a: RefCell<Server<_, 16>> = RefCell::new(Server::new(Eid(8), 0, sender_a)); + + let buf_b = RefCell::new(Vec::new()); + let sender_b = BufferSender { packets: &buf_b }; + let server_b: RefCell<Server<_, 16>> = RefCell::new(Server::new(Eid(42), 0, sender_b)); + + let client_a = DirectClient::new(&server_a); + let client_b = DirectClient::new(&server_b); + + let stack_a = Stack::new(client_a); + let stack_b = Stack::new(client_b); + + let mut listener_a = prepare_listener(&stack_a).expect("listener setup should succeed"); + + let mut req_b = stack_b.req(8, 0).expect("request channel should open"); + let payload = b"echo from host test"; + req_b.send(ECHO_MSG_TYPE, payload) + .expect("request send should succeed"); + + // Deliver request packets from B -> A, run one echo step, then A -> B. + transfer(&buf_b, &mut server_a.borrow_mut()); + buf_b.borrow_mut().clear(); + + let mut echo_buf = [0u8; 255]; + echo_once(&mut listener_a, &mut echo_buf).expect("echo_once should receive and reply"); + + transfer(&buf_a, &mut server_b.borrow_mut()); + buf_a.borrow_mut().clear(); + + let mut resp_buf = [0u8; 255]; + let (meta, response) = req_b.recv(&mut resp_buf).expect("response should arrive"); + assert_eq!(meta.msg_type, ECHO_MSG_TYPE); + assert_eq!(meta.remote_eid, 8); + assert_eq!(response, payload); +}
diff --git a/services/mctp/server/BUILD.bazel b/services/mctp/server/BUILD.bazel new file mode 100644 index 0000000..e8ab5cf --- /dev/null +++ b/services/mctp/server/BUILD.bazel
@@ -0,0 +1,90 @@ +# Licensed under the Apache-2.0 license +# SPDX-License-Identifier: Apache-2.0 + +load("@rules_rust//rust:defs.bzl", "rust_library", "rust_test") + +rust_library( + name = "mctp_server_lib", + srcs = [ + "src/dispatch.rs", + "src/lib.rs", + "src/server.rs", + ], + crate_name = "openprot_mctp_server", + edition = "2024", + visibility = ["//visibility:public"], + deps = [ + "//services/mctp/api:mctp_api", + "@rust_crates//:heapless", + "@rust_crates//:mctp", + "@rust_crates//:mctp-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/common/mod.rs", + "tests/echo.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/common/mod.rs", + "tests/dispatch.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/common/mod.rs", + "tests/server_unit.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/common/mod.rs", + "tests/integration.rs", + ], + crate_root = "tests/integration.rs", + edition = "2024", + deps = [ + ":mctp_server_lib", + "//services/mctp/api:mctp_api", + "@rust_crates//:mctp", + "@rust_crates//:mctp-lib", + ], +)
diff --git a/services/mctp/server/src/dispatch.rs b/services/mctp/server/src/dispatch.rs new file mode 100644 index 0000000..15a11f6 --- /dev/null +++ b/services/mctp/server/src/dispatch.rs
@@ -0,0 +1,128 @@ +// Licensed under the Apache-2.0 license +// SPDX-License-Identifier: Apache-2.0 + +//! MCTP IPC request dispatch. +//! +//! Decodes wire-protocol requests and dispatches them to the [`Server`]. +//! This is the server-side counterpart of `openprot-mctp-client`. + +use openprot_mctp_api::wire::{ + self, flags, MctpOp, MctpRequestHeader, +}; +use openprot_mctp_api::ResponseCode; + +use crate::{Sender, Server}; + +/// Dispatch an IPC request to the MCTP server. +/// +/// Decodes the request header, calls the appropriate `Server` method, +/// and encodes the response into `response`. Returns the response length. +/// +/// This is the MCTP equivalent of `dispatch_i2c_op` in the I2C server. +pub fn dispatch_mctp_op<S: Sender, const N: usize>( + request: &[u8], + response: &mut [u8], + server: &mut Server<S, N>, + recv_buf: &mut [u8], +) -> usize { + let header = match MctpRequestHeader::from_bytes(request) { + Some(h) => h, + None => return encode_error(response, ResponseCode::BadArgument), + }; + + let Some(op) = header.operation() else { + return encode_error(response, ResponseCode::BadArgument); + }; + + match op { + MctpOp::SetEid => match server.set_eid(header.eid) { + Ok(()) => encode_success(response), + Err(e) => encode_error(response, e.code), + }, + + MctpOp::GetEid => { + let eid = server.get_eid(); + wire::encode_get_eid_response(response, eid) + .unwrap_or_else(|_| encode_error(response, ResponseCode::InternalError)) + } + + MctpOp::Listener => match server.listener(header.msg_type) { + Ok(handle) => wire::encode_handle_response(response, handle.0) + .unwrap_or_else(|_| encode_error(response, ResponseCode::InternalError)), + Err(e) => encode_error(response, e.code), + }, + + MctpOp::Req => match server.req(header.eid) { + Ok(handle) => wire::encode_handle_response(response, handle.0) + .unwrap_or_else(|_| encode_error(response, ResponseCode::InternalError)), + Err(e) => encode_error(response, e.code), + }, + + MctpOp::Recv => { + let handle = openprot_mctp_api::Handle(header.handle); + + match server.try_recv(handle, recv_buf) { + Some(meta) => { + let payload = &recv_buf[..meta.payload_size]; + wire::encode_recv_response( + response, + meta.msg_type, + meta.msg_ic, + meta.remote_eid, + meta.msg_tag, + payload, + ) + .unwrap_or_else(|_| encode_error(response, ResponseCode::InternalError)) + } + None => { + // No message available yet. + // In a real Pigweed server, we'd register a pending recv + // and respond later. For now, return TimedOut. + encode_error(response, ResponseCode::TimedOut) + } + } + } + + MctpOp::Send => { + let handle = if header.flags & flags::HAS_HANDLE != 0 { + Some(openprot_mctp_api::Handle(header.handle)) + } else { + None + }; + let eid = if header.flags & flags::HAS_EID != 0 { + Some(header.eid) + } else { + None + }; + let tag = if header.flags & flags::HAS_TAG != 0 { + Some(header.tag) + } else { + None + }; + let ic = header.flags & flags::IC != 0; + let payload = wire::get_request_payload(request); + + match server.send(handle, header.msg_type, eid, tag, ic, payload) { + Ok(tag_val) => wire::encode_send_response(response, tag_val) + .unwrap_or_else(|_| encode_error(response, ResponseCode::InternalError)), + Err(e) => encode_error(response, e.code), + } + } + + MctpOp::Unbind => { + let handle = openprot_mctp_api::Handle(header.handle); + match server.unbind(handle) { + Ok(()) => encode_success(response), + Err(e) => encode_error(response, e.code), + } + } + } +} + +fn encode_error(response: &mut [u8], code: ResponseCode) -> usize { + wire::encode_error_response(response, code).unwrap_or(0) +} + +fn encode_success(response: &mut [u8]) -> usize { + wire::encode_success_response(response).unwrap_or(0) +}
diff --git a/services/mctp/server/src/lib.rs b/services/mctp/server/src/lib.rs new file mode 100644 index 0000000..f61494c --- /dev/null +++ b/services/mctp/server/src/lib.rs
@@ -0,0 +1,35 @@ +// Licensed under the Apache-2.0 license +// SPDX-License-Identifier: Apache-2.0 + +//! # MCTP Server +//! +//! Platform-independent MCTP server implementation for OpenPRoT. +//! +//! This crate provides the core MCTP server logic that manages: +//! - Listener and request handle allocation (via `mctp-lib` [`Router`](mctp_lib::Router)) +//! - Inbound message routing to registered listeners +//! - Outbound message fragmentation and sending +//! - Timeout management for pending receive calls +//! +//! ## Transport Bindings +//! +//! The server is generic over the `mctp-lib` [`Sender`](mctp_lib::Sender) trait +//! for outbound transport. Transport-specific bindings (I2C, serial) implement +//! this trait and feed inbound packets via [`Server::inbound`]. +//! +//! ## Platform Integration +//! +//! The server does not depend on any OS primitives. The platform layer +//! is responsible for: +//! - Driving the event loop (notifications, IPC dispatch) +//! - Providing a time source via [`Server::update`] +//! - Wiring up transport bindings + +#![no_std] +#![warn(missing_docs)] + +pub mod dispatch; +mod server; + +pub use mctp_lib::Sender; +pub use server::{RecvResult, Server, ServerConfig};
diff --git a/services/mctp/server/src/server.rs b/services/mctp/server/src/server.rs new file mode 100644 index 0000000..ab4c6c0 --- /dev/null +++ b/services/mctp/server/src/server.rs
@@ -0,0 +1,294 @@ +// Licensed under the Apache-2.0 license +// SPDX-License-Identifier: Apache-2.0 + +//! Core MCTP server logic. +//! +//! This is a direct port of the Hubris `mctp-server/src/server.rs`. +//! The `Router` integration, handle management, timeout logic, and message +//! routing are preserved as-is. Only Hubris IPC primitives (`sys_reply`, +//! `Leased`, `RecvMessage`) have been replaced with platform-independent +//! equivalents. + +use heapless::LinearMap; +use mctp::{Eid, MsgIC, MsgType, Tag, TagValue}; +use mctp_lib::{AppCookie, Router, Sender}; +use openprot_mctp_api::{Handle, MctpError, RecvMetadata, ResponseCode}; + +/// Maximum payload size in bytes. +// TODO: Use configuration from mctp-lib (mctp-estack) +// see https://github.com/OpenPRoT/mctp-lib/issues/4 +const MAX_PAYLOAD: usize = 1023; + +/// Configuration constants for the MCTP server. +pub struct ServerConfig; + +impl ServerConfig { + /// Maximum number of concurrent requests the server can handle. + pub const MAX_REQUESTS: usize = 8; + /// Maximum number of listeners that can be registered concurrently. + pub const MAX_LISTENERS: usize = 8; + /// Maximum number of concurrent outstanding receive calls. + pub const MAX_OUTSTANDING: usize = 16; + /// Maximum payload size in bytes. + pub const MAX_PAYLOAD: usize = MAX_PAYLOAD; +} + +/// A pending receive call waiting for a message or timeout. +#[derive(Debug, Clone, Copy)] +struct PendingRecv { + /// Deadline in milliseconds (0 = no timeout). + deadline: u64, +} + +/// The platform-independent MCTP server. +/// +/// This struct wraps the `mctp-lib` [`Router`] and manages outstanding +/// receive calls with timeout tracking. It is a direct port of the +/// Hubris `Server` struct with OS-specific IPC removed. +/// +/// # Type Parameters +/// +/// * `S` - The [`Sender`] implementation for outbound transport. +/// * `OUTSTANDING` - Maximum number of concurrent pending receive calls. +pub struct Server<S: Sender, const OUTSTANDING: usize> { + /// The underlying MCTP router (from mctp-lib). + pub stack: Router<S, { ServerConfig::MAX_LISTENERS }, { ServerConfig::MAX_REQUESTS }>, + /// Currently outstanding recv calls, keyed by handle value. + /// + /// Maps the handle to a deadline. The platform layer is responsible + /// for storing any additional per-recv state (e.g., reply channels). + outstanding: LinearMap<u32, PendingRecv, OUTSTANDING>, +} + +impl<S: Sender, const OUTSTANDING: usize> Server<S, OUTSTANDING> { + /// Create a new MCTP server instance. + pub fn new(own_eid: Eid, now_millis: u64, outbound: S) -> Self { + let stack = Router::new(own_eid, now_millis, outbound); + Self { + stack, + outstanding: LinearMap::new(), + } + } + + /// Allocate a request handle for sending messages to the given EID. + pub fn req(&mut self, eid: u8) -> Result<Handle, MctpError> { + match self.stack.req(Eid(eid)) { + Ok(cookie) => Ok(Handle(cookie.0 as u32)), + Err(e) => Err(mctp_error_to_server_error(e)), + } + } + + /// Register a listener for incoming messages of the given type. + pub fn listener(&mut self, typ: u8) -> Result<Handle, MctpError> { + match self.stack.listener(MsgType(typ)) { + Ok(cookie) => Ok(Handle(cookie.0 as u32)), + Err(e) => Err(mctp_error_to_server_error(e)), + } + } + + /// Get the currently configured EID. + pub fn get_eid(&self) -> u8 { + self.stack.get_eid().0 + } + + /// Set the EID for this endpoint. + pub fn set_eid(&mut self, eid: u8) -> Result<(), MctpError> { + self.stack + .set_eid(Eid(eid)) + .map_err(mctp_error_to_server_error) + } + + /// Check for an available message on the given handle. + /// + /// If a message is available, returns the metadata and copies the + /// payload into `buf`. Otherwise returns `None` and the caller + /// should register a pending recv via [`register_recv`](Self::register_recv). + pub fn try_recv( + &mut self, + handle: Handle, + buf: &mut [u8], + ) -> Option<RecvMetadata> { + let cookie = AppCookie(handle.0 as usize); + let msg = self.stack.recv(cookie)?; + + let payload_len = msg.payload.len(); + if payload_len <= buf.len() { + buf[..payload_len].copy_from_slice(msg.payload); + } + + Some(RecvMetadata { + msg_type: msg.typ.0, + msg_ic: msg.ic.0, + msg_tag: msg.tag.tag().0, + remote_eid: msg.source.0, + payload_size: payload_len, + }) + } + + /// Register a pending receive call for the given handle. + /// + /// The platform layer should call this when `try_recv` returns `None` + /// and the client wants to block. Returns an error if the outstanding + /// table is full. + pub fn register_recv( + &mut self, + handle: Handle, + timeout_millis: u32, + now_millis: u64, + ) -> Result<(), MctpError> { + let deadline = if timeout_millis != 0 { + now_millis + timeout_millis as u64 + } else { + 0 + }; + + // Don't overwrite existing entries + if self.outstanding.contains_key(&handle.0) { + return Ok(()); + } + + self.outstanding + .insert(handle.0, PendingRecv { deadline }) + .map_err(|_| MctpError::from_code(ResponseCode::NoSpace))?; + Ok(()) + } + + /// Send a message. + /// + /// For requests, `handle` is `Some`. For responses, `handle` is `None`. + /// When responding to a request received by a listener, `eid` and `tag` + /// must be set. Returns the tag value used. + pub fn send( + &mut self, + handle: Option<Handle>, + typ: u8, + eid: Option<u8>, + tag: Option<u8>, + ic: bool, + buf: &[u8], + ) -> Result<u8, MctpError> { + if buf.len() > MAX_PAYLOAD { + return Err(MctpError::from_code(ResponseCode::NoSpace)); + } + + let tag = if handle.is_none() { + // Responses use unowned tags + tag.map(|x| Tag::Unowned(TagValue(x))) + } else { + // Requests use owned tags (or allocate a new one) + tag.map(|x| Tag::Owned(TagValue(x))) + }; + + // Responses need no handle, use 255 as dummy + let cookie = AppCookie(handle.unwrap_or(Handle(255)).0 as usize); + + let result = self.stack.send( + eid.map(Eid), + MsgType(typ), + tag, + MsgIC(ic), + cookie, + buf, + ); + + match result { + Ok(tag) => Ok(tag.tag().0), + Err(e) => Err(mctp_error_to_server_error(e)), + } + } + + /// Update the stack and check for fulfilled receive calls. + /// + /// Should be called on timer events. Returns the interval (ms) until + /// the next required update, and a list of handles that now have + /// messages available (the platform layer should deliver them). + pub fn update( + &mut self, + now_millis: u64, + recv_buf: &mut [u8], + ) -> (u32, heapless::Vec<(Handle, RecvResult), OUTSTANDING>) { + // Update the mctp-stack; get the next timeout interval + let stack_timeout = self + .stack + .update(now_millis) + .unwrap_or(60_000) as u32; + + let mut ready: heapless::Vec<(Handle, RecvResult), OUTSTANDING> = heapless::Vec::new(); + + for (handle_val, pending) in self.outstanding.iter() { + let handle = Handle(*handle_val); + let cookie = AppCookie(*handle_val as usize); + + // Check if a message arrived for this handle + if let Some(mctp_msg) = self.stack.recv(cookie) { + let payload_len = mctp_msg.payload.len(); + if payload_len <= recv_buf.len() { + recv_buf[..payload_len].copy_from_slice(mctp_msg.payload); + } + let metadata = RecvMetadata { + msg_type: mctp_msg.typ.0, + msg_ic: mctp_msg.ic.0, + msg_tag: mctp_msg.tag.tag().0, + remote_eid: mctp_msg.source.0, + payload_size: payload_len, + }; + let _ = ready.push((handle, RecvResult::Message(metadata))); + continue; + } + + // Check for timeout + if pending.deadline != 0 && now_millis >= pending.deadline { + let _ = ready.push((handle, RecvResult::TimedOut)); + } + } + + // Remove fulfilled/timed-out entries + for (handle, _) in &ready { + self.outstanding.remove(&handle.0); + } + + (stack_timeout, ready) + } + + /// Unbind a handle previously allocated by `req` or `listener`. + pub fn unbind(&mut self, handle: Handle) -> Result<(), MctpError> { + let cookie = AppCookie(handle.0 as usize); + let _ = self.stack.unbind(cookie); + self.outstanding.remove(&handle.0); + Ok(()) + } + + /// Feed an inbound MCTP packet to the router. + /// + /// The platform layer calls this when data arrives from a transport + /// binding. The packet should be a raw MCTP packet without transport + /// headers (the transport binding strips those). + pub fn inbound(&mut self, pkt: &[u8]) -> Result<(), MctpError> { + self.stack + .inbound(pkt) + .map_err(mctp_error_to_server_error) + } +} + +/// Result of a pending receive call. +#[derive(Debug, Clone, Copy)] +pub enum RecvResult { + /// A message was received. + Message(RecvMetadata), + /// The receive call timed out. + TimedOut, +} + +/// Map mctp::Error to our MctpError. +fn mctp_error_to_server_error(e: mctp::Error) -> MctpError { + use mctp::Error::*; + let code = match e { + InternalError => ResponseCode::InternalError, + NoSpace => ResponseCode::NoSpace, + AddrInUse => ResponseCode::AddrInUse, + TimedOut => ResponseCode::TimedOut, + BadArgument => ResponseCode::BadArgument, + _ => ResponseCode::InternalError, + }; + MctpError::from_code(code) +}
diff --git a/services/mctp/server/tests/common/mod.rs b/services/mctp/server/tests/common/mod.rs new file mode 100644 index 0000000..5a747ba --- /dev/null +++ b/services/mctp/server/tests/common/mod.rs
@@ -0,0 +1,357 @@ +// Licensed under the Apache-2.0 license +// SPDX-License-Identifier: Apache-2.0 + +//! Shared test fixtures for MCTP server integration tests. +//! +//! Provides: +//! - [`BufferSender`] — captures outbound packets into a `Vec` +//! - [`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` +//! - [`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. +/// +/// `DirectClient` allows application code written against `MctpClient` to be +/// exercised in pure `std` tests without platform transport dependencies. +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 new file mode 100644 index 0000000..3aab24d --- /dev/null +++ b/services/mctp/server/tests/dispatch.rs
@@ -0,0 +1,263 @@ +// Licensed under the Apache-2.0 license +// SPDX-License-Identifier: Apache-2.0 + +//! Wire-protocol dispatch integration test. +//! +//! Exercises the full request path: encode request → `dispatch_mctp_op` → decode +//! response. This verifies that the wire protocol + dispatch layer + Server +//! work together correctly for a client/server integration boundary. + +mod common; + +use std::cell::RefCell; + +use mctp::Eid; +use openprot_mctp_api::wire; +use openprot_mctp_server::{dispatch::dispatch_mctp_op, Server}; + +use common::{transfer, BufferSender}; + + +// --------------------------------------------------------------------------- +// Tests +// --------------------------------------------------------------------------- + +/// Test SetEid + GetEid via dispatch. +#[test] +fn dispatch_set_get_eid() { + let buf = RefCell::new(Vec::new()); + let sender = BufferSender { packets: &buf }; + let mut server: Server<_, 16> = Server::new(Eid(0), 0, sender); + + let mut req = [0u8; 64]; + let mut resp = [0u8; 64]; + let mut recv_buf = [0u8; 255]; + + // SetEid(42) + let req_len = wire::encode_set_eid(&mut req, 42).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()); + + // GetEid → 42 + let req_len = wire::encode_get_eid(&mut req).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.eid, 42); +} + +/// Test Listener + Req + Send + Recv via dispatch (full echo round-trip). +#[test] +fn dispatch_echo_roundtrip() { + // Server A (echo responder, EID 8) + let buf_a = RefCell::new(Vec::new()); + let sender_a = BufferSender { packets: &buf_a }; + let mut server_a: Server<_, 16> = Server::new(Eid(8), 0, sender_a); + + // Server B (requester, EID 42) + let buf_b = RefCell::new(Vec::new()); + let sender_b = BufferSender { packets: &buf_b }; + let mut server_b: Server<_, 16> = Server::new(Eid(42), 0, sender_b); + + let mut req = [0u8; 128]; + let mut resp = [0u8; 128]; + let mut recv_buf = [0u8; 255]; + + // Register listener on A for MsgType(1) + let req_len = wire::encode_listener(&mut req, 1).unwrap(); + let resp_len = dispatch_mctp_op(&req[..req_len], &mut resp, &mut server_a, &mut recv_buf); + let header = wire::decode_response_header(&resp[..resp_len]).unwrap(); + assert!(header.is_success()); + let listener_handle = header.handle; + + // Register req on B targeting EID 8 + let req_len = wire::encode_req(&mut req, 8).unwrap(); + let resp_len = dispatch_mctp_op(&req[..req_len], &mut resp, &mut server_b, &mut recv_buf); + let header = wire::decode_response_header(&resp[..resp_len]).unwrap(); + assert!(header.is_success()); + let req_handle = header.handle; + + // B sends a message via dispatch + let payload = b"dispatch echo!"; + let req_len = wire::encode_send( + &mut req, + Some(req_handle), + 1, + None, + None, + false, + payload, + ) + .unwrap(); + let resp_len = dispatch_mctp_op(&req[..req_len], &mut resp, &mut server_b, &mut recv_buf); + let header = wire::decode_response_header(&resp[..resp_len]).unwrap(); + assert!(header.is_success()); + + // Transfer B → A + transfer(&buf_b, &mut server_a); + + // A receives via dispatch + 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_a, &mut recv_buf); + let header = wire::decode_response_header(&resp[..resp_len]).unwrap(); + assert!(header.is_success()); + assert_eq!(header.msg_type, 1); + assert_eq!(header.eid, 42); // remote EID + let recv_payload = wire::get_response_payload(&resp[..resp_len], &header).unwrap(); + assert_eq!(recv_payload, payload); + + // A echoes back via dispatch (response: no handle, set eid + tag) + let req_len = wire::encode_send( + &mut req, + None, + header.msg_type, + Some(header.eid), + Some(header.tag), + false, + recv_payload, + ) + .unwrap(); + let resp_len = dispatch_mctp_op(&req[..req_len], &mut resp, &mut server_a, &mut recv_buf); + let send_header = wire::decode_response_header(&resp[..resp_len]).unwrap(); + assert!(send_header.is_success()); + + // Transfer A → B + transfer(&buf_a, &mut server_b); + + // B receives the echo via dispatch + let req_len = wire::encode_recv(&mut req, req_handle, 0).unwrap(); + let resp_len = dispatch_mctp_op(&req[..req_len], &mut resp, &mut server_b, &mut recv_buf); + let header = wire::decode_response_header(&resp[..resp_len]).unwrap(); + assert!(header.is_success()); + assert_eq!(header.msg_type, 1); + assert_eq!(header.eid, 8); // from server A + 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 new file mode 100644 index 0000000..8c13f71 --- /dev/null +++ b/services/mctp/server/tests/echo.rs
@@ -0,0 +1,165 @@ +// Licensed under the Apache-2.0 license +// SPDX-License-Identifier: Apache-2.0 + +//! MCTP echo integration test. +//! +//! This test exercises the full MCTP server stack with a mock transport, +//! replicating a production echo task behavior: +//! +//! 1. Server A (echo responder): listens for MCTP type-1 messages, echoes payload back +//! 2. Server B (requester): sends a request to A and verifies the echo response +//! +//! The test uses a **client/server partition**: the echo application logic +//! interacts exclusively through the `MctpClient` trait (client side), while +//! the `Server` + transport plumbing is the server side. + +mod common; + +use std::cell::RefCell; + +use mctp::Eid; +use openprot_mctp_api::{Handle, MctpClient}; +use openprot_mctp_server::Server; + +use common::{transfer, BufferSender, DirectClient}; + + +// --------------------------------------------------------------------------- +// Echo application logic (client side) +// --------------------------------------------------------------------------- + +/// Echo one message: receive on the listener, send the payload back. +/// +/// This is the same shape as production echo logic: +/// ```ignore +/// let (_, _, msg, mut resp) = listener.recv(&mut recv_buf).unwrap_lite(); +/// resp.send(msg).unwrap(); +/// ``` +/// but expressed through the `MctpClient` trait. +fn echo_once(client: &impl MctpClient, listener_handle: Handle) { + let mut recv_buf = [0u8; 255]; + let meta = client + .recv(listener_handle, 0, &mut recv_buf) + .expect("echo: should receive a message"); + + let payload = &recv_buf[..meta.payload_size]; + client + .send( + None, + meta.msg_type, + Some(meta.remote_eid), + Some(meta.msg_tag), + false, + payload, + ) + .expect("echo: should send response"); +} + +// --------------------------------------------------------------------------- +// Tests +// --------------------------------------------------------------------------- + +/// MCTP echo: send a request, receive it on a listener, echo back, verify. +/// +/// This replicates a production echo task behavior: +/// - EID 8 listens for MsgType(1) and echoes the payload +/// - EID 42 sends a request and checks the response matches +#[test] +fn mctp_echo_roundtrip() { + // -- Server side: set up two MCTP server instances with mock transport -- + let buf_a = RefCell::new(Vec::new()); + let sender_a = BufferSender { packets: &buf_a }; + let server_a: RefCell<Server<_, 16>> = + RefCell::new(Server::new(Eid(8), 0, sender_a)); + + let buf_b = RefCell::new(Vec::new()); + let sender_b = BufferSender { packets: &buf_b }; + let server_b: RefCell<Server<_, 16>> = + RefCell::new(Server::new(Eid(42), 0, sender_b)); + + // -- Client side: wrap servers in DirectClient to use MctpClient trait -- + let client_a = DirectClient::new(&server_a); + let client_b = DirectClient::new(&server_b); + + // Client A: register listener for MsgType(1) — same as echo task + let listener_handle = client_a.listener(1).unwrap(); + + // Client B: get a request handle targeting EID 8 + let req_handle = client_b.req(8).unwrap(); + + // Client B: send a request with MsgType(1) + let payload = b"Hello MCTP echo!"; + let _tag = client_b + .send(Some(req_handle), 1, None, None, false, payload) + .unwrap(); + + // Server side: transfer B's outbound packets to A + transfer(&buf_b, &mut server_a.borrow_mut()); + + // Client A: echo the message back (uses MctpClient trait) + echo_once(&client_a, listener_handle); + + // Server side: transfer A's outbound packets to B + transfer(&buf_a, &mut server_b.borrow_mut()); + + // Client B: receive the echo response (uses MctpClient trait) + let mut resp_buf = [0u8; 255]; + let resp_meta = client_b + .recv(req_handle, 0, &mut resp_buf) + .expect("Client B should have received the echo response"); + + let response = &resp_buf[..resp_meta.payload_size]; + assert_eq!(response, payload, "Echo response should match original payload"); + assert_eq!(resp_meta.msg_type, 1); + assert_eq!(resp_meta.remote_eid, 8); + + // Clean up + client_a.drop_handle(listener_handle); + client_b.drop_handle(req_handle); +} + +/// Test that multiple messages can be echoed in sequence. +#[test] +fn mctp_echo_multiple() { + let buf_a = RefCell::new(Vec::new()); + let sender_a = BufferSender { packets: &buf_a }; + let server_a: RefCell<Server<_, 16>> = + RefCell::new(Server::new(Eid(8), 0, sender_a)); + + let buf_b = RefCell::new(Vec::new()); + let sender_b = BufferSender { packets: &buf_b }; + let server_b: RefCell<Server<_, 16>> = + RefCell::new(Server::new(Eid(42), 0, sender_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(); + + for i in 0..5u8 { + let msg = [i; 32]; + + // Client B: send request + client_b + .send(Some(req), 1, None, None, false, &msg) + .unwrap(); + transfer(&buf_b, &mut server_a.borrow_mut()); + buf_b.borrow_mut().clear(); + + // Client A: echo (uses MctpClient trait) + echo_once(&client_a, listener); + transfer(&buf_a, &mut server_b.borrow_mut()); + buf_a.borrow_mut().clear(); + + // Client B: verify echo response + let mut resp_buf = [0u8; 255]; + let resp = client_b + .recv(req, 0, &mut resp_buf) + .expect("echo response should be available"); + assert_eq!(&resp_buf[..resp.payload_size], &msg); + } + + client_a.drop_handle(listener); + client_b.drop_handle(req); +}
diff --git a/services/mctp/server/tests/integration.rs b/services/mctp/server/tests/integration.rs new file mode 100644 index 0000000..cfe1a54 --- /dev/null +++ b/services/mctp/server/tests/integration.rs
@@ -0,0 +1,548 @@ +// Licensed under the Apache-2.0 license +// SPDX-License-Identifier: Apache-2.0 + +//! 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 platform transport binding is used anywhere in this file. + +mod common; + +use std::cell::RefCell; + +use mctp::Eid; +use openprot_mctp_api::stack::Stack; +use openprot_mctp_api::{MctpClient, MctpListener, MctpReqChannel, MctpRespChannel}; +use openprot_mctp_server::Server; +use openprot_mctp_server::ServerConfig; + +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 +/// platform-specific bindings: +/// ``` +/// 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) + 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 + let sent_tag = 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); + assert_eq!(resp.msg_tag, sent_tag); +} + +// --------------------------------------------------------------------------- +// Stack facade (openprot-mctp-api::stack) +// --------------------------------------------------------------------------- +// +// These tests exercise `Stack<DirectClient>` — the same code path used by the +// real application (`mctp_echo.rs` with `Stack<IpcMctpClient>`), but running +// entirely on the host with no platform transport binding. + +/// Echo via `Stack::listener` → `StackListener::recv` → `StackRespChannel::send`. +/// +/// This is the exact sequence used by `mctp_echo.rs`: +/// ```ignore +/// let mut listener = stack.listener(ECHO_MSG_TYPE, 0).unwrap(); +/// let (meta, msg, mut resp) = listener.recv(&mut buf).unwrap(); +/// resp.send(msg).unwrap(); +/// ``` +#[test] +fn stack_listener_echo() { + 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 })); + + // Application A uses Stack — the same API as in production. + let stack_a = Stack::new(DirectClient::new(&server_a)); + // Application B uses a raw client to send the request and check the reply. + let client_b = DirectClient::new(&server_b); + + let mut listener = stack_a.listener(1, 0).expect("listener alloc"); + let req_handle = client_b.req(8).unwrap(); + + // B sends a request + client_b + .send(Some(req_handle), 1, None, None, false, b"hello from B") + .unwrap(); + transfer(&buf_b, &mut server_a.borrow_mut()); + + // A echoes back via Stack facade + let mut recv_buf = [0u8; 255]; + let (meta, payload, mut resp) = listener + .recv(&mut recv_buf) + .expect("stack listener should receive the message"); + + assert_eq!(payload, b"hello from B"); + assert_eq!(meta.remote_eid, 42); + + resp.send(payload).expect("stack resp send"); + + // Deliver 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], b"hello from B"); + assert_eq!(resp_meta.remote_eid, 8); + assert_eq!(resp_meta.msg_type, 1); +} + +/// Echo via `Stack::req` → `StackReqChannel::send` + `StackReqChannel::recv`. +#[test] +fn stack_req_channel_roundtrip() { + 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); + // B uses Stack for the request side. + let stack_b = Stack::new(DirectClient::new(&server_b)); + + let listener_handle = client_a.listener(1).unwrap(); + + let mut req = stack_b.req(8, 0).expect("req channel alloc"); + req.send(1, b"stack req test").expect("req send"); + assert_eq!(req.remote_eid(), 8); + + transfer(&buf_b, &mut server_a.borrow_mut()); + + // A echoes back manually + 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()); + + let mut resp_buf = [0u8; 255]; + let (resp_meta, resp_payload) = req.recv(&mut resp_buf).expect("req channel recv"); + + assert_eq!(resp_payload, b"stack req test"); + assert_eq!(resp_meta.remote_eid, 8); + assert_eq!(resp_meta.msg_type, 1); +} + +/// Calling `StackReqChannel::recv` before `send` returns `BadArgument`. +#[test] +fn stack_req_channel_recv_before_send_errors() { + let buf = RefCell::new(Vec::new()); + let server: RefCell<Server<_, 16>> = + RefCell::new(Server::new(Eid(8), 0, BufferSender { packets: &buf })); + let stack = Stack::new(DirectClient::new(&server)); + + let mut req = stack.req(42, 0).expect("req alloc"); + let mut resp_buf = [0u8; 255]; + let err = req.recv(&mut resp_buf).expect_err("recv before send must fail"); + assert_eq!(err.code, openprot_mctp_api::ResponseCode::BadArgument); +} + +/// A payload at exactly `ServerConfig::MAX_PAYLOAD` bytes round-trips. +#[test] +fn max_payload_roundtrip() { + 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(7).unwrap(); + let req = client_b.req(8).unwrap(); + + let payload = vec![0xA5; ServerConfig::MAX_PAYLOAD]; + let sent_tag = client_b + .send(Some(req), 7, None, None, false, &payload) + .unwrap(); + transfer(&buf_b, &mut server_a.borrow_mut()); + + let mut recv_buf = [0u8; 1024]; + let meta = client_a.recv(listener, 0, &mut recv_buf).unwrap(); + assert_eq!(meta.payload_size, payload.len()); + assert_eq!(&recv_buf[..meta.payload_size], payload.as_slice()); + + client_a + .send( + None, + meta.msg_type, + Some(meta.remote_eid), + Some(meta.msg_tag), + false, + &recv_buf[..meta.payload_size], + ) + .unwrap(); + transfer(&buf_a, &mut server_b.borrow_mut()); + + let mut resp_buf = [0u8; 1024]; + let resp = client_b.recv(req, 0, &mut resp_buf).unwrap(); + assert_eq!(resp.payload_size, payload.len()); + assert_eq!(&resp_buf[..resp.payload_size], payload.as_slice()); + assert_eq!(resp.msg_tag, sent_tag); +} + +/// Both sides use `Stack` — listener on A, request channel on B. +#[test] +fn stack_both_sides_echo() { + 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 stack_a = Stack::new(DirectClient::new(&server_a)); + let stack_b = Stack::new(DirectClient::new(&server_b)); + + let mut listener = stack_a.listener(1, 0).expect("listener alloc"); + let mut req = stack_b.req(8, 0).expect("req alloc"); + + // B sends + req.send(1, b"both sides").expect("req send"); + transfer(&buf_b, &mut server_a.borrow_mut()); + + // A receives and replies via Stack + let mut recv_buf = [0u8; 255]; + let (_, payload, mut resp) = listener.recv(&mut recv_buf).expect("listener recv"); + assert_eq!(payload, b"both sides"); + resp.send(payload).expect("resp send"); + transfer(&buf_a, &mut server_b.borrow_mut()); + + // B receives via Stack req channel + let mut resp_buf = [0u8; 255]; + let (meta, data) = req.recv(&mut resp_buf).expect("req recv"); + assert_eq!(data, b"both sides"); + assert_eq!(meta.remote_eid, 8); +} + +/// `Stack::get_eid` and `Stack::set_eid` delegate correctly. +#[test] +fn stack_eid_accessors() { + let server: RefCell<Server<_, 16>> = + RefCell::new(Server::new(Eid(8), 0, common::DroppingBufferSender)); + let stack = Stack::new(DirectClient::new(&server)); + + assert_eq!(stack.get_eid(), 8); + stack.set_eid(99).expect("set_eid should succeed"); + assert_eq!(stack.get_eid(), 99); +} +
diff --git a/services/mctp/server/tests/server_unit.rs b/services/mctp/server/tests/server_unit.rs new file mode 100644 index 0000000..165d472 --- /dev/null +++ b/services/mctp/server/tests/server_unit.rs
@@ -0,0 +1,242 @@ +// Licensed under the Apache-2.0 license +// SPDX-License-Identifier: Apache-2.0 + +//! Server unit tests — exercise `Server` methods directly with mock transport. +//! +//! 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); + } +}