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