injm 0.4.0

A CLI tool that injects content into marked regions in source files.
use std::io::Write;
use std::process::Command;

fn injm_bin() -> Command {
    Command::new(env!("CARGO_BIN_EXE_injm"))
}

fn inject_stdin(path: &std::path::Path, input: &[u8]) -> std::process::ExitStatus {
    let mut child = injm_bin()
        .arg("--output")
        .arg(path)
        .stdin(std::process::Stdio::piped())
        .spawn()
        .unwrap();
    let _ = child.stdin.as_mut().unwrap().write_all(input);
    child.wait().unwrap()
}

#[test]
fn test_basic_injection() {
    let mut f = tempfile::NamedTempFile::with_suffix(".rs").unwrap();
    writeln!(f, "fn main() {{").unwrap();
    writeln!(f, "    // injm begin").unwrap();
    writeln!(f, "    // injm end").unwrap();
    writeln!(f, "}}").unwrap();

    let _output = injm_bin()
        .arg("--output")
        .arg(f.path())
        .stdin(std::process::Stdio::piped())
        .output()
        .unwrap();

    let mut child = injm_bin()
        .arg("--output")
        .arg(f.path())
        .stdin(std::process::Stdio::piped())
        .spawn()
        .unwrap();

    child
        .stdin
        .as_mut()
        .unwrap()
        .write_all(b"    println!(\"hello\");")
        .unwrap();
    child.wait().unwrap();

    let result = std::fs::read_to_string(f.path()).unwrap();
    assert!(result.contains("println!(\"hello\")"));
    assert!(!result.contains("injm begin\n    // injm end"));
}

#[test]
fn test_dry_run() {
    let mut f = tempfile::NamedTempFile::with_suffix(".rs").unwrap();
    writeln!(f, "fn main() {{").unwrap();
    writeln!(f, "    // injm begin").unwrap();
    writeln!(f, "    // injm end").unwrap();
    writeln!(f, "}}").unwrap();

    let original = std::fs::read_to_string(f.path()).unwrap();

    let mut child = injm_bin()
        .arg("--output")
        .arg(f.path())
        .arg("--dry-run")
        .stdin(std::process::Stdio::piped())
        .stdout(std::process::Stdio::piped())
        .spawn()
        .unwrap();

    child
        .stdin
        .as_mut()
        .unwrap()
        .write_all(b"    println!(\"hello\");")
        .unwrap();
    child.wait().unwrap();

    let after = std::fs::read_to_string(f.path()).unwrap();
    assert_eq!(original, after);
}

#[test]
fn test_binary_file_returns_error() {
    let mut f = tempfile::NamedTempFile::new().unwrap();
    f.write_all(&[0x00, 0x01, 0x02, 0x03]).unwrap();

    let status = injm_bin()
        .arg("--output")
        .arg(f.path())
        .stdin(std::process::Stdio::piped())
        .status()
        .unwrap();

    assert!(!status.success());
}

#[test]
fn test_file_not_exist_returns_error() {
    let status = injm_bin()
        .arg("--output")
        .arg("not_exist.rs")
        .stdin(std::process::Stdio::piped())
        .status()
        .unwrap();

    assert!(!status.success());
}

#[test]
fn test_inject_with_id() {
    let mut f = tempfile::NamedTempFile::with_suffix(".rs").unwrap();
    writeln!(f, "fn main() {{").unwrap();
    writeln!(f, "    // injm begin >greeting").unwrap();
    writeln!(f, "    // injm end").unwrap();
    writeln!(f, "    // injm begin >farewell").unwrap();
    writeln!(f, "    // injm end").unwrap();
    writeln!(f, "}}").unwrap();

    let mut child = injm_bin()
        .arg("--output")
        .arg(f.path())
        .arg("--id")
        .arg("greeting")
        .stdin(std::process::Stdio::piped())
        .spawn()
        .unwrap();

    child
        .stdin
        .as_mut()
        .unwrap()
        .write_all(b"    println!(\"hello\");")
        .unwrap();
    child.wait().unwrap();

    let result = std::fs::read_to_string(f.path()).unwrap();
    assert!(result.contains("println!(\"hello\")"));
    assert!(!result.contains("println!(\"bye\")"));
}

#[test]
fn test_inject_multiple_ids() {
    let mut f = tempfile::NamedTempFile::with_suffix(".rs").unwrap();
    writeln!(f, "fn main() {{").unwrap();
    writeln!(f, "    // injm begin >first").unwrap();
    writeln!(f, "    old first").unwrap();
    writeln!(f, "    // injm end").unwrap();
    writeln!(f, "    // injm begin >second").unwrap();
    writeln!(f, "    old second").unwrap();
    writeln!(f, "    // injm end").unwrap();
    writeln!(f, "    // injm begin >third").unwrap();
    writeln!(f, "    old third").unwrap();
    writeln!(f, "    // injm end").unwrap();
    writeln!(f, "}}").unwrap();

    let mut child = injm_bin()
        .arg("--output")
        .arg(f.path())
        .arg("--id")
        .arg("first")
        .arg("--id")
        .arg("third")
        .stdin(std::process::Stdio::piped())
        .spawn()
        .unwrap();

    child
        .stdin
        .as_mut()
        .unwrap()
        .write_all(b"new content")
        .unwrap();
    child.wait().unwrap();

    let result = std::fs::read_to_string(f.path()).unwrap();
    assert!(!result.contains("old first"));
    assert!(result.contains("old second"));
    assert!(!result.contains("old third"));
    assert_eq!(result.matches("new content").count(), 2);
}

#[test]
fn test_inject_all_when_no_id_specified() {
    let mut f = tempfile::NamedTempFile::with_suffix(".rs").unwrap();
    writeln!(f, "fn main() {{").unwrap();
    writeln!(f, "    // injm begin").unwrap();
    writeln!(f, "    old default").unwrap();
    writeln!(f, "    // injm end").unwrap();
    writeln!(f, "    // injm begin >first").unwrap();
    writeln!(f, "    old first").unwrap();
    writeln!(f, "    // injm end").unwrap();
    writeln!(f, "}}").unwrap();

    let mut child = injm_bin()
        .arg("--output")
        .arg(f.path())
        .stdin(std::process::Stdio::piped())
        .spawn()
        .unwrap();

    child
        .stdin
        .as_mut()
        .unwrap()
        .write_all(b"new content")
        .unwrap();
    child.wait().unwrap();

    let result = std::fs::read_to_string(f.path()).unwrap();
    assert!(!result.contains("old default"));
    assert!(result.contains("old first"));
}

#[test]
fn test_map_between_files() {
    let mut src = tempfile::NamedTempFile::with_suffix(".rs").unwrap();
    writeln!(src, "fn main() {{").unwrap();
    writeln!(src, "    // injm begin <hello").unwrap();
    writeln!(src, "    println!(\"Hello injm\")").unwrap();
    writeln!(src, "    // injm end").unwrap();
    writeln!(src, "}}").unwrap();

    let mut dest = tempfile::NamedTempFile::with_suffix(".rs").unwrap();
    writeln!(dest, "fn main() {{").unwrap();
    writeln!(dest, "    println!(\"Greeting from injm\")").unwrap();
    writeln!(dest, "    // injm begin >hello").unwrap();
    writeln!(dest, "    // injm end").unwrap();
    writeln!(dest, "}}").unwrap();

    let status = injm_bin()
        .arg("--input")
        .arg(src.path())
        .arg("--output")
        .arg(dest.path())
        .status()
        .unwrap();

    assert!(status.success());

    let result = std::fs::read_to_string(dest.path()).unwrap();
    assert!(result.contains("println!(\"Hello injm\")"));
    assert!(result.contains("println!(\"Greeting from injm\")"));
}

#[test]
fn test_map_missing_input_id_returns_error() {
    let mut src = tempfile::NamedTempFile::with_suffix(".rs").unwrap();
    writeln!(src, "fn main() {{").unwrap();
    writeln!(src, "    // injm begin <hello").unwrap();
    writeln!(src, "    println!(\"Hello injm\")").unwrap();
    writeln!(src, "    // injm end").unwrap();
    writeln!(src, "}}").unwrap();

    let mut dest = tempfile::NamedTempFile::with_suffix(".rs").unwrap();
    writeln!(dest, "fn main() {{").unwrap();
    writeln!(dest, "    // injm begin >not_exist").unwrap();
    writeln!(dest, "    // injm end").unwrap();
    writeln!(dest, "}}").unwrap();

    let status = injm_bin()
        .arg("--input")
        .arg(src.path())
        .arg("--output")
        .arg(dest.path())
        .status()
        .unwrap();

    assert!(!status.success());
}

#[test]
fn test_map_multiple_ids() {
    let mut src = tempfile::NamedTempFile::with_suffix(".rs").unwrap();
    writeln!(src, "fn main() {{").unwrap();
    writeln!(src, "    // injm begin <first").unwrap();
    writeln!(src, "    println!(\"first\")").unwrap();
    writeln!(src, "    // injm end").unwrap();
    writeln!(src, "    // injm begin <second").unwrap();
    writeln!(src, "    println!(\"second\")").unwrap();
    writeln!(src, "    // injm end").unwrap();
    writeln!(src, "}}").unwrap();

    let mut dest = tempfile::NamedTempFile::with_suffix(".rs").unwrap();
    writeln!(dest, "fn main() {{").unwrap();
    writeln!(dest, "    // injm begin >first").unwrap();
    writeln!(dest, "    // injm end").unwrap();
    writeln!(dest, "    // injm begin >second").unwrap();
    writeln!(dest, "    // injm end").unwrap();
    writeln!(dest, "}}").unwrap();

    let status = injm_bin()
        .arg("--input")
        .arg(src.path())
        .arg("--output")
        .arg(dest.path())
        .status()
        .unwrap();

    assert!(status.success());

    let result = std::fs::read_to_string(dest.path()).unwrap();
    assert!(result.contains("println!(\"first\")"));
    assert!(result.contains("println!(\"second\")"));
}

#[test]
fn test_map_dry_run() {
    let mut src = tempfile::NamedTempFile::with_suffix(".rs").unwrap();
    writeln!(src, "fn main() {{").unwrap();
    writeln!(src, "    // injm begin <hello").unwrap();
    writeln!(src, "    println!(\"Hello injm\")").unwrap();
    writeln!(src, "    // injm end").unwrap();
    writeln!(src, "}}").unwrap();

    let mut dest = tempfile::NamedTempFile::with_suffix(".rs").unwrap();
    writeln!(dest, "fn main() {{").unwrap();
    writeln!(dest, "    // injm begin >hello").unwrap();
    writeln!(dest, "    // injm end").unwrap();
    writeln!(dest, "}}").unwrap();

    let original = std::fs::read_to_string(dest.path()).unwrap();

    let status = injm_bin()
        .arg("--input")
        .arg(src.path())
        .arg("--output")
        .arg(dest.path())
        .arg("--dry-run")
        .status()
        .unwrap();

    assert!(status.success());

    let after = std::fs::read_to_string(dest.path()).unwrap();
    assert_eq!(original, after);
}

#[test]
fn test_unclosed_marker_returns_error() {
    let mut f = tempfile::NamedTempFile::with_suffix(".rs").unwrap();
    writeln!(f, "fn main() {{").unwrap();
    writeln!(f, "    // injm begin").unwrap();
    writeln!(f, "}}").unwrap();

    let status = inject_stdin(f.path(), b"x");
    assert!(!status.success());
}

#[test]
fn test_map_input_file_not_exist_returns_error() {
    let mut dest = tempfile::NamedTempFile::with_suffix(".rs").unwrap();
    writeln!(dest, "// injm begin >hello").unwrap();
    writeln!(dest, "// injm end").unwrap();

    let status = injm_bin()
        .arg("--input")
        .arg("not_exist_input.rs")
        .arg("--output")
        .arg(dest.path())
        .status()
        .unwrap();

    assert!(!status.success());
}

#[test]
fn test_reinjection_replaces_content() {
    let mut f = tempfile::NamedTempFile::with_suffix(".rs").unwrap();
    writeln!(f, "// injm begin").unwrap();
    writeln!(f, "// injm end").unwrap();

    assert!(inject_stdin(f.path(), b"first content").success());
    assert!(inject_stdin(f.path(), b"second content").success());

    let result = std::fs::read_to_string(f.path()).unwrap();
    assert!(result.contains("second content"));
    assert!(!result.contains("first content"));
    assert!(result.contains("// injm begin"));
    assert!(result.contains("// injm end"));
}