mirror of
https://github.com/prdlk/jobyssey.git
synced 2026-11-02 13:12:49 +00:00
184 lines
6.7 KiB
Python
184 lines
6.7 KiB
Python
"""Contract tests for tools/generate_cv.py.
|
||||
|
|
|
|||
|
|
The generator turns documents/sources/ YAML plus a role config into a
|
|||
|
|
LaTeX CV. What must hold:
|
|||
|
|
|
|||
|
|
- Source description blocks split into the right bullets, including the
|
|||
|
|
leading-prose case (projects/ecosystem-devrel.yml).
|
|||
|
|
- Every escape from 05-cv-templates.md's special-character table is
|
|||
|
|
applied; `%` is the silent killer (truncates the rest of the line).
|
|||
|
|
- Date ranges use a single ASCII hyphen, never `--` (en-dash ligature
|
|||
|
|
breaks ATS date parsing - confirmed Workday import failure).
|
|||
|
|
- `use: N` bullet refs resolve 1-based and fail loudly out of range.
|
|||
|
|
- Skill labels absent from documents/sources/ warn unless allowlisted
|
|||
|
|
via `extra-skills`; the shipped role configs generate warning-free.
|
|||
|
|
- Generated documents never contain `\\item [` (F9: clipped label trap).
|
|||
|
|
"""
|
|||
|
|
import datetime
|
|||
|
|
import re
|
|||
|
|
import sys
|
|||
|
|
import tempfile
|
|||
|
|
import unittest
|
|||
|
|
from pathlib import Path
|
|||
|
|
|
|||
|
|
REPO = Path(__file__).resolve().parent.parent
|
|||
|
|
sys.path.insert(0, str(REPO / "tools"))
|
|||
|
|
|
|||
|
|
import generate_cv # noqa: E402
|
|||
|
|
|
|||
|
|
UNBRACED_BRACKET_ITEM = re.compile(r"\\item\s*\[")
|
|||
|
|
|
|||
|
|
|
|||
|
|
class TestParseBullets(unittest.TestCase):
|
|||
|
|
def test_splits_bullets_and_joins_continuations(self):
|
|||
|
|
text = "- first bullet\n continues here\n- second bullet\n"
|
|||
|
|
self.assertEqual(
|
|||
|
|
generate_cv.parse_bullets(text),
|
|||
|
|
["first bullet continues here", "second bullet"],
|
|||
|
|
)
|
|||
|
|
|
|||
|
|
def test_leading_prose_becomes_first_bullet(self):
|
|||
|
|
text = "Intro paragraph.\n\n- real bullet\n"
|
|||
|
|
self.assertEqual(
|
|||
|
|
generate_cv.parse_bullets(text),
|
|||
|
|
["Intro paragraph.", "real bullet"],
|
|||
|
|
)
|
|||
|
|
|
|||
|
|
def test_empty_description(self):
|
|||
|
|
self.assertEqual(generate_cv.parse_bullets(None), [])
|
|||
|
|
|
|||
|
|
|
|||
|
|
class TestLatexEscape(unittest.TestCase):
|
|||
|
|
def test_special_characters(self):
|
|||
|
|
self.assertEqual(
|
|||
|
|
generate_cv.latex_escape("99% uptime & $4.7M for C# devs_v1"),
|
|||
|
|
"99\\% uptime \\& \\$4.7M for C\\# devs\\_v1",
|
|||
|
|
)
|
|||
|
|
|
|||
|
|
def test_unicode_punctuation(self):
|
|||
|
|
self.assertEqual(generate_cv.latex_escape("a — b – c"), "a --- b -- c")
|
|||
|
|
self.assertEqual(generate_cv.latex_escape("\u201cMotr\u201d"), "``Motr''")
|
|||
|
|
self.assertEqual(generate_cv.latex_escape("Yu\u0113"), "Yu\\=e")
|
|||
|
|
self.assertEqual(
|
|||
|
|
generate_cv.latex_escape("A \u00b7 B"), "A \\textperiodcentered{} B"
|
|||
|
|
)
|
|||
|
|
|
|||
|
|
|
|||
|
|
class TestFormatRange(unittest.TestCase):
|
|||
|
|
def test_open_range_uses_ascii_hyphen_and_present(self):
|
|||
|
|
start = datetime.date(2021, 3, 1)
|
|||
|
|
self.assertEqual(generate_cv.format_range(start, None), "Mar 2021 - Present")
|
|||
|
|
|
|||
|
|
def test_year_only_range(self):
|
|||
|
|
start = datetime.date(2022, 1, 1)
|
|||
|
|
end = datetime.date(2023, 8, 1)
|
|||
|
|
self.assertEqual(
|
|||
|
|
generate_cv.format_range(start, end, month=False), "2022 - 2023"
|
|||
|
|
)
|
|||
|
|
|
|||
|
|
def test_never_emits_double_hyphen(self):
|
|||
|
|
start = datetime.date(2016, 7, 1)
|
|||
|
|
end = datetime.date(2019, 5, 1)
|
|||
|
|
self.assertNotIn("--", generate_cv.format_range(start, end))
|
|||
|
|
|
|||
|
|
|
|||
|
|
class TestResolveBullets(unittest.TestCase):
|
|||
|
|
SOURCE = {"description": "- alpha\n- beta\n- gamma\n"}
|
|||
|
|
PATH = Path("fake.yml")
|
|||
|
|
|
|||
|
|
def test_use_index_is_one_based(self):
|
|||
|
|
entry = {"bullets": [{"use": 2}, "literal text"]}
|
|||
|
|
self.assertEqual(
|
|||
|
|
generate_cv.resolve_bullets(entry, self.SOURCE, self.PATH),
|
|||
|
|
["beta", "literal text"],
|
|||
|
|
)
|
|||
|
|
|
|||
|
|
def test_default_is_all_bullets(self):
|
|||
|
|
self.assertEqual(
|
|||
|
|
generate_cv.resolve_bullets({}, self.SOURCE, self.PATH),
|
|||
|
|
["alpha", "beta", "gamma"],
|
|||
|
|
)
|
|||
|
|
|
|||
|
|
def test_out_of_range_index_fails(self):
|
|||
|
|
with self.assertRaises(generate_cv.ConfigError):
|
|||
|
|
generate_cv.resolve_bullets(
|
|||
|
|
{"bullets": [{"use": 4}]}, self.SOURCE, self.PATH
|
|||
|
|
)
|
|||
|
|
|
|||
|
|
|
|||
|
|
class TestSkillValidation(unittest.TestCase):
|
|||
|
|
def test_unknown_skill_warns(self):
|
|||
|
|
warnings = []
|
|||
|
|
generate_cv.validate_skills(
|
|||
|
|
[{"group": "X", "items": ["Definitely Not A Skill"]}], [], warnings.append
|
|||
|
|
)
|
|||
|
|
self.assertEqual(len(warnings), 1)
|
|||
|
|
|
|||
|
|
def test_extra_skills_allowlist_silences(self):
|
|||
|
|
warnings = []
|
|||
|
|
generate_cv.validate_skills(
|
|||
|
|
[{"group": "X", "items": ["Definitely Not A Skill"]}],
|
|||
|
|
["Definitely Not A Skill"],
|
|||
|
|
warnings.append,
|
|||
|
|
)
|
|||
|
|
self.assertEqual(warnings, [])
|
|||
|
|
|
|||
|
|
def test_parenthetical_alias_grounds_label(self):
|
|||
|
|
warnings = []
|
|||
|
|
# "Go (Golang)" grounds via "Go"; "DIDs" via the sources'
|
|||
|
|
# "Decentralized Identifiers (DIDs)".
|
|||
|
|
generate_cv.validate_skills(
|
|||
|
|
[{"group": "X", "items": ["Go (Golang)", "DIDs"]}], [], warnings.append
|
|||
|
|
)
|
|||
|
|
self.assertEqual(warnings, [])
|
|||
|
|
|
|||
|
|
|
|||
|
|
class TestShippedRoleConfigs(unittest.TestCase):
|
|||
|
|
"""Every role config in documents/cv/roles/ must generate cleanly."""
|
|||
|
|
|
|||
|
|
def generate_all(self):
|
|||
|
|
results = {}
|
|||
|
|
with tempfile.TemporaryDirectory() as tmp:
|
|||
|
|
for role in generate_cv.list_roles():
|
|||
|
|
warnings = []
|
|||
|
|
target = generate_cv.generate(
|
|||
|
|
role, output=Path(tmp) / f"{role}.tex", warn=warnings.append
|
|||
|
|
)
|
|||
|
|
results[role] = (target.read_text(encoding="utf-8"), warnings)
|
|||
|
|
return results
|
|||
|
|
|
|||
|
|
def test_expected_roles_exist(self):
|
|||
|
|
self.assertLessEqual(
|
|||
|
|
{
|
|||
|
|
"example",
|
|||
|
|
"staff-software-engineer",
|
|||
|
|
"senior-software-engineer",
|
|||
|
|
"founding-software-engineer",
|
|||
|
|
},
|
|||
|
|
set(generate_cv.list_roles()),
|
|||
|
|
)
|
|||
|
|
|
|||
|
|
def test_configs_generate_without_grounding_warnings(self):
|
|||
|
|
for role, (_, warnings) in self.generate_all().items():
|
|||
|
|
self.assertEqual(warnings, [], f"{role}: ungrounded skills: {warnings}")
|
|||
|
|
|
|||
|
|
def test_output_is_wellformed_latex(self):
|
|||
|
|
for role, (text, _) in self.generate_all().items():
|
|||
|
|
body = "\n".join(
|
|||
|
|
line for line in text.splitlines() if not line.startswith("%%")
|
|||
|
|
)
|
|||
|
|
self.assertIn("\\begin{document}", body, role)
|
|||
|
|
self.assertIn("\\end{document}", body, role)
|
|||
|
|
self.assertIn("prnk28@gmail.com", body, role)
|
|||
|
|
# F9 guard: no bullet parsed as an optional label.
|
|||
|
|
self.assertIsNone(UNBRACED_BRACKET_ITEM.search(body), role)
|
|||
|
|
# No unescaped % (silent truncation) or & (compile error).
|
|||
|
|
self.assertIsNone(re.search(r"(?<!\\)%", body), role)
|
|||
|
|
self.assertIsNone(re.search(r"(?<!\\)&", body), role)
|
|||
|
|
# No en-dash-producing "--" inside a date \textit{...}.
|
|||
|
|
for match in re.finditer(r"\\textit\{([^}]*\d{4}[^}]*)\}", body):
|
|||
|
|
self.assertNotIn("--", match.group(1), f"{role}: {match.group(0)}")
|
|||
|
|
|
|||
|
|
|
|||
|
|
if __name__ == "__main__":
|
|||
|
|
unittest.main()
|