jbx 0.6.3

jbx: one-stop Java toolbox for scripts, tools, and agents
Documentation
use std::fs;
use std::io::{BufRead, BufReader, Write};
use std::net::{TcpListener, TcpStream};
use std::process::{Command, Output};
use std::thread;

fn jbx_command() -> Command {
    Command::new(env!("CARGO_BIN_EXE_jbx"))
}

fn assert_success(out: &Output) {
    assert!(
        out.status.success(),
        "stdout={} stderr={}",
        String::from_utf8_lossy(&out.stdout),
        String::from_utf8_lossy(&out.stderr)
    );
}

fn serve_maven_search() -> String {
    let listener = TcpListener::bind("127.0.0.1:0").unwrap();
    let addr = listener.local_addr().unwrap();
    thread::spawn(move || {
        for stream in listener.incoming().flatten() {
            thread::spawn(move || serve_search_response(stream));
        }
    });
    format!("http://{addr}")
}

fn serve_search_response(mut stream: TcpStream) {
    let mut request_line = String::new();
    {
        let mut reader = BufReader::new(&mut stream);
        reader.read_line(&mut request_line).unwrap();
        assert!(
            request_line.starts_with("GET /solrsearch/select?"),
            "unexpected request line: {request_line}"
        );
        loop {
            let mut header = String::new();
            reader.read_line(&mut header).unwrap();
            if header == "\r\n" || header.is_empty() {
                break;
            }
        }
    }
    let body = r#"{"response":{"numFound":1,"docs":[{"g":"org.junit.platform","a":"junit-platform-console-standalone","latestVersion":"1.13.4","p":"jar","versionCount":99}]}}"#;
    let response = format!(
        "HTTP/1.1 200 OK\r\nContent-Type: application/json\r\nContent-Length: {}\r\nConnection: close\r\n\r\n{}",
        body.len(),
        body
    );
    stream.write_all(response.as_bytes()).unwrap();
}

#[test]
fn doctor_json_reports_core_checks_and_skips_contextual_tools() {
    let tmp = tempfile::tempdir().unwrap();
    let fake_jdk = tmp.path().join("jdk-25");
    fs::create_dir_all(fake_jdk.join("bin")).unwrap();
    fs::write(fake_jdk.join("bin/java"), b"#!/bin/sh\n").unwrap();
    fs::write(fake_jdk.join("bin/javac"), b"#!/bin/sh\n").unwrap();
    fs::write(fake_jdk.join("release"), "JAVA_VERSION=\"25.0.1\"\n").unwrap();

    let out = jbx_command()
        .env("JAVA_HOME", &fake_jdk)
        .env("JBX_MAVEN_SEARCH_URL", serve_maven_search())
        .arg("doctor")
        .arg("--json")
        .arg("--cache-dir")
        .arg(tmp.path().join("cache"))
        .output()
        .unwrap();

    assert_success(&out);
    let json: serde_json::Value = serde_json::from_slice(&out.stdout).unwrap();
    let checks = json["checks"].as_array().unwrap();
    assert!(checks
        .iter()
        .any(|check| check["name"] == "jdk" && check["status"] == "ok"));
    assert!(checks
        .iter()
        .any(|check| check["name"] == "maven-central" && check["status"] == "ok"));
    assert!(checks
        .iter()
        .any(|check| check["name"] == "cache" && check["status"] == "ok"));
    assert!(checks
        .iter()
        .any(|check| check["name"] == "gpg" && check["status"] == "skipped"));
    assert!(checks
        .iter()
        .any(|check| check["name"] == "native-image" && check["status"] == "skipped"));
}

#[test]
fn doctor_json_reports_version_drift_for_classifier_coordinate() {
    let tmp = tempfile::tempdir().unwrap();
    let fake_jdk = tmp.path().join("jdk-25");
    fs::create_dir_all(fake_jdk.join("bin")).unwrap();
    fs::write(fake_jdk.join("bin/java"), b"#!/bin/sh\n").unwrap();
    fs::write(fake_jdk.join("bin/javac"), b"#!/bin/sh\n").unwrap();
    fs::write(fake_jdk.join("release"), "JAVA_VERSION=\"25.0.1\"\n").unwrap();
    let script = tmp.path().join("Hello.java");
    fs::write(
        &script,
        "//DEPS org.junit.platform:junit-platform-console-standalone:all:1.11.3\nvoid main() {}\n",
    )
    .unwrap();

    let out = jbx_command()
        .env("JAVA_HOME", &fake_jdk)
        .env("JBX_MAVEN_SEARCH_URL", serve_maven_search())
        .arg("doctor")
        .arg("--json")
        .arg("--cache-dir")
        .arg(tmp.path().join("cache"))
        .arg(&script)
        .output()
        .unwrap();

    let json: serde_json::Value = serde_json::from_slice(&out.stdout).unwrap();
    let checks = json["checks"].as_array().unwrap();
    let drift = checks
        .iter()
        .find(|check| check["name"] == "dependency-version")
        .unwrap();
    assert!(
        drift["summary"]
            .as_str()
            .unwrap()
            .contains("declares 1.11.3"),
        "{drift:#}"
    );
    assert!(
        !drift["summary"].as_str().unwrap().contains("declares all"),
        "{drift:#}"
    );
}

#[test]
fn doctor_cache_probe_does_not_overwrite_fixed_probe_name() {
    let tmp = tempfile::tempdir().unwrap();
    let fake_jdk = tmp.path().join("jdk-25");
    fs::create_dir_all(fake_jdk.join("bin")).unwrap();
    fs::write(fake_jdk.join("bin/java"), b"#!/bin/sh\n").unwrap();
    fs::write(fake_jdk.join("bin/javac"), b"#!/bin/sh\n").unwrap();
    fs::write(fake_jdk.join("release"), "JAVA_VERSION=\"25.0.1\"\n").unwrap();
    let cache = tmp.path().join("cache");
    fs::create_dir_all(&cache).unwrap();
    let existing_probe = cache.join(".jbx-doctor-write-test");
    fs::write(&existing_probe, b"keep me").unwrap();

    let out = jbx_command()
        .env("JAVA_HOME", &fake_jdk)
        .env("JBX_MAVEN_SEARCH_URL", serve_maven_search())
        .arg("doctor")
        .arg("--json")
        .arg("--cache-dir")
        .arg(&cache)
        .output()
        .unwrap();

    assert_success(&out);
    assert_eq!(fs::read(&existing_probe).unwrap(), b"keep me");
}