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(
68 { $($key:expr => $value:expr),+ } => {
69 {
70 ::std::collections::HashMap::from([
71 $(
72 ($key, $value),
73 )+
74 ])
75 }
76 };
77);
78
79pub(crate) trait MyJoin {
82 fn join(&self, sep: &str) -> String;
83}
84
85impl MyJoin for BTreeSet<String> {
96 fn join(&self, sep: &str) -> String {
97 self.iter().map(|item| item.as_str()).collect::<Vec<&str>>().join(sep)
98 }
99}
100
101#[cfg(test)]
102pub(crate) mod tests {
103 use std::collections::HashMap;
104
105 static ERROR_REGEX: LazyLock<regex::Regex> = LazyLock::new(|| regex::Regex::new(r"^error:(?P<error_name>.+)$").unwrap());
106
107 use std::sync::{LazyLock, Mutex};
136
137 pub(crate) struct TestPath {
138 _filename: &'static str,
139 contents: String,
140 read_count: u16,
141 }
142
143 impl TestPath {
144 fn new(filename: &'static str, contents: String) -> Self {
145 Self {
146 _filename: filename,
147 contents,
148 read_count: 0,
149 }
150 }
151
152 fn read(&mut self) -> String {
153 self.read_count += 1;
154 self.contents.clone()
155 }
156
157 fn count(&self) -> u16 {
158 self.read_count
159 }
160 }
161
162 thread_local!(
163 static MOCK_FS: Mutex<HashMap<&'static str, TestPath>> = Mutex::new(HashMap::new());
164 );
165
166 pub(crate) struct ResetFsGuard;
167
168 impl Drop for ResetFsGuard {
169 fn drop(&mut self) {
170 MOCK_FS.with(|fs| {
171 fs.lock().unwrap().clear();
172 });
173 }
174 }
175
176 #[must_use]
177 pub(crate) fn add_test_fs_paths(paths: &[&'static str]) -> ResetFsGuard {
178 MOCK_FS.with(|fs| {
179 let mut fs_map = fs.lock().unwrap();
180 for path in paths {
181 fs_map.insert(path, TestPath::new(path, String::new()));
182 }
183 });
184 ResetFsGuard
185 }
186
187 pub(crate) fn set_test_fs_path_content(path: &'static str, contents: String) {
188 MOCK_FS.with(|fs| {
189 let mut fs_map = fs.lock().unwrap();
190 fs_map.insert(path, TestPath::new(path, contents));
191 });
192 }
193
194 fn with_test_fs<F, R>(callback: F) -> R
195 where F: Fn(&mut HashMap<&'static str, TestPath>) -> R {
196 MOCK_FS.with(|fs| callback(&mut fs.lock().unwrap()))
197 }
198
199 pub(crate) fn is_path_file(path: &Path) -> bool {
200 with_test_fs(|fs| fs.contains_key(&path.to_str().unwrap()))
201 }
202
203 pub(crate) fn get_read_count(path: &str) -> u16 {
204 with_test_fs(|fs| fs.get(path).unwrap().count())
205 }
206
207 pub(crate) fn read_file_to_string(path: &Path) -> std::io::Result<String> {
208 fn str_to_err(str: &str) -> std::io::Result<String> {
209 Err(std::io::Error::from(match str {
210 "InvalidInput" => std::io::ErrorKind::InvalidInput,
211 "Interrupted" => std::io::ErrorKind::Interrupted,
212 "PermissionDenied" => std::io::ErrorKind::PermissionDenied,
213 "NotFound" => std::io::ErrorKind::NotFound,
214 "Other" => std::io::ErrorKind::Other,
215 _ => panic!("Unknown I/O ErrorKind '{str}'")
216 }))
217 }
218
219 with_test_fs(|fs| match fs.get_mut(path.to_str().unwrap()) {
220 None => Err(std::io::Error::new(
221 std::io::ErrorKind::NotFound,
222 format!("Test filesystem path {path:?} does not exist"),
223 )),
224 Some(test_path) => {
225 let contents = test_path.read();
226 match ERROR_REGEX.captures(&contents) {
227 None => Ok(contents),
228 Some(caps) => match caps.name("error_name") {
229 None => Ok(contents),
230 Some(re_match) => str_to_err(re_match.as_str()),
231 },
232 }
233 },
234 })
235 }
236
237 pub(crate) fn read_file_to_bytes(path: &Path) -> std::io::Result<Vec<u8>> {
238 match read_file_to_string(path) {
239 Ok(contents) => Ok(Vec::from(contents.as_bytes())),
240 Err(x) => Err(x),
241 }
242 }
243
244 use super::*;
249
250 #[test]
251 fn fname_from_path_returns_file_name_even_if_file_does_not_exist() {
252 assert_eq!("some_name", fname_from_path(Path::new("some_name")).unwrap());
253 assert_eq!("some_name", fname_from_path(Path::new("/some_name")).unwrap());
254 assert_eq!("some_name", fname_from_path(Path::new("/a/b/some_name")).unwrap());
255 }
256
257 #[test]
258 fn fname_from_path_fails_when_path_is_empty() {
259 assert_eq!(None, fname_from_path(Path::new("")));
260 }
261
262 #[test]
263 fn fname_from_path_fails_when_path_has_no_filename() {
264 assert_eq!(None, fname_from_path(Path::new("/a/")));
265 }
266
267 #[test]
268 fn map_macro() {
269 let mut one = HashMap::new();
270 one.insert(1, 'a');
271 assert_eq!(one, map! { 1 => 'a' });
272
273 let mut two = HashMap::new();
274 two.insert("a", 1);
275 two.insert("b", 2);
276 assert_eq!(two, map! { "a" => 1, "b" => 2 });
277 }
278
279 #[test]
280 fn btreeset_join() {
281 let empty: BTreeSet<String> = vec![].into_iter().collect();
282 assert_eq!("", empty.join(""));
283 assert_eq!("", empty.join(","));
284
285 let one: BTreeSet<String> = vec!["a"].into_iter().map(|s| s.to_owned()).collect();
286 assert_eq!("a", one.join(""));
287 assert_eq!("a", one.join(","));
288
289 let two: BTreeSet<String> = vec!["a", "b"].into_iter().map(|s| s.to_owned()).collect();
290 assert_eq!("ab", two.join(""));
291 assert_eq!("a,b", two.join(","));
292 }
293}