108 lines
3.9 KiB
Python
108 lines
3.9 KiB
Python
"""Tests for scripts/create_release.py — idempotent release creation (#192).
|
|
|
|
Pure tests cover payload construction and the tag-exists check; the dry-run
|
|
mode is exercised end-to-end with monkeypatched env (no API calls). The
|
|
script-level error path (finding 8, #192) runs the real entry point as a
|
|
subprocess against a refused port.
|
|
"""
|
|
|
|
from __future__ import annotations
|
|
|
|
import importlib.util
|
|
import json
|
|
import os
|
|
import socket
|
|
import subprocess
|
|
import sys
|
|
from pathlib import Path
|
|
from typing import Any
|
|
|
|
import pytest
|
|
|
|
ROOT = Path(__file__).resolve().parents[1]
|
|
|
|
|
|
def _load_module() -> Any:
|
|
spec = importlib.util.spec_from_file_location("create_release", ROOT / "scripts" / "create_release.py")
|
|
assert spec is not None and spec.loader is not None
|
|
module = importlib.util.module_from_spec(spec)
|
|
spec.loader.exec_module(module)
|
|
return module
|
|
|
|
|
|
cr = _load_module()
|
|
|
|
|
|
class TestBuildReleasePayload:
|
|
def test_with_pr_context(self) -> None:
|
|
payload = cr.build_release_payload("0.56.0", "feat: widget", "Some body")
|
|
assert payload == {
|
|
"tag_name": "v0.56.0",
|
|
"target_commitish": "master",
|
|
"name": "Rune v0.56.0",
|
|
"body": "## feat: widget\n\nSome body",
|
|
"draft": False,
|
|
"prerelease": False,
|
|
}
|
|
|
|
def test_without_pr_context_falls_back_to_safety_net_body(self) -> None:
|
|
payload = cr.build_release_payload("0.56.0", "", "")
|
|
assert payload["tag_name"] == "v0.56.0"
|
|
assert "safety net" in payload["body"]
|
|
assert payload["draft"] is False
|
|
assert payload["prerelease"] is False
|
|
|
|
|
|
class TestReleaseExists:
|
|
def test_matches_tag_name(self) -> None:
|
|
releases = [{"tag_name": "v0.54.0"}, {"tag_name": "v0.55.0"}]
|
|
assert cr.release_exists(releases, "0.55.0") is True
|
|
assert cr.release_exists(releases, "0.56.0") is False
|
|
assert cr.release_exists([], "0.55.0") is False
|
|
|
|
|
|
class TestURLErrorCaughtAtScriptLevel:
|
|
"""Finding 8 (#192): __main__ catches only HTTPError, so a refused
|
|
connection (URLError) escapes and crashes with a traceback instead of
|
|
the consistent error path. Runs the real entry point as a subprocess."""
|
|
|
|
def test_refused_connection_exits_1_with_message_not_traceback(self, tmp_path: Path) -> None:
|
|
with socket.socket() as s:
|
|
s.bind(("127.0.0.1", 0))
|
|
refused_port = s.getsockname()[1] # bound but never listening → ECONNREFUSED
|
|
env = dict(os.environ)
|
|
env.update(
|
|
RELEASE_VERSION="0.56.0",
|
|
GITEA_TOKEN="test-token",
|
|
RELEASE_SERVER_URL=f"http://127.0.0.1:{refused_port}",
|
|
RELEASE_REPO="rune/rune",
|
|
LIST_API_URL=f"http://127.0.0.1:{refused_port}/api/v1/repos/rune/rune/releases?limit=50",
|
|
RELEASE_API_URL=f"http://127.0.0.1:{refused_port}/api/v1/repos/rune/rune/releases",
|
|
)
|
|
env.pop("PYTEST_CURRENT_TEST", None)
|
|
proc = subprocess.run(
|
|
[sys.executable, str(ROOT / "scripts" / "create_release.py")],
|
|
env=env,
|
|
capture_output=True,
|
|
text=True,
|
|
timeout=60,
|
|
)
|
|
assert proc.returncode == 1, f"expected exit 1, stderr:\n{proc.stderr}"
|
|
assert proc.stderr.startswith("create_release: API failed")
|
|
assert "Traceback" not in proc.stderr
|
|
|
|
|
|
class TestDryRunMain:
|
|
def test_prints_payload_without_api_calls(
|
|
self, monkeypatch: pytest.MonkeyPatch, capsys: pytest.CaptureFixture[str]
|
|
) -> None:
|
|
monkeypatch.setenv("RELEASE_VERSION", "0.56.0")
|
|
monkeypatch.delenv("GITEA_TOKEN", raising=False)
|
|
monkeypatch.delenv("PR_TITLE", raising=False)
|
|
rc = cr.main(["--dry-run"])
|
|
assert rc == 0
|
|
payload = json.loads(capsys.readouterr().out)
|
|
assert payload["tag_name"] == "v0.56.0"
|
|
assert payload["target_commitish"] == "master"
|
|
assert "safety net" in payload["body"]
|