Public Access
Post-merge / detect-type (push) Successful in 7s
Post-merge / validate-commit-msg (push) Successful in 7s
Post-merge / configure-repo (push) Successful in 9s
Post-merge / release (push) Successful in 45s
Post-merge / vikunja (push) Successful in 8s
Post-merge / sync-wiki (push) Successful in 40s
Post-merge / badges (push) Successful in 48s
375 lines
15 KiB
Python
375 lines
15 KiB
Python
"""Unit tests for devx/tools/generate_badges.py."""
|
|
|
|
from pathlib import Path
|
|
from unittest.mock import MagicMock, patch
|
|
|
|
from click.testing import CliRunner
|
|
|
|
from devx.tools.generate_badges import (
|
|
COLOR_HEX,
|
|
cli,
|
|
collect_coverage_and_tests,
|
|
collect_doc_coverage,
|
|
collect_quality,
|
|
coverage_color,
|
|
detect_coverage_target,
|
|
detect_package_name,
|
|
doc_coverage_color,
|
|
extract_coverage,
|
|
extract_doc_coverage,
|
|
extract_test_count,
|
|
generate_badges,
|
|
make_badge,
|
|
read_version,
|
|
render_svg,
|
|
resolve_repo_root,
|
|
run_command,
|
|
)
|
|
|
|
|
|
class TestResolveRepoRoot:
|
|
def test_uses_github_workspace_when_set(self, tmp_path: Path, monkeypatch) -> None: # type: ignore[no-untyped-def]
|
|
monkeypatch.setenv("GITHUB_WORKSPACE", str(tmp_path))
|
|
assert resolve_repo_root() == tmp_path
|
|
|
|
def test_falls_back_to_cwd_when_no_workspace(self, tmp_path: Path, monkeypatch) -> None: # type: ignore[no-untyped-def]
|
|
monkeypatch.delenv("GITHUB_WORKSPACE", raising=False)
|
|
monkeypatch.chdir(tmp_path)
|
|
assert resolve_repo_root() == tmp_path
|
|
|
|
def test_falls_back_to_cwd_when_workspace_invalid(self, monkeypatch) -> None: # type: ignore[no-untyped-def]
|
|
monkeypatch.setenv("GITHUB_WORKSPACE", "/nonexistent/path")
|
|
result = resolve_repo_root()
|
|
assert result == Path.cwd()
|
|
|
|
|
|
class TestDetectPackageName:
|
|
def test_detects_package_with_init(self, tmp_path: Path) -> None: # type: ignore[no-untyped-def]
|
|
src = tmp_path / "src"
|
|
pkg = src / "mypkg"
|
|
pkg.mkdir(parents=True)
|
|
(pkg / "__init__.py").write_text('__version__ = "1.0.0"\n')
|
|
assert detect_package_name(tmp_path) == "mypkg"
|
|
|
|
def test_returns_none_when_no_src(self, tmp_path: Path) -> None: # type: ignore[no-untyped-def]
|
|
assert detect_package_name(tmp_path) is None
|
|
|
|
def test_returns_none_when_no_init(self, tmp_path: Path) -> None: # type: ignore[no-untyped-def]
|
|
src = tmp_path / "src"
|
|
pkg = src / "mypkg"
|
|
pkg.mkdir(parents=True)
|
|
# No __init__.py
|
|
assert detect_package_name(tmp_path) is None
|
|
|
|
def test_picks_first_package_alphabetically(self, tmp_path: Path) -> None: # type: ignore[no-untyped-def]
|
|
src = tmp_path / "src"
|
|
for name in ["zpkg", "apkg"]:
|
|
d = src / name
|
|
d.mkdir(parents=True)
|
|
(d / "__init__.py").write_text("")
|
|
assert detect_package_name(tmp_path) == "apkg"
|
|
|
|
def test_skips_non_dir_entries(self, tmp_path: Path) -> None: # type: ignore[no-untyped-def]
|
|
src = tmp_path / "src"
|
|
src.mkdir(parents=True)
|
|
(src / "README.md").write_text("not a package")
|
|
pkg = src / "mypkg"
|
|
pkg.mkdir()
|
|
(pkg / "__init__.py").write_text("")
|
|
assert detect_package_name(tmp_path) == "mypkg"
|
|
|
|
|
|
class TestDetectCoverageTarget:
|
|
def test_parses_from_pyproject(self, tmp_path: Path) -> None: # type: ignore[no-untyped-def]
|
|
(tmp_path / "pyproject.toml").write_text(
|
|
'[tool.pytest.ini_options]\naddopts = "--cov=src/devx --cov-report=term-missing"\n'
|
|
)
|
|
assert detect_coverage_target(tmp_path) == "src/devx"
|
|
|
|
def test_falls_back_to_src_package(self, tmp_path: Path) -> None: # type: ignore[no-untyped-def]
|
|
src = tmp_path / "src"
|
|
pkg = src / "mypkg"
|
|
pkg.mkdir(parents=True)
|
|
(pkg / "__init__.py").write_text('__version__ = "1.0"\n')
|
|
assert detect_coverage_target(tmp_path) == "src/mypkg"
|
|
|
|
def test_returns_none_when_no_package(self, tmp_path: Path) -> None: # type: ignore[no-untyped-def]
|
|
assert detect_coverage_target(tmp_path) is None
|
|
|
|
|
|
class TestRunCommand:
|
|
@patch("devx.tools.generate_badges.subprocess.run")
|
|
def test_returns_returncode_stdout_stderr(self, mock_run: MagicMock) -> None:
|
|
mock_run.return_value = MagicMock(returncode=0, stdout="out", stderr="err")
|
|
rc, out, err = run_command(["echo", "hello"])
|
|
assert rc == 0
|
|
assert out == "out"
|
|
assert err == "err"
|
|
mock_run.assert_called_once()
|
|
|
|
|
|
class TestMakeBadge:
|
|
def test_creates_valid_badge_dict(self) -> None:
|
|
badge = make_badge("coverage", "100%", "brightgreen")
|
|
assert badge == {
|
|
"schemaVersion": 1,
|
|
"label": "coverage",
|
|
"message": "100%",
|
|
"color": "brightgreen",
|
|
}
|
|
|
|
|
|
class TestRenderSvg:
|
|
def test_generates_valid_svg(self) -> None:
|
|
svg = render_svg("coverage", "100%", "brightgreen")
|
|
assert svg.startswith("<svg")
|
|
assert svg.endswith("</svg>\n")
|
|
assert "coverage" in svg
|
|
assert "100%" in svg
|
|
assert COLOR_HEX["brightgreen"] in svg
|
|
|
|
def test_uses_color_hex_for_known_colors(self) -> None:
|
|
svg = render_svg("tests", "573 passing", "brightgreen")
|
|
assert "#4c1" in svg
|
|
|
|
def test_uses_hex_directly_for_unknown_hex_color(self) -> None:
|
|
svg = render_svg("label", "msg", "#abc123")
|
|
assert "#abc123" in svg
|
|
|
|
def test_uses_lightgrey_for_unknown_named_color(self) -> None:
|
|
svg = render_svg("label", "msg", "nonexistent")
|
|
assert "#9f9f9f" in svg
|
|
|
|
def test_escapes_xml_special_chars(self) -> None:
|
|
svg = render_svg("label", "<script>", "red")
|
|
assert "<script>" not in svg
|
|
assert "<script>" in svg
|
|
|
|
def test_has_correct_dimensions(self) -> None:
|
|
svg = render_svg("coverage", "100%", "brightgreen")
|
|
assert 'width="' in svg
|
|
assert 'height="20"' in svg
|
|
|
|
|
|
class TestExtractCoverage:
|
|
def test_extracts_from_total_line(self) -> None:
|
|
output = (
|
|
"src/devx/cli.py 118 0 100%\n"
|
|
"TOTAL 1798 0 100.00%\n"
|
|
)
|
|
assert extract_coverage(output) == 100.0
|
|
|
|
def test_extracts_partial_coverage(self) -> None:
|
|
output = "TOTAL 100 20 80.00%\n"
|
|
assert extract_coverage(output) == 80.0
|
|
|
|
def test_extracts_without_decimal(self) -> None:
|
|
output = "TOTAL 1900 0 100%\n"
|
|
assert extract_coverage(output) == 100.0
|
|
|
|
def test_returns_none_when_no_match(self) -> None:
|
|
assert extract_coverage("no coverage here") is None
|
|
|
|
|
|
class TestExtractTestCount:
|
|
def test_extracts_passed_count(self) -> None:
|
|
assert extract_test_count("543 passed in 2.32s") == 543
|
|
|
|
def test_extracts_with_warnings(self) -> None:
|
|
assert extract_test_count("543 passed, 1 warning in 2.32s") == 543
|
|
|
|
def test_returns_none_when_no_match(self) -> None:
|
|
assert extract_test_count("no tests here") is None
|
|
|
|
|
|
class TestExtractDocCoverage:
|
|
def test_extracts_percentage(self) -> None:
|
|
output = "\nDoc coverage: 20/20 (100%)"
|
|
assert extract_doc_coverage(output) == 100
|
|
|
|
def test_extracts_partial(self) -> None:
|
|
output = "\nDoc coverage: 18/20 (90%)"
|
|
assert extract_doc_coverage(output) == 90
|
|
|
|
def test_returns_none_when_no_match(self) -> None:
|
|
assert extract_doc_coverage("no doc coverage here") is None
|
|
|
|
|
|
class TestCoverageColor:
|
|
def test_100_is_brightgreen(self) -> None:
|
|
assert coverage_color(100.0) == "brightgreen"
|
|
|
|
def test_90_is_green(self) -> None:
|
|
assert coverage_color(90.0) == "green"
|
|
|
|
def test_80_is_yellowgreen(self) -> None:
|
|
assert coverage_color(80.0) == "yellowgreen"
|
|
|
|
def test_70_is_yellow(self) -> None:
|
|
assert coverage_color(70.0) == "yellow"
|
|
|
|
def test_60_is_orange(self) -> None:
|
|
assert coverage_color(60.0) == "orange"
|
|
|
|
def test_below_60_is_red(self) -> None:
|
|
assert coverage_color(50.0) == "red"
|
|
|
|
|
|
class TestDocCoverageColor:
|
|
def test_100_is_brightgreen(self) -> None:
|
|
assert doc_coverage_color(100) == "brightgreen"
|
|
|
|
def test_90_is_green(self) -> None:
|
|
assert doc_coverage_color(90) == "green"
|
|
|
|
def test_80_is_yellowgreen(self) -> None:
|
|
assert doc_coverage_color(80) == "yellowgreen"
|
|
|
|
def test_70_is_yellow(self) -> None:
|
|
assert doc_coverage_color(70) == "yellow"
|
|
|
|
def test_below_70_is_orange(self) -> None:
|
|
assert doc_coverage_color(60) == "orange"
|
|
|
|
|
|
class TestReadVersion:
|
|
def test_reads_version_from_init(self, tmp_path: Path) -> None: # type: ignore[no-untyped-def]
|
|
src = tmp_path / "src" / "mypkg"
|
|
src.mkdir(parents=True)
|
|
(src / "__init__.py").write_text('__version__ = "0.5.0"\n')
|
|
assert read_version(tmp_path) == "0.5.0"
|
|
|
|
def test_returns_unknown_when_no_version(self, tmp_path: Path) -> None: # type: ignore[no-untyped-def]
|
|
src = tmp_path / "src" / "mypkg"
|
|
src.mkdir(parents=True)
|
|
(src / "__init__.py").write_text("no version here\n")
|
|
assert read_version(tmp_path) == "unknown"
|
|
|
|
def test_returns_unknown_when_no_package(self, tmp_path: Path) -> None: # type: ignore[no-untyped-def]
|
|
assert read_version(tmp_path) == "unknown"
|
|
|
|
@patch("devx.tools.generate_badges.detect_package_name", return_value="mypkg")
|
|
def test_returns_unknown_when_init_missing(self, mock_pkg: MagicMock, tmp_path: Path) -> None: # type: ignore[no-untyped-def]
|
|
# Package detected but __init__.py doesn't exist (edge case)
|
|
assert read_version(tmp_path) == "unknown"
|
|
|
|
|
|
class TestCollectCoverageAndTests:
|
|
@patch("devx.tools.generate_badges.run_command")
|
|
@patch("devx.tools.generate_badges.detect_coverage_target", return_value="src/devx")
|
|
def test_extracts_coverage_and_tests(self, mock_target: MagicMock, mock_run: MagicMock, tmp_path: Path) -> None: # type: ignore[no-untyped-def]
|
|
mock_run.return_value = (0, "1018 passed in 4.23s\nTOTAL 3546 0 100%", "")
|
|
cov, tests = collect_coverage_and_tests(tmp_path)
|
|
assert cov["message"] == "100%"
|
|
assert tests["message"] == "1018 passing"
|
|
|
|
@patch("devx.tools.generate_badges.run_command")
|
|
@patch("devx.tools.generate_badges.detect_coverage_target", return_value="src/devx")
|
|
def test_returns_unknown_when_no_match(self, mock_target: MagicMock, mock_run: MagicMock, tmp_path: Path) -> None: # type: ignore[no-untyped-def]
|
|
mock_run.return_value = (1, "garbled output", "some error")
|
|
cov, tests = collect_coverage_and_tests(tmp_path)
|
|
assert cov["message"] == "unknown"
|
|
assert tests["message"] == "unknown"
|
|
|
|
@patch("devx.tools.generate_badges.detect_coverage_target", return_value=None)
|
|
def test_returns_lightgrey_when_no_target(self, mock_target: MagicMock, tmp_path: Path) -> None: # type: ignore[no-untyped-def]
|
|
cov, tests = collect_coverage_and_tests(tmp_path)
|
|
assert cov["message"] == "unknown"
|
|
assert cov["color"] == "lightgrey"
|
|
assert tests["message"] == "unknown"
|
|
assert tests["color"] == "lightgrey"
|
|
|
|
|
|
class TestCollectDocCoverage:
|
|
@patch("devx.tools.generate_badges.run_command")
|
|
def test_extracts_doc_coverage(self, mock_run: MagicMock, tmp_path: Path) -> None: # type: ignore[no-untyped-def]
|
|
mock_run.return_value = (0, "Doc coverage: 20/20 (100%)", "")
|
|
badge = collect_doc_coverage(tmp_path)
|
|
assert badge["message"] == "100%"
|
|
|
|
@patch("devx.tools.generate_badges.run_command")
|
|
def test_returns_unknown_when_no_match(self, mock_run: MagicMock, tmp_path: Path) -> None: # type: ignore[no-untyped-def]
|
|
mock_run.return_value = (1, "no doc coverage", "error")
|
|
badge = collect_doc_coverage(tmp_path)
|
|
assert badge["message"] == "unknown"
|
|
|
|
|
|
class TestCollectQuality:
|
|
@patch("devx.tools.generate_badges.run_command")
|
|
def test_all_pass_returns_a(self, mock_run: MagicMock, tmp_path: Path) -> None: # type: ignore[no-untyped-def]
|
|
mock_run.return_value = (0, "", "")
|
|
badge = collect_quality(tmp_path)
|
|
assert badge["message"] == "A"
|
|
assert badge["color"] == "brightgreen"
|
|
|
|
@patch("devx.tools.generate_badges.run_command")
|
|
def test_lint_failure_returns_f(self, mock_run: MagicMock, tmp_path: Path) -> None: # type: ignore[no-untyped-def]
|
|
mock_run.return_value = (1, "", "some error")
|
|
badge = collect_quality(tmp_path)
|
|
assert badge["message"] == "F"
|
|
assert badge["color"] == "red"
|
|
|
|
@patch("devx.tools.generate_badges.run_command")
|
|
def test_tool_not_installed_counts_as_pass(self, mock_run: MagicMock, tmp_path: Path) -> None: # type: ignore[no-untyped-def]
|
|
mock_run.return_value = (1, "", "No module named ruff")
|
|
badge = collect_quality(tmp_path)
|
|
assert badge["message"] == "A"
|
|
|
|
|
|
class TestGenerateBadges:
|
|
@patch("devx.tools.generate_badges.collect_quality")
|
|
@patch("devx.tools.generate_badges.collect_doc_coverage")
|
|
@patch("devx.tools.generate_badges.collect_coverage_and_tests")
|
|
@patch("devx.tools.generate_badges.read_version", return_value="0.5.0")
|
|
@patch("devx.tools.generate_badges.detect_package_name", return_value="devx")
|
|
def test_generates_all_badge_files(
|
|
self,
|
|
mock_pkg: MagicMock,
|
|
mock_version: MagicMock,
|
|
mock_cov_tests: MagicMock,
|
|
mock_doc: MagicMock,
|
|
mock_quality: MagicMock,
|
|
tmp_path: Path,
|
|
) -> None: # type: ignore[no-untyped-def]
|
|
mock_cov_tests.return_value = (
|
|
make_badge("coverage", "100%", "brightgreen"),
|
|
make_badge("tests", "573 passing", "brightgreen"),
|
|
)
|
|
mock_doc.return_value = make_badge("docs", "100%", "brightgreen")
|
|
mock_quality.return_value = make_badge("code quality", "A", "brightgreen")
|
|
|
|
badges = generate_badges(tmp_path, repo_root=tmp_path)
|
|
|
|
expected = {"coverage", "tests", "docs", "quality", "version", "python"}
|
|
assert set(badges.keys()) == expected
|
|
|
|
for name in expected:
|
|
svg_file = tmp_path / f"{name}.svg"
|
|
assert svg_file.exists()
|
|
content = svg_file.read_text()
|
|
assert content.startswith("<svg")
|
|
assert "</svg>" in content
|
|
|
|
|
|
class TestCli:
|
|
@patch("devx.tools.generate_badges.generate_badges")
|
|
def test_cli_generates_badges(self, mock_gen: MagicMock, tmp_path: Path) -> None: # type: ignore[no-untyped-def]
|
|
mock_gen.return_value = {
|
|
"coverage": make_badge("coverage", "100%", "brightgreen"),
|
|
"tests": make_badge("tests", "573 passing", "brightgreen"),
|
|
}
|
|
runner = CliRunner()
|
|
result = runner.invoke(cli, ["--output-dir", str(tmp_path)])
|
|
assert result.exit_code == 0
|
|
assert "Generating badges" in result.output
|
|
assert "Generated 2 badges" in result.output
|
|
|
|
|
|
def test_main_module_block() -> None:
|
|
import devx.tools.generate_badges as gb
|
|
|
|
with patch.object(gb, "cli") as mock_cli:
|
|
with patch.object(gb, "__name__", "__main__"):
|
|
gb.cli([])
|
|
mock_cli.assert_called_once_with([])
|