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>
275 lines
9.3 KiB
Python
275 lines
9.3 KiB
Python
"""Behavioral tests for scripts/pr_status_bridge.py.
|
|
|
|
The bridge is the ONLY CI signal the private platform repos get on GitHub, so
|
|
these drive its real functions against fake Gitea/GitHub APIs rather than
|
|
grepping its source: a status painted green that nobody tested is worse than
|
|
no status at all.
|
|
"""
|
|
|
|
from __future__ import annotations
|
|
|
|
import base64
|
|
import importlib.util
|
|
from pathlib import Path
|
|
|
|
import pytest
|
|
|
|
ROOT = Path(__file__).resolve().parents[2]
|
|
_spec = importlib.util.spec_from_file_location(
|
|
"pr_status_bridge", ROOT / "scripts" / "pr_status_bridge.py"
|
|
)
|
|
bridge = importlib.util.module_from_spec(_spec)
|
|
_spec.loader.exec_module(bridge)
|
|
|
|
SHA = "a" * 40
|
|
|
|
|
|
def _run(i, wf, job, status, sha=SHA, n=1):
|
|
return {
|
|
"id": i,
|
|
"workflow_id": wf,
|
|
"name": job,
|
|
"status": status,
|
|
"head_sha": sha,
|
|
"run_number": n,
|
|
}
|
|
|
|
|
|
class Fake:
|
|
def __init__(self, runs=(), statuses=(), gh_prs=(), wg_prs=(), workflows=None):
|
|
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.posted, self.opened, self.closed = [], [], []
|
|
|
|
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:
|
|
page = int(path.rsplit("page=", 1)[1])
|
|
return 200, {"workflow_runs": self.runs[(page - 1) * 50 : page * 50]}
|
|
if method == "GET" and path.endswith("/pulls?state=open&limit=50"):
|
|
return 200, self.wg_prs
|
|
if method == "POST" and path.endswith("/pulls"):
|
|
self.opened.append(body)
|
|
return 201, {}
|
|
if method == "PATCH":
|
|
self.closed.append(path)
|
|
return 201, {}
|
|
raise AssertionError(path)
|
|
|
|
def github(self, method, path, body=None):
|
|
if "/statuses" in path and method == "GET":
|
|
return 200, self.statuses
|
|
if "/statuses/" in path and method == "POST":
|
|
self.posted.append(body)
|
|
return 201, {}
|
|
if "/pulls?" in path:
|
|
return 200, self.gh_prs
|
|
raise AssertionError(path)
|
|
|
|
|
|
@pytest.fixture
|
|
def fake(monkeypatch):
|
|
def make(**kw):
|
|
f = Fake(**kw)
|
|
monkeypatch.setattr(bridge, "gitea", f.gitea)
|
|
monkeypatch.setattr(bridge, "github", f.github)
|
|
return f
|
|
|
|
return make
|
|
|
|
|
|
def test_posts_latest_verdict_per_job(fake):
|
|
f = fake(runs=[_run(1, "ci.yml", "test", "failure"), _run(2, "ci.yml", "test", "success", n=2)])
|
|
bridge.post_statuses("r", SHA)
|
|
assert [(p["context"], p["state"]) for p in f.posted] == [("windy-git/ci/test", "success")]
|
|
assert f.posted[0]["target_url"].endswith("/actions/runs/2")
|
|
|
|
|
|
def test_unchanged_state_is_not_reposted(fake):
|
|
f = fake(
|
|
runs=[_run(1, "ci.yml", "test", "success")],
|
|
statuses=[{"context": "windy-git/ci/test", "state": "success"}],
|
|
)
|
|
bridge.post_statuses("r", SHA)
|
|
assert f.posted == []
|
|
|
|
|
|
def test_skipped_job_is_never_painted_green(fake):
|
|
f = fake(runs=[_run(1, "substrate-drift.yml", "check", "skipped")])
|
|
bridge.post_statuses("r", SHA)
|
|
assert f.posted == []
|
|
|
|
|
|
def test_other_commits_runs_are_ignored(fake):
|
|
f = fake(runs=[_run(1, "ci.yml", "test", "failure", sha="b" * 40)])
|
|
bridge.post_statuses("r", SHA)
|
|
assert f.posted == []
|
|
|
|
|
|
def test_runs_past_the_first_page_are_seen(fake):
|
|
noise = [_run(100 + i, "drift.yml", "x", "skipped", sha="c" * 40) for i in range(50)]
|
|
f = fake(runs=noise + [_run(1, "ci.yml", "test", "success")])
|
|
bridge.post_statuses("r", SHA)
|
|
assert [p["state"] for p in f.posted] == ["success"]
|
|
|
|
|
|
def _gh_pr(n, repo="sneakyfree/r"):
|
|
return {
|
|
"number": n,
|
|
"title": "t",
|
|
"html_url": "u",
|
|
"head": {"ref": f"b{n}", "sha": SHA, "repo": {"full_name": repo} if repo else None},
|
|
"base": {"ref": "main"},
|
|
}
|
|
|
|
|
|
def test_fork_prs_are_never_mirrored(fake, monkeypatch):
|
|
monkeypatch.setattr(bridge, "GH_OWNER", "sneakyfree")
|
|
f = fake(gh_prs=[_gh_pr(1, repo="stranger/r"), _gh_pr(2, repo=None)])
|
|
assert bridge.sync_prs("r") == []
|
|
assert f.opened == []
|
|
|
|
|
|
def test_pr_mirror_opened_once_and_closed_when_github_closes(fake, monkeypatch):
|
|
monkeypatch.setattr(bridge, "GH_OWNER", "sneakyfree")
|
|
f = fake(gh_prs=[_gh_pr(7)], wg_prs=[{"number": 3, "title": "[GH#5] gone"}])
|
|
assert bridge.sync_prs("r") == [SHA]
|
|
assert [o["head"] for o in f.opened] == ["b7"]
|
|
assert f.closed == ["/repos/windyadmin/r/pulls/3"]
|
|
|
|
f2 = fake(gh_prs=[_gh_pr(7)], wg_prs=[{"number": 4, "title": "[GH#7] t"}])
|
|
bridge.sync_prs("r")
|
|
assert f2.opened == [] and f2.closed == []
|
|
|
|
|
|
def test_image_build_jobs_are_not_posted(fake):
|
|
"""No Docker daemon in job containers (I-5): a build job's red is structural."""
|
|
f = fake(
|
|
runs=[_run(1, "ci.yml", "Docker Build", "failure"), _run(2, "ci.yml", "docker", "failure")]
|
|
)
|
|
bridge.post_statuses("r", SHA)
|
|
assert f.posted == []
|
|
|
|
|
|
def test_non_blocking_jobs_are_not_posted_for_that_repo_only(fake, monkeypatch):
|
|
"""Grant ruled windy-pro's desktop/installer jobs non-blocking: they must not
|
|
reach GitHub for windy-pro, and the rule must not leak to other repos."""
|
|
monkeypatch.setattr(bridge, "NON_BLOCKING", {"windy-pro": {"ci/build-desktop"}})
|
|
runs = [_run(1, "ci.yml", "build-desktop", "failure"), _run(2, "ci.yml", "test", "success")]
|
|
f = fake(runs=runs)
|
|
bridge.post_statuses("windy-pro", SHA)
|
|
assert [p["context"] for p in f.posted] == ["windy-git/ci/test"]
|
|
f2 = fake(runs=runs)
|
|
bridge.post_statuses("windy-chat", SHA)
|
|
assert sorted(p["context"] for p in f2.posted) == [
|
|
"windy-git/ci/build-desktop",
|
|
"windy-git/ci/test",
|
|
]
|
|
|
|
|
|
def test_default_non_blocking_is_grants_ruling():
|
|
assert bridge.NON_BLOCKING.get("windy-pro") == {
|
|
"ci/build-desktop",
|
|
"ci/test-installer",
|
|
"ci/reality-check",
|
|
}
|
|
|
|
|
|
def test_transport_blips_are_retried_but_http_errors_are_not(monkeypatch):
|
|
import urllib.error
|
|
|
|
calls = {"n": 0}
|
|
|
|
class _R:
|
|
status = 200
|
|
|
|
def read(self):
|
|
return b"{}"
|
|
|
|
def __enter__(self):
|
|
return self
|
|
|
|
def __exit__(self, *a):
|
|
return False
|
|
|
|
def flaky(req, timeout):
|
|
calls["n"] += 1
|
|
if calls["n"] < 3:
|
|
raise urllib.error.URLError("_ssl.c:983: The handshake operation timed out")
|
|
return _R()
|
|
|
|
monkeypatch.setattr(bridge.urllib.request, "urlopen", flaky)
|
|
monkeypatch.setattr(bridge.time, "sleep", lambda s: None)
|
|
assert bridge._call("http://x", "t", "GET", "/p") == (200, {})
|
|
assert calls["n"] == 3
|
|
|
|
def forbidden(req, timeout):
|
|
calls["n"] += 1
|
|
raise urllib.error.HTTPError("http://x/p", 403, "no", {}, None)
|
|
|
|
calls["n"] = 0
|
|
monkeypatch.setattr(bridge.urllib.request, "urlopen", forbidden)
|
|
assert bridge._call("http://x", "t", "GET", "/p") == (403, None)
|
|
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
|