use std::fmt::Write as _;
use crate::broker::backend_handle::{BackendHandle, DaemonProcess};
use crate::broker::backend_sdk::{BackendEndpointMux, LegacyClassification, MuxError, MuxPoll};
use crate::broker::protocol::{encode_framed, try_decode_framed, Endpoint, Frame};
#[derive(Debug, thiserror::Error)]
pub enum ConformanceError {
#[error("BackendHandle probe failed: {0}")]
Probe(String),
#[error("probed identity does not match expected: {0}")]
IdentityMismatch(String),
#[error("mux verdict mismatch at step {step}: expected {expected}, got {got}")]
UnexpectedVerdict {
step: usize,
expected: String,
got: String,
},
#[error("mux error mismatch at step {step}: {detail}")]
UnexpectedMuxError {
step: usize,
detail: String,
},
#[error(
"framed frame did not match golden bytes:\n expected ({expected_len} bytes): {expected}\n got ({got_len} bytes): {got}"
)]
GoldenMismatch {
expected_len: usize,
expected: String,
got_len: usize,
got: String,
},
}
pub fn assert_framed_frame_matches_golden(
frame: &Frame,
golden_bytes: &[u8],
) -> Result<(), ConformanceError> {
let encoded = encode_framed(frame).map_err(|err| ConformanceError::GoldenMismatch {
expected_len: golden_bytes.len(),
expected: hex(golden_bytes),
got_len: 0,
got: format!("<encode error: {err}>"),
})?;
if encoded == golden_bytes {
return Ok(());
}
Err(ConformanceError::GoldenMismatch {
expected_len: golden_bytes.len(),
expected: hex(golden_bytes),
got_len: encoded.len(),
got: hex(&encoded),
})
}
pub fn encode_framed_for_golden(
frame: &Frame,
) -> Result<Vec<u8>, crate::broker::protocol::FramingError> {
encode_framed(frame)
}
pub fn assert_framed_bytes_decode_to(
golden_bytes: &[u8],
expected_frame: &Frame,
) -> Result<(), ConformanceError> {
let decoded = try_decode_framed(golden_bytes)
.map_err(|err| ConformanceError::GoldenMismatch {
expected_len: golden_bytes.len(),
expected: hex(golden_bytes),
got_len: 0,
got: format!("<decode error: {err}>"),
})?
.ok_or_else(|| ConformanceError::GoldenMismatch {
expected_len: golden_bytes.len(),
expected: hex(golden_bytes),
got_len: 0,
got: "<short read: golden bytes did not contain a complete frame>".to_string(),
})?;
if decoded.consumed != golden_bytes.len() {
return Err(ConformanceError::GoldenMismatch {
expected_len: golden_bytes.len(),
expected: hex(golden_bytes),
got_len: decoded.consumed,
got: format!(
"<trailing bytes: consumed {} of {}>",
decoded.consumed,
golden_bytes.len()
),
});
}
let frame = decoded.frame;
if frame.payload_protocol != expected_frame.payload_protocol
|| frame.kind != expected_frame.kind
|| frame.request_id != expected_frame.request_id
|| frame.payload != expected_frame.payload
{
return Err(ConformanceError::IdentityMismatch(format!(
"decoded frame fields differ: \
payload_protocol {:#06X} vs {:#06X}, kind {} vs {}, \
request_id {} vs {}, payload_len {} vs {}",
frame.payload_protocol,
expected_frame.payload_protocol,
frame.kind,
expected_frame.kind,
frame.request_id,
expected_frame.request_id,
frame.payload.len(),
expected_frame.payload.len(),
)));
}
Ok(())
}
fn hex(bytes: &[u8]) -> String {
let mut out = String::with_capacity(bytes.len() * 3);
for (idx, byte) in bytes.iter().enumerate() {
if idx > 0 {
out.push(' ');
}
let _ = write!(out, "{byte:02X}");
}
out
}
pub fn probe_responds_correctly(
service_name: &str,
service_version: &str,
endpoint: &Endpoint,
expected: &DaemonProcess,
) -> Result<(), ConformanceError> {
let handle =
BackendHandle::probe_with_service(service_name, service_version, endpoint, expected)
.map_err(|err| ConformanceError::Probe(err.to_string()))?;
if handle.daemon_process.pid != expected.pid {
return Err(ConformanceError::IdentityMismatch(format!(
"pid {} (probed) vs {} (expected)",
handle.daemon_process.pid, expected.pid
)));
}
if handle.daemon_process.ipc_endpoint != expected.ipc_endpoint {
return Err(ConformanceError::IdentityMismatch(format!(
"endpoint {:?} (probed) vs {:?} (expected)",
handle.daemon_process.ipc_endpoint, expected.ipc_endpoint
)));
}
Ok(())
}
#[derive(Debug, Clone)]
pub struct MixedWireStep {
pub bytes: Vec<u8>,
pub expect: MixedWireExpect,
}
#[derive(Debug, Clone)]
pub enum MixedWireExpect {
NeedMoreBytes,
Legacy,
ProbeAnswered,
Payload {
payload_protocol: u32,
},
Error {
error_contains: String,
},
}
#[derive(Debug, Default, Clone)]
pub struct MixedWireScenario {
steps: Vec<MixedWireStep>,
}
impl MixedWireScenario {
pub fn new() -> Self {
Self { steps: Vec::new() }
}
pub fn step(mut self, step: MixedWireStep) -> Self {
self.steps.push(step);
self
}
pub fn run<F>(self, mux: &BackendEndpointMux<F>) -> Result<(), ConformanceError>
where
F: Fn(&[u8]) -> LegacyClassification,
{
let mut buf: Vec<u8> = Vec::new();
for (idx, step) in self.steps.into_iter().enumerate() {
buf.extend_from_slice(&step.bytes);
match (&step.expect, mux.poll(&buf)) {
(MixedWireExpect::NeedMoreBytes, Ok(MuxPoll::NeedMoreBytes)) => {}
(MixedWireExpect::Legacy, Ok(MuxPoll::Legacy)) => {
buf.clear();
}
(MixedWireExpect::ProbeAnswered, Ok(MuxPoll::ProbeAnswered { consumed, .. })) => {
buf.drain(..consumed);
}
(
MixedWireExpect::Payload { payload_protocol },
Ok(MuxPoll::Payload { frame, consumed }),
) => {
if frame.payload_protocol != *payload_protocol {
return Err(ConformanceError::UnexpectedVerdict {
step: idx,
expected: format!("Payload protocol {payload_protocol:#06X}"),
got: format!("Payload protocol {:#06X}", frame.payload_protocol),
});
}
buf.drain(..consumed);
}
(MixedWireExpect::Error { error_contains }, Err(err)) => {
let rendered = format!("{err:?}");
if !rendered.contains(error_contains) {
return Err(ConformanceError::UnexpectedMuxError {
step: idx,
detail: format!(
"expected substring {error_contains:?} in {rendered:?}"
),
});
}
buf.clear();
}
(expect, Ok(verdict)) => {
return Err(ConformanceError::UnexpectedVerdict {
step: idx,
expected: describe_expect(expect),
got: describe_verdict(&verdict),
});
}
(_, Err(err)) => {
return Err(ConformanceError::UnexpectedMuxError {
step: idx,
detail: format!("mux returned unexpected error: {err:?}"),
});
}
}
}
Ok(())
}
}
fn describe_expect(expect: &MixedWireExpect) -> String {
match expect {
MixedWireExpect::NeedMoreBytes => "NeedMoreBytes".to_string(),
MixedWireExpect::Legacy => "Legacy".to_string(),
MixedWireExpect::ProbeAnswered => "ProbeAnswered".to_string(),
MixedWireExpect::Payload { payload_protocol } => {
format!("Payload(protocol={payload_protocol:#06X})")
}
MixedWireExpect::Error { error_contains } => {
format!("Error(contains={error_contains:?})")
}
}
}
fn describe_verdict(verdict: &MuxPoll) -> String {
match verdict {
MuxPoll::NeedMoreBytes => "NeedMoreBytes".to_string(),
MuxPoll::Legacy => "Legacy".to_string(),
MuxPoll::ProbeAnswered { consumed, .. } => format!("ProbeAnswered(consumed={consumed})"),
MuxPoll::Payload { frame, consumed } => format!(
"Payload(protocol={:#06X}, consumed={consumed})",
frame.payload_protocol
),
}
}
#[allow(dead_code)]
type _MuxErrorAlias = MuxError;