#![cfg_attr(docsrs, feature(doc_cfg))]
#[cfg(feature = "predicates")]
mod feature_predicates;
#[cfg(feature = "predicates")]
pub use feature_predicates::*;
use atomicwrites::{AtomicFile, OverwriteBehavior};
use console::Style;
use newline_converter::dos2unix;
use similar::{Algorithm, ChangeTag, TextDiff};
use std::{env, ffi::OsStr, fs, io::Write, path::Path};
#[track_caller]
pub fn assert_contents<P: AsRef<Path>>(path: P, actual: &str) {
if let Err(e) =
assert_contents_impl(path, actual, OverwriteMode::from_env())
{
panic!("assertion failed: {e}")
}
}
#[derive(Clone, Copy, Debug)]
pub(crate) enum OverwriteMode {
Check,
Overwrite,
}
impl OverwriteMode {
pub(crate) fn from_env() -> Self {
let var = env::var_os("EXPECTORATE");
if var.as_deref().and_then(OsStr::to_str) == Some("overwrite") {
OverwriteMode::Overwrite
} else {
OverwriteMode::Check
}
}
}
pub(crate) fn assert_contents_impl<P: AsRef<Path>>(
path: P,
actual: &str,
mode: OverwriteMode,
) -> Result<(), String> {
let path = path.as_ref();
let actual = dos2unix(actual);
let current = match fs::read_to_string(path) {
Ok(s) => Some(s),
Err(e) => match e.kind() {
std::io::ErrorKind::NotFound => None,
_ => panic!("unable to read contents of {}: {}", path.display(), e),
},
};
match mode {
OverwriteMode::Overwrite => {
if current.as_deref() != Some(&actual) {
let behavior = if current.is_some() {
OverwriteBehavior::AllowOverwrite
} else {
OverwriteBehavior::DisallowOverwrite
};
let f = AtomicFile::new(path, behavior);
let res = f.write(|f| {
f.write(actual.as_bytes())
});
if let Err(e) = res {
panic!("unable to write to {}: {}", path.display(), e);
}
}
}
OverwriteMode::Check => {
let expected_s = current.unwrap_or_default();
let expected = dos2unix(&expected_s);
if expected != actual {
for hunk in TextDiff::configure()
.algorithm(Algorithm::Myers)
.diff_lines(&expected, &actual)
.unified_diff()
.context_radius(5)
.iter_hunks()
{
println!("{}", hunk.header());
for change in hunk.iter_changes() {
let (marker, style) = match change.tag() {
ChangeTag::Delete => ('-', Style::new().red()),
ChangeTag::Insert => ('+', Style::new().green()),
ChangeTag::Equal => (' ', Style::new()),
};
print!("{}", style.apply_to(marker).bold());
print!("{}", style.apply_to(change));
if change.missing_newline() {
println!();
}
}
}
println!();
return Err(format!(
r#"string doesn't match the contents of file: "{}" see diffset above
set EXPECTORATE=overwrite if these changes are intentional"#,
path.display()
));
}
}
}
Ok(())
}
#[cfg(test)]
mod tests {
use super::*;
use filetime::{set_file_mtime, FileTime};
use tempfile::TempDir;
#[test]
fn overwite_same_mtime_doesnt_change() {
static CONTENTS: &str = "foo";
const MTIME: FileTime = FileTime::from_unix_time(946684800, 0);
let dir = TempDir::with_prefix("expectorate-").unwrap();
let path = dir.path().join("my-file.txt");
fs::write(&path, CONTENTS).unwrap();
set_file_mtime(&path, MTIME).unwrap();
assert_contents_impl(&path, CONTENTS, OverwriteMode::Overwrite)
.unwrap();
let meta = fs::metadata(&path).unwrap();
let mtime2 = FileTime::from_last_modification_time(&meta);
assert_eq!(mtime2, MTIME, "mtime is zero");
}
}