init: archlens — architecture diagram generator
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:
2026-06-16 16:13:04 +02:00
commit 35f27d00b0
106 changed files with 6744 additions and 0 deletions

View File

@@ -0,0 +1,344 @@
mod fakes;
use std::path::Path;
use archlens_application::queries::AnalyzeCodebase;
use archlens_domain::{
AnalysisConfig, AnalysisResult, AnalysisWarning, CodeElement, CodeElementKind, DomainError,
FilePath, Language, Relationship, RelationshipKind, SourceFile,
};
use fakes::{FakeFileDiscovery, FakeSourceAnalyzer};
#[test]
fn analyzes_discovered_files_and_builds_code_graph() {
let files = vec![
SourceFile::new(FilePath::new("src/order.rs").unwrap(), Language::Rust),
SourceFile::new(FilePath::new("src/service.rs").unwrap(), Language::Rust),
];
let discovery = FakeFileDiscovery::new(files);
let analyzer = FakeSourceAnalyzer::new()
.with_result(
"src/order.rs",
AnalysisResult::new(
vec![
CodeElement::new(
"Order",
CodeElementKind::Struct,
FilePath::new("src/order.rs").unwrap(),
1,
)
.unwrap(),
],
vec![],
vec![],
),
)
.with_result(
"src/service.rs",
AnalysisResult::new(
vec![
CodeElement::new(
"OrderService",
CodeElementKind::Class,
FilePath::new("src/service.rs").unwrap(),
1,
)
.unwrap(),
],
vec![
Relationship::new("OrderService", "Order", RelationshipKind::Composition)
.unwrap(),
],
vec![],
),
);
let use_case = AnalyzeCodebase::new(discovery, analyzer);
let result = use_case
.execute(Path::new("."), &AnalysisConfig::default())
.unwrap();
assert_eq!(result.graph().elements().len(), 2);
assert_eq!(result.graph().relationships().len(), 1);
assert!(result.warnings().is_empty());
}
#[test]
fn empty_file_list_returns_empty_graph() {
let discovery = FakeFileDiscovery::empty();
let analyzer = FakeSourceAnalyzer::new();
let use_case = AnalyzeCodebase::new(discovery, analyzer);
let result = use_case
.execute(Path::new("."), &AnalysisConfig::default())
.unwrap();
assert!(result.graph().elements().is_empty());
assert!(result.graph().relationships().is_empty());
assert!(result.warnings().is_empty());
}
#[test]
fn aggregates_warnings_from_multiple_files() {
let files = vec![
SourceFile::new(FilePath::new("src/a.rs").unwrap(), Language::Rust),
SourceFile::new(FilePath::new("src/b.rs").unwrap(), Language::Rust),
];
let discovery = FakeFileDiscovery::new(files);
let analyzer = FakeSourceAnalyzer::new()
.with_result(
"src/a.rs",
AnalysisResult::new(
vec![],
vec![],
vec![
AnalysisWarning::new(FilePath::new("src/a.rs").unwrap(), 10, "unknown macro")
.unwrap(),
],
),
)
.with_result(
"src/b.rs",
AnalysisResult::new(
vec![],
vec![],
vec![
AnalysisWarning::new(
FilePath::new("src/b.rs").unwrap(),
5,
"unparseable block",
)
.unwrap(),
],
),
);
let use_case = AnalyzeCodebase::new(discovery, analyzer);
let result = use_case
.execute(Path::new("."), &AnalysisConfig::default())
.unwrap();
assert_eq!(result.warnings().len(), 2);
}
#[test]
fn analysis_error_on_file_collects_warning_and_continues() {
let files = vec![
SourceFile::new(FilePath::new("src/good.rs").unwrap(), Language::Rust),
SourceFile::new(FilePath::new("src/broken.rs").unwrap(), Language::Rust),
];
let discovery = FakeFileDiscovery::new(files);
let analyzer = FakeSourceAnalyzer::new()
.with_result(
"src/good.rs",
AnalysisResult::new(
vec![
CodeElement::new(
"Good",
CodeElementKind::Struct,
FilePath::new("src/good.rs").unwrap(),
1,
)
.unwrap(),
],
vec![],
vec![],
),
)
.with_error(
"src/broken.rs",
DomainError::AnalysisError("parse failed".to_string()),
);
let use_case = AnalyzeCodebase::new(discovery, analyzer);
let result = use_case
.execute(Path::new("."), &AnalysisConfig::default())
.unwrap();
assert_eq!(result.graph().elements().len(), 1);
assert_eq!(result.graph().elements()[0].name(), "Good");
assert_eq!(result.warnings().len(), 1);
}
#[test]
fn infers_module_from_directory_structure() {
let files = vec![
SourceFile::new(
FilePath::new("/project/src/orders/service.rs").unwrap(),
Language::Rust,
),
SourceFile::new(
FilePath::new("/project/src/billing/invoice.rs").unwrap(),
Language::Rust,
),
SourceFile::new(
FilePath::new("/project/src/lib.rs").unwrap(),
Language::Rust,
),
];
let discovery = FakeFileDiscovery::new(files);
let analyzer = FakeSourceAnalyzer::new()
.with_result(
"/project/src/orders/service.rs",
AnalysisResult::new(
vec![
CodeElement::new(
"OrderService",
CodeElementKind::Class,
FilePath::new("/project/src/orders/service.rs").unwrap(),
1,
)
.unwrap(),
],
vec![],
vec![],
),
)
.with_result(
"/project/src/billing/invoice.rs",
AnalysisResult::new(
vec![
CodeElement::new(
"Invoice",
CodeElementKind::Struct,
FilePath::new("/project/src/billing/invoice.rs").unwrap(),
1,
)
.unwrap(),
],
vec![],
vec![],
),
)
.with_result(
"/project/src/lib.rs",
AnalysisResult::new(
vec![
CodeElement::new(
"App",
CodeElementKind::Struct,
FilePath::new("/project/src/lib.rs").unwrap(),
1,
)
.unwrap(),
],
vec![],
vec![],
),
);
let use_case = AnalyzeCodebase::new(discovery, analyzer);
let result = use_case
.execute(Path::new("/project"), &AnalysisConfig::default())
.unwrap();
let order_svc = result
.graph()
.elements()
.iter()
.find(|e| e.name() == "OrderService")
.unwrap();
assert_eq!(order_svc.module().unwrap().as_str(), "Orders");
let invoice = result
.graph()
.elements()
.iter()
.find(|e| e.name() == "Invoice")
.unwrap();
assert_eq!(invoice.module().unwrap().as_str(), "Billing");
let app = result
.graph()
.elements()
.iter()
.find(|e| e.name() == "App")
.unwrap();
assert!(app.module().is_none());
}
#[test]
fn infers_nested_module_from_deep_directories() {
let files = vec![SourceFile::new(
FilePath::new("/project/src/orders/models/order.rs").unwrap(),
Language::Rust,
)];
let discovery = FakeFileDiscovery::new(files);
let analyzer = FakeSourceAnalyzer::new().with_result(
"/project/src/orders/models/order.rs",
AnalysisResult::new(
vec![
CodeElement::new(
"Order",
CodeElementKind::Struct,
FilePath::new("/project/src/orders/models/order.rs").unwrap(),
1,
)
.unwrap(),
],
vec![],
vec![],
),
);
let use_case = AnalyzeCodebase::new(discovery, analyzer);
let result = use_case
.execute(Path::new("/project"), &AnalysisConfig::default())
.unwrap();
let order = result
.graph()
.elements()
.iter()
.find(|e| e.name() == "Order")
.unwrap();
assert_eq!(order.module().unwrap().as_str(), "Orders");
}
#[test]
fn respects_config_module_mappings() {
let files = vec![SourceFile::new(
FilePath::new("/project/src/infra/db.rs").unwrap(),
Language::Rust,
)];
let discovery = FakeFileDiscovery::new(files);
let analyzer = FakeSourceAnalyzer::new().with_result(
"/project/src/infra/db.rs",
AnalysisResult::new(
vec![
CodeElement::new(
"DbPool",
CodeElementKind::Struct,
FilePath::new("/project/src/infra/db.rs").unwrap(),
1,
)
.unwrap(),
],
vec![],
vec![],
),
);
let mut mappings = std::collections::HashMap::new();
mappings.insert("src/infra".to_string(), "Infrastructure".to_string());
let config = AnalysisConfig::default().with_module_mappings(mappings);
let use_case = AnalyzeCodebase::new(discovery, analyzer);
let result = use_case.execute(Path::new("/project"), &config).unwrap();
let db = result
.graph()
.elements()
.iter()
.find(|e| e.name() == "DbPool")
.unwrap();
assert_eq!(db.module().unwrap().as_str(), "Infrastructure");
}