#![allow(
clippy::expect_used,
clippy::panic,
clippy::print_stderr,
clippy::unwrap_used
)]
#[path = "support/corpus_frontend.rs"]
mod corpus_frontend;
use std::process::Command;
use zerodds_ast::Ast;
use zerodds_lang_rules::adapter::interface::ServiceMapping;
use zerodds_lang_rules::rule::cpp;
use zerodds_lang_rules::{ModelGraph, apply};
#[path = "support/corpus.rs"]
mod corpus_harness;
fn try_generate(idl: &str, origin: &str) -> Result<String, String> {
try_generate_with(idl, origin, ServiceMapping::Basic)
}
fn try_generate_with(idl: &str, origin: &str, mapping: ServiceMapping) -> Result<String, String> {
let _native = zerodds_lang_rules::adapter::cpp::CppMappingScope::enter(
zerodds_lang_rules::adapter::cpp::CppMapping::Native,
);
let parsed = zerodds_idl_parse::parse(idl);
if !parsed.diagnostics().is_empty() {
return Err(format!("parse: {:?}", parsed.diagnostics()));
}
let lowered = zerodds_idl_lower::lower(&parsed.syntax(), origin);
if !lowered.diagnostics.is_empty() {
return Err(format!("lower: {:?}", lowered.diagnostics));
}
let mut ast: Ast = lowered.ast;
let report = ast
.resolve_all_by_name()
.map_err(|error| format!("resolve: {error:?}"))?;
if !report.is_clean() {
return Err(format!("resolve: broken references {:?}", report.broken));
}
let model = ModelGraph::of(&ast);
let applied = apply(&model, &cpp::catalogue()).map_err(|error| format!("apply: {error}"))?;
let mut decls = zerodds_lang_rules::adapter::cpp::cpp_items(&ast, &applied)
.map_err(|error| format!("materialise: {error}"))?;
decls.extend(
zerodds_lang_rules::emit::interface_cpp::cpp_interface_items_with(&ast, mapping)
.map_err(|error| format!("materialise: {error}"))?,
);
Ok(zerodds_lang_render::cpp::file(
&zerodds_lang_cst::cpp::File {
doc: zerodds_lang_cst::common::Doc::none(),
pragma_once: true,
includes: zerodds_lang_rules::adapter::cpp::includes(),
decls,
},
))
}
const MUST_RENDER: usize = 350;
const REFUSED: &[(&str, &str)] = &[];
const DOES_NOT_COMPILE: &[(&str, &str)] = &[];
fn compiler() -> Option<String> {
if let Ok(chosen) = std::env::var("CXX") {
return Some(chosen);
}
["c++", "g++", "clang++"]
.iter()
.find(|candidate| {
Command::new(candidate)
.arg("--version")
.output()
.is_ok_and(|out| out.status.success())
})
.map(|found| (*found).to_string())
}
fn cpp_compiles(cxx: &str, source: &str, tag: &str) -> Result<(), String> {
let dir = std::env::temp_dir().join(format!("zd-cpp-corpus-{}-{tag}", std::process::id()));
std::fs::create_dir_all(&dir).map_err(|error| error.to_string())?;
let header = dir.join("zd_types.hpp");
let unit = dir.join("tu.cpp");
std::fs::write(&header, source).map_err(|error| error.to_string())?;
std::fs::write(&unit, "#include \"zd_types.hpp\"\n").map_err(|error| error.to_string())?;
let runtime = corpus_harness::repository_root().join("runtime/cpp/include");
let output = Command::new(cxx)
.args(["-std=c++20", "-fsyntax-only", "-Wall", "-Wextra", "-Werror"])
.arg(format!("-I{}", dir.display()))
.arg(format!("-I{}", runtime.display()))
.arg(&unit)
.output()
.map_err(|error| error.to_string())?;
let result = if output.status.success() {
Ok(())
} else {
Err(String::from_utf8_lossy(&output.stderr).into_owned())
};
let _ = std::fs::remove_dir_all(&dir);
result
}
#[test]
fn the_compiler_check_catches_a_deliberate_error() {
let Some(cxx) = compiler() else {
eprintln!("SKIPPED: no C++ compiler on the path.");
return;
};
let bad = "#pragma once\nstruct S { NoSuchTypeExists x; };\n";
let why =
cpp_compiles(&cxx, bad, "deliberate").expect_err("the compiler accepted a bad header");
assert!(why.contains("NoSuchTypeExists"), "{why}");
cpp_compiles(&cxx, "#pragma once\nstruct S { int x; };\n", "control")
.expect("the compiler rejected a good header");
}
#[test]
#[ignore = "one C++ compiler invocation per corpus case, ~11 minutes; run with --include-ignored"]
fn every_corpus_case_that_renders_also_compiles() {
let Some(cxx) = compiler() else {
eprintln!("SKIPPED: no C++ compiler on the path.");
return;
};
let mut failures = Vec::new();
let mut unexpectedly_fine = Vec::new();
let mut frontend = Vec::new();
let mut checked = 0_usize;
for (name, path) in corpus_harness::corpus() {
let source = std::fs::read_to_string(&path).expect("a corpus source");
if !corpus_frontend::accepts(&source, &path.display().to_string()) {
continue;
}
let refused = REFUSED.iter().find(|(case, _)| *case == name);
let text = match (try_generate(&source, &path.display().to_string()), refused) {
(Ok(text), None) => text,
(Ok(_), Some((case, why))) => {
unexpectedly_fine.push(format!("{case}: renders now, but is listed as {why}"));
continue;
}
(Err(why), None) => {
if why.starts_with("parse:") || why.starts_with("lower:") {
frontend.push(format!("{name}: {why}"));
} else {
failures.push(format!("{name}: {why}"));
}
continue;
}
(Err(_), Some(_)) => continue,
};
checked += 1;
let known = DOES_NOT_COMPILE.iter().find(|(case, _)| *case == name);
match (cpp_compiles(&cxx, &text, &name), known) {
(Ok(()), None) | (Err(_), Some(_)) => {}
(Ok(()), Some((case, why))) => {
unexpectedly_fine.push(format!("{case}: compiles now, but is listed as {why}"));
}
(Err(why), None) => failures.push(format!("{name}: {why}")),
}
}
eprintln!(
"{checked} corpus case(s) reached C++; {} failed unexpectedly",
failures.len()
);
if !frontend.is_empty() {
eprintln!(
"{} corpus case(s) the frontend refuses, so C++ never saw them:\n{frontend:#?}",
frontend.len()
);
}
assert!(
failures.is_empty(),
"{} of {checked} did not compile:\n{failures:#?}",
failures.len()
);
assert!(unexpectedly_fine.is_empty(), "{unexpectedly_fine:#?}");
assert!(
checked >= MUST_RENDER,
"only {checked} corpus cases reached the compiler; the gate says nothing \
below {MUST_RENDER}"
);
}
#[test]
fn every_rpc_corpus_case_compiles() {
let Some(cxx) = compiler() else {
eprintln!("SKIPPED: no C++ compiler on the path.");
return;
};
let cases = corpus_harness::rpc_corpus();
assert!(
cases.len() >= 4,
"the RPC corpus has Basic and Enhanced cases: {}",
cases.len()
);
let mut failures = Vec::new();
for (name, path, mapping) in cases {
let source = std::fs::read_to_string(&path).expect("a corpus source");
match try_generate_with(&source, &path.display().to_string(), mapping) {
Err(why) => failures.push(format!("{name}: {why}")),
Ok(text) => {
if let Err(why) = cpp_compiles(&cxx, &text, &name) {
failures.push(format!("{name}: {why}"));
}
}
}
}
assert!(failures.is_empty(), "{failures:#?}");
}