use std::fs;
use std::process::Command;
use std::sync::atomic::{AtomicU32, Ordering};
macro_rules! run_fixture {
($name:ident) => {{
let fixture_dir = manifest_dir()
.join("tests")
.join("fixtures")
.join("reorder");
let before_path = fixture_dir.join(concat!(stringify!($name), "_before.rs"));
let expected_after =
include_str!(concat!("fixtures/reorder/", stringify!($name), "_after.rs")).to_string();
let (stdout, stderr, exit) = run_dry_run(&before_path);
(stdout, stderr, exit, before_path, expected_after)
}};
}
macro_rules! synthetic_fixture {
($name:ident) => {
#[test]
fn $name() {
let (stdout, stderr, exit, before_path, expected_after) = run_fixture!($name);
assert_eq!(
exit, 0,
concat!(stringify!($name), " dry-run should succeed")
);
assert!(
stdout.is_empty(),
concat!(
stringify!($name),
" dry-run must not print reconstructed source to stdout"
)
);
for line in stderr.lines() {
assert!(
line.contains("success[REORDER]"),
"{} dry-run stderr must only carry change records: {}",
stringify!($name),
line
);
}
assert_eq!(
reorder_in_place(&before_path),
expected_after,
concat!(
stringify!($name),
" fixture: in-place reorder must match _after.rs"
)
);
}
};
}
synthetic_fixture!(phase_extern_crate_stable);
synthetic_fixture!(phase_other_stable);
synthetic_fixture!(phase_use_stable);
synthetic_fixture!(phase_mod_non_test_stable);
synthetic_fixture!(phase_macro_alphabetical);
synthetic_fixture!(phase_macro_dependency);
synthetic_fixture!(phase_macro_invocation_after_def);
synthetic_fixture!(phase_const_static_alphabetical);
synthetic_fixture!(phase_const_static_dependency);
synthetic_fixture!(phase_type_alphabetical);
synthetic_fixture!(phase_type_dependency);
synthetic_fixture!(phase_trait_alphabetical);
synthetic_fixture!(phase_trait_dependency);
synthetic_fixture!(phase_impl_inherent_before_trait);
synthetic_fixture!(phase_impl_after_matching_type);
synthetic_fixture!(phase_impl_orphan_stable);
synthetic_fixture!(fn_visibility_groups);
synthetic_fixture!(fn_main_first);
synthetic_fixture!(fn_callers_before_callees);
synthetic_fixture!(fn_alphabetical_tie_break);
synthetic_fixture!(fn_mutual_recursion_contiguous);
synthetic_fixture!(cfg_test_mod_last_stable);
synthetic_fixture!(preamble_preserved);
synthetic_fixture!(trailer_preserved);
synthetic_fixture!(fn_interstitial_comment_travels_with_next);
synthetic_fixture!(docs_attrs_travel);
synthetic_fixture!(spacing_compact_use_mod_const_static);
synthetic_fixture!(spacing_blank_line_between_phases);
synthetic_fixture!(spacing_blank_line_fn_visibility);
synthetic_fixture!(safety_line_preservation);
static TEST_COUNTER: AtomicU32 = AtomicU32::new(0);
#[test]
fn all_after_fixtures_should_be_idempotent_on_rerun() {
let fixture_dir = manifest_dir()
.join("tests")
.join("fixtures")
.join("reorder");
let mut after_files: Vec<_> = fs::read_dir(&fixture_dir)
.unwrap()
.filter_map(|entry| {
let entry = entry.ok()?;
let path = entry.path();
let name = path.file_name()?.to_str()?;
if name.ends_with("_after.rs") {
Some(path)
} else {
None
}
})
.collect();
after_files.sort();
assert!(!after_files.is_empty(), "no _after.rs fixtures found");
for after_path in &after_files {
let (stdout, stderr, exit) = run_dry_run(after_path);
assert_eq!(exit, 0, "{} dry-run should succeed", after_path.display());
assert!(
stdout.is_empty(),
"{} dry-run must not print source to stdout",
after_path.display()
);
assert!(
stderr.is_empty(),
"{} is already tidy: dry-run must emit zero change records",
after_path.display()
);
}
}
#[test]
fn dry_run_should_not_write_files() {
let source = "fn a() {}\nfn b() { a(); }\n";
let (stdout, stderr, exit) = run(source, &["--dry-run"]);
assert_eq!(exit, 0, "dry-run should succeed");
assert!(stdout.is_empty(), "stdout must be empty on dry-run success");
assert!(
stderr.contains("success[REORDER]"),
"stderr should report the reorder move as a change line: {stderr}"
);
assert!(
!stderr.contains("fn a()"),
"stderr must not echo reconstructed source: {stderr}"
);
}
#[test]
fn empty_directory_should_run_cleanly() {
let dir = temp_dir();
fs::create_dir(&dir).unwrap();
let (stdout, stderr, exit) = run_dir(&dir, &[]);
let _ = fs::remove_dir_all(&dir);
assert_eq!(exit, 0, "empty directory should exit successfully");
assert!(
stdout.is_empty(),
"stdout should be empty for empty directory"
);
assert!(stderr.is_empty(), "stderr should be empty on success");
}
#[test]
fn in_place_write_should_match_after_fixture() {
let expected = include_str!("fixtures/reorder/phase_use_stable_after.rs");
let dir = std::env::temp_dir();
let pid = std::process::id();
let seq = TEST_COUNTER.fetch_add(1, Ordering::Relaxed);
let tmp = dir.join(format!("rust-llm-tidy-write-test-{}-{}.rs", pid, seq));
fs::write(
&tmp,
include_str!("fixtures/reorder/phase_use_stable_before.rs"),
)
.unwrap();
let output = run_command(&["--include", "reorder"], &tmp);
assert!(
output.status.success(),
"rust-llm-tidy (no --dry-run) failed"
);
let actual = fs::read_to_string(&tmp).unwrap();
let _ = fs::remove_file(&tmp);
assert_eq!(
actual, expected,
"in-place write: temp file content must match phase_use_stable_after.rs"
);
}
#[test]
fn invalid_source_should_abort_with_error() {
let source = "not valid rust {{{";
let (_stdout, stderr, exit) = run(source, &[]);
assert_ne!(exit, 0, "rust-llm-tidy should exit non-zero on parse error");
assert!(!stderr.is_empty(), "stderr should contain error message");
}
#[test]
fn nonexistent_path_should_fail_with_error() {
let nonexistent = std::env::temp_dir().join(format!(
"rust-llm-tidy-missing-{}-{}-{}-{}-{}-{}-{}-{}-{}.rs",
std::process::id(),
std::process::id(),
std::process::id(),
std::process::id(),
std::process::id(),
std::process::id(),
std::process::id(),
std::process::id(),
std::process::id()
));
let output = run_command(&["--include", "reorder"], &nonexistent);
assert!(
!output.status.success(),
"non-existent path should exit non-zero"
);
assert!(
!String::from_utf8_lossy(&output.stderr).is_empty(),
"stderr should report the missing path"
);
}
#[test]
fn recursive_dir_collects_uppercase_variants_excludes_others() {
let dir = temp_dir();
let nested = dir.join("src");
fs::create_dir_all(&nested).unwrap();
fs::write(nested.join("lib.RS"), "fn a() {}\nfn b() { a(); }\n").unwrap();
fs::write(
dir.join("README.MD"),
"| Name | Value | Description |\n| --- | --- | --- |\n| a | 1 | first |\n| longname | 200 | second item |\n",
)
.unwrap();
fs::write(dir.join("notes.txt"), "not rust or markdown\n").unwrap();
let (_stdout, stderr, exit) = run_dir(&dir, &["--dry-run"]);
assert_eq!(exit, 0, "dir with .RS/.MD/.txt should succeed");
assert!(
stderr.contains("lib.RS"),
"recursion must collect and process lib.RS: {stderr}"
);
let (_stdout, md_stderr, md_exit) = run_dir(&dir, &["--include", "tables", "--dry-run"]);
assert_eq!(md_exit, 0, "tables dry-run on dir should succeed");
assert!(
md_stderr.contains("README.MD") && md_stderr.contains("success[FIX]"),
"recursion must collect and process README.MD: {md_stderr}"
);
assert!(
!md_stderr.contains("notes.txt") && !stderr.contains("notes.txt"),
"notes.txt must be excluded silently"
);
let _ = fs::remove_dir_all(&dir);
}
#[test]
fn recursive_directory_dry_run_should_label_each_move_with_path() {
let dir = temp_dir();
fs::create_dir(&dir).unwrap();
let file_a = dir.join("a.rs");
let file_b = dir.join("b.rs");
fs::write(&file_a, "fn a() {}\nfn b() { a(); }\n").unwrap();
fs::write(&file_b, "fn c() {}\nfn d() { c(); }\n").unwrap();
let (stdout, stderr, exit) = run_dir(&dir, &["--dry-run"]);
let _ = fs::remove_dir_all(&dir);
assert_eq!(exit, 0, "dry-run on directory should succeed");
assert!(stdout.is_empty(), "stdout should be empty on success");
assert!(
stderr.contains("a.rs:") && stderr.contains("b.rs:"),
"multi-file dry-run must label each change line with its path: {stderr}"
);
assert!(
stderr.contains("rearrange fn b from pos 2 to pos 1")
&& stderr.contains("rearrange fn d from pos 2 to pos 1"),
"directory dry-run should report each file's move on stderr: {stderr}"
);
}
#[test]
fn recursive_directory_error_should_still_reorder_valid_file() {
let dir = temp_dir();
fs::create_dir(&dir).unwrap();
let good = dir.join("good.rs");
let bad = dir.join("bad.rs");
fs::write(&good, "fn a() {}\nfn b() { a(); }\n").unwrap();
fs::write(&bad, "not valid rust {{{").unwrap();
let (_stdout, stderr, exit) = run_dir(&dir, &[]);
let actual_good = fs::read_to_string(&good).unwrap();
let _ = fs::remove_dir_all(&dir);
assert_ne!(exit, 0, "directory with invalid file should exit non-zero");
assert!(
!stderr.is_empty(),
"stderr should contain error message for invalid file"
);
let a_pos = actual_good.find("fn a").expect("fn a missing");
let b_pos = actual_good.find("fn b").expect("fn b missing");
assert!(
b_pos < a_pos,
"valid file should still be reordered despite sibling error"
);
}
#[test]
fn recursive_directory_should_reorder_every_rs_file() {
let dir = temp_dir();
let root_file = dir.join("phase_use.rs");
let nested_dir = dir.join("utils");
let nested_file = nested_dir.join("phase_mod.rs");
fs::create_dir_all(&nested_dir).unwrap();
fs::write(
&root_file,
include_str!("fixtures/reorder/phase_use_stable_before.rs"),
)
.unwrap();
fs::write(
&nested_file,
include_str!("fixtures/reorder/phase_mod_non_test_stable_before.rs"),
)
.unwrap();
let output = run_command(&["--include", "reorder"], &dir);
assert!(
output.status.success(),
"directory run failed: {}",
String::from_utf8_lossy(&output.stderr)
);
let expected_root = include_str!("fixtures/reorder/phase_use_stable_after.rs");
let expected_nested = include_str!("fixtures/reorder/phase_mod_non_test_stable_after.rs");
let actual_root = fs::read_to_string(&root_file).unwrap();
let actual_nested = fs::read_to_string(&nested_file).unwrap();
let _ = fs::remove_dir_all(&dir);
assert_eq!(
actual_root, expected_root,
"phase_use.rs should be reordered in place"
);
assert_eq!(
actual_nested, expected_nested,
"utils/phase_mod.rs should be reordered in place"
);
}
#[test]
fn reorder_dry_run_reports_change_with_empty_stdout() {
let source = "fn a() {}\r\nfn b() { a(); }\r\n";
let (stdout, stderr, exit) = run(source, &["--dry-run"]);
assert_eq!(exit, 0, "dry-run should succeed");
assert!(stdout.is_empty(), "dry-run must not print source to stdout");
assert!(
stderr.contains("success[REORDER]"),
"dry-run must report a reorder change on stderr: {stderr:?}"
);
}
fn reorder_in_place(path: &std::path::Path) -> String {
let tmp = temp_file();
fs::copy(path, &tmp).unwrap();
let output = run_command(&["--include", "reorder"], &tmp);
assert!(
output.status.success(),
"in-place reorder failed on {}: {}",
path.display(),
String::from_utf8_lossy(&output.stderr)
);
let result = fs::read_to_string(&tmp).unwrap();
let _ = fs::remove_file(&tmp);
result
}
#[test]
fn reorder_in_place_preserves_crlf() {
let source = "fn b() { a(); }\r\nfn a() {}\r\n";
let result = run_and_read(source);
let a_pos = result.find("fn a").expect("fn a missing");
let b_pos = result.find("fn b").expect("fn b missing");
assert!(b_pos < a_pos, "b (caller) before a (callee)");
assert_eq!(
result.matches('\n').count(),
result.matches("\r\n").count(),
"every newline must be CRLF after reorder: {result:?}"
);
}
#[test]
fn reorder_in_place_reports_change_and_writes() {
let fixture = manifest_dir()
.join("tests")
.join("fixtures")
.join("reorder");
let expected =
fs::read_to_string(fixture.join("fn_interstitial_comment_travels_with_next_after.rs"))
.unwrap();
let tmp = temp_file();
fs::copy(
fixture.join("fn_interstitial_comment_travels_with_next_before.rs"),
&tmp,
)
.unwrap();
let output = run_command(&["--include", "reorder"], &tmp);
assert!(
output.status.success(),
"in-place reorder on a moving fixture should succeed"
);
let actual = fs::read_to_string(&tmp).unwrap();
let _ = fs::remove_file(&tmp);
assert_eq!(
actual, expected,
"in-place reorder must write the after fixture"
);
let stderr = String::from_utf8_lossy(&output.stderr);
assert!(
stderr.contains("success[REORDER]"),
"in-place reorder must also report its change line on stderr: {stderr}"
);
}
#[test]
fn reorder_real_file_should_keep_phase_and_caller_order() {
let source = "\
use std::fmt;\n\n\
pub struct Config {\n\
pub name: String,\n}\n\n\
impl Config {\n\
pub fn new(name: &str) -> Self {\n\
Config {\n\
name: name.to_string(),\n\
}\n\
}\n}\n\n\
fn validate(c: &Config) -> bool {\n\
!c.name.is_empty()\n}\n\n\
pub fn build(name: &str) -> Option<Config> {\n\
let c = Config::new(name);\n\
if validate(&c) {\n\
Some(c)\n\
} else {\n\
None\n\
}\n}\n";
let result = run_and_read(source);
let use_pos = result.find("use std::fmt").unwrap();
let struct_pos = result.find("pub struct Config").unwrap();
let impl_pos = result.find("impl Config").unwrap();
let build_pos = result.find("pub fn build").unwrap();
let validate_pos = result.find("fn validate").unwrap();
assert!(use_pos < struct_pos, "use before struct");
assert!(struct_pos < impl_pos, "struct before its impl");
assert!(
build_pos < validate_pos,
"build (caller) before validate (callee)"
);
}
#[test]
fn sorted_file_should_roundtrip_unchanged() {
let source = "\
fn main() {\n\
a();\n\
b();\n}\n\n\
fn a() {\n\
helper();\n}\n\n\
fn b() {}\n\n\
fn helper() {}\n";
let result = run_and_read(source);
let main_pos = result.find("fn main").unwrap();
let a_pos = result.find("fn a").unwrap();
let b_pos = result.find("fn b").unwrap();
let helper_pos = result.find("fn helper").unwrap();
assert!(main_pos < a_pos, "main before a");
assert!(a_pos < helper_pos, "a before helper (a calls helper)");
assert!(b_pos < helper_pos, "b before helper (original order)");
}
#[test]
fn uppercase_md_explicit_file_runs_fix_not_rust_ops() {
let file = temp_file_ext("MD");
fs::write(
&file,
"| Name | Value | Description |\n| --- | --- | --- |\n| a | 1 | first |\n| longname | 200 | second item |\n",
)
.unwrap();
let output = run_command(&["--include", "tables"], &file);
assert!(
output.status.success(),
".MD file should be admitted: {}",
String::from_utf8_lossy(&output.stderr)
);
let stderr = String::from_utf8_lossy(&output.stderr);
assert!(
stderr.contains("success[FIX]"),
".MD must run markdown fix ops: {stderr}"
);
let output = run_command(&["--include", "reorder", "--dry-run"], &file);
assert!(
output.status.success(),
".MD reorder dry-run should succeed without Rust ops: {}",
String::from_utf8_lossy(&output.stderr)
);
assert!(
!String::from_utf8_lossy(&output.stderr).contains("success[REORDER]"),
".MD must never run the Rust reorder op"
);
let _ = fs::remove_file(&file);
}
#[test]
fn uppercase_rs_explicit_file_runs_reorder() {
let file = temp_file_ext("RS");
fs::write(&file, "fn a() {}\nfn b() { a(); }\n").unwrap();
let output = run_command(&["--include", "reorder"], &file);
let _ = fs::remove_file(&file);
assert!(
output.status.success(),
".RS file should be admitted: {}",
String::from_utf8_lossy(&output.stderr)
);
let stderr = String::from_utf8_lossy(&output.stderr);
assert!(
stderr.contains("success[REORDER]"),
".RS must run the Rust reorder op: {stderr}"
);
}
#[test]
fn uppercase_txt_explicit_file_is_silently_skipped() {
let file = temp_file_ext("TXT");
fs::write(&file, "not rust or markdown\n").unwrap();
let output = run_command(&["--json"], &file);
let _ = fs::remove_file(&file);
assert!(
output.status.success(),
"unadmitted .TXT file must succeed silently: {}",
String::from_utf8_lossy(&output.stderr)
);
assert_eq!(
String::from_utf8_lossy(&output.stdout).trim(),
"[]",
".TXT explicit file is a silent skip in JSON mode"
);
}
fn manifest_dir() -> std::path::PathBuf {
std::path::PathBuf::from(env!("CARGO_MANIFEST_DIR"))
}
fn run(content: &str, args: &[&str]) -> (String, String, i32) {
let dir = std::env::temp_dir();
let pid = std::process::id();
let seq = TEST_COUNTER.fetch_add(1, Ordering::Relaxed);
let file = dir.join(format!("rust-llm-tidy-test-{}-{}.rs", pid, seq));
fs::write(&file, content).unwrap();
let mut full_args = vec!["--include", "reorder"];
full_args.extend(args);
let output = run_command(&full_args, &file);
let stdout = String::from_utf8_lossy(&output.stdout).to_string();
let stderr = String::from_utf8_lossy(&output.stderr).to_string();
let exit = output.status.code().unwrap_or(-1);
let _ = fs::remove_file(&file);
(stdout, stderr, exit)
}
fn run_and_read(content: &str) -> String {
let dir = std::env::temp_dir();
let pid = std::process::id();
let seq = TEST_COUNTER.fetch_add(1, Ordering::Relaxed);
let file = dir.join(format!("rust-llm-tidy-test-{}-{}.rs", pid, seq));
fs::write(&file, content).unwrap();
let output = run_command(&["--include", "reorder"], &file);
assert!(
output.status.success(),
"rust-llm-tidy failed: {}",
String::from_utf8_lossy(&output.stderr)
);
let result = fs::read_to_string(&file).unwrap();
let _ = fs::remove_file(&file);
result
}
fn run_dir(dir: &std::path::Path, args: &[&str]) -> (String, String, i32) {
let mut full_args = vec!["--include", "reorder"];
full_args.extend(args);
let output = run_command(&full_args, dir);
let stdout = String::from_utf8_lossy(&output.stdout).to_string();
let stderr = String::from_utf8_lossy(&output.stderr).to_string();
let exit = output.status.code().unwrap_or(-1);
(stdout, stderr, exit)
}
fn run_dry_run(path: &std::path::Path) -> (String, String, i32) {
let output = run_command(&["--include", "reorder", "--dry-run"], path);
assert!(
output.status.success(),
"rust-llm-tidy --dry-run failed on {}: {}",
path.display(),
String::from_utf8_lossy(&output.stderr)
);
(
String::from_utf8_lossy(&output.stdout).to_string(),
String::from_utf8_lossy(&output.stderr).to_string(),
output.status.code().unwrap_or(-1),
)
}
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-dir-{}-{}", pid, seq))
}
fn temp_file() -> 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-file-{}-{}.rs", pid, seq))
}
fn temp_file_ext(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-ext-{}-{}.{}", pid, seq, ext))
}
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 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")
}