use std::fmt;
use std::fs;
use std::path::{Path, PathBuf};
use std::sync::atomic::{AtomicU64, Ordering};
use crate::SnapshotMode;
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct InlineLocation {
pub file: String,
pub line: u32,
pub column: u32,
}
#[derive(Debug)]
pub struct InlineSnapshotFailure {
pub expected: String,
pub actual: String,
}
impl fmt::Display for InlineSnapshotFailure {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
write!(f, "inline snapshot does not match")
}
}
impl std::error::Error for InlineSnapshotFailure {}
#[must_use]
pub fn normalize_inline_literal(raw: &str) -> String {
let body = raw
.strip_prefix("\r\n")
.or_else(|| raw.strip_prefix('\n'))
.unwrap_or(raw);
let indent = body
.lines()
.filter(|line| !line.trim().is_empty())
.map(|line| line.len() - line.trim_start().len())
.min()
.unwrap_or(0);
let dedented = body
.lines()
.map(|line| {
if line.len() >= indent {
&line[indent..]
} else {
""
}
})
.collect::<Vec<_>>()
.join("\n");
dedented.trim_end().to_string()
}
pub fn assert_inline_snapshot(
actual: &str,
raw: &str,
location: &InlineLocation,
mode: SnapshotMode,
) -> Result<(), InlineSnapshotFailure> {
let expected = normalize_inline_literal(raw);
let actual = actual
.strip_suffix("\r\n")
.or_else(|| actual.strip_suffix('\n'))
.unwrap_or(actual);
if actual == expected {
return Ok(());
}
match mode {
SnapshotMode::Compare => Err(InlineSnapshotFailure {
expected,
actual: actual.to_string(),
}),
SnapshotMode::Update => {
let _ = record_pending_patch(location, actual);
Ok(())
}
}
}
pub fn pending_patch_dir() -> std::io::Result<PathBuf> {
if let Some(target) = std::env::var_os("CARGO_TARGET_DIR") {
return Ok(PathBuf::from(target).join("test-better-pending"));
}
let start = std::env::current_dir()?;
let mut dir: &Path = &start;
loop {
if dir.join("Cargo.lock").is_file() {
return Ok(dir.join("target").join("test-better-pending"));
}
match dir.parent() {
Some(parent) => dir = parent,
None => {
return Err(std::io::Error::new(
std::io::ErrorKind::NotFound,
"no Cargo.lock found in any ancestor of the current directory",
));
}
}
}
}
static PATCH_SEQ: AtomicU64 = AtomicU64::new(0);
fn record_pending_patch(location: &InlineLocation, actual: &str) -> std::io::Result<()> {
let dir = pending_patch_dir()?;
fs::create_dir_all(&dir)?;
let seq = PATCH_SEQ.fetch_add(1, Ordering::Relaxed);
let file_name = format!("{}-{}.patch", std::process::id(), seq);
let body = format!(
"{}\n{}:{}\n{}",
location.file, location.line, location.column, actual
);
fs::write(dir.join(file_name), body)
}
pub fn parse_pending_patch(body: &str) -> std::io::Result<(InlineLocation, String)> {
let invalid = |msg: &str| std::io::Error::new(std::io::ErrorKind::InvalidData, msg.to_string());
let mut lines = body.splitn(3, '\n');
let file = lines.next().ok_or_else(|| invalid("empty patch file"))?;
let position = lines
.next()
.ok_or_else(|| invalid("patch file is missing its position line"))?;
let value = lines.next().unwrap_or("");
let (line, column) = position
.split_once(':')
.ok_or_else(|| invalid("position line is not `<line>:<column>`"))?;
let line = line
.parse()
.map_err(|_| invalid("patch line number is not an integer"))?;
let column = column
.parse()
.map_err(|_| invalid("patch column number is not an integer"))?;
Ok((
InlineLocation {
file: file.to_string(),
line,
column,
},
value.to_string(),
))
}
#[cfg(test)]
mod tests {
use test_better_core::{OrFail, TestResult};
use test_better_matchers::{eq, check, is_true};
use super::*;
#[test]
fn normalize_drops_leading_newline_and_common_indentation() -> TestResult {
let raw = "\n first\n second\n ";
check!(normalize_inline_literal(raw)).satisfies(eq("first\nsecond".to_string()))
}
#[test]
fn normalize_keeps_relative_indentation() -> TestResult {
let raw = "\n outer\n inner\n";
check!(normalize_inline_literal(raw)).satisfies(eq("outer\n inner".to_string()))
}
#[test]
fn normalize_leaves_a_bare_single_line_literal_alone() -> TestResult {
check!(normalize_inline_literal("just this")).satisfies(eq("just this".to_string()))
}
#[test]
fn a_matching_literal_passes_in_compare_mode() -> TestResult {
let location = InlineLocation {
file: "src/x.rs".to_string(),
line: 10,
column: 5,
};
assert_inline_snapshot("hello", "\n hello\n", &location, SnapshotMode::Compare)
.or_fail_with("a matching literal must compare equal")
}
#[test]
fn a_differing_literal_fails_in_compare_mode_carrying_both_sides() -> TestResult {
let location = InlineLocation {
file: "src/x.rs".to_string(),
line: 10,
column: 5,
};
let failure = assert_inline_snapshot(
"actual",
"\n expected\n",
&location,
SnapshotMode::Compare,
)
.err()
.or_fail_with("a differing literal must fail in compare mode")?;
check!(failure.expected).satisfies(eq("expected".to_string()))?;
check!(failure.actual).satisfies(eq("actual".to_string()))
}
#[test]
fn parse_pending_patch_round_trips_a_recorded_body() -> TestResult {
let body = "tests/foo.rs\n42:9\nline one\nline two";
let (location, value) = parse_pending_patch(body).or_fail()?;
check!(location.file.as_str()).satisfies(eq("tests/foo.rs"))?;
check!(location.line).satisfies(eq(42u32))?;
check!(location.column).satisfies(eq(9u32))?;
check!(value).satisfies(eq("line one\nline two".to_string()))?;
check!(parse_pending_patch("only-one-line").is_err()).satisfies(is_true())
}
}