Files
computer-playbook/tests/external/roles/test_info_video_embeddable.py
kevinveenbirkenbachandClaude Opus 5 72c17ffaf7 refactor(meta): read the video-embed exemption from info.yml too
The info-media marker moved to meta/info.yml one commit ago, and this rule is
where the reason it could not move earlier was written down: a comment-only
info.yml parses to an empty document, which the schema lint used to reject. That
obstacle is gone, so the docstring now states a justification that no longer
holds, and the two video-related exemptions sit in different files for no reason
a reader can reconstruct.

So the marker is read from info.yml, the failure message names info.yml, and the
docstring says why the marker lives there rather than why it cannot. No role
opts out of this rule today, so nothing under roles/ changes and the move cannot
break in either direction.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-09-09 22:28:43 +02:00

171 lines
6.3 KiB
Python

"""Every invokable role's ``video`` addresses one reachable, embeddable video.
The root-README overview renders a Video link per role, and a reader following
it expects a demo rather than a directory. A channel, a playlist or a product
page cannot be embedded, so a page that merely contains videos does not satisfy
the column even though it resolves.
Accepted shapes:
YouTube watch?v=, youtu.be/, /embed/ (also youtube-nocookie.com)
PeerTube /w/<id> or /videos/watch/<uuid> on any instance, since PeerTube is
federated and the host cannot be enumerated
Vimeo vimeo.com/<id>
A role whose upstream publishes no single embeddable video opts out with
# nocheck: info-video-embed <reason>
in the first 30 lines of ``meta/info.yml``, the same place and mechanism the
``info-media`` marker uses, so an exemption sits beside the ``video`` field it
speaks about.
This is an external test because reachability needs live HTTP. Like its
siblings it never fails on an unanswered probe: a timeout or connection error
is reported as unverified, so a firewalled or geo-blocked runner cannot turn a
good URL red. A shape that cannot be embedded is a repository fact rather than
a network one, so that half is asserted.
"""
from __future__ import annotations
import concurrent.futures
import re
import unittest
import requests
from utils.annotations.message import warning
from utils.annotations.suppress import is_suppressed_in_head
from utils.cache.files import read_text
from utils.cache.yaml import load_yaml_any
from utils.roles.mapping import ROLE_FILE_META_INFO
from utils.roles.validation.invokable import _get_invokable_paths, _is_role_invokable
from . import PROJECT_ROOT
_RULE = "info-video-embed"
_USER_AGENT = "infinito-nexus-video-check"
_TIMEOUT_SECONDS = 15
_MAX_WORKERS = 8
_EMBEDDABLE = re.compile(
r"^https://(?:"
r"(?:www\.)?youtube(?:-nocookie)?\.com/(?:watch\?(?:[^#]*&)?v=|embed/)[\w-]{6,}"
r"|youtu\.be/[\w-]{6,}"
r"|(?:www\.)?vimeo\.com/\d{6,}"
r"|[\w.-]+/(?:w/[\w-]{6,}|videos/(?:watch|embed)/[0-9a-fA-F-]{36})"
r")",
)
def _video_of(role: str) -> str:
path = PROJECT_ROOT / "roles" / role / ROLE_FILE_META_INFO
if not path.is_file():
return ""
data = load_yaml_any(str(path)) or {}
value = data.get("video") if isinstance(data, dict) else None
return value.strip() if isinstance(value, str) else ""
def _candidates() -> list[tuple[str, str]]:
"""Return (role, video url) for every invokable role that has not opted out."""
invokable = _get_invokable_paths()
found: list[tuple[str, str]] = []
for role_dir in sorted((PROJECT_ROOT / "roles").iterdir()):
if not role_dir.is_dir() or not _is_role_invokable(role_dir.name, invokable):
continue
info_path = role_dir / ROLE_FILE_META_INFO
if info_path.is_file() and is_suppressed_in_head(
read_text(str(info_path)).splitlines(), _RULE
):
continue
video = _video_of(role_dir.name)
if video:
found.append((role_dir.name, video))
return found
def _probe_target(url: str) -> tuple[str, dict[str, str]]:
"""Return the URL to probe and its query for ``url``.
YouTube answers a removed or private video with HTTP 200 and a "Video
unavailable" page, so a plain fetch proves only that YouTube is up. Its
oEmbed endpoint answers 400 for the same video, which is the difference
this check needs.
"""
if re.match(r"^https://(?:(?:www\.)?youtube|youtu)\.", url):
# nocheck: url answers 404 without the query this call always supplies
return "https://www.youtube.com/oembed", {"url": url, "format": "json"}
return url, {}
def _probe(url: str) -> tuple[str, str]:
"""Return (kind, detail) with kind one of ok, warn, unverified."""
target, params = _probe_target(url)
try:
response = requests.get(
target,
params=params,
allow_redirects=True,
headers={"User-Agent": _USER_AGENT},
stream=True,
timeout=_TIMEOUT_SECONDS,
)
try:
status = response.status_code
finally:
response.close()
except requests.Timeout as exc:
return "unverified", f"Timeout: {exc}"
except requests.RequestException as exc:
return "unverified", f"{type(exc).__name__}: {exc}"
if status < 400 or status in (401, 403, 405, 406, 415, 429):
return "ok", f"HTTP {status}"
return "warn", f"HTTP {status}"
class TestInfoVideoEmbeddable(unittest.TestCase):
def test_invokable_role_videos_are_embeddable_and_reachable(self) -> None:
candidates = _candidates()
self.assertTrue(candidates, "No invokable role declares a video")
not_embeddable = [
(role, url) for role, url in candidates if not _EMBEDDABLE.match(url)
]
for role, url in not_embeddable:
warning(
f"{role}: {url} addresses a channel, playlist or page rather than "
f"one embeddable video",
title="Video not embeddable",
file=f"roles/{role}/{ROLE_FILE_META_INFO}",
)
embeddable = [pair for pair in candidates if pair not in not_embeddable]
with concurrent.futures.ThreadPoolExecutor(max_workers=_MAX_WORKERS) as pool:
results = list(pool.map(lambda pair: _probe(pair[1]), embeddable))
for (role, url), (kind, detail) in zip(embeddable, results, strict=True):
if kind == "ok":
continue
warning(
f"{role}: {url} answered {detail}"
if kind == "warn"
else f"{role}: {url} was not checked ({detail})",
title="Video unreachable" if kind == "warn" else "Video unverified",
file=f"roles/{role}/{ROLE_FILE_META_INFO}",
)
self.assertFalse(
not_embeddable,
f"{len(not_embeddable)} invokable role(s) point 'video' at something "
f"that cannot be embedded. Replace it with a single video URL, or opt "
f"the role out with '# nocheck: {_RULE} <reason>' in the first 30 "
f"lines of {ROLE_FILE_META_INFO}:\n"
+ "\n".join(f" {role}: {url}" for role, url in not_embeddable),
)
if __name__ == "__main__": # pragma: no cover
unittest.main()