mod harness;
use harness::{Compiled, compile};
use std::path::Path;
use std::process::Command;
use std::sync::OnceLock;
const PROG: &str = "\
:- dynamic(field/1).
color(red). color(blue).
loop :- loop.
go(X) :- missing(X).
ok(yes).
";
fn prog() -> &'static Compiled {
static C: OnceLock<Compiled> = OnceLock::new();
C.get_or_init(|| compile(PROG))
}
fn try_build(source: &str) -> (String, i32) {
let dir = tempfile::tempdir().expect("tempdir");
let src = dir.path().join("prog.pl");
std::fs::write(&src, source).expect("write source");
let bin = dir.path().join("prog");
let out = Command::new(env!("CARGO_BIN_EXE_plgc"))
.arg("build")
.arg(&src)
.arg("-o")
.arg(&bin)
.output()
.expect("run plgc");
(
String::from_utf8_lossy(&out.stderr).into_owned(),
out.status.code().unwrap_or(-1),
)
}
#[test]
fn undefined_predicate_is_existence_error() {
let (out, code) = prog().query("shape(X)", &[]);
assert!(out.contains("existence_error"), "{out}");
assert_eq!(code, 3);
let c = compile("just_a_fact.\n");
let (out, code) = c.query("foo(X)", &[]);
assert!(out.contains("existence_error"), "{out}");
assert_eq!(code, 3);
}
#[test]
fn dynamic_predicate_silently_fails() {
let (out, code) = prog().query("field(X)", &[]);
assert_eq!(out, "{\"count\":0,\"exhausted\":true,\"solutions\":[]}\n");
assert_eq!(code, 0);
}
#[test]
fn ground_index_miss_fails() {
let (out, code) = prog().query("color(purple)", &[]);
assert_eq!(out, "{\"count\":0,\"exhausted\":true,\"solutions\":[]}\n");
assert_eq!(code, 0);
}
#[test]
fn undefined_in_rule_body_raises_when_reached() {
let (out, code) = prog().query("go(X)", &[]);
assert!(
out.contains("existence_error(procedure, /(missing, 1))"),
"{out}"
);
assert_eq!(code, 3);
let (_, code) = prog().query("ok(X)", &[]);
assert_eq!(code, 1);
}
#[test]
fn step_limit_is_uncatchable() {
let (out, code) = prog().query("loop", &[]);
assert!(out.contains("resource_error(steps)"), "{out}");
assert!(out.contains("Maximum step limit exceeded"), "{out}");
assert_eq!(code, 3);
let (out, code) = prog().query("catch(loop, _, true)", &[]);
assert!(out.contains("resource_error(steps)"), "{out}");
assert_eq!(code, 3);
let (out, code) = prog().query("\\+(loop)", &[]);
assert!(out.contains("resource_error(steps)"), "{out}");
assert_eq!(code, 3);
}
#[test]
fn query_parse_errors_exit_2() {
for q in [
"member(X,[1,2,3]) zzz",
"p(X) ]",
"p(X) . extra",
"p(X) trailing",
"X is 1, foo bar",
] {
let (out, code) = prog().query(q, &[]);
assert!(
out.starts_with("{\"error\":\"Parse error:"),
"query {q}: {out}"
);
assert_eq!(code, 2, "query: {q}");
}
}
#[test]
fn valid_queries_still_parse() {
for q in ["color(X)", "color(X)."] {
let (out, code) = prog().query(q, &[]);
assert!(out.contains("\"count\":2"), "query {q}: {out}");
assert_eq!(code, 1, "query: {q}");
}
}
#[test]
fn program_parse_errors_show_surface_lexemes() {
let (err, code) = try_build("p(]).\n");
assert!(err.contains("`]`"), "{err}");
assert!(!err.contains("RBracket"), "{err}");
assert_ne!(code, 0);
let (err, _) = try_build("p :- X is mod 3.\n");
assert!(err.contains("`mod`"), "{err}");
let (err, _) = try_build("foo bar.\n");
assert!(err.contains("atom `bar`"), "{err}");
let (err, _) = try_build("p(x\n");
assert!(err.contains("end of input"), "{err}");
assert!(!err.contains("Eof"), "{err}");
assert!(err.contains("`)`"), "{err}");
assert!(!err.contains("RParen"), "{err}");
let (_, code) = try_build("invalid(((.\n");
assert_ne!(code, 0);
}
#[test]
fn try_build_accepts_valid_program() {
let (err, code) = try_build("ok(yes).\n");
assert_eq!(code, 0, "stderr: {err}");
}
#[test]
fn plgc_binary_exists() {
assert!(Path::new(env!("CARGO_BIN_EXE_plgc")).exists());
}