use active_core::{parse, Engine, IncrementalMemo, Value};
use std::collections::HashMap;
fn doc_10k() -> String {
let mut out = String::new();
for t in 0..50 {
out.push_str(&format!("[t{t}]\n"));
out.push_str(&format!("k0 = {t}\n"));
for k in 1..200 {
out.push_str(&format!("k{k} = k{} + 1\n", k - 1));
}
}
out
}
fn nested<'v>(value: &'v Value, table: &str, key: &str) -> &'v Value {
match value {
Value::Table(map) => match map.get(table) {
Some(Value::Table(inner)) => inner
.get(key)
.unwrap_or_else(|| panic!("{table}.{key} missing")),
other => panic!("table {table} missing: {other:?}"),
},
other => panic!("root table expected: {other:?}"),
}
}
fn int(value: &Value) -> i64 {
match value {
Value::Int(n) => *n,
other => panic!("int expected: {other:?}"),
}
}
fn json(value: &Value) -> String {
active_core::convert::value_to_pretty_json(value)
}
fn engine_10k() -> Engine {
let mut engine = Engine::new();
engine.budget.max_wall_ms = 0;
engine
}
#[test]
fn incremental_matches_full_evaluation_on_10k() {
let src = doc_10k();
let module = parse(&src, "incremental").expect("must parse");
let engine = engine_10k();
let (full, _) = engine.evaluate(&module).expect("must evaluate");
let mut memo = IncrementalMemo::default();
let (incr, _) = engine
.evaluate_incremental(&module, &mut memo)
.expect("must evaluate");
assert_eq!(json(&incr), json(&full));
assert_eq!(int(nested(&incr, "t0", "k199")), 199);
assert_eq!(int(nested(&incr, "t25", "k199")), 25 + 199);
assert_eq!(int(nested(&incr, "t49", "k199")), 49 + 199);
assert!(memo.last_memo_hits >= 9_751, "{}", memo.last_memo_hits);
assert!(memo.last_eval_count >= 10_000, "{}", memo.last_eval_count);
}
#[test]
fn leaf_edit_recomputes_only_the_affected_chain() {
let src = doc_10k();
let module = parse(&src, "incremental").expect("must parse");
let engine = engine_10k();
let mut memo = IncrementalMemo::default();
engine
.evaluate_incremental(&module, &mut memo)
.expect("must evaluate");
let full_count = memo.last_eval_count;
let first_hits = memo.last_memo_hits;
let edited = src.replacen("k0 = 0\n", "k0 = 100000\n", 1);
assert_ne!(edited, src, "fixture must contain the leaf");
let module2 = parse(&edited, "incremental").expect("must parse");
let (value, _) = engine
.evaluate_incremental(&module2, &mut memo)
.expect("must evaluate");
assert_eq!(int(nested(&value, "t0", "k199")), 100_199);
assert_eq!(int(nested(&value, "t1", "k199")), 1 + 199);
assert_eq!(int(nested(&value, "t49", "k199")), 49 + 199);
assert!(
memo.last_eval_count * 15 < full_count,
"recomputed {} of {full_count}",
memo.last_eval_count
);
assert_eq!(
memo.last_memo_hits + memo.last_eval_count,
full_count + first_hits,
);
}
#[test]
fn edit_aliasing_an_existing_value_serves_shared_memo() {
let src = doc_10k();
let module = parse(&src, "incremental").expect("must parse");
let engine = engine_10k();
let mut memo = IncrementalMemo::default();
engine
.evaluate_incremental(&module, &mut memo)
.expect("must evaluate");
let edited = src.replacen("k0 = 0\n", "k0 = 1\n", 1);
let module2 = parse(&edited, "incremental").expect("must parse");
let (value, _) = engine
.evaluate_incremental(&module2, &mut memo)
.expect("must evaluate");
assert_eq!(int(nested(&value, "t0", "k199")), 200);
assert_eq!(int(nested(&value, "t1", "k199")), 200);
assert!(
memo.last_eval_count <= 2,
"aliased chain must hit: {}",
memo.last_eval_count
);
}
#[test]
fn steady_state_reevaluation_serves_everything_from_memo() {
let src = "a = 1\nb = a + 1\n[c]\nd = \"${b}!\"\n";
let module = parse(src, "incremental").expect("must parse");
let engine = Engine::new();
let mut memo = IncrementalMemo::default();
engine
.evaluate_incremental(&module, &mut memo)
.expect("must evaluate");
assert!(memo.last_eval_count > 0);
engine
.evaluate_incremental(&module, &mut memo)
.expect("must evaluate");
assert_eq!(
memo.last_eval_count, 0,
"nothing changed, nothing recomputes"
);
assert!(memo.last_memo_hits > 0);
}
#[test]
fn env_change_invalidates_only_readers() {
let src = "a = env.ACTIVE_INC_A ?: \"x\"\nb = a\nc = 1\nd = c\n";
let module = parse(src, "incremental").expect("must parse");
let mut memo = IncrementalMemo::default();
let empty: HashMap<String, String> = HashMap::new();
let (first, _) = Engine::new()
.with_env(empty)
.evaluate_incremental(&module, &mut memo)
.expect("must evaluate");
let full_count = memo.last_eval_count;
assert!(json(&first).contains("\"x\""), "{}", json(&first));
let changed = HashMap::from([("ACTIVE_INC_A".to_string(), "y".to_string())]);
let (second, _) = Engine::new()
.with_env(changed)
.evaluate_incremental(&module, &mut memo)
.expect("must evaluate");
let rendered = json(&second);
assert!(rendered.contains("\"y\""), "{rendered}");
assert!(!rendered.contains("\"x\""), "{rendered}");
assert!(
memo.last_eval_count < full_count,
"recomputed {} of {full_count}",
memo.last_eval_count
);
}
#[test]
fn interpolation_edit_propagates_through_dependents_only() {
let src = "name = \"a\"\ngreeting = \"hi ${name}\"\nother = 1\n";
let module = parse(src, "incremental").expect("must parse");
let engine = Engine::new();
let mut memo = IncrementalMemo::default();
engine
.evaluate_incremental(&module, &mut memo)
.expect("must evaluate");
let full_count = memo.last_eval_count;
let edited = src.replace("name = \"a\"", "name = \"b\"");
let module2 = parse(&edited, "incremental").expect("must parse");
let (value, _) = engine
.evaluate_incremental(&module2, &mut memo)
.expect("must evaluate");
let rendered = json(&value);
assert!(rendered.contains("hi b"), "{rendered}");
assert!(memo.last_eval_count < full_count);
assert!(memo.last_memo_hits > 0, "untouched nodes must hit");
}
#[test]
fn secret_edit_propagates_and_stays_redacted() {
let src = "pw = \"s3cr3t-1\"\ntoken = secret(pw)\nplain = 1\n";
let module = parse(src, "incremental").expect("must parse");
let engine = Engine::new();
let mut memo = IncrementalMemo::default();
engine
.evaluate_incremental(&module, &mut memo)
.expect("must evaluate");
let edited = src.replace("s3cr3t-1", "s3cr3t-2");
let module2 = parse(&edited, "incremental").expect("must parse");
let (value, _) = engine
.evaluate_incremental(&module2, &mut memo)
.expect("must evaluate");
let rendered = json(&value);
assert!(
rendered.contains("\"token\": \"***SECRET***\""),
"{rendered}"
);
match &value {
Value::Table(map) => match map.get("token") {
Some(Value::Secret(s)) => assert_eq!(s, "s3cr3t-2"),
other => panic!("secret expected: {other:?}"),
},
other => panic!("table expected: {other:?}"),
}
}
#[test]
fn profile_switch_clears_the_memo() {
let src = "active_profile = \"dev\"\nbase = 1\nprofile \"dev\" {\nbase = 2\n}\n";
let module = parse(src, "incremental").expect("must parse");
let mut memo = IncrementalMemo::default();
let mut dev = Engine::new();
dev.profile_override = Some("dev".to_string());
let (first, _) = dev
.evaluate_incremental(&module, &mut memo)
.expect("must evaluate");
let full_count = memo.last_eval_count;
assert!(full_count > 0);
dev.evaluate_incremental(&module, &mut memo)
.expect("must evaluate");
assert_eq!(memo.last_eval_count, 0);
let mut prod = Engine::new();
prod.profile_override = Some("other".to_string());
let (second, _) = prod
.evaluate_incremental(&module, &mut memo)
.expect("must evaluate");
assert_eq!(memo.last_eval_count, full_count);
assert_ne!(json(&first), json(&second));
}
#[test]
fn budget_and_resilient_changes_clear_the_memo() {
let src = "a = 1\nb = a\n";
let module = parse(src, "incremental").expect("must parse");
let mut memo = IncrementalMemo::default();
let mut engine = Engine::new();
engine
.evaluate_incremental(&module, &mut memo)
.expect("must evaluate");
let full_count = memo.last_eval_count;
assert!(full_count > 0);
engine
.evaluate_incremental(&module, &mut memo)
.expect("must evaluate");
assert_eq!(memo.last_eval_count, 0);
engine.budget.max_wall_ms = 30_000;
engine
.evaluate_incremental(&module, &mut memo)
.expect("must evaluate");
assert_eq!(memo.last_eval_count, full_count);
engine.resilient = false;
engine
.evaluate_incremental(&module, &mut memo)
.expect("must evaluate");
assert_eq!(memo.last_eval_count, full_count);
}
#[test]
fn moved_ident_serves_the_current_span() {
let first = "x = nosuch\n";
let module = parse(first, "incremental").expect("must parse");
let engine = Engine::new();
let mut memo = IncrementalMemo::default();
let (_, diags1) = engine
.evaluate_incremental(&module, &mut memo)
.expect("must evaluate");
assert_eq!(diags1.iter().filter(|d| d.code == "W001").count(), 1);
assert_eq!(diags1[0].span.as_ref().map(|s| s.line), Some(1));
let moved = "\n\nx = nosuch\n";
let module2 = parse(moved, "incremental").expect("must parse");
let (_, diags2) = engine
.evaluate_incremental(&module2, &mut memo)
.expect("must evaluate");
let (_, fresh) = engine.evaluate(&module2).expect("must evaluate");
assert_eq!(diags2.len(), fresh.len(), "{diags2:?} vs {fresh:?}");
for (served, expected) in diags2.iter().zip(fresh.iter()) {
assert_eq!(served.code, expected.code);
assert_eq!(
served.span.as_ref().map(|s| (s.line, s.column)),
expected.span.as_ref().map(|s| (s.line, s.column)),
"stale span served: {served:?}"
);
}
assert_eq!(
diags2[0].span.as_ref().map(|s| s.line),
Some(3),
"{diags2:?}"
);
}
#[test]
fn errors_match_between_full_and_incremental() {
let src = "a = b\nb = a\n";
let module = parse(src, "incremental").expect("must parse");
let engine = Engine::new();
let full = engine.evaluate(&module).expect_err("cycle must fail");
let mut memo = IncrementalMemo::default();
let incr = engine
.evaluate_incremental(&module, &mut memo)
.expect_err("cycle must fail");
assert_eq!(full.to_string(), incr.to_string());
assert_eq!(memo.last_eval_count, 0);
assert_eq!(memo.last_memo_hits, 0);
}
#[test]
fn schema_doc_evaluates_identically_under_incremental() {
let src = "schema = { port = Int }\nport = 8080\n";
let module = parse(src, "incremental").expect("must parse");
let engine = Engine::new();
let (full, full_diags) = engine.evaluate(&module).expect("must evaluate");
let mut memo = IncrementalMemo::default();
let (incr, incr_diags) = engine
.evaluate_incremental(&module, &mut memo)
.expect("must evaluate");
assert_eq!(json(&incr), json(&full));
assert_eq!(incr_diags.len(), full_diags.len());
let (again, _) = engine
.evaluate_incremental(&module, &mut memo)
.expect("must evaluate");
assert_eq!(json(&again), json(&full));
}
#[test]
fn memo_debug_reports_shape_not_content() {
let src = "pw = \"s3cr3t-debug\"\ntoken = secret(pw)\n";
let module = parse(src, "incremental").expect("must parse");
let engine = Engine::new();
let mut memo = IncrementalMemo::default();
engine
.evaluate_incremental(&module, &mut memo)
.expect("must evaluate");
let rendered = format!("{memo:?}");
assert!(!rendered.contains("s3cr3t-debug"), "{rendered}");
assert!(!rendered.contains("Secret"), "{rendered}");
assert!(rendered.contains("entries"), "{rendered}");
}
#[test]
fn store_clear_fires_and_recovers_under_version_churn() {
fn version(v: i64) -> String {
(0..600)
.map(|k| format!("k{k} = {}\n", v * 1000 + k as i64))
.collect()
}
let engine = Engine::new();
let mut memo = IncrementalMemo::default();
let mut tripped = None;
for v in 0..30 {
let src = version(v);
let module = parse(&src, "incremental").expect("must parse");
let (first, _) = engine
.evaluate_incremental(&module, &mut memo)
.expect("must evaluate");
let full_count = memo.last_eval_count;
assert!(full_count > 500, "{full_count}");
match &first {
Value::Table(map) => assert_eq!(
int(map.get("k599").expect("k599 present")),
v * 1000 + 599,
"wrong values at v{v}"
),
other => panic!("table expected: {other:?}"),
}
let (second, _) = engine
.evaluate_incremental(&module, &mut memo)
.expect("must evaluate");
assert_eq!(json(&second), json(&first), "repeat diverged at v{v}");
if memo.last_eval_count * 2 > full_count {
tripped = Some(v);
break;
}
assert_eq!(memo.last_eval_count, 0, "no clear yet at v{v}");
}
let v = tripped.expect("cap must trip within 30 novel versions");
let module = parse(&version(v), "incremental").expect("must parse");
engine
.evaluate_incremental(&module, &mut memo)
.expect("must evaluate");
assert_eq!(memo.last_eval_count, 0, "store broken after clear");
}