use ftui_core::geometry::Rect;
use ftui_layout::Rects;
use ftui_layout::dep_graph::{CycleError, DepGraph, InputKind, NodeId};
use ftui_layout::incremental::IncrementalLayout;
use serde_json::json;
use std::collections::HashSet;
use std::io::Write as _;
use std::sync::Mutex;
use std::time::Instant;
struct JsonlLog {
entries: Mutex<Vec<serde_json::Value>>,
}
impl JsonlLog {
fn new() -> Self {
Self {
entries: Mutex::new(Vec::new()),
}
}
fn emit(&self, entry: serde_json::Value) {
self.entries.lock().unwrap().push(entry);
}
fn flush(&self, test_name: &str) {
let entries = self.entries.lock().unwrap();
let mut stderr = std::io::stderr().lock();
for entry in entries.iter() {
let _ = writeln!(stderr, "[JSONL] {test_name}: {entry}");
}
}
fn len(&self) -> usize {
self.entries.lock().unwrap().len()
}
}
fn elapsed_ns(start: &Instant) -> u64 {
start.elapsed().as_nanos() as u64
}
fn area(w: u16, h: u16) -> Rect {
Rect::new(0, 0, w, h)
}
fn split_equal(a: Rect, n: usize) -> Rects {
if n == 0 {
return Rects::new();
}
let w = a.width / n as u16;
(0..n)
.map(|i| Rect::new(a.x + (i as u16) * w, a.y, w, a.height))
.collect()
}
fn xorshift32(state: &mut u32) -> u32 {
let mut x = *state;
x ^= x << 13;
x ^= x >> 17;
x ^= x << 5;
*state = x;
x
}
fn walk_tree(inc: &mut IncrementalLayout, root: NodeId, root_area: Rect) {
let child_count = inc.graph().dependents(root).len();
let root_rects = inc.get_or_compute(root, root_area, |a| split_equal(a, child_count.max(1)));
let children: Vec<_> = inc.graph().dependents(root).to_vec();
for (i, child) in children.iter().enumerate() {
let child_area = if i < root_rects.len() {
root_rects[i]
} else {
Rect::default()
};
let gc_count = inc.graph().dependents(*child).len();
let child_rects =
inc.get_or_compute(*child, child_area, |a| split_equal(a, gc_count.max(1)));
let grandchildren: Vec<_> = inc.graph().dependents(*child).to_vec();
for (j, gc) in grandchildren.iter().enumerate() {
let gc_area = if j < child_rects.len() {
child_rects[j]
} else {
Rect::default()
};
inc.get_or_compute(*gc, gc_area, |a| Rects::from_elem(a, 1));
}
}
}
fn build_tree(
children: usize,
gc_per: usize,
) -> (IncrementalLayout, NodeId, Vec<NodeId>, Vec<NodeId>) {
let total = 1 + children + children * gc_per;
let mut inc = IncrementalLayout::with_capacity(total);
let root = inc.add_node(None);
let mut child_ids = Vec::with_capacity(children);
let mut gc_ids = Vec::with_capacity(children * gc_per);
for _ in 0..children {
let child = inc.add_node(Some(root));
child_ids.push(child);
for _ in 0..gc_per {
let gc = inc.add_node(Some(child));
gc_ids.push(gc);
}
}
(inc, root, child_ids, gc_ids)
}
fn collect_hashes(inc: &IncrementalLayout, root: NodeId) -> Vec<(NodeId, Option<u64>)> {
let mut result = vec![(root, inc.result_hash(root))];
let children: Vec<_> = inc.graph().dependents(root).to_vec();
for child in &children {
result.push((*child, inc.result_hash(*child)));
let gcs: Vec<_> = inc.graph().dependents(*child).to_vec();
for gc in &gcs {
result.push((*gc, inc.result_hash(*gc)));
}
}
result
}
#[test]
fn e2e_dep_graph_construction_widget_tree() {
let log = JsonlLog::new();
let start = Instant::now();
log.emit(json!({
"test": "dep_graph_construction_widget_tree",
"phase": "setup",
"timestamp_ns": elapsed_ns(&start),
}));
let mut g = DepGraph::new();
let root = g.add_node();
let sidebar = g.add_node();
let content = g.add_node();
let header = g.add_node();
let body = g.add_node();
let footer = g.add_node();
g.add_edge(sidebar, root).unwrap();
g.add_edge(content, root).unwrap();
g.set_parent(sidebar, root);
g.set_parent(content, root);
g.add_edge(header, content).unwrap();
g.add_edge(body, content).unwrap();
g.add_edge(footer, content).unwrap();
g.set_parent(header, content);
g.set_parent(body, content);
g.set_parent(footer, content);
log.emit(json!({
"test": "dep_graph_construction_widget_tree",
"phase": "execute",
"node_count": g.node_count(),
"edge_count": g.edge_count(),
"timestamp_ns": elapsed_ns(&start),
}));
assert_eq!(g.node_count(), 6);
assert_eq!(g.edge_count(), 5);
assert_eq!(g.dependents(root).len(), 2);
assert_eq!(g.dependents(content).len(), 3);
assert_eq!(g.dependents(sidebar).len(), 0);
assert_eq!(g.dependencies(sidebar), &[root]);
assert_eq!(g.dependencies(header), &[content]);
assert_eq!(g.parent(sidebar), Some(root));
assert_eq!(g.parent(header), Some(content));
assert_eq!(g.parent(root), None);
log.emit(json!({
"test": "dep_graph_construction_widget_tree",
"phase": "verify",
"pass": true,
"timestamp_ns": elapsed_ns(&start),
}));
log.flush("dep_graph_construction_widget_tree");
}
#[test]
fn e2e_dep_graph_construction_flat_100() {
let log = JsonlLog::new();
let start = Instant::now();
let mut g = DepGraph::with_capacity(101, 100);
let root = g.add_node();
for _ in 0..100 {
let child = g.add_node();
g.add_edge(child, root).unwrap();
g.set_parent(child, root);
}
log.emit(json!({
"test": "dep_graph_construction_flat_100",
"phase": "execute",
"node_count": g.node_count(),
"edge_count": g.edge_count(),
"timestamp_ns": elapsed_ns(&start),
}));
assert_eq!(g.node_count(), 101);
assert_eq!(g.edge_count(), 100);
assert_eq!(g.dependents(root).len(), 100);
log.emit(json!({
"test": "dep_graph_construction_flat_100",
"phase": "verify",
"pass": true,
"timestamp_ns": elapsed_ns(&start),
}));
log.flush("dep_graph_construction_flat_100");
}
#[test]
fn e2e_dirty_propagation_exact_transitive_closure() {
let log = JsonlLog::new();
let start = Instant::now();
let mut g = DepGraph::new();
let r = g.add_node();
let a = g.add_node();
let b = g.add_node();
let c = g.add_node();
let d = g.add_node();
let e = g.add_node();
g.add_edge(a, r).unwrap();
g.add_edge(b, r).unwrap();
g.add_edge(c, a).unwrap();
g.add_edge(d, a).unwrap();
g.add_edge(e, b).unwrap();
g.set_parent(a, r);
g.set_parent(b, r);
g.set_parent(c, a);
g.set_parent(d, a);
g.set_parent(e, b);
g.mark_dirty(a);
let dirty = g.propagate();
let dirty_set: HashSet<_> = dirty.iter().copied().collect();
log.emit(json!({
"test": "dirty_propagation_exact_transitive_closure",
"phase": "execute",
"dirty_count": dirty.len(),
"dirty_ids": dirty.iter().map(|n| n.raw()).collect::<Vec<_>>(),
"timestamp_ns": elapsed_ns(&start),
}));
assert!(dirty_set.contains(&a));
assert!(dirty_set.contains(&c));
assert!(dirty_set.contains(&d));
assert!(!dirty_set.contains(&r));
assert!(!dirty_set.contains(&b));
assert!(!dirty_set.contains(&e));
assert_eq!(dirty.len(), 3, "no over-propagation");
log.emit(json!({
"test": "dirty_propagation_exact_transitive_closure",
"phase": "verify",
"pass": true,
"no_over_propagation": true,
"no_under_propagation": true,
"timestamp_ns": elapsed_ns(&start),
}));
log.flush("dirty_propagation_exact_transitive_closure");
}
#[test]
fn e2e_dirty_propagation_root_dirties_all() {
let log = JsonlLog::new();
let start = Instant::now();
let mut g = DepGraph::with_capacity(111, 110);
let root = g.add_node();
for _ in 0..10 {
let child = g.add_node();
g.add_edge(child, root).unwrap();
g.set_parent(child, root);
for _ in 0..10 {
let gc = g.add_node();
g.add_edge(gc, child).unwrap();
g.set_parent(gc, child);
}
}
g.mark_dirty(root);
let dirty = g.propagate();
log.emit(json!({
"test": "dirty_propagation_root_dirties_all",
"phase": "execute",
"total_nodes": g.node_count(),
"dirty_count": dirty.len(),
"timestamp_ns": elapsed_ns(&start),
}));
assert_eq!(
dirty.len(),
111,
"root dirty should propagate to all descendants"
);
log.emit(json!({
"test": "dirty_propagation_root_dirties_all",
"phase": "verify",
"pass": true,
"timestamp_ns": elapsed_ns(&start),
}));
log.flush("dirty_propagation_root_dirties_all");
}
#[test]
fn e2e_dirty_propagation_leaf_only() {
let log = JsonlLog::new();
let start = Instant::now();
let mut g = DepGraph::with_capacity(111, 110);
let root = g.add_node();
let mut leaves = Vec::new();
for _ in 0..10 {
let child = g.add_node();
g.add_edge(child, root).unwrap();
g.set_parent(child, root);
for _ in 0..10 {
let gc = g.add_node();
g.add_edge(gc, child).unwrap();
g.set_parent(gc, child);
leaves.push(gc);
}
}
g.mark_dirty(leaves[42]);
let dirty = g.propagate();
log.emit(json!({
"test": "dirty_propagation_leaf_only",
"phase": "execute",
"dirty_count": dirty.len(),
"leaf_id": leaves[42].raw(),
"timestamp_ns": elapsed_ns(&start),
}));
assert_eq!(dirty.len(), 1);
assert_eq!(dirty[0], leaves[42]);
log.emit(json!({
"test": "dirty_propagation_leaf_only",
"phase": "verify",
"pass": true,
"timestamp_ns": elapsed_ns(&start),
}));
log.flush("dirty_propagation_leaf_only");
}
#[test]
fn e2e_dirty_propagation_diamond_no_duplicate() {
let log = JsonlLog::new();
let start = Instant::now();
let mut g = DepGraph::new();
let a = g.add_node();
let b = g.add_node();
let c = g.add_node();
let d = g.add_node();
g.add_edge(b, a).unwrap();
g.add_edge(c, a).unwrap();
g.add_edge(d, b).unwrap();
g.add_edge(d, c).unwrap();
g.mark_dirty(a);
let dirty = g.propagate();
log.emit(json!({
"test": "dirty_propagation_diamond_no_duplicate",
"phase": "execute",
"dirty_count": dirty.len(),
"timestamp_ns": elapsed_ns(&start),
}));
assert_eq!(dirty.len(), 4);
let unique: HashSet<_> = dirty.iter().collect();
assert_eq!(unique.len(), 4, "each node appears exactly once in diamond");
log.flush("dirty_propagation_diamond_no_duplicate");
}
#[test]
fn e2e_cycle_detection_self_loop() {
let log = JsonlLog::new();
let start = Instant::now();
let mut g = DepGraph::new();
let a = g.add_node();
let result = g.add_edge(a, a);
log.emit(json!({
"test": "cycle_detection_self_loop",
"phase": "execute",
"cycle_detected": result.is_err(),
"timestamp_ns": elapsed_ns(&start),
}));
assert!(result.is_err());
assert_eq!(result.unwrap_err(), CycleError { from: a, to: a });
log.flush("cycle_detection_self_loop");
}
#[test]
fn e2e_cycle_detection_two_node() {
let log = JsonlLog::new();
let start = Instant::now();
let mut g = DepGraph::new();
let a = g.add_node();
let b = g.add_node();
g.add_edge(a, b).unwrap();
let result = g.add_edge(b, a);
log.emit(json!({
"test": "cycle_detection_two_node",
"phase": "execute",
"cycle_detected": result.is_err(),
"timestamp_ns": elapsed_ns(&start),
}));
assert!(result.is_err());
assert_eq!(result.unwrap_err(), CycleError { from: b, to: a });
log.flush("cycle_detection_two_node");
}
#[test]
fn e2e_cycle_detection_long_chain() {
let log = JsonlLog::new();
let start = Instant::now();
let mut g = DepGraph::new();
let mut nodes = Vec::new();
for _ in 0..5 {
nodes.push(g.add_node());
}
for i in 0..4 {
g.add_edge(nodes[i], nodes[i + 1]).unwrap();
}
let result = g.add_edge(nodes[4], nodes[0]);
log.emit(json!({
"test": "cycle_detection_long_chain",
"phase": "execute",
"chain_length": 5,
"cycle_detected": result.is_err(),
"timestamp_ns": elapsed_ns(&start),
}));
assert!(result.is_err());
log.flush("cycle_detection_long_chain");
}
#[test]
fn e2e_cycle_detection_diamond_no_false_positive() {
let log = JsonlLog::new();
let start = Instant::now();
let mut g = DepGraph::new();
let a = g.add_node();
let b = g.add_node();
let c = g.add_node();
let d = g.add_node();
g.add_edge(a, b).unwrap();
g.add_edge(a, c).unwrap();
g.add_edge(b, d).unwrap();
let result = g.add_edge(c, d);
log.emit(json!({
"test": "cycle_detection_diamond_no_false_positive",
"phase": "execute",
"false_positive": result.is_err(),
"timestamp_ns": elapsed_ns(&start),
}));
assert!(result.is_ok(), "diamond should not be detected as cycle");
log.flush("cycle_detection_diamond_no_false_positive");
}
#[test]
fn e2e_cache_invalidation_exactly_right_nodes() {
let log = JsonlLog::new();
let start = Instant::now();
let (mut inc, root, _children, grandchildren) = build_tree(5, 3);
inc.propagate();
let a = area(200, 60);
walk_tree(&mut inc, root, a);
let s = inc.stats();
let first_pass_recomputed = s.recomputed;
log.emit(json!({
"test": "cache_invalidation_exactly_right_nodes",
"phase": "setup",
"total_nodes": inc.node_count(),
"first_pass_recomputed": first_pass_recomputed,
"timestamp_ns": elapsed_ns(&start),
}));
inc.reset_stats();
inc.mark_dirty(grandchildren[7]);
inc.propagate();
walk_tree(&mut inc, root, a);
let s = inc.stats();
log.emit(json!({
"test": "cache_invalidation_exactly_right_nodes",
"phase": "execute",
"recomputed": s.recomputed,
"cached": s.cached,
"total": s.total,
"hit_rate": s.hit_rate(),
"timestamp_ns": elapsed_ns(&start),
}));
assert_eq!(s.recomputed, 1, "exactly 1 node should recompute");
assert_eq!(s.cached, inc.node_count() - 1, "all other nodes cached");
log.emit(json!({
"test": "cache_invalidation_exactly_right_nodes",
"phase": "verify",
"pass": true,
"timestamp_ns": elapsed_ns(&start),
}));
log.flush("cache_invalidation_exactly_right_nodes");
}
#[test]
fn e2e_cache_invalidation_hash_dedup() {
let log = JsonlLog::new();
let start = Instant::now();
let mut inc = IncrementalLayout::new();
let n = inc.add_node(None);
inc.propagate();
let a = area(80, 24);
inc.get_or_compute(n, a, |a| split_equal(a, 2));
inc.mark_changed(n, InputKind::Constraint, 42);
inc.propagate();
inc.get_or_compute(n, a, |a| split_equal(a, 2));
inc.reset_stats();
inc.mark_changed(n, InputKind::Constraint, 42);
inc.propagate();
inc.get_or_compute(n, a, |a| split_equal(a, 2));
let s = inc.stats();
log.emit(json!({
"test": "cache_invalidation_hash_dedup",
"phase": "execute",
"recomputed": s.recomputed,
"cached": s.cached,
"timestamp_ns": elapsed_ns(&start),
}));
assert_eq!(s.cached, 1, "same hash should not trigger recompute");
assert_eq!(s.recomputed, 0);
log.flush("cache_invalidation_hash_dedup");
}
#[test]
fn e2e_cache_invalidation_area_change() {
let log = JsonlLog::new();
let start = Instant::now();
let mut inc = IncrementalLayout::new();
let n = inc.add_node(None);
inc.propagate();
inc.get_or_compute(n, area(80, 24), |a| split_equal(a, 2));
inc.reset_stats();
inc.get_or_compute(n, area(120, 40), |a| split_equal(a, 2));
let s = inc.stats();
log.emit(json!({
"test": "cache_invalidation_area_change",
"phase": "execute",
"recomputed": s.recomputed,
"cached": s.cached,
"timestamp_ns": elapsed_ns(&start),
}));
assert_eq!(s.recomputed, 1, "area change should trigger recompute");
log.flush("cache_invalidation_area_change");
}
#[test]
fn e2e_generation_counter_o1_dirty_check() {
let log = JsonlLog::new();
let start = Instant::now();
let mut g = DepGraph::with_capacity(10_000, 0);
let mut nodes = Vec::with_capacity(10_000);
for _ in 0..10_000 {
nodes.push(g.add_node());
}
let check_start = Instant::now();
let mut dirty_count = 0;
for &n in &nodes {
if g.is_dirty(n) {
dirty_count += 1;
}
}
let check_ns = elapsed_ns(&check_start);
log.emit(json!({
"test": "generation_counter_o1_dirty_check",
"phase": "execute",
"nodes_checked": 10_000,
"dirty_found": dirty_count,
"check_elapsed_ns": check_ns,
"timestamp_ns": elapsed_ns(&start),
}));
assert_eq!(dirty_count, 0);
assert!(
check_ns < 10_000_000,
"10K dirty checks took {check_ns}ns, expected < 10ms"
);
log.flush("generation_counter_o1_dirty_check");
}
#[test]
fn e2e_generation_counter_clean_all_o1() {
let log = JsonlLog::new();
let start = Instant::now();
let mut g = DepGraph::with_capacity(10_000, 0);
for _ in 0..10_000 {
let n = g.add_node();
g.mark_dirty(n);
}
g.propagate();
assert_eq!(g.dirty_count(), 10_000);
let clean_start = Instant::now();
g.clean_all();
let clean_ns = elapsed_ns(&clean_start);
log.emit(json!({
"test": "generation_counter_clean_all_o1",
"phase": "execute",
"nodes_cleaned": 10_000,
"clean_elapsed_ns": clean_ns,
"dirty_after_clean": g.dirty_count(),
"timestamp_ns": elapsed_ns(&start),
}));
assert_eq!(g.dirty_count(), 0);
log.flush("generation_counter_clean_all_o1");
}
#[test]
fn e2e_generation_counter_wrap_around() {
let log = JsonlLog::new();
let start = Instant::now();
let mut g = DepGraph::new();
let a = g.add_node();
let b = g.add_node();
g.add_edge(b, a).unwrap();
for _ in 0..1000 {
g.mark_dirty(a);
g.propagate();
g.clean_all();
}
g.mark_dirty(a);
let dirty = g.propagate();
assert!(dirty.contains(&a));
assert!(dirty.contains(&b));
g.clean_all();
assert!(!g.is_dirty(a));
assert!(!g.is_dirty(b));
log.emit(json!({
"test": "generation_counter_wrap_around",
"phase": "verify",
"cycles": 1000,
"pass": true,
"timestamp_ns": elapsed_ns(&start),
}));
log.flush("generation_counter_wrap_around");
}
#[test]
fn e2e_edge_case_empty_tree() {
let log = JsonlLog::new();
let start = Instant::now();
let inc = IncrementalLayout::new();
assert_eq!(inc.node_count(), 0);
assert_eq!(inc.cache_len(), 0);
let s = inc.stats();
assert_eq!(s.total, 0);
assert_eq!(s.recomputed, 0);
assert_eq!(s.cached, 0);
log.emit(json!({
"test": "edge_case_empty_tree",
"phase": "verify",
"pass": true,
"timestamp_ns": elapsed_ns(&start),
}));
log.flush("edge_case_empty_tree");
}
#[test]
fn e2e_edge_case_single_node() {
let log = JsonlLog::new();
let start = Instant::now();
let mut inc = IncrementalLayout::new();
let n = inc.add_node(None);
inc.propagate();
let rects = inc.get_or_compute(n, area(80, 24), |_| Rects::new());
assert!(rects.is_empty());
let s = inc.stats();
assert_eq!(s.total, 1);
assert_eq!(s.recomputed, 1);
log.emit(json!({
"test": "edge_case_single_node",
"phase": "verify",
"pass": true,
"timestamp_ns": elapsed_ns(&start),
}));
log.flush("edge_case_single_node");
}
#[test]
fn e2e_edge_case_10k_flat_nodes() {
let log = JsonlLog::new();
let start = Instant::now();
let mut inc = IncrementalLayout::with_capacity(10_001);
let root = inc.add_node(None);
let mut children = Vec::with_capacity(10_000);
for _ in 0..10_000 {
children.push(inc.add_node(Some(root)));
}
inc.propagate();
log.emit(json!({
"test": "edge_case_10k_flat_nodes",
"phase": "setup",
"node_count": inc.node_count(),
"timestamp_ns": elapsed_ns(&start),
}));
let a = area(200, 60);
let root_rects = inc.get_or_compute(root, a, |a| split_equal(a, 1));
for &c in &children {
inc.get_or_compute(c, root_rects[0], |a| Rects::from_elem(a, 1));
}
let s = inc.stats();
assert_eq!(s.recomputed, 10_001);
inc.reset_stats();
inc.mark_dirty(children[5000]);
inc.propagate();
let root_rects = inc.get_or_compute(root, a, |a| split_equal(a, 1));
for &c in &children {
inc.get_or_compute(c, root_rects[0], |a| Rects::from_elem(a, 1));
}
let s = inc.stats();
log.emit(json!({
"test": "edge_case_10k_flat_nodes",
"phase": "execute",
"recomputed": s.recomputed,
"cached": s.cached,
"hit_rate": s.hit_rate(),
"timestamp_ns": elapsed_ns(&start),
}));
assert_eq!(s.recomputed, 1, "only 1 of 10K nodes should recompute");
assert_eq!(s.cached, 10_000);
log.flush("edge_case_10k_flat_nodes");
}
#[test]
fn e2e_edge_case_add_remove_during_layout() {
let log = JsonlLog::new();
let start = Instant::now();
let mut inc = IncrementalLayout::new();
let root = inc.add_node(None);
let _c1 = inc.add_node(Some(root));
let c2 = inc.add_node(Some(root));
inc.propagate();
let a = area(120, 24);
walk_tree(&mut inc, root, a);
assert_eq!(inc.cache_len(), 3);
inc.remove_node(c2);
assert!(
inc.is_dirty(root),
"parent should be dirty after child removal"
);
assert_eq!(inc.cache_len(), 2);
let c3 = inc.add_node(Some(root));
assert!(inc.is_dirty(c3));
inc.propagate();
inc.reset_stats();
walk_tree(&mut inc, root, a);
let s = inc.stats();
log.emit(json!({
"test": "edge_case_add_remove_during_layout",
"phase": "execute",
"nodes_after": inc.node_count(),
"recomputed": s.recomputed,
"cached": s.cached,
"timestamp_ns": elapsed_ns(&start),
}));
assert!(s.recomputed >= 2, "root + c3 should recompute at minimum");
log.flush("edge_case_add_remove_during_layout");
}
#[test]
fn e2e_edge_case_zero_size_area() {
let log = JsonlLog::new();
let start = Instant::now();
let mut inc = IncrementalLayout::new();
let n = inc.add_node(None);
inc.propagate();
let zero = Rect::default();
inc.get_or_compute(n, zero, |_| Rects::new());
inc.reset_stats();
inc.get_or_compute(n, zero, |_| Rects::new());
assert_eq!(inc.stats().cached, 1);
log.emit(json!({
"test": "edge_case_zero_size_area",
"phase": "verify",
"pass": true,
"timestamp_ns": elapsed_ns(&start),
}));
log.flush("edge_case_zero_size_area");
}
#[test]
fn e2e_edge_case_multiple_input_kinds() {
let log = JsonlLog::new();
let start = Instant::now();
let mut inc = IncrementalLayout::new();
let n = inc.add_node(None);
inc.propagate();
let a = area(80, 24);
inc.get_or_compute(n, a, |a| split_equal(a, 2));
inc.mark_changed(n, InputKind::Constraint, 10);
inc.propagate();
inc.get_or_compute(n, a, |a| split_equal(a, 2));
inc.reset_stats();
inc.mark_changed(n, InputKind::Content, 0); inc.propagate();
inc.get_or_compute(n, a, |a| split_equal(a, 2));
assert_eq!(inc.stats().cached, 1, "same content hash should not dirty");
inc.mark_changed(n, InputKind::Style, 99);
inc.propagate();
inc.reset_stats();
inc.get_or_compute(n, a, |a| split_equal(a, 2));
assert_eq!(inc.stats().recomputed, 1, "new style hash should dirty");
log.emit(json!({
"test": "edge_case_multiple_input_kinds",
"phase": "verify",
"pass": true,
"timestamp_ns": elapsed_ns(&start),
}));
log.flush("edge_case_multiple_input_kinds");
}
#[test]
fn e2e_propagation_order_is_deterministic() {
let log = JsonlLog::new();
let start = Instant::now();
let mut g = DepGraph::new();
let r = g.add_node();
let a = g.add_node();
let b = g.add_node();
let c = g.add_node();
let d = g.add_node();
g.add_edge(a, r).unwrap();
g.add_edge(b, r).unwrap();
g.add_edge(c, a).unwrap();
g.add_edge(d, b).unwrap();
g.set_parent(a, r);
g.set_parent(b, r);
g.set_parent(c, a);
g.set_parent(d, b);
let mut reference: Option<Vec<NodeId>> = None;
for i in 0..100 {
g.clean_all();
g.mark_dirty(r);
let dirty = g.propagate();
if let Some(ref expected) = reference {
assert_eq!(
&dirty, expected,
"propagation order diverged at iteration {i}"
);
} else {
reference = Some(dirty);
}
}
log.emit(json!({
"test": "propagation_order_is_deterministic",
"phase": "verify",
"iterations": 100,
"pass": true,
"timestamp_ns": elapsed_ns(&start),
}));
log.flush("propagation_order_is_deterministic");
}
#[test]
fn e2e_node_removal_and_recycling() {
let log = JsonlLog::new();
let start = Instant::now();
let mut g = DepGraph::new();
let a = g.add_node();
let b = g.add_node();
let c = g.add_node();
assert_eq!(g.node_count(), 3);
g.remove_node(b);
assert_eq!(g.node_count(), 2);
let d = g.add_node();
assert_eq!(d.raw(), b.raw(), "slot should be recycled");
assert_eq!(g.node_count(), 3);
assert!(!g.is_dirty(b));
assert!(!g.is_dirty(d));
log.emit(json!({
"test": "node_removal_and_recycling",
"phase": "verify",
"recycled_slot": d.raw(),
"pass": true,
"timestamp_ns": elapsed_ns(&start),
}));
let _ = (a, c);
log.flush("node_removal_and_recycling");
}
#[test]
fn e2e_isomorphism_incremental_equals_full() {
let log = JsonlLog::new();
let start = Instant::now();
for &(children, gc_per) in &[(3, 2), (5, 3), (10, 10), (20, 5)] {
let total = 1 + children + children * gc_per;
let (mut inc, root, _child_ids, _gc_ids) = build_tree(children, gc_per);
inc.propagate();
let a = area(200, 60);
walk_tree(&mut inc, root, a);
let incr_hashes = collect_hashes(&inc, root);
let incr_root_hash = inc.result_hash(root);
inc.set_force_full(true);
inc.reset_stats();
walk_tree(&mut inc, root, a);
let full_hashes = collect_hashes(&inc, root);
let full_root_hash = inc.result_hash(root);
inc.set_force_full(false);
log.emit(json!({
"test": "isomorphism_incremental_equals_full",
"phase": "verify",
"tree_size": total,
"children": children,
"gc_per": gc_per,
"incr_root_hash": incr_root_hash,
"full_root_hash": full_root_hash,
"checksum_match": incr_root_hash == full_root_hash,
"timestamp_ns": elapsed_ns(&start),
}));
assert_eq!(
incr_root_hash, full_root_hash,
"root hash mismatch for {children}x{gc_per} tree"
);
for (i, ((id_i, h_i), (id_f, h_f))) in
incr_hashes.iter().zip(full_hashes.iter()).enumerate()
{
assert_eq!(id_i, id_f, "node order mismatch at position {i}");
assert_eq!(
h_i, h_f,
"hash mismatch at node {id_i} (position {i}) for {children}x{gc_per} tree"
);
}
}
log.flush("isomorphism_incremental_equals_full");
}
#[test]
fn e2e_isomorphism_after_partial_dirty() {
let log = JsonlLog::new();
let start = Instant::now();
let (mut inc, root, _children, grandchildren) = build_tree(10, 10);
inc.propagate();
let a = area(200, 60);
walk_tree(&mut inc, root, a);
let dirty_indices = [0, 15, 42, 77, 99];
for &idx in &dirty_indices {
inc.mark_dirty(grandchildren[idx]);
}
inc.propagate();
walk_tree(&mut inc, root, a);
let incr_hashes = collect_hashes(&inc, root);
inc.invalidate_all();
inc.propagate();
inc.set_force_full(true);
walk_tree(&mut inc, root, a);
let full_hashes = collect_hashes(&inc, root);
inc.set_force_full(false);
log.emit(json!({
"test": "isomorphism_after_partial_dirty",
"phase": "verify",
"dirty_count": dirty_indices.len(),
"total_nodes": inc.node_count(),
"timestamp_ns": elapsed_ns(&start),
}));
for (i, ((id_i, h_i), (id_f, h_f))) in incr_hashes.iter().zip(full_hashes.iter()).enumerate() {
assert_eq!(id_i, id_f, "node order mismatch at {i}");
assert_eq!(
h_i, h_f,
"hash mismatch at node {id_i} (position {i}): incr={h_i:?} full={h_f:?}"
);
}
log.flush("isomorphism_after_partial_dirty");
}
#[test]
fn e2e_mutation_stress_seeded_rng() {
let log = JsonlLog::new();
let start = Instant::now();
let (mut inc, root, _children, grandchildren) = build_tree(10, 10);
inc.propagate();
let a = area(200, 60);
walk_tree(&mut inc, root, a);
let num_frames = 200;
let mut rng_state = 0xDEAD_BEEFu32;
for frame in 0..num_frames {
let num_dirty = (xorshift32(&mut rng_state) % 5 + 1) as usize;
for _ in 0..num_dirty {
let idx = (xorshift32(&mut rng_state) as usize) % grandchildren.len();
inc.mark_dirty(grandchildren[idx]);
}
inc.propagate();
inc.reset_stats();
walk_tree(&mut inc, root, a);
let incr_hash = inc.result_hash(root);
inc.invalidate_all();
inc.propagate();
inc.set_force_full(true);
walk_tree(&mut inc, root, a);
let full_hash = inc.result_hash(root);
inc.set_force_full(false);
if frame % 50 == 0 {
log.emit(json!({
"test": "mutation_stress_seeded_rng",
"phase": "execute",
"frame": frame,
"num_dirty": num_dirty,
"incr_hash": incr_hash,
"full_hash": full_hash,
"checksum_match": incr_hash == full_hash,
"timestamp_ns": elapsed_ns(&start),
}));
}
assert_eq!(
incr_hash, full_hash,
"frame {frame}: incremental != full (dirty={num_dirty})"
);
}
log.emit(json!({
"test": "mutation_stress_seeded_rng",
"phase": "verify",
"frames_tested": num_frames,
"pass": true,
"timestamp_ns": elapsed_ns(&start),
}));
log.flush("mutation_stress_seeded_rng");
}
#[test]
fn e2e_resize_during_incremental() {
let log = JsonlLog::new();
let start = Instant::now();
let (mut inc, root, _children, grandchildren) = build_tree(5, 4);
inc.propagate();
let areas = [
area(200, 60),
area(120, 40),
area(80, 24),
area(300, 80),
area(200, 60), ];
walk_tree(&mut inc, root, areas[0]);
for (i, &a) in areas.iter().enumerate().skip(1) {
if i < grandchildren.len() {
inc.mark_dirty(grandchildren[i]);
}
inc.propagate();
inc.reset_stats();
walk_tree(&mut inc, root, a);
let incr_hash = inc.result_hash(root);
inc.invalidate_all();
inc.propagate();
inc.set_force_full(true);
walk_tree(&mut inc, root, a);
let full_hash = inc.result_hash(root);
inc.set_force_full(false);
log.emit(json!({
"test": "resize_during_incremental",
"phase": "execute",
"resize_step": i,
"area_w": a.width,
"area_h": a.height,
"incr_hash": incr_hash,
"full_hash": full_hash,
"checksum_match": incr_hash == full_hash,
"timestamp_ns": elapsed_ns(&start),
}));
assert_eq!(
incr_hash, full_hash,
"resize step {i} ({}x{}): incremental != full",
a.width, a.height
);
}
log.emit(json!({
"test": "resize_during_incremental",
"phase": "verify",
"resize_steps": areas.len() - 1,
"pass": true,
"timestamp_ns": elapsed_ns(&start),
}));
log.flush("resize_during_incremental");
}
#[test]
fn e2e_concurrent_dirty_marking() {
let log = JsonlLog::new();
let start = Instant::now();
let (mut inc, root, children, grandchildren) = build_tree(10, 10);
inc.propagate();
let a = area(200, 60);
walk_tree(&mut inc, root, a);
inc.mark_changed(grandchildren[0], InputKind::Constraint, 42);
inc.mark_changed(grandchildren[50], InputKind::Content, 99);
inc.mark_changed(children[3], InputKind::Style, 77);
inc.mark_dirty(grandchildren[99]);
inc.propagate();
inc.reset_stats();
walk_tree(&mut inc, root, a);
let incr_hash = inc.result_hash(root);
let incr_stats = inc.stats();
inc.invalidate_all();
inc.propagate();
inc.set_force_full(true);
walk_tree(&mut inc, root, a);
let full_hash = inc.result_hash(root);
inc.set_force_full(false);
log.emit(json!({
"test": "concurrent_dirty_marking",
"phase": "execute",
"dirty_sources": 4,
"recomputed": incr_stats.recomputed,
"cached": incr_stats.cached,
"hit_rate": incr_stats.hit_rate(),
"incr_hash": incr_hash,
"full_hash": full_hash,
"checksum_match": incr_hash == full_hash,
"timestamp_ns": elapsed_ns(&start),
}));
assert_eq!(
incr_hash, full_hash,
"concurrent dirty: incremental != full"
);
assert!(
incr_stats.recomputed < inc.node_count(),
"should not recompute all {} nodes, got {}",
inc.node_count(),
incr_stats.recomputed
);
log.emit(json!({
"test": "concurrent_dirty_marking",
"phase": "verify",
"pass": true,
"timestamp_ns": elapsed_ns(&start),
}));
log.flush("concurrent_dirty_marking");
}
#[test]
fn e2e_flex_sibling_isomorphism() {
let log = JsonlLog::new();
let start = Instant::now();
let (mut inc, root, children, _grandchildren) = build_tree(5, 3);
inc.propagate();
let a = area(200, 60);
walk_tree(&mut inc, root, a);
inc.mark_dirty_with_ancestors(children[2]);
inc.propagate();
inc.reset_stats();
walk_tree(&mut inc, root, a);
let incr_hash = inc.result_hash(root);
let incr_stats = inc.stats();
inc.invalidate_all();
inc.propagate();
inc.set_force_full(true);
walk_tree(&mut inc, root, a);
let full_hash = inc.result_hash(root);
inc.set_force_full(false);
log.emit(json!({
"test": "flex_sibling_isomorphism",
"phase": "execute",
"recomputed": incr_stats.recomputed,
"cached": incr_stats.cached,
"incr_hash": incr_hash,
"full_hash": full_hash,
"checksum_match": incr_hash == full_hash,
"timestamp_ns": elapsed_ns(&start),
}));
assert_eq!(
incr_hash, full_hash,
"flex sibling pattern: incremental != full"
);
assert_eq!(
incr_stats.recomputed,
inc.node_count(),
"all nodes should recompute when root is dirtied via ancestors"
);
log.flush("flex_sibling_isomorphism");
}
#[test]
fn e2e_hit_rate_improves_with_locality() {
let log = JsonlLog::new();
let start = Instant::now();
let (mut inc, root, _children, grandchildren) = build_tree(10, 10);
inc.propagate();
let a = area(200, 60);
walk_tree(&mut inc, root, a);
inc.reset_stats();
walk_tree(&mut inc, root, a);
let s0 = inc.stats();
assert!(
(s0.hit_rate() - 1.0).abs() < 0.001,
"0% dirty should give ~100% hit rate, got {}",
s0.hit_rate()
);
inc.reset_stats();
inc.mark_dirty(grandchildren[42]);
inc.propagate();
walk_tree(&mut inc, root, a);
let s1 = inc.stats();
inc.reset_stats();
for i in 0..10 {
inc.mark_dirty(grandchildren[i * 10]);
}
inc.propagate();
walk_tree(&mut inc, root, a);
let s10 = inc.stats();
log.emit(json!({
"test": "hit_rate_improves_with_locality",
"phase": "verify",
"hit_rate_0pct": s0.hit_rate(),
"hit_rate_1pct": s1.hit_rate(),
"hit_rate_10pct": s10.hit_rate(),
"recomputed_0pct": s0.recomputed,
"recomputed_1pct": s1.recomputed,
"recomputed_10pct": s10.recomputed,
"timestamp_ns": elapsed_ns(&start),
}));
assert!(s0.hit_rate() > s1.hit_rate());
assert!(s1.hit_rate() > s10.hit_rate());
assert!(
s10.hit_rate() > 0.50,
"10% dirty should still have >50% hit rate, got {}",
s10.hit_rate()
);
log.flush("hit_rate_improves_with_locality");
}
#[test]
fn e2e_stats_jsonl_evidence() {
let log = JsonlLog::new();
let start = Instant::now();
let (mut inc, root, _children, grandchildren) = build_tree(10, 10);
inc.propagate();
let a = area(200, 60);
walk_tree(&mut inc, root, a);
let full_stats = inc.stats();
log.emit(json!({
"test": "stats_jsonl_evidence",
"phase": "full_pass",
"recomputed": full_stats.recomputed,
"cached": full_stats.cached,
"total": full_stats.total,
"cache_entries": full_stats.cache_entries,
"hit_rate": full_stats.hit_rate(),
"node_count": inc.node_count(),
"timestamp_ns": elapsed_ns(&start),
}));
inc.reset_stats();
for i in 0..5 {
inc.mark_dirty(grandchildren[i * 20]);
}
inc.propagate();
walk_tree(&mut inc, root, a);
let incr_stats = inc.stats();
log.emit(json!({
"test": "stats_jsonl_evidence",
"phase": "incremental_5pct",
"recomputed": incr_stats.recomputed,
"cached": incr_stats.cached,
"total": incr_stats.total,
"cache_entries": incr_stats.cache_entries,
"hit_rate": incr_stats.hit_rate(),
"dirty_count": 5,
"node_count": inc.node_count(),
"timestamp_ns": elapsed_ns(&start),
}));
assert_eq!(
full_stats.total,
inc.node_count(),
"full pass should visit every node"
);
assert_eq!(full_stats.cached, 0, "cold cache should have 0 hits");
assert!(
incr_stats.cached > incr_stats.recomputed,
"incremental should have more hits than misses"
);
assert!(log.len() >= 2, "at least 2 JSONL records should be emitted");
log.flush("stats_jsonl_evidence");
}
#[test]
fn e2e_perf_regression_1111_nodes() {
let log = JsonlLog::new();
let start = Instant::now();
let (mut inc, root, _children, grandchildren) = build_tree(10, 100);
inc.propagate();
let a = area(200, 60);
walk_tree(&mut inc, root, a);
inc.reset_stats();
for i in 0..10 {
inc.mark_dirty(grandchildren[i * 100]);
}
inc.propagate();
let incr_start = Instant::now();
walk_tree(&mut inc, root, a);
let incr_ns = elapsed_ns(&incr_start);
let incr_stats = inc.stats();
inc.invalidate_all();
inc.propagate();
inc.reset_stats();
let full_start = Instant::now();
walk_tree(&mut inc, root, a);
let full_ns = elapsed_ns(&full_start);
let full_stats = inc.stats();
let overhead_pct = if full_ns > 0 {
(incr_ns as f64 / full_ns as f64) * 100.0
} else {
0.0
};
log.emit(json!({
"test": "perf_regression_1111_nodes",
"phase": "execute",
"incr_ns": incr_ns,
"full_ns": full_ns,
"overhead_pct": overhead_pct,
"incr_recomputed": incr_stats.recomputed,
"incr_cached": incr_stats.cached,
"incr_hit_rate": incr_stats.hit_rate(),
"full_recomputed": full_stats.recomputed,
"total_nodes": inc.node_count(),
"timestamp_ns": elapsed_ns(&start),
}));
assert!(
incr_stats.recomputed < full_stats.recomputed,
"incremental ({}) should recompute fewer than full ({})",
incr_stats.recomputed,
full_stats.recomputed,
);
log.emit(json!({
"test": "perf_regression_1111_nodes",
"phase": "verify",
"pass": true,
"timestamp_ns": elapsed_ns(&start),
}));
log.flush("perf_regression_1111_nodes");
}