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_r4_{}_{}", 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)
);
}
fn tick() {
std::thread::sleep(std::time::Duration::from_millis(50));
}
#[test]
fn test_triple_rev_equals_single_rev_listing() {
let dir = clean_dir();
push(&dir, "A\n");
tick();
push(&dir, "B\n");
tick();
push(&dir, "C\n");
let before = stdout_str(&run(&dir, &["-n"]));
run(&dir, &["--rev"]);
let after_one = stdout_str(&run(&dir, &["-n"]));
run(&dir, &["--rev"]);
run(&dir, &["--rev"]);
let after_three = stdout_str(&run(&dir, &["-n"]));
assert_eq!(
after_one, after_three,
"rev³ must equal rev¹; one={after_one:?} three={after_three:?}"
);
assert_ne!(
before, after_one,
"rev¹ must change order for distinct A,B,C; before={before:?} after={after_one:?}"
);
}
#[test]
fn test_sort_mtime_is_idempotent_after_one_sort() {
let dir = clean_dir();
push(&dir, "first\n");
tick();
push(&dir, "second\n");
tick();
push(&dir, "third\n");
let s1 = run(&dir, &["--sort", "mtime"]);
assert!(
s1.status.success(),
"first --sort mtime must succeed; stderr={}",
String::from_utf8_lossy(&s1.stderr)
);
let after_first = stdout_str(&run(&dir, &["-n"]));
let s2 = run(&dir, &["--sort", "mtime"]);
assert!(
s2.status.success(),
"second --sort mtime must succeed; stderr={}",
String::from_utf8_lossy(&s2.stderr)
);
let after_second = stdout_str(&run(&dir, &["-n"]));
assert_eq!(
after_first, after_second,
"--sort mtime must be idempotent on already-sorted input; first={after_first:?} second={after_second:?}"
);
}
#[test]
fn test_sort_size_is_idempotent_after_one_sort() {
let dir = clean_dir();
push(&dir, "x\n"); push(&dir, "xxxx\n"); push(&dir, "xx\n"); let s1 = run(&dir, &["--sort", "size"]);
assert!(
s1.status.success(),
"first --sort size must succeed; stderr={}",
String::from_utf8_lossy(&s1.stderr)
);
let after_first = stdout_str(&run(&dir, &["-n"]));
let s2 = run(&dir, &["--sort", "size"]);
assert!(s2.status.success(), "second --sort size must succeed");
let after_second = stdout_str(&run(&dir, &["-n"]));
assert_eq!(
after_first, after_second,
"--sort size must be idempotent; first={after_first:?} second={after_second:?}"
);
}
#[test]
fn test_count_reports_exact_push_count_for_one_three_five() {
let dir = clean_dir();
push(&dir, "one\n");
assert_eq!(
stdout_str(&run(&dir, &["-k"])).trim(),
"1",
"count after 1 push must be 1"
);
push(&dir, "two\n");
push(&dir, "three\n");
assert_eq!(
stdout_str(&run(&dir, &["-k"])).trim(),
"3",
"count after 3 pushes must be 3"
);
push(&dir, "four\n");
push(&dir, "five\n");
assert_eq!(
stdout_str(&run(&dir, &["-k"])).trim(),
"5",
"count after 5 pushes must be 5"
);
}
#[test]
fn test_size_subcommand_returns_purely_numeric_digits() {
let dir = clean_dir();
push(&dir, "hello\n"); let out = run(&dir, &["--size", "1"]);
assert!(
out.status.success(),
"--size 1 must succeed; stderr={}",
String::from_utf8_lossy(&out.stderr)
);
let body = stdout_str(&out);
let trimmed = body.trim();
assert!(
trimmed.chars().all(|c| c.is_ascii_digit()),
"--size must output only digits (no commas/suffix/decimal); got {trimmed:?}"
);
assert!(
!trimmed.is_empty(),
"--size must output at least one digit; got empty"
);
let n: usize = trimmed.parse().expect("size must parse as integer");
assert!(n >= 5, "--size on \"hello\\n\" must be >= 5; got {n}");
}
#[test]
fn test_cat_single_index_emits_content_verbatim_no_decoration() {
let dir = clean_dir();
push(&dir, "hello-cat-1\n");
let out = run(&dir, &["--cat", "1"]);
assert!(
out.status.success(),
"--cat 1 must succeed; stderr={}",
String::from_utf8_lossy(&out.stderr)
);
let body = stdout_str(&out);
assert!(
body.contains("hello-cat-1"),
"--cat 1 must emit the content; got {body:?}"
);
assert!(
!body.contains("==="),
"--cat 1 single arg must not add decorations; got {body:?}"
);
assert!(
!body.contains("FILE:"),
"--cat 1 single arg must not add 'FILE:' prefix; got {body:?}"
);
}