Book a Demo!
CoCalc Logo Icon
StoreFeaturesDocsShareSupportNewsAboutPoliciesSign UpSign In
emscripten-core
GitHub Repository: emscripten-core/emscripten
Path: blob/main/test/common.py
4128 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
from enum import Enum
7
from functools import wraps
8
from pathlib import Path
9
from subprocess import PIPE, STDOUT
10
from typing import Dict, Tuple
11
from urllib.parse import unquote, unquote_plus, urlparse, parse_qs
12
from http.server import ThreadingHTTPServer, SimpleHTTPRequestHandler
13
import contextlib
14
import difflib
15
import hashlib
16
import io
17
import itertools
18
import json
19
import logging
20
import multiprocessing
21
import os
22
import re
23
import shlex
24
import shutil
25
import stat
26
import string
27
import subprocess
28
import sys
29
import tempfile
30
import textwrap
31
import threading
32
import time
33
import webbrowser
34
import unittest
35
import queue
36
37
import clang_native
38
import jsrun
39
import line_endings
40
from tools.shared import EMCC, EMXX, DEBUG
41
from tools.shared import get_canonical_temp_dir, path_from_root
42
from tools.utils import MACOS, WINDOWS, read_file, read_binary, write_binary, exit_with_error
43
from tools.settings import COMPILE_TIME_SETTINGS
44
from tools import shared, feature_matrix, building, config, utils
45
46
logger = logging.getLogger('common')
47
48
# User can specify an environment variable EMTEST_BROWSER to force the browser
49
# test suite to run using another browser command line than the default system
50
# browser. If only the path to the browser executable is given, the tests
51
# will run in headless mode with a temporary profile with the same options
52
# used in CI. To use a custom start command specify the executable and command
53
# line flags.
54
#
55
# There are two special values that can be used here if running in an actual
56
# browser is not desired:
57
# EMTEST_BROWSER=0 : This will disable the actual running of the test and simply
58
# verify that it compiles and links.
59
# EMTEST_BROWSER=node : This will attempt to run the browser test under node.
60
# For most browser tests this does not work, but it can
61
# be useful for running pthread tests under node.
62
EMTEST_BROWSER = None
63
EMTEST_BROWSER_AUTO_CONFIG = None
64
EMTEST_HEADLESS = None
65
EMTEST_DETECT_TEMPFILE_LEAKS = None
66
EMTEST_SAVE_DIR = None
67
# generally js engines are equivalent, testing 1 is enough. set this
68
# to force testing on all js engines, good to find js engine bugs
69
EMTEST_ALL_ENGINES = None
70
EMTEST_SKIP_SLOW = None
71
EMTEST_SKIP_FLAKY = None
72
EMTEST_RETRY_FLAKY = None
73
EMTEST_LACKS_NATIVE_CLANG = None
74
EMTEST_VERBOSE = None
75
EMTEST_REBASELINE = None
76
77
# Verbosity level control for subprocess calls to configure + make.
78
# 0: disabled.
79
# 1: Log stderr of configure/make.
80
# 2: Log stdout and stderr configure/make. Print out subprocess commands that were executed.
81
# 3: Log stdout and stderr, and pass VERBOSE=1 to CMake/configure/make steps.
82
EMTEST_BUILD_VERBOSE = int(os.getenv('EMTEST_BUILD_VERBOSE', '0'))
83
EMTEST_CAPTURE_STDIO = int(os.getenv('EMTEST_CAPTURE_STDIO', '0'))
84
if 'EM_BUILD_VERBOSE' in os.environ:
85
exit_with_error('EM_BUILD_VERBOSE has been renamed to EMTEST_BUILD_VERBOSE')
86
87
# If we are drawing a parallel swimlane graph of test output, we need to use a temp
88
# file to track which tests were flaky so they can be graphed in orange color to
89
# visually stand out.
90
flaky_tests_log_filename = os.path.join(path_from_root('out/flaky_tests.txt'))
91
92
93
# Default flags used to run browsers in CI testing:
94
class ChromeConfig:
95
data_dir_flag = '--user-data-dir='
96
default_flags = (
97
# --no-sandbox because we are running as root and chrome requires
98
# this flag for now: https://crbug.com/638180
99
'--no-first-run -start-maximized --no-sandbox --enable-unsafe-swiftshader --use-gl=swiftshader --enable-experimental-web-platform-features --enable-features=JavaScriptSourcePhaseImports',
100
'--enable-experimental-webassembly-features --js-flags="--experimental-wasm-stack-switching --experimental-wasm-type-reflection --experimental-wasm-rab-integration"',
101
# The runners lack sound hardware so fallback to a dummy device (and
102
# bypass the user gesture so audio tests work without interaction)
103
'--use-fake-device-for-media-stream --autoplay-policy=no-user-gesture-required',
104
# Cache options.
105
'--disk-cache-size=1 --media-cache-size=1 --disable-application-cache',
106
# Disable various background tasks downloads (e.g. updates).
107
'--disable-background-networking',
108
)
109
headless_flags = '--headless=new --window-size=1024,768 --remote-debugging-port=1234'
110
111
@staticmethod
112
def configure(data_dir):
113
"""Chrome has no special configuration step."""
114
115
116
class FirefoxConfig:
117
data_dir_flag = '-profile '
118
default_flags = ()
119
headless_flags = '-headless'
120
121
@staticmethod
122
def configure(data_dir):
123
shutil.copy(test_file('firefox_user.js'), os.path.join(data_dir, 'user.js'))
124
125
126
# Special value for passing to assert_returncode which means we expect that program
127
# to fail with non-zero return code, but we don't care about specifically which one.
128
NON_ZERO = -1
129
130
TEST_ROOT = path_from_root('test')
131
LAST_TEST = path_from_root('out/last_test.txt')
132
PREVIOUS_TEST_RUN_RESULTS_FILE = path_from_root('out/previous_test_run_results.json')
133
134
DEFAULT_BROWSER_DATA_DIR = path_from_root('out/browser-profile')
135
136
WEBIDL_BINDER = shared.bat_suffix(path_from_root('tools/webidl_binder'))
137
138
EMBUILDER = shared.bat_suffix(path_from_root('embuilder'))
139
EMMAKE = shared.bat_suffix(path_from_root('emmake'))
140
EMCMAKE = shared.bat_suffix(path_from_root('emcmake'))
141
EMCONFIGURE = shared.bat_suffix(path_from_root('emconfigure'))
142
EMRUN = shared.bat_suffix(shared.path_from_root('emrun'))
143
WASM_DIS = os.path.join(building.get_binaryen_bin(), 'wasm-dis')
144
LLVM_OBJDUMP = shared.llvm_tool_path('llvm-objdump')
145
PYTHON = sys.executable
146
147
assert config.NODE_JS # assert for mypy's benefit
148
# By default we run the tests in the same version of node as emscripten itself used.
149
if not config.NODE_JS_TEST:
150
config.NODE_JS_TEST = config.NODE_JS
151
# The default set of JS_ENGINES contains just node.
152
if not config.JS_ENGINES:
153
config.JS_ENGINES = [config.NODE_JS_TEST]
154
155
requires_network = unittest.skipIf(os.getenv('EMTEST_SKIP_NETWORK_TESTS'), 'This test requires network access')
156
157
158
def load_previous_test_run_results():
159
try:
160
return json.load(open(PREVIOUS_TEST_RUN_RESULTS_FILE))
161
except FileNotFoundError:
162
return {}
163
164
165
def test_file(*path_components):
166
"""Construct a path relative to the emscripten "tests" directory."""
167
return str(Path(TEST_ROOT, *path_components))
168
169
170
def copytree(src, dest):
171
shutil.copytree(src, dest, dirs_exist_ok=True)
172
173
174
# checks if browser testing is enabled
175
def has_browser():
176
return EMTEST_BROWSER != '0'
177
178
179
CHROMIUM_BASED_BROWSERS = ['chrom', 'edge', 'opera']
180
181
182
def is_chrome():
183
return EMTEST_BROWSER and any(pattern in EMTEST_BROWSER.lower() for pattern in CHROMIUM_BASED_BROWSERS)
184
185
186
def is_firefox():
187
return EMTEST_BROWSER and 'firefox' in EMTEST_BROWSER.lower()
188
189
190
def compiler_for(filename, force_c=False):
191
if shared.suffix(filename) in ('.cc', '.cxx', '.cpp') and not force_c:
192
return EMXX
193
else:
194
return EMCC
195
196
197
# Generic decorator that calls a function named 'condition' on the test class and
198
# skips the test if that function returns true
199
def skip_if(func, condition, explanation='', negate=False):
200
assert callable(func)
201
explanation_str = ' : %s' % explanation if explanation else ''
202
203
@wraps(func)
204
def decorated(self, *args, **kwargs):
205
choice = self.__getattribute__(condition)()
206
if negate:
207
choice = not choice
208
if choice:
209
self.skipTest(condition + explanation_str)
210
func(self, *args, **kwargs)
211
212
return decorated
213
214
215
def is_slow_test(func):
216
assert callable(func)
217
218
@wraps(func)
219
def decorated(self, *args, **kwargs):
220
if EMTEST_SKIP_SLOW:
221
return self.skipTest('skipping slow tests')
222
return func(self, *args, **kwargs)
223
224
return decorated
225
226
227
def flaky(note=''):
228
assert not callable(note)
229
230
if EMTEST_SKIP_FLAKY:
231
return unittest.skip(note)
232
233
if not EMTEST_RETRY_FLAKY:
234
return lambda f: f
235
236
def decorated(f):
237
@wraps(f)
238
def modified(*args, **kwargs):
239
for i in range(EMTEST_RETRY_FLAKY):
240
try:
241
return f(*args, **kwargs)
242
except AssertionError as exc:
243
preserved_exc = exc
244
logging.info(f'Retrying flaky test "{f.__name__}" (attempt {i}/{EMTEST_RETRY_FLAKY} failed): {exc}')
245
# Mark down that this was a flaky test.
246
open(flaky_tests_log_filename, 'a').write(f'{f.__name__}\n')
247
248
raise AssertionError('Flaky test has failed too many times') from preserved_exc
249
250
return modified
251
252
return decorated
253
254
255
def disabled(note=''):
256
assert not callable(note)
257
return unittest.skip(note)
258
259
260
def no_mac(note=''):
261
assert not callable(note)
262
if MACOS:
263
return unittest.skip(note)
264
return lambda f: f
265
266
267
def no_windows(note=''):
268
assert not callable(note)
269
if WINDOWS:
270
return unittest.skip(note)
271
return lambda f: f
272
273
274
def no_wasm64(note=''):
275
assert not callable(note)
276
277
def decorated(f):
278
return skip_if(f, 'is_wasm64', note)
279
return decorated
280
281
282
def no_2gb(note):
283
assert not callable(note)
284
285
def decorator(f):
286
assert callable(f)
287
288
@wraps(f)
289
def decorated(self, *args, **kwargs):
290
# 2200mb is the value used by the core_2gb test mode
291
if self.get_setting('INITIAL_MEMORY') == '2200mb':
292
self.skipTest(note)
293
f(self, *args, **kwargs)
294
return decorated
295
return decorator
296
297
298
def no_4gb(note):
299
assert not callable(note)
300
301
def decorator(f):
302
assert callable(f)
303
304
@wraps(f)
305
def decorated(self, *args, **kwargs):
306
if self.is_4gb():
307
self.skipTest(note)
308
f(self, *args, **kwargs)
309
return decorated
310
return decorator
311
312
313
def only_windows(note=''):
314
assert not callable(note)
315
if not WINDOWS:
316
return unittest.skip(note)
317
return lambda f: f
318
319
320
def requires_native_clang(func):
321
assert callable(func)
322
323
@wraps(func)
324
def decorated(self, *args, **kwargs):
325
if EMTEST_LACKS_NATIVE_CLANG:
326
return self.skipTest('native clang tests are disabled')
327
return func(self, *args, **kwargs)
328
329
return decorated
330
331
332
def requires_node(func):
333
assert callable(func)
334
335
@wraps(func)
336
def decorated(self, *args, **kwargs):
337
self.require_node()
338
return func(self, *args, **kwargs)
339
340
return decorated
341
342
343
def requires_node_canary(func):
344
assert callable(func)
345
346
@wraps(func)
347
def decorated(self, *args, **kwargs):
348
self.require_node_canary()
349
return func(self, *args, **kwargs)
350
351
return decorated
352
353
354
def node_bigint_flags(node_version):
355
# The --experimental-wasm-bigint flag was added in v12, and then removed (enabled by default)
356
# in v16.
357
if node_version and node_version < (16, 0, 0) and node_version >= (12, 0, 0):
358
return ['--experimental-wasm-bigint']
359
else:
360
return []
361
362
363
# Used to mark dependencies in various tests to npm developer dependency
364
# packages, which might not be installed on Emscripten end users' systems.
365
def requires_dev_dependency(package):
366
assert not callable(package)
367
368
def decorator(f):
369
assert callable(f)
370
371
@wraps(f)
372
def decorated(self, *args, **kwargs):
373
if 'EMTEST_SKIP_NODE_DEV_PACKAGES' in os.environ:
374
self.skipTest(f'test requires npm development package "{package}" and EMTEST_SKIP_NODE_DEV_PACKAGES is set')
375
f(self, *args, **kwargs)
376
return decorated
377
return decorator
378
379
380
def requires_wasm64(func):
381
assert callable(func)
382
383
@wraps(func)
384
def decorated(self, *args, **kwargs):
385
self.require_wasm64()
386
return func(self, *args, **kwargs)
387
388
return decorated
389
390
391
def requires_wasm_legacy_eh(func):
392
assert callable(func)
393
394
@wraps(func)
395
def decorated(self, *args, **kwargs):
396
self.require_wasm_legacy_eh()
397
return func(self, *args, **kwargs)
398
399
return decorated
400
401
402
def requires_wasm_eh(func):
403
assert callable(func)
404
405
@wraps(func)
406
def decorated(self, *args, **kwargs):
407
self.require_wasm_eh()
408
return func(self, *args, **kwargs)
409
410
return decorated
411
412
413
def requires_v8(func):
414
assert callable(func)
415
416
@wraps(func)
417
def decorated(self, *args, **kwargs):
418
self.require_v8()
419
return func(self, *args, **kwargs)
420
421
return decorated
422
423
424
def requires_wasm2js(f):
425
assert callable(f)
426
427
@wraps(f)
428
def decorated(self, *args, **kwargs):
429
self.require_wasm2js()
430
return f(self, *args, **kwargs)
431
432
return decorated
433
434
435
def requires_jspi(func):
436
assert callable(func)
437
438
@wraps(func)
439
def decorated(self, *args, **kwargs):
440
self.require_jspi()
441
return func(self, *args, **kwargs)
442
443
return decorated
444
445
446
def node_pthreads(f):
447
assert callable(f)
448
449
@wraps(f)
450
def decorated(self, *args, **kwargs):
451
self.setup_node_pthreads()
452
f(self, *args, **kwargs)
453
return decorated
454
455
456
def crossplatform(f):
457
f.is_crossplatform_test = True
458
return f
459
460
461
# without EMTEST_ALL_ENGINES set we only run tests in a single VM by
462
# default. in some tests we know that cross-VM differences may happen and
463
# so are worth testing, and they should be marked with this decorator
464
def all_engines(f):
465
assert callable(f)
466
467
@wraps(f)
468
def decorated(self, *args, **kwargs):
469
self.use_all_engines = True
470
self.set_setting('ENVIRONMENT', 'web,node,shell')
471
f(self, *args, **kwargs)
472
473
return decorated
474
475
476
@contextlib.contextmanager
477
def env_modify(updates):
478
"""A context manager that updates os.environ."""
479
# This could also be done with mock.patch.dict() but taking a dependency
480
# on the mock library is probably not worth the benefit.
481
old_env = os.environ.copy()
482
print("env_modify: " + str(updates))
483
# Setting a value to None means clear the environment variable
484
clears = [key for key, value in updates.items() if value is None]
485
updates = {key: value for key, value in updates.items() if value is not None}
486
os.environ.update(updates)
487
for key in clears:
488
if key in os.environ:
489
del os.environ[key]
490
try:
491
yield
492
finally:
493
os.environ.clear()
494
os.environ.update(old_env)
495
496
497
# Decorator version of env_modify
498
def with_env_modify(updates):
499
assert not callable(updates)
500
501
def decorated(f):
502
@wraps(f)
503
def modified(self, *args, **kwargs):
504
with env_modify(updates):
505
return f(self, *args, **kwargs)
506
return modified
507
508
return decorated
509
510
511
def also_with_wasmfs(f):
512
assert callable(f)
513
514
@wraps(f)
515
def metafunc(self, wasmfs, *args, **kwargs):
516
if DEBUG:
517
print('parameterize:wasmfs=%d' % wasmfs)
518
if wasmfs:
519
self.setup_wasmfs_test()
520
else:
521
self.cflags += ['-DMEMFS']
522
f(self, *args, **kwargs)
523
524
parameterize(metafunc, {'': (False,),
525
'wasmfs': (True,)})
526
return metafunc
527
528
529
def also_with_nodefs(func):
530
@wraps(func)
531
def metafunc(self, fs, *args, **kwargs):
532
if DEBUG:
533
print('parameterize:fs=%s' % (fs))
534
if fs == 'nodefs':
535
self.setup_nodefs_test()
536
else:
537
self.cflags += ['-DMEMFS']
538
assert fs is None
539
func(self, *args, **kwargs)
540
541
parameterize(metafunc, {'': (None,),
542
'nodefs': ('nodefs',)})
543
return metafunc
544
545
546
def also_with_nodefs_both(func):
547
@wraps(func)
548
def metafunc(self, fs, *args, **kwargs):
549
if DEBUG:
550
print('parameterize:fs=%s' % (fs))
551
if fs == 'nodefs':
552
self.setup_nodefs_test()
553
elif fs == 'rawfs':
554
self.setup_noderawfs_test()
555
else:
556
self.cflags += ['-DMEMFS']
557
assert fs is None
558
func(self, *args, **kwargs)
559
560
parameterize(metafunc, {'': (None,),
561
'nodefs': ('nodefs',),
562
'rawfs': ('rawfs',)})
563
return metafunc
564
565
566
def with_all_fs(func):
567
@wraps(func)
568
def metafunc(self, wasmfs, fs, *args, **kwargs):
569
if DEBUG:
570
print('parameterize:fs=%s' % (fs))
571
if wasmfs:
572
self.setup_wasmfs_test()
573
if fs == 'nodefs':
574
self.setup_nodefs_test()
575
elif fs == 'rawfs':
576
self.setup_noderawfs_test()
577
else:
578
self.cflags += ['-DMEMFS']
579
assert fs is None
580
func(self, *args, **kwargs)
581
582
parameterize(metafunc, {'': (False, None),
583
'nodefs': (False, 'nodefs'),
584
'rawfs': (False, 'rawfs'),
585
'wasmfs': (True, None),
586
'wasmfs_nodefs': (True, 'nodefs'),
587
'wasmfs_rawfs': (True, 'rawfs')})
588
return metafunc
589
590
591
def also_with_noderawfs(func):
592
assert callable(func)
593
594
@wraps(func)
595
def metafunc(self, rawfs, *args, **kwargs):
596
if DEBUG:
597
print('parameterize:rawfs=%d' % rawfs)
598
if rawfs:
599
self.setup_noderawfs_test()
600
else:
601
self.cflags += ['-DMEMFS']
602
func(self, *args, **kwargs)
603
604
parameterize(metafunc, {'': (False,),
605
'rawfs': (True,)})
606
return metafunc
607
608
609
# Decorator version of env_modify
610
def also_with_env_modify(name_updates_mapping):
611
612
def decorated(f):
613
@wraps(f)
614
def metafunc(self, updates, *args, **kwargs):
615
if DEBUG:
616
print('parameterize:env_modify=%s' % (updates))
617
if updates:
618
with env_modify(updates):
619
return f(self, *args, **kwargs)
620
else:
621
return f(self, *args, **kwargs)
622
623
params = {'': (None,)}
624
for name, updates in name_updates_mapping.items():
625
params[name] = (updates,)
626
627
parameterize(metafunc, params)
628
629
return metafunc
630
631
return decorated
632
633
634
def also_with_minimal_runtime(f):
635
assert callable(f)
636
637
@wraps(f)
638
def metafunc(self, with_minimal_runtime, *args, **kwargs):
639
if DEBUG:
640
print('parameterize:minimal_runtime=%s' % with_minimal_runtime)
641
if self.get_setting('MINIMAL_RUNTIME'):
642
self.skipTest('MINIMAL_RUNTIME already enabled in test config')
643
if with_minimal_runtime:
644
if self.get_setting('MODULARIZE') == 'instance' or self.get_setting('WASM_ESM_INTEGRATION'):
645
self.skipTest('MODULARIZE=instance is not compatible with MINIMAL_RUNTIME')
646
self.set_setting('MINIMAL_RUNTIME', 1)
647
# This extra helper code is needed to cleanly handle calls to exit() which throw
648
# an ExitCode exception.
649
self.cflags += ['--pre-js', test_file('minimal_runtime_exit_handling.js')]
650
f(self, *args, **kwargs)
651
652
parameterize(metafunc, {'': (False,),
653
'minimal_runtime': (True,)})
654
return metafunc
655
656
657
def also_without_bigint(f):
658
assert callable(f)
659
660
@wraps(f)
661
def metafunc(self, no_bigint, *args, **kwargs):
662
if DEBUG:
663
print('parameterize:no_bigint=%s' % no_bigint)
664
if no_bigint:
665
if self.get_setting('WASM_BIGINT') is not None:
666
self.skipTest('redundant in bigint test config')
667
self.set_setting('WASM_BIGINT', 0)
668
f(self, *args, **kwargs)
669
670
parameterize(metafunc, {'': (False,),
671
'no_bigint': (True,)})
672
return metafunc
673
674
675
def also_with_wasm64(f):
676
assert callable(f)
677
678
@wraps(f)
679
def metafunc(self, with_wasm64, *args, **kwargs):
680
if DEBUG:
681
print('parameterize:wasm64=%s' % with_wasm64)
682
if with_wasm64:
683
self.require_wasm64()
684
self.set_setting('MEMORY64')
685
f(self, *args, **kwargs)
686
687
parameterize(metafunc, {'': (False,),
688
'wasm64': (True,)})
689
return metafunc
690
691
692
def also_with_wasm2js(f):
693
assert callable(f)
694
695
@wraps(f)
696
def metafunc(self, with_wasm2js, *args, **kwargs):
697
assert self.get_setting('WASM') is None
698
if DEBUG:
699
print('parameterize:wasm2js=%s' % with_wasm2js)
700
if with_wasm2js:
701
self.require_wasm2js()
702
self.set_setting('WASM', 0)
703
f(self, *args, **kwargs)
704
705
parameterize(metafunc, {'': (False,),
706
'wasm2js': (True,)})
707
return metafunc
708
709
710
def can_do_standalone(self, impure=False):
711
# Pure standalone engines don't support MEMORY64 yet. Even with MEMORY64=2 (lowered)
712
# the WASI APIs that take pointer values don't have 64-bit variants yet.
713
if not impure:
714
if self.get_setting('MEMORY64'):
715
return False
716
# This is way to detect the core_2gb test mode in test_core.py
717
if self.get_setting('INITIAL_MEMORY') == '2200mb':
718
return False
719
return self.is_wasm() and \
720
self.get_setting('STACK_OVERFLOW_CHECK', 0) < 2 and \
721
not self.get_setting('MINIMAL_RUNTIME') and \
722
not self.get_setting('WASM_ESM_INTEGRATION') and \
723
not self.get_setting('SAFE_HEAP') and \
724
not any(a.startswith('-fsanitize=') for a in self.cflags)
725
726
727
# Impure means a test that cannot run in a wasm VM yet, as it is not 100%
728
# standalone. We can still run them with the JS code though.
729
def also_with_standalone_wasm(impure=False):
730
def decorated(func):
731
@wraps(func)
732
def metafunc(self, standalone, *args, **kwargs):
733
if DEBUG:
734
print('parameterize:standalone=%s' % standalone)
735
if standalone:
736
if not can_do_standalone(self, impure):
737
self.skipTest('Test configuration is not compatible with STANDALONE_WASM')
738
self.set_setting('STANDALONE_WASM')
739
if not impure:
740
self.set_setting('PURE_WASI')
741
self.cflags.append('-Wno-unused-command-line-argument')
742
# if we are impure, disallow all wasm engines
743
if impure:
744
self.wasm_engines = []
745
func(self, *args, **kwargs)
746
747
parameterize(metafunc, {'': (False,),
748
'standalone': (True,)})
749
return metafunc
750
751
return decorated
752
753
754
def also_with_asan(f):
755
assert callable(f)
756
757
@wraps(f)
758
def metafunc(self, asan, *args, **kwargs):
759
if asan:
760
if self.is_wasm64():
761
self.skipTest('TODO: ASAN in memory64')
762
if self.is_2gb() or self.is_4gb():
763
self.skipTest('asan doesnt support GLOBAL_BASE')
764
self.cflags.append('-fsanitize=address')
765
f(self, *args, **kwargs)
766
767
parameterize(metafunc, {'': (False,),
768
'asan': (True,)})
769
return metafunc
770
771
772
def also_with_modularize(f):
773
assert callable(f)
774
775
@wraps(f)
776
def metafunc(self, modularize, *args, **kwargs):
777
if modularize:
778
if self.get_setting('DECLARE_ASM_MODULE_EXPORTS') == 0:
779
self.skipTest('DECLARE_ASM_MODULE_EXPORTS=0 is not compatible with MODULARIZE')
780
if self.get_setting('STRICT_JS'):
781
self.skipTest('MODULARIZE is not compatible with STRICT_JS')
782
if self.get_setting('WASM_ESM_INTEGRATION'):
783
self.skipTest('MODULARIZE is not compatible with WASM_ESM_INTEGRATION')
784
self.cflags += ['--extern-post-js', test_file('modularize_post_js.js'), '-sMODULARIZE']
785
f(self, *args, **kwargs)
786
787
parameterize(metafunc, {'': (False,),
788
'modularize': (True,)})
789
return metafunc
790
791
792
# Tests exception handling and setjmp/longjmp handling. This tests three
793
# combinations:
794
# - Emscripten EH + Emscripten SjLj
795
# - Wasm EH + Wasm SjLj
796
# - Wasm EH + Wasm SjLj (Legacy)
797
def with_all_eh_sjlj(f):
798
assert callable(f)
799
800
@wraps(f)
801
def metafunc(self, mode, *args, **kwargs):
802
if DEBUG:
803
print('parameterize:eh_mode=%s' % mode)
804
if mode in {'wasm', 'wasm_legacy'}:
805
# Wasm EH is currently supported only in wasm backend and V8
806
if self.is_wasm2js():
807
self.skipTest('wasm2js does not support wasm EH/SjLj')
808
self.cflags.append('-fwasm-exceptions')
809
self.set_setting('SUPPORT_LONGJMP', 'wasm')
810
if mode == 'wasm':
811
self.require_wasm_eh()
812
if mode == 'wasm_legacy':
813
self.require_wasm_legacy_eh()
814
f(self, *args, **kwargs)
815
else:
816
self.set_setting('DISABLE_EXCEPTION_CATCHING', 0)
817
self.set_setting('SUPPORT_LONGJMP', 'emscripten')
818
# DISABLE_EXCEPTION_CATCHING=0 exports __cxa_can_catch,
819
# so if we don't build in C++ mode, wasm-ld will
820
# error out because libc++abi is not included. See
821
# https://github.com/emscripten-core/emscripten/pull/14192 for details.
822
self.set_setting('DEFAULT_TO_CXX')
823
f(self, *args, **kwargs)
824
825
parameterize(metafunc, {'emscripten': ('emscripten',),
826
'wasm': ('wasm',),
827
'wasm_legacy': ('wasm_legacy',)})
828
return metafunc
829
830
831
# This works just like `with_all_eh_sjlj` above but doesn't enable exceptions.
832
# Use this for tests that use setjmp/longjmp but not exceptions handling.
833
def with_all_sjlj(f):
834
assert callable(f)
835
836
@wraps(f)
837
def metafunc(self, mode, *args, **kwargs):
838
if mode in {'wasm', 'wasm_legacy'}:
839
if self.is_wasm2js():
840
self.skipTest('wasm2js does not support wasm SjLj')
841
self.set_setting('SUPPORT_LONGJMP', 'wasm')
842
if mode == 'wasm':
843
self.require_wasm_eh()
844
if mode == 'wasm_legacy':
845
self.require_wasm_legacy_eh()
846
f(self, *args, **kwargs)
847
else:
848
self.set_setting('SUPPORT_LONGJMP', 'emscripten')
849
f(self, *args, **kwargs)
850
851
parameterize(metafunc, {'emscripten': ('emscripten',),
852
'wasm': ('wasm',),
853
'wasm_legacy': ('wasm_legacy',)})
854
return metafunc
855
856
857
def ensure_dir(dirname):
858
dirname = Path(dirname)
859
dirname.mkdir(parents=True, exist_ok=True)
860
861
862
def limit_size(string):
863
maxbytes = 800000 * 20
864
if sys.stdout.isatty():
865
maxlines = 500
866
max_line = 500
867
else:
868
max_line = 5000
869
maxlines = 1000
870
lines = string.splitlines()
871
for i, line in enumerate(lines):
872
if len(line) > max_line:
873
lines[i] = line[:max_line] + '[..]'
874
if len(lines) > maxlines:
875
lines = lines[0:maxlines // 2] + ['[..]'] + lines[-maxlines // 2:]
876
lines.append('(not all output shown. See `limit_size`)')
877
string = '\n'.join(lines) + '\n'
878
if len(string) > maxbytes:
879
string = string[0:maxbytes // 2] + '\n[..]\n' + string[-maxbytes // 2:]
880
return string
881
882
883
def create_file(name, contents, binary=False, absolute=False):
884
name = Path(name)
885
assert absolute or not name.is_absolute(), name
886
if binary:
887
name.write_bytes(contents)
888
else:
889
# Dedent the contents of text files so that the files on disc all
890
# start in column 1, even if they are indented when embedded in the
891
# python test code.
892
contents = textwrap.dedent(contents)
893
name.write_text(contents, encoding='utf-8')
894
895
896
@contextlib.contextmanager
897
def chdir(dir):
898
"""A context manager that performs actions in the given directory."""
899
orig_cwd = os.getcwd()
900
os.chdir(dir)
901
try:
902
yield
903
finally:
904
os.chdir(orig_cwd)
905
906
907
def make_executable(name):
908
Path(name).chmod(stat.S_IREAD | stat.S_IWRITE | stat.S_IEXEC)
909
910
911
def make_dir_writeable(dirname):
912
# Some tests make files and subdirectories read-only, so rmtree/unlink will not delete
913
# them. Force-make everything writable in the subdirectory to make it
914
# removable and re-attempt.
915
os.chmod(dirname, 0o777)
916
917
for directory, subdirs, files in os.walk(dirname):
918
for item in files + subdirs:
919
i = os.path.join(directory, item)
920
if not os.path.islink(i):
921
os.chmod(i, 0o777)
922
923
924
def force_delete_dir(dirname):
925
make_dir_writeable(dirname)
926
utils.delete_dir(dirname)
927
928
929
def force_delete_contents(dirname):
930
make_dir_writeable(dirname)
931
utils.delete_contents(dirname)
932
933
934
def find_browser_test_file(filename):
935
"""Looks for files in test/browser and then in test/
936
"""
937
if not os.path.exists(filename):
938
fullname = test_file('browser', filename)
939
if not os.path.exists(fullname):
940
fullname = test_file(filename)
941
filename = fullname
942
return filename
943
944
945
def parameterize(func, parameters):
946
"""Add additional parameterization to a test function.
947
948
This function create or adds to the `_parameterize` property of a function
949
which is then expanded by the RunnerMeta metaclass into multiple separate
950
test functions.
951
"""
952
prev = getattr(func, '_parameterize', None)
953
assert not any(p.startswith('_') for p in parameters)
954
if prev:
955
# If we're parameterizing 2nd time, construct a cartesian product for various combinations.
956
func._parameterize = {
957
'_'.join(filter(None, [k1, k2])): v2 + v1 for (k1, v1), (k2, v2) in itertools.product(prev.items(), parameters.items())
958
}
959
else:
960
func._parameterize = parameters
961
962
963
def parameterized(parameters):
964
"""
965
Mark a test as parameterized.
966
967
Usage:
968
@parameterized({
969
'subtest1': (1, 2, 3),
970
'subtest2': (4, 5, 6),
971
})
972
def test_something(self, a, b, c):
973
... # actual test body
974
975
This is equivalent to defining two tests:
976
977
def test_something_subtest1(self):
978
# runs test_something(1, 2, 3)
979
980
def test_something_subtest2(self):
981
# runs test_something(4, 5, 6)
982
"""
983
def decorator(func):
984
parameterize(func, parameters)
985
return func
986
return decorator
987
988
989
def get_output_suffix(args):
990
if any(a in args for a in ('-sEXPORT_ES6', '-sWASM_ESM_INTEGRATION', '-sMODULARIZE=instance')):
991
return '.mjs'
992
else:
993
return '.js'
994
995
996
class RunnerMeta(type):
997
@classmethod
998
def make_test(mcs, name, func, suffix, args):
999
"""
1000
This is a helper function to create new test functions for each parameterized form.
1001
1002
:param name: the original name of the function
1003
:param func: the original function that we are parameterizing
1004
:param suffix: the suffix to append to the name of the function for this parameterization
1005
:param args: the positional arguments to pass to the original function for this parameterization
1006
:returns: a tuple of (new_function_name, new_function_object)
1007
"""
1008
1009
# Create the new test function. It calls the original function with the specified args.
1010
# We use @functools.wraps to copy over all the function attributes.
1011
@wraps(func)
1012
def resulting_test(self):
1013
return func(self, *args)
1014
1015
# Add suffix to the function name so that it displays correctly.
1016
if suffix:
1017
resulting_test.__name__ = f'{name}_{suffix}'
1018
else:
1019
resulting_test.__name__ = name
1020
1021
# On python 3, functions have __qualname__ as well. This is a full dot-separated path to the
1022
# function. We add the suffix to it as well.
1023
resulting_test.__qualname__ = f'{func.__qualname__}_{suffix}'
1024
1025
return resulting_test.__name__, resulting_test
1026
1027
def __new__(mcs, name, bases, attrs):
1028
# This metaclass expands parameterized methods from `attrs` into separate ones in `new_attrs`.
1029
new_attrs = {}
1030
1031
for attr_name, value in attrs.items():
1032
# Check if a member of the new class has _parameterize, the tag inserted by @parameterized.
1033
if hasattr(value, '_parameterize'):
1034
# If it does, we extract the parameterization information, build new test functions.
1035
for suffix, args in value._parameterize.items():
1036
new_name, func = mcs.make_test(attr_name, value, suffix, args)
1037
assert new_name not in new_attrs, 'Duplicate attribute name generated when parameterizing %s' % attr_name
1038
new_attrs[new_name] = func
1039
else:
1040
# If not, we just copy it over to new_attrs verbatim.
1041
assert attr_name not in new_attrs, '%s collided with an attribute from parameterization' % attr_name
1042
new_attrs[attr_name] = value
1043
1044
# We invoke type, the default metaclass, to actually create the new class, with new_attrs.
1045
return type.__new__(mcs, name, bases, new_attrs)
1046
1047
1048
class RunnerCore(unittest.TestCase, metaclass=RunnerMeta):
1049
# default temporary directory settings. set_temp_dir may be called later to
1050
# override these
1051
temp_dir = shared.TEMP_DIR
1052
canonical_temp_dir = get_canonical_temp_dir(shared.TEMP_DIR)
1053
1054
# This avoids cluttering the test runner output, which is stderr too, with compiler warnings etc.
1055
# Change this to None to get stderr reporting, for debugging purposes
1056
stderr_redirect = STDOUT
1057
1058
def is_wasm(self):
1059
return self.get_setting('WASM') != 0
1060
1061
def is_wasm2js(self):
1062
return not self.is_wasm()
1063
1064
def is_browser_test(self):
1065
return False
1066
1067
def is_wasm64(self):
1068
return self.get_setting('MEMORY64')
1069
1070
def is_4gb(self):
1071
return self.get_setting('INITIAL_MEMORY') == '4200mb'
1072
1073
def is_2gb(self):
1074
return self.get_setting('INITIAL_MEMORY') == '2200mb'
1075
1076
def check_dylink(self):
1077
if self.get_setting('WASM_ESM_INTEGRATION'):
1078
self.skipTest('dynamic linking not supported with WASM_ESM_INTEGRATION')
1079
if '-lllvmlibc' in self.cflags:
1080
self.skipTest('dynamic linking not supported with llvm-libc')
1081
if self.is_wasm2js():
1082
self.skipTest('dynamic linking not supported with wasm2js')
1083
if '-fsanitize=undefined' in self.cflags:
1084
self.skipTest('dynamic linking not supported with UBSan')
1085
# MEMORY64=2 mode doesn't currently support dynamic linking because
1086
# The side modules are lowered to wasm32 when they are built, making
1087
# them unlinkable with wasm64 binaries.
1088
if self.get_setting('MEMORY64') == 2:
1089
self.skipTest('dynamic linking not supported with MEMORY64=2')
1090
1091
def get_v8(self):
1092
"""Return v8 engine, if one is configured, otherwise None"""
1093
if not config.V8_ENGINE or config.V8_ENGINE not in config.JS_ENGINES:
1094
return None
1095
return config.V8_ENGINE
1096
1097
def require_v8(self):
1098
v8 = self.get_v8()
1099
if not v8:
1100
if 'EMTEST_SKIP_V8' in os.environ:
1101
self.skipTest('test requires v8 and EMTEST_SKIP_V8 is set')
1102
else:
1103
self.fail('d8 required to run this test. Use EMTEST_SKIP_V8 to skip')
1104
self.require_engine(v8)
1105
self.cflags.append('-sENVIRONMENT=shell')
1106
1107
def get_nodejs(self):
1108
"""Return nodejs engine, if one is configured, otherwise None"""
1109
if config.NODE_JS_TEST not in config.JS_ENGINES:
1110
return None
1111
return config.NODE_JS_TEST
1112
1113
def require_node(self):
1114
nodejs = self.get_nodejs()
1115
if not nodejs:
1116
if 'EMTEST_SKIP_NODE' in os.environ:
1117
self.skipTest('test requires node and EMTEST_SKIP_NODE is set')
1118
else:
1119
self.fail('node required to run this test. Use EMTEST_SKIP_NODE to skip')
1120
self.require_engine(nodejs)
1121
return nodejs
1122
1123
def node_is_canary(self, nodejs):
1124
return nodejs and nodejs[0] and ('canary' in nodejs[0] or 'nightly' in nodejs[0])
1125
1126
def require_node_canary(self):
1127
nodejs = self.get_nodejs()
1128
if self.node_is_canary(nodejs):
1129
self.require_engine(nodejs)
1130
return
1131
1132
if 'EMTEST_SKIP_NODE_CANARY' in os.environ:
1133
self.skipTest('test requires node canary and EMTEST_SKIP_NODE_CANARY is set')
1134
else:
1135
self.fail('node canary required to run this test. Use EMTEST_SKIP_NODE_CANARY to skip')
1136
1137
def require_engine(self, engine):
1138
logger.debug(f'require_engine: {engine}')
1139
if self.required_engine and self.required_engine != engine:
1140
self.skipTest(f'Skipping test that requires `{engine}` when `{self.required_engine}` was previously required')
1141
self.required_engine = engine
1142
self.js_engines = [engine]
1143
self.wasm_engines = []
1144
1145
def require_wasm64(self):
1146
if self.is_browser_test():
1147
return
1148
1149
if self.try_require_node_version(24):
1150
return
1151
1152
v8 = self.get_v8()
1153
if v8:
1154
self.cflags.append('-sENVIRONMENT=shell')
1155
self.js_engines = [v8]
1156
return
1157
1158
if 'EMTEST_SKIP_WASM64' in os.environ:
1159
self.skipTest('test requires node >= 24 or d8 (and EMTEST_SKIP_WASM64 is set)')
1160
else:
1161
self.fail('either d8 or node >= 24 required to run wasm64 tests. Use EMTEST_SKIP_WASM64 to skip')
1162
1163
def try_require_node_version(self, major, minor = 0, revision = 0):
1164
nodejs = self.get_nodejs()
1165
if not nodejs:
1166
self.skipTest('Test requires nodejs to run')
1167
version = shared.get_node_version(nodejs)
1168
if version < (major, minor, revision):
1169
return False
1170
1171
self.js_engines = [nodejs]
1172
return True
1173
1174
def require_simd(self):
1175
if self.is_browser_test():
1176
return
1177
1178
if self.try_require_node_version(16):
1179
return
1180
1181
v8 = self.get_v8()
1182
if v8:
1183
self.cflags.append('-sENVIRONMENT=shell')
1184
self.js_engines = [v8]
1185
return
1186
1187
if 'EMTEST_SKIP_SIMD' in os.environ:
1188
self.skipTest('test requires node >= 16 or d8 (and EMTEST_SKIP_SIMD is set)')
1189
else:
1190
self.fail('either d8 or node >= 16 required to run wasm64 tests. Use EMTEST_SKIP_SIMD to skip')
1191
1192
def require_wasm_legacy_eh(self):
1193
self.set_setting('WASM_LEGACY_EXCEPTIONS')
1194
if self.try_require_node_version(17):
1195
return
1196
1197
v8 = self.get_v8()
1198
if v8:
1199
self.cflags.append('-sENVIRONMENT=shell')
1200
self.js_engines = [v8]
1201
return
1202
1203
if 'EMTEST_SKIP_EH' in os.environ:
1204
self.skipTest('test requires node >= 17 or d8 (and EMTEST_SKIP_EH is set)')
1205
else:
1206
self.fail('either d8 or node >= 17 required to run wasm-eh tests. Use EMTEST_SKIP_EH to skip')
1207
1208
def require_wasm_eh(self):
1209
self.set_setting('WASM_LEGACY_EXCEPTIONS', 0)
1210
if self.try_require_node_version(24):
1211
self.node_args.append('--experimental-wasm-exnref')
1212
return
1213
1214
if self.is_browser_test():
1215
return
1216
1217
v8 = self.get_v8()
1218
if v8:
1219
self.cflags.append('-sENVIRONMENT=shell')
1220
self.js_engines = [v8]
1221
self.v8_args.append('--experimental-wasm-exnref')
1222
return
1223
1224
if 'EMTEST_SKIP_EH' in os.environ:
1225
self.skipTest('test requires node v24 or d8 (and EMTEST_SKIP_EH is set)')
1226
else:
1227
self.fail('either d8 or node v24 required to run wasm-eh tests. Use EMTEST_SKIP_EH to skip')
1228
1229
def require_jspi(self):
1230
# emcc warns about stack switching being experimental, and we build with
1231
# warnings-as-errors, so disable that warning
1232
self.cflags += ['-Wno-experimental']
1233
self.set_setting('JSPI')
1234
if self.is_wasm2js():
1235
self.skipTest('JSPI is not currently supported for WASM2JS')
1236
if self.get_setting('WASM_ESM_INTEGRATION'):
1237
self.skipTest('WASM_ESM_INTEGRATION is not compatible with JSPI')
1238
1239
if self.is_browser_test():
1240
if 'EMTEST_SKIP_JSPI' in os.environ:
1241
self.skipTest('skipping JSPI (EMTEST_SKIP_JSPI is set)')
1242
return
1243
1244
exp_args = ['--experimental-wasm-stack-switching', '--experimental-wasm-type-reflection']
1245
# Support for JSPI came earlier than 22, but the new API changes require v24
1246
if self.try_require_node_version(24):
1247
self.node_args += exp_args
1248
return
1249
1250
v8 = self.get_v8()
1251
if v8:
1252
self.cflags.append('-sENVIRONMENT=shell')
1253
self.js_engines = [v8]
1254
self.v8_args += exp_args
1255
return
1256
1257
if 'EMTEST_SKIP_JSPI' in os.environ:
1258
self.skipTest('test requires node v24 or d8 (and EMTEST_SKIP_JSPI is set)')
1259
else:
1260
self.fail('either d8 or node v24 required to run JSPI tests. Use EMTEST_SKIP_JSPI to skip')
1261
1262
def require_wasm2js(self):
1263
if self.is_wasm64():
1264
self.skipTest('wasm2js is not compatible with MEMORY64')
1265
if self.is_2gb() or self.is_4gb():
1266
self.skipTest('wasm2js does not support over 2gb of memory')
1267
if self.get_setting('WASM_ESM_INTEGRATION'):
1268
self.skipTest('wasm2js is not compatible with WASM_ESM_INTEGRATION')
1269
1270
def setup_nodefs_test(self):
1271
self.require_node()
1272
if self.get_setting('WASMFS'):
1273
# without this the JS setup code in setup_nodefs.js doesn't work
1274
self.set_setting('FORCE_FILESYSTEM')
1275
self.cflags += ['-DNODEFS', '-lnodefs.js', '--pre-js', test_file('setup_nodefs.js'), '-sINCOMING_MODULE_JS_API=onRuntimeInitialized']
1276
1277
def setup_noderawfs_test(self):
1278
self.require_node()
1279
self.cflags += ['-DNODERAWFS']
1280
self.set_setting('NODERAWFS')
1281
1282
def setup_wasmfs_test(self):
1283
self.set_setting('WASMFS')
1284
self.cflags += ['-DWASMFS']
1285
1286
def setup_node_pthreads(self):
1287
self.require_node()
1288
self.cflags += ['-Wno-pthreads-mem-growth', '-pthread']
1289
if self.get_setting('MINIMAL_RUNTIME'):
1290
self.skipTest('node pthreads not yet supported with MINIMAL_RUNTIME')
1291
nodejs = self.get_nodejs()
1292
self.js_engines = [nodejs]
1293
self.node_args += shared.node_pthread_flags(nodejs)
1294
1295
def set_temp_dir(self, temp_dir):
1296
self.temp_dir = temp_dir
1297
self.canonical_temp_dir = get_canonical_temp_dir(self.temp_dir)
1298
# Explicitly set dedicated temporary directory for parallel tests
1299
os.environ['EMCC_TEMP_DIR'] = self.temp_dir
1300
1301
@classmethod
1302
def setUpClass(cls):
1303
super().setUpClass()
1304
shared.check_sanity(force=True, quiet=True)
1305
1306
def setUp(self):
1307
super().setUp()
1308
self.js_engines = config.JS_ENGINES.copy()
1309
self.settings_mods = {}
1310
self.skip_exec = None
1311
self.cflags = ['-Wclosure', '-Werror', '-Wno-limited-postlink-optimizations']
1312
# TODO(https://github.com/emscripten-core/emscripten/issues/11121)
1313
# For historical reasons emcc compiles and links as C++ by default.
1314
# However we want to run our tests in a more strict manner. We can
1315
# remove this if the issue above is ever fixed.
1316
self.set_setting('NO_DEFAULT_TO_CXX')
1317
self.ldflags = []
1318
# Increate stack trace limit to maximise usefulness of test failure reports
1319
self.node_args = ['--stack-trace-limit=50']
1320
self.spidermonkey_args = ['-w']
1321
1322
nodejs = self.get_nodejs()
1323
if nodejs:
1324
node_version = shared.get_node_version(nodejs)
1325
if node_version < (11, 0, 0):
1326
self.node_args.append('--unhandled-rejections=strict')
1327
self.node_args.append('--experimental-wasm-se')
1328
else:
1329
# Include backtrace for all uncuaght exceptions (not just Error).
1330
self.node_args.append('--trace-uncaught')
1331
if node_version < (15, 0, 0):
1332
# Opt in to node v15 default behaviour:
1333
# https://nodejs.org/api/cli.html#cli_unhandled_rejections_mode
1334
self.node_args.append('--unhandled-rejections=throw')
1335
self.node_args += node_bigint_flags(node_version)
1336
1337
# If the version we are running tests in is lower than the version that
1338
# emcc targets then we need to tell emcc to target that older version.
1339
emcc_min_node_version_str = str(shared.settings.MIN_NODE_VERSION)
1340
emcc_min_node_version = (
1341
int(emcc_min_node_version_str[0:2]),
1342
int(emcc_min_node_version_str[2:4]),
1343
int(emcc_min_node_version_str[4:6]),
1344
)
1345
if node_version < emcc_min_node_version:
1346
self.cflags += building.get_emcc_node_flags(node_version)
1347
self.cflags.append('-Wno-transpile')
1348
1349
# This allows much of the test suite to be run on older versions of node that don't
1350
# support wasm bigint integration
1351
if node_version[0] < feature_matrix.min_browser_versions[feature_matrix.Feature.JS_BIGINT_INTEGRATION]['node'] / 10000:
1352
self.cflags.append('-sWASM_BIGINT=0')
1353
1354
self.v8_args = ['--wasm-staging']
1355
self.env = {}
1356
self.temp_files_before_run = []
1357
self.required_engine = None
1358
self.wasm_engines = config.WASM_ENGINES.copy()
1359
self.use_all_engines = EMTEST_ALL_ENGINES
1360
if self.get_current_js_engine() != config.NODE_JS_TEST:
1361
# If our primary JS engine is something other than node then enable
1362
# shell support.
1363
default_envs = 'web,webview,worker,node'
1364
self.set_setting('ENVIRONMENT', default_envs + ',shell')
1365
1366
if EMTEST_DETECT_TEMPFILE_LEAKS:
1367
for root, dirnames, filenames in os.walk(self.temp_dir):
1368
for dirname in dirnames:
1369
self.temp_files_before_run.append(os.path.normpath(os.path.join(root, dirname)))
1370
for filename in filenames:
1371
self.temp_files_before_run.append(os.path.normpath(os.path.join(root, filename)))
1372
1373
if self.runningInParallel():
1374
self.working_dir = tempfile.mkdtemp(prefix='emscripten_test_' + self.__class__.__name__ + '_', dir=self.temp_dir)
1375
else:
1376
self.working_dir = path_from_root('out/test')
1377
if os.path.exists(self.working_dir):
1378
if EMTEST_SAVE_DIR == 2:
1379
print('Not clearing existing test directory')
1380
else:
1381
logger.debug('Clearing existing test directory: %s', self.working_dir)
1382
# Even when --save-dir is used we still try to start with an empty directory as many tests
1383
# expect this. --no-clean can be used to keep the old contents for the new test
1384
# run. This can be useful when iterating on a given test with extra files you want to keep
1385
# around in the output directory.
1386
force_delete_contents(self.working_dir)
1387
else:
1388
logger.debug('Creating new test output directory: %s', self.working_dir)
1389
ensure_dir(self.working_dir)
1390
utils.write_file(LAST_TEST, self.id() + '\n')
1391
os.chdir(self.working_dir)
1392
1393
def runningInParallel(self):
1394
return getattr(self, 'is_parallel', False)
1395
1396
def tearDown(self):
1397
if self.runningInParallel() and not EMTEST_SAVE_DIR:
1398
# rmtree() fails on Windows if the current working directory is inside the tree.
1399
os.chdir(os.path.dirname(self.get_dir()))
1400
force_delete_dir(self.get_dir())
1401
1402
if EMTEST_DETECT_TEMPFILE_LEAKS and not DEBUG:
1403
temp_files_after_run = []
1404
for root, dirnames, filenames in os.walk(self.temp_dir):
1405
for dirname in dirnames:
1406
temp_files_after_run.append(os.path.normpath(os.path.join(root, dirname)))
1407
for filename in filenames:
1408
temp_files_after_run.append(os.path.normpath(os.path.join(root, filename)))
1409
1410
# Our leak detection will pick up *any* new temp files in the temp dir.
1411
# They may not be due to us, but e.g. the browser when running browser
1412
# tests. Until we figure out a proper solution, ignore some temp file
1413
# names that we see on our CI infrastructure.
1414
ignorable_file_prefixes = [
1415
'/tmp/tmpaddon',
1416
'/tmp/circleci-no-output-timeout',
1417
'/tmp/wasmer',
1418
]
1419
1420
left_over_files = set(temp_files_after_run) - set(self.temp_files_before_run)
1421
left_over_files = [f for f in left_over_files if not any(f.startswith(p) for p in ignorable_file_prefixes)]
1422
if len(left_over_files):
1423
print('ERROR: After running test, there are ' + str(len(left_over_files)) + ' new temporary files/directories left behind:', file=sys.stderr)
1424
for f in left_over_files:
1425
print('leaked file: ' + f, file=sys.stderr)
1426
self.fail('Test leaked ' + str(len(left_over_files)) + ' temporary files!')
1427
1428
def get_setting(self, key, default=None):
1429
return self.settings_mods.get(key, default)
1430
1431
def set_setting(self, key, value=1):
1432
if value is None:
1433
self.clear_setting(key)
1434
if type(value) is bool:
1435
value = int(value)
1436
self.settings_mods[key] = value
1437
1438
def has_changed_setting(self, key):
1439
return key in self.settings_mods
1440
1441
def clear_setting(self, key):
1442
self.settings_mods.pop(key, None)
1443
1444
def serialize_settings(self, compile_only=False):
1445
ret = []
1446
for key, value in self.settings_mods.items():
1447
if compile_only and key not in COMPILE_TIME_SETTINGS:
1448
continue
1449
if value == 1:
1450
ret.append(f'-s{key}')
1451
elif type(value) is list:
1452
ret.append(f'-s{key}={",".join(value)}')
1453
else:
1454
ret.append(f'-s{key}={value}')
1455
return ret
1456
1457
def get_dir(self):
1458
return self.working_dir
1459
1460
def in_dir(self, *pathelems):
1461
return os.path.join(self.get_dir(), *pathelems)
1462
1463
def add_pre_run(self, code):
1464
assert not self.get_setting('MINIMAL_RUNTIME')
1465
create_file('prerun.js', 'Module.preRun = function() { %s }\n' % code)
1466
self.cflags += ['--pre-js', 'prerun.js', '-sINCOMING_MODULE_JS_API=preRun']
1467
1468
def add_post_run(self, code):
1469
assert not self.get_setting('MINIMAL_RUNTIME')
1470
create_file('postrun.js', 'Module.postRun = function() { %s }\n' % code)
1471
self.cflags += ['--pre-js', 'postrun.js', '-sINCOMING_MODULE_JS_API=postRun']
1472
1473
def add_on_exit(self, code):
1474
assert not self.get_setting('MINIMAL_RUNTIME')
1475
create_file('onexit.js', 'Module.onExit = function() { %s }\n' % code)
1476
self.cflags += ['--pre-js', 'onexit.js', '-sINCOMING_MODULE_JS_API=onExit']
1477
1478
# returns the full list of arguments to pass to emcc
1479
# param @main_file whether this is the main file of the test. some arguments
1480
# (like --pre-js) do not need to be passed when building
1481
# libraries, for example
1482
def get_cflags(self, main_file=False, compile_only=False, asm_only=False):
1483
def is_ldflag(f):
1484
return f.startswith('-l') or any(f.startswith(s) for s in ['-sEXPORT_ES6', '--proxy-to-worker', '-sGL_TESTING', '-sPROXY_TO_WORKER', '-sPROXY_TO_PTHREAD', '-sENVIRONMENT=', '--pre-js=', '--post-js=', '-sPTHREAD_POOL_SIZE='])
1485
1486
args = self.serialize_settings(compile_only or asm_only) + self.cflags
1487
if asm_only:
1488
args = [a for a in args if not a.startswith('-O')]
1489
if compile_only or asm_only:
1490
args = [a for a in args if not is_ldflag(a)]
1491
else:
1492
args += self.ldflags
1493
if not main_file:
1494
for i, arg in enumerate(args):
1495
if arg in ('--pre-js', '--post-js'):
1496
args[i] = None
1497
args[i + 1] = None
1498
args = [arg for arg in args if arg is not None]
1499
return args
1500
1501
def verify_es5(self, filename):
1502
es_check = shared.get_npm_cmd('es-check')
1503
# use --quiet once its available
1504
# See: https://github.com/dollarshaveclub/es-check/pull/126/
1505
es_check_env = os.environ.copy()
1506
# Use NODE_JS here (the version of node that the compiler uses) rather then NODE_JS_TEST (the
1507
# version of node being used to run the tests) since we only care about having something that
1508
# can run the es-check tool.
1509
es_check_env['PATH'] = os.path.dirname(config.NODE_JS[0]) + os.pathsep + es_check_env['PATH']
1510
inputfile = os.path.abspath(filename)
1511
# For some reason es-check requires unix paths, even on windows
1512
if WINDOWS:
1513
inputfile = utils.normalize_path(inputfile)
1514
try:
1515
# es-check prints the details of the errors to stdout, but it also prints
1516
# stuff in the case there are no errors:
1517
# ES-Check: there were no ES version matching errors!
1518
# pipe stdout and stderr so that we can choose if/when to print this
1519
# output and avoid spamming stdout when tests are successful.
1520
shared.run_process(es_check + ['es5', inputfile], stdout=PIPE, stderr=STDOUT, env=es_check_env)
1521
except subprocess.CalledProcessError as e:
1522
print(e.stdout)
1523
self.fail('es-check failed to verify ES5 output compliance')
1524
1525
# Build JavaScript code from source code
1526
def build(self, filename, libraries=None, includes=None, force_c=False, cflags=None, output_basename=None, output_suffix=None):
1527
if not os.path.exists(filename):
1528
filename = test_file(filename)
1529
compiler = [compiler_for(filename, force_c)]
1530
1531
if force_c:
1532
assert shared.suffix(filename) != '.c', 'force_c is not needed for source files ending in .c'
1533
compiler.append('-xc')
1534
1535
all_cflags = self.get_cflags(main_file=True)
1536
if cflags:
1537
all_cflags += cflags
1538
if not output_suffix:
1539
output_suffix = get_output_suffix(all_cflags)
1540
1541
if output_basename:
1542
output = output_basename + output_suffix
1543
else:
1544
output = shared.unsuffixed_basename(filename) + output_suffix
1545
cmd = compiler + [str(filename), '-o', output] + all_cflags
1546
if libraries:
1547
cmd += libraries
1548
if includes:
1549
cmd += ['-I' + str(include) for include in includes]
1550
1551
self.run_process(cmd, stderr=self.stderr_redirect if not DEBUG else None)
1552
self.assertExists(output)
1553
1554
if output_suffix in ('.js', '.mjs'):
1555
# Make sure we produced correct line endings
1556
self.assertEqual(line_endings.check_line_endings(output), 0)
1557
1558
return output
1559
1560
def get_func(self, src, name):
1561
start = src.index('function ' + name + '(')
1562
t = start
1563
n = 0
1564
while True:
1565
if src[t] == '{':
1566
n += 1
1567
elif src[t] == '}':
1568
n -= 1
1569
if n == 0:
1570
return src[start:t + 1]
1571
t += 1
1572
assert t < len(src)
1573
1574
def count_funcs(self, javascript_file):
1575
num_funcs = 0
1576
start_tok = "// EMSCRIPTEN_START_FUNCS"
1577
end_tok = "// EMSCRIPTEN_END_FUNCS"
1578
start_off = 0
1579
end_off = 0
1580
1581
js = read_file(javascript_file)
1582
blob = "".join(js.splitlines())
1583
1584
start_off = blob.find(start_tok) + len(start_tok)
1585
end_off = blob.find(end_tok)
1586
asm_chunk = blob[start_off:end_off]
1587
num_funcs = asm_chunk.count('function ')
1588
return num_funcs
1589
1590
def count_wasm_contents(self, wasm_binary, what):
1591
out = self.run_process([os.path.join(building.get_binaryen_bin(), 'wasm-opt'), wasm_binary, '--metrics'], stdout=PIPE).stdout
1592
# output is something like
1593
# [?] : 125
1594
for line in out.splitlines():
1595
if '[' + what + ']' in line:
1596
ret = line.split(':')[1].strip()
1597
return int(ret)
1598
self.fail('Failed to find [%s] in wasm-opt output' % what)
1599
1600
def get_wasm_text(self, wasm_binary):
1601
return self.run_process([WASM_DIS, wasm_binary], stdout=PIPE).stdout
1602
1603
def is_exported_in_wasm(self, name, wasm):
1604
wat = self.get_wasm_text(wasm)
1605
return ('(export "%s"' % name) in wat
1606
1607
def measure_wasm_code_lines(self, wasm):
1608
wat_lines = self.get_wasm_text(wasm).splitlines()
1609
non_data_lines = [line for line in wat_lines if '(data ' not in line]
1610
return len(non_data_lines)
1611
1612
def clean_js_output(self, output):
1613
"""Cleaup the JS output prior to running verification steps on it.
1614
1615
Due to minification, when we get a crash report from JS it can sometimes
1616
contains the entire program in the output (since the entire program is
1617
on a single line). In this case we can sometimes get false positives
1618
when checking for strings in the output. To avoid these false positives
1619
and the make the output easier to read in such cases we attempt to remove
1620
such lines from the JS output.
1621
"""
1622
lines = output.splitlines()
1623
long_lines = []
1624
1625
def cleanup(line):
1626
if len(line) > 2048 and line.startswith('var Module=typeof Module!="undefined"'):
1627
long_lines.append(line)
1628
line = '<REPLACED ENTIRE PROGRAM ON SINGLE LINE>'
1629
return line
1630
1631
lines = [cleanup(l) for l in lines]
1632
if not long_lines:
1633
# No long lines found just return the unmodified output
1634
return output
1635
1636
# Sanity check that we only a single long line.
1637
assert len(long_lines) == 1
1638
return '\n'.join(lines)
1639
1640
def get_current_js_engine(self):
1641
"""Return the default JS engine to run tests under"""
1642
return self.js_engines[0]
1643
1644
def get_engine_with_args(self, engine=None):
1645
if not engine:
1646
engine = self.get_current_js_engine()
1647
# Make a copy of the engine command before we modify/extend it.
1648
engine = list(engine)
1649
if engine == config.NODE_JS_TEST:
1650
engine += self.node_args
1651
elif engine == config.V8_ENGINE:
1652
engine += self.v8_args
1653
elif engine == config.SPIDERMONKEY_ENGINE:
1654
engine += self.spidermonkey_args
1655
return engine
1656
1657
def run_js(self, filename, engine=None, args=None,
1658
assert_returncode=0,
1659
interleaved_output=True,
1660
input=None):
1661
# use files, as PIPE can get too full and hang us
1662
stdout_file = self.in_dir('stdout')
1663
stderr_file = None
1664
if interleaved_output:
1665
stderr = STDOUT
1666
else:
1667
stderr_file = self.in_dir('stderr')
1668
stderr = open(stderr_file, 'w')
1669
stdout = open(stdout_file, 'w')
1670
error = None
1671
timeout_error = None
1672
engine = self.get_engine_with_args(engine)
1673
try:
1674
jsrun.run_js(filename, engine, args,
1675
stdout=stdout,
1676
stderr=stderr,
1677
assert_returncode=assert_returncode,
1678
input=input)
1679
except subprocess.TimeoutExpired as e:
1680
timeout_error = e
1681
except subprocess.CalledProcessError as e:
1682
error = e
1683
finally:
1684
stdout.close()
1685
if stderr != STDOUT:
1686
stderr.close()
1687
1688
ret = read_file(stdout_file)
1689
if not interleaved_output:
1690
ret += read_file(stderr_file)
1691
if assert_returncode != 0:
1692
ret = self.clean_js_output(ret)
1693
if error or timeout_error or EMTEST_VERBOSE:
1694
print('-- begin program output --')
1695
print(limit_size(read_file(stdout_file)), end='')
1696
print('-- end program output --')
1697
if not interleaved_output:
1698
print('-- begin program stderr --')
1699
print(limit_size(read_file(stderr_file)), end='')
1700
print('-- end program stderr --')
1701
if timeout_error:
1702
raise timeout_error
1703
if error:
1704
ret = limit_size(ret)
1705
if assert_returncode == NON_ZERO:
1706
self.fail('JS subprocess unexpectedly succeeded (%s): Output:\n%s' % (error.cmd, ret))
1707
else:
1708
self.fail('JS subprocess failed (%s): %s (expected=%s). Output:\n%s' % (error.cmd, error.returncode, assert_returncode, ret))
1709
1710
return ret
1711
1712
def assertExists(self, filename, msg=None):
1713
if not msg:
1714
msg = f'Expected file not found: {filename}'
1715
self.assertTrue(os.path.exists(filename), msg)
1716
1717
def assertNotExists(self, filename, msg=None):
1718
if not msg:
1719
msg = 'Unexpected file exists: ' + filename
1720
self.assertFalse(os.path.exists(filename), msg)
1721
1722
# Tests that the given two paths are identical, modulo path delimiters. E.g. "C:/foo" is equal to "C:\foo".
1723
def assertPathsIdentical(self, path1, path2):
1724
path1 = utils.normalize_path(path1)
1725
path2 = utils.normalize_path(path2)
1726
return self.assertIdentical(path1, path2)
1727
1728
# Tests that the given two multiline text content are identical, modulo line
1729
# ending differences (\r\n on Windows, \n on Unix).
1730
def assertTextDataIdentical(self, text1, text2, msg=None,
1731
fromfile='expected', tofile='actual'):
1732
text1 = text1.replace('\r\n', '\n')
1733
text2 = text2.replace('\r\n', '\n')
1734
return self.assertIdentical(text1, text2, msg, fromfile, tofile)
1735
1736
def assertIdentical(self, values, y, msg=None,
1737
fromfile='expected', tofile='actual'):
1738
if type(values) not in (list, tuple):
1739
values = [values]
1740
for x in values:
1741
if x == y:
1742
return # success
1743
diff_lines = difflib.unified_diff(x.splitlines(), y.splitlines(),
1744
fromfile=fromfile, tofile=tofile)
1745
diff = ''.join([a.rstrip() + '\n' for a in diff_lines])
1746
if EMTEST_VERBOSE:
1747
print("Expected to have '%s' == '%s'" % (values[0], y))
1748
else:
1749
diff = limit_size(diff)
1750
diff += '\nFor full output run with --verbose.'
1751
fail_message = 'Unexpected difference:\n' + diff
1752
if msg:
1753
fail_message += '\n' + msg
1754
self.fail(fail_message)
1755
1756
def assertTextDataContained(self, text1, text2):
1757
text1 = text1.replace('\r\n', '\n')
1758
text2 = text2.replace('\r\n', '\n')
1759
return self.assertContained(text1, text2)
1760
1761
def assertFileContents(self, filename, contents):
1762
if EMTEST_VERBOSE:
1763
print(f'Comparing results contents of file: {filename}')
1764
1765
contents = contents.replace('\r', '')
1766
1767
if EMTEST_REBASELINE:
1768
with open(filename, 'w') as f:
1769
f.write(contents)
1770
return
1771
1772
if not os.path.exists(filename):
1773
self.fail('Test expectation file not found: ' + filename + '.\n' +
1774
'Run with --rebaseline to generate.')
1775
expected_content = read_file(filename)
1776
message = "Run with --rebaseline to automatically update expectations"
1777
self.assertTextDataIdentical(expected_content, contents, message,
1778
filename, filename + '.new')
1779
1780
def assertContained(self, values, string, additional_info='', regex=False):
1781
if callable(string):
1782
string = string()
1783
1784
if regex:
1785
if type(values) is str:
1786
self.assertTrue(re.search(values, string, re.DOTALL), 'Expected regex "%s" to match on:\n%s' % (values, string))
1787
else:
1788
match_any = any(re.search(o, string, re.DOTALL) for o in values)
1789
self.assertTrue(match_any, 'Expected at least one of "%s" to match on:\n%s' % (values, string))
1790
return
1791
1792
if type(values) not in [list, tuple]:
1793
values = [values]
1794
1795
if not any(v in string for v in values):
1796
diff = difflib.unified_diff(values[0].split('\n'), string.split('\n'), fromfile='expected', tofile='actual')
1797
diff = ''.join(a.rstrip() + '\n' for a in diff)
1798
self.fail("Expected to find '%s' in '%s', diff:\n\n%s\n%s" % (
1799
limit_size(values[0]), limit_size(string), limit_size(diff),
1800
additional_info,
1801
))
1802
1803
def assertNotContained(self, value, string):
1804
if callable(value):
1805
value = value() # lazy loading
1806
if callable(string):
1807
string = string()
1808
if value in string:
1809
self.fail("Expected to NOT find '%s' in '%s'" % (limit_size(value), limit_size(string)))
1810
1811
def assertContainedIf(self, value, string, condition):
1812
if condition:
1813
self.assertContained(value, string)
1814
else:
1815
self.assertNotContained(value, string)
1816
1817
def assertBinaryEqual(self, file1, file2):
1818
self.assertEqual(os.path.getsize(file1),
1819
os.path.getsize(file2))
1820
self.assertEqual(read_binary(file1),
1821
read_binary(file2))
1822
1823
library_cache: Dict[str, Tuple[str, object]] = {}
1824
1825
def get_build_dir(self):
1826
ret = self.in_dir('building')
1827
ensure_dir(ret)
1828
return ret
1829
1830
def get_library(self, name, generated_libs, configure=['sh', './configure'], # noqa
1831
configure_args=None, make=None, make_args=None,
1832
env_init=None, cache_name_extra='', native=False,
1833
force_rebuild=False):
1834
if make is None:
1835
make = ['make']
1836
if env_init is None:
1837
env_init = {}
1838
if make_args is None:
1839
make_args = ['-j', str(shared.get_num_cores())]
1840
1841
build_dir = self.get_build_dir()
1842
1843
cflags = []
1844
if not native:
1845
# get_library() is used to compile libraries, and not link executables,
1846
# so we don't want to pass linker flags here (emscripten warns if you
1847
# try to pass linker settings when compiling).
1848
cflags = self.get_cflags(compile_only=True)
1849
1850
hash_input = (str(cflags) + ' $ ' + str(env_init)).encode('utf-8')
1851
cache_name = name + ','.join([opt for opt in cflags if len(opt) < 7]) + '_' + hashlib.md5(hash_input).hexdigest() + cache_name_extra
1852
1853
valid_chars = "_%s%s" % (string.ascii_letters, string.digits)
1854
cache_name = ''.join([(c if c in valid_chars else '_') for c in cache_name])
1855
1856
if not force_rebuild and self.library_cache.get(cache_name):
1857
print('<load %s from cache> ' % cache_name, file=sys.stderr)
1858
generated_libs = []
1859
for basename, contents in self.library_cache[cache_name]:
1860
bc_file = os.path.join(build_dir, cache_name + '_' + basename)
1861
write_binary(bc_file, contents)
1862
generated_libs.append(bc_file)
1863
return generated_libs
1864
1865
print(f'<building and saving {cache_name} into cache>', file=sys.stderr)
1866
if configure and configure_args:
1867
# Make to copy to avoid mutating default param
1868
configure = list(configure)
1869
configure += configure_args
1870
1871
cflags = ' '.join(cflags)
1872
env_init.setdefault('CFLAGS', cflags)
1873
env_init.setdefault('CXXFLAGS', cflags)
1874
return build_library(name, build_dir, generated_libs, configure,
1875
make, make_args, self.library_cache,
1876
cache_name, env_init=env_init, native=native)
1877
1878
def clear(self):
1879
force_delete_contents(self.get_dir())
1880
if shared.EMSCRIPTEN_TEMP_DIR:
1881
utils.delete_contents(shared.EMSCRIPTEN_TEMP_DIR)
1882
1883
def run_process(self, cmd, check=True, **kwargs):
1884
# Wrapper around shared.run_process. This is desirable so that the tests
1885
# can fail (in the unittest sense) rather than error'ing.
1886
# In the long run it would nice to completely remove the dependency on
1887
# core emscripten code (shared.py) here.
1888
1889
# Handle buffering for subprocesses. The python unittest buffering mechanism
1890
# will only buffer output from the current process (by overwriding sys.stdout
1891
# and sys.stderr), not from sub-processes.
1892
stdout_buffering = 'stdout' not in kwargs and isinstance(sys.stdout, io.StringIO)
1893
stderr_buffering = 'stderr' not in kwargs and isinstance(sys.stderr, io.StringIO)
1894
if stdout_buffering:
1895
kwargs['stdout'] = PIPE
1896
if stderr_buffering:
1897
kwargs['stderr'] = PIPE
1898
1899
try:
1900
rtn = shared.run_process(cmd, check=check, **kwargs)
1901
except subprocess.CalledProcessError as e:
1902
if check and e.returncode != 0:
1903
print(e.stdout)
1904
print(e.stderr)
1905
self.fail(f'subprocess exited with non-zero return code({e.returncode}): `{shlex.join(cmd)}`')
1906
1907
if stdout_buffering:
1908
sys.stdout.write(rtn.stdout)
1909
if stderr_buffering:
1910
sys.stderr.write(rtn.stderr)
1911
return rtn
1912
1913
def emcc(self, filename, args=[], output_filename=None, **kwargs): # noqa
1914
compile_only = '-c' in args or '-sSIDE_MODULE' in args
1915
cmd = [compiler_for(filename), filename] + self.get_cflags(compile_only=compile_only) + args
1916
if output_filename:
1917
cmd += ['-o', output_filename]
1918
self.run_process(cmd, **kwargs)
1919
1920
# Shared test code between main suite and others
1921
1922
def expect_fail(self, cmd, expect_traceback=False, **args):
1923
"""Run a subprocess and assert that it returns non-zero.
1924
1925
Return the stderr of the subprocess.
1926
"""
1927
proc = self.run_process(cmd, check=False, stderr=PIPE, **args)
1928
self.assertNotEqual(proc.returncode, 0, 'subprocess unexpectedly succeeded. stderr:\n' + proc.stderr)
1929
# When we check for failure we expect a user-visible error, not a traceback.
1930
# However, on windows a python traceback can happen randomly sometimes,
1931
# due to "Access is denied" https://github.com/emscripten-core/emscripten/issues/718
1932
if expect_traceback:
1933
self.assertContained('Traceback', proc.stderr)
1934
elif not WINDOWS or 'Access is denied' not in proc.stderr:
1935
self.assertNotContained('Traceback', proc.stderr)
1936
if EMTEST_VERBOSE:
1937
sys.stderr.write(proc.stderr)
1938
return proc.stderr
1939
1940
# excercise dynamic linker.
1941
#
1942
# test that linking to shared library B, which is linked to A, loads A as well.
1943
# main is also linked to C, which is also linked to A. A is loaded/initialized only once.
1944
#
1945
# B
1946
# main < > A
1947
# C
1948
#
1949
# this test is used by both test_core and test_browser.
1950
# when run under browser it excercises how dynamic linker handles concurrency
1951
# - because B and C are loaded in parallel.
1952
def _test_dylink_dso_needed(self, do_run):
1953
create_file('liba.cpp', r'''
1954
#include <stdio.h>
1955
#include <emscripten.h>
1956
1957
static const char *afunc_prev;
1958
1959
extern "C" {
1960
EMSCRIPTEN_KEEPALIVE void afunc(const char *s);
1961
}
1962
1963
void afunc(const char *s) {
1964
printf("a: %s (prev: %s)\n", s, afunc_prev);
1965
afunc_prev = s;
1966
}
1967
1968
struct ainit {
1969
ainit() {
1970
puts("a: loaded");
1971
}
1972
};
1973
1974
static ainit _;
1975
''')
1976
1977
create_file('libb.c', r'''
1978
#include <emscripten.h>
1979
1980
void afunc(const char *s);
1981
1982
EMSCRIPTEN_KEEPALIVE void bfunc() {
1983
afunc("b");
1984
}
1985
''')
1986
1987
create_file('libc.c', r'''
1988
#include <emscripten.h>
1989
1990
void afunc(const char *s);
1991
1992
EMSCRIPTEN_KEEPALIVE void cfunc() {
1993
afunc("c");
1994
}
1995
''')
1996
1997
# _test_dylink_dso_needed can be potentially called several times by a test.
1998
# reset dylink-related options first.
1999
self.clear_setting('MAIN_MODULE')
2000
self.clear_setting('SIDE_MODULE')
2001
2002
# XXX in wasm each lib load currently takes 5MB; default INITIAL_MEMORY=16MB is thus not enough
2003
if not self.has_changed_setting('INITIAL_MEMORY'):
2004
self.set_setting('INITIAL_MEMORY', '32mb')
2005
2006
so = '.wasm' if self.is_wasm() else '.js'
2007
2008
def ccshared(src, linkto=None):
2009
cmdv = [EMCC, src, '-o', shared.unsuffixed(src) + so, '-sSIDE_MODULE'] + self.get_cflags()
2010
if linkto:
2011
cmdv += linkto
2012
self.run_process(cmdv)
2013
2014
ccshared('liba.cpp')
2015
ccshared('libb.c', ['liba' + so])
2016
ccshared('libc.c', ['liba' + so])
2017
2018
self.set_setting('MAIN_MODULE')
2019
extra_args = ['-L.', 'libb' + so, 'libc' + so]
2020
do_run(r'''
2021
#ifdef __cplusplus
2022
extern "C" {
2023
#endif
2024
void bfunc();
2025
void cfunc();
2026
#ifdef __cplusplus
2027
}
2028
#endif
2029
2030
int test_main() {
2031
bfunc();
2032
cfunc();
2033
return 0;
2034
}
2035
''',
2036
'a: loaded\na: b (prev: (null))\na: c (prev: b)\n', cflags=extra_args)
2037
2038
extra_args = []
2039
for libname in ('liba', 'libb', 'libc'):
2040
extra_args += ['--embed-file', libname + so]
2041
do_run(r'''
2042
#include <assert.h>
2043
#include <dlfcn.h>
2044
#include <stddef.h>
2045
2046
int test_main() {
2047
void *bdso, *cdso;
2048
void (*bfunc_ptr)(), (*cfunc_ptr)();
2049
2050
// FIXME for RTLD_LOCAL binding symbols to loaded lib is not currently working
2051
bdso = dlopen("libb%(so)s", RTLD_NOW|RTLD_GLOBAL);
2052
assert(bdso != NULL);
2053
cdso = dlopen("libc%(so)s", RTLD_NOW|RTLD_GLOBAL);
2054
assert(cdso != NULL);
2055
2056
bfunc_ptr = (void (*)())dlsym(bdso, "bfunc");
2057
assert(bfunc_ptr != NULL);
2058
cfunc_ptr = (void (*)())dlsym(cdso, "cfunc");
2059
assert(cfunc_ptr != NULL);
2060
2061
bfunc_ptr();
2062
cfunc_ptr();
2063
return 0;
2064
}
2065
''' % locals(),
2066
'a: loaded\na: b (prev: (null))\na: c (prev: b)\n', cflags=extra_args)
2067
2068
def do_run(self, src, expected_output=None, force_c=False, **kwargs):
2069
if 'no_build' in kwargs:
2070
filename = src
2071
else:
2072
if force_c:
2073
filename = 'src.c'
2074
else:
2075
filename = 'src.cpp'
2076
create_file(filename, src)
2077
return self._build_and_run(filename, expected_output, **kwargs)
2078
2079
def do_runf(self, filename, expected_output=None, **kwargs):
2080
return self._build_and_run(filename, expected_output, **kwargs)
2081
2082
def do_run_in_out_file_test(self, srcfile, **kwargs):
2083
if not os.path.exists(srcfile):
2084
srcfile = test_file(srcfile)
2085
out_suffix = kwargs.pop('out_suffix', '')
2086
outfile = shared.unsuffixed(srcfile) + out_suffix + '.out'
2087
if EMTEST_REBASELINE:
2088
expected = None
2089
else:
2090
expected = read_file(outfile)
2091
output = self._build_and_run(srcfile, expected, **kwargs)
2092
if EMTEST_REBASELINE:
2093
utils.write_file(outfile, output)
2094
return output
2095
2096
## Does a complete test - builds, runs, checks output, etc.
2097
def _build_and_run(self, filename, expected_output, args=None,
2098
no_build=False,
2099
assert_returncode=0, assert_identical=False, assert_all=False,
2100
check_for_error=True,
2101
interleaved_output=True,
2102
regex=False,
2103
input=None,
2104
**kwargs):
2105
logger.debug(f'_build_and_run: {filename}')
2106
2107
if no_build:
2108
js_file = filename
2109
else:
2110
js_file = self.build(filename, **kwargs)
2111
self.assertExists(js_file)
2112
2113
engines = self.js_engines.copy()
2114
if len(engines) > 1 and not self.use_all_engines:
2115
engines = engines[:1]
2116
# In standalone mode, also add wasm vms as we should be able to run there too.
2117
if self.get_setting('STANDALONE_WASM'):
2118
# TODO once standalone wasm support is more stable, apply use_all_engines
2119
# like with js engines, but for now as we bring it up, test in all of them
2120
if not self.wasm_engines:
2121
if 'EMTEST_SKIP_WASM_ENGINE' in os.environ:
2122
self.skipTest('no wasm engine was found to run the standalone part of this test')
2123
else:
2124
logger.warning('no wasm engine was found to run the standalone part of this test (Use EMTEST_SKIP_WASM_ENGINE to skip)')
2125
engines += self.wasm_engines
2126
if len(engines) == 0:
2127
self.fail('No JS engine present to run this test with. Check %s and the paths therein.' % config.EM_CONFIG)
2128
for engine in engines:
2129
js_output = self.run_js(js_file, engine, args,
2130
input=input,
2131
assert_returncode=assert_returncode,
2132
interleaved_output=interleaved_output)
2133
js_output = js_output.replace('\r\n', '\n')
2134
if expected_output:
2135
if type(expected_output) not in [list, tuple]:
2136
expected_output = [expected_output]
2137
try:
2138
if assert_identical:
2139
self.assertIdentical(expected_output, js_output)
2140
elif assert_all or len(expected_output) == 1:
2141
for o in expected_output:
2142
self.assertContained(o, js_output, regex=regex)
2143
else:
2144
self.assertContained(expected_output, js_output, regex=regex)
2145
if assert_returncode == 0 and check_for_error:
2146
self.assertNotContained('ERROR', js_output)
2147
except self.failureException:
2148
print('(test did not pass in JS engine: %s)' % engine)
2149
raise
2150
return js_output
2151
2152
def parallel_stress_test_js_file(self, js_file, assert_returncode=None, expected=None, not_expected=None):
2153
# If no expectations were passed, expect a successful run exit code
2154
if assert_returncode is None and expected is None and not_expected is None:
2155
assert_returncode = 0
2156
2157
# We will use Python multithreading, so prepare the command to run in advance, and keep the threading kernel
2158
# compact to avoid accessing unexpected data/functions across threads.
2159
cmd = self.get_engine_with_args() + [js_file]
2160
2161
exception_thrown = threading.Event()
2162
error_lock = threading.Lock()
2163
error_exception = None
2164
2165
def test_run():
2166
nonlocal error_exception
2167
try:
2168
# Each thread repeatedly runs the test case in a tight loop, which is critical to coax out timing related issues
2169
for _ in range(16):
2170
# Early out from the test, if error was found
2171
if exception_thrown.is_set():
2172
return
2173
result = subprocess.run(cmd, capture_output=True, text=True)
2174
2175
output = f'\n----------------------------\n{result.stdout}{result.stderr}\n----------------------------'
2176
if not_expected is not None and not_expected in output:
2177
raise Exception(f'\n\nWhen running command "{cmd}",\nexpected string "{not_expected}" to NOT be present in output:{output}')
2178
if expected is not None and expected not in output:
2179
raise Exception(f'\n\nWhen running command "{cmd}",\nexpected string "{expected}" was not found in output:{output}')
2180
if assert_returncode is not None:
2181
if assert_returncode == NON_ZERO:
2182
if result.returncode != 0:
2183
raise Exception(f'\n\nCommand "{cmd}" was expected to fail, but did not (returncode=0). Output:{output}')
2184
elif assert_returncode != result.returncode:
2185
raise Exception(f'\n\nWhen running command "{cmd}",\nreturn code {result.returncode} does not match expected return code {assert_returncode}. Output:{output}')
2186
except Exception as e:
2187
if not exception_thrown.is_set():
2188
exception_thrown.set()
2189
with error_lock:
2190
error_exception = e
2191
return
2192
2193
threads = []
2194
# Oversubscribe hardware threads to make sure scheduling becomes erratic
2195
while len(threads) < 2 * multiprocessing.cpu_count() and not exception_thrown.is_set():
2196
threads += [threading.Thread(target=test_run)]
2197
threads[-1].start()
2198
for t in threads:
2199
t.join()
2200
if error_exception:
2201
raise error_exception
2202
2203
def get_freetype_library(self):
2204
self.cflags += [
2205
'-Wno-misleading-indentation',
2206
'-Wno-unused-but-set-variable',
2207
'-Wno-pointer-bool-conversion',
2208
'-Wno-shift-negative-value',
2209
'-Wno-gnu-offsetof-extensions',
2210
# And because gnu-offsetof-extensions is a new warning:
2211
'-Wno-unknown-warning-option',
2212
]
2213
return self.get_library(os.path.join('third_party', 'freetype'),
2214
os.path.join('objs', '.libs', 'libfreetype.a'),
2215
configure_args=['--disable-shared', '--without-zlib'])
2216
2217
def get_poppler_library(self, env_init=None):
2218
freetype = self.get_freetype_library()
2219
2220
self.cflags += [
2221
'-I' + test_file('third_party/freetype/include'),
2222
'-I' + test_file('third_party/poppler/include'),
2223
# Poppler's configure script emits -O2 for gcc, and nothing for other
2224
# compilers, including emcc, so set opts manually.
2225
"-O2",
2226
]
2227
2228
# Poppler has some pretty glaring warning. Suppress them to keep the
2229
# test output readable.
2230
self.cflags += [
2231
'-Wno-sentinel',
2232
'-Wno-logical-not-parentheses',
2233
'-Wno-unused-private-field',
2234
'-Wno-tautological-compare',
2235
'-Wno-unknown-pragmas',
2236
'-Wno-shift-negative-value',
2237
'-Wno-dynamic-class-memaccess',
2238
# The fontconfig symbols are all missing from the poppler build
2239
# e.g. FcConfigSubstitute
2240
'-sERROR_ON_UNDEFINED_SYMBOLS=0',
2241
# Avoid warning about ERROR_ON_UNDEFINED_SYMBOLS being used at compile time
2242
'-Wno-unused-command-line-argument',
2243
'-Wno-js-compiler',
2244
'-Wno-nontrivial-memaccess',
2245
]
2246
env_init = env_init.copy() if env_init else {}
2247
env_init['FONTCONFIG_CFLAGS'] = ' '
2248
env_init['FONTCONFIG_LIBS'] = ' '
2249
2250
poppler = self.get_library(
2251
os.path.join('third_party', 'poppler'),
2252
[os.path.join('utils', 'pdftoppm.o'), os.path.join('utils', 'parseargs.o'), os.path.join('poppler', '.libs', 'libpoppler.a')],
2253
env_init=env_init,
2254
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'])
2255
2256
return poppler + freetype
2257
2258
def get_zlib_library(self, cmake):
2259
assert cmake or not WINDOWS, 'on windows, get_zlib_library only supports cmake'
2260
2261
old_args = self.cflags.copy()
2262
# inflate.c does -1L << 16
2263
self.cflags.append('-Wno-shift-negative-value')
2264
# adler32.c uses K&R sytyle function declarations
2265
self.cflags.append('-Wno-deprecated-non-prototype')
2266
# Work around configure-script error. TODO: remove when
2267
# https://github.com/emscripten-core/emscripten/issues/16908 is fixed
2268
self.cflags.append('-Wno-pointer-sign')
2269
if cmake:
2270
rtn = self.get_library(os.path.join('third_party', 'zlib'), os.path.join('libz.a'),
2271
configure=['cmake', '.'],
2272
make=['cmake', '--build', '.', '--'],
2273
make_args=[])
2274
else:
2275
rtn = self.get_library(os.path.join('third_party', 'zlib'), os.path.join('libz.a'), make_args=['libz.a'])
2276
self.cflags = old_args
2277
return rtn
2278
2279
2280
# Create a server and a web page. When a test runs, we tell the server about it,
2281
# which tells the web page, which then opens a window with the test. Doing
2282
# it this way then allows the page to close() itself when done.
2283
def make_test_server(in_queue, out_queue, port):
2284
class TestServerHandler(SimpleHTTPRequestHandler):
2285
# Request header handler for default do_GET() path in
2286
# SimpleHTTPRequestHandler.do_GET(self) below.
2287
def send_head(self):
2288
if self.headers.get('Range'):
2289
path = self.translate_path(self.path)
2290
try:
2291
fsize = os.path.getsize(path)
2292
f = open(path, 'rb')
2293
except OSError:
2294
self.send_error(404, f'File not found {path}')
2295
return None
2296
self.send_response(206)
2297
ctype = self.guess_type(path)
2298
self.send_header('Content-Type', ctype)
2299
pieces = self.headers.get('Range').split('=')[1].split('-')
2300
start = int(pieces[0]) if pieces[0] != '' else 0
2301
end = int(pieces[1]) if pieces[1] != '' else fsize - 1
2302
end = min(fsize - 1, end)
2303
length = end - start + 1
2304
self.send_header('Content-Range', f'bytes {start}-{end}/{fsize}')
2305
self.send_header('Content-Length', str(length))
2306
self.end_headers()
2307
return f
2308
else:
2309
return SimpleHTTPRequestHandler.send_head(self)
2310
2311
# Add COOP, COEP, CORP, and no-caching headers
2312
def end_headers(self):
2313
self.send_header('Accept-Ranges', 'bytes')
2314
self.send_header('Access-Control-Allow-Origin', '*')
2315
self.send_header('Cross-Origin-Opener-Policy', 'same-origin')
2316
self.send_header('Cross-Origin-Embedder-Policy', 'require-corp')
2317
self.send_header('Cross-Origin-Resource-Policy', 'cross-origin')
2318
self.send_header('Cache-Control', 'no-cache, no-store, must-revalidate')
2319
return SimpleHTTPRequestHandler.end_headers(self)
2320
2321
def do_POST(self):
2322
urlinfo = urlparse(self.path)
2323
query = parse_qs(urlinfo.query)
2324
content_length = int(self.headers['Content-Length'])
2325
post_data = self.rfile.read(content_length)
2326
if urlinfo.path == '/log':
2327
# Logging reported by reportStdoutToServer / reportStderrToServer.
2328
#
2329
# To automatically capture stderr/stdout message from browser tests, modify
2330
# `captureStdoutStderr` in `test/browser_reporting.js`.
2331
filename = query['file'][0]
2332
print(f"[client {filename}: '{post_data.decode()}']")
2333
self.send_response(200)
2334
self.end_headers()
2335
elif urlinfo.path == '/upload':
2336
filename = query['file'][0]
2337
print(f'do_POST: got file: {filename}')
2338
create_file(filename, post_data, binary=True)
2339
self.send_response(200)
2340
self.end_headers()
2341
elif urlinfo.path.startswith('/status/'):
2342
code_str = urlinfo.path[len('/status/'):]
2343
code = int(code_str)
2344
if code in (301, 302, 303, 307, 308):
2345
self.send_response(code)
2346
self.send_header('Location', '/status/200')
2347
self.end_headers()
2348
elif code == 200:
2349
self.send_response(200)
2350
self.send_header('Content-type', 'text/plain')
2351
self.end_headers()
2352
self.wfile.write(b'OK')
2353
else:
2354
self.send_error(400, f'Not implemented for {code}')
2355
else:
2356
print(f'do_POST: unexpected POST: {urlinfo}')
2357
2358
def do_GET(self):
2359
info = urlparse(self.path)
2360
if info.path == '/run_harness':
2361
if DEBUG:
2362
print('[server startup]')
2363
self.send_response(200)
2364
self.send_header('Content-type', 'text/html')
2365
self.end_headers()
2366
self.wfile.write(read_binary(test_file('browser_harness.html')))
2367
elif info.path.startswith('/status/'):
2368
code_str = info.path[len('/status/'):]
2369
code = int(code_str)
2370
if code in (301, 302, 303, 307, 308):
2371
# Redirect to /status/200
2372
self.send_response(code)
2373
self.send_header('Location', '/status/200')
2374
self.end_headers()
2375
elif code == 200:
2376
self.send_response(200)
2377
self.send_header('Content-type', 'text/plain')
2378
self.end_headers()
2379
self.wfile.write(b'OK')
2380
else:
2381
self.send_error(400, f'Not implemented for {code}')
2382
elif 'report_' in self.path:
2383
# for debugging, tests may encode the result and their own url (window.location) as result|url
2384
if '|' in self.path:
2385
path, url = self.path.split('|', 1)
2386
else:
2387
path = self.path
2388
url = '?'
2389
if DEBUG:
2390
print('[server response:', path, url, ']')
2391
if out_queue.empty():
2392
out_queue.put(path)
2393
else:
2394
# a badly-behaving test may send multiple xhrs with reported results; we just care
2395
# about the first (if we queued the others, they might be read as responses for
2396
# later tests, or maybe the test sends more than one in a racy manner).
2397
# we place 'None' in the queue here so that the outside knows something went wrong
2398
# (none is not a valid value otherwise; and we need the outside to know because if we
2399
# raise an error in here, it is just swallowed in python's webserver code - we want
2400
# the test to actually fail, which a webserver response can't do).
2401
out_queue.put(None)
2402
raise Exception('browser harness error, excessive response to server - test must be fixed! "%s"' % self.path)
2403
self.send_response(200)
2404
self.send_header('Content-type', 'text/plain')
2405
self.send_header('Cache-Control', 'no-cache, must-revalidate')
2406
self.send_header('Connection', 'close')
2407
self.send_header('Expires', '-1')
2408
self.end_headers()
2409
self.wfile.write(b'OK')
2410
2411
elif info.path == '/check':
2412
self.send_response(200)
2413
self.send_header('Content-type', 'text/html')
2414
self.end_headers()
2415
if not in_queue.empty():
2416
# there is a new test ready to be served
2417
url, dir = in_queue.get()
2418
if DEBUG:
2419
print('[queue command:', url, dir, ']')
2420
assert in_queue.empty(), 'should not be any blockage - one test runs at a time'
2421
assert out_queue.empty(), 'the single response from the last test was read'
2422
# tell the browser to load the test
2423
self.wfile.write(b'COMMAND:' + url.encode('utf-8'))
2424
else:
2425
# the browser must keep polling
2426
self.wfile.write(b'(wait)')
2427
else:
2428
# Use SimpleHTTPServer default file serving operation for GET.
2429
if DEBUG:
2430
print('[simple HTTP serving:', unquote_plus(self.path), ']')
2431
if self.headers.get('Range'):
2432
self.send_response(206)
2433
path = self.translate_path(self.path)
2434
data = read_binary(path)
2435
ctype = self.guess_type(path)
2436
self.send_header('Content-type', ctype)
2437
pieces = self.headers.get('Range').split('=')[1].split('-')
2438
start = int(pieces[0]) if pieces[0] != '' else 0
2439
end = int(pieces[1]) if pieces[1] != '' else len(data) - 1
2440
end = min(len(data) - 1, end)
2441
length = end - start + 1
2442
self.send_header('Content-Length', str(length))
2443
self.send_header('Content-Range', f'bytes {start}-{end}/{len(data)}')
2444
self.end_headers()
2445
self.wfile.write(data[start:end + 1])
2446
else:
2447
SimpleHTTPRequestHandler.do_GET(self)
2448
2449
def log_request(code=0, size=0):
2450
# don't log; too noisy
2451
pass
2452
2453
# allows streaming compilation to work
2454
SimpleHTTPRequestHandler.extensions_map['.wasm'] = 'application/wasm'
2455
# Firefox browser security does not allow loading .mjs files if they
2456
# do not have the correct MIME type
2457
SimpleHTTPRequestHandler.extensions_map['.mjs'] = 'text/javascript'
2458
2459
return ThreadingHTTPServer(('localhost', port), TestServerHandler)
2460
2461
2462
class HttpServerThread(threading.Thread):
2463
"""A generic thread class to create and run an http server."""
2464
def __init__(self, server):
2465
super().__init__()
2466
self.server = server
2467
2468
def stop(self):
2469
"""Shuts down the server if it is running."""
2470
self.server.shutdown()
2471
2472
def run(self):
2473
"""Creates the server instance and serves forever until stop() is called."""
2474
# Start the server's main loop (this blocks until shutdown() is called)
2475
self.server.serve_forever()
2476
2477
2478
class Reporting(Enum):
2479
"""When running browser tests we normally automatically include support
2480
code for reporting results back to the browser. This enum allows tests
2481
to decide what type of support code they need/want.
2482
"""
2483
NONE = 0
2484
# Include the JS helpers for reporting results
2485
JS_ONLY = 1
2486
# Include C/C++ reporting code (REPORT_RESULT macros) as well as JS helpers
2487
FULL = 2
2488
2489
2490
class BrowserCore(RunnerCore):
2491
# note how many tests hang / do not send an output. if many of these
2492
# happen, likely something is broken and it is best to abort the test
2493
# suite early, as otherwise we will wait for the timeout on every
2494
# single test (hundreds of minutes)
2495
MAX_UNRESPONSIVE_TESTS = 10
2496
PORT = 8888
2497
SERVER_URL = f'http://localhost:{PORT}'
2498
HARNESS_URL = f'{SERVER_URL}/run_harness'
2499
BROWSER_TIMEOUT = 60
2500
2501
unresponsive_tests = 0
2502
2503
def __init__(self, *args, **kwargs):
2504
self.capture_stdio = EMTEST_CAPTURE_STDIO
2505
super().__init__(*args, **kwargs)
2506
2507
@classmethod
2508
def browser_terminate(cls):
2509
cls.browser_proc.terminate()
2510
# If the browser doesn't shut down gracefully (in response to SIGTERM)
2511
# after 2 seconds kill it with force (SIGKILL).
2512
try:
2513
cls.browser_proc.wait(2)
2514
except subprocess.TimeoutExpired:
2515
logger.info('Browser did not respond to `terminate`. Using `kill`')
2516
cls.browser_proc.kill()
2517
cls.browser_proc.wait()
2518
cls.browser_data_dir = None
2519
2520
@classmethod
2521
def browser_restart(cls):
2522
# Kill existing browser
2523
logger.info('Restarting browser process')
2524
cls.browser_terminate()
2525
cls.browser_open(cls.HARNESS_URL)
2526
2527
@classmethod
2528
def browser_open(cls, url):
2529
global EMTEST_BROWSER
2530
if not EMTEST_BROWSER:
2531
logger.info('No EMTEST_BROWSER set. Defaulting to `google-chrome`')
2532
EMTEST_BROWSER = 'google-chrome'
2533
2534
if EMTEST_BROWSER_AUTO_CONFIG:
2535
logger.info('Using default CI configuration.')
2536
cls.browser_data_dir = DEFAULT_BROWSER_DATA_DIR
2537
if os.path.exists(cls.browser_data_dir):
2538
utils.delete_dir(cls.browser_data_dir)
2539
os.mkdir(cls.browser_data_dir)
2540
if is_chrome():
2541
config = ChromeConfig()
2542
elif is_firefox():
2543
config = FirefoxConfig()
2544
else:
2545
exit_with_error("EMTEST_BROWSER_AUTO_CONFIG only currently works with firefox or chrome.")
2546
EMTEST_BROWSER += f" {config.data_dir_flag}{cls.browser_data_dir} {' '.join(config.default_flags)}"
2547
if EMTEST_HEADLESS == 1:
2548
EMTEST_BROWSER += f" {config.headless_flags}"
2549
config.configure(cls.browser_data_dir)
2550
2551
if WINDOWS:
2552
# On Windows env. vars canonically use backslashes as directory delimiters, e.g.
2553
# set EMTEST_BROWSER=C:\Program Files\Mozilla Firefox\firefox.exe
2554
# and spaces are not escaped. But make sure to also support args, e.g.
2555
# set EMTEST_BROWSER="C:\Users\clb\AppData\Local\Google\Chrome SxS\Application\chrome.exe" --enable-unsafe-webgpu
2556
if '"' not in EMTEST_BROWSER and "'" not in EMTEST_BROWSER:
2557
EMTEST_BROWSER = '"' + EMTEST_BROWSER.replace("\\", "/") + '"'
2558
browser_args = shlex.split(EMTEST_BROWSER)
2559
logger.info('Launching browser: %s', str(browser_args))
2560
cls.browser_proc = subprocess.Popen(browser_args + [url])
2561
2562
@classmethod
2563
def setUpClass(cls):
2564
super().setUpClass()
2565
if not has_browser() or EMTEST_BROWSER == 'node':
2566
return
2567
2568
cls.harness_in_queue = queue.Queue()
2569
cls.harness_out_queue = queue.Queue()
2570
cls.harness_server = HttpServerThread(make_test_server(cls.harness_in_queue, cls.harness_out_queue, cls.PORT))
2571
cls.harness_server.start()
2572
2573
print(f'[Browser harness server on thread {cls.harness_server.name}]')
2574
cls.browser_open(cls.HARNESS_URL)
2575
2576
@classmethod
2577
def tearDownClass(cls):
2578
super().tearDownClass()
2579
if not has_browser() or EMTEST_BROWSER == 'node':
2580
return
2581
cls.harness_server.stop()
2582
cls.harness_server.join()
2583
cls.browser_terminate()
2584
2585
if WINDOWS:
2586
# On Windows, shutil.rmtree() in tearDown() raises this exception if we do not wait a bit:
2587
# WindowsError: [Error 32] The process cannot access the file because it is being used by another process.
2588
time.sleep(0.1)
2589
2590
def is_browser_test(self):
2591
return True
2592
2593
def add_browser_reporting(self):
2594
contents = read_file(test_file('browser_reporting.js'))
2595
contents = contents.replace('{{{REPORTING_URL}}}', BrowserCore.SERVER_URL)
2596
create_file('browser_reporting.js', contents)
2597
2598
def assert_out_queue_empty(self, who):
2599
if not self.harness_out_queue.empty():
2600
responses = []
2601
while not self.harness_out_queue.empty():
2602
responses += [self.harness_out_queue.get()]
2603
raise Exception('excessive responses from %s: %s' % (who, '\n'.join(responses)))
2604
2605
# @param extra_tries: how many more times to try this test, if it fails. browser tests have
2606
# many more causes of flakiness (in particular, they do not run
2607
# synchronously, so we have a timeout, which can be hit if the VM
2608
# we run on stalls temporarily), so we let each test try more than
2609
# once by default
2610
def run_browser(self, html_file, expected=None, message=None, timeout=None, extra_tries=1):
2611
if not has_browser():
2612
return
2613
assert '?' not in html_file, 'URL params not supported'
2614
url = html_file
2615
if self.capture_stdio:
2616
url += '?capture_stdio'
2617
if self.skip_exec:
2618
self.skipTest('skipping test execution: ' + self.skip_exec)
2619
if BrowserCore.unresponsive_tests >= BrowserCore.MAX_UNRESPONSIVE_TESTS:
2620
self.skipTest('too many unresponsive tests, skipping remaining tests')
2621
self.assert_out_queue_empty('previous test')
2622
if DEBUG:
2623
print('[browser launch:', html_file, ']')
2624
assert not (message and expected), 'run_browser expects `expected` or `message`, but not both'
2625
if expected is not None:
2626
try:
2627
self.harness_in_queue.put((
2628
'http://localhost:%s/%s' % (self.PORT, url),
2629
self.get_dir(),
2630
))
2631
if timeout is None:
2632
timeout = self.BROWSER_TIMEOUT
2633
try:
2634
output = self.harness_out_queue.get(block=True, timeout=timeout)
2635
except queue.Empty:
2636
BrowserCore.unresponsive_tests += 1
2637
print('[unresponsive tests: %d]' % BrowserCore.unresponsive_tests)
2638
self.browser_restart()
2639
# Rather than fail the test here, let fail on the `assertContained` so
2640
# that the test can be retried via `extra_tries`
2641
output = '[no http server activity]'
2642
if output is None:
2643
# the browser harness reported an error already, and sent a None to tell
2644
# us to also fail the test
2645
self.fail('browser harness error')
2646
output = unquote(output)
2647
if output.startswith('/report_result?skipped:'):
2648
self.skipTest(unquote(output[len('/report_result?skipped:'):]).strip())
2649
else:
2650
# verify the result, and try again if we should do so
2651
try:
2652
self.assertContained(expected, output)
2653
except self.failureException as e:
2654
if extra_tries > 0:
2655
print('[test error (see below), automatically retrying]')
2656
print(e)
2657
if not self.capture_stdio:
2658
print('[enabling stdio/stderr reporting]')
2659
self.capture_stdio = True
2660
return self.run_browser(html_file, expected, message, timeout, extra_tries - 1)
2661
else:
2662
raise e
2663
finally:
2664
time.sleep(0.1) # see comment about Windows above
2665
self.assert_out_queue_empty('this test')
2666
else:
2667
webbrowser.open_new(os.path.abspath(html_file))
2668
print('A web browser window should have opened a page containing the results of a part of this test.')
2669
print('You need to manually look at the page to see that it works ok: ' + message)
2670
print('(sleeping for a bit to keep the directory alive for the web browser..)')
2671
time.sleep(5)
2672
print('(moving on..)')
2673
2674
def compile_btest(self, filename, cflags, reporting=Reporting.FULL):
2675
# Inject support code for reporting results. This adds an include a header so testcases can
2676
# use REPORT_RESULT, and also adds a cpp file to be compiled alongside the testcase, which
2677
# contains the implementation of REPORT_RESULT (we can't just include that implementation in
2678
# the header as there may be multiple files being compiled here).
2679
if reporting != Reporting.NONE:
2680
# For basic reporting we inject JS helper funtions to report result back to server.
2681
self.add_browser_reporting()
2682
cflags += ['--pre-js', 'browser_reporting.js']
2683
if reporting == Reporting.FULL:
2684
# If C reporting (i.e. the REPORT_RESULT macro) is required we
2685
# also include report_result.c and force-include report_result.h
2686
self.run_process([EMCC, '-c', '-I' + TEST_ROOT,
2687
test_file('report_result.c')] + self.get_cflags(compile_only=True) + (['-fPIC'] if '-fPIC' in cflags else []))
2688
cflags += ['report_result.o', '-include', test_file('report_result.h')]
2689
if EMTEST_BROWSER == 'node':
2690
cflags.append('-DEMTEST_NODE')
2691
if not os.path.exists(filename):
2692
filename = test_file(filename)
2693
self.run_process([compiler_for(filename), filename] + self.get_cflags() + cflags)
2694
# Remove the file since some tests have assertions for how many files are in
2695
# the output directory.
2696
utils.delete_file('browser_reporting.js')
2697
2698
def btest_exit(self, filename, assert_returncode=0, *args, **kwargs):
2699
"""Special case of `btest` that reports its result solely via exiting
2700
with a given result code.
2701
2702
In this case we set EXIT_RUNTIME and we don't need to provide the
2703
REPORT_RESULT macro to the C code.
2704
"""
2705
self.set_setting('EXIT_RUNTIME')
2706
assert 'reporting' not in kwargs
2707
assert 'expected' not in kwargs
2708
kwargs['reporting'] = Reporting.JS_ONLY
2709
kwargs['expected'] = 'exit:%d' % assert_returncode
2710
return self.btest(filename, *args, **kwargs)
2711
2712
def btest(self, filename, expected=None,
2713
post_build=None,
2714
cflags=None,
2715
timeout=None,
2716
extra_tries=1,
2717
reporting=Reporting.FULL,
2718
output_basename='test'):
2719
assert expected, 'a btest must have an expected output'
2720
if cflags is None:
2721
cflags = []
2722
cflags = cflags.copy()
2723
filename = find_browser_test_file(filename)
2724
outfile = output_basename + '.html'
2725
cflags += ['-o', outfile]
2726
# print('cflags:', cflags)
2727
utils.delete_file(outfile)
2728
self.compile_btest(filename, cflags, reporting=reporting)
2729
self.assertExists(outfile)
2730
if post_build:
2731
post_build()
2732
if not isinstance(expected, list):
2733
expected = [expected]
2734
if EMTEST_BROWSER == 'node':
2735
nodejs = self.require_node()
2736
self.node_args += shared.node_pthread_flags(nodejs)
2737
output = self.run_js('test.js')
2738
self.assertContained('RESULT: ' + expected[0], output)
2739
else:
2740
self.run_browser(outfile, expected=['/report_result?' + e for e in expected], timeout=timeout, extra_tries=extra_tries)
2741
2742
2743
###################################################################################################
2744
2745
2746
def build_library(name,
2747
build_dir,
2748
generated_libs,
2749
configure,
2750
make,
2751
make_args,
2752
cache,
2753
cache_name,
2754
env_init,
2755
native):
2756
"""Build a library and cache the result. We build the library file
2757
once and cache it for all our tests. (We cache in memory since the test
2758
directory is destroyed and recreated for each test. Note that we cache
2759
separately for different compilers). This cache is just during the test
2760
runner. There is a different concept of caching as well, see |Cache|.
2761
"""
2762
2763
if type(generated_libs) is not list:
2764
generated_libs = [generated_libs]
2765
source_dir = test_file(name.replace('_native', ''))
2766
2767
project_dir = Path(build_dir, name)
2768
if os.path.exists(project_dir):
2769
shutil.rmtree(project_dir)
2770
# Useful in debugging sometimes to comment this out, and two lines above
2771
shutil.copytree(source_dir, project_dir)
2772
2773
generated_libs = [os.path.join(project_dir, lib) for lib in generated_libs]
2774
2775
if native:
2776
env = clang_native.get_clang_native_env()
2777
else:
2778
env = os.environ.copy()
2779
env.update(env_init)
2780
2781
if not native:
2782
# Inject emcmake, emconfigure or emmake accordingly, but only if we are
2783
# cross compiling.
2784
if configure:
2785
if configure[0] == 'cmake':
2786
configure = [EMCMAKE] + configure
2787
else:
2788
configure = [EMCONFIGURE] + configure
2789
else:
2790
make = [EMMAKE] + make
2791
2792
if configure:
2793
try:
2794
with open(os.path.join(project_dir, 'configure_out'), 'w') as out:
2795
with open(os.path.join(project_dir, 'configure_err'), 'w') as err:
2796
stdout = out if EMTEST_BUILD_VERBOSE < 2 else None
2797
stderr = err if EMTEST_BUILD_VERBOSE < 1 else None
2798
shared.run_process(configure, env=env, stdout=stdout, stderr=stderr,
2799
cwd=project_dir)
2800
except subprocess.CalledProcessError:
2801
print('-- configure stdout --')
2802
print(read_file(Path(project_dir, 'configure_out')))
2803
print('-- end configure stdout --')
2804
print('-- configure stderr --')
2805
print(read_file(Path(project_dir, 'configure_err')))
2806
print('-- end configure stderr --')
2807
raise
2808
# if we run configure or cmake we don't then need any kind
2809
# of special env when we run make below
2810
env = None
2811
2812
def open_make_out(mode='r'):
2813
return open(os.path.join(project_dir, 'make.out'), mode)
2814
2815
def open_make_err(mode='r'):
2816
return open(os.path.join(project_dir, 'make.err'), mode)
2817
2818
if EMTEST_BUILD_VERBOSE >= 3:
2819
# VERBOSE=1 is cmake and V=1 is for autoconf
2820
make_args += ['VERBOSE=1', 'V=1']
2821
2822
try:
2823
with open_make_out('w') as make_out:
2824
with open_make_err('w') as make_err:
2825
stdout = make_out if EMTEST_BUILD_VERBOSE < 2 else None
2826
stderr = make_err if EMTEST_BUILD_VERBOSE < 1 else None
2827
shared.run_process(make + make_args, stdout=stdout, stderr=stderr, env=env,
2828
cwd=project_dir)
2829
except subprocess.CalledProcessError:
2830
with open_make_out() as f:
2831
print('-- make stdout --')
2832
print(f.read())
2833
print('-- end make stdout --')
2834
with open_make_err() as f:
2835
print('-- make stderr --')
2836
print(f.read())
2837
print('-- end stderr --')
2838
raise
2839
2840
if cache is not None:
2841
cache[cache_name] = []
2842
for f in generated_libs:
2843
basename = os.path.basename(f)
2844
cache[cache_name].append((basename, read_binary(f)))
2845
2846
return generated_libs
2847
2848