use crate::engine::{
CancelToken, CycleConfig, CycleDetection, CyclePolicy, Engine, EvalConfig,
EvaluationRequestOutcome, EvaluationTarget, FormulaDirtyLeaseOutcome, TargetEvalOptions,
};
use crate::function::{FnCaps, Function};
use crate::test_workbook::TestWorkbook;
use crate::traits::{ArgumentHandle, CalcValue, FunctionContext};
use formualizer_common::{ExcelError, ExcelErrorKind, LiteralValue};
use formualizer_parse::parser::parse;
use std::sync::Arc;
use std::sync::atomic::{AtomicUsize, Ordering};
const NEVER: usize = usize::MAX;
#[derive(Clone, Copy, Debug)]
enum OnCancel {
Err,
Value,
Array,
}
#[derive(Debug)]
struct Probe {
calls: Arc<AtomicUsize>,
cancel_at: Arc<AtomicUsize>,
on_cancel: OnCancel,
array: bool,
}
fn column3() -> LiteralValue {
LiteralValue::Array(vec![
vec![LiteralValue::Number(1.0)],
vec![LiteralValue::Number(2.0)],
vec![LiteralValue::Number(3.0)],
])
}
impl Function for Probe {
fn caps(&self) -> FnCaps {
FnCaps::PURE
}
fn name(&self) -> &'static str {
"PROBE"
}
fn eval<'a, 'b, 'c>(
&self,
_args: &'c [ArgumentHandle<'a, 'b>],
ctx: &dyn FunctionContext<'b>,
) -> Result<CalcValue<'b>, ExcelError> {
let call = self.calls.fetch_add(1, Ordering::SeqCst);
if call == self.cancel_at.load(Ordering::SeqCst) {
ctx.cancellation_token()
.expect("a cancellable request exposes its token")
.cancel();
return match self.on_cancel {
OnCancel::Err => Err(ExcelError::new(ExcelErrorKind::Cancelled)),
OnCancel::Value => Ok(CalcValue::Scalar(LiteralValue::Number(10.0))),
OnCancel::Array => Ok(CalcValue::Scalar(column3())),
};
}
Ok(CalcValue::Scalar(if self.array {
column3()
} else {
LiteralValue::Number(10.0)
}))
}
}
#[derive(Debug)]
struct StoredCancelled {
calls: Arc<AtomicUsize>,
}
impl Function for StoredCancelled {
fn caps(&self) -> FnCaps {
FnCaps::PURE
}
fn name(&self) -> &'static str {
"STORED_CANCELLED"
}
fn eval<'a, 'b, 'c>(
&self,
_args: &'c [ArgumentHandle<'a, 'b>],
_ctx: &dyn FunctionContext<'b>,
) -> Result<CalcValue<'b>, ExcelError> {
self.calls.fetch_add(1, Ordering::SeqCst);
Ok(CalcValue::Scalar(LiteralValue::Error(ExcelError::new(
ExcelErrorKind::Cancelled,
))))
}
}
struct Harness {
engine: Engine<TestWorkbook>,
calls: Arc<AtomicUsize>,
cancel_at: Arc<AtomicUsize>,
}
impl Harness {
fn new(config: EvalConfig, cancel_at: usize, on_cancel: OnCancel, array: bool) -> Self {
crate::builtins::load_builtins();
let calls = Arc::new(AtomicUsize::new(0));
let cancel_at = Arc::new(AtomicUsize::new(cancel_at));
let wb = TestWorkbook::new().with_function(Arc::new(Probe {
calls: calls.clone(),
cancel_at: cancel_at.clone(),
on_cancel,
array,
}));
Self {
engine: Engine::new(wb, config),
calls,
cancel_at,
}
}
fn sequential(cancel_at: usize, on_cancel: OnCancel) -> Self {
Self::new(sequential_config(), cancel_at, on_cancel, false)
}
fn formula(&mut self, row: u32, col: u32, src: &str) {
self.engine
.set_cell_formula("Sheet1", row, col, parse(src).unwrap())
.unwrap();
}
fn value(&mut self, row: u32, col: u32, value: f64) {
self.engine
.set_cell_value("Sheet1", row, col, LiteralValue::Number(value))
.unwrap();
}
fn get(&self, row: u32, col: u32) -> Option<LiteralValue> {
match self.engine.get_cell_value("Sheet1", row, col) {
Some(LiteralValue::Int(i)) => Some(LiteralValue::Number(i as f64)),
Some(LiteralValue::Empty) | None => None,
other => other,
}
}
fn calls(&self) -> usize {
self.calls.load(Ordering::SeqCst)
}
fn assert_cancelled(&self, result: Result<crate::engine::EvalResult, ExcelError>) {
let error = result.expect_err("live cancellation must fail the request");
assert_eq!(error.kind, ExcelErrorKind::Cancelled, "{error:?}");
let stats = self
.engine
.last_evaluation_resource_request_stats()
.expect("request stats");
assert_eq!(stats.outcome, EvaluationRequestOutcome::Cancelled);
assert_ne!(stats.dirty_lease, FormulaDirtyLeaseOutcome::Acknowledged);
}
fn assert_not_cancelled_value(&self, row: u32, col: u32) {
assert!(
!matches!(
self.get(row, col),
Some(LiteralValue::Error(ref e)) if e.kind == ExcelErrorKind::Cancelled
),
"R{row}C{col} published a live cancellation as a value: {:?}",
self.get(row, col)
);
}
}
fn sequential_config() -> EvalConfig {
EvalConfig {
enable_parallel: false,
..Default::default()
}
}
fn parallel_config() -> EvalConfig {
EvalConfig {
enable_parallel: true,
max_threads: Some(2),
..Default::default()
}
}
fn n(x: f64) -> Option<LiteralValue> {
Some(LiteralValue::Number(x))
}
#[test]
fn singleton_final_vertex_err_cancelled_fails_request_and_retry_recomputes() {
let mut h = Harness::sequential(0, OnCancel::Err);
h.formula(1, 1, "=PROBE()");
let result = h.engine.evaluate_all_cancellable(CancelToken::new());
h.assert_cancelled(result);
assert_eq!(h.calls(), 1);
assert_eq!(h.get(1, 1), None, "the cancelled vertex is not committed");
h.engine.evaluate_all().unwrap();
assert_eq!(h.calls(), 2, "retry re-evaluates the cancelled vertex");
assert_eq!(h.get(1, 1), n(10.0));
}
#[test]
fn singleton_final_iferror_over_cancelled_value_retries() {
let mut h = Harness::sequential(0, OnCancel::Err);
h.formula(1, 1, "=IFERROR(PROBE(),-1)");
let result = h.engine.evaluate_all_cancellable(CancelToken::new());
h.assert_cancelled(result);
h.assert_not_cancelled_value(1, 1);
assert_ne!(h.get(1, 1), n(-1.0), "the fallback is not committed");
h.engine.evaluate_all().unwrap();
assert_eq!(h.calls(), 2);
assert_eq!(h.get(1, 1), n(10.0));
}
#[test]
fn singleton_final_value_computed_across_cancellation_is_not_committed() {
let mut h = Harness::sequential(0, OnCancel::Value);
h.formula(1, 1, "=PROBE()+1");
let result = h.engine.evaluate_all_cancellable(CancelToken::new());
h.assert_cancelled(result);
assert_eq!(h.get(1, 1), None);
h.engine.evaluate_all().unwrap();
assert_eq!(h.calls(), 2);
assert_eq!(h.get(1, 1), n(11.0));
}
#[test]
fn stored_cancelled_literal_without_live_cancellation_commits() {
crate::builtins::load_builtins();
let calls = Arc::new(AtomicUsize::new(0));
let wb = TestWorkbook::new().with_function(Arc::new(StoredCancelled {
calls: calls.clone(),
}));
let mut engine = Engine::new(wb, sequential_config());
engine
.set_cell_formula("Sheet1", 1, 1, parse("=STORED_CANCELLED()").unwrap())
.unwrap();
let token = CancelToken::new();
engine.evaluate_all_cancellable(token.clone()).unwrap();
assert!(!token.is_cancelled());
assert!(matches!(
engine.get_cell_value("Sheet1", 1, 1),
Some(LiteralValue::Error(e)) if e.kind == ExcelErrorKind::Cancelled
));
assert_eq!(
engine
.last_evaluation_resource_request_stats()
.unwrap()
.outcome,
EvaluationRequestOutcome::Success
);
engine.evaluate_all().unwrap();
assert_eq!(calls.load(Ordering::SeqCst), 1, "the stored value is clean");
}
#[test]
fn final_layer_in_layer_cancellation_fails_request_and_retry_recomputes() {
let mut h = Harness::sequential(2, OnCancel::Err);
for row in 1..=4 {
h.formula(row, 1, &format!("=PROBE()+{row}"));
}
let result = h.engine.evaluate_all_cancellable(CancelToken::new());
h.assert_cancelled(result);
assert_eq!(h.calls(), 3, "the unit after the cancelling one never runs");
h.assert_not_cancelled_value(3, 1);
assert_eq!(h.get(3, 1), None);
assert_eq!(h.get(4, 1), None);
h.engine.evaluate_all().unwrap();
assert_eq!(h.calls(), 7, "retry recomputes every scheduled vertex");
for row in 1..=4 {
assert_eq!(h.get(row, 1), n(10.0 + row as f64));
}
}
#[test]
fn final_dependent_layer_cancellation_retries() {
let mut h = Harness::sequential(NEVER, OnCancel::Err);
h.value(1, 1, 5.0);
h.formula(1, 2, "=A1*2");
h.formula(1, 3, "=B1+PROBE()");
h.engine.evaluate_all().unwrap();
assert_eq!(h.get(1, 3), n(20.0));
h.value(1, 1, 6.0);
h.cancel_at.store(1, Ordering::SeqCst);
let result = h.engine.evaluate_all_cancellable(CancelToken::new());
h.assert_cancelled(result);
assert_eq!(
h.get(1, 3),
n(20.0),
"the previous value is not overwritten"
);
h.engine.evaluate_all().unwrap();
assert_eq!(h.calls(), 3);
assert_eq!(h.get(1, 2), n(12.0));
assert_eq!(h.get(1, 3), n(22.0));
}
#[test]
fn final_spill_computed_across_cancellation_is_not_committed() {
let mut h = Harness::new(sequential_config(), 0, OnCancel::Array, true);
h.formula(1, 1, "=PROBE()");
let result = h.engine.evaluate_all_cancellable(CancelToken::new());
h.assert_cancelled(result);
for row in 1..=3 {
assert_eq!(h.get(row, 1), None, "no spill cell is projected");
}
h.engine.evaluate_all().unwrap();
assert_eq!(h.calls(), 2);
for row in 1..=3 {
assert_eq!(h.get(row, 1), n(row as f64));
}
}
#[test]
fn final_dynamic_reader_cancellation_retries() {
let setup = |h: &mut Harness| {
h.value(1, 2, 7.0);
h.formula(1, 1, "=PROBE()+INDIRECT(\"B1\")");
};
let mut dry = Harness::sequential(NEVER, OnCancel::Err);
setup(&mut dry);
dry.engine
.evaluate_all_cancellable(CancelToken::new())
.unwrap();
let per_request = dry.calls();
assert!(per_request >= 1);
assert_eq!(dry.get(1, 1), n(17.0));
let mut h = Harness::sequential(per_request - 1, OnCancel::Err);
setup(&mut h);
let result = h.engine.evaluate_all_cancellable(CancelToken::new());
h.assert_cancelled(result);
h.assert_not_cancelled_value(1, 1);
h.engine.evaluate_all().unwrap();
assert_eq!(h.get(1, 1), n(17.0), "retry recomputes the dynamic reader");
}
#[test]
fn final_parallel_layer_cancellation_commits_nothing_and_retries() {
let mut h = Harness::new(parallel_config(), 31, OnCancel::Err, false);
for row in 1..=32 {
h.formula(row, 1, &format!("=PROBE()+{row}"));
}
let result = h.engine.evaluate_all_cancellable(CancelToken::new());
h.assert_cancelled(result);
assert_eq!(h.calls(), 32);
for row in 1..=32 {
assert_eq!(h.get(row, 1), None, "row {row}: the group is not committed");
}
h.engine.evaluate_all().unwrap();
for row in 1..=32 {
assert_eq!(h.get(row, 1), n(10.0 + row as f64));
}
}
fn family_rows(h: &mut Harness, rows: u32) {
for row in 1..=rows {
h.value(row, 1, row as f64);
h.formula(row, 2, &format!("=A{row}*2+PROBE()"));
}
}
#[test]
fn final_family_run_member_cancellation_retries() {
for parallel in [false, true] {
const ROWS: u32 = 16;
let config = || EvalConfig {
family_execution: true,
enable_parallel: parallel,
max_threads: Some(2),
..super::common::arrow_eval_config()
};
let mut dry = Harness::new(config(), NEVER, OnCancel::Err, false);
family_rows(&mut dry, ROWS);
dry.engine
.evaluate_all_cancellable(CancelToken::new())
.unwrap();
let per_request = dry.calls();
assert!(per_request >= 1);
let mut h = Harness::new(config(), per_request - 1, OnCancel::Err, false);
family_rows(&mut h, ROWS);
let result = h.engine.evaluate_all_cancellable(CancelToken::new());
h.assert_cancelled(result);
for row in 1..=ROWS {
h.assert_not_cancelled_value(row, 2);
}
h.engine.evaluate_all().unwrap();
for row in 1..=ROWS {
assert_eq!(
h.get(row, 2),
n(row as f64 * 2.0 + 10.0),
"parallel={parallel} row={row}"
);
}
}
}
#[test]
fn final_runtime_scc_member_cancellation_retries() {
let config = EvalConfig::default().with_cycle(CycleConfig {
detection: CycleDetection::Runtime,
policy: CyclePolicy::Error,
});
let mut h = Harness::new(config, 0, OnCancel::Err, false);
h.engine
.set_cell_value("Sheet1", 1, 1, LiteralValue::Boolean(true))
.unwrap();
h.formula(2, 1, "=IF(A1,PROBE(),A3)");
h.formula(3, 1, "=IF(A1,A2,999)");
let result = h.engine.evaluate_all_cancellable(CancelToken::new());
h.assert_cancelled(result);
h.engine.evaluate_all().unwrap();
assert_eq!(h.get(2, 1), n(10.0));
assert_eq!(h.get(3, 1), n(10.0));
}
#[test]
fn final_target_vertex_cancellation_via_evaluate_until_cancellable() {
let mut h = Harness::sequential(0, OnCancel::Err);
h.formula(1, 1, "=PROBE()");
let result = h
.engine
.evaluate_until_cancellable(&["A1"], CancelToken::new());
h.assert_cancelled(result);
assert_eq!(h.get(1, 1), None);
h.engine.evaluate_all().unwrap();
assert_eq!(h.calls(), 2);
assert_eq!(h.get(1, 1), n(10.0));
}
#[test]
fn final_target_vertex_cancellation_via_target_options() {
let mut h = Harness::sequential(0, OnCancel::Err);
h.formula(1, 1, "=PROBE()");
let target = EvaluationTarget::Cell {
sheet: "Sheet1".to_string(),
row: 1,
col: 1,
};
let result = h.engine.evaluate_targets_with_options(
std::slice::from_ref(&target),
TargetEvalOptions {
cancel: Some(CancelToken::new()),
..Default::default()
},
);
h.assert_cancelled(result);
assert_eq!(h.get(1, 1), None);
h.engine
.evaluate_targets_with_options(&[target], TargetEvalOptions::default())
.unwrap();
assert_eq!(h.calls(), 2);
assert_eq!(h.get(1, 1), n(10.0));
}
#[test]
fn final_vertex_cancellation_via_recalc_plan_controls() {
let mut h = Harness::sequential(0, OnCancel::Err);
h.formula(1, 1, "=PROBE()");
let plan = h.engine.build_recalc_plan().unwrap();
let result = h
.engine
.evaluate_recalc_plan_with_controls(&plan, Some(CancelToken::new()), None);
h.assert_cancelled(result);
assert_eq!(h.get(1, 1), None);
h.engine.evaluate_all().unwrap();
assert_eq!(h.calls(), 2);
assert_eq!(h.get(1, 1), n(10.0));
}