use lemma::{DateTimeValue, Engine, SourceType};
use rust_decimal::Decimal;
use std::collections::HashMap;
fn load(engine: &mut Engine, code: &str) {
engine
.load([(SourceType::Volatile, code.to_string())])
.unwrap();
}
fn run(
engine: &Engine,
spec: &str,
data: HashMap<String, String>,
explain: bool,
) -> lemma::Response {
let now = DateTimeValue::now();
engine
.run(None, spec, Some(&now), data, None, explain)
.expect("evaluation must complete")
}
fn rule_result<'response>(
response: &'response lemma::Response,
rule_name: &str,
) -> &'response lemma::RuleResult {
response
.results
.values()
.find(|result| result.rule.name == rule_name)
.unwrap_or_else(|| panic!("rule '{rule_name}' must be in the response"))
}
#[test]
fn rule_result_rounds_excess_precision_at_output() {
let mut engine = Engine::new();
load(
&mut engine,
r#"
spec precision
rule third: 1 / 3
"#,
);
let response = run(&engine, "precision", HashMap::new(), false);
let result = rule_result(&response, "third");
assert!(!result.vetoed);
assert_eq!(
result.number.as_deref(),
Some("0.3333333333333333333333333333")
);
}
#[test]
fn rule_result_rounds_tiny_value_to_zero_at_output() {
let mut engine = Engine::new();
load(
&mut engine,
r#"
spec tiny
rule dust: 1 / (10 ^ 30)
"#,
);
let response = run(&engine, "tiny", HashMap::new(), false);
let result = rule_result(&response, "dust");
assert!(!result.vetoed);
assert_eq!(result.number.as_deref(), Some("0"));
}
#[test]
fn magnitude_overflow_vetoes_at_output() {
let max = Decimal::MAX.normalize().to_string();
let code = format!(
r#"
spec overflow
data max_val: {max}
data two: 2
rule product: max_val * two
"#
);
let mut engine = Engine::new();
load(&mut engine, &code);
let response = run(&engine, "overflow", HashMap::new(), false);
let result = rule_result(&response, "product");
assert!(result.vetoed);
assert_eq!(
result.veto_reason.as_deref(),
Some("Calculated result exceeds decimal value limit")
);
}
#[test]
fn intermediate_exceeding_decimal_range_stored_exactly_output_rounds() {
let max = Decimal::MAX.normalize().to_string();
let code = format!(
r#"
spec magnitude_overflow_boundary
data max_val: {max}
data two: 2
rule huge: max_val * two
rule safe: huge / two
"#
);
let mut engine = Engine::new();
load(&mut engine, &code);
let response = run(&engine, "magnitude_overflow_boundary", HashMap::new(), true);
let huge = rule_result(&response, "huge");
assert!(huge.vetoed);
assert_eq!(
huge.veto_reason.as_deref(),
Some("Calculated result exceeds decimal value limit")
);
let explanation = huge.explanation.as_ref().expect("huge explanation");
assert!(
!explanation.result.vetoed(),
"rule_results must store exact Q; materialization veto is separate"
);
let response = run(
&engine,
"magnitude_overflow_boundary",
HashMap::new(),
false,
);
let safe = rule_result(&response, "safe");
assert!(!safe.vetoed);
assert_eq!(safe.number.as_deref(), Some(max.as_str()));
}
#[test]
fn accepted_measure_data_plans_and_evaluates_without_decimal_limit_rejection() {
let code = r#"
spec weight
data money: measure
-> unit eur 1
-> unit milli 0.001
-> minimum 1000 eur
rule r: money
"#;
let mut engine = Engine::new();
load(&mut engine, code);
let response = run(
&engine,
"weight",
HashMap::from([("money".to_string(), "1000 eur".to_string())]),
false,
);
let result = rule_result(&response, "r");
assert!(!result.vetoed);
assert!(
result.veto_reason.is_none(),
"accepted measure input must not veto for decimal limit"
);
}
#[test]
fn overlay_decimals_constraint_rejects_excess_precision_at_input() {
let mut engine = Engine::new();
load(
&mut engine,
r#"
spec price_check
data price: number -> decimals 2
rule r: price
"#,
);
let rejected = run(
&engine,
"price_check",
HashMap::from([("price".to_string(), "1.234".to_string())]),
false,
);
let result = rule_result(&rejected, "r");
assert!(
result.vetoed,
"overlay with excess decimal precision must veto"
);
let reason = result.veto_reason.as_deref().unwrap_or("");
assert!(
reason.contains("exceeds decimals constraint 2"),
"expected decimals constraint message, got: {reason}"
);
assert!(
reason.contains("1.234"),
"veto must show rejected scale, not rounded 1.23, got: {reason}"
);
assert!(
!reason.contains("decimal value limit"),
"must not use materialization veto message for input constraint, got: {reason}"
);
let accepted = run(
&engine,
"price_check",
HashMap::from([("price".to_string(), "1.23".to_string())]),
false,
);
let result = rule_result(&accepted, "r");
assert!(!result.vetoed);
assert_eq!(result.number.as_deref(), Some("1.23"));
}
#[test]
fn overlay_measure_decimals_constraint_uses_declared_unit() {
let mut engine = Engine::new();
load(
&mut engine,
r#"
spec delivery
data cost: measure -> decimals 2 -> unit eur 1
rule r: cost
"#,
);
let rejected = run(
&engine,
"delivery",
HashMap::from([("cost".to_string(), "1.234 eur".to_string())]),
false,
);
let result = rule_result(&rejected, "r");
assert!(result.vetoed);
let reason = result.veto_reason.as_deref().unwrap_or("");
assert!(
reason.contains("exceeds decimals constraint 2"),
"expected decimals constraint message, got: {reason}"
);
assert!(
reason.contains("1.234"),
"veto must show rejected scale, not rounded 1.23, got: {reason}"
);
let accepted = run(
&engine,
"delivery",
HashMap::from([("cost".to_string(), "1.23 eur".to_string())]),
false,
);
let result = rule_result(&accepted, "r");
assert!(!result.vetoed);
let measure = result.measure.as_ref().expect("measure map");
assert_eq!(measure.get("eur"), Some(&"1.23".to_string()));
}