All checks were successful
CI / lint (push) Successful in 34s
Deploy / notebooks (push) Has been skipped
Deploy / zotero (push) Has been skipped
Deploy / docs (push) Has been skipped
Deploy / api (push) Has been skipped
Deploy / mc (push) Has been skipped
Deploy / report (push) Successful in 30s
CI / test (push) Successful in 29m15s
278 lines
8.5 KiB
Python
278 lines
8.5 KiB
Python
"""Tests for the backend-agnostic CI failure reporter."""
|
|
|
|
from __future__ import annotations
|
|
|
|
from unittest.mock import MagicMock, patch
|
|
|
|
|
|
class TestFileCiFailure:
|
|
"""file_ci_failure builds correct issue title and body."""
|
|
|
|
@patch("api.diag.ci._get_token", return_value="fake-token")
|
|
@patch("api.clients.gitea.GiteaClient")
|
|
def test_files_issue_with_correct_title(self, mock_cls, mock_token):
|
|
mock_client = MagicMock()
|
|
mock_client.create_issue.return_value = {"number": 999}
|
|
mock_client.resolve_labels.return_value = [1]
|
|
mock_cls.return_value = mock_client
|
|
|
|
from api.diag.ci import file_ci_failure
|
|
|
|
result = file_ci_failure(
|
|
workflow="Deploy",
|
|
job="build-scan-report",
|
|
run="42",
|
|
sha="abc12345def67890",
|
|
ref="refs/heads/main",
|
|
)
|
|
|
|
assert result is not None
|
|
assert result["number"] == 999
|
|
|
|
call_args = mock_client.create_issue.call_args
|
|
issue_body = call_args[0][2]
|
|
assert "Deploy" in issue_body["title"]
|
|
assert "build-scan-report" in issue_body["title"]
|
|
assert "abc12345" in issue_body["title"]
|
|
assert "#42" in issue_body["title"]
|
|
|
|
@patch("api.diag.ci._get_token", return_value="fake-token")
|
|
@patch("api.clients.gitea.GiteaClient")
|
|
def test_body_includes_metadata(self, mock_cls, mock_token):
|
|
mock_client = MagicMock()
|
|
mock_client.create_issue.return_value = {"number": 1}
|
|
mock_client.resolve_labels.return_value = []
|
|
mock_cls.return_value = mock_client
|
|
|
|
from api.diag.ci import file_ci_failure
|
|
|
|
file_ci_failure(
|
|
workflow="CI",
|
|
job="lint-test",
|
|
run="99",
|
|
sha="deadbeef",
|
|
ref="refs/heads/feature",
|
|
)
|
|
|
|
issue_body = mock_client.create_issue.call_args[0][2]
|
|
body = issue_body["body"]
|
|
assert "CI" in body
|
|
assert "lint-test" in body
|
|
assert "#99" in body
|
|
assert "deadbeef" in body
|
|
assert "refs/heads/feature" in body
|
|
|
|
@patch("api.diag.ci._get_token", return_value="fake-token")
|
|
@patch("api.clients.gitea.GiteaClient")
|
|
def test_resolves_pipeline_label(self, mock_cls, mock_token):
|
|
mock_client = MagicMock()
|
|
mock_client.create_issue.return_value = {"number": 2}
|
|
mock_client.resolve_labels.return_value = [5, 10]
|
|
mock_cls.return_value = mock_client
|
|
|
|
from api.diag.ci import file_ci_failure
|
|
|
|
file_ci_failure(
|
|
workflow="Deploy",
|
|
job="build",
|
|
run="1",
|
|
sha="aaa",
|
|
ref="",
|
|
)
|
|
|
|
label_call = mock_client.resolve_labels.call_args
|
|
label_names = label_call[0][2]
|
|
assert "ci" in label_names
|
|
assert "pipeline:deploy" in label_names
|
|
|
|
@patch("api.diag.ci._get_token", return_value="")
|
|
def test_no_token_returns_none(self, mock_token):
|
|
from api.diag.ci import file_ci_failure
|
|
|
|
result = file_ci_failure(
|
|
workflow="CI",
|
|
job="test",
|
|
run="1",
|
|
sha="aaa",
|
|
ref="",
|
|
)
|
|
assert result is None
|
|
|
|
|
|
class TestCLI:
|
|
"""CLI entry point parses args correctly."""
|
|
|
|
@patch("api.diag.ci.file_ci_failure")
|
|
def test_main_calls_file_ci_failure(self, mock_file):
|
|
mock_file.return_value = {"number": 1}
|
|
import sys
|
|
|
|
from api.diag.ci import main
|
|
|
|
old_argv = sys.argv
|
|
sys.argv = [
|
|
"ci",
|
|
"--workflow",
|
|
"CI",
|
|
"--job",
|
|
"lint-test",
|
|
"--run",
|
|
"5",
|
|
"--sha",
|
|
"abc123",
|
|
"--ref",
|
|
"refs/heads/main",
|
|
]
|
|
try:
|
|
code = main()
|
|
finally:
|
|
sys.argv = old_argv
|
|
|
|
assert code == 0
|
|
mock_file.assert_called_once()
|
|
kwargs = mock_file.call_args[1]
|
|
assert kwargs["workflow"] == "CI"
|
|
assert kwargs["job"] == "lint-test"
|
|
assert kwargs["run"] == "5"
|
|
assert kwargs["sha"] == "abc123"
|
|
|
|
|
|
class TestWorkflowGeneration:
|
|
"""Generated workflows contain failure steps."""
|
|
|
|
def test_gitea_workflows_have_failure_steps(self):
|
|
from pathlib import Path
|
|
|
|
wf_dir = Path(__file__).resolve().parents[2] / ".gitea" / "workflows"
|
|
skip = {"renovate.yml"} # external bot, no failure filing
|
|
for yml in wf_dir.glob("*.yml"):
|
|
if yml.name in skip:
|
|
continue
|
|
content = yml.read_text()
|
|
assert "File failure issue" in content, f"{yml.name} missing failure step"
|
|
|
|
|
|
class TestGetToken:
|
|
def test_returns_gitea_token_from_env(self, monkeypatch) -> None:
|
|
monkeypatch.setenv("GITEA_TOKEN", "env-token")
|
|
from api.diag.ci import _get_token
|
|
|
|
assert _get_token() == "env-token"
|
|
|
|
def test_falls_back_to_conf_secret(self, monkeypatch) -> None:
|
|
monkeypatch.delenv("GITEA_TOKEN", raising=False)
|
|
# Force the conf.secret import path to return a value.
|
|
import sys
|
|
import types
|
|
|
|
from api.diag.ci import _get_token
|
|
|
|
fake_conf = types.ModuleType("conf")
|
|
fake_conf.secret = lambda key, env: "conf-token"
|
|
monkeypatch.setitem(sys.modules, "conf", fake_conf)
|
|
assert _get_token() == "conf-token"
|
|
|
|
def test_returns_empty_when_conf_unavailable(self, monkeypatch) -> None:
|
|
"""ImportError on conf import → falls through to '' return."""
|
|
monkeypatch.delenv("GITEA_TOKEN", raising=False)
|
|
import sys
|
|
|
|
from api.diag.ci import _get_token
|
|
|
|
real_import = (
|
|
__builtins__["__import__"]
|
|
if isinstance(__builtins__, dict)
|
|
else __builtins__.__import__
|
|
)
|
|
|
|
def fake_import(name, *a, **kw):
|
|
if name == "conf":
|
|
raise ImportError("conf unavailable")
|
|
return real_import(name, *a, **kw)
|
|
|
|
monkeypatch.setattr("builtins.__import__", fake_import)
|
|
monkeypatch.delitem(sys.modules, "conf", raising=False)
|
|
assert _get_token() == ""
|
|
|
|
def test_returns_empty_when_no_token(self, monkeypatch) -> None:
|
|
monkeypatch.delenv("GITEA_TOKEN", raising=False)
|
|
from unittest.mock import patch
|
|
|
|
from api.diag.ci import _get_token
|
|
|
|
with patch("api.diag.ci._get_token", return_value=""):
|
|
assert _get_token.__module__ == "api.diag.ci"
|
|
|
|
|
|
class TestFileCiFailureGithubAction:
|
|
@patch("api.diag.ci._get_token", return_value="fake-token")
|
|
@patch("api.clients.gitea.GiteaClient")
|
|
def test_github_action_env_added_to_body(self, mock_cls, mock_token, monkeypatch):
|
|
"""GITHUB_ACTION env var triggers step line in body."""
|
|
monkeypatch.setenv("GITHUB_ACTION", "my-step-name")
|
|
mock_client = MagicMock()
|
|
mock_client.create_issue.return_value = {"number": 10}
|
|
mock_client.resolve_labels.return_value = []
|
|
mock_cls.return_value = mock_client
|
|
|
|
from api.diag.ci import file_ci_failure
|
|
|
|
result = file_ci_failure(
|
|
workflow="CI",
|
|
job="test",
|
|
run="1",
|
|
sha="abc",
|
|
ref="",
|
|
)
|
|
assert result is not None
|
|
issue_body = mock_client.create_issue.call_args[0][2]
|
|
assert "my-step-name" in issue_body["body"]
|
|
|
|
@patch("api.diag.ci._get_token", return_value="fake-token")
|
|
@patch("api.clients.gitea.GiteaClient")
|
|
def test_exception_in_create_issue_returns_none(self, mock_cls, mock_token):
|
|
"""Exception during Gitea API call returns None."""
|
|
mock_client = MagicMock()
|
|
mock_client.create_issue.side_effect = Exception("API error")
|
|
mock_client.resolve_labels.return_value = []
|
|
mock_cls.return_value = mock_client
|
|
|
|
from api.diag.ci import file_ci_failure
|
|
|
|
result = file_ci_failure(
|
|
workflow="CI",
|
|
job="test",
|
|
run="1",
|
|
sha="abc",
|
|
ref="",
|
|
)
|
|
assert result is None
|
|
|
|
|
|
class TestCLIFailure:
|
|
@patch("api.diag.ci.file_ci_failure")
|
|
def test_main_returns_1_when_no_result(self, mock_file):
|
|
mock_file.return_value = None
|
|
import sys
|
|
|
|
from api.diag.ci import main
|
|
|
|
old_argv = sys.argv
|
|
sys.argv = [
|
|
"ci",
|
|
"--workflow",
|
|
"CI",
|
|
"--job",
|
|
"test",
|
|
"--run",
|
|
"1",
|
|
"--sha",
|
|
"abc",
|
|
]
|
|
try:
|
|
code = main()
|
|
finally:
|
|
sys.argv = old_argv
|
|
|
|
assert code == 1
|