Skip to main content

dataflow_rs/function/
testing.rs

1//! One function call outside a run, for a host's unit tests.
2//!
3//! [`call`] runs one call of one function through the path a run takes:
4//!
5//! - the config compiles as it would in a workflow: the function is
6//!   registered, its config compiled under a [`Prepare`](super::Prepare) naming it, and the
7//!   checks a workflow's compile runs walk the config's expressions;
8//! - the call gets a real [`Ctx`](super::Ctx) and overlay over the message
9//!   given, and the layers given run around it, as a run's layers do;
10//! - it settles as a run settles a call: an `Ok`, whatever its [`Outcome`],
11//!   applies the call's writes to the message, an `Err` drops them and their
12//!   `Change`s, and the records the call added are kept either way.
13//!
14//! No recorder runs, so the message gains no audit entry, no
15//! `metadata.progress` and no record of the call's own `Err`: the [`Called`]
16//! says what the call returned. No registry, compiler, definition or engine
17//! is assembled by hand. [`config`] compiles a config alone, and
18//! [`config_with`] compiles one with the host's own [`ExprEngine`].
19
20pub use super::overlay::Write;
21use super::registry::Entry;
22use super::{
23    BoundCall, BoxFuture, CallInfo, CallLayer, CallStart, Function, FunctionConfig, Outcome,
24    Registry,
25};
26use crate::base::any_rejected;
27use crate::base::{
28    AuthoredPath, CancelToken, Change, Error, ErrorKind, ErrorRecord, Instant, Issue, ProgramId,
29    Result, StepKind, TaskIx, WorkflowIx,
30};
31use crate::expr::{CompileCx, ExprEngine, Path};
32use crate::program::{Callee, CalleeAt, Checks, compile_callee, refused};
33use crate::run::host::{Invoked, Layers, apply_writes, invoke, keep_records};
34use crate::run::{Message, TaskIds};
35use crate::state::{RunContext, Tuning};
36use chrono::Utc;
37use datavalue::OwnedDataValue;
38use serde_json::Value;
39use std::fmt;
40use std::future::IntoFuture;
41use std::sync::Arc;
42
43/// The workflow and task ids a tested call is compiled and run under.
44const TEST: &str = "test";
45
46/// One call of `f`, with its config compiled from `input` as a workflow
47/// would compile it; `.await` runs it.
48///
49/// The builder's setters are optional: an empty message, no layers,
50/// [`ExprEngine::default`], the name `"function"`, changes captured, and no
51/// channel, deadline or cancellation token.
52///
53/// # Panics
54///
55/// When awaited, wherever [`Registry::register`] panics: on a name it
56/// reserves, or on a spec the function's config cannot honour.
57///
58/// ```
59/// # tokio::runtime::Builder::new_current_thread().build().unwrap().block_on(async {
60/// use dataflow_rs::datavalue::OwnedDataValue;
61/// use dataflow_rs::expr::Output;
62/// use dataflow_rs::function::testing::{self, Write};
63/// use dataflow_rs::function::{BoxFuture, CallLayer, FunctionConfig, Next};
64/// use dataflow_rs::program::IssueCode;
65/// use dataflow_rs::{Ctx, Expr, Function, Message, Outcome, Result};
66/// use serde_json::json;
67/// use std::collections::BTreeMap;
68/// use std::sync::atomic::{AtomicUsize, Ordering};
69/// use std::sync::Arc;
70///
71/// #[derive(FunctionConfig)]
72/// struct DbRead {
73///     query: Expr<String>,
74///     #[output(default = "data.rows")]
75///     output: Output,
76/// }
77///
78/// /// Answers each query from the rows it was built with, as the host's
79/// /// database would.
80/// struct MyFn {
81///     rows: BTreeMap<String, serde_json::Value>,
82/// }
83///
84/// impl MyFn {
85///     fn new(rows: BTreeMap<String, serde_json::Value>) -> Self {
86///         Self { rows }
87///     }
88/// }
89///
90/// impl Function for MyFn {
91///     type Config = DbRead;
92///
93///     async fn call(&self, ctx: &mut Ctx<'_>, cfg: &DbRead) -> Result<Outcome> {
94///         let query = ctx.eval(&cfg.query)?;
95///         let Some(rows) = self.rows.get(&*query) else {
96///             return Ok(Outcome::Status(404));
97///         };
98///         ctx.emit(&cfg.output, rows.into())?;
99///         Ok(Outcome::Ok)
100///     }
101/// }
102///
103/// /// Counts the calls it wraps.
104/// struct Counted(Arc<AtomicUsize>);
105///
106/// impl CallLayer for Counted {
107///     fn call<'a>(&'a self, ctx: &'a mut Ctx<'_>, next: Next<'a>) -> BoxFuture<'a, Result<Outcome>> {
108///         self.0.fetch_add(1, Ordering::Relaxed);
109///         next.run(ctx)
110///     }
111/// }
112///
113/// let deps = BTreeMap::from([(
114///     "select name from customers where id = 7".to_owned(),
115///     json!([{"name": "Ada"}]),
116/// )]);
117/// let calls = Arc::new(AtomicUsize::new(0));
118/// let input = json!({
119///     "query": {"cat": ["select name from customers where id = ", {"var": "data.id"}]},
120///     "output": "data.rows",
121/// });
122/// let data = OwnedDataValue::from(&json!({"id": 7}));
123/// let out = testing::call(MyFn::new(deps), input)
124///     .message(Message::from_parts(OwnedDataValue::Null, data, OwnedDataValue::Null))
125///     .layer(Counted(Arc::clone(&calls)))
126///     .await;
127///
128/// let rows = OwnedDataValue::from(&json!([{"name": "Ada"}]));
129/// assert!(out.issues.is_empty());
130/// assert_eq!(out.outcome.unwrap(), Outcome::Ok);
131/// assert_eq!(out.writes, [("data.rows".parse().unwrap(), Write::Set(rows.clone()))]);
132/// assert_eq!(out.changes.len(), 1);
133/// assert_eq!(out.message.context().get(&"data.rows".parse().unwrap()), Some(&rows));
134/// assert_eq!(calls.load(Ordering::Relaxed), 1);
135///
136/// // A config alone; `Err` carries every issue.
137/// let cfg: DbRead = testing::config(json!({"query": "select 1"})).ok().unwrap();
138/// assert_eq!(cfg.output.constant().unwrap().to_string(), "data.rows");
139/// let issues = testing::config::<DbRead>(json!({"qeury": "select 1"})).err().unwrap();
140/// let codes: Vec<IssueCode> = issues.iter().map(|i| i.code).collect();
141/// assert_eq!(codes, [IssueCode::LikelyTypo, IssueCode::InputParse]);
142/// # });
143/// ```
144pub fn call<F: Function>(f: F, input: Value) -> Call<F> {
145    Call {
146        f,
147        input,
148        message: None,
149        layers: Vec::new(),
150        exprs: None,
151        name: "function".to_owned(),
152        capture: true,
153        channel: None,
154        routing_bucket: None,
155        deadline: None,
156        cancel: None,
157    }
158}
159
160/// One call of a function, waiting to be awaited: what [`call`] returns.
161///
162/// It is [`IntoFuture`], so `.await` runs it, and the future is `Send` and
163/// `'static`, so a test may also spawn it.
164#[must_use = "a call does nothing until it is awaited"]
165pub struct Call<F: Function> {
166    f: F,
167    input: Value,
168    message: Option<Message>,
169    layers: Vec<Arc<dyn CallLayer>>,
170    exprs: Option<ExprEngine>,
171    name: String,
172    capture: bool,
173    channel: Option<String>,
174    routing_bucket: Option<u8>,
175    deadline: Option<Instant>,
176    cancel: Option<CancelToken>,
177}
178
179impl<F: Function> Call<F> {
180    /// The message the call runs over. By default an empty one: a `null`
181    /// payload and empty roots.
182    pub fn message(mut self, m: Message) -> Self {
183        self.message = Some(m);
184        self
185    }
186
187    /// Adds a layer around the call, as a run's layers wrap a call. The
188    /// first given is the outermost.
189    pub fn layer<L: CallLayer>(mut self, l: L) -> Self {
190        self.layers.push(Arc::new(l));
191        self
192    }
193
194    /// The engine the config compiles with and the call evaluates with,
195    /// secrets included. By default [`ExprEngine::default`].
196    pub fn exprs(mut self, e: ExprEngine) -> Self {
197        self.exprs = Some(e);
198        self
199    }
200
201    /// The name the function is registered and called by: what [`Prepare`](super::Prepare)
202    /// and [`CallInfo::function`] say. By default `"function"`; a built-in's
203    /// name is reserved, as [`Registry::register`] reserves it.
204    pub fn name(mut self, n: &str) -> Self {
205        n.clone_into(&mut self.name);
206        self
207    }
208
209    /// Whether the call records its `Change`s, as a run's
210    /// [`capture_changes`](method@crate::RunOptions::capture_changes) says.
211    /// On by default.
212    pub fn capture_changes(mut self, on: bool) -> Self {
213        self.capture = on;
214        self
215    }
216
217    /// The channel the call's run was started on, as
218    /// [`CallInfo::channel`] says. By default none.
219    pub fn channel(mut self, c: &str) -> Self {
220        self.channel = Some(c.to_owned());
221        self
222    }
223
224    /// The run's routing bucket, as [`CallInfo::routing_bucket`] says. By
225    /// default none.
226    pub fn routing_bucket(mut self, b: u8) -> Self {
227        self.routing_bucket = Some(b);
228        self
229    }
230
231    /// The run's deadline, as [`Ctx::deadline`](super::Ctx::deadline) says.
232    /// The call is checked against it and, natively, raced against it, as
233    /// a run's host call is: one it passes ends `Err` of kind
234    /// [`TimedOut`](ErrorKind::TimedOut), its writes dropped. A deadline
235    /// needs a Tokio runtime with its time driver. By default none.
236    pub fn deadline(mut self, at: Instant) -> Self {
237        self.deadline = Some(at);
238        self
239    }
240
241    /// The run's cancellation token, as [`Ctx::cancel`](super::Ctx::cancel)
242    /// says. The call is checked against it and, natively, raced against
243    /// it, as a run's host call is: one it cancels ends `Err` of kind
244    /// [`Cancelled`](ErrorKind::Cancelled), its writes dropped. By default
245    /// none.
246    pub fn cancel(mut self, token: CancelToken) -> Self {
247        self.cancel = Some(token);
248        self
249    }
250
251    /// Compiles the config, runs the call, and settles it.
252    async fn run(self) -> Called {
253        let Self {
254            f,
255            input,
256            message,
257            layers,
258            exprs,
259            name,
260            capture,
261            channel,
262            routing_bucket,
263            deadline,
264            cancel,
265        } = self;
266        let mut message = message.unwrap_or_else(|| Message::new(OwnedDataValue::Null));
267        // Registered as a host registers it, so `register`'s checks of the
268        // spec run here too.
269        let mut registry = Registry::new(exprs.unwrap_or_default());
270        registry.register(&name, f);
271        let (bound, issues) = compile(&registry, &name, &input);
272        let Some(bound) = bound else {
273            return Called {
274                issues,
275                outcome: Err(Error::new(
276                    ErrorKind::Compile,
277                    format!("the config of `{name}` was rejected; `issues` says why"),
278                )),
279                writes: Vec::new(),
280                changes: Vec::new(),
281                errors: Vec::new(),
282                message,
283            };
284        };
285        // The clock is read once, as a run reads it, for the records' stamp.
286        let now = Utc::now();
287        let mut now_text = None;
288        let parts = message.parts();
289        let mut rc = RunContext::new(parts.context, Tuning::default());
290        let Invoked {
291            result,
292            overlay,
293            changes,
294            errors,
295            ..
296        } = {
297            let reader = rc.reader();
298            let start = CallStart {
299                reader,
300                binding: None,
301                payload: parts.payload,
302                exprs: registry.exprs(),
303                info: CallInfo {
304                    program: ProgramId::next(),
305                    workflow: WorkflowIx::new(0),
306                    task: TaskIx::new(0),
307                    function: &name,
308                    workflow_id: TEST,
309                    task_id: TEST,
310                    channel: channel.as_deref(),
311                    routing_bucket,
312                    loop_counter: None,
313                    element_index: None,
314                },
315                deadline,
316                cancel: cancel.as_ref(),
317                capture,
318            };
319            let layers = Layers {
320                run: &layers,
321                registry: &[],
322            };
323            invoke(start, &*bound, layers, None, crate::run::timers_available()).await
324        };
325        // The settle, as a run's: the records the call added are stamped and
326        // pushed onto the message, and an `Ok` replays the overlay and keeps
327        // the `Change`s while an `Err` drops both. The settle takes the
328        // overlay, so `writes` is a copy of it.
329        let outcome = result.into_result();
330        let ok = outcome.is_ok();
331        let writes = if ok {
332            overlay.clone().into_writes()
333        } else {
334            Vec::new()
335        };
336        let test_id: Arc<str> = Arc::from(TEST);
337        let ids = TaskIds {
338            workflow: &test_id,
339            task: &test_id,
340        };
341        let errors_before = parts.errors.len();
342        keep_records(ids, now, &mut now_text, parts.errors, errors, None);
343        let errors = parts.errors[errors_before..].to_vec();
344        let changes = apply_writes(&mut rc, ok, overlay, changes);
345        rc.finish();
346        Called {
347            issues,
348            outcome,
349            writes,
350            changes,
351            errors,
352            message,
353        }
354    }
355}
356
357impl<F: Function> IntoFuture for Call<F> {
358    type Output = Called;
359    type IntoFuture = BoxFuture<'static, Called>;
360
361    fn into_future(self) -> Self::IntoFuture {
362        Box::pin(self.run())
363    }
364}
365
366impl<F: Function> fmt::Debug for Call<F> {
367    /// The name, the input, the layer count, whether changes are captured,
368    /// the channel and the deadline; not the function, the layers or the
369    /// token.
370    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
371        f.debug_struct("Call")
372            .field("name", &self.name)
373            .field("input", &self.input)
374            .field("layers", &self.layers.len())
375            .field("capture_changes", &self.capture)
376            .field("channel", &self.channel)
377            .field("deadline", &self.deadline)
378            .finish_non_exhaustive()
379    }
380}
381
382/// What one [`call`] did.
383#[derive(Debug)]
384#[non_exhaustive]
385pub struct Called {
386    /// Every issue compiling the config found, at its path below the input,
387    /// each stamped with the task `"test"`. Advisories and Defects may sit
388    /// beside an `Ok`.
389    pub issues: Vec<Issue>,
390    /// What the call returned: through the layers, as a run sees it. `Err`
391    /// of kind [`Compile`](ErrorKind::Compile) when the config was rejected
392    /// and nothing ran.
393    pub outcome: Result<Outcome>,
394    /// The call's writes, in the order it made them. Empty after an `Err`,
395    /// which drops them.
396    pub writes: Vec<(Path, Write)>,
397    /// The call's `Change`s, as the call recorded them: a write at a root
398    /// says [`replaced`](Change::replaced), as a trace's copy does, where
399    /// the task's audit entry, which keeps v3's bytes, does not. Empty after
400    /// an `Err`, or without capture.
401    pub changes: Vec<Change>,
402    /// The records the call added, kept whatever it returned, stamped as a
403    /// run stamps them: the ids (`test` for both) and a timestamp, where
404    /// they carry none.
405    pub errors: Vec<ErrorRecord>,
406    /// The message after the settle: the writes applied on `Ok`, the records
407    /// appended to its errors. Its audit is untouched, since no recorder ran.
408    pub message: Message,
409}
410
411/// A config compiled alone, against [`ExprEngine::default`], with nothing
412/// prepared or checked beyond the config's own compile. `Err` carries every
413/// issue, at its path below the input.
414///
415/// # Errors
416///
417/// Every issue the compile found, when any is Rejected or the config
418/// refused its input.
419pub fn config<C: FunctionConfig>(input: Value) -> std::result::Result<C, Vec<Issue>> {
420    config_with(&ExprEngine::default(), input)
421}
422
423/// A config compiled alone, as [`config`] compiles it, against `exprs`: the
424/// host's engine, with its own operators and secrets.
425///
426/// # Errors
427///
428/// Every issue the compile found, when any is Rejected or the config
429/// refused its input.
430pub fn config_with<C: FunctionConfig>(
431    exprs: &ExprEngine,
432    input: Value,
433) -> std::result::Result<C, Vec<Issue>> {
434    let mut issues = Vec::new();
435    let compiled = {
436        let mut cx = CompileCx::new(exprs, &mut issues);
437        let before = cx.rejected();
438        let compiled = C::compile(&input, &mut cx).filter(|_| cx.rejected() == before);
439        if compiled.is_none() && cx.rejected() == before {
440            refused(&mut cx, std::any::type_name::<C>());
441        }
442        compiled
443    };
444    compiled.ok_or(issues)
445}
446
447/// Compiles `name`'s config from `input` through the compiler's own
448/// [`compile_callee`], as a task's host function at workflow and task
449/// `"test"`, walked with the checks a workflow's compile runs. The bound
450/// call, unless anything Rejected was found, and every issue.
451fn compile(
452    registry: &Registry,
453    name: &str,
454    input: &Value,
455) -> (Option<Box<dyn BoundCall>>, Vec<Issue>) {
456    let entry = registry.entry(name);
457    let Some(entry @ Entry::Host(_)) = entry else {
458        unreachable!("`{name}` was registered as a host function");
459    };
460    let mut issues = Vec::new();
461    let at = CalleeAt {
462        name,
463        workflow_id: TEST,
464        step: (StepKind::Task, TEST),
465        input_at: &AuthoredPath::root(),
466    };
467    let mut checks = Checks::new(registry.exprs());
468    let callee = compile_callee(
469        registry.exprs(),
470        entry,
471        input,
472        &at,
473        &mut checks,
474        &mut issues,
475    );
476    let rejected = any_rejected(&issues);
477    let bound = match callee {
478        Some(Callee::Host(bound)) if !rejected => Some(bound),
479        _ => None,
480    };
481    (bound, issues)
482}