use std::fs;
use std::process::Command;
use std::sync::atomic::{AtomicU64, Ordering};
static TEST_COUNTER: AtomicU64 = AtomicU64::new(0);
#[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_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_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_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);
}
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 cleanup(dir: &std::path::Path) {
let _ = fs::remove_dir_all(dir);
}
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))
}
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")
}