use super::common::abs_cell_ref;
use crate::engine::{DependencyGraph, EvalConfig, VertexId};
use formualizer_common::LiteralValue;
use formualizer_parse::parser::{ASTNode, ASTNodeType, ReferenceType};
fn range_ast(
sheet: Option<&str>,
start_row: u32,
start_col: u32,
end_row: u32,
end_col: u32,
) -> ASTNode {
ASTNode {
node_type: ASTNodeType::Reference {
original: format!(
"{}R{}C{}:R{}C{}",
sheet.map(|s| format!("{s}!")).unwrap_or_default(),
start_row,
start_col,
end_row,
end_col
),
reference: ReferenceType::range(
sheet.map(|s| s.to_string()),
Some(start_row),
Some(start_col),
Some(end_row),
Some(end_col),
),
},
source_token: None,
contains_volatile: false,
}
}
fn sum_range_ast(
sheet: Option<&str>,
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(sheet, start_row, start_col, end_row, end_col)],
},
source_token: None,
contains_volatile: false,
}
}
fn config_with_range_limit(limit: usize) -> EvalConfig {
EvalConfig {
range_expansion_limit: limit,
..Default::default()
}
}
#[test]
fn test_property_small_range_dependency_tracking() {
let mut graph = DependencyGraph::new_with_config(config_with_range_limit(100));
let start_row = 1;
let start_col = 1;
let end_row = 3;
let end_col = 3;
let formula_row = 1;
let formula_col = 4;
graph
.set_cell_formula(
"Sheet1",
formula_row,
formula_col,
sum_range_ast(None, start_row, start_col, end_row, end_col),
)
.unwrap();
let formula_addr = abs_cell_ref(0, formula_row, formula_col);
let formula_id = *graph.get_vertex_id_for_address(&formula_addr).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(),
"Should start with no dirty vertices"
);
for row in start_row..=end_row {
for col in start_col..=end_col {
graph
.set_cell_value("Sheet1", row, col, LiteralValue::Int(42))
.unwrap();
let dirty_vertices = graph.get_evaluation_vertices();
assert!(
dirty_vertices.contains(&formula_id),
"Formula should be dirty when cell ({row}, {col}) changes (inside range {start_row}:{start_col} to {end_row}:{end_col})"
);
graph.clear_dirty_flags(&dirty_vertices);
}
}
let outside_cells = vec![
(start_row - 1, start_col), (end_row + 1, end_col), (start_row, start_col - 1), (end_row, end_col + 1), (start_row - 1, start_col - 1), (end_row + 1, end_col + 1), ];
for (row, col) in outside_cells {
if row > 0 && col > 0 {
graph
.set_cell_value("Sheet1", row, col, LiteralValue::Int(99))
.unwrap();
let dirty_vertices = graph.get_evaluation_vertices();
assert!(
!dirty_vertices.contains(&formula_id),
"Formula should NOT be dirty when cell ({row}, {col}) changes (outside range {start_row}:{start_col} to {end_row}:{end_col})"
);
}
}
}
#[test]
fn test_property_large_range_stripe_tracking() {
let mut graph = DependencyGraph::new_with_config(config_with_range_limit(16));
let start_row = 1;
let start_col = 1;
let end_row = 1000;
let end_col = 1;
let formula_row = 1;
let formula_col = 2;
graph
.set_cell_formula(
"Sheet1",
formula_row,
formula_col,
sum_range_ast(None, start_row, start_col, end_row, end_col),
)
.unwrap();
let formula_addr = abs_cell_ref(0, formula_row, formula_col);
let formula_id = *graph.get_vertex_id_for_address(&formula_addr).unwrap();
let all_ids: Vec<VertexId> = graph.cell_to_vertex().values().copied().collect();
graph.clear_dirty_flags(&all_ids);
let inside_test_cells = vec![
(1, 1), (500, 1), (1000, 1), (250, 1), (750, 1), ];
for (row, col) in inside_test_cells {
graph
.set_cell_value("Sheet1", row, col, LiteralValue::Int(42))
.unwrap();
let dirty_vertices = graph.get_evaluation_vertices();
assert!(
dirty_vertices.contains(&formula_id),
"Formula should be dirty when cell ({row}, {col}) changes (inside stripe range A1:A1000)"
);
graph.clear_dirty_flags(&dirty_vertices);
}
let outside_test_cells = vec![
(1, 2), (500, 3), (1000, 2), (1001, 1), (0, 1), ];
for (row, col) in outside_test_cells {
if row > 0 && col > 0 {
graph
.set_cell_value("Sheet1", row, col, LiteralValue::Int(99))
.unwrap();
let dirty_vertices = graph.get_evaluation_vertices();
assert!(
!dirty_vertices.contains(&formula_id),
"Formula should NOT be dirty when cell ({row}, {col}) changes (outside stripe range A1:A1000)"
);
}
}
}
#[test]
fn test_property_wide_range_stripe_tracking() {
let mut graph = DependencyGraph::new_with_config(config_with_range_limit(16));
let start_row = 1;
let start_col = 1;
let end_row = 1;
let end_col = 26;
let formula_row = 2;
let formula_col = 1;
graph
.set_cell_formula(
"Sheet1",
formula_row,
formula_col,
sum_range_ast(None, start_row, start_col, end_row, end_col),
)
.unwrap();
let formula_addr = abs_cell_ref(0, formula_row, formula_col);
let formula_id = *graph.get_vertex_id_for_address(&formula_addr).unwrap();
let all_ids: Vec<VertexId> = graph.cell_to_vertex().values().copied().collect();
graph.clear_dirty_flags(&all_ids);
let inside_test_cells = vec![
(1, 1), (1, 13), (1, 26), (1, 5), (1, 20), ];
for (row, col) in inside_test_cells {
graph
.set_cell_value("Sheet1", row, col, LiteralValue::Int(42))
.unwrap();
let dirty_vertices = graph.get_evaluation_vertices();
assert!(
dirty_vertices.contains(&formula_id),
"Formula should be dirty when cell ({row}, {col}) changes (inside row range A1:Z1)"
);
graph.clear_dirty_flags(&dirty_vertices);
}
let outside_test_cells = vec![
(2, 1), (2, 13), (0, 13), (1, 27), (1, 0), ];
for (row, col) in outside_test_cells {
if row > 0 && col > 0 {
graph
.set_cell_value("Sheet1", row, col, LiteralValue::Int(99))
.unwrap();
let dirty_vertices = graph.get_evaluation_vertices();
assert!(
!dirty_vertices.contains(&formula_id),
"Formula should NOT be dirty when cell ({row}, {col}) changes (outside row range A1:Z1)"
);
}
}
}
#[test]
fn test_property_dense_range_block_stripe_tracking() {
let mut config = config_with_range_limit(16);
config = config.with_block_stripes(true);
let mut graph = DependencyGraph::new_with_config(config);
let start_row = 1;
let start_col = 1;
let end_row = 26;
let end_col = 26;
let formula_row = 1;
let formula_col = 27;
graph
.set_cell_formula(
"Sheet1",
formula_row,
formula_col,
sum_range_ast(None, start_row, start_col, end_row, end_col),
)
.unwrap();
let formula_addr = abs_cell_ref(0, formula_row, formula_col);
let formula_id = *graph.get_vertex_id_for_address(&formula_addr).unwrap();
let all_ids: Vec<VertexId> = graph.cell_to_vertex().values().copied().collect();
graph.clear_dirty_flags(&all_ids);
let inside_test_cells = vec![
(1, 1), (1, 26), (26, 1), (26, 26), (13, 13), (5, 20), (20, 5), ];
for (row, col) in inside_test_cells {
graph
.set_cell_value("Sheet1", row, col, LiteralValue::Int(42))
.unwrap();
let dirty_vertices = graph.get_evaluation_vertices();
assert!(
dirty_vertices.contains(&formula_id),
"Formula should be dirty when cell ({row}, {col}) changes (inside block range A1:Z26)"
);
graph.clear_dirty_flags(&dirty_vertices);
}
let outside_test_cells = vec![
(0, 13), (27, 13), (13, 0), (13, 27), (0, 0), (27, 27), (30, 5), (5, 30), ];
for (row, col) in outside_test_cells {
if row > 0 && col > 0 {
graph
.set_cell_value("Sheet1", row, col, LiteralValue::Int(99))
.unwrap();
let dirty_vertices = graph.get_evaluation_vertices();
assert!(
!dirty_vertices.contains(&formula_id),
"Formula should NOT be dirty when cell ({row}, {col}) changes (outside block range A1:Z26)"
);
}
}
}
#[test]
fn test_property_multiple_overlapping_ranges() {
let mut graph = DependencyGraph::new_with_config(config_with_range_limit(16));
graph
.set_cell_formula("Sheet1", 1, 4, sum_range_ast(None, 1, 1, 100, 3))
.unwrap();
graph
.set_cell_formula("Sheet1", 2, 4, sum_range_ast(None, 50, 1, 150, 3))
.unwrap();
let formula1_id = *graph
.get_vertex_id_for_address(&abs_cell_ref(0, 1, 4))
.unwrap();
let formula2_id = *graph
.get_vertex_id_for_address(&abs_cell_ref(0, 2, 4))
.unwrap();
let all_ids: Vec<VertexId> = graph.cell_to_vertex().values().copied().collect();
graph.clear_dirty_flags(&all_ids);
graph
.set_cell_value("Sheet1", 75, 2, LiteralValue::Int(42))
.unwrap();
let dirty = graph.get_evaluation_vertices();
assert!(
dirty.contains(&formula1_id),
"Formula 1 should be dirty (cell B75 in range A1:C100)"
);
assert!(
dirty.contains(&formula2_id),
"Formula 2 should be dirty (cell B75 in range A50:C150)"
);
graph.clear_dirty_flags(&dirty);
graph
.set_cell_value("Sheet1", 25, 2, LiteralValue::Int(43))
.unwrap();
let dirty = graph.get_evaluation_vertices();
assert!(
dirty.contains(&formula1_id),
"Formula 1 should be dirty (cell B25 in range A1:C100)"
);
assert!(
!dirty.contains(&formula2_id),
"Formula 2 should NOT be dirty (cell B25 not in range A50:C150)"
);
graph.clear_dirty_flags(&dirty);
graph
.set_cell_value("Sheet1", 125, 2, LiteralValue::Int(44))
.unwrap();
let dirty = graph.get_evaluation_vertices();
assert!(
!dirty.contains(&formula1_id),
"Formula 1 should NOT be dirty (cell B125 not in range A1:C100)"
);
assert!(
dirty.contains(&formula2_id),
"Formula 2 should be dirty (cell B125 in range A50:C150)"
);
graph.clear_dirty_flags(&dirty);
graph
.set_cell_value("Sheet1", 200, 2, LiteralValue::Int(45))
.unwrap();
let dirty = graph.get_evaluation_vertices();
assert!(
!dirty.contains(&formula1_id),
"Formula 1 should NOT be dirty (cell B200 outside both ranges)"
);
assert!(
!dirty.contains(&formula2_id),
"Formula 2 should NOT be dirty (cell B200 outside both ranges)"
);
}
#[test]
fn test_property_cross_sheet_ranges() {
let mut graph = DependencyGraph::new_with_config(config_with_range_limit(16));
graph.add_sheet("Sheet2").unwrap();
graph
.set_cell_formula("Sheet1", 1, 1, sum_range_ast(Some("Sheet2"), 1, 1, 100, 1))
.unwrap();
let formula_id = *graph
.get_vertex_id_for_address(&abs_cell_ref(0, 1, 1))
.unwrap();
let all_ids: Vec<VertexId> = graph.cell_to_vertex().values().copied().collect();
graph.clear_dirty_flags(&all_ids);
graph
.set_cell_value("Sheet2", 50, 1, LiteralValue::Int(42))
.unwrap();
let dirty = graph.get_evaluation_vertices();
assert!(
dirty.contains(&formula_id),
"Formula should be dirty when Sheet2!A50 changes"
);
graph.clear_dirty_flags(&dirty);
graph
.set_cell_value("Sheet2", 50, 2, LiteralValue::Int(43))
.unwrap();
let dirty = graph.get_evaluation_vertices();
assert!(
!dirty.contains(&formula_id),
"Formula should NOT be dirty when Sheet2!B50 changes"
);
graph
.set_cell_value("Sheet1", 50, 1, LiteralValue::Int(44))
.unwrap();
let dirty = graph.get_evaluation_vertices();
assert!(
!dirty.contains(&formula_id),
"Formula should NOT be dirty when Sheet1!A50 changes"
);
}
#[test]
fn test_property_edge_cases() {
let mut graph = DependencyGraph::new_with_config(config_with_range_limit(4));
graph
.set_cell_formula(
"Sheet1",
1,
1,
sum_range_ast(None, 5, 5, 5, 5), )
.unwrap();
let formula_id = *graph
.get_vertex_id_for_address(&abs_cell_ref(0, 1, 1))
.unwrap();
let all_ids: Vec<VertexId> = graph.cell_to_vertex().values().copied().collect();
graph.clear_dirty_flags(&all_ids);
graph
.set_cell_value("Sheet1", 5, 5, LiteralValue::Int(42))
.unwrap();
let dirty = graph.get_evaluation_vertices();
assert!(
dirty.contains(&formula_id),
"Formula should be dirty when exact cell (5,5) changes"
);
graph.clear_dirty_flags(&dirty);
let adjacent_cells = vec![(4, 5), (6, 5), (5, 4), (5, 6)];
for (row, col) in adjacent_cells {
graph
.set_cell_value("Sheet1", row, col, LiteralValue::Int(99))
.unwrap();
let dirty = graph.get_evaluation_vertices();
assert!(
!dirty.contains(&formula_id),
"Formula should NOT be dirty when adjacent cell ({row},{col}) changes"
);
}
}
#[test]
fn test_property_formula_replacement_cleanup() {
let mut graph = DependencyGraph::new_with_config(config_with_range_limit(16));
let formula_row = 1;
let formula_col = 1;
let formula_addr = abs_cell_ref(0, formula_row, formula_col);
graph
.set_cell_formula(
"Sheet1",
formula_row,
formula_col,
sum_range_ast(None, 1, 2, 100, 2),
)
.unwrap();
let formula_id = *graph.get_vertex_id_for_address(&formula_addr).unwrap();
let all_ids: Vec<VertexId> = graph.cell_to_vertex().values().copied().collect();
graph.clear_dirty_flags(&all_ids);
graph
.set_cell_value("Sheet1", 50, 2, LiteralValue::Int(42))
.unwrap(); assert!(
graph.get_evaluation_vertices().contains(&formula_id),
"Formula should initially be dirty when B50 changes"
);
graph.clear_dirty_flags(&graph.get_evaluation_vertices());
graph
.set_cell_formula(
"Sheet1",
formula_row,
formula_col,
sum_range_ast(None, 1, 3, 100, 3),
)
.unwrap();
let all_ids: Vec<VertexId> = graph.cell_to_vertex().values().copied().collect();
graph.clear_dirty_flags(&all_ids);
graph
.set_cell_value("Sheet1", 50, 2, LiteralValue::Int(99))
.unwrap(); assert!(
!graph.get_evaluation_vertices().contains(&formula_id),
"Formula should NOT be dirty when old range B50 changes after replacement"
);
graph
.set_cell_value("Sheet1", 50, 3, LiteralValue::Int(100))
.unwrap(); assert!(
graph.get_evaluation_vertices().contains(&formula_id),
"Formula should be dirty when new range C50 changes after replacement"
);
}