419 lines
12 KiB
Rust
419 lines
12 KiB
Rust
use archlens_domain::{
|
|
CodeElement, CodeElementKind, CodeGraph, DiagramLevel, FilePath, ModuleName, Relationship,
|
|
RelationshipKind, Visibility, ports::DiagramRenderer,
|
|
};
|
|
use archlens_mermaid::MermaidRenderer;
|
|
|
|
fn build_type_level_graph() -> CodeGraph {
|
|
let mut graph = CodeGraph::new();
|
|
graph.add_element(
|
|
CodeElement::new(
|
|
"OrderService",
|
|
CodeElementKind::Class,
|
|
FilePath::new("src/service.rs").unwrap(),
|
|
1,
|
|
)
|
|
.unwrap(),
|
|
);
|
|
graph.add_element(
|
|
CodeElement::new(
|
|
"Order",
|
|
CodeElementKind::Struct,
|
|
FilePath::new("src/order.rs").unwrap(),
|
|
1,
|
|
)
|
|
.unwrap(),
|
|
);
|
|
graph.add_relationship(
|
|
Relationship::new("OrderService", "Order", RelationshipKind::Composition).unwrap(),
|
|
);
|
|
graph
|
|
}
|
|
|
|
#[test]
|
|
fn renders_class_diagram_with_elements_and_composition() {
|
|
let renderer = MermaidRenderer::new();
|
|
let output = renderer.render(&build_type_level_graph()).unwrap();
|
|
|
|
let content = output.files()[0].content();
|
|
assert!(content.contains("classDiagram"));
|
|
assert!(content.contains("class OrderService"));
|
|
assert!(content.contains("class Order"));
|
|
assert!(content.contains("OrderService --> Order"));
|
|
}
|
|
|
|
#[test]
|
|
fn inheritance_uses_different_arrow() {
|
|
let mut graph = CodeGraph::new();
|
|
graph.add_element(
|
|
CodeElement::new(
|
|
"Animal",
|
|
CodeElementKind::Class,
|
|
FilePath::new("src/animal.rs").unwrap(),
|
|
1,
|
|
)
|
|
.unwrap(),
|
|
);
|
|
graph.add_element(
|
|
CodeElement::new(
|
|
"Dog",
|
|
CodeElementKind::Class,
|
|
FilePath::new("src/dog.rs").unwrap(),
|
|
1,
|
|
)
|
|
.unwrap(),
|
|
);
|
|
graph.add_relationship(
|
|
Relationship::new("Dog", "Animal", RelationshipKind::Inheritance).unwrap(),
|
|
);
|
|
|
|
let renderer = MermaidRenderer::new();
|
|
let output = renderer.render(&graph).unwrap();
|
|
let content = output.files()[0].content();
|
|
|
|
assert!(content.contains("Dog <|-- Animal"));
|
|
}
|
|
|
|
#[test]
|
|
fn elements_show_kind_and_generics() {
|
|
let mut graph = CodeGraph::new();
|
|
graph.add_element(
|
|
CodeElement::new(
|
|
"Repository",
|
|
CodeElementKind::Trait,
|
|
FilePath::new("src/repo.rs").unwrap(),
|
|
1,
|
|
)
|
|
.unwrap()
|
|
.with_generics(vec!["T".to_string()]),
|
|
);
|
|
|
|
let renderer = MermaidRenderer::new();
|
|
let output = renderer.render(&graph).unwrap();
|
|
let content = output.files()[0].content();
|
|
|
|
assert!(content.contains("class Repository~T~"));
|
|
}
|
|
|
|
#[test]
|
|
fn private_elements_show_visibility_annotation() {
|
|
let mut graph = CodeGraph::new();
|
|
graph.add_element(
|
|
CodeElement::new(
|
|
"InternalHelper",
|
|
CodeElementKind::Class,
|
|
FilePath::new("src/helper.rs").unwrap(),
|
|
1,
|
|
)
|
|
.unwrap()
|
|
.with_visibility(Visibility::Private),
|
|
);
|
|
|
|
let renderer = MermaidRenderer::new();
|
|
let output = renderer.render(&graph).unwrap();
|
|
let content = output.files()[0].content();
|
|
|
|
assert!(content.contains("class InternalHelper"));
|
|
assert!(content.contains("<<private>>"));
|
|
}
|
|
|
|
#[test]
|
|
fn renders_module_level_flowchart() {
|
|
let mut graph = CodeGraph::new();
|
|
graph.add_element(
|
|
CodeElement::new(
|
|
"OrderService",
|
|
CodeElementKind::Class,
|
|
FilePath::new("src/orders/service.rs").unwrap(),
|
|
1,
|
|
)
|
|
.unwrap()
|
|
.with_module(ModuleName::new("Orders").unwrap()),
|
|
);
|
|
graph.add_element(
|
|
CodeElement::new(
|
|
"BillingService",
|
|
CodeElementKind::Class,
|
|
FilePath::new("src/billing/service.rs").unwrap(),
|
|
1,
|
|
)
|
|
.unwrap()
|
|
.with_module(ModuleName::new("Billing").unwrap()),
|
|
);
|
|
graph.add_relationship(
|
|
Relationship::new(
|
|
"OrderService",
|
|
"BillingService",
|
|
RelationshipKind::Composition,
|
|
)
|
|
.unwrap(),
|
|
);
|
|
|
|
let renderer = MermaidRenderer::with_level(DiagramLevel::Module);
|
|
let output = renderer.render(&graph).unwrap();
|
|
let content = output.files()[0].content();
|
|
|
|
assert!(content.contains("graph TD"));
|
|
assert!(content.contains("Orders"));
|
|
assert!(content.contains("Billing"));
|
|
assert!(content.contains("Orders --") && content.contains("Billing"));
|
|
}
|
|
|
|
#[test]
|
|
fn empty_graph_produces_valid_diagram() {
|
|
let renderer = MermaidRenderer::new();
|
|
let output = renderer.render(&CodeGraph::new()).unwrap();
|
|
let content = output.files()[0].content();
|
|
|
|
assert!(content.contains("classDiagram"));
|
|
}
|
|
|
|
#[test]
|
|
fn project_level_renders_subgraphs_for_grouped_projects() {
|
|
let mut graph = CodeGraph::new();
|
|
graph.add_element(
|
|
CodeElement::new(
|
|
"myapp-domain",
|
|
CodeElementKind::Project,
|
|
FilePath::new("crates/domain/Cargo.toml").unwrap(),
|
|
1,
|
|
)
|
|
.unwrap(),
|
|
);
|
|
graph.add_element(
|
|
CodeElement::new(
|
|
"myapp-sqlite",
|
|
CodeElementKind::Project,
|
|
FilePath::new("crates/adapters/sqlite/Cargo.toml").unwrap(),
|
|
1,
|
|
)
|
|
.unwrap()
|
|
.with_module(ModuleName::new("Adapters").unwrap()),
|
|
);
|
|
graph.add_element(
|
|
CodeElement::new(
|
|
"myapp-nats",
|
|
CodeElementKind::Project,
|
|
FilePath::new("crates/adapters/nats/Cargo.toml").unwrap(),
|
|
1,
|
|
)
|
|
.unwrap()
|
|
.with_module(ModuleName::new("Adapters").unwrap()),
|
|
);
|
|
graph.add_relationship(
|
|
Relationship::new(
|
|
"myapp-sqlite",
|
|
"myapp-domain",
|
|
RelationshipKind::Composition,
|
|
)
|
|
.unwrap(),
|
|
);
|
|
|
|
let renderer = MermaidRenderer::with_level(DiagramLevel::Project);
|
|
let output = renderer.render(&graph).unwrap();
|
|
let content = output.files()[0].content();
|
|
|
|
assert!(content.contains("graph TD"));
|
|
assert!(content.contains("subgraph Adapters"));
|
|
assert!(content.contains("myapp-sqlite"));
|
|
assert!(content.contains("myapp-nats"));
|
|
assert!(content.contains("myapp-domain"));
|
|
assert!(content.contains("-->"));
|
|
}
|
|
|
|
#[test]
|
|
fn type_level_groups_elements_by_module() {
|
|
let mut graph = CodeGraph::new();
|
|
graph.add_element(
|
|
CodeElement::new(
|
|
"OrderService",
|
|
CodeElementKind::Class,
|
|
FilePath::new("src/orders/service.rs").unwrap(),
|
|
1,
|
|
)
|
|
.unwrap()
|
|
.with_module(ModuleName::new("Orders").unwrap()),
|
|
);
|
|
graph.add_element(
|
|
CodeElement::new(
|
|
"Order",
|
|
CodeElementKind::Struct,
|
|
FilePath::new("src/orders/order.rs").unwrap(),
|
|
1,
|
|
)
|
|
.unwrap()
|
|
.with_module(ModuleName::new("Orders").unwrap()),
|
|
);
|
|
graph.add_element(
|
|
CodeElement::new(
|
|
"Invoice",
|
|
CodeElementKind::Struct,
|
|
FilePath::new("src/billing/invoice.rs").unwrap(),
|
|
1,
|
|
)
|
|
.unwrap()
|
|
.with_module(ModuleName::new("Billing").unwrap()),
|
|
);
|
|
|
|
let renderer = MermaidRenderer::new();
|
|
let output = renderer.render(&graph).unwrap();
|
|
let content = output.files()[0].content();
|
|
|
|
assert!(content.contains("namespace Orders"));
|
|
assert!(content.contains("namespace Billing"));
|
|
assert!(content.contains("OrderService"));
|
|
assert!(content.contains("Invoice"));
|
|
}
|
|
|
|
#[test]
|
|
fn module_level_aggregates_cross_module_deps_into_single_arrow() {
|
|
let mut graph = CodeGraph::new();
|
|
graph.add_element(
|
|
CodeElement::new(
|
|
"OrderService",
|
|
CodeElementKind::Class,
|
|
FilePath::new("src/orders/service.rs").unwrap(),
|
|
1,
|
|
)
|
|
.unwrap()
|
|
.with_module(ModuleName::new("Orders").unwrap()),
|
|
);
|
|
graph.add_element(
|
|
CodeElement::new(
|
|
"OrderRepo",
|
|
CodeElementKind::Trait,
|
|
FilePath::new("src/orders/repo.rs").unwrap(),
|
|
1,
|
|
)
|
|
.unwrap()
|
|
.with_module(ModuleName::new("Orders").unwrap()),
|
|
);
|
|
graph.add_element(
|
|
CodeElement::new(
|
|
"DbPool",
|
|
CodeElementKind::Struct,
|
|
FilePath::new("src/infra/db.rs").unwrap(),
|
|
1,
|
|
)
|
|
.unwrap()
|
|
.with_module(ModuleName::new("Infra").unwrap()),
|
|
);
|
|
graph.add_element(
|
|
CodeElement::new(
|
|
"SqliteRepo",
|
|
CodeElementKind::Struct,
|
|
FilePath::new("src/infra/sqlite.rs").unwrap(),
|
|
1,
|
|
)
|
|
.unwrap()
|
|
.with_module(ModuleName::new("Infra").unwrap()),
|
|
);
|
|
// Two types in Orders depend on two types in Infra
|
|
graph.add_relationship(
|
|
Relationship::new("OrderService", "DbPool", RelationshipKind::Composition).unwrap(),
|
|
);
|
|
graph.add_relationship(
|
|
Relationship::new("OrderRepo", "SqliteRepo", RelationshipKind::Composition).unwrap(),
|
|
);
|
|
|
|
let renderer = MermaidRenderer::with_level(DiagramLevel::Module);
|
|
let output = renderer.render(&graph).unwrap();
|
|
let content = output.files()[0].content();
|
|
|
|
let arrow_count =
|
|
content.matches("Orders --> Infra").count() + content.matches("Orders -->|").count();
|
|
assert_eq!(
|
|
arrow_count, 1,
|
|
"should have exactly one aggregated arrow, got:\n{content}"
|
|
);
|
|
}
|
|
|
|
#[test]
|
|
fn module_level_shows_dep_count_as_edge_label() {
|
|
let mut graph = CodeGraph::new();
|
|
graph.add_element(
|
|
CodeElement::new(
|
|
"ServiceA",
|
|
CodeElementKind::Class,
|
|
FilePath::new("src/app/a.rs").unwrap(),
|
|
1,
|
|
)
|
|
.unwrap()
|
|
.with_module(ModuleName::new("App").unwrap()),
|
|
);
|
|
graph.add_element(
|
|
CodeElement::new(
|
|
"ServiceB",
|
|
CodeElementKind::Class,
|
|
FilePath::new("src/app/b.rs").unwrap(),
|
|
1,
|
|
)
|
|
.unwrap()
|
|
.with_module(ModuleName::new("App").unwrap()),
|
|
);
|
|
graph.add_element(
|
|
CodeElement::new(
|
|
"Order",
|
|
CodeElementKind::Class,
|
|
FilePath::new("src/domain/order.rs").unwrap(),
|
|
1,
|
|
)
|
|
.unwrap()
|
|
.with_module(ModuleName::new("Domain").unwrap()),
|
|
);
|
|
graph.add_relationship(
|
|
Relationship::new("ServiceA", "Order", RelationshipKind::Composition).unwrap(),
|
|
);
|
|
graph.add_relationship(
|
|
Relationship::new("ServiceB", "Order", RelationshipKind::Composition).unwrap(),
|
|
);
|
|
let graph = graph.qualify();
|
|
|
|
let renderer = MermaidRenderer::with_level(DiagramLevel::Module);
|
|
let output = renderer.render(&graph).unwrap();
|
|
let content = output.files()[0].content();
|
|
|
|
assert!(
|
|
content.contains("|2 deps|"),
|
|
"expected dep count label in: {content}"
|
|
);
|
|
}
|
|
|
|
#[test]
|
|
fn module_level_single_dep_uses_singular_label() {
|
|
let mut graph = CodeGraph::new();
|
|
graph.add_element(
|
|
CodeElement::new(
|
|
"Service",
|
|
CodeElementKind::Class,
|
|
FilePath::new("src/app/s.rs").unwrap(),
|
|
1,
|
|
)
|
|
.unwrap()
|
|
.with_module(ModuleName::new("App").unwrap()),
|
|
);
|
|
graph.add_element(
|
|
CodeElement::new(
|
|
"Order",
|
|
CodeElementKind::Class,
|
|
FilePath::new("src/domain/o.rs").unwrap(),
|
|
1,
|
|
)
|
|
.unwrap()
|
|
.with_module(ModuleName::new("Domain").unwrap()),
|
|
);
|
|
graph.add_relationship(
|
|
Relationship::new("Service", "Order", RelationshipKind::Composition).unwrap(),
|
|
);
|
|
let graph = graph.qualify();
|
|
|
|
let renderer = MermaidRenderer::with_level(DiagramLevel::Module);
|
|
let output = renderer.render(&graph).unwrap();
|
|
let content = output.files()[0].content();
|
|
|
|
assert!(
|
|
content.contains("|1 dep|"),
|
|
"expected singular dep label in: {content}"
|
|
);
|
|
}
|