1use std::collections::{HashMap, HashSet};
7use std::ffi::OsString;
8use std::fs::{self, Metadata, OpenOptions};
9use std::io::{self, Write};
10use std::path::{Path, PathBuf};
11use std::time::{Duration, SystemTime};
12
13use filetime::FileTime;
14use regex::Regex;
15use time::macros::format_description;
16use time::OffsetDateTime;
17
18pub use crate::prelude::*;
19
20pub use super::datadir::Datadir;
21
22#[derive(Debug, Default)]
23pub struct Files {
34 metadata: HashMap<PathBuf, Metadata>,
35}
36
37impl ContextElement for Files {
38 fn created(&mut self, scenario: &Scenario) {
39 scenario.register_context_type::<Datadir>();
40 }
41}
42
43#[step]
51#[context(Datadir)]
52pub fn create_from_embedded(context: &ScenarioContext, embedded_file: SubplotDataFile) {
53 let filename_on_disk = PathBuf::from(format!("{}", embedded_file.name().display()));
54 create_from_embedded_with_other_name::call(context, &filename_on_disk, embedded_file)?;
55}
56
57#[step]
64pub fn create_from_embedded_with_other_name(
65 context: &Datadir,
66 filename_on_disk: &Path,
67 embedded_file: SubplotDataFile,
68) {
69 let filename_on_disk = PathBuf::from(filename_on_disk);
70 let parentpath = filename_on_disk.parent().ok_or_else(|| {
71 format!(
72 "No parent directory found for {}",
73 filename_on_disk.display()
74 )
75 })?;
76 context.create_dir_all(parentpath)?;
77 context
78 .open_write(filename_on_disk)?
79 .write_all(embedded_file.data())?;
80}
81
82#[step]
88pub fn remove_file(context: &Datadir, filename: &Path) {
89 let filename = PathBuf::from(filename);
90 context.remove_file(filename)?;
91}
92
93#[step]
100pub fn touch_with_timestamp(context: &Datadir, filename: &Path, mtime: &str) {
101 let fd = format_description!(
102 "[year]-[month]-[day] [hour]:[minute]:[second] [offset_hour]:[offset_minute]"
103 );
104 let full_time = format!("{mtime} +00:00");
105 let ts = OffsetDateTime::parse(&full_time, &fd)?;
106 let (secs, nanos) = (ts.unix_timestamp(), 0);
107 let mtime = FileTime::from_unix_time(secs, nanos);
108 let full_path = context.canonicalise_filename(filename)?;
109 drop(
111 OpenOptions::new()
112 .create(true)
113 .truncate(false)
114 .write(true)
115 .open(&full_path)?,
116 );
117 filetime::set_file_mtime(full_path, mtime)?;
119}
120
121#[step]
127pub fn create_from_text(context: &Datadir, text: &str, filename: &Path) {
128 context.open_write(filename)?.write_all(text.as_bytes())?;
129}
130
131#[step]
138#[context(Datadir)]
139#[context(Files)]
140pub fn remember_metadata(context: &ScenarioContext, filename: &Path) {
141 let full_path = context.with(
142 |context: &Datadir| context.canonicalise_filename(filename),
143 false,
144 )?;
145 let metadata = fs::metadata(full_path)?;
146 context.with_mut(
147 |context: &mut Files| {
148 context.metadata.insert(filename.to_owned(), metadata);
149 Ok(())
150 },
151 false,
152 )?;
153}
154
155#[step]
162pub fn touch(context: &Datadir, filename: &Path) {
163 let full_path = context.canonicalise_filename(filename)?;
164 let now = FileTime::now();
165 drop(
167 OpenOptions::new()
168 .create(true)
169 .truncate(false)
170 .write(true)
171 .open(&full_path)?,
172 );
173 filetime::set_file_mtime(full_path, now)?;
175}
176
177#[step]
183pub fn file_exists(context: &Datadir, filename: &Path) {
184 let full_path = context.canonicalise_filename(filename)?;
185 match fs::metadata(full_path) {
186 Ok(_) => (),
187 Err(e) => {
188 if matches!(e.kind(), io::ErrorKind::NotFound) {
189 throw!(format!("file '{}' was not found", filename.display()))
190 } else {
191 throw!(e);
192 }
193 }
194 }
195}
196
197#[step]
203pub fn file_does_not_exist(context: &Datadir, filename: &Path) {
204 let full_path = context.canonicalise_filename(filename)?;
205 match fs::metadata(full_path) {
206 Ok(_) => {
207 throw!(format!(
208 "file '{}' was unexpectedly found",
209 filename.display()
210 ))
211 }
212 Err(e) => {
213 if !matches!(e.kind(), io::ErrorKind::NotFound) {
214 throw!(e);
215 }
216 }
217 }
218}
219
220#[step]
229pub fn only_these_exist(context: &Datadir, filenames: &str) {
230 let filenames: HashSet<OsString> = filenames
231 .replace(',', "")
232 .split_ascii_whitespace()
233 .map(|s| s.into())
234 .collect();
235 let fnames: HashSet<OsString> = fs::read_dir(context.base_path())?
236 .map(|entry| entry.map(|entry| entry.file_name()))
237 .collect::<Result<_, _>>()?;
238 assert_eq!(filenames, fnames);
239}
240
241#[step]
248pub fn file_contains(context: &Datadir, filename: &Path, data: &str) {
249 let full_path = context.canonicalise_filename(filename)?;
250 let body = fs::read_to_string(full_path)?;
251 if !body.contains(data) {
252 println!("file {} contains:\n{}", filename.display(), body);
253 throw!("expected file content not found");
254 }
255}
256
257#[step]
264pub fn file_doesnt_contain(context: &Datadir, filename: &Path, data: &str) {
265 let full_path = context.canonicalise_filename(filename)?;
266 let body = fs::read_to_string(full_path)?;
267 if body.contains(data) {
268 println!("file {} contains:\n{}", filename.display(), body);
269 throw!("unexpected file content found");
270 }
271}
272
273#[step]
281pub fn file_matches_regex(context: &Datadir, filename: &Path, regex: &str) {
282 let full_path = context.canonicalise_filename(filename)?;
283 let regex = Regex::new(regex)?;
284 let body = fs::read_to_string(full_path)?;
285 if !regex.is_match(&body) {
286 println!("file {} contains:\n{}", filename.display(), body);
287 throw!("file content does not match given regex");
288 }
289}
290
291#[step]
297pub fn file_match(context: &Datadir, filename1: &Path, filename2: &Path) {
298 let full_path1 = context.canonicalise_filename(filename1)?;
299 let full_path2 = context.canonicalise_filename(filename2)?;
300 let body1 = fs::read(full_path1)?;
301 let body2 = fs::read(full_path2)?;
302 if body1 != body2 {
303 println!(
304 "file {} contains:\n{}",
305 filename1.display(),
306 String::from_utf8_lossy(&body1)
307 );
308 println!(
309 "file {} contains:\n{}",
310 filename2.display(),
311 String::from_utf8_lossy(&body2)
312 );
313 throw!("file contents do not match each other");
314 }
315}
316
317#[step]
324pub fn file_do_not_match(context: &Datadir, filename1: &Path, filename2: &Path) {
325 let full_path1 = context.canonicalise_filename(filename1)?;
326 let full_path2 = context.canonicalise_filename(filename2)?;
327 let body1 = fs::read(full_path1)?;
328 let body2 = fs::read(full_path2)?;
329 if body1 == body2 {
330 println!(
331 "file {} contains:\n{}",
332 filename1.display(),
333 String::from_utf8_lossy(&body1)
334 );
335 println!(
336 "file {} contains:\n{}",
337 filename2.display(),
338 String::from_utf8_lossy(&body2)
339 );
340 throw!("file contents do not differ");
341 }
342}
343
344#[step]
350pub fn file_and_embedded_file_match(context: &Datadir, filename: &Path, embedded: SubplotDataFile) {
351 let full_path = context.canonicalise_filename(filename)?;
352 let body1 = fs::read(full_path)?;
353
354 let body2 = embedded.data();
355 if body1 != body2 {
356 println!(
357 "file {} contains:\n{}",
358 filename.display(),
359 String::from_utf8_lossy(&body1)
360 );
361 println!(
362 "embedded file {} contains:\n{}",
363 embedded.name().display(),
364 String::from_utf8_lossy(body2)
365 );
366 throw!("file contents do not match each other");
367 }
368}
369
370#[step]
376pub fn file_and_embedded_file_do_not_match(
377 context: &Datadir,
378 filename: &Path,
379 embedded: SubplotDataFile,
380) {
381 let full_path = context.canonicalise_filename(filename)?;
382 let body1 = fs::read(full_path)?;
383
384 let body2 = embedded.data();
385 if body1 == body2 {
386 println!(
387 "file {} contains:\n{}",
388 filename.display(),
389 String::from_utf8_lossy(&body1)
390 );
391 println!(
392 "embedded file {} contains:\n{}",
393 embedded.name().display(),
394 String::from_utf8_lossy(body2)
395 );
396 throw!("file contents match each other");
397 }
398}
399
400#[step]
412#[context(Datadir)]
413#[context(Files)]
414pub fn has_remembered_metadata(context: &ScenarioContext, filename: &Path) {
415 let full_path = context.with(
416 |context: &Datadir| context.canonicalise_filename(filename),
417 false,
418 )?;
419 let metadata = fs::metadata(full_path)?;
420 if let Some(remembered) = context.with(
421 |context: &Files| Ok(context.metadata.get(filename).cloned()),
422 false,
423 )? {
424 if metadata.permissions() != remembered.permissions()
425 || metadata.modified()? != remembered.modified()?
426 || metadata.len() != remembered.len()
427 || metadata.is_file() != remembered.is_file()
428 {
429 throw!(format!(
430 "metadata change detected for {}",
431 filename.display()
432 ));
433 }
434 } else {
435 throw!(format!("no remembered metadata for {}", filename.display()));
436 }
437}
438
439#[step]
451#[context(Datadir)]
452#[context(Files)]
453pub fn has_different_metadata(context: &ScenarioContext, filename: &Path) {
454 let full_path = context.with(
455 |context: &Datadir| context.canonicalise_filename(filename),
456 false,
457 )?;
458 let metadata = fs::metadata(full_path)?;
459 if let Some(remembered) = context.with(
460 |context: &Files| Ok(context.metadata.get(filename).cloned()),
461 false,
462 )? {
463 if metadata.permissions() == remembered.permissions()
464 && metadata.modified()? == remembered.modified()?
465 && metadata.len() == remembered.len()
466 && metadata.is_file() == remembered.is_file()
467 {
468 throw!(format!(
469 "metadata change not detected for {}",
470 filename.display()
471 ));
472 }
473 } else {
474 throw!(format!("no remembered metadata for {}", filename.display()));
475 }
476}
477
478#[step]
484pub fn mtime_is_recent(context: &Datadir, filename: &Path) {
485 let full_path = context.canonicalise_filename(filename)?;
486 let metadata = fs::metadata(full_path)?;
487 let mtime = metadata.modified()?;
488 let diff = SystemTime::now().duration_since(mtime)?;
489 if diff > (Duration::from_secs(5)) {
490 throw!(format!("{} is older than 5 seconds", filename.display()));
491 }
492}
493
494#[step]
500pub fn mtime_is_ancient(context: &Datadir, filename: &Path) {
501 let full_path = context.canonicalise_filename(filename)?;
502 let metadata = fs::metadata(full_path)?;
503 let mtime = metadata.modified()?;
504 let diff = SystemTime::now().duration_since(mtime)?;
505 if diff < (Duration::from_secs(39 * 365 * 24 * 3600)) {
506 throw!(format!("{} is younger than 39 years", filename.display()));
507 }
508}
509
510#[step]
516pub fn make_directory(context: &Datadir, path: &Path) {
517 context.create_dir_all(path)?;
518}
519
520#[step]
526pub fn remove_directory(context: &Datadir, path: &Path) {
527 let full_path = context.canonicalise_filename(path)?;
528 remove_dir_all::remove_dir_all(full_path)?;
529}
530
531#[step]
537pub fn remove_empty_directory(context: &Datadir, path: &Path) {
538 let full_path = context.canonicalise_filename(path)?;
539 std::fs::remove_dir(full_path)?;
540}
541
542#[step]
549pub fn path_exists(context: &Datadir, path: &Path) {
550 let full_path = context.canonicalise_filename(path)?;
551 if !fs::metadata(&full_path)?.is_dir() {
552 throw!(format!(
553 "{} exists but is not a directory",
554 full_path.display()
555 ))
556 }
557}
558
559#[step]
566pub fn path_does_not_exist(context: &Datadir, path: &Path) {
567 let full_path = context.canonicalise_filename(path)?;
568 match fs::metadata(&full_path) {
569 Ok(_) => throw!(format!("{} exists", full_path.display())),
570 Err(e) => {
571 if !matches!(e.kind(), io::ErrorKind::NotFound) {
572 throw!(e);
573 }
574 }
575 };
576}
577
578#[step]
585pub fn path_is_empty(context: &Datadir, path: &Path) {
586 let full_path = context.canonicalise_filename(path)?;
587 let mut iter = fs::read_dir(&full_path)?;
588 match iter.next() {
589 None => {}
590 Some(Ok(_)) => throw!(format!("{} is not empty", full_path.display())),
591 Some(Err(e)) => throw!(e),
592 }
593}
594
595#[step]
603pub fn path_is_not_empty(context: &Datadir, path: &Path) {
604 let full_path = context.canonicalise_filename(path)?;
605 let mut iter = fs::read_dir(&full_path)?;
606 match iter.next() {
607 None => throw!(format!("{} is empty", full_path.display())),
608 Some(Ok(_)) => {}
609 Some(Err(e)) => throw!(e),
610 }
611}