#![allow(
clippy::expect_used,
clippy::panic,
clippy::print_stderr,
clippy::unwrap_used
)]
#[path = "support/corpus_frontend.rs"]
mod corpus_frontend;
use std::path::{Path, PathBuf};
use std::process::Command;
use std::sync::LazyLock;
use zerodds_ast::Ast;
use zerodds_lang_rules::adapter::interface::ServiceMapping;
use zerodds_lang_rules::rule::java;
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 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, &java::catalogue()).map_err(|error| format!("apply: {error}"))?;
let mut types = zerodds_lang_rules::adapter::java::java_items(&ast, &applied)
.map_err(|error| format!("materialise: {error}"))?;
types.extend(
zerodds_lang_rules::emit::interface_java::java_interface_items_with(&ast, mapping)
.map_err(|error| format!("materialise: {error}"))?,
);
Ok(zerodds_lang_render::java::file(
&zerodds_lang_cst::java::File {
doc: zerodds_lang_cst::common::Doc::none(),
package: None,
imports: Vec::new(),
types,
},
))
}
const MUST_RENDER: usize = 350;
const REFUSED: &[(&str, &str)] = &[(
"resolve-module-named-as-type",
"a declaration named `common` inside a class of that name (JLS 8.1: \
a nested class may not share the simple name of an enclosing one)",
)];
const DOES_NOT_COMPILE: &[(&str, &str)] = &[];
fn javac_available() -> bool {
Command::new("javac")
.arg("-version")
.output()
.is_ok_and(|output| output.status.success())
}
static RUNTIME: LazyLock<Result<PathBuf, String>> = LazyLock::new(build_runtime);
fn runtime() -> Result<PathBuf, String> {
RUNTIME.clone()
}
fn build_runtime() -> Result<PathBuf, String> {
let classes = std::env::temp_dir().join(format!("zd-java-runtime-{}", std::process::id()));
std::fs::create_dir_all(&classes).map_err(|error| error.to_string())?;
let wire =
corpus_harness::repository_root().join("runtime/java/src/main/java/org/zerodds/wire");
let mut sources: Vec<PathBuf> = std::fs::read_dir(&wire)
.map_err(|error| error.to_string())?
.filter_map(|entry| entry.ok().map(|entry| entry.path()))
.filter(|path| path.extension().is_some_and(|ext| ext == "java"))
.collect();
sources.sort();
let output = Command::new("javac")
.args(["-encoding", "UTF-8"])
.arg("-d")
.arg(&classes)
.args(&sources)
.output()
.map_err(|error| error.to_string())?;
if output.status.success() {
Ok(classes)
} else {
Err(String::from_utf8_lossy(&output.stderr).into_owned())
}
}
fn java_compiles(runtime: &Path, source: &str, tag: &str) -> Result<(), String> {
let dir = std::env::temp_dir().join(format!("zd-java-corpus-{}-{tag}", std::process::id()));
let classes = dir.join("classes");
std::fs::create_dir_all(&classes).map_err(|error| error.to_string())?;
let units = zerodds_lang_rules::generate::java_units(source);
let mut files = Vec::new();
if units.is_empty() {
let file = dir.join("ZdTypes.java");
std::fs::write(&file, source).map_err(|error| error.to_string())?;
files.push(file);
}
for unit in &units {
let file = dir.join(format!("{}.java", unit.class));
std::fs::write(&file, &unit.text).map_err(|error| error.to_string())?;
files.push(file);
}
let output = Command::new("javac")
.args(["-encoding", "UTF-8"])
.arg("-cp")
.arg(runtime)
.arg("-d")
.arg(&classes)
.args(&files)
.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() {
if !javac_available() {
eprintln!("SKIPPED: `javac` is not usable.");
return;
}
let runtime = runtime().expect("runtime/java compiles");
let bad = "public final class ZdTypes { NoSuchTypeExists x; }\n";
let why = java_compiles(&runtime, bad, "deliberate").expect_err("javac accepted a bad unit");
assert!(why.contains("NoSuchTypeExists"), "{why}");
java_compiles(
&runtime,
"public final class ZdTypes { int x; org.zerodds.wire.Float128 wide; }\n",
"control",
)
.expect("javac rejected a good unit");
}
#[test]
#[ignore = "one `javac` start per corpus case; run with --include-ignored"]
fn every_corpus_case_that_renders_also_compiles() {
if !javac_available() {
eprintln!("SKIPPED: `javac` is not usable.");
return;
}
let runtime = runtime().expect("runtime/java compiles");
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 (java_compiles(&runtime, &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}")),
}
}
if !frontend.is_empty() {
eprintln!(
"{} corpus case(s) the frontend refuses, so Java 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 `javac`; the gate says nothing \
below {MUST_RENDER}"
);
}
fn rpc_case_compiles(
source: &str,
origin: &str,
mapping: ServiceMapping,
tag: &str,
) -> Result<(), String> {
let parsed = zerodds_idl_parse::parse(source);
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 text = zerodds_lang_rules::generate::source_with_mapping(
&ast,
zerodds_lang_cst::Language::Java,
zerodds_lang_rules::emit::interface_rust::rpc_layout_key(),
zerodds_lang_rules::emit::encode::CodecOptions::default(),
mapping,
)
.map_err(|error| format!("generate: {error}"))?;
let root = corpus_harness::repository_root();
let dir = std::env::temp_dir().join(format!("zd-java-rpc-{}-{tag}", std::process::id()));
let classes = dir.join("classes");
std::fs::create_dir_all(&classes).map_err(|error| error.to_string())?;
let mut files = Vec::new();
for unit in zerodds_lang_rules::generate::java_units(&text) {
let body = if unit.class == "ZdCodec" {
format!("import org.zerodds.wire.*;\n\n{}", unit.text)
} else {
unit.text.clone()
};
let file = dir.join(format!("{}.java", unit.class));
std::fs::write(&file, body).map_err(|error| error.to_string())?;
files.push(file);
}
for package in [
"runtime/java/src/main/java/org/omg/dds/rpc",
"runtime/java/src/main/java/org/zerodds/ddsrpc",
] {
for entry in std::fs::read_dir(root.join(package)).map_err(|error| error.to_string())? {
let path = entry.map_err(|error| error.to_string())?.path();
if path.extension().is_some_and(|ext| ext == "java") {
files.push(path);
}
}
}
let output = Command::new("javac")
.args(["-encoding", "UTF-8", "-Xlint:all"])
.arg("-d")
.arg(&classes)
.arg("-sourcepath")
.arg(format!(
"{}:{}:{}",
root.join("runtime/java/src/main/java").display(),
root.join("crates/java-omgdds/java/src/main/java").display(),
dir.display()
))
.args(&files)
.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 every_rpc_corpus_case_compiles() {
if !javac_available() {
eprintln!("SKIPPED: `javac` is not usable.");
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");
if let Err(why) = rpc_case_compiles(&source, &path.display().to_string(), mapping, &name) {
failures.push(format!("{name}: {why}"));
}
}
assert!(failures.is_empty(), "{failures:#?}");
}