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"); }