use crate::domain::analysis_data::{FunctionClassification, FunctionRecord};
use crate::domain::{AnalysisData, AnalysisFindings};
use crate::ports::Reporter;
use crate::report::dot::*;
fn make_record(name: &str, classification: FunctionClassification) -> FunctionRecord {
FunctionRecord {
name: name.to_string(),
file: "test.rs".to_string(),
line: 1,
qualified_name: name.to_string(),
parent_type: None,
classification,
severity: None,
complexity: None,
parameter_count: 0,
own_calls: vec![],
is_trait_impl: false,
is_test: false,
effort_score: None,
suppressed: false,
complexity_suppressed: false,
}
}
fn data_with(functions: Vec<FunctionRecord>) -> AnalysisData {
AnalysisData {
functions,
modules: vec![],
}
}
#[test]
fn test_print_dot_empty_yields_digraph_envelope() {
let data = data_with(vec![]);
let findings = AnalysisFindings::default();
let out = DotReporter.render(&findings, &data);
assert!(out.starts_with("digraph rustqual {"), "got {out}");
assert!(out.ends_with("}\n"), "got {out}");
}
#[test]
fn test_print_dot_integration_node_present() {
let data = data_with(vec![make_record(
"orchestrator",
FunctionClassification::Integration,
)]);
let findings = AnalysisFindings::default();
let out = DotReporter.render(&findings, &data);
assert!(
out.contains("\"orchestrator\""),
"Integration function node must be emitted; got {out}"
);
}
#[test]
fn test_print_dot_violation_node_present() {
let data = data_with(vec![make_record(
"bad_fn",
FunctionClassification::Violation,
)]);
let findings = AnalysisFindings::default();
let out = DotReporter.render(&findings, &data);
assert!(
out.contains("\"bad_fn\""),
"Violation function node must be emitted; got {out}"
);
}
#[test]
fn test_print_dot_suppressed_skipped() {
let mut rec = make_record("suppressed", FunctionClassification::Operation);
rec.suppressed = true;
let data = data_with(vec![rec]);
print_dot(&data);
}
#[test]
fn test_print_dot_all_classifications() {
let data = data_with(vec![
make_record("integration_fn", FunctionClassification::Integration),
make_record("operation_fn", FunctionClassification::Operation),
make_record("trivial_fn", FunctionClassification::Trivial),
make_record("violation_fn", FunctionClassification::Violation),
]);
print_dot(&data);
}
#[test]
fn test_dot_render_returns_digraph_envelope() {
let findings = AnalysisFindings::default();
let data = data_with(vec![]);
let out = DotReporter.render(&findings, &data);
assert!(
out.starts_with("digraph rustqual {"),
"render output must open with digraph envelope, got: {out:?}",
);
assert!(out.ends_with("}\n"), "render output must close envelope");
}
#[test]
fn test_dot_render_emits_node_and_edge_for_function_with_calls() {
let mut caller = make_record("caller", FunctionClassification::Integration);
caller.own_calls = vec!["callee".to_string()];
let callee = make_record("callee", FunctionClassification::Operation);
let data = data_with(vec![caller, callee]);
let findings = AnalysisFindings::default();
let out = DotReporter.render(&findings, &data);
assert!(out.contains("\"caller\""), "caller node missing in output");
assert!(out.contains("\"callee\""), "callee node missing in output");
assert!(
out.contains("\"caller\" -> \"callee\";"),
"edge from caller to callee missing in output: {out}",
);
}
#[test]
fn test_dot_render_ignores_findings() {
let data = data_with(vec![make_record("f", FunctionClassification::Integration)]);
let empty_findings = AnalysisFindings::default();
let out_empty = DotReporter.render(&empty_findings, &data);
assert!(out_empty.contains("\"f\""), "function node missing");
assert!(out_empty.starts_with("digraph rustqual {"));
assert!(out_empty.ends_with("}\n"));
}
#[test]
fn dot_reporter_intentionally_omits_orphan_rendering() {
use crate::domain::findings::OrphanSuppression;
let data = data_with(vec![make_record("f", FunctionClassification::Integration)]);
let mut findings = AnalysisFindings::default();
findings.orphan_suppressions = vec![OrphanSuppression {
file: "src/foo.rs".into(),
line: 42,
dimensions: vec![crate::findings::Dimension::Iosp],
reason: Some("legacy".into()),
}];
let out = DotReporter.render(&findings, &data);
assert!(
!out.to_lowercase().contains("orphan"),
"dot reporter must NOT render orphan markers (intentional no-op), got:\n{out}"
);
assert!(
!out.contains("qual:allow"),
"dot reporter must NOT render orphan reason text, got:\n{out}"
);
}