112 lines
3.8 KiB
Python
112 lines
3.8 KiB
Python
|
|
#!/usr/bin/env python3
|
||
|
|
# © 2026 TechnoLibre (http://www.technolibre.ca)
|
||
|
|
# License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl)
|
||
|
|
"""Le pool de coroutines, et les deux retraits d'asyncio qui le tuaient.
|
||
|
|
|
||
|
|
Le drapeau `--max_process` de `run_parallel_test.py` et de
|
||
|
|
`show_evolution_module.py` passe par cette classe, et elle ne pouvait plus
|
||
|
|
tourner du tout : `asyncio.get_event_loop()` LÈVE hors d'une loop en marche
|
||
|
|
depuis Python 3.14, ce qui rendait l'instanciation impossible, et le mot-clé
|
||
|
|
`loop=` a été RETIRÉ d'`asyncio.wait` en Python 3.10, ce qui lève un
|
||
|
|
TypeError. Deux échecs à des étages différents, tous deux sur le chemin d'un
|
||
|
|
drapeau que l'aide annonce encore.
|
||
|
|
|
||
|
|
Ces tests exercent le pool pour de vrai — de vraies coroutines, une vraie
|
||
|
|
loop, un vrai résultat — parce que c'est la seule façon de prouver qu'un
|
||
|
|
retrait d'API ne le traverse plus. Ils n'affirment rien sur l'ordre des
|
||
|
|
résultats : le pool rend ce qui finit, dans l'ordre où cela finit.
|
||
|
|
"""
|
||
|
|
|
||
|
|
import asyncio
|
||
|
|
import io
|
||
|
|
import unittest
|
||
|
|
from contextlib import redirect_stdout
|
||
|
|
|
||
|
|
from script.lib_asyncio import AsyncioPool
|
||
|
|
|
||
|
|
|
||
|
|
async def rendre(valeur, delai=0):
|
||
|
|
if delai:
|
||
|
|
await asyncio.sleep(delai)
|
||
|
|
return valeur
|
||
|
|
|
||
|
|
|
||
|
|
class TestLePoolTourne(unittest.TestCase):
|
||
|
|
"""Le pool s'instancie, tourne et se ferme sans loop fournie."""
|
||
|
|
|
||
|
|
def _lancer(self, pool):
|
||
|
|
"""Exécute en silence : le pool imprime son état à chaque ajout."""
|
||
|
|
with redirect_stdout(io.StringIO()):
|
||
|
|
return pool.run_until_complete()
|
||
|
|
|
||
|
|
def tearDown(self):
|
||
|
|
asyncio.set_event_loop(None)
|
||
|
|
|
||
|
|
def test_it_can_be_built_without_a_loop(self):
|
||
|
|
"""Le cas qui levait : construire hors de toute loop en marche."""
|
||
|
|
pool = AsyncioPool(2)
|
||
|
|
self.assertIsNone(pool._loop)
|
||
|
|
|
||
|
|
def test_it_runs_more_coros_than_its_concurrency(self):
|
||
|
|
pool = AsyncioPool(2)
|
||
|
|
with redirect_stdout(io.StringIO()):
|
||
|
|
for valeur in range(5):
|
||
|
|
pool.add_coro(rendre(valeur))
|
||
|
|
resultats = self._lancer(pool)
|
||
|
|
pool.close()
|
||
|
|
self.assertEqual(sorted(resultats), [0, 1, 2, 3, 4])
|
||
|
|
|
||
|
|
def test_a_single_coro(self):
|
||
|
|
pool = AsyncioPool(4)
|
||
|
|
with redirect_stdout(io.StringIO()):
|
||
|
|
pool.add_coro(rendre("seule"))
|
||
|
|
resultats = self._lancer(pool)
|
||
|
|
pool.close()
|
||
|
|
self.assertEqual(resultats, ["seule"])
|
||
|
|
|
||
|
|
def test_staggered_coros_all_come_back(self):
|
||
|
|
"""Des durées inégales : c'est là que FIRST_COMPLETED est exercé."""
|
||
|
|
pool = AsyncioPool(2)
|
||
|
|
with redirect_stdout(io.StringIO()):
|
||
|
|
for valeur, delai in ((1, 0.03), (2, 0.01), (3, 0.02), (4, 0)):
|
||
|
|
pool.add_coro(rendre(valeur, delai))
|
||
|
|
resultats = self._lancer(pool)
|
||
|
|
pool.close()
|
||
|
|
self.assertEqual(sorted(resultats), [1, 2, 3, 4])
|
||
|
|
|
||
|
|
|
||
|
|
class TestLaFermeture(unittest.TestCase):
|
||
|
|
"""`close` ne ferme que ce que la classe a ouvert."""
|
||
|
|
|
||
|
|
def tearDown(self):
|
||
|
|
asyncio.set_event_loop(None)
|
||
|
|
|
||
|
|
def test_close_before_any_run_is_harmless(self):
|
||
|
|
AsyncioPool(2).close()
|
||
|
|
|
||
|
|
def test_close_closes_the_loop_it_created(self):
|
||
|
|
pool = AsyncioPool(1)
|
||
|
|
with redirect_stdout(io.StringIO()):
|
||
|
|
pool.add_coro(rendre(1))
|
||
|
|
pool.run_until_complete()
|
||
|
|
boucle = pool._loop
|
||
|
|
pool.close()
|
||
|
|
self.assertTrue(boucle.is_closed())
|
||
|
|
|
||
|
|
def test_a_borrowed_loop_is_left_open(self):
|
||
|
|
"""Une loop reçue appartient à l'appelant, qui compte encore dessus."""
|
||
|
|
boucle = asyncio.new_event_loop()
|
||
|
|
try:
|
||
|
|
pool = AsyncioPool(1, loop=boucle)
|
||
|
|
with redirect_stdout(io.StringIO()):
|
||
|
|
pool.add_coro(rendre(1))
|
||
|
|
pool.run_until_complete()
|
||
|
|
pool.close()
|
||
|
|
self.assertFalse(boucle.is_closed())
|
||
|
|
finally:
|
||
|
|
boucle.close()
|
||
|
|
|
||
|
|
|
||
|
|
if __name__ == "__main__":
|
||
|
|
unittest.main()
|