HR-ATS-Portal/tests/unit/test_pdf_fragmentation.py

98 lines
3.8 KiB
Python

"""Glyph-fragmented CVs must not silently lose every substring check.
The defect these cover, found on a live application: a CV whose PDF positions
each glyph separately came out of pypdf's default mode as one character per
line. The text was all there, so the LLM read it and scored the candidate
fine — but `linkedin.com/in/...` was spelled across forty lines, so the
LinkedIn scan found nothing, and so did the skills, company and education
clamps, which all ask whether a string appears in the resume.
No real PDF and no pypdf call: a stub reader returns canned page text, which
is the only input the function under test actually reads.
"""
from __future__ import annotations
import pytest
from app.services.pdf import extract_pdf_text, is_glyph_fragmented
# The real shape of the failure, taken from the CV that exposed it.
FRAGMENTED = "\n".join("LinkedIn:linkedin.com/in/mohammad-raza-digital-marketer")
REPAIRED = (
"MOHAMMAD RAZA\n"
"Performance Marketing Specialist\n"
"Karachi, Pakistan | +923362837939\n"
"LinkedIn:linkedin.com/in/mohammad-raza-digital-marketer\n"
)
ORDINARY = "\n".join(f"Line {i} of an ordinary resume with real words on it." for i in range(30))
class _Page:
"""Minimal pypdf page: default text, and optionally a layout-mode variant."""
def __init__(self, default, layout=None, layout_raises=False):
self._default = default
self._layout = layout
self._layout_raises = layout_raises
def extract_text(self, extraction_mode="plain"):
if extraction_mode == "layout":
if self._layout_raises:
raise ValueError("layout mode unsupported")
return self._layout
return self._default
class _Reader:
def __init__(self, *pages):
self.pages = list(pages)
class TestIsGlyphFragmented:
def test_one_character_per_line_is_fragmented(self):
assert is_glyph_fragmented(FRAGMENTED) is True
def test_ordinary_text_is_not(self):
assert is_glyph_fragmented(ORDINARY) is False
def test_short_input_never_trips_it(self):
# A two-line PDF of initials is not evidence of a broken extractor, and
# treating it as such would send every tiny document through layout mode.
assert is_glyph_fragmented("A\nB\nC") is False
@pytest.mark.parametrize("text", ["", None])
def test_empty_is_not_fragmented(self, text):
assert is_glyph_fragmented(text) is False
class TestExtractPdfText:
def test_ordinary_pdf_is_returned_untouched(self):
# The guarantee that matters most: a CV that extracts cleanly today must
# keep extracting byte-identically, never routed through layout mode.
reader = _Reader(_Page(ORDINARY, layout="LAYOUT SHOULD NOT BE USED"))
assert extract_pdf_text(reader) == ORDINARY
def test_fragmented_pdf_falls_back_to_layout(self):
reader = _Reader(_Page(FRAGMENTED, layout=REPAIRED))
out = extract_pdf_text(reader)
assert out == REPAIRED
assert "linkedin.com/in/mohammad-raza-digital-marketer" in out.lower()
def test_layout_that_is_also_fragmented_is_rejected(self):
reader = _Reader(_Page(FRAGMENTED, layout=FRAGMENTED))
assert extract_pdf_text(reader) == FRAGMENTED
def test_empty_layout_is_rejected(self):
# Fragmented text still scores a candidate. Empty text fails them outright.
reader = _Reader(_Page(FRAGMENTED, layout=" "))
assert extract_pdf_text(reader) == FRAGMENTED
def test_layout_mode_unsupported_falls_back_to_default(self):
reader = _Reader(_Page(FRAGMENTED, layout_raises=True))
assert extract_pdf_text(reader) == FRAGMENTED
def test_pages_are_joined(self):
reader = _Reader(_Page("page one text here"), _Page("page two text here"))
assert extract_pdf_text(reader) == "page one text here\npage two text here"