tox: eighty-six 3.6

When we landed d53b3e8400 ("pytests: update fixtures to not use
deprecated event_loop") we promised ourselves "this is the last time".
The intensity of hacks required to keep up with being able to run the
same set of tests under modern Python versions and a long-dead version
which isn't getting any new releases of pytest or its plugins is just
too much.

And here we are again.  Since fb319e4f43 ("Pin pytest-asyncio version
to unbreak main") we've been avoiding taking a new pytest-asyncio
because it breaks our fragile pile of hacks yet again.  It's time.

Drop support for running the pytest unit tests tests under Python 3.6
and remove the massive pile of hacks we had for that.  Without any
further effort this has already unbroken the latest pytest-asyncio
version, so we can unpin it.

We still have full coverage of the bridge code on Python 3.6 via
integration testing (beiboot vs container-ws on RHEL 8) and the fact
that we're not as actively hacking on the bridge code anymore means that
any time that was once saved by getting "quick fails" from unit tests is
no longer worth the large and growing amount of effort required to keep
those tests from constantly breaking.
This commit is contained in:
Allison Karlitskaya 2025-09-22 12:10:45 +02:00 committed by Allison Karlitskaya
parent 289b449e3a
commit 5acce3c6da
2 changed files with 31 additions and 60 deletions

View file

@ -213,7 +213,7 @@ envlist = lint,pytest
isolated_build = True
requires = virtualenv<20.22.0
labels =
venv = py3{6,9,10,11,12,13}-pytest
venv = py3{9,10,11,12,13}-pytest
# The default test environments use system packages and never PyPI.
[testenv:{lint,pytest}]
@ -221,7 +221,7 @@ sitepackages = True
install_command = python3 -m pip install --no-index --no-build-isolation {opts} {packages}
wheel_build_env = pkg
# All other environments (names like py311-lint, py36-pytest, etc) are isolated
# All other environments (names like py311-lint, py39-pytest, etc) are isolated
# from the system and get their packages from PyPI, according to the specific
# test environment being requested. We build the wheel in a common environment.
# These tests will not run the PCP tests as there is no wheel available.
@ -235,7 +235,7 @@ deps =
lint: ruff
lint: vulture
pytest
pytest-asyncio < 1.1.1
pytest-asyncio
pytest: pytest-cov
pytest: pytest-timeout
pytest: pytest-xdist
@ -243,6 +243,6 @@ allowlist_externals = test/common/static-code
setenv =
NO_QUNIT=1
commands =
pytest: python3 -m pytest -opythonpath= {posargs}
pytest: python3 -m pytest -opythonpath=test/common {posargs}
lint: test/common/static-code --tap
"""

View file

@ -1,32 +1,16 @@
import asyncio
import os
import subprocess
import sys
from typing import Any, AsyncGenerator, Iterable
from importlib.metadata import version
from pathlib import Path
from typing import TYPE_CHECKING, AsyncGenerator
import pytest
import pytest_asyncio
from packaging.specifiers import SpecifierSet
from cockpit import polyfills
from cockpit._vendor.systemd_ctypes import EventLoopPolicy
polyfills.install()
try:
import pytest_asyncio._version
have_event_loop_policy_fixture = pytest_asyncio._version.__version_tuple__ >= (0, 22)
except (ImportError, AttributeError):
# old versions don't have __version_tuple__ or event_loop_policy
have_event_loop_policy_fixture = False
# pytest fixture polyfill
# pytest_asyncio.fixture() was added in 0.22.0
if not hasattr(pytest_asyncio, 'fixture'):
# mypy is strict here because of slightly different signatures.
# On old versions of pytest_asyncio this is fine to do as `pytest.fixture`
# was always used before `pytest_asyncio.fixture` existed.
pytest_asyncio.fixture = pytest.fixture # type: ignore[assignment]
def any_subprocesses() -> bool:
# Make sure we don't leak subprocesses
@ -42,66 +26,53 @@ def any_subprocesses() -> bool:
# - even sourcing this will break old python versions in ways we don't care to fix
# - even sourcing this requires aiohttp, which isn't available everywhere
# - there are general issues running under tox which need investigation
# - this also requires a bots/ checkout which we don't have in distros
if 'NO_QUNIT' in os.environ:
@pytest.hookimpl
def pytest_ignore_collect(path) -> 'bool | None':
return path.basename == 'test_browser.py' or None
def pytest_ignore_collect(collection_path: Path) -> bool:
return collection_path.name == "test_browser.py"
# if m.image in ['ubuntu-2204', '🙃']:
if not TYPE_CHECKING and version("pytest") not in SpecifierSet(">=7.0"):
@pytest.hookimpl
def pytest_ignore_collect(path: object) -> bool:
return Path(str(path)).name == "test_browser.py"
else:
pytest_plugins = "js_coverage"
if not have_event_loop_policy_fixture:
@pytest.fixture
def event_loop_policy() -> asyncio.AbstractEventLoopPolicy:
return EventLoopPolicy()
# if m.image in ['ubuntu-2204', 'ubuntu-2404', '🙃']:
if version("pytest-asyncio") not in SpecifierSet(">=0.22"):
# Compatibility with pre-`event_loop_policy` versions of pytest-asyncio
@pytest.fixture(autouse=True)
def event_loop(monkeypatch) -> Iterable[asyncio.AbstractEventLoop]:
loop = EventLoopPolicy().new_event_loop()
# Polyfills for Python 3.6
if sys.version_info < (3, 7, 0):
def all_tasks(loop=loop):
return {t for t in asyncio.Task.all_tasks(loop=loop) if not t.done()}
monkeypatch.setattr(asyncio, 'get_running_loop', lambda: loop, raising=False)
monkeypatch.setattr(asyncio, 'create_task', loop.create_task, raising=False)
monkeypatch.setattr(asyncio, 'all_tasks', all_tasks, raising=False)
yield loop
loop.close()
else:
@pytest.fixture
def event_loop_policy() -> asyncio.AbstractEventLoopPolicy:
return EventLoopPolicy()
def filter_current_task(tasks: 'set[asyncio.Task[Any]]',
current_task: 'asyncio.Task[Any]') -> 'set[asyncio.Task[Any]]':
return {task for task in tasks if task is not current_task}
def event_loop() -> asyncio.AbstractEventLoop:
return EventLoopPolicy().new_event_loop()
@pytest_asyncio.fixture(autouse=True)
async def _check_settled() -> AsyncGenerator[None, None]:
yield
# python 3.6 asyncio does not have 'current_task' attribute
if hasattr(asyncio, 'current_task'):
current_task = asyncio.current_task()
else:
current_task = asyncio.Task.current_task() # type: ignore[attr-defined]
current_task = asyncio.current_task()
assert current_task is not None
event_loop = asyncio.get_running_loop()
# Let all tasks and subprocesses run to completion
for _ in range(200):
all_tasks = filter_current_task(asyncio.all_tasks(loop=event_loop), current_task)
all_tasks = asyncio.all_tasks(loop=event_loop) - {current_task}
if not (all_tasks or any_subprocesses()):
break
await asyncio.sleep(0.005)
# No tasks left
assert filter_current_task(asyncio.all_tasks(loop=event_loop), current_task) == set()
assert asyncio.all_tasks(loop=event_loop) == {current_task}
# No subprocesses left
if any_subprocesses():