use crate::engine::named_range::{NameScope, NamedDefinition};
use crate::engine::{Engine, EvalConfig, EvaluationTarget, TargetEvalOptions};
use crate::reference::{SharedRangeRef, SharedSheetLocator};
use crate::test_workbook::TestWorkbook;
use formualizer_common::{AxisBound, LiteralValue};
use formualizer_parse::parser::parse;
fn engine_with_two_sheets() -> Engine<TestWorkbook> {
let mut engine = Engine::new(TestWorkbook::new(), EvalConfig::default());
engine.add_sheet("Sheet2").unwrap();
engine
}
#[test]
fn sheet_scoped_name_does_not_leak_into_unscoped_has_name() {
let mut engine = engine_with_two_sheets();
let sheet1 = engine.sheet_id("Sheet1").unwrap();
assert_eq!(
sheet1,
engine.default_sheet_id(),
"precondition: Sheet1 is the default sheet"
);
engine
.define_name(
"SecretLocal",
NamedDefinition::Literal(LiteralValue::Number(42.0)),
NameScope::Sheet(sheet1),
)
.unwrap();
assert!(
engine.has_name("SecretLocal", Some("Sheet1")),
"the name is in scope on the sheet it is scoped to"
);
assert!(
!engine.has_name("SecretLocal", Some("Sheet2")),
"the name is not in scope on another sheet"
);
assert!(
!engine.has_name("SecretLocal", None),
"an unscoped query asks about workbook scope only; a name scoped to the \
default sheet must not answer it"
);
}
#[test]
fn sheet_scoped_name_does_not_leak_into_unscoped_resolved_name_value() {
let mut engine = engine_with_two_sheets();
let sheet1 = engine.sheet_id("Sheet1").unwrap();
engine
.define_name(
"SecretLocal",
NamedDefinition::Literal(LiteralValue::Number(42.0)),
NameScope::Sheet(sheet1),
)
.unwrap();
engine.evaluate_all().unwrap();
assert_eq!(
engine.resolved_name_value("SecretLocal", Some("Sheet1")),
Some(LiteralValue::Number(42.0))
);
assert_eq!(
engine.resolved_name_value("SecretLocal", Some("Sheet2")),
None
);
assert_eq!(
engine.resolved_name_value("SecretLocal", None),
None,
"an unscoped read must not see a name scoped to the default sheet"
);
}
#[test]
fn workbook_scoped_name_answers_unscoped_and_sheet_scoped_queries() {
let mut engine = engine_with_two_sheets();
engine
.define_name(
"Global",
NamedDefinition::Literal(LiteralValue::Number(7.0)),
NameScope::Workbook,
)
.unwrap();
engine.evaluate_all().unwrap();
assert!(engine.has_name("Global", None));
assert!(engine.has_name("Global", Some("Sheet1")));
assert!(engine.has_name("Global", Some("Sheet2")));
assert_eq!(
engine.resolved_name_value("Global", None),
Some(LiteralValue::Number(7.0))
);
}
#[test]
fn unscoped_name_target_does_not_select_default_sheet_scoped_name() {
let mut engine = engine_with_two_sheets();
let sheet1 = engine.sheet_id("Sheet1").unwrap();
engine
.define_name(
"SecretLocal",
NamedDefinition::Literal(LiteralValue::Number(42.0)),
NameScope::Sheet(sheet1),
)
.unwrap();
let target = EvaluationTarget::Name {
name: "SecretLocal".to_string(),
scope_sheet: None,
};
let report = engine
.prepare_graph_for_targets(std::slice::from_ref(&target), TargetEvalOptions::default())
.unwrap();
assert!(
!report.widening_reasons.is_empty(),
"an unscoped target naming a sheet-scoped name is unresolved and must \
widen, not silently resolve through the default sheet: {report:?}"
);
let scoped = EvaluationTarget::Name {
name: "SecretLocal".to_string(),
scope_sheet: Some("Sheet1".to_string()),
};
let scoped_report = engine
.prepare_graph_for_targets(std::slice::from_ref(&scoped), TargetEvalOptions::default())
.unwrap();
assert!(
scoped_report.widening_reasons.is_empty(),
"the correctly scoped target resolves: {scoped_report:?}"
);
}
fn compressed_range(sheet: SharedSheetLocator<'static>) -> SharedRangeRef<'static> {
SharedRangeRef {
sheet,
start_row: Some(AxisBound::new(0, true)),
start_col: Some(AxisBound::new(0, true)),
end_row: Some(AxisBound::new(9, true)),
end_col: Some(AxisBound::new(9, true)),
}
}
fn staging_engine() -> Engine<TestWorkbook> {
let config = EvalConfig {
defer_graph_building: true,
..EvalConfig::default()
};
let mut engine = Engine::new(TestWorkbook::new(), config);
for sheet in ["Alpha", "Beta"] {
engine.add_sheet(sheet).unwrap();
}
engine
}
fn define_name_with_range_deps(
engine: &mut Engine<TestWorkbook>,
name: &str,
scope: NameScope,
range_deps: Vec<SharedRangeRef<'static>>,
) {
engine
.define_name(
name,
NamedDefinition::Formula {
ast: parse("=1").unwrap(),
dependencies: Vec::new(),
range_deps: Vec::new(),
},
scope,
)
.unwrap();
engine
.update_name(
name,
NamedDefinition::Formula {
ast: parse("=1").unwrap(),
dependencies: Vec::new(),
range_deps,
},
scope,
)
.unwrap();
}
fn selected_sheets(report: &crate::engine::PreparedTargetGraphReport) -> Vec<String> {
let mut sheets = report
.selected_cells
.iter()
.map(|cell| cell.sheet.to_string())
.collect::<Vec<_>>();
sheets.sort();
sheets.dedup();
sheets
}
#[test]
fn current_locator_in_named_formula_deps_uses_the_names_sheet_not_the_default() {
let mut engine = staging_engine();
let alpha = engine.sheet_id("Alpha").unwrap();
assert_ne!(alpha, engine.default_sheet_id());
engine.stage_formula_text("Alpha", 3, 3, "=1".into());
engine.stage_formula_text(
engine.default_sheet_name().to_string().as_str(),
3,
3,
"=2".into(),
);
define_name_with_range_deps(
&mut engine,
"AlphaBox",
NameScope::Sheet(alpha),
vec![compressed_range(SharedSheetLocator::Current)],
);
let target = EvaluationTarget::Name {
name: "AlphaBox".to_string(),
scope_sheet: Some("Alpha".to_string()),
};
let report = engine
.prepare_graph_for_targets(&[target], TargetEvalOptions::default())
.unwrap();
assert_eq!(
selected_sheets(&report),
vec!["Alpha".to_string()],
"the name is scoped to Alpha, so its `Current` range dependency covers Alpha - not the workbook default sheet: {report:?}"
);
}
#[test]
fn named_sheet_locator_in_named_formula_deps_resolves_to_that_sheet() {
let mut engine = staging_engine();
let alpha = engine.sheet_id("Alpha").unwrap();
engine.stage_formula_text("Beta", 3, 3, "=1".into());
engine.stage_formula_text(
engine.default_sheet_name().to_string().as_str(),
3,
3,
"=2".into(),
);
define_name_with_range_deps(
&mut engine,
"BetaBox",
NameScope::Sheet(alpha),
vec![compressed_range(SharedSheetLocator::Name("Beta".into()))],
);
let target = EvaluationTarget::Name {
name: "BetaBox".to_string(),
scope_sheet: Some("Alpha".to_string()),
};
let report = engine
.prepare_graph_for_targets(&[target], TargetEvalOptions::default())
.unwrap();
assert_eq!(
selected_sheets(&report),
vec!["Beta".to_string()],
"a Beta! range dependency covers Beta, not the default sheet: {report:?}"
);
}
#[test]
fn unresolvable_sheet_locator_in_named_formula_deps_widens() {
let mut engine = staging_engine();
let alpha = engine.sheet_id("Alpha").unwrap();
engine.stage_formula_text(
engine.default_sheet_name().to_string().as_str(),
3,
3,
"=2".into(),
);
define_name_with_range_deps(
&mut engine,
"GhostBox",
NameScope::Sheet(alpha),
vec![compressed_range(SharedSheetLocator::Name(
"NoSuchSheet".into(),
))],
);
let target = EvaluationTarget::Name {
name: "GhostBox".to_string(),
scope_sheet: Some("Alpha".to_string()),
};
let report = engine
.prepare_graph_for_targets(&[target], TargetEvalOptions::default())
.unwrap();
assert!(
report
.widening_reasons
.contains(&crate::engine::OpaqueReason::UnresolvedCrossSheetBinding),
"an unresolved cross-sheet binding must be surfaced, not replaced by \
the default sheet: {report:?}"
);
}