jbx 0.6.1

jbx: one-stop Java toolbox for scripts, tools, and agents
Documentation
use std::collections::HashMap;
use std::fs;
use std::io::{Read, Write};
use std::net::TcpListener;
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(),
        "expected success\nstatus: {}\nstdout:\n{}\nstderr:\n{}",
        out.status,
        String::from_utf8_lossy(&out.stdout),
        String::from_utf8_lossy(&out.stderr)
    );
}

fn command_output(mut command: Command) -> String {
    let output = command.output().expect("failed to run command");
    assert_success(&output);
    String::from_utf8_lossy(&output.stdout).to_string()
}

fn build_executable_jar(tmp: &tempfile::TempDir) -> Vec<u8> {
    let source_dir = tmp.path().join("src/dev/telegraphic/tool");
    let classes_dir = tmp.path().join("classes");
    fs::create_dir_all(&source_dir).unwrap();
    fs::create_dir_all(&classes_dir).unwrap();
    let source = source_dir.join("Tool.java");
    fs::write(
        &source,
        r#"
package dev.telegraphic.tool;

public class Tool {
  public static void main(String[] args) {
    System.out.println("jbx " + String.join(",", args));
  }
}
"#,
    )
    .unwrap();

    let mut javac = Command::new("javac");
    javac.arg("-d").arg(&classes_dir).arg(&source);
    command_output(javac);

    let jar_path = tmp.path().join("hello-tool-1.0.0.jar");
    let mut jar = Command::new("jar");
    jar.arg("--create")
        .arg("--file")
        .arg(&jar_path)
        .arg("--main-class")
        .arg("dev.telegraphic.tool.Tool")
        .arg("-C")
        .arg(&classes_dir)
        .arg(".");
    command_output(jar);

    fs::read(jar_path).unwrap()
}

fn serve_files(files: HashMap<&'static str, Vec<u8>>) -> String {
    let listener = TcpListener::bind("127.0.0.1:0").unwrap();
    let base = format!("http://{}", listener.local_addr().unwrap());
    thread::spawn(move || loop {
        let Ok((mut stream, _)) = listener.accept() else {
            break;
        };
        let mut request = [0_u8; 2048];
        let read = stream.read(&mut request).unwrap_or(0);
        let request_text = String::from_utf8_lossy(&request[..read]);
        let path = request_text
            .lines()
            .next()
            .and_then(|line| line.split_whitespace().nth(1))
            .unwrap_or("/");
        let (status, body): (&str, &[u8]) = match files.get(path) {
            Some(body) => ("200 OK", body.as_slice()),
            None => ("404 Not Found", b"not found"),
        };
        let response = format!(
                "HTTP/1.1 {status}\r\nContent-Type: application/octet-stream\r\nContent-Length: {}\r\nConnection: close\r\n\r\n",
                body.len()
            );
        stream.write_all(response.as_bytes()).unwrap();
        stream.write_all(body).unwrap();
    });
    base
}

#[test]
fn jbx_runs_executable_jar_from_gav_shorthand() {
    let tmp = tempfile::tempdir().unwrap();
    let jar = build_executable_jar(&tmp);
    let pom = br#"
<project>
  <modelVersion>4.0.0</modelVersion>
  <groupId>dev.telegraphic</groupId>
  <artifactId>hello-tool</artifactId>
  <version>1.0.0</version>
</project>
"#
    .to_vec();
    let repo = serve_files(HashMap::from([
        (
            "/dev/telegraphic/hello-tool/1.0.0/hello-tool-1.0.0.pom",
            pom,
        ),
        (
            "/dev/telegraphic/hello-tool/1.0.0/hello-tool-1.0.0.jar",
            jar,
        ),
    ]));

    let output = jbx_command()
        .arg("--repo")
        .arg(format!("local={repo}"))
        .arg("--cache-dir")
        .arg(tmp.path().join("cache-jbx"))
        .arg("dev.telegraphic:hello-tool:1.0.0")
        .arg("--")
        .arg("jay")
        .arg("box")
        .output()
        .expect("failed to run jbx Maven executable shorthand");

    assert_success(&output);
    assert_eq!(
        String::from_utf8_lossy(&output.stdout).trim(),
        "jbx jay,box"
    );
}

#[test]
fn jbx_runs_executable_jar_from_gav() {
    let tmp = tempfile::tempdir().unwrap();
    let jar = build_executable_jar(&tmp);
    let pom = br#"
<project>
  <modelVersion>4.0.0</modelVersion>
  <groupId>dev.telegraphic</groupId>
  <artifactId>hello-tool</artifactId>
  <version>1.0.0</version>
</project>
"#
    .to_vec();
    let repo = serve_files(HashMap::from([
        (
            "/dev/telegraphic/hello-tool/1.0.0/hello-tool-1.0.0.pom",
            pom,
        ),
        (
            "/dev/telegraphic/hello-tool/1.0.0/hello-tool-1.0.0.jar",
            jar,
        ),
    ]));

    let output = jbx_command()
        .arg("--repo")
        .arg(format!("local={repo}"))
        .arg("--cache-dir")
        .arg(tmp.path().join("cache"))
        .arg("dev.telegraphic:hello-tool:1.0.0")
        .arg("--")
        .arg("alpha")
        .arg("beta")
        .output()
        .expect("failed to run jbx Maven executable");

    assert_success(&output);
    assert_eq!(
        String::from_utf8_lossy(&output.stdout).trim(),
        "jbx alpha,beta"
    );

    let output = jbx_command()
        .arg("--repo")
        .arg(format!("local={repo}"))
        .arg("--cache-dir")
        .arg(tmp.path().join("cache-main"))
        .arg("dev.telegraphic:hello-tool:1.0.0")
        .arg("--main")
        .arg("dev.telegraphic.tool.Tool")
        .arg("--")
        .arg("gamma")
        .output()
        .expect("failed to run jbx Maven executable with --main after coordinate");

    assert_success(&output);
    assert_eq!(String::from_utf8_lossy(&output.stdout).trim(), "jbx gamma");
}

#[test]
fn jbx_passes_dash_args_to_maven_tool_without_separator() {
    let tmp = tempfile::tempdir().unwrap();
    let jar = build_executable_jar(&tmp);
    let pom = br#"
<project>
  <modelVersion>4.0.0</modelVersion>
  <groupId>dev.telegraphic</groupId>
  <artifactId>hello-tool</artifactId>
  <version>1.0.0</version>
</project>
"#
    .to_vec();
    let repo = serve_files(HashMap::from([
        (
            "/dev/telegraphic/hello-tool/1.0.0/hello-tool-1.0.0.pom",
            pom,
        ),
        (
            "/dev/telegraphic/hello-tool/1.0.0/hello-tool-1.0.0.jar",
            jar,
        ),
    ]));

    let output = jbx_command()
        .arg("--repo")
        .arg(format!("local={repo}"))
        .arg("--cache-dir")
        .arg(tmp.path().join("cache-no-separator"))
        .arg("dev.telegraphic:hello-tool:1.0.0")
        .arg("--help")
        .output()
        .expect("failed to run jbx Maven executable with dash arg");

    assert_success(&output);
    assert_eq!(String::from_utf8_lossy(&output.stdout).trim(), "jbx --help");
}

#[test]
fn jbx_uses_latest_metadata_version_when_gav_version_is_omitted() {
    let tmp = tempfile::tempdir().unwrap();
    let jar = build_executable_jar(&tmp);
    let metadata = br#"
<metadata>
  <groupId>dev.telegraphic</groupId>
  <artifactId>hello-tool</artifactId>
  <versioning>
    <latest>1.0.0</latest>
    <release>1.0.0</release>
    <versions>
      <version>0.9.0</version>
      <version>1.0.0</version>
    </versions>
  </versioning>
</metadata>
"#
    .to_vec();
    let pom = br#"
<project>
  <modelVersion>4.0.0</modelVersion>
  <groupId>dev.telegraphic</groupId>
  <artifactId>hello-tool</artifactId>
  <version>1.0.0</version>
</project>
"#
    .to_vec();
    let repo = serve_files(HashMap::from([
        ("/dev/telegraphic/hello-tool/maven-metadata.xml", metadata),
        (
            "/dev/telegraphic/hello-tool/1.0.0/hello-tool-1.0.0.pom",
            pom,
        ),
        (
            "/dev/telegraphic/hello-tool/1.0.0/hello-tool-1.0.0.jar",
            jar,
        ),
    ]));

    let output = jbx_command()
        .arg("--repo")
        .arg(format!("local={repo}"))
        .arg("--cache-dir")
        .arg(tmp.path().join("cache-latest"))
        .arg("dev.telegraphic:hello-tool")
        .arg("--")
        .arg("delta")
        .output()
        .expect("failed to run jbx with omitted coordinate version");

    assert_success(&output);
    assert_eq!(String::from_utf8_lossy(&output.stdout).trim(), "jbx delta");
}