use std::fs;
use std::process::Command;
use std::sync::atomic::{AtomicU64, Ordering};
static TEST_COUNTER: AtomicU64 = AtomicU64::new(0);
#[test]
fn fix_doc_comment_dry_run_matches_after() {
let before = fixture_dir().join("table_doc_comment_before.rs");
let expected = fs::read_to_string(fixture_dir().join("table_doc_comment_after.rs")).unwrap();
let output = run_command(&["--include", "tables", "--dry-run"], &before);
assert!(
output.status.success(),
"fix --dry-run should succeed: {}",
String::from_utf8_lossy(&output.stderr)
);
let stdout = String::from_utf8_lossy(&output.stdout);
assert_eq!(
stdout, expected,
"dry-run stdout must match table_doc_comment_after.rs"
);
}
#[test]
fn fix_fence_md_dry_run_matches_after() {
let before = fixture_dir().join("fence_md_before.md");
let expected = fs::read_to_string(fixture_dir().join("fence_md_after.md")).unwrap();
let output = run_command(&["--include", "fences", "--dry-run"], &before);
assert!(
output.status.success(),
"fix --dry-run should succeed: {}",
String::from_utf8_lossy(&output.stderr)
);
let stdout = String::from_utf8_lossy(&output.stdout);
assert_eq!(
stdout, expected,
"dry-run stdout must match fence_md_after.md"
);
}
#[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 expected = fs::read_to_string(&path).unwrap();
let output = run_command(&["--include", "tables", "--dry-run"], &path);
assert!(
output.status.success(),
"fix --dry-run on {name} should succeed"
);
let stdout = String::from_utf8_lossy(&output.stdout);
assert_eq!(
&*stdout, &*expected,
"{name} must be idempotent (output unchanged)"
);
}
{
let path = fixture_dir().join("fence_md_after.md");
let expected = fs::read_to_string(&path).unwrap();
let output = run_command(&["--include", "fences", "--dry-run"], &path);
assert!(
output.status.success(),
"fix --dry-run on fence_md_after.md should succeed"
);
let stdout = String::from_utf8_lossy(&output.stdout);
assert_eq!(
&*stdout, &*expected,
"fence_md_after.md must be idempotent (output unchanged)"
);
}
}
#[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_md_dry_run_matches_after() {
let before = fixture_dir().join("table_md_before.md");
let expected = fs::read_to_string(fixture_dir().join("table_md_after.md")).unwrap();
let output = run_command(&["--include", "tables", "--dry-run"], &before);
assert!(
output.status.success(),
"fix --dry-run should succeed: {}",
String::from_utf8_lossy(&output.stderr)
);
let stdout = String::from_utf8_lossy(&output.stdout);
assert_eq!(
stdout, expected,
"dry-run stdout must match table_md_after.md"
);
}
#[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);
}
#[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:?}"
);
}
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 binary() -> std::path::PathBuf {
if let Some(path) = std::env::var_os("CARGO_BIN_EXE_rust_llm_tidy") {
return std::path::PathBuf::from(path);
}
let mut path = std::env::current_exe().expect("current_exe must resolve");
path.pop();
path.pop();
path.join("rust-llm-tidy")
}
fn manifest_dir() -> std::path::PathBuf {
std::path::PathBuf::from(env!("CARGO_MANIFEST_DIR"))
}