mod builder;
mod dir;
mod file_list;
mod output_guard;
use crate::{config::Config, error::Result};
use std::path::PathBuf;
pub use dir::DirectoryWalker;
pub use file_list::FileListWalker;
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct WalkerEntry {
pub path: PathBuf,
pub relative_path: PathBuf,
pub is_dir: bool,
}
pub enum Walker {
Directory(Box<DirectoryWalker>),
FileList(FileListWalker),
}
impl std::fmt::Debug for Walker {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
match self {
Self::Directory(_) => f.debug_tuple("Walker::Directory").field(&"..").finish(),
Self::FileList(w) => f
.debug_tuple("Walker::FileList")
.field(&format_args!("{} remaining", w.len()))
.finish(),
}
}
}
impl Walker {
pub fn from_dir(config: &Config) -> Result<Self> {
Ok(Self::Directory(Box::new(DirectoryWalker::new(config)?)))
}
pub fn from_file_list(files: &[PathBuf], config: &Config) -> Result<Self> {
Ok(Self::FileList(FileListWalker::new(files, config)?))
}
}
#[derive(Debug)]
pub enum WalkerItem {
Entry(WalkerEntry),
Error(crate::error::Error),
}
impl From<WalkerEntry> for WalkerItem {
fn from(entry: WalkerEntry) -> Self {
Self::Entry(entry)
}
}
impl From<crate::error::Error> for WalkerItem {
fn from(error: crate::error::Error) -> Self {
Self::Error(error)
}
}
impl WalkerItem {
#[must_use]
pub fn into_entry(self) -> Option<WalkerEntry> {
match self {
Self::Entry(e) => Some(e),
Self::Error(_) => None,
}
}
#[must_use]
pub fn into_error(self) -> Option<crate::error::Error> {
match self {
Self::Entry(_) => None,
Self::Error(e) => Some(e),
}
}
}
impl Iterator for Walker {
type Item = WalkerItem;
fn next(&mut self) -> Option<Self::Item> {
match self {
Self::Directory(walker) => walker.next(),
Self::FileList(walker) => walker.next(),
}
}
fn size_hint(&self) -> (usize, Option<usize>) {
match self {
Self::Directory(walker) => walker.size_hint(),
Self::FileList(walker) => walker.size_hint(),
}
}
}
impl std::iter::FusedIterator for Walker {}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_walker_entry_creation() {
let entry = WalkerEntry {
path: PathBuf::from("/tmp/test.txt"),
relative_path: PathBuf::from("test.txt"),
is_dir: false,
};
assert_eq!(entry.path, PathBuf::from("/tmp/test.txt"));
assert_eq!(entry.relative_path, PathBuf::from("test.txt"));
assert!(!entry.is_dir);
}
#[test]
fn test_walker_entry_directory() {
let entry = WalkerEntry {
path: PathBuf::from("/tmp/mydir"),
relative_path: PathBuf::from("mydir"),
is_dir: true,
};
assert!(entry.is_dir);
}
#[test]
fn test_walker_entry_equality() {
let a = WalkerEntry {
path: PathBuf::from("/tmp/test.txt"),
relative_path: PathBuf::from("test.txt"),
is_dir: false,
};
let b = a.clone();
assert_eq!(a, b);
let c = WalkerEntry {
path: PathBuf::from("/tmp/other.txt"),
relative_path: PathBuf::from("other.txt"),
is_dir: false,
};
assert_ne!(a, c);
}
#[test]
fn test_walker_enum_size() {
let size = std::mem::size_of::<Walker>();
assert!(
size < 200,
"Walker enum should be reasonably small (got {size} bytes)"
);
}
#[test]
fn test_walker_item_from_entry() {
let entry = WalkerEntry {
path: PathBuf::from("/tmp/test.txt"),
relative_path: PathBuf::from("test.txt"),
is_dir: false,
};
let item: WalkerItem = entry.into();
assert!(item.into_entry().is_some());
}
#[test]
fn test_walker_item_from_error() {
let error = crate::error::Error::Printer {
message: "test".to_string(),
};
let item: WalkerItem = error.into();
assert!(item.into_error().is_some());
}
#[test]
fn test_walker_item_convenience_methods() {
let entry = WalkerEntry {
path: PathBuf::from("/tmp/test.txt"),
relative_path: PathBuf::from("test.txt"),
is_dir: false,
};
let item = WalkerItem::Entry(entry.clone());
let entry_result = item.into_entry();
assert!(entry_result.is_some());
assert_eq!(entry_result.unwrap().path, entry.path);
let item = WalkerItem::Entry(entry);
let error_result = item.into_error();
assert!(error_result.is_none());
let item = WalkerItem::Error(crate::error::Error::Printer {
message: "test error".to_string(),
});
let entry_result = item.into_entry();
assert!(entry_result.is_none());
let item = WalkerItem::Error(crate::error::Error::Printer {
message: "test error".to_string(),
});
let error_result = item.into_error();
assert!(error_result.is_some());
match error_result.unwrap() {
crate::error::Error::Printer { message } => assert_eq!(message, "test error"),
_ => panic!("Expected Printer error"),
}
}
#[test]
fn test_walker_item_error_preserves_type() {
let io_error = crate::error::Error::Io(std::io::Error::new(
std::io::ErrorKind::NotFound,
"file not found",
));
let item = WalkerItem::Error(io_error);
let error = item.into_error().unwrap();
match error {
crate::error::Error::Io(e) => assert_eq!(e.kind(), std::io::ErrorKind::NotFound),
_ => panic!("Expected Io error variant"),
}
}
#[test]
fn test_walker_debug_output() {
let entry = WalkerEntry {
path: PathBuf::from("/tmp/test.txt"),
relative_path: PathBuf::from("test.txt"),
is_dir: false,
};
let item = WalkerItem::Entry(entry);
let debug_str = format!("{item:?}");
assert!(
debug_str.contains("Entry"),
"WalkerItem::Entry debug should contain 'Entry'"
);
let item = WalkerItem::Error(crate::error::Error::Printer {
message: "test".to_string(),
});
let debug_str = format!("{item:?}");
assert!(
debug_str.contains("Error"),
"WalkerItem::Error debug should contain 'Error'"
);
}
}
#[cfg(test)]
#[cfg(feature = "cli")]
mod cli_tests {
use super::*;
use crate::cli::Args;
use crate::config::Config;
use crate::env::MockEnv;
use clap::Parser;
use serial_test::serial;
use tempfile::TempDir;
#[test]
#[serial]
fn test_walker_streams_items() {
use std::env;
let temp = TempDir::new().unwrap();
env::set_current_dir(temp.path()).unwrap();
let args = Args::parse_from(["test"]);
let env = MockEnv::new();
let config = Config::from_args_with_env(&args, &env).unwrap();
let mut walker = Walker::from_dir(&config).unwrap();
let _item: Option<WalkerItem> = walker.next();
}
#[test]
#[serial]
fn test_walker_constructors() {
let temp = TempDir::new().unwrap();
let root = temp.path().canonicalize().unwrap();
let env = MockEnv::new();
std::env::set_current_dir(&root).unwrap();
let args = Args::parse_from(["luff"]);
let config = Config::from_args_with_env(&args, &env).unwrap();
let dir_walker = Walker::from_dir(&config);
assert!(dir_walker.is_ok());
assert!(matches!(dir_walker.unwrap(), Walker::Directory(_)));
let file_path = root.join("test.txt");
std::fs::write(&file_path, "content").unwrap();
let files = vec![file_path];
let list_walker = Walker::from_file_list(&files, &config);
assert!(list_walker.is_ok());
assert!(matches!(list_walker.unwrap(), Walker::FileList(_)));
}
#[test]
#[serial]
fn test_walker_debug_impls() {
let temp = TempDir::new().unwrap();
let root = temp.path().canonicalize().unwrap();
let env = MockEnv::new();
std::env::set_current_dir(&root).unwrap();
let args = Args::parse_from(["luff"]);
let config = Config::from_args_with_env(&args, &env).unwrap();
let dir_walker = Walker::from_dir(&config).unwrap();
let debug_str = format!("{dir_walker:?}");
assert!(
debug_str.contains("Directory"),
"Walker::Directory debug should contain 'Directory', got: {debug_str}"
);
let file_path = root.join("test.txt");
std::fs::write(&file_path, "content").unwrap();
let list_walker =
Walker::from_file_list(std::slice::from_ref(&file_path), &config).unwrap();
let debug_str = format!("{list_walker:?}");
assert!(
debug_str.contains("FileList"),
"Walker::FileList debug should contain 'FileList', got: {debug_str}"
);
}
#[test]
#[serial]
fn test_walker_only_yields_files_from_its_root() {
let temp = TempDir::new().unwrap();
let root = temp.path().canonicalize().unwrap();
std::env::set_current_dir(&root).unwrap();
std::fs::write(root.join("expected.txt"), "content").unwrap();
let args = Args::parse_from(["test"]);
let env = MockEnv::new();
let config = Config::from_args_with_env(&args, &env).unwrap();
let walker = Walker::from_dir(&config).unwrap();
let entries: Vec<WalkerEntry> = walker.filter_map(WalkerItem::into_entry).collect();
let file_entries: Vec<_> = entries.iter().filter(|e| !e.is_dir).collect();
assert_eq!(
file_entries.len(),
1,
"Should yield exactly the one file we created"
);
assert_eq!(
file_entries[0].relative_path,
PathBuf::from("expected.txt"),
"Should find our file, not a file from another test's directory"
);
}
#[test]
#[serial]
fn test_walker_fused_through_enum() {
let temp = TempDir::new().unwrap();
let root = temp.path().canonicalize().unwrap();
std::env::set_current_dir(&root).unwrap();
std::fs::write(root.join("file.txt"), "content").unwrap();
let args = Args::parse_from(["test"]);
let env = MockEnv::new();
let config = Config::from_args_with_env(&args, &env).unwrap();
let mut walker = Walker::from_dir(&config).unwrap();
while walker.next().is_some() {}
assert!(walker.next().is_none());
assert!(walker.next().is_none());
}
}