jbx 0.6.3

jbx: one-stop Java toolbox for scripts, tools, and agents
Documentation
use std::fs;
use std::io::{BufRead, BufReader};
use std::process::{Child, Command, Output, Stdio};
use std::time::{Duration, Instant};

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 write_project(root: &std::path::Path) {
    fs::write(
        root.join("Hello.java"),
        r#"
public class Hello {
  public static String message() { return "hello"; }
  public static void main(String[] args) { System.out.println(message()); }
}
"#,
    )
    .unwrap();
    fs::write(
        root.join("jbx.json"),
        r#"{
  "main": "Hello.java",
  "group": "dev.telegraphic.demo",
  "id": "hello-tool",
  "version": "1.0.0",
  "java": "21",
  "package": "dev.telegraphic.demo.hello",
  "description": "Hello tool",
  "url": "https://github.com/telegraphic-dev/hello-tool",
  "licenses": [{"name": "MIT License", "url": "https://opensource.org/licenses/MIT"}],
  "developers": [{"name": "Telegraphic", "organizationUrl": "https://github.com/telegraphic-dev"}],
  "scm": {
    "connection": "scm:git:https://github.com/telegraphic-dev/hello-tool.git",
    "developerConnection": "scm:git:ssh://git@github.com/telegraphic-dev/hello-tool.git",
    "url": "https://github.com/telegraphic-dev/hello-tool"
  }
}
"#,
    )
    .unwrap();
}

fn write_minimal_project(root: &std::path::Path) {
    fs::write(
        root.join("Hello.java"),
        r#"
public class Hello {
  public static void main(String[] args) { System.out.println("hello"); }
}
"#,
    )
    .unwrap();
    fs::write(
        root.join("jbx.json"),
        r#"{
  "main": "Hello.java",
  "group": "dev.telegraphic.demo",
  "id": "minimal-tool",
  "version": "1.0.0",
  "package": "dev.telegraphic.demo.minimal"
}
"#,
    )
    .unwrap();
}

#[test]
fn install_writes_current_project_to_selected_maven_repository_layout() {
    let tmp = tempfile::tempdir().unwrap();
    write_project(tmp.path());
    let destination = tmp.path().join("repo");

    let out = jbx_command()
        .arg("install")
        .arg("--file")
        .arg(tmp.path().join("jbx.json"))
        .arg("--destination")
        .arg(&destination)
        .arg("--target-dir")
        .arg(tmp.path().join("install-target"))
        .arg("--cache-dir")
        .arg(tmp.path().join("cache"))
        .output()
        .unwrap();

    assert_success(&out);
    let base = destination.join("dev/telegraphic/demo/hello-tool/1.0.0");
    assert!(base.join("hello-tool-1.0.0.jar").is_file());
    assert!(base.join("hello-tool-1.0.0-sources.jar").is_file());
    assert!(base.join("hello-tool-1.0.0-javadoc.jar").is_file());
    assert!(base.join("hello-tool-1.0.0.pom").is_file());
    let metadata = fs::read_to_string(
        destination.join("dev/telegraphic/demo/hello-tool/maven-metadata-local.xml"),
    )
    .unwrap();
    assert!(metadata.contains("<latest>1.0.0</latest>"), "{metadata}");
    assert!(metadata.contains("<release>1.0.0</release>"), "{metadata}");
    assert!(metadata.contains("<version>1.0.0</version>"), "{metadata}");
    assert!(String::from_utf8_lossy(&out.stdout).contains(base.to_string_lossy().as_ref()));
}

#[test]
fn install_merges_existing_local_maven_metadata_versions() {
    let tmp = tempfile::tempdir().unwrap();
    write_project(tmp.path());
    let destination = tmp.path().join("repo");
    let metadata_dir = destination.join("dev/telegraphic/demo/hello-tool");
    fs::create_dir_all(&metadata_dir).unwrap();
    fs::write(
        metadata_dir.join("maven-metadata-local.xml"),
        r#"<?xml version="1.0" encoding="UTF-8"?>
<metadata>
  <groupId>dev.telegraphic.demo</groupId>
  <artifactId>hello-tool</artifactId>
  <versioning>
    <latest>0.9.0</latest>
    <release>0.9.0</release>
    <versions>
      <version>0.9.0</version>
    </versions>
    <lastUpdated>20250102030405</lastUpdated>
  </versioning>
</metadata>
"#,
    )
    .unwrap();

    let out = jbx_command()
        .arg("install")
        .arg("--file")
        .arg(tmp.path().join("jbx.json"))
        .arg("--destination")
        .arg(&destination)
        .arg("--target-dir")
        .arg(tmp.path().join("install-target"))
        .arg("--cache-dir")
        .arg(tmp.path().join("cache"))
        .output()
        .unwrap();

    assert_success(&out);
    let metadata = fs::read_to_string(metadata_dir.join("maven-metadata-local.xml")).unwrap();
    assert!(metadata.contains("<latest>1.0.0</latest>"), "{metadata}");
    assert!(metadata.contains("<release>1.0.0</release>"), "{metadata}");
    assert!(metadata.contains("<version>0.9.0</version>"), "{metadata}");
    assert!(metadata.contains("<version>1.0.0</version>"), "{metadata}");
}

#[test]
fn install_accepts_minimal_non_central_metadata() {
    let tmp = tempfile::tempdir().unwrap();
    write_minimal_project(tmp.path());
    let destination = tmp.path().join("repo");

    let out = jbx_command()
        .arg("install")
        .arg("--file")
        .arg(tmp.path().join("jbx.json"))
        .arg("--destination")
        .arg(&destination)
        .arg("--target-dir")
        .arg(tmp.path().join("install-target"))
        .arg("--cache-dir")
        .arg(tmp.path().join("cache"))
        .current_dir(tmp.path())
        .output()
        .unwrap();

    assert_success(&out);
    let pom = fs::read_to_string(
        destination.join("dev/telegraphic/demo/minimal-tool/1.0.0/minimal-tool-1.0.0.pom"),
    )
    .unwrap();
    assert!(
        pom.contains("<artifactId>minimal-tool</artifactId>"),
        "{pom}"
    );
    assert!(!pom.contains("<licenses>"), "{pom}");
}

#[test]
fn install_defaults_to_current_project_and_maven_local_repository() {
    let tmp = tempfile::tempdir().unwrap();
    write_project(tmp.path());
    let home = tmp.path().join("home");
    fs::create_dir_all(&home).unwrap();

    let out = jbx_command()
        .arg("install")
        .arg("--target-dir")
        .arg(tmp.path().join("install-target"))
        .arg("--cache-dir")
        .arg(tmp.path().join("cache"))
        .env("HOME", &home)
        .current_dir(tmp.path())
        .output()
        .unwrap();

    assert_success(&out);
    let base = home.join(".m2/repository/dev/telegraphic/demo/hello-tool/1.0.0");
    assert!(base.join("hello-tool-1.0.0.jar").is_file());
    assert!(base.join("hello-tool-1.0.0.pom").is_file());
}

#[test]
fn publish_serve_exposes_prepared_artifacts_over_maven_repository_layout() {
    let tmp = tempfile::tempdir().unwrap();
    write_project(tmp.path());
    let mut child = jbx_command()
        .arg("publish")
        .arg("--serve")
        .arg("0")
        .arg("--skip-signing")
        .arg("--file")
        .arg(tmp.path().join("jbx.json"))
        .arg("--target-dir")
        .arg(tmp.path().join("serve-target"))
        .arg("--cache-dir")
        .arg(tmp.path().join("cache"))
        .stdout(Stdio::piped())
        .spawn()
        .unwrap();
    let stdout = child.stdout.take().unwrap();
    let mut reader = BufReader::new(stdout);
    let mut line = String::new();
    reader.read_line(&mut line).unwrap();
    assert!(line.contains("serving Maven repository at"), "{line}");
    let url = line
        .split_whitespace()
        .find(|part| part.starts_with("http://"))
        .unwrap()
        .trim_end_matches('/')
        .to_string();

    let pom_url = format!("{url}/dev/telegraphic/demo/hello-tool/1.0.0/hello-tool-1.0.0.pom");
    let metadata_url = format!("{url}/dev/telegraphic/demo/hello-tool/maven-metadata.xml");
    let metadata_sha1_url = format!("{metadata_url}.sha1");
    let deadline = Instant::now() + Duration::from_secs(10);
    let mut pom = None;
    let mut metadata = None;
    let mut metadata_sha1 = None;
    while Instant::now() < deadline {
        if pom.is_none() {
            if let Ok(response) = ureq::get(&pom_url).call() {
                if response.status() == 200 {
                    pom = Some(response.into_string().unwrap());
                }
            }
        }
        if metadata.is_none() {
            if let Ok(response) = ureq::get(&metadata_url).call() {
                if response.status() == 200 {
                    metadata = Some(response.into_string().unwrap());
                }
            }
        }
        if metadata_sha1.is_none() {
            if let Ok(response) = ureq::get(&metadata_sha1_url).call() {
                if response.status() == 200 {
                    metadata_sha1 = Some(response.into_string().unwrap());
                }
            }
        }
        if pom.is_some() && metadata.is_some() && metadata_sha1.is_some() {
            break;
        }
        std::thread::sleep(Duration::from_millis(100));
    }
    let pom = pom.unwrap_or_else(|| panic!("failed to fetch {pom_url}"));
    assert!(
        pom.contains("<groupId>dev.telegraphic.demo</groupId>"),
        "{pom}"
    );
    assert!(pom.contains("<artifactId>hello-tool</artifactId>"), "{pom}");
    let metadata = metadata.unwrap_or_else(|| panic!("failed to fetch {metadata_url}"));
    assert!(metadata.contains("<latest>1.0.0</latest>"), "{metadata}");
    assert!(metadata.contains("<release>1.0.0</release>"), "{metadata}");
    assert!(metadata.contains("<version>1.0.0</version>"), "{metadata}");
    let metadata_sha1 =
        metadata_sha1.unwrap_or_else(|| panic!("failed to fetch {metadata_sha1_url}"));
    assert_eq!(metadata_sha1.trim().len(), 40, "{metadata_sha1}");

    let run = jbx_command()
        .arg("--repo")
        .arg(&url)
        .arg("dev.telegraphic.demo:hello-tool:1.0.0")
        .arg("--cache-dir")
        .arg(tmp.path().join("run-cache"))
        .output()
        .unwrap();
    assert_success(&run);
    assert_eq!(String::from_utf8_lossy(&run.stdout).trim(), "hello");
    kill_child(&mut child);
}

fn kill_child(child: &mut Child) {
    let _ = child.kill();
    let _ = child.wait();
}