#![cfg(test)]
use tensorlogic_ir::{TLExpr, Term};
use super::collector::DiagnosticCollector;
use super::diagnostic::{Diagnostic, Severity, SourceSpan};
use super::strategy::{RecoveryAction, RecoveryStrategy};
use super::tolerant_compiler::{
compile_tolerant, compile_tolerant_with_strategy, PartialCompilationResult, TolerantCompiler,
};
fn good_expr(name: &str) -> TLExpr {
TLExpr::pred(name, vec![Term::var("x")])
}
fn malformed_expr() -> TLExpr {
TLExpr::exists(
"y",
"UnregisteredDomain",
TLExpr::pred("q", vec![Term::var("y")]),
)
}
#[test]
fn collector_collects_in_insertion_order() {
let c = DiagnosticCollector::new();
c.push(Diagnostic::error("first").with_expression_index(0));
c.push(Diagnostic::warning("second").with_expression_index(1));
c.push(Diagnostic::fatal("third").with_expression_index(2));
let snap = c.snapshot();
assert_eq!(snap.len(), 3);
assert_eq!(snap[0].message, "first");
assert_eq!(snap[1].message, "second");
assert_eq!(snap[2].message, "third");
}
#[test]
fn collector_severity_filtering() {
let c = DiagnosticCollector::new();
c.push(Diagnostic::info("i1"));
c.push(Diagnostic::warning("w1"));
c.push(Diagnostic::warning("w2"));
c.push(Diagnostic::error("e1"));
c.push(Diagnostic::fatal("f1"));
assert_eq!(c.of_severity(Severity::Info).len(), 1);
assert_eq!(c.of_severity(Severity::Warning).len(), 2);
assert_eq!(c.of_severity(Severity::Error).len(), 1);
assert_eq!(c.of_severity(Severity::Fatal).len(), 1);
let blocking = c.at_least(Severity::Error);
assert_eq!(blocking.len(), 2);
assert!(blocking.iter().all(Diagnostic::is_blocking));
assert!(c.has_blocking());
assert!(c.has_fatal());
}
#[test]
fn diagnostic_carries_location_and_index() {
let d = Diagnostic::error("bad")
.with_expression_index(4)
.with_location(SourceSpan::with_source(10, 20, "file.tl"));
assert_eq!(d.expression_index, Some(4));
let loc = d.location.as_ref().expect("location present");
assert_eq!(loc.start, 10);
assert_eq!(loc.end, 20);
assert_eq!(loc.source.as_deref(), Some("file.tl"));
}
#[test]
fn compile_tolerant_all_good_yields_all_some() {
let program = vec![good_expr("a"), good_expr("b"), good_expr("c")];
let res = compile_tolerant(&program);
assert_eq!(res.graphs.len(), 3);
assert!(res.is_all_success());
assert_eq!(res.failure_count(), 0);
assert!(res.diagnostics.is_empty());
assert!(!res.aborted);
}
#[test]
fn compile_tolerant_single_bad_in_position_2_of_5() {
let program = vec![
good_expr("a"),
good_expr("b"),
malformed_expr(), good_expr("d"),
good_expr("e"),
];
let res = compile_tolerant(&program);
assert_eq!(res.graphs.len(), 5);
assert!(res.graphs[0].is_some(), "expr #0 should compile");
assert!(res.graphs[1].is_some(), "expr #1 should compile");
assert!(res.graphs[2].is_none(), "expr #2 should fail");
assert!(res.graphs[3].is_some(), "expr #3 should compile");
assert!(res.graphs[4].is_some(), "expr #4 should compile");
assert_eq!(res.success_count(), 4);
assert_eq!(res.failure_count(), 1);
let errors = res.diagnostics.errors();
assert_eq!(errors.len(), 1, "exactly one Error diagnostic");
assert_eq!(errors[0].expression_index, Some(2));
assert_eq!(res.diagnostics.fatals().len(), 0);
assert!(!res.aborted);
}
#[test]
fn tolerant_driver_catches_panic_and_converts_to_fatal() {
use std::panic::{self, AssertUnwindSafe};
use crate::error_recovery::collector::DiagnosticCollector;
use crate::error_recovery::diagnostic::{Diagnostic, Severity};
let old_hook = panic::take_hook();
panic::set_hook(Box::new(|_info| {}));
let collector = DiagnosticCollector::new();
let unwind = panic::catch_unwind(AssertUnwindSafe(|| -> anyhow::Result<()> {
panic!("synthetic panic for testing");
}));
panic::set_hook(old_hook);
match unwind {
Ok(_) => panic!("expected synthetic panic, got Ok(..)"),
Err(payload) => {
let msg = if let Some(s) = payload.downcast_ref::<&'static str>() {
(*s).to_string()
} else if let Some(s) = payload.downcast_ref::<String>() {
s.clone()
} else {
"<non-string panic payload>".to_string()
};
collector.push(
Diagnostic::fatal(format!("panic while compiling expression #0: {}", msg))
.with_expression_index(0),
);
}
}
assert_eq!(collector.fatals().len(), 1);
assert_eq!(collector.count_of(Severity::Fatal), 1);
}
#[test]
fn abort_on_any_aborts_on_first_error() {
let program = vec![
good_expr("a"),
malformed_expr(), good_expr("c"),
good_expr("d"),
];
let res = compile_tolerant_with_strategy(&program, RecoveryStrategy::AbortOnAny);
assert_eq!(res.graphs.len(), 4);
assert!(res.graphs[0].is_some());
assert!(res.graphs[1].is_none());
assert!(res.graphs[2].is_none(), "aborted: should not compile");
assert!(res.graphs[3].is_none(), "aborted: should not compile");
assert!(res.aborted);
assert_eq!(res.aborted_at, Some(1));
assert_eq!(res.diagnostics.errors().len(), 1);
}
#[test]
fn skip_on_fatal_reports_errors_but_aborts_on_fatal() {
let s = RecoveryStrategy::SkipOnFatal;
assert_eq!(s.decide(Severity::Error), RecoveryAction::SkipExpression);
assert_eq!(s.decide(Severity::Fatal), RecoveryAction::AbortProgram);
let program = vec![good_expr("a"), malformed_expr(), good_expr("c")];
let res = compile_tolerant_with_strategy(&program, RecoveryStrategy::SkipOnFatal);
assert_eq!(res.graphs.len(), 3);
assert!(res.graphs[0].is_some());
assert!(res.graphs[1].is_none());
assert!(
res.graphs[2].is_some(),
"SkipOnFatal must still compile sibling after Error"
);
assert!(!res.aborted, "Error must not abort under SkipOnFatal");
assert_eq!(res.diagnostics.errors().len(), 1);
assert_eq!(res.diagnostics.fatals().len(), 0);
}
#[test]
fn tolerant_compiler_strategy_accessors() {
let mut c = TolerantCompiler::new();
assert_eq!(c.strategy(), RecoveryStrategy::SkipOnError);
c.set_strategy(RecoveryStrategy::AbortOnAny);
assert_eq!(c.strategy(), RecoveryStrategy::AbortOnAny);
}
#[test]
fn partial_result_helpers() {
let program = vec![good_expr("a"), malformed_expr(), good_expr("c")];
let res: PartialCompilationResult = compile_tolerant(&program);
assert_eq!(res.success_count(), 2);
assert_eq!(res.failure_count(), 1);
assert_eq!(res.failures(), vec![1]);
let idxs: Vec<usize> = res.successes().map(|(i, _)| i).collect();
assert_eq!(idxs, vec![0, 2]);
}
#[test]
fn empty_program_yields_empty_result() {
let program: Vec<TLExpr> = vec![];
let res = compile_tolerant(&program);
assert!(res.graphs.is_empty());
assert!(res.diagnostics.is_empty());
assert!(!res.aborted);
assert!(res.is_all_success());
}