Skip to content

Commit 756dfdd

Browse files
authored
Implement automatic test suite skipping facility into the browser test harness (#25533)
Implement automatic test suite skipping facility into the browser test harness, so that users do not need to manually maintain skip lists of features. (or in present state, have to maintain fewer of them). Paves the way toward test harness working for users out of the box, improving First-Time User Experience, rather than have a large bulk of tests fail for users by default.
1 parent 4d16f75 commit 756dfdd

File tree

2 files changed

+98
-18
lines changed

2 files changed

+98
-18
lines changed

test/test_browser.py

Lines changed: 67 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -12,7 +12,6 @@
1212
import shutil
1313
import subprocess
1414
import time
15-
import unittest
1615
import zlib
1716
from functools import wraps
1817
from http.server import (
@@ -65,7 +64,7 @@
6564
)
6665

6766
from tools import ports, shared
68-
from tools.feature_matrix import UNSUPPORTED
67+
from tools.feature_matrix import UNSUPPORTED, Feature, min_browser_versions
6968
from tools.shared import DEBUG, EMCC, FILE_PACKAGER, PIPE, WINDOWS
7069
from tools.utils import delete_dir, memoize
7170

@@ -195,6 +194,23 @@ def get_safari_version():
195194
return parts[0] * 10000 + parts[1] * 100 + parts[2]
196195

197196

197+
@memoize
198+
def get_firefox_version():
199+
if not is_firefox():
200+
return UNSUPPORTED
201+
exe_path = shlex.split(common.EMTEST_BROWSER)[0]
202+
ini_path = os.path.join(os.path.dirname(exe_path), "platform.ini")
203+
# Extract the first numeric part before any dot (e.g. "Milestone=102.15.1" → 102)
204+
m = re.search(r"^Milestone=(.*)$", open(ini_path).read(), re.MULTILINE)
205+
milestone = m.group(1).strip()
206+
version = int(re.match(r"(\d+)", milestone).group(1))
207+
# On Nightly and Beta, e.g. 145.0a1, pretend it to still mean version 144,
208+
# since it is a pre-release version
209+
if any(c in milestone for c in ('a', 'b')):
210+
version -= 1
211+
return version
212+
213+
198214
no_swiftshader = skip_if_simple('not compatible with swiftshader', is_swiftshader)
199215

200216
no_chrome = skip_if('no_chrome', lambda _: is_chrome(), 'chrome is not supported')
@@ -235,13 +251,50 @@ def decorated(self, threads, *args, **kwargs):
235251
return decorated
236252

237253

238-
def skipExecIf(cond, message):
254+
def browser_should_skip_feature(skip_env_var, feature):
255+
# If an env. var. EMTEST_LACKS_x to skip the given test is set (to either
256+
# value 0 or 1), don't bother checking if current browser supports the feature
257+
# - just unconditionally run the test, or skip the test.
258+
if os.getenv(skip_env_var) is not None:
259+
return int(os.getenv(skip_env_var)) != 0
260+
261+
# If there is no Feature object associated with this capability, then we
262+
# should run the test.
263+
if feature is None:
264+
return False
265+
266+
# If EMTEST_AUTOSKIP=0, also never skip.
267+
if os.getenv('EMTEST_AUTOSKIP') == '0':
268+
return False
269+
270+
# Otherwise EMTEST_AUTOSKIP=1 or EMTEST_AUTOSKIP is not set: check whether
271+
# the current browser supports the test or not.
272+
min_required = min_browser_versions[feature]
273+
not_supported = get_firefox_version() < min_required['firefox'] or get_safari_version() < min_required['safari']
274+
275+
# Current browser does not support the test, and EMTEST_AUTOSKIP is not set?
276+
# Then error out to have end user decide what to do in this situation.
277+
if not_supported and os.getenv('EMTEST_AUTOSKIP') is None:
278+
return 'error'
279+
280+
# Report whether to skip the test based on browser support.
281+
return not_supported
282+
283+
284+
def skipIfFeatureNotAvailable(skip_env_var, feature, message):
285+
for env_var in skip_env_var if type(skip_env_var) == list else [skip_env_var]:
286+
should_skip = browser_should_skip_feature(env_var, feature)
287+
if should_skip:
288+
break
289+
239290
def decorator(f):
240291
assert callable(f)
241292

242293
@wraps(f)
243294
def decorated(self, *args, **kwargs):
244-
if cond:
295+
if should_skip == 'error':
296+
raise Exception(f'This test requires a browser that supports {feature.name} but your browser {common.EMTEST_BROWSER} does not support this. Run with {skip_env_var}=1 or EMTEST_AUTOSKIP=1 to skip this test automatically.')
297+
elif should_skip:
245298
self.skip_exec = message
246299
f(self, *args, **kwargs)
247300

@@ -251,25 +304,21 @@ def decorated(self, *args, **kwargs):
251304

252305

253306
def webgl2_disabled():
254-
return os.getenv('EMTEST_LACKS_WEBGL2') or os.getenv('EMTEST_LACKS_GRAPHICS_HARDWARE')
255-
256-
257-
def webgpu_disabled():
258-
return os.getenv('EMTEST_LACKS_WEBGPU') or os.getenv('EMTEST_LACKS_GRAPHICS_HARDWARE')
307+
return browser_should_skip_feature('EMTEST_LACKS_WEBGL2', Feature.WEBGL2) or browser_should_skip_feature('EMTEST_LACKS_GRAPHICS_HARDWARE', Feature.WEBGL2)
259308

260309

261-
requires_graphics_hardware = skipExecIf(os.getenv('EMTEST_LACKS_GRAPHICS_HARDWARE'), 'This test requires graphics hardware')
262-
requires_webgl2 = unittest.skipIf(webgl2_disabled(), "This test requires WebGL2 to be available")
263-
requires_webgpu = unittest.skipIf(webgpu_disabled(), "This test requires WebGPU to be available")
264-
requires_sound_hardware = skipExecIf(os.getenv('EMTEST_LACKS_SOUND_HARDWARE'), 'This test requires sound hardware')
265-
requires_microphone_access = skipExecIf(os.getenv('EMTEST_LACKS_MICROPHONE_ACCESS'), 'This test accesses microphone, which may need accepting a user prompt to enable it.')
266-
requires_offscreen_canvas = unittest.skipIf(os.getenv('EMTEST_LACKS_OFFSCREEN_CANVAS'), 'This test requires a browser with OffscreenCanvas')
267-
requires_es6_workers = unittest.skipIf(os.getenv('EMTEST_LACKS_ES6_WORKERS'), 'This test requires a browser with ES6 Module Workers support')
268-
requires_growable_arraybuffers = unittest.skipIf(os.getenv('EMTEST_LACKS_GROWABLE_ARRAYBUFFERS'), 'This test requires a browser that supports growable ArrayBuffers')
310+
requires_graphics_hardware = skipIfFeatureNotAvailable('EMTEST_LACKS_GRAPHICS_HARDWARE', None, 'This test requires graphics hardware')
311+
requires_webgl2 = skipIfFeatureNotAvailable(['EMTEST_LACKS_WEBGL2', 'EMTEST_LACKS_GRAPHICS_HARDWARE'], Feature.WEBGL2, 'This test requires WebGL2 to be available')
312+
requires_webgpu = skipIfFeatureNotAvailable(['EMTEST_LACKS_WEBGPU', 'EMTEST_LACKS_GRAPHICS_HARDWARE'], Feature.WEBGPU, 'This test requires WebGPU to be available')
313+
requires_sound_hardware = skipIfFeatureNotAvailable('EMTEST_LACKS_SOUND_HARDWARE', None, 'This test requires sound hardware')
314+
requires_microphone_access = skipIfFeatureNotAvailable('EMTEST_LACKS_MICROPHONE_ACCESS', None, 'This test accesses microphone, which may need accepting a user prompt to enable it.')
315+
requires_offscreen_canvas = skipIfFeatureNotAvailable('EMTEST_LACKS_OFFSCREEN_CANVAS', Feature.OFFSCREENCANVAS_SUPPORT, 'This test requires a browser with OffscreenCanvas')
316+
requires_es6_workers = skipIfFeatureNotAvailable('EMTEST_LACKS_ES6_WORKERS', Feature.WORKER_ES6_MODULES, 'This test requires a browser with ES6 Module Workers support')
317+
requires_growable_arraybuffers = skipIfFeatureNotAvailable('EMTEST_LACKS_GROWABLE_ARRAYBUFFERS', Feature.GROWABLE_ARRAYBUFFERS, 'This test requires a browser that supports growable ArrayBuffers')
269318
# N.b. not all SharedArrayBuffer requiring tests are annotated with this decorator, since at this point there are so many of such tests.
270319
# 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
271320
# 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*"
272-
requires_shared_array_buffer = unittest.skipIf(os.getenv('EMTEST_LACKS_SHARED_ARRAY_BUFFER'), 'This test requires a browser with SharedArrayBuffer support')
321+
requires_shared_array_buffer = skipIfFeatureNotAvailable('EMTEST_LACKS_SHARED_ARRAY_BUFFER', Feature.THREADS, 'This test requires a browser with SharedArrayBuffer support')
273322

274323

275324
class browser(BrowserCore):

tools/feature_matrix.py

Lines changed: 31 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -43,6 +43,9 @@ class Feature(IntEnum):
4343
OFFSCREENCANVAS_SUPPORT = auto()
4444
WASM_LEGACY_EXCEPTIONS = auto()
4545
WASM_EXCEPTIONS = auto()
46+
WEBGL2 = auto()
47+
WEBGPU = auto()
48+
GROWABLE_ARRAYBUFFERS = auto()
4649

4750

4851
disable_override_features = set()
@@ -97,6 +100,24 @@ class Feature(IntEnum):
97100
'safari': UNSUPPORTED,
98101
'node': 230000,
99102
},
103+
# Emscripten itself does not use this feature but we use it in our browser
104+
# tests.
105+
Feature.WEBGL2: {
106+
'chrome': 56,
107+
'firefox': 51,
108+
'safari': 150000,
109+
'node': UNSUPPORTED,
110+
},
111+
# Emscripten itself does not use this feature but we use it in our browser
112+
# tests.
113+
Feature.WEBGPU: {
114+
'chrome': 113,
115+
'firefox': 141,
116+
'safari': 260000,
117+
'node': UNSUPPORTED,
118+
},
119+
# Emscripten itself does not use this feature but we use it in our browser
120+
# tests.
100121
# https://caniuse.com/mdn-api_worker_worker_ecmascript_modules: The ability to
101122
# call new Worker(url, { type: 'module' });
102123
Feature.WORKER_ES6_MODULES: {
@@ -134,6 +155,16 @@ class Feature(IntEnum):
134155
# Node.js 26)
135156
'node': 240000,
136157
},
158+
# Growable SharedArrayBuffers improves memory growth feature in multithreaded
159+
# builds by avoiding need to poll resizes to ArrayBuffer views in Workers.
160+
# This feature is not used anywhere else except the test harness to detect
161+
# browser version.
162+
Feature.GROWABLE_ARRAYBUFFERS: {
163+
'chrome': 136,
164+
'firefox': 145,
165+
'safari': UNSUPPORTED,
166+
'node': 240000,
167+
},
137168
}
138169

139170
# Static assertion to check that we actually need each of the above feature flags

0 commit comments

Comments
 (0)