use elenchus_parser::{Diagnostics, KEYWORDS, card_for, parse};
fn diags(src: &str) -> Diagnostics {
match parse(src) {
Ok(_) => panic!("expected a parse error, but it parsed:\n{src}"),
Err(d) => d,
}
}
fn err(src: &str) -> String {
diags(src).render(None, None)
}
#[test]
fn fact_missing_predicate() {
insta::assert_snapshot!(err("FACT lonely\n"));
}
#[test]
fn not_missing_predicate() {
insta::assert_snapshot!(err("NOT lonely\n"));
}
#[test]
fn assume_missing_predicate() {
insta::assert_snapshot!(err("ASSUME lonely\n"));
}
#[test]
fn import_unterminated_string() {
insta::assert_snapshot!(err("IMPORT \"physics.vrf\n"));
}
#[test]
fn premise_missing_colon() {
insta::assert_snapshot!(err(r#"
PREMISE modes
EXCLUSIVE
a b
a c
"#));
}
#[test]
fn rule_missing_colon() {
insta::assert_snapshot!(err(r#"
RULE r
WHEN x a
THEN x b
"#));
}
#[test]
fn premise_implication_missing_then() {
insta::assert_snapshot!(err(r#"
PREMISE wings_need_bone:
WHEN Creature.A has flying
CHECK Creature.A
"#));
}
#[test]
fn then_without_literal() {
insta::assert_snapshot!(err(r#"
RULE r:
WHEN x a
THEN
"#));
}
#[test]
fn and_literal_missing() {
insta::assert_snapshot!(err(r#"
PREMISE g:
WHEN x a
AND
THEN x b
"#));
}
#[test]
fn list_premise_needs_two_atoms() {
insta::assert_snapshot!(err(r#"
PREMISE modes:
EXCLUSIVE
Sys mode idle
CHECK Sys
"#));
}
#[test]
fn rule_body_not_an_implication() {
insta::assert_snapshot!(err(r#"
RULE r:
EXCLUSIVE
x a
x b
"#));
}
#[test]
fn reserved_word_as_subject() {
insta::assert_snapshot!(err("FACT WHEN has flying\n"));
}
#[test]
fn trailing_text_after_fact_atom() {
insta::assert_snapshot!(err("FACT a b c d\n"));
}
#[test]
fn garbage_top_level_line() {
insta::assert_snapshot!(err(r#"
FACT a b
%%% not a statement
FACT c d
"#));
}
const BROKEN: &str = "\
FACT lonely
FACT a b
NOT also_lonely
CHECK
PREMISE p:
WHEN x y
THEN model uses too many words
";
#[test]
fn reports_every_error_in_one_pass() {
insta::assert_snapshot!(err(BROKEN));
}
const REPEATED: &str = "\
FACT one
FACT two
FACT three
NOT four
";
#[test]
fn max_per_class_caps_places_within_a_class() {
insta::assert_snapshot!(diags(REPEATED).render(None, Some(2)));
}
#[test]
fn max_classes_caps_the_number_of_classes() {
insta::assert_snapshot!(diags(REPEATED).render(Some(1), None));
}
#[test]
fn recovery_yields_exactly_one_error_per_broken_statement() {
assert_eq!(diags(BROKEN).len(), 3);
}
#[test]
fn recovery_does_not_swallow_following_valid_statements() {
let src = "FACT lonely\nFACT good one\n";
assert_eq!(diags(src).len(), 1);
}
#[test]
fn many_errors_are_grouped_without_panicking() {
let mut src = String::new();
for i in 0..200 {
src.push_str(&format!("FACT lonely{i}\n"));
}
let d = diags(&src);
assert_eq!(d.len(), 200);
let shown = d.render(None, Some(3));
assert!(shown.contains("FACT (200 problems)"), "{shown}");
assert!(shown.contains("... and 197 more FACT problems"), "{shown}");
}
#[test]
fn the_extension_plan_example_is_valid() {
let src = include_str!("../../../docs/examples/extension-plan.vrf");
assert!(parse(src).is_ok(), "the example should parse cleanly");
}
#[test]
fn showcase_every_failure_mode() {
insta::assert_snapshot!(err(include_str!("fixtures/showcase.vrf")));
}
#[test]
fn every_reserved_word_has_a_complete_card() {
for k in KEYWORDS {
let card = card_for(k.text).unwrap_or_else(|| panic!("no syntax card for {}", k.text));
assert!(!card.form.is_empty(), "{}: empty form", k.text);
assert!(!card.gloss.is_empty(), "{}: empty gloss", k.text);
assert!(!card.example.is_empty(), "{}: empty example", k.text);
assert!(
card.form.contains(k.text),
"{}: form must name the keyword",
k.text
);
}
}
#[test]
fn unknown_keyword_has_no_card() {
assert!(card_for("DEFINITELY_NOT_A_KEYWORD").is_none());
}
#[test]
fn top_level_card_examples_actually_parse() {
for kw in [
"FACT", "NOT", "ASSUME", "IMPORT", "CHECK", "PREMISE", "RULE",
] {
let example = card_for(kw).unwrap().example;
assert!(
parse(example).is_ok(),
"{kw} card example must parse:\n{example}"
);
}
}