mini-build 0.1.0

Builds the directory a static server serves: CSS/JS bundling and minification via external tools, plus asset mirroring.
Documentation
use super::*;

use tempfile::TempDir;

fn sh_args(script: &str) -> Vec<OsString> {
    vec![OsString::from("-c"), OsString::from(script)]
}

#[test]
fn succeeds_when_process_writes_output_and_exits_zero() {
    let dir = TempDir::new().unwrap();
    let output = dir.path().join("out.txt");
    let script = format!("printf hi > {}", output.display());

    let result = execute(
        "sh-test",
        "n/a",
        "sh",
        &sh_args(&script),
        &[&output],
        TOOL_TIMEOUT,
    );

    assert!(result.is_ok(), "{result:?}");
}

#[test]
fn maps_nonzero_exit_to_exit_non_zero_with_captured_stderr() {
    let dir = TempDir::new().unwrap();
    let output = dir.path().join("out.txt");

    let result = execute(
        "sh-test",
        "n/a",
        "sh",
        &sh_args("echo boom 1>&2; exit 3"),
        &[&output],
        TOOL_TIMEOUT,
    );

    match result {
        Err(ToolError::ExitNonZero { code, stderr, .. }) => {
            assert_eq!(code, Some(3));
            assert!(stderr.contains("boom"), "stderr was: {stderr}");
        }
        other => panic!("expected ExitNonZero, got {other:?}"),
    }
}

#[test]
fn maps_missing_binary_to_not_found() {
    let dir = TempDir::new().unwrap();
    let output = dir.path().join("out.txt");

    let result = execute(
        "missing-test",
        "install it from nowhere",
        "definitely-not-a-real-binary-9f3c2a",
        &[],
        &[&output],
        TOOL_TIMEOUT,
    );

    assert!(
        matches!(result, Err(ToolError::NotFound { .. })),
        "{result:?}"
    );
}

#[test]
fn zero_exit_with_no_output_file_is_no_output() {
    let dir = TempDir::new().unwrap();
    let output = dir.path().join("out.txt");

    let result = execute(
        "sh-test",
        "n/a",
        "sh",
        &sh_args("true"),
        &[&output],
        TOOL_TIMEOUT,
    );

    assert!(
        matches!(result, Err(ToolError::NoOutput { .. })),
        "{result:?}"
    );
}

#[test]
fn timed_out_process_is_reported() {
    let dir = TempDir::new().unwrap();
    let output = dir.path().join("out.txt");

    let result = execute(
        "sh-test",
        "n/a",
        "sh",
        &sh_args("sleep 5"),
        &[&output],
        Duration::from_millis(50),
    );

    assert!(
        matches!(result, Err(ToolError::TimedOut { .. })),
        "{result:?}"
    );
}

#[test]
fn locates_a_binary_known_to_exist_on_path() {
    assert!(
        locate_on_path("sh"),
        "sh should be on PATH in any test environment"
    );
}

#[test]
fn does_not_locate_a_nonexistent_binary() {
    assert!(!locate_on_path("definitely-not-a-real-binary-9f3c2a"));
}

/// The bound that replaced `tokio::time::timeout` plus `kill_on_drop`. A2's equivalence
/// harness compares output bytes and so cannot see this at all — a rewrite that dropped
/// the timeout entirely would still produce identical output for every tool that happens
/// to terminate, and would hang the build forever on the one that does not.
#[test]
fn a_hanging_tool_is_killed_at_the_timeout() {
    let dir = TempDir::new().unwrap();
    let output = dir.path().join("never-written.txt");
    let started = std::time::Instant::now();

    let result = execute(
        "sh-test",
        "n/a",
        "sh",
        &sh_args("sleep 30"),
        &[&output],
        Duration::from_millis(200),
    );

    assert!(
        matches!(result, Err(ToolError::TimedOut { .. })),
        "expected TimedOut, got: {result:?}"
    );
    assert!(
        started.elapsed() < Duration::from_secs(5),
        "the call returned only after {:?} — the timeout is not bounding anything",
        started.elapsed()
    );
}

/// Killing the child is explicit now: `std::process::Child` does not terminate on drop
/// the way tokio's did, so a timed-out process would otherwise outlive the build that
/// gave up on it.
#[test]
fn a_timed_out_tool_leaves_no_process_behind() {
    let dir = TempDir::new().unwrap();
    let output = dir.path().join("never-written.txt");
    let marker = dir.path().join("still-alive.txt");

    // Sleeps past the timeout, then writes a marker. If the kill works the marker never
    // appears; if the child is merely abandoned, it survives and writes it.
    let script = format!("sleep 1; printf alive > {}", marker.display());
    let result = execute(
        "sh-test",
        "n/a",
        "sh",
        &sh_args(&script),
        &[&output],
        Duration::from_millis(100),
    );
    assert!(matches!(result, Err(ToolError::TimedOut { .. })));

    std::thread::sleep(Duration::from_millis(1500));
    assert!(
        !marker.exists(),
        "the timed-out child kept running and wrote its marker — it was abandoned, not killed"
    );
}