Files
computer-playbook/tests/unit/python/roles/web-app-docs/files/test/test_extract.py
T
kevinveenbirkenbach a490b28af6 fix(web-app-docs): let the end-to-end round finish
The docs round ended the same way twice: seven specs passed, four never ran,
and the suite was killed at its 89-minute cap after an hour and a half. Three
separate causes, each of which hid the next.

A waiter could outlive the suite that contained it. The specs budget a poll
against the role's run timeout, but Playwright is handed that timeout minus a
60-second buffer, so the global timeout fires first. When it does, Playwright
counts the in-flight test, and every test behind it, as "did not run", which
names nothing and reads like a suite that was merely slow. resolveTimeout now
caps what it returns at the budget the suite actually has, so an exhausted
waiter reports which predicate it was waiting on.

What it was waiting on could never arrive. 59e7b0a2a7 retargeted the yaml page
from /latest to /deployed, but /deployed is built from DOCS_SNAPSHOT_PATHS and
that list does not carry compose.yml, while /latest is built from a full git
archive and does. A servable version whose file does not resolve answers 302,
not 404 and not 202, and the poll rejects redirects, so it waited 88 minutes
for a page the version could not produce.

With the suite finishing, the CLI test's own failure became reachable: it reads
the published instructions from /latest, and /latest is stale. It still serves
web-app-sphinx and desk-zoom, names this repository renamed away, so ten roles
have no page there at all and web-app-docs is one of them. It now reads the
version the run just deployed, which is what a post-deploy test should replay.

Measured on the stack: /deployed/roles/web-app-docs/README.html answers 200
with the production block, /latest answers 302, and the published index lists
275 of 282 roles. The round now passes nine specs in 17.5 minutes.

Only five of the 1512 resolveTimeout call sites across 87 roles are large
enough for the cap to reach: three here and two in web-app-openwebui, whose
suite has not been re-run.
2026-09-27 17:02:05 +02:00

122 lines
4.4 KiB
Python

from __future__ import annotations
import importlib.util
import unittest
import urllib.error
from unittest import mock
from . import PROJECT_ROOT
_SOURCE = PROJECT_ROOT / "roles" / "web-app-docs" / "files" / "test" / "extract.py"
_spec = importlib.util.spec_from_file_location("docs_test_extract", _SOURCE)
extract = importlib.util.module_from_spec(_spec)
_spec.loader.exec_module(extract)
PAGE = """\
<section id="quick-setup"><h2>Quick Setup</h2>
<section id="development"><h3>Development</h3>
<div class="highlight-bash"><div class="highlight"><pre><span></span>\
<span class="nb">make</span> onboard
</pre></div></div>
</section>
<section id="production"><h3>Production</h3>
<div class="highlight-bash"><div class="highlight"><pre><span></span>\
<span class="nv">APP</span><span class="o">=</span>web-app-x
<span class="nb">echo</span> <span class="s2">&quot;a &amp; b&quot;</span>
</pre></div></div>
</section></section>
"""
WITHOUT = '<section id="overview"><h2>Overview</h2><p>No setup here.</p></section>'
class TestBlock(unittest.TestCase):
def _parsed(self, page: str) -> str:
parser = extract.Block()
parser.feed(page)
return parser.text
def test_it_takes_the_block_below_the_production_heading(self) -> None:
text = self._parsed(PAGE)
self.assertIn("APP=web-app-x", text)
self.assertNotIn("make onboard", text)
def test_entities_come_back_as_characters(self) -> None:
self.assertIn('"a & b"', self._parsed(PAGE))
def test_highlight_markup_does_not_reach_the_commands(self) -> None:
self.assertNotIn("<span", self._parsed(PAGE))
def test_a_page_without_the_heading_yields_nothing(self) -> None:
self.assertEqual(self._parsed(WITHOUT).strip(), "")
class TestPageUrl(unittest.TestCase):
def test_it_addresses_the_deployed_working_tree(self) -> None:
"""`latest` is the newest commit, which need not carry this role.
A role renamed or added since that commit has no page there, and the
test then reports a missing production block for a page the version
never had. The deployed working tree is the one the run just built.
"""
self.assertEqual(
extract.page_url("http://host:8036/", "web-app-x"),
"http://host:8036/deployed/roles/web-app-x/README.html",
)
class TestFetch(unittest.TestCase):
def _response(self, status: int, body: bytes = b"<html></html>"):
response = mock.MagicMock()
response.status = status
response.read.return_value = body
response.__enter__.return_value = response
return response
def test_a_ready_page_is_returned_at_once(self) -> None:
with mock.patch.object(extract.urllib.request, "urlopen") as opener:
opener.return_value = self._response(200, b"<p>ok</p>")
self.assertEqual(extract.fetch("http://x/", timeout=1), "<p>ok</p>")
def test_it_waits_while_the_version_is_building(self) -> None:
answers = [self._response(202), self._response(200, b"<p>ok</p>")]
with (
mock.patch.object(extract.urllib.request, "urlopen", side_effect=answers),
mock.patch.object(extract.time, "sleep"),
):
self.assertEqual(
extract.fetch("http://x/", timeout=60, poll=0), "<p>ok</p>"
)
def test_a_build_that_never_finishes_times_out(self) -> None:
with (
mock.patch.object(extract.urllib.request, "urlopen") as opener,
mock.patch.object(extract.time, "sleep"),
):
opener.return_value = self._response(202)
with self.assertRaises(TimeoutError):
extract.fetch("http://x/", timeout=0, poll=0)
def test_an_error_other_than_building_is_raised(self) -> None:
failure = urllib.error.HTTPError("http://x/", 404, "gone", {}, None)
with (
mock.patch.object(extract.urllib.request, "urlopen", side_effect=failure),
self.assertRaises(urllib.error.HTTPError),
):
extract.fetch("http://x/", timeout=1)
class TestBlockEntry(unittest.TestCase):
def test_a_page_without_instructions_is_an_error(self) -> None:
with (
mock.patch.object(extract, "fetch", return_value=WITHOUT),
self.assertRaises(ValueError),
):
extract.block("http://x/", "web-app-x", timeout=1)
if __name__ == "__main__":
unittest.main()