1354679e33
Phase 2 Tasks T2.1-T2.4 of the startup_speedup_20260606 track.
NEW: src/io_pool.py
make_io_pool() factory: 4-worker ThreadPoolExecutor with
thread_name_prefix='controller-io'. The sanctioned way for any
background work. Replaces ad-hoc threading.Thread() calls per
the 'no new threads' rule.
NEW: src/warmup.py
WarmupManager: manages a list of modules to import on the shared
pool. Public API:
.submit(modules) - start warmup (call once)
.status() - {pending, completed, failed}
.is_done() - bool
.wait(timeout) - block until done
.on_complete(callback) - register completion callback
.reset() - clear state
Thread-safe (lock-guarded). 10 tests cover all paths.
NEW: tests/test_io_pool.py (4 tests):
- ThreadPoolExecutor returned
- 4 workers
- Threads named 'controller-io-*'
- Jobs run in parallel (barrier test)
NEW: tests/test_warmup.py (10 tests):
- One job per module submitted
- Initial pending list correct
- Failed imports tracked
- Done event set after all complete
- wait() blocks until done
- on_complete callback fires (and immediately if already done)
- Modules actually end up in sys.modules
- reset() clears state
- Jobs run concurrently (not serially)
All 14 tests pass. AppController integration is the next commit.
130 lines
3.3 KiB
Python
130 lines
3.3 KiB
Python
"""Tests for src/warmup.py (the WarmupManager class)."""
|
|
|
|
import sys
|
|
import threading
|
|
import time
|
|
from concurrent.futures import ThreadPoolExecutor
|
|
from pathlib import Path
|
|
|
|
ROOT = Path(__file__).resolve().parent.parent
|
|
sys.path.insert(0, str(ROOT))
|
|
|
|
from src.warmup import WarmupManager # noqa: E402
|
|
|
|
|
|
def _make_pool() -> ThreadPoolExecutor:
|
|
return ThreadPoolExecutor(max_workers=2, thread_name_prefix="warmup-test")
|
|
|
|
|
|
def test_warmup_submits_one_job_per_module() -> None:
|
|
pool = _make_pool()
|
|
mgr = WarmupManager(pool)
|
|
mgr.submit(["json", "os", "sys"])
|
|
time.sleep(0.5)
|
|
status = mgr.status()
|
|
assert len(status["pending"]) == 0
|
|
assert set(status["completed"]) == {"json", "os", "sys"}
|
|
assert status["failed"] == []
|
|
pool.shutdown(wait=False)
|
|
|
|
|
|
def test_warmup_status_pending_initially() -> None:
|
|
pool = _make_pool()
|
|
mgr = WarmupManager(pool)
|
|
mgr.submit(["json"])
|
|
snap = mgr.status()
|
|
assert "pending" in snap
|
|
assert "completed" in snap
|
|
assert "failed" in snap
|
|
pool.shutdown(wait=False)
|
|
mgr.wait(timeout=2)
|
|
|
|
|
|
def test_warmup_status_reflects_failures() -> None:
|
|
pool = _make_pool()
|
|
mgr = WarmupManager(pool)
|
|
mgr.submit(["definitely_not_a_real_module_xyz123"])
|
|
mgr.wait(timeout=5)
|
|
status = mgr.status()
|
|
assert "definitely_not_a_real_module_xyz123" in status["failed"]
|
|
assert status["completed"] == []
|
|
pool.shutdown(wait=False)
|
|
|
|
|
|
def test_warmup_done_event_set_after_all_complete() -> None:
|
|
pool = _make_pool()
|
|
mgr = WarmupManager(pool)
|
|
mgr.submit(["os", "sys"])
|
|
assert not mgr.is_done()
|
|
mgr.wait(timeout=5)
|
|
assert mgr.is_done()
|
|
pool.shutdown(wait=False)
|
|
|
|
|
|
def test_warmup_wait_blocks_until_done() -> None:
|
|
pool = _make_pool()
|
|
mgr = WarmupManager(pool)
|
|
mgr.submit(["json", "os"])
|
|
completed = mgr.wait(timeout=10)
|
|
assert completed is True
|
|
pool.shutdown(wait=False)
|
|
|
|
|
|
def test_warmup_on_complete_callback_fires() -> None:
|
|
pool = _make_pool()
|
|
mgr = WarmupManager(pool)
|
|
received: list[dict] = []
|
|
mgr.on_complete(lambda status: received.append(dict(status)))
|
|
mgr.submit(["json"])
|
|
mgr.wait(timeout=5)
|
|
assert len(received) == 1
|
|
assert "json" in received[0]["completed"]
|
|
pool.shutdown(wait=False)
|
|
|
|
|
|
def test_warmup_on_complete_callback_fires_immediately_if_already_done() -> None:
|
|
pool = _make_pool()
|
|
mgr = WarmupManager(pool)
|
|
mgr.submit(["json"])
|
|
mgr.wait(timeout=5)
|
|
received: list[dict] = []
|
|
mgr.on_complete(lambda status: received.append(dict(status)))
|
|
assert len(received) == 1
|
|
pool.shutdown(wait=False)
|
|
|
|
|
|
def test_warmup_modules_actually_loaded_in_sys_modules() -> None:
|
|
pool = _make_pool()
|
|
mgr = WarmupManager(pool)
|
|
mgr.submit(["json", "os"])
|
|
mgr.wait(timeout=5)
|
|
import json as _json
|
|
import os as _os
|
|
assert _json in sys.modules.values()
|
|
assert _os in sys.modules.values()
|
|
pool.shutdown(wait=False)
|
|
|
|
|
|
def test_warmup_reset_clears_state() -> None:
|
|
pool = _make_pool()
|
|
mgr = WarmupManager(pool)
|
|
mgr.submit(["json"])
|
|
mgr.wait(timeout=5)
|
|
assert mgr.is_done()
|
|
mgr.reset()
|
|
assert not mgr.is_done()
|
|
assert mgr.status()["pending"] == []
|
|
assert mgr.status()["completed"] == []
|
|
pool.shutdown(wait=False)
|
|
|
|
|
|
def test_warmup_runs_jobs_concurrently_not_serially() -> None:
|
|
pool = _make_pool()
|
|
mgr = WarmupManager(pool)
|
|
mgr.submit(["json", "os", "sys", "re"])
|
|
started = time.perf_counter()
|
|
mgr.wait(timeout=5)
|
|
elapsed = time.perf_counter() - started
|
|
assert elapsed < 1.0, f"warmup took {elapsed:.2f}s; expected concurrent execution"
|
|
pool.shutdown(wait=False)
|