diff options
author | 3gg <3gg@shellblade.net> | 2025-08-30 16:53:58 -0700 |
---|---|---|
committer | 3gg <3gg@shellblade.net> | 2025-08-30 16:53:58 -0700 |
commit | 6aaedb813fa11ba0679c3051bc2eb28646b9506c (patch) | |
tree | 34acbfc9840e02cb4753e6306ea7ce978bf8b58e /src/contrib/SDL-3.2.20/test/emscripten | |
parent | 8f228ade99dd3d4c8da9b78ade1815c9adf85c8f (diff) |
Update to SDL3
Diffstat (limited to 'src/contrib/SDL-3.2.20/test/emscripten')
-rwxr-xr-x | src/contrib/SDL-3.2.20/test/emscripten/driver.py | 184 | ||||
-rw-r--r-- | src/contrib/SDL-3.2.20/test/emscripten/joystick-pre.js | 25 | ||||
-rw-r--r-- | src/contrib/SDL-3.2.20/test/emscripten/pre.js | 54 | ||||
-rwxr-xr-x | src/contrib/SDL-3.2.20/test/emscripten/server.py | 102 |
4 files changed, 365 insertions, 0 deletions
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 @@ | |||
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/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 @@ | |||
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/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 @@ | |||
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/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 @@ | |||
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()) | ||