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
2 changes: 1 addition & 1 deletion .github/workflows/ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -78,7 +78,7 @@ jobs:
strategy:
fail-fast: false
matrix:
python-version: ["3.12", "3.13"]
python-version: ["3.12", "3.13", "3.14"]

steps:
- uses: actions/checkout@v4
Expand Down
42 changes: 42 additions & 0 deletions tests/conftest.py
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@

import os
import shutil
import sys
import tempfile
from pathlib import Path
from typing import Generator
Expand Down Expand Up @@ -163,6 +164,47 @@ def _cleanup_stray_tmp_dirs() -> Generator[None, None, None]:
sweep_stray_tmp_dirs()


@pytest.fixture(autouse=True)
def _no_process_rlimit_leak() -> Generator[None, None, None]:
"""
Un test ne doit jamais modifier les limites de ressources du process pytest.

RLIMIT_AS / RLIMIT_DATA / RLIMIT_CPU sont globaux au process et la limite DURE
ne se relève pas : un `setrlimit` réel dans un test s'applique à tous les tests
suivants de la session. RLIMIT_AS à 512 Mo suffisait à faire échouer
`threading.Thread.start()` (« can't start new thread ») dans les tests placés
plus loin — selon l'empreinte mémoire de l'interpréteur. Tester ces limites en
patchant `resource.setrlimit`, ou dans un sous-processus.
"""
if sys.platform == "win32":
yield
return
import resource

tracked = [
getattr(resource, name)
for name in ("RLIMIT_AS", "RLIMIT_DATA", "RLIMIT_CPU")
if hasattr(resource, name)
]
before = {r: resource.getrlimit(r) for r in tracked}
yield
changed = {
r: resource.getrlimit(r) for r in tracked if resource.getrlimit(r) != before[r]
}
if changed:
for r, (soft, _hard) in changed.items(): # remet au moins la limite souple
try:
resource.setrlimit(r, (before[r][0], resource.getrlimit(r)[1]))
except (ValueError, OSError):
pass
pytest.fail(
"ce test a modifié les limites de ressources du process pytest "
f"({sorted(changed)}) — irréversible et valable pour toute la session ; "
"patcher resource.setrlimit ou utiliser un sous-processus",
pytrace=False,
)


@pytest.fixture
def invalid_plugin_dir(plugins_dir: Path) -> Path:
"""Plugin invalide — manque PLUGIN_INFO."""
Expand Down
29 changes: 25 additions & 4 deletions tests/unit/kernel/test_isolation.py
Original file line number Diff line number Diff line change
Expand Up @@ -69,10 +69,31 @@ def test_apply_negative_noop(self):
MemoryLimiter.apply(-1) # should not raise

def test_apply_positive(self):
"""
Vérifie l'appel à setrlimit SANS l'exécuter : RLIMIT_AS est irréversible
(la limite dure baisse) et s'appliquerait à tout le process pytest — chaque
thread créé ensuite (TestClient, to_thread…) réserve ~72 Mo d'espace
d'adressage, et au bout de quelques-uns `can't start new thread`. Les tests
placés après celui-ci dans la session plantaient selon l'empreinte mémoire
de l'interpréteur (vu sur Python 3.14).
"""
import resource
import sys
from unittest.mock import patch

if sys.platform == "win32":
pytest.skip("Not supported on Windows")
with patch("resource.setrlimit") as mock_setrlimit:
MemoryLimiter.apply(512)

limit = 512 * 1024 * 1024
mock_setrlimit.assert_called_once_with(resource.RLIMIT_AS, (limit, limit))

def test_apply_failure_is_logged_not_raised(self):
import sys
from unittest.mock import patch

if sys.platform == "win32":
pytest.skip("Not supported on Windows")
try:
MemoryLimiter.apply(512) # may raise or not depending on OS limits
except Exception:
pass # some systems don't allow raising RLIMIT_AS
with patch("resource.setrlimit", side_effect=ValueError("not allowed")):
MemoryLimiter.apply(512) # ne lève pas
Loading