#[cfg(unix)] use std::path::Path;
use assert_cmd::Command;
fn isolated_config() -> (tempfile::TempDir, std::path::PathBuf) {
let dir = tempfile::tempdir().expect("tempdir");
let path = dir.path().join("config.toml");
(dir, path)
}
#[cfg(unix)] fn fake_tool(dir: &Path, tag: &str) {
let tool = dir.join("ignition-lint");
let argv_out = dir.join(format!("{tag}-argv.txt"));
let script = format!(
"#!/bin/sh\nprintf '%s\\n' \"$@\" > \"{}\"\nprintf '%s' \"$LINT_STDOUT\"\nexit 1\n",
argv_out.display()
);
std::fs::write(&tool, script).expect("write fake tool");
#[cfg(unix)]
{
use std::os::unix::fs::PermissionsExt;
let mut perms = std::fs::metadata(&tool).expect("stat").permissions();
perms.set_mode(0o755);
std::fs::set_permissions(&tool, perms).expect("chmod");
}
}
fn ign_lint(path_var: &str, stdout_payload: &str, args: &[&str]) -> std::process::Output {
let mut command = Command::cargo_bin("ign").expect("binary 'ign' not found");
command
.env("PATH", path_var)
.env("LINT_STDOUT", stdout_payload);
command.args(args).output().expect("spawn ign")
}
#[cfg(unix)] fn stdout_for_golden(out: &std::process::Output) -> &str {
let stdout = std::str::from_utf8(&out.stdout).expect("utf-8 stdout");
stdout.strip_suffix('\n').unwrap_or(stdout)
}
fn stderr_envelope(out: &std::process::Output) -> String {
let stderr = String::from_utf8_lossy(&out.stderr);
let start = stderr.find('{').unwrap_or(0);
stderr[start..].to_string()
}
#[cfg(unix)] const REPORT: &str = r#"{"issues":[{"severity":"error"}],"summary":{"errors":1}}"#;
#[test]
fn lint_tool_absent_refusal_golden() {
let empty = tempfile::tempdir().expect("empty path dir");
let (_config_dir, config) = isolated_config();
std::fs::write(&config, "active = \"dev\"\n").expect("a loadable config");
let out = ign_lint(
empty.path().to_str().unwrap(),
"",
&["lint", "src", "--compact"],
);
assert_eq!(out.status.code(), Some(6), "target-state class");
assert!(out.stdout.is_empty(), "errors never touch stdout");
snapbox::Assert::new().action_env("SNAPSHOTS").eq(
stderr_envelope(&out),
snapbox::str![[r#"
{"ok":false,"profile":null,"error":{"code":"lint_tool_absent","message":"ignition-lint is not installed (no executable found on PATH)","endpoint":null,"hint":"install the linter: `uv tool install ignition-lint-toolkit` (or `pip install ignition-lint-toolkit`) — github.com/TheThoughtagen/ignition-lint; then re-run with ignition-lint on PATH"}}
"#]],
);
}
#[cfg(unix)] #[test]
fn lint_findings_ride_as_data_golden() {
let tool_dir = tempfile::tempdir().expect("tool dir");
fake_tool(tool_dir.path(), "data");
let (_config_dir, config) = isolated_config();
std::fs::write(&config, "active = \"dev\"\n").expect("a loadable config");
let out = ign_lint(
tool_dir.path().to_str().unwrap(),
REPORT,
&["lint", "src", "--compact"],
);
assert_eq!(
out.status.code(),
Some(0),
"the doctor posture: the child RAN — findings are data\nstderr: {}",
String::from_utf8_lossy(&out.stderr)
);
snapbox::Assert::new().action_env("SNAPSHOTS").eq(
stdout_for_golden(&out),
snapbox::str![[r#"{"ok":true,"profile":null,"data":{"ran":true,"tool":"[..]ignition-lint","child_exit_code":1,"issues_found":1,"report":{"issues":[{"severity":"error"}],"summary":{"errors":1}},"stdout":"{/"issues/":[{/"severity/":/"error/"}],/"summary/":{/"errors/":1}}","stderr_preview":""}}"#]],
);
let argv = std::fs::read_to_string(tool_dir.path().join("data-argv.txt")).expect("argv");
assert_eq!(
argv.lines().collect::<Vec<_>>(),
vec!["--report-format", "json", "--target", "src"],
);
}
#[cfg(unix)] #[test]
fn lint_human_render() {
let tool_dir = tempfile::tempdir().expect("tool dir");
fake_tool(tool_dir.path(), "human");
let (_config_dir, config) = isolated_config();
std::fs::write(&config, "active = \"dev\"\n").expect("a loadable config");
let out = ign_lint(tool_dir.path().to_str().unwrap(), REPORT, &["lint", "src"]);
assert!(
out.status.success(),
"stderr: {}",
String::from_utf8_lossy(&out.stderr)
);
snapbox::Assert::new().action_env("SNAPSHOTS").eq(
stdout_for_golden(&out),
snapbox::str![[r#"
lint: 1 issue(s), child exit 1
summary: {"errors":1}
"#]],
);
}
#[cfg(unix)] #[test]
fn lint_strict_passthrough_exits_with_the_child_code() {
let tool_dir = tempfile::tempdir().expect("tool dir");
fake_tool(tool_dir.path(), "strict");
let (_config_dir, config) = isolated_config();
std::fs::write(&config, "active = \"dev\"\n").expect("a loadable config");
let out = ign_lint(
tool_dir.path().to_str().unwrap(),
REPORT,
&["lint", "src", "--strict", "--compact"],
);
assert_eq!(out.status.code(), Some(1), "the child's code, literally");
let stdout = stdout_for_golden(&out);
assert!(
stdout.starts_with(r#"{"ok":true,"profile":null,"data":{"ran":true"#),
"the envelope still renders BEFORE the exit: {stdout}"
);
}
#[cfg(unix)] #[test]
fn lint_passthrough_args_ride_verbatim() {
let tool_dir = tempfile::tempdir().expect("tool dir");
fake_tool(tool_dir.path(), "pass");
let (_config_dir, config) = isolated_config();
std::fs::write(&config, "active = \"dev\"\n").expect("a loadable config");
let out = ign_lint(
tool_dir.path().to_str().unwrap(),
REPORT,
&[
"lint",
"a",
"b",
"--",
"--profile",
"perspective",
"--fail-on",
"warning",
],
);
assert!(
out.status.success(),
"stderr: {}",
String::from_utf8_lossy(&out.stderr)
);
let argv = std::fs::read_to_string(tool_dir.path().join("pass-argv.txt")).expect("argv");
assert_eq!(
argv.lines().collect::<Vec<_>>(),
vec![
"--report-format",
"json",
"--target",
"a",
"--target",
"b",
"--profile",
"perspective",
"--fail-on",
"warning",
],
"paths map to --target pairs; extras ride verbatim after --"
);
}