Compare commits
2 commits
bd97e68af0
...
a859053bc6
| Author | SHA1 | Date | |
|---|---|---|---|
| a859053bc6 | |||
| a88fb68eab |
1 changed files with 439 additions and 13 deletions
|
|
@ -1808,25 +1808,26 @@ impl SpreadsheetData {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Defensive invariant check (review bug B17):
|
// Defensive invariant check (review bug B17): every cell
|
||||||
// every cell that was in `changed_cells` and has a
|
// that was in `changed_cells` and has a formula MUST have
|
||||||
// formula MUST now have a non-empty `computed_value`.
|
// been visited by the topological sort. If one was skipped,
|
||||||
// If any is empty, it means `topological_sort` failed
|
// the dep-graph walk dropped a cell — a bug. (We cannot use
|
||||||
// to visit that cell — a bug in the dep-graph walk.
|
// "computed_value is empty" as the signal: a formula may
|
||||||
|
// legitimately evaluate to the empty string, e.g. `=""`.)
|
||||||
// In debug builds this panics so the bug is caught
|
// In debug builds this panics so the bug is caught
|
||||||
// immediately; in release builds we patch the cell
|
// immediately; in release builds we patch the cell with
|
||||||
// with `#ERROR!` so it displays something rather
|
// `#ERROR!` so it displays something actionable.
|
||||||
// than appearing empty.
|
|
||||||
//
|
//
|
||||||
// Note: this check is O(changed_cells.len()), which
|
// Note: this check is O(changed_cells.len()), which is
|
||||||
// is typically 1 (single-cell edit) or the size of
|
// typically 1 (single-cell edit) or the size of a
|
||||||
// a paste/autofill range. Not a hot path.
|
// paste/autofill range. Not a hot path.
|
||||||
|
let visited: HashSet<CellId> = sorted.iter().copied().collect();
|
||||||
for &cell_id in changed_cells {
|
for &cell_id in changed_cells {
|
||||||
if let Some(cell) = self.cells.get(&cell_id) {
|
if let Some(cell) = self.cells.get(&cell_id) {
|
||||||
if cell.formula.is_some() && cell.computed_value.is_empty() {
|
if cell.formula.is_some() && !visited.contains(&cell_id) {
|
||||||
debug_assert!(
|
debug_assert!(
|
||||||
false,
|
false,
|
||||||
"recalculate_incremental left cell ({}, {}) with empty computed_value",
|
"recalculate_incremental did not visit formula cell ({}, {})",
|
||||||
cell_id.row(),
|
cell_id.row(),
|
||||||
cell_id.col()
|
cell_id.col()
|
||||||
);
|
);
|
||||||
|
|
@ -3024,3 +3025,428 @@ mod formula2_integration_tests {
|
||||||
assert_eq!(sheet.get_display_value(0, 1), "42");
|
assert_eq!(sheet.get_display_value(0, 1), "42");
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[cfg(test)]
|
||||||
|
mod dep_graph_branch_tests {
|
||||||
|
use super::*;
|
||||||
|
|
||||||
|
/// Build a plain non-formula cell with a raw value and number format,
|
||||||
|
/// mirroring `cell_with` in `display_value_tests`.
|
||||||
|
fn cell_with(value: &str, fmt: NumberFormat) -> CellData {
|
||||||
|
CellData {
|
||||||
|
value: value.to_string(),
|
||||||
|
style: CellStyle {
|
||||||
|
number_format: fmt,
|
||||||
|
..Default::default()
|
||||||
|
},
|
||||||
|
..Default::default()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// ── Formula replacement / removal edge cases ────────────────────────
|
||||||
|
|
||||||
|
/// `set_cell("")` on an existing *formula* cell removes the cell,
|
||||||
|
/// clears its dep-graph edges, recalcs dependents, and records the
|
||||||
|
/// change.
|
||||||
|
#[test]
|
||||||
|
fn set_cell_empty_removes_existing_formula_cell() {
|
||||||
|
let mut sheet = SpreadsheetData::default();
|
||||||
|
sheet.set_cell(0, 0, "10");
|
||||||
|
sheet.set_cell(0, 1, "=A1");
|
||||||
|
assert_eq!(sheet.get_display_value(0, 1), "10");
|
||||||
|
|
||||||
|
sheet.set_cell(0, 1, "");
|
||||||
|
assert!(!sheet.cells.contains_key(&CellId::new(0, 1)));
|
||||||
|
assert!(!sheet.dependencies.contains_key(&CellId::new(0, 1)));
|
||||||
|
let a1_dependents = sheet.dependents.get(&CellId::new(0, 0));
|
||||||
|
assert!(!a1_dependents.is_some_and(|s| s.contains(&CellId::new(0, 1))));
|
||||||
|
assert_eq!(sheet.get_display_value(0, 1), "");
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Replacing a formula with a constant drops the old dependency edges.
|
||||||
|
#[test]
|
||||||
|
fn set_cell_replacing_formula_with_value_cleans_graph() {
|
||||||
|
let mut sheet = SpreadsheetData::default();
|
||||||
|
sheet.set_cell(0, 0, "10");
|
||||||
|
sheet.set_cell(0, 1, "=A1");
|
||||||
|
assert!(sheet.dependencies.contains_key(&CellId::new(0, 1)));
|
||||||
|
|
||||||
|
sheet.set_cell(0, 1, "7");
|
||||||
|
assert_eq!(sheet.get_display_value(0, 1), "7");
|
||||||
|
assert!(!sheet.dependencies.contains_key(&CellId::new(0, 1)));
|
||||||
|
assert!(!sheet
|
||||||
|
.dependents
|
||||||
|
.get(&CellId::new(0, 0))
|
||||||
|
.is_some_and(|s| s.contains(&CellId::new(0, 1))));
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Replacing one formula with another re-wires the graph end-to-end
|
||||||
|
/// (through `set_cell`, not `update_dependency_graph` directly).
|
||||||
|
#[test]
|
||||||
|
fn set_cell_replacing_formula_with_formula_rewires_graph() {
|
||||||
|
let mut sheet = SpreadsheetData::default();
|
||||||
|
sheet.set_cell(0, 0, "1");
|
||||||
|
sheet.set_cell(0, 1, "2");
|
||||||
|
sheet.set_cell(0, 2, "=A1");
|
||||||
|
assert_eq!(sheet.get_display_value(0, 2), "1");
|
||||||
|
|
||||||
|
sheet.set_cell(0, 2, "=B1");
|
||||||
|
assert_eq!(sheet.get_display_value(0, 2), "2");
|
||||||
|
let deps = sheet.dependencies.get(&CellId::new(0, 2)).unwrap();
|
||||||
|
assert!(!deps.contains(&CellId::new(0, 0)));
|
||||||
|
assert!(deps.contains(&CellId::new(0, 1)));
|
||||||
|
assert!(!sheet
|
||||||
|
.dependents
|
||||||
|
.get(&CellId::new(0, 0))
|
||||||
|
.is_some_and(|s| s.contains(&CellId::new(0, 2))));
|
||||||
|
assert!(sheet
|
||||||
|
.dependents
|
||||||
|
.get(&CellId::new(0, 1))
|
||||||
|
.is_some_and(|s| s.contains(&CellId::new(0, 2))));
|
||||||
|
}
|
||||||
|
|
||||||
|
// ── No-op removal paths ─────────────────────────────────────────────
|
||||||
|
|
||||||
|
/// `set_cell("")` on a cell that does not exist is a no-op: no cell,
|
||||||
|
/// no dep-graph entry, no panic.
|
||||||
|
#[test]
|
||||||
|
fn set_cell_empty_on_missing_cell_is_noop() {
|
||||||
|
let mut sheet = SpreadsheetData::default();
|
||||||
|
sheet.set_cell(0, 0, "");
|
||||||
|
assert!(sheet.cells.is_empty());
|
||||||
|
assert!(sheet.dependencies.is_empty());
|
||||||
|
assert!(sheet.dependents.is_empty());
|
||||||
|
}
|
||||||
|
|
||||||
|
// ── Dependency-graph cleanup after formula deletion ─────────────────
|
||||||
|
|
||||||
|
/// `remove_cell` on a *formula* cell removes its dependency edges and
|
||||||
|
/// the reverse dependent edges of the cells it referenced.
|
||||||
|
#[test]
|
||||||
|
fn remove_cell_formula_cleans_dependency_graph() {
|
||||||
|
let mut sheet = SpreadsheetData::default();
|
||||||
|
sheet.set_cell(0, 0, "10");
|
||||||
|
sheet.set_cell(0, 1, "=A1");
|
||||||
|
assert!(sheet.dependencies.contains_key(&CellId::new(0, 1)));
|
||||||
|
|
||||||
|
sheet.remove_cell(0, 1);
|
||||||
|
assert!(!sheet.cells.contains_key(&CellId::new(0, 1)));
|
||||||
|
assert!(!sheet.dependencies.contains_key(&CellId::new(0, 1)));
|
||||||
|
assert!(!sheet
|
||||||
|
.dependents
|
||||||
|
.get(&CellId::new(0, 0))
|
||||||
|
.is_some_and(|s| s.contains(&CellId::new(0, 1))));
|
||||||
|
}
|
||||||
|
|
||||||
|
/// `update_dependency_graph`'s defensive branch: an edge listed in
|
||||||
|
/// `dependencies` whose dependent has no `dependents` entry must be
|
||||||
|
/// dropped without panicking.
|
||||||
|
#[test]
|
||||||
|
fn update_dependency_graph_ignores_missing_dependent_entry() {
|
||||||
|
let mut sheet = SpreadsheetData::default();
|
||||||
|
let cell_id = CellId::new(0, 0);
|
||||||
|
let mut deps = HashSet::new();
|
||||||
|
deps.insert(CellId::new(1, 0));
|
||||||
|
// Inconsistent graph: dependencies claims B1, but dependents has
|
||||||
|
// no B1 entry.
|
||||||
|
sheet.dependencies.insert(cell_id, deps);
|
||||||
|
sheet.update_dependency_graph(cell_id, None);
|
||||||
|
assert!(!sheet.dependencies.contains_key(&cell_id));
|
||||||
|
}
|
||||||
|
|
||||||
|
// ── Affected-cell recalculation error paths ─────────────────────────
|
||||||
|
|
||||||
|
/// A formula that evaluates to the empty string must not trip the B17
|
||||||
|
/// "empty computed_value" invariant — that was a false positive on a
|
||||||
|
/// legitimate result (regression for the `=""` panic).
|
||||||
|
#[test]
|
||||||
|
fn empty_text_formula_does_not_panic() {
|
||||||
|
let mut sheet = SpreadsheetData::default();
|
||||||
|
sheet.set_cell(0, 0, "=\"\"");
|
||||||
|
// Empty computed_value falls back to the raw formula text, as
|
||||||
|
// documented by `display_formula_empty_computed_falls_back_to_value`.
|
||||||
|
assert_eq!(sheet.get_display_value(0, 0), "=\"\"");
|
||||||
|
// A full recalc must also leave it alone rather than panic.
|
||||||
|
sheet.recalculate_all();
|
||||||
|
assert_eq!(sheet.get_display_value(0, 0), "=\"\"");
|
||||||
|
}
|
||||||
|
|
||||||
|
/// When a cycle is detected, non-formula cells in the affected set get
|
||||||
|
/// their computed_value cleared (they keep their raw value), while
|
||||||
|
/// formula cells are marked `#CYCLE!`.
|
||||||
|
#[test]
|
||||||
|
fn cycle_recalc_clears_non_formula_affected_cells() {
|
||||||
|
let mut sheet = SpreadsheetData::default();
|
||||||
|
sheet.set_cell(0, 0, "1"); // A1 value
|
||||||
|
sheet.set_cell(0, 1, "=A1+C1"); // B1 depends on A1 and C1
|
||||||
|
sheet.set_cell(0, 2, "=B1"); // C1 depends on B1 -> B1<->C1 cycle
|
||||||
|
assert_eq!(sheet.get_display_value(0, 1), "#CYCLE!");
|
||||||
|
assert_eq!(sheet.get_display_value(0, 2), "#CYCLE!");
|
||||||
|
|
||||||
|
// Editing A1 pulls A1, B1 and C1 into the affected set; the cycle
|
||||||
|
// is still there, so B1/C1 stay #CYCLE! while A1 (a value) keeps
|
||||||
|
// its raw value with a cleared computed_value.
|
||||||
|
sheet.set_cell(0, 0, "2");
|
||||||
|
assert_eq!(sheet.get_display_value(0, 0), "2");
|
||||||
|
assert_eq!(sheet.get_display_value(0, 1), "#CYCLE!");
|
||||||
|
assert_eq!(sheet.get_display_value(0, 2), "#CYCLE!");
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Legacy public `get_cell_value` API: missing cells read as 0.0,
|
||||||
|
/// values coerce to Number/Boolean/Text, formulas evaluate, and a
|
||||||
|
/// re-entrant visit reports CycleDetected.
|
||||||
|
#[test]
|
||||||
|
fn public_get_cell_value_covers_all_arms() {
|
||||||
|
let mut sheet = SpreadsheetData::default();
|
||||||
|
let mut visiting = HashSet::new();
|
||||||
|
|
||||||
|
// Missing cell.
|
||||||
|
assert!(matches!(
|
||||||
|
sheet.get_cell_value(0, 0, &mut visiting),
|
||||||
|
Ok(Value::Number(n)) if n == 0.0
|
||||||
|
));
|
||||||
|
|
||||||
|
// Value cells.
|
||||||
|
sheet.set_cell(0, 0, "42");
|
||||||
|
sheet.set_cell(0, 1, "TRUE");
|
||||||
|
sheet.set_cell(0, 2, "FALSE");
|
||||||
|
sheet.set_cell(0, 3, "hello");
|
||||||
|
assert!(matches!(
|
||||||
|
sheet.get_cell_value(0, 0, &mut visiting),
|
||||||
|
Ok(Value::Number(n)) if n == 42.0
|
||||||
|
));
|
||||||
|
assert!(matches!(
|
||||||
|
sheet.get_cell_value(0, 1, &mut visiting),
|
||||||
|
Ok(Value::Boolean(b)) if b
|
||||||
|
));
|
||||||
|
assert!(matches!(
|
||||||
|
sheet.get_cell_value(0, 2, &mut visiting),
|
||||||
|
Ok(Value::Boolean(b)) if !b
|
||||||
|
));
|
||||||
|
assert!(matches!(
|
||||||
|
sheet.get_cell_value(0, 3, &mut visiting),
|
||||||
|
Ok(Value::Text(t)) if t == "hello"
|
||||||
|
));
|
||||||
|
|
||||||
|
// Formula evaluation through the recursive path.
|
||||||
|
sheet.set_cell(1, 0, "=A1+1");
|
||||||
|
assert!(matches!(
|
||||||
|
sheet.get_cell_value(1, 0, &mut visiting),
|
||||||
|
Ok(Value::Number(n)) if n == 43.0
|
||||||
|
));
|
||||||
|
|
||||||
|
// Re-entrant visit -> CycleDetected.
|
||||||
|
let mut visiting = HashSet::new();
|
||||||
|
visiting.insert((1, 0));
|
||||||
|
assert!(matches!(
|
||||||
|
sheet.get_cell_value(1, 0, &mut visiting),
|
||||||
|
Err(FormulaError::CycleDetected)
|
||||||
|
));
|
||||||
|
|
||||||
|
// parse_cell_ref.
|
||||||
|
assert_eq!(SpreadsheetData::parse_cell_ref("B2"), Some((1, 1)));
|
||||||
|
assert_eq!(SpreadsheetData::parse_cell_ref("not a ref"), None);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Recursive eval slow path inside `DataEvalContext::get_cell_value`:
|
||||||
|
/// a formula whose computed_value is empty is re-evaluated — cached
|
||||||
|
/// AST first, then the parse fallback — and a self-reference reports
|
||||||
|
/// CycleDetected from the context's own visiting set.
|
||||||
|
#[test]
|
||||||
|
fn eval_context_slow_path_cached_and_parse_fallback() {
|
||||||
|
let mut sheet = SpreadsheetData::default();
|
||||||
|
sheet.set_cell(0, 0, "42"); // A1
|
||||||
|
sheet.set_cell(0, 1, "=A1+1"); // B1 -> 43, cached AST set
|
||||||
|
sheet.set_cell(0, 2, "=B1+1"); // C1 -> 44
|
||||||
|
|
||||||
|
// Force B1 through the slow path with a cached AST.
|
||||||
|
sheet
|
||||||
|
.cells
|
||||||
|
.get_mut(&CellId::new(0, 1))
|
||||||
|
.unwrap()
|
||||||
|
.computed_value
|
||||||
|
.clear();
|
||||||
|
let mut visiting = HashSet::new();
|
||||||
|
assert!(matches!(
|
||||||
|
sheet.get_cell_value(0, 2, &mut visiting),
|
||||||
|
Ok(Value::Number(n)) if n == 44.0
|
||||||
|
));
|
||||||
|
|
||||||
|
// Force B1 through the parse-fallback path (no cached AST).
|
||||||
|
let b1 = sheet.cells.get_mut(&CellId::new(0, 1)).unwrap();
|
||||||
|
b1.computed_value.clear();
|
||||||
|
b1.cached_ast = None;
|
||||||
|
let mut visiting = HashSet::new();
|
||||||
|
assert!(matches!(
|
||||||
|
sheet.get_cell_value(0, 2, &mut visiting),
|
||||||
|
Ok(Value::Number(n)) if n == 44.0
|
||||||
|
));
|
||||||
|
|
||||||
|
// Self-reference with an empty computed value: the recursion
|
||||||
|
// re-enters the cell and the context's visiting set reports the
|
||||||
|
// cycle.
|
||||||
|
sheet.set_cell(2, 0, "=A3");
|
||||||
|
sheet
|
||||||
|
.cells
|
||||||
|
.get_mut(&CellId::new(2, 0))
|
||||||
|
.unwrap()
|
||||||
|
.computed_value
|
||||||
|
.clear();
|
||||||
|
let mut visiting = HashSet::new();
|
||||||
|
assert!(matches!(
|
||||||
|
sheet.get_cell_value(2, 0, &mut visiting),
|
||||||
|
Err(FormulaError::CycleDetected)
|
||||||
|
));
|
||||||
|
}
|
||||||
|
|
||||||
|
/// `parse_cell_computed_value`: empty -> 0.0, booleans, text, and
|
||||||
|
/// error-string propagation.
|
||||||
|
#[test]
|
||||||
|
fn parse_cell_computed_value_arms() {
|
||||||
|
assert!(matches!(
|
||||||
|
DataEvalContext::parse_cell_computed_value(""),
|
||||||
|
Ok(Value::Number(n)) if n == 0.0
|
||||||
|
));
|
||||||
|
assert!(matches!(
|
||||||
|
DataEvalContext::parse_cell_computed_value("TRUE"),
|
||||||
|
Ok(Value::Boolean(b)) if b
|
||||||
|
));
|
||||||
|
assert!(matches!(
|
||||||
|
DataEvalContext::parse_cell_computed_value("false"),
|
||||||
|
Ok(Value::Boolean(b)) if !b
|
||||||
|
));
|
||||||
|
assert!(matches!(
|
||||||
|
DataEvalContext::parse_cell_computed_value("hello"),
|
||||||
|
Ok(Value::Text(t)) if t == "hello"
|
||||||
|
));
|
||||||
|
assert!(matches!(
|
||||||
|
DataEvalContext::parse_cell_computed_value("#DIV/0!"),
|
||||||
|
Err(FormulaError::DivByZero)
|
||||||
|
));
|
||||||
|
}
|
||||||
|
|
||||||
|
// ── Named-range dependency expansion ────────────────────────────────
|
||||||
|
|
||||||
|
/// A unary expression over a named range (`=-Total`) still expands the
|
||||||
|
/// named range into its constituent cells in the dependency graph.
|
||||||
|
#[test]
|
||||||
|
fn unary_named_range_dependencies_expand() {
|
||||||
|
let mut sheet = SpreadsheetData::default();
|
||||||
|
sheet.set_cell(0, 0, "10");
|
||||||
|
sheet.set_cell(1, 0, "20");
|
||||||
|
sheet.named_ranges.insert("Total".to_string(), (0, 0, 1, 0));
|
||||||
|
sheet.set_cell(2, 0, "=-Total");
|
||||||
|
let deps = sheet.dependencies.get(&CellId::new(2, 0)).unwrap();
|
||||||
|
assert!(deps.contains(&CellId::new(0, 0)));
|
||||||
|
assert!(deps.contains(&CellId::new(1, 0)));
|
||||||
|
}
|
||||||
|
|
||||||
|
// ── Large-range fast path (get_range_values, >64 cells) ─────────────
|
||||||
|
|
||||||
|
/// SUM over a range wider than 64 cells exercises the large-range fast
|
||||||
|
/// path, converting numbers, booleans, text and formula cells.
|
||||||
|
#[test]
|
||||||
|
fn large_range_sum_uses_fast_path() {
|
||||||
|
let mut sheet = SpreadsheetData::default();
|
||||||
|
sheet.set_cell(0, 0, "10"); // number
|
||||||
|
sheet.set_cell(0, 1, "TRUE"); // boolean (counts as 1 in SUM)
|
||||||
|
sheet.set_cell(0, 2, "FALSE"); // boolean (counts as 0)
|
||||||
|
sheet.set_cell(0, 3, "hello"); // text (skipped by SUM)
|
||||||
|
sheet.set_cell(0, 4, "=A1+5"); // formula with cached value 15
|
||||||
|
sheet.set_cell(8, 8, "7"); // bottom-right number
|
||||||
|
sheet.set_cell(9, 0, "=SUM(A1:I9)");
|
||||||
|
assert_eq!(sheet.get_display_value(9, 0), "33"); // 10+1+0+15+7
|
||||||
|
}
|
||||||
|
|
||||||
|
/// A formula cell inside a large range that was inserted without a
|
||||||
|
/// recalc (`put_cell`) forces the recursive slow path during range
|
||||||
|
/// evaluation, including the parse fallback.
|
||||||
|
#[test]
|
||||||
|
fn large_range_recursive_formula_cell() {
|
||||||
|
let mut sheet = SpreadsheetData::default();
|
||||||
|
sheet.set_cell(0, 0, "10");
|
||||||
|
let cell = CellData {
|
||||||
|
value: "=A1+1".to_string(),
|
||||||
|
formula: Some("=A1+1".to_string()),
|
||||||
|
..Default::default()
|
||||||
|
};
|
||||||
|
sheet.put_cell(1, 0, cell); // A2 formula, no recalc -> empty computed
|
||||||
|
sheet.set_cell(9, 0, "=SUM(A1:I9)");
|
||||||
|
assert_eq!(sheet.get_display_value(9, 0), "21"); // 10 + 11
|
||||||
|
}
|
||||||
|
|
||||||
|
// ── Number-format parse failures keep raw text ──────────────────────
|
||||||
|
|
||||||
|
/// Non-numeric cells formatted as Integer/Decimal/Percent keep their
|
||||||
|
/// raw text (the parse-failure branch of `write_display_value`).
|
||||||
|
#[test]
|
||||||
|
fn non_numeric_formatted_cells_keep_raw_text() {
|
||||||
|
let sheet = SpreadsheetData::default();
|
||||||
|
for fmt in [
|
||||||
|
NumberFormat::Integer,
|
||||||
|
NumberFormat::Decimal1,
|
||||||
|
NumberFormat::Decimal2,
|
||||||
|
NumberFormat::Percent,
|
||||||
|
] {
|
||||||
|
assert_eq!(sheet.display_value_from_cell(&cell_with("n/a", fmt)), "n/a");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// ── Demo constructors ───────────────────────────────────────────────
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn demo_q3_and_roi_construct_non_empty_sheets() {
|
||||||
|
let q3 = SpreadsheetData::demo_q3();
|
||||||
|
assert!(!q3.cells.is_empty());
|
||||||
|
let roi = SpreadsheetData::demo_roi();
|
||||||
|
assert!(!roi.cells.is_empty());
|
||||||
|
}
|
||||||
|
|
||||||
|
/// A formula referenced through another formula that fails to parse on
|
||||||
|
/// the recursive path propagates the parse error instead of panicking.
|
||||||
|
#[test]
|
||||||
|
fn parse_fallback_error_propagates() {
|
||||||
|
let mut sheet = SpreadsheetData::default();
|
||||||
|
let cell = CellData {
|
||||||
|
value: "=1+".to_string(),
|
||||||
|
formula: Some("=1+".to_string()),
|
||||||
|
..Default::default()
|
||||||
|
};
|
||||||
|
sheet.put_cell(0, 1, cell); // B1 invalid formula, no cached AST
|
||||||
|
sheet.set_cell(0, 0, "=B1"); // A1 evaluates B1 through the parse fallback
|
||||||
|
assert!(sheet.get_display_value(0, 0).starts_with("#ERROR!"));
|
||||||
|
}
|
||||||
|
|
||||||
|
/// A formula cell inside a large range that carries a cached AST (but no
|
||||||
|
/// computed value yet) is evaluated via the cached-AST branch.
|
||||||
|
#[test]
|
||||||
|
fn large_range_cached_ast_branch() {
|
||||||
|
let mut sheet = SpreadsheetData::default();
|
||||||
|
sheet.set_cell(0, 0, "10");
|
||||||
|
let cell = CellData {
|
||||||
|
value: "=A1+1".to_string(),
|
||||||
|
formula: Some("=A1+1".to_string()),
|
||||||
|
cached_ast: formula2::parse("A1+1").ok(),
|
||||||
|
..Default::default()
|
||||||
|
};
|
||||||
|
sheet.put_cell(0, 1, cell); // B1 formula w/ cached AST, empty computed
|
||||||
|
sheet.set_cell(9, 0, "=SUM(A1:I9)");
|
||||||
|
assert_eq!(sheet.get_display_value(9, 0), "21"); // 10 + 11
|
||||||
|
}
|
||||||
|
|
||||||
|
/// A formula cell inside a large range that fails to parse propagates the
|
||||||
|
/// error out of the range evaluation.
|
||||||
|
#[test]
|
||||||
|
fn large_range_parse_error_propagates() {
|
||||||
|
let mut sheet = SpreadsheetData::default();
|
||||||
|
sheet.set_cell(0, 0, "5");
|
||||||
|
let cell = CellData {
|
||||||
|
value: "=1+".to_string(),
|
||||||
|
formula: Some("=1+".to_string()),
|
||||||
|
..Default::default()
|
||||||
|
};
|
||||||
|
sheet.put_cell(0, 1, cell); // B1 invalid formula inside the range
|
||||||
|
sheet.set_cell(9, 0, "=SUM(A1:I9)");
|
||||||
|
assert!(sheet.get_display_value(9, 0).starts_with("#ERROR!"));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue