OC_P8 / tests /unit /test_logger.py
KLEB38's picture
Upload folder using huggingface_hub
1091d74 verified
Raw
History Blame Contribute Delete
5.08 kB
"""Unit tests for api.logger.
We never connect to a real database — the engine and connection are
replaced by mocks. The point is to verify that:
- log_prediction silently no-ops when no engine is configured;
- it scrubs NaN/Inf to None in the JSONB payloads;
- it builds an INSERT with the right values;
- it never raises, even when the DB fails.
"""
from __future__ import annotations
from unittest.mock import MagicMock
import numpy as np
import pandas as pd
import pytest
from api import db, logger as api_logger
@pytest.fixture(autouse=True)
def _reset_engine():
db.reset_engine()
yield
db.reset_engine()
def _call(features: pd.DataFrame | None = None, **overrides):
defaults = dict(
sk_id_curr=100002,
client_known=True,
raw_input={"CODE_GENDER": "M", "AMT_INCOME_TOTAL": 200_000.0},
features=features if features is not None else pd.DataFrame([{"EXT_SOURCE_1": 0.3}]),
probability_default=0.27,
decision="GRANTED",
threshold=0.33,
model_version="test-1",
latency_ms=42,
)
defaults.update(overrides)
api_logger.log_prediction(**defaults)
def test_noop_when_engine_unset() -> None:
"""No engine = no DB call = no error."""
_call() # must not raise
def _build_mock_engine(captured: dict, monkeypatch, fake_id: int = 42) -> None:
"""Wire a MagicMock engine that captures INSERT and UPDATE payloads.
log_prediction now performs two roundtrips:
1. INSERT ... RETURNING id -> mock returns fake_id from scalar_one()
2. UPDATE ... SET db_log_ms WHERE id = X
The mock distinguishes them by inspecting the compiled SQL string.
"""
fake_engine = MagicMock()
class _Result:
def scalar_one(self):
return fake_id
class _Conn:
def execute(self, stmt):
compiled = stmt.compile()
params = compiled.params
if "INSERT" in str(compiled).upper():
captured["insert"] = params
else:
captured["update"] = params
return _Result()
class _Ctx:
def __enter__(self):
return _Conn()
def __exit__(self, *a):
return False
fake_engine.begin.return_value = _Ctx()
monkeypatch.setattr(db, "_engine", fake_engine)
def test_insert_payload_contains_expected_fields(monkeypatch) -> None:
captured: dict = {}
_build_mock_engine(captured, monkeypatch)
_call(
features=pd.DataFrame([{"EXT_SOURCE_1": 0.42, "FOO": np.nan, "BAR": np.inf}]),
feature_assembly_ms=12,
inference_ms=3,
inference_cpu_ms=3,
plumbing_ms=1,
)
values = captured["insert"]
assert values["sk_id_curr"] == 100002
assert values["decision"] == "GRANTED"
assert values["probability_default"] == 0.27
assert values["client_known"] is True
assert values["features"]["EXT_SOURCE_1"] == pytest.approx(0.42)
# NaN/Inf scrubbed to None for JSONB compatibility
assert values["features"]["FOO"] is None
assert values["features"]["BAR"] is None
# Fine-grained timings propagated through to the insert payload.
assert values["feature_assembly_ms"] == 12
assert values["inference_ms"] == 3
assert values["inference_cpu_ms"] == 3
assert values["plumbing_ms"] == 1
# db_log_ms is filled in by the follow-up UPDATE, not the INSERT.
assert values["db_log_ms"] is None
# And the UPDATE carries an int db_log_ms.
assert "update" in captured
assert isinstance(captured["update"]["db_log_ms"], int)
assert captured["update"]["db_log_ms"] >= 0
def test_db_failure_is_swallowed(monkeypatch, caplog) -> None:
fake_engine = MagicMock()
fake_engine.begin.side_effect = RuntimeError("connection refused")
monkeypatch.setattr(db, "_engine", fake_engine)
_call() # must not raise
assert any("Failed to log prediction" in rec.message for rec in caplog.records)
def test_error_path_logs_status_and_message(monkeypatch) -> None:
captured: dict = {}
_build_mock_engine(captured, monkeypatch)
api_logger.log_prediction(
sk_id_curr=999,
client_known=False,
raw_input={"CODE_GENDER": "F"},
features=None,
probability_default=None,
decision=None,
threshold=0.33,
model_version="test-1",
latency_ms=5,
status_code=500,
error_message="boom",
)
values = captured["insert"]
assert values["status_code"] == 500
assert values["error_message"] == "boom"
assert values["decision"] == "ERROR"
assert values["features"] == {}
# Error rows leave timing breakdown NULL (defaults to None when omitted).
assert values["feature_assembly_ms"] is None
assert values["inference_ms"] is None
assert values["inference_cpu_ms"] is None
assert values["plumbing_ms"] is None
# db_log_ms is still measured on the error path — the INSERT still happened.
assert "update" in captured
assert isinstance(captured["update"]["db_log_ms"], int)