use std::collections::{HashMap, HashSet};
use std::ffi::OsString;
use std::fs::{self, Metadata, OpenOptions};
use std::io::{self, Write};
use std::path::{Path, PathBuf};
use std::time::{Duration, SystemTime};
use filetime::FileTime;
use regex::Regex;
use time::macros::format_description;
use time::OffsetDateTime;
pub use crate::prelude::*;
pub use super::datadir::Datadir;
pub use super::runcmd::Runcmd;
#[derive(Debug, Default)]
pub struct Files {
metadata: HashMap<PathBuf, Metadata>,
}
impl ContextElement for Files {
fn created(&mut self, scenario: &Scenario) {
scenario.register_context_type::<Datadir>();
}
}
#[step]
#[context(Datadir)]
#[context(Runcmd)]
pub fn create_from_embedded(context: &ScenarioContext, embedded_file: SubplotDataFile) {
let filename_on_disk = PathBuf::from(format!("{}", embedded_file.name().display()));
create_from_embedded_with_other_name::call(context, &filename_on_disk, embedded_file)?;
}
#[step]
#[context(Datadir)]
#[context(Runcmd)]
pub fn create_from_embedded_with_other_name(
context: &ScenarioContext,
filename_on_disk: &Path,
embedded_file: SubplotDataFile,
) {
_create_from_embedded_with_other_name_executable::call(
context,
filename_on_disk,
embedded_file,
false,
)?;
}
#[step]
#[context(Datadir)]
#[context(Runcmd)]
pub fn create_executable_from_embedded(context: &ScenarioContext, embedded_file: SubplotDataFile) {
let filename_on_disk = PathBuf::from(format!("{}", embedded_file.name().display()));
create_executable_from_embedded_with_other_name::call(
context,
&filename_on_disk,
embedded_file,
)?;
}
#[step]
#[context(Datadir)]
#[context(Runcmd)]
pub fn create_executable_from_embedded_with_other_name(
context: &ScenarioContext,
filename_on_disk: &Path,
embedded_file: SubplotDataFile,
) {
_create_from_embedded_with_other_name_executable::call(
context,
filename_on_disk,
embedded_file,
true,
)?;
}
#[doc(hidden)]
#[step]
#[context(Datadir)]
#[context(Runcmd)]
fn _create_from_embedded_with_other_name_executable(
context: &ScenarioContext,
filename_on_disk: &Path,
embedded_file: SubplotDataFile,
executable: bool,
) {
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()
)
})?;
let full_filename = context.with_mut(
|runcmd: &mut Runcmd| {
runcmd.with_forklock(|| {
context.with(
|datadir: &Datadir| -> Result<PathBuf, StepError> {
datadir.create_dir_all(parentpath)?;
datadir
.open_write(&filename_on_disk)?
.write_all(embedded_file.data())?;
datadir.canonicalise_filename(&filename_on_disk)
},
false,
)
})
},
false,
)?;
if executable {
#[cfg(unix)]
use std::os::unix::fs::PermissionsExt;
let mut perms = std::fs::symlink_metadata(&full_filename)?.permissions();
#[cfg(unix)]
perms.set_mode(perms.mode() | 0o111);
std::fs::set_permissions(&full_filename, perms)?;
}
}
#[step]
pub fn remove_file(context: &Datadir, filename: &Path) {
let filename = PathBuf::from(filename);
context.remove_file(filename)?;
}
#[step]
pub fn touch_with_timestamp(context: &Datadir, filename: &Path, mtime: &str) {
let fd = format_description!(
"[year]-[month]-[day] [hour]:[minute]:[second] [offset_hour]:[offset_minute]"
);
let full_time = format!("{mtime} +00:00");
let ts = OffsetDateTime::parse(&full_time, &fd)?;
let (secs, nanos) = (ts.unix_timestamp(), 0);
let mtime = FileTime::from_unix_time(secs, nanos);
let full_path = context.canonicalise_filename(filename)?;
drop(
OpenOptions::new()
.create(true)
.truncate(false)
.write(true)
.open(&full_path)?,
);
filetime::set_file_mtime(full_path, mtime)?;
}
#[step]
pub fn create_from_text(context: &Datadir, text: &str, filename: &Path) {
context.open_write(filename)?.write_all(text.as_bytes())?;
}
#[step]
#[context(Datadir)]
#[context(Files)]
pub fn remember_metadata(context: &ScenarioContext, filename: &Path) {
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: &Path) {
let full_path = context.canonicalise_filename(filename)?;
let now = FileTime::now();
drop(
OpenOptions::new()
.create(true)
.truncate(false)
.write(true)
.open(&full_path)?,
);
filetime::set_file_mtime(full_path, now)?;
}
#[step]
pub fn file_exists(context: &Datadir, filename: &Path) {
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.display()))
} else {
throw!(e);
}
}
}
}
#[step]
pub fn file_does_not_exist(context: &Datadir, filename: &Path) {
let full_path = context.canonicalise_filename(filename)?;
match fs::metadata(full_path) {
Ok(_) => {
throw!(format!(
"file '{}' was unexpectedly found",
filename.display()
))
}
Err(e) => {
if !matches!(e.kind(), io::ErrorKind::NotFound) {
throw!(e);
}
}
}
}
#[step]
pub fn only_these_exist(context: &Datadir, filenames: &str) {
let filenames: HashSet<OsString> = filenames
.replace(',', "")
.split_ascii_whitespace()
.map(|s| s.into())
.collect();
let fnames: HashSet<OsString> = fs::read_dir(context.base_path())?
.map(|entry| entry.map(|entry| entry.file_name()))
.collect::<Result<_, _>>()?;
assert_eq!(filenames, fnames);
}
#[step]
pub fn file_contains(context: &Datadir, filename: &Path, data: &str) {
let full_path = context.canonicalise_filename(filename)?;
let body = fs::read_to_string(full_path)?;
if !body.contains(data) {
println!("file {} contains:\n{}", filename.display(), body);
throw!("expected file content not found");
}
}
#[step]
pub fn file_doesnt_contain(context: &Datadir, filename: &Path, data: &str) {
let full_path = context.canonicalise_filename(filename)?;
let body = fs::read_to_string(full_path)?;
if body.contains(data) {
println!("file {} contains:\n{}", filename.display(), body);
throw!("unexpected file content found");
}
}
#[step]
pub fn file_matches_regex(context: &Datadir, filename: &Path, 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) {
println!("file {} contains:\n{}", filename.display(), body);
throw!("file content does not match given regex");
}
}
#[step]
pub fn file_match(context: &Datadir, filename1: &Path, filename2: &Path) {
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 {
println!(
"file {} contains:\n{}",
filename1.display(),
String::from_utf8_lossy(&body1)
);
println!(
"file {} contains:\n{}",
filename2.display(),
String::from_utf8_lossy(&body2)
);
throw!("file contents do not match each other");
}
}
#[step]
pub fn file_do_not_match(context: &Datadir, filename1: &Path, filename2: &Path) {
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 {
println!(
"file {} contains:\n{}",
filename1.display(),
String::from_utf8_lossy(&body1)
);
println!(
"file {} contains:\n{}",
filename2.display(),
String::from_utf8_lossy(&body2)
);
throw!("file contents do not differ");
}
}
#[step]
pub fn file_and_embedded_file_match(context: &Datadir, filename: &Path, embedded: SubplotDataFile) {
let full_path = context.canonicalise_filename(filename)?;
let body1 = fs::read(full_path)?;
let body2 = embedded.data();
if body1 != body2 {
println!(
"file {} contains:\n{}",
filename.display(),
String::from_utf8_lossy(&body1)
);
println!(
"embedded file {} contains:\n{}",
embedded.name().display(),
String::from_utf8_lossy(body2)
);
throw!("file contents do not match each other");
}
}
#[step]
pub fn file_and_embedded_file_do_not_match(
context: &Datadir,
filename: &Path,
embedded: SubplotDataFile,
) {
let full_path = context.canonicalise_filename(filename)?;
let body1 = fs::read(full_path)?;
let body2 = embedded.data();
if body1 == body2 {
println!(
"file {} contains:\n{}",
filename.display(),
String::from_utf8_lossy(&body1)
);
println!(
"embedded file {} contains:\n{}",
embedded.name().display(),
String::from_utf8_lossy(body2)
);
throw!("file contents match each other");
}
}
#[step]
#[context(Datadir)]
#[context(Files)]
pub fn has_remembered_metadata(context: &ScenarioContext, filename: &Path) {
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.display()
));
}
} else {
throw!(format!("no remembered metadata for {}", filename.display()));
}
}
#[step]
#[context(Datadir)]
#[context(Files)]
pub fn has_different_metadata(context: &ScenarioContext, filename: &Path) {
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.display()
));
}
} else {
throw!(format!("no remembered metadata for {}", filename.display()));
}
}
#[step]
pub fn mtime_is_recent(context: &Datadir, filename: &Path) {
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.display()));
}
}
#[step]
pub fn mtime_is_ancient(context: &Datadir, filename: &Path) {
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.display()));
}
}
#[step]
pub fn make_directory(context: &Datadir, path: &Path) {
context.create_dir_all(path)?;
}
#[step]
pub fn remove_directory(context: &Datadir, path: &Path) {
let full_path = context.canonicalise_filename(path)?;
remove_dir_all::remove_dir_all(full_path)?;
}
#[step]
pub fn remove_empty_directory(context: &Datadir, path: &Path) {
let full_path = context.canonicalise_filename(path)?;
std::fs::remove_dir(full_path)?;
}
#[step]
pub fn path_exists(context: &Datadir, path: &Path) {
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: &Path) {
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: &Path) {
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: &Path) {
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),
}
}