Files
improvise/crates/improvise-incremental/tests/live_source.rs
T

285 lines
10 KiB
Rust

//! `Incremental` over a source that reports its own changes (ADR 009 step 7, improvise-rnak).
//! Evidence: `context/test-evidence/improvise-incremental.md`. Counts, never time.
mod support;
use improvise_core::formula::ast::AggFunc;
use improvise_core::model::cell::{CellKey, CellValue};
use improvise_core::model::source::{ChangeSource, SharedFeed};
use improvise_core::model::{Aggregation, Model};
use improvise_incremental::{
Derived, Eager, Edit, Incremental, Maintained, Meter, differential_in, edit_probes, probes,
same_value,
};
use std::sync::Arc;
use support::live::{Fed, LiveTable};
use support::{corpus, edits_for, hand_model, key};
#[test]
fn a_live_source_matches_eager_over_the_corpus_and_its_edits() {
let (mut compared, mut files_done) = (0, Vec::<String>::new());
for (name, model, none, views) in corpus() {
let file = name.split(':').next().unwrap().to_string();
if files_done.contains(&file) {
continue;
}
files_done.push(file);
let mut edits = edits_for(&model);
if model.data.iter_cells().count() > 40 {
let picked = [0, 2, 4].iter().filter_map(|&i| edits.get(i).cloned());
edits = picked.collect();
}
let candidate = Fed::new(&model, none.clone());
let ran = differential_in(&model, &views, &none, candidate, &edits);
let report = ran.unwrap_or_else(|d| panic!("{name}: {d}"));
assert_eq!(report.steps, edits.len() + 1, "{name}");
compared += report.probes;
}
assert!(compared > 10_000, "only {compared} answers were compared");
}
#[test]
fn changes_named_by_the_caller_are_maintained_as_the_drain_would() {
let (model, none) = (hand_model(), vec!["Region".to_string()]);
let mut eager = Eager::new(model.clone(), none.clone());
let mut fed = Fed::new(&model, none);
let edits = [
Edit::SetCell(key("East", "A", "Revenue"), CellValue::Number(99.0)),
Edit::SetCell(key("North", "A", "Revenue"), CellValue::Number(1.0)),
Edit::ClearCell(key("West", "B", "Cost")),
Edit::SetCell(key("East", "A", "Note"), CellValue::Text("z".into())),
];
let mut questions = probes(eager.model());
let mut asked = 0;
for edit in &edits {
eager.apply(edit);
let (cell, value) = match edit {
Edit::SetCell(cell, value) => (cell, Some(value.clone())),
Edit::ClearCell(cell) => (cell, None),
_ => unreachable!("cell edits only"),
};
fed.live.write(cell.clone(), value);
let changes = fed.live.drain_changes();
assert_eq!(changes.len(), 1);
fed.inc.apply_changes(&changes);
questions.extend(probes(eager.model()));
questions.extend(edit_probes(edit));
for probe in &questions {
let (want, got) = (probe.ask(&eager), probe.ask(&fed));
assert!(
same_value(&want, &got),
"{probe:?}: eager {want:?} != fed {got:?}"
);
asked += 1;
}
}
assert!(asked > 500, "only {asked} answers were compared");
}
#[test]
fn a_write_is_invisible_until_it_is_drained() {
let model = hand_model();
let mut fed = Fed::new(&model, vec![]);
let cell = key("East", "A", "Revenue");
let before = fed.inc.value(&cell, Aggregation::Exact);
assert_eq!(before, Some(CellValue::Number(10.0)));
fed.live.write(cell.clone(), Some(CellValue::Number(50.0)));
assert_eq!(
fed.inc.value(&cell, Aggregation::Exact),
before,
"not yet drained"
);
fed.inc.sync();
assert_eq!(
fed.inc.value(&cell, Aggregation::Exact),
Some(CellValue::Number(50.0))
);
fed.inc.sync();
assert_eq!(
fed.inc.value(&cell, Aggregation::Exact),
Some(CellValue::Number(50.0))
);
}
fn row(r: usize, p: usize, m: usize) -> CellKey {
CellKey::new(vec![
("Region".into(), format!("r{r}")),
("Product".into(), format!("p{p}")),
("Month".into(), format!("m{m}")),
("_Measure".into(), "Revenue".into()),
])
}
/// `n` regions by `n` products by 6 months of one measure, with no formula.
fn table(n: usize) -> Model {
let mut model = Model::new("table");
for (cat, prefix, count) in [("Region", "r", n), ("Product", "p", n), ("Month", "m", 6)] {
model.add_category(cat).unwrap();
for i in 0..count {
model
.category_mut(cat)
.unwrap()
.add_item(format!("{prefix}{i}"));
}
}
model.category_mut("_Measure").unwrap().add_item("Revenue");
model.measure_agg.insert("Revenue".into(), AggFunc::Max);
let mut seed = 1u64;
for (r, p, m) in (0..n).flat_map(|r| (0..n).flat_map(move |p| (0..6).map(move |m| (r, p, m)))) {
seed = seed
.wrapping_mul(6364136223846793005)
.wrapping_add(1442695040888963407);
model.set_cell(
row(r, p, m),
CellValue::Number(((seed >> 33) % 1000) as f64),
);
}
model
}
fn since(before: Meter, after: Meter) -> Meter {
Meter {
updates: after.updates - before.updates,
sum_updates: after.sum_updates - before.sum_updates,
reads: after.reads - before.reads,
builds: after.builds - before.builds,
built_cells: after.built_cells - before.built_cells,
stems: after.stems - before.stems,
formula_cells: after.formula_cells - before.formula_cells,
rebuilds: after.rebuilds - before.rebuilds,
recomputes: after.recomputes - before.recomputes,
}
}
fn region_partial() -> CellKey {
CellKey::new(vec![
("Region".into(), "r1".into()),
("_Measure".into(), "Revenue".into()),
])
}
/// What one reported change costs once a shape is built, and what the source served for it.
fn change_cost(n: usize) -> (Meter, u64) {
let mut fed = Fed::new(&table(n), vec![]);
fed.inc.raw_fold(&region_partial());
let (before, served) = (fed.inc.meter(), fed.live.snapshot_rows());
assert_eq!(
served,
(n * n * 6) as u64,
"the build is one pass over the table"
);
fed.live
.write(row(1, 2, 3), Some(CellValue::Number(2000.0)));
fed.inc.sync();
let cost = since(before, fed.inc.meter());
assert_eq!(
fed.inc.raw_fold(&region_partial()),
Some(CellValue::Number(2000.0)),
"the new maximum comes from the change"
);
(cost, fed.live.snapshot_rows() - served)
}
#[test]
fn the_cost_of_a_reported_change_does_not_depend_on_the_size_of_the_table() {
let (small, small_rows) = change_cost(6);
let (large, large_rows) = change_cost(12);
assert_eq!(
(small.updates, small.sum_updates),
(2, 2),
"retract and insert"
);
assert_eq!(small, large, "216 cells and 864 cells cost the same");
assert_eq!(
(small.builds, small.built_cells),
(0, 0),
"no shape is rebuilt"
);
assert_eq!(
(small_rows, large_rows),
(0, 0),
"the source serves no snapshot for a change"
);
}
#[test]
fn an_overwrite_by_the_same_value_moves_nothing() {
let mut fed = Fed::new(&hand_model(), vec![]);
let cell = key("East", "A", "Revenue");
let revenue = CellKey::new(vec![("_Measure".into(), "Revenue".into())]);
let before = fed.inc.raw_fold(&revenue);
fed.live.write(cell, Some(CellValue::Number(10.0)));
fed.inc.sync();
assert_eq!(fed.inc.raw_fold(&revenue), before);
assert_eq!(before, Some(CellValue::Number(23.0)));
}
#[test]
fn a_backend_can_be_built_over_the_exported_feed_alias() {
let model = hand_model();
let rows = model.data.iter_cells().map(|(k, v)| (k, v.clone()));
let feed: SharedFeed = Arc::new(LiveTable::of(rows));
let mut inc = Incremental::over(model.clone(), feed, vec![]);
let revenue = CellKey::new(vec![("_Measure".into(), "Revenue".into())]);
assert_eq!(inc.raw_fold(&revenue), Some(CellValue::Number(23.0)));
inc.sync();
}
#[test]
fn a_populated_model_put_over_a_feed_keeps_no_cells_of_its_own() {
let (model, none) = (hand_model(), vec!["Region".to_string()]);
assert!(
model.data.iter_cells().count() > 0,
"the input is populated"
);
let rows = model.data.iter_cells().map(|(k, v)| (k, v.clone()));
let live = Arc::new(LiveTable::of(rows));
let inc = Incremental::over(model.clone(), live, none.clone());
assert_eq!(inc.model().data.iter_cells().count(), 0, "no stale copy");
assert_eq!(inc.model().get_cell(&key("East", "A", "Revenue")), None);
let eager = Eager::new(model, none);
for probe in probes(eager.model()) {
let (want, got) = (probe.ask(&eager), probe.ask(&inc));
assert!(
same_value(&want, &got),
"{probe:?}: eager {want:?} != fed {got:?}"
);
}
}
#[test]
#[should_panic(expected = "cell edits go through the source")]
fn a_cell_edit_on_a_fed_backend_is_refused() {
let mut fed = Fed::new(&hand_model(), vec![]);
let edit = Edit::SetCell(key("East", "A", "Revenue"), CellValue::Number(1.0));
fed.inc.apply(&edit);
}
#[test]
#[should_panic(expected = "cell edits go through the source")]
fn a_cell_clear_on_a_fed_backend_is_refused() {
let mut fed = Fed::new(&hand_model(), vec![]);
fed.inc.apply(&Edit::ClearCell(key("East", "A", "Revenue")));
}
#[test]
fn a_feed_has_one_drainer() {
let model = hand_model();
let rows = model.data.iter_cells().map(|(k, v)| (k, v.clone()));
let live = Arc::new(LiveTable::of(rows));
let mut a = Incremental::over(model.clone(), live.clone(), vec![]);
let mut b = Incremental::over(model, live.clone(), vec![]);
let revenue = CellKey::new(vec![("_Measure".into(), "Revenue".into())]);
let (before_a, before_b) = (a.raw_fold(&revenue), b.raw_fold(&revenue));
assert_eq!(before_a, before_b);
live.write(key("East", "A", "Revenue"), Some(CellValue::Number(100.0)));
a.sync();
b.sync();
let (after_a, after_b) = (a.raw_fold(&revenue), b.raw_fold(&revenue));
assert_ne!(after_a, before_a, "the first drainer saw the write");
assert_eq!(
after_b, before_b,
"the second saw nothing: a drain is destructive"
);
}