#![allow(clippy::unwrap_used)]
use std::process::Command;
fn rustdl() -> Command {
Command::new(env!("CARGO_BIN_EXE_rustdl"))
}
fn verified_fixture() -> &'static str {
concat!(
env!("CARGO_MANIFEST_DIR"),
"/../owl-dl-verify/tests/fixtures/unsatconj.ofn"
)
}
fn violated_fixture() -> &'static str {
concat!(
env!("CARGO_MANIFEST_DIR"),
"/../owl-dl-verify/tests/fixtures/conjexists-builder-gap.ofn"
)
}
fn build_refused_fixture() -> &'static str {
concat!(
env!("CARGO_MANIFEST_DIR"),
"/../owl-dl-verify/tests/fixtures/chainrange.ofn"
)
}
fn cascade_fixture() -> &'static str {
concat!(
env!("CARGO_MANIFEST_DIR"),
"/../owl-dl-verify/tests/fixtures/cascade.ofn"
)
}
fn residue_fixture() -> &'static str {
concat!(
env!("CARGO_MANIFEST_DIR"),
"/../owl-dl-verify/tests/fixtures/topwitness.ofn"
)
}
fn dropped_axiom_fixture() -> &'static str {
concat!(
env!("CARGO_MANIFEST_DIR"),
"/tests/fixtures/dropped-haskey.ofn"
)
}
fn pizza_path() -> std::path::PathBuf {
std::path::PathBuf::from(concat!(
env!("CARGO_MANIFEST_DIR"),
"/../../ontologies/real/pizza.ofn"
))
}
#[test]
fn verified_exits_zero() {
let out = rustdl()
.arg("verify-el")
.arg(verified_fixture())
.output()
.unwrap();
assert_eq!(
out.status.code(),
Some(0),
"stderr: {}",
String::from_utf8_lossy(&out.stderr)
);
let stdout = String::from_utf8_lossy(&out.stdout);
assert!(
stdout.starts_with("verified:"),
"expected a verified: line, got {stdout:?}"
);
}
#[test]
fn violated_exits_two() {
let out = rustdl()
.arg("verify-el")
.arg(violated_fixture())
.output()
.unwrap();
assert_eq!(
out.status.code(),
Some(2),
"stderr: {}",
String::from_utf8_lossy(&out.stderr)
);
let stdout = String::from_utf8_lossy(&out.stdout);
assert!(
stdout.starts_with("violated:"),
"expected a violated: line, got {stdout:?}"
);
}
#[test]
fn build_refusal_is_unresolved_and_exits_three() {
let out = rustdl()
.arg("verify-el")
.arg(build_refused_fixture())
.output()
.unwrap();
assert_eq!(
out.status.code(),
Some(3),
"stderr: {}",
String::from_utf8_lossy(&out.stderr)
);
let stdout = String::from_utf8_lossy(&out.stdout);
assert!(
stdout.starts_with("unresolved:"),
"expected an unresolved: line, got {stdout:?}"
);
assert!(
stdout.contains("ChainRangeOutOfProfile"),
"the build-time refusal reason must be surfaced, not swallowed: {stdout:?}"
);
}
#[test]
fn dropped_axioms_downgrade_a_verified_check_to_unresolved_and_exit_three() {
let out = rustdl()
.arg("verify-el")
.arg(dropped_axiom_fixture())
.output()
.unwrap();
assert_eq!(
out.status.code(),
Some(3),
"stderr: {}",
String::from_utf8_lossy(&out.stderr)
);
let stdout = String::from_utf8_lossy(&out.stdout);
assert!(
stdout.starts_with("unresolved:"),
"expected an unresolved: line, got {stdout:?}"
);
assert!(
stdout.contains("AxiomsDroppedAtConversion"),
"the dropped-axiom reason must be surfaced, not swallowed: {stdout:?}"
);
let stderr = String::from_utf8_lossy(&out.stderr);
assert!(
stderr.contains("HasKey"),
"warn_if_dropped's own warning must still fire: {stderr:?}"
);
}
#[test]
fn dropped_axioms_downgrade_is_visible_in_json_too() {
let out = rustdl()
.arg("verify-el")
.arg("--json")
.arg(dropped_axiom_fixture())
.output()
.unwrap();
assert_eq!(
out.status.code(),
Some(3),
"stderr: {}",
String::from_utf8_lossy(&out.stderr)
);
let stdout = String::from_utf8_lossy(&out.stdout);
let json: serde_json::Value = serde_json::from_str(&stdout).expect("valid JSON on stdout");
assert_eq!(json["verdict"], "unresolved");
assert_eq!(json["dropped"]["HasKey: unsupported axiom (HasKey)"], 1);
}
#[test]
fn out_of_fragment_ontology_is_unresolved_and_exits_three() {
let path = pizza_path();
if !path.exists() {
eprintln!("SKIP: missing corpus fixture {}", path.display());
return;
}
let out = rustdl().arg("verify-el").arg(&path).output().unwrap();
assert_eq!(
out.status.code(),
Some(3),
"stderr: {}",
String::from_utf8_lossy(&out.stderr)
);
let stdout = String::from_utf8_lossy(&out.stdout);
assert!(
stdout.starts_with("unresolved:"),
"expected an unresolved: line, got {stdout:?}"
);
assert!(
stdout.contains("not pure-EL"),
"the fragment-gate reason must be surfaced: {stdout:?}"
);
}
#[test]
fn missing_file_is_an_io_error_and_exits_one() {
let out = rustdl()
.arg("verify-el")
.arg("/does/not/exist/nowhere.ofn")
.output()
.unwrap();
assert_eq!(
out.status.code(),
Some(1),
"stdout: {}",
String::from_utf8_lossy(&out.stdout)
);
assert!(
!out.stderr.is_empty(),
"an I/O error must be reported on stderr"
);
}
#[test]
fn malformed_ontology_is_a_parse_error_and_exits_one() {
let dir = std::env::temp_dir();
let path = dir.join(format!("verify-el-malformed-{}.ofn", std::process::id()));
std::fs::write(&path, b"this is not valid OWL functional syntax (((").unwrap();
let out = rustdl().arg("verify-el").arg(&path).output().unwrap();
let _ = std::fs::remove_file(&path);
assert_eq!(
out.status.code(),
Some(1),
"stdout: {}",
String::from_utf8_lossy(&out.stdout)
);
assert!(
!out.stderr.is_empty(),
"a parse error must be reported on stderr"
);
}
#[test]
fn json_output_is_byte_identical_across_two_separate_process_runs() {
let run = || -> Vec<u8> {
rustdl()
.arg("verify-el")
.arg("--json")
.arg(cascade_fixture())
.output()
.unwrap()
.stdout
};
let first = run();
let second = run();
assert!(!first.is_empty(), "expected non-empty --json output");
assert_eq!(
first, second,
"verify-el --json must be byte-identical across separate process runs \
on the same input — see issue #59"
);
}
#[test]
fn text_output_is_byte_identical_across_two_separate_process_runs() {
let run = || -> Vec<u8> {
rustdl()
.arg("verify-el")
.arg(cascade_fixture())
.output()
.unwrap()
.stdout
};
let first = run();
let second = run();
assert!(!first.is_empty(), "expected non-empty text output");
assert_eq!(
first, second,
"verify-el's plain-text output must be byte-identical across separate \
process runs on the same input"
);
}
#[test]
fn json_violated_has_the_expected_shape() {
let out = rustdl()
.arg("verify-el")
.arg("--json")
.arg(violated_fixture())
.output()
.unwrap();
assert_eq!(out.status.code(), Some(2));
let v: serde_json::Value = serde_json::from_slice(&out.stdout).expect("valid JSON");
assert_eq!(v["schema_version"], 1);
assert_eq!(v["verdict"], "violated");
assert!(v["violations"].is_array());
assert!(!v["violations"].as_array().unwrap().is_empty());
assert!(v["unresolved"].is_array());
assert!(v["dropped"].is_object());
}
#[test]
fn build_reasons_downgrade_a_verified_check_to_unresolved_and_exit_three() {
let out = rustdl()
.arg("verify-el")
.arg(residue_fixture())
.output()
.unwrap();
assert_eq!(
out.status.code(),
Some(3),
"a Verified check paired with non-empty build_model reasons must be \
downgraded to Unresolved (exit 3), never reported as exit 0 \
(Verified) or exit 2 (Violated) — stdout: {}, stderr: {}",
String::from_utf8_lossy(&out.stdout),
String::from_utf8_lossy(&out.stderr)
);
let stdout = String::from_utf8_lossy(&out.stdout);
assert!(
stdout.starts_with("unresolved:"),
"expected an unresolved: line, got {stdout:?}"
);
assert!(
stdout.contains("LabelNotClosed"),
"the build-time reason must be surfaced on the text path, not \
swallowed by the downgrade: {stdout:?}"
);
let json_out = rustdl()
.arg("verify-el")
.arg("--json")
.arg(residue_fixture())
.output()
.unwrap();
assert_eq!(json_out.status.code(), Some(3));
let v: serde_json::Value = serde_json::from_slice(&json_out.stdout).expect("valid JSON");
assert_eq!(v["verdict"], "unresolved");
assert_eq!(v["axioms_checked"], 0);
let unresolved = v["unresolved"].as_array().expect("unresolved array");
assert!(
!unresolved.is_empty(),
"the build reason must be surfaced on the --json path too: {v:?}"
);
assert!(
unresolved
.iter()
.any(|r| r.as_str().unwrap_or("").contains("LabelNotClosed")),
"expected a LabelNotClosed entry in unresolved: {unresolved:?}"
);
}