283 lines
12 KiB
Python
283 lines
12 KiB
Python
import json
|
|
from pathlib import Path
|
|
|
|
import pytest
|
|
|
|
from band_aider.context.class_meta import ClassMeta
|
|
from band_aider.judgement.python_ast_dependency_graph_maintainer import (
|
|
PythonAstDependencyGraphMaintainer,
|
|
)
|
|
from band_aider.strategies.python.python_source_discovery import (
|
|
PythonSourceDiscovery,
|
|
SearchableBeat,
|
|
)
|
|
from tests.support.memory_path import MemoryPath
|
|
from tests.support.memory_repo_exec import MemoryRepoExec
|
|
|
|
|
|
@pytest.fixture
|
|
def memory_root() -> MemoryPath:
|
|
"""Return a fresh source-discovery tree held entirely in memory."""
|
|
|
|
return MemoryPath.from_repo(MemoryRepoExec())
|
|
|
|
|
|
def _write(path: Path, content: str = "") -> None:
|
|
path.parent.mkdir(parents=True, exist_ok=True)
|
|
path.write_text(content, encoding="utf-8")
|
|
|
|
|
|
def _facts(discovery: PythonSourceDiscovery, path: Path) -> SearchableBeat:
|
|
beats = discovery.searchable_beats(str(path), path.read_text(encoding="utf-8"))
|
|
return next(
|
|
beat
|
|
for beat in beats
|
|
if isinstance(beat, SearchableBeat) and beat.kind == "static_facts"
|
|
)
|
|
|
|
|
|
def test_package_root_absolute_import_builds_graph_edge(memory_root: MemoryPath) -> None:
|
|
root = memory_root / "repo"
|
|
src_root = root / "band_aider"
|
|
_write(src_root / "__init__.py")
|
|
_write(src_root / "context" / "__init__.py")
|
|
_write(src_root / "context" / "memento.py", "class Memento: pass\n")
|
|
class_meta_path = src_root / "context" / "class_meta.py"
|
|
_write(class_meta_path, "from band_aider.context.memento import Memento\n")
|
|
|
|
discovery = PythonSourceDiscovery(src_root, root / "tests", repo_exec=memory_root.repository)
|
|
memento_facts = _facts(discovery, src_root / "context" / "memento.py")
|
|
class_meta_facts = _facts(discovery, class_meta_path)
|
|
assert dict(memento_facts.attributes)["module_name"] == "band_aider.context.memento"
|
|
assert any(
|
|
item[0] == "band_aider.context.memento"
|
|
for item in json.loads(dict(class_meta_facts.attributes)["imported_names"])
|
|
)
|
|
|
|
source_paths = [src_root / "context" / "memento.py", class_meta_path]
|
|
source_files = [
|
|
ClassMeta(
|
|
location=str(path.relative_to(root)),
|
|
summary="",
|
|
relevance=[],
|
|
relevant_symbols={},
|
|
)
|
|
for path in source_paths
|
|
]
|
|
maintainer = PythonAstDependencyGraphMaintainer(repo_exec=memory_root.repository)
|
|
graph = maintainer.build(
|
|
source_files, [], root, src_root, root / "tests"
|
|
)
|
|
assert graph.source_dependencies["band_aider/context/class_meta.py"] == {
|
|
"band_aider/context/memento.py"
|
|
}
|
|
assert "band_aider/context/class_meta.py" in graph.source_dependents[
|
|
"band_aider/context/memento.py"
|
|
]
|
|
|
|
|
|
def test_src_layout_and_package_initializer_names(memory_root: MemoryPath) -> None:
|
|
root = memory_root / "repo"
|
|
src_root = root / "src"
|
|
package = src_root / "band_aider"
|
|
_write(package / "__init__.py")
|
|
_write(package / "context" / "__init__.py")
|
|
memento = package / "context" / "memento.py"
|
|
_write(memento)
|
|
discovery = PythonSourceDiscovery(src_root, root / "tests", repo_exec=memory_root.repository)
|
|
|
|
assert dict(_facts(discovery, memento).attributes)["module_name"] == "band_aider.context.memento"
|
|
assert discovery._module_name(str(package / "context" / "__init__.py")) == "band_aider.context"
|
|
|
|
|
|
def test_relative_imports_keep_existing_module_identity(memory_root: MemoryPath) -> None:
|
|
root = memory_root / "repo"
|
|
src_root = root / "band_aider"
|
|
_write(src_root / "__init__.py")
|
|
_write(src_root / "ui" / "__init__.py")
|
|
splash = src_root / "ui" / "splash.py"
|
|
startup = src_root / "ui" / "startup_wizard.py"
|
|
_write(splash, "ASCII_ART = 'x'\n")
|
|
_write(startup, "from .splash import ASCII_ART\n")
|
|
discovery = PythonSourceDiscovery(src_root, root / "tests", repo_exec=memory_root.repository)
|
|
imported = json.loads(dict(_facts(discovery, startup).attributes)["imported_names"])
|
|
assert any(item[0] == "band_aider.ui.splash" for item in imported)
|
|
|
|
|
|
def test_unrelated_repeated_root_name_is_rejected(memory_root: MemoryPath) -> None:
|
|
root = memory_root / "repo"
|
|
src_root = root / "src"
|
|
_write(src_root / "__init__.py")
|
|
discovery = PythonSourceDiscovery(
|
|
src_root,
|
|
root / "tests",
|
|
repo_exec=memory_root.repository,
|
|
repo_root=root,
|
|
)
|
|
|
|
with pytest.raises(ValueError, match="outside configured source and test roots"):
|
|
discovery._module_name(str(root / "other" / "src" / "module.py"))
|
|
|
|
|
|
def test_dynamic_resolution_uses_canonical_qualified_type_name(memory_root: MemoryPath) -> None:
|
|
root = memory_root / "repo"
|
|
src_root = root / "src"
|
|
files = {
|
|
"package_a/__init__.py": "",
|
|
"package_a/result.py": "class Result: pass\n",
|
|
"package_b/__init__.py": "",
|
|
"package_b/result.py": "class Result: pass\n",
|
|
"consumer.py": (
|
|
"from package_a.result import Result\n"
|
|
"class Consumer:\n"
|
|
" def __init__(self, result: Result): pass\n"
|
|
"Consumer(Result())\n"
|
|
),
|
|
}
|
|
for location, source in files.items():
|
|
_write(src_root / location, source)
|
|
|
|
source_files = [
|
|
ClassMeta(
|
|
location=str((src_root / location).relative_to(root)),
|
|
summary="",
|
|
relevance=[],
|
|
relevant_symbols={},
|
|
)
|
|
for location in files
|
|
]
|
|
maintainer = PythonAstDependencyGraphMaintainer(repo_exec=memory_root.repository)
|
|
graph = maintainer.build(
|
|
source_files, [], root, src_root, root / "tests"
|
|
)
|
|
|
|
result_types = {
|
|
record.qualified_name: record.type_id
|
|
for record in graph.type_definitions.values()
|
|
if record.local_name == "Result"
|
|
}
|
|
assert result_types["package_a.result.Result"] != result_types["package_b.result.Result"]
|
|
resolved = maintainer._resolve_type(
|
|
"Result",
|
|
maintainer.graph.type_definitions,
|
|
"src/consumer.py",
|
|
)
|
|
assert resolved == result_types["package_a.result.Result"]
|
|
|
|
|
|
def test_incremental_src_creation_preserves_import_root(memory_root: MemoryPath) -> None:
|
|
root = memory_root / "repo"
|
|
src_root = root / "src"
|
|
files = {
|
|
"package_a/__init__.py": "",
|
|
"package_a/dependency.py": "class Dependency: pass\n",
|
|
"package_a/consumer.py": "from package_a.dependency import Dependency\n",
|
|
}
|
|
for location, source in files.items():
|
|
_write(src_root / location, source)
|
|
source_files = [
|
|
ClassMeta(
|
|
location=f"src/{location}",
|
|
summary="",
|
|
relevance=[],
|
|
relevant_symbols={},
|
|
)
|
|
for location in files
|
|
]
|
|
maintainer = PythonAstDependencyGraphMaintainer(repo_exec=memory_root.repository)
|
|
graph = maintainer.build(source_files, [], root, src_root, root / "tests")
|
|
assert not (src_root / "__init__.py").exists()
|
|
assert maintainer._static_facts_by_location["src/package_a/dependency.py"].module_name == "package_a.dependency"
|
|
assert maintainer._static_facts_by_location["src/package_a/consumer.py"].module_name == "package_a.consumer"
|
|
assert graph.source_dependencies["src/package_a/consumer.py"] == {"src/package_a/dependency.py"}
|
|
|
|
new_path = src_root / "package_a/new_area/new_source.py"
|
|
_write(new_path, "class NewSource: pass\n")
|
|
new_meta = ClassMeta("src/package_a/new_area/new_source.py", "", [], {})
|
|
graph = maintainer.upsert(
|
|
new_meta,
|
|
[*source_files, new_meta],
|
|
[],
|
|
root,
|
|
src_root,
|
|
root / "tests",
|
|
)
|
|
assert not (src_root / "__init__.py").exists()
|
|
assert (src_root / "package_a/new_area/__init__.py").exists()
|
|
assert maintainer._static_facts_by_location["src/package_a/dependency.py"].module_name == "package_a.dependency"
|
|
assert maintainer._static_facts_by_location["src/package_a/consumer.py"].module_name == "package_a.consumer"
|
|
assert maintainer._static_facts_by_location["src/package_a/new_area/new_source.py"].module_name == "package_a.new_area.new_source"
|
|
assert graph.source_dependencies["src/package_a/consumer.py"] == {"src/package_a/dependency.py"}
|
|
assert all("src." not in value for value in graph.source_by_location)
|
|
assert all("src." not in value for value in maintainer._static_facts_by_location)
|
|
|
|
|
|
def test_package_root_incremental_creation_keeps_package_prefix(memory_root: MemoryPath) -> None:
|
|
root = memory_root / "repo"
|
|
src_root = root / "package_a"
|
|
_write(src_root / "__init__.py")
|
|
_write(src_root / "existing.py", "class Existing: pass\n")
|
|
existing = ClassMeta("package_a/existing.py", "", [], {})
|
|
maintainer = PythonAstDependencyGraphMaintainer(repo_exec=memory_root.repository)
|
|
maintainer.build([existing], [], root, src_root, root / "tests")
|
|
new_path = src_root / "new_area/new_source.py"
|
|
_write(new_path, "class NewSource: pass\n")
|
|
new = ClassMeta("package_a/new_area/new_source.py", "", [], {})
|
|
maintainer.upsert(new, [existing, new], [], root, src_root, root / "tests")
|
|
assert (src_root / "__init__.py").exists()
|
|
assert (src_root / "new_area/__init__.py").exists()
|
|
assert maintainer._static_facts_by_location[existing.location].module_name == "package_a.existing"
|
|
assert maintainer._static_facts_by_location[new.location].module_name == "package_a.new_area.new_source"
|
|
|
|
|
|
def test_test_mapping_uses_discovered_module_facts_for_both_layouts(memory_root: MemoryPath) -> None:
|
|
for layout in ("package_root", "src"):
|
|
root = memory_root / layout
|
|
src_root = root / "package_a" if layout == "package_root" else root / "src"
|
|
package_root = src_root if layout == "package_root" else src_root / "package_a"
|
|
_write(package_root / "__init__.py")
|
|
dependency_path = package_root / "dependency.py"
|
|
_write(dependency_path, "class Dependency: pass\n")
|
|
test_path = root / "tests/test_dependency.py"
|
|
_write(test_path, "from package_a.dependency import Dependency\n")
|
|
source_location = (
|
|
"package_a/dependency.py"
|
|
if layout == "package_root"
|
|
else "src/package_a/dependency.py"
|
|
)
|
|
source = ClassMeta(source_location, "", [], {})
|
|
test = ClassMeta("tests/test_dependency.py", "", [], {})
|
|
maintainer = PythonAstDependencyGraphMaintainer(repo_exec=memory_root.repository)
|
|
graph = maintainer.build(
|
|
[source],
|
|
[test],
|
|
root,
|
|
src_root,
|
|
root / "tests",
|
|
)
|
|
assert source_location in graph.tests_for_source
|
|
assert test.location in graph.tests_for_source[source_location]
|
|
|
|
|
|
def test_incremental_static_edges_follow_import_changes_and_new_dependencies(memory_root: MemoryPath) -> None:
|
|
root = memory_root / "repo"
|
|
src_root = root / "src"
|
|
_write(src_root / "package_a/__init__.py")
|
|
_write(src_root / "package_a/old_dependency.py", "class Dependency: pass\n")
|
|
_write(src_root / "package_a/new_dependency.py", "class Dependency: pass\n")
|
|
importer_path = src_root / "package_a/consumer.py"
|
|
_write(importer_path, "from package_a.old_dependency import Dependency\n")
|
|
old = ClassMeta("src/package_a/old_dependency.py", "", [], {})
|
|
new = ClassMeta("src/package_a/new_dependency.py", "", [], {})
|
|
importer = ClassMeta("src/package_a/consumer.py", "", [], {})
|
|
maintainer = PythonAstDependencyGraphMaintainer(repo_exec=memory_root.repository)
|
|
graph = maintainer.build([old, new, importer], [], root, src_root, root / "tests")
|
|
assert graph.source_dependencies[importer.location] == {old.location}
|
|
_write(importer_path, "from package_a.new_dependency import Dependency\n")
|
|
graph = maintainer.upsert(importer, [old, new, importer], [], root, src_root, root / "tests")
|
|
assert graph.source_dependencies[importer.location] == {new.location}
|
|
assert importer.location not in graph.source_dependents.get(old.location, set())
|
|
assert importer.location in graph.source_dependents[new.location]
|
|
_write(src_root / "package_a/new_dependency.py", "class Dependency: pass\nvalue = 1\n")
|
|
graph = maintainer.upsert(new, [old, new, importer], [], root, src_root, root / "tests")
|
|
assert graph.source_dependencies[importer.location] == {new.location}
|