From bc28afab4a417f3fc7923075c2359a14bffd15b8 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jukka=20Jyl=C3=A4nki?= Date: Thu, 9 Oct 2025 03:10:11 +0300 Subject: [PATCH 1/3] Implement automatic test suite skipping facility into the browser test harness, so that users do not need to manually maintain skip lists of features. Paves the way toward test harness working for users out of the box. --- test/test_browser.py | 36 +++++++++++++++++++++++++++++------- tools/feature_matrix.py | 23 +++++++++++++++++++++++ 2 files changed, 52 insertions(+), 7 deletions(-) diff --git a/test/test_browser.py b/test/test_browser.py index a9f9b71d86be2..74baaecc3ffd9 100644 --- a/test/test_browser.py +++ b/test/test_browser.py @@ -28,7 +28,7 @@ from common import HttpServerThread, requires_dev_dependency from tools import shared from tools import ports -from tools.feature_matrix import UNSUPPORTED +from tools.feature_matrix import UNSUPPORTED, min_browser_versions, Feature from tools.shared import EMCC, WINDOWS, FILE_PACKAGER, PIPE, DEBUG from tools.utils import delete_dir, memoize @@ -158,6 +158,23 @@ def get_safari_version(): return parts[0] * 10000 + parts[1] * 100 + parts[2] +@memoize +def get_firefox_version(): + if not is_firefox(): + return UNSUPPORTED + exe_path = shlex.split(common.EMTEST_BROWSER)[0] + ini_path = os.path.join(os.path.dirname(exe_path), "platform.ini") + # Extract the first numeric part before any dot (e.g. "Milestone=102.15.1" → 102) + m = re.search(r"^Milestone=([^\n\r]+)", open(ini_path).read(), re.MULTILINE) + milestone = m.group(1).strip() + version = int(re.match(r"(\d+)", milestone).group(1)) + # On Nightly and BEta, e.g. 145.0a1, pretend it to still mean version 144, + # since it is a pre-release version + if any(c in milestone for c in ("a", "b")): + version -= 1 + return version + + no_swiftshader = skip_if_simple('not compatible with swiftshader', is_swiftshader) no_chrome = skip_if('no_chrome', lambda _: is_chrome(), 'chrome is not supported') @@ -214,11 +231,16 @@ def decorated(self, *args, **kwargs): def webgl2_disabled(): - return os.getenv('EMTEST_LACKS_WEBGL2') or os.getenv('EMTEST_LACKS_GRAPHICS_HARDWARE') + return os.getenv('EMTEST_LACKS_WEBGL2') or os.getenv('EMTEST_LACKS_GRAPHICS_HARDWARE') or current_browser_lacks_feature(Feature.WEBGL2) def webgpu_disabled(): - return os.getenv('EMTEST_LACKS_WEBGPU') or os.getenv('EMTEST_LACKS_GRAPHICS_HARDWARE') + return os.getenv('EMTEST_LACKS_WEBGPU') or os.getenv('EMTEST_LACKS_GRAPHICS_HARDWARE') or current_browser_lacks_feature(Feature.WEBGPU) + + +def current_browser_lacks_feature(feature): + min_required = min_browser_versions[feature] + return get_firefox_version() < min_required['firefox'] or get_safari_version() < min_required['safari'] requires_graphics_hardware = skipExecIf(os.getenv('EMTEST_LACKS_GRAPHICS_HARDWARE'), 'This test requires graphics hardware') @@ -226,13 +248,13 @@ def webgpu_disabled(): requires_webgpu = unittest.skipIf(webgpu_disabled(), "This test requires WebGPU to be available") requires_sound_hardware = skipExecIf(os.getenv('EMTEST_LACKS_SOUND_HARDWARE'), 'This test requires sound hardware') requires_microphone_access = skipExecIf(os.getenv('EMTEST_LACKS_MICROPHONE_ACCESS'), 'This test accesses microphone, which may need accepting a user prompt to enable it.') -requires_offscreen_canvas = unittest.skipIf(os.getenv('EMTEST_LACKS_OFFSCREEN_CANVAS'), 'This test requires a browser with OffscreenCanvas') -requires_es6_workers = unittest.skipIf(os.getenv('EMTEST_LACKS_ES6_WORKERS'), 'This test requires a browser with ES6 Module Workers support') -requires_growable_arraybuffers = unittest.skipIf(os.getenv('EMTEST_LACKS_GROWABLE_ARRAYBUFFERS'), 'This test requires a browser that supports growable ArrayBuffers') +requires_offscreen_canvas = unittest.skipIf(os.getenv('EMTEST_LACKS_OFFSCREEN_CANVAS') or current_browser_lacks_feature(Feature.OFFSCREENCANVAS_SUPPORT), 'This test requires a browser with OffscreenCanvas') +requires_es6_workers = unittest.skipIf(os.getenv('EMTEST_LACKS_ES6_WORKERS') or current_browser_lacks_feature(Feature.WORKER_ES6_MODULES), 'This test requires a browser with ES6 Module Workers support') +requires_growable_arraybuffers = unittest.skipIf(os.getenv('EMTEST_LACKS_GROWABLE_ARRAYBUFFERS') or current_browser_lacks_feature(Feature.GROWABLE_ARRAYBUFFERS), 'This test requires a browser that supports growable ArrayBuffers') # N.b. not all SharedArrayBuffer requiring tests are annotated with this decorator, since at this point there are so many of such tests. # As a middle ground, if a test has a name 'thread' or 'wasm_worker' in it, then it does not need decorating. To run all single-threaded tests in # the suite, one can run "EMTEST_LACKS_SHARED_ARRAY_BUFFER=1 test/runner browser skip:browser.test_*thread* skip:browser.test_*wasm_worker* skip:browser.test_*audio_worklet*" -requires_shared_array_buffer = unittest.skipIf(os.getenv('EMTEST_LACKS_SHARED_ARRAY_BUFFER'), 'This test requires a browser with SharedArrayBuffer support') +requires_shared_array_buffer = unittest.skipIf(os.getenv('EMTEST_LACKS_SHARED_ARRAY_BUFFER') or current_browser_lacks_feature(Feature.THREADS), 'This test requires a browser with SharedArrayBuffer support') class browser(BrowserCore): diff --git a/tools/feature_matrix.py b/tools/feature_matrix.py index c953922b8f5c9..a1e3d2c3258cc 100644 --- a/tools/feature_matrix.py +++ b/tools/feature_matrix.py @@ -42,6 +42,9 @@ class Feature(IntEnum): OFFSCREENCANVAS_SUPPORT = auto() WASM_LEGACY_EXCEPTIONS = auto() WASM_EXCEPTIONS = auto() + WEBGL2 = auto() + WEBGPU = auto() + GROWABLE_ARRAYBUFFERS = auto() disable_override_features = set() @@ -90,6 +93,18 @@ class Feature(IntEnum): 'safari': UNSUPPORTED, 'node': 230000, }, + Feature.WEBGL2: { + 'chrome': 56, + 'firefox': 51, + 'safari': 150000, + 'node': UNSUPPORTED, + }, + Feature.WEBGPU: { + 'chrome': 113, + 'firefox': 141, + 'safari': 260000, + 'node': UNSUPPORTED, + }, # https://caniuse.com/mdn-api_worker_worker_ecmascript_modules: The ability to # call new Worker(url, { type: 'module' }); Feature.WORKER_ES6_MODULES: { @@ -127,6 +142,14 @@ class Feature(IntEnum): # Node.js 26) 'node': 240000, }, + # Growable SharedArrayBuffers improves memory growth feature in multithreaded + # builds by avoiding need to poll resizes to ArrayBuffer views in Workers. + Feature.GROWABLE_ARRAYBUFFERS: { + 'chrome': 111, + 'firefox': 128, + 'safari': 160400, + 'node': 200000, + } } # Static assertion to check that we actually need each of the above feature flags From 3e966e5053f6444a158d209d5b2c68bf9ba45cea Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jukka=20Jyl=C3=A4nki?= Date: Thu, 9 Oct 2025 03:14:32 +0300 Subject: [PATCH 2/3] ruff --- tools/feature_matrix.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tools/feature_matrix.py b/tools/feature_matrix.py index a1e3d2c3258cc..b2aa9cf8e37a1 100644 --- a/tools/feature_matrix.py +++ b/tools/feature_matrix.py @@ -149,7 +149,7 @@ class Feature(IntEnum): 'firefox': 128, 'safari': 160400, 'node': 200000, - } + }, } # Static assertion to check that we actually need each of the above feature flags From 50b553eaa76dc7ceaa037f0c27c78bfa80732210 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jukka=20Jyl=C3=A4nki?= Date: Thu, 9 Oct 2025 13:26:31 +0300 Subject: [PATCH 3/3] Refactor the test skip auto-detect feature so that env. var. values EMTEST_LACKS_x=0 can be used to force-don't-skip tests. --- test/test_browser.py | 24 +++++++++++++----------- 1 file changed, 13 insertions(+), 11 deletions(-) diff --git a/test/test_browser.py b/test/test_browser.py index 74baaecc3ffd9..824bb5d37ecdc 100644 --- a/test/test_browser.py +++ b/test/test_browser.py @@ -230,17 +230,19 @@ def decorated(self, *args, **kwargs): return decorator -def webgl2_disabled(): - return os.getenv('EMTEST_LACKS_WEBGL2') or os.getenv('EMTEST_LACKS_GRAPHICS_HARDWARE') or current_browser_lacks_feature(Feature.WEBGL2) +def test_browser_should_skip_feature(skip_env_var, feature): + if os.getenv(skip_env_var) is None: + min_required = min_browser_versions[feature] + return get_firefox_version() < min_required['firefox'] or get_safari_version() < min_required['safari'] + return int(os.getenv(skip_env_var)) != 0 -def webgpu_disabled(): - return os.getenv('EMTEST_LACKS_WEBGPU') or os.getenv('EMTEST_LACKS_GRAPHICS_HARDWARE') or current_browser_lacks_feature(Feature.WEBGPU) +def webgl2_disabled(): + return os.getenv('EMTEST_LACKS_GRAPHICS_HARDWARE') or test_browser_should_skip_feature('EMTEST_LACKS_WEBGL2', Feature.WEBGL2) -def current_browser_lacks_feature(feature): - min_required = min_browser_versions[feature] - return get_firefox_version() < min_required['firefox'] or get_safari_version() < min_required['safari'] +def webgpu_disabled(): + return os.getenv('EMTEST_LACKS_GRAPHICS_HARDWARE') or test_browser_should_skip_feature('EMTEST_LACKS_WEBGPU', Feature.WEBGPU) requires_graphics_hardware = skipExecIf(os.getenv('EMTEST_LACKS_GRAPHICS_HARDWARE'), 'This test requires graphics hardware') @@ -248,13 +250,13 @@ def current_browser_lacks_feature(feature): requires_webgpu = unittest.skipIf(webgpu_disabled(), "This test requires WebGPU to be available") requires_sound_hardware = skipExecIf(os.getenv('EMTEST_LACKS_SOUND_HARDWARE'), 'This test requires sound hardware') requires_microphone_access = skipExecIf(os.getenv('EMTEST_LACKS_MICROPHONE_ACCESS'), 'This test accesses microphone, which may need accepting a user prompt to enable it.') -requires_offscreen_canvas = unittest.skipIf(os.getenv('EMTEST_LACKS_OFFSCREEN_CANVAS') or current_browser_lacks_feature(Feature.OFFSCREENCANVAS_SUPPORT), 'This test requires a browser with OffscreenCanvas') -requires_es6_workers = unittest.skipIf(os.getenv('EMTEST_LACKS_ES6_WORKERS') or current_browser_lacks_feature(Feature.WORKER_ES6_MODULES), 'This test requires a browser with ES6 Module Workers support') -requires_growable_arraybuffers = unittest.skipIf(os.getenv('EMTEST_LACKS_GROWABLE_ARRAYBUFFERS') or current_browser_lacks_feature(Feature.GROWABLE_ARRAYBUFFERS), 'This test requires a browser that supports growable ArrayBuffers') +requires_offscreen_canvas = unittest.skipIf(test_browser_should_skip_feature('EMTEST_LACKS_OFFSCREEN_CANVAS', Feature.OFFSCREENCANVAS_SUPPORT), 'This test requires a browser with OffscreenCanvas') +requires_es6_workers = unittest.skipIf(test_browser_should_skip_feature('EMTEST_LACKS_ES6_WORKERS', Feature.WORKER_ES6_MODULES), 'This test requires a browser with ES6 Module Workers support') +requires_growable_arraybuffers = unittest.skipIf(test_browser_should_skip_feature('EMTEST_LACKS_GROWABLE_ARRAYBUFFERS', Feature.GROWABLE_ARRAYBUFFERS), 'This test requires a browser that supports growable ArrayBuffers') # N.b. not all SharedArrayBuffer requiring tests are annotated with this decorator, since at this point there are so many of such tests. # As a middle ground, if a test has a name 'thread' or 'wasm_worker' in it, then it does not need decorating. To run all single-threaded tests in # the suite, one can run "EMTEST_LACKS_SHARED_ARRAY_BUFFER=1 test/runner browser skip:browser.test_*thread* skip:browser.test_*wasm_worker* skip:browser.test_*audio_worklet*" -requires_shared_array_buffer = unittest.skipIf(os.getenv('EMTEST_LACKS_SHARED_ARRAY_BUFFER') or current_browser_lacks_feature(Feature.THREADS), 'This test requires a browser with SharedArrayBuffer support') +requires_shared_array_buffer = unittest.skipIf(test_browser_should_skip_feature('EMTEST_LACKS_SHARED_ARRAY_BUFFER', Feature.THREADS), 'This test requires a browser with SharedArrayBuffer support') class browser(BrowserCore):