jbx 0.6.1

jbx: one-stop Java toolbox for scripts, tools, and agents
Documentation
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(),
        "stdout={} stderr={}",
        String::from_utf8_lossy(&out.stdout),
        String::from_utf8_lossy(&out.stderr)
    );
}

#[test]
fn init_creates_default_java_script_from_filename() {
    let tmp = tempfile::tempdir().unwrap();
    let script = tmp.path().join("Hello.java");

    let out = juv_command()
        .current_dir(tmp.path())
        .arg("init")
        .arg("Hello.java")
        .output()
        .unwrap();

    assert_success(&out);
    let content = fs::read_to_string(&script).unwrap();
    assert!(content.starts_with("///usr/bin/env jbx"));
    assert!(content.contains("//JAVA 25+"));
    assert!(content.contains("void main(String... args)"));
    assert!(content.contains("IO.println(\"Hello World\")"));
    assert!(!content.contains("public class"));
}

#[test]
fn init_writes_deps_and_java_directives() {
    let tmp = tempfile::tempdir().unwrap();
    let script = tmp.path().join("UseDeps.java");

    let out = juv_command()
        .current_dir(tmp.path())
        .arg("init")
        .arg("--java")
        .arg("25")
        .arg("--deps")
        .arg("info.picocli:picocli:4.7.6,com.google.guava:guava:33.3.1-jre")
        .arg("UseDeps.java")
        .output()
        .unwrap();

    assert_success(&out);
    let content = fs::read_to_string(&script).unwrap();
    assert!(content.contains("//JAVA 25"));
    assert!(content.contains("//DEPS info.picocli:picocli:4.7.6"));
    assert!(content.contains("//DEPS com.google.guava:guava:33.3.1-jre"));
}

#[test]
fn init_rejects_lower_java_for_unnamed_class_templates() {
    let tmp = tempfile::tempdir().unwrap();

    let out = juv_command()
        .current_dir(tmp.path())
        .arg("init")
        .arg("--java")
        .arg("17")
        .arg("Old.java")
        .output()
        .unwrap();

    assert!(!out.status.success());
    let stderr = String::from_utf8_lossy(&out.stderr);
    assert!(stderr.contains("uses Java 25 unnamed classes"), "{stderr}");
}

#[test]
fn init_allows_lower_java_for_class_based_templates() {
    let tmp = tempfile::tempdir().unwrap();
    let script = tmp.path().join("Greet.java");

    let out = juv_command()
        .current_dir(tmp.path())
        .arg("init")
        .arg("--template")
        .arg("cli")
        .arg("--java")
        .arg("17")
        .arg("Greet.java")
        .output()
        .unwrap();

    assert_success(&out);
    let content = fs::read_to_string(&script).unwrap();
    assert!(content.contains("//JAVA 17"));
    assert!(content.contains("class Greet implements Callable<Integer>"));
}

#[test]
fn init_refuses_to_overwrite_without_force() {
    let tmp = tempfile::tempdir().unwrap();
    let script = tmp.path().join("Existing.java");
    fs::write(&script, "keep me").unwrap();

    let out = juv_command()
        .current_dir(tmp.path())
        .arg("init")
        .arg("Existing.java")
        .output()
        .unwrap();

    assert!(!out.status.success());
    assert_eq!(fs::read_to_string(&script).unwrap(), "keep me");
}

#[test]
fn init_force_overwrites_existing_file() {
    let tmp = tempfile::tempdir().unwrap();
    let script = tmp.path().join("Existing.java");
    fs::write(&script, "replace me").unwrap();

    let out = juv_command()
        .current_dir(tmp.path())
        .arg("init")
        .arg("--force")
        .arg("Existing.java")
        .output()
        .unwrap();

    assert_success(&out);
    let content = fs::read_to_string(&script).unwrap();
    assert!(content.contains("void main(String... args)"));
    assert!(!content.contains("replace me"));
}

#[test]
fn init_cli_template_creates_picocli_script() {
    let tmp = tempfile::tempdir().unwrap();
    let script = tmp.path().join("Greet.java");

    let out = juv_command()
        .current_dir(tmp.path())
        .arg("init")
        .arg("--template")
        .arg("cli")
        .arg("--java")
        .arg("25")
        .arg("Greet.java")
        .output()
        .unwrap();

    assert_success(&out);
    let content = fs::read_to_string(&script).unwrap();
    assert!(content.contains("//JAVA 25"));
    assert!(content.contains("//DEPS info.picocli:picocli:4.7.6"));
    assert!(content.contains("@Command(name = \"Greet\""));
    assert!(content.contains("class Greet implements Callable<Integer>"));
}

#[test]
fn init_agent_template_creates_java_agent_skeleton() {
    let tmp = tempfile::tempdir().unwrap();
    let script = tmp.path().join("Spy.java");

    let out = juv_command()
        .current_dir(tmp.path())
        .arg("init")
        .arg("--template")
        .arg("agent")
        .arg("Spy.java")
        .output()
        .unwrap();

    assert_success(&out);
    let content = fs::read_to_string(&script).unwrap();
    assert!(content.contains("//JAVAAGENT\n"));
    assert!(content.contains("//MANIFEST Premain-Class=Spy"));
    assert!(content.contains("//MANIFEST Can-Redefine-Classes=true"));
    assert!(content.contains("public static void premain"));
    assert!(content.contains("public class Spy"));
}

#[test]
fn init_compact_template_uses_java_25_compact_source_shape() {
    let tmp = tempfile::tempdir().unwrap();
    let script = tmp.path().join("HelloCompact.java");

    let out = juv_command()
        .current_dir(tmp.path())
        .arg("init")
        .arg("--template")
        .arg("compact")
        .arg("HelloCompact.java")
        .output()
        .unwrap();

    assert_success(&out);
    let content = fs::read_to_string(&script).unwrap();
    assert!(content.contains("//JAVA 25+"));
    assert!(content.contains("void main(String... args)"));
    assert!(content.contains("IO.println(\"Hello World\")"));
    assert!(!content.contains("public class"));
}

#[test]
fn init_test_template_creates_junit_test_from_snake_case_filename() {
    let tmp = tempfile::tempdir().unwrap();
    let script = tmp.path().join("hello_test.java");

    let out = juv_command()
        .current_dir(tmp.path())
        .arg("init")
        .arg("-t")
        .arg("test")
        .arg("hello_test.java")
        .output()
        .unwrap();

    assert_success(&out);
    let content = fs::read_to_string(&script).unwrap();
    assert!(content.starts_with("///usr/bin/env jbx"));
    assert!(content.contains("//DEPS org.junit.jupiter:junit-jupiter-api:5.11.4"));
    assert!(content.contains("import org.junit.jupiter.api.Test;"));
    assert!(content.contains("class HelloTest"));
    assert!(content.contains("@Test"));
}

#[test]
fn init_rejects_unknown_template() {
    let tmp = tempfile::tempdir().unwrap();

    let out = juv_command()
        .current_dir(tmp.path())
        .arg("init")
        .arg("--template")
        .arg("bogus")
        .arg("Bad.java")
        .output()
        .unwrap();

    assert!(!out.status.success());
    let stderr = String::from_utf8_lossy(&out.stderr);
    assert!(stderr.contains("unknown init template 'bogus'"), "{stderr}");
}

#[test]
fn template_list_prints_builtin_templates() {
    let out = juv_command().arg("template").arg("list").output().unwrap();

    assert_success(&out);
    let stdout = String::from_utf8_lossy(&out.stdout);
    assert!(stdout.contains("hello\tBasic Java 25 unnamed-class Hello World script"));
    assert!(stdout.contains("compact\tJava 25 compact-source Hello World script"));
    assert!(stdout.contains("cli\tPicocli command-line application"));
    assert!(stdout.contains("agent\tJava agent skeleton"));
    assert!(stdout.contains("test\tJUnit test class"));
}