From 6aaedb813fa11ba0679c3051bc2eb28646b9506c Mon Sep 17 00:00:00 2001 From: 3gg <3gg@shellblade.net> Date: Sat, 30 Aug 2025 16:53:58 -0700 Subject: Update to SDL3 --- src/contrib/SDL-3.2.20/test/emscripten/driver.py | 184 +++++++++++++++++++++ .../SDL-3.2.20/test/emscripten/joystick-pre.js | 25 +++ src/contrib/SDL-3.2.20/test/emscripten/pre.js | 54 ++++++ src/contrib/SDL-3.2.20/test/emscripten/server.py | 102 ++++++++++++ 4 files changed, 365 insertions(+) create mode 100755 src/contrib/SDL-3.2.20/test/emscripten/driver.py create mode 100644 src/contrib/SDL-3.2.20/test/emscripten/joystick-pre.js create mode 100644 src/contrib/SDL-3.2.20/test/emscripten/pre.js create mode 100755 src/contrib/SDL-3.2.20/test/emscripten/server.py (limited to 'src/contrib/SDL-3.2.20/test/emscripten') diff --git a/src/contrib/SDL-3.2.20/test/emscripten/driver.py b/src/contrib/SDL-3.2.20/test/emscripten/driver.py new file mode 100755 index 0000000..ee91610 --- /dev/null +++ b/src/contrib/SDL-3.2.20/test/emscripten/driver.py @@ -0,0 +1,184 @@ +#!/usr/bin/env python + +import argparse +import contextlib +import logging +import os +import pathlib +import shlex +import sys +import time +from typing import Optional +import urllib.parse + +from selenium import webdriver +import selenium.common.exceptions +from selenium.webdriver.common.by import By +from selenium.webdriver.support.ui import WebDriverWait + + +logger = logging.getLogger(__name__) + + +class SDLSeleniumTestDriver: + def __init__(self, server: str, test: str, arguments: list[str], browser: str, firefox_binary: Optional[str]=None, chrome_binary: Optional[str]=None): + self. server = server + self.test = test + self.arguments = arguments + self.chrome_binary = chrome_binary + self.firefox_binary = firefox_binary + self.driver = None + self.stdout_printed = False + self.failed_messages: list[str] = [] + self.return_code = None + + options = [ + "--headless", + ] + + driver_contructor = None + match browser: + case "firefox": + driver_contructor = webdriver.Firefox + driver_options = webdriver.FirefoxOptions() + if self.firefox_binary: + driver_options.binary_location = self.firefox_binary + case "chrome": + driver_contructor = webdriver.Chrome + driver_options = webdriver.ChromeOptions() + if self.chrome_binary: + driver_options.binary_location = self.chrome_binary + options.append("--no-sandbox") + if driver_contructor is None: + raise ValueError(f"Invalid {browser=}") + for o in options: + driver_options.add_argument(o) + logger.debug("About to create driver") + self.driver = driver_contructor(options=driver_options) + + @property + def finished(self): + return len(self.failed_messages) > 0 or self.return_code is not None + + def __del__(self): + if self.driver: + self.driver.quit() + + @property + def url(self): + req = { + "loghtml": "1", + "SDL_ASSERT": "abort", + } + for key, value in os.environ.items(): + if key.startswith("SDL_"): + req[key] = value + req.update({f"arg_{i}": a for i, a in enumerate(self.arguments, 1) }) + req_str = urllib.parse.urlencode(req) + return f"{self.server}/{self.test}.html?{req_str}" + + @contextlib.contextmanager + def _selenium_catcher(self): + try: + yield + success = True + except selenium.common.exceptions.UnexpectedAlertPresentException as e: + # FIXME: switch context, verify text of dialog and answer "a" for abort + wait = WebDriverWait(self.driver, timeout=2) + try: + alert = wait.until(lambda d: d.switch_to.alert) + except selenium.common.exceptions.NoAlertPresentException: + self.failed_messages.append(e.msg) + return False + self.failed_messages.append(alert) + if "Assertion failure" in e.msg and "[ariA]" in e.msg: + alert.send_keys("a") + alert.accept() + else: + self.failed_messages.append(e.msg) + success = False + return success + + def get_stdout_and_print(self): + if self.stdout_printed: + return + with self._selenium_catcher(): + div_terminal = self.driver.find_element(by=By.ID, value="terminal") + assert div_terminal + text = div_terminal.text + print(text) + self.stdout_printed = True + + def update_return_code(self): + with self._selenium_catcher(): + div_process_quit = self.driver.find_element(by=By.ID, value="process-quit") + if not div_process_quit: + return + if div_process_quit.text != "": + try: + self.return_code = int(div_process_quit.text) + except ValueError: + raise ValueError(f"process-quit element contains invalid data: {div_process_quit.text:r}") + + def loop(self): + print(f"Connecting to \"{self.url}\"", file=sys.stderr) + self.driver.get(url=self.url) + self.driver.implicitly_wait(0.2) + + while True: + self.update_return_code() + if self.finished: + break + time.sleep(0.1) + + self.get_stdout_and_print() + if not self.stdout_printed: + self.failed_messages.append("Failed to get stdout/stderr") + + + +def main() -> int: + parser = argparse.ArgumentParser(allow_abbrev=False, description="Selenium SDL test driver") + parser.add_argument("--browser", default="firefox", choices=["firefox", "chrome"], help="browser") + parser.add_argument("--server", default="http://localhost:8080", help="Server where SDL tests live") + parser.add_argument("--verbose", action="store_true", help="Verbose logging") + parser.add_argument("--chrome-binary", help="Chrome binary") + parser.add_argument("--firefox-binary", help="Firefox binary") + + index_double_dash = sys.argv.index("--") + if index_double_dash < 0: + parser.error("Missing test arguments. Need -- ") + driver_arguments = sys.argv[1:index_double_dash] + test = pathlib.Path(sys.argv[index_double_dash+1]).name + test_arguments = sys.argv[index_double_dash+2:] + + args = parser.parse_args(args=driver_arguments) + + logging.basicConfig(level=logging.DEBUG if args.verbose else logging.INFO) + + logger.debug("driver_arguments=%r test=%r test_arguments=%r", driver_arguments, test, test_arguments) + + sdl_test_driver = SDLSeleniumTestDriver( + server=args.server, + test=test, + arguments=test_arguments, + browser=args.browser, + chrome_binary=args.chrome_binary, + firefox_binary=args.firefox_binary, + ) + sdl_test_driver.loop() + + rc = sdl_test_driver.return_code + if sdl_test_driver.failed_messages: + for msg in sdl_test_driver.failed_messages: + print(f"FAILURE MESSAGE: {msg}", file=sys.stderr) + if rc == 0: + print(f"Test signaled success (rc=0) but a failure happened", file=sys.stderr) + rc = 1 + sys.stdout.flush() + logger.info("Exit code = %d", rc) + return rc + + +if __name__ == "__main__": + raise SystemExit(main()) diff --git a/src/contrib/SDL-3.2.20/test/emscripten/joystick-pre.js b/src/contrib/SDL-3.2.20/test/emscripten/joystick-pre.js new file mode 100644 index 0000000..5fd789d --- /dev/null +++ b/src/contrib/SDL-3.2.20/test/emscripten/joystick-pre.js @@ -0,0 +1,25 @@ +Module['arguments'] = ['0']; +//Gamepads don't appear until a button is pressed and the joystick/gamepad tests expect one to be connected +Module['preRun'].push(function() +{ + Module['print']("Waiting for gamepad..."); + Module['addRunDependency']("gamepad"); + window.addEventListener('gamepadconnected', function() + { + //OK, got one + Module['removeRunDependency']("gamepad"); + }, false); + + //chrome + if(!!navigator.webkitGetGamepads) + { + var timeout = function() + { + if(navigator.webkitGetGamepads()[0] !== undefined) + Module['removeRunDependency']("gamepad"); + else + setTimeout(timeout, 100); + } + setTimeout(timeout, 100); + } +}); diff --git a/src/contrib/SDL-3.2.20/test/emscripten/pre.js b/src/contrib/SDL-3.2.20/test/emscripten/pre.js new file mode 100644 index 0000000..74ebd1c --- /dev/null +++ b/src/contrib/SDL-3.2.20/test/emscripten/pre.js @@ -0,0 +1,54 @@ +const searchParams = new URLSearchParams(window.location.search); + +Module.preRun = () => { +}; + +const arguments = []; +for (let i = 1; true; i++) { + const arg_i = searchParams.get(`arg_${i}`); + if (arg_i == null) { + break; + } + arguments.push(arg_i); +} + +Module.arguments = arguments; + +if (searchParams.get("loghtml") === "1") { + const divTerm = document.createElement("div"); + divTerm.id = "terminal"; + document.body.append(divTerm); + + function printToStdOut(msg, id) { + const divMsg = document.createElement("div", {class: "stdout"}); + divMsg.id = id; + divMsg.append(document.createTextNode(msg)); + divTerm.append(divMsg); + return divMsg; + } + + Module.print = (msg) => { + console.log(msg); + printToStdOut(msg, "stdout"); + } + + Module.printErr = (msg) => { + console.error(msg); + const e = printToStdOut(msg, "stderr"); + e.style = "color:red"; + } + + const divQuit = document.createElement("div"); + divQuit.id = "process-quit"; + document.body.append(divQuit); + + Module.quit = (msg) => { + divQuit.innerText = msg; + console.log(`QUIT: ${msg}`) + } + + Module.onabort = (msg) => { + printToStdOut(`ABORT: ${msg}`, "stderr"); + console.log(`ABORT: ${msg}`); + } +} diff --git a/src/contrib/SDL-3.2.20/test/emscripten/server.py b/src/contrib/SDL-3.2.20/test/emscripten/server.py new file mode 100755 index 0000000..103d164 --- /dev/null +++ b/src/contrib/SDL-3.2.20/test/emscripten/server.py @@ -0,0 +1,102 @@ +#!/usr/bin/env python + +# Based on http/server.py from Python + +from argparse import ArgumentParser +import contextlib +from http.server import SimpleHTTPRequestHandler +from http.server import ThreadingHTTPServer +import os +import socket + + +class MyHTTPRequestHandler(SimpleHTTPRequestHandler): + extensions_map = { + ".manifest": "text/cache-manifest", + ".html": "text/html", + ".png": "image/png", + ".jpg": "image/jpg", + ".svg": "image/svg+xml", + ".css": "text/css", + ".js": "application/x-javascript", + ".wasm": "application/wasm", + "": "application/octet-stream", + } + + def __init__(self, *args, maps=None, **kwargs): + self.maps = maps or [] + SimpleHTTPRequestHandler.__init__(self, *args, **kwargs) + + def end_headers(self): + self.send_my_headers() + SimpleHTTPRequestHandler.end_headers(self) + + def send_my_headers(self): + self.send_header("Cache-Control", "no-cache, no-store, must-revalidate") + self.send_header("Pragma", "no-cache") + self.send_header("Expires", "0") + + def translate_path(self, path): + for map_path, map_prefix in self.maps: + if path.startswith(map_prefix): + res = os.path.join(map_path, path.removeprefix(map_prefix).lstrip("/")) + break + else: + res = super().translate_path(path) + return res + + +def serve_forever(port: int, ServerClass): + handler = MyHTTPRequestHandler + + addr = ("0.0.0.0", port) + with ServerClass(addr, handler) as httpd: + host, port = httpd.socket.getsockname()[:2] + url_host = f"[{host}]" if ":" in host else host + print(f"Serving HTTP on {host} port {port} (http://{url_host}:{port}/) ...") + try: + httpd.serve_forever() + except KeyboardInterrupt: + print("\nKeyboard interrupt received, exiting.") + return 0 + + +def main(): + parser = ArgumentParser(allow_abbrev=False) + parser.add_argument("port", nargs="?", type=int, default=8080) + parser.add_argument("-d", dest="directory", type=str, default=None) + parser.add_argument("--map", dest="maps", nargs="+", type=str, help="Mappings, used as e.g. \"$HOME/projects/SDL:/sdl\"") + args = parser.parse_args() + + maps = [] + for m in args.maps: + try: + path, uri = m.split(":", 1) + except ValueError: + parser.error(f"Invalid mapping: \"{m}\"") + maps.append((path, uri)) + + class DualStackServer(ThreadingHTTPServer): + def server_bind(self): + # suppress exception when protocol is IPv4 + with contextlib.suppress(Exception): + self.socket.setsockopt(socket.IPPROTO_IPV6, socket.IPV6_V6ONLY, 0) + return super().server_bind() + + def finish_request(self, request, client_address): + self.RequestHandlerClass( + request, + client_address, + self, + directory=args.directory, + maps=maps, + ) + + return serve_forever( + port=args.port, + ServerClass=DualStackServer, + ) + + +if __name__ == "__main__": + raise SystemExit(main()) -- cgit v1.2.3