| // Licensed under the Apache-2.0 license |
| |
| //! MCTP echo integration test. |
| //! |
| //! This test exercises the full MCTP server stack with a mock transport, |
| //! replicating the Hubris `mctp-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. This mirrors the |
| //! real deployment where the echo task is an IPC client of the MCTP server. |
| |
| use std::cell::RefCell; |
| |
| use mctp::{Eid, Tag}; |
| use mctp_stack::fragment::{Fragmenter, SendOutput}; |
| use mctp_stack::Sender; |
| use openprot_mctp_api::{Handle, MctpClient, MctpError, RecvMetadata, ResponseCode}; |
| use openprot_mctp_server::Server; |
| |
| // --------------------------------------------------------------------------- |
| // Mock transport |
| // --------------------------------------------------------------------------- |
| |
| /// 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) |
| // --------------------------------------------------------------------------- |
| |
| /// Echo one message: receive on the listener, send the payload back. |
| /// |
| /// This is the same logic as `hubris/task/mctp-echo/src/main.rs`: |
| /// ```ignore |
| /// let (_, _, msg, mut resp) = listener.recv(&mut recv_buf).unwrap_lite(); |
| /// resp.send(msg).unwrap(); |
| /// ``` |
| /// but expressed through the `MctpClient` trait instead of Hubris IPC types. |
| 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 the Hubris mctp-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) |
| .unwrap_or_else(|_| panic!("iteration {i}: no response")); |
| assert_eq!(&resp_buf[..resp.payload_size], &msg); |
| } |
| |
| client_a.drop_handle(listener); |
| client_b.drop_handle(req); |
| } |