use std::fs;
use rstest::rstest;
use tempfile::tempdir;
#[path = "support/cli_args.rs"]
mod cli_args;
#[path = "support/cli_stdin.rs"]
mod cli_stdin;
use cli_args::run_cli_with_args;
use cli_stdin::run_cli_with_stdin;
struct LineEndingCase {
name: &'static str,
input: &'static str,
expected: &'static str,
}
const CASES: &[LineEndingCase] = &[
LineEndingCase {
name: "line_feeds_only",
input: "| A | B |\n| --- | --- |\n| 1 | 2 |\n",
expected: "| A | B |\n| --- | --- |\n| 1 | 2 |\n",
},
LineEndingCase {
name: "carriage_returns_only",
input: "| A | B |\r\n| --- | --- |\r\n| 1 | 2 |\r\n",
expected: "| A | B |\r\n| --- | --- |\r\n| 1 | 2 |\r\n",
},
LineEndingCase {
name: "carriage_return_majority",
input: "| A | B |\r\n| --- | --- |\r\n| 1 | 2 |\n",
expected: "| A | B |\r\n| --- | --- |\r\n| 1 | 2 |\r\n",
},
LineEndingCase {
name: "line_feed_majority",
input: "| A | B |\n| --- | --- |\n| 1 | 2 |\r\n",
expected: "| A | B |\n| --- | --- |\n| 1 | 2 |\n",
},
LineEndingCase {
name: "no_majority_defaults_to_line_feeds",
input: "| A | B |\n| --- | --- |\r\n",
expected: "| A | B |\n| --- | --- |\n",
},
LineEndingCase {
name: "no_line_endings",
input: "Only prose",
expected: "Only prose\n",
},
];
fn assert_case_output(case: &LineEndingCase, actual: &[u8]) {
let actual = String::from_utf8(actual.to_vec()).expect("formatter output is not UTF-8");
assert_eq!(
actual, case.expected,
"case `{}` produced the wrong bytes",
case.name
);
}
#[rstest]
fn stdout_preserves_the_majority_line_ending() -> Result<(), Box<dyn std::error::Error>> {
let dir = tempdir().expect("failed to create temporary directory");
for case in CASES {
let path = dir.path().join(format!("{}.md", case.name));
fs::write(&path, case.input).expect("failed to write fixture");
let path = path.to_str().expect("fixture path is not valid UTF-8");
let output = run_cli_with_args(&[path])?
.success()
.get_output()
.stdout
.clone();
assert_case_output(case, &output);
}
Ok(())
}
#[rstest]
fn in_place_preserves_the_majority_line_ending() -> Result<(), Box<dyn std::error::Error>> {
let dir = tempdir().expect("failed to create temporary directory");
for case in CASES {
let path = dir.path().join(format!("in-place-{}.md", case.name));
fs::write(&path, case.input).expect("failed to write fixture");
let path = path.to_str().expect("fixture path is not valid UTF-8");
run_cli_with_args(&["--in-place", path])?.success();
let actual = fs::read(path).expect("failed to read rewritten fixture");
assert_case_output(case, &actual);
}
Ok(())
}
#[rstest]
fn stdin_preserves_the_majority_line_ending() -> Result<(), Box<dyn std::error::Error>> {
for case in CASES {
let output = run_cli_with_stdin(&[], case.input)?
.success()
.get_output()
.stdout
.clone();
assert_case_output(case, &output);
}
Ok(())
}
#[rstest]
fn wrapped_prose_keeps_its_carriage_return_endings() -> Result<(), Box<dyn std::error::Error>> {
const INPUT: &str = concat!(
"The formatter must preserve the majority line-ending style of the input ",
"document so that a file authored on Windows is not rewritten with Unix ",
"line feeds.\r\n",
);
const EXPECTED: &str = concat!(
"The formatter must preserve the majority line-ending style of the input\r\n",
"document so that a file authored on Windows is not rewritten with Unix line\r\n",
"feeds.\r\n",
);
let dir = tempdir().expect("failed to create temporary directory");
let path = dir.path().join("wrapped.md");
fs::write(&path, INPUT).expect("failed to write fixture");
let path = path.to_str().expect("fixture path is not valid UTF-8");
let output = run_cli_with_args(&["--wrap", path])?
.success()
.get_output()
.stdout
.clone();
assert_case_output(
&LineEndingCase {
name: "wrapped_prose_stdout",
input: INPUT,
expected: EXPECTED,
},
&output,
);
run_cli_with_args(&["--wrap", "--in-place", path])?.success();
let actual = fs::read(path).expect("failed to read rewritten fixture");
assert_case_output(
&LineEndingCase {
name: "wrapped_prose_in_place",
input: INPUT,
expected: EXPECTED,
},
&actual,
);
Ok(())
}
#[rstest]
fn fenced_code_block_endings_decide_the_majority() -> Result<(), Box<dyn std::error::Error>> {
const INPUT: &str = "Before.\n\n```\na\r\nb\r\nc\r\nd\r\n```\r\nAfter.\n";
const EXPECTED: &str = "Before.\r\n\r\n```\r\na\r\nb\r\nc\r\nd\r\n```\r\nAfter.\r\n";
let dir = tempdir().expect("failed to create temporary directory");
let path = dir.path().join("fenced.md");
fs::write(&path, INPUT).expect("failed to write fixture");
let path = path.to_str().expect("fixture path is not valid UTF-8");
let output = run_cli_with_args(&[path])?
.success()
.get_output()
.stdout
.clone();
assert_case_output(
&LineEndingCase {
name: "fenced_code_block_stdout",
input: INPUT,
expected: EXPECTED,
},
&output,
);
run_cli_with_args(&["--in-place", path])?.success();
let actual = fs::read(path).expect("failed to read rewritten fixture");
assert_case_output(
&LineEndingCase {
name: "fenced_code_block_in_place",
input: INPUT,
expected: EXPECTED,
},
&actual,
);
Ok(())
}
#[rstest]
fn multiple_files_each_keep_their_own_line_endings() -> Result<(), Box<dyn std::error::Error>> {
const LF_INPUT: &str = "|A|B|\n|---|---|\n|1|2|\n";
const LF_EXPECTED: &str = "| A | B |\n| --- | --- |\n| 1 | 2 |\n";
const CRLF_INPUT: &str = "|A|B|\r\n|---|---|\r\n|1|2|\r\n";
const CRLF_EXPECTED: &str = "| A | B |\r\n| --- | --- |\r\n| 1 | 2 |\r\n";
let dir = tempdir().expect("failed to create temporary directory");
let lf_path = dir.path().join("lf.md");
let crlf_path = dir.path().join("crlf.md");
fs::write(&lf_path, LF_INPUT).expect("failed to write fixture");
fs::write(&crlf_path, CRLF_INPUT).expect("failed to write fixture");
let lf = lf_path.to_str().expect("fixture path is not valid UTF-8");
let crlf = crlf_path.to_str().expect("fixture path is not valid UTF-8");
let output = run_cli_with_args(&[crlf, lf])?
.success()
.get_output()
.stdout
.clone();
assert_eq!(
String::from_utf8(output)?,
format!("{CRLF_EXPECTED}{LF_EXPECTED}"),
"multi-file standard output mixed the detected styles"
);
run_cli_with_args(&["--in-place", lf, crlf])?.success();
assert_eq!(
fs::read(&lf_path).expect("failed to read rewritten fixture"),
LF_EXPECTED.as_bytes(),
"in-place rewriting converted the LF file"
);
assert_eq!(
fs::read(&crlf_path).expect("failed to read rewritten fixture"),
CRLF_EXPECTED.as_bytes(),
"in-place rewriting converted the CRLF file"
);
Ok(())
}
#[rstest]
fn empty_file_produces_empty_output() -> Result<(), Box<dyn std::error::Error>> {
let dir = tempdir().expect("failed to create temporary directory");
let path = dir.path().join("empty.md");
fs::write(&path, "").expect("failed to write fixture");
let path = path.to_str().expect("fixture path is not valid UTF-8");
let output = run_cli_with_args(&[path])?
.success()
.get_output()
.stdout
.clone();
assert!(output.is_empty(), "empty file printed {output:?}");
run_cli_with_args(&["--in-place", path])?.success();
assert!(
fs::read(path)
.expect("failed to read rewritten fixture")
.is_empty(),
"in-place rewriting an empty file wrote bytes"
);
Ok(())
}
#[rstest]
fn empty_stdin_prints_one_line_feed() -> Result<(), Box<dyn std::error::Error>> {
let output = run_cli_with_stdin(&[], "")?
.success()
.get_output()
.stdout
.clone();
assert_eq!(String::from_utf8(output)?, "\n");
Ok(())
}