1use anyhow::Result;
4use std::path::{Path, PathBuf};
5
6pub struct FileUtils;
8
9impl FileUtils {
10 pub fn collect_test_files(path: &Path, exclude_patterns: &[String]) -> Vec<PathBuf> {
12 let mut files = Vec::new();
13 let walker = walkdir::WalkDir::new(path).follow_links(true).into_iter();
14 for entry in walker.flatten() {
15 let path = entry.path();
16 if path
17 .extension()
18 .is_some_and(|ext| ext == "gctf" || ext == "apif")
19 && !is_excluded(path, exclude_patterns)
20 {
21 files.push(path.to_path_buf());
22 }
23 }
24 files
25 }
26
27 pub fn sort_files(files: &mut [PathBuf], sort_by: &str) {
29 match sort_by {
30 "name" => files.sort_by(|a, b| a.file_name().cmp(&b.file_name())),
31 "mtime" | "time" => files.sort_by(|a, b| {
32 Self::get_mtime(b)
33 .unwrap_or(0)
34 .cmp(&Self::get_mtime(a).unwrap_or(0))
35 }),
36 "random" => {
37 use rand::SeedableRng;
38 use rand::rngs::StdRng;
39 use rand::seq::SliceRandom;
40 let mut rng = StdRng::from_rng(&mut rand::rng());
41 files.shuffle(&mut rng);
42 }
43 _ => {}
44 }
45 }
46
47 pub fn get_mtime(path: &Path) -> Result<i64> {
49 let metadata = std::fs::metadata(path)?;
50 let mtime = metadata
51 .modified()?
52 .duration_since(std::time::UNIX_EPOCH)
53 .unwrap_or_default()
54 .as_secs() as i64;
55 Ok(mtime)
56 }
57
58 pub fn get_file_size(path: &Path) -> Result<u64> {
60 let metadata = std::fs::metadata(path)?;
61 Ok(metadata.len())
62 }
63
64 pub fn resolve_relative_path(base_file_path: &Path, relative_path: &str) -> PathBuf {
66 let base_dir = base_file_path.parent().unwrap_or(Path::new("."));
67 base_dir.join(relative_path)
68 }
69}
70
71fn is_excluded(path: &Path, exclude_patterns: &[String]) -> bool {
72 if exclude_patterns.is_empty() {
73 return false;
74 }
75 let path_str = path.to_string_lossy();
76 exclude_patterns.iter().any(|pattern| {
77 if let Ok(glob) = globset::Glob::new(pattern).map(|g| g.compile_matcher()) {
78 glob.is_match(path_str.as_ref())
79 } else {
80 path_str.contains(pattern.as_str())
81 }
82 })
83}
84
85#[cfg(test)]
86mod tests {
87 use super::*;
88 use std::fs;
89
90 #[test]
91 #[cfg(not(miri))]
92 fn test_collect_test_files_empty_dir() {
93 let dir = std::env::temp_dir().join("gctf_test_empty");
94 let _ = fs::remove_dir_all(&dir);
95 fs::create_dir_all(&dir).unwrap();
96 let files = FileUtils::collect_test_files(&dir, &[]);
97 assert!(files.is_empty());
98 let _ = fs::remove_dir_all(&dir);
99 }
100
101 #[test]
102 #[cfg(not(miri))]
103 fn test_collect_test_files_with_gctf() {
104 let dir = std::env::temp_dir().join("gctf_test_files");
105 let _ = fs::remove_dir_all(&dir);
106 fs::create_dir_all(&dir).unwrap();
107 fs::write(dir.join("test.gctf"), "content").unwrap();
108 fs::write(dir.join("other.txt"), "content").unwrap();
109 let files = FileUtils::collect_test_files(&dir, &[]);
110 assert_eq!(files.len(), 1);
111 let _ = fs::remove_dir_all(&dir);
112 }
113
114 #[test]
115 fn test_resolve_relative_path() {
116 let base = Path::new("/home/user/tests/test.gctf");
117 let resolved = FileUtils::resolve_relative_path(base, "data/file.csv");
118 assert_eq!(resolved, Path::new("/home/user/tests/data/file.csv"));
119 }
120
121 #[test]
122 #[cfg(not(miri))]
123 fn test_get_file_size() {
124 let dir = std::env::temp_dir().join("gctf_test_size");
125 let _ = fs::remove_dir_all(&dir);
126 fs::create_dir_all(&dir).unwrap();
127 let file = dir.join("test.gctf");
128 fs::write(&file, "hello").unwrap();
129 assert_eq!(FileUtils::get_file_size(&file).unwrap(), 5);
130 let _ = fs::remove_dir_all(&dir);
131 }
132
133 #[test]
134 fn test_is_excluded() {
135 assert!(!is_excluded(Path::new("test.gctf"), &[]));
136 assert!(is_excluded(Path::new("test.gctf"), &["test*".into()]));
137 assert!(!is_excluded(Path::new("other.gctf"), &["test*".into()]));
138 assert!(is_excluded(Path::new("test.gctf"), &["*.gctf".into()]));
140 assert!(!is_excluded(Path::new("test.gctf"), &["*.txt".into()]));
141 }
142
143 #[test]
144 fn test_sort_files_by_name() {
145 let mut files = vec![
146 PathBuf::from("b.gctf"),
147 PathBuf::from("a.gctf"),
148 PathBuf::from("c.gctf"),
149 ];
150 FileUtils::sort_files(&mut files, "name");
151 assert_eq!(files[0].file_name().unwrap(), "a.gctf");
152 assert_eq!(files[1].file_name().unwrap(), "b.gctf");
153 assert_eq!(files[2].file_name().unwrap(), "c.gctf");
154 }
155
156 #[test]
157 fn test_sort_files_unsupported() {
158 let mut files = vec![PathBuf::from("b.gctf"), PathBuf::from("a.gctf")];
159 FileUtils::sort_files(&mut files, "unsupported");
160 assert_eq!(files[0].file_name().unwrap(), "b.gctf");
162 }
163
164 #[test]
165 fn test_get_mtime_nonexistent() {
166 let result = FileUtils::get_mtime(Path::new("/nonexistent/path.gctf"));
167 assert!(result.is_err());
168 }
169}