//! Library of steps for handling files in the data dir. //! //! The files step library is intended to help with standard operations which //! people might need when writing subplot scenarios which use embedded files. use std::collections::{HashMap, HashSet}; use std::ffi::OsString; use std::fs::{self, Metadata, OpenOptions}; use std::io::{self, Write}; use std::path::PathBuf; use std::time::{Duration, SystemTime}; use chrono::{TimeZone, Utc}; use filetime::FileTime; use regex::Regex; pub use crate::prelude::*; pub use super::datadir::Datadir; #[derive(Default)] pub struct Files { metadata: HashMap, } impl ContextElement for Files { fn created(&mut self, scenario: &Scenario) { scenario.register_context_type::(); } } #[step] #[context(Datadir)] pub fn create_from_embedded(context: &ScenarioContext, embedded_file: SubplotDataFile) { let filename_on_disk = format!("{}", embedded_file.name().display()); create_from_embedded_with_other_name::call(context, &filename_on_disk, embedded_file) } #[step] pub fn create_from_embedded_with_other_name( context: &Datadir, filename_on_disk: &str, embedded_file: SubplotDataFile, ) { let filename_on_disk = PathBuf::from(filename_on_disk); let parentpath = filename_on_disk.parent().ok_or_else(|| { format!( "No parent directory found for {}", filename_on_disk.display() ) })?; context.create_dir_all(parentpath)?; context .open_write(filename_on_disk)? .write_all(embedded_file.data())?; } #[step] pub fn touch_with_timestamp(context: &Datadir, filename: &str, mtime: &str) { let ts = Utc.datetime_from_str(mtime, "%Y-%m-%d %H:%M:%S")?; let (secs, nanos) = (ts.timestamp(), ts.timestamp_subsec_nanos()); let mtime = FileTime::from_unix_time(secs, nanos); let full_path = context.canonicalise_filename(filename)?; // If the file doesn't exist, create it drop( OpenOptions::new() .create(true) .write(true) .open(&full_path)?, ); // And set its mtime filetime::set_file_mtime(full_path, mtime)?; } #[step] pub fn create_from_text(context: &Datadir, text: &str, filename: &str) { context.open_write(filename)?.write_all(text.as_bytes())?; } #[step] #[context(Datadir)] #[context(Files)] pub fn remember_metadata(context: &ScenarioContext, filename: &str) { let full_path = context.with( |context: &Datadir| context.canonicalise_filename(filename), false, )?; let metadata = fs::metadata(&full_path)?; context.with_mut( |context: &mut Files| { context.metadata.insert(filename.to_owned(), metadata); Ok(()) }, false, ) } #[step] pub fn touch(context: &Datadir, filename: &str) { let full_path = context.canonicalise_filename(filename)?; let now = FileTime::now(); // If the file doesn't exist, create it drop( OpenOptions::new() .create(true) .write(true) .open(&full_path)?, ); // And set its mtime filetime::set_file_mtime(full_path, now)?; } #[step] pub fn file_exists(context: &Datadir, filename: &str) { let full_path = context.canonicalise_filename(filename)?; match fs::metadata(full_path) { Ok(_) => (), Err(e) => { if matches!(e.kind(), io::ErrorKind::NotFound) { throw!(format!("file '{}' was not found", filename)) } else { throw!(e); } } } } #[step] pub fn file_does_not_exist(context: &Datadir, filename: &str) { let full_path = context.canonicalise_filename(filename)?; match fs::metadata(full_path) { Ok(_) => { throw!(format!("file '{}' was unexpectedly found", filename)) } Err(e) => { if !matches!(e.kind(), io::ErrorKind::NotFound) { throw!(e); } } } } #[step] pub fn only_these_exist(context: &Datadir, filenames: &str) { let filenames: HashSet = filenames .replace(',', "") .split_ascii_whitespace() .map(|s| s.into()) .collect(); let fnames: HashSet = fs::read_dir(context.base_path())? .map(|entry| entry.map(|entry| entry.file_name())) .collect::>()?; assert_eq!(filenames, fnames); } #[step] pub fn file_contains(context: &Datadir, filename: &str, data: &str) { let full_path = context.canonicalise_filename(filename)?; let body = fs::read_to_string(full_path)?; if !body.contains(data) { throw!("expected file content not found"); } } #[step] pub fn file_matches_regex(context: &Datadir, filename: &str, regex: &str) { let full_path = context.canonicalise_filename(filename)?; let regex = Regex::new(regex)?; let body = fs::read_to_string(full_path)?; if !regex.is_match(&body) { throw!("file content does not match given regex"); } } #[step] pub fn file_match(context: &Datadir, filename1: &str, filename2: &str) { let full_path1 = context.canonicalise_filename(filename1)?; let full_path2 = context.canonicalise_filename(filename2)?; let body1 = fs::read(full_path1)?; let body2 = fs::read(full_path2)?; if body1 != body2 { throw!("file contents do not match each other"); } } #[step] #[context(Datadir)] #[context(Files)] pub fn has_remembered_metadata(context: &ScenarioContext, filename: &str) { let full_path = context.with( |context: &Datadir| context.canonicalise_filename(filename), false, )?; let metadata = fs::metadata(&full_path)?; if let Some(remembered) = context.with( |context: &Files| Ok(context.metadata.get(filename).cloned()), false, )? { if metadata.permissions() != remembered.permissions() || metadata.modified()? != remembered.modified()? || metadata.len() != remembered.len() || metadata.is_file() != remembered.is_file() { throw!(format!("metadata change detected for {}", filename)); } } else { throw!(format!("no remembered metadata for {}", filename)); } } #[step] #[context(Datadir)] #[context(Files)] pub fn has_different_metadata(context: &ScenarioContext, filename: &str) { let full_path = context.with( |context: &Datadir| context.canonicalise_filename(filename), false, )?; let metadata = fs::metadata(&full_path)?; if let Some(remembered) = context.with( |context: &Files| Ok(context.metadata.get(filename).cloned()), false, )? { if metadata.permissions() == remembered.permissions() && metadata.modified()? == remembered.modified()? && metadata.len() == remembered.len() && metadata.is_file() == remembered.is_file() { throw!(format!("metadata change not detected for {}", filename)); } } else { throw!(format!("no remembered metadata for {}", filename)); } } #[step] pub fn mtime_is_recent(context: &Datadir, filename: &str) { let full_path = context.canonicalise_filename(filename)?; let metadata = fs::metadata(full_path)?; let mtime = metadata.modified()?; let diff = SystemTime::now().duration_since(mtime)?; if diff > (Duration::from_secs(5)) { throw!(format!("{} is older than 5 seconds", filename)); } } #[step] pub fn mtime_is_ancient(context: &Datadir, filename: &str) { let full_path = context.canonicalise_filename(filename)?; let metadata = fs::metadata(full_path)?; let mtime = metadata.modified()?; let diff = SystemTime::now().duration_since(mtime)?; if diff < (Duration::from_secs(39 * 365 * 24 * 3600)) { throw!(format!("{} is younger than 39 years", filename)); } } #[step] pub fn make_directory(context: &Datadir, path: &str) { context.create_dir_all(path)?; } #[step] pub fn remove_directory(context: &Datadir, path: &str) { let full_path = context.canonicalise_filename(path)?; remove_dir_all::remove_dir_all(full_path)?; } #[step] pub fn path_exists(context: &Datadir, path: &str) { let full_path = context.canonicalise_filename(path)?; if !fs::metadata(&full_path)?.is_dir() { throw!(format!( "{} exists but is not a directory", full_path.display() )) } } #[step] pub fn path_does_not_exist(context: &Datadir, path: &str) { let full_path = context.canonicalise_filename(path)?; match fs::metadata(&full_path) { Ok(_) => throw!(format!("{} exists", full_path.display())), Err(e) => { if !matches!(e.kind(), io::ErrorKind::NotFound) { throw!(e); } } }; } #[step] pub fn path_is_empty(context: &Datadir, path: &str) { let full_path = context.canonicalise_filename(path)?; let mut iter = fs::read_dir(&full_path)?; match iter.next() { None => {} Some(Ok(_)) => throw!(format!("{} is not empty", full_path.display())), Some(Err(e)) => throw!(e), } } #[step] pub fn path_is_not_empty(context: &Datadir, path: &str) { let full_path = context.canonicalise_filename(path)?; let mut iter = fs::read_dir(&full_path)?; match iter.next() { None => throw!(format!("{} is empty", full_path.display())), Some(Ok(_)) => {} Some(Err(e)) => throw!(e), } }