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