use common::binary;
use core::sync::atomic::{AtomicU64, Ordering};
use std::fs;
use std::process::Command;
mod common;
static TEST_COUNTER: AtomicU64 = AtomicU64::new(0);
#[test]
fn no_args_empty_diff_succeeds() {
let Some(repo) = init_repo() else {
return;
};
fs::write(repo.join("a.rs"), "fn a() {}\n").unwrap();
git(&repo, &["add", "a.rs"]);
git(&repo, &["commit", "--quiet", "-m", "init"]);
fs::remove_file(repo.join("a.rs")).unwrap();
let out = run(&repo, &["--no-config"]);
assert!(
out.status.success(),
"empty diff must succeed (0 files processed): {}",
String::from_utf8_lossy(&out.stderr)
);
cleanup(&repo);
}
#[test]
fn no_args_empty_diff_with_bad_config_errors() {
let Some(repo) = init_repo() else {
return;
};
fs::write(repo.join("a.rs"), "fn a() {}\n").unwrap();
git(&repo, &["add", "a.rs"]);
git(&repo, &["commit", "--quiet", "-m", "init"]);
fs::remove_file(repo.join("a.rs")).unwrap();
let cfg = repo.join(".rust-llm-tidy.yml");
fs::write(
&cfg,
"include:\n - rules: [tables]\nexclude:\n - rules: [reorder]\n",
)
.unwrap();
let out = run(&repo, &["--config", cfg.to_str().unwrap()]);
assert!(
!out.status.success(),
"empty diff must still hard-fail on an invalid config: {}",
String::from_utf8_lossy(&out.stderr)
);
cleanup(&repo);
}
#[test]
fn no_args_nested_cwd_ignores_relative_diff_config() {
let Some(repo) = init_repo() else {
return;
};
fs::write(repo.join("a.rs"), "fn a() {}\n").unwrap();
git(&repo, &["add", "a.rs"]);
git(&repo, &["commit", "--quiet", "-m", "init"]);
fs::write(repo.join("a.rs"), "fn a() {}\nfn b() { a(); }\n").unwrap();
git(&repo, &["config", "diff.relative", "true"]);
let nested = repo.join("nested");
fs::create_dir_all(&nested).unwrap();
let out = run(&nested, &["--no-config", "--include", "reorder"]);
assert!(
out.status.success(),
"{}",
String::from_utf8_lossy(&out.stderr)
);
let actual = fs::read_to_string(repo.join("a.rs")).unwrap();
assert!(actual.find("fn b").unwrap() < actual.find("fn a").unwrap());
cleanup(&repo);
}
#[test]
fn no_args_not_in_repo_errors() {
let dir = temp_dir();
fs::create_dir_all(&dir).unwrap();
let out = run(&dir, &["--no-config"]);
assert!(
!out.status.success(),
"no args + not in a repo must exit non-zero"
);
let stderr = String::from_utf8_lossy(&out.stderr);
assert!(
!stderr.is_empty(),
"stderr should tell the user to pass paths: {stderr}"
);
cleanup(&dir);
}
#[test]
fn no_args_processes_git_diff() {
let Some(repo) = init_repo() else {
return;
};
let file = "a file 'quoted'.rs";
fs::write(repo.join(file), "fn b() { a(); }\nfn a() {}\n").unwrap();
git(&repo, &["add", file]);
git(&repo, &["commit", "--quiet", "-m", "init"]);
fs::write(repo.join(file), "fn a() {}\nfn b() { a(); }\n").unwrap();
git(&repo, &["add", file]);
let out = run(&repo, &["--no-config", "--include", "reorder"]);
assert!(
out.status.success(),
"{}",
String::from_utf8_lossy(&out.stderr)
);
let actual = fs::read_to_string(repo.join(file)).unwrap();
assert!(actual.find("fn b").unwrap() < actual.find("fn a").unwrap()); cleanup(&repo);
}
#[test]
fn no_args_selects_newly_allowed_extensions() {
let Some(repo) = init_repo() else {
return;
};
let py = repo.join("notes.py");
let cs = repo.join("Doc.CS");
let md = repo.join("README.MARKDOWN");
fs::write(&py, "# | a | b |\n# | --- | --- |\n# | 1 | 22 |\n").unwrap();
fs::write(&cs, "// | a | b |\n// | --- | --- |\n// | 1 | 22 |\n").unwrap();
fs::write(&md, "| a | b |\n| --- | --- |\n| 1 | 22 |\n").unwrap();
git(&repo, &["add", "."]);
let out = run(&repo, &["--no-config", "--include", "tables"]);
assert!(
out.status.success(),
"git-diff must allow .py/.CS/.MARKDOWN: {}",
String::from_utf8_lossy(&out.stderr)
);
let stderr = String::from_utf8_lossy(&out.stderr);
for name in ["notes.py", "Doc.CS", "README.MARKDOWN"] {
assert!(
stderr.contains(name),
"{name} must be selected and table-fixed: {stderr}"
);
}
assert_eq!(
stderr.matches("tables were aligned").count(),
3,
"each staged file must report one table fix: {stderr}"
);
let out = run(&repo, &["--no-config", "--include", "tables"]);
assert!(
out.status.success(),
"second git-diff run should succeed: {}",
String::from_utf8_lossy(&out.stderr)
);
assert!(
!String::from_utf8_lossy(&out.stderr).contains("success[FIX]"),
"second run must emit zero change records"
);
cleanup(&repo);
}
#[test]
fn no_args_selects_uppercase_extension_variants() {
let Some(repo) = init_repo() else {
return;
};
fs::write(repo.join("lib.RS"), "fn b() { a(); }\nfn a() {}\n").unwrap();
fs::write(
repo.join("README.MD"),
"| Name | Value |\n| --- | --- |\n| a | 1 |\n| long | 2 |\n",
)
.unwrap();
git(&repo, &["add", "."]);
git(&repo, &["commit", "--quiet", "-m", "init"]);
fs::write(repo.join("lib.RS"), "fn a() {}\nfn b() { a(); }\n").unwrap();
git(&repo, &["add", "lib.RS"]);
fs::write(
repo.join("README.MD"),
"| Name | Value |\n| --- | --- |\n| a | 1 |\n",
)
.unwrap();
git(&repo, &["add", "README.MD"]);
let out = run(
&repo,
&["--no-config", "--include", "reorder", "--include", "tables"],
);
assert!(
out.status.success(),
"git-diff must allow .RS/.MD variants: {}",
String::from_utf8_lossy(&out.stderr)
);
let stderr = String::from_utf8_lossy(&out.stderr);
assert!(
stderr.contains("success[REORDER]"),
"staged .RS must be selected and reordered: {stderr}"
);
assert!(
stderr.contains("success[FIX]"),
"staged .MD must be selected and table-fixed: {stderr}"
);
cleanup(&repo);
}
fn cleanup(dir: &std::path::Path) {
let _ = fs::remove_dir_all(dir);
}
fn init_repo() -> Option<std::path::PathBuf> {
if !git_available() {
return None;
}
let repo = temp_dir();
fs::create_dir_all(&repo).unwrap();
git(&repo, &["init", "--quiet"]);
git(&repo, &["config", "user.email", "t@t"]);
git(&repo, &["config", "user.name", "t"]);
Some(repo)
}
fn run(current_dir: &std::path::Path, args: &[&str]) -> std::process::Output {
Command::new(binary())
.current_dir(current_dir)
.args(args)
.output()
.expect("failed to spawn")
}
fn git(repo: &std::path::Path, args: &[&str]) -> String {
let out = Command::new("git")
.current_dir(repo)
.args(args)
.output()
.unwrap_or_else(|e| panic!("failed to run git {}: {e}", args.join(" ")));
if !out.status.success() {
panic!(
"git {} failed: {}",
args.join(" "),
String::from_utf8_lossy(&out.stderr)
);
}
String::from_utf8_lossy(&out.stdout).into_owned()
}
fn git_available() -> bool {
Command::new("git")
.arg("--version")
.status()
.map(|s| s.success())
.unwrap_or(false)
}
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-git-{}-{}", pid, seq))
}