jqrutils 0.1.0

Small Unix utilities: quiet, errfail, prefix
use std::process::Command;
use std::time::Instant;

fn quiet() -> Command {
    Command::new(env!("CARGO_BIN_EXE_quiet"))
}

#[test]
fn success_suppresses_output() {
    let out = quiet()
        .args(["sh", "-c", "echo hello; echo world >&2"])
        .output()
        .unwrap();
    assert!(out.status.success());
    assert!(out.stdout.is_empty());
    assert!(out.stderr.is_empty());
}

#[test]
fn failure_shows_stdout() {
    let out = quiet()
        .args(["sh", "-c", "echo visible; exit 1"])
        .output()
        .unwrap();
    assert_eq!(out.status.code(), Some(1));
    assert_eq!(String::from_utf8_lossy(&out.stdout), "visible\n");
}

#[test]
fn failure_shows_stderr() {
    let out = quiet()
        .args(["sh", "-c", "echo oops >&2; exit 3"])
        .output()
        .unwrap();
    assert_eq!(out.status.code(), Some(3));
    assert_eq!(String::from_utf8_lossy(&out.stderr), "oops\n");
}

#[test]
fn preserves_exit_code() {
    let out = quiet().args(["sh", "-c", "exit 42"]).output().unwrap();
    assert_eq!(out.status.code(), Some(42));
}

#[test]
fn missing_command_exits_127() {
    let out = quiet()
        .args(["nonexistent_command_xyz_123"])
        .output()
        .unwrap();
    assert_eq!(out.status.code(), Some(127));
}

#[test]
fn no_args_shows_usage() {
    let out = quiet().output().unwrap();
    assert_eq!(out.status.code(), Some(1));
    assert!(String::from_utf8_lossy(&out.stderr).contains("Usage:"));
}

#[test]
fn double_dash_separator() {
    let out = quiet()
        .args(["--", "sh", "-c", "echo hidden"])
        .output()
        .unwrap();
    assert!(out.status.success());
    assert!(out.stdout.is_empty());
}

#[test]
fn timeout_shows_output_for_slow_command() {
    use std::io::Read;
    use std::process::Stdio;

    let start = Instant::now();
    let mut child = Command::new(env!("CARGO_BIN_EXE_quiet"))
        .args(["-t", "0.3", "sh", "-c", "echo early; sleep 2; echo late"])
        .stdout(Stdio::piped())
        .stderr(Stdio::null())
        .spawn()
        .unwrap();

    let mut stdout = child.stdout.take().unwrap();
    let mut buf = [0u8; 1024];
    let n = stdout.read(&mut buf).unwrap();
    let first_output_at = start.elapsed().as_secs_f64();
    assert!(n > 0);
    assert!(
        first_output_at >= 0.25 && first_output_at < 1.0,
        "first output arrived at {first_output_at:.2}s, expected ~0.3s"
    );

    let mut rest = String::from_utf8_lossy(&buf[..n]).to_string();
    let n = stdout.read_to_string(&mut rest).unwrap_or(0);
    let _ = n;
    let status = child.wait().unwrap();
    assert!(status.success());
    assert!(rest.contains("late"));
}

#[test]
fn timeout_suppresses_fast_success() {
    let out = quiet()
        .args(["-t", "5", "sh", "-c", "echo fast"])
        .output()
        .unwrap();
    assert!(out.status.success());
    assert!(out.stdout.is_empty());
}

#[test]
fn timeout_still_shows_on_failure() {
    let out = quiet()
        .args(["-t", "5", "sh", "-c", "echo fail; exit 1"])
        .output()
        .unwrap();
    assert_eq!(out.status.code(), Some(1));
    assert_eq!(String::from_utf8_lossy(&out.stdout), "fail\n");
}

#[test]
fn timeout_missing_value() {
    let out = quiet().args(["-t"]).output().unwrap();
    assert_eq!(out.status.code(), Some(1));
    let stderr = String::from_utf8_lossy(&out.stderr);
    assert!(stderr.contains("-t requires a numeric argument"), "{stderr}");
}

#[test]
fn timeout_invalid_value() {
    let out = quiet().args(["-t", "notanumber"]).output().unwrap();
    assert_eq!(out.status.code(), Some(1));
    let stderr = String::from_utf8_lossy(&out.stderr);
    assert!(stderr.contains("invalid timeout 'notanumber'"), "{stderr}");
}