use std::fs;
use std::path::PathBuf;
use std::process::{Command, Stdio};
use std::sync::atomic::{AtomicU64, Ordering};
static COUNTER: AtomicU64 = AtomicU64::new(0);
fn bin() -> PathBuf {
let mut p = PathBuf::from(env!("CARGO_BIN_EXE_tp"));
if !p.exists() {
p = PathBuf::from(env!("CARGO_BIN_EXE_temprs"));
}
p
}
fn clean_dir() -> PathBuf {
let id = COUNTER.fetch_add(1, Ordering::SeqCst);
let d = std::env::temp_dir().join(format!("temprs_contract2_{}_{}", std::process::id(), id));
let _ = fs::remove_dir_all(&d);
d
}
fn run(dir: &PathBuf, args: &[&str]) -> std::process::Output {
Command::new(bin())
.env("TEMPRS_DIR", dir)
.args(args)
.stdin(Stdio::null())
.output()
.expect("spawn tp")
}
fn run_stdin(dir: &PathBuf, args: &[&str], input: &[u8]) -> std::process::Output {
use std::io::Write;
let mut child = Command::new(bin())
.env("TEMPRS_DIR", dir)
.args(args)
.stdin(Stdio::piped())
.stdout(Stdio::piped())
.stderr(Stdio::piped())
.spawn()
.expect("spawn tp");
child.stdin.as_mut().unwrap().write_all(input).unwrap();
child.wait_with_output().unwrap()
}
fn stdout_str(o: &std::process::Output) -> String {
String::from_utf8_lossy(&o.stdout).to_string()
}
fn push(dir: &PathBuf, content: &str) {
let out = run_stdin(dir, &["-q"], content.as_bytes());
assert!(
out.status.success(),
"push must succeed; stderr={}",
String::from_utf8_lossy(&out.stderr)
);
}
#[test]
fn test_input_index_zero_is_rejected() {
let dir = clean_dir();
push(&dir, "alpha\n");
let out = run_stdin(&dir, &["-i", "0"], b"replacement\n");
assert!(
!out.status.success(),
"-i 0 must reject zero (1-based indices start at 1); stdout={:?} stderr={:?}",
stdout_str(&out),
String::from_utf8_lossy(&out.stderr)
);
}
#[test]
fn test_input_index_above_stack_len_is_rejected() {
let dir = clean_dir();
push(&dir, "alpha\n");
let out = run_stdin(&dir, &["-i", "99"], b"replacement\n");
assert!(
!out.status.success(),
"-i 99 with only 1 entry must reject; stdout={:?} stderr={:?}",
stdout_str(&out),
String::from_utf8_lossy(&out.stderr)
);
}
#[test]
fn test_master_record_contains_absolute_paths_for_each_push() {
let dir = clean_dir();
push(&dir, "first\n");
push(&dir, "second\n");
let out = run(&dir, &["--master"]);
assert!(
out.status.success(),
"--master must succeed; stderr={}",
String::from_utf8_lossy(&out.stderr)
);
let body = stdout_str(&out);
let non_empty = body.lines().filter(|l| !l.trim().is_empty()).count();
assert!(
non_empty >= 1,
"--master after two pushes must emit at least one non-empty line; got {body:?}"
);
}
#[test]
fn test_cat_two_indices_preserves_argument_order_not_stack_order() {
let dir = clean_dir();
push(&dir, "first\n"); push(&dir, "second\n"); let out = run(&dir, &["--cat", "2", "1"]);
assert!(
out.status.success(),
"--cat 2 1 must succeed; stderr={}",
String::from_utf8_lossy(&out.stderr)
);
let body = stdout_str(&out);
assert!(
body.contains("first") && body.contains("second"),
"--cat must include both file contents; got {body:?}"
);
let pos_second = body.find("second").expect("second present");
let pos_first = body.find("first").expect("first present");
assert!(
pos_second < pos_first,
"--cat 2 1 must emit index 2 (second) before index 1 (first); got {body:?}"
);
}
#[test]
fn test_double_swap_is_identity_on_three_item_stack() {
let dir = clean_dir();
push(&dir, "A\n");
push(&dir, "B\n");
push(&dir, "C\n");
let before_listing = stdout_str(&run(&dir, &["-n"]));
let s1 = run(&dir, &["-S", "1", "3"]);
assert!(s1.status.success(), "first swap must succeed");
let s2 = run(&dir, &["-S", "1", "3"]);
assert!(s2.status.success(), "second swap must succeed");
let after_listing = stdout_str(&run(&dir, &["-n"]));
assert_eq!(
before_listing, after_listing,
"double-swap must restore listing; before={before_listing:?} after={after_listing:?}"
);
}
#[test]
fn test_dup_top_increments_count_by_one_on_two_item_stack() {
let dir = clean_dir();
push(&dir, "lower\n"); push(&dir, "upper\n"); let before_count = stdout_str(&run(&dir, &["-k"]));
assert_eq!(before_count.trim(), "2", "pre-dup count must be 2");
let dup = run(&dir, &["-x", "-1"]);
assert!(
dup.status.success(),
"--dup -1 must succeed; stderr={}",
String::from_utf8_lossy(&dup.stderr)
);
let after_count = stdout_str(&run(&dir, &["-k"]));
assert_eq!(
after_count.trim(),
"3",
"after dup, count must be 3; got {after_count:?}"
);
}
#[test]
fn test_mv_index_to_itself_is_noop_on_count_and_listing() {
let dir = clean_dir();
push(&dir, "x\n");
push(&dir, "y\n");
let before_listing = stdout_str(&run(&dir, &["-n"]));
let mv = run(&dir, &["-M", "1", "1"]);
let _ = mv.status; let after_count = stdout_str(&run(&dir, &["-k"]));
assert_eq!(
after_count.trim(),
"2",
"mv 1 1 must not change count; got {after_count:?}"
);
let after_listing = stdout_str(&run(&dir, &["-n"]));
assert_eq!(
before_listing, after_listing,
"mv 1 1 must not change listing; before={before_listing:?} after={after_listing:?}"
);
}
#[test]
fn test_sort_by_name_is_idempotent_when_already_sorted() {
let dir = clean_dir();
push(&dir, "alpha\n");
push(&dir, "bravo\n");
push(&dir, "charlie\n");
let s1 = run(&dir, &["--sort", "name"]);
assert!(
s1.status.success(),
"first sort must succeed; stderr={}",
String::from_utf8_lossy(&s1.stderr)
);
let listing_after_first = stdout_str(&run(&dir, &["-n"]));
let s2 = run(&dir, &["--sort", "name"]);
assert!(
s2.status.success(),
"second sort must succeed; stderr={}",
String::from_utf8_lossy(&s2.stderr)
);
let listing_after_second = stdout_str(&run(&dir, &["-n"]));
assert_eq!(
listing_after_first, listing_after_second,
"--sort name must be idempotent; first={listing_after_first:?} second={listing_after_second:?}"
);
}
#[test]
fn test_count_on_fresh_dir_reports_zero_without_initializing_phantom_entries() {
let dir = clean_dir();
let out = run(&dir, &["-k"]);
assert!(out.status.success(), "count on fresh dir must succeed");
let body = stdout_str(&out);
assert_eq!(body.trim(), "0", "fresh dir count must be 0; got {body:?}");
}