116 lines
3.9 KiB
Python
116 lines
3.9 KiB
Python
|
|
"""Validate generated decks against the real WML 1.3 DTD.
|
||
|
|
|
||
|
|
The DTD is vendored at `tests/dtd/wml13.dtd` (fetched from
|
||
|
|
http://www.wapforum.org/DTD/wml13.dtd) so the suite never depends on a
|
||
|
|
network round trip to a twenty-year-old URL. Validation needs lxml, which
|
||
|
|
is an optional extra: `uv run --extra dtd pytest`.
|
||
|
|
"""
|
||
|
|
from __future__ import annotations
|
||
|
|
|
||
|
|
from pathlib import Path
|
||
|
|
|
||
|
|
import pytest
|
||
|
|
|
||
|
|
lxml_etree = pytest.importorskip(
|
||
|
|
"lxml.etree", reason="install the 'dtd' extra for DTD validation"
|
||
|
|
)
|
||
|
|
|
||
|
|
DTD_PATH = Path(__file__).resolve().parent / "dtd" / "wml13.dtd"
|
||
|
|
|
||
|
|
|
||
|
|
@pytest.fixture(scope="module")
|
||
|
|
def dtd():
|
||
|
|
with DTD_PATH.open("rb") as handle:
|
||
|
|
return lxml_etree.DTD(handle)
|
||
|
|
|
||
|
|
|
||
|
|
def validation_errors(dtd, markup: str) -> list[str]:
|
||
|
|
# The document's own DOCTYPE points at a dead URL, so parse with the
|
||
|
|
# external DTD left alone and validate against the vendored copy.
|
||
|
|
parser = lxml_etree.XMLParser(resolve_entities=False, load_dtd=False)
|
||
|
|
tree = lxml_etree.fromstring(markup.encode("utf-8"), parser)
|
||
|
|
if dtd.validate(tree):
|
||
|
|
return []
|
||
|
|
return [str(error) for error in dtd.error_log.filter_from_errors()]
|
||
|
|
|
||
|
|
|
||
|
|
def assert_valid(dtd, markup: str) -> None:
|
||
|
|
errors = validation_errors(dtd, markup)
|
||
|
|
if errors:
|
||
|
|
pytest.fail("\n".join(errors))
|
||
|
|
|
||
|
|
|
||
|
|
SOURCES = {
|
||
|
|
"single_card": "# Title\n\nA paragraph with **bold** and a [link](http://x.test/).\n",
|
||
|
|
"two_sections": "Intro.\n\n{.card One}\nFirst.\n\n{.card Two}\nSecond.\n",
|
||
|
|
"table": "| A | B |\n| --- | --- |\n| 1 | 2 |\n",
|
||
|
|
"code_block": "```\n<wml> & $HOME\n```\n",
|
||
|
|
"blockquote_and_lists": "> quoted\n\n- a\n- b\n\n1. one\n2. two\n\n---\n",
|
||
|
|
"image": "\n",
|
||
|
|
"entities": 'Fish & chips < > " $5\n',
|
||
|
|
}
|
||
|
|
|
||
|
|
|
||
|
|
class TestDeckValidity:
|
||
|
|
@pytest.mark.parametrize("name", sorted(SOURCES))
|
||
|
|
def test_sources_are_valid(self, dtd, render, name):
|
||
|
|
assert_valid(dtd, render(SOURCES[name]))
|
||
|
|
|
||
|
|
@pytest.mark.parametrize(
|
||
|
|
"options",
|
||
|
|
[
|
||
|
|
{},
|
||
|
|
{"menu": False},
|
||
|
|
{"menu_style": "select"},
|
||
|
|
{"template_nav": False},
|
||
|
|
{"home_label": "Home"},
|
||
|
|
{"cache_control": 0},
|
||
|
|
{"access_domain": "example.com", "access_path": "/wap"},
|
||
|
|
{"max_card_bytes": 80},
|
||
|
|
{"title": "Deck"},
|
||
|
|
],
|
||
|
|
ids=[
|
||
|
|
"defaults",
|
||
|
|
"linear",
|
||
|
|
"select-menu",
|
||
|
|
"no-template",
|
||
|
|
"home-softkey",
|
||
|
|
"cache-control",
|
||
|
|
"access",
|
||
|
|
"paginated",
|
||
|
|
"titled",
|
||
|
|
],
|
||
|
|
)
|
||
|
|
def test_option_combinations_are_valid(self, dtd, render, options):
|
||
|
|
assert_valid(dtd, render(SOURCES["two_sections"], **options))
|
||
|
|
|
||
|
|
def test_every_deck_file_is_valid(self, dtd, render_output):
|
||
|
|
output = render_output(SOURCES["two_sections"], deck_per_card=True, home_label="Home")
|
||
|
|
for document in output.documents:
|
||
|
|
assert_valid(dtd, document.text)
|
||
|
|
|
||
|
|
def test_example_documents_are_valid(self, dtd, example_decks):
|
||
|
|
for name, markup in example_decks.items():
|
||
|
|
assert_valid(dtd, markup)
|
||
|
|
|
||
|
|
|
||
|
|
class TestDetectsInvalidMarkup:
|
||
|
|
"""The validator has to actually reject things, or it proves nothing."""
|
||
|
|
|
||
|
|
def test_table_outside_a_paragraph_is_rejected(self, dtd):
|
||
|
|
# The shape the renderer used to emit before the <p> wrapper landed.
|
||
|
|
bad = (
|
||
|
|
'<wml><card id="c"><table columns="1"><tr><td>x</td></tr></table>'
|
||
|
|
"</card></wml>"
|
||
|
|
)
|
||
|
|
assert validation_errors(dtd, bad)
|
||
|
|
|
||
|
|
def test_emphasis_inside_an_anchor_is_rejected(self, dtd):
|
||
|
|
# The shape the renderer used to emit for `[**go**](url)`.
|
||
|
|
bad = '<wml><card id="c"><p><a href="#x"><b>no</b></a></p></card></wml>'
|
||
|
|
assert validation_errors(dtd, bad)
|
||
|
|
|
||
|
|
def test_a_valid_deck_reports_no_errors(self, dtd):
|
||
|
|
good = '<wml><card id="c"><p>ok</p></card></wml>'
|
||
|
|
assert validation_errors(dtd, good) == []
|