init: archlens — architecture diagram generator
Some checks failed
CI / Check / Test (push) Failing after 1m24s
Some checks failed
CI / Check / Test (push) Failing after 1m24s
Hex arch + DDD, tree-sitter parsing, Mermaid/ASCII output. Supports Rust + Python. 92 tests. CI, diff, --check for staleness detection.
This commit is contained in:
39
crates/domain/tests/analysis_result_tests.rs
Normal file
39
crates/domain/tests/analysis_result_tests.rs
Normal file
@@ -0,0 +1,39 @@
|
||||
use archlens_domain::{
|
||||
AnalysisResult, AnalysisWarning, CodeElement, CodeElementKind, FilePath, Relationship,
|
||||
RelationshipKind,
|
||||
};
|
||||
|
||||
#[test]
|
||||
fn analysis_result_collects_elements_relationships_and_warnings() {
|
||||
let element = CodeElement::new(
|
||||
"Order",
|
||||
CodeElementKind::Struct,
|
||||
FilePath::new("src/order.rs").unwrap(),
|
||||
1,
|
||||
)
|
||||
.unwrap();
|
||||
|
||||
let relationship =
|
||||
Relationship::new("OrderService", "Order", RelationshipKind::Composition).unwrap();
|
||||
|
||||
let warning = AnalysisWarning::new(
|
||||
FilePath::new("src/broken.rs").unwrap(),
|
||||
10,
|
||||
"unparseable macro",
|
||||
)
|
||||
.unwrap();
|
||||
|
||||
let result = AnalysisResult::new(vec![element], vec![relationship], vec![warning]);
|
||||
|
||||
assert_eq!(result.elements().len(), 1);
|
||||
assert_eq!(result.relationships().len(), 1);
|
||||
assert_eq!(result.warnings().len(), 1);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn empty_analysis_result() {
|
||||
let result = AnalysisResult::empty();
|
||||
assert!(result.elements().is_empty());
|
||||
assert!(result.relationships().is_empty());
|
||||
assert!(result.warnings().is_empty());
|
||||
}
|
||||
21
crates/domain/tests/analysis_warning_tests.rs
Normal file
21
crates/domain/tests/analysis_warning_tests.rs
Normal file
@@ -0,0 +1,21 @@
|
||||
use archlens_domain::{AnalysisWarning, FilePath};
|
||||
|
||||
#[test]
|
||||
fn warning_carries_location_and_message() {
|
||||
let warning = AnalysisWarning::new(
|
||||
FilePath::new("src/broken.rs").unwrap(),
|
||||
42,
|
||||
"could not parse struct definition",
|
||||
)
|
||||
.unwrap();
|
||||
|
||||
assert_eq!(warning.file_path().as_str(), "src/broken.rs");
|
||||
assert_eq!(warning.line(), 42);
|
||||
assert_eq!(warning.message(), "could not parse struct definition");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn warning_rejects_empty_message() {
|
||||
let result = AnalysisWarning::new(FilePath::new("src/broken.rs").unwrap(), 1, "");
|
||||
assert!(result.is_err());
|
||||
}
|
||||
107
crates/domain/tests/code_element_tests.rs
Normal file
107
crates/domain/tests/code_element_tests.rs
Normal file
@@ -0,0 +1,107 @@
|
||||
use archlens_domain::{CodeElement, CodeElementKind, FilePath, ModuleName, Visibility};
|
||||
|
||||
#[test]
|
||||
fn code_element_is_created_with_required_fields() {
|
||||
let element = CodeElement::new(
|
||||
"OrderService",
|
||||
CodeElementKind::Class,
|
||||
FilePath::new("src/orders/service.rs").unwrap(),
|
||||
42,
|
||||
)
|
||||
.unwrap();
|
||||
|
||||
assert_eq!(element.name(), "OrderService");
|
||||
assert_eq!(element.kind(), CodeElementKind::Class);
|
||||
assert_eq!(element.file_path().as_str(), "src/orders/service.rs");
|
||||
assert_eq!(element.line(), 42);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn code_element_with_empty_name_is_rejected() {
|
||||
let result = CodeElement::new(
|
||||
"",
|
||||
CodeElementKind::Class,
|
||||
FilePath::new("src/main.rs").unwrap(),
|
||||
1,
|
||||
);
|
||||
assert!(result.is_err());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn code_element_defaults_to_public_visibility() {
|
||||
let element = CodeElement::new(
|
||||
"Order",
|
||||
CodeElementKind::Struct,
|
||||
FilePath::new("src/order.rs").unwrap(),
|
||||
1,
|
||||
)
|
||||
.unwrap();
|
||||
|
||||
assert_eq!(element.visibility(), Visibility::Public);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn code_element_with_visibility() {
|
||||
let element = CodeElement::new(
|
||||
"Order",
|
||||
CodeElementKind::Struct,
|
||||
FilePath::new("src/order.rs").unwrap(),
|
||||
1,
|
||||
)
|
||||
.unwrap()
|
||||
.with_visibility(Visibility::Private);
|
||||
|
||||
assert_eq!(element.visibility(), Visibility::Private);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn code_element_with_module_path() {
|
||||
let module = ModuleName::new("Orders").unwrap();
|
||||
let element = CodeElement::new(
|
||||
"Order",
|
||||
CodeElementKind::Struct,
|
||||
FilePath::new("src/orders/order.rs").unwrap(),
|
||||
1,
|
||||
)
|
||||
.unwrap()
|
||||
.with_module(module.clone());
|
||||
|
||||
assert_eq!(element.module(), Some(&module));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn code_element_with_generics() {
|
||||
let element = CodeElement::new(
|
||||
"Repository",
|
||||
CodeElementKind::Trait,
|
||||
FilePath::new("src/repo.rs").unwrap(),
|
||||
1,
|
||||
)
|
||||
.unwrap()
|
||||
.with_generics(vec!["T".to_string()]);
|
||||
|
||||
assert_eq!(element.generics(), &["T"]);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn code_element_with_attributes() {
|
||||
let element = CodeElement::new(
|
||||
"OrderController",
|
||||
CodeElementKind::Class,
|
||||
FilePath::new("src/controller.cs").unwrap(),
|
||||
1,
|
||||
)
|
||||
.unwrap()
|
||||
.with_attributes(vec!["ApiController".to_string()]);
|
||||
|
||||
assert_eq!(element.attributes(), &["ApiController"]);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn all_element_kinds_exist() {
|
||||
let _class = CodeElementKind::Class;
|
||||
let _struct = CodeElementKind::Struct;
|
||||
let _trait = CodeElementKind::Trait;
|
||||
let _interface = CodeElementKind::Interface;
|
||||
let _enum = CodeElementKind::Enum;
|
||||
}
|
||||
130
crates/domain/tests/code_graph_tests.rs
Normal file
130
crates/domain/tests/code_graph_tests.rs
Normal file
@@ -0,0 +1,130 @@
|
||||
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 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"));
|
||||
}
|
||||
37
crates/domain/tests/config_tests.rs
Normal file
37
crates/domain/tests/config_tests.rs
Normal file
@@ -0,0 +1,37 @@
|
||||
use archlens_domain::{AnalysisConfig, DiagramLevel, OutputConfig};
|
||||
|
||||
#[test]
|
||||
fn analysis_config_has_sensible_defaults() {
|
||||
let config = AnalysisConfig::default();
|
||||
assert!(config.excludes().is_empty());
|
||||
assert_eq!(config.level(), DiagramLevel::Module);
|
||||
assert!(config.module_mappings().is_empty());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn analysis_config_with_excludes() {
|
||||
let config =
|
||||
AnalysisConfig::default().with_excludes(vec!["tests/".to_string(), "vendor/".to_string()]);
|
||||
|
||||
assert_eq!(config.excludes().len(), 2);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn output_config_has_sensible_defaults() {
|
||||
let config = OutputConfig::default();
|
||||
assert!(!config.split_by_module());
|
||||
assert!(config.output_path().is_none());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn output_config_with_split() {
|
||||
let config = OutputConfig::default().with_split_by_module(true);
|
||||
assert!(config.split_by_module());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn all_diagram_levels_exist() {
|
||||
let _project = DiagramLevel::Project;
|
||||
let _module = DiagramLevel::Module;
|
||||
let _type_level = DiagramLevel::Type;
|
||||
}
|
||||
29
crates/domain/tests/file_path_tests.rs
Normal file
29
crates/domain/tests/file_path_tests.rs
Normal file
@@ -0,0 +1,29 @@
|
||||
use archlens_domain::FilePath;
|
||||
|
||||
#[test]
|
||||
fn valid_file_path_is_created() {
|
||||
let path = FilePath::new("src/main.rs").unwrap();
|
||||
assert_eq!(path.as_str(), "src/main.rs");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn empty_file_path_is_rejected() {
|
||||
let result = FilePath::new("");
|
||||
assert!(result.is_err());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn whitespace_only_file_path_is_rejected() {
|
||||
let result = FilePath::new(" ");
|
||||
assert!(result.is_err());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn file_paths_are_comparable() {
|
||||
let a = FilePath::new("src/main.rs").unwrap();
|
||||
let b = FilePath::new("src/main.rs").unwrap();
|
||||
let c = FilePath::new("src/lib.rs").unwrap();
|
||||
|
||||
assert_eq!(a, b);
|
||||
assert_ne!(a, c);
|
||||
}
|
||||
18
crates/domain/tests/language_tests.rs
Normal file
18
crates/domain/tests/language_tests.rs
Normal file
@@ -0,0 +1,18 @@
|
||||
use archlens_domain::Language;
|
||||
|
||||
#[test]
|
||||
fn known_languages_are_available() {
|
||||
let rust = Language::Rust;
|
||||
let csharp = Language::CSharp;
|
||||
let python = Language::Python;
|
||||
|
||||
assert_eq!(rust.name(), "Rust");
|
||||
assert_eq!(csharp.name(), "CSharp");
|
||||
assert_eq!(python.name(), "Python");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn languages_are_comparable() {
|
||||
assert_eq!(Language::Rust, Language::Rust);
|
||||
assert_ne!(Language::Rust, Language::Python);
|
||||
}
|
||||
23
crates/domain/tests/module_name_tests.rs
Normal file
23
crates/domain/tests/module_name_tests.rs
Normal file
@@ -0,0 +1,23 @@
|
||||
use archlens_domain::ModuleName;
|
||||
|
||||
#[test]
|
||||
fn valid_module_name_is_created() {
|
||||
let name = ModuleName::new("Orders").unwrap();
|
||||
assert_eq!(name.as_str(), "Orders");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn empty_module_name_is_rejected() {
|
||||
let result = ModuleName::new("");
|
||||
assert!(result.is_err());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn module_names_are_comparable() {
|
||||
let a = ModuleName::new("Orders").unwrap();
|
||||
let b = ModuleName::new("Orders").unwrap();
|
||||
let c = ModuleName::new("Billing").unwrap();
|
||||
|
||||
assert_eq!(a, b);
|
||||
assert_ne!(a, c);
|
||||
}
|
||||
37
crates/domain/tests/render_output_tests.rs
Normal file
37
crates/domain/tests/render_output_tests.rs
Normal file
@@ -0,0 +1,37 @@
|
||||
use archlens_domain::{RenderOutput, RenderedFile};
|
||||
|
||||
#[test]
|
||||
fn rendered_file_carries_name_and_content() {
|
||||
let file = RenderedFile::new("overview.mmd", "graph TD;").unwrap();
|
||||
assert_eq!(file.name(), "overview.mmd");
|
||||
assert_eq!(file.content(), "graph TD;");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn rendered_file_rejects_empty_name() {
|
||||
let result = RenderedFile::new("", "content");
|
||||
assert!(result.is_err());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn rendered_file_rejects_empty_content() {
|
||||
let result = RenderedFile::new("file.mmd", "");
|
||||
assert!(result.is_err());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn render_output_holds_multiple_files() {
|
||||
let files = vec![
|
||||
RenderedFile::new("overview.mmd", "graph TD;").unwrap(),
|
||||
RenderedFile::new("orders.mmd", "classDiagram").unwrap(),
|
||||
];
|
||||
let output = RenderOutput::new(files);
|
||||
assert_eq!(output.files().len(), 2);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn render_output_can_be_single_file() {
|
||||
let file = RenderedFile::new("arch.mmd", "graph TD;").unwrap();
|
||||
let output = RenderOutput::single(file);
|
||||
assert_eq!(output.files().len(), 1);
|
||||
}
|
||||
10
crates/domain/tests/source_file_tests.rs
Normal file
10
crates/domain/tests/source_file_tests.rs
Normal file
@@ -0,0 +1,10 @@
|
||||
use archlens_domain::{FilePath, Language, SourceFile};
|
||||
|
||||
#[test]
|
||||
fn source_file_carries_path_and_language() {
|
||||
let path = FilePath::new("src/main.rs").unwrap();
|
||||
let file = SourceFile::new(path.clone(), Language::Rust);
|
||||
|
||||
assert_eq!(file.path(), &path);
|
||||
assert_eq!(file.language(), Language::Rust);
|
||||
}
|
||||
Reference in New Issue
Block a user