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>)
166 lines
3.9 KiB
Rust
166 lines
3.9 KiB
Rust
use std::fs;
|
|
|
|
use archlens_domain::{CodeElementKind, RelationshipKind, ports::ProjectAnalyzer};
|
|
use archlens_python_project::PythonProjectAnalyzer;
|
|
|
|
fn create_monorepo(dir: &std::path::Path) {
|
|
fs::create_dir_all(dir.join("api")).unwrap();
|
|
fs::create_dir_all(dir.join("commons")).unwrap();
|
|
fs::create_dir_all(dir.join("worker")).unwrap();
|
|
|
|
fs::write(
|
|
dir.join("api/pyproject.toml"),
|
|
r#"
|
|
[project]
|
|
name = "my-api"
|
|
dependencies = [
|
|
"my-commons>=1.0",
|
|
"fastapi",
|
|
]
|
|
"#,
|
|
)
|
|
.unwrap();
|
|
|
|
fs::write(
|
|
dir.join("commons/pyproject.toml"),
|
|
r#"
|
|
[project]
|
|
name = "my-commons"
|
|
dependencies = []
|
|
"#,
|
|
)
|
|
.unwrap();
|
|
|
|
fs::write(
|
|
dir.join("worker/pyproject.toml"),
|
|
r#"
|
|
[project]
|
|
name = "my-worker"
|
|
dependencies = [
|
|
"my-commons>=1.0",
|
|
"celery",
|
|
]
|
|
"#,
|
|
)
|
|
.unwrap();
|
|
}
|
|
|
|
#[test]
|
|
fn discovers_python_packages_as_project_elements() {
|
|
let dir = tempfile::tempdir().unwrap();
|
|
create_monorepo(dir.path());
|
|
|
|
let analyzer = PythonProjectAnalyzer::new();
|
|
let graph = analyzer.analyze(dir.path()).unwrap();
|
|
|
|
assert_eq!(graph.elements().len(), 3);
|
|
assert!(
|
|
graph
|
|
.elements()
|
|
.iter()
|
|
.all(|e| e.kind() == CodeElementKind::Project)
|
|
);
|
|
|
|
let names: Vec<&str> = graph.elements().iter().map(|e| e.name()).collect();
|
|
assert!(names.contains(&"my-api"));
|
|
assert!(names.contains(&"my-commons"));
|
|
assert!(names.contains(&"my-worker"));
|
|
}
|
|
|
|
#[test]
|
|
fn extracts_intra_project_dependencies_from_pep621() {
|
|
let dir = tempfile::tempdir().unwrap();
|
|
create_monorepo(dir.path());
|
|
|
|
let analyzer = PythonProjectAnalyzer::new();
|
|
let graph = analyzer.analyze(dir.path()).unwrap();
|
|
|
|
let deps: Vec<(&str, &str)> = graph
|
|
.relationships()
|
|
.iter()
|
|
.map(|r| (r.source(), r.target()))
|
|
.collect();
|
|
|
|
assert!(
|
|
deps.contains(&("my-api", "my-commons")),
|
|
"missing api->commons: {deps:?}"
|
|
);
|
|
assert!(
|
|
deps.contains(&("my-worker", "my-commons")),
|
|
"missing worker->commons: {deps:?}"
|
|
);
|
|
assert!(
|
|
graph
|
|
.relationships()
|
|
.iter()
|
|
.all(|r| r.kind() == RelationshipKind::Composition)
|
|
);
|
|
}
|
|
|
|
#[test]
|
|
fn excludes_external_dependencies() {
|
|
let dir = tempfile::tempdir().unwrap();
|
|
create_monorepo(dir.path());
|
|
|
|
let analyzer = PythonProjectAnalyzer::new();
|
|
let graph = analyzer.analyze(dir.path()).unwrap();
|
|
|
|
let targets: Vec<&str> = graph.relationships().iter().map(|r| r.target()).collect();
|
|
assert!(!targets.contains(&"fastapi"), "fastapi should be excluded");
|
|
assert!(!targets.contains(&"celery"), "celery should be excluded");
|
|
}
|
|
|
|
fn create_poetry_monorepo(dir: &std::path::Path) {
|
|
fs::create_dir_all(dir.join("api")).unwrap();
|
|
fs::create_dir_all(dir.join("commons")).unwrap();
|
|
|
|
fs::write(
|
|
dir.join("api/pyproject.toml"),
|
|
r#"
|
|
[tool.poetry]
|
|
name = "my-api"
|
|
|
|
[tool.poetry.dependencies]
|
|
python = "^3.11"
|
|
my-commons = {path = "../commons"}
|
|
httpx = "^0.27"
|
|
"#,
|
|
)
|
|
.unwrap();
|
|
|
|
fs::write(
|
|
dir.join("commons/pyproject.toml"),
|
|
r#"
|
|
[tool.poetry]
|
|
name = "my-commons"
|
|
|
|
[tool.poetry.dependencies]
|
|
python = "^3.11"
|
|
"#,
|
|
)
|
|
.unwrap();
|
|
}
|
|
|
|
#[test]
|
|
fn extracts_intra_project_dependencies_from_poetry() {
|
|
let dir = tempfile::tempdir().unwrap();
|
|
create_poetry_monorepo(dir.path());
|
|
|
|
let analyzer = PythonProjectAnalyzer::new();
|
|
let graph = analyzer.analyze(dir.path()).unwrap();
|
|
|
|
assert_eq!(graph.elements().len(), 2);
|
|
let deps: Vec<(&str, &str)> = graph
|
|
.relationships()
|
|
.iter()
|
|
.map(|r| (r.source(), r.target()))
|
|
.collect();
|
|
assert!(
|
|
deps.contains(&("my-api", "my-commons")),
|
|
"missing api->commons: {deps:?}"
|
|
);
|
|
|
|
let targets: Vec<&str> = graph.relationships().iter().map(|r| r.target()).collect();
|
|
assert!(!targets.contains(&"httpx"), "httpx should be excluded");
|
|
}
|