use std::fs;
use std::io::Write;
use std::path::Path;
use std::process::{Command, Output, Stdio};
const SWIFT: &str = "#if DEBUG\nlet x = 1\n#endif\n";
const KOTLIN: &str = "/**\n * Adds two numbers.\n * @param a first\n */\nfun add(a: Int, b: Int) = a + b\n";
const MARKDOWN: &str = "# Title\n\na\n\n\n\nb\n";
const REFUSED: &str = "is source code, not Markdown";
fn run(cwd: &Path, args: &[&str], input: &[u8]) -> Output {
let mut child = Command::new(env!("CARGO_BIN_EXE_rumdl"))
.current_dir(cwd)
.args(args)
.stdin(Stdio::piped())
.stdout(Stdio::piped())
.stderr(Stdio::piped())
.spawn()
.expect("failed to execute rumdl");
child
.stdin
.as_mut()
.unwrap()
.write_all(input)
.expect("failed to write stdin");
child.wait_with_output().expect("failed to collect rumdl output")
}
fn text(bytes: &[u8]) -> String {
String::from_utf8_lossy(bytes).replace("\r\n", "\n")
}
fn describe(output: &Output) -> String {
format!(
"exit: {:?}\nstdout:\n{}\nstderr:\n{}",
output.status.code(),
text(&output.stdout),
text(&output.stderr)
)
}
fn project(files: &[(&str, &str)]) -> tempfile::TempDir {
let temp = tempfile::tempdir().unwrap();
fs::create_dir_all(temp.path().join(".git")).unwrap();
for (name, content) in files {
let path = temp.path().join(name);
fs::create_dir_all(path.parent().unwrap()).unwrap();
fs::write(path, content).unwrap();
}
temp
}
fn read(dir: &Path, name: &str) -> String {
fs::read_to_string(dir.join(name)).unwrap()
}
fn assert_refused(output: &Output, name: &str) {
assert_eq!(output.status.code(), Some(2), "{}", describe(output));
let stderr = text(&output.stderr);
assert!(stderr.contains(&format!("{name} {REFUSED}")), "{}", describe(output));
}
#[test]
fn fmt_leaves_a_named_swift_file_unchanged() {
let temp = project(&[("a.swift", SWIFT)]);
let output = run(temp.path(), &["fmt", "--no-config", "--no-cache", "a.swift"], b"");
assert_refused(&output, "a.swift");
assert_eq!(read(temp.path(), "a.swift"), SWIFT);
}
#[test]
fn check_fix_leaves_a_named_kotlin_file_unchanged() {
let temp = project(&[("b.kt", KOTLIN)]);
let output = run(
temp.path(),
&["check", "--fix", "--no-config", "--no-cache", "b.kt"],
b"",
);
assert_refused(&output, "b.kt");
assert_eq!(read(temp.path(), "b.kt"), KOTLIN);
}
#[test]
fn check_reports_nothing_about_the_code_of_a_named_source_file() {
let temp = project(&[("a.swift", SWIFT)]);
let output = run(temp.path(), &["check", "--no-config", "--no-cache", "a.swift"], b"");
assert_refused(&output, "a.swift");
assert!(!text(&output.stdout).contains("MD018"), "{}", describe(&output));
}
#[test]
fn an_uppercase_rs_extension_is_refused_not_read_for_doc_comments() {
let source = "#[derive(Debug)]\nstruct S;\n";
let temp = project(&[("LIB.RS", source)]);
let output = run(temp.path(), &["fmt", "--no-config", "--no-cache", "LIB.RS"], b"");
assert_refused(&output, "LIB.RS");
assert_eq!(read(temp.path(), "LIB.RS"), source);
}
#[test]
fn a_run_naming_markdown_and_source_files_changes_neither() {
let temp = project(&[("a.md", MARKDOWN), ("a.swift", SWIFT)]);
let output = run(
temp.path(),
&["fmt", "--no-config", "--no-cache", "a.md", "a.swift"],
b"",
);
assert_refused(&output, "a.swift");
assert_eq!(read(temp.path(), "a.swift"), SWIFT);
assert_eq!(read(temp.path(), "a.md"), MARKDOWN);
}
#[test]
fn a_cli_include_matching_source_files_is_refused() {
let temp = project(&[("README.md", MARKDOWN), ("src/a.swift", SWIFT)]);
let output = run(
temp.path(),
&["fmt", "--no-config", "--no-cache", "--include", "**/*.swift", "."],
b"",
);
assert_refused(&output, "a.swift");
assert_eq!(read(temp.path(), "src/a.swift"), SWIFT);
}
#[test]
fn a_config_include_matching_source_files_is_refused() {
let temp = project(&[
(".rumdl.toml", "[global]\ninclude = [\"**/*.swift\"]\n"),
("src/a.swift", SWIFT),
]);
let output = run(temp.path(), &["fmt", "--no-cache"], b"");
assert_refused(&output, "a.swift");
assert_eq!(read(temp.path(), "src/a.swift"), SWIFT);
}
#[test]
fn a_directory_scan_passes_over_source_files() {
let temp = project(&[("README.md", MARKDOWN), ("src/a.swift", SWIFT)]);
let output = run(temp.path(), &["fmt", "--no-config", "--no-cache", "."], b"");
assert_eq!(output.status.code(), Some(0), "{}", describe(&output));
assert!(!text(&output.stderr).contains(REFUSED), "{}", describe(&output));
assert_eq!(read(temp.path(), "src/a.swift"), SWIFT);
assert_ne!(read(temp.path(), "README.md"), MARKDOWN);
}
#[test]
fn an_excluded_source_file_is_excluded_rather_than_refused() {
let temp = project(&[
(".rumdl.toml", "[global]\nexclude = [\"*.swift\"]\n"),
("a.swift", SWIFT),
]);
let output = run(temp.path(), &["check", "--no-cache", "a.swift"], b"");
assert_eq!(output.status.code(), Some(0), "{}", describe(&output));
assert!(!text(&output.stderr).contains(REFUSED), "{}", describe(&output));
}
#[test]
fn files_rumdl_reads_as_markdown_are_still_fixed() {
for name in [
"notes.txt",
"NOTES",
"template.md.jinja",
"rules.mdc",
"intro.litcoffee",
] {
let temp = project(&[(name, MARKDOWN)]);
let output = run(temp.path(), &["fmt", "--no-config", "--no-cache", name], b"");
assert_eq!(output.status.code(), Some(0), "{name}\n{}", describe(&output));
assert_eq!(read(temp.path(), name), "# Title\n\na\n\nb\n", "{name}");
}
}
#[test]
fn rust_files_are_still_read_through_their_doc_comments() {
let source = "/// #Heading\n#[derive(Debug)]\nstruct S;\n";
let temp = project(&[("lib.rs", source)]);
let output = run(temp.path(), &["fmt", "--no-config", "--no-cache", "lib.rs"], b"");
assert_eq!(output.status.code(), Some(0), "{}", describe(&output));
assert_eq!(
read(temp.path(), "lib.rs"),
"/// # Heading\n#[derive(Debug)]\nstruct S;\n"
);
}
#[test]
fn fmt_over_stdin_named_as_source_hands_the_bytes_back() {
let temp = project(&[]);
let output = run(
temp.path(),
&["fmt", "--no-config", "--no-cache", "--stdin-filename", "a.swift", "-"],
SWIFT.as_bytes(),
);
assert_refused(&output, "a.swift");
assert_eq!(output.stdout, SWIFT.as_bytes());
}
#[test]
fn check_fix_over_stdin_named_as_source_hands_the_bytes_back() {
let temp = project(&[]);
let output = run(
temp.path(),
&[
"check",
"--fix",
"--no-config",
"--no-cache",
"--stdin-filename",
"b.kt",
"-",
],
KOTLIN.as_bytes(),
);
assert_refused(&output, "b.kt");
assert_eq!(output.stdout, KOTLIN.as_bytes());
}
#[test]
fn a_diff_over_stdin_named_as_source_prints_nothing() {
let temp = project(&[]);
for args in [
&[
"fmt",
"--check",
"--no-config",
"--no-cache",
"--stdin-filename",
"a.swift",
"-",
][..],
&["check", "--no-config", "--no-cache", "--stdin-filename", "a.swift", "-"][..],
] {
let output = run(temp.path(), args, SWIFT.as_bytes());
assert_refused(&output, "a.swift");
assert!(output.stdout.is_empty(), "{args:?}\n{}", describe(&output));
}
}
#[test]
fn stdin_without_a_name_is_still_markdown() {
let temp = project(&[]);
let output = run(
temp.path(),
&["check", "--no-config", "--no-cache", "--enable", "MD018", "-"],
SWIFT.as_bytes(),
);
assert_eq!(output.status.code(), Some(1), "{}", describe(&output));
assert!(text(&output.stdout).contains("MD018"), "{}", describe(&output));
}
#[test]
fn a_stdin_batch_supplying_a_source_file_is_refused() {
let temp = project(&[]);
let mut input = Vec::new();
for (path, content) in [("a.md", MARKDOWN), ("a.swift", SWIFT)] {
input.extend_from_slice(path.as_bytes());
input.push(0);
input.extend_from_slice(content.as_bytes());
input.push(0);
}
let output = run(
temp.path(),
&["check", "--stdin-batch", "--no-config", "--no-cache"],
&input,
);
assert_refused(&output, "a.swift");
assert!(!text(&output.stdout).contains("MD018"), "{}", describe(&output));
}