This commit is contained in:
+76
-82
@@ -25,6 +25,17 @@ async def gitea_client(handler: Handler, *, retries: int = 3) -> AsyncGenerator[
|
||||
await client.close()
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
def recorded_backoffs(monkeypatch: pytest.MonkeyPatch) -> list[int]:
|
||||
backoffs: list[int] = []
|
||||
|
||||
async def record_backoff(delay: int) -> None:
|
||||
backoffs.append(delay)
|
||||
|
||||
monkeypatch.setattr("agentci.integrations.gitea.client.asyncio.sleep", record_backoff)
|
||||
return backoffs
|
||||
|
||||
|
||||
async def test_sends_authenticated_json_request_contract() -> None:
|
||||
requests: list[httpx.Request] = []
|
||||
|
||||
@@ -221,19 +232,17 @@ async def test_pagination_stops_only_after_a_short_page(
|
||||
|
||||
|
||||
@pytest.mark.parametrize("status", [400, 401, 403, 404, 422])
|
||||
async def test_nonretryable_status_fails_once(status: int, monkeypatch: pytest.MonkeyPatch) -> None:
|
||||
async def test_nonretryable_status_fails_once(
|
||||
status: int,
|
||||
recorded_backoffs: list[int],
|
||||
) -> None:
|
||||
attempts = 0
|
||||
sleeps: list[int] = []
|
||||
|
||||
async def handler(_request: httpx.Request) -> httpx.Response:
|
||||
nonlocal attempts
|
||||
attempts += 1
|
||||
return httpx.Response(status)
|
||||
|
||||
async def sleep(delay: int) -> None:
|
||||
sleeps.append(delay)
|
||||
|
||||
monkeypatch.setattr("agentci.integrations.gitea.client.asyncio.sleep", sleep)
|
||||
async with gitea_client(handler) as client:
|
||||
with pytest.raises(
|
||||
GiteaError,
|
||||
@@ -242,104 +251,89 @@ async def test_nonretryable_status_fails_once(status: int, monkeypatch: pytest.M
|
||||
await client.default_branch("org", "repo")
|
||||
|
||||
assert attempts == 1
|
||||
assert sleeps == []
|
||||
assert recorded_backoffs == []
|
||||
|
||||
|
||||
@pytest.mark.parametrize("status", [429, 500, 502, 503, 504])
|
||||
async def test_retryable_status_recovers_after_backoff(
|
||||
status: int, monkeypatch: pytest.MonkeyPatch
|
||||
@pytest.mark.parametrize(
|
||||
("status", "failures", "expected_backoffs"),
|
||||
[
|
||||
(429, 1, [1]),
|
||||
(500, 1, [1]),
|
||||
(502, 1, [1]),
|
||||
(503, 1, [1]),
|
||||
(504, 1, [1]),
|
||||
pytest.param(None, 2, [1, 2], id="transport"),
|
||||
],
|
||||
)
|
||||
async def test_retryable_failure_recovers_after_exponential_backoff(
|
||||
status: int | None,
|
||||
failures: int,
|
||||
expected_backoffs: list[int],
|
||||
recorded_backoffs: list[int],
|
||||
) -> None:
|
||||
attempts = 0
|
||||
sleeps: list[int] = []
|
||||
|
||||
async def handler(_request: httpx.Request) -> httpx.Response:
|
||||
async def handler(request: httpx.Request) -> httpx.Response:
|
||||
nonlocal attempts
|
||||
attempts += 1
|
||||
if attempts == 1:
|
||||
if attempts <= failures:
|
||||
if status is None:
|
||||
raise httpx.ConnectError("connection refused", request=request)
|
||||
return httpx.Response(status)
|
||||
return httpx.Response(200, json={"default_branch": "main"})
|
||||
|
||||
async def sleep(delay: int) -> None:
|
||||
sleeps.append(delay)
|
||||
|
||||
monkeypatch.setattr("agentci.integrations.gitea.client.asyncio.sleep", sleep)
|
||||
async with gitea_client(handler) as client:
|
||||
assert await client.default_branch("org", "repo") == "main"
|
||||
|
||||
assert attempts == 2
|
||||
assert sleeps == [1]
|
||||
assert attempts == failures + 1
|
||||
assert recorded_backoffs == expected_backoffs
|
||||
|
||||
|
||||
async def test_retryable_status_exhaustion_uses_exponential_backoff(
|
||||
monkeypatch: pytest.MonkeyPatch,
|
||||
@pytest.mark.parametrize(
|
||||
("status", "retries", "expected_message", "expected_cause", "expected_backoffs"),
|
||||
[
|
||||
pytest.param(
|
||||
503,
|
||||
3,
|
||||
"Gitea remained unavailable for GET /repos/org/repo",
|
||||
None,
|
||||
[1, 2],
|
||||
id="status",
|
||||
),
|
||||
pytest.param(
|
||||
None,
|
||||
2,
|
||||
"Gitea request failed: GET /repos/org/repo",
|
||||
httpx.ConnectError,
|
||||
[1],
|
||||
id="transport",
|
||||
),
|
||||
],
|
||||
)
|
||||
async def test_retry_exhaustion_reports_failure(
|
||||
status: int | None,
|
||||
retries: int,
|
||||
expected_message: str,
|
||||
expected_cause: type[BaseException] | None,
|
||||
expected_backoffs: list[int],
|
||||
recorded_backoffs: list[int],
|
||||
) -> None:
|
||||
attempts = 0
|
||||
sleeps: list[int] = []
|
||||
|
||||
async def handler(_request: httpx.Request) -> httpx.Response:
|
||||
nonlocal attempts
|
||||
attempts += 1
|
||||
return httpx.Response(503)
|
||||
|
||||
async def sleep(delay: int) -> None:
|
||||
sleeps.append(delay)
|
||||
|
||||
monkeypatch.setattr("agentci.integrations.gitea.client.asyncio.sleep", sleep)
|
||||
async with gitea_client(handler) as client:
|
||||
with pytest.raises(
|
||||
GiteaError,
|
||||
match="Gitea remained unavailable for GET /repos/org/repo",
|
||||
):
|
||||
await client.default_branch("org", "repo")
|
||||
|
||||
assert attempts == 3
|
||||
assert sleeps == [1, 2]
|
||||
|
||||
|
||||
async def test_transport_failure_retries_and_recovers(monkeypatch: pytest.MonkeyPatch) -> None:
|
||||
attempts = 0
|
||||
sleeps: list[int] = []
|
||||
|
||||
async def handler(request: httpx.Request) -> httpx.Response:
|
||||
nonlocal attempts
|
||||
attempts += 1
|
||||
if attempts < 3:
|
||||
if status is None:
|
||||
raise httpx.ConnectError("connection refused", request=request)
|
||||
return httpx.Response(200, json={"default_branch": "main"})
|
||||
return httpx.Response(status)
|
||||
|
||||
async def sleep(delay: int) -> None:
|
||||
sleeps.append(delay)
|
||||
|
||||
monkeypatch.setattr("agentci.integrations.gitea.client.asyncio.sleep", sleep)
|
||||
async with gitea_client(handler) as client:
|
||||
assert await client.default_branch("org", "repo") == "main"
|
||||
|
||||
assert attempts == 3
|
||||
assert sleeps == [1, 2]
|
||||
|
||||
|
||||
async def test_transport_failure_exhaustion_preserves_cause(
|
||||
monkeypatch: pytest.MonkeyPatch,
|
||||
) -> None:
|
||||
attempts = 0
|
||||
sleeps: list[int] = []
|
||||
|
||||
async def handler(request: httpx.Request) -> httpx.Response:
|
||||
nonlocal attempts
|
||||
attempts += 1
|
||||
raise httpx.ConnectError("connection refused", request=request)
|
||||
|
||||
async def sleep(delay: int) -> None:
|
||||
sleeps.append(delay)
|
||||
|
||||
monkeypatch.setattr("agentci.integrations.gitea.client.asyncio.sleep", sleep)
|
||||
async with gitea_client(handler, retries=2) as client:
|
||||
with pytest.raises(
|
||||
GiteaError,
|
||||
match="Gitea request failed: GET /repos/org/repo",
|
||||
) as raised:
|
||||
async with gitea_client(handler, retries=retries) as client:
|
||||
with pytest.raises(GiteaError, match=expected_message) as raised:
|
||||
await client.default_branch("org", "repo")
|
||||
|
||||
assert isinstance(raised.value.__cause__, httpx.ConnectError)
|
||||
assert attempts == 2
|
||||
assert sleeps == [1]
|
||||
if expected_cause is None:
|
||||
assert raised.value.__cause__ is None
|
||||
else:
|
||||
assert isinstance(raised.value.__cause__, expected_cause)
|
||||
assert attempts == retries
|
||||
assert recorded_backoffs == expected_backoffs
|
||||
|
||||
Reference in New Issue
Block a user