use std::{
ffi::OsStr,
fs,
path::{Path, PathBuf},
process::Command as ProcessCommand,
};
use assert_cmd::Command;
use rstest_bdd::Slot;
use rstest_bdd_macros::ScenarioState;
use tempfile::TempDir;
pub const RAGGED: &str = "|A|B|\n|---|---|\n|1|2|\n";
pub const CLEAN: &str = "| A | B |\n| --- | --- |\n| 1 | 2 |\n";
const IDENTITY_NAME: &str = "mdtablefix tests";
const IDENTITY_EMAIL: &str = "tests@example.invalid";
#[derive(Clone, Debug)]
pub struct Run {
pub status: i32,
pub stdout: String,
pub stderr: String,
}
#[derive(Default, ScenarioState)]
pub struct GitSelectionState {
repo: Slot<TempDir>,
pub(crate) elsewhere: Slot<TempDir>,
pub(crate) run_dir: Slot<PathBuf>,
pub(crate) before: Slot<Vec<(String, Vec<u8>)>>,
pub(crate) run: Slot<Run>,
}
pub fn repo_path(state: &GitSelectionState) -> PathBuf {
state
.repo
.get_or_insert_with(|| tempfile::tempdir().expect("create temporary directory"))
.path()
.to_path_buf()
}
pub fn run_directory(state: &GitSelectionState) -> PathBuf {
state.run_dir.get().unwrap_or_else(|| repo_path(state))
}
pub fn git_raw(directory: &Path, args: &[&str]) -> std::process::Output {
ProcessCommand::new("git")
.current_dir(directory)
.args(args)
.env("GIT_CONFIG_NOSYSTEM", "1")
.env("GIT_CONFIG_GLOBAL", "/dev/null")
.env("HOME", directory)
.env("LC_ALL", "C")
.env("LANGUAGE", "")
.env("GIT_AUTHOR_NAME", IDENTITY_NAME)
.env("GIT_AUTHOR_EMAIL", IDENTITY_EMAIL)
.env("GIT_COMMITTER_NAME", IDENTITY_NAME)
.env("GIT_COMMITTER_EMAIL", IDENTITY_EMAIL)
.output()
.expect("run git; the fixture needs it on PATH")
}
pub fn git(directory: &Path, args: &[&str]) {
let output = git_raw(directory, args);
assert!(
output.status.success(),
"git {args:?} failed with {}: {}",
output.status,
String::from_utf8_lossy(&output.stderr)
);
}
pub fn write_fixture(path: &Path, content: &str) {
if let Some(parent) = path.parent() {
fs::create_dir_all(parent).expect("create the fixture's directory");
}
fs::write(path, content).expect("write a fixture");
}
pub fn commit_file(state: &GitSelectionState, name: &str, content: &str) {
let repo = repo_path(state);
write_fixture(&repo.join(name), content);
git(&repo, &["add", "--", name]);
git(&repo, &["commit", "-m", &format!("add {name}")]);
}
pub fn snapshot(root: &Path) -> Vec<(String, Vec<u8>)> {
fn walk(root: &Path, directory: &Path, entries: &mut Vec<(String, Vec<u8>)>) {
for entry in fs::read_dir(directory).expect("read the fixture directory") {
let entry = entry.expect("read a fixture directory entry");
let path = entry.path();
let name = entry.file_name();
if name == OsStr::new(".git") {
continue;
}
let relative = path
.strip_prefix(root)
.expect("every entry is beneath the root")
.components()
.map(|part| part.as_os_str().to_string_lossy().into_owned())
.collect::<Vec<_>>()
.join("/");
let metadata = fs::symlink_metadata(&path).expect("read fixture metadata");
if metadata.file_type().is_symlink() {
let target = fs::read_link(&path).expect("read the link target");
entries.push((relative, target.to_string_lossy().into_owned().into_bytes()));
} else if metadata.is_dir() {
walk(root, &path, entries);
} else {
entries.push((relative, fs::read(&path).expect("read a fixture")));
}
}
}
let mut entries = Vec::new();
walk(root, root, &mut entries);
entries.sort();
entries
}
pub fn before_bytes(state: &GitSelectionState, name: &str) -> Vec<u8> {
state
.before
.get()
.expect("the run must capture the fixture's bytes first")
.into_iter()
.find(|(path, _)| path == name)
.unwrap_or_else(|| panic!("{name} was not part of the fixture before the run"))
.1
}
pub fn last_run(state: &GitSelectionState) -> Run {
state
.run
.get()
.expect("the scenario must run mdtablefix before asserting on it")
}
pub fn run_once(directory: &Path, flags: &str) -> Run {
let output = Command::cargo_bin("mdtablefix")
.expect("cargo binary")
.current_dir(directory)
.args(flags.split_whitespace())
.env("GIT_CONFIG_NOSYSTEM", "1")
.env("GIT_CONFIG_GLOBAL", "/dev/null")
.env("HOME", directory)
.env("LC_ALL", "C")
.env("LANGUAGE", "")
.write_stdin(RAGGED)
.output()
.expect("run mdtablefix");
Run {
status: output.status.code().unwrap_or(-1),
stdout: String::from_utf8_lossy(&output.stdout).into_owned(),
stderr: String::from_utf8_lossy(&output.stderr).into_owned(),
}
}