use std::path::{Path, PathBuf};
use crate::error::Error;
use crate::util;
pub fn snapshot_path(fixture_path: &Path) -> PathBuf {
fixture_path.with_extension("stderr")
}
#[derive(Debug)]
pub enum ReadOutcome {
Found(String),
Missing,
Malformed {
byte_offset: usize,
#[allow(dead_code)]
total_bytes: usize,
},
}
pub fn try_read(fixture_path: &Path) -> Result<ReadOutcome, Error> {
let p = snapshot_path(fixture_path);
match std::fs::read(&p) {
Ok(bytes) => match std::str::from_utf8(&bytes) {
Ok(s) => Ok(ReadOutcome::Found(normalize_for_compare(s))),
Err(e) => Ok(ReadOutcome::Malformed {
byte_offset: e.valid_up_to(),
total_bytes: bytes.len(),
}),
},
Err(e) if e.kind() == std::io::ErrorKind::NotFound => Ok(ReadOutcome::Missing),
Err(e) => Err(Error::io(e, "reading snapshot", Some(p))),
}
}
pub fn write(fixture_path: &Path, normalized_stderr: &str) -> Result<PathBuf, Error> {
let p = snapshot_path(fixture_path);
let mut bytes: Vec<u8> = normalized_stderr.bytes().collect();
while bytes.last().copied() == Some(b'\n') {
bytes.pop();
}
bytes.push(b'\n');
util::write_file_atomic(&p, &bytes)?;
Ok(p)
}
fn normalize_for_compare(s: &str) -> String {
let mut s = s.replace("\r\n", "\n").replace('\r', "\n");
while s.ends_with('\n') {
s.pop();
}
s
}
#[cfg(test)]
mod tests {
use super::*;
use tempfile::tempdir;
#[test]
fn snapshot_path_swaps_extension() {
assert_eq!(
snapshot_path(Path::new("foo/bar.rs")),
PathBuf::from("foo/bar.stderr")
);
}
#[test]
fn write_then_read_round_trips() {
let tmp = tempdir().unwrap();
let fixture = tmp.path().join("fixture.rs");
let p = write(&fixture, "alpha\nbeta").unwrap();
assert_eq!(p, tmp.path().join("fixture.stderr"));
let bytes = std::fs::read(&p).unwrap();
assert_eq!(bytes, b"alpha\nbeta\n".to_vec());
match try_read(&fixture).unwrap() {
ReadOutcome::Found(s) => assert_eq!(s, "alpha\nbeta"),
other => panic!("expected Found, got {other:?}"),
}
}
#[test]
fn write_strips_then_appends_single_trailing_newline() {
let tmp = tempdir().unwrap();
let fixture = tmp.path().join("x.rs");
write(&fixture, "alpha\n\n\n").unwrap();
let bytes = std::fs::read(tmp.path().join("x.stderr")).unwrap();
assert_eq!(bytes, b"alpha\n".to_vec());
}
#[test]
fn try_read_returns_missing_when_absent() {
let tmp = tempdir().unwrap();
match try_read(&tmp.path().join("absent.rs")).unwrap() {
ReadOutcome::Missing => {}
other => panic!("expected Missing, got {other:?}"),
}
}
#[test]
fn try_read_normalizes_crlf() {
let tmp = tempdir().unwrap();
let fixture = tmp.path().join("crlf.rs");
let snap = tmp.path().join("crlf.stderr");
std::fs::write(&snap, b"a\r\nb\r\n").unwrap();
match try_read(&fixture).unwrap() {
ReadOutcome::Found(s) => assert_eq!(s, "a\nb"),
other => panic!("expected Found, got {other:?}"),
}
}
#[test]
fn try_read_returns_malformed_with_correct_offset() {
let tmp = tempdir().unwrap();
let fixture = tmp.path().join("badbytes.rs");
let snap = tmp.path().join("badbytes.stderr");
let mut payload: Vec<u8> = b"hello".to_vec();
payload.push(0xFE);
payload.extend_from_slice(b"world");
std::fs::write(&snap, &payload).unwrap();
match try_read(&fixture).unwrap() {
ReadOutcome::Malformed {
byte_offset,
total_bytes,
} => {
assert_eq!(byte_offset, 5, "first invalid byte is at offset 5");
assert_eq!(total_bytes, payload.len());
}
other => panic!("expected Malformed, got {other:?}"),
}
}
#[test]
fn try_read_malformed_at_offset_zero_when_first_byte_invalid() {
let tmp = tempdir().unwrap();
let fixture = tmp.path().join("badfirst.rs");
let snap = tmp.path().join("badfirst.stderr");
std::fs::write(&snap, [0xFE]).unwrap();
match try_read(&fixture).unwrap() {
ReadOutcome::Malformed {
byte_offset,
total_bytes,
} => {
assert_eq!(byte_offset, 0);
assert_eq!(total_bytes, 1);
}
other => panic!("expected Malformed, got {other:?}"),
}
}
}