use crate::adapters::analyzers::architecture::forbidden_rule::{
check_forbidden_rules, CompiledForbiddenRule,
};
use crate::adapters::analyzers::architecture::{MatchLocation, ViolationKind};
use globset::{Glob, GlobMatcher, GlobSet, GlobSetBuilder};
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 parse_file(src: &str) -> syn::File {
syn::parse_str(src).expect("test fixture must parse")
}
struct Fixture {
parsed: Vec<(String, syn::File)>,
}
impl Fixture {
fn new(files: &[(&str, &str)]) -> Self {
Self {
parsed: files
.iter()
.map(|(p, s)| (p.to_string(), parse_file(s)))
.collect(),
}
}
fn refs(&self) -> Vec<(String, &syn::File)> {
self.parsed.iter().map(|(p, f)| (p.clone(), f)).collect()
}
}
fn rule(from: &str, to: &str, reason: &str) -> CompiledForbiddenRule {
CompiledForbiddenRule {
from: matcher(from),
to: matcher(to),
except: globset(&[]),
reason: reason.to_string(),
}
}
fn rule_with_except(from: &str, to: &str, except: &[&str], reason: &str) -> CompiledForbiddenRule {
CompiledForbiddenRule {
from: matcher(from),
to: matcher(to),
except: globset(except),
reason: reason.to_string(),
}
}
fn run(fx: &Fixture, rules: &[CompiledForbiddenRule]) -> Vec<MatchLocation> {
let refs = fx.refs();
check_forbidden_rules(&refs, rules)
}
#[test]
fn clean_file_no_violations() {
let fx = Fixture::new(&[("src/domain/foo.rs", "pub struct Foo;")]);
let rules = vec![rule(
"src/adapters/analyzers/iosp/**",
"src/adapters/analyzers/*/**",
"peers isolated",
)];
assert!(run(&fx, &rules).is_empty());
}
#[test]
fn file_not_matching_from_is_skipped() {
let fx = Fixture::new(&[(
"src/domain/foo.rs",
"use crate::adapters::analyzers::srp::X;",
)]);
let rules = vec![rule(
"src/adapters/analyzers/iosp/**",
"src/adapters/analyzers/srp/**",
"peers isolated",
)];
assert!(run(&fx, &rules).is_empty());
}
#[test]
fn from_matching_file_with_to_matching_import_flagged() {
let fx = Fixture::new(&[(
"src/adapters/analyzers/iosp/mod.rs",
"use crate::adapters::analyzers::srp::Something;",
)]);
let rules = vec![rule(
"src/adapters/analyzers/iosp/**",
"src/adapters/analyzers/srp/**",
"peers isolated",
)];
let hits = run(&fx, &rules);
assert_eq!(hits.len(), 1, "{hits:?}");
match &hits[0].kind {
ViolationKind::ForbiddenEdge {
reason,
imported_path,
} => {
assert_eq!(reason, "peers isolated");
assert!(imported_path.starts_with("crate::adapters::analyzers::srp"));
}
other => panic!("unexpected kind: {other:?}"),
}
assert_eq!(hits[0].file, "src/adapters/analyzers/iosp/mod.rs");
}
#[test]
fn import_of_different_module_same_adapter_tree_ok_when_to_is_peer_only() {
let fx = Fixture::new(&[(
"src/adapters/analyzers/iosp/mod.rs",
"use crate::adapters::analyzers::iosp::scope::ProjectScope;",
)]);
let rules = vec![rule_with_except(
"src/adapters/analyzers/iosp/**",
"src/adapters/analyzers/**",
&["src/adapters/analyzers/iosp/**"],
"isolate from peers",
)];
assert!(run(&fx, &rules).is_empty());
}
#[test]
fn except_suppresses_specific_targets() {
let fx = Fixture::new(&[
("src/domain/a.rs", "use crate::shared::util::X;"),
("src/domain/b.rs", "use crate::adapters::mod_::Y;"),
]);
let rules = vec![rule_with_except(
"src/domain/**",
"src/**",
&["src/domain/**", "src/shared/**"],
"domain isolated",
)];
let hits = run(&fx, &rules);
assert_eq!(hits.len(), 1, "only adapters import flagged: {hits:?}");
assert_eq!(hits[0].file, "src/domain/b.rs");
}
#[test]
fn except_matching_any_candidate_suppresses_hit() {
let fx = Fixture::new(&[(
"src/adapters/analyzers/iosp/mod.rs",
"use crate::adapters::analyzers::iosp::types::Foo;",
)]);
let rules = vec![rule_with_except(
"src/adapters/analyzers/iosp/**",
"src/adapters/analyzers/**",
&["src/adapters/analyzers/iosp/**"],
"isolate peers",
)];
assert!(run(&fx, &rules).is_empty());
}
#[test]
fn import_matches_leaf_module_file_candidate() {
let fx = Fixture::new(&[(
"src/domain/x.rs",
"use crate::adapters::analyzers::report::print;",
)]);
let rules = vec![rule(
"src/domain/**",
"src/adapters/analyzers/report/**",
"domain → report forbidden",
)];
let hits = run(&fx, &rules);
assert_eq!(hits.len(), 1, "{hits:?}");
}
#[test]
fn import_matches_module_dir_mod_rs_candidate() {
let fx = Fixture::new(&[("src/domain/x.rs", "use crate::adapters::report;")]);
let rules = vec![rule(
"src/domain/**",
"src/adapters/report/**",
"domain → report forbidden",
)];
assert_eq!(run(&fx, &rules).len(), 1);
}
#[test]
fn external_crate_imports_not_affected() {
let fx = Fixture::new(&[(
"src/adapters/analyzers/iosp/mod.rs",
"use tokio::spawn; use serde::Deserialize;",
)]);
let rules = vec![rule(
"src/adapters/analyzers/iosp/**",
"**",
"nothing imported",
)];
assert!(run(&fx, &rules).is_empty());
}
#[test]
fn std_import_is_ignored() {
let fx = Fixture::new(&[("src/adapters/analyzers/iosp/mod.rs", "use std::io;")]);
let rules = vec![rule(
"src/adapters/analyzers/iosp/**",
"**",
"std not matched",
)];
assert!(run(&fx, &rules).is_empty());
}
#[test]
fn multiple_rules_evaluated_independently() {
let fx = Fixture::new(&[(
"src/adapters/analyzers/iosp/mod.rs",
"use crate::adapters::analyzers::srp::X; use crate::adapters::report::Y;",
)]);
let rules = vec![
rule(
"src/adapters/analyzers/iosp/**",
"src/adapters/analyzers/srp/**",
"peers",
),
rule(
"src/adapters/analyzers/iosp/**",
"src/adapters/report/**",
"no reports",
),
];
let hits = run(&fx, &rules);
assert_eq!(hits.len(), 2, "one hit per rule: {hits:?}");
let reasons: Vec<&str> = hits
.iter()
.filter_map(|h| match &h.kind {
ViolationKind::ForbiddenEdge { reason, .. } => Some(reason.as_str()),
_ => None,
})
.collect();
assert!(reasons.contains(&"peers"));
assert!(reasons.contains(&"no reports"));
}
#[test]
fn grouped_use_flags_each_matching_leaf() {
let fx = Fixture::new(&[(
"src/adapters/analyzers/iosp/mod.rs",
"use crate::{adapters::analyzers::srp::X, domain::Y};",
)]);
let rules = vec![rule(
"src/adapters/analyzers/iosp/**",
"src/adapters/analyzers/srp/**",
"peers",
)];
assert_eq!(run(&fx, &rules).len(), 1);
}
#[test]
fn super_import_resolves_to_crate_absolute() {
let fx = Fixture::new(&[(
"src/adapters/analyzers/iosp/mod.rs",
"use super::srp::measure;",
)]);
let rules = vec![rule(
"src/adapters/analyzers/iosp/**",
"src/adapters/analyzers/srp/**",
"peers",
)];
assert_eq!(run(&fx, &rules).len(), 1);
}
#[test]
fn super_super_import_resolves_to_crate_absolute() {
let fx = Fixture::new(&[(
"src/adapters/analyzers/iosp/scope.rs",
"use super::super::srp::measure;",
)]);
let rules = vec![rule(
"src/adapters/analyzers/iosp/**",
"src/adapters/analyzers/srp/**",
"peers",
)];
assert_eq!(run(&fx, &rules).len(), 1);
}
#[test]
fn self_import_resolves_to_crate_absolute() {
let fx = Fixture::new(&[("src/adapters/analyzers/mod.rs", "use self::srp::measure;")]);
let rules = vec![rule(
"src/adapters/analyzers/**",
"src/adapters/analyzers/srp/**",
"peer",
)];
assert_eq!(run(&fx, &rules).len(), 1);
}
#[test]
fn super_does_not_match_unrelated_rule() {
let fx = Fixture::new(&[(
"src/adapters/analyzers/iosp/mod.rs",
"use super::shared::use_tree;",
)]);
let rules = vec![rule(
"src/adapters/analyzers/iosp/**",
"src/adapters/analyzers/srp/**",
"peers",
)];
assert!(run(&fx, &rules).is_empty());
}