bridge tests: fake serves workflow contents; cover invalid-workflow status
All checks were successful
check / gate (push) Successful in 23s
All checks were successful
check / gate (push) Successful in 23s
b7a7e94 made the bridge read workflow files, which the strict fake Gitea
refused (7 red). The fake now serves contents (404 when absent), and new
tests cover: error posted with no runs, valid files add nothing, no repost,
.gitea/workflows wins over .github/workflows, and each workflow_problem
shape. pyyaml declared in dev extras (the bridge imports it).
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
@@ -8,6 +8,7 @@ no status at all.
|
|||||||
|
|
||||||
from __future__ import annotations
|
from __future__ import annotations
|
||||||
|
|
||||||
|
import base64
|
||||||
import importlib.util
|
import importlib.util
|
||||||
from pathlib import Path
|
from pathlib import Path
|
||||||
|
|
||||||
@@ -35,12 +36,23 @@ def _run(i, wf, job, status, sha=SHA, n=1):
|
|||||||
|
|
||||||
|
|
||||||
class Fake:
|
class Fake:
|
||||||
def __init__(self, runs=(), statuses=(), gh_prs=(), wg_prs=()):
|
def __init__(self, runs=(), statuses=(), gh_prs=(), wg_prs=(), workflows=None):
|
||||||
self.runs, self.statuses = list(runs), list(statuses)
|
self.runs, self.statuses = list(runs), list(statuses)
|
||||||
|
self.workflows = workflows or {} # {path: yaml text} at every commit
|
||||||
self.gh_prs, self.wg_prs = list(gh_prs), list(wg_prs)
|
self.gh_prs, self.wg_prs = list(gh_prs), list(wg_prs)
|
||||||
self.posted, self.opened, self.closed = [], [], []
|
self.posted, self.opened, self.closed = [], [], []
|
||||||
|
|
||||||
def gitea(self, method, path, body=None):
|
def gitea(self, method, path, body=None):
|
||||||
|
if "/contents/" in path:
|
||||||
|
want = path.split("/contents/", 1)[1].split("?", 1)[0]
|
||||||
|
if want in self.workflows:
|
||||||
|
return 200, {"content": base64.b64encode(self.workflows[want].encode()).decode()}
|
||||||
|
files = [
|
||||||
|
{"type": "file", "name": k.rsplit("/", 1)[1], "path": k}
|
||||||
|
for k in self.workflows
|
||||||
|
if k.rsplit("/", 1)[0] == want
|
||||||
|
]
|
||||||
|
return (200, files) if files else (404, None)
|
||||||
if "/actions/tasks" in path:
|
if "/actions/tasks" in path:
|
||||||
page = int(path.rsplit("page=", 1)[1])
|
page = int(path.rsplit("page=", 1)[1])
|
||||||
return 200, {"workflow_runs": self.runs[(page - 1) * 50 : page * 50]}
|
return 200, {"workflow_runs": self.runs[(page - 1) * 50 : page * 50]}
|
||||||
@@ -209,3 +221,54 @@ def test_transport_blips_are_retried_but_http_errors_are_not(monkeypatch):
|
|||||||
monkeypatch.setattr(bridge.urllib.request, "urlopen", forbidden)
|
monkeypatch.setattr(bridge.urllib.request, "urlopen", forbidden)
|
||||||
assert bridge._call("http://x", "t", "GET", "/p") == (403, None)
|
assert bridge._call("http://x", "t", "GET", "/p") == (403, None)
|
||||||
assert calls["n"] == 1
|
assert calls["n"] == 1
|
||||||
|
|
||||||
|
|
||||||
|
GOOD = "on: push\njobs:\n test:\n runs-on: ubuntu-latest\n steps: []\n"
|
||||||
|
BROKEN = "on: push\njobs:\n test:\n runs-on: x\n steps: [\n"
|
||||||
|
|
||||||
|
|
||||||
|
def test_invalid_workflow_gets_an_error_status_even_with_no_runs(fake):
|
||||||
|
# Gitea fires NO run for an invalid file: without this the PR shows nothing.
|
||||||
|
f = fake(workflows={".github/workflows/ci.yml": BROKEN})
|
||||||
|
bridge.post_statuses("windy-chat", SHA)
|
||||||
|
assert [(p["context"], p["state"]) for p in f.posted] == [("windy-git/ci/workflow", "error")]
|
||||||
|
assert "invalid YAML at line 5" in f.posted[0]["description"]
|
||||||
|
assert f.posted[0]["target_url"].endswith(f"/src/commit/{SHA}/.github/workflows/ci.yml")
|
||||||
|
|
||||||
|
|
||||||
|
def test_valid_workflows_post_nothing_extra(fake):
|
||||||
|
f = fake(runs=[_run(1, "ci.yml", "test", "success")], workflows={".github/workflows/ci.yml": GOOD})
|
||||||
|
bridge.post_statuses("windy-chat", SHA)
|
||||||
|
assert [p["context"] for p in f.posted] == ["windy-git/ci/test"]
|
||||||
|
|
||||||
|
|
||||||
|
def test_workflow_error_is_not_reposted(fake):
|
||||||
|
f = fake(
|
||||||
|
workflows={".github/workflows/ci.yml": BROKEN},
|
||||||
|
statuses=[{"context": "windy-git/ci/workflow", "state": "error"}],
|
||||||
|
)
|
||||||
|
bridge.post_statuses("windy-chat", SHA)
|
||||||
|
assert f.posted == []
|
||||||
|
|
||||||
|
|
||||||
|
def test_gitea_dir_wins_over_github_dir(fake):
|
||||||
|
# Gitea runs .gitea/workflows when it has files and ignores .github/workflows.
|
||||||
|
f = fake(workflows={".gitea/workflows/ci.yml": GOOD, ".github/workflows/old.yml": BROKEN})
|
||||||
|
bridge.post_statuses("windy-chat", SHA)
|
||||||
|
assert f.posted == []
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.mark.parametrize(
|
||||||
|
"text, problem",
|
||||||
|
[
|
||||||
|
(GOOD, None),
|
||||||
|
("on: push\njobs:\n a:\n uses: ./x.yml\n", None),
|
||||||
|
(BROKEN, "invalid YAML at line 5"),
|
||||||
|
("jobs:\n a:\n runs-on: x\n", "no `on:` trigger"),
|
||||||
|
("on: push\n", "no `jobs:`"),
|
||||||
|
("on: push\njobs:\n a:\n steps: []\n", "job `a` has no `runs-on:`"),
|
||||||
|
("- a\n", "not a YAML mapping"),
|
||||||
|
],
|
||||||
|
)
|
||||||
|
def test_workflow_problem(text, problem):
|
||||||
|
assert bridge.workflow_problem(text) == problem
|
||||||
|
|||||||
@@ -31,7 +31,7 @@ dependencies = [
|
|||||||
]
|
]
|
||||||
|
|
||||||
[project.optional-dependencies]
|
[project.optional-dependencies]
|
||||||
dev = ["pytest>=8.3", "pytest-asyncio>=0.24", "ruff>=0.7", "mypy>=1.13"]
|
dev = ["pytest>=8.3", "pyyaml>=6.0", "pytest-asyncio>=0.24", "ruff>=0.7", "mypy>=1.13"]
|
||||||
|
|
||||||
[tool.ruff]
|
[tool.ruff]
|
||||||
line-length = 100
|
line-length = 100
|
||||||
|
|||||||
Reference in New Issue
Block a user