use crate::adapters::analyzers::architecture::compiled::CompiledArchitecture;
use crate::adapters::analyzers::architecture::explain::{
explain_file, ExplainReport, ImportEntry, ImportKind,
};
use crate::adapters::analyzers::architecture::forbidden_rule::CompiledForbiddenRule;
use crate::adapters::analyzers::architecture::layer_rule::{LayerDefinitions, UnmatchedBehavior};
use globset::{Glob, GlobMatcher, GlobSet, GlobSetBuilder};
use std::collections::HashMap;
fn matcher(pattern: &str) -> GlobMatcher {
Glob::new(pattern).expect("valid glob").compile_matcher()
}
fn globset(patterns: &[&str]) -> GlobSet {
let mut b = GlobSetBuilder::new();
for p in patterns {
b.add(Glob::new(p).expect("valid glob"));
}
b.build().expect("valid glob set")
}
fn minimal_compiled() -> CompiledArchitecture {
CompiledArchitecture {
layers: LayerDefinitions::new(
vec![
"domain".to_string(),
"port".to_string(),
"application".to_string(),
"adapter".to_string(),
],
vec![
("domain".to_string(), globset(&["src/domain/**"])),
("port".to_string(), globset(&["src/ports/**"])),
("application".to_string(), globset(&["src/app/**"])),
("adapter".to_string(), globset(&["src/adapters/**"])),
],
),
reexport_points: globset(&["src/lib.rs"]),
unmatched_behavior: UnmatchedBehavior::CompositionRoot,
external_exact: HashMap::new(),
external_glob: Vec::new(),
forbidden: Vec::new(),
trait_contracts: Vec::new(),
call_parity: None,
}
}
fn parse_file(src: &str) -> syn::File {
syn::parse_str(src).expect("parse")
}
fn explain_domain_importing_adapter() -> ExplainReport {
let mut compiled = minimal_compiled();
compiled.forbidden.push(CompiledForbiddenRule {
from: matcher("src/domain/**"),
to: matcher("src/adapters/**"),
except: globset(&[]),
reason: "no outward imports".to_string(),
});
let ast = parse_file("use crate::adapters::X;");
explain_file("src/domain/bad.rs", &ast, &compiled)
}
#[test]
fn reports_assigned_layer() {
let ast = parse_file("fn f() {}");
let report = explain_file("src/domain/foo.rs", &ast, &minimal_compiled());
assert_eq!(report.layer.as_deref(), Some("domain"));
assert_eq!(report.rank, Some(0));
assert!(!report.is_reexport);
}
#[test]
fn reports_reexport_point() {
let ast = parse_file("pub use crate::adapters::X;");
let report = explain_file("src/lib.rs", &ast, &minimal_compiled());
assert!(report.is_reexport);
assert!(report.layer_violations.is_empty());
assert!(report.forbidden_violations.is_empty());
}
#[test]
fn reports_unmatched_file() {
let ast = parse_file("fn f() {}");
let report = explain_file("src/misc/foo.rs", &ast, &minimal_compiled());
assert!(report.layer.is_none());
assert!(!report.is_reexport);
}
#[test]
fn classifies_crate_import_resolved() {
let ast = parse_file("use crate::adapters::foo::Bar;");
let report = explain_file("src/adapters/mod.rs", &ast, &minimal_compiled());
let imports = &report.imports;
assert_eq!(imports.len(), 1);
match &imports[0].kind {
ImportKind::CrateInternal { target_layer, .. } => {
assert_eq!(target_layer.as_deref(), Some("adapter"));
}
other => panic!("expected CrateInternal, got {other:?}"),
}
}
#[test]
fn classifies_stdlib_imports_as_ignored() {
let ast = parse_file("use std::collections::HashMap; use core::fmt;");
let report = explain_file("src/domain/foo.rs", &ast, &minimal_compiled());
assert!(report
.imports
.iter()
.all(|i| matches!(i.kind, ImportKind::Ignored { .. })));
}
#[test]
fn classifies_external_exact() {
let mut compiled = minimal_compiled();
compiled
.external_exact
.insert("tokio".to_string(), "adapter".to_string());
let ast = parse_file("use tokio::spawn;");
let report = explain_file("src/adapters/net.rs", &ast, &compiled);
match &report.imports[0].kind {
ImportKind::ExternalCrate {
crate_name,
resolved_layer,
..
} => {
assert_eq!(crate_name, "tokio");
assert_eq!(resolved_layer.as_deref(), Some("adapter"));
}
other => panic!("expected ExternalCrate, got {other:?}"),
}
}
#[test]
fn classifies_external_unknown_as_no_mapping() {
let ast = parse_file("use mystery_crate::Foo;");
let report = explain_file("src/adapters/x.rs", &ast, &minimal_compiled());
match &report.imports[0].kind {
ImportKind::ExternalCrate { resolved_layer, .. } => assert!(resolved_layer.is_none()),
other => panic!("unexpected kind: {other:?}"),
}
}
#[test]
fn layer_violation_surfaced() {
let ast = parse_file("use crate::adapters::X;");
let report = explain_file("src/domain/bad.rs", &ast, &minimal_compiled());
assert_eq!(
report.layer_violations.len(),
1,
"{:?}",
report.layer_violations
);
}
#[test]
fn forbidden_violation_surfaced() {
let report = explain_domain_importing_adapter();
assert_eq!(report.forbidden_violations.len(), 1);
}
#[test]
fn render_mentions_layer_name() {
let ast = parse_file("fn f() {}");
let report = explain_file("src/domain/foo.rs", &ast, &minimal_compiled());
let text = report.render();
assert!(text.contains("domain"), "{text}");
assert!(text.contains("src/domain/foo.rs"), "{text}");
}
#[test]
fn render_marks_reexport_point() {
let ast = parse_file("fn f() {}");
let report = explain_file("src/lib.rs", &ast, &minimal_compiled());
let text = report.render();
assert!(text.to_lowercase().contains("re-export"), "{text}");
}
#[test]
fn render_includes_violation_sections() {
let text = explain_domain_importing_adapter().render();
assert!(text.to_lowercase().contains("layer violation"), "{text}");
assert!(text.to_lowercase().contains("forbidden"), "{text}");
}
use crate::adapters::analyzers::architecture::{MatchLocation, ViolationKind};
fn base_report() -> ExplainReport {
ExplainReport {
file: "src/x.rs".into(),
layer: None,
rank: None,
is_reexport: false,
imports: vec![],
layer_violations: vec![],
forbidden_violations: vec![],
}
}
fn layer_violation() -> MatchLocation {
MatchLocation {
file: "src/x.rs".into(),
line: 5,
column: 0,
kind: ViolationKind::LayerViolation {
from_layer: "application".into(),
to_layer: "domain".into(),
imported_path: "crate::domain::Y".into(),
},
}
}
#[test]
fn header_shows_layer_and_rank() {
let mut r = base_report();
r.layer = Some("domain".into());
r.rank = Some(2);
let text = r.render();
assert!(text.contains("Layer: domain (rank 2)"), "{text}");
}
#[test]
fn layer_violation_line_rendered_and_no_all_clear() {
let mut r = base_report();
r.layer_violations = vec![layer_violation()];
let text = r.render();
assert!(text.contains("Layer violations:"), "section header: {text}");
assert!(
text.contains("application") && text.contains("domain"),
"from/to layers + import in the line: {text}"
);
assert!(text.contains("crate::domain::Y"), "imported path: {text}");
assert!(
!text.contains("No architecture violations"),
"all-clear suppressed when violations exist: {text}"
);
}
#[test]
fn forbidden_violation_line_rendered() {
let mut r = base_report();
r.forbidden_violations = vec![MatchLocation {
file: "src/x.rs".into(),
line: 9,
column: 0,
kind: ViolationKind::ForbiddenEdge {
reason: "no outward imports".into(),
imported_path: "crate::app::Z".into(),
},
}];
let text = r.render();
assert!(text.contains("Forbidden rule violations:"), "{text}");
assert!(
text.contains("crate::app::Z") || text.contains("no outward imports"),
"{text}"
);
}
#[test]
fn import_entry_renders_tail() {
let mut r = base_report();
r.imports = vec![ImportEntry {
line: 3,
rendered: "use std::fmt;".into(),
kind: ImportKind::Ignored {
first_segment: "std".into(),
},
}];
let text = r.render();
assert!(text.contains("ignored (std)"), "import tail: {text}");
}