#![cfg(not(coverage_nightly))]
#![expect(
clippy::tests_outside_test_module,
reason = "Cargo integration tests live at the file's module root"
)]
#![expect(
clippy::unwrap_used,
reason = "a generator/materialization failure should abort the test loudly"
)]
use std::ffi::{OsStr, OsString};
use std::os::unix::ffi::OsStrExt;
use std::process::ExitCode;
use proptest::prelude::*;
use proptest::test_runner::{Config, RngAlgorithm, TestRng, TestRunner};
use tempfile::TempDir;
fn check<S>(strategy: S, test: impl Fn(S::Value) -> Result<(), TestCaseError>)
where
S: Strategy,
S::Value: std::fmt::Debug,
{
let cases = std::env::var("PROPTEST_CASES")
.ok()
.and_then(|s| s.parse().ok())
.unwrap_or(128);
let mut config = Config::with_cases(cases);
config.failure_persistence = None;
let mut runner =
TestRunner::new_with_rng(config, TestRng::deterministic_rng(RngAlgorithm::ChaCha));
runner
.run(&strategy, test)
.expect("property should hold for every generated tree");
}
#[derive(Clone, Debug)]
enum Node {
File,
Dir,
Symlink(Vec<u8>),
}
fn wild_byte() -> impl Strategy<Value = u8> {
(1u8..=0x7f).prop_filter("not /", |b| *b != b'/')
}
fn wild_bytes(max: usize) -> impl Strategy<Value = Vec<u8>> {
prop::collection::vec(wild_byte(), 0..max)
}
fn node_strategy() -> impl Strategy<Value = Node> {
let symlink = prop::collection::vec(wild_byte(), 1..6)
.prop_filter("not . or ..", |t| {
t.as_slice() != b"." && t.as_slice() != b".."
})
.prop_map(Node::Symlink);
prop_oneof![
3 => Just(Node::File),
2 => Just(Node::Dir),
2 => symlink,
]
}
type EntrySpec = (Vec<u8>, Vec<u8>, Node);
fn entry_strategy() -> impl Strategy<Value = EntrySpec> {
(
prop::collection::vec(0u8..2, 0..=2),
wild_bytes(6),
node_strategy(),
)
}
fn leaf_name(idx: usize, raw: &[u8]) -> Vec<u8> {
let mut name = format!("{idx}_").into_bytes();
name.extend_from_slice(raw);
name
}
fn materialize(entries: &[EntrySpec]) -> TempDir {
let tmp = tempfile::tempdir().unwrap();
for (i, (dirs, raw, node)) in entries.iter().enumerate() {
let mut parent = tmp.path().to_path_buf();
for d in dirs {
parent.push(format!("g{d}"));
}
std::fs::create_dir_all(&parent).unwrap();
let path = parent.join(OsStr::from_bytes(&leaf_name(i, raw)));
match node {
Node::File => {
std::fs::write(&path, b"x").unwrap();
}
Node::Dir => {
std::fs::create_dir(&path).unwrap();
}
Node::Symlink(target) => {
std::os::unix::fs::symlink(OsStr::from_bytes(target), &path).unwrap();
}
}
}
tmp
}
fn code_repr(code: ExitCode) -> String {
format!("{code:?}")
}
fn run(args: &[&OsStr]) -> (ExitCode, Vec<u8>, Vec<u8>) {
let owned: Vec<OsString> = args.iter().map(|a| a.to_os_string()).collect();
let mut out = Vec::new();
let mut err = Vec::new();
let code = freshl::run(owned, &mut out, &mut err);
(code, out, err)
}
fn contains(haystack: &[u8], needle: &[u8]) -> bool {
!needle.is_empty() && haystack.windows(needle.len()).any(|w| w == needle)
}
fn strip_ansi(line: &[u8]) -> Vec<u8> {
let mut out = Vec::with_capacity(line.len());
let mut it = line.iter().copied();
while let Some(b) = it.next() {
if b == 0x1b {
for c in it.by_ref() {
if c == b'm' {
break;
}
}
} else {
out.push(b);
}
}
out
}
fn is_iso(w: &[u8]) -> bool {
let d = |i: usize| w.get(i).is_some_and(u8::is_ascii_digit);
let lit = |i: usize, c: u8| w.get(i) == Some(&c);
d(0) && d(1)
&& d(2)
&& d(3)
&& lit(4, b'-')
&& d(5)
&& d(6)
&& lit(7, b'-')
&& d(8)
&& d(9)
&& lit(10, b'T')
&& d(11)
&& d(12)
&& lit(13, b':')
&& d(14)
&& d(15)
&& lit(16, b':')
&& d(17)
&& d(18)
&& lit(19, b'Z')
}
fn iso_offset(stripped: &[u8]) -> Option<usize> {
stripped.windows(20).position(is_iso)
}
#[test]
fn recursive_listing_is_robust_deterministic_and_byte_faithful() {
check(prop::collection::vec(entry_strategy(), 0..=8), |entries| {
let tmp = materialize(&entries);
let root = tmp.path().as_os_str();
let dash_r = OsStr::new("-R");
let (code, out, err) = run(&[dash_r, root]);
prop_assert_eq!(
code_repr(code),
code_repr(ExitCode::SUCCESS),
"non-success exit; stderr: {}",
String::from_utf8_lossy(&err)
);
let (_, rerun, _) = run(&[dash_r, root]);
prop_assert!(out == rerun, "output was not deterministic");
for (i, (_, raw, _)) in entries.iter().enumerate() {
let name = leaf_name(i, raw);
prop_assert!(
contains(&out, &name),
"name {:?} missing from output",
String::from_utf8_lossy(&name)
);
}
Ok(())
});
}
const TAME: &[u8] = b"abAB019.- _zZ";
fn tame_entry() -> impl Strategy<Value = (Vec<u8>, Node)> {
let name = prop::collection::vec(prop::sample::select(TAME), 0..6);
let node = prop_oneof![
Just(Node::File),
Just(Node::Dir),
prop::collection::vec(prop::sample::select(TAME), 1..5).prop_map(Node::Symlink),
];
(name, node)
}
#[test]
fn columns_are_aligned_across_a_block() {
check(prop::collection::vec(tame_entry(), 0..=8), |entries| {
let flat: Vec<EntrySpec> = entries
.into_iter()
.map(|(name, node)| (Vec::new(), name, node))
.collect();
let tmp = materialize(&flat);
let (code, out, _) = run(&[tmp.path().as_os_str()]);
prop_assert_eq!(code_repr(code), code_repr(ExitCode::SUCCESS));
let lines: Vec<&[u8]> = out
.split(|&b| b == b'\n')
.filter(|l| !l.is_empty())
.collect();
prop_assert_eq!(lines.len(), flat.len(), "one row per entry expected");
let mut offsets = Vec::new();
for line in &lines {
let stripped = strip_ansi(line);
let off = iso_offset(&stripped);
prop_assert!(off.is_some(), "row has no mtime column: {:?}", stripped);
offsets.push(off);
}
if let Some(first) = offsets.first() {
prop_assert!(
offsets.iter().all(|o| o == first),
"mtime columns not aligned: {offsets:?}"
);
}
Ok(())
});
}