use brick::model::{Building, EdgeKind}; use brick::scan; use std::fs; fn scan_files(files: &[(&str, &str)]) -> Building { let root = tempfile::tempdir().unwrap(); for (path, content) in files { let file = root.path().join(path); fs::create_dir_all(file.parent().unwrap()).unwrap(); fs::write(file, content).unwrap(); } scan::scan(root.path()).unwrap() } /// (caller name, target path) for every resolved call. fn resolved_calls(model: &Building) -> Vec<(String, String)> { model .edges .iter() .filter(|edge| edge.kind == EdgeKind::Calls) .filter_map(|edge| { let source = model.nodes.iter().find(|n| n.id == edge.source)?; let target = model.nodes.iter().find(|n| n.id == edge.target)?; Some((source.name.clone(), target.path.clone()?)) }) .collect() } #[test] fn relative_package_and_qualified_python_imports_resolve_to_the_right_file() { // Python calls resolve only through import edges, and the lexical importer // produced none for relative imports (the regex needed a leading letter), // for packages (`from pkg import f` looked up the stem "pkg", not // pkg/__init__.py), or when two files shared a stem. let model = scan_files(&[ ("pkg/__init__.py", "def from_package():\n return 1\n"), ("pkg/util.py", "def helper():\n return 1\n"), ( "pkg/rel.py", "from .util import helper\n\ndef use_relative():\n return helper()\n", ), ("x/common.py", "def only_here():\n return 1\n"), ("y/common.py", "def elsewhere():\n return 2\n"), ( "main.py", "from x.common import only_here\nfrom pkg import from_package\n\ndef run():\n only_here()\n return from_package()\n", ), ]); let calls = resolved_calls(&model); for (caller, target) in [ ("use_relative", "pkg/util.py"), ("run", "x/common.py"), ("run", "pkg/__init__.py"), ] { assert!( calls .iter() .any(|(source, path)| source == caller && path == target), "{caller} -> {target} missing: {calls:?}" ); } } #[test] fn an_unknown_dotted_python_module_does_not_fall_back_to_a_basename() { // `from other.common import f` names a module this repository does not have; // matching the stem "common" to y/common.py would invent a dependency. let model = scan_files(&[ ("y/common.py", "def f():\n return 2\n"), ( "main.py", "from other.common import f\n\ndef run():\n return f()\n", ), ]); let calls = resolved_calls(&model); assert!( !calls.iter().any(|(_, path)| path == "y/common.py"), "{calls:?}" ); }