From fcac234e2b2427fc00ff8ec490495ee531fe5ff0 Mon Sep 17 00:00:00 2001 From: traoreera Date: Fri, 2 Oct 2026 00:01:44 +0000 Subject: [PATCH 1/2] test: un test ne pose plus RLIMIT_AS sur le process pytest (can't start new thread en 3.14) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit TestMemoryLimiter.test_apply_positive appelait MemoryLimiter.apply(512) pour de bon dans le process pytest : setrlimit(RLIMIT_AS, 512 Mo) est irréversible (la limite dure baisse) et valable pour toute la session. Chaque thread créé ensuite réserve ~72 Mo d'espace d'adressage (pile + arène glibc) : après 8 threads, RuntimeError « can't start new thread » — reproduit en local. Les tests placés plus loin dans la session (TestClient de test_xcore_plugin_routes) échouaient selon l'empreinte mémoire de l'interpréteur : jamais en 3.12/3.13, systématiquement sur le runner en 3.14 (Quick Test : « can't start new thread » puis abort dans pydantic-core sur un thread de portail). - le test vérifie l'appel (resource.setrlimit patché) au lieu de l'exécuter, plus un test du cas d'échec (journalisé, pas levé) - tests/conftest.py : fixture autouse qui échoue si un test modifie RLIMIT_AS / RLIMIT_DATA / RLIMIT_CPU du process pytest --- tests/conftest.py | 42 +++++++++++++++++++++++++++++ tests/unit/kernel/test_isolation.py | 29 +++++++++++++++++--- 2 files changed, 67 insertions(+), 4 deletions(-) diff --git a/tests/conftest.py b/tests/conftest.py index 621d9709..c3102c53 100644 --- a/tests/conftest.py +++ b/tests/conftest.py @@ -6,6 +6,7 @@ import os import shutil +import sys import tempfile from pathlib import Path from typing import Generator @@ -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.""" diff --git a/tests/unit/kernel/test_isolation.py b/tests/unit/kernel/test_isolation.py index 2d98ae53..2ed037fa 100644 --- a/tests/unit/kernel/test_isolation.py +++ b/tests/unit/kernel/test_isolation.py @@ -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 From 4884d6b7289eb38eb5ceb50a0af1ce820c6c619b Mon Sep 17 00:00:00 2001 From: traoreera Date: Fri, 2 Oct 2026 09:01:58 +0000 Subject: [PATCH 2/2] ci: tester aussi sur Python 3.14 MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit requires-python est >=3.12,<4.0 : 3.14 est dans la plage supportée, et c'est la version utilisée en développement et pour les smoke tests de release, mais la matrice CI ne testait que 3.12 et 3.13. Elle couvre maintenant 3.12, 3.13 et 3.14. --- .github/workflows/ci.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 8aea0871..0ed2b136 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -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