job-research/tests/profile/test_cv_extractor.py
Antoine a4c1ff3c0b fix: broaden cv heuristics for french cvs
Handle French experience connectors, common degree forms, and en/em dash bullets while keeping the extraction deterministic and narrow.
2026-05-28 16:56:31 +02:00

236 lines
6.0 KiB
Python
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

from textwrap import dedent
from pathlib import Path
from job_research.profile.cv_extractor import extract_cv_signals
from job_research.profile.cv_extractor import extract_pdf_text
def test_extract_cv_signals_reads_basic_fields_from_text() -> None:
text = dedent(
"""
Tonio
Location: France
Languages: French, English
Skills: Python, SQL, Terraform, GCP, BigQuery
Data Engineer at Company A
Analytics Engineer at Company B
"""
).strip()
extracted = extract_cv_signals(text)
assert extracted["name"] == "Tonio"
assert extracted["location"] == "France"
assert extracted["languages"] == ["French", "English"]
assert extracted["skills"] == ["Python", "SQL", "Terraform", "GCP", "BigQuery"]
assert extracted["experience_entries"][0]["title"] == "Data Engineer"
assert len(extracted["experience_entries"]) == 2
def test_extract_cv_signals_allows_single_word_titles() -> None:
text = dedent(
"""
Tonio
Location: France
Consultant at Accenture
"""
).strip()
extracted = extract_cv_signals(text)
assert extracted["experience_entries"] == [
{"title": "Consultant", "company": "Accenture"}
]
def test_extract_cv_signals_allows_lowercase_company_names() -> None:
text = dedent(
"""
Tonio
Location: France
Data Engineer at leboncoin
"""
).strip()
extracted = extract_cv_signals(text)
assert extracted["experience_entries"] == [
{"title": "Data Engineer", "company": "leboncoin"}
]
def test_extract_cv_signals_ignores_prose_after_company_name() -> None:
text = dedent(
"""
Tonio
Location: France
Senior engineer at Microsoft before moving to Paris.
"""
).strip()
extracted = extract_cv_signals(text)
assert extracted["experience_entries"] == []
def test_extract_cv_signals_normalizes_bullet_prefixed_fields_and_experience() -> None:
text = dedent(
"""
Tonio
- Location: France
- Data Engineer at Company A
"""
).strip()
extracted = extract_cv_signals(text)
assert extracted["location"] == "France"
assert extracted["experience_entries"] == [
{"title": "Data Engineer", "company": "Company A"}
]
def test_extract_cv_signals_normalizes_en_dash_bullet_prefixed_fields_and_experience() -> None:
text = dedent(
"""
Tonio
Location: France
— Data Engineer at Company A
"""
).strip()
extracted = extract_cv_signals(text)
assert extracted["location"] == "France"
assert extracted["experience_entries"] == [
{"title": "Data Engineer", "company": "Company A"}
]
def test_extract_cv_signals_parses_french_experience_connectors() -> None:
text = dedent(
"""
Tonio
Location: France
Ingénieur chez Dassault Systèmes
Développeur au CNRS
"""
).strip()
extracted = extract_cv_signals(text)
assert extracted["experience_entries"] == [
{"title": "Ingénieur", "company": "Dassault Systèmes"},
{"title": "Développeur", "company": "CNRS"},
]
def test_extract_cv_signals_ignores_in_paris_prose_tail() -> None:
text = dedent(
"""
Tonio
Location: France
Data Engineer at Microsoft in Paris
"""
).strip()
extracted = extract_cv_signals(text)
assert extracted["experience_entries"] == []
def test_extract_cv_signals_extracts_education_entries_after_heading() -> None:
text = dedent(
"""
Tonio
Location: France
Education:
- Master of Science in Data Science at University of Paris
"""
).strip()
extracted = extract_cv_signals(text)
assert extracted["education_entries"] == [
{
"credential": "Master of Science in Data Science",
"institution": "University of Paris",
}
]
def test_extract_cv_signals_extracts_common_french_education_entries() -> None:
text = dedent(
"""
Tonio
Location: France
Education:
Diplôme d'ingénieur
CentraleSupélec
MSc
University of Paris
BSc
University of Oxford
Bac+5
École Polytechnique
"""
).strip()
extracted = extract_cv_signals(text)
assert extracted["education_entries"] == [
{
"credential": "Diplôme d'ingénieur",
"institution": "CentraleSupélec",
},
{"credential": "MSc", "institution": "University of Paris"},
{"credential": "BSc", "institution": "University of Oxford"},
{"credential": "Bac+5", "institution": "École Polytechnique"},
]
def test_extract_cv_signals_ignores_before_moving_prose_tail() -> None:
text = dedent(
"""
Tonio
Location: France
Senior Engineer at Microsoft Before moving to Paris.
"""
).strip()
extracted = extract_cv_signals(text)
assert extracted["experience_entries"] == []
def test_extract_pdf_text_skips_blank_pages(monkeypatch) -> None:
class FakePage:
def __init__(self, text: str | None) -> None:
self._text = text
def extract_text(self) -> str | None:
return self._text
class FakePdfReader:
def __init__(self, path: str) -> None:
self.path = path
self.pages = [
FakePage(None),
FakePage("Tonio"),
FakePage(""),
FakePage("Data Engineer"),
]
seen_paths: list[str] = []
def fake_pdf_reader(path: str) -> FakePdfReader:
seen_paths.append(path)
return FakePdfReader(path)
monkeypatch.setattr("job_research.profile.cv_extractor.PdfReader", fake_pdf_reader)
extracted = extract_pdf_text(Path("cv.pdf"))
assert extracted == "Tonio\nData Engineer"
assert seen_paths == ["cv.pdf"]