Public Access
DEVX-63: feat: extract generic tools into devx, expand devx.mak, remove personal references
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
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
This commit was merged in pull request #103.
This commit is contained in:
@@ -0,0 +1,239 @@
|
||||
"""Unit tests for devx.tools.check_test_coverage."""
|
||||
|
||||
from pathlib import Path
|
||||
from unittest.mock import MagicMock, patch
|
||||
|
||||
from devx.tools.check_test_coverage import (
|
||||
BUILTIN_RULES,
|
||||
DEFAULT_SKIP_EXTENSIONS,
|
||||
DEFAULT_TEST_INDICATORS,
|
||||
_changed_files,
|
||||
_find_missing_tests,
|
||||
_is_test_file,
|
||||
_load_rules,
|
||||
_resolve_test_path,
|
||||
_should_skip_file,
|
||||
main,
|
||||
)
|
||||
|
||||
|
||||
class TestIsTestFile:
|
||||
def test_tests_dir(self) -> None:
|
||||
assert _is_test_file("tests/unit/test_foo.py", DEFAULT_TEST_INDICATORS) is True
|
||||
|
||||
def test_test_prefix(self) -> None:
|
||||
assert _is_test_file("src/test_foo.py", DEFAULT_TEST_INDICATORS) is True
|
||||
|
||||
def test_test_suffix(self) -> None:
|
||||
assert _is_test_file("src/foo_test.py", DEFAULT_TEST_INDICATORS) is True
|
||||
|
||||
def test_non_test_file(self) -> None:
|
||||
assert _is_test_file("src/foo.py", DEFAULT_TEST_INDICATORS) is False
|
||||
|
||||
|
||||
class TestShouldSkipFile:
|
||||
def test_skips_dotfiles(self) -> None:
|
||||
assert _should_skip_file(".gitignore", [], DEFAULT_SKIP_EXTENSIONS) is True
|
||||
|
||||
def test_skips_markdown(self) -> None:
|
||||
assert _should_skip_file("README.md", [], DEFAULT_SKIP_EXTENSIONS) is True
|
||||
|
||||
def test_skips_yaml(self) -> None:
|
||||
assert _should_skip_file("config.yml", [], DEFAULT_SKIP_EXTENSIONS) is True
|
||||
|
||||
def test_does_not_skip_python(self) -> None:
|
||||
assert _should_skip_file("src/foo.py", [], DEFAULT_SKIP_EXTENSIONS) is False
|
||||
|
||||
def test_skips_by_pattern(self) -> None:
|
||||
assert _should_skip_file("src/__init__.py", ["__init__.py"], DEFAULT_SKIP_EXTENSIONS) is True
|
||||
|
||||
def test_skips_by_glob_pattern(self) -> None:
|
||||
assert _should_skip_file("src/config.py", ["config.py"], DEFAULT_SKIP_EXTENSIONS) is True
|
||||
|
||||
|
||||
class TestResolveTestPath:
|
||||
def test_resolves_name(self, tmp_path: Path) -> None:
|
||||
result = _resolve_test_path("tests/unit/test_{name}", "src/foo.py", tmp_path)
|
||||
assert result == tmp_path / "tests" / "unit" / "test_foo"
|
||||
|
||||
def test_resolves_module(self, tmp_path: Path) -> None:
|
||||
result = _resolve_test_path("tests/unit/test_{module}_{name}", "src/pkg/foo.py", tmp_path)
|
||||
assert result == tmp_path / "tests" / "unit" / "test_pkg_foo"
|
||||
|
||||
def test_resolves_package_prefix(self, tmp_path: Path) -> None:
|
||||
result = _resolve_test_path(
|
||||
"tests/unit/test_{package_prefix}_{name}",
|
||||
"scripts/utils/secrets.py",
|
||||
tmp_path,
|
||||
)
|
||||
assert result == tmp_path / "tests" / "unit" / "test_utils_secrets"
|
||||
|
||||
def test_normalizes_hyphens(self, tmp_path: Path) -> None:
|
||||
result = _resolve_test_path("tests/test_{name}", "scripts/my-script.py", tmp_path)
|
||||
assert result == tmp_path / "tests" / "test_my_script"
|
||||
|
||||
|
||||
class TestFindMissingTests:
|
||||
def test_finds_missing_test(self, tmp_path: Path) -> None:
|
||||
files = ["scripts/foo.py"]
|
||||
rules = BUILTIN_RULES
|
||||
missing = _find_missing_tests(files, tmp_path, rules, [], DEFAULT_TEST_INDICATORS, DEFAULT_SKIP_EXTENSIONS)
|
||||
assert "scripts/foo.py" in missing
|
||||
|
||||
def test_no_missing_when_test_exists(self, tmp_path: Path) -> None:
|
||||
(tmp_path / "scripts" / "tests").mkdir(parents=True)
|
||||
(tmp_path / "scripts" / "tests" / "test_foo.py").write_text("")
|
||||
files = ["scripts/foo.py"]
|
||||
rules = BUILTIN_RULES
|
||||
missing = _find_missing_tests(files, tmp_path, rules, [], DEFAULT_TEST_INDICATORS, DEFAULT_SKIP_EXTENSIONS)
|
||||
assert missing == {}
|
||||
|
||||
def test_skips_test_files(self, tmp_path: Path) -> None:
|
||||
files = ["tests/unit/test_foo.py"]
|
||||
rules = BUILTIN_RULES
|
||||
missing = _find_missing_tests(files, tmp_path, rules, [], DEFAULT_TEST_INDICATORS, DEFAULT_SKIP_EXTENSIONS)
|
||||
assert missing == {}
|
||||
|
||||
def test_skips_non_python_files(self, tmp_path: Path) -> None:
|
||||
files = ["README.md", "config.yml"]
|
||||
rules = BUILTIN_RULES
|
||||
missing = _find_missing_tests(files, tmp_path, rules, [], DEFAULT_TEST_INDICATORS, DEFAULT_SKIP_EXTENSIONS)
|
||||
assert missing == {}
|
||||
|
||||
def test_no_rule_no_requirement(self, tmp_path: Path) -> None:
|
||||
files = ["unknown_type.xyz"]
|
||||
rules = BUILTIN_RULES
|
||||
missing = _find_missing_tests(files, tmp_path, rules, [], DEFAULT_TEST_INDICATORS, DEFAULT_SKIP_EXTENSIONS)
|
||||
assert missing == {}
|
||||
|
||||
|
||||
class TestChangedFiles:
|
||||
@patch("devx.tools.check_test_coverage.subprocess.run")
|
||||
def test_staged_only(self, mock_run: MagicMock, tmp_path: Path) -> None:
|
||||
mock_run.return_value = MagicMock(stdout="file1.py\nfile2.py\n", returncode=0)
|
||||
files = _changed_files(staged_only=True, repo_root=tmp_path)
|
||||
assert files == ["file1.py", "file2.py"]
|
||||
cmd = mock_run.call_args.args[0]
|
||||
assert "--cached" in cmd
|
||||
|
||||
@patch("devx.tools.check_test_coverage.subprocess.run")
|
||||
def test_ci_mode(self, mock_run: MagicMock, tmp_path: Path) -> None:
|
||||
mock_run.return_value = MagicMock(stdout="file1.py\n", returncode=0)
|
||||
files = _changed_files(staged_only=False, repo_root=tmp_path)
|
||||
assert files == ["file1.py"]
|
||||
cmd = mock_run.call_args.args[0]
|
||||
assert "origin/master...HEAD" in cmd
|
||||
|
||||
@patch("devx.tools.check_test_coverage.subprocess.run")
|
||||
def test_fallback_to_staged(self, mock_run: MagicMock, tmp_path: Path) -> None:
|
||||
# First call fails, second succeeds
|
||||
mock_run.side_effect = [
|
||||
MagicMock(stdout="", returncode=1),
|
||||
MagicMock(stdout="file1.py\n", returncode=0),
|
||||
]
|
||||
files = _changed_files(staged_only=False, repo_root=tmp_path)
|
||||
assert files == ["file1.py"]
|
||||
|
||||
|
||||
class TestLoadRules:
|
||||
def test_defaults_when_no_config(self) -> None:
|
||||
with patch("devx.tools.check_test_coverage._load_pyproject_devx", return_value={}):
|
||||
rules, skip, indicators, skip_ext = _load_rules()
|
||||
assert rules == BUILTIN_RULES
|
||||
assert skip == []
|
||||
assert indicators == DEFAULT_TEST_INDICATORS
|
||||
assert skip_ext == DEFAULT_SKIP_EXTENSIONS
|
||||
|
||||
def test_custom_rules(self) -> None:
|
||||
cfg = {
|
||||
"check_test_coverage": {
|
||||
"rules": [
|
||||
{
|
||||
"source_pattern": "lib/*.py",
|
||||
"test_paths": ["tests/test_{name}"],
|
||||
"description": "Missing: tests/test_{name}",
|
||||
}
|
||||
],
|
||||
"skip_patterns": ["__init__.py"],
|
||||
}
|
||||
}
|
||||
with patch("devx.tools.check_test_coverage._load_pyproject_devx", return_value=cfg):
|
||||
rules, skip, indicators, skip_ext = _load_rules()
|
||||
assert len(rules) == 1
|
||||
assert rules[0]["source_pattern"] == "lib/*.py"
|
||||
assert "__init__.py" in skip
|
||||
|
||||
def test_returns_defaults_when_cfg_not_dict(self) -> None:
|
||||
with patch(
|
||||
"devx.tools.check_test_coverage._load_pyproject_devx", return_value={"check_test_coverage": "not a dict"}
|
||||
):
|
||||
rules, skip, indicators, skip_ext = _load_rules()
|
||||
assert rules == BUILTIN_RULES
|
||||
assert skip == []
|
||||
|
||||
def test_skip_extensions_not_list_returns_default(self) -> None:
|
||||
cfg = {"check_test_coverage": {"skip_extensions": "not a list"}}
|
||||
with patch("devx.tools.check_test_coverage._load_pyproject_devx", return_value=cfg):
|
||||
_, _, _, skip_ext = _load_rules()
|
||||
assert skip_ext == DEFAULT_SKIP_EXTENSIONS
|
||||
|
||||
def test_test_paths_not_list_skips_rule(self, tmp_path: Path) -> None:
|
||||
files = ["scripts/foo.py"]
|
||||
rules = [
|
||||
{
|
||||
"source_pattern": "scripts/*.py",
|
||||
"test_paths": "not a list",
|
||||
"description": "Missing test",
|
||||
}
|
||||
]
|
||||
missing = _find_missing_tests(files, tmp_path, rules, [], DEFAULT_TEST_INDICATORS, DEFAULT_SKIP_EXTENSIONS)
|
||||
# Rule matches but test_paths is not a list, so it's skipped — no missing
|
||||
assert missing == {}
|
||||
|
||||
|
||||
class TestMain:
|
||||
def test_no_changed_files(self, tmp_path: Path) -> None:
|
||||
with (
|
||||
patch("devx.tools.check_test_coverage._changed_files", return_value=[]),
|
||||
patch(
|
||||
"devx.tools.check_test_coverage._load_rules",
|
||||
return_value=(BUILTIN_RULES, [], DEFAULT_TEST_INDICATORS, DEFAULT_SKIP_EXTENSIONS),
|
||||
),
|
||||
patch("devx.tools.check_test_coverage.Path.cwd", return_value=tmp_path),
|
||||
):
|
||||
assert main([]) == 0
|
||||
|
||||
def test_all_have_tests(self, tmp_path: Path) -> None:
|
||||
(tmp_path / "scripts" / "tests").mkdir(parents=True)
|
||||
(tmp_path / "scripts" / "tests" / "test_foo.py").write_text("")
|
||||
with (
|
||||
patch("devx.tools.check_test_coverage._changed_files", return_value=["scripts/foo.py"]),
|
||||
patch(
|
||||
"devx.tools.check_test_coverage._load_rules",
|
||||
return_value=(BUILTIN_RULES, [], DEFAULT_TEST_INDICATORS, DEFAULT_SKIP_EXTENSIONS),
|
||||
),
|
||||
patch("devx.tools.check_test_coverage.Path.cwd", return_value=tmp_path),
|
||||
):
|
||||
assert main([]) == 0
|
||||
|
||||
def test_missing_test_returns_1(self, tmp_path: Path) -> None:
|
||||
with (
|
||||
patch("devx.tools.check_test_coverage._changed_files", return_value=["scripts/foo.py"]),
|
||||
patch(
|
||||
"devx.tools.check_test_coverage._load_rules",
|
||||
return_value=(BUILTIN_RULES, [], DEFAULT_TEST_INDICATORS, DEFAULT_SKIP_EXTENSIONS),
|
||||
),
|
||||
patch("devx.tools.check_test_coverage.Path.cwd", return_value=tmp_path),
|
||||
):
|
||||
assert main([]) == 1
|
||||
|
||||
def test_warn_only_returns_0(self, tmp_path: Path) -> None:
|
||||
with (
|
||||
patch("devx.tools.check_test_coverage._changed_files", return_value=["scripts/foo.py"]),
|
||||
patch(
|
||||
"devx.tools.check_test_coverage._load_rules",
|
||||
return_value=(BUILTIN_RULES, [], DEFAULT_TEST_INDICATORS, DEFAULT_SKIP_EXTENSIONS),
|
||||
),
|
||||
patch("devx.tools.check_test_coverage.Path.cwd", return_value=tmp_path),
|
||||
):
|
||||
assert main(["--warn-only"]) == 0
|
||||
Reference in New Issue
Block a user