with-watch 0.1.0

Watch command inputs and rerun commands when they change
Documentation
use std::{
    fs,
    path::Path,
    process::{Command as ProcessCommand, Stdio},
    thread,
    time::Duration,
};

use assert_cmd::Command;
use predicates::prelude::*;

fn with_watch_command() -> Command {
    Command::new(assert_cmd::cargo::cargo_bin!("with-watch"))
}

#[test]
fn help_lists_shell_and_exec_modes() {
    with_watch_command()
        .arg("--help")
        .assert()
        .success()
        .stdout(predicate::str::contains("--shell"))
        .stdout(predicate::str::contains("exec"))
        .stdout(predicate::str::contains("--no-hash"));
}

#[test]
fn pathless_passthrough_guides_users_to_exec_input() {
    with_watch_command()
        .args(["ls", "-l"])
        .assert()
        .failure()
        .stderr(predicate::str::contains(
            "No watch inputs could be inferred from the delegated command",
        ))
        .stderr(predicate::str::contains("with-watch exec --input"));
}

#[test]
fn shell_and_subcommand_cannot_be_combined() {
    with_watch_command()
        .args([
            "--shell",
            "echo hi",
            "exec",
            "--input",
            "src/**/*.rs",
            "--",
            "echo",
        ])
        .assert()
        .failure()
        .stderr(predicate::str::contains("cannot be combined"));
}

#[cfg(unix)]
#[test]
fn passthrough_mode_runs_a_posix_utility_once_with_test_hook() {
    let temp_dir = tempfile::tempdir().expect("create tempdir");
    let input_path = temp_dir.path().join("input.txt");
    fs::write(&input_path, "hello\n").expect("write input");

    with_watch_command()
        .env("WITH_WATCH_TEST_MAX_RUNS", "1")
        .arg("cat")
        .arg(&input_path)
        .assert()
        .success()
        .stdout(predicate::str::contains("hello"));
}

#[cfg(unix)]
#[test]
fn shell_mode_supports_operators_and_exits_after_one_run_with_test_hook() {
    let temp_dir = tempfile::tempdir().expect("create tempdir");
    let input_path = temp_dir.path().join("input.txt");
    fs::write(&input_path, "hello\n").expect("write input");

    with_watch_command()
        .env("WITH_WATCH_TEST_MAX_RUNS", "1")
        .args([
            "--shell",
            &format!("cat '{}' | grep hello", input_path.display()),
        ])
        .assert()
        .success()
        .stdout(predicate::str::contains("hello"));
}

#[cfg(unix)]
#[test]
fn exec_mode_reruns_when_an_explicit_input_changes() {
    let temp_dir = tempfile::tempdir().expect("create tempdir");
    let input_path = temp_dir.path().join("input.txt");
    let output_path = temp_dir.path().join("output.txt");
    fs::write(&input_path, "alpha\n").expect("write input");

    let mut child = ProcessCommand::new(assert_cmd::cargo::cargo_bin!("with-watch"))
        .current_dir(temp_dir.path())
        .env("WITH_WATCH_TEST_MAX_RUNS", "2")
        .env("WITH_WATCH_TEST_DEBOUNCE_MS", "25")
        .args([
            "exec",
            "--input",
            input_path.to_string_lossy().as_ref(),
            "--",
            "sh",
            "-c",
            &format!(
                "cat '{}' >> '{}'",
                input_path.display(),
                output_path.display()
            ),
        ])
        .stdin(Stdio::null())
        .stdout(Stdio::null())
        .stderr(Stdio::null())
        .spawn()
        .expect("spawn with-watch");

    wait_for_file_lines(&output_path, 1);
    thread::sleep(Duration::from_millis(50));
    fs::write(&input_path, "beta\n").expect("rewrite input");

    let status = child.wait().expect("wait for child");
    assert!(status.success());

    let output = fs::read_to_string(&output_path).expect("read output");
    let lines = output.lines().collect::<Vec<_>>();
    assert_eq!(lines, vec!["alpha", "beta"]);
}

#[cfg(unix)]
fn wait_for_file_lines(path: &Path, expected_lines: usize) {
    for _ in 0..80 {
        if let Ok(contents) = fs::read_to_string(path) {
            if contents.lines().count() >= expected_lines {
                return;
            }
        }
        thread::sleep(Duration::from_millis(25));
    }
    panic!(
        "timed out waiting for {expected_lines} lines in {}",
        path.display()
    );
}