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;
21pub use super::runcmd::Runcmd;
22
23#[derive(Debug, Default)]
24pub struct Files {
35 metadata: HashMap<PathBuf, Metadata>,
36}
37
38impl ContextElement for Files {
39 fn created(&mut self, scenario: &Scenario) {
40 scenario.register_context_type::<Datadir>();
41 }
42}
43
44#[step]
52#[context(Datadir)]
53#[context(Runcmd)]
54pub fn create_from_embedded(context: &ScenarioContext, embedded_file: SubplotDataFile) {
55 let filename_on_disk = PathBuf::from(format!("{}", embedded_file.name().display()));
56 create_from_embedded_with_other_name::call(context, &filename_on_disk, embedded_file)?;
57}
58
59#[step]
66#[context(Datadir)]
67#[context(Runcmd)]
68pub fn create_from_embedded_with_other_name(
69 context: &ScenarioContext,
70 filename_on_disk: &Path,
71 embedded_file: SubplotDataFile,
72) {
73 _create_from_embedded_with_other_name_executable::call(
74 context,
75 filename_on_disk,
76 embedded_file,
77 false,
78 )?;
79}
80
81#[step]
89#[context(Datadir)]
90#[context(Runcmd)]
91pub fn create_executable_from_embedded(context: &ScenarioContext, embedded_file: SubplotDataFile) {
92 let filename_on_disk = PathBuf::from(format!("{}", embedded_file.name().display()));
93 create_executable_from_embedded_with_other_name::call(
94 context,
95 &filename_on_disk,
96 embedded_file,
97 )?;
98}
99
100#[step]
107#[context(Datadir)]
108#[context(Runcmd)]
109pub fn create_executable_from_embedded_with_other_name(
110 context: &ScenarioContext,
111 filename_on_disk: &Path,
112 embedded_file: SubplotDataFile,
113) {
114 _create_from_embedded_with_other_name_executable::call(
115 context,
116 filename_on_disk,
117 embedded_file,
118 true,
119 )?;
120}
121
122#[doc(hidden)]
124#[step]
125#[context(Datadir)]
126#[context(Runcmd)]
127fn _create_from_embedded_with_other_name_executable(
128 context: &ScenarioContext,
129 filename_on_disk: &Path,
130 embedded_file: SubplotDataFile,
131 executable: bool,
132) {
133 let filename_on_disk = PathBuf::from(filename_on_disk);
134 let parentpath = filename_on_disk.parent().ok_or_else(|| {
135 format!(
136 "No parent directory found for {}",
137 filename_on_disk.display()
138 )
139 })?;
140 let full_filename = context.with_mut(
141 |runcmd: &mut Runcmd| {
142 runcmd.with_forklock(|| {
146 context.with(
147 |datadir: &Datadir| -> Result<PathBuf, StepError> {
148 datadir.create_dir_all(parentpath)?;
149 datadir
150 .open_write(&filename_on_disk)?
151 .write_all(embedded_file.data())?;
152 datadir.canonicalise_filename(&filename_on_disk)
153 },
154 false,
155 )
156 })
157 },
158 false,
159 )?;
160 if executable {
161 #[cfg(unix)]
162 use std::os::unix::fs::PermissionsExt;
163
164 let mut perms = std::fs::symlink_metadata(&full_filename)?.permissions();
165 #[cfg(unix)]
166 perms.set_mode(perms.mode() | 0o111);
167 std::fs::set_permissions(&full_filename, perms)?;
168 }
169}
170
171#[step]
177pub fn remove_file(context: &Datadir, filename: &Path) {
178 let filename = PathBuf::from(filename);
179 context.remove_file(filename)?;
180}
181
182#[step]
189pub fn touch_with_timestamp(context: &Datadir, filename: &Path, mtime: &str) {
190 let fd = format_description!(
191 "[year]-[month]-[day] [hour]:[minute]:[second] [offset_hour]:[offset_minute]"
192 );
193 let full_time = format!("{mtime} +00:00");
194 let ts = OffsetDateTime::parse(&full_time, &fd)?;
195 let (secs, nanos) = (ts.unix_timestamp(), 0);
196 let mtime = FileTime::from_unix_time(secs, nanos);
197 let full_path = context.canonicalise_filename(filename)?;
198 drop(
200 OpenOptions::new()
201 .create(true)
202 .truncate(false)
203 .write(true)
204 .open(&full_path)?,
205 );
206 filetime::set_file_mtime(full_path, mtime)?;
208}
209
210#[step]
216pub fn create_from_text(context: &Datadir, text: &str, filename: &Path) {
217 context.open_write(filename)?.write_all(text.as_bytes())?;
218}
219
220#[step]
227#[context(Datadir)]
228#[context(Files)]
229pub fn remember_metadata(context: &ScenarioContext, filename: &Path) {
230 let full_path = context.with(
231 |context: &Datadir| context.canonicalise_filename(filename),
232 false,
233 )?;
234 let metadata = fs::metadata(full_path)?;
235 context.with_mut(
236 |context: &mut Files| {
237 context.metadata.insert(filename.to_owned(), metadata);
238 Ok(())
239 },
240 false,
241 )?;
242}
243
244#[step]
251pub fn touch(context: &Datadir, filename: &Path) {
252 let full_path = context.canonicalise_filename(filename)?;
253 let now = FileTime::now();
254 drop(
256 OpenOptions::new()
257 .create(true)
258 .truncate(false)
259 .write(true)
260 .open(&full_path)?,
261 );
262 filetime::set_file_mtime(full_path, now)?;
264}
265
266#[step]
272pub fn file_exists(context: &Datadir, filename: &Path) {
273 let full_path = context.canonicalise_filename(filename)?;
274 match fs::metadata(full_path) {
275 Ok(_) => (),
276 Err(e) => {
277 if matches!(e.kind(), io::ErrorKind::NotFound) {
278 throw!(format!("file '{}' was not found", filename.display()))
279 } else {
280 throw!(e);
281 }
282 }
283 }
284}
285
286#[step]
292pub fn file_does_not_exist(context: &Datadir, filename: &Path) {
293 let full_path = context.canonicalise_filename(filename)?;
294 match fs::metadata(full_path) {
295 Ok(_) => {
296 throw!(format!(
297 "file '{}' was unexpectedly found",
298 filename.display()
299 ))
300 }
301 Err(e) => {
302 if !matches!(e.kind(), io::ErrorKind::NotFound) {
303 throw!(e);
304 }
305 }
306 }
307}
308
309#[step]
318pub fn only_these_exist(context: &Datadir, filenames: &str) {
319 let filenames: HashSet<OsString> = filenames
320 .replace(',', "")
321 .split_ascii_whitespace()
322 .map(|s| s.into())
323 .collect();
324 let fnames: HashSet<OsString> = fs::read_dir(context.base_path())?
325 .map(|entry| entry.map(|entry| entry.file_name()))
326 .collect::<Result<_, _>>()?;
327 assert_eq!(filenames, fnames);
328}
329
330#[step]
337pub fn file_contains(context: &Datadir, filename: &Path, data: &str) {
338 let full_path = context.canonicalise_filename(filename)?;
339 let body = fs::read_to_string(full_path)?;
340 if !body.contains(data) {
341 println!("file {} contains:\n{}", filename.display(), body);
342 throw!("expected file content not found");
343 }
344}
345
346#[step]
353pub fn file_doesnt_contain(context: &Datadir, filename: &Path, data: &str) {
354 let full_path = context.canonicalise_filename(filename)?;
355 let body = fs::read_to_string(full_path)?;
356 if body.contains(data) {
357 println!("file {} contains:\n{}", filename.display(), body);
358 throw!("unexpected file content found");
359 }
360}
361
362#[step]
370pub fn file_matches_regex(context: &Datadir, filename: &Path, regex: &str) {
371 let full_path = context.canonicalise_filename(filename)?;
372 let regex = Regex::new(regex)?;
373 let body = fs::read_to_string(full_path)?;
374 if !regex.is_match(&body) {
375 println!("file {} contains:\n{}", filename.display(), body);
376 throw!("file content does not match given regex");
377 }
378}
379
380#[step]
386pub fn file_match(context: &Datadir, filename1: &Path, filename2: &Path) {
387 let full_path1 = context.canonicalise_filename(filename1)?;
388 let full_path2 = context.canonicalise_filename(filename2)?;
389 let body1 = fs::read(full_path1)?;
390 let body2 = fs::read(full_path2)?;
391 if body1 != body2 {
392 println!(
393 "file {} contains:\n{}",
394 filename1.display(),
395 String::from_utf8_lossy(&body1)
396 );
397 println!(
398 "file {} contains:\n{}",
399 filename2.display(),
400 String::from_utf8_lossy(&body2)
401 );
402 throw!("file contents do not match each other");
403 }
404}
405
406#[step]
413pub fn file_do_not_match(context: &Datadir, filename1: &Path, filename2: &Path) {
414 let full_path1 = context.canonicalise_filename(filename1)?;
415 let full_path2 = context.canonicalise_filename(filename2)?;
416 let body1 = fs::read(full_path1)?;
417 let body2 = fs::read(full_path2)?;
418 if body1 == body2 {
419 println!(
420 "file {} contains:\n{}",
421 filename1.display(),
422 String::from_utf8_lossy(&body1)
423 );
424 println!(
425 "file {} contains:\n{}",
426 filename2.display(),
427 String::from_utf8_lossy(&body2)
428 );
429 throw!("file contents do not differ");
430 }
431}
432
433#[step]
439pub fn file_and_embedded_file_match(context: &Datadir, filename: &Path, embedded: SubplotDataFile) {
440 let full_path = context.canonicalise_filename(filename)?;
441 let body1 = fs::read(full_path)?;
442
443 let body2 = embedded.data();
444 if body1 != body2 {
445 println!(
446 "file {} contains:\n{}",
447 filename.display(),
448 String::from_utf8_lossy(&body1)
449 );
450 println!(
451 "embedded file {} contains:\n{}",
452 embedded.name().display(),
453 String::from_utf8_lossy(body2)
454 );
455 throw!("file contents do not match each other");
456 }
457}
458
459#[step]
465pub fn file_and_embedded_file_do_not_match(
466 context: &Datadir,
467 filename: &Path,
468 embedded: SubplotDataFile,
469) {
470 let full_path = context.canonicalise_filename(filename)?;
471 let body1 = fs::read(full_path)?;
472
473 let body2 = embedded.data();
474 if body1 == body2 {
475 println!(
476 "file {} contains:\n{}",
477 filename.display(),
478 String::from_utf8_lossy(&body1)
479 );
480 println!(
481 "embedded file {} contains:\n{}",
482 embedded.name().display(),
483 String::from_utf8_lossy(body2)
484 );
485 throw!("file contents match each other");
486 }
487}
488
489#[step]
501#[context(Datadir)]
502#[context(Files)]
503pub fn has_remembered_metadata(context: &ScenarioContext, filename: &Path) {
504 let full_path = context.with(
505 |context: &Datadir| context.canonicalise_filename(filename),
506 false,
507 )?;
508 let metadata = fs::metadata(full_path)?;
509 if let Some(remembered) = context.with(
510 |context: &Files| Ok(context.metadata.get(filename).cloned()),
511 false,
512 )? {
513 if metadata.permissions() != remembered.permissions()
514 || metadata.modified()? != remembered.modified()?
515 || metadata.len() != remembered.len()
516 || metadata.is_file() != remembered.is_file()
517 {
518 throw!(format!(
519 "metadata change detected for {}",
520 filename.display()
521 ));
522 }
523 } else {
524 throw!(format!("no remembered metadata for {}", filename.display()));
525 }
526}
527
528#[step]
540#[context(Datadir)]
541#[context(Files)]
542pub fn has_different_metadata(context: &ScenarioContext, filename: &Path) {
543 let full_path = context.with(
544 |context: &Datadir| context.canonicalise_filename(filename),
545 false,
546 )?;
547 let metadata = fs::metadata(full_path)?;
548 if let Some(remembered) = context.with(
549 |context: &Files| Ok(context.metadata.get(filename).cloned()),
550 false,
551 )? {
552 if metadata.permissions() == remembered.permissions()
553 && metadata.modified()? == remembered.modified()?
554 && metadata.len() == remembered.len()
555 && metadata.is_file() == remembered.is_file()
556 {
557 throw!(format!(
558 "metadata change not detected for {}",
559 filename.display()
560 ));
561 }
562 } else {
563 throw!(format!("no remembered metadata for {}", filename.display()));
564 }
565}
566
567#[step]
573pub fn mtime_is_recent(context: &Datadir, filename: &Path) {
574 let full_path = context.canonicalise_filename(filename)?;
575 let metadata = fs::metadata(full_path)?;
576 let mtime = metadata.modified()?;
577 let diff = SystemTime::now().duration_since(mtime)?;
578 if diff > (Duration::from_secs(5)) {
579 throw!(format!("{} is older than 5 seconds", filename.display()));
580 }
581}
582
583#[step]
589pub fn mtime_is_ancient(context: &Datadir, filename: &Path) {
590 let full_path = context.canonicalise_filename(filename)?;
591 let metadata = fs::metadata(full_path)?;
592 let mtime = metadata.modified()?;
593 let diff = SystemTime::now().duration_since(mtime)?;
594 if diff < (Duration::from_secs(39 * 365 * 24 * 3600)) {
595 throw!(format!("{} is younger than 39 years", filename.display()));
596 }
597}
598
599#[step]
605pub fn make_directory(context: &Datadir, path: &Path) {
606 context.create_dir_all(path)?;
607}
608
609#[step]
615pub fn remove_directory(context: &Datadir, path: &Path) {
616 let full_path = context.canonicalise_filename(path)?;
617 remove_dir_all::remove_dir_all(full_path)?;
618}
619
620#[step]
626pub fn remove_empty_directory(context: &Datadir, path: &Path) {
627 let full_path = context.canonicalise_filename(path)?;
628 std::fs::remove_dir(full_path)?;
629}
630
631#[step]
638pub fn path_exists(context: &Datadir, path: &Path) {
639 let full_path = context.canonicalise_filename(path)?;
640 if !fs::metadata(&full_path)?.is_dir() {
641 throw!(format!(
642 "{} exists but is not a directory",
643 full_path.display()
644 ))
645 }
646}
647
648#[step]
655pub fn path_does_not_exist(context: &Datadir, path: &Path) {
656 let full_path = context.canonicalise_filename(path)?;
657 match fs::metadata(&full_path) {
658 Ok(_) => throw!(format!("{} exists", full_path.display())),
659 Err(e) => {
660 if !matches!(e.kind(), io::ErrorKind::NotFound) {
661 throw!(e);
662 }
663 }
664 };
665}
666
667#[step]
674pub fn path_is_empty(context: &Datadir, path: &Path) {
675 let full_path = context.canonicalise_filename(path)?;
676 let mut iter = fs::read_dir(&full_path)?;
677 match iter.next() {
678 None => {}
679 Some(Ok(_)) => throw!(format!("{} is not empty", full_path.display())),
680 Some(Err(e)) => throw!(e),
681 }
682}
683
684#[step]
692pub fn path_is_not_empty(context: &Datadir, path: &Path) {
693 let full_path = context.canonicalise_filename(path)?;
694 let mut iter = fs::read_dir(&full_path)?;
695 match iter.next() {
696 None => throw!(format!("{} is empty", full_path.display())),
697 Some(Ok(_)) => {}
698 Some(Err(e)) => throw!(e),
699 }
700}