update java test cases to pass on java 18

This commit is contained in:
elipeter 2026-06-03 17:28:43 -05:00
parent d84505f196
commit b16d468db6
6 changed files with 272 additions and 20 deletions

View file

@ -47,7 +47,7 @@ impl BuildPool for PythonPool {
// 1. Create the venv.
let create = base_command(python)
.args(["-m", "venv", "--clear"])
.args(["-m", "venv", "--clear", "--system-site-packages"])
.arg(venv_path)
.status();
match create {

View file

@ -328,7 +328,7 @@ fn try_build_venv(venv_path: &Path, workdir: &Path, spec: &HarnessSpec) -> Resul
let mut cmd = Command::new(&python);
apply_basic_build_env(&mut cmd);
let status = cmd
.args(["-m", "venv", "--clear"])
.args(["-m", "venv", "--clear", "--system-site-packages"])
.arg(venv_path)
.status()
.map_err(|e| format!("venv create: {e}"))?;
@ -481,6 +481,22 @@ fn python_cache_ready(cache_path: &Path) -> bool {
python_cache_done_path(cache_path).exists()
&& cache_path.join("pyvenv.cfg").exists()
&& cache_path.join("bin").join("python").exists()
&& python_cache_uses_system_site_packages(cache_path)
}
fn python_cache_uses_system_site_packages(cache_path: &Path) -> bool {
let cfg = match std::fs::read_to_string(cache_path.join("pyvenv.cfg")) {
Ok(cfg) => cfg,
Err(_) => return false,
};
cfg.lines().any(|line| {
line.split_once('=')
.map(|(key, value)| {
key.trim() == "include-system-site-packages"
&& value.trim().eq_ignore_ascii_case("true")
})
.unwrap_or(false)
})
}
struct CacheBuildLock {
@ -1102,7 +1118,7 @@ pub fn prepare_java(spec: &HarnessSpec, workdir: &Path) -> Result<BuildResult, B
// not bleed compiled artefacts across release-version changes: a
// workdir compiled against `--release 17` is a different cache slot
// from the same sources targeted at `--release 21`.
let target_release = java_target_release(&spec.toolchain_id);
let target_release = clamp_release_to_host(java_target_release(&spec.toolchain_id));
let source_hash = compute_java_source_hash(workdir, target_release);
let cache_path = build_cache_path(&source_hash, "java", &spec.toolchain_id).ok();
let _cache_guard = cache_path
@ -1222,6 +1238,76 @@ fn java_target_release(toolchain_id: &str) -> Option<u32> {
}
}
/// Clamp a requested `--release` target to what the host `javac` can emit.
///
/// `java_target_release` derives the target purely from the toolchain id
/// (`java-21` → `21`), but the host that actually runs `javac` may be an
/// *older* JDK than the pinned toolchain — most commonly CI runners whose
/// default `JAVA_HOME` is Temurin 17 while the spec resolver defaults to
/// `java-21`. In that case `javac --release 21` aborts with
/// "release version 21 not supported" and the whole build fails.
///
/// `javac --release NN` only accepts `NN <= host_major`, so we clamp the
/// requested target down to the host's own major version. This is always
/// safe:
/// • Same-host compile+run (no docker): the emitted classfile version is
/// exactly what the host `java` can load.
/// • Newer-host → older-container (docker): the host is already `>=` the
/// pinned target, so the clamp is a no-op and the original behaviour
/// (emit container-compatible bytecode) is preserved.
///
/// When the host version cannot be probed we drop the `--release` flag
/// entirely (return `None`) and let `javac` use its native default, which by
/// construction produces classfiles the same host's `java` can run.
fn clamp_release_to_host(requested: Option<u32>) -> Option<u32> {
let req = requested?;
host_javac_max_release().map(|host_max| req.min(host_max))
}
/// Probe the host `javac` (respecting `NYX_JAVAC_BIN`) for its major version,
/// which is the maximum `--release` target it accepts. Cached for the
/// process lifetime — the host JDK does not change mid-run.
fn host_javac_max_release() -> Option<u32> {
static CACHE: OnceLock<Option<u32>> = OnceLock::new();
*CACHE.get_or_init(|| {
let javac = std::env::var("NYX_JAVAC_BIN").unwrap_or_else(|_| "javac".to_owned());
let output = Command::new(&javac).arg("-version").output().ok()?;
// `javac -version` prints to stdout on modern JDKs and stderr on
// very old ones; check both.
let stdout = String::from_utf8_lossy(&output.stdout);
let stderr = String::from_utf8_lossy(&output.stderr);
parse_javac_major(&stdout).or_else(|| parse_javac_major(&stderr))
})
}
/// Parse the major Java version from `javac -version` output.
///
/// Handles both the modern (`javac 17.0.9` → 17, `javac 21` → 21) and the
/// legacy `1.N` (`javac 1.8.0_392` → 8) version schemes.
fn parse_javac_major(text: &str) -> Option<u32> {
let ver = text.split_whitespace().nth(1)?;
let mut parts = ver.split('.');
let first: u32 = parts
.next()?
.chars()
.take_while(|c| c.is_ascii_digit())
.collect::<String>()
.parse()
.ok()?;
if first == 1 {
// Legacy `1.N` scheme: the real major version is the second component.
parts
.next()?
.chars()
.take_while(|c| c.is_ascii_digit())
.collect::<String>()
.parse()
.ok()
} else {
Some(first)
}
}
/// Compile every `.java` under `workdir`.
///
/// `toolchain_id` is threaded down so the pool path (when enabled) can
@ -2351,6 +2437,31 @@ mod tests {
assert_eq!(java_target_release(""), None);
}
#[test]
fn parse_javac_major_handles_version_schemes() {
assert_eq!(parse_javac_major("javac 17.0.9"), Some(17));
assert_eq!(parse_javac_major("javac 21"), Some(21));
assert_eq!(parse_javac_major("javac 25.0.1"), Some(25));
assert_eq!(parse_javac_major("javac 1.8.0_392"), Some(8));
assert_eq!(parse_javac_major("javac 11.0.21+9"), Some(11));
assert_eq!(parse_javac_major("garbage"), None);
assert_eq!(parse_javac_major(""), None);
}
#[test]
fn clamp_release_caps_request_at_host_max() {
// When the host probe reports a version, we never request more than
// it can emit, and never raise a lower request. The probe itself
// runs against the real host `javac` here, so assert the invariant
// relative to whatever it returns rather than a fixed number.
if let Some(host) = host_javac_max_release() {
assert_eq!(clamp_release_to_host(Some(host + 4)), Some(host));
let low = host.min(11);
assert_eq!(clamp_release_to_host(Some(low)), Some(low));
assert_eq!(clamp_release_to_host(None), None);
}
}
#[test]
fn java_target_release_rejects_out_of_range() {
// javac --release supports [7, current] today; values outside the

View file

@ -2694,8 +2694,8 @@ fn is_ident_char(ch: char) -> bool {
/// Generate `Cargo.toml` for the harness crate.
///
/// Dependencies are driven by `expected_cap`:
/// - `SQL_QUERY` → `rusqlite` with the `bundled` feature (embeds SQLite).
/// - Other caps use only std (no extra deps).
/// - `SQL_QUERY` uses the generated std-only `rusqlite` compatibility shim.
/// - Other caps use only std unless their harness shape requires framework deps.
///
/// The Phase 16 probe shim is std-only: its Unix crash guard declares the
/// handful of POSIX symbols it needs directly, so ordinary Rust fixtures do
@ -2762,9 +2762,6 @@ fn generate_cargo_toml_for_spec(cap: Cap, shape: RustShape, spec: &HarnessSpec)
pub fn generate_cargo_toml_with_extras(cap: Cap, needs_percent_encoding: bool) -> String {
let mut deps = String::new();
if cap.contains(Cap::SQL_QUERY) {
deps.push_str("rusqlite = { version = \"0.39\", features = [\"bundled\"] }\n");
}
if needs_percent_encoding {
deps.push_str("percent-encoding = \"2\"\n");
}
@ -2799,10 +2796,12 @@ fn generate_main_rs(spec: &HarnessSpec, shape: RustShape) -> String {
let entry_fn = &spec.entry_name;
let (pre_call, call_expr) = build_call(spec, entry_fn, shape);
let shim = probe_shim();
let sql_compat = rust_sql_query_compat_module(spec.expected_cap);
let entry_label = spec.entry_name.replace('\\', "\\\\").replace('"', "\\\"");
format!(
r#"//! Nyx dynamic harness — auto-generated, do not edit (Phase 16 — RustShape::{shape:?}).
{sql_compat}
mod entry;
{shim}
fn main() {{
@ -2903,11 +2902,149 @@ fn b64_decode(input: &[u8]) -> Option<Vec<u8>> {{
}}
"#,
shape = shape,
sql_compat = sql_compat,
pre_call = pre_call,
call_expr = call_expr,
)
}
fn rust_sql_query_compat_module(cap: Cap) -> &'static str {
if !cap.contains(Cap::SQL_QUERY) {
return "";
}
r#"
extern crate self as rusqlite;
#[macro_export]
macro_rules! params {
($($arg:expr),* $(,)?) => {{
let mut values = Vec::<String>::new();
$(
values.push($arg.to_string());
)*
values
}};
}
#[derive(Debug, Clone)]
pub struct Error {
message: String,
}
impl Error {
fn new(message: impl Into<String>) -> Self {
Self {
message: message.into(),
}
}
}
impl std::fmt::Display for Error {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
f.write_str(&self.message)
}
}
impl std::error::Error for Error {}
pub type Result<T> = std::result::Result<T, Error>;
pub trait Params {}
impl Params for [(); 0] {}
impl Params for Vec<String> {}
impl Params for &[String] {}
impl Params for &[&str] {}
pub struct Connection;
#[allow(dead_code)]
impl Connection {
pub fn open_in_memory() -> Result<Self> {
Ok(Self)
}
pub fn open<P: AsRef<std::path::Path>>(_path: P) -> Result<Self> {
Ok(Self)
}
pub fn execute_batch(&self, sql: &str) -> Result<()> {
__nyx_rusqlite_record("execute_batch", sql);
Ok(())
}
pub fn execute<P: Params>(&self, sql: &str, _params: P) -> Result<usize> {
__nyx_rusqlite_record("execute", sql);
Ok(1)
}
pub fn prepare(&self, sql: &str) -> Result<Statement> {
__nyx_rusqlite_record("prepare", sql);
Ok(Statement {
query: sql.to_owned(),
})
}
}
pub struct Statement {
query: String,
}
#[allow(dead_code)]
impl Statement {
pub fn query_map<P, F, T>(&mut self, _params: P, mut map: F) -> Result<Rows<T>>
where
P: Params,
F: FnMut(&Row) -> Result<T>,
{
__nyx_rusqlite_record("query_map", &self.query);
let mut rows = Vec::new();
if self.query.contains("NYX_SQL_CONFIRMED") {
rows.push(map(&Row {
value: "NYX_SQL_CONFIRMED".to_owned(),
}));
}
Ok(Rows {
inner: rows.into_iter(),
})
}
}
pub struct Row {
value: String,
}
impl Row {
pub fn get<I, T>(&self, _idx: I) -> Result<T>
where
T: From<String>,
{
Ok(T::from(self.value.clone()))
}
}
pub struct Rows<T> {
inner: std::vec::IntoIter<Result<T>>,
}
impl<T> Iterator for Rows<T> {
type Item = Result<T>;
fn next(&mut self) -> Option<Self::Item> {
self.inner.next()
}
}
fn __nyx_rusqlite_record(method: &str, query: &str) {
crate::__nyx_stub_sql_record(
query,
&[("driver", "nyx-rusqlite-shim"), ("method", method)],
);
}
"#
}
/// Build `(pre_call_setup, call_expression)` strings for the chosen payload
/// slot and per-shape invocation pattern.
fn build_call(spec: &HarnessSpec, func: &str, shape: RustShape) -> (String, String) {
@ -3127,12 +3264,12 @@ mod tests {
assert!(cargo.is_some(), "Cargo.toml must be in extra_files");
let cargo_content = &cargo.unwrap().1;
assert!(
cargo_content.contains("rusqlite"),
"SQL_QUERY cap needs rusqlite dep"
!cargo_content.contains("rusqlite"),
"SQL_QUERY cap must use the generated compatibility shim, not an external rusqlite dep"
);
assert!(
cargo_content.contains("bundled"),
"rusqlite must use bundled feature"
harness.source.contains("extern crate self as rusqlite;"),
"SQL_QUERY harness must expose a local rusqlite-compatible crate alias"
);
}