Files
PS5_RetroArch/tests/test_webui_language.py
T
Mihawk f25de5852e Tests: a WebUI server whose port was taken starts again on another
The host tests found a free port by binding port 0 and closing it, then started the
WebUI's server on it. In between, any connection on the machine can take that port as
its own (the same ephemeral range): the server then exits at once, "Failed to bind to
port N: Address already in use", and the test waited out its window and failed with
"the server did not start" (test_scraper, test_scraper_launchbox) or "HTTP server
exited" (test_webui). It happened in about one gate run in three, under JOBS=4 or not.

tests/webui_build.py start() starts the server, waits for its status and returns the
process, port and session token; a server that exits before answering is started again
on another port (five tries). Every harness that started the server itself uses it:
test_scraper, test_scraper_launchbox, test_scraper_screenscraper, test_webui (its start
and two restarts), test_webui_language, test_webui_transfer_browser. test_scraper's
free_port() had no other user and is gone.

Evidence:
- the cause: 40 sequential starts with each server's stderr kept: 1 failed, "Failed to
  bind to port 44505: Address already in use" (exit 1, after 0.02 s); through start(),
  40 of 40 started.
- python3 -m unittest tests.test_scraper tests.test_scraper_launchbox
  tests.test_scraper_screenscraper: OK; tests.test_webui tests.test_webui_language
  tests.test_webui_transfer_browser: 21 tests OK.
- gates on the tree with the next commit: format, unit (210 tests), build, integration
  (210 tests), evidence - PASS.
2026-10-18 07:39:56 -04:00

111 lines
5.7 KiB
Python

"""The WebUI's language: the catalogs (tools/webui-strings.py), the choice kept on the
console (/api/preferences) and the page in Chromium (tests/webui_language_browser.cjs)."""
from pathlib import Path
import http.client
import json
import os
import shutil
import subprocess
import tempfile
import unittest
import sys
sys.path.insert(0, str(Path(__file__).resolve().parent))
import webui_build # noqa: E402
from test_webui_transfer_browser import PLAYWRIGHT # noqa: E402
ROOT = Path(__file__).resolve().parent.parent
LANGUAGES = ['en', 'es', 'fr', 'de', 'it', 'pt-BR', 'ru', 'ja', 'ko', 'zh-CN', 'zh-TW', 'vi', 'tr', 'pl', 'nl', 'id']
class Catalogs(unittest.TestCase):
def test_every_language_has_every_string(self):
run = subprocess.run([sys.executable, 'tools/webui-strings.py', '--check'], cwd=ROOT, capture_output=True, text=True)
self.assertEqual(run.returncode, 0, run.stdout + run.stderr)
catalogs = sorted(p.stem for p in (ROOT / 'webui/i18n').glob('*.json') if p.stem != 'strings')
self.assertEqual(catalogs, sorted(LANGUAGES[1:]))
script = (ROOT / 'webui/i18n.js').read_text()
for code in LANGUAGES:
self.assertIn(f"['{code}',", script)
flags = {'en': 'us', 'es': 'es', 'fr': 'fr', 'de': 'de', 'it': 'it', 'pt-BR': 'br', 'ru': 'ru', 'ja': 'jp', 'ko': 'kr',
'zh-CN': 'cn', 'zh-TW': 'tw', 'vi': 'vn', 'tr': 'tr', 'pl': 'pl', 'nl': 'nl', 'id': 'id'}
source = json.loads((ROOT / 'webui/assets/flags/SOURCE.json').read_text())
for code, flag in flags.items():
self.assertTrue((ROOT / f'webui/assets/flags/{flag}.svg').is_file(), flag)
self.assertIn(flag, source['icons'])
class Server:
def __init__(self, root):
subprocess.check_output(['bash', 'tools/build-webui-http.sh', 'host'], cwd=ROOT, text=True)
subprocess.check_output(['bash', 'tools/build-webui-update.sh', 'host'], cwd=ROOT, text=True)
self.binary = root.parent / 'server'
webui_build.build(self.binary, 'tests/webui_server_main.cpp', flags=('-O1',))
self.process, self.port, self.token = webui_build.start(self.binary, root)
def request(self, method, path, token=None):
conn = http.client.HTTPConnection('127.0.0.1', self.port, timeout=5)
conn.request(method, path, None, {'X-RetroArch-Token': self.token if token is None else token})
response = conn.getresponse(); body = response.read(); conn.close()
return response.status, body
def json(self, method, path, token=None):
status, body = self.request(method, path, token)
return status, json.loads(body)
def stop(self):
self.process.terminate(); self.process.wait(timeout=5)
class Preference(unittest.TestCase):
def test_language_is_kept_on_the_console(self):
with tempfile.TemporaryDirectory() as temp:
root = Path(temp) / 'title'
for name in ('config', 'content', 'webui'):
(root / name).mkdir(parents=True)
(root / 'webui/index.html').write_text('<!doctype html><title>RetroArch</title>')
server = Server(root)
try:
saved = root / 'config/webui-preferences.cfg'
self.assertEqual(server.json('GET', '/api/status')[1]['language'], '')
self.assertEqual(server.request('POST', '/api/preferences?language=fr', token='wrong')[0], 403)
for bad in ('evil', '', 'fr%22%0Ainjected%3D1', '../en', 'FR'):
self.assertEqual(server.request('POST', '/api/preferences?language=' + bad)[0], 400, bad)
self.assertFalse(saved.exists())
for code in ('fr', 'zh-TW', 'pt-BR'):
self.assertEqual(server.json('POST', '/api/preferences?language=' + code), (200, {'language': code}))
self.assertEqual(saved.read_text(), f'webui_language = "{code}"\n')
self.assertEqual(server.json('GET', '/api/status')[1]['language'], code)
# A value edited by hand to something unknown reads as no choice.
saved.write_text('webui_language = "xx"\n')
self.assertEqual(server.json('GET', '/api/status')[1]['language'], '')
finally:
server.stop()
@unittest.skipUnless(PLAYWRIGHT and shutil.which('node') and Path('/usr/bin/chromium').exists(), 'needs node, Playwright and Chromium')
class LanguageBrowser(unittest.TestCase):
def test_picker_translates_and_follows_the_console(self):
with tempfile.TemporaryDirectory() as temp:
root = Path(temp) / 'title'
for name in ('config', 'content', 'webui'):
(root / name).mkdir(parents=True)
shutil.copytree(ROOT / 'webui', root / 'webui', dirs_exist_ok=True)
# A name that reads like a catalog pattern ("{0} of {1}"): shown as it is.
(root / 'content/Test Disc of the Week.iso').write_bytes(b'fixture')
server = Server(root)
try:
shots = Path(os.environ.get('WEBUI_SCREENSHOTS', ROOT / 'build/tmp/webui-language'))
run = subprocess.run(['node', 'tests/webui_language_browser.cjs'], cwd=ROOT, capture_output=True, text=True, timeout=300,
env={**os.environ, 'PLAYWRIGHT_PATH': PLAYWRIGHT, 'WEBUI_TEST_URL': f'http://127.0.0.1:{server.port}',
'WEBUI_SCREENSHOTS': str(shots)})
self.assertEqual(run.returncode, 0, run.stdout + run.stderr)
print(run.stdout.strip())
self.assertEqual((root / 'config/webui-preferences.cfg').read_text(), 'webui_language = "en"\n')
finally:
server.stop()
if __name__ == '__main__':
unittest.main()