Public Access
Post-merge / detect-type (push) Failing after 9s
Post-merge / validate-commit-msg (push) Has been skipped
Post-merge / release (push) Has been skipped
Post-merge / sync-wiki (push) Has been skipped
Post-merge / vikunja (push) Has been skipped
Post-merge / configure-repo (push) Has been skipped
Post-merge / badges (push) Failing after 25s
Port core modules (config, exceptions, i18n, api_clients, gitea_cli), 14 CI scripts, 6 dev tools, 5 molecule tools, CLI entry point, workflows, Makefile, tests (784 tests, 100% coverage), and documentation from GRM. The devx package is published to the Gitea PyPI registry and consumed by GRM, infra, and other oblachno-oss projects as a pip dependency. Generated with [Devin](https://devin.ai) Co-Authored-By: Devin <158243242+devin-ai-integration[bot]@users.noreply.github.com>
352 lines
13 KiB
Python
352 lines
13 KiB
Python
"""Unit tests for scripts/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,
|
|
coverage_color,
|
|
doc_coverage_color,
|
|
extract_coverage,
|
|
extract_doc_coverage,
|
|
extract_test_count,
|
|
generate_badges,
|
|
make_badge,
|
|
read_version,
|
|
render_svg,
|
|
run_command,
|
|
)
|
|
|
|
|
|
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:
|
|
@patch("devx.tools.generate_badges._find_package_init")
|
|
def test_reads_version_from_init(self, mock_find: MagicMock) -> None:
|
|
mock_init = MagicMock()
|
|
mock_init.read_text.return_value = '__version__ = "0.5.0"\n'
|
|
mock_find.return_value = mock_init
|
|
assert read_version() == "0.5.0"
|
|
|
|
@patch("devx.tools.generate_badges._find_package_init")
|
|
def test_returns_unknown_when_no_version(self, mock_find: MagicMock) -> None:
|
|
mock_init = MagicMock()
|
|
mock_init.read_text.return_value = "no version here\n"
|
|
mock_find.return_value = mock_init
|
|
assert read_version() == "unknown"
|
|
|
|
@patch("devx.tools.generate_badges._find_package_init", return_value=None)
|
|
def test_returns_unknown_when_no_init(self, mock_find: MagicMock) -> None:
|
|
assert read_version() == "unknown"
|
|
|
|
|
|
class TestFindPackageInit:
|
|
@patch("devx.tools.generate_badges.REPO_ROOT")
|
|
def test_no_src_dir(self, mock_root: MagicMock) -> None:
|
|
"""Returns None when src/ directory doesn't exist."""
|
|
from devx.tools.generate_badges import _find_package_init
|
|
|
|
mock_src = MagicMock()
|
|
mock_src.exists.return_value = False
|
|
mock_root.__truediv__ = MagicMock(return_value=mock_src)
|
|
assert _find_package_init() is None
|
|
|
|
@patch("devx.tools.generate_badges.REPO_ROOT")
|
|
def test_no_version_in_init_files(self, mock_root: MagicMock, tmp_path: Path) -> None:
|
|
"""Returns None when no __init__.py has __version__."""
|
|
from devx.tools.generate_badges import _find_package_init
|
|
|
|
src_dir = tmp_path / "src"
|
|
src_dir.mkdir()
|
|
(src_dir / "__init__.py").write_text("# no version here\n")
|
|
mock_root.__truediv__ = MagicMock(return_value=src_dir)
|
|
assert _find_package_init() is None
|
|
|
|
@patch("devx.tools.generate_badges.REPO_ROOT")
|
|
def test_finds_init_with_version(self, mock_root: MagicMock, tmp_path: Path) -> None:
|
|
"""Returns the __init__.py that has __version__."""
|
|
from devx.tools.generate_badges import _find_package_init
|
|
|
|
src_dir = tmp_path / "src"
|
|
pkg_dir = src_dir / "mypkg"
|
|
pkg_dir.mkdir(parents=True)
|
|
(src_dir / "__init__.py").write_text("# no version\n")
|
|
(pkg_dir / "__init__.py").write_text('__version__ = "1.0.0"\n')
|
|
mock_root.__truediv__ = MagicMock(return_value=src_dir)
|
|
result = _find_package_init()
|
|
assert result is not None
|
|
assert "__version__" in result.read_text()
|
|
|
|
@patch("devx.tools.generate_badges.REPO_ROOT")
|
|
def test_handles_oserror(self, mock_root: MagicMock, tmp_path: Path) -> None:
|
|
"""Handles OSError when reading init files."""
|
|
from devx.tools.generate_badges import _find_package_init
|
|
|
|
src_dir = tmp_path / "src"
|
|
src_dir.mkdir()
|
|
init_file = src_dir / "__init__.py"
|
|
init_file.write_text('__version__ = "1.0.0"\n')
|
|
mock_root.__truediv__ = MagicMock(return_value=src_dir)
|
|
# Patch Path.read_text to raise OSError
|
|
with patch.object(Path, "read_text", side_effect=OSError("permission denied")):
|
|
result = _find_package_init()
|
|
assert result is None
|
|
|
|
|
|
class TestGenerateBadges:
|
|
@patch("devx.tools.generate_badges.run_command")
|
|
@patch("devx.tools.generate_badges.read_version", return_value="0.5.0")
|
|
@patch("devx.tools.generate_badges.extract_coverage", return_value=100.0)
|
|
@patch("devx.tools.generate_badges.extract_test_count", return_value=573)
|
|
@patch("devx.tools.generate_badges.extract_doc_coverage", return_value=100)
|
|
def test_generates_all_badge_files(
|
|
self,
|
|
mock_doc_cov: MagicMock,
|
|
mock_test_count: MagicMock,
|
|
mock_cov: MagicMock,
|
|
mock_version: MagicMock,
|
|
mock_run: MagicMock,
|
|
tmp_path: Path,
|
|
) -> None:
|
|
mock_run.return_value = (0, "output", "")
|
|
badges = generate_badges(tmp_path)
|
|
|
|
expected = {"coverage", "tests", "docs", "quality", "version", "python"}
|
|
assert set(badges.keys()) == expected
|
|
|
|
# Verify SVG files were written
|
|
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
|
|
|
|
@patch("devx.tools.generate_badges.run_command")
|
|
@patch("devx.tools.generate_badges.read_version", return_value="0.5.0")
|
|
@patch("devx.tools.generate_badges.extract_coverage", return_value=100.0)
|
|
@patch("devx.tools.generate_badges.extract_test_count", return_value=573)
|
|
@patch("devx.tools.generate_badges.extract_doc_coverage", return_value=100)
|
|
def test_quality_badge_pass_when_all_lint_passes(
|
|
self,
|
|
mock_doc_cov: MagicMock,
|
|
mock_test_count: MagicMock,
|
|
mock_cov: MagicMock,
|
|
mock_version: MagicMock,
|
|
mock_run: MagicMock,
|
|
tmp_path: Path,
|
|
) -> None:
|
|
mock_run.return_value = (0, "output", "")
|
|
badges = generate_badges(tmp_path)
|
|
assert badges["quality"]["message"] == "A"
|
|
assert badges["quality"]["color"] == "brightgreen"
|
|
|
|
@patch("devx.tools.generate_badges.run_command")
|
|
@patch("devx.tools.generate_badges.read_version", return_value="0.5.0")
|
|
@patch("devx.tools.generate_badges.extract_coverage", return_value=100.0)
|
|
@patch("devx.tools.generate_badges.extract_test_count", return_value=573)
|
|
@patch("devx.tools.generate_badges.extract_doc_coverage", return_value=100)
|
|
def test_quality_badge_fails_when_lint_fails(
|
|
self,
|
|
mock_doc_cov: MagicMock,
|
|
mock_test_count: MagicMock,
|
|
mock_cov: MagicMock,
|
|
mock_version: MagicMock,
|
|
mock_run: MagicMock,
|
|
tmp_path: Path,
|
|
) -> None:
|
|
mock_run.side_effect = [
|
|
(0, "output", ""),
|
|
(0, "output", ""),
|
|
(1, "error", ""),
|
|
(0, "output", ""),
|
|
(0, "output", ""),
|
|
(0, "output", ""),
|
|
]
|
|
badges = generate_badges(tmp_path)
|
|
assert badges["quality"]["message"] == "F"
|
|
assert badges["quality"]["color"] == "red"
|
|
|
|
@patch("devx.tools.generate_badges.run_command")
|
|
@patch("devx.tools.generate_badges.read_version", return_value="0.5.0")
|
|
@patch("devx.tools.generate_badges.extract_coverage", return_value=None)
|
|
@patch("devx.tools.generate_badges.extract_test_count", return_value=None)
|
|
@patch("devx.tools.generate_badges.extract_doc_coverage", return_value=None)
|
|
def test_badges_show_unknown_when_extraction_fails(
|
|
self,
|
|
mock_doc_cov: MagicMock,
|
|
mock_test_count: MagicMock,
|
|
mock_cov: MagicMock,
|
|
mock_version: MagicMock,
|
|
mock_run: MagicMock,
|
|
tmp_path: Path,
|
|
) -> None:
|
|
mock_run.return_value = (1, "garbled output", "")
|
|
badges = generate_badges(tmp_path)
|
|
assert badges["coverage"]["message"] == "unknown"
|
|
assert badges["coverage"]["color"] == "red"
|
|
assert badges["tests"]["message"] == "unknown"
|
|
assert badges["tests"]["color"] == "red"
|
|
assert badges["docs"]["message"] == "unknown"
|
|
assert badges["docs"]["color"] == "red"
|
|
|
|
|
|
class TestCli:
|
|
@patch("devx.tools.generate_badges.generate_badges")
|
|
def test_cli_generates_badges(self, mock_gen: MagicMock, tmp_path: Path) -> None:
|
|
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
|
|
mock_gen.assert_called_once_with(tmp_path)
|
|
|
|
|
|
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([])
|