use super::common::{abs_cell_ref, eval_config_with_range_limit};
use crate::engine::{DependencyGraph, Engine, EvalConfig, VertexId};
use crate::test_workbook::TestWorkbook;
use formualizer_common::{LiteralValue, parse_a1_1based};
use formualizer_parse::parse;
use formualizer_parse::parser::{ASTNode, ASTNodeType, ReferenceType};
fn range_ast(start_row: u32, start_col: u32, end_row: u32, end_col: u32) -> ASTNode {
ASTNode {
node_type: ASTNodeType::Reference {
original: format!("R{start_row}C{start_col}:R{end_row}C{end_col}"),
reference: ReferenceType::range(
None,
Some(start_row),
Some(start_col),
Some(end_row),
Some(end_col),
),
},
source_token: None,
contains_volatile: false,
}
}
fn sum_ast(start_row: u32, start_col: u32, end_row: u32, end_col: u32) -> ASTNode {
ASTNode {
node_type: ASTNodeType::Function {
name: "SUM".to_string(),
args: vec![range_ast(start_row, start_col, end_row, end_col)],
},
source_token: None,
contains_volatile: false,
}
}
fn graph_with_range_limit(limit: usize) -> DependencyGraph {
DependencyGraph::new_with_config(eval_config_with_range_limit(limit))
}
#[test]
fn test_tiny_range_expands_to_cell_dependencies() {
let mut graph = graph_with_range_limit(4);
graph
.set_cell_formula("Sheet1", 1, 3, sum_ast(1, 1, 4, 1))
.unwrap();
let c1_id = *graph
.get_vertex_id_for_address(&abs_cell_ref(0, 1, 3))
.unwrap();
let c1_vertex = graph
.get_vertex_id_for_address(&abs_cell_ref(0, 1, 3))
.unwrap();
let dependencies = graph.get_dependencies(c1_id);
assert_eq!(
dependencies.len(),
4,
"Should expand to 4 cell dependencies"
);
assert!(
graph.formula_to_range_deps().is_empty(),
"Should not create a compressed range dependency"
);
let mut dep_addrs = Vec::new();
for &dep_id in &dependencies {
let cell_ref = graph.get_cell_ref(dep_id).unwrap();
dep_addrs.push((cell_ref.coord.row(), cell_ref.coord.col()));
}
dep_addrs.sort();
let expected_addrs = vec![(0, 0), (1, 0), (2, 0), (3, 0)];
assert_eq!(dep_addrs, expected_addrs);
}
#[test]
fn test_range_dependency_dirtiness() {
let mut graph = DependencyGraph::new();
graph
.set_cell_formula("Sheet1", 1, 3, sum_ast(1, 1, 10, 1))
.unwrap();
let c1_id = *graph.cell_to_vertex().get(&abs_cell_ref(0, 1, 3)).unwrap();
graph
.set_cell_value("Sheet1", 5, 1, LiteralValue::Int(100))
.unwrap();
let all_ids: Vec<VertexId> = graph.cell_to_vertex().values().copied().collect();
graph.clear_dirty_flags(&all_ids);
assert!(graph.get_evaluation_vertices().is_empty());
graph
.set_cell_value("Sheet1", 5, 1, LiteralValue::Int(200))
.unwrap();
let eval_vertices = graph.get_evaluation_vertices();
assert!(!eval_vertices.is_empty());
assert!(eval_vertices.contains(&c1_id));
}
#[test]
fn test_range_dependency_updates_on_formula_change() {
let mut graph = DependencyGraph::new();
graph
.set_cell_formula("Sheet1", 1, 2, sum_ast(1, 1, 2, 1))
.unwrap();
let b1_id = *graph.cell_to_vertex().get(&abs_cell_ref(0, 1, 2)).unwrap();
graph
.set_cell_value("Sheet1", 1, 1, LiteralValue::Int(10))
.unwrap();
assert!(graph.get_evaluation_vertices().contains(&b1_id));
graph.clear_dirty_flags(&[b1_id]);
assert!(!graph.get_evaluation_vertices().contains(&b1_id));
graph
.set_cell_value("Sheet1", 3, 1, LiteralValue::Int(30))
.unwrap();
assert!(!graph.get_evaluation_vertices().contains(&b1_id));
graph
.set_cell_formula("Sheet1", 1, 2, sum_ast(1, 1, 5, 1))
.unwrap();
graph.clear_dirty_flags(&[b1_id]);
graph
.set_cell_value("Sheet1", 3, 1, LiteralValue::Int(40))
.unwrap();
assert!(graph.get_evaluation_vertices().contains(&b1_id));
}
#[test]
fn test_large_range_creates_single_compressed_ref() {
let mut graph = graph_with_range_limit(4);
graph
.set_cell_formula("Sheet1", 1, 3, sum_ast(1, 1, 100, 1))
.unwrap();
let c1_id = *graph
.get_vertex_id_for_address(&abs_cell_ref(0, 1, 3))
.unwrap();
let c1_dependencies = graph.get_dependencies(c1_id);
assert!(
c1_dependencies.is_empty(),
"Should not have any direct cell dependencies"
);
let range_deps = graph.formula_to_range_deps();
assert_eq!(
range_deps.len(),
1,
"Should create one compressed range dependency"
);
assert!(range_deps.contains_key(&c1_id));
assert_eq!(range_deps.get(&c1_id).unwrap().len(), 1);
}
#[test]
fn test_duplicate_range_refs_in_formula() {
let mut graph = graph_with_range_limit(4);
let formula = ASTNode {
node_type: ASTNodeType::BinaryOp {
op: "+".to_string(),
left: Box::new(sum_ast(1, 1, 100, 1)),
right: Box::new(ASTNode {
node_type: ASTNodeType::Function {
name: "COUNT".to_string(),
args: vec![range_ast(1, 1, 100, 1)],
},
source_token: None,
contains_volatile: false,
}),
},
source_token: None,
contains_volatile: false,
};
graph.set_cell_formula("Sheet1", 1, 2, formula).unwrap();
let b1_id = *graph
.get_vertex_id_for_address(&abs_cell_ref(0, 1, 2))
.unwrap();
let range_deps = graph.formula_to_range_deps();
assert_eq!(range_deps.get(&b1_id).unwrap().len(), 1);
}
#[test]
fn test_zero_sized_range_behavior() {
let mut graph = DependencyGraph::new();
let result = graph.set_cell_formula("Sheet1", 1, 2, sum_ast(1, 1, 0, 1));
assert!(result.is_err());
assert_eq!(
result.unwrap_err().kind,
formualizer_common::ExcelErrorKind::Ref
);
}
fn create_simple_engine() -> Engine<TestWorkbook> {
let wb = TestWorkbook::new();
let config = EvalConfig::default();
Engine::new(wb, config)
}
#[test]
fn test_partial_range_overlap_dependency_propagation() {
let mut engine = create_simple_engine();
engine
.set_cell_value("Sheet1", 30, 1, LiteralValue::Number(10.0))
.unwrap();
let sum_a1_a50 = sum_ast(1, 1, 50, 1);
engine.set_cell_formula("Sheet1", 1, 2, sum_a1_a50).unwrap();
let sum_a25_a75 = sum_ast(25, 1, 75, 1);
engine
.set_cell_formula("Sheet1", 2, 2, sum_a25_a75)
.unwrap();
engine.evaluate_all().unwrap();
assert_eq!(
engine.get_cell_value("Sheet1", 1, 2),
Some(LiteralValue::Number(10.0))
);
assert_eq!(
engine.get_cell_value("Sheet1", 2, 2),
Some(LiteralValue::Number(10.0))
);
engine
.set_cell_value("Sheet1", 30, 1, LiteralValue::Number(20.0))
.unwrap();
engine.evaluate_all().unwrap();
assert_eq!(
engine.get_cell_value("Sheet1", 1, 2),
Some(LiteralValue::Number(20.0)),
"B1 (A1:A50) failed to update when overlapping cell A30 changed"
);
assert_eq!(
engine.get_cell_value("Sheet1", 2, 2),
Some(LiteralValue::Number(20.0)),
"B2 (A25:A75) failed to update when overlapping cell A30 changed"
);
}
#[test]
fn test_cross_sheet_range_dependency() {
let mut engine = create_simple_engine();
engine.graph.add_sheet("Sheet2").unwrap();
engine
.set_cell_value("Sheet2", 1, 1, LiteralValue::Number(10.0))
.unwrap();
let range_cross = ASTNode {
node_type: ASTNodeType::Reference {
original: "Sheet2!A1:A10".to_string(),
reference: ReferenceType::Range {
sheet: Some("Sheet2".to_string()),
start_row: Some(1),
start_col: Some(1),
end_row: Some(10),
end_col: Some(1),
start_row_abs: true,
start_col_abs: true,
end_row_abs: true,
end_col_abs: true,
},
},
source_token: None,
contains_volatile: false,
};
let sum_formula = ASTNode {
node_type: ASTNodeType::Function {
name: "SUM".to_string(),
args: vec![range_cross],
},
source_token: None,
contains_volatile: false,
};
engine
.set_cell_formula("Sheet1", 1, 2, sum_formula)
.unwrap(); engine.evaluate_all().unwrap();
assert_eq!(
engine.get_cell_value("Sheet1", 1, 2),
Some(LiteralValue::Number(10.0))
);
engine
.set_cell_value("Sheet2", 1, 1, LiteralValue::Number(50.0))
.unwrap();
engine.evaluate_all().unwrap();
assert_eq!(
engine.get_cell_value("Sheet1", 1, 2),
Some(LiteralValue::Number(50.0)),
"Cross-sheet range dependency failed to propagate"
);
}
#[test]
fn test_nested_formula_within_range_propagation() {
let mut engine = create_simple_engine();
engine
.set_cell_value("Sheet1", 1, 3, LiteralValue::Number(10.0))
.unwrap();
let a5_formula = ASTNode {
node_type: ASTNodeType::Reference {
original: "C1".to_string(),
reference: ReferenceType::cell(None, 1, 3),
},
source_token: None,
contains_volatile: false,
};
engine.set_cell_formula("Sheet1", 5, 1, a5_formula).unwrap();
engine
.set_cell_formula("Sheet1", 1, 2, sum_ast(1, 1, 10, 1))
.unwrap();
engine.evaluate_all().unwrap();
assert_eq!(
engine.get_cell_value("Sheet1", 1, 2),
Some(LiteralValue::Number(10.0))
);
engine
.set_cell_value("Sheet1", 1, 3, LiteralValue::Number(50.0))
.unwrap();
engine.evaluate_all().unwrap();
assert_eq!(
engine.get_cell_value("Sheet1", 1, 2),
Some(LiteralValue::Number(50.0)),
"Range dependency failed to trigger via a nested formula change"
);
}
#[test]
fn test_sheet_recreation_dependency_recovery() {
use formualizer_parse::parse; let mut engine = create_simple_engine();
engine.graph.add_sheet("Sheet2").unwrap();
engine
.set_cell_value("Sheet2", 1, 1, LiteralValue::Number(10.0))
.unwrap();
engine
.set_cell_formula("Sheet1", 1, 1, parse("=Sheet2!A1").unwrap())
.unwrap();
engine.evaluate_all().unwrap();
let s2_id = engine
.graph
.sheet_id("Sheet2")
.expect("Sheet2 should exist");
engine.graph.remove_sheet(s2_id).unwrap();
engine.evaluate_all().unwrap();
let val = engine.get_cell_value("Sheet1", 1, 1).unwrap();
match val {
LiteralValue::Error(_) => { }
_ => panic!(
"Expected an error value after sheet removal, found {:?}",
val
),
}
engine.graph.add_sheet("Sheet2").unwrap();
engine
.set_cell_value("Sheet2", 1, 1, LiteralValue::Number(50.0))
.unwrap();
engine.evaluate_all().unwrap();
assert_eq!(
engine.get_cell_value("Sheet1", 1, 1),
Some(LiteralValue::Number(50.0)),
"Dependency failed to 'heal' after sheet was removed and re-added"
);
}
#[test]
fn test_ast_surgical_rename() {
let mut ast = parse("=Sheet2!A1 + Sheet3!A1").unwrap();
ast.update_sheet_references(Some("Sheet2"), "NewSheet");
let normalized = format!("{:?}", ast);
assert!(normalized.contains("\"NewSheet\""));
assert!(normalized.contains("\"Sheet3\""));
}
#[test]
fn test_rename_fixes_ref_errors() {
let mut engine = create_simple_engine();
let _ = engine.add_sheet("Sheet2").unwrap();
engine
.set_cell_formula("Sheet1", 1, 1, parse("=Sheet2!A1").unwrap())
.unwrap();
engine.evaluate_all().unwrap();
let s2_id = engine.graph.sheet_id("Sheet2").unwrap();
engine.remove_sheet(s2_id).unwrap();
engine.evaluate_all().unwrap();
let s3_id = engine.add_sheet("Sheet3").unwrap();
engine
.set_cell_value("Sheet3", 1, 1, LiteralValue::Number(100.0))
.unwrap();
engine.rename_sheet(s3_id, "Sheet2").unwrap();
let id_check = engine.graph.sheet_id("Sheet2");
let val_check = engine.get_cell_value("Sheet2", 1, 1);
engine.evaluate_all().unwrap();
assert_eq!(
engine.get_cell_value("Sheet1", 1, 1),
Some(LiteralValue::Number(100.0))
);
}
#[test]
fn test_graph_orphan_healing_isolation() {
let mut engine = create_simple_engine();
let _ = engine.add_sheet("Sheet2").unwrap();
engine
.set_cell_formula("Sheet1", 1, 1, parse("=Sheet2!B2").unwrap())
.unwrap();
engine.evaluate_all().unwrap();
let s2_id = engine.graph.sheet_id("Sheet2").unwrap();
engine.remove_sheet(s2_id).unwrap();
let s3_id = engine.add_sheet("Sheet3").unwrap();
engine
.set_cell_value("Sheet3", 2, 2, LiteralValue::Number(100.0)) .unwrap();
engine.rename_sheet(s3_id, "Sheet2").unwrap();
engine.evaluate_all().unwrap();
let val = engine.get_cell_value("Sheet1", 1, 1);
assert!(val.is_some(), "Value should be Some(100.0)");
match val.as_ref().unwrap() {
LiteralValue::Number(n) => assert_eq!(*n, 100.0),
LiteralValue::Error(e) => panic!("Still got an error: {:?}", e),
_ => panic!("Unexpected value type: {:?}", val),
}
}
#[test]
fn test_cross_sheet_row_insertion_dependency() {
use formualizer_parse::parse;
let mut engine = create_simple_engine();
engine.graph.add_sheet("Sheet2").unwrap();
engine
.set_cell_value("Sheet2", 1, 1, LiteralValue::Number(10.0))
.unwrap();
engine
.set_cell_formula("Sheet1", 1, 1, parse("=SUM(Sheet2!A1:A10)").unwrap())
.unwrap();
engine.evaluate_all().unwrap();
assert_eq!(
engine.get_cell_value("Sheet1", 1, 1),
Some(LiteralValue::Number(10.0))
);
engine.insert_rows("Sheet2", 1, 1).unwrap();
engine.evaluate_all().unwrap();
assert_eq!(
engine.get_cell_value("Sheet1", 1, 1),
Some(LiteralValue::Number(10.0)),
"Inserting a row on Sheet2 failed to shift the reference on Sheet1"
);
}
#[test]
fn test_massive_range_fan_out_performance() {
let mut engine = create_simple_engine();
let sheet = "Sheet1";
engine
.set_cell_value(sheet, 1, 1, LiteralValue::Number(1.0))
.unwrap();
for i in 2..5002 {
engine
.set_cell_formula(sheet, i, 2, sum_ast(1, 1, i, 1))
.unwrap();
}
let start = std::time::Instant::now();
engine
.set_cell_value(sheet, 1, 1, LiteralValue::Number(2.0))
.unwrap();
engine.evaluate_all().unwrap();
let duration = start.elapsed();
assert_eq!(
engine.get_cell_value(sheet, 5001, 2),
Some(LiteralValue::Number(2.0))
);
assert!(duration.as_secs() < 2, "Fan-out propagation is too slow!");
}
#[test]
fn test_stripe_boundary_range_trigger() {
let mut engine = create_simple_engine();
engine
.set_cell_formula("Sheet1", 1, 2, sum_ast(60, 1, 70, 1))
.unwrap();
engine
.set_cell_value("Sheet1", 63, 1, LiteralValue::Number(10.0))
.unwrap();
engine.evaluate_all().unwrap();
assert_eq!(
engine.get_cell_value("Sheet1", 1, 2),
Some(LiteralValue::Number(10.0))
);
engine
.set_cell_value("Sheet1", 65, 1, LiteralValue::Number(5.0))
.unwrap();
engine.evaluate_all().unwrap();
assert_eq!(
engine.get_cell_value("Sheet1", 1, 2),
Some(LiteralValue::Number(15.0)),
"Range failed to catch update from the second stripe"
);
}
#[test]
fn test_rename_layer_registry() {
let mut engine = create_simple_engine(); let sheet_id = engine.graph.sheet_reg().get_id("Sheet1").unwrap();
engine.graph.rename_sheet(sheet_id, "NewSheet").unwrap();
assert_eq!(engine.graph.sheet_name(sheet_id), "NewSheet");
assert!(engine.graph.sheet_reg().get_id("Sheet1").is_none());
}
#[test]
fn test_rename_layer_storage() {
let mut engine = create_simple_engine();
let _ = engine.set_cell_value("Sheet1", 1, 1, LiteralValue::Number(100.0));
let sheet_id = engine.graph.sheet_reg().get_id("Sheet1").unwrap();
engine.rename_sheet(sheet_id, "Sheet2").unwrap();
let val = engine.read_cell_value("Sheet2", 1, 1);
assert_eq!(
val,
Some(LiteralValue::Number(100.0)),
"Storage failed to follow the rename!"
);
}
#[test]
fn test_rename_layer_identity() {
let mut engine = create_simple_engine();
let _ = engine.set_cell_value("Sheet1", 1, 1, LiteralValue::Number(100.0));
let (row, col, _, _) = parse_a1_1based("A1").unwrap();
let addr = engine.graph.make_cell_ref("Sheet1", row, col);
let v_id = *engine.graph.get_vertex_id_for_address(&addr).unwrap();
let sheet_id = engine
.graph
.sheet_reg()
.get_id("Sheet1")
.expect("Sheet1 missing");
engine.rename_sheet(sheet_id, "SheetX").unwrap();
let cell_ref = engine
.graph
.get_cell_ref(v_id) .expect("Vertex lost its cell ref!");
let current_name = engine.graph.sheet_name(cell_ref.sheet_id);
assert_eq!(
current_name, "SheetX",
"Vertex still thinks it is on the old sheet name!"
);
}
#[test]
fn test_rename_layer_vertex_read() {
let mut engine = create_simple_engine();
let _ = engine.set_cell_value("Sheet1", 1, 1, LiteralValue::Number(100.0));
let (row, col, _, _) = parse_a1_1based("A1").unwrap();
let addr = engine.graph.make_cell_ref("Sheet1", row, col);
let v_id = *engine.graph.get_vertex_id_for_address(&addr).unwrap();
let sheet_id = engine.graph.sheet_reg().get_id("Sheet1").unwrap();
engine.rename_sheet(sheet_id, "SheetX").unwrap();
let val = engine.evaluate_vertex(v_id).expect("Evaluation failed");
assert_eq!(
val,
LiteralValue::Number(100.0),
"Data not found after rename!"
);
}
#[test]
fn test_rename_check_formula_healing() {
let mut engine = create_simple_engine();
let _ = engine.set_cell_value("Sheet2", 1, 1, LiteralValue::Number(100.0));
let ast = parse("=Sheet2!A1").expect("Parse failed");
engine.set_cell_formula("Sheet1", 1, 2, ast).unwrap();
engine.evaluate_all().unwrap();
let sheet2_id = engine.graph.sheet_reg().get_id("Sheet2").unwrap();
engine.rename_sheet(sheet2_id, "Other").unwrap();
engine.evaluate_all().unwrap();
engine.rename_sheet(sheet2_id, "Sheet2").unwrap();
engine.evaluate_all().unwrap();
let v_id = *engine
.graph
.get_vertex_id_for_address(&engine.graph.make_cell_ref("Sheet1", 1, 2))
.unwrap();
let val = engine.evaluate_vertex(v_id).expect("Evaluation failed");
assert_eq!(
val,
LiteralValue::Number(100.0),
"Formula did not heal correctly!"
);
}
#[test]
fn test_rename_cross_sheet_link() {
let mut engine = create_simple_engine();
let _ = engine.set_cell_value("Sheet1", 1, 1, LiteralValue::Number(100.0));
let sheet_id = engine.graph.sheet_reg().get_id("Sheet1").unwrap();
engine.rename_sheet(sheet_id, "DataSheet").unwrap();
let ast = parse("=DataSheet!A1").expect("Parse failed");
engine.set_cell_formula("Sheet2", 1, 1, ast).unwrap();
let _ = engine.evaluate_all().unwrap();
let addr = engine.graph.make_cell_ref("Sheet2", 1, 1);
let v_id = *engine
.graph
.get_vertex_id_for_address(&addr)
.expect("Vertex not found at Sheet2!A1");
let val = engine.evaluate_vertex(v_id).expect("Evaluation failed");
assert_eq!(
val,
LiteralValue::Number(100.0),
"Cross-sheet link failed to retrieve value"
);
}
#[test]
fn test_update_in_revived_sheeet() {
let mut engine = create_simple_engine();
let s2 = engine.add_sheet("Sheet2");
let _ = engine.set_cell_value("Sheet2", 1, 1, LiteralValue::Number(12.34));
engine
.set_cell_formula("Sheet1", 1, 1, parse("=Sheet2!A1").unwrap())
.unwrap();
engine.evaluate_all().unwrap();
assert_eq!(
engine.get_cell_value("Sheet1", 1, 1),
Some(LiteralValue::Number(12.34)),
"Cross-sheet link failed to retrieve value"
);
let _ = engine.remove_sheet(s2.expect("Sheet2 must exist"));
engine.evaluate_all().unwrap();
let val = engine.get_cell_value("Sheet1", 1, 1).unwrap();
match val {
LiteralValue::Error(_) => { }
_ => panic!(
"Expected an error value after sheet removal, found {:?}",
val
),
}
let _ = engine.add_sheet("Sheet2");
let _ = engine.set_cell_value("Sheet2", 1, 1, LiteralValue::Number(774422.987));
engine.evaluate_all().unwrap();
assert_eq!(
engine.get_cell_value("Sheet1", 1, 1),
Some(LiteralValue::Number(774422.987)),
"ref should work after sheet was re-added"
);
let _ = engine.set_cell_value("Sheet2", 1, 1, LiteralValue::Number(999.0));
engine.evaluate_all().unwrap();
assert_eq!(
engine.get_cell_value("Sheet1", 1, 1),
Some(LiteralValue::Number(999.0)),
"Dependency tracking failed after heal! The second update did not propagate."
);
}
#[test]
fn test_readded_sheet_keeps_mixed_sheet_formula_intact() {
let mut engine = create_simple_engine();
engine.add_sheet("Sheet2").unwrap();
engine.add_sheet("Sheet3").unwrap();
engine
.set_cell_value("Sheet2", 1, 1, LiteralValue::Number(10.0))
.unwrap();
engine
.set_cell_value("Sheet3", 1, 1, LiteralValue::Number(5.0))
.unwrap();
engine
.set_cell_formula("Sheet1", 1, 1, parse("=Sheet2!A1+Sheet3!A1").unwrap())
.unwrap();
engine.evaluate_all().unwrap();
let s2 = engine.sheet_id("Sheet2").unwrap();
engine.remove_sheet(s2).unwrap();
engine.evaluate_all().unwrap();
engine.add_sheet("Sheet2").unwrap();
engine
.set_cell_value("Sheet2", 1, 1, LiteralValue::Number(10.0))
.unwrap();
engine.evaluate_all().unwrap();
assert_eq!(
engine.get_cell_value("Sheet1", 1, 1),
Some(LiteralValue::Number(15.0)),
"Healing must not rewrite unrelated Sheet3 references"
);
}
#[test]
fn test_readd_sheet_does_not_overwrite_user_formula_edit_while_missing() {
let mut engine = create_simple_engine();
engine.add_sheet("Sheet2").unwrap();
engine.add_sheet("Sheet3").unwrap();
engine
.set_cell_value("Sheet2", 1, 1, LiteralValue::Number(10.0))
.unwrap();
engine
.set_cell_value("Sheet3", 1, 1, LiteralValue::Number(5.0))
.unwrap();
engine
.set_cell_formula("Sheet1", 1, 1, parse("=Sheet2!A1").unwrap())
.unwrap();
engine.evaluate_all().unwrap();
let s2 = engine.sheet_id("Sheet2").unwrap();
engine.remove_sheet(s2).unwrap();
engine.evaluate_all().unwrap();
engine
.set_cell_formula("Sheet1", 1, 1, parse("=Sheet3!A1").unwrap())
.unwrap();
engine.evaluate_all().unwrap();
assert_eq!(
engine.get_cell_value("Sheet1", 1, 1),
Some(LiteralValue::Number(5.0))
);
engine.add_sheet("Sheet2").unwrap();
engine
.set_cell_value("Sheet2", 1, 1, LiteralValue::Number(99.0))
.unwrap();
engine.evaluate_all().unwrap();
assert_eq!(
engine.get_cell_value("Sheet1", 1, 1),
Some(LiteralValue::Number(5.0)),
"Stale orphan metadata must not override updated formulas"
);
}
#[test]
fn test_healed_formula_recomputes_downstream_dependents() {
let mut engine = create_simple_engine();
engine.add_sheet("Sheet2").unwrap();
engine
.set_cell_value("Sheet2", 1, 1, LiteralValue::Number(10.0))
.unwrap();
engine
.set_cell_formula("Sheet1", 1, 1, parse("=Sheet2!A1").unwrap())
.unwrap();
engine
.set_cell_formula("Sheet1", 1, 2, parse("=A1+1").unwrap())
.unwrap();
engine.evaluate_all().unwrap();
let s2 = engine.sheet_id("Sheet2").unwrap();
engine.remove_sheet(s2).unwrap();
engine.evaluate_all().unwrap();
engine.add_sheet("Sheet2").unwrap();
engine
.set_cell_value("Sheet2", 1, 1, LiteralValue::Number(50.0))
.unwrap();
engine.evaluate_all().unwrap();
assert_eq!(
engine.get_cell_value("Sheet1", 1, 1),
Some(LiteralValue::Number(50.0))
);
assert_eq!(
engine.get_cell_value("Sheet1", 1, 2),
Some(LiteralValue::Number(51.0)),
"Downstream dependents should recover after healing"
);
engine
.set_cell_value("Sheet2", 1, 1, LiteralValue::Number(60.0))
.unwrap();
engine.evaluate_all().unwrap();
assert_eq!(
engine.get_cell_value("Sheet1", 1, 1),
Some(LiteralValue::Number(60.0))
);
assert_eq!(
engine.get_cell_value("Sheet1", 1, 2),
Some(LiteralValue::Number(61.0)),
"Downstream dependents should continue tracking subsequent edits"
);
}
#[test]
fn test_invalid_rename_rolls_back_arrow_storage() {
let mut engine = create_simple_engine();
engine
.set_cell_value("Sheet1", 1, 1, LiteralValue::Number(7.0))
.unwrap();
let sheet_id = engine.sheet_id("Sheet1").unwrap();
let result = engine.rename_sheet(sheet_id, "");
assert!(result.is_err(), "Invalid rename should fail");
assert_eq!(
engine.get_cell_value("Sheet1", 1, 1),
Some(LiteralValue::Number(7.0)),
"Failed rename must not mutate Arrow storage sheet mapping"
);
}
#[test]
fn test_whole_column_cross_sheet_recovers_after_readd() {
let mut engine = create_simple_engine();
engine.add_sheet("Sheet2").unwrap();
engine
.set_cell_value("Sheet2", 1, 1, LiteralValue::Number(10.0))
.unwrap();
engine
.set_cell_formula("Sheet1", 1, 1, parse("=SUM(Sheet2!A:A)").unwrap())
.unwrap();
engine.evaluate_all().unwrap();
assert_eq!(
engine.get_cell_value("Sheet1", 1, 1),
Some(LiteralValue::Number(10.0))
);
let s2 = engine.sheet_id("Sheet2").unwrap();
engine.remove_sheet(s2).unwrap();
engine.evaluate_all().unwrap();
match engine.get_cell_value("Sheet1", 1, 1) {
Some(LiteralValue::Error(_)) => {}
other => panic!("expected #REF after removing Sheet2, got {other:?}"),
}
engine.add_sheet("Sheet2").unwrap();
engine
.set_cell_value("Sheet2", 1, 1, LiteralValue::Number(50.0))
.unwrap();
engine.evaluate_all().unwrap();
assert_eq!(
engine.get_cell_value("Sheet1", 1, 1),
Some(LiteralValue::Number(50.0))
);
engine
.set_cell_value("Sheet2", 1, 1, LiteralValue::Number(70.0))
.unwrap();
engine.evaluate_all().unwrap();
assert_eq!(
engine.get_cell_value("Sheet1", 1, 1),
Some(LiteralValue::Number(70.0)),
"Whole-column dependency should continue tracking after heal"
);
}
#[test]
fn test_heal_one_of_multiple_missing_sheets_does_not_double_bind() {
let mut engine = create_simple_engine();
engine.add_sheet("S2").unwrap();
engine.add_sheet("S3").unwrap();
engine
.set_cell_value("S2", 1, 1, LiteralValue::Number(10.0))
.unwrap();
engine
.set_cell_value("S3", 1, 1, LiteralValue::Number(20.0))
.unwrap();
engine
.set_cell_formula("Sheet1", 1, 1, parse("=S2!A1+S3!A1").unwrap())
.unwrap();
engine.evaluate_all().unwrap();
assert_eq!(
engine.get_cell_value("Sheet1", 1, 1),
Some(LiteralValue::Number(30.0))
);
let s2 = engine.sheet_id("S2").unwrap();
let s3 = engine.sheet_id("S3").unwrap();
engine.remove_sheet(s2).unwrap();
engine.remove_sheet(s3).unwrap();
engine.evaluate_all().unwrap();
engine.add_sheet("S2").unwrap();
engine
.set_cell_value("S2", 1, 1, LiteralValue::Number(100.0))
.unwrap();
engine.evaluate_all().unwrap();
match engine.get_cell_value("Sheet1", 1, 1) {
Some(LiteralValue::Error(_)) => {}
other => panic!("expected unresolved error until S3 returns, got {other:?}"),
}
engine.add_sheet("S3").unwrap();
engine
.set_cell_value("S3", 1, 1, LiteralValue::Number(20.0))
.unwrap();
engine.evaluate_all().unwrap();
assert_eq!(
engine.get_cell_value("Sheet1", 1, 1),
Some(LiteralValue::Number(120.0)),
"Healing S2 first must not rewrite S3 references"
);
}