mirror of
https://github.com/elicpeter/nyx.git
synced 2026-06-09 19:45:13 +02:00
194 lines
6.6 KiB
Rust
194 lines
6.6 KiB
Rust
|
|
//! Phase 22 / Track O.0 acceptance test for the warm `javac` daemon.
|
||
|
|
//!
|
||
|
|
//! Asserts that 50 sequential harness-shaped Java compiles run through the
|
||
|
|
//! pool in < 5s on the dev reference machine (down from > 30s baseline with
|
||
|
|
//! one fresh `javac` per build). The test is gated on the `dynamic`
|
||
|
|
//! feature and skips silently when `javac` / `java` are not on PATH so a
|
||
|
|
//! JDK-less CI image does not break the gate.
|
||
|
|
|
||
|
|
#![cfg(feature = "dynamic")]
|
||
|
|
|
||
|
|
use std::path::{Path, PathBuf};
|
||
|
|
use std::process::Command;
|
||
|
|
use std::sync::{Mutex, MutexGuard};
|
||
|
|
use std::time::{Duration, Instant};
|
||
|
|
|
||
|
|
use nyx_scanner::dynamic::build_pool::BuildPool;
|
||
|
|
use nyx_scanner::dynamic::build_pool::java::JavacPool;
|
||
|
|
|
||
|
|
static BUILD_POOL_ENV_LOCK: Mutex<()> = Mutex::new(());
|
||
|
|
|
||
|
|
struct BuildPoolEnvGuard {
|
||
|
|
_lock: MutexGuard<'static, ()>,
|
||
|
|
prior: Option<String>,
|
||
|
|
}
|
||
|
|
|
||
|
|
impl BuildPoolEnvGuard {
|
||
|
|
fn set(path: &Path) -> Self {
|
||
|
|
let lock = BUILD_POOL_ENV_LOCK
|
||
|
|
.lock()
|
||
|
|
.unwrap_or_else(|poisoned| poisoned.into_inner());
|
||
|
|
let prior = std::env::var("NYX_BUILD_POOL_DIR").ok();
|
||
|
|
unsafe { std::env::set_var("NYX_BUILD_POOL_DIR", path) };
|
||
|
|
Self { _lock: lock, prior }
|
||
|
|
}
|
||
|
|
}
|
||
|
|
|
||
|
|
impl Drop for BuildPoolEnvGuard {
|
||
|
|
fn drop(&mut self) {
|
||
|
|
match self.prior.take() {
|
||
|
|
Some(value) => unsafe { std::env::set_var("NYX_BUILD_POOL_DIR", value) },
|
||
|
|
None => unsafe { std::env::remove_var("NYX_BUILD_POOL_DIR") },
|
||
|
|
}
|
||
|
|
}
|
||
|
|
}
|
||
|
|
|
||
|
|
fn jdk_available() -> bool {
|
||
|
|
fn ok(bin: &str) -> bool {
|
||
|
|
Command::new(bin)
|
||
|
|
.arg("-version")
|
||
|
|
.output()
|
||
|
|
.map(|o| o.status.success())
|
||
|
|
.unwrap_or(false)
|
||
|
|
}
|
||
|
|
ok(&std::env::var("NYX_JAVAC_BIN").unwrap_or_else(|_| "javac".to_owned()))
|
||
|
|
&& ok(&std::env::var("NYX_JAVA_BIN").unwrap_or_else(|_| "java".to_owned()))
|
||
|
|
}
|
||
|
|
|
||
|
|
/// Drop a self-contained Java source into `workdir/Harness{idx}.java`
|
||
|
|
/// and return the args list the pool expects.
|
||
|
|
fn write_harness(workdir: &Path, idx: usize) -> Vec<String> {
|
||
|
|
let class_name = format!("Harness{idx}");
|
||
|
|
let src = format!(
|
||
|
|
"public final class {class_name} {{\n \
|
||
|
|
public static int answer() {{ return {idx}; }}\n \
|
||
|
|
public static void main(String[] argv) {{ \
|
||
|
|
System.out.println({class_name}.answer()); }}\n\
|
||
|
|
}}\n",
|
||
|
|
);
|
||
|
|
let src_path = workdir.join(format!("{class_name}.java"));
|
||
|
|
std::fs::write(&src_path, src).unwrap();
|
||
|
|
vec![
|
||
|
|
"-d".to_owned(),
|
||
|
|
workdir.to_string_lossy().into_owned(),
|
||
|
|
src_path.to_string_lossy().into_owned(),
|
||
|
|
]
|
||
|
|
}
|
||
|
|
|
||
|
|
#[test]
|
||
|
|
#[ignore = "real-toolchain perf bench: runs 50 real `javac` compiles. Opt-in so the default suite stays hermetic + fast. Run: cargo nextest run --features dynamic --run-ignored ignored-only -E 'binary(~build_pool) | binary(~compile_pool)'"]
|
||
|
|
fn batch_of_fifty_harness_compiles_meets_perf_target() {
|
||
|
|
if !jdk_available() {
|
||
|
|
eprintln!("skipping: javac / java not available on PATH");
|
||
|
|
return;
|
||
|
|
}
|
||
|
|
|
||
|
|
// Isolate the pool bootstrap dir so this test does not race with
|
||
|
|
// another concurrent build-pool test or pollute the user's cache.
|
||
|
|
let bootstrap_root = tempfile::TempDir::new().unwrap();
|
||
|
|
let _env = BuildPoolEnvGuard::set(bootstrap_root.path());
|
||
|
|
|
||
|
|
let pool = match JavacPool::try_new("phase22-batch-test") {
|
||
|
|
Ok(p) => p,
|
||
|
|
Err(e) => {
|
||
|
|
eprintln!("skipping: pool bootstrap failed: {e}");
|
||
|
|
return;
|
||
|
|
}
|
||
|
|
};
|
||
|
|
|
||
|
|
// First call warms JIT + classpath caches inside the worker JVM.
|
||
|
|
// We deliberately measure the steady-state 50 builds with the
|
||
|
|
// bootstrap already paid because the acceptance gate is the
|
||
|
|
// amortised per-build cost.
|
||
|
|
let warmup_dir = tempfile::TempDir::new().unwrap();
|
||
|
|
let warmup_args = write_harness(warmup_dir.path(), 0);
|
||
|
|
let warmup = pool.compile_batch(warmup_dir.path(), &warmup_args);
|
||
|
|
assert!(
|
||
|
|
warmup.success,
|
||
|
|
"warmup compile must succeed: {}",
|
||
|
|
warmup.stderr
|
||
|
|
);
|
||
|
|
assert!(
|
||
|
|
warmup_dir.path().join("Harness0.class").exists(),
|
||
|
|
"warmup compile must emit a class file",
|
||
|
|
);
|
||
|
|
|
||
|
|
// 50 sequential builds, each in its own workdir so the JVM-side
|
||
|
|
// file resolution touches a fresh path every time -- closest
|
||
|
|
// analogue to the per-finding shape the verifier produces.
|
||
|
|
let mut workdirs: Vec<(tempfile::TempDir, PathBuf, Vec<String>)> = Vec::with_capacity(50);
|
||
|
|
for i in 1..=50 {
|
||
|
|
let d = tempfile::TempDir::new().unwrap();
|
||
|
|
let args = write_harness(d.path(), i);
|
||
|
|
let path = d.path().to_path_buf();
|
||
|
|
workdirs.push((d, path, args));
|
||
|
|
}
|
||
|
|
|
||
|
|
let start = Instant::now();
|
||
|
|
for (i, (_dir, path, args)) in workdirs.iter().enumerate() {
|
||
|
|
let r = pool.compile_batch(path, args);
|
||
|
|
assert!(r.success, "compile {} failed: {}", i + 1, r.stderr,);
|
||
|
|
let class_file = path.join(format!("Harness{}.class", i + 1));
|
||
|
|
assert!(
|
||
|
|
class_file.exists(),
|
||
|
|
"compile {} produced no class file at {}",
|
||
|
|
i + 1,
|
||
|
|
class_file.display(),
|
||
|
|
);
|
||
|
|
}
|
||
|
|
let elapsed = start.elapsed();
|
||
|
|
|
||
|
|
eprintln!(
|
||
|
|
"phase22 javac-pool: 50 hot compiles in {:.2?} (avg {:.2}ms/build)",
|
||
|
|
elapsed,
|
||
|
|
elapsed.as_secs_f64() * 1000.0 / 50.0,
|
||
|
|
);
|
||
|
|
|
||
|
|
let cap = Duration::from_secs(5);
|
||
|
|
assert!(
|
||
|
|
elapsed <= cap,
|
||
|
|
"phase22 acceptance gate: 50 hot compiles took {elapsed:?}, expected ≤ {cap:?}",
|
||
|
|
);
|
||
|
|
|
||
|
|
assert!(
|
||
|
|
pool.is_healthy(),
|
||
|
|
"pool must stay healthy after 50 compiles"
|
||
|
|
);
|
||
|
|
}
|
||
|
|
|
||
|
|
#[test]
|
||
|
|
fn pool_surfaces_real_compile_errors_intact() {
|
||
|
|
if !jdk_available() {
|
||
|
|
eprintln!("skipping: javac / java not available on PATH");
|
||
|
|
return;
|
||
|
|
}
|
||
|
|
let bootstrap_root = tempfile::TempDir::new().unwrap();
|
||
|
|
let _env = BuildPoolEnvGuard::set(bootstrap_root.path());
|
||
|
|
|
||
|
|
let pool = match JavacPool::try_new("phase22-error-test") {
|
||
|
|
Ok(p) => p,
|
||
|
|
Err(e) => {
|
||
|
|
eprintln!("skipping: pool bootstrap failed: {e}");
|
||
|
|
return;
|
||
|
|
}
|
||
|
|
};
|
||
|
|
|
||
|
|
let dir = tempfile::TempDir::new().unwrap();
|
||
|
|
let src = dir.path().join("Broken.java");
|
||
|
|
std::fs::write(&src, "public class Broken { int x = ; }").unwrap();
|
||
|
|
let args = vec![
|
||
|
|
"-d".to_owned(),
|
||
|
|
dir.path().to_string_lossy().into_owned(),
|
||
|
|
src.to_string_lossy().into_owned(),
|
||
|
|
];
|
||
|
|
let r = pool.compile_batch(dir.path(), &args);
|
||
|
|
assert!(!r.success, "syntactically invalid source must fail");
|
||
|
|
assert!(
|
||
|
|
!r.stderr.is_empty(),
|
||
|
|
"compile failure must produce a non-empty stderr payload (got {:?})",
|
||
|
|
r.stderr,
|
||
|
|
);
|
||
|
|
// Pool should still be alive for the next caller.
|
||
|
|
assert!(pool.is_healthy());
|
||
|
|
}
|