jbx 0.6.1

jbx: one-stop Java toolbox for scripts, tools, and agents
Documentation
use serde_json::Value;
use std::fs;
use std::process::{Command, Output};

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

fn assert_success(out: &Output) {
    assert!(
        out.status.success(),
        "expected success\nstatus: {}\nstdout:\n{}\nstderr:\n{}",
        out.status,
        String::from_utf8_lossy(&out.stdout),
        String::from_utf8_lossy(&out.stderr)
    );
}

#[test]
fn run_accepts_jbang_style_cli_overrides_for_java_sources_files_and_runtime_options() {
    let tmp = tempfile::tempdir().unwrap();
    let helper = tmp.path().join("Helper.java");
    let resource = tmp.path().join("message.txt");
    let src = tmp.path().join("CliOptions.java");

    fs::write(
        &helper,
        "class Helper { static String text() { return \"helper\"; } }\n",
    )
    .unwrap();
    fs::write(&resource, "resource").unwrap();
    fs::write(
        &src,
        r#"class CliOptions {
  public static void main(String[] args) throws Exception {
    String message = new String(CliOptions.class.getResourceAsStream("/data/message.txt").readAllBytes());
    System.out.println(Helper.text() + ":" + message + ":" + System.getProperty("juv.test"));
  }
}
"#,
    )
    .unwrap();

    let out = juv_command()
        .arg("run")
        .arg("--source")
        .arg("Helper.java")
        .arg("--files")
        .arg("data/message.txt=message.txt")
        .arg("--java-option")
        .arg("-Djuv.test=ok")
        .arg(&src)
        .current_dir(tmp.path())
        .output()
        .unwrap();

    assert_success(&out);
    assert_eq!(
        String::from_utf8_lossy(&out.stdout).trim(),
        "helper:resource:ok"
    );
}

#[test]
fn build_accepts_java_repo_source_files_and_compile_option_overrides() {
    let tmp = tempfile::tempdir().unwrap();
    let helper = tmp.path().join("BuildHelper.java");
    let resource = tmp.path().join("build.txt");
    let src = tmp.path().join("BuildOptions.java");
    let cache = tmp.path().join("cache");

    fs::write(&helper, "class BuildHelper {}\n").unwrap();
    fs::write(&resource, "build resource").unwrap();
    fs::write(&src, "class BuildOptions { BuildHelper h; }\n").unwrap();

    let out = juv_command()
        .arg("build")
        .arg("--java")
        .arg("25")
        .arg("--repo")
        .arg("central=https://repo1.maven.org/maven2")
        .arg("--source")
        .arg("BuildHelper.java")
        .arg("--files")
        .arg("assets/build.txt=build.txt")
        .arg("--compile-option")
        .arg("-Xlint:none")
        .arg("--cache-dir")
        .arg(&cache)
        .arg(&src)
        .current_dir(tmp.path())
        .output()
        .unwrap();

    assert_success(&out);
    let listed = juv_command()
        .arg("cache")
        .arg("list")
        .arg("--json")
        .arg("--cache-dir")
        .arg(&cache)
        .output()
        .unwrap();
    assert_success(&listed);
    let entries: Value = serde_json::from_slice(&listed.stdout).unwrap();
    let classes = entries[0]["classesDir"].as_str().unwrap();
    assert!(std::path::Path::new(classes)
        .join("BuildHelper.class")
        .exists());
    assert!(std::path::Path::new(classes)
        .join("assets/build.txt")
        .exists());
}

#[test]
fn info_exposes_option_collections_added_by_directives_and_cli() {
    let tmp = tempfile::tempdir().unwrap();
    let src = tmp.path().join("InfoOptions.java");
    fs::write(
        &src,
        r#"//JAVAC_OPTIONS -Xlint:none
//JAVA_OPTIONS -Dfrom.directive=yes
//NATIVE_OPTIONS --no-fallback
//JAVAAGENT agent.jar=debug
//MANIFEST Main-Class=InfoOptions
class InfoOptions { public static void main(String[] args) {} }
"#,
    )
    .unwrap();

    for (field, expected) in [
        ("compile-options", "-Xlint:none"),
        ("runtime-options", "-Dfrom.directive=yes"),
        ("native-options", "--no-fallback"),
        ("javaagents", "agent.jar=debug"),
        ("manifest", "Main-Class=InfoOptions"),
    ] {
        let out = juv_command()
            .arg("info")
            .arg(field)
            .arg(&src)
            .output()
            .unwrap();
        assert_success(&out);
        assert_eq!(
            String::from_utf8_lossy(&out.stdout).trim(),
            expected,
            "field {field}"
        );
    }

    let tools = juv_command()
        .arg("info")
        .arg("tools")
        .arg("--java")
        .arg("25")
        .arg("--repo")
        .arg("custom=https://repo.example.invalid/maven")
        .arg("--source")
        .arg("InfoOptions.java")
        .arg("--files")
        .arg("data.txt=InfoOptions.java")
        .arg("--compile-option")
        .arg("-parameters")
        .arg("--java-option")
        .arg("-Dfrom.cli=yes")
        .arg(&src)
        .output()
        .unwrap();
    assert_success(&tools);
    let json: Value = serde_json::from_slice(&tools.stdout).unwrap();
    assert_eq!(json["requestedJavaVersion"], "25");
    assert!(json["repositories"]
        .as_array()
        .unwrap()
        .iter()
        .any(|repo| repo["id"] == "custom"));
    assert!(json["sources"]
        .as_array()
        .unwrap()
        .iter()
        .any(|v| v == "InfoOptions.java"));
    assert!(json["files"]
        .as_array()
        .unwrap()
        .iter()
        .any(|v| v == "data.txt=InfoOptions.java"));
    assert!(json["compileOptions"]
        .as_array()
        .unwrap()
        .iter()
        .any(|v| v == "-parameters"));
    assert!(json["runtimeOptions"]
        .as_array()
        .unwrap()
        .iter()
        .any(|v| v == "-Dfrom.cli=yes"));
}