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.
345 lines
10 KiB
Rust
345 lines
10 KiB
Rust
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");
|
|
}
|