#[cfg(any(unix, windows))]
use crate::backends::typed::CommandInvocation;
#[cfg(any(unix, windows))]
use crate::execution::TEST_ENV_LOCK;
#[cfg(any(unix, windows))]
use crate::execution::command::run_invocation_capture;
#[cfg(windows)]
use crate::execution::command::run_shell_capture;
use crate::execution::command::{
PROGRESS_OUTPUT_LIMIT, push_tail, recent_output_line, strip_sudo_from_apt_commands,
};
use crate::ui::progress::{
ProgressMessage, ProgressSnapshot, apply_progress_message, progress_summary_lines,
};
#[cfg(unix)]
use crate::util::{now_secs, shell_quote};
use std::collections::BTreeMap;
#[cfg(windows)]
#[test]
fn typed_capture_executes_windows_cmd_shims_from_path() {
let _guard = TEST_ENV_LOCK.lock().unwrap();
let root = std::env::temp_dir().join(format!("bot-forge-cmd-shim-{}", std::process::id()));
let _ = std::fs::remove_dir_all(&root);
std::fs::create_dir_all(&root).unwrap();
std::fs::write(root.join("demo"), "#!/bin/sh\necho wrong\n").unwrap();
std::fs::write(root.join("demo.cmd"), "@echo 1.4.1\r\n").unwrap();
let path = std::env::var_os("PATH");
let path_ext = std::env::var_os("PATHEXT");
crate::util::set_process_var("PATH", &root);
crate::util::set_process_var("PATHEXT", ".COM;.EXE;.BAT;.CMD");
let result = run_invocation_capture(&CommandInvocation {
program: "demo".into(),
args: vec!["--version".into()],
env: BTreeMap::new(),
current_dir: None,
clear_env: false,
null_stdin: false,
timeout_secs: Some(2),
inactivity_timeout_secs: None,
idempotent: true,
success_codes: vec![0],
stdout_contains: None,
});
match path {
Some(value) => crate::util::set_process_var("PATH", value),
None => crate::util::remove_process_var("PATH"),
}
match path_ext {
Some(value) => crate::util::set_process_var("PATHEXT", value),
None => crate::util::remove_process_var("PATHEXT"),
}
std::fs::remove_dir_all(root).unwrap();
assert_eq!(result.unwrap().trim(), "1.4.1");
}
#[cfg(unix)]
fn test_invocation(
script: &str,
success_codes: Vec<i32>,
timeout_secs: Option<u64>,
) -> CommandInvocation {
CommandInvocation {
program: "sh".into(),
args: vec!["-c".into(), script.into()],
env: BTreeMap::new(),
current_dir: None,
clear_env: false,
null_stdin: false,
timeout_secs,
inactivity_timeout_secs: None,
idempotent: true,
success_codes,
stdout_contains: None,
}
}
#[cfg(unix)]
#[test]
fn typed_capture_honors_configured_success_codes() {
let _guard = TEST_ENV_LOCK.lock().unwrap();
assert!(run_invocation_capture(&test_invocation("exit 7", vec![7], Some(2))).is_ok());
assert!(run_invocation_capture(&test_invocation("exit 8", vec![7], Some(2))).is_err());
}
#[cfg(unix)]
#[test]
fn typed_capture_honors_stdout_predicates() {
let _guard = TEST_ENV_LOCK.lock().unwrap();
let mut invocation = test_invocation("printf rust-src", vec![0], Some(2));
invocation.stdout_contains = Some("rust-src".into());
assert!(run_invocation_capture(&invocation).is_ok());
invocation.stdout_contains = Some("miri".into());
assert!(run_invocation_capture(&invocation).is_err());
}
#[cfg(unix)]
#[test]
fn typed_capture_terminates_on_timeout() {
let _guard = TEST_ENV_LOCK.lock().unwrap();
let started = std::time::Instant::now();
let error = run_invocation_capture(&test_invocation("sleep 5", vec![0], Some(1))).unwrap_err();
assert!(error.to_string().contains("timed out"));
assert!(started.elapsed() < std::time::Duration::from_secs(3));
}
#[cfg(unix)]
#[test]
fn typed_capture_timeout_terminates_the_entire_process_group() {
let _guard = TEST_ENV_LOCK.lock().unwrap();
let marker = std::env::temp_dir().join(format!(
"bot-forge-process-tree-{}-{}",
std::process::id(),
now_secs()
));
let script = format!("(sleep 2; printf orphan > {}) & wait", shell_quote(&marker));
let error = run_invocation_capture(&test_invocation(&script, vec![0], Some(1))).unwrap_err();
assert!(error.to_string().contains("timed out"));
std::thread::sleep(std::time::Duration::from_millis(1500));
assert!(
!marker.exists(),
"timeout left a descendant process running"
);
}
#[test]
fn strips_sudo_only_from_apt_commands() {
assert_eq!(
strip_sudo_from_apt_commands("sudo apt-get update"),
"apt-get update"
);
assert_eq!(
strip_sudo_from_apt_commands("cd /tmp && sudo apt install -y cmake"),
"cd /tmp && apt install -y cmake"
);
assert_eq!(
strip_sudo_from_apt_commands("sudo systemctl restart demo"),
"sudo systemctl restart demo"
);
}
#[test]
fn recent_output_line_keeps_the_latest_nonempty_line() {
assert_eq!(recent_output_line("line1\nline2\n\nline3\n"), "line3");
}
#[test]
fn recent_output_line_reports_empty_output() {
assert_eq!(recent_output_line("\n\r\n"), "no command output");
}
#[test]
fn command_output_buffer_keeps_only_the_tail() {
let mut output = String::new();
push_tail(&mut output, &"x".repeat(PROGRESS_OUTPUT_LIMIT * 2));
assert_eq!(output.len(), PROGRESS_OUTPUT_LIMIT);
}
#[test]
fn progress_summary_includes_all_active_commands() {
let active = BTreeMap::from([
(
1,
ProgressSnapshot {
id: 1,
label: "demo-tool".into(),
step: Some((2, 8)),
elapsed: 123,
recent: "recent output".into(),
frame: 0,
},
),
(
2,
ProgressSnapshot {
id: 2,
label: "other-tool".into(),
step: None,
elapsed: 5,
recent: "Compiling demo".into(),
frame: 1,
},
),
]);
let lines = progress_summary_lines(&active);
let line = lines.join("\n");
assert_eq!(lines.len(), 3);
assert!(line.contains("2 running"));
assert!(line.contains("demo-tool"));
assert!(line.contains("2/8"));
assert!(line.contains("123s"));
assert!(line.contains("recent output"));
assert!(line.contains("other-tool"));
}
#[test]
fn progress_summary_omits_unknown_zero_progress() {
let active = BTreeMap::from([(
1,
ProgressSnapshot {
id: 1,
label: "demo-tool".into(),
step: None,
elapsed: 5,
recent: "Compiling demo".into(),
frame: 0,
},
)]);
let line = progress_summary_lines(&active).join("\n");
assert!(line.contains("demo-tool"));
assert!(!line.contains("0/0"));
}
#[test]
fn progress_pause_hides_updates_until_output_is_complete() {
let mut active = BTreeMap::from([(
1,
ProgressSnapshot {
id: 1,
label: "demo-tool".into(),
step: None,
elapsed: 5,
recent: "Compiling demo".into(),
frame: 0,
},
)]);
let mut acknowledgements = Vec::new();
let mut pause_depth = 0;
let (sender, _receiver) = std::sync::mpsc::sync_channel(0);
apply_progress_message(
ProgressMessage::Pause(sender),
&mut active,
&mut acknowledgements,
&mut pause_depth,
);
assert_eq!(pause_depth, 1);
assert_eq!(acknowledgements.len(), 1);
apply_progress_message(
ProgressMessage::Resume,
&mut active,
&mut acknowledgements,
&mut pause_depth,
);
assert_eq!(pause_depth, 0);
assert_eq!(active.len(), 1);
}
#[cfg(windows)]
#[test]
fn preserves_quoted_powershell_command_on_windows() {
let output = run_shell_capture(
"%SystemRoot%\\System32\\WindowsPowerShell\\v1.0\\powershell.exe -NoProfile -NonInteractive -Command \"$value = 'quoted-command-ok'; Write-Output $value\"",
)
.unwrap();
assert_eq!(output.trim(), "quoted-command-ok");
}