mod support;
use std::process::Command;
use support::*;
fn python() -> Option<String> {
for candidate in ["python3", "python"] {
let ok = Command::new(candidate)
.arg("--version")
.output()
.map(|output| output.status.success())
.unwrap_or(false);
if ok {
return Some(candidate.to_string());
}
}
if std::env::var_os("INGOT_REQUIRE_PYTHON").is_some() {
panic!("INGOT_REQUIRE_PYTHON is set and neither `python3` nor `python` is on PATH");
}
eprintln!("skipping: no python3 on PATH");
None
}
fn suite() -> String {
repo_root()
.join("crates/ingot-conformance")
.display()
.to_string()
}
#[test]
fn the_reference_interpreter_conforms() {
let output = run(&["conform", "--suite", &suite()], None);
assert_eq!(
code(&output),
EXIT_OK,
"{}\n{}",
stdout(&output),
stderr(&output)
);
assert!(stdout(&output).contains("all pass"), "{}", stdout(&output));
}
#[test]
fn the_python_backend_conforms() {
let Some(python) = python() else { return };
let adapter = repo_root()
.join("crates/ingot-conformance/tools/python-adapter.py")
.display()
.to_string();
let backend = format!("{python} {adapter}");
let output = run(
&["conform", "--suite", &suite(), "--backend", &backend],
None,
);
assert_eq!(
code(&output),
EXIT_OK,
"{}\n{}",
stdout(&output),
stderr(&output)
);
assert!(stdout(&output).contains("all pass"), "{}", stdout(&output));
}
#[test]
fn a_backend_that_does_nothing_fails_every_case() {
let Some(python) = python() else { return };
let dir = TempDir::new("conform-idle");
let idle = dir.path().join("idle.py");
std::fs::write(&idle, "import sys\nsys.exit(0)\n").expect("writing the idle backend");
let backend = format!("{python} {}", idle.display());
let output = run(
&["conform", "--suite", &suite(), "--backend", &backend],
None,
);
assert_eq!(code(&output), EXIT_DIAGNOSTICS, "{}", stdout(&output));
let report = stdout(&output);
assert!(report.contains("case(s) fail"), "{report}");
assert!(report.contains("the stream ended early"), "{report}");
assert!(report.contains("pins Runtime"), "{report}");
}
#[test]
fn every_case_names_the_clauses_it_pins() {
let output = run(&["conform", "--suite", &suite(), "--list", "--json"], None);
assert_eq!(code(&output), EXIT_OK, "{}", stderr(&output));
let described: serde_json::Value =
serde_json::from_str(&stdout(&output)).expect("the listing must be JSON");
let cases = described["cases"].as_array().expect("a list of cases");
assert!(!cases.is_empty(), "the suite must have cases");
for case in cases {
let name = case["case"].as_str().unwrap_or_default();
assert!(
!case["what"].as_str().unwrap_or_default().is_empty(),
"{name} says nothing about what it is for"
);
let pins = case["pins"].as_array().expect("pins is a list");
assert!(!pins.is_empty(), "{name} pins no clause");
for pin in pins {
let text = pin.as_str().unwrap_or_default();
assert!(
text.contains('§'),
"{name} pins `{text}`, which names no section"
);
}
}
}
#[test]
fn an_adapter_refuses_a_contract_version_it_does_not_implement() {
let dir = TempDir::new("conform-future");
let request = dir.path().join("request.json");
std::fs::write(
&request,
serde_json::json!({
"conformance": "9.9",
"artifact": "nowhere.json",
"cassette": "nowhere.json",
"inputs": {},
"outDir": dir.path(),
})
.to_string(),
)
.expect("writing the request");
let output = run(
&["conform", "--adapter", &request.display().to_string()],
None,
);
assert_ne!(code(&output), EXIT_OK);
assert!(
stderr(&output).contains("refusing rather than guessing"),
"{}",
stderr(&output)
);
}
#[test]
fn the_built_in_suite_is_the_suite_in_the_tree() {
let dir = TempDir::new("suite-drift");
let exported = dir.path().join("suite");
let output = run(
&["conform", "--export", &exported.display().to_string()],
None,
);
assert_eq!(code(&output), EXIT_OK, "{}", stderr(&output));
let tree = repo_root().join("crates").join("ingot-conformance");
let mut differing = Vec::new();
let mut missing = Vec::new();
let mut stack = vec![tree.join("cases")];
let mut loose = vec![tree.join("README.md")];
while let Some(here) = stack.pop() {
for entry in std::fs::read_dir(&here)
.expect("reading the suite")
.flatten()
{
let path = entry.path();
if path.is_dir() {
stack.push(path);
continue;
}
loose.push(path);
}
}
let mut expected = Vec::new();
for path in loose {
let relative = path.strip_prefix(&tree).expect("under the suite");
expected.push(relative.display().to_string());
let copy = exported.join(relative);
if !copy.is_file() {
missing.push(relative.display().to_string());
continue;
}
if std::fs::read(&path).ok() != std::fs::read(©).ok() {
differing.push(relative.display().to_string());
}
}
assert!(missing.is_empty(), "not built into the binary: {missing:?}");
assert!(
differing.is_empty(),
"the built-in copy has drifted: {differing:?}"
);
let mut extra = Vec::new();
let mut stack = vec![exported.clone()];
while let Some(here) = stack.pop() {
for entry in std::fs::read_dir(&here)
.expect("reading the export")
.flatten()
{
let path = entry.path();
if path.is_dir() {
stack.push(path);
continue;
}
let relative = path
.strip_prefix(&exported)
.expect("under the export")
.display()
.to_string();
if !expected.contains(&relative) {
extra.push(relative);
}
}
}
assert!(
extra.is_empty(),
"built into the binary but no longer in the tree: {extra:?}"
);
}
#[test]
fn the_built_in_suite_runs_with_no_checkout_in_sight() {
let dir = TempDir::new("suite-standalone");
let output = run_in(dir.path(), &["conform", "--list"]);
assert_eq!(code(&output), EXIT_OK, "{}", stderr(&output));
assert!(stdout(&output).contains("prose"), "{}", stdout(&output));
}