Skip to content
Merged
10 changes: 9 additions & 1 deletion emrun.py
Original file line number Diff line number Diff line change
Expand Up @@ -1768,7 +1768,7 @@ def run(args): # noqa: C901, PLR0912, PLR0915
if browser_exe == 'cmd':
url = url.replace('&', '^&')
url = url.replace('0.0.0.0', 'localhost')
browser += browser_args + [url]
browser += browser_args

if options.kill_start:
pname = processname_killed_atexit
Expand Down Expand Up @@ -1801,6 +1801,14 @@ def run(cmd):

browser += ['-no-remote', '--profile', profile_dir.replace('\\', '/')]

# Pass the URL to open as the very last item on the command line, and use the -url xxx parameter
# to open the url to work around https://bugzil.la/1996614.
if browser_exe and not options.android:
if 'firefox' in browser_exe:
browser += ['-url', url]
else:
browser += [url]

if options.system_info:
logi('Time of run: ' + time.strftime("%x %X"))
logi(get_system_info(format_json=options.json))
Expand Down
26 changes: 23 additions & 3 deletions test/browser_common.py
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@
import queue
import re
import shlex
import shutil
import subprocess
import threading
import time
Expand All @@ -35,9 +36,9 @@
)

from tools import feature_matrix, utils
from tools.feature_matrix import UNSUPPORTED
from tools.feature_matrix import OLDEST_SUPPORTED_FIREFOX, UNSUPPORTED
from tools.shared import DEBUG, EMCC, exit_with_error
from tools.utils import MACOS, WINDOWS, memoize, path_from_root, read_binary
from tools.utils import LINUX, MACOS, WINDOWS, memoize, path_from_root, read_binary

logger = logging.getLogger('common')

Expand Down Expand Up @@ -165,8 +166,22 @@ def get_safari_version():
def get_firefox_version():
if not is_firefox():
return UNSUPPORTED
exe_path = shlex.split(EMTEST_BROWSER)[0]
exe_path = shutil.which(shlex.split(EMTEST_BROWSER)[0])
ini_path = os.path.join(os.path.dirname(exe_path), '../Resources/platform.ini' if MACOS else 'platform.ini')
# On Linux, Firefox system installation uses a specific directory structure,
# where platform.ini is not located in same directory as the browser executable.
if LINUX and exe_path.startswith('/usr/bin/'):
def find_system_firefox_platform_ini():
for path in ['/usr/lib/firefox-esr/', '/usr/lib/firefox/']:
ini = os.path.join(path, 'platform.ini')
if os.path.isfile(ini):
return ini

ini_path = find_system_firefox_platform_ini()
if not ini_path:
logger.warning(f'Firefox browser detected in {EMTEST_BROWSER}, but could not find Firefox platform.ini to detect Firefox version. Assuming OLDEST_SUPPORTED_FIREFOX={OLDEST_SUPPORTED_FIREFOX}')
return OLDEST_SUPPORTED_FIREFOX

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Have you ever run into this issue?

Maybe we should just assert os.path.exists(init_path), 'unable to find firefox ini file' below before reading it?

@juj juj Apr 28, 2026

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yes, the CircleCI build-linux bot just ran into it.. that is why I added that.

It has /usr/bin/firefox but no platform.ini anywhere in sight.

On my own systems I do have platform.ini in /usr/lib/firefox-esr/platform.ini or /usr/lib/firefox/platform.ini

@juj juj Apr 28, 2026

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I debugged CircleCI with a search from /usr/ to find if there's a platform.ini somewhere, but that turned up nothing.

8b43aae#diff-eb5e6400ad6f92fec8556d07ec1d8d14915c79814e87f354c5ab65e6163f6ad1R176-R177

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The firefox we use in circleci installed from tar archive in EMTEST_BROWSER="$HOME/firefox/firefox"? Is that the one you mean?

@juj juj Apr 28, 2026

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This step: https://app.circleci.com/pipelines/github/emscripten-core/emscripten/50907/workflows/6696a39d-0750-4b09-9165-349d61aa099b/jobs/1184191

has /usr/bin/firefox (shutil.which('firefox') returned /usr/bin/firefox), but does not have /usr/.../platform.ini anywhere.

EMTEST_BROWSER: /usr/bin/firefox -new-instance -wait-for-browser
Searching for platform.ini
/usr/local/lib/python3.10/dist-packages/numpy/typing/tests/data/mypy.ini
/usr/local/lib/python3.10/dist-packages/numpy/_core/lib/npy-pkg-config/npymath.ini
/usr/local/lib/python3.10/dist-packages/numpy/_core/lib/npy-pkg-config/mlib.ini
Searching for platform.ini over
Traceback (most recent call last):
  File "/root/project/./test/runner.py", line 775, in <module>
    sys.exit(main())
  File "/root/project/./test/runner.py", line 732, in main
    log_test_environment()
  File "/root/project/./test/runner.py", line 655, in log_test_environment
    print(f'Firefox version: {browser_common.get_firefox_version()}')
  File "/root/project/test/browser_common.py", line 189, in get_firefox_version
    m = re.search(r"^Milestone=(.*)$", read_file(ini_path), re.MULTILINE)
  File "/root/project/tools/utils.py", line 157, in read_file
    with open(file_path, encoding='utf-8') as fh:
FileNotFoundError: [Errno 2] No such file or directory: '/usr/bin/platform.ini'

@juj juj Apr 28, 2026

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Now I see.. this task gives a clue: https://app.circleci.com/pipelines/github/emscripten-core/emscripten/50916/workflows/c9a01534-7402-4de2-aac7-028abe35de8e/jobs/1184421

It prints

Command '/usr/bin/firefox' requires the firefox snap to be installed.
Please install it with:

snap install firefox

so I wonder if there's a stub executable in /usr/bin/firefox present that prints out that message. So it is expected that there wouldn't exist any platform.ini on the system.

But now I'm puzzled a bit as to why this is detecting /usr/bin/firefox in the first place...

Oh that's because I wrote a silly typo.. fixed now.

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

That CI job should not have EMTEST_BROWSER set at all.. its not trying to run any browser tests.

I guess your new code is causing that error because its now finding the /usr/bin/firefox even though it doesn't needed to for this CI step.

My guess is that /usr/bin/firefox is a symlink to somewhere like /snap/..., so maybe following the symlinks would fix it.

Ideally we could fix that TODO since then it would be moot point, but I know it wasn't easy last time I tried to move that code around.

I'm happy to iterate on this stuff in followups if you want to land this change now.

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Thanks, yeah hopefully after this lands, the instability from emrun suite will be gone.


# Extract the first numeric part before any dot (e.g. "Milestone=102.15.1" → 102)
m = re.search(r"^Milestone=(.*)$", read_file(ini_path), re.MULTILINE)
milestone = m.group(1).strip()
Expand Down Expand Up @@ -319,6 +334,11 @@ def configure_test_browser():

if not EMTEST_BROWSER:
EMTEST_BROWSER = 'google-chrome'
if not shutil.which(EMTEST_BROWSER):
EMTEST_BROWSER = 'firefox'
if not shutil.which(EMTEST_BROWSER):
# FIXME: This should really be and error, but this code currently also runs for non-browser tests.
EMTEST_BROWSER = 'default-browser-not-found'

if WINDOWS and '"' not in EMTEST_BROWSER and "'" not in EMTEST_BROWSER:
# On Windows env. vars canonically use backslashes as directory delimiters, e.g.
Expand Down
2 changes: 1 addition & 1 deletion test/runner.py
Original file line number Diff line number Diff line change
Expand Up @@ -372,7 +372,7 @@ def sort_tests_failing_and_slowest_first_comparator(x, y):


def use_parallel_suite(module):
suite_supported = module.__name__ not in {'test_sanity', 'test_benchmark', 'test_sockets', 'test_interactive', 'test_stress'}
suite_supported = module.__name__ not in {'test_sanity', 'test_benchmark', 'test_sockets', 'test_interactive', 'test_stress', 'test_emrun'}
if not common.EMTEST_SAVE_DIR and not shared.DEBUG:
has_multiple_cores = parallel_testsuite.num_cores() > 1
if suite_supported and has_multiple_cores:
Expand Down
124 changes: 1 addition & 123 deletions test/test_browser.py
Original file line number Diff line number Diff line change
Expand Up @@ -3,11 +3,9 @@
# University of Illinois/NCSA Open Source License. Both these licenses can be
# found in the LICENSE file.

import argparse
import os
import random
import re
import shlex
import shutil
import struct
import subprocess
Expand All @@ -33,15 +31,12 @@
find_browser_test_file,
get_browser,
get_safari_version,
has_browser,
is_chrome,
is_firefox,
is_safari,
)
from common import (
EMRUN,
WEBIDL_BINDER,
RunnerCore,
copy_asset,
copytree,
create_file,
Expand Down Expand Up @@ -73,7 +68,7 @@
from tools import ports, shared, utils
from tools.feature_matrix import Feature
from tools.link import binary_encode
from tools.shared import EMCC, FILE_PACKAGER, PIPE
from tools.shared import EMCC, FILE_PACKAGER
from tools.utils import WINDOWS, delete_dir, write_binary, write_file


Expand Down Expand Up @@ -5708,123 +5703,6 @@ def test_shell_minimal(self, args):
self.btest_exit('browser_test_hello_world.c', cflags=['--shell-file', path_from_root('html/shell_minimal.html')] + args)


class emrun(RunnerCore):
def test_emrun_info(self):
if not has_browser():
self.skipTest('need a browser')
result = self.run_process([EMRUN, '--system-info', '--browser_info'], stdout=PIPE).stdout
assert 'CPU' in result
assert 'Browser' in result
assert 'Traceback' not in result

result = self.run_process([EMRUN, '--list-browsers'], stdout=PIPE).stdout
assert 'Traceback' not in result

def test_no_browser(self):
# Test --no-browser mode where we have to take care of launching the browser ourselves
# and then killing emrun when we are done.
if not has_browser():
self.skipTest('need a browser')

self.run_process([EMCC, test_file('test_emrun.c'), '--emrun', '-o', 'hello_world.html'])
proc = subprocess.Popen([EMRUN, '--no-browser', '.', '--port=3333'], stdout=PIPE)
try:
if get_browser():
url = 'http://localhost:3333/hello_world.html?argv0'
print(f'Starting browser to {url}')
BrowserCore.browser_open(url)
try:
while True:
stdout = proc.stdout.read()
if b'Dumping out file' in stdout:
break
finally:
print('Terminating browser')
BrowserCore.browser_terminate()
finally:
print('Terminating emrun server')
proc.terminate()
proc.wait()

def test_program_arg_separator(self):
# Verify that trying to pass argument to the page without the `--` separator will
# generate an actionable error message
err = self.expect_fail([EMRUN, '--foo'])
self.assertContained('error: unrecognized arguments: --foo', err)
self.assertContained('remember to add `--` between arguments', err)

@also_with_threads
def test_emrun(self):
self.emcc('test_emrun.c', ['--emrun', '-o', 'test_emrun.html'])
if not has_browser():
self.skipTest('need a browser')

# We cannot run emrun from the temp directory the suite will clean up afterwards, since the
# browser that is launched will have that directory as startup directory, and the browser will
# not close as part of the test, pinning down the cwd on Windows and it wouldn't be possible to
# delete it. Therefore switch away from that directory before launching.
os.chdir(path_from_root())

# emrun tests may run in parallel processes, so each test case should use a unique port number
# to avoid port address already in use errors.
port = '6939'
if '-pthread' in self.cflags:
port = '6940'

args_base = [EMRUN, '--timeout', '30', '--safe_firefox_profile',
'--kill-exit', '--port', port, '--verbose',
'--log-stdout', self.in_dir('stdout.txt'),
'--log-stderr', self.in_dir('stderr.txt')]

if get_browser() is not None:
# If EMTEST_BROWSER carried command line arguments to pass to the browser,
# (e.g. "firefox -profile /path/to/foo") those can't be passed via emrun,
# so strip them out.
browser_cmd = shlex.split(get_browser())
browser_path = browser_cmd[0]
args_base += ['--browser', browser_path]
if len(browser_cmd) > 1:
browser_args = browser_cmd[1:]
if 'firefox' in browser_path and ('-profile' in browser_args or '--profile' in browser_args):
# emrun uses its own -profile, strip it out
parser = argparse.ArgumentParser(add_help=False) # otherwise it throws with -headless
parser.add_argument('-profile')
parser.add_argument('--profile')
browser_args = parser.parse_known_args(browser_args)[1]
if browser_args:
args_base += ['--browser_args', ' ' + ' '.join(browser_args)]

for args in [
[],
['--port', '0'],
['--private_browsing'],
['--dump_out_directory', 'other dir/multiple'],
['--dump_out_directory=foo_bar'],
]:
args = args_base + args + [self.in_dir('test_emrun.html'), '--', '1', '2', '--3', 'escaped space', 'with_underscore']
print(shlex.join(args))
proc = self.run_process(args, check=False)
self.assertEqual(proc.returncode, 100)
dump_dir = 'dump_out'
if '--dump_out_directory' in args:
dump_dir = 'other dir/multiple'
elif '--dump_out_directory=foo_bar' in args:
dump_dir = 'foo_bar'
self.assertExists(self.in_dir(f'{dump_dir}/test.dat'))
self.assertExists(self.in_dir(f'{dump_dir}/heap.dat'))
self.assertExists(self.in_dir(f'{dump_dir}/nested/with space.dat'))
stdout = read_file(self.in_dir('stdout.txt'))
stderr = read_file(self.in_dir('stderr.txt'))
self.assertContained('argc: 6', stdout)
self.assertContained('argv[3]: --3', stdout)
self.assertContained('argv[4]: escaped space', stdout)
self.assertContained('argv[5]: with_underscore', stdout)
self.assertContained('Hello, world!', stdout)
self.assertContained('Testing ASCII characters: !"$%&\'()*+,-./:;<=>?@[\\]^_`{|}~', stdout)
self.assertContained('Testing char sequences: %20%21 &auml;', stdout)
self.assertContained('hello, error stream!', stderr)


class browser64(browser):
def setUp(self):
super().setUp()
Expand Down
132 changes: 132 additions & 0 deletions test/test_emrun.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,132 @@
# Copyright 2026 The Emscripten Authors. All rights reserved.
# Emscripten is available under two separate licenses, the MIT license and the
# University of Illinois/NCSA Open Source License. Both these licenses can be
# found in the LICENSE file.

import argparse
import os
import shlex
import subprocess

from browser_common import BrowserCore, get_browser, has_browser
from common import EMRUN, RunnerCore, path_from_root, read_file, test_file
from test_browser import also_with_threads

from tools.shared import EMCC, PIPE


Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The emrun class below is moved without changes.

class emrun(RunnerCore):
def test_emrun_info(self):
if not has_browser():
self.skipTest('need a browser')
result = self.run_process([EMRUN, '--system-info', '--browser_info'], stdout=PIPE).stdout
assert 'CPU' in result
assert 'Browser' in result
assert 'Traceback' not in result

result = self.run_process([EMRUN, '--list-browsers'], stdout=PIPE).stdout
assert 'Traceback' not in result

def test_no_browser(self):
# Test --no-browser mode where we have to take care of launching the browser ourselves
# and then killing emrun when we are done.
if not has_browser():
self.skipTest('need a browser')

self.run_process([EMCC, test_file('test_emrun.c'), '--emrun', '-o', 'hello_world.html'])
proc = subprocess.Popen([EMRUN, '--no-browser', '.', '--port=3333'], stdout=PIPE)
try:
if get_browser():
url = 'http://localhost:3333/hello_world.html?argv0'
print(f'Starting browser to {url}')
BrowserCore.browser_open(url)
try:
while True:
stdout = proc.stdout.read()
if b'Dumping out file' in stdout:
break
finally:
print('Terminating browser')
BrowserCore.browser_terminate()
finally:
print('Terminating emrun server')
proc.terminate()
proc.wait()

def test_program_arg_separator(self):
# Verify that trying to pass argument to the page without the `--` separator will
# generate an actionable error message
err = self.expect_fail([EMRUN, '--foo'])
self.assertContained('error: unrecognized arguments: --foo', err)
self.assertContained('remember to add `--` between arguments', err)

@also_with_threads
def test_emrun(self):
self.emcc('test_emrun.c', ['--emrun', '-o', 'test_emrun.html'])
if not has_browser():
self.skipTest('need a browser')

# We cannot run emrun from the temp directory the suite will clean up afterwards, since the
# browser that is launched will have that directory as startup directory, and the browser will
# not close as part of the test, pinning down the cwd on Windows and it wouldn't be possible to
# delete it. Therefore switch away from that directory before launching.
os.chdir(path_from_root())

# emrun tests may run in parallel processes, so each test case should use a unique port number
# to avoid port address already in use errors.
port = '6939'
if '-pthread' in self.cflags:
port = '6940'

args_base = [EMRUN, '--timeout', '30', '--safe_firefox_profile',
'--kill-exit', '--port', port, '--verbose',
'--log-stdout', self.in_dir('stdout.txt'),
'--log-stderr', self.in_dir('stderr.txt')]

if get_browser() is not None:
# If EMTEST_BROWSER carried command line arguments to pass to the browser,
# (e.g. "firefox -profile /path/to/foo") those can't be passed via emrun,
# so strip them out.
browser_cmd = shlex.split(get_browser())
browser_path = browser_cmd[0]
args_base += ['--browser', browser_path]
if len(browser_cmd) > 1:
browser_args = browser_cmd[1:]
if 'firefox' in browser_path and ('-profile' in browser_args or '--profile' in browser_args):
# emrun uses its own -profile, strip it out
parser = argparse.ArgumentParser(add_help=False) # otherwise it throws with -headless
parser.add_argument('-profile')
parser.add_argument('--profile')
browser_args = parser.parse_known_args(browser_args)[1]
if browser_args:
args_base += ['--browser_args', ' ' + ' '.join(browser_args)]

for args in [
[],
['--port', '0'],
['--private_browsing'],
['--dump_out_directory', 'other dir/multiple'],
['--dump_out_directory=foo_bar'],
]:
args = args_base + args + [self.in_dir('test_emrun.html'), '--', '1', '2', '--3', 'escaped space', 'with_underscore']
print(shlex.join(args))
proc = self.run_process(args, check=False)
self.assertEqual(proc.returncode, 100)
dump_dir = 'dump_out'
if '--dump_out_directory' in args:
dump_dir = 'other dir/multiple'
elif '--dump_out_directory=foo_bar' in args:
dump_dir = 'foo_bar'
self.assertExists(self.in_dir(f'{dump_dir}/test.dat'))
self.assertExists(self.in_dir(f'{dump_dir}/heap.dat'))
self.assertExists(self.in_dir(f'{dump_dir}/nested/with space.dat'))
stdout = read_file(self.in_dir('stdout.txt'))
stderr = read_file(self.in_dir('stderr.txt'))
self.assertContained('argc: 6', stdout)
self.assertContained('argv[3]: --3', stdout)
self.assertContained('argv[4]: escaped space', stdout)
self.assertContained('argv[5]: with_underscore', stdout)
self.assertContained('Hello, world!', stdout)
self.assertContained('Testing ASCII characters: !"$%&\'()*+,-./:;<=>?@[\\]^_`{|}~', stdout)
self.assertContained('Testing char sequences: %20%21 &auml;', stdout)
self.assertContained('hello, error stream!', stderr)

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Can you make the "move emrun into its own file" into separate PR?

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

(Seems reasonable to combine with adding it to list of non-parall suites, since that is the motivation for moving it).

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I'd prefer not, that would cause more churn at this point.

Loading