Skip to content

Commit 505ecae

Browse files
Try multithreading spec tests
1 parent c0bcdd6 commit 505ecae

File tree

3 files changed

+128
-60
lines changed

3 files changed

+128
-60
lines changed

check.py

Lines changed: 90 additions & 39 deletions
Original file line numberDiff line numberDiff line change
@@ -20,6 +20,12 @@
2020
import sys
2121
import unittest
2222
from collections import OrderedDict
23+
from concurrent.futures import ThreadPoolExecutor
24+
from pathlib import Path
25+
import queue
26+
import io
27+
import threading
28+
from functools import partial
2329

2430
from scripts.test import binaryenjs
2531
from scripts.test import lld
@@ -175,73 +181,118 @@ def run_wasm_reduce_tests():
175181
assert after < 0.85 * before, [before, after]
176182

177183

184+
def run_spec_test(wast, stdout=None, stderr=None):
185+
cmd = shared.WASM_SHELL + [wast]
186+
output = support.run_command(cmd, stdout=stdout, stderr=stderr)
187+
# filter out binaryen interpreter logging that the spec suite
188+
# doesn't expect
189+
filtered = [line for line in output.splitlines() if not line.startswith('[trap')]
190+
return '\n'.join(filtered) + '\n'
191+
192+
193+
def run_opt_test(wast, stdout=None, stderr=None):
194+
# check optimization validation
195+
cmd = shared.WASM_OPT + [wast, '-O', '-all', '-q']
196+
support.run_command(cmd, stdout=stdout, stderr=stderr)
197+
198+
199+
def check_expected(actual, expected, stdout=None):
200+
if expected and os.path.exists(expected):
201+
expected = open(expected).read()
202+
print(' (using expected output)', file=stdout)
203+
actual = actual.strip()
204+
expected = expected.strip()
205+
if actual != expected:
206+
shared.fail(actual, expected)
207+
208+
178209
def run_spec_tests():
179210
print('\n[ checking wasm-shell spec testcases... ]\n')
180211

181-
for wast in shared.options.spec_tests:
182-
base = os.path.basename(wast)
183-
print('..', base)
212+
def run_one(wast: Path, stdout=None, stderr=None):
213+
base = wast.name
214+
print('..', base, file=stdout)
184215
# windows has some failures that need to be investigated
185216
if base == 'names.wast' and shared.skip_if_on_windows('spec: ' + base):
186-
continue
187-
188-
def run_spec_test(wast):
189-
cmd = shared.WASM_SHELL + [wast]
190-
output = support.run_command(cmd, stderr=subprocess.PIPE)
191-
# filter out binaryen interpreter logging that the spec suite
192-
# doesn't expect
193-
filtered = [line for line in output.splitlines() if not line.startswith('[trap')]
194-
return '\n'.join(filtered) + '\n'
195-
196-
def run_opt_test(wast):
197-
# check optimization validation
198-
cmd = shared.WASM_OPT + [wast, '-O', '-all', '-q']
199-
support.run_command(cmd)
200-
201-
def check_expected(actual, expected):
202-
if expected and os.path.exists(expected):
203-
expected = open(expected).read()
204-
print(' (using expected output)')
205-
actual = actual.strip()
206-
expected = expected.strip()
207-
if actual != expected:
208-
shared.fail(actual, expected)
217+
return
209218

210219
expected = os.path.join(shared.get_test_dir('spec'), 'expected-output', base + '.log')
211220

212221
# some spec tests should fail (actual process failure, not just assert_invalid)
213222
try:
214-
actual = run_spec_test(wast)
223+
actual = run_spec_test(str(wast), stdout=stdout, stderr=stderr)
215224
except Exception as e:
216225
if ('wasm-validator error' in str(e) or 'error: ' in str(e)) and '.fail.' in base:
217-
print('<< test failed as expected >>')
218-
continue # don't try all the binary format stuff TODO
226+
print('<< test failed as expected >>', file=stdout)
227+
return # don't try all the binary format stuff TODO
219228
else:
220229
shared.fail_with_error(str(e))
221230

222-
check_expected(actual, expected)
231+
check_expected(actual, expected, stdout=stdout)
223232

224233
# check binary format. here we can verify execution of the final
225234
# result, no need for an output verification
226235
actual = ''
227-
with open(base, 'w') as transformed_spec_file:
228-
for i, (module, asserts) in enumerate(support.split_wast(wast)):
236+
with open("-".join(wast.parts[-3:]) + ".transformed", 'w') as transformed_spec_file:
237+
for i, (module, asserts) in enumerate(support.split_wast(str(wast))):
229238
if not module:
230239
# Skip any initial assertions that don't have a module
231-
continue
232-
print(f' testing split module {i}')
233-
split_name = os.path.splitext(base)[0] + f'_split{i}.wast'
240+
return
241+
print(f' testing split module {i}', file=stdout)
242+
split_name = "-".join(wast.parts[-3:]) + f'_split{i}.wast'
234243
support.write_wast(split_name, module)
235-
run_opt_test(split_name) # also that our optimizer doesn't break on it
236-
result_wast_file = shared.binary_format_check(split_name, verify_final_result=False)
244+
run_opt_test(split_name, stdout=stdout, stderr=stderr) # also that our optimizer doesn't break on it
245+
246+
result_wast_file = shared.binary_format_check(split_name, verify_final_result=False, base_name="-".join(wast.parts[-3:]), stdout=stdout, stderr=stderr)
237247
with open(result_wast_file) as f:
238248
result_wast = f.read()
239249
# add the asserts, and verify that the test still passes
240250
transformed_spec_file.write(result_wast + '\n' + '\n'.join(asserts))
241251

242252
# compare all the outputs to the expected output
243-
actual = run_spec_test(base)
244-
check_expected(actual, os.path.join(shared.get_test_dir('spec'), 'expected-output', base + '.log'))
253+
actual = run_spec_test("-".join(wast.parts[-3:]) + ".transformed", stdout=stdout, stderr=stderr)
254+
check_expected(actual, os.path.join(shared.get_test_dir('spec'), 'expected-output', base + '.log'), stdout=stdout)
255+
256+
output_queue = queue.Queue()
257+
258+
def run_with_wrapped_stdout(output_queue, wast: Path):
259+
out = io.StringIO()
260+
try:
261+
ret = run_one(wast, stdout=out, stderr=out)
262+
except Exception as e:
263+
print(e, file=out)
264+
raise
265+
finally:
266+
# If a test fails, it's important to keep its output
267+
output_queue.put(out.getvalue())
268+
return ret
269+
270+
def printer():
271+
while True:
272+
try:
273+
string = output_queue.get()
274+
except queue.ShutDown:
275+
break
276+
print(string, end="")
277+
278+
printing_thread = threading.Thread(target=printer)
279+
printing_thread.start()
280+
281+
executor = ThreadPoolExecutor(max_workers=max(1, min(len(shared.options.spec_tests), os.cpu_count() * 4)))
282+
try:
283+
results = executor.map(partial(run_with_wrapped_stdout, output_queue), map(Path, shared.options.spec_tests))
284+
for _ in results:
285+
# Iterating joins the threads. No return value here.
286+
pass
287+
except KeyboardInterrupt:
288+
# Hard exit to avoid threads continuing to run after Ctrl-C.
289+
# There's no concern of deadlocking during shutdown here.
290+
os._exit(1)
291+
finally:
292+
executor.shutdown()
293+
294+
output_queue.shutdown()
295+
printing_thread.join()
245296

246297

247298
def run_validator_tests():

scripts/test/shared.py

Lines changed: 19 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -519,34 +519,37 @@ def _can_run_spec_test(test):
519519

520520

521521
def binary_format_check(wast, verify_final_result=True, wasm_as_args=['-g'],
522-
binary_suffix='.fromBinary'):
522+
binary_suffix='.fromBinary', base_name=None, stdout=None, stderr=None):
523523
# checks we can convert the wast to binary and back
524524

525-
print(' (binary format check)')
526-
cmd = WASM_AS + [wast, '-o', 'a.wasm', '-all'] + wasm_as_args
527-
print(' ', ' '.join(cmd))
528-
if os.path.exists('a.wasm'):
529-
os.unlink('a.wasm')
525+
as_file = f"{base_name}-a.wasm" if base_name is not None else "a.wasm"
526+
disassembled_file = f"{base_name}-ab.wast" if base_name is not None else "ab.wast"
527+
528+
print(' (binary format check)', file=stdout)
529+
cmd = WASM_AS + [wast, '-o', as_file, '-all'] + wasm_as_args
530+
print(' ', ' '.join(cmd), file=stdout)
531+
if os.path.exists(as_file):
532+
os.unlink(as_file)
530533
subprocess.check_call(cmd, stdout=subprocess.PIPE)
531-
assert os.path.exists('a.wasm')
534+
assert os.path.exists(as_file)
532535

533-
cmd = WASM_DIS + ['a.wasm', '-o', 'ab.wast', '-all']
534-
print(' ', ' '.join(cmd))
535-
if os.path.exists('ab.wast'):
536-
os.unlink('ab.wast')
536+
cmd = WASM_DIS + [as_file, '-o', disassembled_file, '-all']
537+
print(' ', ' '.join(cmd), file=stdout)
538+
if os.path.exists(disassembled_file):
539+
os.unlink(disassembled_file)
537540
subprocess.check_call(cmd, stdout=subprocess.PIPE)
538-
assert os.path.exists('ab.wast')
541+
assert os.path.exists(disassembled_file)
539542

540543
# make sure it is a valid wast
541-
cmd = WASM_OPT + ['ab.wast', '-all', '-q']
542-
print(' ', ' '.join(cmd))
544+
cmd = WASM_OPT + [disassembled_file, '-all', '-q']
545+
print(' ', ' '.join(cmd), file=stdout)
543546
subprocess.check_call(cmd, stdout=subprocess.PIPE)
544547

545548
if verify_final_result:
546-
actual = open('ab.wast').read()
549+
actual = open(disassembled_file).read()
547550
fail_if_not_identical_to_file(actual, wast + binary_suffix)
548551

549-
return 'ab.wast'
552+
return disassembled_file
550553

551554

552555
def minify_check(wast, verify_final_result=True):

scripts/test/support.py

Lines changed: 19 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,7 @@
1212
# See the License for the specific language governing permissions and
1313
# limitations under the License.
1414

15+
import io
1516
import filecmp
1617
import os
1718
import re
@@ -185,16 +186,29 @@ def write_wast(filename, wast, asserts=[]):
185186
o.write(wast + '\n'.join(asserts))
186187

187188

188-
def run_command(cmd, expected_status=0, stderr=None,
189+
def run_command(cmd, expected_status=0, stdout=None, stderr=None,
189190
expected_err=None, err_contains=False, err_ignore=None):
190191
if expected_err is not None:
191192
assert stderr == subprocess.PIPE or stderr is None, \
192193
"Can't redirect stderr if using expected_err"
193194
stderr = subprocess.PIPE
194-
print('executing: ', ' '.join(cmd))
195-
proc = subprocess.Popen(cmd, stdout=subprocess.PIPE, stderr=stderr, universal_newlines=True, encoding='UTF-8')
196-
out, err = proc.communicate()
197-
code = proc.returncode
195+
print('executing: ', ' '.join(cmd), file=stdout)
196+
197+
# Popen's streams require a file handle with a fileno, which StringIO doesn't have
198+
# In this case, print the streams after the fact.
199+
if isinstance(stderr, io.StringIO):
200+
proc = subprocess.Popen(cmd, stdout=subprocess.PIPE, stderr=subprocess.PIPE, universal_newlines=True, encoding='UTF-8')
201+
202+
out, err = proc.communicate()
203+
code = proc.returncode
204+
205+
print(out, file=stdout, end='')
206+
print(err, file=stderr, end='')
207+
else:
208+
proc = subprocess.Popen(cmd, stdout=subprocess.PIPE, stderr=stderr, universal_newlines=True, encoding='UTF-8')
209+
out, err = proc.communicate()
210+
code = proc.returncode
211+
198212
if expected_status is not None and code != expected_status:
199213
raise Exception(f"run_command `{' '.join(cmd)}` failed ({code}) {err or ''}")
200214
if expected_err is not None:

0 commit comments

Comments
 (0)