use std::fs;
use std::process::Command;
const BIN: &str = env!("CARGO_BIN_EXE_pathlint");
fn run_doctor_in(cwd: &std::path::Path) -> (i32, String, String) {
let out = Command::new(BIN)
.arg("doctor")
.current_dir(cwd)
.env_remove("XDG_CONFIG_HOME")
.output()
.expect("failed to run pathlint binary");
let code = out.status.code().unwrap_or(-1);
let stdout = String::from_utf8_lossy(&out.stdout).into_owned();
let stderr = String::from_utf8_lossy(&out.stderr).into_owned();
(code, stdout, stderr)
}
#[test]
fn doctor_does_not_report_path_anomalies() {
let tmp = tempfile::tempdir().unwrap();
let dir = tmp.path().join("x");
fs::create_dir_all(&dir).unwrap();
let sep = if cfg!(windows) { ";" } else { ":" };
let path = format!("{0}{1}{0}", dir.display(), sep);
let out = Command::new(BIN)
.arg("doctor")
.env("PATH", &path)
.env_remove("XDG_CONFIG_HOME")
.output()
.unwrap();
let stdout = String::from_utf8_lossy(&out.stdout);
assert!(
!stdout.contains("duplicate"),
"doctor must not surface duplicate (moved to lint): {stdout}"
);
assert!(
!stdout.contains("shortenable"),
"doctor must not surface shortenable (moved to lint): {stdout}"
);
assert!(
!stdout.contains("shadows"),
"doctor must not surface shadow (moved to lint): {stdout}"
);
}
#[test]
fn doctor_reports_config_not_found_when_no_toml() {
let tmp = tempfile::tempdir().unwrap();
let (code, stdout, stderr) = run_doctor_in(tmp.path());
let context = format!("code={code} stdout={stdout:?} stderr={stderr:?}");
assert_eq!(code, 0, "{context}");
assert!(
stdout.contains("config_not_found") || stdout.contains("pathlint.toml"),
"{context}"
);
}
#[test]
fn doctor_parses_a_valid_toml_without_error() {
let tmp = tempfile::tempdir().unwrap();
let cfg = tmp.path().join("pathlint.toml");
fs::write(&cfg, "# valid empty config\n").unwrap();
let (code, stdout, stderr) = run_doctor_in(tmp.path());
let context = format!("code={code} stdout={stdout:?} stderr={stderr:?}");
assert_eq!(code, 0, "{context}");
assert!(
!stdout.contains("config_parse_error"),
"valid toml must not produce parse error: {context}"
);
}
#[test]
fn doctor_reports_config_parse_error_on_broken_toml() {
let tmp = tempfile::tempdir().unwrap();
let cfg = tmp.path().join("pathlint.toml");
fs::write(&cfg, "[broken\nnot = valid toml\n").unwrap();
let (code, stdout, stderr) = run_doctor_in(tmp.path());
let context = format!("code={code} stdout={stdout:?} stderr={stderr:?}");
assert_ne!(code, 0, "broken toml must produce non-zero exit; {context}");
assert!(
stdout.contains("failed to parse") || stderr.contains("parse"),
"{context}"
);
}
#[test]
fn doctor_json_emits_top_level_array() {
let tmp = tempfile::tempdir().unwrap();
let out = Command::new(BIN)
.args(["doctor", "--json"])
.current_dir(tmp.path())
.env_remove("XDG_CONFIG_HOME")
.output()
.unwrap();
let stdout = String::from_utf8_lossy(&out.stdout).into_owned();
let v: serde_json::Value = serde_json::from_str(stdout.trim())
.unwrap_or_else(|_| panic!("doctor --json not valid JSON: {stdout}"));
assert!(v.is_array(), "expected array, got: {stdout}");
}
#[test]
fn doctor_emits_selfcheck_kinds_only() {
let tmp = tempfile::tempdir().unwrap();
let out = Command::new(BIN)
.args(["doctor", "--json"])
.current_dir(tmp.path())
.env_remove("XDG_CONFIG_HOME")
.output()
.unwrap();
let stdout = String::from_utf8_lossy(&out.stdout).into_owned();
let v: serde_json::Value = serde_json::from_str(stdout.trim()).expect(&stdout);
let lint_kinds = [
"duplicate",
"duplicate_but_shadowed",
"shortenable",
"missing",
"trailing_slash",
"writeable_path_dir",
"relative_path_entry",
"conflict",
"case_variant",
"short_name",
"malformed",
"per_source_missing_required",
];
if let Some(arr) = v.as_array() {
for diag in arr {
let kind = diag.get("kind").and_then(|k| k.as_str()).unwrap_or("");
assert!(
!lint_kinds.contains(&kind),
"doctor emitted lint kind {kind}: {stdout}"
);
}
}
}
#[test]
fn doctor_emits_binary_not_in_path_when_running_dir_not_on_path() {
let tmp = tempfile::tempdir().unwrap();
let other = tmp.path().join("other");
fs::create_dir_all(&other).unwrap();
let out = Command::new(BIN)
.arg("doctor")
.current_dir(tmp.path())
.env("PATH", other.display().to_string())
.env_remove("XDG_CONFIG_HOME")
.output()
.unwrap();
let stdout = String::from_utf8_lossy(&out.stdout);
assert!(
stdout.contains("not on PATH"),
"expected binary_not_in_path: {stdout}"
);
}
#[test]
fn doctor_binary_self_locate_green_path() {
let tmp = tempfile::tempdir().unwrap();
let bin_dir = std::path::Path::new(BIN).parent().unwrap();
let path_value = bin_dir.display().to_string();
let out = Command::new(BIN)
.arg("doctor")
.current_dir(tmp.path())
.env("PATH", &path_value)
.env_remove("XDG_CONFIG_HOME")
.output()
.unwrap();
let stdout = String::from_utf8_lossy(&out.stdout);
assert!(
!stdout.contains("not on PATH"),
"binary should self-locate: {stdout}"
);
}
#[test]
#[cfg(not(windows))]
fn doctor_emits_env_lookup_failed_when_home_unset() {
let tmp = tempfile::tempdir().unwrap();
let out = Command::new(BIN)
.arg("doctor")
.current_dir(tmp.path())
.env_remove("XDG_CONFIG_HOME")
.env_remove("HOME")
.output()
.unwrap();
let stdout = String::from_utf8_lossy(&out.stdout);
assert!(
stdout.contains("env_lookup failed for HOME"),
"expected env_lookup_failed for HOME: {stdout}"
);
}
#[test]
#[cfg(windows)]
fn doctor_emits_env_lookup_failed_when_userprofile_unset() {
let tmp = tempfile::tempdir().unwrap();
let out = Command::new(BIN)
.arg("doctor")
.current_dir(tmp.path())
.env_remove("XDG_CONFIG_HOME")
.env_remove("USERPROFILE")
.output()
.unwrap();
let stdout = String::from_utf8_lossy(&out.stdout);
assert!(
stdout.contains("env_lookup failed for USERPROFILE"),
"expected env_lookup_failed for USERPROFILE: {stdout}"
);
}