Files
devx/tests/unit/test_push_badges.py
T
emilandDevin <158243242+devin-ai-integration[bot]@users.noreply.github.com> 60fd11419c
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
feat: extract reusable dev/CI tools from GRM into devx package
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>
2026-06-22 17:01:20 +02:00

226 lines
9.3 KiB
Python

from __future__ import annotations
from pathlib import Path
from unittest.mock import MagicMock, patch
import pytest
from click import ClickException
from click.testing import CliRunner
import devx.ci.push_badges as push_badges
class TestFetchLatestMaster:
def test_fetch_and_reset(self) -> None:
with patch("subprocess.run") as mock_run:
push_badges.fetch_latest_master("master")
# Should call git fetch and git reset --hard
calls = [str(c.args[0]) for c in mock_run.call_args_list]
assert any("fetch" in c for c in calls)
assert any("reset" in c for c in calls)
def test_custom_branch(self) -> None:
with patch("subprocess.run") as mock_run:
push_badges.fetch_latest_master("develop")
calls = [list(c.args[0]) for c in mock_run.call_args_list]
# fetch call should include the branch name
fetch_call = [c for c in calls if "fetch" in c][0]
assert "develop" in fetch_call
# reset call should include origin/develop
reset_call = [c for c in calls if "reset" in c][0]
assert "origin/develop" in reset_call
def test_fetch_failure_raises(self) -> None:
import subprocess
with patch("subprocess.run", side_effect=subprocess.CalledProcessError(1, "git fetch")):
with pytest.raises(subprocess.CalledProcessError):
push_badges.fetch_latest_master()
class TestGenerateBadges:
def test_success(self, tmp_path: Path) -> None:
output_dir = tmp_path / ".badges"
output_dir.mkdir()
(output_dir / "badge1.svg").touch()
with patch("subprocess.run") as mock_run:
push_badges.generate_badges(str(output_dir))
mock_run.assert_called_once()
def test_no_badges_generated(self, tmp_path: Path) -> None:
output_dir = tmp_path / ".badges"
output_dir.mkdir()
with patch("subprocess.run"):
with pytest.raises(ClickException, match="No badge SVG files generated"):
push_badges.generate_badges(str(output_dir))
class TestPushToBadgesBranch:
def test_success(self, tmp_path: Path, monkeypatch: pytest.MonkeyPatch) -> None:
monkeypatch.chdir(tmp_path)
badges_dir = tmp_path / ".badges"
badges_dir.mkdir()
svg = badges_dir / "badge1.svg"
svg.write_text("<svg></svg>")
sha_result = MagicMock()
sha_result.stdout = "abc123\n"
default_result = MagicMock()
with patch(
"subprocess.run",
side_effect=[default_result] * 7 + [sha_result],
) as mock_run:
sha = push_badges.push_to_badges_branch(str(badges_dir))
# Should have called git config x2, checkout, rm, add, commit, push, rev-parse
assert mock_run.call_count >= 8
# Verify the SVG was copied to cwd
assert (tmp_path / "badge1.svg").exists()
assert sha == "abc123"
class TestUpdateBadgeUrls:
def test_replaces_branch_url(self) -> None:
content = "[![Tests](https://git.oblachno.oblachno.fyi/oblachno-oss/grm/raw/branch/badges/tests.svg)]"
result = push_badges.update_badge_urls(content, "abc123def456")
assert "raw/commit/abc123def456/tests.svg" in result
assert "raw/branch/badges" not in result
def test_replaces_commit_url(self) -> None:
"""Old commit SHA URLs should be replaced with the new one."""
old_sha = "aabb123456789012345678901234567890123456" # 40 hex chars
new_sha = "ccdd123456789012345678901234567890123456" # 40 hex chars
content = f"[![Tests](https://git.oblachno.oblachno.fyi/oblachno-oss/grm/raw/commit/{old_sha}/tests.svg)]"
result = push_badges.update_badge_urls(content, new_sha)
assert f"raw/commit/{new_sha}/tests.svg" in result
assert old_sha not in result
def test_no_badge_urls(self) -> None:
content = "# No badges here\nJust text."
result = push_badges.update_badge_urls(content, "abc123")
assert result == content
def test_multiple_badges(self) -> None:
content = (
"[![Coverage](https://git.oblachno.oblachno.fyi/oblachno-oss/grm/raw/branch/badges/coverage.svg)]\n"
"[![Tests](https://git.oblachno.oblachno.fyi/oblachno-oss/grm/raw/branch/badges/tests.svg)]\n"
"[![Version](https://git.oblachno.oblachno.fyi/oblachno-oss/grm/raw/branch/badges/version.svg)]"
)
result = push_badges.update_badge_urls(content, "abc123def456")
assert result.count("raw/commit/abc123def456/") == 3
assert "raw/branch/badges" not in result
def test_preserves_non_badge_urls(self) -> None:
content = "[![CI](https://git.oblachno.oblachno.fyi/oblachno-oss/grm/actions/workflows/ci.yml/badge.svg)]"
result = push_badges.update_badge_urls(content, "abc123")
assert result == content
class TestUpdateReadmeWithBadgeSha:
def test_updates_readme(self, tmp_path: Path) -> None:
readme = tmp_path / "README.md"
readme.write_text("[![Tests](https://git.oblachno.oblachno.fyi/oblachno-oss/grm/raw/branch/badges/tests.svg)]")
with patch("subprocess.run"):
push_badges.update_readme_with_badge_sha("abc123def456", repo_root=tmp_path)
content = readme.read_text()
assert "raw/commit/abc123def456/tests.svg" in content
def test_no_badge_urls_skips_commit(self, tmp_path: Path) -> None:
readme = tmp_path / "README.md"
readme.write_text("# No badges here")
with patch("subprocess.run") as mock_run:
push_badges.update_readme_with_badge_sha("abc123def456", repo_root=tmp_path)
# Should checkout master but not commit/push
calls = [list(c.args[0]) for c in mock_run.call_args_list]
assert not any("commit" in c for c in calls)
assert not any("push" in c for c in calls)
def test_missing_readme_skips(self, tmp_path: Path) -> None:
with patch("subprocess.run"):
push_badges.update_readme_with_badge_sha("abc123def456", repo_root=tmp_path)
# Should not raise
class TestMain:
def test_success(self, tmp_path: Path, monkeypatch: pytest.MonkeyPatch) -> None:
monkeypatch.chdir(tmp_path)
badges_dir = tmp_path / ".badges"
badges_dir.mkdir()
(badges_dir / "badge1.svg").touch()
runner = CliRunner()
with patch("subprocess.run"):
result = runner.invoke(push_badges.main, ["--output-dir", str(badges_dir), "--no-readme-update"])
assert result.exit_code == 0
def test_no_badges(self, tmp_path: Path, monkeypatch: pytest.MonkeyPatch) -> None:
monkeypatch.chdir(tmp_path)
badges_dir = tmp_path / ".badges"
badges_dir.mkdir()
runner = CliRunner()
with patch("subprocess.run"):
result = runner.invoke(push_badges.main, ["--output-dir", str(badges_dir), "--no-readme-update"])
assert result.exit_code != 0
def test_custom_branch(self, tmp_path: Path, monkeypatch: pytest.MonkeyPatch) -> None:
monkeypatch.chdir(tmp_path)
badges_dir = tmp_path / ".badges"
badges_dir.mkdir()
(badges_dir / "badge1.svg").touch()
runner = CliRunner()
with patch("subprocess.run") as mock_run:
result = runner.invoke(
push_badges.main,
["--output-dir", str(badges_dir), "--branch", "develop", "--no-readme-update"],
)
assert result.exit_code == 0
# Verify fetch was called with the custom branch
calls = [list(c.args[0]) for c in mock_run.call_args_list]
fetch_call = [c for c in calls if "fetch" in c][0]
assert "develop" in fetch_call
def test_fetch_failure(self, tmp_path: Path, monkeypatch: pytest.MonkeyPatch) -> None:
monkeypatch.chdir(tmp_path)
import subprocess
runner = CliRunner()
with patch("subprocess.run", side_effect=subprocess.CalledProcessError(1, "git fetch")):
result = runner.invoke(push_badges.main, ["--no-readme-update"])
assert result.exit_code != 0
def test_readme_update_called_by_default(self, tmp_path: Path, monkeypatch: pytest.MonkeyPatch) -> None:
"""Without --no-readme-update, update_readme_with_badge_sha is called."""
monkeypatch.chdir(tmp_path)
badges_dir = tmp_path / ".badges"
badges_dir.mkdir()
(badges_dir / "badge1.svg").touch()
runner = CliRunner()
with (
patch("subprocess.run"),
patch("devx.ci.push_badges.update_readme_with_badge_sha") as mock_update,
):
result = runner.invoke(push_badges.main, ["--output-dir", str(badges_dir)])
assert result.exit_code == 0
mock_update.assert_called_once()
def test_readme_update_skipped_with_flag(self, tmp_path: Path, monkeypatch: pytest.MonkeyPatch) -> None:
"""With --no-readme-update, update_readme_with_badge_sha is not called."""
monkeypatch.chdir(tmp_path)
badges_dir = tmp_path / ".badges"
badges_dir.mkdir()
(badges_dir / "badge1.svg").touch()
runner = CliRunner()
with (
patch("subprocess.run"),
patch("devx.ci.push_badges.update_readme_with_badge_sha") as mock_update,
):
result = runner.invoke(push_badges.main, ["--output-dir", str(badges_dir), "--no-readme-update"])
assert result.exit_code == 0
mock_update.assert_not_called()