Skip to content

Commit bb0d612

Browse files
Add thread pool for spec tests
1 parent c0bcdd6 commit bb0d612

File tree

3 files changed

+159
-85
lines changed

3 files changed

+159
-85
lines changed

check.py

Lines changed: 121 additions & 64 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,124 @@ def run_wasm_reduce_tests():
175181
assert after < 0.85 * before, [before, after]
176182

177183

178-
def run_spec_tests():
179-
print('\n[ checking wasm-shell spec testcases... ]\n')
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'
180191

181-
for wast in shared.options.spec_tests:
182-
base = os.path.basename(wast)
183-
print('..', base)
184-
# windows has some failures that need to be investigated
185-
if base == 'names.wast' and shared.skip_if_on_windows('spec: ' + base):
186-
continue
187192

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)
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+
209+
def run_one_spec_test(wast: Path, stdout=None, stderr=None):
210+
test_name = wast.name
211+
base_name = "-".join(wast.parts[-3:])
212+
213+
print('..', test_name, file=stdout)
214+
# windows has some failures that need to be investigated
215+
if test_name == 'names.wast' and shared.skip_if_on_windows('spec: ' + test_name):
216+
return
217+
218+
expected = os.path.join(shared.get_test_dir('spec'), 'expected-output', test_name + '.log')
219+
220+
# some spec tests should fail (actual process failure, not just assert_invalid)
221+
try:
222+
actual = run_spec_test(str(wast), stdout=stdout, stderr=stderr)
223+
except Exception as e:
224+
if ('wasm-validator error' in str(e) or 'error: ' in str(e)) and '.fail.' in test_name:
225+
print('<< test failed as expected >>', file=stdout)
226+
return # don't try all the binary format stuff TODO
227+
else:
228+
shared.fail_with_error(str(e))
229+
230+
check_expected(actual, expected, stdout=stdout)
231+
232+
# check binary format. here we can verify execution of the final
233+
# result, no need for an output verification
234+
actual = ''
235+
with open(base_name + ".transformed", 'w') as transformed_spec_file:
236+
for i, (module, asserts) in enumerate(support.split_wast(str(wast))):
237+
if not module:
238+
# Skip any initial assertions that don't have a module
239+
return
240+
print(f' testing split module {i}', file=stdout)
241+
split_name = base_name + f'_split{i}.wast'
242+
support.write_wast(split_name, module)
243+
run_opt_test(split_name, stdout=stdout, stderr=stderr) # also that our optimizer doesn't break on it
244+
245+
result_wast_file = shared.binary_format_check(split_name, verify_final_result=False, base_name=base_name, stdout=stdout, stderr=stderr)
246+
with open(result_wast_file) as f:
247+
result_wast = f.read()
248+
# add the asserts, and verify that the test still passes
249+
transformed_spec_file.write(result_wast + '\n' + '\n'.join(asserts))
250+
251+
# compare all the outputs to the expected output
252+
actual = run_spec_test(base_name + ".transformed", stdout=stdout, stderr=stderr)
253+
check_expected(actual, os.path.join(shared.get_test_dir('spec'), 'expected-output', test_name + '.log'), stdout=stdout)
254+
255+
256+
def run_spec_test_with_wrapped_stdout(output_queue, wast: Path):
257+
out = io.StringIO()
258+
try:
259+
ret = run_one_spec_test(wast, stdout=out, stderr=out)
260+
except Exception as e:
261+
print(e, file=out)
262+
raise
263+
finally:
264+
# If a test fails, it's important to keep its output
265+
output_queue.put(out.getvalue())
266+
return ret
267+
268+
269+
def run_spec_tests():
270+
print('\n[ checking wasm-shell spec testcases... ]\n')
200271

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)
209-
210-
expected = os.path.join(shared.get_test_dir('spec'), 'expected-output', base + '.log')
211-
212-
# some spec tests should fail (actual process failure, not just assert_invalid)
213-
try:
214-
actual = run_spec_test(wast)
215-
except Exception as e:
216-
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
219-
else:
220-
shared.fail_with_error(str(e))
221-
222-
check_expected(actual, expected)
223-
224-
# check binary format. here we can verify execution of the final
225-
# result, no need for an output verification
226-
actual = ''
227-
with open(base, 'w') as transformed_spec_file:
228-
for i, (module, asserts) in enumerate(support.split_wast(wast)):
229-
if not module:
230-
# 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'
234-
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)
237-
with open(result_wast_file) as f:
238-
result_wast = f.read()
239-
# add the asserts, and verify that the test still passes
240-
transformed_spec_file.write(result_wast + '\n' + '\n'.join(asserts))
241-
242-
# 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'))
272+
output_queue = queue.Queue()
273+
274+
def printer():
275+
while True:
276+
try:
277+
string = output_queue.get()
278+
except queue.ShutDown:
279+
break
280+
print(string, end="")
281+
282+
printing_thread = threading.Thread(target=printer)
283+
printing_thread.start()
284+
285+
worker_count = os.cpu_count() * 2
286+
print("Running with", worker_count, "workers")
287+
executor = ThreadPoolExecutor(max_workers=worker_count)
288+
try:
289+
results = executor.map(partial(run_spec_test_with_wrapped_stdout, output_queue), map(Path, shared.options.spec_tests))
290+
for _ in results:
291+
# Iterating joins the threads. No return value here.
292+
pass
293+
except KeyboardInterrupt:
294+
# Hard exit to avoid threads continuing to run after Ctrl-C.
295+
# There's no concern of deadlocking during shutdown here.
296+
os._exit(1)
297+
finally:
298+
executor.shutdown(cancel_futures=True)
299+
300+
output_queue.shutdown()
301+
printing_thread.join()
245302

246303

247304
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)