#![allow(
clippy::unwrap_used,
clippy::expect_used,
clippy::panic,
reason = "tests use unwrap/expect/panic to keep fixture setup and schema lookups concise"
)]
use std::path::{Path, PathBuf};
use fallow_api::{
AnalysisOptions, CombinedOptions, ComplexityOptions, DeadCodeFilters, DeadCodeOptions,
DuplicationOptions, FeatureFlagsOptions, TraceExportOptions, run_boundary_violations,
run_circular_dependencies, run_combined, run_dead_code, run_duplication, run_feature_flags,
run_health, run_trace_export, serialize_boundary_violations_programmatic_json,
serialize_circular_dependencies_programmatic_json, serialize_combined_programmatic_json,
serialize_dead_code_programmatic_json, serialize_duplication_programmatic_json,
serialize_feature_flags_programmatic_json, serialize_health_programmatic_json,
serialize_trace_export_programmatic_json,
};
use serde_json::Value;
fn schema_path() -> PathBuf {
Path::new(env!("CARGO_MANIFEST_DIR")).join("../../docs/output-schema.json")
}
struct EnvelopeSchema {
root: Value,
}
impl EnvelopeSchema {
fn load() -> Self {
let text = std::fs::read_to_string(schema_path()).expect("read output-schema.json");
let root = serde_json::from_str(&text).expect("parse output-schema.json");
Self { root }
}
fn definitions(&self) -> &Value {
self.root
.get("definitions")
.expect("schema has a definitions object")
}
fn branch_schema(&self, kind: &str) -> Value {
let branches = self
.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_matches_kind(branch, kind))
.unwrap_or_else(|| panic!("no FallowOutput oneOf branch declares kind {kind:?}"));
serde_json::json!({
"$schema": "http://json-schema.org/draft-07/schema#",
"allOf": branch.get("allOf").expect("branch has an allOf"),
"definitions": self.definitions(),
})
}
fn assert_conforms(&self, kind: &str, value: &Value) {
assert_eq!(
value.get("kind").and_then(Value::as_str),
Some(kind),
"document kind must be {kind:?}, got {:?}",
value.get("kind"),
);
let schema = self.branch_schema(kind);
let validator = jsonschema::draft7::new(&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}' document does not conform to docs/output-schema.json:\n{}",
errors.join("\n"),
);
}
}
fn branch_matches_kind(branch: &Value, kind: &str) -> bool {
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)
})
})
}
fn analysis_at(root: &Path) -> AnalysisOptions {
AnalysisOptions {
root: Some(root.to_path_buf()),
no_cache: true,
..AnalysisOptions::default()
}
}
fn small_project() -> tempfile::TempDir {
let project = tempfile::tempdir().expect("temp dir");
let root = project.path();
std::fs::create_dir(root.join("src")).expect("src dir");
std::fs::write(
root.join("package.json"),
r#"{"name":"conformance-fixture","main":"src/index.ts"}"#,
)
.expect("package.json");
std::fs::write(
root.join("src/index.ts"),
"import './lib';\nexport const entry = 1;\nconsole.log(entry);\n",
)
.expect("entry");
std::fs::write(
root.join("src/lib.ts"),
"export const used = 1;\nexport const deadA = 2;\nexport const deadB = 3;\n",
)
.expect("lib");
project
}
#[test]
fn dead_code_document_conforms() {
let project = small_project();
let run = run_dead_code(&DeadCodeOptions {
analysis: analysis_at(project.path()),
filters: DeadCodeFilters {
unused_exports: true,
..DeadCodeFilters::default()
},
..DeadCodeOptions::default()
})
.expect("dead-code runs");
let json = serialize_dead_code_programmatic_json(run).expect("serialize dead-code");
EnvelopeSchema::load().assert_conforms("dead-code", &json);
}
#[test]
fn duplication_document_conforms() {
let project = small_project();
let run = run_duplication(&DuplicationOptions {
analysis: analysis_at(project.path()),
..DuplicationOptions::default()
})
.expect("duplication runs");
let json = serialize_duplication_programmatic_json(run).expect("serialize duplication");
EnvelopeSchema::load().assert_conforms("dupes", &json);
}
#[test]
fn feature_flags_document_conforms() {
let project = tempfile::tempdir().expect("temp dir");
let root = project.path();
std::fs::create_dir(root.join("src")).expect("src dir");
std::fs::write(
root.join("package.json"),
r#"{"name":"flags-fixture","main":"src/index.ts"}"#,
)
.expect("package.json");
std::fs::write(
root.join("src/index.ts"),
"if (process.env.FEATURE_ALPHA) {\n console.log('on');\n}\n",
)
.expect("entry");
let run = run_feature_flags(&FeatureFlagsOptions {
analysis: analysis_at(root),
top: None,
})
.expect("feature-flags runs");
let json = serialize_feature_flags_programmatic_json(run).expect("serialize feature-flags");
EnvelopeSchema::load().assert_conforms("feature-flags", &json);
}
#[test]
fn feature_flags_explain_document_conforms() {
let project = tempfile::tempdir().expect("temp dir");
let root = project.path();
std::fs::create_dir(root.join("src")).expect("src dir");
std::fs::write(
root.join("package.json"),
r#"{"name":"flags-explain-fixture","main":"src/index.ts"}"#,
)
.expect("package.json");
std::fs::write(
root.join("src/index.ts"),
"if (process.env.FEATURE_ALPHA) {\n console.log('on');\n}\n",
)
.expect("entry");
let run = run_feature_flags(&FeatureFlagsOptions {
analysis: AnalysisOptions {
explain: true,
..analysis_at(root)
},
top: None,
})
.expect("feature-flags --explain runs");
let json = serialize_feature_flags_programmatic_json(run).expect("serialize feature-flags");
assert!(
json["_meta"]["feature_flags"].is_object(),
"--explain must populate _meta.feature_flags"
);
EnvelopeSchema::load().assert_conforms("feature-flags", &json);
}
#[test]
fn health_document_conforms() {
let project = small_project();
let run = run_health(&ComplexityOptions {
analysis: analysis_at(project.path()),
complexity: true,
score: true,
..ComplexityOptions::default()
})
.expect("health runs");
let json = serialize_health_programmatic_json(run).expect("serialize health");
EnvelopeSchema::load().assert_conforms("health", &json);
}
#[test]
fn combined_document_conforms() {
let project = small_project();
let run = run_combined(&CombinedOptions {
analysis: analysis_at(project.path()),
health_options: ComplexityOptions {
complexity: true,
score: true,
..ComplexityOptions::default()
},
..CombinedOptions::default()
})
.expect("combined runs");
let json = serialize_combined_programmatic_json(run).expect("serialize combined");
EnvelopeSchema::load().assert_conforms("combined", &json);
}
#[test]
fn circular_dependencies_document_conforms_as_dead_code() {
let project = small_project();
let run = run_circular_dependencies(&DeadCodeOptions {
analysis: analysis_at(project.path()),
..DeadCodeOptions::default()
})
.expect("circular-dependencies runs");
let json =
serialize_circular_dependencies_programmatic_json(run).expect("serialize circular deps");
EnvelopeSchema::load().assert_conforms("dead-code", &json);
}
#[test]
fn boundary_violations_document_conforms_as_dead_code() {
let project = small_project();
let run = run_boundary_violations(&DeadCodeOptions {
analysis: analysis_at(project.path()),
..DeadCodeOptions::default()
})
.expect("boundary-violations runs");
let json = serialize_boundary_violations_programmatic_json(run)
.expect("serialize boundary violations");
EnvelopeSchema::load().assert_conforms("dead-code", &json);
}
#[test]
fn trace_family_is_not_a_published_envelope() {
let project = small_project();
let out = run_trace_export(&TraceExportOptions {
analysis: analysis_at(project.path()),
file: "src/lib.ts".to_string(),
export_name: "deadA".to_string(),
})
.expect("trace export runs");
let json = serialize_trace_export_programmatic_json(out).expect("serialize trace export");
assert!(
json.get("kind").is_none(),
"programmatic trace output must stay un-enveloped (no root kind); if this \
changes, schematize the trace shapes in docs/output-schema.json first. Got: {json}"
);
}