Skip to content

Commit 8bd4bec

Browse files
authored
Enable support for running the test harness in Safari. (#25499)
Add support for running the `browser` harness with `EMTEST_BROWSER=/Applications/Safari.App/Contents/MacOS/Safari` on a macOS system. This fixes both the singlethreaded and multithreaded harness runs.
1 parent 5fc27cc commit 8bd4bec

File tree

3 files changed

+100
-29
lines changed

3 files changed

+100
-29
lines changed

test/common.py

Lines changed: 77 additions & 26 deletions
Original file line numberDiff line numberDiff line change
@@ -52,6 +52,11 @@
5252
# used in CI. To use a custom start command specify the executable and command
5353
# line flags.
5454
#
55+
# Note that when specifying EMTEST_BROWSER to run tests on a Safari browser:
56+
# the command line must point to the root of the app bundle, and not to the
57+
# Safari executable inside the bundle. I.e. pass EMTEST_BROWSER=/Applications/Safari.app
58+
# instead of EMTEST_BROWSER=/Applications/Safari.app/Contents/MacOS/Safari
59+
#
5560
# There are two special values that can be used here if running in an actual
5661
# browser is not desired:
5762
# EMTEST_BROWSER=0 : This will disable the actual running of the test and simply
@@ -127,6 +132,21 @@ def configure(data_dir):
127132
shutil.copy(test_file('firefox_user.js'), os.path.join(data_dir, 'user.js'))
128133

129134

135+
class SafariConfig:
136+
default_flags = ('', )
137+
executable_name = 'Safari'
138+
# For the macOS 'open' command, pass
139+
# --new: to make a new Safari app be launched, rather than add a tab to an existing Safari process/window
140+
# --fresh: do not restore old tabs (e.g. if user had old navigated windows open)
141+
# --background: Open the new Safari window behind the current Terminal window, to make following the test run more pleasing (this is for convenience only)
142+
# -a <exe_name>: The path to the executable to open, in this case Safari
143+
launch_prefix = ('open', '--new', '--fresh', '--background', '-a')
144+
145+
@staticmethod
146+
def configure(data_dir):
147+
""" Safari has no special configuration step."""
148+
149+
130150
# Special value for passing to assert_returncode which means we expect that program
131151
# to fail with non-zero return code, but we don't care about specifically which one.
132152
NON_ZERO = -1
@@ -205,11 +225,17 @@ def is_firefox():
205225
return EMTEST_BROWSER and 'firefox' in EMTEST_BROWSER.lower()
206226

207227

228+
def is_safari():
229+
return EMTEST_BROWSER and 'safari' in EMTEST_BROWSER.lower()
230+
231+
208232
def get_browser_config():
209233
if is_chrome():
210234
return ChromeConfig()
211235
elif is_firefox():
212236
return FirefoxConfig()
237+
elif is_safari():
238+
return SafariConfig()
213239
return None
214240

215241

@@ -2216,7 +2242,12 @@ def end_headers(self):
22162242
self.send_header('Cross-Origin-Opener-Policy', 'same-origin')
22172243
self.send_header('Cross-Origin-Embedder-Policy', 'require-corp')
22182244
self.send_header('Cross-Origin-Resource-Policy', 'cross-origin')
2219-
self.send_header('Cache-Control', 'no-cache, no-store, must-revalidate')
2245+
2246+
self.send_header('Cache-Control', 'no-cache, no-store, must-revalidate, private, max-age=0')
2247+
self.send_header('Expires', '0')
2248+
self.send_header('Pragma', 'no-cache')
2249+
self.send_header('Vary', '*') # Safari insists on caching if this header is not present in addition to the above
2250+
22202251
return SimpleHTTPRequestHandler.end_headers(self)
22212252

22222253
def do_POST(self): # noqa: DC04
@@ -2303,9 +2334,13 @@ def do_GET(self):
23032334
raise Exception('browser harness error, excessive response to server - test must be fixed! "%s"' % self.path)
23042335
self.send_response(200)
23052336
self.send_header('Content-type', 'text/plain')
2306-
self.send_header('Cache-Control', 'no-cache, must-revalidate')
2337+
2338+
self.send_header('Cache-Control', 'no-cache, no-store, must-revalidate, private, max-age=0')
2339+
self.send_header('Expires', '0')
2340+
self.send_header('Pragma', 'no-cache')
2341+
self.send_header('Vary', '*') # Safari insists on caching if this header is not present in addition to the above
2342+
23072343
self.send_header('Connection', 'close')
2308-
self.send_header('Expires', '-1')
23092344
self.end_headers()
23102345
self.wfile.write(b'OK')
23112346

@@ -2558,11 +2593,17 @@ def browser_restart(cls):
25582593
def browser_open(cls, url):
25592594
assert has_browser()
25602595
browser_args = EMTEST_BROWSER
2596+
parallel_harness = worker_id is not None
2597+
2598+
config = get_browser_config()
2599+
if not config and EMTEST_BROWSER_AUTO_CONFIG:
2600+
exit_with_error(f'EMTEST_BROWSER_AUTO_CONFIG only currently works with firefox, chrome and safari. EMTEST_BROWSER was "{EMTEST_BROWSER}"')
25612601

2562-
if EMTEST_BROWSER_AUTO_CONFIG:
2602+
# Prepare the browser data directory, if it uses one.
2603+
if EMTEST_BROWSER_AUTO_CONFIG and config and hasattr(config, 'data_dir_flag'):
25632604
logger.info('Using default CI configuration.')
25642605
browser_data_dir = DEFAULT_BROWSER_DATA_DIR
2565-
if worker_id is not None:
2606+
if parallel_harness:
25662607
# Running in parallel mode, give each browser its own profile dir.
25672608
browser_data_dir += '-' + str(worker_id)
25682609

@@ -2577,49 +2618,58 @@ def browser_open(cls, url):
25772618
# Recreate the new data directory.
25782619
os.mkdir(browser_data_dir)
25792620

2580-
config = get_browser_config()
2581-
if not config:
2582-
exit_with_error(f'EMTEST_BROWSER_AUTO_CONFIG only currently works with firefox or chrome. EMTEST_BROWSER was "{EMTEST_BROWSER}"')
25832621
if WINDOWS:
25842622
# Escape directory delimiter backslashes for shlex.split.
25852623
browser_data_dir = browser_data_dir.replace('\\', '\\\\')
25862624
config.configure(browser_data_dir)
25872625
browser_args += f' {config.data_dir_flag}"{browser_data_dir}"'
25882626

25892627
browser_args = shlex.split(browser_args)
2628+
if hasattr(config, 'launch_prefix'):
2629+
browser_args = list(config.launch_prefix) + browser_args
2630+
25902631
logger.info('Launching browser: %s', str(browser_args))
25912632

2592-
if WINDOWS and is_firefox():
2593-
cls.launch_browser_harness_windows_firefox(worker_id, config, browser_args, url)
2633+
if (WINDOWS and is_firefox()) or is_safari():
2634+
cls.launch_browser_harness_with_proc_snapshot_workaround(parallel_harness, config, browser_args, url)
25942635
else:
25952636
cls.browser_procs = [subprocess.Popen(browser_args + [url])]
25962637

25972638
@classmethod
2598-
def launch_browser_harness_windows_firefox(cls, worker_id, config, browser_args, url):
2599-
''' Dedicated function for launching browser harness on Firefox on Windows,
2600-
which requires extra care for window positioning and process tracking.'''
2639+
def launch_browser_harness_with_proc_snapshot_workaround(cls, parallel_harness, config, browser_args, url):
2640+
''' Dedicated function for launching browser harness in scenarios where
2641+
we need to identify the launched browser processes via a before-after
2642+
subprocess snapshotting delta workaround.'''
26012643

2644+
# In order for this to work, each browser needs to be launched one at a time
2645+
# so that we know which process belongs to which browser.
26022646
with FileLock(browser_spawn_lock_filename) as count:
2603-
# Firefox is a multiprocess browser. On Windows, killing the spawned
2604-
# process will not bring down the whole browser, but only one browser tab.
2605-
# So take a delta snapshot before->after spawning the browser to find
2606-
# which subprocesses we launched.
2607-
if worker_id is not None:
2647+
# Take a snapshot before spawning the browser to find which processes
2648+
# existed before launching the browser.
2649+
if parallel_harness or is_safari():
26082650
procs_before = list_processes_by_name(config.executable_name)
2651+
2652+
# Browser launch
26092653
cls.browser_procs = [subprocess.Popen(browser_args + [url])]
2610-
# Give Firefox time to spawn its subprocesses. Use an increasing timeout
2611-
# as a crude way to account for system load.
2612-
if worker_id is not None:
2654+
2655+
# Give the browser time to spawn its subprocesses. Use an increasing
2656+
# timeout as a crude way to account for system load.
2657+
if parallel_harness or is_safari():
26132658
time.sleep(2 + count * 0.3)
26142659
procs_after = list_processes_by_name(config.executable_name)
2660+
2661+
# Take a snapshot again to find which processes exist after launching
2662+
# the browser. Then the newly launched browser processes are determined
2663+
# by the delta before->after.
2664+
cls.browser_procs = list(set(procs_after).difference(set(procs_before)))
2665+
if len(cls.browser_procs) == 0:
2666+
exit_with_error('Could not detect the launched browser subprocesses. The test harness will not be able to close the browser after testing is done, so aborting the test run here.')
2667+
2668+
# Firefox on Windows quirk:
26152669
# Make sure that each browser window is visible on the desktop. Otherwise
26162670
# browser might decide that the tab is backgrounded, and not load a test,
26172671
# or it might not tick rAF()s forward, causing tests to hang.
2618-
if worker_id is not None and not EMTEST_HEADLESS:
2619-
# On Firefox on Windows we needs to track subprocesses that got created
2620-
# by Firefox. Other setups can use 'browser_proc' directly to terminate
2621-
# the browser.
2622-
cls.browser_procs = list(set(procs_after).difference(set(procs_before)))
2672+
if WINDOWS and parallel_harness and not EMTEST_HEADLESS:
26232673
# Wrap window positions on a Full HD desktop area modulo primes.
26242674
for proc in cls.browser_procs:
26252675
move_browser_window(proc.pid, (300 + count * 47) % 1901, (10 + count * 37) % 997)
@@ -2693,6 +2743,7 @@ def run_browser(self, html_file, expected=None, message=None, timeout=None, extr
26932743
if DEBUG:
26942744
print('[browser launch:', html_file, ']')
26952745
assert not (message and expected), 'run_browser expects `expected` or `message`, but not both'
2746+
26962747
if expected is not None:
26972748
try:
26982749
self.harness_in_queue.put((

test/parallel_testsuite.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -36,7 +36,7 @@ def python_multiprocessing_structures_are_buggy():
3636
def cap_max_workers_in_pool(max_workers, is_browser):
3737
if is_browser and 'EMTEST_CORES' not in os.environ and 'EMCC_CORES' not in os.environ:
3838
# TODO experiment with this number. In browser tests we'll be creating
39-
# a chrome instance per worker which is expensive.
39+
# a browser instance per worker which is expensive.
4040
max_workers = max_workers // 2
4141
# Python has an issue that it can only use max 61 cores on Windows: https://github.com/python/cpython/issues/89240
4242
if WINDOWS:

test/test_browser.py

Lines changed: 22 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -18,7 +18,7 @@
1818
from urllib.request import urlopen
1919

2020
import common
21-
from common import BrowserCore, RunnerCore, path_from_root, has_browser, Reporting, is_chrome, is_firefox, CHROMIUM_BASED_BROWSERS
21+
from common import BrowserCore, RunnerCore, path_from_root, has_browser, Reporting, is_chrome, is_firefox, is_safari, CHROMIUM_BASED_BROWSERS
2222
from common import create_file, parameterized, ensure_dir, disabled, flaky, test_file, WEBIDL_BINDER
2323
from common import read_file, EMRUN, no_wasm64, no_2gb, no_4gb, copytree, skip_if, skip_if_simple
2424
from common import requires_wasm2js, parameterize, find_browser_test_file, with_all_sjlj
@@ -40,7 +40,12 @@ def sendheaders(s, extra=None, length=None):
4040
s.send_header("Content-Length", str(length))
4141
s.send_header("Access-Control-Allow-Origin", "http://localhost:%s" % port)
4242
s.send_header('Cross-Origin-Resource-Policy', 'cross-origin')
43-
s.send_header('Cache-Control', 'no-cache, no-store, must-revalidate')
43+
44+
s.send_header('Cache-Control', 'no-cache, no-store, must-revalidate, private, max-age=0')
45+
s.send_header('Expires', '0')
46+
s.send_header('Pragma', 'no-cache')
47+
s.send_header('Vary', '*') # Safari insists on caching if this header is not present in addition to the above
48+
4449
s.send_header("Access-Control-Expose-Headers", "Content-Length, Accept-Ranges")
4550
s.send_header("Content-type", "application/octet-stream")
4651
if support_byte_ranges:
@@ -142,6 +147,8 @@ def is_swiftshader(_):
142147

143148
no_firefox = skip_if('no_firefox', lambda _: is_firefox(), 'firefox is not supported')
144149

150+
no_safari = skip_if('no_safari', lambda _: is_safari(), 'safari is not supported')
151+
145152

146153
def is_jspi(args):
147154
return '-sASYNCIFY=2' in args
@@ -2797,6 +2804,7 @@ def test_webgl2_pbo(self):
27972804
self.btest_exit('webgl2_pbo.c', cflags=['-sMAX_WEBGL_VERSION=2', '-lGL'])
27982805

27992806
@no_firefox('fails on CI likely due to GPU drivers there')
2807+
@no_safari('TODO: Fails with report_result?5') # Safari 17.6 (17618.3.11.11.7, 17618)
28002808
@requires_graphics_hardware
28012809
def test_webgl2_sokol_mipmap(self):
28022810
self.reftest('third_party/sokol/mipmap-emsc.c', 'third_party/sokol/mipmap-emsc.png',
@@ -2955,6 +2963,7 @@ def test_sdl2_image_jpeg(self):
29552963
@also_with_wasmfs
29562964
@requires_graphics_hardware
29572965
@with_all_sjlj
2966+
@no_safari('Test enables Wasm exceptions, so will not run in Safari 17.6 (17618.3.11.11.7, 17618)') # Safari 17.6 (17618.3.11.11.7, 17618)
29582967
def test_sdl2_image_formats(self):
29592968
shutil.copy(test_file('screenshot.png'), '.')
29602969
shutil.copy(test_file('screenshot.jpg'), '.')
@@ -3356,6 +3365,7 @@ def test_async_returnvalue(self, args):
33563365
create_file('filey.txt', 'sync_tunnel\nsync_tunnel_bool\n')
33573366
self.btest('test_async_returnvalue.c', '0', cflags=['-sASSERTIONS', '-sASYNCIFY', '-sASYNCIFY_IGNORE_INDIRECT', '--js-library', test_file('browser/test_async_returnvalue.js')] + args)
33583367

3368+
@no_safari('TODO: Never reports a result, so times out') # Safari 17.6 (17618.3.11.11.7, 17618)
33593369
def test_async_bad_list(self):
33603370
self.btest('test_async_bad_list.c', '0', cflags=['-sASYNCIFY', '-sASYNCIFY_ONLY=waka', '--profiling'])
33613371

@@ -3408,6 +3418,7 @@ def test_modularize(self, args, code, opts):
34083418
self.run_browser('a.html', '/report_result?0')
34093419

34103420
@no_firefox('source phase imports not implemented yet in firefox')
3421+
@no_safari('croaks on line "import source wasmModule from \'./out.wasm\';"') # Safari 17.6 (17618.3.11.11.7, 17618)
34113422
def test_source_phase_imports(self):
34123423
self.compile_btest('browser_test_hello_world.c', ['-sEXPORT_ES6', '-sSOURCE_PHASE_IMPORTS', '-Wno-experimental', '-o', 'out.mjs'])
34133424
create_file('a.html', '''
@@ -3794,6 +3805,7 @@ def test_pthread_gcc_64bit_atomic_fetch_and_op(self):
37943805

37953806
# Test the old GCC atomic __sync_op_and_fetch builtin operations.
37963807
@also_with_wasm2js
3808+
@no_safari('TODO: browser.test_pthread_gcc_atomic_op_and_fetch_wasm2js fails with "abort:Assertion failed: nand_and_fetch_data == -1"') # Safari 17.6 (17618.3.11.11.7, 17618)
37973809
def test_pthread_gcc_atomic_op_and_fetch(self):
37983810
self.cflags += ['-Wno-sync-fetch-and-nand-semantics-changed']
37993811
self.btest_exit('pthread/test_pthread_gcc_atomic_op_and_fetch.c', cflags=['-O3', '-pthread', '-sPTHREAD_POOL_SIZE=8'])
@@ -3891,6 +3903,7 @@ def test_pthread_cleanup(self):
38913903
'': ([],),
38923904
'spinlock': (['-DSPINLOCK_TEST'],),
38933905
})
3906+
@no_safari('TODO: browser.test_pthread_mutex and browser.test_pthread_mutex_spinlock both hang Safari') # Safari 17.6 (17618.3.11.11.7, 17618)
38943907
def test_pthread_mutex(self, args):
38953908
self.btest_exit('pthread/test_pthread_mutex.c', cflags=['-O3', '-pthread', '-sPTHREAD_POOL_SIZE=8'] + args)
38963909

@@ -4078,6 +4091,7 @@ def test_pthread_safe_stack(self):
40784091
'no_leak': ['test_pthread_lsan_no_leak', []],
40794092
})
40804093
@no_firefox('https://github.com/emscripten-core/emscripten/issues/15978')
4094+
@no_safari('TODO: browser.test_pthread_lsan_leak fails with /report_result?0') # Safari 17.6 (17618.3.11.11.7, 17618)
40814095
def test_pthread_lsan(self, name, args):
40824096
self.btest(Path('pthread', name + '.cpp'), expected='1', cflags=['-fsanitize=leak', '-pthread', '-sPROXY_TO_PTHREAD', '--pre-js', test_file('pthread', name + '.js')] + args)
40834097

@@ -4089,6 +4103,7 @@ def test_pthread_lsan(self, name, args):
40894103
'leak': ['test_pthread_lsan_leak', ['-gsource-map']],
40904104
'no_leak': ['test_pthread_lsan_no_leak', []],
40914105
})
4106+
@no_safari('TODO: browser.test_pthread_asan_leak fails with /report_result?0') # Safari 17.6 (17618.3.11.11.7, 17618)
40924107
def test_pthread_asan(self, name, args):
40934108
self.btest(Path('pthread', name + '.cpp'), expected='1', cflags=['-fsanitize=address', '-pthread', '-sPROXY_TO_PTHREAD', '--pre-js', test_file('pthread', name + '.js')] + args)
40944109

@@ -4102,6 +4117,7 @@ def test_pthread_asan_use_after_free(self):
41024117
@no_2gb('ASAN + GLOBAL_BASE')
41034118
@no_4gb('ASAN + GLOBAL_BASE')
41044119
@no_firefox('https://github.com/emscripten-core/emscripten/issues/20006')
4120+
@no_safari('TODO: Hangs') # Safari Version 18.5 (20621.2.5.11.8)
41054121
@also_with_wasmfs
41064122
def test_pthread_asan_use_after_free_2(self):
41074123
# similiar to test_pthread_asan_use_after_free, but using a pool instead
@@ -4120,6 +4136,7 @@ def test_pthread_exit_process(self):
41204136
args += ['--pre-js', test_file('core/pthread/test_pthread_exit_runtime.pre.js')]
41214137
self.btest('core/pthread/test_pthread_exit_runtime.c', expected='onExit status: 42', cflags=args)
41224138

4139+
@no_safari('TODO: Fails with report_result?unexpected: [object ErrorEvent]') # Safari 17.6 (17618.3.11.11.7, 17618)
41234140
def test_pthread_trap(self):
41244141
create_file('pre.js', '''
41254142
if (typeof window === 'object' && window) {
@@ -5376,6 +5393,7 @@ def test_wasmfs_fetch_backend_threaded(self, args):
53765393
'jspi': (['-Wno-experimental', '-sASYNCIFY=2'],),
53775394
'jspi_wasm_bigint': (['-Wno-experimental', '-sASYNCIFY=2', '-sWASM_BIGINT'],),
53785395
})
5396+
@no_safari('TODO: Fails with abort:Assertion failed: err == 0') # Safari 17.6 (17618.3.11.11.7, 17618)
53795397
def test_wasmfs_opfs(self, args):
53805398
if '-sASYNCIFY=2' in args:
53815399
self.require_jspi()
@@ -5385,6 +5403,7 @@ def test_wasmfs_opfs(self, args):
53855403
self.btest_exit(test, cflags=args + ['-DWASMFS_RESUME'])
53865404

53875405
@no_firefox('no OPFS support yet')
5406+
@no_safari('TODO: Fails with exception:Did not get expected EIO when unlinking file') # Safari 17.6 (17618.3.11.11.7, 17618)
53885407
def test_wasmfs_opfs_errors(self):
53895408
test = test_file('wasmfs/wasmfs_opfs_errors.c')
53905409
postjs = test_file('wasmfs/wasmfs_opfs_errors_post.js')
@@ -5459,6 +5478,7 @@ def test_manual_pthread_proxy_hammer(self, args):
54595478
def test_assert_failure(self):
54605479
self.btest('test_assert_failure.c', 'abort:Assertion failed: false && "this is a test"')
54615480

5481+
@no_safari('TODO: Fails with report_result?exception:rejected! / undefined') # Safari 17.6 (17618.3.11.11.7, 17618)
54625482
def test_pthread_unhandledrejection(self):
54635483
# Check that an unhandled promise rejection is propagated to the main thread
54645484
# as an error. This test is failing if it hangs!

0 commit comments

Comments
 (0)