use std::{
fs,
path::{Path, PathBuf},
process::{ExitStatus, Output},
};
use anyhow::{Context as _, Result};
use assert_cmd::Command;
use tempfile::tempdir;
#[path = "cases.rs"]
mod cases;
#[path = "invariants.rs"]
mod invariants;
#[path = "reporting.rs"]
mod reporting;
#[path = "support_tests.rs"]
mod support_tests;
pub(crate) use cases::{ALL_FLAGS, BASE_MATRIX_CASES, STAGED_FILE};
pub(crate) use reporting::{assert_reporting_invariants, check_counts, diff_counts};
#[derive(Clone, Copy, Debug, Eq, Ord, PartialEq, PartialOrd)]
pub(crate) enum TransformFlag {
Renumber,
Breaks,
Ellipsis,
Fences,
Footnotes,
CodeEmphasis,
Headings,
}
impl TransformFlag {
pub(crate) fn as_arg(self) -> &'static str {
match self {
Self::Renumber => "--renumber",
Self::Breaks => "--breaks",
Self::Ellipsis => "--ellipsis",
Self::Fences => "--fences",
Self::Footnotes => "--footnotes",
Self::CodeEmphasis => "--code-emphasis",
Self::Headings => "--headings",
}
}
}
#[derive(Clone, Copy)]
pub(crate) struct BaseCase {
pub(crate) id: &'static str,
pub(crate) fixture: &'static str,
pub(crate) flags: &'static [TransformFlag],
pub(crate) reporting: &'static [ExecutionMode],
}
#[derive(Clone, Copy, Debug, Eq, Ord, PartialEq, PartialOrd)]
pub(crate) enum WrapVariant {
Wrapped,
Unwrapped,
}
impl WrapVariant {
fn id_part(self) -> &'static str {
match self {
Self::Wrapped => "wrap",
Self::Unwrapped => "nowrap",
}
}
}
#[derive(Clone, Copy, Debug, Eq, Ord, PartialEq, PartialOrd)]
pub(crate) enum ExecutionMode {
Stdout,
InPlace,
Check,
Diff,
}
impl ExecutionMode {
fn id_part(self) -> &'static str {
match self {
Self::Stdout => "stdout",
Self::InPlace => "in_place",
Self::Check => "check",
Self::Diff => "diff",
}
}
fn flag(self) -> Option<&'static str> {
match self {
Self::Stdout => None,
Self::InPlace => Some("--in-place"),
Self::Check => Some("--check"),
Self::Diff => Some("--diff"),
}
}
pub(crate) fn writes_file(self) -> bool { self == Self::InPlace }
pub(crate) fn reports(self) -> bool { matches!(self, Self::Check | Self::Diff) }
}
#[derive(Clone)]
pub(crate) struct LogicalCase {
pub(crate) id: String,
pub(crate) fixture: &'static str,
pub(crate) is_wrapped: bool,
pub(crate) flags: Vec<TransformFlag>,
pub(crate) reporting: Vec<ExecutionMode>,
}
pub(crate) struct PhysicalCase {
pub(crate) logical: LogicalCase,
pub(crate) mode: ExecutionMode,
}
pub(crate) struct RunResult {
pub(crate) output: Output,
pub(crate) file_content: Vec<u8>,
}
fn status_text(status: ExitStatus) -> String {
match status.code() {
Some(code) => format!("code: {code}"),
None => "no exit code".to_string(),
}
}
impl RunResult {
pub(crate) fn envelope(&self, case: &PhysicalCase) -> String {
let stdout = String::from_utf8_lossy(&self.output.stdout);
let stderr = String::from_utf8_lossy(&self.output.stderr);
let file = if case.mode.writes_file() {
String::from_utf8_lossy(&self.file_content).into_owned()
} else {
"<not applicable>\n".to_string()
};
format!(
"case: {}\nmode: {}\nargs: {}\nstatus: {}\n\n[stdout]\n{}\n[stderr]\n{}\n[file]\n{}",
case.logical.id,
case.mode.id_part(),
case.args().join(" "),
status_text(self.output.status),
stdout,
stderr,
file,
)
}
}
impl PhysicalCase {
pub(crate) fn snapshot_name(&self) -> String {
format!("{}_{}", self.logical.id, self.mode.id_part())
}
pub(crate) fn args(&self) -> Vec<&'static str> {
let mut args = Vec::new();
if self.logical.is_wrapped {
args.push("--wrap");
}
args.extend(self.logical.flags.iter().map(|flag| flag.as_arg()));
args.extend(self.mode.flag());
args
}
}
pub(crate) fn logical_cases() -> Vec<LogicalCase> {
BASE_MATRIX_CASES
.iter()
.flat_map(|case| {
[WrapVariant::Unwrapped, WrapVariant::Wrapped].map(move |variant| LogicalCase {
id: format!("{}_{}", case.id, variant.id_part()),
fixture: case.fixture,
is_wrapped: variant == WrapVariant::Wrapped,
flags: case.flags.to_vec(),
reporting: case.reporting.to_vec(),
})
})
.collect()
}
pub(crate) fn reporting_cases() -> Vec<LogicalCase> {
logical_cases()
.into_iter()
.filter(|case| !case.reporting.is_empty())
.collect()
}
pub(crate) fn physical_case(logical: &LogicalCase, mode: ExecutionMode) -> PhysicalCase {
PhysicalCase {
logical: logical.clone(),
mode,
}
}
fn modes_for(logical: &LogicalCase) -> Vec<ExecutionMode> {
let mut modes = vec![ExecutionMode::Stdout, ExecutionMode::InPlace];
modes.extend(logical.reporting.iter().copied());
modes
}
pub(crate) fn physical_cases() -> Vec<PhysicalCase> {
logical_cases()
.into_iter()
.flat_map(|logical| {
modes_for(&logical)
.into_iter()
.map(move |mode| PhysicalCase {
logical: logical.clone(),
mode,
})
})
.collect()
}
pub(crate) fn assert_transform_invariants(logical: &LogicalCase, stdout: &[u8]) -> Result<()> {
invariants::assert_transform_invariants(logical, stdout)
}
pub(crate) fn fixture_has_table(file_name: &str) -> Result<bool> {
invariants::fixture_has_table(file_name)
}
pub(crate) fn stage_fixture(case: &PhysicalCase, dir: &Path) -> Result<PathBuf> {
let fixture = fixture_path(case.logical.fixture);
let file_path = dir.join(STAGED_FILE);
fs::copy(&fixture, &file_path).with_context(|| {
format!(
"copy fixture '{}' to '{}'",
case.logical.fixture,
file_path.display(),
)
})?;
Ok(file_path)
}
pub(crate) fn collect_result(
output: Output,
file_path: &Path,
mode: ExecutionMode,
) -> Result<RunResult> {
let file_content = fs::read(file_path)
.with_context(|| format!("read file '{}' after {:?} run", file_path.display(), mode))?;
Ok(RunResult {
output,
file_content,
})
}
pub(crate) fn run_physical_case(case: &PhysicalCase) -> Result<RunResult> {
let dir = tempdir().context("create temporary directory for matrix case")?;
let file_path = stage_fixture(case, dir.path())?;
let mut command = Command::cargo_bin("mdtablefix").context("create mdtablefix test command")?;
command
.current_dir(dir.path())
.args(case.args())
.arg(STAGED_FILE);
let output = command.output().with_context(|| {
format!(
"execute mdtablefix for matrix case '{}'",
case.snapshot_name()
)
})?;
collect_result(output, &file_path, case.mode)
}
pub(crate) fn fixture_path(file_name: &str) -> PathBuf {
Path::new(env!("CARGO_MANIFEST_DIR"))
.join("tests")
.join("data")
.join("cli-matrix")
.join(file_name)
}
pub(crate) fn is_case_id(id: &str) -> bool {
!id.is_empty()
&& id.bytes().all(|byte| {
byte.is_ascii_lowercase() || byte.is_ascii_digit() || byte == b'_' || byte == b'-'
})
}
pub(crate) fn non_wrap_signature(fixture: &str, flags: &[TransformFlag]) -> String {
let args = flags
.iter()
.map(|flag| flag.as_arg())
.collect::<Vec<_>>()
.join(",");
format!("{fixture}:{args}")
}
pub(crate) fn has_flag(case: &BaseCase, flag: TransformFlag) -> bool { case.flags.contains(&flag) }