use std::fs;
use std::path::PathBuf;
use std::process::Command;
use std::sync::atomic::{AtomicU64, Ordering};
static TEST_COUNTER: AtomicU64 = AtomicU64::new(0);
#[test]
fn all_pipeline_runs_vis_after_reorder() {
let source = "\
/// Module doc.
pub(crate) mod m {
/// Fn doc.
pub fn f() {}
}
";
let tmp = temp_file("rs");
fs::write(&tmp, source).unwrap();
let output = run_command(&[], &tmp);
assert!(
output.status.success(),
"all should succeed: {}",
String::from_utf8_lossy(&output.stderr)
);
let actual = fs::read_to_string(&tmp).unwrap();
let _ = fs::remove_file(&tmp);
assert!(
actual.contains("pub(crate) fn f"),
"vis must narrow bare pub inside all: {actual}"
);
assert!(
!actual.contains("pub fn f"),
"bare pub fn must be gone after all: {actual}"
);
}
#[test]
fn vis_crate_aware_narrows_cross_file() {
let lib_path = make_temp_crate("pub(crate) mod foo;\n", "pub fn f() {}\n");
let foo_path = src_sibling(&lib_path, "foo.rs");
let output = run_command(&["--include", "vis"], &foo_path);
assert!(
output.status.success(),
"vis crate-aware should succeed: {}",
String::from_utf8_lossy(&output.stderr)
);
let actual = fs::read_to_string(&foo_path).unwrap();
assert!(
actual.contains("pub(crate) fn f"),
"cross-file floor must narrow foo::f: {actual}"
);
let _ = fs::remove_dir_all(lib_path.parent().unwrap().parent().unwrap());
}
#[test]
fn vis_dry_run_matches_after() {
let before = fixture_dir().join("narrow_pub_crate_before.rs");
let expected = fs::read_to_string(fixture_dir().join("narrow_pub_crate_after.rs")).unwrap();
let output = run_command(&["--include", "vis", "--dry-run"], &before);
assert!(
output.status.success(),
"vis --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 narrow_pub_crate_after.rs"
);
}
#[test]
fn vis_idempotent_on_after_fixtures() {
for name in ["narrow_pub_crate_after.rs", "reexport_guard_after.rs"] {
let path = fixture_dir().join(name);
let expected = fs::read_to_string(&path).unwrap();
let output = run_command(&["--include", "vis", "--dry-run"], &path);
assert!(
output.status.success(),
"vis --dry-run on {name} should succeed"
);
let stdout = String::from_utf8_lossy(&output.stdout);
assert_eq!(
&*stdout, &*expected,
"{name} must be idempotent (output unchanged)"
);
}
}
#[test]
fn vis_in_place_write() {
let expected = fs::read_to_string(fixture_dir().join("narrow_pub_crate_after.rs")).unwrap();
let tmp = temp_file("rs");
fs::write(
&tmp,
fs::read_to_string(fixture_dir().join("narrow_pub_crate_before.rs")).unwrap(),
)
.unwrap();
let output = run_command(&["--include", "vis"], &tmp);
assert!(
output.status.success(),
"vis 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 vis_nonexistent_path_fails() {
let nonexistent = std::env::temp_dir().join(format!(
"rust-llm-tidy-vis-missing-{}-{}.rs",
std::process::id(),
TEST_COUNTER.fetch_add(1, Ordering::Relaxed)
));
let output = run_command(&["--include", "vis"], &nonexistent);
assert!(
!output.status.success(),
"non-existent path should exit non-zero"
);
}
#[test]
fn vis_reexport_guard_unchanged() {
let before = fixture_dir().join("reexport_guard_before.rs");
let expected = fs::read_to_string(fixture_dir().join("reexport_guard_after.rs")).unwrap();
let output = run_command(&["--include", "vis", "--dry-run"], &before);
assert!(
output.status.success(),
"vis --dry-run should succeed: {}",
String::from_utf8_lossy(&output.stderr)
);
let stdout = String::from_utf8_lossy(&output.stdout);
assert_eq!(
stdout, expected,
"re-export guard: dry-run stdout must match reexport_guard_after.rs (unchanged)"
);
}
#[test]
fn vis_standalone_without_cargo_toml() {
let tmp = temp_file("rs");
fs::write(&tmp, "pub(crate) mod m {\n pub fn f() {}\n}\n").unwrap();
let output = run_command(&["--include", "vis"], &tmp);
let stderr = String::from_utf8_lossy(&output.stderr);
let actual = fs::read_to_string(&tmp).unwrap();
let _ = fs::remove_file(&tmp);
assert!(
output.status.success(),
"standalone path must succeed when no Cargo.toml is found: {stderr}"
);
assert!(
actual.contains("pub(crate) fn f"),
"standalone fallback still narrows inline mod: {actual}"
);
}
#[test]
fn vis_warns_on_unresolved_mod() {
let lib_path = make_temp_crate("pub(crate) mod foo;\nmod missing;\n", "pub fn f() {}\n");
let foo_path = src_sibling(&lib_path, "foo.rs");
let output = run_command(&["--include", "vis"], &foo_path);
let stderr = String::from_utf8_lossy(&output.stderr);
let foo_actual = fs::read_to_string(src_sibling(&lib_path, "foo.rs")).unwrap();
let _ = fs::remove_dir_all(lib_path.parent().unwrap().parent().unwrap());
assert!(
output.status.success(),
"vis must still succeed with an unresolved mod (REQ-006): {stderr}"
);
assert!(
stderr.contains("mod missing") && stderr.contains("resolves to no"),
"unresolved mod warning must name the mod and its empty resolution: {stderr}"
);
assert!(
foo_actual.contains("pub(crate) fn f"),
"vis must still narrow foo::f despite the unresolved sibling mod: {foo_actual}"
);
}
fn fixture_dir() -> std::path::PathBuf {
manifest_dir().join("tests").join("fixtures").join("vis")
}
fn make_temp_crate(lib_src: &str, foo_src: &str) -> PathBuf {
let seq = TEST_COUNTER.fetch_add(1, Ordering::Relaxed);
let pid = std::process::id();
let root = std::env::temp_dir().join(format!("rlt-vis-crate-{pid}-{seq}"));
let src = root.join("src");
fs::create_dir_all(&src).unwrap();
fs::write(
root.join("Cargo.toml"),
format!(
"[package]\nname = \"rlt_vis_crate_{pid}_{seq}\"\nversion = \"0.0.0\"\nedition = \"2021\"\n[lib]\npath = \"src/lib.rs\"\n"
),
)
.unwrap();
fs::write(src.join("lib.rs"), lib_src).unwrap();
fs::write(src.join("foo.rs"), foo_src).unwrap();
src.join("lib.rs")
}
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 src_sibling(lib_path: &std::path::Path, name: &str) -> PathBuf {
lib_path.parent().unwrap().join(name)
}
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-vis-{}-{}.{}", 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"))
}