Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
63 changes: 0 additions & 63 deletions buildbotapi.py
Original file line number Diff line number Diff line change
@@ -1,4 +1,3 @@
import asyncio
import json
from dataclasses import dataclass
from typing import Any, cast
Expand All @@ -12,7 +11,6 @@
class Builder:
builderid: int
description: str | None
masterids: list[int]
name: str
tags: list[str]

Expand All @@ -23,26 +21,6 @@ def __hash__(self) -> int:
return hash(self.builderid)


@dataclass
class Build:
id: int
is_currently_failing: bool
builderid: int
builder: Builder | None

def __init__(self, **kwargs: Any) -> None:
self.__dict__.update(**kwargs)
self.id = int(kwargs.get("number", -1))
self.is_currently_failing = kwargs.get("currently_failing", False)
self.builder = None

def __eq__(self, other: object) -> bool:
return isinstance(other, Build) and self.id == other.id

def __hash__(self) -> int:
return hash(self.id)


class BuildBotAPI:
def __init__(self, session: ClientSession) -> None:
self._session = session
Expand Down Expand Up @@ -91,44 +69,3 @@ async def is_builder_failing_currently(self, builder: Builder) -> bool:
if build["results"] == 2:
return True
return False

async def get_build(self, builder_id: int, build_id: int) -> Build:
data = await self._fetch_json(
f"https://buildbot.python.org/all/api/v2/builders/{builder_id}"
f"/builds/{build_id}"
)
(build_data,) = data["builds"]
build: Build = Build(**build_data)
build.builder = (await self.all_builders())[build.builderid]
build.is_currently_failing = await self.is_builder_failing_currently(
build.builder
)
return build

async def get_recent_failures(self, limit: int = 100) -> set[Build]:
data = await self._fetch_json(
f"https://buildbot.python.org/all/api/v2/builds?"
f"complete__eq=true&&results__eq=2&&"
f"order=-complete_at&&limit={limit}"
)

stable_builders = await self.stable_builders()

all_failures = {
Build(**build)
for build in data["builds"]
if build["builderid"] in stable_builders
}

for failure in all_failures:
failure.builder = stable_builders[failure.builderid]

async def _get_missing_info(failure: Build) -> None:
assert failure.builder is not None
failure.is_currently_failing = await self.is_builder_failing_currently(
failure.builder
)

await asyncio.gather(*[_get_missing_info(failure) for failure in all_failures])

return all_failures
1 change: 1 addition & 0 deletions dev-requirements.in
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
pytest
pytest-aiohttp
pytest-cov
pytest-mock
466 changes: 466 additions & 0 deletions dev-requirements.txt

Large diffs are not rendered by default.

4 changes: 4 additions & 0 deletions pyproject.toml
Original file line number Diff line number Diff line change
@@ -1,3 +1,7 @@
[tool.pytest.ini_options]
asyncio_mode = "auto"
asyncio_default_fixture_loop_scope = "function"

[tool.mypy]
python_version = "3.12"
pretty = true
Expand Down
781 changes: 437 additions & 344 deletions requirements.txt

Large diffs are not rendered by default.

45 changes: 45 additions & 0 deletions tests/buildbotapi/builders.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,45 @@
{
"builders": [
{
"builderid": 3,
"description": null,
"description_format": null,
"description_html": null,
"masterids": [
1
],
"name": "AMD64 RHEL8 LTO 3.13",
"projectid": null,
"tags": [
"3.13",
"stable",
"lto",
"nondebug",
"tier-1"
]
},
{
"builderid": 1623,
"description": null,
"description_format": null,
"description_html": null,
"masterids": [
1
],
"name": "AMD64 Windows PGO NoGIL PR",
"projectid": null,
"tags": [
"PullRequest",
"unstable",
"win64",
"nogil",
"nondebug",
"pgo",
"tier-1"
]
}
],
"meta": {
"total": 3
}
}
22 changes: 22 additions & 0 deletions tests/buildbotapi/failure.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
{
"builds": [
{
"builderid": 3,
"buildid": 1732278,
"buildrequestid": 2341889,
"complete": true,
"complete_at": 1734198808,
"locks_duration_s": 0,
"masterid": 1,
"number": 228,
"properties": {},
"results": 2,
"started_at": 1734197714,
"state_string": "failed test (failure)",
"workerid": 28
}
],
"meta": {
"total": 1
}
}
7 changes: 7 additions & 0 deletions tests/buildbotapi/no-builds.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
{
"builds": [
],
"meta": {
"total": 0
}
}
22 changes: 22 additions & 0 deletions tests/buildbotapi/success.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
{
"builds": [
{
"builderid": 3,
"buildid": 1645411,
"buildrequestid": 2211085,
"complete": true,
"complete_at": 1728312495,
"locks_duration_s": 531,
"masterid": 1,
"number": 6844,
"properties": {},
"results": 0,
"started_at": 1728311538,
"state_string": "build successful",
"workerid": 27
}
],
"meta": {
"total": 1
}
}
140 changes: 140 additions & 0 deletions tests/test_buildbotapi.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,140 @@
from functools import cache
from unittest.mock import AsyncMock

import aiohttp
import pytest

import buildbotapi


def test_builder_class() -> None:
# Arrange / Act
builder = buildbotapi.Builder(
builderid=123,
description="my description",
name="my name",
tags=["tag1", "tag2"],
)

# Assert
assert builder.builderid == 123
assert builder.description == "my description"
assert builder.name == "my name"
assert builder.tags == ["tag1", "tag2"]
assert hash(builder) == 123


@cache
def load(filename: str) -> str:
with open(filename) as f:
return f.read()


@pytest.mark.asyncio
async def test_buildbotapi_authenticate() -> None:
# Arrange
async with AsyncMock(aiohttp.ClientSession) as mock_session:
api = buildbotapi.BuildBotAPI(mock_session)

# Act
await api.authenticate(token="")

# Assert
mock_session.get.assert_called_with(
"https://buildbot.python.org/all/auth/login", params={"token": ""}
)


@pytest.mark.asyncio
async def test_buildbotapi_all_builders() -> None:
# Arrange
mock_session = AsyncMock(aiohttp.ClientSession)
mock_session.get.return_value.__aenter__.return_value.status = 200
mock_session.get.return_value.__aenter__.return_value.text.return_value = load(
"tests/buildbotapi/builders.json"
)
api = buildbotapi.BuildBotAPI(mock_session)

# Act
all_builders = await api.all_builders()

# Assert
mock_session.get.assert_called_with(
"https://buildbot.python.org/all/api/v2/builders"
)
assert len(all_builders) == 2
assert all_builders[3].name == "AMD64 RHEL8 LTO 3.13"
assert all_builders[1623].name == "AMD64 Windows PGO NoGIL PR"


@pytest.mark.asyncio
async def test_buildbotapi_all_builders_with_branch() -> None:
# Arrange
mock_session = AsyncMock(aiohttp.ClientSession)
mock_session.get.return_value.__aenter__.return_value.status = 200
mock_session.get.return_value.__aenter__.return_value.text.return_value = load(
"tests/buildbotapi/builders.json"
)
api = buildbotapi.BuildBotAPI(mock_session)

# Act
await api.all_builders(branch="3.13")

# Assert
mock_session.get.assert_called_with(
"https://buildbot.python.org/all/api/v2/builders?tags__contains=3.13"
)


@pytest.mark.asyncio
async def test_buildbotapi_stable_builders() -> None:
# Arrange
mock_session = AsyncMock(aiohttp.ClientSession)
mock_session.get.return_value.__aenter__.return_value.status = 200
mock_session.get.return_value.__aenter__.return_value.text.return_value = load(
"tests/buildbotapi/builders.json"
)
api = buildbotapi.BuildBotAPI(mock_session)

# Act
all_builders = await api.stable_builders()

# Assert
mock_session.get.assert_called_with(
"https://buildbot.python.org/all/api/v2/builders"
)
assert len(all_builders) == 1
assert all_builders[3].name == "AMD64 RHEL8 LTO 3.13"
assert "stable" in all_builders[3].tags


@pytest.mark.asyncio
@pytest.mark.parametrize(
["json_data", "expected"],
[
("tests/buildbotapi/success.json", False),
("tests/buildbotapi/failure.json", True),
("tests/buildbotapi/no-builds.json", False),
],
)
async def test_buildbotapi_is_builder_failing_currently_yes(
json_data: str, expected: bool
) -> None:
# Arrange
mock_session = AsyncMock(aiohttp.ClientSession)
mock_session.get.return_value.__aenter__.return_value.status = 200
mock_session.get.return_value.__aenter__.return_value.text.return_value = load(
json_data
)
api = buildbotapi.BuildBotAPI(mock_session)
builder = buildbotapi.Builder(builderid=3)

# Act
failing = await api.is_builder_failing_currently(builder=builder)

# Assert
mock_session.get.assert_called_with(
"https://buildbot.python.org/all/api/v2/builds?complete__eq=true"
"&&builderid__eq=3&&order=-complete_at&&limit=1"
)
assert failing is expected
Loading