Public Access
Post-merge / detect-type (push) Successful in 37s
Post-merge / validate-commit-msg (push) Successful in 42s
Post-merge / sync-wiki (push) Successful in 52s
Post-merge / vikunja (push) Successful in 49s
Post-merge / release (push) Successful in 59s
Post-merge / badges (push) Successful in 1m0s
Post-merge / configure-repo (push) Successful in 49s
250 lines
9.7 KiB
Python
250 lines
9.7 KiB
Python
"""Unit tests for devx.tools.check_mutable_globals."""
|
|
|
|
from pathlib import Path
|
|
from unittest.mock import patch
|
|
|
|
from click.testing import CliRunner
|
|
|
|
from devx.tools.check_mutable_globals import (
|
|
DEFAULT_SCAN_DIRS,
|
|
DEFAULT_SKIP_DIRS,
|
|
_load_config,
|
|
_should_skip,
|
|
cli,
|
|
find_mutable_globals,
|
|
)
|
|
|
|
|
|
class TestFindMutableGlobals:
|
|
def test_detects_set_global_with_path_hint(self, tmp_path: Path) -> None:
|
|
source = "_SEEN: set[Path] = set()\n"
|
|
f = tmp_path / "mod.py"
|
|
f.write_text(source)
|
|
issues = find_mutable_globals(f, tmp_path, set())
|
|
assert len(issues) == 1
|
|
assert "_SEEN" in issues[0]
|
|
assert "set()" in issues[0]
|
|
|
|
def test_detects_dict_global_with_path_hint(self, tmp_path: Path) -> None:
|
|
source = "_CACHE: dict[Path, Any] = {}\n"
|
|
f = tmp_path / "mod.py"
|
|
f.write_text(source)
|
|
issues = find_mutable_globals(f, tmp_path, set())
|
|
assert len(issues) == 1
|
|
assert "_CACHE" in issues[0]
|
|
|
|
def test_detects_list_global_with_path_hint(self, tmp_path: Path) -> None:
|
|
source = "PATHS: list[Path] = []\n"
|
|
f = tmp_path / "mod.py"
|
|
f.write_text(source)
|
|
issues = find_mutable_globals(f, tmp_path, set())
|
|
assert len(issues) == 1
|
|
assert "PATHS" in issues[0]
|
|
|
|
def test_skips_non_mutable_globals(self, tmp_path: Path) -> None:
|
|
source = "_MAX: int = 10\n_SEEN: set[Path] = set()\n"
|
|
f = tmp_path / "mod.py"
|
|
f.write_text(source)
|
|
issues = find_mutable_globals(f, tmp_path, set())
|
|
assert len(issues) == 1
|
|
assert "_SEEN" in issues[0]
|
|
|
|
def test_skips_globals_without_path_hint(self, tmp_path: Path) -> None:
|
|
source = "_DATA: dict[str, int] = {}\n"
|
|
f = tmp_path / "mod.py"
|
|
f.write_text(source)
|
|
issues = find_mutable_globals(f, tmp_path, set())
|
|
assert len(issues) == 0
|
|
|
|
def test_detects_path_type_annotation(self, tmp_path: Path) -> None:
|
|
source = "_FILES: set[Path] = set()\n"
|
|
f = tmp_path / "mod.py"
|
|
f.write_text(source)
|
|
issues = find_mutable_globals(f, tmp_path, set())
|
|
assert len(issues) == 1
|
|
|
|
def test_known_safe_exception(self, tmp_path: Path) -> None:
|
|
source = "_SEEN: set[Path] = set()\n"
|
|
f = tmp_path / "mod.py"
|
|
f.write_text(source)
|
|
known_safe = {("mod.py", 1, "_SEEN")}
|
|
issues = find_mutable_globals(f, tmp_path, known_safe)
|
|
assert len(issues) == 0
|
|
|
|
def test_syntax_error_returns_empty(self, tmp_path: Path) -> None:
|
|
f = tmp_path / "mod.py"
|
|
f.write_text("def broken(:\n")
|
|
issues = find_mutable_globals(f, tmp_path, set())
|
|
assert issues == []
|
|
|
|
def test_detects_mutable_literal_dict(self, tmp_path: Path) -> None:
|
|
source = "_CACHE: dict[Path, Any] = {}\n"
|
|
f = tmp_path / "mod.py"
|
|
f.write_text(source)
|
|
issues = find_mutable_globals(f, tmp_path, set())
|
|
assert len(issues) == 1
|
|
|
|
def test_detects_mutable_literal_list(self, tmp_path: Path) -> None:
|
|
source = "SEEN_PATHS: list[Path] = []\n"
|
|
f = tmp_path / "mod.py"
|
|
f.write_text(source)
|
|
issues = find_mutable_globals(f, tmp_path, set())
|
|
assert len(issues) == 1
|
|
|
|
def test_detects_mutable_literal_set(self, tmp_path: Path) -> None:
|
|
source = "REGISTRY: set[Path] = set()\n"
|
|
f = tmp_path / "mod.py"
|
|
f.write_text(source)
|
|
issues = find_mutable_globals(f, tmp_path, set())
|
|
assert len(issues) == 1
|
|
|
|
def test_skips_function_definitions(self, tmp_path: Path) -> None:
|
|
source = "def foo():\n pass\n"
|
|
f = tmp_path / "mod.py"
|
|
f.write_text(source)
|
|
issues = find_mutable_globals(f, tmp_path, set())
|
|
assert issues == []
|
|
|
|
def test_handles_assign_with_name_target(self, tmp_path: Path) -> None:
|
|
source = "SEEN_PATHS = set()\n"
|
|
f = tmp_path / "mod.py"
|
|
f.write_text(source)
|
|
issues = find_mutable_globals(f, tmp_path, set())
|
|
assert len(issues) == 1
|
|
assert "SEEN_PATHS" in issues[0]
|
|
|
|
def test_skips_annotation_without_value(self, tmp_path: Path) -> None:
|
|
source = "_CACHE: dict[Path, Any]\n"
|
|
f = tmp_path / "mod.py"
|
|
f.write_text(source)
|
|
issues = find_mutable_globals(f, tmp_path, set())
|
|
assert issues == []
|
|
|
|
def test_skips_attribute_call(self, tmp_path: Path) -> None:
|
|
# collections.defaultdict is an Attribute call, not a Name call
|
|
source = "_CACHE: dict[Path, Any] = collections.defaultdict(list)\n"
|
|
f = tmp_path / "mod.py"
|
|
f.write_text(source)
|
|
issues = find_mutable_globals(f, tmp_path, set())
|
|
# Attribute calls are skipped (pass), so not flagged as mutable literal
|
|
assert issues == []
|
|
|
|
def test_multiple_assign_targets(self, tmp_path: Path) -> None:
|
|
source = "SEEN = CACHE = set()\n"
|
|
f = tmp_path / "mod.py"
|
|
f.write_text(source)
|
|
issues = find_mutable_globals(f, tmp_path, set())
|
|
# Both SEEN and CACHE should be flagged
|
|
assert len(issues) == 2
|
|
|
|
|
|
class TestShouldSkip:
|
|
def test_skips_pycache(self) -> None:
|
|
assert _should_skip(Path("/a/__pycache__/b.py"), DEFAULT_SKIP_DIRS) is True
|
|
|
|
def test_skips_venv(self) -> None:
|
|
assert _should_skip(Path("/a/.venv/b.py"), DEFAULT_SKIP_DIRS) is True
|
|
|
|
def test_does_not_skip_normal(self) -> None:
|
|
assert _should_skip(Path("/a/src/b.py"), DEFAULT_SKIP_DIRS) is False
|
|
|
|
|
|
class TestLoadConfig:
|
|
def test_defaults_when_no_pyproject(self, tmp_path: Path) -> None:
|
|
with patch("devx.tools.check_mutable_globals._load_pyproject_devx", return_value={}):
|
|
scan_dirs, skip_dirs, known_safe = _load_config()
|
|
assert scan_dirs == DEFAULT_SCAN_DIRS
|
|
assert skip_dirs == DEFAULT_SKIP_DIRS
|
|
assert known_safe == set()
|
|
|
|
def test_reads_config_from_pyproject(self) -> None:
|
|
cfg = {
|
|
"check_mutable_globals": {
|
|
"scan_dirs": ["src", "tests"],
|
|
"skip_dirs": ["__pycache__", ".tox"],
|
|
"known_safe": ["src/mod.py:10:_CACHE"],
|
|
}
|
|
}
|
|
with patch("devx.tools.check_mutable_globals._load_pyproject_devx", return_value=cfg):
|
|
scan_dirs, skip_dirs, known_safe = _load_config()
|
|
assert scan_dirs == ["src", "tests"]
|
|
assert ".tox" in skip_dirs
|
|
assert ("src/mod.py", 10, "_CACHE") in known_safe
|
|
|
|
def test_returns_defaults_when_cfg_not_dict(self) -> None:
|
|
with patch(
|
|
"devx.tools.check_mutable_globals._load_pyproject_devx",
|
|
return_value={"check_mutable_globals": "not a dict"},
|
|
):
|
|
scan_dirs, skip_dirs, known_safe = _load_config()
|
|
assert scan_dirs == DEFAULT_SCAN_DIRS
|
|
assert skip_dirs == DEFAULT_SKIP_DIRS
|
|
assert known_safe == set()
|
|
|
|
def test_known_safe_with_invalid_line_number(self) -> None:
|
|
cfg = {"check_mutable_globals": {"known_safe": ["mod.py:abc:_CACHE"]}}
|
|
with patch("devx.tools.check_mutable_globals._load_pyproject_devx", return_value=cfg):
|
|
_, _, known_safe = _load_config()
|
|
assert known_safe == set()
|
|
|
|
def test_scan_dirs_not_list_returns_default(self) -> None:
|
|
cfg = {"check_mutable_globals": {"scan_dirs": "not a list"}}
|
|
with patch("devx.tools.check_mutable_globals._load_pyproject_devx", return_value=cfg):
|
|
scan_dirs, _, _ = _load_config()
|
|
assert scan_dirs == DEFAULT_SCAN_DIRS
|
|
|
|
|
|
class TestCli:
|
|
def test_passes_when_no_issues(self, tmp_path: Path) -> None:
|
|
runner = CliRunner()
|
|
with (
|
|
patch("devx.tools.check_mutable_globals._load_config", return_value=(["empty_dir"], set(), set())),
|
|
patch("devx.tools.check_mutable_globals.Path.cwd", return_value=tmp_path),
|
|
):
|
|
result = runner.invoke(cli, [])
|
|
assert result.exit_code == 0
|
|
assert "Passed" in result.output
|
|
|
|
def test_fails_when_issues_found(self, tmp_path: Path) -> None:
|
|
scan_dir = tmp_path / "src"
|
|
scan_dir.mkdir()
|
|
(scan_dir / "mod.py").write_text("_SEEN: set[Path] = set()\n")
|
|
|
|
runner = CliRunner()
|
|
with (
|
|
patch("devx.tools.check_mutable_globals._load_config", return_value=(["src"], set(), set())),
|
|
patch("devx.tools.check_mutable_globals.Path.cwd", return_value=tmp_path),
|
|
):
|
|
result = runner.invoke(cli, [])
|
|
assert result.exit_code != 0
|
|
assert "FAILED" in result.output
|
|
|
|
def test_scan_dir_option_overrides_config(self, tmp_path: Path) -> None:
|
|
scan_dir = tmp_path / "custom"
|
|
scan_dir.mkdir()
|
|
(scan_dir / "mod.py").write_text("_SEEN: set[Path] = set()\n")
|
|
|
|
runner = CliRunner()
|
|
with (
|
|
patch("devx.tools.check_mutable_globals._load_config", return_value=(["other"], set(), set())),
|
|
patch("devx.tools.check_mutable_globals.Path.cwd", return_value=tmp_path),
|
|
):
|
|
result = runner.invoke(cli, ["--scan-dir", "custom"])
|
|
assert result.exit_code != 0
|
|
assert "FAILED" in result.output
|
|
|
|
def test_skips_files_in_skip_dirs(self, tmp_path: Path) -> None:
|
|
scan_dir = tmp_path / "src"
|
|
pycache = scan_dir / "__pycache__"
|
|
pycache.mkdir(parents=True)
|
|
(pycache / "mod.py").write_text("_SEEN: set[Path] = set()\n")
|
|
|
|
runner = CliRunner()
|
|
with (
|
|
patch("devx.tools.check_mutable_globals._load_config", return_value=(["src"], {"__pycache__"}, set())),
|
|
patch("devx.tools.check_mutable_globals.Path.cwd", return_value=tmp_path),
|
|
):
|
|
result = runner.invoke(cli, [])
|
|
assert result.exit_code == 0
|
|
assert "Passed" in result.output
|