1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121
// Copyright 2023 Oxide Computer Company
#![cfg_attr(docsrs, feature(doc_cfg))]
//! This library is for comparing multi-line output to data stored in version
//! controlled files. It makes it easy to update the contents when should be
//! updated to match the new results.
//!
//! Use it like this:
//!
//! ```rust
//! # fn compose() -> &'static str { "" }
//! let actual: &str = compose();
//! expectorate::assert_contents("lyrics.txt", actual);
//! ```
//!
//! If the output doesn't match, the program will panic! and emit the
//! color-coded diffs.
//!
//! To accept the changes from `compose()`, run with `EXPECTORATE=overwrite`.
//! Assuming `lyrics.txt` is checked in, `git diff` will show you something
//! like this:
//!
//! ```diff
//! diff --git a/examples/lyrics.txt b/examples/lyrics.txt
//! index e4104c1..ea6beaf 100644
//! --- a/examples/lyrics.txt
//! +++ b/examples/lyrics.txt
//! @@ -1,5 +1,2 @@
//! -No one hits like Gaston
//! -Matches wits like Gaston
//! -In a spitting match nobody spits like Gaston
//! +In a testing match nobody tests like Gaston
//! I'm especially good at expectorating
//! -Ten points for Gaston
//! ```
//!
//! # `predicates` feature
//!
//! Enable the `predicates` feature for compatibility with `predicates` via
//! [`eq_file`] and [`eq_file_or_panic`].
//! # Predicates (feature: predicates)
//! Expectorate can be used in places where you might use the [`predicates`
//! crate](https://crates.io/crates/predicates). If you're using
//! `predicates::path::eq_file` you can instead use `expectorate::eq_file` or
//! `expectorate::eq_file_or_panic`. Populate or update the specified file as
//! above.
#[cfg(feature = "predicates")]
mod feature_predicates;
#[cfg(feature = "predicates")]
pub use feature_predicates::*;
use console::Style;
use newline_converter::dos2unix;
use similar::{Algorithm, ChangeTag, TextDiff};
use std::{env, ffi::OsStr, fs, path::Path};
/// Compare the contents of the file to the string provided
#[track_caller]
pub fn assert_contents<P: AsRef<Path>>(path: P, actual: &str) {
if let Err(e) = assert_contents_impl(path, actual) {
panic!("assertion failed: {e}")
}
}
pub(crate) fn assert_contents_impl<P: AsRef<Path>>(path: P, actual: &str) -> Result<(), String> {
let path = path.as_ref();
let var = env::var_os("EXPECTORATE");
let overwrite = var.as_deref().and_then(OsStr::to_str) == Some("overwrite");
let actual = dos2unix(actual);
if overwrite {
if let Err(e) = fs::write(path, actual.as_ref()) {
panic!("unable to write to {}: {}", path.display(), e);
}
} else {
// Treat a nonexistent file like an empty file.
let expected_s = match fs::read_to_string(path) {
Ok(s) => s,
Err(e) => match e.kind() {
std::io::ErrorKind::NotFound => String::new(),
_ => panic!("unable to read contents of {}: {}", path.display(), e),
},
};
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(())
}