use lemma::DateTimeValue;
use lemma::Engine;
use std::collections::HashMap;
fn rule_value(result: &lemma::Response, rule_name: &str) -> String {
let rr = result
.results
.get(rule_name)
.unwrap_or_else(|| panic!("rule '{}' not found", rule_name));
if rr.vetoed {
return format!("VETO({})", rr.veto_reason.as_deref().unwrap_or("Vetoed"));
}
rr.display.clone().expect("display")
}
fn load_err_joined(engine_res: Result<(), lemma::Errors>) -> String {
let err = engine_res.expect_err("expected load to fail");
err.iter()
.map(|e| e.to_string())
.collect::<Vec<_>>()
.join("\n")
}
#[test]
fn local_fill_literal_rejected_at_parse() {
let mut engine = Engine::new();
let joined = load_err_joined(engine.load(
r#"spec s
with x: 42"#,
lemma::SourceType::Path(std::sync::Arc::new(std::path::PathBuf::from(
"reference.lemma",
))),
));
assert!(
joined.contains("imported spec") || joined.contains("alias.field"),
"local with must be rejected at parse, got: {joined}"
);
}
#[test]
fn local_fill_import_reference_rejected_at_parse() {
let mut engine = Engine::new();
let joined = load_err_joined(engine.load(
r#"spec inner
data v: number
spec outer
uses i: inner
with copy: i.v"#,
lemma::SourceType::Path(std::sync::Arc::new(std::path::PathBuf::from(
"reference.lemma",
))),
));
assert!(
joined.contains("imported spec") || joined.contains("alias.field"),
"local with must be rejected at parse, got: {joined}"
);
}
#[test]
fn binding_reference_copies_cross_spec_target_value() {
let code = r#"
spec law
data other: number -> default 99
spec inner
uses l: law
data slot: number
spec top
uses lic: inner
uses lw: law
with lic.slot: lw.other
rule answer: lic.slot
"#;
let mut engine = Engine::new();
engine
.load(
code,
lemma::SourceType::Path(std::sync::Arc::new(std::path::PathBuf::from(
"reference.lemma",
))),
)
.unwrap();
let now = DateTimeValue::now();
let result = engine
.run(None, "top", Some(&now), HashMap::new(), false, None)
.expect("should run");
assert_eq!(rule_value(&result, "answer"), "99");
}
#[test]
fn closed_reference_cycle_is_rejected() {
let code = r#"
spec inner
data a: number
data b: number
spec outer
uses i: inner
with i.a: i.b
with i.b: i.a
"#;
let mut engine = Engine::new();
let joined = load_err_joined(engine.load(
code,
lemma::SourceType::Path(std::sync::Arc::new(std::path::PathBuf::from(
"reference.lemma",
))),
));
assert!(
joined.contains("Circular data reference"),
"closed reference cycle must be reported as a circular data reference, got: {joined}"
);
}
#[test]
fn self_referential_binding_reference_is_rejected() {
let code = r#"
spec inner
data x: number
spec outer
uses i: inner
with i.x: i.x
"#;
let mut engine = Engine::new();
let joined = load_err_joined(engine.load(
code,
lemma::SourceType::Path(std::sync::Arc::new(std::path::PathBuf::from(
"reference.lemma",
))),
));
assert!(
joined.contains("Circular data reference"),
"self-referential binding reference must be reported as a circular data reference, got: {joined}"
);
}
#[test]
fn binding_reference_target_type_incompatible_with_child_declared_type_is_rejected() {
let code = r#"
spec inner
data n: number
spec source_spec
data s: text -> default "hello"
spec outer
uses i: inner
uses src: source_spec
with i.n: src.s
rule r: i.n
"#;
let mut engine = Engine::new();
let joined = load_err_joined(engine.load(
code,
lemma::SourceType::Path(std::sync::Arc::new(std::path::PathBuf::from(
"reference.lemma",
))),
));
assert!(
joined.contains("type mismatch"),
"binding reference with target of a different base kind must be rejected, got: {joined}"
);
}
#[test]
fn rule_target_binding_reference_cycle_through_self_is_rejected() {
let code = r#"
spec inner
data slot: number
spec outer
uses i: inner
with i.slot: r
rule r: i.slot
"#;
let mut engine = Engine::new();
let joined = load_err_joined(engine.load(
code,
lemma::SourceType::Path(std::sync::Arc::new(std::path::PathBuf::from(
"reference.lemma",
))),
));
assert!(
joined.to_lowercase().contains("circular") || joined.to_lowercase().contains("cycle"),
"rule-target binding forming a cycle must be rejected, got: {joined}"
);
}
#[test]
fn rule_target_binding_reference_lhs_type_mismatch_is_rejected() {
let code = r#"
spec inner
data v: number
spec source_spec
rule greeting: "hello"
spec outer
uses i: inner
uses src: source_spec
with i.v: src.greeting
rule r: i.v
"#;
let mut engine = Engine::new();
let joined = load_err_joined(engine.load(
code,
lemma::SourceType::Path(std::sync::Arc::new(std::path::PathBuf::from(
"reference.lemma",
))),
));
assert!(
joined.contains("type mismatch"),
"rule-target binding with type kind mismatch must be rejected, got: {joined}"
);
}
#[test]
fn reference_value_violating_child_declared_max_is_rejected() {
let code = r#"
spec inner
data limited: number -> maximum 5
spec source_spec
data v: number -> default 10
spec outer
uses i: inner
uses src: source_spec
with i.limited: src.v
rule r: i.limited
"#;
let mut engine = Engine::new();
let load_result = engine.load(
code,
lemma::SourceType::Path(std::sync::Arc::new(std::path::PathBuf::from(
"reference.lemma",
))),
);
match load_result {
Ok(()) => {
let now = DateTimeValue::now();
let run_result = engine.run(None, "outer", Some(&now), HashMap::new(), false, None);
match run_result {
Ok(resp) => {
let rr = resp.results.get("r").expect("rule 'r'");
if rr.vetoed {
let s = rr.veto_reason.as_deref().expect("veto reason");
assert!(
s.contains("maximum") || s.contains("exceeds"),
"expected max-constraint veto, got: {s}"
);
} else {
panic!("expected constraint-violation veto; got {:?}", rr.display);
}
}
Err(err) => {
let s = err.to_string();
assert!(
s.contains("maximum") || s.contains("exceeds") || s.contains("constraint"),
"expected constraint error at run time, got: {s}"
);
}
}
}
Err(errors) => {
let joined = errors
.iter()
.map(|e| e.to_string())
.collect::<Vec<_>>()
.join("\n");
assert!(
joined.contains("maximum")
|| joined.contains("exceeds")
|| joined.contains("constraint"),
"expected constraint error at load time, got: {joined}"
);
}
}
}
#[test]
fn local_non_dotted_rhs_stays_definition() {
let code = r#"
spec s
data age: number -> default 30
data person: age
rule r: person
"#;
let mut engine = Engine::new();
engine
.load(
code,
lemma::SourceType::Path(std::sync::Arc::new(std::path::PathBuf::from(
"reference.lemma",
))),
)
.expect("loads: `data person: age` is a typedef reference, not a value-copy reference");
let now = DateTimeValue::now();
let result = engine
.run(None, "s", Some(&now), HashMap::new(), false, None)
.expect("evaluates; `person` is typed 'age' and inherits its default");
assert_eq!(rule_value(&result, "r"), "30");
}
#[test]
fn binding_non_dotted_rhs_resolves_in_enclosing_spec() {
let code = r#"
spec inner
data slot: number
spec outer
uses i: inner
data src: number -> default 123
with i.slot: src
rule r: i.slot
"#;
let mut engine = Engine::new();
engine
.load(
code,
lemma::SourceType::Path(std::sync::Arc::new(std::path::PathBuf::from(
"reference.lemma",
))),
)
.expect("non-dotted RHS in binding context must resolve in the enclosing spec");
let now = DateTimeValue::now();
let result = engine
.run(None, "outer", Some(&now), HashMap::new(), false, None)
.expect("evaluates");
assert_eq!(rule_value(&result, "r"), "123");
}
#[test]
fn binding_reference_quantity_family_mismatch_is_rejected() {
let code = r#"
spec inner
data money: quantity -> unit eur 1.00
data payment: money
spec source_spec
data temp_unit: quantity -> unit celsius 1.0
data temperature: temp_unit
spec outer
uses i: inner
uses src: source_spec
with i.payment: src.temperature
rule r: i.payment
"#;
let mut engine = Engine::new();
let res = engine.load(
code,
lemma::SourceType::Path(std::sync::Arc::new(std::path::PathBuf::from(
"reference.lemma",
))),
);
let joined = load_err_joined(res);
assert!(
joined.contains("quantity family")
|| joined.contains("quantity_family")
|| joined.contains("family")
|| joined.contains("type mismatch"),
"expected quantity-family-mismatch error, got: {joined}"
);
}