use super::Builtin;
use crate::base::{Change, ErrorRecord};
use crate::def::{ParseOptions, parse};
use crate::function::{Registry, SyncCtx, task_record};
use crate::program::{Callee, CompileOptions, Compiled};
use crate::run::policy::decide;
use crate::state::{RunContext, Tuning};
use datavalue::OwnedDataValue;
use serde_json::{Map, Value, json};
use std::collections::BTreeMap;
use std::sync::{Arc, Mutex};
const NOW: &str = "2026-10-14T00:00:00+00:00";
const TIMESTAMP: &str = "<timestamp>";
const ENGINE_KEYS: [&str; 3] = ["processed_at", "engine_version", "progress"];
#[derive(Debug, PartialEq, Eq)]
pub(crate) struct Observed {
pub(crate) context: String,
pub(crate) changes: String,
pub(crate) errors: String,
pub(crate) status: Option<u16>,
pub(crate) error_code: Option<String>,
}
impl Observed {
pub(crate) fn golden(&self) -> OwnedDataValue {
let parsed = |text: &str| OwnedDataValue::from_json(text).expect("observed JSON");
OwnedDataValue::Object(vec![
("context".to_owned(), parsed(&self.context)),
("changes".to_owned(), parsed(&self.changes)),
("errors".to_owned(), parsed(&self.errors)),
(
"status".to_owned(),
self.status
.map_or(OwnedDataValue::Null, |s| OwnedDataValue::from(&json!(s))),
),
(
"error_code".to_owned(),
self.error_code
.as_ref()
.map_or(OwnedDataValue::Null, |c| OwnedDataValue::String(c.clone())),
),
])
}
}
fn workflow(function: &str, input: &Value) -> Value {
json!({"id": "w", "name": "w", "tasks": [{
"id": "t", "name": "t", "continue_on_error": true,
"function": {"name": function, "input": input}
}]})
}
pub(crate) fn seed(context: &Value) -> OwnedDataValue {
let root = |key: &str| match context.get(key) {
Some(v @ Value::Object(_)) => OwnedDataValue::from(&sorted(v)),
_ => OwnedDataValue::Object(Vec::new()),
};
OwnedDataValue::Object(vec![
("data".to_string(), root("data")),
("metadata".to_string(), root("metadata")),
("temp_data".to_string(), root("temp_data")),
])
}
fn context_text(mut context: OwnedDataValue) -> String {
if let OwnedDataValue::Object(roots) = &mut context
&& let Some((_, OwnedDataValue::Object(metadata))) =
roots.iter_mut().find(|(k, _)| k == "metadata")
{
metadata.retain(|(k, _)| !ENGINE_KEYS.contains(&k.as_str()));
}
serde_json::to_string(&context).expect("a context serializes")
}
pub(crate) fn untouched(context: &Value) -> String {
context_text(seed(context))
}
pub(crate) fn context_text_after(function: &str, input: &Value, context: OwnedDataValue) -> String {
let task = task(function, input).unwrap_or_else(|| panic!("{function} compiles"));
let mut slot = context;
{
let mut rc = RunContext::new(&mut slot, Tuning::default());
let _ = task.call(
&mut rc,
&OwnedDataValue::Null,
false,
&mut Vec::new(),
&mut Vec::new(),
);
rc.finish();
}
context_text(slot)
}
pub(crate) struct Task {
compiled: Compiled,
}
pub(crate) fn task(function: &str, input: &Value) -> Option<Task> {
let def = parse(&workflow(function, input), ParseOptions::default()).def?;
let compiled = Registry::default()
.compiler(CompileOptions::default())
.compile_one(&def)
.ok()?;
Some(Task { compiled })
}
impl Task {
fn builtin(&self) -> &Builtin {
match &self.compiled.workflow.calls[0].callee {
Callee::Builtin(builtin) => builtin,
Callee::Host(_) => panic!("the task calls a built-in"),
}
}
pub(crate) fn call<'p>(
&'p self,
rc: &mut RunContext<'_, 'p>,
payload: &OwnedDataValue,
capture: bool,
changes: &mut Vec<Change>,
errors: &mut Vec<ErrorRecord>,
) -> (Option<u16>, Option<String>) {
let workflow = &self.compiled.workflow;
let workflow_id: Arc<str> = Arc::from(workflow.id.as_str());
let task_id: Arc<str> = Arc::from(workflow.tasks[0].id.as_str());
let now: Arc<str> = Arc::from(NOW);
let mut now_text = Some(Arc::clone(&now));
let budget = crate::run::TaskBudget::unlimited();
let outcome = self.builtin().call(&mut SyncCtx {
rc,
exprs: workflow.registry.exprs(),
budget: &budget,
condition_error_policy: crate::run::ConditionErrorPolicy::Skip,
payload,
workflow_id: &workflow_id,
task_id: &task_id,
task_path: &workflow.tasks[0].path,
loop_counter: None,
capture,
traced: false,
changes,
marked: false,
mapping_ends: None,
errors: &mut *errors,
now: chrono::Utc::now(),
now_text: &mut now_text,
});
let error_code = outcome.as_ref().err().map(|e| e.code().to_owned());
let decision = decide(workflow.calls[0].policy, &task_id, outcome);
if let Some((code, message)) = decision.record {
let mut rec = task_record(code, message, &workflow_id, &task_id, &now);
if let Some((cause, site)) = decision.meta {
rec.cause = cause;
rec.site = site;
}
errors.push(rec);
}
(decision.status, error_code)
}
pub(crate) fn run(&self, payload: &Value, context: &Value, capture: bool) -> Observed {
let payload = OwnedDataValue::from(&sorted(payload));
let mut slot = seed(context);
let mut changes = Vec::new();
let mut errors = Vec::new();
let (status, error_code) = {
let mut rc = RunContext::new(&mut slot, Tuning::default());
let called = self.call(&mut rc, &payload, capture, &mut changes, &mut errors);
rc.finish();
called
};
if status.is_none() || error_code.is_some() {
changes.clear();
}
for error in &mut errors {
if error.timestamp.is_some() {
error.timestamp = Some(Arc::from(TIMESTAMP));
}
error.message = dataflow_corpus::normalise::dependency_text(&error.message);
}
Observed {
context: context_text(slot),
changes: serde_json::to_string(&changes).expect("changes serialize"),
errors: serde_json::to_string(&errors).expect("errors serialize"),
status,
error_code,
}
}
pub(crate) fn run_both(&self, payload: &Value, context: &Value) -> Observed {
let on = self.run(payload, context, true);
let off = self.run(payload, context, false);
assert_eq!(
(&off.context, &off.errors, off.status, &off.error_code),
(&on.context, &on.errors, on.status, &on.error_code),
"capture off over {context} (payload {payload})"
);
assert_eq!(off.changes, "[]", "capture off records no change");
on
}
}
pub(crate) fn run_once(
function: &str,
input: &Value,
payload: &Value,
context: &Value,
capture: bool,
) -> Observed {
task(function, input)
.unwrap_or_else(|| panic!("{function} compiles with {input}"))
.run(payload, context, capture)
}
pub(crate) fn observe(function: &str, input: &Value, payload: &Value, context: &Value) -> Observed {
task(function, &sorted(input))
.unwrap_or_else(|| panic!("{function} compiles with {input}"))
.run_both(payload, context)
}
pub(crate) fn sorted(value: &Value) -> Value {
match value {
Value::Object(map) => {
let mut keys: Vec<&String> = map.keys().collect();
keys.sort();
let mut out = Map::new();
for key in keys {
out.insert(key.clone(), sorted(&map[key]));
}
Value::Object(out)
}
Value::Array(items) => Value::Array(items.iter().map(sorted).collect()),
other => other.clone(),
}
}
fn sorted_text(value: &Value) -> String {
sorted(value).to_string()
}
fn writing() -> bool {
std::env::var_os("DATAFLOW_GOLDEN_WRITE").is_some_and(|v| v == "1")
}
fn golden_path(file: &str) -> std::path::PathBuf {
std::path::Path::new(env!("CARGO_MANIFEST_DIR"))
.join("tests/golden/lib")
.join(format!("{file}.json"))
}
type Cases = BTreeMap<String, Vec<(String, OwnedDataValue)>>;
fn read_golden(file: &str) -> Cases {
let Ok(text) = std::fs::read_to_string(golden_path(file)) else {
return Cases::new();
};
let OwnedDataValue::Object(tests) = OwnedDataValue::from_json(&text).expect("a golden parses")
else {
panic!("{file}.json is an object of tests");
};
tests
.into_iter()
.map(|(test, cases)| {
let OwnedDataValue::Array(cases) = cases else {
panic!("{file}.json: {test} is a list of cases");
};
let cases = cases
.into_iter()
.map(|case| match case {
OwnedDataValue::Array(mut pair) if pair.len() == 2 => {
let value = pair.pop().expect("two");
let Some(OwnedDataValue::String(name)) = pair.pop() else {
panic!("{file}.json: {test}: a case is named");
};
(name, value)
}
other => panic!("{file}.json: {test}: a case is [name, value]: {other:?}"),
})
.collect();
(test, cases)
})
.collect()
}
fn write_golden(file: &str, tests: &Cases) {
let mut text = String::from("{");
for (i, (test, cases)) in tests.iter().enumerate() {
text.push_str(if i == 0 { "\n" } else { ",\n" });
text.push_str(&format!(" {}: [", Value::from(test.as_str())));
for (j, (name, value)) in cases.iter().enumerate() {
text.push_str(if j == 0 { "\n" } else { ",\n" });
let value = serde_json::to_string(value).expect("a golden serializes");
text.push_str(&format!(" [{}, {value}]", Value::from(name.as_str())));
}
text.push_str("\n ]");
}
text.push_str("\n}\n");
std::fs::create_dir_all(golden_path(file).parent().expect("a directory"))
.expect("the golden directory");
std::fs::write(golden_path(file), text).expect("the golden is written");
}
static WRITE: Mutex<()> = Mutex::new(());
pub(crate) struct Golden {
file: String,
test: &'static str,
cases: Vec<(String, OwnedDataValue)>,
}
impl Golden {
pub(crate) fn new(file: &str, test: &'static str) -> Self {
Self::at(&format!("builtins/{file}"), test)
}
pub(crate) fn at(file: &str, test: &'static str) -> Self {
Self {
file: file.to_owned(),
test,
cases: Vec::new(),
}
}
pub(crate) fn case(&mut self, name: String, value: OwnedDataValue) {
self.cases.push((name, value));
}
pub(crate) fn observe(
&mut self,
function: &str,
input: &Value,
payload: &Value,
context: &Value,
) -> Observed {
let observed = observe(function, input, payload, context);
let name = format!(
"{function} {} payload {} over {}",
sorted_text(input),
sorted_text(payload),
sorted_text(context)
);
self.case(name, observed.golden());
observed
}
fn text(value: &OwnedDataValue) -> String {
serde_json::to_string(value).expect("a golden serializes")
}
}
impl Drop for Golden {
fn drop(&mut self) {
if std::thread::panicking() {
return;
}
let (file, test) = (self.file.as_str(), self.test);
if writing() {
let _alone = WRITE
.lock()
.unwrap_or_else(std::sync::PoisonError::into_inner);
let mut tests = read_golden(file);
tests.insert(test.to_owned(), std::mem::take(&mut self.cases));
write_golden(file, &tests);
return;
}
let mut tests = read_golden(file);
let want = tests.remove(test).unwrap_or_else(|| {
panic!("{file}.json has no {test}: run with DATAFLOW_GOLDEN_WRITE=1")
});
for (i, ((got_name, got), (want_name, want))) in self.cases.iter().zip(&want).enumerate() {
assert_eq!(got_name, want_name, "{file}.json: {test}: case {i}'s name");
assert_eq!(
Self::text(got),
Self::text(want),
"{file}.json: {test}: {got_name}"
);
}
assert_eq!(
self.cases.len(),
want.len(),
"{file}.json: {test}: the number of cases"
);
}
}
pub(crate) mod logs {
use super::{Observed, task};
use log::{Level, LevelFilter, Log, Metadata, Record};
use serde_json::Value;
use std::sync::{Mutex, Once};
const TARGET: &str = "dataflow::log";
const FAILED: &str = "Log: Failed to evaluate expression";
static STATE: Mutex<(Option<Level>, Vec<String>)> = Mutex::new((None, Vec::new()));
static INSTALL: Once = Once::new();
static ONE_AT_A_TIME: Mutex<()> = Mutex::new(());
static CAPTURE: Capture = Capture;
struct Capture;
impl Log for Capture {
fn enabled(&self, metadata: &Metadata<'_>) -> bool {
let level = STATE.lock().expect("the log state").0;
level.is_some_and(|level| {
if metadata.target() == TARGET {
metadata.level() <= level
} else {
metadata.level() == Level::Error
}
})
}
fn log(&self, record: &Record<'_>) {
if !self.enabled(record.metadata()) {
return;
}
let line = if record.target() == TARGET {
format!("{}: {}", record.level(), record.args())
} else if record.args().to_string().starts_with(FAILED) {
format!("{}: {FAILED}", record.level())
} else {
return;
};
STATE.lock().expect("the log state").1.push(line);
}
fn flush(&self) {}
}
fn capture<R>(level: Level, run: impl FnOnce() -> R) -> (R, Vec<String>) {
let _alone = ONE_AT_A_TIME
.lock()
.unwrap_or_else(std::sync::PoisonError::into_inner);
INSTALL.call_once(|| {
log::set_logger(&CAPTURE).expect("no other logger is installed");
log::set_max_level(LevelFilter::Trace);
});
{
let mut state = STATE.lock().expect("the log state");
*state = (Some(level), Vec::new());
}
let result = run();
let mut state = STATE.lock().expect("the log state");
state.0 = None;
(result, std::mem::take(&mut state.1))
}
pub(crate) fn logged(level: Level, input: &Value, context: &Value) -> (Observed, Vec<String>) {
let task = task("log", input).expect("log compiles");
capture(level, || task.run(&Value::Null, context, true))
}
}