#![allow(
clippy::unwrap_used,
clippy::expect_used,
clippy::panic,
reason = "tests use unwrap/expect/panic to keep fixture setup and schema lookups concise"
)]
#[path = "common/mod.rs"]
mod common;
use std::path::{Path, PathBuf};
use std::process::Command;
use common::{configure_type_aware_sidecar, fallow_bin};
use serde_json::Value;
use tempfile::TempDir;
fn schema_path() -> PathBuf {
Path::new(env!("CARGO_MANIFEST_DIR")).join("../../docs/output-schema.json")
}
fn load_schema_root() -> Value {
let text = std::fs::read_to_string(schema_path()).expect("read output-schema.json");
serde_json::from_str(&text).expect("parse output-schema.json")
}
fn assert_conforms(root: &Value, kind: &str, value: &Value) {
assert_eq!(
value.get("kind").and_then(Value::as_str),
Some(kind),
"document kind must be {kind:?}, got {:?}\ndocument:\n{value}",
value.get("kind"),
);
let definitions = root
.get("definitions")
.expect("schema has a definitions object");
let branches = definitions
.get("FallowOutput")
.and_then(|f| f.get("oneOf"))
.and_then(Value::as_array)
.expect("FallowOutput.oneOf is an array");
let branch = branches
.iter()
.find(|branch| {
branch
.get("allOf")
.and_then(Value::as_array)
.is_some_and(|parts| {
parts.iter().any(|part| {
part.get("properties")
.and_then(|p| p.get("kind"))
.and_then(|k| k.get("const"))
.and_then(Value::as_str)
== Some(kind)
})
})
})
.unwrap_or_else(|| panic!("no FallowOutput oneOf branch declares kind {kind:?}"));
let branch_schema = serde_json::json!({
"$schema": "http://json-schema.org/draft-07/schema#",
"allOf": branch.get("allOf").expect("branch has an allOf"),
"definitions": definitions,
});
let validator = jsonschema::draft7::new(&branch_schema)
.unwrap_or_else(|err| panic!("compile draft-07 branch for {kind:?}: {err}"));
let errors: Vec<String> = validator
.iter_errors(value)
.map(|err| format!(" at {} : {err}", err.instance_path()))
.collect();
assert!(
errors.is_empty(),
"'{kind}' stdout document does not conform to docs/output-schema.json:\n{}",
errors.join("\n"),
);
}
fn git(dir: &Path, args: &[&str]) {
let output = Command::new("git")
.args(args)
.current_dir(dir)
.env_remove("GIT_DIR")
.env_remove("GIT_WORK_TREE")
.env("GIT_CONFIG_GLOBAL", "/dev/null")
.env("GIT_CONFIG_SYSTEM", "/dev/null")
.env("GIT_AUTHOR_NAME", "test")
.env("GIT_AUTHOR_EMAIL", "test@example.invalid")
.env("GIT_COMMITTER_NAME", "test")
.env("GIT_COMMITTER_EMAIL", "test@example.invalid")
.output()
.expect("git command failed");
assert!(
output.status.success(),
"git {args:?} failed: {}",
String::from_utf8_lossy(&output.stderr),
);
}
fn git_fixture() -> TempDir {
let tmp = TempDir::new().expect("temp dir");
let dir = tmp.path();
std::fs::create_dir_all(dir.join("src")).unwrap();
std::fs::write(
dir.join("package.json"),
r#"{"name":"cli-conformance","main":"src/index.ts"}"#,
)
.unwrap();
std::fs::write(
dir.join("tsconfig.json"),
r#"{"compilerOptions":{"strict":true},"include":["src/**/*.ts"]}"#,
)
.unwrap();
std::fs::write(
dir.join("src/index.ts"),
"import { used } from './lib';\n\
if (process.env.FEATURE_ALPHA) {\n used();\n}\n",
)
.unwrap();
std::fs::write(
dir.join("src/lib.ts"),
"export const used = () => 42;\n\
// fallow-ignore-next-line unused-export\n\
export const suppressed = () => 0;\n\
export const dead = () => 1;\n",
)
.unwrap();
git(dir, &["init", "-b", "main"]);
git(dir, &["add", "."]);
git(dir, &["-c", "commit.gpgsign=false", "commit", "-m", "base"]);
tmp
}
fn run_and_validate(schema: &Value, root: &Path, args: &[&str], expected_kind: &str) -> Value {
run_and_validate_with(schema, root, args, expected_kind, |_| {})
}
fn run_and_validate_with(
schema: &Value,
root: &Path,
args: &[&str],
expected_kind: &str,
configure: impl FnOnce(&mut Command),
) -> Value {
let mut cmd = Command::new(fallow_bin());
cmd.env("RUST_LOG", "").env("NO_COLOR", "1");
configure(&mut cmd);
for arg in args {
cmd.arg(arg);
}
cmd.arg("--root")
.arg(root)
.arg("--format")
.arg("json")
.arg("--quiet");
if expected_kind != "doctor" {
cmd.arg("--no-cache");
}
let output = cmd.output().expect("run fallow binary");
let stdout = String::from_utf8_lossy(&output.stdout);
let value: Value = serde_json::from_str(&stdout).unwrap_or_else(|err| {
panic!(
"{expected_kind}: stdout is not JSON: {err}\nstdout:\n{stdout}\nstderr:\n{}",
String::from_utf8_lossy(&output.stderr),
)
});
assert_conforms(schema, expected_kind, &value);
value
}
#[test]
fn cli_json_documents_conform_to_output_schema() {
let fixture = git_fixture();
let root = fixture.path();
let schema = load_schema_root();
run_and_validate(&schema, root, &["dead-code"], "dead-code");
run_and_validate(&schema, root, &["health"], "health");
run_and_validate(&schema, root, &["dupes"], "dupes");
run_and_validate(&schema, root, &[], "combined");
run_and_validate(&schema, root, &["suppressions"], "suppression-inventory");
run_and_validate(&schema, root, &["flags"], "feature-flags");
run_and_validate(&schema, root, &["doctor"], "doctor");
run_and_validate(&schema, root, &["audit", "--base", "HEAD"], "audit");
run_and_validate(
&schema,
root,
&["audit", "--brief", "--base", "HEAD"],
"audit-brief",
);
run_and_validate(
&schema,
root,
&["decision-surface", "--base", "HEAD"],
"decision-surface",
);
run_and_validate(
&schema,
root,
&["trace", "src/lib.ts:used", "--callers"],
"trace",
);
run_and_validate(
&schema,
root,
&["trace", "--path", "src/index.ts", "src/lib.ts"],
"trace",
);
run_and_validate(
&schema,
root,
&["trace", "--path", "src/lib.ts", "src/index.ts"],
"trace",
);
run_and_validate_with(
&schema,
root,
&["dead-code", "--trace", "src/lib.ts:used", "--type-aware"],
"trace",
configure_type_aware_sidecar,
);
run_and_validate(
&schema,
root,
&["type-aware", "status"],
"type-aware-status",
);
}
#[test]
fn run_fact_documents_conform_to_output_schema() {
let fixture = git_fixture();
let root = fixture.path();
let schema = load_schema_root();
let diff = root.join("scoped.diff");
std::fs::write(
&diff,
"diff --git a/src/lib.ts b/src/lib.ts\n\
--- a/src/lib.ts\n\
+++ b/src/lib.ts\n\
@@ -1,1 +1,1 @@\n\
+export const used = () => 42;\n",
)
.unwrap();
let scoped = run_and_validate(
&schema,
root,
&[
"dead-code",
"--changed-since",
"refs/heads/does-not-exist",
"--diff-file",
diff.to_str().unwrap(),
],
"dead-code",
);
assert_eq!(
scoped["request_outcomes"]["changed-since"]["status"], "not-applied",
"the case must carry the member it validates: {scoped}"
);
assert_eq!(
scoped["request_outcomes"]["diff-filter"]["status"], "applied",
"and both entry shapes: {scoped}"
);
let non_repo = TempDir::new().expect("temp dir");
std::fs::create_dir_all(non_repo.path().join("src")).unwrap();
std::fs::write(
non_repo.path().join("package.json"),
r#"{"name":"health-diagnostics","main":"src/index.ts"}"#,
)
.unwrap();
std::fs::write(
non_repo.path().join("src/index.ts"),
"export const entry = () => 1;\n",
)
.unwrap();
let degraded = run_and_validate(
&schema,
non_repo.path(),
&["health", "--hotspots"],
"health",
);
assert!(
degraded["workspace_diagnostics"]
.as_array()
.is_some_and(|entries| entries
.iter()
.any(|entry| entry["kind"] == "hotspots-skipped")),
"the case must carry the kind it validates: {degraded}"
);
}
#[test]
fn trace_error_documents_conform_to_output_schema() {
let fixture = git_fixture();
let root = fixture.path();
let schema = load_schema_root();
std::fs::write(
root.join("resolving.txt"),
"TypeError: used is not a function\n\
\x20 at used (src/lib.ts:1:14)\n\
\x20 at Object.<anonymous> (src/index.ts:3:3)\n\
\x20 at Module._compile (node:internal/modules/cjs/loader:1105:14)\n",
)
.unwrap();
std::fs::write(
root.join("unrecognised.txt"),
"something went wrong\nsee the logs\n",
)
.unwrap();
std::fs::write(root.join("empty.txt"), "").unwrap();
for trace in ["resolving.txt", "unrecognised.txt", "empty.txt"] {
run_and_validate(&schema, root, &["trace-error", trace], "trace-error");
}
}
#[test]
fn error_envelope_conforms_to_output_schema() {
let schema = load_schema_root();
let validator = jsonschema::draft7::new(&schema).expect("compile output schema root");
let output = Command::new(fallow_bin())
.args(["dead-code", "--root", "/nonexistent-fallow-path-xyz"])
.args(["--format", "json", "--quiet", "--no-cache"])
.env("RUST_LOG", "")
.env("NO_COLOR", "1")
.output()
.expect("run fallow binary");
let stdout = String::from_utf8_lossy(&output.stdout);
let value: Value = serde_json::from_str(&stdout)
.unwrap_or_else(|err| panic!("error stdout is not JSON: {err}\nstdout:\n{stdout}"));
assert_eq!(value.get("error").and_then(Value::as_bool), Some(true));
assert!(
value.get("kind").is_none(),
"error envelope carries no kind"
);
let errors: Vec<String> = validator
.iter_errors(&value)
.map(|err| format!(" at {} : {err}", err.instance_path()))
.collect();
assert!(
errors.is_empty(),
"error envelope does not conform to docs/output-schema.json:\n{}",
errors.join("\n"),
);
}