diff options
| author | 3gg <3gg@shellblade.net> | 2025-12-27 12:03:39 -0800 |
|---|---|---|
| committer | 3gg <3gg@shellblade.net> | 2025-12-27 12:03:39 -0800 |
| commit | 5a079a2d114f96d4847d1ee305d5b7c16eeec50e (patch) | |
| tree | 8926ab44f168acf787d8e19608857b3af0f82758 /contrib/SDL-3.2.8/test/emscripten | |
Initial commit
Diffstat (limited to 'contrib/SDL-3.2.8/test/emscripten')
| -rwxr-xr-x | contrib/SDL-3.2.8/test/emscripten/driver.py | 184 | ||||
| -rw-r--r-- | contrib/SDL-3.2.8/test/emscripten/joystick-pre.js | 25 | ||||
| -rw-r--r-- | contrib/SDL-3.2.8/test/emscripten/pre.js | 54 | ||||
| -rwxr-xr-x | contrib/SDL-3.2.8/test/emscripten/server.py | 102 |
4 files changed, 365 insertions, 0 deletions
diff --git a/contrib/SDL-3.2.8/test/emscripten/driver.py b/contrib/SDL-3.2.8/test/emscripten/driver.py new file mode 100755 index 0000000..ee91610 --- /dev/null +++ b/contrib/SDL-3.2.8/test/emscripten/driver.py | |||
| @@ -0,0 +1,184 @@ | |||
| 1 | #!/usr/bin/env python | ||
| 2 | |||
| 3 | import argparse | ||
| 4 | import contextlib | ||
| 5 | import logging | ||
| 6 | import os | ||
| 7 | import pathlib | ||
| 8 | import shlex | ||
| 9 | import sys | ||
| 10 | import time | ||
| 11 | from typing import Optional | ||
| 12 | import urllib.parse | ||
| 13 | |||
| 14 | from selenium import webdriver | ||
| 15 | import selenium.common.exceptions | ||
| 16 | from selenium.webdriver.common.by import By | ||
| 17 | from selenium.webdriver.support.ui import WebDriverWait | ||
| 18 | |||
| 19 | |||
| 20 | logger = logging.getLogger(__name__) | ||
| 21 | |||
| 22 | |||
| 23 | class SDLSeleniumTestDriver: | ||
| 24 | def __init__(self, server: str, test: str, arguments: list[str], browser: str, firefox_binary: Optional[str]=None, chrome_binary: Optional[str]=None): | ||
| 25 | self. server = server | ||
| 26 | self.test = test | ||
| 27 | self.arguments = arguments | ||
| 28 | self.chrome_binary = chrome_binary | ||
| 29 | self.firefox_binary = firefox_binary | ||
| 30 | self.driver = None | ||
| 31 | self.stdout_printed = False | ||
| 32 | self.failed_messages: list[str] = [] | ||
| 33 | self.return_code = None | ||
| 34 | |||
| 35 | options = [ | ||
| 36 | "--headless", | ||
| 37 | ] | ||
| 38 | |||
| 39 | driver_contructor = None | ||
| 40 | match browser: | ||
| 41 | case "firefox": | ||
| 42 | driver_contructor = webdriver.Firefox | ||
| 43 | driver_options = webdriver.FirefoxOptions() | ||
| 44 | if self.firefox_binary: | ||
| 45 | driver_options.binary_location = self.firefox_binary | ||
| 46 | case "chrome": | ||
| 47 | driver_contructor = webdriver.Chrome | ||
| 48 | driver_options = webdriver.ChromeOptions() | ||
| 49 | if self.chrome_binary: | ||
| 50 | driver_options.binary_location = self.chrome_binary | ||
| 51 | options.append("--no-sandbox") | ||
| 52 | if driver_contructor is None: | ||
| 53 | raise ValueError(f"Invalid {browser=}") | ||
| 54 | for o in options: | ||
| 55 | driver_options.add_argument(o) | ||
| 56 | logger.debug("About to create driver") | ||
| 57 | self.driver = driver_contructor(options=driver_options) | ||
| 58 | |||
| 59 | @property | ||
| 60 | def finished(self): | ||
| 61 | return len(self.failed_messages) > 0 or self.return_code is not None | ||
| 62 | |||
| 63 | def __del__(self): | ||
| 64 | if self.driver: | ||
| 65 | self.driver.quit() | ||
| 66 | |||
| 67 | @property | ||
| 68 | def url(self): | ||
| 69 | req = { | ||
| 70 | "loghtml": "1", | ||
| 71 | "SDL_ASSERT": "abort", | ||
| 72 | } | ||
| 73 | for key, value in os.environ.items(): | ||
| 74 | if key.startswith("SDL_"): | ||
| 75 | req[key] = value | ||
| 76 | req.update({f"arg_{i}": a for i, a in enumerate(self.arguments, 1) }) | ||
| 77 | req_str = urllib.parse.urlencode(req) | ||
| 78 | return f"{self.server}/{self.test}.html?{req_str}" | ||
| 79 | |||
| 80 | @contextlib.contextmanager | ||
| 81 | def _selenium_catcher(self): | ||
| 82 | try: | ||
| 83 | yield | ||
| 84 | success = True | ||
| 85 | except selenium.common.exceptions.UnexpectedAlertPresentException as e: | ||
| 86 | # FIXME: switch context, verify text of dialog and answer "a" for abort | ||
| 87 | wait = WebDriverWait(self.driver, timeout=2) | ||
| 88 | try: | ||
| 89 | alert = wait.until(lambda d: d.switch_to.alert) | ||
| 90 | except selenium.common.exceptions.NoAlertPresentException: | ||
| 91 | self.failed_messages.append(e.msg) | ||
| 92 | return False | ||
| 93 | self.failed_messages.append(alert) | ||
| 94 | if "Assertion failure" in e.msg and "[ariA]" in e.msg: | ||
| 95 | alert.send_keys("a") | ||
| 96 | alert.accept() | ||
| 97 | else: | ||
| 98 | self.failed_messages.append(e.msg) | ||
| 99 | success = False | ||
| 100 | return success | ||
| 101 | |||
| 102 | def get_stdout_and_print(self): | ||
| 103 | if self.stdout_printed: | ||
| 104 | return | ||
| 105 | with self._selenium_catcher(): | ||
| 106 | div_terminal = self.driver.find_element(by=By.ID, value="terminal") | ||
| 107 | assert div_terminal | ||
| 108 | text = div_terminal.text | ||
| 109 | print(text) | ||
| 110 | self.stdout_printed = True | ||
| 111 | |||
| 112 | def update_return_code(self): | ||
| 113 | with self._selenium_catcher(): | ||
| 114 | div_process_quit = self.driver.find_element(by=By.ID, value="process-quit") | ||
| 115 | if not div_process_quit: | ||
| 116 | return | ||
| 117 | if div_process_quit.text != "": | ||
| 118 | try: | ||
| 119 | self.return_code = int(div_process_quit.text) | ||
| 120 | except ValueError: | ||
| 121 | raise ValueError(f"process-quit element contains invalid data: {div_process_quit.text:r}") | ||
| 122 | |||
| 123 | def loop(self): | ||
| 124 | print(f"Connecting to \"{self.url}\"", file=sys.stderr) | ||
| 125 | self.driver.get(url=self.url) | ||
| 126 | self.driver.implicitly_wait(0.2) | ||
| 127 | |||
| 128 | while True: | ||
| 129 | self.update_return_code() | ||
| 130 | if self.finished: | ||
| 131 | break | ||
| 132 | time.sleep(0.1) | ||
| 133 | |||
| 134 | self.get_stdout_and_print() | ||
| 135 | if not self.stdout_printed: | ||
| 136 | self.failed_messages.append("Failed to get stdout/stderr") | ||
| 137 | |||
| 138 | |||
| 139 | |||
| 140 | def main() -> int: | ||
| 141 | parser = argparse.ArgumentParser(allow_abbrev=False, description="Selenium SDL test driver") | ||
| 142 | parser.add_argument("--browser", default="firefox", choices=["firefox", "chrome"], help="browser") | ||
| 143 | parser.add_argument("--server", default="http://localhost:8080", help="Server where SDL tests live") | ||
| 144 | parser.add_argument("--verbose", action="store_true", help="Verbose logging") | ||
| 145 | parser.add_argument("--chrome-binary", help="Chrome binary") | ||
| 146 | parser.add_argument("--firefox-binary", help="Firefox binary") | ||
| 147 | |||
| 148 | index_double_dash = sys.argv.index("--") | ||
| 149 | if index_double_dash < 0: | ||
| 150 | parser.error("Missing test arguments. Need -- <FILENAME> <ARGUMENTS>") | ||
| 151 | driver_arguments = sys.argv[1:index_double_dash] | ||
| 152 | test = pathlib.Path(sys.argv[index_double_dash+1]).name | ||
| 153 | test_arguments = sys.argv[index_double_dash+2:] | ||
| 154 | |||
| 155 | args = parser.parse_args(args=driver_arguments) | ||
| 156 | |||
| 157 | logging.basicConfig(level=logging.DEBUG if args.verbose else logging.INFO) | ||
| 158 | |||
| 159 | logger.debug("driver_arguments=%r test=%r test_arguments=%r", driver_arguments, test, test_arguments) | ||
| 160 | |||
| 161 | sdl_test_driver = SDLSeleniumTestDriver( | ||
| 162 | server=args.server, | ||
| 163 | test=test, | ||
| 164 | arguments=test_arguments, | ||
| 165 | browser=args.browser, | ||
| 166 | chrome_binary=args.chrome_binary, | ||
| 167 | firefox_binary=args.firefox_binary, | ||
| 168 | ) | ||
| 169 | sdl_test_driver.loop() | ||
| 170 | |||
| 171 | rc = sdl_test_driver.return_code | ||
| 172 | if sdl_test_driver.failed_messages: | ||
| 173 | for msg in sdl_test_driver.failed_messages: | ||
| 174 | print(f"FAILURE MESSAGE: {msg}", file=sys.stderr) | ||
| 175 | if rc == 0: | ||
| 176 | print(f"Test signaled success (rc=0) but a failure happened", file=sys.stderr) | ||
| 177 | rc = 1 | ||
| 178 | sys.stdout.flush() | ||
| 179 | logger.info("Exit code = %d", rc) | ||
| 180 | return rc | ||
| 181 | |||
| 182 | |||
| 183 | if __name__ == "__main__": | ||
| 184 | raise SystemExit(main()) | ||
diff --git a/contrib/SDL-3.2.8/test/emscripten/joystick-pre.js b/contrib/SDL-3.2.8/test/emscripten/joystick-pre.js new file mode 100644 index 0000000..5fd789d --- /dev/null +++ b/contrib/SDL-3.2.8/test/emscripten/joystick-pre.js | |||
| @@ -0,0 +1,25 @@ | |||
| 1 | Module['arguments'] = ['0']; | ||
| 2 | //Gamepads don't appear until a button is pressed and the joystick/gamepad tests expect one to be connected | ||
| 3 | Module['preRun'].push(function() | ||
| 4 | { | ||
| 5 | Module['print']("Waiting for gamepad..."); | ||
| 6 | Module['addRunDependency']("gamepad"); | ||
| 7 | window.addEventListener('gamepadconnected', function() | ||
| 8 | { | ||
| 9 | //OK, got one | ||
| 10 | Module['removeRunDependency']("gamepad"); | ||
| 11 | }, false); | ||
| 12 | |||
| 13 | //chrome | ||
| 14 | if(!!navigator.webkitGetGamepads) | ||
| 15 | { | ||
| 16 | var timeout = function() | ||
| 17 | { | ||
| 18 | if(navigator.webkitGetGamepads()[0] !== undefined) | ||
| 19 | Module['removeRunDependency']("gamepad"); | ||
| 20 | else | ||
| 21 | setTimeout(timeout, 100); | ||
| 22 | } | ||
| 23 | setTimeout(timeout, 100); | ||
| 24 | } | ||
| 25 | }); | ||
diff --git a/contrib/SDL-3.2.8/test/emscripten/pre.js b/contrib/SDL-3.2.8/test/emscripten/pre.js new file mode 100644 index 0000000..74ebd1c --- /dev/null +++ b/contrib/SDL-3.2.8/test/emscripten/pre.js | |||
| @@ -0,0 +1,54 @@ | |||
| 1 | const searchParams = new URLSearchParams(window.location.search); | ||
| 2 | |||
| 3 | Module.preRun = () => { | ||
| 4 | }; | ||
| 5 | |||
| 6 | const arguments = []; | ||
| 7 | for (let i = 1; true; i++) { | ||
| 8 | const arg_i = searchParams.get(`arg_${i}`); | ||
| 9 | if (arg_i == null) { | ||
| 10 | break; | ||
| 11 | } | ||
| 12 | arguments.push(arg_i); | ||
| 13 | } | ||
| 14 | |||
| 15 | Module.arguments = arguments; | ||
| 16 | |||
| 17 | if (searchParams.get("loghtml") === "1") { | ||
| 18 | const divTerm = document.createElement("div"); | ||
| 19 | divTerm.id = "terminal"; | ||
| 20 | document.body.append(divTerm); | ||
| 21 | |||
| 22 | function printToStdOut(msg, id) { | ||
| 23 | const divMsg = document.createElement("div", {class: "stdout"}); | ||
| 24 | divMsg.id = id; | ||
| 25 | divMsg.append(document.createTextNode(msg)); | ||
| 26 | divTerm.append(divMsg); | ||
| 27 | return divMsg; | ||
| 28 | } | ||
| 29 | |||
| 30 | Module.print = (msg) => { | ||
| 31 | console.log(msg); | ||
| 32 | printToStdOut(msg, "stdout"); | ||
| 33 | } | ||
| 34 | |||
| 35 | Module.printErr = (msg) => { | ||
| 36 | console.error(msg); | ||
| 37 | const e = printToStdOut(msg, "stderr"); | ||
| 38 | e.style = "color:red"; | ||
| 39 | } | ||
| 40 | |||
| 41 | const divQuit = document.createElement("div"); | ||
| 42 | divQuit.id = "process-quit"; | ||
| 43 | document.body.append(divQuit); | ||
| 44 | |||
| 45 | Module.quit = (msg) => { | ||
| 46 | divQuit.innerText = msg; | ||
| 47 | console.log(`QUIT: ${msg}`) | ||
| 48 | } | ||
| 49 | |||
| 50 | Module.onabort = (msg) => { | ||
| 51 | printToStdOut(`ABORT: ${msg}`, "stderr"); | ||
| 52 | console.log(`ABORT: ${msg}`); | ||
| 53 | } | ||
| 54 | } | ||
diff --git a/contrib/SDL-3.2.8/test/emscripten/server.py b/contrib/SDL-3.2.8/test/emscripten/server.py new file mode 100755 index 0000000..103d164 --- /dev/null +++ b/contrib/SDL-3.2.8/test/emscripten/server.py | |||
| @@ -0,0 +1,102 @@ | |||
| 1 | #!/usr/bin/env python | ||
| 2 | |||
| 3 | # Based on http/server.py from Python | ||
| 4 | |||
| 5 | from argparse import ArgumentParser | ||
| 6 | import contextlib | ||
| 7 | from http.server import SimpleHTTPRequestHandler | ||
| 8 | from http.server import ThreadingHTTPServer | ||
| 9 | import os | ||
| 10 | import socket | ||
| 11 | |||
| 12 | |||
| 13 | class MyHTTPRequestHandler(SimpleHTTPRequestHandler): | ||
| 14 | extensions_map = { | ||
| 15 | ".manifest": "text/cache-manifest", | ||
| 16 | ".html": "text/html", | ||
| 17 | ".png": "image/png", | ||
| 18 | ".jpg": "image/jpg", | ||
| 19 | ".svg": "image/svg+xml", | ||
| 20 | ".css": "text/css", | ||
| 21 | ".js": "application/x-javascript", | ||
| 22 | ".wasm": "application/wasm", | ||
| 23 | "": "application/octet-stream", | ||
| 24 | } | ||
| 25 | |||
| 26 | def __init__(self, *args, maps=None, **kwargs): | ||
| 27 | self.maps = maps or [] | ||
| 28 | SimpleHTTPRequestHandler.__init__(self, *args, **kwargs) | ||
| 29 | |||
| 30 | def end_headers(self): | ||
| 31 | self.send_my_headers() | ||
| 32 | SimpleHTTPRequestHandler.end_headers(self) | ||
| 33 | |||
| 34 | def send_my_headers(self): | ||
| 35 | self.send_header("Cache-Control", "no-cache, no-store, must-revalidate") | ||
| 36 | self.send_header("Pragma", "no-cache") | ||
| 37 | self.send_header("Expires", "0") | ||
| 38 | |||
| 39 | def translate_path(self, path): | ||
| 40 | for map_path, map_prefix in self.maps: | ||
| 41 | if path.startswith(map_prefix): | ||
| 42 | res = os.path.join(map_path, path.removeprefix(map_prefix).lstrip("/")) | ||
| 43 | break | ||
| 44 | else: | ||
| 45 | res = super().translate_path(path) | ||
| 46 | return res | ||
| 47 | |||
| 48 | |||
| 49 | def serve_forever(port: int, ServerClass): | ||
| 50 | handler = MyHTTPRequestHandler | ||
| 51 | |||
| 52 | addr = ("0.0.0.0", port) | ||
| 53 | with ServerClass(addr, handler) as httpd: | ||
| 54 | host, port = httpd.socket.getsockname()[:2] | ||
| 55 | url_host = f"[{host}]" if ":" in host else host | ||
| 56 | print(f"Serving HTTP on {host} port {port} (http://{url_host}:{port}/) ...") | ||
| 57 | try: | ||
| 58 | httpd.serve_forever() | ||
| 59 | except KeyboardInterrupt: | ||
| 60 | print("\nKeyboard interrupt received, exiting.") | ||
| 61 | return 0 | ||
| 62 | |||
| 63 | |||
| 64 | def main(): | ||
| 65 | parser = ArgumentParser(allow_abbrev=False) | ||
| 66 | parser.add_argument("port", nargs="?", type=int, default=8080) | ||
| 67 | parser.add_argument("-d", dest="directory", type=str, default=None) | ||
| 68 | parser.add_argument("--map", dest="maps", nargs="+", type=str, help="Mappings, used as e.g. \"$HOME/projects/SDL:/sdl\"") | ||
| 69 | args = parser.parse_args() | ||
| 70 | |||
| 71 | maps = [] | ||
| 72 | for m in args.maps: | ||
| 73 | try: | ||
| 74 | path, uri = m.split(":", 1) | ||
| 75 | except ValueError: | ||
| 76 | parser.error(f"Invalid mapping: \"{m}\"") | ||
| 77 | maps.append((path, uri)) | ||
| 78 | |||
| 79 | class DualStackServer(ThreadingHTTPServer): | ||
| 80 | def server_bind(self): | ||
| 81 | # suppress exception when protocol is IPv4 | ||
| 82 | with contextlib.suppress(Exception): | ||
| 83 | self.socket.setsockopt(socket.IPPROTO_IPV6, socket.IPV6_V6ONLY, 0) | ||
| 84 | return super().server_bind() | ||
| 85 | |||
| 86 | def finish_request(self, request, client_address): | ||
| 87 | self.RequestHandlerClass( | ||
| 88 | request, | ||
| 89 | client_address, | ||
| 90 | self, | ||
| 91 | directory=args.directory, | ||
| 92 | maps=maps, | ||
| 93 | ) | ||
| 94 | |||
| 95 | return serve_forever( | ||
| 96 | port=args.port, | ||
| 97 | ServerClass=DualStackServer, | ||
| 98 | ) | ||
| 99 | |||
| 100 | |||
| 101 | if __name__ == "__main__": | ||
| 102 | raise SystemExit(main()) | ||
