use std::sync::Arc;
use crate::adapter::WrappingDispenser;
use crate::adapter::{ExecutionError, OpDispenser, OpResult};
use crate::wrapper_registry::{WrapperName, WrapperRegistration, WrapperSubject};
pub const NAME: WrapperName = WrapperName::new("dryrun");
fn triggers(s: WrapperSubject) -> bool {
let Some(template) = s.op() else {
return false;
};
template.params.contains_key("dryrun")
}
fn describe_assignment(s: WrapperSubject) -> Option<String> {
let template = s.op()?;
let v = template.params.get("dryrun")?;
let mode = v.as_str().unwrap_or("silent");
Some(format!("dryrun: short-circuit (mode={mode})"))
}
const FORBIDS_OUTER: &[WrapperName] = &[
super::traverse::NAME,
super::delay::NAME,
crate::validation::WRAPPER_NAME,
super::poll::NAME,
super::r#if::NAME,
super::result::NAME,
super::metrics::NAME,
super::memo::NAME,
super::gutter::NAME,
super::r#while::NAME,
super::rate::NAME,
];
inventory::submit! {
WrapperRegistration {
name: NAME,
owned_fields: &["dryrun"],
triggers,
requires_inner: &[],
forbids_outer: FORBIDS_OUTER,
mutually_exclusive_with: &[],
describe_assignment,
levels: &[crate::wrapper_registry::WrapperLevel::Op],
}
}
pub struct DryRunWrapper {
inner: Arc<dyn OpDispenser>,
}
impl DryRunWrapper {
pub fn wrap(inner: Arc<dyn OpDispenser>) -> Arc<dyn OpDispenser> {
Arc::new(Self { inner })
}
}
impl WrappingDispenser for DryRunWrapper {}
impl OpDispenser for DryRunWrapper {
fn execute<'a>(
&'a self,
_cycle: u64,
_ctx: &'a crate::fixture::ExecCtx<'a>,
) -> std::pin::Pin<
Box<dyn std::future::Future<Output = Result<OpResult, ExecutionError>> + Send + 'a>,
> {
Box::pin(async move {
Ok(OpResult {
body: None,
skipped: true,
})
})
}
fn inner_dispenser(&self) -> Option<&dyn OpDispenser> {
Some(self.inner.as_ref())
}
}
#[cfg(test)]
mod tests {
use super::*;
use crate::adapter::{ExecutionError, OpResult};
use crate::fixture::{ExecCtx, ResolvedPulls};
struct PanicIfCalled;
impl OpDispenser for PanicIfCalled {
fn execute<'a>(
&'a self,
_cycle: u64,
_ctx: &'a ExecCtx<'a>,
) -> std::pin::Pin<
Box<dyn std::future::Future<Output = Result<OpResult, ExecutionError>> + Send + 'a>,
> {
Box::pin(async {
panic!(
"DryRunWrapper invariant violated: inner dispenser was called \
in dryrun mode. The wrapper must short-circuit BEFORE any \
wrapped layer (verify, metrics, poll, …) observes the result."
);
})
}
}
#[tokio::test]
async fn dry_run_wrapper_short_circuits_inner() {
let inner: Arc<dyn OpDispenser> = Arc::new(PanicIfCalled);
let wrapper = DryRunWrapper::wrap(inner);
let mut kernel =
polydat::dsl::compile::compile_polydat_interpreter("input cycle: u64\n").unwrap();
let cw = crate::wires::CycleWires::new(&mut kernel);
let fields = crate::adapter::ResolvedFields::new(vec![], vec![]);
let pulls = ResolvedPulls::empty();
let ctx = ExecCtx::with_wires(&fields, &pulls, &cw);
let result = wrapper
.execute(0, &ctx)
.await
.expect("dryrun should succeed");
assert!(result.body.is_none(), "dryrun result carries no body");
assert!(
result.skipped,
"dryrun result is marked skipped so any wrapper that DID sit \
outside us (defensive) honours the existing skip-on-skipped \
contract"
);
}
}