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"))