191 lines
8.1 KiB
Python
191 lines
8.1 KiB
Python
"""Unit tests for scripts/ci/publish.py."""
|
|
|
|
from unittest.mock import MagicMock, patch
|
|
|
|
import click
|
|
import pytest
|
|
from click.testing import CliRunner
|
|
|
|
from scripts.ci.publish import (
|
|
build_package,
|
|
generate_release_notes,
|
|
main,
|
|
publish_to_pypi,
|
|
)
|
|
from scripts.gitea_cli import TeaCLIError
|
|
|
|
|
|
class TestGenerateReleaseNotes:
|
|
@patch("scripts.ci.publish.subprocess.run")
|
|
@patch("scripts.ci.publish.shutil.which", return_value="/usr/local/bin/git-cliff")
|
|
def test_generates_from_git_cliff(self, mock_which: MagicMock, mock_run: MagicMock) -> None:
|
|
mock_run.return_value = MagicMock(returncode=0, stdout="## v1.0.0\n- feat: x")
|
|
result = generate_release_notes("v1.0.0")
|
|
assert "## v1.0.0" in result
|
|
assert "feat: x" in result
|
|
|
|
@patch("scripts.ci.publish.subprocess.run")
|
|
@patch("scripts.ci.publish.shutil.which", return_value="/usr/local/bin/git-cliff")
|
|
def test_strips_whitespace(self, mock_which: MagicMock, mock_run: MagicMock) -> None:
|
|
mock_run.return_value = MagicMock(returncode=0, stdout=" changelog \n")
|
|
result = generate_release_notes("v1.0.0")
|
|
assert result == "changelog"
|
|
|
|
@patch("scripts.ci.publish.subprocess.run")
|
|
@patch("scripts.ci.publish.shutil.which", return_value="/usr/local/bin/git-cliff")
|
|
def test_falls_back_on_failure(self, mock_which: MagicMock, mock_run: MagicMock) -> None:
|
|
mock_run.return_value = MagicMock(returncode=1, stdout="")
|
|
result = generate_release_notes("v1.0.0")
|
|
assert "Release v1.0.0" in result
|
|
assert "CHANGELOG.md" in result
|
|
|
|
@patch("scripts.ci.publish.subprocess.run")
|
|
@patch("scripts.ci.publish.shutil.which", return_value="/usr/local/bin/git-cliff")
|
|
def test_falls_back_on_empty_output(self, mock_which: MagicMock, mock_run: MagicMock) -> None:
|
|
mock_run.return_value = MagicMock(returncode=0, stdout=" ")
|
|
result = generate_release_notes("v1.0.0")
|
|
assert "Release v1.0.0" in result
|
|
|
|
@patch("scripts.ci.publish.subprocess.run", side_effect=FileNotFoundError)
|
|
@patch("scripts.ci.publish.shutil.which", return_value="/usr/local/bin/git-cliff")
|
|
def test_falls_back_on_file_not_found(self, mock_which: MagicMock, mock_run: MagicMock) -> None:
|
|
result = generate_release_notes("v1.0.0")
|
|
assert "Release v1.0.0" in result
|
|
assert "CHANGELOG.md" in result
|
|
|
|
@patch("scripts.ci.publish.shutil.which", return_value=None)
|
|
def test_falls_back_when_not_installed(self, mock_which: MagicMock) -> None:
|
|
result = generate_release_notes("v1.0.0")
|
|
assert "Release v1.0.0" in result
|
|
assert "CHANGELOG.md" in result
|
|
|
|
|
|
class TestBuildPackage:
|
|
@patch("scripts.ci.publish.subprocess.run")
|
|
def test_success(self, mock_run: MagicMock) -> None:
|
|
mock_run.return_value = MagicMock(returncode=0, stderr="")
|
|
build_package()
|
|
args, _ = mock_run.call_args
|
|
assert args[0][1] == "-m"
|
|
assert args[0][2] == "build"
|
|
|
|
@patch("scripts.ci.publish.subprocess.run")
|
|
def test_failure_raises(self, mock_run: MagicMock) -> None:
|
|
mock_run.return_value = MagicMock(returncode=1, stderr="build error")
|
|
with pytest.raises(click.ClickException) as exc:
|
|
build_package()
|
|
assert "build" in str(exc.value)
|
|
|
|
|
|
class TestPublishToPypi:
|
|
@patch("scripts.ci.publish.subprocess.run")
|
|
def test_success(self, mock_run: MagicMock) -> None:
|
|
mock_run.return_value = MagicMock(returncode=0, stderr="")
|
|
publish_to_pypi("pypi-tok")
|
|
args, _ = mock_run.call_args
|
|
assert "twine" in args[0]
|
|
assert "pypi-tok" in args[0]
|
|
|
|
@patch("scripts.ci.publish.subprocess.run")
|
|
def test_failure_raises(self, mock_run: MagicMock) -> None:
|
|
mock_run.return_value = MagicMock(returncode=1, stderr="upload failed")
|
|
with pytest.raises(click.ClickException) as exc:
|
|
publish_to_pypi("pypi-tok")
|
|
assert "PyPI" in str(exc.value)
|
|
|
|
|
|
class TestMain:
|
|
@patch.dict("os.environ", {"REPO_TOKEN": "gitea-tok", "PYPI_TOKEN": "pypi-tok"})
|
|
@patch("scripts.ci.publish.generate_release_notes", return_value="Release notes")
|
|
@patch("scripts.ci.publish.TeaCLI")
|
|
@patch("scripts.ci.publish.publish_to_pypi")
|
|
@patch("scripts.ci.publish.build_package")
|
|
def test_full_flow_with_pypi(
|
|
self,
|
|
mock_build: MagicMock,
|
|
mock_publish: MagicMock,
|
|
mock_tea_cls: MagicMock,
|
|
mock_notes: MagicMock,
|
|
) -> None:
|
|
mock_tea = MagicMock()
|
|
mock_tea_cls.return_value = mock_tea
|
|
runner = CliRunner()
|
|
result = runner.invoke(main, ["v1.0.0", "owner/repo"])
|
|
assert result.exit_code == 0
|
|
assert "Gitea release v1.0.0 created" in result.output
|
|
mock_build.assert_called_once()
|
|
mock_publish.assert_called_once_with("pypi-tok")
|
|
mock_tea.create_release.assert_called_once_with(
|
|
"owner/repo", tag="v1.0.0", title="v1.0.0", body="Release notes"
|
|
)
|
|
|
|
@patch.dict("os.environ", {"REPO_TOKEN": "gitea-tok"}, clear=True)
|
|
@patch("scripts.ci.publish.generate_release_notes", return_value="Release notes")
|
|
@patch("scripts.ci.publish.TeaCLI")
|
|
@patch("scripts.ci.publish.build_package")
|
|
def test_without_pypi(
|
|
self,
|
|
mock_build: MagicMock,
|
|
mock_tea_cls: MagicMock,
|
|
mock_notes: MagicMock,
|
|
) -> None:
|
|
mock_tea = MagicMock()
|
|
mock_tea_cls.return_value = mock_tea
|
|
runner = CliRunner()
|
|
result = runner.invoke(main, ["v1.0.0", "owner/repo"])
|
|
assert result.exit_code == 0
|
|
mock_build.assert_called_once()
|
|
mock_tea.create_release.assert_called_once()
|
|
assert "PYPI_TOKEN not set" in result.output
|
|
|
|
@patch.dict("os.environ", {"REPO_TOKEN": ""}, clear=True)
|
|
def test_missing_repo_token_exits(self) -> None:
|
|
runner = CliRunner()
|
|
result = runner.invoke(main, ["v1.0.0", "owner/repo"])
|
|
assert result.exit_code == 1
|
|
assert "REPO_TOKEN" in result.output
|
|
|
|
@patch.dict("os.environ", {"REPO_TOKEN": "gitea-tok", "PYPI_TOKEN": "pypi-tok"})
|
|
@patch("scripts.ci.publish.generate_release_notes", return_value="Release notes")
|
|
@patch("scripts.ci.publish.TeaCLI")
|
|
@patch("scripts.ci.publish.publish_to_pypi")
|
|
@patch("scripts.ci.publish.build_package")
|
|
def test_build_failure_raises_click(
|
|
self, mock_build: MagicMock, mock_publish: MagicMock, mock_tea_cls: MagicMock, mock_notes: MagicMock
|
|
) -> None:
|
|
mock_build.side_effect = click.ClickException("build failed")
|
|
runner = CliRunner()
|
|
result = runner.invoke(main, ["v1.0.0", "owner/repo"])
|
|
assert result.exit_code == 1
|
|
assert "build" in result.output
|
|
|
|
@patch.dict("os.environ", {"REPO_TOKEN": "gitea-tok", "PYPI_TOKEN": "pypi-tok"})
|
|
@patch("scripts.ci.publish.generate_release_notes", return_value="Release notes")
|
|
@patch("scripts.ci.publish.TeaCLI")
|
|
@patch("scripts.ci.publish.publish_to_pypi")
|
|
@patch("scripts.ci.publish.build_package")
|
|
def test_publish_failure_raises_click(
|
|
self, mock_build: MagicMock, mock_publish: MagicMock, mock_tea_cls: MagicMock, mock_notes: MagicMock
|
|
) -> None:
|
|
mock_publish.side_effect = click.ClickException("publish failed")
|
|
runner = CliRunner()
|
|
result = runner.invoke(main, ["v1.0.0", "owner/repo"])
|
|
assert result.exit_code == 1
|
|
assert "publish" in result.output
|
|
|
|
@patch.dict("os.environ", {"REPO_TOKEN": "gitea-tok", "PYPI_TOKEN": "pypi-tok"})
|
|
@patch("scripts.ci.publish.generate_release_notes", return_value="Release notes")
|
|
@patch("scripts.ci.publish.TeaCLI")
|
|
@patch("scripts.ci.publish.publish_to_pypi")
|
|
@patch("scripts.ci.publish.build_package")
|
|
def test_release_failure_raises_click(
|
|
self, mock_build: MagicMock, mock_publish: MagicMock, mock_tea_cls: MagicMock, mock_notes: MagicMock
|
|
) -> None:
|
|
mock_tea = MagicMock()
|
|
mock_tea.create_release.side_effect = TeaCLIError("server error")
|
|
mock_tea_cls.return_value = mock_tea
|
|
runner = CliRunner()
|
|
result = runner.invoke(main, ["v1.0.0", "owner/repo"])
|
|
assert result.exit_code == 1
|
|
assert "Release creation failed" in result.output
|