1use std::collections::BTreeMap;
5use std::collections::BTreeSet;
6use std::fmt::Write;
7use std::fs;
8use std::io;
9use std::path::Path;
10use std::path::PathBuf;
11
12use tempdir::TempDir;
13
14use crate::result::ResultEx;
15use crate::result::io_not_found;
16
17pub const TEMP_DIR_PREFIX: &str = "tytanic-utils";
19
20pub fn create_dir<P>(path: P, all: bool) -> io::Result<()>
31where
32 P: AsRef<Path>,
33{
34 fn inner(path: &Path, all: bool) -> io::Result<()> {
35 let res = if all {
36 fs::create_dir_all(path)
37 } else {
38 fs::create_dir(path)
39 };
40 res.ignore_default(|e| e.kind() == io::ErrorKind::AlreadyExists)
41 }
42
43 inner(path.as_ref(), all)
44}
45
46pub fn remove_file<P>(path: P) -> io::Result<()>
56where
57 P: AsRef<Path>,
58{
59 fn inner(path: &Path) -> io::Result<()> {
60 std::fs::remove_file(path).ignore_default(io_not_found)
61 }
62
63 inner(path.as_ref())
64}
65
66pub fn remove_dir<P>(path: P, all: bool) -> io::Result<()>
76where
77 P: AsRef<Path>,
78{
79 fn inner(path: &Path, all: bool) -> io::Result<()> {
80 let res = if all {
81 fs::remove_dir_all(path)
82 } else {
83 fs::remove_dir(path)
84 };
85
86 res.ignore_default(|e| {
87 if io_not_found(e) {
88 let parent_exists = path
89 .parent()
90 .and_then(|p| p.try_exists().ok())
91 .is_some_and(|b| b);
92
93 if !parent_exists {
94 tracing::error!(?path, "tried removing dir, but parent did not exist");
95 }
96
97 parent_exists
98 } else {
99 false
100 }
101 })
102 }
103
104 inner(path.as_ref(), all)
105}
106
107pub fn ensure_empty_dir<P>(path: P, all: bool) -> io::Result<()>
117where
118 P: AsRef<Path>,
119{
120 fn inner(path: &Path, all: bool) -> io::Result<()> {
121 let res = remove_dir(path, true);
122 if all {
123 res.ignore_default(io_not_found)?;
125 } else {
126 res?;
127 }
128
129 create_dir(path, all)
130 }
131
132 inner(path.as_ref(), all)
133}
134
135#[derive(Debug)]
138pub struct TempTestEnv {
139 root: TempDir,
140 found: BTreeMap<PathBuf, Option<Vec<u8>>>,
141 expected: BTreeMap<PathBuf, Option<Option<Vec<u8>>>>,
142}
143
144pub struct Setup(TempTestEnv);
148
149impl Setup {
150 pub fn setup_dir<P: AsRef<Path>>(&mut self, path: P) -> &mut Self {
154 let abs_path = self.0.root.path().join(path.as_ref());
155 create_dir(abs_path, true).unwrap();
156 self
157 }
158
159 pub fn setup_file<P: AsRef<Path>>(&mut self, path: P, content: impl AsRef<[u8]>) -> &mut Self {
163 let abs_path = self.0.root.path().join(path.as_ref());
164 let parent = abs_path.parent().unwrap();
165 if parent != self.0.root.path() {
166 create_dir(parent, true).unwrap();
167 }
168
169 let content = content.as_ref();
170 std::fs::write(&abs_path, content).unwrap();
171 self
172 }
173
174 pub fn setup_file_empty<P: AsRef<Path>>(&mut self, path: P) -> &mut Self {
178 let abs_path = self.0.root.path().join(path.as_ref());
179 let parent = abs_path.parent().unwrap();
180 if parent != self.0.root.path() {
181 create_dir(parent, true).unwrap();
182 }
183
184 std::fs::write(&abs_path, "").unwrap();
185 self
186 }
187}
188
189pub struct Expect(TempTestEnv);
193
194impl Expect {
195 pub fn expect_dir<P: AsRef<Path>>(&mut self, path: P) -> &mut Self {
197 self.0.add_expected(path.as_ref().to_path_buf(), None);
198 self
199 }
200
201 pub fn expect_file<P: AsRef<Path>>(&mut self, path: P) -> &mut Self {
203 self.0.add_expected(path.as_ref().to_path_buf(), Some(None));
204 self
205 }
206
207 pub fn expect_file_content<P: AsRef<Path>>(
209 &mut self,
210 path: P,
211 content: impl AsRef<[u8]>,
212 ) -> &mut Self {
213 let content = content.as_ref();
214 self.0
215 .add_expected(path.as_ref().to_path_buf(), Some(Some(content.to_owned())));
216 self
217 }
218
219 pub fn expect_file_empty<P: AsRef<Path>>(&mut self, path: P) -> &mut Self {
221 self.0.add_expected(path.as_ref().to_path_buf(), None);
222 self
223 }
224}
225
226impl TempTestEnv {
227 pub fn run(
232 setup: impl FnOnce(&mut Setup) -> &mut Setup,
233 test: impl FnOnce(&Path),
234 expect: impl FnOnce(&mut Expect) -> &mut Expect,
235 ) {
236 let dir = Self {
237 root: TempDir::new(TEMP_DIR_PREFIX).unwrap(),
238 found: BTreeMap::new(),
239 expected: BTreeMap::new(),
240 };
241
242 let mut s = Setup(dir);
243 setup(&mut s);
244 let Setup(dir) = s;
245
246 test(dir.root.path());
247
248 let mut e = Expect(dir);
249 expect(&mut e);
250 let Expect(mut dir) = e;
251
252 dir.collect();
253 dir.assert();
254 }
255
256 pub fn run_no_check(setup: impl FnOnce(&mut Setup) -> &mut Setup, test: impl FnOnce(&Path)) {
261 let dir = Self {
262 root: TempDir::new(TEMP_DIR_PREFIX).unwrap(),
263 found: BTreeMap::new(),
264 expected: BTreeMap::new(),
265 };
266
267 let mut s = Setup(dir);
268 setup(&mut s);
269 let Setup(dir) = s;
270
271 test(dir.root.path());
272 }
273}
274
275impl TempTestEnv {
276 fn add_expected(&mut self, expected: PathBuf, content: Option<Option<Vec<u8>>>) {
277 for ancestor in expected.ancestors() {
278 self.expected.insert(ancestor.to_path_buf(), None);
279 }
280 self.expected.insert(expected, content);
281 }
282
283 fn add_found(&mut self, found: PathBuf, content: Option<Vec<u8>>) {
284 for ancestor in found.ancestors() {
285 self.found.insert(ancestor.to_path_buf(), None);
286 }
287 self.found.insert(found, content);
288 }
289
290 fn read(&mut self, path: PathBuf) {
291 let rel = path.strip_prefix(self.root.path()).unwrap().to_path_buf();
292 if path.metadata().unwrap().is_file() {
293 let content = std::fs::read(&path).unwrap();
294 self.add_found(rel, Some(content));
295 } else {
296 let mut empty = true;
297 for entry in path.read_dir().unwrap() {
298 let entry = entry.unwrap();
299 self.read(entry.path());
300 empty = false;
301 }
302
303 if empty && self.root.path() != path {
304 self.add_found(rel, None);
305 }
306 }
307 }
308
309 fn collect(&mut self) {
310 self.read(self.root.path().to_path_buf())
311 }
312
313 fn assert(mut self) {
314 let mut not_found = BTreeSet::new();
315 let mut not_matched = BTreeMap::new();
316 for (expected_path, expected_value) in self.expected {
317 if let Some(found) = self.found.remove(&expected_path) {
318 let expected = expected_value.unwrap_or_default();
319 let found = found.unwrap_or_default();
320 if let Some(expected) = expected {
321 if expected != found {
322 not_matched.insert(expected_path, (found, expected));
323 }
324 }
325 } else {
326 not_found.insert(expected_path);
327 }
328 }
329
330 let not_expected: BTreeSet<_> = self.found.into_keys().collect();
331
332 let mut mismatch = false;
333 let mut msg = String::new();
334 if !not_found.is_empty() {
335 mismatch = true;
336 writeln!(&mut msg, "\n=== Not found ===").unwrap();
337 for not_found in not_found {
338 writeln!(&mut msg, "/{}", not_found.display()).unwrap();
339 }
340 }
341
342 if !not_expected.is_empty() {
343 mismatch = true;
344 writeln!(&mut msg, "\n=== Not expected ===").unwrap();
345 for not_expected in not_expected {
346 writeln!(&mut msg, "/{}", not_expected.display()).unwrap();
347 }
348 }
349
350 if !not_matched.is_empty() {
351 mismatch = true;
352 writeln!(&mut msg, "\n=== Content matched ===").unwrap();
353 for (path, (found, expected)) in not_matched {
354 writeln!(&mut msg, "/{}", path.display()).unwrap();
355 match (std::str::from_utf8(&found), std::str::from_utf8(&expected)) {
356 (Ok(found), Ok(expected)) => {
357 writeln!(&mut msg, "=== Expected ===\n>>>\n{expected}\n<<<\n").unwrap();
358 writeln!(&mut msg, "=== Found ===\n>>>\n{found}\n<<<\n").unwrap();
359 }
360 _ => {
361 writeln!(&mut msg, "Binary data differed").unwrap();
362 }
363 }
364 }
365 }
366
367 if mismatch {
368 panic!("{msg}")
369 }
370 }
371}
372
373#[cfg(test)]
374mod tests {
375 use super::*;
376
377 #[test]
378 fn test_temp_env_run() {
379 TempTestEnv::run(
380 |test| {
381 test.setup_file_empty("foo/bar/empty.txt")
382 .setup_file_empty("foo/baz/other.txt")
383 },
384 |root| {
385 std::fs::remove_file(root.join("foo/bar/empty.txt")).unwrap();
386 },
387 |test| {
388 test.expect_dir("foo/bar/")
389 .expect_file_empty("foo/baz/other.txt")
390 },
391 );
392 }
393
394 #[test]
395 #[should_panic]
396 fn test_temp_env_run_panic() {
397 TempTestEnv::run(
398 |test| {
399 test.setup_file_empty("foo/bar/empty.txt")
400 .setup_file_empty("foo/baz/other.txt")
401 },
402 |root| {
403 std::fs::remove_file(root.join("foo/bar/empty.txt")).unwrap();
404 },
405 |test| test.expect_dir("foo/bar/"),
406 );
407 }
408
409 #[test]
410 fn test_temp_env_run_no_check() {
411 TempTestEnv::run_no_check(
412 |test| {
413 test.setup_file_empty("foo/bar/empty.txt")
414 .setup_file_empty("foo/baz/other.txt")
415 },
416 |root| {
417 std::fs::remove_file(root.join("foo/bar/empty.txt")).unwrap();
418 },
419 );
420 }
421}