syntheogenesis / tests /test_scoring.py
Tengo Gzirishvili
Turing: wire ESM-2, fix auth header, friendly credits message, tool cards
1290677
Raw
History Blame Contribute Delete
4.28 kB
"""Tests for dee/core/scoring.py — the shared ESM-2 scorer cache and
scoring concurrency guard used by both dee/server.py's /api/run pipeline
and dee/core/agent_tools.py's design_variant_library tool.
"""
import threading
import time
import pandas as pd
import pytest
from dee.core import scoring
@pytest.fixture(autouse=True)
def _semaphore_not_leaked():
"""Every test must return SCORING_SEMAPHORE to its initial (available)
state — a leaked acquire would hang every later test using it."""
yield
acquired = scoring.SCORING_SEMAPHORE.acquire(timeout=0)
assert acquired, "a test left SCORING_SEMAPHORE held"
scoring.SCORING_SEMAPHORE.release()
@pytest.fixture(autouse=True)
def _clear_scorer_cache():
scoring._SCORER_CACHE.clear()
yield
scoring._SCORER_CACHE.clear()
def test_effective_model_downgrades_gpu_models_without_gpu(monkeypatch):
monkeypatch.setattr(scoring, "gpu_available", lambda: False)
assert scoring.effective_model("medium") == "small"
assert scoring.effective_model("large") == "small"
assert scoring.effective_model("small") == "small"
assert scoring.effective_model(None) == "small"
def test_effective_model_keeps_gpu_models_with_gpu(monkeypatch):
monkeypatch.setattr(scoring, "gpu_available", lambda: True)
assert scoring.effective_model("medium") == "medium"
assert scoring.effective_model("large") == "large"
def test_get_scorer_caches_by_effective_config(monkeypatch):
monkeypatch.setattr(scoring, "gpu_available", lambda: False)
built = []
class _FakeScorer:
def __init__(self, config):
built.append(config)
monkeypatch.setattr("dee.models.scorer.ESM2Scorer", _FakeScorer)
a = scoring.get_scorer("small")
b = scoring.get_scorer("small")
# 'medium' downgrades to 'small' on a no-GPU host — same cache key.
c = scoring.get_scorer("medium")
assert a is b is c
assert len(built) == 1
def test_get_scorer_separate_cache_entries_per_model(monkeypatch):
monkeypatch.setattr(scoring, "gpu_available", lambda: True)
built = []
class _FakeScorer:
def __init__(self, config):
built.append(config.model_name)
monkeypatch.setattr("dee.models.scorer.ESM2Scorer", _FakeScorer)
scoring.get_scorer("small")
scoring.get_scorer("medium")
assert built == ["small", "medium"]
def test_score_guarded_calls_scorer_and_returns_result():
df = pd.DataFrame({"position": [0], "wt_aa": ["A"], "mut_aa": ["G"], "delta_ll": [1.0]})
class _Scorer:
def score_all_substitutions(self, protein):
assert protein == "ACDE"
return df
result = scoring.score_guarded(_Scorer(), "ACDE")
assert result is df
def test_score_guarded_raises_busy_when_semaphore_held():
scoring.SCORING_SEMAPHORE.acquire() # simulate another request scoring
try:
class _Scorer:
def score_all_substitutions(self, protein):
raise AssertionError("must not run while the semaphore is held")
with pytest.raises(scoring.ScoringBusyError):
scoring.score_guarded(_Scorer(), "ACDE", wait_timeout=0.05)
finally:
scoring.SCORING_SEMAPHORE.release()
def test_score_guarded_blocks_then_succeeds_once_released():
scoring.SCORING_SEMAPHORE.acquire()
def _release_soon():
time.sleep(0.1)
scoring.SCORING_SEMAPHORE.release()
threading.Thread(target=_release_soon, daemon=True).start()
class _Scorer:
def score_all_substitutions(self, protein):
return "scored"
# No wait_timeout — blocks until the background thread releases, then
# proceeds. If this hangs, the semaphore isn't being released/acquired
# correctly.
result = scoring.score_guarded(_Scorer(), "ACDE", wait_timeout=2.0)
assert result == "scored"
def test_score_guarded_releases_semaphore_even_if_scorer_raises():
class _Scorer:
def score_all_substitutions(self, protein):
raise RuntimeError("boom")
with pytest.raises(RuntimeError):
scoring.score_guarded(_Scorer(), "ACDE")
# The autouse _semaphore_not_leaked fixture asserts this held true;
# this test exists to name the specific behavior it's protecting.