From 62c4e033dad4b919d7ad364e054b2291f9535c2d Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jukka=20Jyl=C3=A4nki?= Date: Sun, 14 Sep 2025 22:50:44 +0300 Subject: [PATCH 01/18] Fix parallel browser test harness to also work with Firefox on Windows. --- pyproject.toml | 4 ++ requirements-dev.txt | 1 + test/common.py | 148 ++++++++++++++++++++++++++++++++++++++----- test/runner.py | 4 ++ 4 files changed, 142 insertions(+), 15 deletions(-) diff --git a/pyproject.toml b/pyproject.toml index fbaa74b8b6bdc..658b7874f5e3a 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -104,3 +104,7 @@ module = [ "ply.*", ] ignore_errors = true + +[[tool.mypy.overrides]] +module = ["psutil", "win32gui", "win32process"] +ignore_missing_imports = true diff --git a/requirements-dev.txt b/requirements-dev.txt index ed1fcc60080d3..39bbf5621b136 100644 --- a/requirements-dev.txt +++ b/requirements-dev.txt @@ -6,6 +6,7 @@ coverage[toml]==6.5 mypy==1.14 +psutil==7.0.0 ruff==0.11.7 types-requests==2.32.0.20241016 unittest-xml-reporting==3.2.0 diff --git a/test/common.py b/test/common.py index 66d5944ed14f6..c370583d83122 100644 --- a/test/common.py +++ b/test/common.py @@ -18,6 +18,7 @@ import json import logging import os +import psutil import re import shlex import shutil @@ -36,7 +37,7 @@ import clang_native import jsrun import line_endings -from tools.shared import EMCC, EMXX, DEBUG +from tools.shared import EMCC, EMXX, DEBUG, exe_suffix from tools.shared import get_canonical_temp_dir, path_from_root from tools.utils import MACOS, WINDOWS, read_file, read_binary, write_binary, exit_with_error from tools.settings import COMPILE_TIME_SETTINGS @@ -87,6 +88,7 @@ # file to track which tests were flaky so they can be graphed in orange color to # visually stand out. flaky_tests_log_filename = os.path.join(path_from_root('out/flaky_tests.txt')) +browser_spawn_lock_filename = os.path.join(path_from_root('out/browser_spawn_lock')) # Default flags used to run browsers in CI testing: @@ -116,6 +118,7 @@ class FirefoxConfig: data_dir_flag = '-profile ' default_flags = () headless_flags = '-headless' + executable_name = exe_suffix('firefox') @staticmethod def configure(data_dir): @@ -938,8 +941,26 @@ def make_dir_writeable(dirname): def force_delete_dir(dirname): - make_dir_writeable(dirname) - utils.delete_dir(dirname) + """Deletes a directory. Returns whether deletion succeeded.""" + if not os.path.exists(dirname): + return True + if os.path.isfile(dirname): + return False + + try: + make_dir_writeable(dirname) + utils.delete_dir(dirname) + except PermissionError as e: + # This issue currently occurs on Windows when running browser tests e.g. + # on Firefox browser. Killing Firefox browser is not 100% watertight, and + # occassionally a Firefox browser process can be left behind, holding on + # to a file handle, preventing the deletion from succeeding. + # We expect this issue to only occur on Windows. + if not WINDOWS: + raise e + print(f'Warning: Failed to delete directory "{dirname}"\n{e}') + return False + return True def force_delete_contents(dirname): @@ -2499,6 +2520,69 @@ def configure_test_browser(): EMTEST_BROWSER += f" {config.headless_flags}" +def list_processes_by_name(exe_name): + pids = [] + if exe_name: + for proc in psutil.process_iter(): + try: + pinfo = proc.as_dict(attrs=['pid', 'name', 'exe']) + if pinfo['exe'] and exe_name in pinfo['exe'].replace('\\', '/').split('/'): + pids.append(psutil.Process(pinfo['pid'])) + except psutil.NoSuchProcess: # E.g. "process no longer exists (pid=13132)" (code raced to acquire the iterator and process it) + pass + + return pids + + +class FileLock: + """Implements a filesystem-based mutex, with an additional feature that it + returns an integer counter denoting how many times the lock has been locked + before (during the current python test run instance)""" + def __init__(self, path): + self.path = path + self.counter = 0 + + def __enter__(self): + # Acquire the lock + while True: + try: + self.fd = os.open(self.path, os.O_CREAT | os.O_EXCL | os.O_WRONLY) + break + except FileExistsError: + time.sleep(0.1) + # Return the locking count number + try: + self.counter = int(open(f'{self.path}_counter').read()) + except Exception: + pass + return self.counter + + def __exit__(self, *a): + # Increment locking count number before releasing the lock + with open(f'{self.path}_counter', 'w') as f: + f.write(str(self.counter + 1)) + # And release the lock + os.close(self.fd) + try: + os.remove(self.path) + except Exception: + pass # Another process has raced to acquire the lock, and will delete it. + + +def move_browser_window(pid, x, y): + """Utility function to move the top-level window owned by given process to + (x,y) coordinate. Used to ensure each browser window has some visible area.""" + import win32gui, win32process + def enum_windows_callback(hwnd, _unused): + _, win_pid = win32process.GetWindowThreadProcessId(hwnd) + if win_pid == pid and win32gui.IsWindowVisible(hwnd): + rect = win32gui.GetWindowRect(hwnd) + win32gui.MoveWindow(hwnd, x, y, rect[2] - rect[0], rect[3] - rect[1], True) + return True + + win32gui.EnumWindows(enum_windows_callback, None) + + class BrowserCore(RunnerCore): # note how many tests hang / do not send an output. if many of these # happen, likely something is broken and it is best to abort the test @@ -2515,15 +2599,19 @@ def __init__(self, *args, **kwargs): @classmethod def browser_terminate(cls): - cls.browser_proc.terminate() - # If the browser doesn't shut down gracefully (in response to SIGTERM) - # after 2 seconds kill it with force (SIGKILL). - try: - cls.browser_proc.wait(2) - except subprocess.TimeoutExpired: - logger.info('Browser did not respond to `terminate`. Using `kill`') - cls.browser_proc.kill() - cls.browser_proc.wait() + for proc in cls.browser_procs: + try: + proc.terminate() + # If the browser doesn't shut down gracefully (in response to SIGTERM) + # after 2 seconds kill it with force (SIGKILL). + try: + proc.wait(2) + except (subprocess.TimeoutExpired, psutil.TimeoutExpired): + logger.info('Browser did not respond to `terminate`. Using `kill`') + proc.kill() + proc.wait() + except (psutil.NoSuchProcess, ProcessLookupError): + pass @classmethod def browser_restart(cls): @@ -2542,9 +2630,15 @@ def browser_open(cls, url): if worker_id is not None: # Running in parallel mode, give each browser its own profile dir. browser_data_dir += '-' + str(worker_id) - if os.path.exists(browser_data_dir): - utils.delete_dir(browser_data_dir) + + # Delete old browser data directory. If we cannot (the data dir is in use on Windows), + # switch to another dir. + while not force_delete_dir(browser_data_dir): + browser_data_dir += '-another' + + # Recreate the new data directory. os.mkdir(browser_data_dir) + if is_chrome(): config = ChromeConfig() elif is_firefox(): @@ -2559,7 +2653,31 @@ def browser_open(cls, url): browser_args = shlex.split(browser_args) logger.info('Launching browser: %s', str(browser_args)) - cls.browser_proc = subprocess.Popen(browser_args + [url]) + + with FileLock(browser_spawn_lock_filename) as count: + # Firefox is a multiprocess browser. Killing the spawned process will not + # bring down the whole browser, but only one browser tab. So take a delta + # snapshot before->after spawning the browser to find which subprocesses + # we launched. + if worker_id is not None and WINDOWS and is_firefox(): + procs_before = list_processes_by_name(config.executable_name) + cls.browser_procs = [subprocess.Popen(browser_args + [url])] + # Give Firefox time to spawn its subprocesses. Use an increasing timeout + # as a crude way to account for system load. + if worker_id is not None and WINDOWS and is_firefox(): + time.sleep(2 + count * 0.3) + procs_after = list_processes_by_name(config.executable_name) + # Make sure that each browser window is visible on the desktop. Otherwise + # browser might decide that the tab is backgrounded, and not load a test, + # or it might not tick rAF()s forward, causing tests to hang. + if worker_id is not None and WINDOWS and is_firefox(): + # On Firefox on Windows we needs to track subprocesses that got created + # by Firefox. Other setups can use 'browser_proc' directly to terminate + # the browser. + cls.browser_procs = list(set(procs_after).difference(set(procs_before))) + # Wrap window positions on a Full HD desktop area modulo primes. + for proc in cls.browser_procs: + move_browser_window(proc.pid, (300 + count * 47) % 1901, (10 + count * 37) % 997) @classmethod def setUpClass(cls): diff --git a/test/runner.py b/test/runner.py index 6b67587d2266b..49ec57a6379ef 100755 --- a/test/runner.py +++ b/test/runner.py @@ -562,6 +562,10 @@ def set_env(name, option_value): check_js_engines() + # Remove any old test files before starting the run + utils.delete_file(common.browser_spawn_lock_filename) + utils.delete_file(f'{common.browser_spawn_lock_filename}_counter') + def prepend_default(arg): if arg.startswith('test_'): return default_core_test_mode + '.' + arg From 90b6d7e0e0ac2326b077f87a21d98e40b2284ae3 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jukka=20Jyl=C3=A4nki?= Date: Sun, 14 Sep 2025 22:54:49 +0300 Subject: [PATCH 02/18] ruff --- test/common.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/test/common.py b/test/common.py index c370583d83122..12161da9e9fa8 100644 --- a/test/common.py +++ b/test/common.py @@ -2572,7 +2572,8 @@ def __exit__(self, *a): def move_browser_window(pid, x, y): """Utility function to move the top-level window owned by given process to (x,y) coordinate. Used to ensure each browser window has some visible area.""" - import win32gui, win32process + import win32gui + import win32process def enum_windows_callback(hwnd, _unused): _, win_pid = win32process.GetWindowThreadProcessId(hwnd) if win_pid == pid and win32gui.IsWindowVisible(hwnd): From 54d34f6bb808ad68c6fd308f259d8029fa36689c Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jukka=20Jyl=C3=A4nki?= Date: Mon, 15 Sep 2025 22:51:32 +0300 Subject: [PATCH 03/18] Add newline --- test/common.py | 1 + 1 file changed, 1 insertion(+) diff --git a/test/common.py b/test/common.py index 9e6e327ed012d..933f9593bed74 100644 --- a/test/common.py +++ b/test/common.py @@ -2574,6 +2574,7 @@ def move_browser_window(pid, x, y): (x,y) coordinate. Used to ensure each browser window has some visible area.""" import win32gui import win32process + def enum_windows_callback(hwnd, _unused): _, win_pid = win32process.GetWindowThreadProcessId(hwnd) if win_pid == pid and win32gui.IsWindowVisible(hwnd): From d571e9b095a4d1bca56aa0dd3fd7560a2f71f6e1 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jukka=20Jyl=C3=A4nki?= Date: Wed, 17 Sep 2025 01:36:57 +0300 Subject: [PATCH 04/18] Review --- test/common.py | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/test/common.py b/test/common.py index 933f9593bed74..87dec30d3e8ef 100644 --- a/test/common.py +++ b/test/common.py @@ -944,8 +944,7 @@ def force_delete_dir(dirname): """Deletes a directory. Returns whether deletion succeeded.""" if not os.path.exists(dirname): return True - if os.path.isfile(dirname): - return False + assert not os.path.isfile(dirname) try: make_dir_writeable(dirname) From 16819f67df8a6881d3b46e302f58a95ca3445ad9 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jukka=20Jyl=C3=A4nki?= Date: Wed, 17 Sep 2025 01:39:02 +0300 Subject: [PATCH 05/18] Update comment --- test/common.py | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/test/common.py b/test/common.py index 87dec30d3e8ef..740fbd0b1311b 100644 --- a/test/common.py +++ b/test/common.py @@ -2656,10 +2656,10 @@ def browser_open(cls, url): logger.info('Launching browser: %s', str(browser_args)) with FileLock(browser_spawn_lock_filename) as count: - # Firefox is a multiprocess browser. Killing the spawned process will not - # bring down the whole browser, but only one browser tab. So take a delta - # snapshot before->after spawning the browser to find which subprocesses - # we launched. + # Firefox is a multiprocess browser. On Windows, killing the spawned + # process will not bring down the whole browser, but only one browser tab. + # So take a delta snapshot before->after spawning the browser to find + # which subprocesses we launched. if worker_id is not None and WINDOWS and is_firefox(): procs_before = list_processes_by_name(config.executable_name) cls.browser_procs = [subprocess.Popen(browser_args + [url])] From f4c1bb28fd4b011e83174fc630866a9a5df88442 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jukka=20Jyl=C3=A4nki?= Date: Wed, 17 Sep 2025 01:49:23 +0300 Subject: [PATCH 06/18] Use _number instead of appending a suffix --- test/common.py | 21 +++++++++++++++++---- 1 file changed, 17 insertions(+), 4 deletions(-) diff --git a/test/common.py b/test/common.py index 740fbd0b1311b..92ac3e667e70b 100644 --- a/test/common.py +++ b/test/common.py @@ -2584,6 +2584,16 @@ def enum_windows_callback(hwnd, _unused): win32gui.EnumWindows(enum_windows_callback, None) +def increment_suffix_number(str_with_maybe_suffix): + match = re.match(r"^(.*?)(?:_(\d+))?$", str_with_maybe_suffix) + if match: + base, number = match.groups() + if number: + return f'{base}_{int(number) + 1}' + + return f'{str_with_maybe_suffix}_1' + + class BrowserCore(RunnerCore): # note how many tests hang / do not send an output. if many of these # happen, likely something is broken and it is best to abort the test @@ -2632,10 +2642,13 @@ def browser_open(cls, url): # Running in parallel mode, give each browser its own profile dir. browser_data_dir += '-' + str(worker_id) - # Delete old browser data directory. If we cannot (the data dir is in use on Windows), - # switch to another dir. - while not force_delete_dir(browser_data_dir): - browser_data_dir += '-another' + # Delete old browser data directory. + if WINDOWS: + # If we cannot (the data dir is in use on Windows), switch to another dir. + while not force_delete_dir(browser_data_dir): + browser_data_dir = increment_suffix_number(browser_data_dir) + else: + force_delete_dir(browser_data_dir) # Recreate the new data directory. os.mkdir(browser_data_dir) From cee618b939782d9c6f2d26ff9bd3c74c3e276402 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jukka=20Jyl=C3=A4nki?= Date: Wed, 17 Sep 2025 02:05:35 +0300 Subject: [PATCH 07/18] Run Firefox browser tests on Windows. --- .circleci/config.yml | 60 ++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 60 insertions(+) diff --git a/.circleci/config.yml b/.circleci/config.yml index 0178d0cf4f91b..9598d75b1cca6 100644 --- a/.circleci/config.yml +++ b/.circleci/config.yml @@ -395,6 +395,61 @@ commands: # "Firefox is already running, but is not responding." # TODO: find out a way to shut down and restart firefox - upload-test-results + run-tests-firefox-windows: + description: "Runs emscripten tests under firefox on Windows" + parameters: + test_targets: + description: "Test suites to run" + type: string + title: + description: "Name of given test suite" + type: string + default: "" + steps: + - run: + name: download firefox + shell: powershell.exe -ExecutionPolicy Bypass + command: | + $nightlyUrl = "https://archive.mozilla.org/pub/firefox/nightly/latest-mozilla-central/firefox_latest-win64.zip" + $outZip = "$env:TEMP\firefox_nightly.zip" + Write-Host "Downloading Firefox Nightly from $nightlyUrl" + Invoke-WebRequest -Uri $nightlyUrl -OutFile $outZip + + $installDir = "C:\Program Files\FirefoxNightly" + if (Test-Path $installDir) { + Remove-Item -Recurse -Force $installDir + } + Expand-Archive -LiteralPath $outZip -DestinationPath $installDir + + $ffExe = Join-Path $installDir "firefox.exe" + [System.Environment]::SetEnvironmentVariable("EMTEST_BROWSER", $ffExe, "Machine") + Write-Host "EMTEST_BROWSER set to $env:EMTEST_BROWSER" + - run: + name: run tests (<< parameters.title >>) + environment: + EMTEST_LACKS_GRAPHICS_HARDWARE: "1" + # TODO(https://github.com/emscripten-core/emscripten/issues/24205) + EMTEST_LACKS_SOUND_HARDWARE: "1" + EMTEST_LACKS_WEBGPU: "1" + # OffscreenCanvas support is not yet done in Firefox. + EMTEST_LACKS_OFFSCREEN_CANVAS: "1" + EMTEST_DETECT_TEMPFILE_LEAKS: "0" + EMTEST_HEADLESS: "1" + EMTEST_CORES: "2" + DISPLAY: ":0" + command: | + # There are tests in the browser test suite that using libraries + # that are not included by "./embuilder build ALL". For example the + # PIC version of libSDL which is used by test_sdl2_misc_main_module + set EM_FROZEN_CACHE= + echo "-----" + echo "Running browser tests" + echo "-----" + test\runner << parameters.test_targets >> + # posix and emrun suites are disabled because firefox errors on + # "Firefox is already running, but is not responding." + # TODO: find out a way to shut down and restart firefox + - upload-test-results test-sockets-chrome: description: "Runs emscripten sockets tests under chrome" steps: @@ -1064,6 +1119,11 @@ jobs: - run-tests: title: "sockets.test_nodejs_sockets_echo*" test_targets: "sockets.test_nodejs_sockets_echo*" + - run-tests-firefox-windows: + title: "browser" + test_targets: " + browser.test_hello* + " - upload-test-results test-mac-arm64: From f4abd111b73e79f22ea8e9fc6e7f93d2144fd107 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jukka=20Jyl=C3=A4nki?= Date: Wed, 17 Sep 2025 02:06:45 +0300 Subject: [PATCH 08/18] Comment --- .circleci/config.yml | 1 + 1 file changed, 1 insertion(+) diff --git a/.circleci/config.yml b/.circleci/config.yml index 9598d75b1cca6..d19f12181492c 100644 --- a/.circleci/config.yml +++ b/.circleci/config.yml @@ -1119,6 +1119,7 @@ jobs: - run-tests: title: "sockets.test_nodejs_sockets_echo*" test_targets: "sockets.test_nodejs_sockets_echo*" + # Run a few browser tests as well. - run-tests-firefox-windows: title: "browser" test_targets: " From a12d00dcc89a746223aba1a5f46260435b5e3dd3 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jukka=20Jyl=C3=A4nki?= Date: Wed, 17 Sep 2025 02:08:09 +0300 Subject: [PATCH 09/18] Adjust title --- .circleci/config.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.circleci/config.yml b/.circleci/config.yml index d19f12181492c..f2365bae8b003 100644 --- a/.circleci/config.yml +++ b/.circleci/config.yml @@ -1121,7 +1121,7 @@ jobs: test_targets: "sockets.test_nodejs_sockets_echo*" # Run a few browser tests as well. - run-tests-firefox-windows: - title: "browser" + title: "browser on firefox on windows" test_targets: " browser.test_hello* " From 8c7b3d329e9a5ba4d09c86281d379c299fe22b32 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jukka=20Jyl=C3=A4nki?= Date: Wed, 17 Sep 2025 02:10:25 +0300 Subject: [PATCH 10/18] Install under user home --- .circleci/config.yml | 5 +---- 1 file changed, 1 insertion(+), 4 deletions(-) diff --git a/.circleci/config.yml b/.circleci/config.yml index f2365bae8b003..6b10083ec4a4e 100644 --- a/.circleci/config.yml +++ b/.circleci/config.yml @@ -415,10 +415,7 @@ commands: Write-Host "Downloading Firefox Nightly from $nightlyUrl" Invoke-WebRequest -Uri $nightlyUrl -OutFile $outZip - $installDir = "C:\Program Files\FirefoxNightly" - if (Test-Path $installDir) { - Remove-Item -Recurse -Force $installDir - } + $installDir = Join-Path $env:USERPROFILE "firefox" Expand-Archive -LiteralPath $outZip -DestinationPath $installDir $ffExe = Join-Path $installDir "firefox.exe" From 982174fdd12231745c362b03ed0840f2d22e2231 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jukka=20Jyl=C3=A4nki?= Date: Wed, 17 Sep 2025 02:50:11 +0300 Subject: [PATCH 11/18] Update download logic --- .circleci/config.yml | 22 +++++++++++++++++++--- 1 file changed, 19 insertions(+), 3 deletions(-) diff --git a/.circleci/config.yml b/.circleci/config.yml index 6b10083ec4a4e..abe13df7fe05a 100644 --- a/.circleci/config.yml +++ b/.circleci/config.yml @@ -410,16 +410,32 @@ commands: name: download firefox shell: powershell.exe -ExecutionPolicy Bypass command: | - $nightlyUrl = "https://archive.mozilla.org/pub/firefox/nightly/latest-mozilla-central/firefox_latest-win64.zip" + # To download Firefox, we must first figure out what the latest Firefox version name is. + # This is because there does not exist a stable/static URL to download latest Firefox from. + $html = Invoke-WebRequest "https://archive.mozilla.org/pub/firefox/nightly/latest-mozilla-central/" + $zipLink = $html.Links | + Where-Object { $_.href -match "firefox-.*\.en-US\.win64\.zip$" } | + Select-Object -Last 1 -ExpandProperty href + if (-not $zipLink) { + throw "Could not find Firefox Nightly ZIP!" + } + + # Download Win64 Firefox. + $nightlyUrl = "https://archive.mozilla.org$zipLink" $outZip = "$env:TEMP\firefox_nightly.zip" - Write-Host "Downloading Firefox Nightly from $nightlyUrl" + + Write-Host "Downloading latest Firefox Nightly: $nightlyUrl" Invoke-WebRequest -Uri $nightlyUrl -OutFile $outZip + # Extract to user home directory $installDir = Join-Path $env:USERPROFILE "firefox" + if (Test-Path $installDir) { Remove-Item -Recurse -Force $installDir } Expand-Archive -LiteralPath $outZip -DestinationPath $installDir + # Set environment variable for tests $ffExe = Join-Path $installDir "firefox.exe" - [System.Environment]::SetEnvironmentVariable("EMTEST_BROWSER", $ffExe, "Machine") + [System.Environment]::SetEnvironmentVariable("EMTEST_BROWSER", $ffExe, "User") + $env:EMTEST_BROWSER = $ffExe Write-Host "EMTEST_BROWSER set to $env:EMTEST_BROWSER" - run: name: run tests (<< parameters.title >>) From df587ca3b0c730ec4592090383724e46bed842fd Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jukka=20Jyl=C3=A4nki?= Date: Wed, 17 Sep 2025 03:23:18 +0300 Subject: [PATCH 12/18] Slash, also speed up test --- .circleci/config.yml | 13 +------------ 1 file changed, 1 insertion(+), 12 deletions(-) diff --git a/.circleci/config.yml b/.circleci/config.yml index abe13df7fe05a..44fc4bd89d8cb 100644 --- a/.circleci/config.yml +++ b/.circleci/config.yml @@ -416,9 +416,6 @@ commands: $zipLink = $html.Links | Where-Object { $_.href -match "firefox-.*\.en-US\.win64\.zip$" } | Select-Object -Last 1 -ExpandProperty href - if (-not $zipLink) { - throw "Could not find Firefox Nightly ZIP!" - } # Download Win64 Firefox. $nightlyUrl = "https://archive.mozilla.org$zipLink" @@ -458,7 +455,7 @@ commands: echo "-----" echo "Running browser tests" echo "-----" - test\runner << parameters.test_targets >> + test/runner << parameters.test_targets >> # posix and emrun suites are disabled because firefox errors on # "Firefox is already running, but is not responding." # TODO: find out a way to shut down and restart firefox @@ -1124,14 +1121,6 @@ jobs: - install-emsdk - pip-install: python: "$EMSDK_PYTHON" - - run-tests: - title: "crossplatform tests" - test_targets: "--crossplatform-only" - - upload-test-results - # Run a single websockify-based test to ensure it works on windows. - - run-tests: - title: "sockets.test_nodejs_sockets_echo*" - test_targets: "sockets.test_nodejs_sockets_echo*" # Run a few browser tests as well. - run-tests-firefox-windows: title: "browser on firefox on windows" From 46cd3938bae536953220b13656fe64fea64a6d55 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jukka=20Jyl=C3=A4nki?= Date: Wed, 17 Sep 2025 03:35:57 +0300 Subject: [PATCH 13/18] Add back tests --- .circleci/config.yml | 13 +++++++++++-- 1 file changed, 11 insertions(+), 2 deletions(-) diff --git a/.circleci/config.yml b/.circleci/config.yml index 44fc4bd89d8cb..26ae15263c7b4 100644 --- a/.circleci/config.yml +++ b/.circleci/config.yml @@ -1121,11 +1121,20 @@ jobs: - install-emsdk - pip-install: python: "$EMSDK_PYTHON" - # Run a few browser tests as well. + - run-tests: + title: "crossplatform tests" + test_targets: "--crossplatform-only" + - upload-test-results + # Run a single websockify-based test to ensure it works on windows. + - run-tests: + title: "sockets.test_nodejs_sockets_echo*" + test_targets: "sockets.test_nodejs_sockets_echo*" + - upload-test-results + # Run browser tests as well. - run-tests-firefox-windows: title: "browser on firefox on windows" test_targets: " - browser.test_hello* + browser " - upload-test-results From 82bb79b6c296487d45d880ed7f33fb402e377614 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jukka=20Jyl=C3=A4nki?= Date: Wed, 17 Sep 2025 11:20:14 +0300 Subject: [PATCH 14/18] Add skips --- .circleci/config.yml | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/.circleci/config.yml b/.circleci/config.yml index 26ae15263c7b4..e7e3995459e44 100644 --- a/.circleci/config.yml +++ b/.circleci/config.yml @@ -1133,8 +1133,12 @@ jobs: # Run browser tests as well. - run-tests-firefox-windows: title: "browser on firefox on windows" + # skip browser.test_glbook, as it requires UNIX tool 'make' + # skip browser.test_sdl2_mixer_wav_dash_l, fails to build on Windows test_targets: " browser + skip:browser.test_glbook + skip:browser.test_sdl2_mixer_wav_dash_l " - upload-test-results From 2210afa329ea047785c5e5c6e1da2d9e3a51abbe Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jukka=20Jyl=C3=A4nki?= Date: Wed, 17 Sep 2025 12:46:18 +0300 Subject: [PATCH 15/18] Remove stale comment --- .circleci/config.yml | 3 --- 1 file changed, 3 deletions(-) diff --git a/.circleci/config.yml b/.circleci/config.yml index e7e3995459e44..65f950d5df6d0 100644 --- a/.circleci/config.yml +++ b/.circleci/config.yml @@ -456,9 +456,6 @@ commands: echo "Running browser tests" echo "-----" test/runner << parameters.test_targets >> - # posix and emrun suites are disabled because firefox errors on - # "Firefox is already running, but is not responding." - # TODO: find out a way to shut down and restart firefox - upload-test-results test-sockets-chrome: description: "Runs emscripten sockets tests under chrome" From b4f114c76e34217e550db154f37f3d39d7f56cac Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jukka=20Jyl=C3=A4nki?= Date: Wed, 17 Sep 2025 12:48:00 +0300 Subject: [PATCH 16/18] Update comments --- .circleci/config.yml | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/.circleci/config.yml b/.circleci/config.yml index 65f950d5df6d0..607b891ead6f8 100644 --- a/.circleci/config.yml +++ b/.circleci/config.yml @@ -1130,8 +1130,10 @@ jobs: # Run browser tests as well. - run-tests-firefox-windows: title: "browser on firefox on windows" - # skip browser.test_glbook, as it requires UNIX tool 'make' + # skip browser.test_glbook, as it requires mingw32-make, which is not + # installed on CircleCI. # skip browser.test_sdl2_mixer_wav_dash_l, fails to build on Windows + # on CircleCI (works locally) test_targets: " browser skip:browser.test_glbook From aabbba6e4b6c85d6816c4acb07eb2fe98e5750b9 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jukka=20Jyl=C3=A4nki?= Date: Wed, 17 Sep 2025 21:45:05 +0300 Subject: [PATCH 17/18] Refactor --- test/common.py | 18 +++++++++++++++--- 1 file changed, 15 insertions(+), 3 deletions(-) diff --git a/test/common.py b/test/common.py index a50ca10466ab5..064c808befd67 100644 --- a/test/common.py +++ b/test/common.py @@ -2670,23 +2670,34 @@ def browser_open(cls, url): browser_args = shlex.split(browser_args) logger.info('Launching browser: %s', str(browser_args)) + if WINDOWS and is_firefox(): + cls.launch_browser_harness_windows_firefox(worker_id, config, browser_args, url) + else: + cls.browser_procs = [subprocess.Popen(browser_args + [url])] + + + @classmethod + def launch_browser_harness_windows_firefox(cls, worker_id, config, browser_args, url): + ''' Dedicated function for launching browser harness on Firefox on Windows, + which requires extra care for window positioning and process tracking.''' + with FileLock(browser_spawn_lock_filename) as count: # Firefox is a multiprocess browser. On Windows, killing the spawned # process will not bring down the whole browser, but only one browser tab. # So take a delta snapshot before->after spawning the browser to find # which subprocesses we launched. - if worker_id is not None and WINDOWS and is_firefox(): + if worker_id is not None: procs_before = list_processes_by_name(config.executable_name) cls.browser_procs = [subprocess.Popen(browser_args + [url])] # Give Firefox time to spawn its subprocesses. Use an increasing timeout # as a crude way to account for system load. - if worker_id is not None and WINDOWS and is_firefox(): + if worker_id is not None: time.sleep(2 + count * 0.3) procs_after = list_processes_by_name(config.executable_name) # Make sure that each browser window is visible on the desktop. Otherwise # browser might decide that the tab is backgrounded, and not load a test, # or it might not tick rAF()s forward, causing tests to hang. - if worker_id is not None and WINDOWS and is_firefox(): + if worker_id is not None and not EMTEST_HEADLESS: # On Firefox on Windows we needs to track subprocesses that got created # by Firefox. Other setups can use 'browser_proc' directly to terminate # the browser. @@ -2695,6 +2706,7 @@ def browser_open(cls, url): for proc in cls.browser_procs: move_browser_window(proc.pid, (300 + count * 47) % 1901, (10 + count * 37) % 997) + @classmethod def setUpClass(cls): super().setUpClass() From 4846cece4743a50e17f94d1fe804106261654c3a Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jukka=20Jyl=C3=A4nki?= Date: Wed, 17 Sep 2025 21:47:53 +0300 Subject: [PATCH 18/18] ruff --- test/common.py | 2 -- 1 file changed, 2 deletions(-) diff --git a/test/common.py b/test/common.py index 064c808befd67..7cfbb6abc06e0 100644 --- a/test/common.py +++ b/test/common.py @@ -2675,7 +2675,6 @@ def browser_open(cls, url): else: cls.browser_procs = [subprocess.Popen(browser_args + [url])] - @classmethod def launch_browser_harness_windows_firefox(cls, worker_id, config, browser_args, url): ''' Dedicated function for launching browser harness on Firefox on Windows, @@ -2706,7 +2705,6 @@ def launch_browser_harness_windows_firefox(cls, worker_id, config, browser_args, for proc in cls.browser_procs: move_browser_window(proc.pid, (300 + count * 47) % 1901, (10 + count * 37) % 997) - @classmethod def setUpClass(cls): super().setUpClass()