98 lines
3.8 KiB
Python
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"
|