#![cfg(unix)]
use std::fs;
use std::path::{Path, PathBuf};
use std::process::Command;
fn bin() -> PathBuf {
PathBuf::from(env!("CARGO_BIN_EXE_f00"))
}
fn system_ls() -> Option<&'static str> {
for p in ["/run/current-system/sw/bin/ls", "/usr/bin/ls", "/bin/ls"] {
if Path::new(p).is_file() {
let out = Command::new(p).arg("--version").output().ok()?;
let v = String::from_utf8_lossy(&out.stdout);
if v.contains("coreutils") || v.contains("GNU") {
return Some(p);
}
}
}
None
}
fn temp_dir(label: &str) -> PathBuf {
let base = std::env::temp_dir().join(format!(
"f00-byte-{}-{}-{}",
label,
std::process::id(),
std::time::SystemTime::now()
.duration_since(std::time::UNIX_EPOCH)
.map(|d| d.as_nanos())
.unwrap_or(0)
));
fs::create_dir_all(&base).expect("mkdir");
base
}
fn empty_config(fixture: &Path) -> PathBuf {
let p = fixture.parent().unwrap_or(fixture).join(format!(
"f00-byte-cfg-{}-{}.toml",
std::process::id(),
fixture.file_name().and_then(|n| n.to_str()).unwrap_or("x")
));
fs::write(&p, "").unwrap();
p
}
fn build_fixture() -> (PathBuf, PathBuf) {
let dir = temp_dir("fx");
fs::create_dir(dir.join("empty_dir")).unwrap();
fs::create_dir(dir.join("has_child")).unwrap();
fs::write(dir.join("has_child").join("c"), b"c").unwrap();
fs::write(dir.join("a"), b"").unwrap();
fs::write(dir.join("file1"), b"").unwrap();
fs::write(dir.join("file2"), b"").unwrap();
fs::write(dir.join("file10"), b"").unwrap();
fs::write(dir.join("file~"), b"").unwrap();
fs::write(dir.join("x y"), b"sp").unwrap();
fs::write(dir.join("small"), b"a").unwrap();
fs::write(dir.join("large"), vec![0u8; 4096]).unwrap();
#[cfg(unix)]
{
std::os::unix::fs::symlink("a", dir.join("link")).unwrap();
let sh = dir.join("exec.sh");
fs::write(&sh, b"#!/bin/sh\n").unwrap();
use std::os::unix::fs::PermissionsExt;
let mut perms = fs::metadata(&sh).unwrap().permissions();
perms.set_mode(0o755);
fs::set_permissions(&sh, perms).unwrap();
}
let cfg = empty_config(&dir);
(dir, cfg)
}
fn normalize(s: &str) -> String {
s.lines()
.map(|l| l.trim_end())
.filter(|l| !l.is_empty())
.collect::<Vec<_>>()
.join("\n")
}
fn assert_ls_match(label: &str, ls: &str, dir: &Path, cfg: &Path, args: &[&str]) {
let mut g = Command::new(ls);
g.env("LC_ALL", "C")
.env("TZ", "UTC")
.env("NO_COLOR", "1")
.env("LS_COLORS", "")
.arg("--color=never")
.args(args)
.arg(dir);
let gout = g.output().expect("run system ls");
let mut f = Command::new(bin());
f.env("LC_ALL", "C")
.env("TZ", "UTC")
.env("NO_COLOR", "1")
.args([
"--gnu",
"--color=never",
"--git=false",
"--icons=never",
"--config",
])
.arg(cfg)
.args(args)
.arg(dir);
let fout = f.output().expect("run f00");
assert_eq!(
gout.status.code(),
fout.status.code(),
"{label}: exit mismatch\nGNU stderr={}\nf00 stderr={}",
String::from_utf8_lossy(&gout.stderr),
String::from_utf8_lossy(&fout.stderr)
);
let gs = normalize(&String::from_utf8_lossy(&gout.stdout));
let fs = normalize(&String::from_utf8_lossy(&fout.stdout));
assert_eq!(
gs, fs,
"{label}: stdout mismatch\n--- GNU ---\n{gs}\n--- f00 ---\n{fs}"
);
}
#[test]
fn byte_parity_core_modes() {
let Some(ls) = system_ls() else {
eprintln!("skip: no GNU coreutils ls on PATH");
return;
};
let (dir, cfg) = build_fixture();
let cases: &[(&str, &[&str])] = &[
("-1", &["-1"]),
("-1a", &["-1a"]),
("-1A", &["-1A"]),
("-1S", &["-1S"]),
("-1v", &["-1v"]),
("-1 --sort=width", &["-1", "--sort=width"]),
("-1b", &["-1b"]),
(
"-1 --group-directories-first",
&["-1", "--group-directories-first"],
),
("-1F", &["-1F"]),
("-1p", &["-1p"]),
("-1R", &["-1R"]),
("-s1", &["-s1"]),
("-l --time-style=long-iso", &["-l", "--time-style=long-iso"]),
];
for (label, args) in cases {
assert_ls_match(label, ls, &dir, &cfg, args);
}
let mut g = Command::new(ls);
g.current_dir(&dir)
.env("LC_ALL", "C")
.env("TZ", "UTC")
.env("NO_COLOR", "1")
.args(["--color=never", "-s1", "large"]);
let gout = g.output().unwrap();
let mut f = Command::new(bin());
f.current_dir(&dir)
.env("LC_ALL", "C")
.env("TZ", "UTC")
.env("NO_COLOR", "1")
.args([
"--gnu",
"--color=never",
"--git=false",
"--icons=never",
"--config",
])
.arg(&cfg)
.args(["-s1", "large"]);
let fout = f.output().unwrap();
assert_eq!(gout.status.code(), fout.status.code());
assert_eq!(
normalize(&String::from_utf8_lossy(&gout.stdout)),
normalize(&String::from_utf8_lossy(&fout.stdout)),
"single-file -s1"
);
let _ = fs::remove_dir_all(&dir);
}