""" 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}"