Files
chia-blockchain/chia/_tests/tools/test_virtual_project.py
T
Matt HauffandGitHub 40db4635a8 [LABS-245] Enable PEP604 Ruff rules (#20269)
* Enable PEP604 Ruff rules

* Fix harcoded signature in test

* Hack CLVMStreamable test with note to fast follow
2025-11-18 12:34:00 -08:00

593 lines
21 KiB
Python

from __future__ import annotations
import textwrap
from collections.abc import Callable
from pathlib import Path
from typing import Any
import click
import pytest
import yaml
from click.testing import CliRunner
from chia.util.virtual_project_analysis import (
Annotation,
ChiaFile,
Config,
DirectoryParameters,
File,
Package,
build_dependency_graph,
build_virtual_dependency_graph,
cli,
config,
find_cycles,
parse_file_or_package,
)
@pytest.mark.parametrize(
"file_string, expected, annotated",
[
("# Package: example\n# Some other comment", "example", True),
("# Some comment\n# Some other comment", "chia-blockchain", False),
],
)
def test_parse_annotation(file_string: str, expected: str, annotated: bool) -> None:
"""
Test that parse returns an Annotation instance for a valid annotation or
raises ValueError for an invalid one.
"""
annotation = Annotation.parse(file_string)
assert isinstance(annotation, Annotation)
assert annotation.package == expected
assert annotation.is_annotated == annotated
# Temporary directory fixture to create test files
@pytest.fixture
def create_test_file(tmp_path: Path) -> Callable[[str, str], Path]:
def _create_test_file(name: str, content: str) -> Path:
file_path = tmp_path / name
file_path.write_text(content, encoding="utf-8")
return file_path
return _create_test_file
def test_parse_with_annotation(create_test_file: Callable[[str, str], Path]) -> None:
"""Test parsing a file that contains a valid annotation."""
file_content = "# Package: test_package\n# Some other comment"
test_file = create_test_file("annotated_file.txt", file_content)
parsed_file = ChiaFile.parse(test_file)
assert parsed_file.path == test_file
assert isinstance(parsed_file.annotations, Annotation)
assert parsed_file.annotations.package == "test_package"
def test_parse_without_annotation(create_test_file: Callable[[str, str], Path]) -> None:
"""Test parsing a file that does not contain any annotations."""
file_content = "# Some comment\n# Some other comment"
test_file = create_test_file("non_annotated_file.txt", file_content)
parsed_file = ChiaFile.parse(test_file)
assert parsed_file.path == test_file
assert not parsed_file.annotations.is_annotated
# This test is optional and can be adapted based on expected behavior for non-existent files
def test_parse_nonexistent_file() -> None:
"""Test attempting to parse a non-existent file."""
with pytest.raises(FileNotFoundError):
ChiaFile.parse(Path("/path/to/nonexistent/file.txt"))
# Helper function to create a non-empty Python file
def create_python_file(dir_path: Path, name: str, content: str) -> Path:
file_path = dir_path / name
file_path.write_text(content, encoding="utf-8")
return file_path
# Helper function to create an empty Python file
def create_empty_python_file(dir_path: Path, name: str) -> Path:
file_path = dir_path / name
file_path.touch()
return file_path
def test_gather_non_empty_python_files(tmp_path: Path) -> None:
# Set up directory structure
dir_path = tmp_path / "test_dir"
dir_path.mkdir()
excluded_dir = tmp_path / "excluded_dir"
excluded_dir.mkdir()
# Create test files
non_empty_file = create_python_file(dir_path, "non_empty.py", "print('Hello World')")
create_empty_python_file(dir_path, "empty.py")
create_python_file(excluded_dir, "excluded.py", "print('Hello World')")
# Initialize DirectoryParameters with excluded paths
dir_params = DirectoryParameters(dir_path=dir_path, excluded_paths=[excluded_dir])
# Perform the test
python_files = dir_params.gather_non_empty_python_files()
# Assertions
assert len(python_files) == 1 # Only one non-empty Python file should be found
assert python_files[0].path == non_empty_file # The path of the gathered file should match the non-empty file
def test_gather_with_nested_directories_and_exclusions(tmp_path: Path) -> None:
# Set up directory structure
base_dir = tmp_path / "base_dir"
base_dir.mkdir()
nested_dir = base_dir / "nested_dir"
nested_dir.mkdir()
excluded_dir = base_dir / "excluded_dir"
excluded_dir.mkdir()
# Create test files
nested_file = create_python_file(nested_dir, "nested.py", "print('Hello World')")
create_empty_python_file(nested_dir, "nested_empty.py")
create_python_file(excluded_dir, "excluded.py", "print('Hello World')")
# Initialize DirectoryParameters without excluded paths
dir_params = DirectoryParameters(dir_path=base_dir, excluded_paths=[excluded_dir])
# Perform the test
python_files = dir_params.gather_non_empty_python_files()
# Assertions
assert len(python_files) == 1 # Only the non-empty Python file in the nested directory should be found
assert python_files[0].path == nested_file # The path of the gathered file should match the nested non-empty file
def test_find_missing_annotations(tmp_path: Path) -> None:
# Set up directory structure
dir_path = tmp_path / "test_dir"
dir_path.mkdir()
# Create test files
create_python_file(dir_path, "non_empty.py", "print('Hello World')")
# Run the command
runner = CliRunner()
result = runner.invoke(cli, ["find_missing_annotations", "--directory", str(dir_path)])
assert result.output == f"{dir_path / 'non_empty.py'}\n"
# Rewrite file to have annotation
create_python_file(dir_path, "non_empty.py", "# Package: misc\n")
# Run the command again with no results
result = runner.invoke(cli, ["find_missing_annotations", "--directory", str(dir_path)])
assert result.output == ""
def test_parse_file_or_package() -> None:
assert parse_file_or_package("example.py") == File(Path("example.py"))
assert parse_file_or_package("example.py (extra info)") == File(Path("example.py"))
assert parse_file_or_package("(package_name)") == Package("package_name")
assert parse_file_or_package("package_name") == Package("package_name")
assert parse_file_or_package("package_name(") == Package("package_name(")
assert parse_file_or_package("(package_name") == Package("(package_name")
assert parse_file_or_package("package_name)") == Package("package_name)")
@pytest.fixture
def chia_package_structure(tmp_path: Path) -> Path:
base_dir = tmp_path / "chia_project"
base_dir.mkdir()
chia_dir = base_dir / "chia"
chia_dir.mkdir()
# Create some files within the chia package
create_python_file(chia_dir, "module1.py", "def func1(): pass")
create_python_file(chia_dir, "module2.py", "def func2(): pass\nfrom chia.module1 import func1")
create_python_file(chia_dir, "module3.py", "def func3(): pass\nimport chia.module2")
return chia_dir
def test_build_dependency_graph(chia_package_structure: Path) -> None:
chia_dir = chia_package_structure
dir_params = DirectoryParameters(dir_path=chia_dir)
graph = build_dependency_graph(dir_params)
assert chia_dir / "module1.py" in graph
assert chia_dir / "module2.py" in graph
assert chia_dir / "module3.py" in graph
assert chia_dir / "module1.py" in graph[chia_dir / "module2.py"]
assert chia_dir / "module2.py" in graph[chia_dir / "module3.py"]
def test_print_dependency_graph(chia_package_structure: Path) -> None:
# Run the command
runner = CliRunner()
result = runner.invoke(cli, ["print_dependency_graph", "--directory", str(chia_package_structure)])
assert "module1.py" in result.output
assert "module2.py" in result.output
assert "module3.py" in result.output
# Mock the build_dependency_graph function to control its output
def mock_build_dependency_graph(dir_params: DirectoryParameters) -> dict[Path, list[Path]]:
return {
Path("/path/to/package1/module1.py"): [
Path("/path/to/package2/module2.py"),
Path("/path/to/package3/module3.py"),
],
Path("/path/to/package2/module2.py"): [],
Path("/path/to/package3/module3.py"): [Path("/path/to/package2/module2.py")],
}
# Helper function to simulate ChiaFile.parse for testing
def mock_chia_file_parse(path: Path) -> ChiaFile:
annotations_map = {
Path("/path/to/package1/module1.py"): Annotation("Package1", True),
Path("/path/to/package2/module2.py"): Annotation("Package2", True),
Path("/path/to/package3/module3.py"): Annotation("Package3", True),
}
return ChiaFile(path=Path(path), annotations=annotations_map.get(path, Annotation("chia-blockchain", False)))
@pytest.fixture
def prepare_mocks(monkeypatch: pytest.MonkeyPatch) -> None:
monkeypatch.setattr("chia.util.virtual_project_analysis.build_dependency_graph", mock_build_dependency_graph)
monkeypatch.setattr("chia.util.virtual_project_analysis.ChiaFile.parse", mock_chia_file_parse)
def test_build_virtual_dependency_graph(prepare_mocks: None) -> None:
dir_params = DirectoryParameters(dir_path=Path("/path/to/package1"))
virtual_graph = build_virtual_dependency_graph(dir_params)
assert "Package2" in virtual_graph["Package1"]
assert "Package3" in virtual_graph["Package1"]
assert virtual_graph["Package2"] == []
assert "Package2" in virtual_graph["Package3"]
def test_print_virtual_dependency_graph(tmp_path: Path) -> None:
chia_dir = tmp_path / "chia"
chia_dir.mkdir()
# Create some files within the chia package
create_python_file(chia_dir, "module1.py", "# Package: one\ndef func1(): pass")
create_python_file(chia_dir, "module2.py", "# Package: two\ndef func2(): pass\nfrom chia.module1 import func1")
create_python_file(chia_dir, "module3.py", "# Package: three\ndef func3(): pass\nimport chia.module2")
# Run the command
runner = CliRunner()
result = runner.invoke(cli, ["print_virtual_dependency_graph", "--directory", str(chia_dir)])
assert "one" in result.output
assert "two" in result.output
assert "three" in result.output
# Helper function to simulate ChiaFile.parse for testing
def mock_chia_file_parse2(path: Path) -> ChiaFile:
annotations_map = {
Path("/path/to/package1/module1.py"): Annotation("Package1", True),
Path("/path/to/package2/module2.py"): Annotation("Package2", True),
Path("/path/to/package3/module3.py"): Annotation("Package1", True),
}
return ChiaFile(path=Path(path), annotations=annotations_map.get(path, Annotation("chia-blockchain", False)))
@pytest.fixture
def prepare_mocks2(monkeypatch: pytest.MonkeyPatch) -> None:
monkeypatch.setattr("chia.util.virtual_project_analysis.ChiaFile.parse", mock_chia_file_parse2)
def test_cycle_detection(prepare_mocks2: None) -> None:
# Example graph with a simple cycle
graph: dict[Path, list[Path]] = {
Path("/path/to/package1/module1.py"): [Path("/path/to/package2/module2.py")],
Path("/path/to/package2/module2.py"): [Path("/path/to/package3/module3.py")], # Cycle here
Path("/path/to/package3/module3.py"): [],
}
cycles = find_cycles(
graph,
build_virtual_dependency_graph(DirectoryParameters(dir_path=Path("path")), existing_graph=graph),
excluded_paths=[],
ignore_cycles_in=[],
ignore_specific_files=[],
ignore_specific_edges=[],
)
# \path\to\package1\module1.py (Package1) -> \path\to\package2\module2.py (Package2) -> (Package1)
# \path\to\package2\module2.py (Package2) -> \path\to\package3\module3.py (Package1) -> (Package2)
assert len(cycles) == 2
def test_print_cycles(tmp_path: Path) -> None:
chia_dir = tmp_path / "chia"
chia_dir.mkdir()
# Create some files within the chia package
create_python_file(chia_dir, "module1.py", "# Package: one\ndef func1(): pass\nfrom chia.module2 import func2")
create_python_file(chia_dir, "module2.py", "# Package: two\ndef func2(): pass\nfrom chia.module3 import func3")
create_python_file(chia_dir, "module3.py", "# Package: one\ndef func3(): pass\n")
# Run the command
runner = CliRunner()
result = runner.invoke(cli, ["print_cycles", "--directory", str(chia_dir)])
assert "module1.py (one) -> " in result.output
def test_check_config(tmp_path: Path) -> None:
chia_dir = tmp_path / "chia"
chia_dir.mkdir()
# Create some files within the chia package
create_python_file(
chia_dir,
"module1.py",
textwrap.dedent(
"""
# Package: one
def func1(): pass
from chia.module2 import func2
"""
),
)
create_python_file(
chia_dir,
"module1b.py",
textwrap.dedent(
"""
# Package: one
def func1b(): pass
"""
),
)
create_python_file(
chia_dir,
"module2.py",
textwrap.dedent(
"""
# Package: two
def func2(): pass
from chia.module3 import func3
from chia.module1b import func1b
"""
),
)
create_python_file(
chia_dir,
"module3.py",
textwrap.dedent(
"""
# Package: three
def func3(): pass
"""
),
)
# Run the command
runner = CliRunner()
result = runner.invoke(
cli,
[
"check_config",
"--directory",
str(chia_dir),
"--ignore-cycles-in",
"three",
"--ignore-specific-file",
str(chia_dir / "module3.py"),
"--ignore-specific-edge",
str(chia_dir / "module2.py") + " -> " + str(chia_dir / "module3.py"),
"--ignore-specific-edge",
str(chia_dir / "module2.py") + " -> " + str(chia_dir / "module1b.py"),
],
)
assert " module three ignored but no cycles were found" in result.output
assert f" file {chia_dir / 'module3.py'!s} ignored but no cycles were found" in result.output
assert (
f"edge {str(chia_dir / 'module2.py') + ' -> ' + str(chia_dir / 'module3.py')} ignored but no cycles were found"
in result.output
)
assert (
f"edge {str(chia_dir / 'module2.py') + ' -> ' + str(chia_dir / 'module1b.py')} ignored but no cycles were found"
not in result.output
)
def test_excluded_paths_handling(prepare_mocks2: None) -> None:
# Graph where module2.py is excluded
graph = {
Path("/path/to/package1/module1.py"): [Path("/path/to/package2/module2.py")],
Path("/path/to/package2/module2.py"): [Path("/path/to/package1/module1.py")],
}
cycles = find_cycles(
graph,
build_virtual_dependency_graph(DirectoryParameters(dir_path=Path("path")), existing_graph=graph),
excluded_paths=[Path("/path/to/package2/module2.py")],
ignore_cycles_in=[],
ignore_specific_files=[],
ignore_specific_edges=[],
)
assert len(cycles) == 0 # No cycles due to exclusion
def test_ignore_cycles_in_specific_packages(prepare_mocks2: None) -> None:
graph: dict[Path, list[Path]] = {
Path("/path/to/package1/module1.py"): [Path("/path/to/package2/module2.py")],
Path("/path/to/package2/module2.py"): [Path("/path/to/package3/module3.py")],
Path("/path/to/package3/module3.py"): [],
}
# Assuming module1.py and module3.py belong to Package1, which is ignored
cycles = find_cycles(
graph,
build_virtual_dependency_graph(DirectoryParameters(dir_path=Path("path")), existing_graph=graph),
excluded_paths=[],
ignore_cycles_in=["Package1"],
ignore_specific_files=[],
ignore_specific_edges=[],
)
assert len(cycles) == 1 # Cycles in Package1 are ignored
def test_ignore_cycles_with_specific_edges(monkeypatch: pytest.MonkeyPatch) -> None:
def _mock_chia_file_parse(path: Path) -> ChiaFile:
annotations_map = {
Path("/path/to/package1/module1a.py"): Annotation("Package1", True),
Path("/path/to/package2/module2.py"): Annotation("Package2", True),
Path("/path/to/package3/module3.py"): Annotation("Package3", True),
Path("/path/to/package1/module1b.py"): Annotation("Package1", True),
}
return ChiaFile(path=Path(path), annotations=annotations_map.get(path, Annotation("chia-blockchain", False)))
monkeypatch.setattr("chia.util.virtual_project_analysis.ChiaFile.parse", _mock_chia_file_parse)
graph = {
Path("/path/to/package1/module1a.py"): [Path("/path/to/package2/module2.py")],
Path("/path/to/package2/module2.py"): [Path("/path/to/package3/module3.py")],
Path("/path/to/package3/module3.py"): [Path("/path/to/package1/module1b.py")],
Path("/path/to/package1/module1b.py"): [],
}
virtual_graph = build_virtual_dependency_graph(DirectoryParameters(dir_path=Path("path")), existing_graph=graph)
cycles = find_cycles(
graph,
virtual_graph,
excluded_paths=[],
ignore_cycles_in=[],
ignore_specific_files=[],
ignore_specific_edges=[
(File(Path("/path/to/package3/module3.py")), File(Path("/path/to/package2/module2.py"))),
(Package("Package3"), Package("Package2")),
],
)
assert len(cycles) == 0
def test_ignore_cycles_with_specific_files(prepare_mocks2: None) -> None:
graph = {
Path("/path/to/package1/module1.py"): [Path("/path/to/package2/module2.py")],
Path("/path/to/package2/module2.py"): [Path("/path/to/package3/module3.py")], # Cycle here
}
cycles = find_cycles(
graph,
build_virtual_dependency_graph(DirectoryParameters(dir_path=Path("path")), existing_graph=graph),
excluded_paths=[],
ignore_cycles_in=[],
ignore_specific_files=[Path("/path/to/package1/module1.py"), Path("/path/to/package2/module2.py")],
ignore_specific_edges=[],
)
assert len(cycles) == 0
# Sample function to use with the decorator for testing
@click.command("blah")
@config
def sample_function(config: Config) -> None:
print(config)
# Helper function to create a temporary YAML configuration file
@pytest.fixture
def create_yaml_config(tmp_path: Path) -> Callable[[dict[str, Any]], Path]:
def _create_yaml_config(content: dict[str, Any]) -> Path:
path = tmp_path / "config.yaml"
with open(path, "w") as f:
yaml.dump(content, f)
return path
return _create_yaml_config
def test_config_with_yaml(create_yaml_config: Callable[[dict[str, Any]], Path]) -> None:
# Create a temporary YAML configuration file
yaml_config = {
"exclude_paths": ["path/to/exclude"],
"ignore": {
"packages": ["ignored.package"],
"files": ["ignored_file.py"],
"edges": ["ignored_parent -> ignored_child"],
},
}
config_path = create_yaml_config(yaml_config)
runner = CliRunner()
# Invoke the CLI with the --config option
result = runner.invoke(sample_function, ["--directory", ".", "--config", str(config_path)])
# Check if the command ran successfully
assert result.exit_code == 0
# Verify the config object created by the decorator
config = result.output
path_type = config_path.__class__.__name__
assert config == (
"Config("
"directory_parameters=DirectoryParameters("
f"dir_path={path_type}('.'), "
f"excluded_paths=[{path_type}('path/to/exclude')]"
"), "
"ignore_cycles_in=['ignored.package'], "
f"ignore_specific_files=[{path_type}('ignored_file.py')], "
f"ignore_specific_edges=[(Package(name='ignored_child', is_file=False), "
f"Package(name='ignored_parent', is_file=False))]"
")\n"
)
def test_parse_edges(tmp_path: Path) -> None:
chia_dir = tmp_path / "chia"
chia_dir.mkdir()
# Create some files within the chia package
create_python_file(
chia_dir,
"module1.py",
textwrap.dedent(
"""
# Package: one
def func1(): pass
from chia.module2 import func2
from chia.module3 import func3
"""
),
)
create_python_file(
chia_dir,
"module2.py",
textwrap.dedent(
"""
# Package: two
def func2(): pass
"""
),
)
create_python_file(
chia_dir,
"module3.py",
textwrap.dedent(
"""
# Package: three
def func3(): pass
"""
),
)
# Run the command
runner = CliRunner()
result = runner.invoke(
cli,
[
"print_edges",
"--directory",
str(chia_dir),
"--dependent-package",
"one",
"--provider-package",
"two",
],
)
assert result.output.strip() == f"{chia_dir / 'module1.py'!s} (one) -> {chia_dir / 'module2.py'!s} (two)"