Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
28 changes: 28 additions & 0 deletions services/mctp/transport-loopback/BUILD.bazel
Original file line number Diff line number Diff line change
@@ -0,0 +1,28 @@
# 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_transport_loopback",
srcs = [
"src/lib.rs",
"src/tests.rs",
],
crate_name = "openprot_mctp_transport_loopback",
edition = "2024",
visibility = ["//visibility:public"],
deps = [
"@rust_crates//:heapless",
"@rust_crates//:mctp",
"@rust_crates//:mctp-lib",
],
)

# Host tests: the round trip goes through a real server, which is why the
# server is a test-only dependency rather than a dependency of the library.
rust_test(
name = "mctp_transport_loopback_test",
crate = ":mctp_transport_loopback",
deps = ["//services/mctp/server:mctp_server_lib"],
)
160 changes: 160 additions & 0 deletions services/mctp/transport-loopback/src/lib.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,160 @@
// Licensed under the Apache-2.0 license
// SPDX-License-Identifier: Apache-2.0

//! An MCTP transport that goes nowhere: packets come straight back in.
//!
//! On a board the server's [`Sender`] hands fragments to a wire. In a QEMU
//! test there is no wire and no second chip, so both endpoints live in one
//! image. This sender queues what its router fragments, and the caller
//! feeds that queue to the *other* endpoint's `Server::inbound`. One
//! server's outbox is the other's wire.
//!
//! The queue is owned by the caller rather than by the sender, because the
//! router takes the sender by value and offers no way back to it. The
//! caller keeps [`LoopbackQueue`] and lends it to [`LoopbackSender`]:
//!
//! ```ignore
//! let out_a = LoopbackQueue::<8>::new();
//! let out_b = LoopbackQueue::<8>::new();
//! let mut a = Server::new(Eid(8), now, LoopbackSender::new(&out_a));
//! let mut b = Server::new(Eid(42), now, LoopbackSender::new(&out_b));
//! // a sends; what a queued is what b receives.
//! while let Some(pkt) = out_a.take() {
//! b.inbound(&pkt)?;
//! }
//! ```
//!
//! Two servers, not one looping to itself: a message addressed to a
//! server's own EID has no route out and back.
//!
//! Draining in the caller's loop rather than re-entering the router keeps
//! the send path non-blocking and the borrow straight, since the router
//! owns the sender and offers no way back to it.
//!
//! What this does not model: a wire that drops, reorders, delays or
//! corrupts. Every packet arrives, in order, as soon as the caller drains.
//! A test that wants retry or timeout behaviour needs something else.

#![cfg_attr(not(test), no_std)]
#![forbid(unsafe_code)]

use core::cell::RefCell;

use heapless::Deque;
use mctp::{Error, Result, Tag};
use mctp_lib::fragment::{Fragmenter, SendOutput};
use mctp_lib::Sender;

/// Largest payload this transport carries in one fragment. Nothing here is
/// limited by a bus, so the number only has to be agreed by both ends,
/// which it is by construction.
pub const LOOPBACK_MTU: usize = 255;

/// MCTP transport header, which rides in front of the payload. The
/// fragment buffer has to hold both: a buffer of exactly the MTU makes the
/// router reject the send rather than truncate it.
const MCTP_HEADER_SIZE: usize = 4;

/// One fragmented packet waiting to be fed back in, header included.
pub type Packet = heapless::Vec<u8, { LOOPBACK_MTU + MCTP_HEADER_SIZE }>;

/// Packets on their way from the router back to it.
///
/// `DEPTH` bounds how many fragments may be in flight between drains. A
/// caller that drains after every send needs one; one that sends a whole
/// message first needs as many fragments as that message takes.
pub struct LoopbackQueue<const DEPTH: usize> {
inner: RefCell<Inner<DEPTH>>,
}

struct Inner<const DEPTH: usize> {
packets: Deque<Packet, DEPTH>,
dropped: u32,
}

impl<const DEPTH: usize> Default for LoopbackQueue<DEPTH> {
fn default() -> Self {
Self::new()
}
}

impl<const DEPTH: usize> LoopbackQueue<DEPTH> {
/// An empty queue.
#[must_use]
pub const fn new() -> Self {
Self {
inner: RefCell::new(Inner {
packets: Deque::new(),
dropped: 0,
}),
}
}

/// Takes the oldest queued packet, or `None` when none are waiting.
/// Feed what this returns to `Server::inbound`.
pub fn take(&self) -> Option<Packet> {
self.inner.borrow_mut().packets.pop_front()
}

/// How many packets are waiting.
#[must_use]
pub fn pending(&self) -> usize {
self.inner.borrow().packets.len()
}

/// How many packets were discarded because the queue was full.
///
/// A wire drops packets silently and MCTP is built to cope, so a full
/// queue is not an error here. It is still worth reporting, because in
/// this transport it means `DEPTH` is too small rather than that the
/// medium is lossy, and the test that follows will fail for a reason
/// that looks unrelated.
#[must_use]
pub fn dropped(&self) -> u32 {
self.inner.borrow().dropped
}
}

/// The [`Sender`] half. Holds no packets of its own; everything goes to the
/// queue the caller kept.
pub struct LoopbackSender<'q, const DEPTH: usize> {
queue: &'q LoopbackQueue<DEPTH>,
}

impl<'q, const DEPTH: usize> LoopbackSender<'q, DEPTH> {
/// Lends `queue` to the router.
#[must_use]
pub const fn new(queue: &'q LoopbackQueue<DEPTH>) -> Self {
Self { queue }
}
}

impl<const DEPTH: usize> Sender for LoopbackSender<'_, DEPTH> {
fn send_vectored(&mut self, mut fragmenter: Fragmenter, payload: &[&[u8]]) -> Result<Tag> {
loop {
let mut buf = [0u8; LOOPBACK_MTU + MCTP_HEADER_SIZE];
match fragmenter.fragment_vectored(payload, &mut buf) {
SendOutput::Packet(p) => {
let mut packet = Packet::new();
if packet.extend_from_slice(p).is_err() {
// A fragment longer than the MTU we advertised.
return Err(Error::NoSpace);
}
let mut inner = self.queue.inner.borrow_mut();
if inner.packets.push_back(packet).is_err() {
inner.dropped = inner.dropped.saturating_add(1);
}
}
SendOutput::Complete { tag, .. } => break Ok(tag),
SendOutput::Error { err, .. } => break Err(err),
}
}
}

fn get_mtu(&self) -> usize {
LOOPBACK_MTU
}
}

#[cfg(test)]
mod tests;
137 changes: 137 additions & 0 deletions services/mctp/transport-loopback/src/tests.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,137 @@
// Licensed under the Apache-2.0 license
// SPDX-License-Identifier: Apache-2.0

//! The queue contract, plus a round trip between two cross-wired servers.
//!
//! A `Fragmenter` is built by the router rather than by callers, so
//! `send_vectored` is exercised through real servers rather than called
//! directly. The two-server arrangement mirrors
//! `services/mctp/echo/tests/echo_host.rs`, which is where it comes from.

use super::*;

use mctp::Eid;
use openprot_mctp_server::Server;

/// The two endpoints. Values are arbitrary; they only have to differ.
const EID_A: u8 = 8;
const EID_B: u8 = 42;

/// Vendor-defined message type. Nothing here cares what the bytes mean.
const MSG_TYPE: u8 = 0x7E;

/// Moves everything one side queued into the other side, the way a wire
/// would. Returns how many packets crossed.
fn transfer<S: Sender, const N: usize, const DEPTH: usize>(
from: &LoopbackQueue<DEPTH>,
to: &mut Server<S, N>,
) -> usize {
let mut crossed = 0;
while let Some(pkt) = from.take() {
to.inbound(&pkt).expect("inbound accepts the packet");
crossed += 1;
}
crossed
}

#[test]
fn a_new_queue_has_nothing_in_it() {
let q: LoopbackQueue<4> = LoopbackQueue::new();
assert_eq!(q.pending(), 0);
assert_eq!(q.dropped(), 0);
}

#[test]
fn the_advertised_mtu_excludes_the_header() {
let q: LoopbackQueue<4> = LoopbackQueue::new();
let s = LoopbackSender::new(&q);
assert_eq!(s.get_mtu(), LOOPBACK_MTU);
// The packets it queues carry the header on top of that payload.
assert_eq!(Packet::new().capacity(), LOOPBACK_MTU + MCTP_HEADER_SIZE);
}

#[test]
fn taking_from_an_empty_queue_gives_nothing() {
let q: LoopbackQueue<4> = LoopbackQueue::new();
assert!(q.take().is_none());
}

#[test]
fn a_message_crosses_from_one_server_to_the_other() {
let out_a: LoopbackQueue<16> = LoopbackQueue::new();
let out_b: LoopbackQueue<16> = LoopbackQueue::new();
let mut a: Server<_, 16> = Server::new(Eid(EID_A), 0, LoopbackSender::new(&out_a));
let mut b: Server<_, 16> = Server::new(Eid(EID_B), 0, LoopbackSender::new(&out_b));

let listener = b.listener(MSG_TYPE).expect("B binds a listener");

let payload = b"openprot";
let req = a.req(EID_B).expect("A opens a request to B");
a.send(Some(req), MSG_TYPE, Some(EID_B), None, false, payload)
.expect("A sends");

assert!(transfer(&out_a, &mut b) > 0, "nothing crossed the wire");

let mut buf = [0u8; LOOPBACK_MTU];
let meta = b.try_recv(listener, &mut buf).expect("B receives it");
assert_eq!(&buf[..payload.len()], payload);
assert_eq!(meta.remote_eid, EID_A);
}

#[test]
fn a_payload_longer_than_the_mtu_arrives_whole() {
let out_a: LoopbackQueue<16> = LoopbackQueue::new();
let out_b: LoopbackQueue<16> = LoopbackQueue::new();
let mut a: Server<_, 16> = Server::new(Eid(EID_A), 0, LoopbackSender::new(&out_a));
let mut b: Server<_, 16> = Server::new(Eid(EID_B), 0, LoopbackSender::new(&out_b));

let listener = b.listener(MSG_TYPE).expect("B binds a listener");

// More than one fragment, so reassembly is actually exercised.
let payload: std::vec::Vec<u8> = (0..(LOOPBACK_MTU * 2) as u16).map(|i| i as u8).collect();
let req = a.req(EID_B).expect("A opens a request to B");
a.send(Some(req), MSG_TYPE, Some(EID_B), None, false, &payload)
.expect("A sends");

let fragments = transfer(&out_a, &mut b);
assert!(fragments > 1, "expected several fragments, got {fragments}");

let mut buf = [0u8; LOOPBACK_MTU * 3];
b.try_recv(listener, &mut buf).expect("B receives it");
assert_eq!(&buf[..payload.len()], &payload[..]);
}

#[test]
fn the_reply_crosses_back() {
let out_a: LoopbackQueue<16> = LoopbackQueue::new();
let out_b: LoopbackQueue<16> = LoopbackQueue::new();
let mut a: Server<_, 16> = Server::new(Eid(EID_A), 0, LoopbackSender::new(&out_a));
let mut b: Server<_, 16> = Server::new(Eid(EID_B), 0, LoopbackSender::new(&out_b));

let listener = b.listener(MSG_TYPE).expect("B binds a listener");
let req = a.req(EID_B).expect("A opens a request to B");
a.send(Some(req), MSG_TYPE, Some(EID_B), None, false, b"ping")
.expect("A sends");
transfer(&out_a, &mut b);

let mut buf = [0u8; LOOPBACK_MTU];
let meta = b
.try_recv(listener, &mut buf)
.expect("B receives the request");

// A response carries no handle and reuses the request's tag.
b.send(
None,
MSG_TYPE,
Some(EID_A),
Some(meta.msg_tag),
false,
b"pong",
)
.expect("B replies");
assert!(transfer(&out_b, &mut a) > 0, "the reply did not cross");

let mut reply = [0u8; LOOPBACK_MTU];
a.try_recv(req, &mut reply).expect("A receives the reply");
assert_eq!(&reply[..4], b"pong");
}
25 changes: 25 additions & 0 deletions services/test/mock-bmc/BUILD.bazel
Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@
# 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 = "mock_bmc",
srcs = [
"src/lib.rs",
"src/tests.rs",
],
crate_name = "openprot_mock_bmc",
edition = "2024",
visibility = ["//visibility:public"],
deps = [
"//hal/blocking",
"@rust_crates//:embedded-hal",
],
)

# Host tests: the model is pin-generic, so it runs without a kernel.
rust_test(
name = "mock_bmc_test",
crate = ":mock_bmc",
)
Loading
Loading