job-research/tests/profile/test_cv_extractor.py

415 lines
11 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
import pytest
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
@pytest.mark.parametrize(
("line", "expected"),
[
("Years of experience: 2", 2),
("Années d'expérience : 3", 3),
],
)
def test_extract_cv_signals_extracts_years_of_experience_from_explicit_line(
line: str, expected: int
) -> None:
text = dedent(
f"""
Tonio
Location: France
{line}
"""
).strip()
extracted = extract_cv_signals(text)
assert extracted["years_of_experience"] == expected
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_recognizes_french_field_labels() -> None:
text = dedent(
"""
Tonio
Formation:
M.Sc. in Data Engineering at EPITA
Langues: French, English
Compétences: Python, SQL
"""
).strip()
extracted = extract_cv_signals(text)
assert extracted["languages"] == ["French", "English"]
assert extracted["skills"] == ["Python", "SQL"]
assert extracted["education_entries"] == [
{"credential": "M.Sc. in Data Engineering", "institution": "EPITA"}
]
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_parses_clear_titles_with_french_and_english_connectors() -> None:
text = dedent(
"""
Tonio
Location: France
Ingénieur à Thales
Ingénieur Data chez BNP Paribas
Consultant BI chez Accenture
Head of Data at Qonto
Product Owner at Qonto
"""
).strip()
extracted = extract_cv_signals(text)
assert extracted["experience_entries"] == [
{"title": "Ingénieur", "company": "Thales"},
{"title": "Ingénieur Data", "company": "BNP Paribas"},
{"title": "Consultant BI", "company": "Accenture"},
{"title": "Head of Data", "company": "Qonto"},
{"title": "Product Owner", "company": "Qonto"},
]
def test_extract_cv_signals_accepts_lowercase_short_and_real_company_titles() -> None:
text = dedent(
"""
Tonio
Location: France
data engineer at Company A
iOS Engineer at Company A
3D Artist at Studio
R Developer at Company A
Data Engineer at Made in Design
"""
).strip()
extracted = extract_cv_signals(text)
assert extracted["experience_entries"] == [
{"title": "data engineer", "company": "Company A"},
{"title": "iOS Engineer", "company": "Company A"},
{"title": "3D Artist", "company": "Studio"},
{"title": "R Developer", "company": "Company A"},
{"title": "Data Engineer", "company": "Made in Design"},
]
def test_extract_cv_signals_ignores_french_prose_continuations() -> None:
text = dedent(
"""
Tonio
Location: France
Ingénieur au sein de BNP Paribas
"""
).strip()
extracted = extract_cv_signals(text)
assert extracted["experience_entries"] == []
def test_extract_cv_signals_rejects_label_like_lines_without_colons() -> None:
text = dedent(
"""
Tonio
Location at Paris
Summary at a glance
Profile at LinkedIn
Education at EPITA
"""
).strip()
extracted = extract_cv_signals(text)
assert extracted["experience_entries"] == []
def test_extract_cv_signals_rejects_narrative_bullet_experience_lines() -> None:
text = dedent(
"""
Tonio
Location: France
Implemented data pipelines at Airbnb
Designed dashboards at Company A
"""
).strip()
extracted = extract_cv_signals(text)
assert extracted["experience_entries"] == []
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_extracts_dotted_degree_variants() -> None:
text = dedent(
"""
Tonio
Location: France
Education:
M.Sc. in Data Engineering at EPITA
B.Sc. in Computer Science at University X
"""
).strip()
extracted = extract_cv_signals(text)
assert extracted["education_entries"] == [
{"credential": "M.Sc. in Data Engineering", "institution": "EPITA"},
{
"credential": "B.Sc. in Computer Science",
"institution": "University X",
},
]
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"]
def test_extract_pdf_text_rejects_textless_pdfs(monkeypatch) -> None:
class FakePage:
def extract_text(self) -> str | None:
return " "
class FakePdfReader:
def __init__(self, path: str) -> None:
self.path = path
self.pages = [FakePage(), FakePage()]
def fake_pdf_reader(path: str) -> FakePdfReader:
return FakePdfReader(path)
monkeypatch.setattr("job_research.profile.cv_extractor.PdfReader", fake_pdf_reader)
with pytest.raises(ValueError, match="No extractable text"):
extract_pdf_text(Path("cv.pdf"))