use std::process::Command;
fn run_rwatch(args: &[&str]) -> (bool, String, String) {
let output = Command::new(env!("CARGO_BIN_EXE_rwatch"))
.args(args)
.output()
.expect("failed to execute rwatch");
let stdout = String::from_utf8_lossy(&output.stdout).to_string();
let stderr = String::from_utf8_lossy(&output.stderr).to_string();
(output.status.success(), stdout, stderr)
}
fn run_rwatch_with_env(args: &[&str], key: &str, val: &str) -> (bool, String, String) {
let output = Command::new(env!("CARGO_BIN_EXE_rwatch"))
.args(args)
.env(key, val)
.output()
.expect("failed to execute rwatch");
let stdout = String::from_utf8_lossy(&output.stdout).to_string();
let stderr = String::from_utf8_lossy(&output.stderr).to_string();
(output.status.success(), stdout, stderr)
}
fn echo_cmd(text: &str) -> Vec<String> {
vec![format!("echo {}", text)]
}
fn exit_cmd(code: i32) -> Vec<String> {
vec![format!("exit {}", code)]
}
#[cfg(windows)]
fn exec_echo_cmd(text: &str) -> Vec<String> {
vec!["cmd.exe".into(), "/C".into(), "echo".into(), text.into()]
}
#[cfg(not(windows))]
fn exec_echo_cmd(text: &str) -> Vec<String> {
vec!["/bin/echo".into(), text.into()]
}
fn has_diff_line(output: &str, prefix: char, text: &str) -> bool {
output.lines().any(|l| {
l.strip_prefix(prefix)
.map_or(false, |rest| rest.trim() == text)
})
}
#[test]
fn test_has_diff_line_unit() {
assert!(has_diff_line("abc\n+foo\ndef", '+', "foo"));
assert!(has_diff_line("abc\n foo\ndef", ' ', "foo"));
assert!(has_diff_line("+foo", '+', "foo"));
assert!(has_diff_line(" foo", ' ', "foo"));
assert!(has_diff_line("header\n+foo\r\n", '+', "foo"));
assert!(!has_diff_line("abc\n foo\n", '+', "foo"), "wrong prefix");
}
#[test]
fn rwatch_runs_and_exits() {
let mut args: Vec<String> = vec!["--chgexit".into(), "--".into()];
args.extend(echo_cmd("hello"));
let args_ref: Vec<&str> = args.iter().map(|s| s.as_str()).collect();
let (ok, out, _) = run_rwatch(&args_ref);
assert!(ok, "rwatch did not exit successfully");
assert!(out.to_lowercase().contains("hello"), "output: {}", out);
}
#[test]
fn test_basic_echo() {
let mut args: Vec<String> = vec!["--chgexit".into(), "--".into()];
args.extend(echo_cmd("hello"));
let args_ref: Vec<&str> = args.iter().map(|s| s.as_str()).collect();
let (ok, out, _) = run_rwatch(&args_ref);
assert!(ok);
assert!(out.to_lowercase().contains("hello"));
}
#[test]
fn test_diff_flag() {
let mut args: Vec<String> = vec!["-d".into(), "--chgexit".into(), "--".into()];
args.extend(echo_cmd("foo"));
let args_ref: Vec<&str> = args.iter().map(|s| s.as_str()).collect();
let (ok, out, _) = run_rwatch(&args_ref);
assert!(ok);
assert!(has_diff_line(&out, '+', "foo"),
"diff output should mark first run as added; lines: {:?}",
out.lines().collect::<Vec<_>>());
}
#[test]
fn test_diff_permanent_basic() {
let mut args: Vec<String> = vec!["-d=permanent".into(), "--chgexit".into(), "--".into()];
args.extend(echo_cmd("bar"));
let args_ref: Vec<&str> = args.iter().map(|s| s.as_str()).collect();
let (ok, out, _) = run_rwatch(&args_ref);
assert!(ok);
assert!(has_diff_line(&out, ' ', "bar") || has_diff_line(&out, '+', "bar"),
"diff output should contain bar; lines: {:?}", out.lines().collect::<Vec<_>>());
}
#[test]
fn test_permanent_diff_first_run_is_base() {
let text = "perm_diff_sentinel";
let mut args: Vec<String> = vec!["-d=permanent".into(), "--chgexit".into(), "--".into()];
args.extend(echo_cmd(text));
let args_ref: Vec<&str> = args.iter().map(|s| s.as_str()).collect();
let (ok, out, _) = run_rwatch(&args_ref);
assert!(ok);
assert!(
has_diff_line(&out, ' ', text),
"permanent diff: first run output becomes the base, so diff(base, output) = Same \
(space prefix); lines: {:?}",
out.lines().collect::<Vec<_>>()
);
assert!(
!has_diff_line(&out, '+', text),
"permanent diff: first run should NOT appear as Added (+); lines: {:?}",
out.lines().collect::<Vec<_>>()
);
}
#[test]
fn test_regular_diff_first_run_is_added() {
let text = "regular_diff_sentinel";
let mut args: Vec<String> = vec!["-d".into(), "--chgexit".into(), "--".into()];
args.extend(echo_cmd(text));
let args_ref: Vec<&str> = args.iter().map(|s| s.as_str()).collect();
let (ok, out, _) = run_rwatch(&args_ref);
assert!(ok);
assert!(
has_diff_line(&out, '+', text),
"regular diff: first run should be Added since prev is empty; lines: {:?}",
out.lines().collect::<Vec<_>>()
);
assert!(
!has_diff_line(&out, ' ', text),
"regular diff: first run should NOT be Same; lines: {:?}",
out.lines().collect::<Vec<_>>()
);
}
#[test]
fn test_color_flag() {
let mut args: Vec<String> = vec!["-c".into(), "--chgexit".into(), "--".into()];
args.extend(echo_cmd("\x1b[31mred\x1b[0m"));
let args_ref: Vec<&str> = args.iter().map(|s| s.as_str()).collect();
let (ok, out, _) = run_rwatch(&args_ref);
assert!(ok);
assert!(out.contains("\x1b[31mred\x1b[0m") || out.contains("[31mred"),
"color mode should preserve ANSI sequences: {}", out);
}
#[test]
fn test_no_color_strips_ansi() {
let mut args: Vec<String> = vec!["--chgexit".into(), "--".into()];
args.extend(echo_cmd("\x1b[31mred\x1b[0m"));
let args_ref: Vec<&str> = args.iter().map(|s| s.as_str()).collect();
let (ok, out, _) = run_rwatch(&args_ref);
assert!(ok);
let content = out.lines().rev().find(|l| !l.trim().is_empty()).unwrap_or("");
assert!(content.to_lowercase().contains("red"), "output should still contain 'red': {}", out);
assert!(!content.contains("\x1b[31m"), "ANSI escape should be stripped without -c: {}", out);
}
#[test]
fn test_chgexit_flag() {
let mut args: Vec<String> = vec!["--chgexit".into(), "--".into()];
args.extend(echo_cmd("chgexit_val"));
let args_ref: Vec<&str> = args.iter().map(|s| s.as_str()).collect();
let (ok, out, _) = run_rwatch(&args_ref);
assert!(ok);
assert!(out.contains("chgexit_val"), "output: {}", out);
}
#[test]
fn test_equexit_flag() {
let mut args: Vec<String> = vec!["-q".into(), "2".into(), "--".into()];
args.extend(echo_cmd("same_value"));
let args_ref: Vec<&str> = args.iter().map(|s| s.as_str()).collect();
let (ok, out, _) = run_rwatch(&args_ref);
assert!(ok);
assert!(out.contains("same_value"), "output: {}", out);
}
#[test]
fn test_exit_code_zero_on_success() {
let mut args: Vec<String> = vec!["-q".into(), "1".into(), "--".into()];
args.extend(echo_cmd("ok"));
let args_ref: Vec<&str> = args.iter().map(|s| s.as_str()).collect();
let (ok, _, err) = run_rwatch(&args_ref);
assert!(ok, "successful command should exit 0; stderr: {}", err);
}
#[test]
fn test_exit_code_nonzero_on_failure() {
let mut args: Vec<String> = vec!["-q".into(), "1".into(), "--".into()];
args.extend(exit_cmd(1));
let args_ref: Vec<&str> = args.iter().map(|s| s.as_str()).collect();
let (ok, _, _) = run_rwatch(&args_ref);
assert!(!ok, "failing command should exit non-zero");
}
#[test]
fn test_equexit_success_exits_zero() {
let mut args: Vec<String> = vec!["-q".into(), "1".into(), "--".into()];
args.extend(echo_cmd("should_be_zero"));
let args_ref: Vec<&str> = args.iter().map(|s| s.as_str()).collect();
let (ok, _, err) = run_rwatch(&args_ref);
assert!(ok, "equexit with successful command should exit 0; stderr: {}", err);
}
#[test]
fn test_no_title_flag() {
let mut args: Vec<String> = vec!["-t".into(), "--chgexit".into(), "--".into()];
args.extend(echo_cmd("foo"));
let args_ref: Vec<&str> = args.iter().map(|s| s.as_str()).collect();
let (ok, out, _) = run_rwatch(&args_ref);
assert!(ok);
assert!(!out.contains("Every "), "no-title should suppress header: {}", out);
}
#[test]
fn test_no_wrap_flag() {
let long = "a".repeat(200);
let mut args: Vec<String> = vec!["-w".into(), "--chgexit".into(), "--".into()];
args.extend(echo_cmd(&long));
let args_ref: Vec<&str> = args.iter().map(|s| s.as_str()).collect();
let (ok, out, _) = run_rwatch(&args_ref);
assert!(ok);
assert!(out.contains(&long), "no-wrap should not truncate long lines");
}
#[test]
fn test_long_line_no_panic() {
let long = "x".repeat(500);
let mut args: Vec<String> = vec!["--chgexit".into(), "--".into()];
args.extend(echo_cmd(&long));
let args_ref: Vec<&str> = args.iter().map(|s| s.as_str()).collect();
let (ok, _, err) = run_rwatch(&args_ref);
assert!(ok, "long line should not panic; stderr: {}", err);
}
#[test]
fn test_title_shows_interval() {
let mut args: Vec<String> = vec!["-n".into(), "0.5".into(), "--chgexit".into(), "--".into()];
args.extend(echo_cmd("itest"));
let args_ref: Vec<&str> = args.iter().map(|s| s.as_str()).collect();
let (ok, out, _) = run_rwatch(&args_ref);
assert!(ok);
assert!(out.contains("0.5s"), "title should show 0.5s interval: {}", out);
}
#[test]
fn test_watch_interval_env_var() {
let mut args: Vec<String> = vec!["--chgexit".into(), "--".into()];
args.extend(echo_cmd("env_interval_val"));
let args_ref: Vec<&str> = args.iter().map(|s| s.as_str()).collect();
let (ok, out, _) = run_rwatch_with_env(&args_ref, "WATCH_INTERVAL", "0.3");
assert!(ok);
assert!(out.contains("env_interval_val"), "output: {}", out);
assert!(out.contains("0.3s"), "title should show env-var interval 0.3s: {}", out);
}
#[test]
fn test_cli_interval_overrides_env_var() {
let mut args: Vec<String> = vec!["-n".into(), "0.7".into(), "--chgexit".into(), "--".into()];
args.extend(echo_cmd("override_test"));
let args_ref: Vec<&str> = args.iter().map(|s| s.as_str()).collect();
let (ok, out, _) = run_rwatch_with_env(&args_ref, "WATCH_INTERVAL", "9.9");
assert!(ok);
assert!(out.contains("0.7s"), "CLI interval should override env var: {}", out);
assert!(!out.contains("9.9s"), "env-var interval should not appear when -n is set: {}", out);
}
#[test]
fn test_exec_flag() {
let mut args: Vec<String> = vec!["-x".into(), "--chgexit".into(), "--".into()];
args.extend(exec_echo_cmd("exec_output"));
let args_ref: Vec<&str> = args.iter().map(|s| s.as_str()).collect();
let (ok, out, _) = run_rwatch(&args_ref);
assert!(ok);
assert!(out.to_lowercase().contains("exec_output"), "exec output: {}", out);
}
#[test]
fn test_beep_flag_no_crash() {
let mut args: Vec<String> = vec!["-b".into(), "--chgexit".into(), "--".into()];
args.extend(echo_cmd("beep_test"));
let args_ref: Vec<&str> = args.iter().map(|s| s.as_str()).collect();
let (ok, _, _) = run_rwatch(&args_ref);
assert!(ok, "beep flag should not cause a crash on success");
}
#[cfg(windows)]
#[test]
fn test_powershell_flag() {
let args: Vec<&str> = vec!["--powershell", "--chgexit", "--", "Write-Output pshell_works"];
let (ok, out, err) = run_rwatch(&args);
assert!(ok, "rwatch --powershell did not exit successfully: {}", err);
assert!(out.to_lowercase().contains("pshell_works"), "output: {}", out);
}
#[cfg(windows)]
#[test]
fn test_powershell_exit_code() {
let args: Vec<&str> = vec!["--powershell", "--equexit", "1", "--", "exit 42"];
let (ok, _, _) = run_rwatch(&args);
assert!(!ok, "rwatch should propagate non-zero PowerShell exit code");
}
#[cfg(windows)]
#[test]
fn test_powershell_overrides_cmd() {
let args: Vec<&str> = vec![
"--powershell",
"--chgexit",
"--",
"Write-Output ps_only_marker",
];
let (ok, out, err) = run_rwatch(&args);
assert!(ok, "PowerShell command failed: {}", err);
assert!(out.contains("ps_only_marker"), "output: {}", out);
}