Skip to content

Commit

Permalink
fix: ensure all timers are cancelled when after staggered race finish…
Browse files Browse the repository at this point in the history
…es (#136)
  • Loading branch information
bdraco authored Feb 7, 2025
1 parent cbc674d commit f75891d
Show file tree
Hide file tree
Showing 3 changed files with 35 additions and 3 deletions.
2 changes: 2 additions & 0 deletions pyproject.toml
Original file line number Diff line number Diff line change
Expand Up @@ -82,6 +82,8 @@ prerelease = true
[tool.pytest.ini_options]
addopts = "-v -Wdefault --cov=aiohappyeyeballs --cov-report=term-missing:skip-covered"
pythonpath = ["src"]
asyncio_mode = "auto"
asyncio_default_fixture_loop_scope = "function"

[tool.coverage.run]
branch = true
Expand Down
4 changes: 2 additions & 2 deletions src/aiohappyeyeballs/_staggered.py
Original file line number Diff line number Diff line change
Expand Up @@ -156,9 +156,9 @@ async def run_one_coro(
# so we have no winner and all coroutines failed.
break

while tasks:
while tasks or start_next:
done = await _wait_one(
[*tasks, start_next] if start_next else tasks, loop
(*tasks, start_next) if start_next else tasks, loop
)
if done is start_next:
# The current task has failed or the timer has expired
Expand Down
32 changes: 31 additions & 1 deletion tests/conftest.py
Original file line number Diff line number Diff line change
@@ -1,21 +1,45 @@
"""Configuration for the tests."""

import asyncio
import reprlib
import threading
from asyncio.events import AbstractEventLoop, TimerHandle
from contextlib import contextmanager
from typing import Generator

import pytest


@pytest.fixture(autouse=True)
def verify_threads_ended():
def verify_threads_ended() -> Generator[None, None, None]:
"""Verify that the threads are not running after the test."""
threads_before = frozenset(threading.enumerate())
yield
threads = frozenset(threading.enumerate()) - threads_before
assert not threads


def get_scheduled_timer_handles(loop: AbstractEventLoop) -> list[TimerHandle]:
"""Return a list of scheduled TimerHandles."""
handles: list[TimerHandle] = loop._scheduled # type: ignore[attr-defined]
return handles


@contextmanager
def long_repr_strings() -> Generator[None, None, None]:
"""Increase reprlib maxstring and maxother to 300."""
arepr = reprlib.aRepr
original_maxstring = arepr.maxstring
original_maxother = arepr.maxother
arepr.maxstring = 300
arepr.maxother = 300
try:
yield
finally:
arepr.maxstring = original_maxstring
arepr.maxother = original_maxother


@pytest.fixture(autouse=True)
def verify_no_lingering_tasks(
event_loop: asyncio.AbstractEventLoop,
Expand All @@ -30,3 +54,9 @@ def verify_no_lingering_tasks(
task.cancel()
if tasks:
event_loop.run_until_complete(asyncio.wait(tasks))

for handle in get_scheduled_timer_handles(event_loop):
if not handle.cancelled():
with long_repr_strings():
pytest.fail(f"Lingering timer after test {handle!r}")
handle.cancel()

0 comments on commit f75891d

Please sign in to comment.