From 161761653f9f97bf186bd1dff0a0404bdca49043 Mon Sep 17 00:00:00 2001
From: Anonymous Maarten <[EMAIL REDACTED]>
Date: Thu, 30 May 2024 21:08:30 +0200
Subject: [PATCH] test: use Selenium to run Emscripten tests
---
.github/workflows/create-test-plan.py | 27 +++-
.github/workflows/generic.yml | 19 ++-
test/CMakeLists.txt | 94 ++++++++++----
test/emscripten/driver.py | 179 ++++++++++++++++++++++++++
test/emscripten/pre.js | 54 ++++++++
5 files changed, 341 insertions(+), 32 deletions(-)
create mode 100755 test/emscripten/driver.py
create mode 100644 test/emscripten/pre.js
diff --git a/.github/workflows/create-test-plan.py b/.github/workflows/create-test-plan.py
index 99b3a7db96ef1..340514c3bc732 100755
--- a/.github/workflows/create-test-plan.py
+++ b/.github/workflows/create-test-plan.py
@@ -175,6 +175,7 @@ class JobDetails:
test_pkg_config: bool = True
cc_from_cmake: bool = False
source_cmd: str = ""
+ pretest_cmd: str = ""
java: bool = False
android_apks: list[str] = dataclasses.field(default_factory=list)
android_ndk: bool = False
@@ -224,6 +225,7 @@ def to_workflow(self, enable_artifacts: bool) -> dict[str, str|bool]:
"no-cmake": self.no_cmake,
"build-tests": self.build_tests,
"source-cmd": self.source_cmd,
+ "pretest-cmd": self.pretest_cmd,
"cmake-config-emulator": self.cmake_config_emulator,
"cc": self.cc,
"cxx": self.cxx,
@@ -484,11 +486,30 @@ def spec_to_job(spec: JobSpec) -> JobDetails:
"testsprite-apk",
]
case SdlPlatform.Emscripten:
- job.run_tests = False
+ job.clang_tidy = False # clang-tidy does not understand -gsource-map
job.shared = False
job.cmake_config_emulator = "emcmake"
job.cmake_build_type = "Debug"
job.test_pkg_config = False
+ job.apt_packages.append("python3-selenium")
+ job.cmake_arguments.extend((
+ "-DSDLTEST_BROWSER=chrome",
+ "-DSDLTEST_TIMEOUT_MULTIPLIER=4",
+ "-DSDLTEST_CHROME_BINARY=${CHROME_BINARY}",
+ ))
+ job.cflags.extend((
+ "-gsource-map",
+ "-ffile-prefix-map=${PWD}=/SDL",
+ ))
+ job.ldflags.extend((
+ "--source-map-base", "/",
+ ))
+ job.pretest_cmd = "\n".join([
+ "# Start local HTTP server",
+ "cmake --build build --target serve-sdl-tests --verbose &",
+ "chrome --version",
+ "chromedriver --version",
+ ])
case SdlPlatform.Ps2:
build_parallel = False
job.shared = False
@@ -623,9 +644,9 @@ def spec_to_job(spec: JobSpec) -> JobDetails:
if not build_parallel:
job.cmake_build_arguments.append("-j1")
if job.cflags:
- job.cmake_arguments.append(f"-DCMAKE_C_FLAGS={my_shlex_join(job.cflags)}")
+ job.cmake_arguments.append(f"-DCMAKE_C_FLAGS=\"{my_shlex_join(job.cflags)}\"")
if job.cxxflags:
- job.cmake_arguments.append(f"-DCMAKE_CXX_FLAGS={my_shlex_join(job.cxxflags)}")
+ job.cmake_arguments.append(f"-DCMAKE_CXX_FLAGS=\"{my_shlex_join(job.cxxflags)}\"")
if job.ldflags:
job.cmake_arguments.append(f"-DCMAKE_SHARED_LINKER_FLAGS=\"{my_shlex_join(job.ldflags)}\"")
job.cmake_arguments.append(f"-DCMAKE_EXE_LINKER_FLAGS=\"{my_shlex_join(job.ldflags)}\"")
diff --git a/.github/workflows/generic.yml b/.github/workflows/generic.yml
index 2798ec7cda98b..da916b3d0da9e 100644
--- a/.github/workflows/generic.yml
+++ b/.github/workflows/generic.yml
@@ -47,6 +47,22 @@ jobs:
if: ${{ matrix.platform.platform == 'emscripten' }}
with:
version: 3.1.35
+ - uses: browser-actions/setup-chrome@v1
+ id: setup-chrome
+ if: ${{ matrix.platform.platform == 'emscripten' }}
+ with:
+ install-chromedriver: true
+ - name: 'Add chrome to PATH'
+ if: ${{ matrix.platform.platform == 'emscripten' }}
+ run: |
+ chrome_dir="$(dirname "${{ steps.setup-chrome.outputs.chrome-path }}")"
+ chromedriver_dir="$(dirname "${{ steps.setup-chrome.outputs.chromedriver-path }}")"
+ echo "CHROME_BINARY=${{ steps.setup-chrome.outputs.chrome-path }}" >>$GITHUB_ENV
+ echo "CHROMEDRIVER_BINARY=${{ steps.setup-chrome.outputs.chromedriver-path }}" >>$GITHUB_ENV
+ echo "chrome_dir=${chrome_dir}"
+ echo "chromedriver_dir=${chromedriver_dir}"
+ echo "${chrome_dir}" >>${GITHUB_PATH}
+ echo "${chromedriver_dir}" >>${GITHUB_PATH}
- uses: nttld/setup-ndk@v1
if: ${{ matrix.platform.android-ndk }}
id: setup-ndk
@@ -115,7 +131,7 @@ jobs:
with:
type: ${{ matrix.platform.setup-vita-gles-type }}
- - name: 'Pollute toolchain with "bad SDL headers'
+ - name: 'Pollute toolchain with "bad" SDL headers'
if: ${{ matrix.platform.pollute-directories != '' }}
#shell: ${{ matrix.platform.shell }}
run: |
@@ -169,6 +185,7 @@ jobs:
# shell: ${{ matrix.platform.shell }}
run: |
${{ matrix.platform.source-cmd }}
+ ${{ matrix.platform.pretest-cmd }}
set -eu
export SDL_TESTS_QUICK=1
ctest -VV --test-dir build/ -j2
diff --git a/test/CMakeLists.txt b/test/CMakeLists.txt
index 12314f5c781bf..4252988534291 100644
--- a/test/CMakeLists.txt
+++ b/test/CMakeLists.txt
@@ -52,6 +52,20 @@ if(WIN32 AND NOT WINDOWS_STORE)
endif()
endif()
+if(EMSCRIPTEN)
+ set(SDLTEST_BROWSER "firefox" CACHE STRING "Browser in which to run SDL unit tests (chrome or firefox)")
+ set(SDLTEST_PORT "8080" CACHE STRING "Port on which to serve the tests")
+ set(SDLTEST_CHROME_BINARY "" CACHE STRING "Chrome/Chromium browser binary (optional)")
+ find_package(Python3 COMPONENTS Interpreter)
+ if(TARGET Python3::Interpreter)
+ add_custom_target(serve-sdl-tests
+ COMMAND Python3::Interpreter "${CMAKE_CURRENT_SOURCE_DIR}/emscripten/server.py"
+ "${SDLTEST_PORT}"
+ -d "${CMAKE_CURRENT_BINARY_DIR}"
+ --map "${CMAKE_CURRENT_SOURCE_DIR}/..:/SDL")
+ endif()
+endif()
+
if(CMAKE_RUNTIME_OUTPUT_DIRECTORY)
set(test_bin_dir "${CMAKE_RUNTIME_OUTPUT_DIRECTORY}")
if(NOT IS_ABSOLUTE "${CMAKE_RUNTIME_OUTPUT_DIRECTORY}")
@@ -99,7 +113,7 @@ if(WINDOWS_STORE)
endif()
macro(add_sdl_test_executable TARGET)
- cmake_parse_arguments(AST "BUILD_DEPENDENT;NONINTERACTIVE;NEEDS_RESOURCES;TESTUTILS;NO_C90;MAIN_CALLBACKS;NOTRACKMEM" "" "NONINTERACTIVE_TIMEOUT;NONINTERACTIVE_ARGS;SOURCES" ${ARGN})
+ cmake_parse_arguments(AST "BUILD_DEPENDENT;NONINTERACTIVE;NEEDS_RESOURCES;TESTUTILS;THREADS;NO_C90;MAIN_CALLBACKS;NOTRACKMEM" "" "DISABLE_THREADS_ARGS;NONINTERACTIVE_TIMEOUT;NONINTERACTIVE_ARGS;SOURCES" ${ARGN})
if(AST_UNPARSED_ARGUMENTS)
message(FATAL_ERROR "Unknown argument(s): ${AST_UNPARSED_ARGUMENTS}")
endif()
@@ -157,6 +171,8 @@ macro(add_sdl_test_executable TARGET)
if(AST_NONINTERACTIVE)
set_property(TARGET ${TARGET} PROPERTY SDL_NONINTERACTIVE 1)
endif()
+ set_property(TARGET ${TARGET} PROPERTY SDL_DISABLE_THREADS_ARGS "${AST_DISABLE_THREADS_ARGS}")
+ set_property(TARGET ${TARGET} PROPERTY SDL_THREADS "${AST_THREADS}")
if(AST_NONINTERACTIVE_ARGS)
set_property(TARGET ${TARGET} PROPERTY SDL_NONINTERACTIVE_ARGUMENTS "${AST_NONINTERACTIVE_ARGS}")
endif()
@@ -221,6 +237,9 @@ macro(add_sdl_test_executable TARGET)
if(EMSCRIPTEN)
set_property(TARGET ${TARGET} PROPERTY SUFFIX ".html")
+ target_link_options(${TARGET} PRIVATE "SHELL:--pre-js ${CMAKE_CURRENT_SOURCE_DIR}/emscripten/pre.js")
+ target_link_options(${TARGET} PRIVATE "-sEXIT_RUNTIME=1")
+ set_property(TARGET ${TARGET} APPEND PROPERTY LINK_DEPENDS "${CMAKE_CURRENT_SOURCE_DIR}/emscripten/pre.js")
endif()
if(OPENGL_FOUND)
@@ -279,17 +298,20 @@ add_sdl_test_executable(testaudiostreamdynamicresample NEEDS_RESOURCES TESTUTILS
file(GLOB TESTAUTOMATION_SOURCE_FILES testautomation*.c)
add_sdl_test_executable(testautomation NONINTERACTIVE NONINTERACTIVE_TIMEOUT 120 NEEDS_RESOURCES NO_C90 SOURCES ${TESTAUTOMATION_SOURCE_FILES})
+if(EMSCRIPTEN)
+ target_link_options(testautomation PRIVATE -sALLOW_MEMORY_GROWTH=1 -sMAXIMUM_MEMORY=1gb)
+endif()
add_sdl_test_executable(testmultiaudio NEEDS_RESOURCES TESTUTILS SOURCES testmultiaudio.c)
add_sdl_test_executable(testaudiohotplug NEEDS_RESOURCES TESTUTILS SOURCES testaudiohotplug.c)
add_sdl_test_executable(testaudiorecording MAIN_CALLBACKS SOURCES testaudiorecording.c)
-add_sdl_test_executable(testatomic NONINTERACTIVE SOURCES testatomic.c)
+add_sdl_test_executable(testatomic NONINTERACTIVE DISABLE_THREADS_ARGS "--no-threads" SOURCES testatomic.c)
add_sdl_test_executable(testintersections SOURCES testintersections.c)
add_sdl_test_executable(testrelative SOURCES testrelative.c)
add_sdl_test_executable(testhittesting SOURCES testhittesting.c)
add_sdl_test_executable(testdraw SOURCES testdraw.c)
add_sdl_test_executable(testdrawchessboard SOURCES testdrawchessboard.c)
add_sdl_test_executable(testdropfile MAIN_CALLBACKS SOURCES testdropfile.c)
-add_sdl_test_executable(testerror NONINTERACTIVE SOURCES testerror.c)
+add_sdl_test_executable(testerror NONINTERACTIVE DISABLE_THREADS_ARGS "--no-threads" SOURCES testerror.c)
set(build_options_dependent_tests )
@@ -320,7 +342,7 @@ elseif(HAVE_X11 OR HAVE_WAYLAND)
endif ()
endif()
-find_package(Python3)
+find_package(Python3 COMPONENTS Interpreter)
function(files2headers OUTPUT)
set(xxd "${CMAKE_CURRENT_SOURCE_DIR}/../cmake/xxd.py")
set(inputs ${ARGN})
@@ -335,7 +357,7 @@ function(files2headers OUTPUT)
# Don't add the 'output' header to the output, to avoid marking them as GENERATED
# (generated files are removed when running the CLEAN target)
add_custom_command(OUTPUT "${intermediate}"
- COMMAND "${Python3_EXECUTABLE}" "${xxd}" -i "${CMAKE_CURRENT_SOURCE_DIR}/${input}" "-o" "${intermediate}"
+ COMMAND Python3::Interpreter "${xxd}" -i "${CMAKE_CURRENT_SOURCE_DIR}/${input}" "-o" "${intermediate}"
COMMAND "${CMAKE_COMMAND}" -E copy_if_different "${intermediate}" "${output}"
DEPENDS "${xxd}" "${bmp}"
)
@@ -384,7 +406,7 @@ add_sdl_test_executable(testhaptic SOURCES testhaptic.c)
add_sdl_test_executable(testhotplug SOURCES testhotplug.c)
add_sdl_test_executable(testpen SOURCES testpen.c)
add_sdl_test_executable(testrumble SOURCES testrumble.c)
-add_sdl_test_executable(testthread NONINTERACTIVE NONINTERACTIVE_TIMEOUT 40 SOURCES testthread.c)
+add_sdl_test_executable(testthread NONINTERACTIVE THREADS NONINTERACTIVE_TIMEOUT 40 SOURCES testthread.c)
add_sdl_test_executable(testiconv NEEDS_RESOURCES TESTUTILS SOURCES testiconv.c)
add_sdl_test_executable(testime NEEDS_RESOURCES TESTUTILS SOURCES testime.c)
add_sdl_test_executable(testkeys SOURCES testkeys.c)
@@ -403,7 +425,7 @@ if(WIN32 AND CMAKE_SIZEOF_VOID_P EQUAL 4)
endif()
add_sdl_test_executable(testrendertarget NEEDS_RESOURCES TESTUTILS SOURCES testrendertarget.c)
add_sdl_test_executable(testscale NEEDS_RESOURCES TESTUTILS SOURCES testscale.c)
-add_sdl_test_executable(testsem NONINTERACTIVE NONINTERACTIVE_ARGS 10 NONINTERACTIVE_TIMEOUT 30 SOURCES testsem.c)
+add_sdl_test_executable(testsem NONINTERACTIVE DISABLE_THREADS_ARGS "--no-threads" NONINTERACTIVE_ARGS 10 NONINTERACTIVE_TIMEOUT 30 SOURCES testsem.c)
add_sdl_test_executable(testsensor SOURCES testsensor.c)
add_sdl_test_executable(testshader NEEDS_RESOURCES TESTUTILS SOURCES testshader.c)
if(EMSCRIPTEN)
@@ -421,7 +443,7 @@ add_sdl_test_executable(testcamera MAIN_CALLBACKS SOURCES testcamera.c)
add_sdl_test_executable(testviewport NEEDS_RESOURCES TESTUTILS SOURCES testviewport.c)
add_sdl_test_executable(testwm SOURCES testwm.c)
add_sdl_test_executable(testyuv NONINTERACTIVE NONINTERACTIVE_ARGS "--automated" NEEDS_RESOURCES TESTUTILS SOURCES testyuv.c testyuv_cvt.c)
-add_sdl_test_executable(torturethread NONINTERACTIVE NONINTERACTIVE_TIMEOUT 30 SOURCES torturethread.c)
+add_sdl_test_executable(torturethread NONINTERACTIVE THREADS NONINTERACTIVE_TIMEOUT 30 SOURCES torturethread.c)
add_sdl_test_executable(testrendercopyex NEEDS_RESOURCES TESTUTILS SOURCES testrendercopyex.c)
add_sdl_test_executable(testmessage SOURCES testmessage.c)
add_sdl_test_executable(testdisplayinfo SOURCES testdisplayinfo.c)
@@ -489,15 +511,6 @@ if(OPENGL_FOUND)
target_link_libraries(testgl PRIVATE ${OPENGL_gl_LIBRARY})
endif()
endif()
-if(EMSCRIPTEN)
- set_property(TARGET testshader APPEND_STRING PROPERTY LINK_FLAGS " -sLEGACY_GL_EMULATION")
-
- find_package(Python3 COMPONENTS Interpreter)
- if(TARGET Python3::Interpreter)
- add_custom_target(serve-sdl-tests
- COMMAND Python3::Interpreter "${CMAKE_CURRENT_SOURCE_DIR}/emscripten/server.py" -d "${CMAKE_CURRENT_BINARY_DIR}")
- endif()
-endif()
if(MACOS)
target_link_options(testnative PRIVATE "-Wl,-framework,Cocoa")
endif()
@@ -605,15 +618,29 @@ endif()
set(TESTS_ENVIRONMENT
SDL_AUDIO_DRIVER=dummy
SDL_VIDEO_DRIVER=dummy
- PATH=$<TARGET_FILE_DIR:SDL3::${sdl_name_component}>
)
+set(SDLTEST_TIMEOUT_MULTIPLIER "1" CACHE STRING "SDL test time-out multiplier")
+
function(add_sdl_test TEST TARGET)
cmake_parse_arguments(ast "INSTALL" "" "" ${ARGN})
get_property(noninteractive TARGET ${TARGET} PROPERTY SDL_NONINTERACTIVE)
if(noninteractive)
- set(command ${TARGET})
+ if(EMSCRIPTEN)
+ if(NOT PYTHON3_EXECUTABLE)
+ set(PYTHON3_EXECUTABLE "python3")
+ endif()
+ set(command "${PYTHON3_EXECUTABLE};${CMAKE_CURRENT_SOURCE_DIR}/emscripten/driver.py;--server;http://localhost:${SDLTEST_PORT};--browser;${SDLTEST_BROWSER}")
+ if(SDLTEST_CHROME_BINARY)
+ list(APPEND command "--chrome-binary;${SDLTEST_CHROME_BINARY}")
+ endif()
+ list(APPEND command "--;${TARGET}")
+ else()
+ set(command ${TARGET})
+ endif()
get_property(noninteractive_arguments TARGET ${TARGET} PROPERTY SDL_NONINTERACTIVE_ARGUMENTS)
+ get_property(disable_threads_args TARGET ${TARGET} PROPERTY SDL_DISABLE_THREADS_ARGS)
+ get_property(uses_threads TARGET ${TARGET} PROPERTY SDL_THREADS)
if(noninteractive_arguments)
list(APPEND command ${noninteractive_arguments})
endif()
@@ -623,20 +650,29 @@ function(add_sdl_test TEST TARGET)
list(APPEND command --trackmem)
endif()
endif()
+ if(EMSCRIPTEN)
+ list(APPEND command ${disable_threads_args})
+ endif()
add_test(
- NAME ${TEST}
- COMMAND ${command}
- WORKING_DIRECTORY ${CMAKE_CURRENT_BINARY_DIR}
+ NAME ${TEST}
+ COMMAND ${command}
+ WORKING_DIRECTORY ${CMAKE_CURRENT_BINARY_DIR}
)
+ if(WIN32 AND CMAKE_VERSION VERSION_GREATER_EQUAL "3.27")
+ set_property(TEST ${TEST} APPEND PROPERTY ENVIRONMENT_MODIFICATION "PATH=path_list_prepend:$<TARGET_RUNTIME_DLL_DIRS:${TEST}>")
+ endif()
if(NOT notrackmem)
set_property(TEST ${TEST} PROPERTY FAIL_REGULAR_EXPRESSION "Total: [0-9]+\\.[0-9]+ Kb in [1-9][0-9]* allocations")
endif()
set_tests_properties(${TEST} PROPERTIES ENVIRONMENT "${TESTS_ENVIRONMENT}")
+ if(EMSCRIPTEN AND uses_threads)
+ set_tests_properties(${TEST} PROPERTIES DISABLED 1)
+ endif()
get_property(noninteractive_timeout TARGET ${TARGET} PROPERTY SDL_NONINTERACTIVE_TIMEOUT)
if(NOT noninteractive_timeout)
set(noninteractive_timeout 10)
endif()
- math(EXPR noninteractive_timeout "${noninteractive_timeout}*${SDL_TESTS_TIMEOUT_MULTIPLIER}")
+ math(EXPR noninteractive_timeout "${noninteractive_timeout}*${SDLTEST_TIMEOUT_MULTIPLIER}")
set_tests_properties(${TEST} PROPERTIES TIMEOUT "${noninteractive_timeout}")
if(ast_INSTALL AND SDL_INSTALL_TESTS)
set(exe ${TARGET})
@@ -657,12 +693,14 @@ foreach(TARGET ${SDL_TEST_EXECUTABLES})
add_sdl_test(${TARGET} ${TARGET} INSTALL)
endforeach()
-add_sdl_test(testautomation-no-simd testautomation)
-add_sdl_test(testplatform-no-simd testplatform)
-set_property(TEST testautomation-no-simd testplatform-no-simd APPEND PROPERTY ENVIRONMENT "SDL_CPU_FEATURE_MASK=-all")
+if(NOT EMSCRIPTEN)
+ add_sdl_test(testautomation-no-simd testautomation)
+ add_sdl_test(testplatform-no-simd testplatform)
+ set_property(TEST testautomation-no-simd testplatform-no-simd APPEND PROPERTY ENVIRONMENT "SDL_CPU_FEATURE_MASK=-all")
-# testautomation creates temporary files which might conflict
-set_property(TEST testautomation-no-simd testautomation PROPERTY RUN_SERIAL TRUE)
+ # testautomation creates temporary files which might conflict
+ set_property(TEST testautomation-no-simd testautomation PROPERTY RUN_SERIAL TRUE)
+endif()
if(SDL_INSTALL_TESTS)
if(RISCOS)
diff --git a/test/emscripten/driver.py b/test/emscripten/driver.py
new file mode 100755
index 0000000000000..78b7a49e05fb6
--- /dev/null
+++ b/test/emscripten/driver.py
@@ -0,0 +1,179 @@
+#!/usr/bin/env python
+
+import argparse
+import contextlib
+import logging
+import urllib.parse
+import shlex
+import sys
+import time
+import pathlib
+from typing import Optional
+
+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
+
+ 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
+ if driver_contructor is None:
+ raise ValueError(f"Invalid {browser=}")
+
+ options = [
+ "--headless",
+ ]
+ 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",
+ }
+ 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 -- <FILENAME> <ARGUMENTS>")
+ 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/test/emscripten/pre.js b/test/emscripten/pre.js
new file mode 100644
index 0000000000000..74ebd1c3c04a2
--- /dev/null
+++ b/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}`);
+ }
+}