275 lines
9.3 KiB
Python
275 lines
9.3 KiB
Python
"""Tests for pfs.cpt_epub — the pure CPT EPUB parser.
|
|
|
|
The synthetic fixture (tests/pfs/fixtures/cpt_sample.xhtml) is written
|
|
in the 2024 template's markup style with invented text; it is never
|
|
copied from the real AMA codebook. The integration tests at the bottom
|
|
run the real parser against the actual EPUB files on disk (read-only,
|
|
in data/zotero/data/storage/) and are skipped when a file is absent —
|
|
they assert structure and counts only, never descriptor text.
|
|
"""
|
|
|
|
from __future__ import annotations
|
|
|
|
from pathlib import Path
|
|
|
|
import pytest
|
|
|
|
from pfs.cpt_epub import parse_epub, parse_xhtml
|
|
|
|
FIXTURE = Path(__file__).parent / "fixtures" / "cpt_sample.xhtml"
|
|
REPO_ROOT = Path(__file__).resolve().parents[2]
|
|
|
|
EPUB_2024 = (
|
|
REPO_ROOT
|
|
/ "data/zotero/data/storage/EIGIRKRK/CPT Professional 2024 - American Medical Association.epub"
|
|
)
|
|
EPUB_2022 = REPO_ROOT / "data/zotero/data/storage/UG4R55FW/CPT Professional 2022.epub"
|
|
EPUB_2021 = (
|
|
REPO_ROOT / "data/zotero/data/storage/NM3NJZV5/CPT 2021 Professional Edition.epub"
|
|
)
|
|
EPUB_2019 = REPO_ROOT / "data/zotero/data/storage/VA34EEIM/CPT 2019.epub"
|
|
|
|
|
|
@pytest.fixture(scope="module")
|
|
def parsed():
|
|
text = FIXTURE.read_text(encoding="utf-8")
|
|
sections, codes, instructions, references = parse_xhtml(text, year=2024)
|
|
return {
|
|
"sections": {s.sec_id: s for s in sections},
|
|
"sections_list": sections,
|
|
"codes": {c.code: c for c in codes},
|
|
"instructions": instructions,
|
|
"references": references,
|
|
}
|
|
|
|
|
|
class TestSections:
|
|
def test_two_top_level_and_two_nested(self, parsed):
|
|
levels = sorted((s.sec_id, s.level) for s in parsed["sections_list"])
|
|
assert levels == [
|
|
("sec_1", 1),
|
|
("sec_2", 2),
|
|
("sec_3", 1),
|
|
("sec_4", 2),
|
|
]
|
|
|
|
def test_paths_include_ancestors(self, parsed):
|
|
sec2 = parsed["sections"]["sec_2"]
|
|
assert sec2.title == "Sample Chronic Wellness Services"
|
|
assert sec2.path == (
|
|
"Care Coordination Services",
|
|
"Sample Chronic Wellness Services",
|
|
)
|
|
|
|
sec4 = parsed["sections"]["sec_4"]
|
|
assert sec4.path == ("Other Sample Services", "Empty Category Services")
|
|
|
|
def test_lo_hi_from_toc_snippet(self, parsed):
|
|
sec1 = parsed["sections"]["sec_1"]
|
|
assert (sec1.code_lo, sec1.code_hi) == ("55000", "55010")
|
|
sec2 = parsed["sections"]["sec_2"]
|
|
assert (sec2.code_lo, sec2.code_hi) == ("55000", "55003")
|
|
sec3 = parsed["sections"]["sec_3"]
|
|
assert (sec3.code_lo, sec3.code_hi) == ("55100", "55101")
|
|
|
|
def test_heading_without_range_is_empty(self, parsed):
|
|
sec4 = parsed["sections"]["sec_4"]
|
|
assert (sec4.code_lo, sec4.code_hi) == ("", "")
|
|
|
|
def test_section_with_no_codes_still_has_a_path(self, parsed):
|
|
sec4 = parsed["sections"]["sec_4"]
|
|
assert sec4.path
|
|
assert sec4.guideline
|
|
|
|
def test_entities_and_whitespace_cleaned(self, parsed):
|
|
sec1 = parsed["sections"]["sec_1"]
|
|
assert "&" not in sec1.guideline
|
|
assert "&" in sec1.guideline # entity decoded, not dropped
|
|
assert " " not in sec1.guideline
|
|
|
|
|
|
class TestPrimaryCode:
|
|
def test_three_elements_and_tail(self, parsed):
|
|
code = parsed["codes"]["55000"]
|
|
assert code.sec_id == "sec_2"
|
|
assert len(code.elements) == 3
|
|
assert code.elements[0] == "first invented element of the service,"
|
|
assert (
|
|
code.tail == "first 20 minutes of invented staff time, per calendar month."
|
|
)
|
|
assert (
|
|
code.stem
|
|
== "Sample wellness coordination services with the following required elements:"
|
|
)
|
|
|
|
def test_symbols(self, parsed):
|
|
code = parsed["codes"]["55000"]
|
|
assert code.resequenced is True
|
|
assert code.addon is False
|
|
assert code.new is False
|
|
assert code.revised is False
|
|
assert code.telemedicine is False
|
|
assert code.parent == ""
|
|
|
|
def test_category_i(self, parsed):
|
|
assert parsed["codes"]["55000"].category == "I"
|
|
|
|
def test_descriptor_assembled(self, parsed):
|
|
code = parsed["codes"]["55000"]
|
|
assert code.stem in code.descriptor
|
|
assert code.tail in code.descriptor
|
|
assert code.elements[0] in code.descriptor
|
|
|
|
|
|
class TestAddonCode:
|
|
def test_addon_flag_and_parent(self, parsed):
|
|
code = parsed["codes"]["55001"]
|
|
assert code.addon is True
|
|
assert code.resequenced is True
|
|
assert code.parent == "55000"
|
|
|
|
def test_inherits_stem_and_elements(self, parsed):
|
|
addon = parsed["codes"]["55001"]
|
|
primary = parsed["codes"]["55000"]
|
|
assert addon.stem == primary.stem
|
|
assert addon.elements == primary.elements
|
|
|
|
def test_own_tail(self, parsed):
|
|
addon = parsed["codes"]["55001"]
|
|
assert "each additional 20 minutes" in addon.tail
|
|
assert "primary procedure" in addon.tail
|
|
|
|
|
|
class TestInstructions:
|
|
def test_use_with_targets_primary(self, parsed):
|
|
use_with = [
|
|
i
|
|
for i in parsed["instructions"]
|
|
if i.code == "55001" and i.kind == "use-with"
|
|
]
|
|
assert len(use_with) == 1
|
|
assert use_with[0].targets == ("55000",)
|
|
|
|
def test_not_with_targets_expanded_range_plus_listed(self, parsed):
|
|
not_with = [
|
|
i
|
|
for i in parsed["instructions"]
|
|
if i.code == "55001" and i.kind == "not-with"
|
|
]
|
|
assert len(not_with) == 1
|
|
targets = not_with[0].targets
|
|
expanded_range = tuple(f"909{n}" for n in range(51, 71))
|
|
assert len(expanded_range) == 20
|
|
for t in expanded_range:
|
|
assert t in targets
|
|
assert "55555" in targets
|
|
|
|
def test_plain_parenthetical_is_other(self, parsed):
|
|
others = [
|
|
i for i in parsed["instructions"] if i.code == "55001" and i.kind == "other"
|
|
]
|
|
assert len(others) == 1
|
|
assert others[0].text.startswith("(Sample wellness services")
|
|
|
|
def test_three_instructions_on_addon(self, parsed):
|
|
addon_instructions = [i for i in parsed["instructions"] if i.code == "55001"]
|
|
assert len(addon_instructions) == 3
|
|
|
|
|
|
class TestReferences:
|
|
def test_cpt_changes_years(self, parsed):
|
|
refs = [
|
|
r
|
|
for r in parsed["references"]
|
|
if r.code == "55000" and r.kind == "cpt-changes"
|
|
]
|
|
assert len(refs) == 1
|
|
assert refs[0].years == (2015, 2021, 2022)
|
|
|
|
def test_cpt_assistant_kept_as_text(self, parsed):
|
|
refs = [
|
|
r
|
|
for r in parsed["references"]
|
|
if r.code == "55000" and r.kind == "cpt-assistant"
|
|
]
|
|
assert len(refs) == 1
|
|
assert "Jan 21:5" in refs[0].text
|
|
|
|
def test_addon_reference_year(self, parsed):
|
|
refs = [
|
|
r
|
|
for r in parsed["references"]
|
|
if r.code == "55001" and r.kind == "cpt-changes"
|
|
]
|
|
assert refs[0].years == (2022,)
|
|
|
|
|
|
# --- Real-file integration tests (skipped when the file is absent) ---
|
|
|
|
|
|
@pytest.mark.skipif(not EPUB_2024.exists(), reason="real 2024 CPT EPUB not on disk")
|
|
class TestReal2024:
|
|
@pytest.fixture(scope="class")
|
|
def edition(self):
|
|
return parse_epub(EPUB_2024, year=2024)
|
|
|
|
def test_at_least_8000_codes(self, edition):
|
|
assert len(edition.codes) >= 8000
|
|
|
|
def test_every_section_has_a_path(self, edition):
|
|
assert edition.sections
|
|
for sec in edition.sections:
|
|
assert sec.path
|
|
assert sec.path[-1] == sec.title
|
|
|
|
def test_chronic_care_management_section_has_expected_codes(self, edition):
|
|
by_code = {c.code: c for c in edition.codes}
|
|
sec = next(
|
|
s for s in edition.sections if s.title == "Chronic Care Management Services"
|
|
)
|
|
codes_in_sec = {c.code for c in edition.codes if c.sec_id == sec.sec_id}
|
|
for code in ("99490", "99439", "99491", "99437"):
|
|
assert code in codes_in_sec, code
|
|
assert code in by_code
|
|
|
|
def test_99439_is_addon_using_99490(self, edition):
|
|
by_code = {c.code: c for c in edition.codes}
|
|
code_99439 = by_code["99439"]
|
|
assert code_99439.addon is True
|
|
use_with = [
|
|
i
|
|
for i in edition.instructions
|
|
if i.code == "99439" and i.kind == "use-with"
|
|
]
|
|
assert use_with
|
|
assert "99490" in use_with[0].targets
|
|
|
|
def test_appendix_d_addon_codes(self, edition):
|
|
d_entries = [e for e in edition.lists if e.appendix == "D"]
|
|
assert len(d_entries) >= 500
|
|
|
|
def test_appendix_m_crosswalk_former_codes_end_in_t(self, edition):
|
|
assert edition.crosswalks
|
|
assert any(row.former_code.endswith("T") for row in edition.crosswalks)
|
|
|
|
|
|
@pytest.mark.skipif(not EPUB_2022.exists(), reason="real 2022 CPT EPUB not on disk")
|
|
def test_real_2022_parses():
|
|
edition = parse_epub(EPUB_2022, year=2022)
|
|
assert len(edition.codes) >= 5000
|
|
assert edition.sections
|
|
|
|
|
|
@pytest.mark.skipif(not EPUB_2021.exists(), reason="real 2021 CPT EPUB not on disk")
|
|
def test_real_2021_parses():
|
|
edition = parse_epub(EPUB_2021, year=2021)
|
|
assert len(edition.codes) >= 5000
|
|
assert edition.sections
|
|
|
|
|
|
@pytest.mark.skipif(not EPUB_2019.exists(), reason="real 2019 CPT EPUB not on disk")
|
|
def test_real_2019_parses_without_code_ids():
|
|
edition = parse_epub(EPUB_2019, year=2019)
|
|
assert len(edition.codes) >= 3000
|
|
assert edition.sections
|