364 lines
12 KiB
Python
364 lines
12 KiB
Python
# SPDX-License-Identifier: Apache-2.0
|
|
# SPDX-FileCopyrightText: Copyright contributors to the vLLM project
|
|
|
|
import unittest
|
|
from typing import Any
|
|
|
|
from run_ci_command import (
|
|
COMMAND_RETRY_FAILED,
|
|
COMMAND_RUN_CI,
|
|
RETRY_STATES,
|
|
BuildkiteClient,
|
|
authorize,
|
|
create_build_payload,
|
|
has_trusted_approval,
|
|
is_active_build,
|
|
is_build_for_pr,
|
|
parse_command,
|
|
parse_trusted_users,
|
|
run,
|
|
select_latest_build,
|
|
)
|
|
|
|
|
|
def make_pr(**overrides: Any) -> dict[str, Any]:
|
|
pr = {
|
|
"base": {"ref": "main"},
|
|
"draft": False,
|
|
"head": {
|
|
"ref": "feature",
|
|
"repo": {"clone_url": "https://github.com/contributor/vllm.git"},
|
|
"sha": "0123456789abcdef",
|
|
},
|
|
"labels": [],
|
|
"number": 42,
|
|
"state": "open",
|
|
"user": {"login": "author"},
|
|
}
|
|
pr.update(overrides)
|
|
return pr
|
|
|
|
|
|
def make_event(command: str, actor: str = "reviewer") -> dict[str, Any]:
|
|
return {
|
|
"comment": {
|
|
"body": command,
|
|
"id": 99,
|
|
"user": {"login": actor},
|
|
},
|
|
"issue": {
|
|
"number": 42,
|
|
"pull_request": {},
|
|
},
|
|
}
|
|
|
|
|
|
class FakeGitHub:
|
|
def __init__(
|
|
self,
|
|
*,
|
|
permission: str = "write",
|
|
permissions: dict[str, str] | None = None,
|
|
pr: dict[str, Any] | None = None,
|
|
review_decision: str = "REVIEW_REQUIRED",
|
|
reviews: list[dict[str, Any]] | None = None,
|
|
) -> None:
|
|
self.comments: list[str] = []
|
|
self.permission = permission
|
|
self.permissions = permissions or {}
|
|
self.pr = pr or make_pr()
|
|
self.reactions: list[str] = []
|
|
self.review_decision = review_decision
|
|
self.reviews = reviews or []
|
|
|
|
def get_pr(self, number: int) -> dict[str, Any]:
|
|
return self.pr
|
|
|
|
def get_permission(self, actor: str) -> str:
|
|
return self.permissions.get(actor, self.permission)
|
|
|
|
def get_review_decision(self, number: int) -> str:
|
|
return self.review_decision
|
|
|
|
def list_reviews(self, number: int) -> list[dict[str, Any]]:
|
|
return self.reviews
|
|
|
|
def list_reactions(self, comment_id: int) -> list[dict[str, Any]]:
|
|
return []
|
|
|
|
def add_reaction(self, comment_id: int, content: str) -> None:
|
|
self.reactions.append(content)
|
|
|
|
def add_comment(self, issue_number: int, body: str) -> None:
|
|
self.comments.append(body)
|
|
|
|
|
|
class FakeBuildkite:
|
|
def __init__(
|
|
self,
|
|
build_lists: list[list[dict[str, Any]]] | None = None,
|
|
) -> None:
|
|
self.build_lists = build_lists or []
|
|
self.created_builds: list[dict[str, Any]] = []
|
|
self.list_calls: list[tuple[str, tuple[str, str] | None]] = []
|
|
self.retry_calls: list[tuple[int, str]] = []
|
|
|
|
def list_builds(
|
|
self,
|
|
commit: str,
|
|
*,
|
|
metadata: tuple[str, str] | None = None,
|
|
) -> list[dict[str, Any]]:
|
|
self.list_calls.append((commit, metadata))
|
|
return self.build_lists.pop(0)
|
|
|
|
def create_build(self, body: dict[str, Any]) -> dict[str, Any]:
|
|
self.created_builds.append(body)
|
|
return {
|
|
"number": 123,
|
|
"web_url": "https://buildkite.example/builds/123",
|
|
}
|
|
|
|
def retry_failed_jobs(
|
|
self,
|
|
build_number: int,
|
|
states: str,
|
|
) -> dict[str, Any]:
|
|
self.retry_calls.append((build_number, states))
|
|
return {"retried_jobs_count": 3}
|
|
|
|
|
|
class FakeTransport:
|
|
def __init__(self, response: Any) -> None:
|
|
self.calls: list[dict[str, Any]] = []
|
|
self.response = response
|
|
|
|
def request(self, url: str, **kwargs: Any) -> Any:
|
|
self.calls.append({"url": url, **kwargs})
|
|
return self.response
|
|
|
|
|
|
class RunCiCommandTest(unittest.TestCase):
|
|
def test_only_exact_ci_commands_are_accepted(self) -> None:
|
|
self.assertEqual(parse_command(COMMAND_RUN_CI), COMMAND_RUN_CI)
|
|
self.assertEqual(
|
|
parse_command(COMMAND_RETRY_FAILED),
|
|
COMMAND_RETRY_FAILED,
|
|
)
|
|
self.assertIsNone(parse_command("/ci run please"))
|
|
self.assertIsNone(parse_command(" /ci run"))
|
|
|
|
def test_write_access_authorizes_reviewers_and_authors(self) -> None:
|
|
allowed, _ = authorize(
|
|
actor="reviewer",
|
|
permission="write",
|
|
pr=make_pr(),
|
|
)
|
|
self.assertTrue(allowed)
|
|
|
|
def test_configured_trusted_contributors_can_run_ci(self) -> None:
|
|
trusted_users = parse_trusted_users("trusted-one, TRUSTED-TWO")
|
|
allowed, _ = authorize(
|
|
actor="trusted-two",
|
|
permission="read",
|
|
pr=make_pr(),
|
|
trusted_users=trusted_users,
|
|
)
|
|
self.assertTrue(allowed)
|
|
|
|
def test_authors_need_an_approval_or_ready_label(self) -> None:
|
|
pending, _ = authorize(
|
|
actor="author",
|
|
permission="read",
|
|
pr=make_pr(),
|
|
)
|
|
approved, _ = authorize(
|
|
actor="author",
|
|
permission="read",
|
|
pr=make_pr(),
|
|
trusted_approval=True,
|
|
)
|
|
ready, _ = authorize(
|
|
actor="author",
|
|
permission="read",
|
|
pr=make_pr(labels=[{"name": "ready"}]),
|
|
)
|
|
self.assertFalse(pending)
|
|
self.assertTrue(approved)
|
|
self.assertTrue(ready)
|
|
|
|
def test_non_author_contributors_without_write_are_denied(self) -> None:
|
|
allowed, _ = authorize(
|
|
actor="contributor",
|
|
permission="read",
|
|
pr=make_pr(),
|
|
trusted_approval=True,
|
|
)
|
|
self.assertFalse(allowed)
|
|
|
|
def test_authors_cannot_use_ready_state_on_draft_prs(self) -> None:
|
|
allowed, _ = authorize(
|
|
actor="author",
|
|
permission="read",
|
|
pr=make_pr(draft=True, labels=[{"name": "ready"}]),
|
|
trusted_approval=True,
|
|
)
|
|
self.assertFalse(allowed)
|
|
|
|
def test_only_trusted_reviewers_can_delegate_through_approval(self) -> None:
|
|
approved_review = {
|
|
"state": "APPROVED",
|
|
"user": {"login": "reviewer"},
|
|
}
|
|
trusted = FakeGitHub(
|
|
permission="read",
|
|
permissions={"reviewer": "write"},
|
|
review_decision="APPROVED",
|
|
reviews=[approved_review],
|
|
)
|
|
untrusted = FakeGitHub(
|
|
permission="read",
|
|
review_decision="APPROVED",
|
|
reviews=[approved_review],
|
|
)
|
|
self.assertTrue(has_trusted_approval(trusted, 42, set()))
|
|
self.assertFalse(has_trusted_approval(untrusted, 42, set()))
|
|
|
|
def test_build_matching_is_scoped_to_the_pr(self) -> None:
|
|
self.assertTrue(is_build_for_pr({"pull_request": {"id": 42}}, 42))
|
|
self.assertFalse(is_build_for_pr({"pull_request": {"id": 43}}, 42))
|
|
self.assertTrue(
|
|
is_build_for_pr(
|
|
{"meta_data": {"github-pr-number": "42"}},
|
|
42,
|
|
)
|
|
)
|
|
|
|
def test_latest_build_selection_ignores_other_prs(self) -> None:
|
|
latest = select_latest_build(
|
|
[
|
|
{
|
|
"created_at": "2026-07-28T02:00:00Z",
|
|
"number": 3,
|
|
"pull_request": {"id": 43},
|
|
},
|
|
{
|
|
"created_at": "2026-07-28T01:00:00Z",
|
|
"number": 2,
|
|
"pull_request": {"id": 42},
|
|
},
|
|
{
|
|
"created_at": "2026-07-28T00:00:00Z",
|
|
"number": 1,
|
|
"pull_request": {"id": 42},
|
|
},
|
|
],
|
|
42,
|
|
)
|
|
self.assertEqual(latest["number"], 2)
|
|
|
|
def test_active_build_states_prevent_duplicate_runs(self) -> None:
|
|
self.assertTrue(is_active_build({"state": "scheduled"}))
|
|
self.assertTrue(is_active_build({"state": "running"}))
|
|
self.assertTrue(is_active_build({"state": "waiting"}))
|
|
self.assertTrue(is_active_build({"blocked": True, "state": "passed"}))
|
|
self.assertFalse(is_active_build({"state": "failed"}))
|
|
|
|
def test_build_payload_preserves_pr_context(self) -> None:
|
|
payload = create_build_payload(
|
|
actor="reviewer",
|
|
comment_id=99,
|
|
pr=make_pr(labels=[{"name": "ready"}, {"name": "v1"}]),
|
|
)
|
|
self.assertEqual(
|
|
payload,
|
|
{
|
|
"commit": "0123456789abcdef",
|
|
"branch": "feature",
|
|
"message": "PR #42 /ci run by @reviewer",
|
|
"pull_request_id": 42,
|
|
"pull_request_base_branch": "main",
|
|
"pull_request_repository": ("https://github.com/contributor/vllm.git"),
|
|
"pull_request_labels": ["ready", "v1"],
|
|
"ignore_pipeline_branch_filters": True,
|
|
"env": {
|
|
"VLLM_CI_GITHUB_COMMENT_ID": "99",
|
|
"VLLM_CI_TRIGGERED_BY": "reviewer",
|
|
},
|
|
"meta_data": {
|
|
"github-comment-id": "99",
|
|
"github-pr-number": "42",
|
|
"github-triggered-by": "reviewer",
|
|
},
|
|
},
|
|
)
|
|
|
|
def test_ci_run_dispatches_build_with_current_pr_metadata(self) -> None:
|
|
github = FakeGitHub()
|
|
buildkite = FakeBuildkite([[], []])
|
|
run(make_event(COMMAND_RUN_CI), github, buildkite)
|
|
|
|
self.assertEqual(len(buildkite.created_builds), 1)
|
|
self.assertEqual(
|
|
buildkite.created_builds[0]["message"],
|
|
"PR #42 /ci run by @reviewer",
|
|
)
|
|
self.assertEqual(github.reactions, ["eyes", "rocket"])
|
|
self.assertIn("Buildkite CI #123", github.comments[0])
|
|
|
|
def test_unapproved_authors_are_denied_without_buildkite(self) -> None:
|
|
github = FakeGitHub(
|
|
permission="read",
|
|
pr=make_pr(),
|
|
review_decision="REVIEW_REQUIRED",
|
|
)
|
|
buildkite = FakeBuildkite()
|
|
run(make_event(COMMAND_RUN_CI, "author"), github, buildkite)
|
|
|
|
self.assertEqual(buildkite.list_calls, [])
|
|
self.assertEqual(github.reactions, ["eyes", "-1"])
|
|
self.assertIn("approve the PR", github.comments[0])
|
|
|
|
def test_ci_retry_uses_latest_current_sha_build(self) -> None:
|
|
github = FakeGitHub(
|
|
permission="read",
|
|
pr=make_pr(labels=[{"name": "ready"}]),
|
|
)
|
|
buildkite = FakeBuildkite(
|
|
[
|
|
[
|
|
{
|
|
"created_at": "2026-07-28T01:00:00Z",
|
|
"finished_at": "2026-07-28T02:00:00Z",
|
|
"number": 123,
|
|
"pull_request": {"id": 42},
|
|
"state": "failed",
|
|
"web_url": "https://buildkite.example/builds/123",
|
|
}
|
|
]
|
|
]
|
|
)
|
|
run(make_event(COMMAND_RETRY_FAILED, "author"), github, buildkite)
|
|
|
|
self.assertEqual(buildkite.retry_calls, [(123, RETRY_STATES)])
|
|
self.assertIn("Queued 3 failed job", github.comments[0])
|
|
|
|
def test_buildkite_retry_uses_retry_failed_jobs_endpoint(self) -> None:
|
|
transport = FakeTransport({"retried_jobs_count": 2})
|
|
client = BuildkiteClient(
|
|
"secret",
|
|
"vllm",
|
|
"ci",
|
|
transport=transport,
|
|
)
|
|
client.retry_failed_jobs(123, RETRY_STATES)
|
|
|
|
call = transport.calls[0]
|
|
self.assertEqual(call["method"], "PUT")
|
|
self.assertTrue(call["url"].endswith("/123/retry_failed_jobs"))
|
|
self.assertEqual(call["body"], {"states": RETRY_STATES})
|
|
|
|
|
|
if __name__ == "__main__":
|
|
unittest.main()
|