Book a Demo!
CoCalc Logo Icon
StoreFeaturesDocsShareSupportNewsAboutPoliciesSign UpSign In
emscripten-core
GitHub Repository: emscripten-core/emscripten
Path: blob/main/test/common.py
6183 views
1
# Copyright 2021 The Emscripten Authors. All rights reserved.
2
# Emscripten is available under two separate licenses, the MIT license and the
3
# University of Illinois/NCSA Open Source License. Both these licenses can be
4
# found in the LICENSE file.
5
6
import contextlib
7
import difflib
8
import hashlib
9
import io
10
import json
11
import logging
12
import os
13
import re
14
import shlex
15
import shutil
16
import stat
17
import string
18
import subprocess
19
import sys
20
import tempfile
21
import textwrap
22
from functools import wraps
23
from pathlib import Path
24
from subprocess import PIPE, STDOUT
25
26
import clang_native
27
import jsrun
28
import line_endings
29
from retryable_unittest import RetryableTestCase
30
31
from tools import building, config, feature_matrix, shared, utils
32
from tools.feature_matrix import Feature
33
from tools.settings import COMPILE_TIME_SETTINGS
34
from tools.shared import DEBUG, EMCC, EMXX, get_canonical_temp_dir
35
from tools.utils import (
36
WINDOWS,
37
exe_path_from_root,
38
path_from_root,
39
read_binary,
40
read_file,
41
write_binary,
42
)
43
44
logger = logging.getLogger('common')
45
46
# If we are drawing a parallel swimlane graph of test output, we need to use a temp
47
# file to track which tests were flaky so they can be graphed in orange color to
48
# visually stand out.
49
flaky_tests_log_filename = os.path.join(path_from_root('out/flaky_tests.txt'))
50
51
EMTEST_DETECT_TEMPFILE_LEAKS = None
52
EMTEST_SAVE_DIR = None
53
# generally js engines are equivalent, testing 1 is enough. set this
54
# to force testing on all js engines, good to find js engine bugs
55
EMTEST_ALL_ENGINES = None
56
EMTEST_SKIP_SLOW = None
57
EMTEST_SKIP_FLAKY = None
58
EMTEST_RETRY_FLAKY = None
59
EMTEST_LACKS_NATIVE_CLANG = None
60
EMTEST_VERBOSE = None
61
EMTEST_REBASELINE = None
62
63
# Special value for passing to assert_returncode which means we expect that program
64
# to fail with non-zero return code, but we don't care about specifically which one.
65
NON_ZERO = -1
66
67
TEST_ROOT = path_from_root('test')
68
LAST_TEST = path_from_root('out/last_test.txt')
69
PREVIOUS_TEST_RUN_RESULTS_FILE = path_from_root('out/previous_test_run_results.json')
70
71
WEBIDL_BINDER = exe_path_from_root('tools/webidl_binder')
72
73
EMBUILDER = exe_path_from_root('embuilder')
74
EMMAKE = exe_path_from_root('emmake')
75
EMCMAKE = exe_path_from_root('emcmake')
76
EMCONFIGURE = exe_path_from_root('emconfigure')
77
EMRUN = exe_path_from_root('emrun')
78
WASM_DIS = os.path.join(building.get_binaryen_bin(), 'wasm-dis')
79
LLVM_OBJDUMP = shared.llvm_tool_path('llvm-objdump')
80
PYTHON = sys.executable
81
82
assert config.NODE_JS # assert for mypy's benefit
83
# By default we run the tests in the same version of node as emscripten itself used.
84
if not config.NODE_JS_TEST:
85
config.NODE_JS_TEST = config.NODE_JS
86
# The default set of JS_ENGINES contains just node.
87
if not config.JS_ENGINES:
88
config.JS_ENGINES = [config.NODE_JS_TEST]
89
90
91
def errlog(*args):
92
"""Shorthand for print with file=sys.stderr
93
94
Use this for all internal test framework logging..
95
"""
96
print(*args, file=sys.stderr)
97
98
99
def load_previous_test_run_results():
100
try:
101
return json.load(open(PREVIOUS_TEST_RUN_RESULTS_FILE))
102
except FileNotFoundError:
103
return {}
104
105
106
def test_file(*path_components):
107
"""Construct a path relative to the emscripten "tests" directory."""
108
return str(Path(TEST_ROOT, *path_components))
109
110
111
def maybe_test_file(filename):
112
if not os.path.exists(filename) and os.path.exists(test_file(filename)):
113
filename = test_file(filename)
114
return filename
115
116
117
def copytree(src, dest):
118
shutil.copytree(src, dest, dirs_exist_ok=True)
119
120
121
def exe_suffix(cmd):
122
return cmd + '.exe' if WINDOWS else cmd
123
124
125
def compiler_for(filename, force_c=False):
126
if utils.suffix(filename) in ('.cc', '.cxx', '.cpp') and not force_c:
127
return EMXX
128
else:
129
return EMCC
130
131
132
def record_flaky_test(test_name, attempt_count, max_attempts, exception_msg):
133
logger.info(f'Retrying flaky test "{test_name}" (attempt {attempt_count}/{max_attempts} failed):\n{exception_msg}')
134
open(flaky_tests_log_filename, 'a').write(f'{test_name}\n')
135
136
137
def node_bigint_flags(node_version):
138
# The --experimental-wasm-bigint flag was added in v12, and then removed (enabled by default)
139
# in v16.
140
if node_version and node_version < (16, 0, 0):
141
return ['--experimental-wasm-bigint']
142
else:
143
return []
144
145
146
@contextlib.contextmanager
147
def env_modify(updates):
148
"""A context manager that updates os.environ."""
149
# This could also be done with mock.patch.dict() but taking a dependency
150
# on the mock library is probably not worth the benefit.
151
old_env = os.environ.copy()
152
print("env_modify: " + str(updates))
153
# Setting a value to None means clear the environment variable
154
clears = [key for key, value in updates.items() if value is None]
155
updates = {key: value for key, value in updates.items() if value is not None}
156
os.environ.update(updates)
157
for key in clears:
158
if key in os.environ:
159
del os.environ[key]
160
try:
161
yield
162
finally:
163
os.environ.clear()
164
os.environ.update(old_env)
165
166
167
def ensure_dir(dirname):
168
dirname = Path(dirname)
169
dirname.mkdir(parents=True, exist_ok=True)
170
171
172
def limit_size(string):
173
maxbytes = 800000 * 20
174
if sys.stdout.isatty():
175
maxlines = 500
176
max_line = 500
177
else:
178
max_line = 5000
179
maxlines = 1000
180
lines = string.splitlines()
181
for i, line in enumerate(lines):
182
if len(line) > max_line:
183
lines[i] = line[:max_line] + '[..]'
184
if len(lines) > maxlines:
185
lines = lines[0:maxlines // 2] + ['[..]'] + lines[-maxlines // 2:]
186
lines.append('(not all output shown. See `limit_size`)')
187
string = '\n'.join(lines) + '\n'
188
if len(string) > maxbytes:
189
string = string[0:maxbytes // 2] + '\n[..]\n' + string[-maxbytes // 2:]
190
return string
191
192
193
def create_file(name, contents, binary=False, absolute=False):
194
name = Path(name)
195
assert absolute or not name.is_absolute(), name
196
if binary:
197
name.write_bytes(contents)
198
else:
199
# Dedent the contents of text files so that the files on disc all
200
# start in column 1, even if they are indented when embedded in the
201
# python test code.
202
contents = textwrap.dedent(contents)
203
name.write_text(contents, encoding='utf-8')
204
205
206
@contextlib.contextmanager
207
def chdir(dir):
208
"""A context manager that performs actions in the given directory."""
209
orig_cwd = os.getcwd()
210
os.chdir(dir)
211
try:
212
yield
213
finally:
214
os.chdir(orig_cwd)
215
216
217
def make_executable(name):
218
Path(name).chmod(stat.S_IREAD | stat.S_IWRITE | stat.S_IEXEC)
219
220
221
def make_dir_writeable(dirname):
222
# Some tests make files and subdirectories read-only, so rmtree/unlink will not delete
223
# them. Force-make everything writable in the subdirectory to make it
224
# removable and re-attempt.
225
os.chmod(dirname, 0o777)
226
227
for directory, subdirs, files in os.walk(dirname):
228
for item in files + subdirs:
229
i = os.path.join(directory, item)
230
if not os.path.islink(i):
231
os.chmod(i, 0o777)
232
233
234
def force_delete_dir(dirname):
235
"""Deletes a directory. Returns whether deletion succeeded."""
236
if not os.path.exists(dirname):
237
return True
238
assert not os.path.isfile(dirname)
239
240
try:
241
make_dir_writeable(dirname)
242
utils.delete_dir(dirname)
243
except PermissionError as e:
244
# This issue currently occurs on Windows when running browser tests e.g.
245
# on Firefox browser. Killing Firefox browser is not 100% watertight, and
246
# occasionally a Firefox browser process can be left behind, holding on
247
# to a file handle, preventing the deletion from succeeding.
248
# We expect this issue to only occur on Windows.
249
if not WINDOWS:
250
raise e
251
print(f'Warning: Failed to delete directory "{dirname}"\n{e}')
252
return False
253
return True
254
255
256
def force_delete_contents(dirname):
257
make_dir_writeable(dirname)
258
utils.delete_contents(dirname)
259
260
261
def get_output_suffix(args):
262
if any(a in args for a in ('-sEXPORT_ES6', '-sWASM_ESM_INTEGRATION', '-sMODULARIZE=instance')):
263
return '.mjs'
264
else:
265
return '.js'
266
267
268
def match_engine_executable(engine, name):
269
assert type(engine) is list
270
basename = os.path.basename(engine[0])
271
return name in basename
272
273
274
def engine_is_node(engine):
275
assert type(engine) is list
276
return match_engine_executable(engine, 'node')
277
278
279
def engine_is_v8(engine):
280
assert type(engine) is list
281
return match_engine_executable(engine, 'd8') or match_engine_executable(engine, 'v8')
282
283
284
def engine_is_deno(engine):
285
assert type(engine) is list
286
return match_engine_executable(engine, 'deno')
287
288
289
def engine_is_bun(engine):
290
assert type(engine) is list
291
return match_engine_executable(engine, 'bun')
292
293
294
def get_engine(predicate):
295
"""Return engine that satifies predicate, if one is configured, otherwise None"""
296
for engine in config.JS_ENGINES:
297
if predicate(engine):
298
return engine
299
return None
300
301
302
def get_nodejs():
303
return get_engine(engine_is_node)
304
305
306
def get_v8():
307
return get_engine(engine_is_v8)
308
309
310
def get_bun():
311
return get_engine(engine_is_bun)
312
313
314
class RunnerMeta(type):
315
@classmethod
316
def make_test(mcs, name, func, suffix, args):
317
"""
318
This is a helper function to create new test functions for each parameterized form.
319
320
:param name: the original name of the function
321
:param func: the original function that we are parameterizing
322
:param suffix: the suffix to append to the name of the function for this parameterization
323
:param args: the positional arguments to pass to the original function for this parameterization
324
:returns: a tuple of (new_function_name, new_function_object)
325
"""
326
327
# Create the new test function. It calls the original function with the specified args.
328
# We use @functools.wraps to copy over all the function attributes.
329
@wraps(func)
330
def resulting_test(self):
331
return func(self, *args)
332
333
# Add suffix to the function name so that it displays correctly.
334
if suffix:
335
resulting_test.__name__ = f'{name}_{suffix}'
336
else:
337
resulting_test.__name__ = name
338
339
# On python 3, functions have __qualname__ as well. This is a full dot-separated path to the
340
# function. We add the suffix to it as well.
341
resulting_test.__qualname__ = f'{func.__qualname__}_{suffix}'
342
343
return resulting_test.__name__, resulting_test
344
345
def __new__(mcs, name, bases, attrs):
346
# This metaclass expands parameterized methods from `attrs` into separate ones in `new_attrs`.
347
new_attrs = {}
348
349
for attr_name, value in attrs.items():
350
# Check if a member of the new class has _parameterize, the tag inserted by @parameterized.
351
if hasattr(value, '_parameterize'):
352
# If it does, we extract the parameterization information, build new test functions.
353
for suffix, args in value._parameterize.items():
354
new_name, func = mcs.make_test(attr_name, value, suffix, args)
355
assert new_name not in new_attrs, 'Duplicate attribute name generated when parameterizing %s' % attr_name
356
new_attrs[new_name] = func
357
else:
358
# If not, we just copy it over to new_attrs verbatim.
359
assert attr_name not in new_attrs, '%s collided with an attribute from parameterization' % attr_name
360
new_attrs[attr_name] = value
361
362
# We invoke type, the default metaclass, to actually create the new class, with new_attrs.
363
return type.__new__(mcs, name, bases, new_attrs)
364
365
366
class RunnerCore(RetryableTestCase, metaclass=RunnerMeta):
367
# default temporary directory settings. set_temp_dir may be called later to
368
# override these
369
temp_dir = shared.TEMP_DIR
370
canonical_temp_dir = get_canonical_temp_dir(shared.TEMP_DIR)
371
372
# This avoids cluttering the test runner output, which is stderr too, with compiler warnings etc.
373
# Change this to None to get stderr reporting, for debugging purposes
374
stderr_redirect = STDOUT
375
376
library_cache: dict[str, tuple[str, object]] = {}
377
378
def is_wasm(self):
379
return self.get_setting('WASM') != 0
380
381
def is_wasm2js(self):
382
return not self.is_wasm()
383
384
def is_browser_test(self):
385
return False
386
387
def is_wasm64(self):
388
return self.get_setting('MEMORY64')
389
390
def is_4gb(self):
391
return self.get_setting('INITIAL_MEMORY') == '4200mb'
392
393
def is_2gb(self):
394
return self.get_setting('INITIAL_MEMORY') == '2200mb'
395
396
def check_dylink(self):
397
if self.get_setting('WASM_ESM_INTEGRATION'):
398
self.skipTest('dynamic linking not supported with WASM_ESM_INTEGRATION')
399
if '-lllvmlibc' in self.cflags:
400
self.skipTest('dynamic linking not supported with llvm-libc')
401
if self.is_wasm2js():
402
self.skipTest('dynamic linking not supported with wasm2js')
403
# MEMORY64=2 mode doesn't currently support dynamic linking because
404
# The side modules are lowered to wasm32 when they are built, making
405
# them unlinkable with wasm64 binaries.
406
if self.get_setting('MEMORY64') == 2:
407
self.skipTest('dynamic linking not supported with MEMORY64=2')
408
409
def require_pthreads(self):
410
self.cflags += ['-Wno-pthreads-mem-growth', '-pthread']
411
if self.get_setting('MINIMAL_RUNTIME'):
412
self.skipTest('non-browser pthreads not yet supported with MINIMAL_RUNTIME')
413
for engine in self.js_engines:
414
if engine_is_node(engine):
415
self.require_node()
416
nodejs = get_nodejs()
417
self.node_args += shared.node_pthread_flags(nodejs)
418
return
419
elif engine_is_bun(engine) or engine_is_deno(engine):
420
self.require_engine(engine)
421
return
422
self.fail('no JS engine found capable of running pthreads')
423
424
def require_v8(self):
425
if 'EMTEST_SKIP_V8' in os.environ:
426
self.skipTest('test requires v8 and EMTEST_SKIP_V8 is set')
427
v8 = get_v8()
428
if not v8:
429
self.fail('d8 required to run this test. Use EMTEST_SKIP_V8 to skip')
430
self.require_engine(v8)
431
self.cflags.append('-sENVIRONMENT=shell')
432
433
def require_node(self):
434
if 'EMTEST_SKIP_NODE' in os.environ:
435
self.skipTest('test requires node and EMTEST_SKIP_NODE is set')
436
nodejs = get_nodejs() or get_bun()
437
if not nodejs:
438
self.fail('node required to run this test. Use EMTEST_SKIP_NODE to skip')
439
self.require_engine(nodejs)
440
return nodejs
441
442
def require_node_25(self):
443
if 'EMTEST_SKIP_NODE_25' in os.environ:
444
self.skipTest('test requires node v25 and EMTEST_SKIP_NODE_25 is set')
445
nodejs = get_nodejs()
446
if not nodejs:
447
self.skipTest('Test requires nodejs to run')
448
if not self.try_require_node_version(25, 0, 0):
449
self.fail('node v25 required to run this test. Use EMTEST_SKIP_NODE_25 to skip')
450
451
def require_engine(self, engine, force=False):
452
logger.debug(f'require_engine: {engine}')
453
if not force and self.required_engine and self.required_engine != engine:
454
self.fail(f'test requires `{engine}` but `{self.required_engine}` was previously required')
455
self.required_engine = engine
456
self.js_engines = [engine]
457
self.wasm_engines = []
458
459
def require_wasm64(self):
460
if 'EMTEST_SKIP_WASM64' in os.environ:
461
self.skipTest('test requires node >= 24 or d8 (and EMTEST_SKIP_WASM64 is set)')
462
if self.is_browser_test():
463
return
464
465
if self.try_require_node_version(24):
466
return
467
468
v8 = get_v8()
469
if v8:
470
self.cflags.append('-sENVIRONMENT=shell')
471
self.require_engine(v8)
472
return
473
474
self.fail('either d8 or node >= 24 required to run wasm64 tests. Use EMTEST_SKIP_WASM64 to skip')
475
476
def try_require_node_version(self, major, minor = 0, revision = 0):
477
nodejs = get_nodejs()
478
if not nodejs:
479
self.skipTest('Test requires nodejs to run')
480
version = shared.get_node_version(nodejs)
481
if version < (major, minor, revision):
482
return False
483
484
self.require_engine(nodejs)
485
return True
486
487
def require_simd(self):
488
if 'EMTEST_SKIP_SIMD' in os.environ:
489
self.skipTest('test requires node >= 16 or d8 (and EMTEST_SKIP_SIMD is set)')
490
if self.is_browser_test():
491
return
492
493
if self.try_require_node_version(16):
494
return
495
496
v8 = get_v8()
497
if v8:
498
self.cflags.append('-sENVIRONMENT=shell')
499
self.require_engine(v8)
500
return
501
502
self.fail('either d8 or node >= 16 required to run wasm64 tests. Use EMTEST_SKIP_SIMD to skip')
503
504
def require_wasm_legacy_eh(self):
505
if 'EMTEST_SKIP_WASM_LEGACY_EH' in os.environ:
506
self.skipTest('test requires node >= 17 or d8 (and EMTEST_SKIP_WASM_LEGACY_EH is set)')
507
self.set_setting('WASM_LEGACY_EXCEPTIONS')
508
509
if self.is_browser_test():
510
self.check_browser_feature('EMTEST_SKIP_WASM_LEGACY_EH', Feature.WASM_LEGACY_EXCEPTIONS, 'test requires Wasm Legacy EH')
511
return
512
513
if self.try_require_node_version(17):
514
return
515
516
v8 = get_v8()
517
if v8:
518
self.cflags.append('-sENVIRONMENT=shell')
519
self.require_engine(v8)
520
return
521
522
self.fail('either d8 or node >= 17 required to run legacy wasm-eh tests. Use EMTEST_SKIP_WASM_LEGACY_EH to skip')
523
524
def require_wasm_eh(self):
525
if 'EMTEST_SKIP_WASM_EH' in os.environ:
526
self.skipTest('test requires node v24 or d8 (and EMTEST_SKIP_WASM_EH is set)')
527
self.set_setting('WASM_LEGACY_EXCEPTIONS', 0)
528
529
if self.is_browser_test():
530
self.check_browser_feature('EMTEST_SKIP_WASM_EH', Feature.WASM_EXCEPTIONS, 'test requires Wasm EH')
531
return
532
533
if self.try_require_node_version(22):
534
self.node_args.append('--experimental-wasm-exnref')
535
return
536
537
v8 = get_v8()
538
if v8:
539
self.cflags.append('-sENVIRONMENT=shell')
540
self.require_engine(v8)
541
self.v8_args.append('--experimental-wasm-exnref')
542
return
543
544
self.fail('either d8 or node v24 required to run wasm-eh tests. Use EMTEST_SKIP_WASM_EH to skip')
545
546
def require_jspi(self):
547
if 'EMTEST_SKIP_JSPI' in os.environ:
548
self.skipTest('skipping JSPI (EMTEST_SKIP_JSPI is set)')
549
# emcc warns about stack switching being experimental, and we build with
550
# warnings-as-errors, so disable that warning
551
self.cflags += ['-Wno-experimental']
552
self.set_setting('JSPI')
553
if self.is_wasm2js():
554
self.skipTest('JSPI is not currently supported for WASM2JS')
555
if self.get_setting('WASM_ESM_INTEGRATION'):
556
self.skipTest('WASM_ESM_INTEGRATION is not compatible with JSPI')
557
558
if self.is_browser_test():
559
return
560
561
# Support for JSPI came earlier than 22, but the new API changes require v24
562
if self.try_require_node_version(24):
563
self.node_args += ['--experimental-wasm-stack-switching']
564
return
565
566
v8 = get_v8()
567
if v8:
568
self.cflags.append('-sENVIRONMENT=shell')
569
self.require_engine(v8)
570
return
571
572
self.fail('either d8 or node v24 required to run JSPI tests. Use EMTEST_SKIP_JSPI to skip')
573
574
def require_wasm2js(self):
575
if self.is_wasm64():
576
self.skipTest('wasm2js is not compatible with MEMORY64')
577
if self.is_2gb() or self.is_4gb():
578
self.skipTest('wasm2js does not support over 2gb of memory')
579
if self.get_setting('WASM_ESM_INTEGRATION'):
580
self.skipTest('wasm2js is not compatible with WASM_ESM_INTEGRATION')
581
582
def setup_nodefs_test(self):
583
self.require_node()
584
if self.get_setting('WASMFS'):
585
# without this the JS setup code in setup_nodefs.js doesn't work
586
self.set_setting('FORCE_FILESYSTEM')
587
self.cflags += ['-DNODEFS', '-lnodefs.js', '--pre-js', test_file('setup_nodefs.js'), '-sINCOMING_MODULE_JS_API=onRuntimeInitialized']
588
589
def setup_noderawfs_test(self):
590
self.require_node()
591
self.cflags += ['-DNODERAWFS']
592
self.set_setting('NODERAWFS')
593
594
def setup_wasmfs_test(self):
595
self.set_setting('WASMFS')
596
self.cflags += ['-DWASMFS']
597
598
def set_temp_dir(self, temp_dir):
599
self.temp_dir = temp_dir
600
self.canonical_temp_dir = get_canonical_temp_dir(self.temp_dir)
601
# Explicitly set dedicated temporary directory for parallel tests
602
os.environ['EMCC_TEMP_DIR'] = self.temp_dir
603
604
def parse_wasm(self, filename):
605
wat = self.get_wasm_text(filename)
606
imports = []
607
exports = []
608
funcs = []
609
for line in wat.splitlines():
610
line = line.strip()
611
if line.startswith('(import '):
612
line = line.strip('()')
613
parts = line.split()
614
module = parts[1].strip('"')
615
name = parts[2].strip('"')
616
imports.append('%s.%s' % (module, name))
617
if line.startswith('(export '):
618
line = line.strip('()')
619
name = line.split()[1].strip('"')
620
exports.append(name)
621
if line.startswith('(func '):
622
line = line.strip('()')
623
name = line.split()[1].strip('"')
624
funcs.append(name)
625
return imports, exports, funcs
626
627
def output_name(self, basename):
628
suffix = get_output_suffix(self.get_cflags())
629
return basename + suffix
630
631
@classmethod
632
def setUpClass(cls):
633
super().setUpClass()
634
shared.check_sanity(force=True, quiet=True)
635
636
def setUp(self):
637
super().setUp()
638
self.js_engines = config.JS_ENGINES.copy()
639
self.settings_mods = {}
640
self.skip_exec = None
641
self.flaky = False
642
self.cflags = ['-Wclosure', '-Werror', '-Wno-limited-postlink-optimizations']
643
# TODO(https://github.com/emscripten-core/emscripten/issues/11121)
644
# For historical reasons emcc compiles and links as C++ by default.
645
# However we want to run our tests in a more strict manner. We can
646
# remove this if the issue above is ever fixed.
647
self.set_setting('NO_DEFAULT_TO_CXX')
648
self.ldflags = []
649
# Increase the stack trace limit to maximise usefulness of test failure reports.
650
# Also, include backtrace for all uncaught exceptions (not just Error).
651
self.node_args = ['--stack-trace-limit=50', '--trace-uncaught']
652
self.spidermonkey_args = ['-w']
653
654
nodejs = get_nodejs()
655
if nodejs:
656
node_version = shared.get_node_version(nodejs)
657
if node_version < (13, 0, 0):
658
self.node_args.append('--unhandled-rejections=strict')
659
elif node_version < (15, 0, 0):
660
# Opt in to node v15 default behaviour:
661
# https://nodejs.org/api/cli.html#cli_unhandled_rejections_mode
662
self.node_args.append('--unhandled-rejections=throw')
663
self.node_args += node_bigint_flags(node_version)
664
665
# If the version we are running tests in is lower than the version that
666
# emcc targets then we need to tell emcc to target that older version.
667
emcc_min_node_version_str = str(shared.settings.MIN_NODE_VERSION)
668
emcc_min_node_version = (
669
int(emcc_min_node_version_str[0:2]),
670
int(emcc_min_node_version_str[2:4]),
671
int(emcc_min_node_version_str[4:6]),
672
)
673
if node_version < emcc_min_node_version:
674
self.cflags.append('-sMIN_NODE_VERSION=%02d%02d%02d' % node_version)
675
self.cflags.append('-Wno-transpile')
676
677
# This allows much of the test suite to be run on older versions of node that don't
678
# support wasm bigint integration
679
if node_version[0] < feature_matrix.min_browser_versions[feature_matrix.Feature.JS_BIGINT_INTEGRATION]['node'] / 10000:
680
self.cflags.append('-sWASM_BIGINT=0')
681
682
self.v8_args = ['--wasm-staging']
683
self.env = {}
684
self.temp_files_before_run = []
685
self.required_engine = None
686
self.wasm_engines = config.WASM_ENGINES.copy()
687
self.use_all_engines = EMTEST_ALL_ENGINES
688
engine = self.get_current_js_engine()
689
if not engine_is_node(engine) and not engine_is_bun(engine):
690
# If our current JS engine a "shell" environment we need to explicitly enable support for
691
# it in ENVIRONMENT.
692
default_envs = 'web,webview,worker,node'
693
self.set_setting('ENVIRONMENT', default_envs + ',shell')
694
695
if EMTEST_DETECT_TEMPFILE_LEAKS:
696
for root, dirnames, filenames in os.walk(self.temp_dir):
697
for dirname in dirnames:
698
self.temp_files_before_run.append(os.path.normpath(os.path.join(root, dirname)))
699
for filename in filenames:
700
self.temp_files_before_run.append(os.path.normpath(os.path.join(root, filename)))
701
702
if self.runningInParallel():
703
self.working_dir = tempfile.mkdtemp(prefix='emscripten_test_' + self.__class__.__name__ + '_', dir=self.temp_dir)
704
else:
705
self.working_dir = path_from_root('out/test')
706
if os.path.exists(self.working_dir):
707
if EMTEST_SAVE_DIR == 2:
708
print('Not clearing existing test directory')
709
else:
710
logger.debug('Clearing existing test directory: %s', self.working_dir)
711
# Even when --save-dir is used we still try to start with an empty directory as many tests
712
# expect this. --no-clean can be used to keep the old contents for the new test
713
# run. This can be useful when iterating on a given test with extra files you want to keep
714
# around in the output directory.
715
force_delete_contents(self.working_dir)
716
else:
717
logger.debug('Creating new test output directory: %s', self.working_dir)
718
ensure_dir(self.working_dir)
719
utils.write_file(LAST_TEST, self.id() + '\n')
720
os.chdir(self.working_dir)
721
722
def runningInParallel(self):
723
return getattr(self, 'is_parallel', False)
724
725
def tearDown(self):
726
if self.runningInParallel() and not EMTEST_SAVE_DIR:
727
# rmtree() fails on Windows if the current working directory is inside the tree.
728
os.chdir(os.path.dirname(self.get_dir()))
729
force_delete_dir(self.get_dir())
730
731
if EMTEST_DETECT_TEMPFILE_LEAKS:
732
temp_files_after_run = []
733
for root, dirnames, filenames in os.walk(self.temp_dir):
734
for dirname in dirnames:
735
temp_files_after_run.append(os.path.normpath(os.path.join(root, dirname)))
736
for filename in filenames:
737
temp_files_after_run.append(os.path.normpath(os.path.join(root, filename)))
738
739
# Our leak detection will pick up *any* new temp files in the temp dir.
740
# They may not be due to us, but e.g. the browser when running browser
741
# tests. Until we figure out a proper solution, ignore some temp file
742
# names that we see on our CI infrastructure.
743
ignorable_file_prefixes = [
744
'/tmp/tmpaddon',
745
'/tmp/circleci-no-output-timeout',
746
'/tmp/wasmer',
747
]
748
749
left_over_files = set(temp_files_after_run) - set(self.temp_files_before_run)
750
left_over_files = [f for f in left_over_files if not any(f.startswith(p) for p in ignorable_file_prefixes)]
751
if left_over_files:
752
errlog(f'ERROR: After running test, there are {len(left_over_files)} new temporary files/directories left behind:')
753
for f in left_over_files:
754
errlog('leaked file: ', f)
755
self.fail(f'Test leaked {len(left_over_files)} temporary files!')
756
757
def get_setting(self, key, default=None):
758
return self.settings_mods.get(key, default)
759
760
def set_setting(self, key, value=1):
761
if value is None:
762
self.clear_setting(key)
763
if type(value) is bool:
764
value = int(value)
765
self.settings_mods[key] = value
766
767
def has_changed_setting(self, key):
768
return key in self.settings_mods
769
770
def clear_setting(self, key):
771
self.settings_mods.pop(key, None)
772
773
def serialize_settings(self, compile_only=False):
774
ret = []
775
for key, value in self.settings_mods.items():
776
if compile_only and key not in COMPILE_TIME_SETTINGS:
777
continue
778
if value == 1:
779
ret.append(f'-s{key}')
780
elif type(value) is list:
781
ret.append(f'-s{key}={",".join(value)}')
782
else:
783
ret.append(f'-s{key}={value}')
784
return ret
785
786
def get_dir(self):
787
return self.working_dir
788
789
def in_dir(self, *pathelems):
790
return os.path.join(self.get_dir(), *pathelems)
791
792
def add_pre_run(self, code):
793
assert not self.get_setting('MINIMAL_RUNTIME')
794
create_file('prerun.js', 'Module.preRun = function() { %s }\n' % code)
795
self.cflags += ['--pre-js', 'prerun.js', '-sINCOMING_MODULE_JS_API=preRun']
796
797
def add_post_run(self, code):
798
assert not self.get_setting('MINIMAL_RUNTIME')
799
create_file('postrun.js', 'Module.postRun = function() { %s }\n' % code)
800
self.cflags += ['--pre-js', 'postrun.js', '-sINCOMING_MODULE_JS_API=postRun']
801
802
def add_on_exit(self, code):
803
assert not self.get_setting('MINIMAL_RUNTIME')
804
create_file('onexit.js', 'Module.onExit = function() { %s }\n' % code)
805
self.cflags += ['--pre-js', 'onexit.js', '-sINCOMING_MODULE_JS_API=onExit']
806
807
# returns the full list of arguments to pass to emcc
808
# param @main_file whether this is the main file of the test. some arguments
809
# (like --pre-js) do not need to be passed when building
810
# libraries, for example
811
def get_cflags(self, main_file=False, compile_only=False, asm_only=False):
812
def is_ldflag(f):
813
return f.startswith('-l') or any(f.startswith(s) for s in ['-sEXPORT_ES6', '-sGL_TESTING', '-sPROXY_TO_PTHREAD', '-sENVIRONMENT=', '--pre-js=', '--post-js=', '-sPTHREAD_POOL_SIZE='])
814
815
args = self.serialize_settings(compile_only or asm_only) + self.cflags
816
if asm_only:
817
args = [a for a in args if not a.startswith('-O')]
818
if compile_only or asm_only:
819
args = [a for a in args if not is_ldflag(a)]
820
else:
821
args += self.ldflags
822
if not main_file:
823
for i, arg in enumerate(args):
824
if arg in ('--pre-js', '--post-js'):
825
args[i] = None
826
args[i + 1] = None
827
args = [arg for arg in args if arg is not None]
828
return args
829
830
def verify_es5(self, filename):
831
es_check = shared.get_npm_cmd('es-check')
832
# use --quiet once its available
833
# See: https://github.com/dollarshaveclub/es-check/pull/126/
834
es_check_env = os.environ.copy()
835
# Use NODE_JS here (the version of node that the compiler uses) rather than the version of node
836
# from JS_ENGINES (the version of node being used to run the tests) since we only care about
837
# having something that can run the es-check tool.
838
es_check_env['PATH'] = os.path.dirname(config.NODE_JS[0]) + os.pathsep + es_check_env['PATH']
839
inputfile = os.path.abspath(filename)
840
# For some reason es-check requires unix paths, even on windows
841
if WINDOWS:
842
inputfile = utils.normalize_path(inputfile)
843
try:
844
# es-check prints the details of the errors to stdout, but it also prints
845
# stuff in the case there are no errors:
846
# ES-Check: there were no ES version matching errors!
847
# pipe stdout and stderr so that we can choose if/when to print this
848
# output and avoid spamming stdout when tests are successful.
849
utils.run_process(es_check + ['es5', inputfile], stdout=PIPE, stderr=STDOUT, env=es_check_env)
850
except subprocess.CalledProcessError as e:
851
print(e.stdout)
852
self.fail('es-check failed to verify ES5 output compliance')
853
854
# Build JavaScript code from source code
855
def build(self, filename, libraries=None, includes=None, force_c=False, cflags=None, output_basename=None, output_suffix=None):
856
filename = maybe_test_file(filename)
857
compiler = [compiler_for(filename, force_c)]
858
859
if force_c:
860
assert utils.suffix(filename) != '.c', 'force_c is not needed for source files ending in .c'
861
compiler.append('-xc')
862
863
all_cflags = self.get_cflags(main_file=True)
864
if cflags:
865
all_cflags += cflags
866
if not output_suffix:
867
output_suffix = get_output_suffix(all_cflags)
868
869
if output_basename:
870
output = output_basename + output_suffix
871
else:
872
output = utils.unsuffixed_basename(filename) + output_suffix
873
cmd = compiler + [str(filename), '-o', output] + all_cflags
874
if libraries:
875
cmd += libraries
876
if includes:
877
cmd += ['-I' + str(include) for include in includes]
878
879
self.run_process(cmd, stderr=self.stderr_redirect if not DEBUG else None)
880
self.assertExists(output)
881
882
if output_suffix in ('.js', '.mjs'):
883
# Make sure we produced correct line endings
884
self.assertEqual(line_endings.check_line_endings(output), 0)
885
886
return output
887
888
def get_func(self, src, name):
889
start = src.index('function ' + name + '(')
890
t = start
891
n = 0
892
while True:
893
if src[t] == '{':
894
n += 1
895
elif src[t] == '}':
896
n -= 1
897
if n == 0:
898
return src[start:t + 1]
899
t += 1
900
assert t < len(src)
901
902
def get_wasm_text(self, wasm_binary):
903
return self.run_process([WASM_DIS, wasm_binary], stdout=PIPE).stdout
904
905
def is_exported_in_wasm(self, name, wasm):
906
wat = self.get_wasm_text(wasm)
907
return ('(export "%s"' % name) in wat
908
909
def measure_wasm_code_lines(self, wasm):
910
wat_lines = self.get_wasm_text(wasm).splitlines()
911
non_data_lines = [line for line in wat_lines if '(data ' not in line]
912
return len(non_data_lines)
913
914
def clean_js_output(self, output):
915
"""Cleanup the JS output prior to running verification steps on it.
916
917
Due to minification, when we get a crash report from JS it can sometimes
918
contains the entire program in the output (since the entire program is
919
on a single line). In this case we can sometimes get false positives
920
when checking for strings in the output. To avoid these false positives
921
and the make the output easier to read in such cases we attempt to remove
922
such lines from the JS output.
923
"""
924
lines = output.splitlines()
925
long_lines = []
926
927
def cleanup(line):
928
if len(line) > 2048 and line.startswith('var Module=typeof Module!="undefined"'):
929
long_lines.append(line)
930
line = '<REPLACED ENTIRE PROGRAM ON SINGLE LINE>'
931
return line
932
933
lines = [cleanup(l) for l in lines]
934
if not long_lines:
935
# No long lines found just return the unmodified output
936
return output
937
938
# Sanity check that we only a single long line.
939
assert len(long_lines) == 1
940
return '\n'.join(lines)
941
942
def get_current_js_engine(self):
943
"""Return the default JS engine to run tests under"""
944
return self.js_engines[0]
945
946
def engine_is_bun(self):
947
return engine_is_bun(self.get_current_js_engine())
948
949
def engine_is_node(self):
950
return engine_is_node(self.get_current_js_engine())
951
952
def get_engine_with_args(self, engine=None):
953
if not engine:
954
engine = self.get_current_js_engine()
955
# Make a copy of the engine command before we modify/extend it.
956
engine = list(engine)
957
if engine_is_node(engine) or engine_is_bun(engine):
958
engine += self.node_args
959
elif engine_is_v8(engine):
960
engine += self.v8_args
961
elif engine == config.SPIDERMONKEY_ENGINE:
962
engine += self.spidermonkey_args
963
return engine
964
965
def run_js(self, filename, engine=None, args=None,
966
assert_returncode=0,
967
interleaved_output=True,
968
input=None):
969
# use files, as PIPE can get too full and hang us
970
stdout_file = self.in_dir('stdout')
971
stderr_file = None
972
if interleaved_output:
973
stderr = STDOUT
974
else:
975
stderr_file = self.in_dir('stderr')
976
stderr = open(stderr_file, 'w')
977
stdout = open(stdout_file, 'w')
978
error = None
979
timeout_error = None
980
engine = self.get_engine_with_args(engine)
981
try:
982
jsrun.run_js(filename, engine, args,
983
stdout=stdout,
984
stderr=stderr,
985
assert_returncode=assert_returncode,
986
input=input)
987
except subprocess.TimeoutExpired as e:
988
timeout_error = e
989
except subprocess.CalledProcessError as e:
990
error = e
991
finally:
992
stdout.close()
993
if stderr != STDOUT:
994
stderr.close()
995
996
ret = read_file(stdout_file)
997
if not interleaved_output:
998
ret += read_file(stderr_file)
999
if assert_returncode != 0:
1000
ret = self.clean_js_output(ret)
1001
if error or timeout_error or EMTEST_VERBOSE:
1002
print('-- begin program output --')
1003
print(limit_size(read_file(stdout_file)), end='')
1004
print('-- end program output --')
1005
if not interleaved_output:
1006
print('-- begin program stderr --')
1007
print(limit_size(read_file(stderr_file)), end='')
1008
print('-- end program stderr --')
1009
if timeout_error:
1010
raise timeout_error
1011
if error:
1012
ret = limit_size(ret)
1013
if assert_returncode == NON_ZERO:
1014
self.fail('JS subprocess unexpectedly succeeded (%s): Output:\n%s' % (error.cmd, ret))
1015
else:
1016
self.fail('JS subprocess failed (%s): %s (expected=%s). Output:\n%s' % (error.cmd, error.returncode, assert_returncode, ret))
1017
1018
return ret
1019
1020
def assertExists(self, filename, msg=None):
1021
if not msg:
1022
msg = f'Expected file not found: {filename}'
1023
self.assertTrue(os.path.exists(filename), msg)
1024
1025
def assertNotExists(self, filename, msg=None):
1026
if not msg:
1027
msg = 'Unexpected file exists: ' + filename
1028
self.assertFalse(os.path.exists(filename), msg)
1029
1030
# Tests that the given two paths are identical, modulo path delimiters. E.g. "C:/foo" is equal to "C:\foo".
1031
def assertPathsIdentical(self, path1, path2):
1032
path1 = utils.normalize_path(path1)
1033
path2 = utils.normalize_path(path2)
1034
return self.assertIdentical(path1, path2)
1035
1036
# Tests that the given two multiline text content are identical, modulo line
1037
# ending differences (\r\n on Windows, \n on Unix).
1038
def assertTextDataIdentical(self, text1, text2, msg=None,
1039
fromfile='expected', tofile='actual'):
1040
text1 = text1.replace('\r\n', '\n')
1041
text2 = text2.replace('\r\n', '\n')
1042
return self.assertIdentical(text1, text2, msg, fromfile, tofile)
1043
1044
def assertIdentical(self, values, y, msg=None,
1045
fromfile='expected', tofile='actual'):
1046
if type(values) not in (list, tuple):
1047
values = [values]
1048
for x in values:
1049
if x == y:
1050
return # success
1051
diff_lines = difflib.unified_diff(x.splitlines(), y.splitlines(),
1052
fromfile=fromfile, tofile=tofile)
1053
diff = ''.join([a.rstrip() + '\n' for a in diff_lines])
1054
if EMTEST_VERBOSE:
1055
print("Expected to have '%s' == '%s'" % (values[0], y))
1056
else:
1057
diff = limit_size(diff)
1058
diff += '\nFor full output run with --verbose.'
1059
fail_message = 'Unexpected difference:\n' + diff
1060
if msg:
1061
fail_message += '\n' + msg
1062
self.fail(fail_message)
1063
1064
def assertFileContents(self, filename, contents):
1065
if EMTEST_VERBOSE:
1066
print(f'Comparing results contents of file: {filename}')
1067
1068
contents = contents.replace('\r', '')
1069
1070
if EMTEST_REBASELINE:
1071
with open(filename, 'w') as f:
1072
f.write(contents)
1073
return
1074
1075
if not os.path.exists(filename):
1076
self.fail('Test expectation file not found: ' + filename + '.\n' +
1077
'Run with --rebaseline to generate.')
1078
expected_content = read_file(filename)
1079
message = "Run with --rebaseline to automatically update expectations"
1080
self.assertTextDataIdentical(expected_content, contents, message,
1081
filename, filename + '.new')
1082
1083
def assertContained(self, values, string, additional_info='', regex=False):
1084
if callable(string):
1085
string = string()
1086
1087
if regex:
1088
if type(values) is str:
1089
self.assertTrue(re.search(values, string, re.DOTALL), 'Expected regex "%s" to match on:\n%s' % (values, limit_size(string)))
1090
else:
1091
match_any = any(re.search(o, string, re.DOTALL) for o in values)
1092
self.assertTrue(match_any, 'Expected at least one of "%s" to match on:\n%s' % (values, limit_size(string)))
1093
return
1094
1095
if type(values) not in [list, tuple]:
1096
values = [values]
1097
1098
if not any(v in string for v in values):
1099
diff = difflib.unified_diff(values[0].split('\n'), string.split('\n'), fromfile='expected', tofile='actual')
1100
diff = ''.join(a.rstrip() + '\n' for a in diff)
1101
self.fail("Expected to find '%s' in '%s', diff:\n\n%s\n%s" % (
1102
limit_size(values[0]), limit_size(string), limit_size(diff),
1103
additional_info,
1104
))
1105
1106
def assertNotContained(self, value, string, regex=False):
1107
if callable(value):
1108
value = value() # lazy loading
1109
if callable(string):
1110
string = string()
1111
if regex:
1112
self.assertFalse(re.search(value, string, re.DOTALL), 'Expected regex "%s" NOT to match on:\n%s' % (value, limit_size(string)))
1113
else:
1114
if value in string:
1115
self.fail("Expected to NOT find '%s' in '%s'" % (limit_size(value), limit_size(string)))
1116
1117
def assertContainedIf(self, value, string, condition):
1118
if condition:
1119
self.assertContained(value, string)
1120
else:
1121
self.assertNotContained(value, string)
1122
1123
def assertBinaryEqual(self, file1, file2):
1124
self.assertEqual(os.path.getsize(file1),
1125
os.path.getsize(file2))
1126
self.assertEqual(read_binary(file1),
1127
read_binary(file2))
1128
1129
def get_build_dir(self):
1130
ret = self.in_dir('building')
1131
ensure_dir(ret)
1132
return ret
1133
1134
def get_library(self, name, generated_libs, configure=['sh', './configure'], # noqa
1135
configure_args=None, make=None, make_args=None,
1136
env_init=None, cache_name_extra='', native=False,
1137
force_rebuild=False):
1138
if make is None:
1139
make = ['make']
1140
if env_init is None:
1141
env_init = {}
1142
if make_args is None:
1143
make_args = ['-j', str(utils.get_num_cores())]
1144
1145
build_dir = self.get_build_dir()
1146
1147
cflags = []
1148
if not native:
1149
# get_library() is used to compile libraries, and not link executables,
1150
# so we don't want to pass linker flags here (emscripten warns if you
1151
# try to pass linker settings when compiling).
1152
cflags = self.get_cflags(compile_only=True)
1153
1154
hash_input = (str(cflags) + ' $ ' + str(env_init)).encode('utf-8')
1155
cache_name = name + ','.join([opt for opt in cflags if len(opt) < 7]) + '_' + hashlib.md5(hash_input).hexdigest() + cache_name_extra
1156
1157
valid_chars = "_%s%s" % (string.ascii_letters, string.digits)
1158
cache_name = ''.join([(c if c in valid_chars else '_') for c in cache_name])
1159
1160
if not force_rebuild and self.library_cache.get(cache_name):
1161
errlog('<load %s from cache> ' % cache_name)
1162
generated_libs = []
1163
for basename, contents in self.library_cache[cache_name]:
1164
bc_file = os.path.join(build_dir, cache_name + '_' + basename)
1165
write_binary(bc_file, contents)
1166
generated_libs.append(bc_file)
1167
return generated_libs
1168
1169
errlog(f'<building and saving {cache_name} into cache>')
1170
if configure and configure_args:
1171
# Make to copy to avoid mutating default param
1172
configure = list(configure)
1173
configure += configure_args
1174
1175
cflags = ' '.join(cflags)
1176
env_init.setdefault('CFLAGS', cflags)
1177
env_init.setdefault('CXXFLAGS', cflags)
1178
return self.build_library(name, build_dir, generated_libs, configure,
1179
make, make_args, cache_name, env_init=env_init, native=native)
1180
1181
def clear(self):
1182
force_delete_contents(self.get_dir())
1183
if shared.EMSCRIPTEN_TEMP_DIR:
1184
utils.delete_contents(shared.EMSCRIPTEN_TEMP_DIR)
1185
1186
def run_process(self, cmd, check=True, **kwargs):
1187
# Wrapper around utils.run_process. This is desirable so that the tests
1188
# can fail (in the unittest sense) rather than error'ing.
1189
# In the long run it would nice to completely remove the dependency on
1190
# core emscripten code (shared.py) here.
1191
1192
# Handle buffering for subprocesses. The python unittest buffering mechanism
1193
# will only buffer output from the current process (by overriding sys.stdout
1194
# and sys.stderr), not from sub-processes.
1195
stdout_buffering = 'stdout' not in kwargs and isinstance(sys.stdout, io.StringIO)
1196
stderr_buffering = 'stderr' not in kwargs and isinstance(sys.stderr, io.StringIO)
1197
if stdout_buffering:
1198
kwargs['stdout'] = PIPE
1199
if stderr_buffering:
1200
kwargs['stderr'] = PIPE
1201
1202
try:
1203
rtn = utils.run_process(cmd, check=check, **kwargs)
1204
except subprocess.CalledProcessError as e:
1205
if check and e.returncode != 0:
1206
print(e.stdout)
1207
print(e.stderr)
1208
self.fail(f'subprocess exited with non-zero return code({e.returncode}): `{shlex.join(cmd)}`')
1209
1210
if stdout_buffering:
1211
sys.stdout.write(rtn.stdout)
1212
# When we inject stdout/stderr buffering for our own internal purposes, we do not also want to
1213
# make it available on the returned object.
1214
# If we don't do this then callers would have access to rtn.stdout/rtn.stderr even when they
1215
# didn't request it (i.e. even when they did not pass stderr=PIPE), which can lead to tests
1216
# that pass in buffering mode, but fail without it.
1217
rtn.stdout = None
1218
if stderr_buffering:
1219
sys.stderr.write(rtn.stderr)
1220
rtn.stderr = None
1221
return rtn
1222
1223
def emcc(self, filename, args=[], **kwargs): # noqa
1224
filename = maybe_test_file(filename)
1225
compile_only = '-c' in args or '-sSIDE_MODULE' in args
1226
cmd = [compiler_for(filename), filename] + self.get_cflags(compile_only=compile_only) + args
1227
self.run_process(cmd, **kwargs)
1228
1229
# Shared test code between main suite and others
1230
1231
def expect_fail(self, cmd, expect_traceback=False, **kwargs):
1232
"""Run a subprocess and assert that it returns non-zero.
1233
1234
Return the stderr of the subprocess.
1235
"""
1236
proc = self.run_process(cmd, check=False, stderr=PIPE, **kwargs)
1237
self.assertNotEqual(proc.returncode, 0, 'subprocess unexpectedly succeeded. stderr:\n' + proc.stderr)
1238
# When we check for failure we expect a user-visible error, not a traceback.
1239
# However, on windows a python traceback can happen randomly sometimes,
1240
# due to "Access is denied" https://github.com/emscripten-core/emscripten/issues/718
1241
if expect_traceback:
1242
self.assertContained('Traceback', proc.stderr)
1243
elif not WINDOWS or 'Access is denied' not in proc.stderr:
1244
self.assertNotContained('Traceback', proc.stderr)
1245
if EMTEST_VERBOSE:
1246
sys.stderr.write(proc.stderr)
1247
return proc.stderr
1248
1249
def assert_fail(self, cmd, expected, **kwargs):
1250
"""Just like expect_fail, but also check for expected message in stderr.
1251
"""
1252
err = self.expect_fail(cmd, **kwargs)
1253
self.assertContained(expected, err)
1254
return err
1255
1256
# exercise dynamic linker.
1257
#
1258
# test that linking to shared library B, which is linked to A, loads A as well.
1259
# main is also linked to C, which is also linked to A. A is loaded/initialized only once.
1260
#
1261
# B
1262
# main < > A
1263
# C
1264
#
1265
# this test is used by both test_core and test_browser.
1266
# when run under browser it exercises how dynamic linker handles concurrency
1267
# - because B and C are loaded in parallel.
1268
def _test_dylink_dso_needed(self, do_run):
1269
create_file('liba.cpp', r'''
1270
#include <stdio.h>
1271
#include <emscripten.h>
1272
1273
static const char *afunc_prev;
1274
1275
extern "C" {
1276
EMSCRIPTEN_KEEPALIVE void afunc(const char *s);
1277
}
1278
1279
void afunc(const char *s) {
1280
printf("a: %s (prev: %s)\n", s, afunc_prev);
1281
afunc_prev = s;
1282
}
1283
1284
struct ainit {
1285
ainit() {
1286
puts("a: loaded");
1287
}
1288
};
1289
1290
static ainit _;
1291
''')
1292
1293
create_file('libb.c', r'''
1294
#include <emscripten.h>
1295
1296
void afunc(const char *s);
1297
1298
EMSCRIPTEN_KEEPALIVE void bfunc() {
1299
afunc("b");
1300
}
1301
''')
1302
1303
create_file('libc.c', r'''
1304
#include <emscripten.h>
1305
1306
void afunc(const char *s);
1307
1308
EMSCRIPTEN_KEEPALIVE void cfunc() {
1309
afunc("c");
1310
}
1311
''')
1312
1313
def ccshared(src, linkto=None):
1314
cmdv = [EMCC, src, '-o', utils.unsuffixed(src) + '.wasm', '-sSIDE_MODULE'] + self.get_cflags()
1315
if linkto:
1316
cmdv += linkto
1317
self.run_process(cmdv)
1318
1319
ccshared('liba.cpp')
1320
ccshared('libb.c', ['liba.wasm'])
1321
ccshared('libc.c', ['liba.wasm'])
1322
1323
self.set_setting('MAIN_MODULE')
1324
extra_args = ['-L.', 'libb.wasm', 'libc.wasm']
1325
do_run(r'''
1326
#ifdef __cplusplus
1327
extern "C" {
1328
#endif
1329
void bfunc();
1330
void cfunc();
1331
#ifdef __cplusplus
1332
}
1333
#endif
1334
1335
int test_main() {
1336
bfunc();
1337
cfunc();
1338
return 0;
1339
}
1340
''',
1341
'a: loaded\na: b (prev: (null))\na: c (prev: b)\n', cflags=extra_args)
1342
1343
extra_args = []
1344
for libname in ('liba.wasm', 'libb.wasm', 'libc.wasm'):
1345
extra_args += ['--embed-file', libname]
1346
do_run(r'''
1347
#include <assert.h>
1348
#include <dlfcn.h>
1349
#include <stddef.h>
1350
1351
int test_main() {
1352
void *bdso, *cdso;
1353
void (*bfunc_ptr)(), (*cfunc_ptr)();
1354
1355
// FIXME for RTLD_LOCAL binding symbols to loaded lib is not currently working
1356
bdso = dlopen("libb.wasm", RTLD_NOW|RTLD_GLOBAL);
1357
assert(bdso != NULL);
1358
cdso = dlopen("libc.wasm", RTLD_NOW|RTLD_GLOBAL);
1359
assert(cdso != NULL);
1360
1361
bfunc_ptr = (void (*)())dlsym(bdso, "bfunc");
1362
assert(bfunc_ptr != NULL);
1363
cfunc_ptr = (void (*)())dlsym(cdso, "cfunc");
1364
assert(cfunc_ptr != NULL);
1365
1366
bfunc_ptr();
1367
cfunc_ptr();
1368
return 0;
1369
}
1370
''' % locals(),
1371
'a: loaded\na: b (prev: (null))\na: c (prev: b)\n', cflags=extra_args)
1372
1373
def do_run(self, src, expected_output=None, force_c=False, **kwargs):
1374
if 'no_build' in kwargs:
1375
filename = src
1376
else:
1377
if force_c:
1378
filename = 'src.c'
1379
else:
1380
filename = 'src.cpp'
1381
create_file(filename, src)
1382
return self._build_and_run(filename, expected_output, **kwargs)
1383
1384
def do_runf(self, filename, expected_output=None, **kwargs):
1385
return self._build_and_run(filename, expected_output, **kwargs)
1386
1387
def do_run_in_out_file_test(self, srcfile, **kwargs):
1388
srcfile = maybe_test_file(srcfile)
1389
out_suffix = kwargs.pop('out_suffix', '')
1390
outfile = utils.unsuffixed(srcfile) + out_suffix + '.out'
1391
if EMTEST_REBASELINE:
1392
expected = None
1393
else:
1394
expected = read_file(outfile)
1395
output = self._build_and_run(srcfile, expected, **kwargs)
1396
if EMTEST_REBASELINE:
1397
utils.write_file(outfile, output)
1398
return output
1399
1400
## Does a complete test - builds, runs, checks output, etc.
1401
def _build_and_run(self, filename, expected_output, args=None,
1402
no_build=False,
1403
assert_returncode=0, assert_identical=False, assert_all=False,
1404
check_for_error=True,
1405
interleaved_output=True,
1406
regex=False,
1407
input=None,
1408
**kwargs):
1409
logger.debug(f'_build_and_run: {filename}')
1410
1411
if no_build:
1412
js_file = filename
1413
else:
1414
js_file = self.build(filename, **kwargs)
1415
self.assertExists(js_file)
1416
1417
engines = self.js_engines.copy()
1418
if len(engines) > 1 and not self.use_all_engines:
1419
engines = engines[:1]
1420
# In standalone mode, also add wasm vms as we should be able to run there too.
1421
if self.get_setting('STANDALONE_WASM'):
1422
# TODO once standalone wasm support is more stable, apply use_all_engines
1423
# like with js engines, but for now as we bring it up, test in all of them
1424
if not self.wasm_engines:
1425
if 'EMTEST_SKIP_WASM_ENGINE' in os.environ:
1426
self.skipTest('no wasm engine was found to run the standalone part of this test')
1427
else:
1428
logger.warning('no wasm engine was found to run the standalone part of this test (Use EMTEST_SKIP_WASM_ENGINE to skip)')
1429
engines += self.wasm_engines
1430
if len(engines) == 0:
1431
self.fail('No JS engine present to run this test with. Check %s and the paths therein.' % config.EM_CONFIG)
1432
for engine in engines:
1433
js_output = self.run_js(js_file, engine, args,
1434
input=input,
1435
assert_returncode=assert_returncode,
1436
interleaved_output=interleaved_output)
1437
js_output = js_output.replace('\r\n', '\n')
1438
if expected_output:
1439
if type(expected_output) not in [list, tuple]:
1440
expected_output = [expected_output]
1441
try:
1442
if assert_identical:
1443
self.assertIdentical(expected_output, js_output)
1444
elif assert_all or len(expected_output) == 1:
1445
for o in expected_output:
1446
self.assertContained(o, js_output, regex=regex)
1447
else:
1448
self.assertContained(expected_output, js_output, regex=regex)
1449
if assert_returncode == 0 and check_for_error:
1450
self.assertNotContained('ERROR', js_output)
1451
except self.failureException:
1452
print('(test did not pass in JS engine: %s)' % engine)
1453
raise
1454
return js_output
1455
1456
def get_freetype_library(self):
1457
self.cflags += [
1458
'-Wno-misleading-indentation',
1459
'-Wno-unused-but-set-variable',
1460
'-Wno-pointer-bool-conversion',
1461
'-Wno-shift-negative-value',
1462
'-Wno-gnu-offsetof-extensions',
1463
# And because gnu-offsetof-extensions is a new warning:
1464
'-Wno-unknown-warning-option',
1465
]
1466
return self.get_library(os.path.join('third_party', 'freetype'),
1467
os.path.join('objs', '.libs', 'libfreetype.a'),
1468
configure_args=['--disable-shared', '--without-zlib'])
1469
1470
def get_poppler_library(self, env_init=None):
1471
freetype = self.get_freetype_library()
1472
1473
self.cflags += [
1474
'-I' + test_file('third_party/freetype/include'),
1475
'-I' + test_file('third_party/poppler/include'),
1476
# Poppler's configure script emits -O2 for gcc, and nothing for other
1477
# compilers, including emcc, so set opts manually.
1478
"-O2",
1479
]
1480
1481
# Poppler has some pretty glaring warning. Suppress them to keep the
1482
# test output readable.
1483
self.cflags += [
1484
'-Wno-sentinel',
1485
'-Wno-logical-not-parentheses',
1486
'-Wno-unused-private-field',
1487
'-Wno-tautological-compare',
1488
'-Wno-unknown-pragmas',
1489
'-Wno-shift-negative-value',
1490
'-Wno-dynamic-class-memaccess',
1491
# The fontconfig symbols are all missing from the poppler build
1492
# e.g. FcConfigSubstitute
1493
'-sERROR_ON_UNDEFINED_SYMBOLS=0',
1494
# Avoid warning about ERROR_ON_UNDEFINED_SYMBOLS being used at compile time
1495
'-Wno-unused-command-line-argument',
1496
'-Wno-js-compiler',
1497
'-Wno-nontrivial-memaccess',
1498
]
1499
env_init = env_init.copy() if env_init else {}
1500
env_init['FONTCONFIG_CFLAGS'] = ' '
1501
env_init['FONTCONFIG_LIBS'] = ' '
1502
1503
poppler = self.get_library(
1504
os.path.join('third_party', 'poppler'),
1505
[os.path.join('utils', 'pdftoppm.o'), os.path.join('utils', 'parseargs.o'), os.path.join('poppler', '.libs', 'libpoppler.a')],
1506
env_init=env_init,
1507
configure_args=['--disable-libjpeg', '--disable-libpng', '--disable-poppler-qt', '--disable-poppler-qt4', '--disable-cms', '--disable-cairo-output', '--disable-abiword-output', '--disable-shared', '--host=wasm32-emscripten'])
1508
1509
return poppler + freetype
1510
1511
def get_zlib_library(self, cmake, cflags=None):
1512
assert cmake or not WINDOWS, 'on windows, get_zlib_library only supports cmake'
1513
1514
old_args = self.cflags.copy()
1515
if cflags:
1516
self.cflags += cflags
1517
# inflate.c does -1L << 16
1518
self.cflags.append('-Wno-shift-negative-value')
1519
# adler32.c uses K&R style function declarations
1520
self.cflags.append('-Wno-deprecated-non-prototype')
1521
# Work around configure-script error. TODO: remove when
1522
# https://github.com/emscripten-core/emscripten/issues/16908 is fixed
1523
self.cflags.append('-Wno-pointer-sign')
1524
if cmake:
1525
rtn = self.get_library(os.path.join('third_party', 'zlib'), os.path.join('libz.a'),
1526
configure=['cmake', '.'],
1527
make=['cmake', '--build', '.', '--'],
1528
make_args=[])
1529
else:
1530
rtn = self.get_library(os.path.join('third_party', 'zlib'), os.path.join('libz.a'), make_args=['libz.a'])
1531
self.cflags = old_args
1532
return rtn
1533
1534
def build_library(self, name, build_dir, generated_libs, configure, make, make_args, cache_name, env_init, native):
1535
"""Build a library and cache the result. We build the library file
1536
once and cache it for all our tests. (We cache in memory since the test
1537
directory is destroyed and recreated for each test. Note that we cache
1538
separately for different compilers). This cache is just during the test
1539
runner. There is a different concept of caching as well, see |Cache|.
1540
"""
1541
if type(generated_libs) is not list:
1542
generated_libs = [generated_libs]
1543
source_dir = test_file(name.replace('_native', ''))
1544
1545
project_dir = Path(build_dir, name)
1546
if os.path.exists(project_dir):
1547
shutil.rmtree(project_dir)
1548
# Useful in debugging sometimes to comment this out, and two lines above
1549
shutil.copytree(source_dir, project_dir)
1550
1551
generated_libs = [os.path.join(project_dir, lib) for lib in generated_libs]
1552
1553
if native:
1554
env = clang_native.get_clang_native_env()
1555
else:
1556
env = os.environ.copy()
1557
env.update(env_init)
1558
1559
if not native:
1560
# Inject emcmake, emconfigure or emmake accordingly, but only if we are
1561
# cross compiling.
1562
if configure:
1563
if configure[0] == 'cmake':
1564
configure = [EMCMAKE] + configure
1565
else:
1566
configure = [EMCONFIGURE] + configure
1567
else:
1568
make = [EMMAKE] + make
1569
1570
if configure:
1571
self.run_process(configure, env=env, cwd=project_dir)
1572
# if we run configure or cmake we don't then need any kind
1573
# of special env when we run make below
1574
env = None
1575
1576
def open_make_out(mode='r'):
1577
return open(os.path.join(project_dir, 'make.out'), mode)
1578
1579
def open_make_err(mode='r'):
1580
return open(os.path.join(project_dir, 'make.err'), mode)
1581
1582
if EMTEST_VERBOSE:
1583
# VERBOSE=1 is cmake and V=1 is for autoconf
1584
make_args += ['VERBOSE=1', 'V=1']
1585
1586
self.run_process(make + make_args, env=env, cwd=project_dir)
1587
1588
# Cache the result
1589
self.library_cache[cache_name] = []
1590
for f in generated_libs:
1591
basename = os.path.basename(f)
1592
self.library_cache[cache_name].append((basename, read_binary(f)))
1593
1594
return generated_libs
1595
1596