use dataflow_rs::Engine;
use serde_json::json;
use std::collections::HashSet;
mod common;
fn is_live_operator(engine: &Engine, name: &str) -> bool {
let expression = json!({ name: [] });
match common::eval(engine, expression.clone(), json!({})) {
Err(_) => true,
Ok(rendered) => rendered != expression,
}
}
#[test]
fn a_name_outside_the_vocabulary_is_inert_data() {
let engine = Engine::builder().build().unwrap();
for typo in ["lenght", "not_an_operator", "starts_wth"] {
assert!(
!is_live_operator(&engine, typo),
"'{typo}' is not an operator, so it must echo back as data"
);
assert!(
!engine.operator_names().any(|n| n == typo),
"and it must not be in the vocabulary"
);
}
}
#[test]
fn every_reported_operator_name_is_live() {
let engine = Engine::builder().build().unwrap();
for name in engine.operator_names() {
assert!(
is_live_operator(&engine, name),
"'{name}' is reported as an operator but the engine treats it as \
inert data — the mirrored list has drifted from datalogic"
);
}
}
#[test]
fn the_vocabulary_tracks_the_compiled_families() {
let engine = Engine::builder().build().unwrap();
let names: HashSet<&str> = engine.operator_names().collect();
for core in ["var", "if", "==", "map", "reduce", "missing"] {
assert!(names.contains(core), "'{core}' is core vocabulary");
}
#[cfg(feature = "ext-string")]
{
assert!(names.contains("length"));
assert!(is_live_operator(&engine, "length"));
}
#[cfg(not(feature = "ext-string"))]
{
assert!(!names.contains("length"));
assert!(
!is_live_operator(&engine, "length"),
"with ext-string off, {{\"length\": …}} is data, and the vocabulary agrees"
);
}
#[cfg(feature = "ext-control")]
{
assert!(names.contains("switch"));
assert!(names.contains("match"));
}
#[cfg(not(feature = "ext-control"))]
{
assert!(!names.contains("switch"));
assert!(!names.contains("match"));
}
#[cfg(feature = "datetime")]
assert!(names.contains("now"));
#[cfg(not(feature = "datetime"))]
assert!(!names.contains("now"));
#[cfg(feature = "ext-object")]
assert!(names.contains("entries"));
#[cfg(not(feature = "ext-object"))]
assert!(!names.contains("entries"));
}
#[test]
fn no_name_is_reported_twice() {
let engine = Engine::builder().build().unwrap();
let reported: Vec<&str> = engine.operator_names().collect();
let unique: HashSet<&str> = reported.iter().copied().collect();
assert_eq!(
reported.len(),
unique.len(),
"a name is reported more than once: {reported:?}"
);
}
#[test]
fn a_registered_custom_operator_joins_the_vocabulary() {
let bare = Engine::builder().build().unwrap();
assert!(!bare.operator_names().any(|n| n == "shout"));
let engine = Engine::builder()
.with_datalogic_operator("shout", common::Shout)
.build()
.unwrap();
assert!(
engine.operator_names().any(|n| n == "shout"),
"an operator registered through the builder is part of this build's vocabulary"
);
}
#[test]
fn a_custom_operator_survives_a_hot_reload() {
let engine = Engine::builder()
.with_datalogic_operator("shout", common::Shout)
.build()
.unwrap();
let reloaded = engine.with_new_workflows(Vec::new()).unwrap();
assert!(
reloaded.operator_names().any(|n| n == "shout"),
"hot reload rebuilds the datalogic engine and must re-register operators"
);
}
#[test]
fn a_custom_name_shadowing_a_builtin_is_reported_once() {
let engine = Engine::builder()
.with_datalogic_operator("cat", common::Shout)
.build()
.unwrap();
assert_eq!(
engine.operator_names().filter(|n| *n == "cat").count(),
1,
"one name is one entry, whichever side supplied it"
);
}