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:
344
crates/application/tests/analyze_codebase_tests.rs
Normal file
344
crates/application/tests/analyze_codebase_tests.rs
Normal 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");
|
||||
}
|
||||
Reference in New Issue
Block a user