mod common;
use std::fs;
use std::path::{Path, PathBuf};
use common::{LedgerDir, qctl, stderr};
use rstest::rstest;
use similar::{ChangeTag, TextDiff};
#[rstest]
fn a_verb_changes_only_what_it_must(
#[files("tests/fixtures/mutations/*")]
#[dirs]
case: PathBuf,
) {
let name = case
.file_name()
.and_then(|name| name.to_str())
.expect("a named case");
let before = read(&case, "before.yaml");
let line = read(&case, "command");
let line = line.trim();
let argv = shlex::split(line).unwrap_or_else(|| panic!("{name}: {line} is not a command"));
let dir = LedgerDir::empty();
dir.write(&before);
let path = dir.path.to_string_lossy().into_owned();
let mut args: Vec<&str> = argv.iter().map(String::as_str).collect();
args.extend(["--file", &path]);
let output = qctl(&args);
assert!(
output.status.success(),
"qctl {line} refused the ledger:\n{}",
stderr(&output)
);
let after = dir.read();
let checked = qctl(&["check", "--file", &path, "--no-git"]);
assert!(
checked.status.success(),
"check refuses what qctl {line} wrote:\n{}",
stderr(&checked)
);
let stamped = argv.first().is_some_and(|verb| verb == "archive");
insta::with_settings!({
filters => if stamped {
vec![(r"(?m)^(\+\s*completed: )\d{4}-\d{2}-\d{2}T\d{2}:\d{2}:\d{2}$", "${1}[NOW]")]
} else {
Vec::new()
},
description => format!("qctl {line}"),
omit_expression => true,
}, {
insta::assert_snapshot!(name, marked(&before, &after));
});
}
fn marked(before: &str, after: &str) -> String {
let diff = TextDiff::from_lines(before, after);
let mut body = String::new();
let (mut removed, mut added) = (0, 0);
for change in diff.iter_all_changes() {
let mark = match change.tag() {
ChangeTag::Delete => {
removed += 1;
'-'
}
ChangeTag::Insert => {
added += 1;
'+'
}
ChangeTag::Equal => ' ',
};
body.push(mark);
body.push_str(change.value());
}
format!("# -{removed} +{added}\n{body}")
}
fn read(case: &Path, name: &str) -> String {
fs::read_to_string(case.join(name))
.unwrap_or_else(|error| panic!("{}: {error}", case.join(name).display()))
}