#![cfg(feature = "cli")]
use clap::Parser;
use luff::walker::WalkerItem;
use luff::{cli::Args, config::Config, walker::Walker};
use serial_test::serial;
use std::env;
use std::fs;
use std::time::Duration;
use tempfile::TempDir;
fn create_test_file(dir: &TempDir, name: &str) -> std::path::PathBuf {
let path = dir.path().join(name);
fs::write(&path, "content").unwrap();
path
}
#[test]
#[serial]
fn test_streaming_walker_memory_efficient() {
unsafe {
env::remove_var("LUFF_OUTPUT_PROTECTION_MS");
}
let temp = TempDir::new().unwrap();
for i in 0..1000 {
let _ = create_test_file(&temp, &format!("file_{i}.txt"));
}
env::set_current_dir(temp.path()).unwrap();
let args = Args::parse_from(["test"]);
let config = Config::from_args(&args).unwrap();
let walker = Walker::from_dir(&config).unwrap();
let mut count = 0;
for item in walker {
let entry = match item {
luff::walker::WalkerItem::Entry(e) => e,
luff::walker::WalkerItem::Error(_) => continue,
};
if entry.is_dir {
continue;
}
assert!(!entry.path.to_string_lossy().is_empty());
count += 1;
}
assert_eq!(count, 1000);
}
#[test]
#[serial]
fn test_max_files_limit_enforced() {
unsafe {
env::remove_var("LUFF_OUTPUT_PROTECTION_MS");
}
let temp = TempDir::new().unwrap();
for i in 0..20 {
let _ = create_test_file(&temp, &format!("file_{i}.txt"));
}
env::set_current_dir(temp.path()).unwrap();
let args = Args::parse_from(["test", "--max-files", "10"]);
let config = Config::from_args(&args).unwrap();
let walker = Walker::from_dir(&config).unwrap();
let entries: Vec<_> = walker
.filter_map(|item| item.into_entry())
.filter(|e| !e.is_dir) .collect();
assert_eq!(entries.len(), 10);
}
#[test]
#[serial]
fn test_output_file_protection_streaming() {
unsafe {
env::set_var("LUFF_OUTPUT_PROTECTION_MS", "5000");
}
let temp = TempDir::new().unwrap();
let output_file = temp.path().join("output.txt");
fs::write(&output_file, "").unwrap();
env::set_current_dir(temp.path()).unwrap();
let args = Args::parse_from(["test"]);
let config = Config::from_args(&args).unwrap();
let walker = Walker::from_dir(&config).unwrap();
let entries: Vec<_> = walker
.filter_map(|item| item.into_entry())
.filter(|e| !e.is_dir) .collect();
assert_eq!(entries.len(), 0);
unsafe {
env::remove_var("LUFF_OUTPUT_PROTECTION_MS");
}
}
#[test]
#[serial]
fn test_streamer_never_panics_single_file() {
unsafe {
env::remove_var("LUFF_OUTPUT_PROTECTION_MS");
}
let temp = TempDir::new().unwrap();
let _ = create_test_file(&temp, "file.txt");
env::set_current_dir(temp.path()).unwrap();
let args = Args::parse_from(["test"]);
let config = Config::from_args(&args).unwrap();
let walker = Walker::from_dir(&config).unwrap();
let count = walker
.filter_map(|item| item.into_entry())
.filter(|e| !e.is_dir) .count();
assert_eq!(count, 1);
}
#[test]
#[serial]
fn test_streamer_never_panics_empty_directory() {
unsafe {
env::remove_var("LUFF_OUTPUT_PROTECTION_MS");
}
let temp = TempDir::new().unwrap();
env::set_current_dir(temp.path()).unwrap();
let args = Args::parse_from(["test"]);
let config = Config::from_args(&args).unwrap();
let walker = Walker::from_dir(&config).unwrap();
let count = walker
.filter_map(|item| item.into_entry())
.filter(|e| !e.is_dir) .count();
assert_eq!(count, 0);
}
const TOTAL_ENTRIES_MINIMUM: usize = 10_000;
const TOTAL_ENTRIES_SAFETY_FACTOR: usize = 2;
#[test]
#[serial]
fn test_size_hint_consistency() {
unsafe {
env::remove_var("LUFF_OUTPUT_PROTECTION_MS");
}
let temp = TempDir::new().unwrap();
let max_files: usize = 75;
for i in 0..max_files {
let _ = create_test_file(&temp, &format!("file_{i}.txt"));
}
env::set_current_dir(temp.path()).unwrap();
let args = Args::parse_from(["test", "--max-files", &max_files.to_string()]);
let config = Config::from_args(&args).unwrap();
let mut walker = Walker::from_dir(&config).unwrap();
let expected_max_total = max_files
.saturating_mul(TOTAL_ENTRIES_SAFETY_FACTOR)
.max(TOTAL_ENTRIES_MINIMUM);
let (lower_init, upper_init) = walker.size_hint();
assert_eq!(lower_init, 0, "Lower bound should always be 0 (unknown)");
assert_eq!(
upper_init,
Some(expected_max_total),
"Initial upper bound should equal max_total_entries"
);
let target_calls = 25;
let mut entries_yielded: usize = 0;
let mut files_processed: usize = 0;
for _ in 0..target_calls {
match walker.next() {
Some(WalkerItem::Entry(entry)) => {
entries_yielded += 1;
if !entry.is_dir {
files_processed += 1;
}
}
Some(WalkerItem::Error(_)) => {
}
None => break,
}
}
let (lower, upper) = walker.size_hint();
assert!(
lower <= upper.unwrap_or(usize::MAX),
"Lower bound ({lower}) must be <= upper bound ({upper:?})"
);
assert_eq!(
upper,
Some(expected_max_total - entries_yielded),
"Upper bound should be max_total_entries minus total entries \
yielded (got entries_yielded={entries_yielded}, \
files_processed={files_processed})"
);
assert!(
upper.unwrap() < upper_init.unwrap(),
"Upper bound should decrease after consuming entries"
);
}
#[test]
#[serial]
fn test_size_hint_exhausted_walker() {
unsafe {
env::remove_var("LUFF_OUTPUT_PROTECTION_MS");
}
let temp = TempDir::new().unwrap();
for i in 0..5 {
let _ = create_test_file(&temp, &format!("file_{i}.txt"));
}
env::set_current_dir(temp.path()).unwrap();
let args = Args::parse_from(["test"]);
let config = Config::from_args(&args).unwrap();
let mut walker = Walker::from_dir(&config).unwrap();
let mut total = 0;
for item in walker.by_ref() {
if item.into_entry().is_some() {
total += 1;
}
}
assert!(total >= 5, "Should have yielded at least the 5 files");
let (lower, upper) = walker.size_hint();
assert_eq!(lower, 0);
assert!(
upper.is_some(),
"Upper bound should still be Some after exhaustion"
);
}
#[test]
#[serial]
fn test_size_hint_monotonically_decreasing() {
unsafe {
env::remove_var("LUFF_OUTPUT_PROTECTION_MS");
}
let temp = TempDir::new().unwrap();
for i in 0..10 {
let _ = create_test_file(&temp, &format!("file_{i}.txt"));
}
env::set_current_dir(temp.path()).unwrap();
let args = Args::parse_from(["test"]);
let config = Config::from_args(&args).unwrap();
let mut walker = Walker::from_dir(&config).unwrap();
let mut prev_upper = walker.size_hint().1.unwrap();
while let Some(item) = walker.next() {
if item.into_entry().is_some() {
let current_upper = walker.size_hint().1.unwrap();
assert!(
current_upper < prev_upper,
"Upper bound must strictly decrease after yielding an \
entry: {current_upper} >= {prev_upper}"
);
prev_upper = current_upper;
}
}
}
#[test]
#[serial]
fn test_output_protection_threshold_parsing() {
unsafe {
std::env::set_var("LUFF_OUTPUT_PROTECTION_MS", "500");
}
let threshold = luff::fs_utils::output_protection_threshold();
assert_eq!(threshold, Duration::from_millis(500));
unsafe {
std::env::remove_var("LUFF_OUTPUT_PROTECTION_MS");
}
let threshold_default = luff::fs_utils::output_protection_threshold();
assert_eq!(threshold_default, Duration::from_millis(10));
}