use lemma::{DateTimeValue, Engine, SourceType};
use std::collections::HashMap;
use std::path::PathBuf;
use std::sync::Arc;
fn src(name: &str) -> SourceType {
SourceType::Path(Arc::new(PathBuf::from(name)))
}
fn run_spec(engine: &Engine, spec: &str, data: &[(&str, &str)]) -> lemma::Response {
let now = DateTimeValue::now();
let data_map: HashMap<String, String> = data
.iter()
.map(|(k, v)| (k.to_string(), v.to_string()))
.collect();
engine
.run(None, spec, Some(&now), data_map, false, None)
.unwrap()
}
fn rule_display(response: &lemma::Response, rule_name: &str) -> String {
response
.results
.values()
.find(|r| r.rule.name == rule_name)
.unwrap_or_else(|| panic!("rule '{}' not found", rule_name))
.display
.clone()
.unwrap_or_else(|| panic!("rule '{}' has no display", rule_name))
}
fn rule_vetoed(response: &lemma::Response, rule_name: &str) -> bool {
response
.results
.values()
.find(|r| r.rule.name == rule_name)
.unwrap_or_else(|| panic!("rule '{}' not found", rule_name))
.vetoed
}
#[test]
fn hunt_as_precedence_with_division() {
let mut engine = Engine::new();
engine
.load(
r#"
spec as_prec
uses lemma units
data money: measure
-> unit eur 1.00
data wage: measure
-> unit eur_per_hour eur/hour
data hours_worked: 160 hours
data hourly_rate: wage -> default 50 eur_per_hour
rule total_pay: (hourly_rate * hours_worked) as eur
"#,
src("as_prec.lemma"),
)
.unwrap();
let resp = run_spec(&engine, "as_prec", &[]);
let display = rule_display(&resp, "total_pay");
assert_eq!(display, "8000 eur", "50 eur/hour * 160 hours = 8000 eur");
}
#[test]
fn hunt_ratio_comparison() {
let mut engine = Engine::new();
engine
.load(
r#"
spec ratio_cmp
data discount: ratio
rule big_discount: discount > 20%
rule small_discount: discount < 5%
rule exact_ten: discount is 10%
"#,
src("ratio_cmp.lemma"),
)
.unwrap();
let resp = run_spec(&engine, "ratio_cmp", &[("discount", "25%")]);
assert_eq!(rule_display(&resp, "big_discount"), "true", "25% > 20%");
assert_eq!(
rule_display(&resp, "small_discount"),
"false",
"25% not < 5%"
);
assert_eq!(rule_display(&resp, "exact_ten"), "false", "25% is not 10%");
let resp = run_spec(&engine, "ratio_cmp", &[("discount", "10%")]);
assert_eq!(rule_display(&resp, "exact_ten"), "true", "10% is 10%");
}
#[test]
fn hunt_veto_in_boolean_expression() {
let mut engine = Engine::new();
engine
.load(
r#"
spec veto_bool
data value: number
data flag: boolean
rule validated: value
unless value < 0 then veto "negative"
rule combined: validated > 10 and flag
"#,
src("veto_bool.lemma"),
)
.unwrap();
let resp = run_spec(&engine, "veto_bool", &[("value", "-5"), ("flag", "true")]);
assert!(rule_vetoed(&resp, "validated"), "negative value vetoes");
assert!(
rule_vetoed(&resp, "combined"),
"veto should propagate through comparison"
);
}
#[test]
fn hunt_is_veto_on_transitive_veto() {
let mut engine = Engine::new();
engine
.load(
r#"
spec deep_veto
data input: number
rule step1: input
unless input < 0 then veto "bad input"
rule step2: step1 * 2
rule step3: step2 + 1
rule check_step3: step3 is veto
"#,
src("deep_veto.lemma"),
)
.unwrap();
let resp = run_spec(&engine, "deep_veto", &[("input", "-1")]);
assert!(
rule_vetoed(&resp, "step3"),
"step3 should veto transitively"
);
assert_eq!(
rule_display(&resp, "check_step3"),
"true",
"step3 is veto = true"
);
let resp = run_spec(&engine, "deep_veto", &[("input", "5")]);
assert!(!rule_vetoed(&resp, "step3"));
assert_eq!(
rule_display(&resp, "check_step3"),
"false",
"step3 is not veto"
);
}
#[test]
fn hunt_measure_comparison() {
let mut engine = Engine::new();
engine
.load(
r#"
spec quantity_cmp
uses lemma units
data weight: units.mass
rule over_five: weight > 5 kilogram
rule under_one: weight < 1 kilogram
rule exactly_three: weight is 3 kilogram
"#,
src("quantity_cmp.lemma"),
)
.unwrap();
let resp = run_spec(&engine, "quantity_cmp", &[("weight", "3 kilogram")]);
assert_eq!(rule_display(&resp, "over_five"), "false");
assert_eq!(rule_display(&resp, "under_one"), "false");
assert_eq!(rule_display(&resp, "exactly_three"), "true");
let resp = run_spec(&engine, "quantity_cmp", &[("weight", "10 kilogram")]);
assert_eq!(rule_display(&resp, "over_five"), "true");
}
#[test]
fn hunt_money_precision() {
let mut engine = Engine::new();
engine
.load(
r#"
spec money_test
data money: measure
-> decimals 2
-> unit eur 1.00
data price: money -> default 19.99 eur
data tax_rate: 21%
rule tax_amount: price * tax_rate
rule total: price + tax_amount
rule ten_items: price * 10
"#,
src("money.lemma"),
)
.unwrap();
let resp = run_spec(&engine, "money_test", &[]);
let tax = rule_display(&resp, "tax_amount");
assert_eq!(
tax, "4.20 eur",
"19.99 * 21% = 4.1979, rounded to 2dp = 4.20 eur"
);
let ten = rule_display(&resp, "ten_items");
assert_eq!(ten, "199.90 eur", "19.99 * 10 = 199.90 eur");
}
#[test]
fn hunt_unless_condition_with_veto_rule() {
let mut engine = Engine::new();
engine
.load(
r#"
spec unless_veto_cond
data input: number
rule validated: input
unless input > 1000 then veto "too large"
rule result: 0
unless validated > 50 then 100
unless validated is veto then 0
"#,
src("unless_veto.lemma"),
)
.unwrap();
let resp = run_spec(&engine, "unless_veto_cond", &[("input", "2000")]);
let display = rule_display(&resp, "result");
assert_eq!(
display, "0",
"vetoed condition should not match → default applies"
);
let resp = run_spec(&engine, "unless_veto_cond", &[("input", "100")]);
assert_eq!(rule_display(&resp, "result"), "100");
}
#[test]
fn hunt_operator_precedence() {
let mut engine = Engine::new();
engine
.load(
r#"
spec precedence
data a: number
data b: number
data c: number
rule add_then_mult: a + b * c
rule explicit_parens: a + (b * c)
rule mult_first: a * b + c
"#,
src("precedence.lemma"),
)
.unwrap();
let resp = run_spec(&engine, "precedence", &[("a", "2"), ("b", "3"), ("c", "4")]);
let result = rule_display(&resp, "add_then_mult");
assert_eq!(
result, "14",
"2 + 3*4 = 14 (multiplication before addition)"
);
let explicit = rule_display(&resp, "explicit_parens");
assert_eq!(explicit, "14", "2 + (3*4) = 14");
let mult = rule_display(&resp, "mult_first");
assert_eq!(mult, "10", "2*3 + 4 = 10");
}
#[test]
fn hunt_text_empty_string() {
let mut engine = Engine::new();
let result = engine.load(
r#"
spec empty_text
data label: text
-> option ""
-> option "hello"
rule is_empty: label is ""
rule is_hello: label is "hello"
"#,
src("empty.lemma"),
);
match result {
Ok(_) => {
let resp = run_spec(&engine, "empty_text", &[("label", "")]);
assert_eq!(rule_display(&resp, "is_empty"), "true");
assert_eq!(rule_display(&resp, "is_hello"), "false");
}
Err(_) => {
panic!("Empty string in -> option is rejected at parse time — is this intended?");
}
}
}
#[test]
fn hunt_small_decimal_precision() {
let mut engine = Engine::new();
engine
.load(
r#"
spec precision
data tiny: number
data large: number
rule product: tiny * large
rule sum: tiny + tiny + tiny + tiny + tiny + tiny + tiny + tiny + tiny + tiny
"#,
src("precision.lemma"),
)
.unwrap();
let resp = run_spec(
&engine,
"precision",
&[("tiny", "0.0000001"), ("large", "10000000")],
);
assert_eq!(
rule_display(&resp, "product"),
"1",
"0.0000001 * 10000000 = 1 exactly"
);
let resp = run_spec(&engine, "precision", &[("tiny", "0.1"), ("large", "3")]);
let sum = rule_display(&resp, "sum");
assert_eq!(sum, "1", "0.1 added 10 times = 1 (exact rational)");
}
#[test]
fn hunt_name_collision_data_rule() {
let mut engine = Engine::new();
let result = engine.load(
r#"
spec collision
data value: 10
rule value: value * 2
"#,
src("collision.lemma"),
);
match result {
Ok(_) => {
panic!("Expected name collision between data 'value' and rule 'value' to be rejected");
}
Err(e) => {
let msg = format!("{:?}", e);
assert!(
msg.contains("value")
|| msg.contains("collision")
|| msg.contains("already")
|| msg.contains("duplicate"),
"Error should mention the conflicting name: {}",
msg
);
}
}
}
#[test]
fn hunt_circular_dependency() {
let mut engine = Engine::new();
let result = engine.load(
r#"
spec circular
data seed: 1
rule a: b + 1
rule b: a + 1
"#,
src("circular.lemma"),
);
match result {
Ok(_) => panic!("Circular dependency should be rejected at planning time"),
Err(e) => {
let msg = format!("{:?}", e);
assert!(
msg.contains("cycle") || msg.contains("circular") || msg.contains("depend"),
"Error should mention circular dependency: {}",
msg
);
}
}
}
#[test]
fn hunt_cross_unit_comparison_in_unless() {
let mut engine = Engine::new();
engine
.load(
r#"
spec cross_unit
uses lemma units
data weight: units.mass
rule category: "normal"
unless weight > 5 kilogram then "heavy"
unless weight < 100 gram then "tiny"
"#,
src("cross_unit.lemma"),
)
.unwrap();
let resp = run_spec(&engine, "cross_unit", &[("weight", "3 kilogram")]);
assert_eq!(rule_display(&resp, "category"), "normal");
let resp = run_spec(&engine, "cross_unit", &[("weight", "50 gram")]);
assert_eq!(rule_display(&resp, "category"), "tiny");
let resp = run_spec(&engine, "cross_unit", &[("weight", "10 kilogram")]);
assert_eq!(rule_display(&resp, "category"), "heavy");
}
fn main() {}