177 lines
6.9 KiB
Python
177 lines
6.9 KiB
Python
"""Tests for material/serial sanitization in ADE detail PDF parsing."""
|
||
|
||
from __future__ import annotations
|
||
|
||
import unittest
|
||
|
||
from sfda_parser.sfda_scraper.detail_pdf import (
|
||
DeviceRow,
|
||
_clean_material_value,
|
||
_clean_serial_value,
|
||
_is_likely_date_value,
|
||
_is_likely_prose,
|
||
_paired_rows_from_summary,
|
||
_parse_summary_kv,
|
||
_sanitize_device_row,
|
||
)
|
||
|
||
|
||
class TestMaterialCleaning(unittest.TestCase):
|
||
def test_strips_product_concerned_prefix(self):
|
||
self.assertEqual(
|
||
_clean_material_value("of the product concerned: TU2000 TRYTABLE"),
|
||
"TU2000 TRYTABLE",
|
||
)
|
||
|
||
def test_strips_trade_name_product_concerned_prefix(self):
|
||
self.assertEqual(
|
||
_clean_material_value("Trade name of the product concerned: TU2000 TRYTABLE"),
|
||
"TU2000 TRYTABLE",
|
||
)
|
||
|
||
def test_parse_summary_trade_name_line(self):
|
||
lines = ["Trade name of the product concerned: TU2000 TRYTABLE"]
|
||
summary = _parse_summary_kv(lines)
|
||
self.assertEqual(summary.get("material"), "TU2000 TRYTABLE")
|
||
|
||
|
||
class TestSerialDateRejection(unittest.TestCase):
|
||
def test_detects_day_month_year_dates(self):
|
||
self.assertTrue(_is_likely_date_value("21-APR-2025"))
|
||
self.assertTrue(_is_likely_date_value("21/APR/2025"))
|
||
|
||
def test_allows_real_serial_numbers(self):
|
||
self.assertFalse(_is_likely_date_value("24B0729"))
|
||
self.assertFalse(_is_likely_date_value("TSMICS1"))
|
||
|
||
def test_clean_serial_rejects_dates(self):
|
||
self.assertEqual(_clean_serial_value("21-APR-2025"), "")
|
||
self.assertEqual(_clean_serial_value("ABC12345"), "ABC12345")
|
||
|
||
def test_sanitize_device_row_clears_date_serial(self):
|
||
row = DeviceRow(
|
||
material="Pump",
|
||
material_description="",
|
||
catalog_number="",
|
||
udi="",
|
||
serial_no="21-APR-2025",
|
||
gstn="",
|
||
batch="",
|
||
)
|
||
cleaned = _sanitize_device_row(row)
|
||
self.assertEqual(cleaned.serial_no, "")
|
||
|
||
|
||
class TestPairedUdiSn(unittest.TestCase):
|
||
def test_pairs_udi_and_sn_by_position(self):
|
||
udi_values = (
|
||
"04020012, 04020014, 04020017, 04020018, 04020026, 04020021, "
|
||
"04020009, 04020010, 04020011, 04020001, 04020025, 04020013, "
|
||
"04020003, 04020005, 04020016, 04020023, 04020028, 04020008, "
|
||
"04020020, 04020019, 04020015, 04020024, 04020022, 04020006, "
|
||
"04020027, 04020029, 04020030, 04020031"
|
||
)
|
||
sn_values = (
|
||
"32090012, 32090014, 32090017, 32090018, 32090026, 32090021, "
|
||
"32090009, 32090010, 32090011, TR05617, 32090025, 32090013, "
|
||
"32090003, 302090005, 32090016, 32090023, 32090028, 32090008, "
|
||
"32090020, 32090019, 32090015, 32090024, 32090022, 302090006, "
|
||
"32090027, 32090029, 32090030, 32090031"
|
||
)
|
||
summary = {
|
||
"material": "TU2000 TRYTABLE",
|
||
"udi": udi_values,
|
||
"serial_no": sn_values,
|
||
}
|
||
rows = _paired_rows_from_summary(summary)
|
||
self.assertEqual(len(rows), 28)
|
||
self.assertEqual(rows[0].udi, "04020012")
|
||
self.assertEqual(rows[0].serial_no, "32090012")
|
||
self.assertEqual(rows[1].udi, "04020014")
|
||
self.assertEqual(rows[1].serial_no, "32090014")
|
||
self.assertEqual(rows[9].udi, "04020001")
|
||
self.assertEqual(rows[9].serial_no, "TR05617")
|
||
self.assertEqual(rows[27].udi, "04020031")
|
||
self.assertEqual(rows[27].serial_no, "32090031")
|
||
self.assertEqual(rows[0].material, "TU2000 TRYTABLE")
|
||
|
||
def test_parse_summary_kv_stops_before_narrative(self):
|
||
lines = [
|
||
"Trade name of the product concerned: TU2000 TRYTABLE",
|
||
"UDI-DI: 04020012, 04020014",
|
||
"SN: 32090012, 32090014",
|
||
"Dear Customer",
|
||
"The manufacturer has identified an error in the user manual.",
|
||
]
|
||
summary = _parse_summary_kv(lines)
|
||
self.assertEqual(summary.get("udi"), "04020012, 04020014")
|
||
self.assertEqual(summary.get("serial_no"), "32090012, 32090014")
|
||
self.assertNotIn("Dear Customer", summary.get("serial_no", ""))
|
||
|
||
def test_parse_summary_kv_multiline_udi_sn_lists(self):
|
||
lines = [
|
||
"Trade name of the product concerned: TU2000 TRYTABLE",
|
||
"UDI-DI: 04020012, 04020014, 04020017, 04020018, 04020026, 04020021, 04020009,",
|
||
"04020010, 04020011, 04020001, 04020025, 04020013, 04020003, 04020005, 04020016,",
|
||
"04020023, 04020028, 04020008, 04020020, 04020019, 04020015, 04020024, 04020022,",
|
||
"04020006, 04020027, 04020029, 04020030, 04020031",
|
||
"SN: 32090012, 32090014, 32090017, 32090018, 32090026, 32090021, 32090009, 32090010,",
|
||
"32090011, TR05617, 32090025, 32090013, 32090003, 302090005, 32090016, 32090023,",
|
||
"32090028, 32090008, 32090020, 32090019, 32090015, 32090024, 32090022, 302090006,",
|
||
"32090027, 32090029, 32090030, 32090031",
|
||
"Manufactured: 18/07/2024 – 17/03/2026",
|
||
"Dear Customer,",
|
||
]
|
||
summary = _parse_summary_kv(lines)
|
||
rows = _paired_rows_from_summary(summary)
|
||
self.assertEqual(len(rows), 28)
|
||
self.assertEqual(rows[9].udi, "04020001")
|
||
self.assertEqual(rows[9].serial_no, "TR05617")
|
||
|
||
|
||
class TestProseRejection(unittest.TestCase):
|
||
def test_detects_narrative_fragments(self):
|
||
self.assertTrue(_is_likely_prose("Dear Customer"))
|
||
self.assertTrue(
|
||
_is_likely_prose(
|
||
"The manufacturer has identified an error in the user manual."
|
||
)
|
||
)
|
||
self.assertTrue(_is_likely_prose("Manufactured: 18/07/2024 – 17/03/2026"))
|
||
|
||
def test_clean_serial_rejects_prose(self):
|
||
self.assertEqual(_clean_serial_value("Dear Customer"), "")
|
||
self.assertEqual(_clean_serial_value("32090012"), "32090012")
|
||
|
||
def test_allows_long_comma_separated_identifier_lists(self):
|
||
line = (
|
||
"0012508285, 0012517045, 0012526102, 0012526103, 0012614786, 0012614787, "
|
||
"0012623740, 0012623741, 0012642959, 0012659419"
|
||
)
|
||
self.assertFalse(_is_likely_prose(line))
|
||
|
||
def test_clean_serial_keeps_comma_separated_lot_list(self):
|
||
value = "24B0729, 25B0770, 25H0171, 26B0542"
|
||
self.assertEqual(_clean_serial_value(value), value)
|
||
|
||
|
||
class TestSerialLabelParsing(unittest.TestCase):
|
||
def test_parse_sn_label_from_text(self):
|
||
lines = [
|
||
"UDI-DI: 01234567890123",
|
||
"SN: ABC12345",
|
||
"Date: 21-APR-2025",
|
||
]
|
||
summary = _parse_summary_kv(lines)
|
||
self.assertEqual(summary.get("udi"), "01234567890123")
|
||
self.assertEqual(summary.get("serial_no"), "ABC12345")
|
||
|
||
def test_sn_label_ignores_date_values(self):
|
||
lines = ["SN: 21-APR-2025"]
|
||
summary = _parse_summary_kv(lines)
|
||
self.assertNotIn("serial_no", summary)
|
||
|
||
|
||
if __name__ == "__main__":
|
||
unittest.main()
|