use truecalc_workbook::{
Address, CellInput, EngineFlavor, RecalcContext, Value, Workbook, Worksheet,
};
fn addr(a1: &str) -> Address {
Address::from_a1(a1).expect("valid A1")
}
fn ctx() -> RecalcContext {
RecalcContext::new(1_780_878_600_000, "Etc/GMT", 0).expect("Etc/GMT is a valid tz")
}
fn wb_with_range_formula() -> Workbook {
let mut wb = Workbook::new(EngineFlavor::Sheets);
wb.add_sheet(Worksheet::new("S")).unwrap();
wb.set("S", addr("A1"), CellInput::Literal(Value::Number(1.0)))
.unwrap();
wb.set("S", addr("A2"), CellInput::Literal(Value::Number(2.0)))
.unwrap();
wb.set("S", addr("B1"), CellInput::Formula("=SUM(A1:A2)".into()))
.unwrap();
wb
}
fn incremental_on_a1(wb: &mut Workbook) {
wb.recalc_incremental(&ctx(), &[("S".to_owned(), addr("A1"))]);
}
fn assert_incremental_equals_full(wb: &mut Workbook, edited: &[(&str, &str)]) {
let mut full = wb.clone();
full.recalc(&ctx());
let owned: Vec<(String, Address)> = edited
.iter()
.map(|(s, a)| ((*s).to_string(), addr(a)))
.collect();
wb.recalc_incremental(&ctx(), &owned);
assert_eq!(
wb.to_json().unwrap(),
full.to_json().unwrap(),
"incremental and full grids diverged"
);
}
#[test]
fn a_cold_workbook_builds_the_index_on_first_incremental_recalc() {
let mut wb = wb_with_range_formula();
assert_eq!(wb.authored_index_builds(), 0);
assert!(!wb.authored_index_cache_is_warm());
incremental_on_a1(&mut wb);
assert_eq!(
wb.authored_index_builds(),
1,
"the first incremental recalc must build the index exactly once"
);
assert!(wb.authored_index_cache_is_warm());
}
#[test]
fn repeating_an_unchanged_incremental_recalc_never_rebuilds() {
let mut wb = wb_with_range_formula();
incremental_on_a1(&mut wb);
let builds = wb.authored_index_builds();
for _ in 0..20 {
incremental_on_a1(&mut wb);
}
assert_eq!(
wb.authored_index_builds(),
builds,
"recalculating an unchanged workbook must not rebuild the index"
);
}
#[test]
fn an_edit_touching_neither_the_authored_set_nor_sheet_structure_leaves_the_cache_warm() {
let mut wb = wb_with_range_formula();
wb.set("S", addr("Z1"), CellInput::Literal(Value::Number(1.0)))
.unwrap();
incremental_on_a1(&mut wb);
let builds_before = wb.authored_index_builds();
assert!(wb.authored_index_cache_is_warm());
wb.set("S", addr("Z1"), CellInput::Literal(Value::Number(99.0)))
.unwrap();
assert_incremental_equals_full(&mut wb, &[("S", "Z1")]);
assert_eq!(
wb.authored_index_builds(),
builds_before,
"an edit touching neither the authored set nor sheet structure must \
not rebuild the index"
);
assert!(wb.authored_index_cache_is_warm());
}
#[test]
fn a_new_authored_cell_rebuilds() {
let mut wb = wb_with_range_formula();
incremental_on_a1(&mut wb);
let builds_before = wb.authored_index_builds();
assert!(wb.authored_index_cache_is_warm());
wb.set("S", addr("Z1"), CellInput::Literal(Value::Number(1.0)))
.unwrap();
assert!(
!wb.authored_index_cache_is_warm(),
"a new authored cell must invalidate on the write itself"
);
assert_incremental_equals_full(&mut wb, &[("S", "Z1")]);
assert!(
wb.authored_index_builds() > builds_before,
"a new authored cell must have forced a rebuild"
);
}
#[test]
fn a_removed_authored_cell_rebuilds() {
let mut wb = wb_with_range_formula();
wb.set("S", addr("Z1"), CellInput::Literal(Value::Number(1.0)))
.unwrap();
incremental_on_a1(&mut wb);
let builds_before = wb.authored_index_builds();
assert!(wb.authored_index_cache_is_warm());
wb.clear("S", addr("Z1"));
assert!(
!wb.authored_index_cache_is_warm(),
"removing an authored cell must invalidate on the write itself"
);
incremental_on_a1(&mut wb);
assert!(
wb.authored_index_builds() > builds_before,
"removing an authored cell must have forced a rebuild"
);
}
#[test]
fn every_sheet_structure_operation_that_can_add_or_remove_or_rekey_an_authored_cell_rebuilds() {
for op in ["add", "rename", "remove"] {
let mut wb = wb_with_range_formula();
wb.add_sheet(Worksheet::new("T")).unwrap(); incremental_on_a1(&mut wb);
assert!(wb.authored_index_cache_is_warm(), "{op}: warm before");
match op {
"add" => {
wb.add_sheet(Worksheet::new("U")).unwrap();
}
"rename" => {
wb.rename_sheet("S", "S2").unwrap();
}
"remove" => {
wb.remove_sheet("T").unwrap();
}
_ => unreachable!(),
}
assert!(
!wb.authored_index_cache_is_warm(),
"{op} sheet must invalidate the authored-cell-index cache"
);
}
}
#[test]
fn moving_a_sheet_does_not_invalidate_the_authored_index_cache() {
let mut wb = wb_with_range_formula();
wb.add_sheet(Worksheet::new("T")).unwrap();
incremental_on_a1(&mut wb);
assert!(wb.authored_index_cache_is_warm());
let builds_before = wb.authored_index_builds();
wb.move_sheet(0, 1).unwrap();
assert!(
wb.authored_index_cache_is_warm(),
"moving a sheet must not invalidate the authored-cell-index cache"
);
assert_eq!(wb.authored_index_builds(), builds_before);
}
#[test]
fn every_mutable_accessor_invalidates_on_the_borrow() {
let mut wb = wb_with_range_formula();
incremental_on_a1(&mut wb);
assert!(wb.authored_index_cache_is_warm());
let _ = wb.sheets_mut();
assert!(
!wb.authored_index_cache_is_warm(),
"sheets_mut must invalidate on the borrow"
);
let mut wb = wb_with_range_formula();
incremental_on_a1(&mut wb);
assert!(wb.authored_index_cache_is_warm());
let _ = wb.sheet_mut("S");
assert!(
!wb.authored_index_cache_is_warm(),
"sheet_mut must invalidate on the borrow"
);
}
#[test]
fn a_workbook_loaded_from_json_builds_the_index_correctly_on_its_first_incremental_call() {
let json = wb_with_range_formula().to_json().unwrap();
let mut wb = Workbook::from_json(json.as_bytes()).unwrap();
assert!(!wb.authored_index_cache_is_warm());
wb.set("S", addr("A1"), CellInput::Literal(Value::Number(5.0)))
.unwrap();
assert_incremental_equals_full(&mut wb, &[("S", "A1")]);
assert!(wb.authored_index_cache_is_warm());
assert_eq!(wb.authored_index_builds(), 1);
assert_eq!(
wb.get("S", addr("B1")).unwrap().value(),
&Value::Number(7.0)
);
}