Files
cleanyourtv/tests/test_card_scripts.py
xbsheng ea6dd3fe02 tools: the card scripts say where the font comes from
The three scripts that draw README's card read sys.argv[1], sys.argv[2] at the top of the module,
so a run with no arguments died on IndexError, and press.md's Fredoka.ttf - a Google Fonts file no
folder of this repo holds - died inside Pillow on 'OSError: cannot open resource'. One font_and_out()
guard per script, before the Pillow import; gen_tables.py --font refuses a missing path itself
instead of wrapping their message in CalledProcessError; press.md says where the font comes from.

The one test that ran gen_tables.py --check on this tree and demanded exit 0 is gone: the tables are
stale on any branch that adds a list folder, which docs/workflows/tv-list.md tells a list PR to do
and leaves to a maintainer after merging, so that assertion would have been red on every list PR.
In its place, --check must give the same verdict with and without --font.
2026-10-25 07:19:31 +08:00

136 lines
7.1 KiB
Python

"""The three scripts that draw the README card need Pillow, and nothing a contributor reads said so.
`tools/requirements.txt` is what a reader installs for the tools and it has no Pillow; the package lives
in `tools/requirements-perf.txt`, a file whose header used to call itself "only for the performance
tools". `docs/workflows/press.md` is the page that asks a contributor to redraw `docs/featured.png` and
`docs/card.jpg`, so following it ended in `ModuleNotFoundError: No module named 'PIL'` with no hint of
what to install. The three card scripts now name that file the way tools/flamechart.py and
tools/screendiff.py already did, and this test keeps the three of them, the requirements file and the
workflow page in step.
readme_card.py had the second half of the same problem: its own usage line said `docs/card.png`, a file
the repo does not ship and no page links, while the front page displays `docs/card.jpg` - so a
contributor who ran the command the script documents wrote a stray file and left the README card stale.
The picture every card script names is checked against the files in the repo, and readme_card.py's
against the one README.md shows.
Run from the repository root: python3 -m unittest discover -s tests -v
"""
import ast
import os
import re
import subprocess
import sys
import tempfile
import unittest
from pathlib import Path
ROOT = Path(__file__).resolve().parents[1]
TOOLS = ROOT / "tools"
PRESS = ROOT / "docs" / "workflows" / "press.md"
PERF = TOOLS / "requirements-perf.txt"
# The scripts press.md sends a reader to, with the picture each one draws. The font they take is not in
# the repo; the tests below pass a file that exists so the Pillow check is the one they reach.
CARD_SCRIPTS = {
"readme_stats.py": "stats.png",
"readme_featured.py": "featured.png",
"readme_card.py": "card.jpg",
}
def shadow_dir(tmp):
"""A PIL package that cannot be imported, so a script behaves as on a venv without Pillow."""
pkg = Path(tmp) / "PIL"
pkg.mkdir(exist_ok=True)
(pkg / "__init__.py").write_text('raise ImportError("No module named \'PIL\'")\n', encoding="utf-8")
return str(tmp)
def run_without_pillow(argv, tmp):
env = dict(os.environ)
env["PYTHONPATH"] = shadow_dir(tmp)
return subprocess.run([sys.executable, *argv], cwd=ROOT, env=env, capture_output=True, text=True)
class CardScriptsPillowTest(unittest.TestCase):
def test_the_shadow_really_hides_pillow(self):
with tempfile.TemporaryDirectory() as tmp:
done = run_without_pillow(["-c", "import PIL"], tmp)
self.assertNotEqual(done.returncode, 0, "the PIL shadow did not take effect: the rest of this file would prove nothing")
def test_each_card_script_is_importable_without_pillow_and_names_the_file(self):
with tempfile.TemporaryDirectory() as tmp:
font = Path(tmp) / "Fredoka.ttf"
font.write_bytes(b"") # any font path that exists: the scripts check the font before Pillow
for name, picture in CARD_SCRIPTS.items():
with self.subTest(script=name):
self.assertTrue((TOOLS / name).is_file(), f"press.md names {name}, which is not in tools/")
done = run_without_pillow([str(TOOLS / name), str(font), picture], tmp)
self.assertNotEqual(done.returncode, 0, f"{name} ran without Pillow, so the shadow missed it")
self.assertIn("requirements-perf.txt", done.stderr, f"{name} does not say where Pillow comes from")
self.assertNotIn("Traceback", done.stderr, f"{name} still dies on a bare traceback")
def test_the_requirements_file_installs_pillow(self):
lines = [l.strip().lower() for l in PERF.read_text(encoding="utf-8").split("\n")]
self.assertTrue(any(l.startswith("pillow") for l in lines), "tools/requirements-perf.txt no longer installs Pillow")
header = "\n".join(l for l in PERF.read_text(encoding="utf-8").split("\n") if l.startswith("#"))
for name in CARD_SCRIPTS:
with self.subTest(script=name):
self.assertIn(name, header, f"the header of requirements-perf.txt does not say it is needed by {name}")
def test_press_md_sends_the_reader_to_that_file(self):
text = PRESS.read_text(encoding="utf-8")
self.assertIn("tools/requirements-perf.txt", text, "press.md asks for a redraw without saying what to install")
self.assertIn("docs/card.jpg", text, "press.md no longer names the card the README links")
self.assertIn("docs/featured.png", text)
# readme_stats.py draws the stat strip, which a press entry does not change, so press.md is not
# required to name it; the two scripts it does send a reader to are.
for name in ("readme_featured.py", "readme_card.py"):
with self.subTest(script=name):
self.assertIn(name, text, f"press.md does not name {name}")
DOCS_PICTURE = re.compile(r"docs/[\w.\-]+\.(?:png|jpg|jpeg|webp)")
def docstring_of(path):
"""The module docstring of a tools/ script, which is where each one documents its two arguments."""
return ast.get_docstring(ast.parse(path.read_text(encoding="utf-8"))) or ""
class CardScriptOutputTest(unittest.TestCase):
def test_every_picture_a_card_script_names_is_one_the_repo_ships(self):
for name in CARD_SCRIPTS:
named = set(DOCS_PICTURE.findall(docstring_of(TOOLS / name)))
self.assertTrue(named, f"{name}'s docstring names no picture, so this check would prove nothing")
for rel in sorted(named):
with self.subTest(script=name, picture=rel):
self.assertTrue((ROOT / rel).is_file(),
f"{name}'s usage line names {rel}, which the repo does not ship: "
"a reader who follows it writes a new file that nothing links")
def test_the_card_script_writes_the_picture_the_front_page_shows(self):
readme = (ROOT / "README.md").read_text(encoding="utf-8")
shown = set(re.findall(r'<img src="(docs/[^"]*card[^"]*)"', readme))
self.assertTrue(shown, "README.md no longer displays a card image, so this check would prove nothing")
named = set(DOCS_PICTURE.findall(docstring_of(TOOLS / "readme_card.py")))
for rel in sorted(shown):
with self.subTest(picture=rel):
self.assertIn(rel, named,
f"README.md displays {rel} but readme_card.py's usage line writes {sorted(named)}")
def test_the_docstring_reader_reads_a_script_and_not_its_comments(self):
with tempfile.TemporaryDirectory() as tmp:
made = Path(tmp) / "example.py"
made.write_text('"""Writes docs/one.png.\n\npython3 example.py docs/one.png\n"""\n'
'# docs/two.png is only a comment\n', encoding="utf-8")
self.assertIn("docs/one.png", docstring_of(made))
self.assertNotIn("docs/two.png", docstring_of(made))
made.write_text("import sys # no docstring at all\n", encoding="utf-8")
self.assertEqual(docstring_of(made), "")
if __name__ == "__main__":
unittest.main()