358 lines
12 KiB
Rust
358 lines
12 KiB
Rust
use archlens_domain::{
|
|
CodeElement, CodeElementKind, CodeGraph, FilePath, ModuleName, Relationship, RelationshipKind,
|
|
};
|
|
|
|
fn make_element(name: &str, module: Option<&str>) -> CodeElement {
|
|
let mut element = CodeElement::new(
|
|
name,
|
|
CodeElementKind::Class,
|
|
FilePath::new(&format!("src/{name}.rs")).unwrap(),
|
|
1,
|
|
)
|
|
.unwrap();
|
|
|
|
if let Some(m) = module {
|
|
element = element.with_module(ModuleName::new(m).unwrap());
|
|
}
|
|
|
|
element
|
|
}
|
|
|
|
#[test]
|
|
fn empty_graph_has_no_elements() {
|
|
let graph = CodeGraph::new();
|
|
assert!(graph.elements().is_empty());
|
|
assert!(graph.relationships().is_empty());
|
|
}
|
|
|
|
#[test]
|
|
fn graph_stores_added_elements() {
|
|
let mut graph = CodeGraph::new();
|
|
graph.add_element(make_element("OrderService", None));
|
|
graph.add_element(make_element("Order", None));
|
|
|
|
assert_eq!(graph.elements().len(), 2);
|
|
}
|
|
|
|
#[test]
|
|
fn graph_stores_relationships() {
|
|
let mut graph = CodeGraph::new();
|
|
let service = make_element("OrderService", None);
|
|
let repo = make_element("OrderRepository", None);
|
|
|
|
graph.add_element(service);
|
|
graph.add_element(repo);
|
|
graph.add_relationship(
|
|
Relationship::new(
|
|
"OrderService",
|
|
"OrderRepository",
|
|
RelationshipKind::Composition,
|
|
)
|
|
.unwrap(),
|
|
);
|
|
|
|
assert_eq!(graph.relationships().len(), 1);
|
|
let rel = &graph.relationships()[0];
|
|
assert_eq!(rel.source(), "OrderService");
|
|
assert_eq!(rel.target(), "OrderRepository");
|
|
assert_eq!(rel.kind(), RelationshipKind::Composition);
|
|
}
|
|
|
|
#[test]
|
|
fn subgraph_by_module_filters_elements() {
|
|
let mut graph = CodeGraph::new();
|
|
graph.add_element(make_element("OrderService", Some("Orders")));
|
|
graph.add_element(make_element("Order", Some("Orders")));
|
|
graph.add_element(make_element("BillingService", Some("Billing")));
|
|
|
|
let module = ModuleName::new("Orders").unwrap();
|
|
let subgraph = graph.subgraph_by_module(&module);
|
|
|
|
assert_eq!(subgraph.elements().len(), 2);
|
|
assert!(
|
|
subgraph
|
|
.elements()
|
|
.iter()
|
|
.all(|e| e.module().unwrap().as_str() == "Orders")
|
|
);
|
|
}
|
|
|
|
#[test]
|
|
fn subgraph_includes_relationships_within_module() {
|
|
let mut graph = CodeGraph::new();
|
|
graph.add_element(make_element("OrderService", Some("Orders")));
|
|
graph.add_element(make_element("Order", Some("Orders")));
|
|
graph.add_element(make_element("BillingService", Some("Billing")));
|
|
|
|
graph.add_relationship(
|
|
Relationship::new("OrderService", "Order", RelationshipKind::Composition).unwrap(),
|
|
);
|
|
graph.add_relationship(
|
|
Relationship::new(
|
|
"OrderService",
|
|
"BillingService",
|
|
RelationshipKind::Composition,
|
|
)
|
|
.unwrap(),
|
|
);
|
|
|
|
let module = ModuleName::new("Orders").unwrap();
|
|
let subgraph = graph.subgraph_by_module(&module);
|
|
|
|
assert_eq!(subgraph.relationships().len(), 1);
|
|
assert_eq!(subgraph.relationships()[0].target(), "Order");
|
|
}
|
|
|
|
#[test]
|
|
fn subgraph_of_nonexistent_module_is_empty() {
|
|
let mut graph = CodeGraph::new();
|
|
graph.add_element(make_element("OrderService", Some("Orders")));
|
|
|
|
let module = ModuleName::new("Unknown").unwrap();
|
|
let subgraph = graph.subgraph_by_module(&module);
|
|
|
|
assert!(subgraph.elements().is_empty());
|
|
assert!(subgraph.relationships().is_empty());
|
|
}
|
|
|
|
#[test]
|
|
fn qualify_sets_qualified_name_on_elements_with_modules() {
|
|
let mut graph = CodeGraph::new();
|
|
graph.add_element(make_element("DtoBaseModel", Some("Commons")));
|
|
graph.add_element(make_element("DtoBaseModel", Some("Api")));
|
|
graph.add_element(make_element("Orphan", None));
|
|
|
|
let graph = graph.qualify();
|
|
|
|
let commons_dto = graph
|
|
.elements()
|
|
.iter()
|
|
.find(|e| e.module().map(|m| m.as_str()) == Some("Commons"))
|
|
.unwrap();
|
|
assert_eq!(commons_dto.qualified_name(), "Commons::DtoBaseModel");
|
|
|
|
let api_dto = graph
|
|
.elements()
|
|
.iter()
|
|
.find(|e| e.module().map(|m| m.as_str()) == Some("Api"))
|
|
.unwrap();
|
|
assert_eq!(api_dto.qualified_name(), "Api::DtoBaseModel");
|
|
|
|
let orphan = graph
|
|
.elements()
|
|
.iter()
|
|
.find(|e| e.name() == "Orphan")
|
|
.unwrap();
|
|
assert_eq!(orphan.qualified_name(), "Orphan");
|
|
}
|
|
|
|
#[test]
|
|
fn qualify_rewrites_unambiguous_relationship_target() {
|
|
let mut graph = CodeGraph::new();
|
|
graph.add_element(make_element("OrderService", Some("App")));
|
|
graph.add_element(make_element("Order", Some("Domain")));
|
|
graph.add_relationship(
|
|
Relationship::new("OrderService", "Order", RelationshipKind::Composition).unwrap(),
|
|
);
|
|
|
|
let graph = graph.qualify();
|
|
|
|
let rel = &graph.relationships()[0];
|
|
assert_eq!(rel.source(), "App::OrderService");
|
|
assert_eq!(rel.target(), "Domain::Order");
|
|
}
|
|
|
|
#[test]
|
|
fn qualify_disambiguates_target_by_source_module() {
|
|
let mut graph = CodeGraph::new();
|
|
// DtoBaseModel exists in both Commons and Api
|
|
graph.add_element(make_element("DtoBaseModel", Some("Commons")));
|
|
graph.add_element(make_element("DtoBaseModel", Some("Api")));
|
|
// GlobalAudienceDefinition inherits DtoBaseModel, and is in Commons
|
|
graph.add_element(make_element("GlobalAudienceDefinition", Some("Commons")));
|
|
|
|
let mut rel = Relationship::new(
|
|
"GlobalAudienceDefinition",
|
|
"DtoBaseModel",
|
|
RelationshipKind::Inheritance,
|
|
)
|
|
.unwrap();
|
|
// source_file is in the Commons module path
|
|
rel = rel.with_source_file(
|
|
archlens_domain::FilePath::new("src/commons/global_audience.rs").unwrap(),
|
|
);
|
|
let gad = CodeElement::new(
|
|
"GlobalAudienceDefinition",
|
|
archlens_domain::CodeElementKind::Class,
|
|
archlens_domain::FilePath::new("src/commons/global_audience.rs").unwrap(),
|
|
1,
|
|
)
|
|
.unwrap()
|
|
.with_module(archlens_domain::ModuleName::new("Commons").unwrap());
|
|
|
|
let mut graph = CodeGraph::new();
|
|
graph.add_element(make_element("DtoBaseModel", Some("Commons")));
|
|
graph.add_element(make_element("DtoBaseModel", Some("Api")));
|
|
graph.add_element(gad);
|
|
graph.add_relationship(rel);
|
|
|
|
let graph = graph.qualify();
|
|
|
|
let rel = &graph.relationships()[0];
|
|
assert_eq!(rel.source(), "Commons::GlobalAudienceDefinition");
|
|
assert_eq!(rel.target(), "Commons::DtoBaseModel");
|
|
}
|
|
|
|
#[test]
|
|
fn resolve_preserves_relationship_when_both_qualified_names_exist() {
|
|
let mut graph = CodeGraph::new();
|
|
graph.add_element(make_element("GlobalAudienceDefinition", Some("Commons")));
|
|
graph.add_element(make_element("DtoBaseModel", Some("Commons")));
|
|
graph.add_element(make_element("DtoBaseModel", Some("Api")));
|
|
|
|
graph.add_relationship(
|
|
Relationship::new(
|
|
"GlobalAudienceDefinition",
|
|
"DtoBaseModel",
|
|
RelationshipKind::Inheritance,
|
|
)
|
|
.unwrap(),
|
|
);
|
|
|
|
let graph = graph.qualify().resolve_relationships();
|
|
|
|
// The relationship should survive — Commons::GlobalAudienceDefinition --> Commons::DtoBaseModel
|
|
assert_eq!(graph.relationships().len(), 1);
|
|
assert_eq!(
|
|
graph.relationships()[0].source(),
|
|
"Commons::GlobalAudienceDefinition"
|
|
);
|
|
assert_eq!(graph.relationships()[0].target(), "Commons::DtoBaseModel");
|
|
}
|
|
|
|
#[test]
|
|
fn cross_module_deps_for_returns_target_module_with_count() {
|
|
let mut graph = CodeGraph::new();
|
|
graph.add_element(make_element("WidgetJobData", Some("aiss_worker")));
|
|
graph.add_element(make_element("WidgetType", Some("commons")));
|
|
|
|
graph.add_relationship(
|
|
Relationship::new("WidgetJobData", "WidgetType", RelationshipKind::Composition).unwrap(),
|
|
);
|
|
|
|
let module = ModuleName::new("aiss_worker").unwrap();
|
|
let deps = graph.cross_module_deps_for(&module);
|
|
|
|
assert_eq!(deps.len(), 1);
|
|
assert_eq!(deps[0].0.as_str(), "commons");
|
|
assert_eq!(deps[0].1, 1);
|
|
}
|
|
|
|
#[test]
|
|
fn cross_module_deps_for_returns_empty_for_intra_module_only() {
|
|
let mut graph = CodeGraph::new();
|
|
graph.add_element(make_element("OrderService", Some("Orders")));
|
|
graph.add_element(make_element("Order", Some("Orders")));
|
|
graph.add_relationship(
|
|
Relationship::new("OrderService", "Order", RelationshipKind::Composition).unwrap(),
|
|
);
|
|
|
|
let module = ModuleName::new("Orders").unwrap();
|
|
let deps = graph.cross_module_deps_for(&module);
|
|
|
|
assert!(deps.is_empty());
|
|
}
|
|
|
|
#[test]
|
|
fn cross_module_deps_for_aggregates_multiple_relationships_to_same_module() {
|
|
let mut graph = CodeGraph::new();
|
|
graph.add_element(make_element("ServiceA", Some("app")));
|
|
graph.add_element(make_element("ServiceB", Some("app")));
|
|
graph.add_element(make_element("DomainType1", Some("domain")));
|
|
graph.add_element(make_element("DomainType2", Some("domain")));
|
|
|
|
graph.add_relationship(
|
|
Relationship::new("ServiceA", "DomainType1", RelationshipKind::Composition).unwrap(),
|
|
);
|
|
graph.add_relationship(
|
|
Relationship::new("ServiceB", "DomainType2", RelationshipKind::Composition).unwrap(),
|
|
);
|
|
|
|
let module = ModuleName::new("app").unwrap();
|
|
let deps = graph.cross_module_deps_for(&module);
|
|
|
|
assert_eq!(deps.len(), 1);
|
|
assert_eq!(deps[0].0.as_str(), "domain");
|
|
assert_eq!(deps[0].1, 2);
|
|
}
|
|
|
|
#[test]
|
|
fn graph_lists_unique_modules() {
|
|
let mut graph = CodeGraph::new();
|
|
graph.add_element(make_element("OrderService", Some("Orders")));
|
|
graph.add_element(make_element("Order", Some("Orders")));
|
|
graph.add_element(make_element("BillingService", Some("Billing")));
|
|
graph.add_element(make_element("Orphan", None));
|
|
|
|
let modules = graph.modules();
|
|
assert_eq!(modules.len(), 2);
|
|
assert!(modules.iter().any(|m| m.as_str() == "Orders"));
|
|
assert!(modules.iter().any(|m| m.as_str() == "Billing"));
|
|
}
|
|
|
|
#[test]
|
|
fn module_edges_aggregates_type_level_relationships_into_module_pairs() {
|
|
let mut graph = CodeGraph::new();
|
|
graph.add_element(make_element("ServiceA", Some("App")));
|
|
graph.add_element(make_element("ServiceB", Some("App")));
|
|
graph.add_element(make_element("Order", Some("Domain")));
|
|
graph.add_element(make_element("Product", Some("Domain")));
|
|
|
|
graph.add_relationship(
|
|
Relationship::new("ServiceA", "Order", RelationshipKind::Composition).unwrap(),
|
|
);
|
|
graph.add_relationship(
|
|
Relationship::new("ServiceB", "Product", RelationshipKind::Composition).unwrap(),
|
|
);
|
|
|
|
let graph = graph.qualify();
|
|
let edges = graph.module_edges();
|
|
|
|
assert_eq!(edges.len(), 1, "should have one cross-module edge pair");
|
|
assert_eq!(edges[&("App".to_string(), "Domain".to_string())], 2);
|
|
}
|
|
|
|
#[test]
|
|
fn module_edges_handles_direct_module_to_module_relationships() {
|
|
let mut graph = CodeGraph::new();
|
|
graph.add_element(make_element("Order", Some("Domain")));
|
|
graph.add_element(make_element("Service", Some("App")));
|
|
// Direct module-name edge (injected by merge_project_deps)
|
|
graph.add_relationship(
|
|
Relationship::new("App", "Domain", RelationshipKind::Composition).unwrap(),
|
|
);
|
|
|
|
let graph = graph.qualify();
|
|
let edges = graph.module_edges();
|
|
|
|
assert!(
|
|
edges.contains_key(&("App".to_string(), "Domain".to_string())),
|
|
"direct module edge should appear: {edges:?}"
|
|
);
|
|
}
|
|
|
|
#[test]
|
|
fn module_edges_excludes_intra_module_relationships() {
|
|
let mut graph = CodeGraph::new();
|
|
graph.add_element(make_element("Service", Some("App")));
|
|
graph.add_element(make_element("Helper", Some("App")));
|
|
graph.add_relationship(
|
|
Relationship::new("Service", "Helper", RelationshipKind::Composition).unwrap(),
|
|
);
|
|
|
|
let graph = graph.qualify();
|
|
let edges = graph.module_edges();
|
|
|
|
assert!(edges.is_empty(), "intra-module relationships should not produce edges");
|
|
}
|