mirror of
https://github.com/MadsLorentzen/ai-job-search.git
synced 2026-11-02 13:12:50 +00:00
feat(verify_pdf): add --ascii-dates, the non-folding check for the documented date-range rule (#385) (#498)
* feat(verify_pdf): add --ascii-dates, the non-folding check for the documented date-range rule (#385) 05-cv-templates.md records a real Workday import that dropped a role's end date because `2016--2024` reaches the text layer as 2016<U+2013>2024, and tells Step 5d to confirm every entry's years are joined by an ASCII hyphen. Nothing executed that. The natural probe, --contains "2016-2024", failed identically for "en-dashed" and "absent" before #458 and passes on both since, because the fold maps the en-dash back to "-" on purpose. Add --ascii-dates: scan the raw text layer (never the fold) for a year joined to any Unicode dash or the minus sign and fail naming each hit with its line and code point. A year on either side of the dash is enough (Mar 2016 - Jul 2016, 2016 - Present); a numeric range with no year is not a date. The dump is still written before the check fails. Wire it in: Step 5d's extraction command carries the flag and the "Dates recognizable" item points at it, keeping the bare single-year half as a read-through; the guide's "add this to the step 5d checks" paragraph names the command (framework_version 1.4.4 -> 1.4.5); the upstream-only CI assertion runs it on the stock CV. Demonstrated on the stock template compiled with lualatex through both extractors: a \cventry{2016--2024} build fails with U+2013 while --contains "2016-2024" passes on it; the \cventry{2016-2024} build passes both; the untouched stock CV passes the CI assertion. Ten new test_verify_pdf.py cases. Proposed by 9scorp4 in Discussion #385. * fix(verify_pdf): --ascii-dates only joins a year and a dash across horizontal whitespace `\s*` between the year and the dash also matched a newline, so a heading ending in an ASCII date ("Heading 1988-1994\n- note") joined the dash that merely opened the next line and was reported as a U+2013 range; the mirror shape ("Six Sigma -\n2016 onwards") did the same. Both are real text-layer shapes: a dash used as a bullet, a wrapped prose line. Match horizontal whitespace only ([^\S\n]*). Every existing case still passes; three fixtures pin the two newline shapes and the same-line spaced dash that must still be caught. Found in review by yang2632, with the regex and the fixture.
This commit is contained in:
1 parent
8cbab6823e
commit
a45bbd1a2a
6 files changed
+188
-5
No files matched your search
@@ -6,7 +6,9 @@ from unittest.mock import patch
|
||||
|
||||
from tools.verify_pdf import (
|
||||
VerificationError,
|
||||
build_parser,
|
||||
extract_text_layer,
|
||||
find_non_ascii_date_ranges,
|
||||
normalize_text,
|
||||
parse_page_count,
|
||||
run_tool,
|
||||
@@ -61,6 +63,61 @@ class NormalizeTextTests(unittest.TestCase):
|
||||
self.assertEqual(normalize_text("Master\u2019s"), normalize_text("Master's"))
|
||||
|
||||
|
||||
class FindNonAsciiDateRangesTests(unittest.TestCase):
|
||||
"""The date-range rule reads the raw layer; `--contains` folds and cannot.
|
||||
|
||||
`05-cv-templates.md` documents a Workday import that dropped a role's end
|
||||
date because `2016--2024` reaches the text layer as `2016<U+2013>2024`, and
|
||||
asks Step 5d to confirm every entry's years are joined by an ASCII hyphen.
|
||||
The fold that makes `--contains "2016-2024"` pass on that layer (#458) is
|
||||
what makes `--contains` unable to detect it - so this check never folds.
|
||||
|
||||
The newline cases are yang2632's finding on the PR: `\s*` between year and
|
||||
dash also matched a line break, so a heading ending in an ASCII date joined
|
||||
a dash that merely opened the next line.
|
||||
"""
|
||||
|
||||
def test_en_dash_between_years_is_reported_with_its_code_point(self):
|
||||
hits = find_non_ascii_date_ranges("Six Sigma Green Belt, 2016\u20132024.\n")
|
||||
self.assertEqual(hits, [("Six Sigma Green Belt, 2016\u20132024.", "\u2013")])
|
||||
|
||||
def test_ascii_hyphen_range_is_clean(self):
|
||||
self.assertEqual(find_non_ascii_date_ranges("2016-2024\nMar 2016 - Jul 2016\n"), [])
|
||||
|
||||
def test_a_year_on_either_side_of_the_dash_is_enough(self):
|
||||
# Month-qualified and open-ended ranges: the year is only on one side.
|
||||
for text in ("Mar 2016 \u2013 Jul 2016", "2016 \u2013 Present", "\u2013 2024"):
|
||||
with self.subTest(text=text):
|
||||
self.assertEqual(len(find_non_ascii_date_ranges(text)), 1)
|
||||
|
||||
def test_other_unicode_dashes_and_the_minus_sign_are_caught(self):
|
||||
for dash in ("\u2010", "\u2011", "\u2012", "\u2014", "\u2015", "\u2212"):
|
||||
with self.subTest(dash=dash):
|
||||
self.assertEqual(find_non_ascii_date_ranges(f"2016{dash}2024")[0][1], dash)
|
||||
|
||||
def test_numeric_range_without_a_year_is_not_a_date(self):
|
||||
# 05-cv-templates.md keeps `--` in prose ranges like EUR 600k--1M.
|
||||
self.assertEqual(find_non_ascii_date_ranges("EUR 600k\u20131M, 12\u201315 people"), [])
|
||||
|
||||
def test_year_ending_a_line_is_not_joined_to_the_next_lines_dash(self):
|
||||
text = "Heading 1988-1994\n\u2013 note\n"
|
||||
self.assertEqual(find_non_ascii_date_ranges(text), [])
|
||||
|
||||
def test_dash_ending_a_line_is_not_joined_to_the_next_lines_year(self):
|
||||
text = "Six Sigma \u2013\n2016 onwards\n"
|
||||
self.assertEqual(find_non_ascii_date_ranges(text), [])
|
||||
|
||||
def test_same_line_spaces_around_the_dash_are_still_caught(self):
|
||||
self.assertEqual(len(find_non_ascii_date_ranges("1988 \u2013 1994")), 1)
|
||||
|
||||
def test_hits_are_reported_in_document_order_one_per_range(self):
|
||||
text = "2016\u20132024 role\nmore text\nJan 2010 \u2013 Dec 2012 degree\n"
|
||||
self.assertEqual(
|
||||
[line for line, _ in find_non_ascii_date_ranges(text)],
|
||||
["2016\u20132024 role", "Jan 2010 \u2013 Dec 2012 degree"],
|
||||
)
|
||||
|
||||
|
||||
class VerifyPdfTests(unittest.TestCase):
|
||||
def setUp(self):
|
||||
self.temp_dir = tempfile.TemporaryDirectory()
|
||||
@@ -147,6 +204,41 @@ class VerifyPdfTests(unittest.TestCase):
|
||||
|
||||
self.assertEqual(dump.read_text(encoding="utf-8"), "2016\u20132024\n")
|
||||
|
||||
@patch("tools.verify_pdf._extract_pypdf", return_value=None)
|
||||
@patch("tools.verify_pdf.run_tool")
|
||||
def test_ascii_dates_rejects_the_en_dash_range_that_contains_accepts(self, mock_run_tool, _pypdf):
|
||||
# The same layer, two verdicts: --contains folds U+2013 to "-" and passes;
|
||||
# --ascii-dates reads the raw layer and fails, naming the code point.
|
||||
layer = "Role Title 2016\u20132024\n"
|
||||
mock_run_tool.side_effect = [layer, "Pages: 1\n", layer, "Pages: 1\n"]
|
||||
|
||||
verify_pdf(self.pdf, required_text=("2016-2024",))
|
||||
|
||||
with self.assertRaisesRegex(VerificationError, r"U\+2013.*2016\u20132024"):
|
||||
verify_pdf(self.pdf, required_text=("2016-2024",), ascii_dates=True)
|
||||
|
||||
@patch("tools.verify_pdf._extract_pypdf", return_value=None)
|
||||
@patch("tools.verify_pdf.run_tool")
|
||||
def test_ascii_dates_accepts_hyphen_ranges(self, mock_run_tool, _pypdf):
|
||||
mock_run_tool.side_effect = ["Role Title 2016-2024\nMar 2016 - Jul 2016\n", "Pages: 1\n"]
|
||||
|
||||
verify_pdf(self.pdf, ascii_dates=True)
|
||||
|
||||
@patch("tools.verify_pdf._extract_pypdf", return_value=None)
|
||||
@patch("tools.verify_pdf.run_tool")
|
||||
def test_ascii_dates_still_writes_the_dump_before_failing(self, mock_run_tool, _pypdf):
|
||||
mock_run_tool.side_effect = ["2016\u20132024\n", "Pages: 1\n"]
|
||||
dump = Path(self.temp_dir.name) / "dump.txt"
|
||||
|
||||
with self.assertRaises(VerificationError):
|
||||
verify_pdf(self.pdf, dump_text=dump, ascii_dates=True)
|
||||
|
||||
self.assertEqual(dump.read_text(encoding="utf-8"), "2016\u20132024\n")
|
||||
|
||||
def test_ascii_dates_flag_is_off_by_default_and_parses(self):
|
||||
self.assertFalse(build_parser().parse_args(["x.pdf"]).ascii_dates)
|
||||
self.assertTrue(build_parser().parse_args(["x.pdf", "--ascii-dates"]).ascii_dates)
|
||||
|
||||
def test_rejects_missing_pdf(self):
|
||||
with self.assertRaisesRegex(VerificationError, "PDF does not exist"):
|
||||
verify_pdf(Path(self.temp_dir.name) / "missing.pdf")
|
||||
|
||||
Reference in new issue
Block a user