mirror of
https://github.com/rendercv/rendercv.git
synced 2026-04-22 07:49:36 -04:00
Fix placeholder removal eating provided placeholders with overlapping names
The regex in remove_not_provided_placeholders used a bare alternation without word boundaries, so removing placeholder "AA" would also destroy "AAA" in the same template. Fix: add \b word boundaries and sort longest-first. Also add Hypothesis tests for: - remove_not_provided_placeholders: provided keys survive removal - remove_connectors_of_missing_placeholders: connectors removed/preserved - validate_arbitrary_date: pass-through for valid dates and custom text - Mastodon URL: domain and username appear in generated URL
This commit is contained in:
@@ -470,9 +470,11 @@ def remove_not_provided_placeholders(
|
||||
for key, value in entry_templates.items()
|
||||
}
|
||||
|
||||
# Then remove the placeholders themselves and adjacent non-space chars:
|
||||
# Then remove the placeholders themselves and adjacent non-space chars.
|
||||
# Sort longest-first so e.g. "AAA" matches before "AA":
|
||||
sorted_placeholders = sorted(not_provided_placeholders, key=len, reverse=True)
|
||||
not_provided_placeholders_pattern = re.compile(
|
||||
r"\S*(?:" + "|".join(not_provided_placeholders) + r")\S*"
|
||||
r"\S*\b(?:" + "|".join(sorted_placeholders) + r")\b\S*"
|
||||
)
|
||||
entry_templates = {
|
||||
key: clean_trailing_parts(
|
||||
|
||||
@@ -3,7 +3,7 @@ from unittest.mock import patch
|
||||
|
||||
import pydantic
|
||||
import pytest
|
||||
from hypothesis import given, settings
|
||||
from hypothesis import assume, given, settings
|
||||
from hypothesis import strategies as st
|
||||
|
||||
from rendercv.exception import RenderCVInternalError
|
||||
@@ -602,6 +602,34 @@ def test_remove_not_provided_placeholders(entry_templates, entry_fields, expecte
|
||||
assert result == expected
|
||||
|
||||
|
||||
class TestRemoveNotProvidedPlaceholders:
|
||||
@settings(deadline=None)
|
||||
@given(
|
||||
provided_key=st.from_regex(r"[A-Z]{2,10}", fullmatch=True),
|
||||
missing_key=st.from_regex(r"[A-Z]{2,10}", fullmatch=True),
|
||||
value=st.from_regex(r"[a-z ]{1,20}", fullmatch=True),
|
||||
)
|
||||
def test_provided_placeholders_survive(
|
||||
self, provided_key: str, missing_key: str, value: str
|
||||
) -> None:
|
||||
assume(provided_key != missing_key)
|
||||
templates = {"main": f"{provided_key} {missing_key}"}
|
||||
fields = {provided_key: value}
|
||||
result = remove_not_provided_placeholders(templates, fields)
|
||||
assert provided_key in result["main"]
|
||||
|
||||
@settings(deadline=None)
|
||||
@given(
|
||||
missing_key=st.from_regex(r"[A-Z]{2,10}", fullmatch=True),
|
||||
)
|
||||
def test_missing_placeholders_removed(self, missing_key: str) -> None:
|
||||
templates = {"main": f"PREFIX {missing_key} SUFFIX"}
|
||||
fields = {"PREFIX": "a", "SUFFIX": "b"}
|
||||
assume(missing_key not in ("PREFIX", "SUFFIX"))
|
||||
result = remove_not_provided_placeholders(templates, fields)
|
||||
assert missing_key not in result["main"]
|
||||
|
||||
|
||||
class TestRenderEntryTemplatesInternalErrors:
|
||||
"""Test defensive guards when model_dump includes a key but the attribute is None."""
|
||||
|
||||
@@ -774,6 +802,28 @@ class TestRemoveConnectorsOfMissingPlaceholders:
|
||||
== expected
|
||||
)
|
||||
|
||||
@settings(deadline=None)
|
||||
@given(
|
||||
connector=st.from_regex(r"[a-z]{2,8}", fullmatch=True),
|
||||
)
|
||||
def test_connector_removed_when_adjacent_placeholder_missing(
|
||||
self, connector: str
|
||||
) -> None:
|
||||
template = f"PRESENT {connector} MISSING"
|
||||
result = remove_connectors_of_missing_placeholders(template, {"MISSING"})
|
||||
assert connector not in result
|
||||
|
||||
@settings(deadline=None)
|
||||
@given(
|
||||
connector=st.from_regex(r"[a-z]{2,8}", fullmatch=True),
|
||||
)
|
||||
def test_connector_preserved_when_both_placeholders_present(
|
||||
self, connector: str
|
||||
) -> None:
|
||||
template = f"LEFT {connector} RIGHT"
|
||||
result = remove_connectors_of_missing_placeholders(template, set())
|
||||
assert connector in result
|
||||
|
||||
|
||||
class TestRenderEntryTemplatesWithMissingDegree:
|
||||
def test_no_connector_word_when_degree_is_none(self):
|
||||
|
||||
@@ -10,6 +10,9 @@ from rendercv.schema.models.cv.entries.bases.entry_with_complex_fields import (
|
||||
BaseEntryWithComplexFields,
|
||||
get_date_object,
|
||||
)
|
||||
from rendercv.schema.models.cv.entries.bases.entry_with_date import (
|
||||
validate_arbitrary_date,
|
||||
)
|
||||
from tests.strategies import valid_date_strings
|
||||
|
||||
|
||||
@@ -91,3 +94,35 @@ class TestBaseEntryWithComplexFields:
|
||||
assert entry.date == end_date
|
||||
assert entry.start_date is None
|
||||
assert entry.end_date is None
|
||||
|
||||
|
||||
class TestValidateArbitraryDate:
|
||||
@settings(deadline=None)
|
||||
@given(date_str=valid_date_strings())
|
||||
def test_valid_date_strings_pass_through(self, date_str: str) -> None:
|
||||
result = validate_arbitrary_date(date_str)
|
||||
assert result == date_str
|
||||
|
||||
@settings(deadline=None)
|
||||
@given(year=st.integers(min_value=1, max_value=9999))
|
||||
def test_integer_years_pass_through(self, year: int) -> None:
|
||||
result = validate_arbitrary_date(year)
|
||||
assert result == year
|
||||
|
||||
@settings(deadline=None)
|
||||
@given(
|
||||
text=st.text(min_size=1, max_size=20).filter(
|
||||
lambda s: not s.strip().isdigit() and "-" not in s
|
||||
)
|
||||
)
|
||||
def test_custom_text_passes_through(self, text: str) -> None:
|
||||
result = validate_arbitrary_date(text)
|
||||
assert result == text
|
||||
|
||||
def test_invalid_month_raises(self) -> None:
|
||||
with pytest.raises(ValueError):
|
||||
validate_arbitrary_date("2020-13-01")
|
||||
|
||||
def test_invalid_day_raises(self) -> None:
|
||||
with pytest.raises(ValueError):
|
||||
validate_arbitrary_date("2020-02-30")
|
||||
|
||||
@@ -109,6 +109,20 @@ class TestSocialNetwork:
|
||||
sn = SocialNetwork(network="Mastodon", username=username)
|
||||
assert sn.username == username
|
||||
|
||||
@settings(deadline=None)
|
||||
@given(
|
||||
user=st.from_regex(r"[a-zA-Z0-9_]{1,15}", fullmatch=True),
|
||||
domain=st.from_regex(r"[a-z]{2,10}\.[a-z]{2,4}", fullmatch=True),
|
||||
)
|
||||
def test_mastodon_url_contains_username_and_domain(
|
||||
self, user: str, domain: str
|
||||
) -> None:
|
||||
username = f"@{user}@{domain}"
|
||||
sn = SocialNetwork(network="Mastodon", username=username)
|
||||
assert domain in sn.url
|
||||
assert f"/@{user}" in sn.url
|
||||
assert sn.url.startswith("https://")
|
||||
|
||||
@settings(deadline=None)
|
||||
@given(username=st.from_regex(r"\d{4}-\d{4}-\d{4}-\d{3}[\dX]", fullmatch=True))
|
||||
def test_orcid_valid_format_accepted(self, username: str) -> None:
|
||||
|
||||
Reference in New Issue
Block a user