#![allow(dead_code)]
use zerocopy::{FromBytes, IntoBytes};
#[derive(Copy, Clone, Eq, PartialEq, Hash, Debug)]
pub enum MsgType {
Stimulus,
ScenarioStart,
ScenarioEnd,
Exit,
TestResult,
SchedExit,
Crash,
PayloadMetrics,
RawPayloadOutput,
Profraw,
Stdout,
Stderr,
SchedLog,
Lifecycle,
ExecExit,
Dmesg,
ProbeOutput,
SnapshotRequest,
SnapshotReply,
SysRdy,
}
impl MsgType {
pub const fn wire_value(self) -> u32 {
match self {
MsgType::Stimulus => MSG_TYPE_STIMULUS,
MsgType::ScenarioStart => MSG_TYPE_SCENARIO_START,
MsgType::ScenarioEnd => MSG_TYPE_SCENARIO_END,
MsgType::Exit => MSG_TYPE_EXIT,
MsgType::TestResult => MSG_TYPE_TEST_RESULT,
MsgType::SchedExit => MSG_TYPE_SCHED_EXIT,
MsgType::Crash => MSG_TYPE_CRASH,
MsgType::PayloadMetrics => MSG_TYPE_PAYLOAD_METRICS,
MsgType::RawPayloadOutput => MSG_TYPE_RAW_PAYLOAD_OUTPUT,
MsgType::Profraw => MSG_TYPE_PROFRAW,
MsgType::SnapshotRequest => MSG_TYPE_SNAPSHOT_REQUEST,
MsgType::SnapshotReply => MSG_TYPE_SNAPSHOT_REPLY,
MsgType::SysRdy => MSG_TYPE_SYS_RDY,
MsgType::Stdout => MSG_TYPE_STDOUT,
MsgType::Stderr => MSG_TYPE_STDERR,
MsgType::SchedLog => MSG_TYPE_SCHED_LOG,
MsgType::Lifecycle => MSG_TYPE_LIFECYCLE,
MsgType::ExecExit => MSG_TYPE_EXEC_EXIT,
MsgType::Dmesg => MSG_TYPE_DMESG,
MsgType::ProbeOutput => MSG_TYPE_PROBE_OUTPUT,
}
}
pub const fn from_wire(value: u32) -> Option<Self> {
match value {
MSG_TYPE_STIMULUS => Some(MsgType::Stimulus),
MSG_TYPE_SCENARIO_START => Some(MsgType::ScenarioStart),
MSG_TYPE_SCENARIO_END => Some(MsgType::ScenarioEnd),
MSG_TYPE_EXIT => Some(MsgType::Exit),
MSG_TYPE_TEST_RESULT => Some(MsgType::TestResult),
MSG_TYPE_SCHED_EXIT => Some(MsgType::SchedExit),
MSG_TYPE_CRASH => Some(MsgType::Crash),
MSG_TYPE_PAYLOAD_METRICS => Some(MsgType::PayloadMetrics),
MSG_TYPE_RAW_PAYLOAD_OUTPUT => Some(MsgType::RawPayloadOutput),
MSG_TYPE_PROFRAW => Some(MsgType::Profraw),
MSG_TYPE_SNAPSHOT_REQUEST => Some(MsgType::SnapshotRequest),
MSG_TYPE_SNAPSHOT_REPLY => Some(MsgType::SnapshotReply),
MSG_TYPE_SYS_RDY => Some(MsgType::SysRdy),
MSG_TYPE_STDOUT => Some(MsgType::Stdout),
MSG_TYPE_STDERR => Some(MsgType::Stderr),
MSG_TYPE_SCHED_LOG => Some(MsgType::SchedLog),
MSG_TYPE_LIFECYCLE => Some(MsgType::Lifecycle),
MSG_TYPE_EXEC_EXIT => Some(MsgType::ExecExit),
MSG_TYPE_DMESG => Some(MsgType::Dmesg),
MSG_TYPE_PROBE_OUTPUT => Some(MsgType::ProbeOutput),
_ => None,
}
}
pub const fn is_coordinator_internal(self) -> bool {
matches!(
self,
MsgType::SnapshotRequest | MsgType::SnapshotReply | MsgType::SysRdy
)
}
}
#[derive(Copy, Clone, Eq, PartialEq, Hash, Debug)]
pub enum LifecyclePhase {
InitStarted,
PayloadStarting,
SchedulerDied,
SchedulerNotAttached,
}
impl LifecyclePhase {
pub const fn wire_value(self) -> u8 {
match self {
LifecyclePhase::InitStarted => 1,
LifecyclePhase::PayloadStarting => 2,
LifecyclePhase::SchedulerDied => 3,
LifecyclePhase::SchedulerNotAttached => 4,
}
}
pub const fn from_wire(value: u8) -> Option<Self> {
match value {
1 => Some(LifecyclePhase::InitStarted),
2 => Some(LifecyclePhase::PayloadStarting),
3 => Some(LifecyclePhase::SchedulerDied),
4 => Some(LifecyclePhase::SchedulerNotAttached),
_ => None,
}
}
}
pub const MSG_TYPE_STIMULUS: u32 = 0x5354_494D;
pub const MSG_TYPE_SCENARIO_START: u32 = 0x5343_5354;
pub const MSG_TYPE_SCENARIO_END: u32 = 0x5343_454E;
pub const MSG_TYPE_EXIT: u32 = 0x4558_4954;
pub const MSG_TYPE_TEST_RESULT: u32 = 0x5445_5354;
pub const MSG_TYPE_SCHED_EXIT: u32 = 0x5343_4458;
pub const MSG_TYPE_CRASH: u32 = 0x4352_5348;
pub const MSG_TYPE_PAYLOAD_METRICS: u32 = 0x504d_4554;
pub const MSG_TYPE_RAW_PAYLOAD_OUTPUT: u32 = 0x5241_574f;
pub const MSG_TYPE_PROFRAW: u32 = 0x5052_4157;
pub const MSG_TYPE_SNAPSHOT_REQUEST: u32 = 0x534e_5251;
pub const MSG_TYPE_SNAPSHOT_REPLY: u32 = 0x534e_5250;
pub const MSG_TYPE_SYS_RDY: u32 = 0x5352_4459;
pub const MSG_TYPE_STDOUT: u32 = 0x534f_5554;
pub const MSG_TYPE_STDERR: u32 = 0x5345_5252;
pub const MSG_TYPE_SCHED_LOG: u32 = 0x5343_4c47;
pub const MSG_TYPE_LIFECYCLE: u32 = 0x4c49_4645;
pub const MSG_TYPE_EXEC_EXIT: u32 = 0x4558_4358;
pub const MSG_TYPE_DMESG: u32 = 0x444d_5347;
pub const MSG_TYPE_PROBE_OUTPUT: u32 = 0x5052_4f42;
#[repr(C)]
#[derive(
Clone, Copy, Default, Debug, FromBytes, IntoBytes, zerocopy::Immutable, zerocopy::KnownLayout,
)]
pub struct ShmMessage {
pub msg_type: u32,
pub length: u32,
pub crc32: u32,
pub _pad: u32,
}
const _SHM_MESSAGE_SIZE: () = assert!(std::mem::size_of::<ShmMessage>() == 16);
pub const FRAME_HEADER_SIZE: usize = std::mem::size_of::<ShmMessage>();
#[derive(Debug, Clone)]
pub struct ShmEntry {
pub msg_type: u32,
pub payload: Vec<u8>,
pub crc_ok: bool,
}
#[repr(C)]
#[derive(Clone, Copy, Default, Debug, IntoBytes, zerocopy::Immutable, zerocopy::KnownLayout)]
pub struct StimulusPayload {
pub elapsed_ms: u32,
pub step_index: u16,
pub op_count: u16,
pub op_kinds: u32,
pub cgroup_count: u16,
pub worker_count: u16,
pub total_iterations: u64,
}
const _STIMULUS_SIZE: () = assert!(std::mem::size_of::<StimulusPayload>() == 24);
#[derive(Debug, Clone)]
pub struct StimulusEvent {
pub elapsed_ms: u32,
pub step_index: u16,
pub op_count: u16,
pub op_kinds: u32,
pub cgroup_count: u16,
pub worker_count: u16,
pub total_iterations: u64,
}
impl StimulusEvent {
pub fn from_payload(data: &[u8]) -> Option<Self> {
if data.len() < std::mem::size_of::<StimulusPayload>() {
return None;
}
Some(StimulusEvent {
elapsed_ms: u32::from_ne_bytes(data[0..4].try_into().ok()?),
step_index: u16::from_ne_bytes(data[4..6].try_into().ok()?),
op_count: u16::from_ne_bytes(data[6..8].try_into().ok()?),
op_kinds: u32::from_ne_bytes(data[8..12].try_into().ok()?),
cgroup_count: u16::from_ne_bytes(data[12..14].try_into().ok()?),
worker_count: u16::from_ne_bytes(data[14..16].try_into().ok()?),
total_iterations: u64::from_ne_bytes(data[16..24].try_into().ok()?),
})
}
}
pub const SNAPSHOT_TAG_MAX: usize = 64;
pub const SNAPSHOT_REASON_MAX: usize = 64;
pub const SNAPSHOT_KIND_NONE: u32 = 0;
pub const SNAPSHOT_KIND_CAPTURE: u32 = 1;
pub const SNAPSHOT_KIND_WATCH: u32 = 2;
pub const SNAPSHOT_STATUS_OK: u32 = 1;
pub const SNAPSHOT_STATUS_ERR: u32 = 2;
#[derive(Debug)]
pub enum SnapshotRequestResult {
Ok,
HostError { reason: String },
TransportError { reason: String },
}
#[repr(C)]
#[derive(Copy, Clone, Debug, FromBytes, IntoBytes, zerocopy::Immutable, zerocopy::KnownLayout)]
pub struct SnapshotRequestPayload {
pub request_id: u32,
pub kind: u32,
pub tag: [u8; SNAPSHOT_TAG_MAX],
}
const _SNAPSHOT_REQUEST_PAYLOAD_SIZE: () =
assert!(std::mem::size_of::<SnapshotRequestPayload>() == 8 + SNAPSHOT_TAG_MAX);
#[repr(C)]
#[derive(Copy, Clone, Debug, FromBytes, IntoBytes, zerocopy::Immutable, zerocopy::KnownLayout)]
pub struct SnapshotReplyPayload {
pub request_id: u32,
pub status: u32,
pub reason: [u8; SNAPSHOT_REASON_MAX],
}
const _SNAPSHOT_REPLY_PAYLOAD_SIZE: () =
assert!(std::mem::size_of::<SnapshotReplyPayload>() == 8 + SNAPSHOT_REASON_MAX);
#[derive(Copy, Clone, Eq, PartialEq, Hash, Debug)]
pub enum ControlEvent {
DeviceReady,
PortAdd,
PortRemove,
PortReady,
ConsolePort,
Resize,
PortOpen,
PortName,
}
impl ControlEvent {
pub const fn wire_value(self) -> u16 {
match self {
ControlEvent::DeviceReady => 0,
ControlEvent::PortAdd => 1,
ControlEvent::PortRemove => 2,
ControlEvent::PortReady => 3,
ControlEvent::ConsolePort => 4,
ControlEvent::Resize => 5,
ControlEvent::PortOpen => 6,
ControlEvent::PortName => 7,
}
}
pub const fn from_wire(value: u16) -> Option<Self> {
match value {
0 => Some(ControlEvent::DeviceReady),
1 => Some(ControlEvent::PortAdd),
2 => Some(ControlEvent::PortRemove),
3 => Some(ControlEvent::PortReady),
4 => Some(ControlEvent::ConsolePort),
5 => Some(ControlEvent::Resize),
6 => Some(ControlEvent::PortOpen),
7 => Some(ControlEvent::PortName),
_ => None,
}
}
}
#[repr(C)]
#[derive(Copy, Clone, Debug, FromBytes, IntoBytes, zerocopy::Immutable, zerocopy::KnownLayout)]
pub struct VirtioConsoleControl {
pub id: u32,
pub event: u16,
pub value: u16,
}
const _VIRTIO_CONSOLE_CONTROL_SIZE: () = assert!(std::mem::size_of::<VirtioConsoleControl>() == 8);
pub const NUM_PORTS: u32 = 2;
pub const PORT1_NAME: &str = "ktstr-bulk";
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn shm_message_round_trip_through_bytes() {
let f = ShmMessage {
msg_type: MSG_TYPE_EXIT,
length: 4,
crc32: 0xDEAD_BEEF,
_pad: 0,
};
let bytes = f.as_bytes();
assert_eq!(bytes.len(), FRAME_HEADER_SIZE);
let back = ShmMessage::read_from_bytes(bytes).expect("16-byte slice deserializes");
let msg_type = back.msg_type;
let length = back.length;
let crc32 = back.crc32;
let pad = back._pad;
assert_eq!(msg_type, MSG_TYPE_EXIT);
assert_eq!(length, 4);
assert_eq!(crc32, 0xDEAD_BEEF);
assert_eq!(pad, 0);
}
#[test]
fn msg_type_constants_are_unique() {
let ids = [
MSG_TYPE_STIMULUS,
MSG_TYPE_SCENARIO_START,
MSG_TYPE_SCENARIO_END,
MSG_TYPE_EXIT,
MSG_TYPE_TEST_RESULT,
MSG_TYPE_SCHED_EXIT,
MSG_TYPE_CRASH,
MSG_TYPE_PAYLOAD_METRICS,
MSG_TYPE_RAW_PAYLOAD_OUTPUT,
MSG_TYPE_PROFRAW,
MSG_TYPE_SNAPSHOT_REQUEST,
MSG_TYPE_SNAPSHOT_REPLY,
MSG_TYPE_SYS_RDY,
MSG_TYPE_STDOUT,
MSG_TYPE_STDERR,
MSG_TYPE_SCHED_LOG,
MSG_TYPE_LIFECYCLE,
MSG_TYPE_EXEC_EXIT,
MSG_TYPE_DMESG,
MSG_TYPE_PROBE_OUTPUT,
];
for (i, a) in ids.iter().enumerate() {
for b in &ids[i + 1..] {
assert_ne!(a, b, "duplicate MSG_TYPE id 0x{a:08x}");
}
}
}
#[test]
fn msg_type_exit_wire_bytes_are_le() {
let f = ShmMessage {
msg_type: MSG_TYPE_EXIT,
length: 0,
crc32: 0,
_pad: 0,
};
let bytes = f.as_bytes();
assert_eq!(&bytes[..4], &MSG_TYPE_EXIT.to_le_bytes());
assert_eq!(&bytes[..4], b"TIXE");
}
#[test]
fn shm_message_size_is_16() {
assert_eq!(FRAME_HEADER_SIZE, 16);
assert_eq!(std::mem::size_of::<ShmMessage>(), 16);
}
#[test]
fn msg_type_round_trips() {
let all = [
MsgType::Stimulus,
MsgType::ScenarioStart,
MsgType::ScenarioEnd,
MsgType::Exit,
MsgType::TestResult,
MsgType::SchedExit,
MsgType::Crash,
MsgType::PayloadMetrics,
MsgType::RawPayloadOutput,
MsgType::Profraw,
MsgType::SnapshotRequest,
MsgType::SnapshotReply,
MsgType::SysRdy,
MsgType::Stdout,
MsgType::Stderr,
MsgType::SchedLog,
MsgType::Lifecycle,
MsgType::ExecExit,
MsgType::Dmesg,
MsgType::ProbeOutput,
];
for variant in all {
let v = variant.wire_value();
assert_eq!(MsgType::from_wire(v), Some(variant));
}
}
#[test]
fn msg_type_from_wire_unknown_returns_none() {
assert_eq!(MsgType::from_wire(0xDEAD_BEEF), None);
assert_eq!(MsgType::from_wire(0), None);
}
#[test]
fn msg_type_wire_value_matches_constants() {
assert_eq!(MsgType::Stimulus.wire_value(), MSG_TYPE_STIMULUS);
assert_eq!(MsgType::ScenarioStart.wire_value(), MSG_TYPE_SCENARIO_START);
assert_eq!(MsgType::ScenarioEnd.wire_value(), MSG_TYPE_SCENARIO_END);
assert_eq!(MsgType::Exit.wire_value(), MSG_TYPE_EXIT);
assert_eq!(MsgType::TestResult.wire_value(), MSG_TYPE_TEST_RESULT);
assert_eq!(MsgType::SchedExit.wire_value(), MSG_TYPE_SCHED_EXIT);
assert_eq!(MsgType::Crash.wire_value(), MSG_TYPE_CRASH);
assert_eq!(
MsgType::PayloadMetrics.wire_value(),
MSG_TYPE_PAYLOAD_METRICS
);
assert_eq!(
MsgType::RawPayloadOutput.wire_value(),
MSG_TYPE_RAW_PAYLOAD_OUTPUT
);
assert_eq!(MsgType::Profraw.wire_value(), MSG_TYPE_PROFRAW);
assert_eq!(
MsgType::SnapshotRequest.wire_value(),
MSG_TYPE_SNAPSHOT_REQUEST
);
assert_eq!(MsgType::SnapshotReply.wire_value(), MSG_TYPE_SNAPSHOT_REPLY);
assert_eq!(MsgType::SysRdy.wire_value(), MSG_TYPE_SYS_RDY);
assert_eq!(MsgType::Stdout.wire_value(), MSG_TYPE_STDOUT);
assert_eq!(MsgType::Stderr.wire_value(), MSG_TYPE_STDERR);
assert_eq!(MsgType::SchedLog.wire_value(), MSG_TYPE_SCHED_LOG);
assert_eq!(MsgType::Lifecycle.wire_value(), MSG_TYPE_LIFECYCLE);
assert_eq!(MsgType::ExecExit.wire_value(), MSG_TYPE_EXEC_EXIT);
assert_eq!(MsgType::Dmesg.wire_value(), MSG_TYPE_DMESG);
assert_eq!(MsgType::ProbeOutput.wire_value(), MSG_TYPE_PROBE_OUTPUT);
}
#[test]
fn is_coordinator_internal_matches_filter_set() {
let internal = [
MsgType::SnapshotRequest,
MsgType::SnapshotReply,
MsgType::SysRdy,
];
let verdict = [
MsgType::Stimulus,
MsgType::ScenarioStart,
MsgType::ScenarioEnd,
MsgType::Exit,
MsgType::TestResult,
MsgType::SchedExit,
MsgType::Crash,
MsgType::PayloadMetrics,
MsgType::RawPayloadOutput,
MsgType::Profraw,
MsgType::Stdout,
MsgType::Stderr,
MsgType::SchedLog,
MsgType::Lifecycle,
MsgType::ExecExit,
MsgType::Dmesg,
MsgType::ProbeOutput,
];
for v in internal {
assert!(
v.is_coordinator_internal(),
"{v:?} must be classified as coordinator-internal"
);
}
for v in verdict {
assert!(
!v.is_coordinator_internal(),
"{v:?} carries test verdict data and must NOT be filtered out"
);
}
}
#[test]
fn lifecycle_phase_round_trips() {
let all = [
LifecyclePhase::InitStarted,
LifecyclePhase::PayloadStarting,
LifecyclePhase::SchedulerDied,
LifecyclePhase::SchedulerNotAttached,
];
for p in all {
let v = p.wire_value();
assert_eq!(LifecyclePhase::from_wire(v), Some(p));
}
}
#[test]
fn lifecycle_phase_zero_is_reserved() {
assert_eq!(LifecyclePhase::from_wire(0), None);
assert_eq!(LifecyclePhase::from_wire(0xFF), None);
}
#[test]
fn lifecycle_phase_wire_values_are_stable() {
assert_eq!(LifecyclePhase::InitStarted.wire_value(), 1);
assert_eq!(LifecyclePhase::PayloadStarting.wire_value(), 2);
assert_eq!(LifecyclePhase::SchedulerDied.wire_value(), 3);
assert_eq!(LifecyclePhase::SchedulerNotAttached.wire_value(), 4);
}
#[test]
fn snapshot_request_payload_round_trip_through_bytes() {
let mut tag = [0u8; SNAPSHOT_TAG_MAX];
tag[..6].copy_from_slice(b"hello!");
let p = SnapshotRequestPayload {
request_id: 0xDEAD_BEEF,
kind: SNAPSHOT_KIND_CAPTURE,
tag,
};
let bytes = p.as_bytes();
assert_eq!(bytes.len(), 8 + SNAPSHOT_TAG_MAX);
let back = SnapshotRequestPayload::read_from_bytes(bytes).expect("payload deserializes");
let request_id = back.request_id;
let kind = back.kind;
assert_eq!(request_id, 0xDEAD_BEEF);
assert_eq!(kind, SNAPSHOT_KIND_CAPTURE);
assert_eq!(&back.tag[..6], b"hello!");
}
#[test]
fn snapshot_reply_payload_round_trip_through_bytes() {
let mut reason = [0u8; SNAPSHOT_REASON_MAX];
reason[..4].copy_from_slice(b"oops");
let p = SnapshotReplyPayload {
request_id: 0xCAFE_BABE,
status: SNAPSHOT_STATUS_ERR,
reason,
};
let bytes = p.as_bytes();
assert_eq!(bytes.len(), 8 + SNAPSHOT_REASON_MAX);
let back = SnapshotReplyPayload::read_from_bytes(bytes).expect("payload deserializes");
let request_id = back.request_id;
let status = back.status;
assert_eq!(request_id, 0xCAFE_BABE);
assert_eq!(status, SNAPSHOT_STATUS_ERR);
assert_eq!(&back.reason[..4], b"oops");
}
#[test]
fn snapshot_kind_constants_are_unique() {
assert_ne!(SNAPSHOT_KIND_NONE, SNAPSHOT_KIND_CAPTURE);
assert_ne!(SNAPSHOT_KIND_NONE, SNAPSHOT_KIND_WATCH);
assert_ne!(SNAPSHOT_KIND_CAPTURE, SNAPSHOT_KIND_WATCH);
}
#[test]
fn snapshot_status_constants_are_unique() {
assert_ne!(SNAPSHOT_STATUS_OK, SNAPSHOT_STATUS_ERR);
}
#[test]
fn control_event_round_trips() {
let all = [
ControlEvent::DeviceReady,
ControlEvent::PortAdd,
ControlEvent::PortRemove,
ControlEvent::PortReady,
ControlEvent::ConsolePort,
ControlEvent::Resize,
ControlEvent::PortOpen,
ControlEvent::PortName,
];
for variant in all {
let v = variant.wire_value();
assert_eq!(ControlEvent::from_wire(v), Some(variant));
}
}
#[test]
fn control_event_from_wire_unknown_returns_none() {
assert_eq!(ControlEvent::from_wire(8), None);
assert_eq!(ControlEvent::from_wire(0xFFFF), None);
}
#[test]
fn control_event_discriminants_match_uapi() {
assert_eq!(ControlEvent::DeviceReady.wire_value(), 0);
assert_eq!(ControlEvent::PortAdd.wire_value(), 1);
assert_eq!(ControlEvent::PortRemove.wire_value(), 2);
assert_eq!(ControlEvent::PortReady.wire_value(), 3);
assert_eq!(ControlEvent::ConsolePort.wire_value(), 4);
assert_eq!(ControlEvent::Resize.wire_value(), 5);
assert_eq!(ControlEvent::PortOpen.wire_value(), 6);
assert_eq!(ControlEvent::PortName.wire_value(), 7);
}
#[test]
fn virtio_console_control_size_is_8() {
assert_eq!(std::mem::size_of::<VirtioConsoleControl>(), 8);
}
#[test]
fn virtio_console_control_round_trip() {
let c = VirtioConsoleControl {
id: 1,
event: ControlEvent::PortOpen.wire_value(),
value: 1,
};
let bytes = c.as_bytes();
assert_eq!(bytes.len(), 8);
let back = VirtioConsoleControl::read_from_bytes(bytes).unwrap();
let id = back.id;
let event = back.event;
let value = back.value;
assert_eq!(id, 1);
assert_eq!(event, ControlEvent::PortOpen.wire_value());
assert_eq!(value, 1);
}
}