Book a Demo!
CoCalc Logo Icon
StoreFeaturesDocsShareSupportNewsAboutPoliciesSign UpSign In
emscripten-core
GitHub Repository: emscripten-core/emscripten
Path: blob/main/test/decorators.py
6170 views
1
# Copyright 2025 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
"""Common decorators shared between the various suites.
7
8
When decorators are only used a single file they can be defined locally. If they
9
are used from multiple locations they should be defined here.
10
"""
11
12
import itertools
13
import os
14
import subprocess
15
import unittest
16
from functools import wraps
17
18
import common
19
from common import test_file
20
21
from tools.shared import DEBUG
22
from tools.utils import MACOS, WINDOWS
23
24
requires_network = unittest.skipIf(os.getenv('EMTEST_SKIP_NETWORK_TESTS'), 'This test requires network access')
25
26
27
# Generic decorator that calls a function named 'condition' on the test class and
28
# skips the test if that function returns true
29
def skip_if_simple(name, condition, note=''):
30
assert callable(condition)
31
assert not callable(note)
32
33
def decorator(func):
34
assert callable(func)
35
36
@wraps(func)
37
def decorated(self, *args, **kwargs):
38
if condition(self):
39
explanation_str = name
40
if note:
41
explanation_str += ': %s' % note
42
self.skipTest(explanation_str)
43
return func(self, *args, **kwargs)
44
45
return decorated
46
47
return decorator
48
49
50
# Same as skip_if_simple but creates a decorator that takes a note as an argument.
51
def skip_if(name, condition, default_note=''):
52
assert callable(condition)
53
54
def decorator(note=default_note):
55
return skip_if_simple(name, condition, note)
56
57
return decorator
58
59
60
def is_slow_test(func):
61
assert callable(func)
62
decorated = skip_if_simple('skipping slow tests', lambda _: common.EMTEST_SKIP_SLOW)(func)
63
decorated.is_slow = True
64
return decorated
65
66
67
def flaky(note=''):
68
assert not callable(note)
69
70
if common.EMTEST_SKIP_FLAKY:
71
return unittest.skip(note)
72
73
if not common.EMTEST_RETRY_FLAKY:
74
return lambda f: f
75
76
def decorated(func):
77
@wraps(func)
78
def modified(self, *args, **kwargs):
79
# Browser tests have their own method of retrying tests.
80
if self.is_browser_test():
81
self.flaky = True
82
return func(self, *args, **kwargs)
83
84
for i in range(common.EMTEST_RETRY_FLAKY):
85
try:
86
return func(self, *args, **kwargs)
87
except (AssertionError, subprocess.TimeoutExpired) as exc:
88
preserved_exc = exc
89
common.record_flaky_test(self.id(), i, common.EMTEST_RETRY_FLAKY, exc)
90
91
raise AssertionError('Flaky test has failed too many times') from preserved_exc
92
93
return modified
94
95
return decorated
96
97
98
def disabled(note=''):
99
assert not callable(note)
100
return unittest.skip(note)
101
102
103
no_mac = skip_if('no_mac', lambda _: MACOS)
104
105
no_windows = skip_if('no_windows', lambda _: WINDOWS)
106
107
no_wasm64 = skip_if('no_wasm64', lambda t: t.is_wasm64())
108
109
no_bun = skip_if('no_bun', lambda t: t.engine_is_bun())
110
111
# 2200mb is the value used by the core_2gb test mode
112
no_2gb = skip_if('no_2gb', lambda t: t.get_setting('INITIAL_MEMORY') == '2200mb')
113
114
no_4gb = skip_if('no_4gb', lambda t: t.is_4gb())
115
116
only_windows = skip_if('only_windows', lambda _: not WINDOWS)
117
118
requires_native_clang = skip_if_simple('native clang tests are disabled', lambda _: common.EMTEST_LACKS_NATIVE_CLANG)
119
120
needs_make = skip_if('tool not available on windows bots', lambda _: WINDOWS)
121
122
123
def requires_node(func):
124
assert callable(func)
125
126
@wraps(func)
127
def decorated(self, *args, **kwargs):
128
self.require_node()
129
return func(self, *args, **kwargs)
130
131
return decorated
132
133
134
def requires_node_25(func):
135
assert callable(func)
136
137
@wraps(func)
138
def decorated(self, *args, **kwargs):
139
self.require_node_25()
140
return func(self, *args, **kwargs)
141
142
return decorated
143
144
145
# Used to mark dependencies in various tests to npm developer dependency
146
# packages, which might not be installed on Emscripten end users' systems.
147
def requires_dev_dependency(package):
148
assert not callable(package)
149
note = f'requires npm development package "{package}" and EMTEST_SKIP_NODE_DEV_PACKAGES is set'
150
return skip_if_simple('requires_dev_dependency', lambda _: 'EMTEST_SKIP_NODE_DEV_PACKAGES' in os.environ, note)
151
152
153
def requires_wasm64(func):
154
assert callable(func)
155
156
@wraps(func)
157
def decorated(self, *args, **kwargs):
158
self.require_wasm64()
159
return func(self, *args, **kwargs)
160
161
return decorated
162
163
164
def requires_wasm_eh(func):
165
assert callable(func)
166
167
@wraps(func)
168
def decorated(self, *args, **kwargs):
169
self.require_wasm_eh()
170
return func(self, *args, **kwargs)
171
172
return decorated
173
174
175
def requires_v8(func):
176
assert callable(func)
177
178
@wraps(func)
179
def decorated(self, *args, **kwargs):
180
self.require_v8()
181
return func(self, *args, **kwargs)
182
183
return decorated
184
185
186
def requires_wasm2js(func):
187
assert callable(func)
188
189
@wraps(func)
190
def decorated(self, *args, **kwargs):
191
self.require_wasm2js()
192
return func(self, *args, **kwargs)
193
194
return decorated
195
196
197
def requires_jspi(func):
198
assert callable(func)
199
200
@wraps(func)
201
def decorated(self, *args, **kwargs):
202
self.require_jspi()
203
return func(self, *args, **kwargs)
204
205
return decorated
206
207
208
def requires_pthreads(func):
209
assert callable(func)
210
211
@wraps(func)
212
def decorated(self, *args, **kwargs):
213
self.require_pthreads()
214
return func(self, *args, **kwargs)
215
return decorated
216
217
218
def crossplatform(func):
219
assert callable(func)
220
func.is_crossplatform_test = True
221
return func
222
223
224
# without EMTEST_ALL_ENGINES set we only run tests in a single VM by
225
# default. in some tests we know that cross-VM differences may happen and
226
# so are worth testing, and they should be marked with this decorator
227
def all_engines(func):
228
assert callable(func)
229
230
@wraps(func)
231
def decorated(self, *args, **kwargs):
232
self.use_all_engines = True
233
self.set_setting('ENVIRONMENT', 'web,node,shell')
234
return func(self, *args, **kwargs)
235
236
return decorated
237
238
239
# Decorator version of env_modify
240
def with_env_modify(updates):
241
assert not callable(updates)
242
243
def decorated(func):
244
@wraps(func)
245
def modified(self, *args, **kwargs):
246
with common.env_modify(updates):
247
return func(self, *args, **kwargs)
248
return modified
249
250
return decorated
251
252
253
def also_with_wasmfs(func):
254
assert callable(func)
255
256
@wraps(func)
257
def metafunc(self, wasmfs, *args, **kwargs):
258
if DEBUG:
259
print('parameterize:wasmfs=%d' % wasmfs)
260
if wasmfs:
261
self.setup_wasmfs_test()
262
else:
263
self.cflags += ['-DMEMFS']
264
return func(self, *args, **kwargs)
265
266
parameterize(metafunc, {'': (False,),
267
'wasmfs': (True,)})
268
return metafunc
269
270
271
def also_with_nodefs(func):
272
@wraps(func)
273
def metafunc(self, fs, *args, **kwargs):
274
if DEBUG:
275
print('parameterize:fs=%s' % (fs))
276
if fs == 'nodefs':
277
self.setup_nodefs_test()
278
else:
279
self.cflags += ['-DMEMFS']
280
assert fs is None
281
return func(self, *args, **kwargs)
282
283
parameterize(metafunc, {'': (None,),
284
'nodefs': ('nodefs',)})
285
return metafunc
286
287
288
def also_with_nodefs_both(func):
289
@wraps(func)
290
def metafunc(self, fs, *args, **kwargs):
291
if DEBUG:
292
print('parameterize:fs=%s' % (fs))
293
if fs == 'nodefs':
294
self.setup_nodefs_test()
295
elif fs == 'rawfs':
296
self.setup_noderawfs_test()
297
else:
298
self.cflags += ['-DMEMFS']
299
assert fs is None
300
return func(self, *args, **kwargs)
301
302
parameterize(metafunc, {'': (None,),
303
'nodefs': ('nodefs',),
304
'rawfs': ('rawfs',)})
305
return metafunc
306
307
308
def with_all_fs(func):
309
@wraps(func)
310
def metafunc(self, wasmfs, fs, *args, **kwargs):
311
if DEBUG:
312
print('parameterize:fs=%s' % (fs))
313
if wasmfs:
314
self.setup_wasmfs_test()
315
if fs == 'nodefs':
316
self.setup_nodefs_test()
317
elif fs == 'rawfs':
318
self.setup_noderawfs_test()
319
else:
320
self.cflags += ['-DMEMFS']
321
assert fs is None
322
return func(self, *args, **kwargs)
323
324
parameterize(metafunc, {'': (False, None),
325
'nodefs': (False, 'nodefs'),
326
'rawfs': (False, 'rawfs'),
327
'wasmfs': (True, None),
328
'wasmfs_nodefs': (True, 'nodefs'),
329
'wasmfs_rawfs': (True, 'rawfs')})
330
return metafunc
331
332
333
def also_with_noderawfs(func):
334
assert callable(func)
335
336
@wraps(func)
337
def metafunc(self, rawfs, *args, **kwargs):
338
if DEBUG:
339
print('parameterize:rawfs=%d' % rawfs)
340
if rawfs:
341
self.setup_noderawfs_test()
342
else:
343
self.cflags += ['-DMEMFS']
344
return func(self, *args, **kwargs)
345
346
parameterize(metafunc, {'': (False,),
347
'rawfs': (True,)})
348
return metafunc
349
350
351
def also_with_minimal_runtime(func):
352
assert callable(func)
353
354
@wraps(func)
355
def metafunc(self, with_minimal_runtime, *args, **kwargs):
356
if DEBUG:
357
print('parameterize:minimal_runtime=%s' % with_minimal_runtime)
358
if self.get_setting('MINIMAL_RUNTIME'):
359
self.skipTest('MINIMAL_RUNTIME already enabled in test config')
360
if with_minimal_runtime:
361
if self.get_setting('MODULARIZE') == 'instance' or self.get_setting('WASM_ESM_INTEGRATION'):
362
self.skipTest('MODULARIZE=instance is not compatible with MINIMAL_RUNTIME')
363
self.set_setting('MINIMAL_RUNTIME', 1)
364
# This extra helper code is needed to cleanly handle calls to exit() which throw
365
# an ExitCode exception.
366
self.cflags += ['--pre-js', test_file('minimal_runtime_exit_handling.js')]
367
return func(self, *args, **kwargs)
368
369
parameterize(metafunc, {'': (False,),
370
'minimal_runtime': (True,)})
371
return metafunc
372
373
374
def also_without_bigint(func):
375
assert callable(func)
376
377
@wraps(func)
378
def metafunc(self, no_bigint, *args, **kwargs):
379
if DEBUG:
380
print('parameterize:no_bigint=%s' % no_bigint)
381
if no_bigint:
382
if self.get_setting('WASM_BIGINT') is not None:
383
self.skipTest('redundant in bigint test config')
384
self.set_setting('WASM_BIGINT', 0)
385
return func(self, *args, **kwargs)
386
387
parameterize(metafunc, {'': (False,),
388
'no_bigint': (True,)})
389
return metafunc
390
391
392
def also_with_wasm64(func):
393
assert callable(func)
394
395
@wraps(func)
396
def metafunc(self, with_wasm64, *args, **kwargs):
397
if DEBUG:
398
print('parameterize:wasm64=%s' % with_wasm64)
399
if with_wasm64:
400
self.require_wasm64()
401
self.set_setting('MEMORY64')
402
return func(self, *args, **kwargs)
403
404
parameterize(metafunc, {'': (False,),
405
'wasm64': (True,)})
406
return metafunc
407
408
409
def also_with_fetch_streaming(f):
410
assert callable(f)
411
412
@wraps(f)
413
def metafunc(self, with_fetch, *args, **kwargs):
414
if with_fetch:
415
self.set_setting('FETCH_STREAMING', '2')
416
self.cflags += ['-DSKIP_SYNC_FETCH_TESTS']
417
f(self, *args, **kwargs)
418
419
parameterize(metafunc, {'': (False,),
420
'fetch_backend': (True,)})
421
return metafunc
422
423
424
def also_with_wasm2js(func):
425
assert callable(func)
426
427
@wraps(func)
428
def metafunc(self, with_wasm2js, *args, **kwargs):
429
assert self.get_setting('WASM') is None
430
if DEBUG:
431
print('parameterize:wasm2js=%s' % with_wasm2js)
432
if with_wasm2js:
433
self.require_wasm2js()
434
self.set_setting('WASM', 0)
435
return func(self, *args, **kwargs)
436
437
parameterize(metafunc, {'': (False,),
438
'wasm2js': (True,)})
439
return metafunc
440
441
442
def can_do_standalone(self, impure=False):
443
# Pure standalone engines don't support MEMORY64 yet. Even with MEMORY64=2 (lowered)
444
# the WASI APIs that take pointer values don't have 64-bit variants yet.
445
if not impure:
446
if self.get_setting('MEMORY64'):
447
return False
448
# This is way to detect the core_2gb test mode in test_core.py
449
if self.get_setting('INITIAL_MEMORY') == '2200mb':
450
return False
451
return self.is_wasm() and \
452
self.get_setting('STACK_OVERFLOW_CHECK', 0) < 2 and \
453
not self.get_setting('MINIMAL_RUNTIME') and \
454
not self.get_setting('WASM_ESM_INTEGRATION') and \
455
not self.get_setting('SAFE_HEAP') and \
456
not any(a.startswith('-fsanitize=') for a in self.cflags)
457
458
459
# Impure means a test that cannot run in a wasm VM yet, as it is not 100%
460
# standalone. We can still run them with the JS code though.
461
def also_with_standalone_wasm(impure=False):
462
def decorated(func):
463
@wraps(func)
464
def metafunc(self, standalone, *args, **kwargs):
465
if DEBUG:
466
print('parameterize:standalone=%s' % standalone)
467
if standalone:
468
if not can_do_standalone(self, impure):
469
self.skipTest('Test configuration is not compatible with STANDALONE_WASM')
470
self.set_setting('STANDALONE_WASM')
471
if not impure:
472
self.set_setting('PURE_WASI')
473
self.cflags.append('-Wno-unused-command-line-argument')
474
# if we are impure, disallow all wasm engines
475
if impure:
476
self.wasm_engines = []
477
return func(self, *args, **kwargs)
478
479
parameterize(metafunc, {'': (False,),
480
'standalone': (True,)})
481
return metafunc
482
483
return decorated
484
485
486
def also_with_asan(func):
487
assert callable(func)
488
489
@wraps(func)
490
def metafunc(self, asan, *args, **kwargs):
491
if asan:
492
if self.is_wasm64():
493
self.skipTest('TODO: ASAN in memory64')
494
if self.is_2gb() or self.is_4gb():
495
self.skipTest('asan doesnt support GLOBAL_BASE')
496
self.cflags.append('-fsanitize=address')
497
return func(self, *args, **kwargs)
498
499
parameterize(metafunc, {'': (False,),
500
'asan': (True,)})
501
return metafunc
502
503
504
def also_with_modularize(func):
505
assert callable(func)
506
507
@wraps(func)
508
def metafunc(self, modularize, *args, **kwargs):
509
if modularize:
510
if self.get_setting('DECLARE_ASM_MODULE_EXPORTS') == 0:
511
self.skipTest('DECLARE_ASM_MODULE_EXPORTS=0 is not compatible with MODULARIZE')
512
if self.get_setting('STRICT_JS'):
513
self.skipTest('MODULARIZE is not compatible with STRICT_JS')
514
if self.get_setting('WASM_ESM_INTEGRATION'):
515
self.skipTest('MODULARIZE is not compatible with WASM_ESM_INTEGRATION')
516
self.cflags += ['--extern-post-js', test_file('modularize_post_js.js'), '-sMODULARIZE']
517
return func(self, *args, **kwargs)
518
519
parameterize(metafunc, {'': (False,),
520
'modularize': (True,)})
521
return metafunc
522
523
524
# Tests exception handling and setjmp/longjmp handling. This tests three
525
# combinations:
526
# - Emscripten EH + Emscripten SjLj
527
# - Wasm EH + Wasm SjLj
528
# - Wasm EH + Wasm SjLj (Legacy)
529
def with_all_eh_sjlj(func):
530
assert callable(func)
531
532
@wraps(func)
533
def metafunc(self, mode, *args, **kwargs):
534
if DEBUG:
535
print('parameterize:eh_mode=%s' % mode)
536
if mode in {'wasm', 'wasm_legacy'}:
537
# Wasm EH is currently supported only in wasm backend and V8
538
if self.is_wasm2js():
539
self.skipTest('wasm2js does not support wasm EH/SjLj')
540
self.cflags.append('-fwasm-exceptions')
541
self.set_setting('SUPPORT_LONGJMP', 'wasm')
542
if mode == 'wasm':
543
self.require_wasm_eh()
544
if mode == 'wasm_legacy':
545
self.require_wasm_legacy_eh()
546
else:
547
self.set_setting('DISABLE_EXCEPTION_CATCHING', 0)
548
self.set_setting('SUPPORT_LONGJMP', 'emscripten')
549
# DISABLE_EXCEPTION_CATCHING=0 exports __cxa_can_catch,
550
# so if we don't build in C++ mode, wasm-ld will
551
# error out because libc++abi is not included. See
552
# https://github.com/emscripten-core/emscripten/pull/14192 for details.
553
self.set_setting('DEFAULT_TO_CXX')
554
return func(self, *args, **kwargs)
555
556
parameterize(metafunc, {'emscripten': ('emscripten',),
557
'wasm': ('wasm',),
558
'wasm_legacy': ('wasm_legacy',)})
559
return metafunc
560
561
562
# This works just like `with_all_eh_sjlj` above but doesn't enable exceptions.
563
# Use this for tests that use setjmp/longjmp but not exceptions handling.
564
def with_all_sjlj(func):
565
assert callable(func)
566
567
@wraps(func)
568
def metafunc(self, mode, *args, **kwargs):
569
if mode in {'wasm', 'wasm_legacy'}:
570
if self.is_wasm2js():
571
self.skipTest('wasm2js does not support wasm SjLj')
572
self.set_setting('SUPPORT_LONGJMP', 'wasm')
573
if mode == 'wasm':
574
self.require_wasm_eh()
575
if mode == 'wasm_legacy':
576
self.require_wasm_legacy_eh()
577
else:
578
self.set_setting('SUPPORT_LONGJMP', 'emscripten')
579
return func(self, *args, **kwargs)
580
581
parameterize(metafunc, {'emscripten': ('emscripten',),
582
'wasm': ('wasm',),
583
'wasm_legacy': ('wasm_legacy',)})
584
return metafunc
585
586
587
def parameterize(func, parameters):
588
"""Add additional parameterization to a test function.
589
590
This function create or adds to the `_parameterize` property of a function
591
which is then expanded by the RunnerMeta metaclass into multiple separate
592
test functions.
593
"""
594
prev = getattr(func, '_parameterize', None)
595
assert not any(p.startswith('_') for p in parameters)
596
if prev:
597
# If we're parameterizing 2nd time, construct a cartesian product for various combinations.
598
func._parameterize = {
599
'_'.join(filter(None, [k1, k2])): v2 + v1 for (k1, v1), (k2, v2) in itertools.product(prev.items(), parameters.items())
600
}
601
else:
602
func._parameterize = parameters
603
604
605
def parameterized(parameters):
606
"""
607
Mark a test as parameterized.
608
609
Usage:
610
@parameterized({
611
'subtest1': (1, 2, 3),
612
'subtest2': (4, 5, 6),
613
})
614
def test_something(self, a, b, c):
615
... # actual test body
616
617
This is equivalent to defining two tests:
618
619
def test_something_subtest1(self):
620
# runs test_something(1, 2, 3)
621
622
def test_something_subtest2(self):
623
# runs test_something(4, 5, 6)
624
"""
625
def decorator(func):
626
parameterize(func, parameters)
627
return func
628
return decorator
629
630