use assert_cmd::Command;
use camino::{Utf8Path, Utf8PathBuf};
use cap_std::{ambient_authority, fs_utf8::Dir};
use rstest::rstest;
use tempfile::{TempDir, tempdir};
#[macro_use]
#[path = "common/mod.rs"]
mod common;
#[path = "cli/ellipsis.rs"]
mod ellipsis;
#[path = "support/fixtures.rs"]
mod fixtures;
use fixtures::broken_table;
#[test]
fn test_cli_in_place_requires_file() {
Command::cargo_bin("mdtablefix")
.expect("Failed to create cargo command for mdtablefix")
.arg("--in-place")
.assert()
.failure();
}
#[test]
fn test_cli_version_flag() {
Command::cargo_bin("mdtablefix")
.expect("Failed to create cargo command for mdtablefix")
.arg("--version")
.assert()
.success()
.stdout(format!("mdtablefix {}\n", env!("CARGO_PKG_VERSION")));
}
#[rstest]
fn test_cli_process_file(broken_table: Vec<String>) {
let dir = tempdir().expect("failed to create temporary directory");
let (directory, parent_path) = capability_directory(&dir);
let file_name = Utf8Path::new("sample.md");
directory
.write(file_name, format!("{}\n", broken_table.join("\n")))
.expect("failed to write temporary file");
Command::cargo_bin("mdtablefix")
.expect("Failed to create cargo command for mdtablefix")
.arg(parent_path.join(file_name).as_std_path())
.assert()
.success()
.stdout("| A | B |\n| 1 | 2 |\n| 3 | 4 |\n");
}
#[test]
fn cli_output_modes_snapshot_table_prose() {
let input = include_str!("data/cli-output-parity.dat");
let expected = include_str!("data/cli-output-parity.expected.md");
let dir = tempdir().expect("failed to create temporary directory");
let (directory, parent_path) = capability_directory(&dir);
let stdout_path = parent_path.join("stdout.md");
let in_place_path = parent_path.join("in-place.md");
directory
.write("stdout.md", input)
.expect("failed to write stdout fixture");
directory
.write("in-place.md", input)
.expect("failed to write in-place fixture");
let stdout = Command::cargo_bin("mdtablefix")
.expect("failed to create cargo command for mdtablefix")
.arg(stdout_path.as_std_path())
.output()
.expect("failed to run stdout command");
assert!(
stdout.status.success(),
"stdout command failed: {}",
String::from_utf8_lossy(&stdout.stderr)
);
let stdout_text = String::from_utf8_lossy(&stdout.stdout);
assert_eq!(stdout_text, expected, "stdout output must match the oracle");
assert_eq!(
directory
.read_to_string("stdout.md")
.expect("failed to read stdout fixture"),
input,
"stdout formatting must not modify the input file"
);
insta::assert_snapshot!("format_to_string_table_prose", stdout_text);
let in_place = Command::cargo_bin("mdtablefix")
.expect("failed to create cargo command for mdtablefix")
.args(["--in-place", in_place_path.as_str()])
.output()
.expect("failed to run in-place command");
assert!(
in_place.status.success(),
"in-place command failed: {}",
String::from_utf8_lossy(&in_place.stderr)
);
assert!(
in_place.stdout.is_empty(),
"in-place command wrote unexpected stdout: {}",
String::from_utf8_lossy(&in_place.stdout)
);
let rewritten = directory
.read_to_string("in-place.md")
.expect("failed to read rewritten fixture");
assert_eq!(rewritten, expected, "in-place output must match the oracle");
insta::assert_snapshot!("rewrite_in_place_table_prose", rewritten);
}
#[test]
fn test_cli_fences_option() {
Command::cargo_bin("mdtablefix")
.expect("Failed to create cargo command for mdtablefix")
.arg("--fences")
.write_stdin("````rust\nfn main() {}\n````\n")
.assert()
.success()
.stdout("```rust\nfn main() {}\n```\n");
}
#[test]
fn test_cli_headings_option() {
Command::cargo_bin("mdtablefix")
.expect("Failed to create cargo command for mdtablefix")
.arg("--headings")
.write_stdin("Title\n=====\n")
.assert()
.success()
.stdout("# Title\n");
}
#[test]
fn test_cli_headings_disabled_by_default() {
Command::cargo_bin("mdtablefix")
.expect("Failed to create cargo command for mdtablefix")
.write_stdin("Heading\n-----\n")
.assert()
.success()
.stdout("Heading\n-----\n");
}
#[test]
fn test_cli_headings_requires_long_marker() {
Command::cargo_bin("mdtablefix")
.expect("Failed to create cargo command for mdtablefix")
.arg("--headings")
.write_stdin("Maybe not\n==\n")
.assert()
.success()
.stdout("Maybe not\n==\n");
}
#[test]
fn test_cli_headings_preserves_blockquote_paragraphs() {
Command::cargo_bin("mdtablefix")
.expect("Failed to create cargo command for mdtablefix")
.arg("--headings")
.write_stdin("> Quote\n-----\n")
.assert()
.success()
.stdout("> Quote\n-----\n");
}
#[test]
fn test_cli_headings_blockquote_conversion() {
Command::cargo_bin("mdtablefix")
.expect("Failed to create cargo command for mdtablefix")
.arg("--headings")
.write_stdin("> Quote\n> ----\n")
.assert()
.success()
.stdout("> ## Quote\n");
}
#[rstest]
#[case("## aa\n---\n")]
#[case("# aa\n===\n")]
#[case("###### aa ######\n---\n")]
#[case("> ## aa\n> ---\n")]
#[case("---\n---\n")]
#[case("***\n---\n")]
#[case("- item\n---\n")]
#[case("1. item\n---\n")]
#[case(" - item\n ---\n")]
#[case("[^1]: note\n---\n")]
#[case("[label]: https://example.com\n---\n")]
#[case("<!-- markdownlint-disable MD013 -->\n---\n")]
fn test_cli_headings_preserves_block_starts(#[case] input: &'static str) {
Command::cargo_bin("mdtablefix")
.expect("Failed to create cargo command for mdtablefix")
.arg("--headings")
.write_stdin(input)
.assert()
.success()
.stdout(input);
}
#[test]
fn test_cli_fences_option_tilde() {
Command::cargo_bin("mdtablefix")
.expect("Failed to create cargo command for mdtablefix")
.arg("--fences")
.write_stdin("~~~~rust\nfn main() {}\n~~~~\n")
.assert()
.success()
.stdout("```rust\nfn main() {}\n```\n");
}
#[test]
fn test_cli_fences_before_ellipsis() {
Command::cargo_bin("mdtablefix")
.expect("Failed to create cargo command for mdtablefix")
.args(["--fences", "--ellipsis"])
.write_stdin("````\nlet x = ...;\n````\n")
.assert()
.success()
.stdout("```\nlet x = ...;\n```\n");
}
#[test]
fn test_cli_fences_orphan_specifier() {
Command::cargo_bin("mdtablefix")
.expect("Failed to create cargo command for mdtablefix")
.arg("--fences")
.write_stdin("Rust\n```\nfn main() {}\n```\n")
.assert()
.success()
.stdout("```rust\nfn main() {}\n```\n");
}
#[test]
fn test_cli_fences_with_renumber() {
let input = concat!(
"Rust\n",
"\n",
"~~~~~~\n",
"fn main() {}\n",
"~~~~~~\n",
"\n",
"1. first\n",
"3. second\n",
);
Command::cargo_bin("mdtablefix")
.expect("Failed to create cargo command for mdtablefix")
.args(["--fences", "--renumber"])
.write_stdin(input)
.assert()
.success()
.stdout("```rust\nfn main() {}\n```\n\n1. first\n2. second\n");
}
#[test]
fn test_cli_fences_preserve_existing_language() {
Command::cargo_bin("mdtablefix")
.expect("Failed to create cargo command for mdtablefix")
.arg("--fences")
.write_stdin("ruby\n```rust\nfn main() {}\n```\n")
.assert()
.success()
.stdout("ruby\n```rust\nfn main() {}\n```\n");
}
#[test]
fn test_cli_fences_orphan_specifier_symbols() {
Command::cargo_bin("mdtablefix")
.expect("Failed to create cargo command for mdtablefix")
.arg("--fences")
.write_stdin("C++\n```\nfn main() {}\n```\n")
.assert()
.success()
.stdout("```c++\nfn main() {}\n```\n");
}
#[test]
fn test_cli_no_attach_without_preceding_blank_line() {
let input = concat!("text\n", "Rust\n", "```\n", "fn main() {}\n", "```\n");
Command::cargo_bin("mdtablefix")
.expect("Failed to create cargo command for mdtablefix")
.arg("--fences")
.write_stdin(input)
.assert()
.success()
.stdout("text\nRust\n```\nfn main() {}\n```\n");
}
#[test]
fn test_cli_footnotes_option() {
let input = include_str!("data/footnotes_input.txt");
let expected = include_str!("data/footnotes_expected.txt");
Command::cargo_bin("mdtablefix")
.expect("Failed to create cargo command for mdtablefix")
.arg("--footnotes")
.write_stdin(input)
.assert()
.success()
.stdout(format!("{}\n", expected.trim_end()));
}
fn run_in_place(flags: &[&str], input: &str, expected: &str) {
let dir = tempdir().expect("failed to create temporary directory");
let (directory, parent_path) = capability_directory(&dir);
let file_name = Utf8Path::new("sample.md");
let file_path = parent_path.join(file_name);
directory
.write(file_name, input)
.expect("failed to write test file");
Command::cargo_bin("mdtablefix")
.expect("Failed to create cargo command for mdtablefix")
.args(["--in-place"])
.args(flags)
.arg(file_path.as_std_path())
.assert()
.success()
.stdout("")
.stderr("");
let out = directory
.read_to_string(file_name)
.expect("failed to read output file");
assert_eq!(out.trim_end(), expected.trim_end());
assert!(
out.ends_with('\n'),
"output file must end with a trailing newline"
);
Command::cargo_bin("mdtablefix")
.expect("Failed to create cargo command for mdtablefix")
.args(["--in-place"])
.args(flags)
.arg(file_path.as_std_path())
.assert()
.success()
.stdout("")
.stderr("");
let out2 = directory
.read_to_string(file_name)
.expect("failed to read output file");
assert!(
out2.ends_with('\n'),
"output file must end with a trailing newline"
);
assert_eq!(out2, out);
}
fn capability_directory(tempdir: &TempDir) -> (Dir, Utf8PathBuf) {
let path = Utf8PathBuf::from_path_buf(tempdir.path().to_path_buf())
.expect("temporary directory path is UTF-8");
let directory = Dir::open_ambient_dir(&path, ambient_authority())
.expect("failed to open temporary directory");
(directory, path)
}
#[rstest]
#[case(&["--fences"], "Rust\n```\nfn main() {}\n```\n", "```rust\nfn main() {}\n```\n")]
#[case(&["--footnotes"], include_str!("data/footnotes_input.txt"), include_str!("data/footnotes_expected.txt"))]
#[case(&["--fences", "--footnotes"], include_str!("data/fences_footnotes_input.txt"), include_str!("data/fences_footnotes_expected.txt"))]
#[case(
// The four-backtick opener makes the three-backtick line below it literal
// content, so the payload stays inside the unclosed fence and the ellipsis
// pass must not reach it.
&["--fences", "--ellipsis"],
"````\n```\nliteral...\n",
"````\n```\nliteral...\n"
)]
#[case(&["--wrap", "--footnotes"], include_str!("data/footnotes_input.txt"), include_str!("data/footnotes_wrap_expected.txt"))]
#[case(&["--wrap", "--ellipsis"], include_str!("data/ellipsis_wrap_input.txt"), include_str!("data/ellipsis_wrap_expected.txt"))]
#[case(&["--headings"], "Title\n=====\n", "# Title\n")]
#[case(
&["--footnotes", "--code-emphasis", "--headings"],
"aa\n-----\n---\n",
"## aa\n---\n"
)]
fn test_cli_in_place_variants(#[case] flags: &[&str], #[case] input: &str, #[case] expected: &str) {
run_in_place(flags, input, expected);
}
#[rstest]
#[case("```null\nfn main() {}\n```\n", "```\nfn main() {}\n```\n")]
#[case("```NULL\nfn main() {}\n```\n", "```\nfn main() {}\n```\n")]
#[case("```Null\nfn main() {}\n```\n", "```\nfn main() {}\n```\n")]
#[case("```null \nfn main() {}\n```\n", "```\nfn main() {}\n```\n")]
#[case("```NULL \nfn main() {}\n```\n", "```\nfn main() {}\n```\n")]
#[case("```Null \nfn main() {}\n```\n", "```\nfn main() {}\n```\n")]
#[case("~~~~null\nfn main() {}\n~~~~\n", "```\nfn main() {}\n```\n")]
#[case("~~~~NULL\nfn main() {}\n~~~~\n", "```\nfn main() {}\n```\n")]
#[case("~~~~Null\nfn main() {}\n~~~~\n", "```\nfn main() {}\n```\n")]
#[case("~~~~null \nfn main() {}\n~~~~\n", "```\nfn main() {}\n```\n")]
#[case("~~~~NULL \nfn main() {}\n~~~~\n", "```\nfn main() {}\n```\n")]
#[case("~~~~Null \nfn main() {}\n~~~~\n", "```\nfn main() {}\n```\n")]
#[case(" ```null\nfn main() {}\n```\n", " ```\nfn main() {}\n```\n")]
fn test_cli_fences_null_language(#[case] input: &str, #[case] expected: &'static str) {
Command::cargo_bin("mdtablefix")
.expect("Failed to create cargo command for mdtablefix")
.arg("--fences")
.write_stdin(input)
.assert()
.success()
.stdout(expected);
}