#![deny(unsafe_code)]
pub mod fixtures;
pub mod prelude;
use cc_lb_plugin_api::SlotKey;
use cc_lb_plugin_wire::{
ArchivedFilterResponse, ArchivedShapeResponse, FilterRequest, FilterResponse, ObserveEvent,
ShapeRequest, ShapeResponse,
};
use cc_lb_runtime_wasmtime::{
HotEngineConfig, ModuleInspection, SlotKind, WasmtimeRuntime, inspect_wasm,
};
use rkyv::rancor::Error as RkyvError;
use rkyv::util::AlignedVec;
use crate::fixtures::observe_event_samples;
pub struct ConformanceSuite<'a> {
wasm: &'a [u8],
kind: SlotKind,
plugin_name: String,
engine_config: HotEngineConfig,
}
impl<'a> ConformanceSuite<'a> {
pub fn for_filter(wasm: &'a [u8]) -> Self {
Self::with_kind(wasm, SlotKind::Filter)
}
pub fn for_shape(wasm: &'a [u8]) -> Self {
Self::with_kind(wasm, SlotKind::Shape)
}
pub fn for_observe(wasm: &'a [u8]) -> Self {
Self::with_kind(wasm, SlotKind::Observe)
}
pub fn from_wasm(wasm: &'a [u8]) -> Self {
let mut matches = SlotKind::ALL
.iter()
.copied()
.filter(|kind| inspect_wasm(*kind, wasm).is_ok());
let Some(kind) = matches.next() else {
panic!("inspect_wasm did not recognise filter, shape, or observe exports")
};
assert!(
matches.next().is_none(),
"wasm exports multiple hooks; use for_filter, for_shape, or for_observe"
);
Self::with_kind(wasm, kind)
}
fn with_kind(wasm: &'a [u8], kind: SlotKind) -> Self {
let label = match kind {
SlotKind::Filter => "filter",
SlotKind::Shape => "shape",
SlotKind::Observe => "observe",
};
Self {
wasm,
kind,
plugin_name: format!("conformance-{label}"),
engine_config: conformance_engine_config(),
}
}
pub fn with_plugin_name(mut self, name: impl Into<String>) -> Self {
self.plugin_name = name.into();
self
}
pub fn with_engine_config(mut self, cfg: HotEngineConfig) -> Self {
self.engine_config = cfg;
self
}
pub fn assert_static_admission(&self) {
inspect_wasm(self.kind, self.wasm)
.unwrap_or_else(|e| panic!("inspect_wasm rejected plugin: {e}"));
}
pub fn inspect(&self) -> ModuleInspection {
inspect_wasm(self.kind, self.wasm)
.unwrap_or_else(|e| panic!("inspect_wasm rejected plugin: {e}"))
}
pub fn assert_recognisable_by_current_host(&self) {
let runtime = WasmtimeRuntime::new(self.engine_config.clone())
.expect("wasmtime engine build must succeed");
runtime
.admit_wasm(self.kind, self.wasm)
.unwrap_or_else(|e| panic!("admit_wasm rejected plugin: {e}"));
}
pub fn session(&self) -> PluginSession {
let runtime = WasmtimeRuntime::new(self.engine_config.clone())
.expect("wasmtime engine build must succeed");
let slot_key = SlotKey::global(self.plugin_name.clone());
match self.kind {
SlotKind::Filter => runtime
.register_filter(slot_key.clone(), self.plugin_name.clone(), self.wasm)
.map(|_| ())
.expect("register_filter must accept a conforming plugin"),
SlotKind::Shape => runtime
.register_shape(slot_key.clone(), self.plugin_name.clone(), self.wasm)
.map(|_| ())
.expect("register_shape must accept a conforming plugin"),
SlotKind::Observe => runtime
.register_observe(slot_key.clone(), self.plugin_name.clone(), self.wasm)
.map(|_| ())
.expect("register_observe must accept a conforming plugin"),
}
PluginSession {
runtime,
slot_key,
kind: self.kind,
}
}
pub fn run(&self) {
let session = self.session();
match self.kind {
SlotKind::Filter => {
let _ = session.call_filter(fixtures::sample_filter_request());
}
SlotKind::Shape => {
let _ = session.call_shape(fixtures::sample_shape_request());
}
SlotKind::Observe => {
session.exercise_observe_variants();
}
}
}
}
pub struct PluginSession {
runtime: WasmtimeRuntime,
slot_key: SlotKey,
kind: SlotKind,
}
impl PluginSession {
pub fn kind(&self) -> SlotKind {
self.kind
}
pub fn slot_key(&self) -> &SlotKey {
&self.slot_key
}
pub fn runtime(&self) -> &WasmtimeRuntime {
&self.runtime
}
pub fn call_filter(&self, request: FilterRequest) -> FilterResponse {
assert!(
matches!(self.kind, SlotKind::Filter),
"call_filter requires SlotKind::Filter, got {:?}",
self.kind
);
let in_bytes = rkyv::to_bytes::<RkyvError>(&request).expect("rkyv encode FilterRequest");
let out_bytes = self
.runtime
.call_filter(&self.slot_key, in_bytes.as_slice())
.expect("guest cc_lb_filter must complete without trap");
let mut aligned = AlignedVec::<16>::with_capacity(out_bytes.len());
aligned.extend_from_slice(&out_bytes);
let archived = rkyv::access::<ArchivedFilterResponse, RkyvError>(&aligned)
.expect("rkyv access FilterResponse");
rkyv::deserialize::<FilterResponse, RkyvError>(archived)
.expect("rkyv deserialize FilterResponse")
}
pub fn call_shape(&self, request: ShapeRequest) -> ShapeResponse {
assert!(
matches!(self.kind, SlotKind::Shape),
"call_shape requires SlotKind::Shape, got {:?}",
self.kind
);
let in_bytes = rkyv::to_bytes::<RkyvError>(&request).expect("rkyv encode ShapeRequest");
let out_bytes = self
.runtime
.call_shape(&self.slot_key, in_bytes.as_slice())
.expect("guest cc_lb_shape must complete without trap");
let mut aligned = AlignedVec::<16>::with_capacity(out_bytes.len());
aligned.extend_from_slice(&out_bytes);
let archived = rkyv::access::<ArchivedShapeResponse, RkyvError>(&aligned)
.expect("rkyv access ShapeResponse");
rkyv::deserialize::<ShapeResponse, RkyvError>(archived)
.expect("rkyv deserialize ShapeResponse")
}
pub fn call_observe(&self, event: ObserveEvent) {
assert!(
matches!(self.kind, SlotKind::Observe),
"call_observe requires SlotKind::Observe, got {:?}",
self.kind
);
let in_bytes = rkyv::to_bytes::<RkyvError>(&event).expect("rkyv encode ObserveEvent");
self.runtime
.call_observe(&self.slot_key, in_bytes.as_slice())
.expect("guest cc_lb_observe must complete without trap");
}
pub fn exercise_observe_variants(&self) {
for event in observe_event_samples() {
self.call_observe(event);
}
}
}
pub fn conformance_engine_config() -> HotEngineConfig {
HotEngineConfig {
memory_max_pages: 1024,
max_wasm_stack: 1024 * 1024,
pool_total_memories: 64,
pool_total_core_instances: 64,
..HotEngineConfig::default()
}
}