labcardai-backend / tests /test_parser.py
ayush712145's picture
Upload folder using huggingface_hub
0aaa5bc verified
Raw
History Blame Contribute Delete
13.8 kB
"""
LabCard AI β€” Biomarker Parser Tests
Covers all 4 real-world Indian lab report format variants.
Run with: pytest tests/test_parser.py -v
"""
import sys
import os
sys.path.insert(0, os.path.join(os.path.dirname(__file__), ".."))
import pytest
from app.core.biomarker_parser import (
clean_test_name,
parse_biomarkers,
parse_range,
parse_value,
)
from app.models.biomarker import BiomarkerRaw
# ── parse_value tests ─────────────────────────────────────────────────────────
class TestParseValue:
def test_standard_float(self):
assert parse_value("10.2") == 10.2
def test_integer(self):
assert parse_value("9800") == 9800.0
def test_indian_comma(self):
assert parse_value("10,200") == 10200.0
def test_large_indian_comma(self):
assert parse_value("1,85,000") == 185000.0
def test_less_than(self):
# "<0.1" β†’ half of 0.1
result = parse_value("<0.1")
assert result == pytest.approx(0.05, abs=0.001)
def test_greater_than(self):
# ">100" β†’ 101
assert parse_value(">100") == 101.0
def test_with_trailing_flag(self):
# Some labs print "10.2 L" (L = Low)
assert parse_value("10.2 L") == 10.2
assert parse_value("185000 H") == 185000.0
def test_invalid(self):
assert parse_value("N/A") is None
assert parse_value("") is None
assert parse_value("abc") is None
# ── parse_range tests ─────────────────────────────────────────────────────────
class TestParseRange:
def test_hyphen_range(self):
assert parse_range("13.0 - 17.0") == (13.0, 17.0)
def test_endash_range(self):
assert parse_range("13.0 – 17.0") == (13.0, 17.0)
def test_no_spaces(self):
assert parse_range("13.0-17.0") == (13.0, 17.0)
def test_large_numbers(self):
assert parse_range("150000-400000") == (150000.0, 400000.0)
def test_less_than(self):
low, high = parse_range("< 200")
assert low is None
assert high == 200.0
def test_less_than_no_space(self):
low, high = parse_range("<200")
assert low is None
assert high == 200.0
def test_greater_than(self):
low, high = parse_range("> 40")
assert low == 40.0
assert high is None
def test_decimal_small(self):
assert parse_range("0.4 - 4.0") == (0.4, 4.0)
def test_inverted_range(self):
# Some labs print high first β€” parser should swap
assert parse_range("17.0 - 13.0") == (13.0, 17.0)
def test_empty(self):
assert parse_range("") == (None, None)
def test_unparseable(self):
assert parse_range("Normal") == (None, None)
# ── clean_test_name tests ─────────────────────────────────────────────────────
class TestCleanTestName:
def test_all_caps(self):
result = clean_test_name("HEMOGLOBIN")
assert result == "Hemoglobin"
def test_strips_automated(self):
result = clean_test_name("Hemoglobin (Automated)")
assert result == "Hemoglobin"
def test_strips_serum(self):
result = clean_test_name("Creatinine (Serum)")
assert result == "Creatinine"
def test_strips_quantitative(self):
result = clean_test_name("Vitamin B12 (Quantitative)")
assert result == "Vitamin B12"
def test_preserves_meaningful_paren(self):
# (25-OH) is meaningful β€” keep it
result = clean_test_name("Vitamin D (25-OH)")
# Should not strip (25-OH) β€” it's not in the suffix list
assert "25-Oh" in result or "25-oh" in result.lower() or "Vitamin D" in result
def test_max_length(self):
long_name = "A" * 60
result = clean_test_name(long_name)
assert len(result) <= 50
def test_collapse_spaces(self):
result = clean_test_name("Hemoglobin Count")
assert " " not in result
# ── parse_biomarkers tests β€” 4 real-world formats ────────────────────────────
class TestParseBiomarkers:
"""Test the main parser with all 4 Indian lab report formats."""
# Format A β€” Thyrocare colon-separated with brackets
FORMAT_A = """
COMPLETE BLOOD COUNT (CBC)
Hemoglobin : 10.2 g/dL [13.0 - 17.0]
WBC Count : 9800 cells/uL [4000 - 11000]
Platelet Count : 185000 /uL [150000 - 400000]
MCV : 68 fL [80 - 100]
"""
# Format B β€” Dr. Lal tabular, no colon
FORMAT_B = """
TEST RESULT UNIT REFERENCE RANGE
HEMOGLOBIN (Hb) 10.2 g/dL 13.0 - 17.0 L
WBC COUNT 9800 cells/uL 4000 - 11000
PLATELET COUNT 185000 /uL 150000 - 400000
MCV 68 fL 80 - 100 L
"""
# Format C β€” Apollo with Ref: prefix
FORMAT_C = """
Haemoglobin 10.2 g/dL Ref: 13.0-17.0
WBC 9800 cells/uL Ref: 4000-11000
Platelet 185000 /uL Ref: 150000-400000
"""
# Format D β€” inline abbreviated
FORMAT_D = """
Hb: 10.2 g/dL (N: 13.0-17.0)
WBC: 9800 cells/uL (N: 4000-11000)
PLT: 185000 /uL (N: 150000-400000)
"""
def _get_parsed(self, text: str) -> dict[str, BiomarkerRaw]:
"""Helper β€” parse text and return dict by lowercase name."""
results = parse_biomarkers(text)
return {r.name.lower(): r for r in results}
def test_format_a_hemoglobin(self):
parsed = self._get_parsed(self.FORMAT_A)
# Find hemoglobin (may be titled)
hb = next(
(v for k, v in parsed.items() if "hemoglobin" in k or "hb" == k),
None,
)
assert hb is not None, f"Hemoglobin not found. Got: {list(parsed.keys())}"
assert parse_value(hb.value_raw) == pytest.approx(10.2)
assert "13" in hb.range_raw and "17" in hb.range_raw
def test_format_a_wbc(self):
parsed = self._get_parsed(self.FORMAT_A)
wbc = next((v for k, v in parsed.items() if "wbc" in k or "white" in k), None)
assert wbc is not None, f"WBC not found. Got: {list(parsed.keys())}"
val = parse_value(wbc.value_raw)
assert val == pytest.approx(9800.0)
def test_format_a_platelet(self):
parsed = self._get_parsed(self.FORMAT_A)
plt = next((v for k, v in parsed.items() if "platelet" in k or "plt" in k), None)
assert plt is not None, f"Platelet not found. Got: {list(parsed.keys())}"
val = parse_value(plt.value_raw)
assert val == pytest.approx(185000.0)
def test_format_b_hemoglobin(self):
parsed = self._get_parsed(self.FORMAT_B)
hb = next((v for k, v in parsed.items() if "hemoglobin" in k or "hb" in k), None)
assert hb is not None, f"Hemoglobin not found in Format B. Got: {list(parsed.keys())}"
assert parse_value(hb.value_raw) == pytest.approx(10.2)
def test_format_c_haemoglobin(self):
parsed = self._get_parsed(self.FORMAT_C)
hb = next((v for k, v in parsed.items() if "haemoglobin" in k or "hemoglobin" in k), None)
assert hb is not None, f"Haemoglobin not found in Format C. Got: {list(parsed.keys())}"
def test_deduplication(self):
"""Same test on two lines β€” keep the one with a range."""
text = """
Hemoglobin : 10.2 g/dL
Hemoglobin : 10.2 g/dL [13.0 - 17.0]
"""
results = parse_biomarkers(text)
hb_results = [r for r in results if "hemoglobin" in r.name.lower()]
assert len(hb_results) == 1, "Deduplication failed β€” got duplicates"
assert hb_results[0].range_raw != "", "Should keep entry with range"
def test_skips_header_lines(self):
text = """
TEST NAME RESULT UNIT REFERENCE RANGE
Hemoglobin : 10.2 g/dL [13.0 - 17.0]
PARAMETER VALUE NORMAL
"""
results = parse_biomarkers(text)
names = [r.name.lower() for r in results]
assert not any("test name" in n or "parameter" in n for n in names)
assert any("hemoglobin" in n for n in names)
def test_skips_empty_lines(self):
text = "\n\n\n\nHemoglobin : 10.2 g/dL [13.0 - 17.0]\n\n"
results = parse_biomarkers(text)
assert len(results) >= 1
def test_empty_text(self):
assert parse_biomarkers("") == []
assert parse_biomarkers(" ") == []
# ── Integration: Demo report from frontend ────────────────────────────────────
class TestDemoReport:
"""
Parse the actual demo-report.txt from the frontend project.
Validates end-to-end that all major biomarkers are extracted.
"""
DEMO_REPORT = """
THYROCARE TECHNOLOGIES LIMITED
Test Report
Patient Name: Rahul Sharma
Age/Gender: 28 Years / Male
Sample Collected: 15 May 2025
Report Date: 16 May 2025
Lab No: TH9823451
AAROGYAM 1.3 (FULL BODY CHECKUP)
COMPLETE BLOOD COUNT (CBC)
Hemoglobin : 10.2 g/dL [13.0 - 17.0]
RBC Count : 4.1 million/uL [4.5 - 5.5]
WBC Count : 9800 cells/uL [4000 - 11000]
Platelet Count : 185000 /uL [150000 - 400000]
MCV : 68 fL [80 - 100]
MCH : 22 pg [27 - 32]
MCHC : 29 g/dL [31.5 - 34.5]
Hematocrit (PCV) : 32 % [40 - 50]
IRON STUDIES
Serum Iron : 42 ug/dL [60 - 170]
TIBC : 420 ug/dL [250 - 370]
Serum Ferritin : 8 ng/mL [12 - 300]
Transferrin Saturation: 10 % [20 - 50]
THYROID PROFILE
TSH : 2.8 uIU/mL [0.4 - 4.0]
T3 (Triiodothyronine): 98 ng/dL [60 - 200]
T4 (Thyroxine) : 7.2 ug/dL [4.5 - 12.5]
VITAMINS
Vitamin D (25-OH) : 14.2 ng/mL [30 - 100]
Vitamin B12 : 185 pg/mL [200 - 900]
Folic Acid : 5.8 ng/mL [3.0 - 17.0]
DIABETES
Fasting Blood Glucose: 88 mg/dL [70 - 100]
HbA1c : 5.2 % [4.0 - 5.6]
LIVER FUNCTION TEST
SGPT (ALT) : 32 U/L [0 - 40]
SGOT (AST) : 28 U/L [0 - 40]
Alkaline Phosphatase : 78 U/L [44 - 147]
Bilirubin Total : 0.8 mg/dL [0.2 - 1.2]
Albumin : 4.1 g/dL [3.5 - 5.0]
KIDNEY FUNCTION TEST
Serum Creatinine : 0.9 mg/dL [0.7 - 1.3]
Blood Urea Nitrogen : 14 mg/dL [7 - 20]
Uric Acid : 5.8 mg/dL [3.5 - 7.2]
LIPID PROFILE
Total Cholesterol : 198 mg/dL [< 200]
HDL Cholesterol : 38 mg/dL [> 40]
LDL Cholesterol : 128 mg/dL [< 100]
Triglycerides : 185 mg/dL [< 150]
VLDL : 37 mg/dL [< 30]
"""
def test_extracts_at_least_20_biomarkers(self):
results = parse_biomarkers(self.DEMO_REPORT)
assert len(results) >= 20, (
f"Expected β‰₯20 biomarkers, got {len(results)}: "
f"{[r.name for r in results]}"
)
def test_key_biomarkers_found(self):
results = parse_biomarkers(self.DEMO_REPORT)
names_lower = [r.name.lower() for r in results]
required = [
"hemoglobin", "wbc", "platelet", "tsh", "vitamin d",
"vitamin b12", "sgpt", "creatinine", "cholesterol",
]
for req in required:
found = any(req in n for n in names_lower)
assert found, (
f"Required biomarker '{req}' not found. "
f"Got: {[r.name for r in results]}"
)
def test_hemoglobin_value_correct(self):
results = parse_biomarkers(self.DEMO_REPORT)
hb = next((r for r in results if "hemoglobin" in r.name.lower()), None)
assert hb is not None
assert parse_value(hb.value_raw) == pytest.approx(10.2)
low, high = parse_range(hb.range_raw)
assert low == pytest.approx(13.0)
assert high == pytest.approx(17.0)
def test_vitamin_d_range_parsed(self):
results = parse_biomarkers(self.DEMO_REPORT)
vd = next((r for r in results if "vitamin d" in r.name.lower()), None)
assert vd is not None, "Vitamin D not found"
low, high = parse_range(vd.range_raw)
assert low == pytest.approx(30.0)
assert high == pytest.approx(100.0)
def test_lipid_less_than_range(self):
results = parse_biomarkers(self.DEMO_REPORT)
chol = next((r for r in results if "cholesterol" in r.name.lower() and "total" in r.name.lower()), None)
if chol:
low, high = parse_range(chol.range_raw)
assert high == pytest.approx(200.0)
assert low is None
def test_no_header_lines_in_results(self):
results = parse_biomarkers(self.DEMO_REPORT)
names = [r.name.lower() for r in results]
bad = ["test name", "parameter", "result", "reference", "thyrocare", "patient"]
for b in bad:
assert not any(b == n.strip() for n in names), (
f"Header/metadata '{b}' appeared in biomarker results"
)
def test_no_duplicates(self):
results = parse_biomarkers(self.DEMO_REPORT)
names_lower = [r.name.lower() for r in results]
duplicates = [n for n in names_lower if names_lower.count(n) > 1]
assert not duplicates, f"Duplicate biomarkers found: {duplicates}"