Spaces:
Running
Running
File size: 5,912 Bytes
5370232 | 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 | """
Smoke tests for the ADMET descriptor endpoint.
Covers:
- Valid SMILES return 200 with correct shape
- Invalid SMILES return 422 or meaningful error
- Response contains 3a (core descriptors) and 3b (toxicity) sections
- _methodology metadata present
- Toxicity section contains _disclaimer
- All core numeric fields are finite numbers
"""
import math
import pytest
from tests.conftest import requires_rdkit
BASE = "/api/admet/descriptors"
@requires_rdkit
class TestADMETBasic:
"""Core endpoint health checks."""
def test_aspirin_returns_200(self, client):
resp = client.post(BASE, json={"smiles": "CC(=O)OC1=CC=CC=C1C(=O)O"})
assert resp.status_code == 200
body = resp.json()
assert body["status"] == "complete"
assert body["result"] is not None
def test_result_has_core_fields(self, client, valid_smiles):
result = client.post(BASE, json={"smiles": valid_smiles}).json()["result"]
for field in [
"smiles", "formula", "molecular_weight", "logp", "tpsa",
"hbd", "hba", "rotatable_bonds", "qed_score",
"heavy_atoms", "molar_refractivity", "molecular_volume",
"fsp3", "ring_count", "aromatic_ring_count",
]:
assert field in result, f"Missing core field: {field}"
def test_numeric_fields_are_finite(self, client, valid_smiles):
result = client.post(BASE, json={"smiles": valid_smiles}).json()["result"]
numeric_fields = [
"molecular_weight", "logp", "tpsa", "qed_score",
"molar_refractivity", "molecular_volume", "fsp3",
]
for field in numeric_fields:
val = result[field]
assert isinstance(val, (int, float)), f"{field} is not numeric: {val}"
assert math.isfinite(val), f"{field} is not finite: {val}"
@requires_rdkit
class TestADMETMethodology:
"""Verify 3a/3b split metadata."""
def test_methodology_present(self, client, valid_smiles):
result = client.post(BASE, json={"smiles": valid_smiles}).json()["result"]
assert "_methodology" in result
def test_core_descriptors_are_3a(self, client, valid_smiles):
meth = client.post(BASE, json={"smiles": valid_smiles}).json()["result"]["_methodology"]
assert meth["core_descriptors"]["tier"] == "3a"
assert meth["core_descriptors"]["confidence"] == "high"
def test_toxicity_is_3b(self, client, valid_smiles):
meth = client.post(BASE, json={"smiles": valid_smiles}).json()["result"]["_methodology"]
assert meth["toxicity"]["tier"] == "3b"
assert meth["toxicity"]["confidence"] == "approximate"
def test_drug_likeness_is_3a(self, client, valid_smiles):
meth = client.post(BASE, json={"smiles": valid_smiles}).json()["result"]["_methodology"]
assert meth["drug_likeness"]["tier"] == "3a"
@requires_rdkit
class TestADMETToxicity:
"""Verify toxicity section shape and disclaimer."""
def test_toxicity_has_disclaimer(self, client, valid_smiles):
tox = client.post(BASE, json={"smiles": valid_smiles}).json()["result"]["toxicity"]
assert "_disclaimer" in tox
assert "heuristic" in tox["_disclaimer"].lower() or "no ML" in tox["_disclaimer"]
def test_toxicity_has_all_fields(self, client, valid_smiles):
tox = client.post(BASE, json={"smiles": valid_smiles}).json()["result"]["toxicity"]
for field in [
"ames_mutagenicity", "ames_alerts", "herg_liability",
"hepatotoxicity_dili", "skin_sensitization",
"acute_toxicity_ld50", "ld50_estimate_log", "risk_score",
]:
assert field in tox, f"Missing toxicity field: {field}"
def test_risk_score_in_range(self, client, valid_smiles):
score = client.post(BASE, json={"smiles": valid_smiles}).json()["result"]["toxicity"]["risk_score"]
assert 0 <= score <= 10
@requires_rdkit
class TestADMETSafety:
"""Verify drug-likeness and structural alerts sections."""
def test_drug_likeness_has_lipinski(self, client, valid_smiles):
dl = client.post(BASE, json={"smiles": valid_smiles}).json()["result"]["drug_likeness"]
assert "lipinski" in dl
assert "pass" in dl["lipinski"]
assert "violation_count" in dl["lipinski"]
def test_structural_alerts_present(self, client, valid_smiles):
sa = client.post(BASE, json={"smiles": valid_smiles}).json()["result"]["structural_alerts"]
assert "pains" in sa
assert "brenk" in sa
assert "total_alert_count" in sa
def test_absorption_section_present(self, client, valid_smiles):
abs_ = client.post(BASE, json={"smiles": valid_smiles}).json()["result"]["absorption"]
assert "oral_bioavailability" in abs_
assert "caco2_permeability" in abs_
assert "hia" in abs_
@requires_rdkit
class TestADMETEdgeCases:
"""Edge cases and error handling."""
def test_invalid_smiles_returns_error(self, client, invalid_smiles):
resp = client.post(BASE, json={"smiles": invalid_smiles})
assert resp.status_code in (400, 422, 500)
body = resp.json()
assert "error" in body or "detail" in body
def test_empty_smiles_returns_422(self, client):
resp = client.post(BASE, json={"smiles": ""})
assert resp.status_code == 422
def test_missing_smiles_returns_422(self, client):
resp = client.post(BASE, json={})
assert resp.status_code == 422
def test_parametrized_molecules(self, client, sample_smiles):
"""Run every molecule in the parametrized fixture."""
name, smiles = sample_smiles
resp = client.post(BASE, json={"smiles": smiles})
assert resp.status_code == 200, f"{name} failed: {resp.text}"
result = resp.json()["result"]
assert result is not None
assert result["molecular_weight"] > 0
|