1use std::collections::BTreeSet;
2use std::path::Path;
3
4pub(crate) mod pathbytes;
5pub(crate) mod wordsplit;
6
7pub mod compress;
8
9pub(crate) fn fname_from_path(path: &Path) -> Option<String> {
13 if path.to_bytes().ends_with(b"/") {
14 return None;
15 }
16 let path = path.file_name()?.to_string_lossy();
17 Some(path.into_owned())
18}
19
20use pathbytes::AsUnixPathBytes;
21#[cfg(test)]
22pub(crate) use tests::is_path_file;
23
24#[cfg(not(test))]
25pub(crate) fn is_path_file(path: &Path) -> bool {
26 path.is_file()
27}
28
29#[cfg(test)]
30pub(crate) use tests::read_file_to_string;
31
32#[cfg(not(test))]
33pub(crate) fn read_file_to_string(path: &Path) -> std::io::Result<String> {
34 std::fs::read_to_string(path)
35}
36
37#[cfg(test)]
38pub(crate) use tests::read_file_to_bytes;
39
40#[cfg(not(test))]
41pub(crate) fn read_file_to_bytes(path: &Path) -> std::io::Result<Vec<u8>> {
42 std::fs::read(path)
43}
44
45macro_rules! map(
72 { $($key:expr => $value:expr),+ } => {
73 {
74 let mut m = ::std::collections::HashMap::new();
75 $(
76 m.insert($key, $value);
77 )+
78 m
79 }
80 };
81);
82
83pub(crate) trait MyJoin {
86 fn join(&self, sep: &str) -> String;
87}
88
89impl MyJoin for BTreeSet<String> {
100 fn join(&self, sep: &str) -> String {
101 self.iter().map(|item| item.as_str()).collect::<Vec<&str>>().join(sep)
102 }
103}
104
105#[cfg(test)]
106pub(crate) mod tests {
107 use std::collections::HashMap;
108
109 static ERROR_REGEX: LazyLock<regex::Regex> = LazyLock::new(|| regex::Regex::new(r"^error:(?P<error_name>.+)$").unwrap());
110
111 use std::sync::{LazyLock, Mutex};
140
141 pub(crate) struct TestPath {
142 _filename: &'static str,
143 contents: String,
144 read_count: u16,
145 }
146
147 impl TestPath {
148 fn new(filename: &'static str, contents: String) -> Self {
149 Self {
150 _filename: filename,
151 contents,
152 read_count: 0,
153 }
154 }
155
156 fn read(&mut self) -> String {
157 self.read_count += 1;
158 self.contents.clone()
159 }
160
161 fn count(&self) -> u16 {
162 self.read_count
163 }
164 }
165
166 thread_local!(
167 static MOCK_FS: Mutex<HashMap<&'static str, TestPath>> = Mutex::new(HashMap::new());
168 );
169
170 pub(crate) struct ResetFsGuard;
171
172 impl Drop for ResetFsGuard {
173 fn drop(&mut self) {
174 MOCK_FS.with(|fs| {
175 fs.lock().unwrap().clear();
176 });
177 }
178 }
179
180 #[must_use]
181 pub(crate) fn add_test_fs_paths(paths: &[&'static str]) -> ResetFsGuard {
182 MOCK_FS.with(|fs| {
183 let mut fs_map = fs.lock().unwrap();
184 for path in paths {
185 fs_map.insert(path, TestPath::new(path, String::new()));
186 }
187 });
188 ResetFsGuard
189 }
190
191 pub(crate) fn set_test_fs_path_content(path: &'static str, contents: String) {
192 MOCK_FS.with(|fs| {
193 let mut fs_map = fs.lock().unwrap();
194 fs_map.insert(path, TestPath::new(path, contents));
195 });
196 }
197
198 fn with_test_fs<F, R>(callback: F) -> R
199 where F: Fn(&mut HashMap<&'static str, TestPath>) -> R {
200 MOCK_FS.with(|fs| callback(&mut fs.lock().unwrap()))
201 }
202
203 pub(crate) fn is_path_file(path: &Path) -> bool {
204 with_test_fs(|fs| fs.contains_key(&path.to_str().unwrap()))
205 }
206
207 pub(crate) fn get_read_count(path: &str) -> u16 {
208 with_test_fs(|fs| fs.get(path).unwrap().count())
209 }
210
211 pub(crate) fn read_file_to_string(path: &Path) -> std::io::Result<String> {
212 fn str_to_err(str: &str) -> std::io::Result<String> {
213 Err(std::io::Error::from(match str {
214 "InvalidInput" => std::io::ErrorKind::InvalidInput,
215 "Interrupted" => std::io::ErrorKind::Interrupted,
216 "PermissionDenied" => std::io::ErrorKind::PermissionDenied,
217 "NotFound" => std::io::ErrorKind::NotFound,
218 "Other" => std::io::ErrorKind::Other,
219 _ => panic!("Unknown I/O ErrorKind '{str}'")
220 }))
221 }
222
223 with_test_fs(|fs| match fs.get_mut(path.to_str().unwrap()) {
224 None => Err(std::io::Error::new(
225 std::io::ErrorKind::NotFound,
226 format!("Test filesystem path {path:?} does not exist"),
227 )),
228 Some(test_path) => {
229 let contents = test_path.read();
230 match ERROR_REGEX.captures(&contents) {
231 None => Ok(contents),
232 Some(caps) => match caps.name("error_name") {
233 None => Ok(contents),
234 Some(re_match) => str_to_err(re_match.as_str()),
235 },
236 }
237 },
238 })
239 }
240
241 pub(crate) fn read_file_to_bytes(path: &Path) -> std::io::Result<Vec<u8>> {
242 match read_file_to_string(path) {
243 Ok(contents) => Ok(Vec::from(contents.as_bytes())),
244 Err(x) => Err(x),
245 }
246 }
247
248 use super::*;
253
254 #[test]
255 fn fname_from_path_returns_file_name_even_if_file_does_not_exist() {
256 assert_eq!("some_name", fname_from_path(Path::new("some_name")).unwrap());
257 assert_eq!("some_name", fname_from_path(Path::new("/some_name")).unwrap());
258 assert_eq!("some_name", fname_from_path(Path::new("/a/b/some_name")).unwrap());
259 }
260
261 #[test]
262 fn fname_from_path_fails_when_path_is_empty() {
263 assert_eq!(None, fname_from_path(Path::new("")));
264 }
265
266 #[test]
267 fn fname_from_path_fails_when_path_has_no_filename() {
268 assert_eq!(None, fname_from_path(Path::new("/a/")));
269 }
270
271 #[test]
272 fn map_macro() {
273 let mut one = HashMap::new();
274 one.insert(1, 'a');
275 assert_eq!(one, map! { 1 => 'a' });
276
277 let mut two = HashMap::new();
278 two.insert("a", 1);
279 two.insert("b", 2);
280 assert_eq!(two, map! { "a" => 1, "b" => 2 });
281 }
282
283 #[test]
284 fn btreeset_join() {
285 let empty: BTreeSet<String> = vec![].into_iter().collect();
286 assert_eq!("", empty.join(""));
287 assert_eq!("", empty.join(","));
288
289 let one: BTreeSet<String> = vec!["a"].into_iter().map(|s| s.to_owned()).collect();
290 assert_eq!("a", one.join(""));
291 assert_eq!("a", one.join(","));
292
293 let two: BTreeSet<String> = vec!["a", "b"].into_iter().map(|s| s.to_owned()).collect();
294 assert_eq!("ab", two.join(""));
295 assert_eq!("a,b", two.join(","));
296 }
297}