use std::fs;
use std::process::Command;
use std::sync::atomic::{AtomicU64, Ordering};
mod common;
use common::binary;
static TEST_COUNTER: AtomicU64 = AtomicU64::new(0);
#[test]
fn fix_default_passes_borrowed_restore_preserves_earlier_change() {
let before = fixture_dir().join("table_md_before.md");
let expected = fs::read_to_string(fixture_dir().join("table_md_after.md")).unwrap();
let tmp = temp_file("md");
fs::write(&tmp, fs::read_to_string(&before).unwrap()).unwrap();
let output = run_command(&[], &tmp); assert!(
output.status.success(),
"default fix should succeed: {}",
String::from_utf8_lossy(&output.stderr)
);
let actual = fs::read_to_string(&tmp).unwrap();
let _ = fs::remove_file(&tmp);
assert_eq!(
actual, expected,
"fences/links no-op restore must keep the table fix"
);
let stderr = String::from_utf8_lossy(&output.stderr);
assert_eq!(
stderr.matches("success[FIX]").count(),
1,
"only the realigned table reports a record: {stderr}"
);
}
#[test]
fn fix_doc_comment_dry_run_reports_change() {
let before = fixture_dir().join("table_doc_comment_before.rs");
let output = run_command(&["--include", "tables", "--dry-run"], &before);
assert!(
output.status.success(),
"fix --dry-run should succeed: {}",
String::from_utf8_lossy(&output.stderr)
);
assert!(
output.stdout.is_empty(),
"dry-run must not print reconstructed source to stdout"
);
let stderr = String::from_utf8_lossy(&output.stderr);
assert!(
stderr.contains("success[FIX]"),
"dry-run must report a fix change line on stderr: {stderr}"
);
}
#[test]
fn fix_fence_in_place_matches_after() {
let expected = fs::read_to_string(fixture_dir().join("fence_md_after.md")).unwrap();
let tmp = temp_file("md");
fs::write(
&tmp,
fs::read_to_string(fixture_dir().join("fence_md_before.md")).unwrap(),
)
.unwrap();
let output = run_command(&["--include", "fences"], &tmp);
assert!(
output.status.success(),
"fence fix in-place should succeed: {}",
String::from_utf8_lossy(&output.stderr)
);
let actual = fs::read_to_string(&tmp).unwrap();
let _ = fs::remove_file(&tmp);
assert_eq!(actual, expected, "fence fix must match fence_md_after.md");
}
#[test]
fn fix_fence_md_dry_run_reports_change() {
let before = fixture_dir().join("fence_md_before.md");
let output = run_command(&["--include", "fences", "--dry-run"], &before);
assert!(
output.status.success(),
"fix --dry-run should succeed: {}",
String::from_utf8_lossy(&output.stderr)
);
assert!(
output.stdout.is_empty(),
"dry-run must not print reconstructed source to stdout"
);
let stderr = String::from_utf8_lossy(&output.stderr);
assert!(
stderr.contains("success[FIX]"),
"dry-run must report a fix change line on stderr: {stderr}"
);
}
#[test]
fn fix_idempotent_on_after_fixtures() {
for name in ["table_md_after.md", "table_doc_comment_after.rs"] {
let path = fixture_dir().join(name);
let output = run_command(&["--include", "tables", "--dry-run"], &path);
assert!(
output.status.success(),
"fix --dry-run on {name} should succeed"
);
assert!(
output.stdout.is_empty(),
"{name} dry-run must not print source to stdout"
);
assert!(
output.stderr.is_empty(),
"{name} is already tidy: dry-run must emit zero change records"
);
}
{
let path = fixture_dir().join("fence_md_after.md");
let output = run_command(&["--include", "fences", "--dry-run"], &path);
assert!(
output.status.success(),
"fix --dry-run on fence_md_after.md should succeed"
);
assert!(
output.stdout.is_empty(),
"fence_md_after.md dry-run must not print source to stdout"
);
assert!(
output.stderr.is_empty(),
"fence_md_after.md is already tidy: dry-run must emit zero change records"
);
}
}
#[test]
fn fix_in_place_reports_same_records_and_writes() {
let before = fixture_dir().join("multi_md_before.md");
let dry_run = run_command(&["--include", "tables", "--dry-run"], &before);
let dry_stderr = String::from_utf8_lossy(&dry_run.stderr);
let tmp = temp_file("md");
fs::write(&tmp, fs::read_to_string(&before).unwrap()).unwrap();
let output = run_command(&["--include", "tables"], &tmp);
assert!(
output.status.success(),
"fix in-place should succeed: {}",
String::from_utf8_lossy(&output.stderr)
);
let actual = fs::read_to_string(&tmp).unwrap();
let _ = fs::remove_file(&tmp);
let stderr = String::from_utf8_lossy(&output.stderr);
assert_eq!(
stderr.matches("success[FIX]").count(),
1,
"in-place run reports the same record: {stderr}"
);
assert!(
stderr.contains("tables were aligned"),
"in-place change line matches dry-run: {stderr}"
);
assert_eq!(
stderr.matches("success[FIX]").count(),
dry_stderr.matches("success[FIX]").count(),
"in-place reports the same change lines as its dry-run twin"
);
assert_ne!(
actual,
fs::read_to_string(&before).unwrap(),
"in-place run must write the fixed file"
);
}
#[test]
fn fix_in_place_write() {
let expected = fs::read_to_string(fixture_dir().join("table_md_after.md")).unwrap();
let tmp = temp_file("md");
fs::write(
&tmp,
fs::read_to_string(fixture_dir().join("table_md_before.md")).unwrap(),
)
.unwrap();
let output = run_command(&["--include", "tables"], &tmp);
assert!(
output.status.success(),
"fix in-place should succeed: {}",
String::from_utf8_lossy(&output.stderr)
);
let actual = fs::read_to_string(&tmp).unwrap();
let _ = fs::remove_file(&tmp);
assert_eq!(actual, expected, "in-place file must match _after fixture");
}
#[test]
fn fix_links_in_place_preserves_crlf() {
let tmp = temp_file("md");
let input = "see [A](http://x) and [A](http://x)\r\n";
fs::write(&tmp, input).unwrap();
let output = run_command(&["--include", "links"], &tmp);
assert!(
output.status.success(),
"fix in-place should succeed: {}",
String::from_utf8_lossy(&output.stderr)
);
let actual = fs::read_to_string(&tmp).unwrap();
let _ = fs::remove_file(&tmp);
assert!(
actual.contains("[A]: http://x"),
"definition hoisted: {actual:?}"
);
assert!(
actual.contains("[A]: http://x\r\n"),
"hoisted definition must end with CRLF: {actual:?}"
);
assert_eq!(
actual.matches('\n').count(),
actual.matches("\r\n").count(),
"every newline must be CRLF after fix: {actual:?}"
);
}
#[test]
fn fix_md_dry_run_reports_change() {
let before = fixture_dir().join("table_md_before.md");
let output = run_command(&["--include", "tables", "--dry-run"], &before);
assert!(
output.status.success(),
"fix --dry-run should succeed: {}",
String::from_utf8_lossy(&output.stderr)
);
assert!(
output.stdout.is_empty(),
"dry-run must not print reconstructed source to stdout"
);
let stderr = String::from_utf8_lossy(&output.stderr);
assert!(
stderr.contains("success[FIX]"),
"dry-run must report a fix change line on stderr: {stderr}"
);
}
#[test]
fn fix_multi_entity_dry_run_reports_one_record_per_file() {
let before = fixture_dir().join("multi_md_before.md");
let output = run_command(&["--include", "tables", "--dry-run"], &before);
assert!(
output.status.success(),
"fix --dry-run should succeed: {}",
String::from_utf8_lossy(&output.stderr)
);
assert!(
output.stdout.is_empty(),
"dry-run must not print reconstructed source to stdout"
);
let stderr = String::from_utf8_lossy(&output.stderr);
assert_eq!(
stderr.matches("success[FIX]").count(),
1,
"one record for the whole file: {stderr}"
);
assert!(
stderr.contains("tables were aligned"),
"record covers both tables with no line: {stderr}"
);
}
#[test]
fn fix_nonexistent_path_fails() {
let nonexistent = std::env::temp_dir().join(format!(
"rust-llm-tidy-fix-missing-{}-{}.md",
std::process::id(),
TEST_COUNTER.fetch_add(1, Ordering::Relaxed)
));
let output = run_command(&["--include", "tables"], &nonexistent);
assert!(
!output.status.success(),
"non-existent path should exit non-zero"
);
}
#[test]
fn fix_recursive_directory_collects_md_and_rs() {
let dir = temp_dir();
let sub = dir.join("sub");
fs::create_dir_all(&sub).unwrap();
fs::write(
dir.join("readme.md"),
fs::read_to_string(fixture_dir().join("table_md_before.md")).unwrap(),
)
.unwrap();
fs::write(
sub.join("code.rs"),
fs::read_to_string(fixture_dir().join("table_doc_comment_before.rs")).unwrap(),
)
.unwrap();
let output = run_command(&["--include", "tables"], &dir);
assert!(
output.status.success(),
"fix directory should succeed: {}",
String::from_utf8_lossy(&output.stderr)
);
let md_expected = fs::read_to_string(fixture_dir().join("table_md_after.md")).unwrap();
let rs_expected = fs::read_to_string(fixture_dir().join("table_doc_comment_after.rs")).unwrap();
assert_eq!(
fs::read_to_string(dir.join("readme.md")).unwrap(),
md_expected,
".md file should be fixed"
);
assert_eq!(
fs::read_to_string(sub.join("code.rs")).unwrap(),
rs_expected,
".rs file should be fixed"
);
let _ = fs::remove_dir_all(&dir);
}
fn fixture_dir() -> std::path::PathBuf {
manifest_dir().join("tests").join("fixtures").join("fix")
}
fn run_command(args: &[&str], path: &std::path::Path) -> std::process::Output {
let mut cmd = Command::new(binary());
cmd.args(["--no-config"]).args(args).arg(path);
cmd.output()
.unwrap_or_else(|e| panic!("failed to spawn rust-llm-tidy on {}: {e}", path.display()))
}
fn temp_dir() -> std::path::PathBuf {
let seq = TEST_COUNTER.fetch_add(1, Ordering::Relaxed);
let pid = std::process::id();
std::env::temp_dir().join(format!("rust-llm-tidy-fix-dir-{}-{}", pid, seq))
}
fn temp_file(ext: &str) -> std::path::PathBuf {
let seq = TEST_COUNTER.fetch_add(1, Ordering::Relaxed);
let pid = std::process::id();
std::env::temp_dir().join(format!("rust-llm-tidy-fix-{}-{}.{}", pid, seq, ext))
}
fn manifest_dir() -> std::path::PathBuf {
std::path::PathBuf::from(env!("CARGO_MANIFEST_DIR"))
}