P1 correctness: - filter test files by default (--include-tests to opt in) - per-module diagrams show cross-module dependency arrows - qualified type names (Module::TypeName) fix false edges from duplicate names P2 output richness: - method parameter types and return types in class diagrams (Rust + Python) - Python pyproject.toml project analyzer (--level project for monorepos) P3 unique value: - boundary rules in archlens.toml ([rules] allow/deny, --strict enforcement) P4 nice to have: - dependency weight labels on module arrows (--no-weights to disable) - --watch mode with 500ms debounce - D2 renderer adapter (--format d2) - interactive self-contained HTML viewer (--format html) - git-aware incremental analysis (--since <ref>)
202 lines
5.5 KiB
Rust
202 lines
5.5 KiB
Rust
use std::fs;
|
|
|
|
use archlens::run;
|
|
|
|
fn create_rust_project(dir: &std::path::Path) {
|
|
fs::create_dir_all(dir.join("src")).unwrap();
|
|
fs::write(
|
|
dir.join("src/order.rs"),
|
|
"pub struct Order {\n pub id: u64,\n}\n",
|
|
)
|
|
.unwrap();
|
|
fs::write(
|
|
dir.join("src/service.rs"),
|
|
"pub struct OrderService {\n order: Order,\n}\n",
|
|
)
|
|
.unwrap();
|
|
}
|
|
|
|
fn create_multi_module_project(dir: &std::path::Path) {
|
|
fs::create_dir_all(dir.join("src/orders")).unwrap();
|
|
fs::create_dir_all(dir.join("src/billing")).unwrap();
|
|
fs::write(
|
|
dir.join("src/orders/order.rs"),
|
|
"pub struct Order {\n pub id: u64,\n}\n",
|
|
)
|
|
.unwrap();
|
|
fs::write(
|
|
dir.join("src/orders/service.rs"),
|
|
"pub struct OrderService {\n order: Order,\n}\n",
|
|
)
|
|
.unwrap();
|
|
fs::write(
|
|
dir.join("src/billing/invoice.rs"),
|
|
"pub struct Invoice {\n pub total: f64,\n}\n",
|
|
)
|
|
.unwrap();
|
|
}
|
|
|
|
#[test]
|
|
fn analyzes_rust_project_and_writes_mermaid_to_file() {
|
|
let project = tempfile::tempdir().unwrap();
|
|
create_rust_project(project.path());
|
|
|
|
let output_dir = tempfile::tempdir().unwrap();
|
|
let output_file = output_dir.path().join("arch.mmd");
|
|
|
|
run(archlens::CliArgs {
|
|
command: None,
|
|
path: project.path().to_path_buf(),
|
|
level: "type".to_string(),
|
|
format: "mermaid".to_string(),
|
|
output: Some(output_file.to_str().unwrap().to_string()),
|
|
config: None,
|
|
scope: None,
|
|
exclude: vec![],
|
|
include_tests: false,
|
|
no_weights: false,
|
|
watch: false,
|
|
since: None,
|
|
split_by_module: false,
|
|
strict: false,
|
|
check: false,
|
|
verbose: 0,
|
|
})
|
|
.unwrap();
|
|
|
|
let content = fs::read_to_string(&output_file).unwrap();
|
|
assert!(content.contains("classDiagram"));
|
|
assert!(content.contains("Order"));
|
|
assert!(content.contains("OrderService"));
|
|
}
|
|
|
|
#[test]
|
|
fn works_without_config_file() {
|
|
let project = tempfile::tempdir().unwrap();
|
|
create_rust_project(project.path());
|
|
|
|
let output_dir = tempfile::tempdir().unwrap();
|
|
let output_file = output_dir.path().join("arch.mmd");
|
|
|
|
let result = run(archlens::CliArgs {
|
|
command: None,
|
|
path: project.path().to_path_buf(),
|
|
level: "type".to_string(),
|
|
format: "mermaid".to_string(),
|
|
output: Some(output_file.to_str().unwrap().to_string()),
|
|
config: None,
|
|
scope: None,
|
|
exclude: vec![],
|
|
include_tests: false,
|
|
no_weights: false,
|
|
watch: false,
|
|
since: None,
|
|
split_by_module: false,
|
|
strict: false,
|
|
check: false,
|
|
verbose: 0,
|
|
});
|
|
|
|
assert!(result.is_ok());
|
|
}
|
|
|
|
#[test]
|
|
fn split_by_module_writes_overview_and_per_module_files() {
|
|
let project = tempfile::tempdir().unwrap();
|
|
create_multi_module_project(project.path());
|
|
|
|
let output_dir = tempfile::tempdir().unwrap();
|
|
|
|
run(archlens::CliArgs {
|
|
command: None,
|
|
path: project.path().to_path_buf(),
|
|
level: "module".to_string(),
|
|
format: "mermaid".to_string(),
|
|
output: Some(output_dir.path().to_str().unwrap().to_string()),
|
|
config: None,
|
|
scope: None,
|
|
exclude: vec![],
|
|
include_tests: false,
|
|
no_weights: false,
|
|
watch: false,
|
|
since: None,
|
|
split_by_module: true,
|
|
strict: false,
|
|
check: false,
|
|
verbose: 0,
|
|
})
|
|
.unwrap();
|
|
|
|
let overview = output_dir.path().join("overview.mmd");
|
|
assert!(overview.exists(), "overview.mmd should exist");
|
|
|
|
let overview_content = fs::read_to_string(&overview).unwrap();
|
|
assert!(overview_content.contains("graph TD") || overview_content.contains("classDiagram"));
|
|
|
|
let entries: Vec<_> = fs::read_dir(output_dir.path())
|
|
.unwrap()
|
|
.filter_map(|e| e.ok())
|
|
.collect();
|
|
|
|
assert!(
|
|
entries.len() > 1,
|
|
"should have overview + at least one module file"
|
|
);
|
|
}
|
|
|
|
fn create_cross_module_project(dir: &std::path::Path) {
|
|
fs::create_dir_all(dir.join("src/app")).unwrap();
|
|
fs::create_dir_all(dir.join("src/domain")).unwrap();
|
|
fs::write(
|
|
dir.join("src/domain/order.rs"),
|
|
"pub struct Order { pub id: u64 }\n",
|
|
)
|
|
.unwrap();
|
|
fs::write(
|
|
dir.join("src/app/service.rs"),
|
|
"use crate::domain::Order;\npub struct OrderService { order: Order }\n",
|
|
)
|
|
.unwrap();
|
|
}
|
|
|
|
#[test]
|
|
fn per_module_file_shows_cross_module_dependency_arrows() {
|
|
let project = tempfile::tempdir().unwrap();
|
|
create_cross_module_project(project.path());
|
|
|
|
let output_dir = tempfile::tempdir().unwrap();
|
|
|
|
run(archlens::CliArgs {
|
|
command: None,
|
|
path: project.path().to_path_buf(),
|
|
level: "type".to_string(),
|
|
format: "mermaid".to_string(),
|
|
output: Some(output_dir.path().to_str().unwrap().to_string()),
|
|
config: None,
|
|
scope: None,
|
|
exclude: vec![],
|
|
include_tests: false,
|
|
no_weights: false,
|
|
watch: false,
|
|
since: None,
|
|
split_by_module: true,
|
|
strict: false,
|
|
check: false,
|
|
verbose: 0,
|
|
})
|
|
.unwrap();
|
|
|
|
let app_file = output_dir.path().join("app.mmd");
|
|
assert!(app_file.exists(), "app.mmd should exist");
|
|
|
|
let content = fs::read_to_string(&app_file).unwrap();
|
|
assert!(
|
|
content.contains("<<module>>"),
|
|
"per-module file should contain external module placeholder: {content}"
|
|
);
|
|
assert!(
|
|
content.contains("domain"),
|
|
"per-module file should reference the domain module: {content}"
|
|
);
|
|
}
|