| # Copyright 2015 WebAssembly Community Group participants |
| # |
| # Licensed under the Apache License, Version 2.0 (the "License"); |
| # you may not use this file except in compliance with the License. |
| # You may obtain a copy of the License at |
| # |
| # http://www.apache.org/licenses/LICENSE-2.0 |
| # |
| # Unless required by applicable law or agreed to in writing, software |
| # distributed under the License is distributed on an "AS IS" BASIS, |
| # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. |
| # See the License for the specific language governing permissions and |
| # limitations under the License. |
| |
| from __future__ import print_function |
| |
| import argparse |
| import difflib |
| import fnmatch |
| import glob |
| import os |
| import shutil |
| import subprocess |
| import sys |
| |
| # The C++ standard whose features are required to build Binaryen. |
| # Keep in sync with CMakeLists.txt CXX_STANDARD |
| cxx_standard = 17 |
| |
| |
| def parse_args(args): |
| usage_str = ("usage: 'python check.py [options]'\n\n" |
| "Runs the Binaryen test suite.") |
| parser = argparse.ArgumentParser(description=usage_str) |
| parser.add_argument( |
| '--torture', dest='torture', action='store_true', default=True, |
| help='Chooses whether to run the torture testcases. Default: true.') |
| parser.add_argument( |
| '--no-torture', dest='torture', action='store_false', |
| help='Disables running the torture testcases.') |
| parser.add_argument( |
| '--abort-on-first-failure', dest='abort_on_first_failure', |
| action='store_true', default=True, |
| help=('Specifies whether to halt test suite execution on first test error.' |
| ' Default: true.')) |
| parser.add_argument( |
| '--no-abort-on-first-failure', dest='abort_on_first_failure', |
| action='store_false', |
| help=('If set, the whole test suite will run to completion independent of' |
| ' earlier errors.')) |
| parser.add_argument( |
| '--binaryen-bin', dest='binaryen_bin', default='', |
| help=('Specifies the path to the Binaryen executables in the CMake build' |
| ' directory. Default: bin/ of current directory (i.e. assume an' |
| ' in-tree build).' |
| ' If not specified, the environment variable BINARYEN_ROOT= can also' |
| ' be used to adjust this.')) |
| parser.add_argument( |
| '--binaryen-lib', dest='binaryen_lib', default='', |
| help=('Specifies a path to where the built Binaryen shared library resides at.' |
| ' Default: ./lib relative to bin specified above.')) |
| parser.add_argument( |
| '--binaryen-root', dest='binaryen_root', default='', |
| help=('Specifies a path to the root of the Binaryen repository tree.' |
| ' Default: the directory where this file check.py resides.')) |
| parser.add_argument( |
| '--out-dir', dest='out_dir', default='', |
| help=('Specifies a path to the output directory for temp files, which ' |
| 'is also where the test runner changes directory into.' |
| ' Default:. out/test under the binaryen root.')) |
| parser.add_argument( |
| '--valgrind', dest='valgrind', default='', |
| help=('Specifies a path to Valgrind tool, which will be used to validate' |
| ' execution if specified. (Pass --valgrind=valgrind to search in' |
| ' PATH)')) |
| parser.add_argument( |
| '--valgrind-full-leak-check', dest='valgrind_full_leak_check', |
| action='store_true', default=False, |
| help=('If specified, all unfreed (but still referenced) pointers at the' |
| ' end of execution are considered memory leaks. Default: disabled.')) |
| parser.add_argument( |
| '--spec-test', action='append', nargs='*', default=[], dest='spec_tests', |
| help='Names specific spec tests to run.') |
| parser.add_argument( |
| 'positional_args', metavar='TEST_SUITE', nargs='*', |
| help=('Names specific test suites to run. Use --list-suites to see a ' |
| 'list of all test suites')) |
| parser.add_argument( |
| '--list-suites', action='store_true', |
| help='List the test suites that can be run.') |
| parser.add_argument( |
| '--filter', dest='test_name_filter', default='', |
| help=('Specifies a filter. Only tests whose paths contains this ' |
| 'substring will be run')) |
| # This option is only for fuzz_opt.py |
| # TODO Allow each script to inherit the default set of options and add its |
| # own custom options on top of that |
| parser.add_argument( |
| '--auto-initial-contents', dest='auto_initial_contents', |
| action='store_true', default=False, |
| help='Select important initial contents automaticaly in fuzzer. ' |
| 'Default: disabled.') |
| |
| return parser.parse_args(args) |
| |
| |
| options = parse_args(sys.argv[1:]) |
| requested = options.positional_args |
| script_dir = os.path.dirname(os.path.abspath(__file__)) |
| |
| num_failures = 0 |
| warnings = [] |
| |
| |
| def warn(text): |
| global warnings |
| warnings.append(text) |
| print('warning:', text, file=sys.stderr) |
| |
| |
| # setup |
| |
| # Locate Binaryen build artifacts directory (bin/ by default) |
| if not options.binaryen_bin: |
| if os.environ.get('BINARYEN_ROOT'): |
| if os.path.isdir(os.path.join(os.environ.get('BINARYEN_ROOT'), 'bin')): |
| options.binaryen_bin = os.path.join( |
| os.environ.get('BINARYEN_ROOT'), 'bin') |
| else: |
| options.binaryen_bin = os.environ.get('BINARYEN_ROOT') |
| else: |
| options.binaryen_bin = 'bin' |
| |
| options.binaryen_bin = os.path.normpath(os.path.abspath(options.binaryen_bin)) |
| |
| if not options.binaryen_lib: |
| options.binaryen_lib = os.path.join(os.path.dirname(options.binaryen_bin), 'lib') |
| |
| options.binaryen_lib = os.path.normpath(os.path.abspath(options.binaryen_lib)) |
| |
| options.binaryen_build = os.path.dirname(options.binaryen_bin) |
| |
| # ensure BINARYEN_ROOT is set up |
| os.environ['BINARYEN_ROOT'] = os.path.dirname(options.binaryen_bin) |
| |
| wasm_dis_filenames = ['wasm-dis', 'wasm-dis.exe', 'wasm-dis.js'] |
| if not any(os.path.isfile(os.path.join(options.binaryen_bin, f)) |
| for f in wasm_dis_filenames): |
| warn('Binaryen not found (or has not been successfully built to bin/ ?') |
| |
| # Locate Binaryen source directory if not specified. |
| if not options.binaryen_root: |
| options.binaryen_root = os.path.dirname(os.path.dirname(script_dir)) |
| |
| options.binaryen_test = os.path.join(options.binaryen_root, 'test') |
| |
| if not options.out_dir: |
| options.out_dir = os.path.join(options.binaryen_root, 'out', 'test') |
| |
| if not os.path.exists(options.out_dir): |
| os.makedirs(options.out_dir) |
| os.chdir(options.out_dir) |
| |
| |
| # Finds the given executable 'program' in PATH. |
| # Operates like the Unix tool 'which'. |
| def which(program): |
| def is_exe(fpath): |
| return os.path.isfile(fpath) and os.access(fpath, os.X_OK) |
| fpath, fname = os.path.split(program) |
| if fpath: |
| if is_exe(program): |
| return program |
| else: |
| paths = [ |
| # Prefer tools installed using third_party/setup.py |
| os.path.join(options.binaryen_root, 'third_party', 'mozjs'), |
| os.path.join(options.binaryen_root, 'third_party', 'v8'), |
| os.path.join(options.binaryen_root, 'third_party', 'wabt', 'bin') |
| ] + os.environ['PATH'].split(os.pathsep) |
| for path in paths: |
| path = path.strip('"') |
| exe_file = os.path.join(path, program) |
| if is_exe(exe_file): |
| return exe_file |
| if '.' not in fname: |
| if is_exe(exe_file + '.exe'): |
| return exe_file + '.exe' |
| if is_exe(exe_file + '.cmd'): |
| return exe_file + '.cmd' |
| if is_exe(exe_file + '.bat'): |
| return exe_file + '.bat' |
| |
| |
| WATERFALL_BUILD_DIR = os.path.join(options.binaryen_test, 'wasm-install') |
| BIN_DIR = os.path.abspath(os.path.join(WATERFALL_BUILD_DIR, 'wasm-install', 'bin')) |
| |
| NATIVECC = (os.environ.get('CC') or which('mingw32-gcc') or |
| which('gcc') or which('clang')) |
| NATIVEXX = (os.environ.get('CXX') or which('mingw32-g++') or |
| which('g++') or which('clang++')) |
| NODEJS = os.getenv('NODE', which('node') or which('nodejs')) |
| MOZJS = which('mozjs') or which('spidermonkey') |
| |
| V8 = which('v8') or which('d8') |
| |
| BINARYEN_INSTALL_DIR = os.path.dirname(options.binaryen_bin) |
| WASM_OPT = [os.path.join(options.binaryen_bin, 'wasm-opt')] |
| WASM_AS = [os.path.join(options.binaryen_bin, 'wasm-as')] |
| WASM_DIS = [os.path.join(options.binaryen_bin, 'wasm-dis')] |
| WASM2JS = [os.path.join(options.binaryen_bin, 'wasm2js')] |
| WASM_CTOR_EVAL = [os.path.join(options.binaryen_bin, 'wasm-ctor-eval')] |
| WASM_SHELL = [os.path.join(options.binaryen_bin, 'wasm-shell')] |
| WASM_REDUCE = [os.path.join(options.binaryen_bin, 'wasm-reduce')] |
| WASM_METADCE = [os.path.join(options.binaryen_bin, 'wasm-metadce')] |
| WASM_EMSCRIPTEN_FINALIZE = [os.path.join(options.binaryen_bin, |
| 'wasm-emscripten-finalize')] |
| BINARYEN_JS = os.path.join(options.binaryen_bin, 'binaryen_js.js') |
| BINARYEN_WASM = os.path.join(options.binaryen_bin, 'binaryen_wasm.js') |
| |
| |
| def wrap_with_valgrind(cmd): |
| # Exit code 97 is arbitrary, used to easily detect when an error occurs that |
| # is detected by Valgrind. |
| valgrind = [options.valgrind, '--quiet', '--error-exitcode=97'] |
| if options.valgrind_full_leak_check: |
| valgrind += ['--leak-check=full', '--show-leak-kinds=all'] |
| return valgrind + cmd |
| |
| |
| if options.valgrind: |
| WASM_OPT = wrap_with_valgrind(WASM_OPT) |
| WASM_AS = wrap_with_valgrind(WASM_AS) |
| WASM_DIS = wrap_with_valgrind(WASM_DIS) |
| WASM_SHELL = wrap_with_valgrind(WASM_SHELL) |
| |
| |
| def in_binaryen(*args): |
| return os.path.join(options.binaryen_root, *args) |
| |
| |
| os.environ['BINARYEN'] = in_binaryen() |
| |
| |
| def get_platform(): |
| return {'linux': 'linux', |
| 'linux2': 'linux', |
| 'darwin': 'mac', |
| 'win32': 'windows', |
| 'cygwin': 'windows'}[sys.platform] |
| |
| |
| def has_shell_timeout(): |
| return get_platform() != 'windows' and os.system('timeout 1s pwd') == 0 |
| |
| |
| # Default options to pass to v8. These enable all features. |
| # See https://github.com/v8/v8/blob/master/src/wasm/wasm-feature-flags.h |
| V8_OPTS = [ |
| '--wasm-staging', |
| '--experimental-wasm-compilation-hints', |
| '--experimental-wasm-gc', |
| '--experimental-wasm-typed-funcref', |
| '--experimental-wasm-memory64', |
| '--experimental-wasm-extended-const', |
| '--wasm-final-types', |
| ] |
| |
| # external tools |
| |
| try: |
| if NODEJS is not None: |
| subprocess.check_call([NODEJS, '--version'], |
| stdout=subprocess.PIPE, |
| stderr=subprocess.PIPE) |
| except (OSError, subprocess.CalledProcessError): |
| NODEJS = None |
| if NODEJS is None: |
| warn('no node found (did not check proper js form)') |
| |
| |
| # utilities |
| |
| # removes a file if it exists, using any and all ways of doing so |
| def delete_from_orbit(filename): |
| try: |
| os.unlink(filename) |
| except OSError: |
| pass |
| if not os.path.exists(filename): |
| return |
| try: |
| shutil.rmtree(filename, ignore_errors=True) |
| except OSError: |
| pass |
| if not os.path.exists(filename): |
| return |
| try: |
| import stat |
| os.chmod(filename, os.stat(filename).st_mode | stat.S_IWRITE) |
| |
| def remove_readonly_and_try_again(func, path, exc_info): |
| if not (os.stat(path).st_mode & stat.S_IWRITE): |
| os.chmod(path, os.stat(path).st_mode | stat.S_IWRITE) |
| func(path) |
| else: |
| raise |
| shutil.rmtree(filename, onerror=remove_readonly_and_try_again) |
| except OSError: |
| pass |
| |
| |
| # This is a workaround for https://bugs.python.org/issue9400 |
| class Py2CalledProcessError(subprocess.CalledProcessError): |
| def __init__(self, returncode, cmd, output=None, stderr=None): |
| super(Exception, self).__init__(returncode, cmd, output, stderr) |
| self.returncode = returncode |
| self.cmd = cmd |
| self.output = output |
| self.stderr = stderr |
| |
| |
| def run_process(cmd, check=True, input=None, capture_output=False, decode_output=True, *args, **kw): |
| if input and type(input) == str: |
| input = bytes(input, 'utf-8') |
| if capture_output: |
| kw['stdout'] = subprocess.PIPE |
| kw['stderr'] = subprocess.PIPE |
| ret = subprocess.run(cmd, check=check, input=input, *args, **kw) |
| if decode_output and ret.stdout is not None: |
| ret.stdout = ret.stdout.decode('utf-8') |
| if ret.stderr is not None: |
| ret.stderr = ret.stderr.decode('utf-8') |
| return ret |
| |
| |
| def fail_with_error(msg): |
| global num_failures |
| try: |
| num_failures += 1 |
| raise Exception(msg) |
| except Exception as e: |
| print(str(e)) |
| if options.abort_on_first_failure: |
| raise |
| |
| |
| def fail(actual, expected, fromfile='expected'): |
| diff_lines = difflib.unified_diff( |
| expected.split('\n'), actual.split('\n'), |
| fromfile=fromfile, tofile='actual') |
| diff_str = ''.join([a.rstrip() + '\n' for a in diff_lines])[:] |
| fail_with_error("incorrect output, diff:\n\n%s" % diff_str) |
| |
| |
| def fail_if_not_identical(actual, expected, fromfile='expected'): |
| if expected != actual: |
| fail(actual, expected, fromfile=fromfile) |
| |
| |
| def fail_if_not_contained(actual, expected): |
| if expected not in actual: |
| fail(actual, expected) |
| |
| |
| def fail_if_not_identical_to_file(actual, expected_file): |
| binary = expected_file.endswith(".wasm") or type(actual) == bytes |
| with open(expected_file, 'rb' if binary else 'r') as f: |
| fail_if_not_identical(actual, f.read(), fromfile=expected_file) |
| |
| |
| def get_test_dir(name): |
| """Returns the test directory located at BINARYEN_ROOT/test/[name].""" |
| return os.path.join(options.binaryen_test, name) |
| |
| |
| def get_tests(test_dir, extensions=[], recursive=False): |
| """Returns the list of test files in a given directory. 'extensions' is a |
| list of file extensions. If 'extensions' is empty, returns all files. |
| """ |
| tests = [] |
| star = '**/*' if recursive else '*' |
| if not extensions: |
| tests += glob.glob(os.path.join(test_dir, star), recursive=True) |
| for ext in extensions: |
| tests += glob.glob(os.path.join(test_dir, star + ext), recursive=True) |
| if options.test_name_filter: |
| tests = fnmatch.filter(tests, options.test_name_filter) |
| return sorted(tests) |
| |
| |
| if not options.spec_tests: |
| options.spec_tests = get_tests(get_test_dir('spec'), ['.wast']) |
| else: |
| options.spec_tests = options.spec_tests[:] |
| |
| # 11/27/2019: We updated the spec test suite to upstream spec repo. For some |
| # files that started failing after this update, we added the new files to this |
| # skip-list and preserved old ones by renaming them to 'old_[FILENAME].wast' |
| # not to lose coverage. When the cause of the error is fixed or the unsupported |
| # construct gets support so the new test passes, we can delete the |
| # corresponding 'old_[FILENAME].wast' file. When you fix the new file and |
| # delete the old file, make sure you rename the corresponding .wast.log file in |
| # expected-output/ if any. |
| SPEC_TESTS_TO_SKIP = [ |
| # Stacky code / notation |
| 'block.wast', |
| 'call.wast', |
| 'float_exprs.wast', |
| 'globals.wast', |
| 'loop.wast', |
| 'nop.wast', |
| 'select.wast', |
| 'stack.wast', |
| 'unwind.wast', |
| |
| # Binary module |
| 'binary.wast', |
| 'binary-leb128.wast', |
| 'custom.wast', |
| |
| # Empty 'then' or 'else' in 'if' |
| 'if.wast', |
| 'local_set.wast', |
| 'store.wast', |
| |
| # No module in a file |
| 'token.wast', |
| 'utf8-custom-section-id.wast', |
| 'utf8-import-field.wast', |
| 'utf8-import-module.wast', |
| 'utf8-invalid-encoding.wast', |
| |
| # 'register' command |
| 'linking.wast', |
| |
| # Misc. unsupported constructs |
| 'call_indirect.wast', # Empty (param) and (result) |
| 'const.wast', # Unparenthesized expression |
| 'data.wast', # Various unsupported (data) notations |
| 'elem.wast', # Unsupported 'offset' syntax in (elem) |
| 'exports.wast', # Multiple inlined exports for a function |
| 'func.wast', # Forward named type reference |
| 'skip-stack-guard-page.wast', # Hexadecimal style (0x..) in memory offset |
| |
| # Untriaged: We don't know the cause of the error yet |
| 'address.wast', # wasm2js 'assert_return' failure |
| 'br_if.wast', # Validation error |
| 'float_literals.wast', # 'assert_return' failure |
| 'int_literals.wast', # 'assert_return' failure |
| 'local_tee.wast', # Validation failure |
| 'memory_grow.wast', # 'assert_return' failure |
| 'start.wast', # Assertion failure |
| 'type.wast', # 'assertion_invalid' failure |
| 'unreachable.wast', # Validation failure |
| 'unreached-invalid.wast' # 'assert_invalid' failure |
| ] |
| options.spec_tests = [t for t in options.spec_tests if os.path.basename(t) not |
| in SPEC_TESTS_TO_SKIP] |
| |
| |
| # check utilities |
| |
| def binary_format_check(wast, verify_final_result=True, wasm_as_args=['-g'], |
| binary_suffix='.fromBinary', original_wast=None): |
| # checks we can convert the wast to binary and back |
| |
| print(' (binary format check)') |
| cmd = WASM_AS + [wast, '-o', 'a.wasm', '-all'] + wasm_as_args |
| print(' ', ' '.join(cmd)) |
| if os.path.exists('a.wasm'): |
| os.unlink('a.wasm') |
| subprocess.check_call(cmd, stdout=subprocess.PIPE) |
| assert os.path.exists('a.wasm') |
| |
| cmd = WASM_DIS + ['a.wasm', '-o', 'ab.wast', '-all'] |
| print(' ', ' '.join(cmd)) |
| if os.path.exists('ab.wast'): |
| os.unlink('ab.wast') |
| subprocess.check_call(cmd, stdout=subprocess.PIPE) |
| assert os.path.exists('ab.wast') |
| |
| # make sure it is a valid wast |
| cmd = WASM_OPT + ['ab.wast', '-all'] |
| print(' ', ' '.join(cmd)) |
| subprocess.check_call(cmd, stdout=subprocess.PIPE) |
| |
| if verify_final_result: |
| actual = open('ab.wast').read() |
| fail_if_not_identical_to_file(actual, wast + binary_suffix) |
| |
| return 'ab.wast' |
| |
| |
| def minify_check(wast, verify_final_result=True): |
| # checks we can parse minified output |
| |
| print(' (minify check)') |
| cmd = WASM_OPT + [wast, '--print-minified', '-all'] |
| print(' ', ' '.join(cmd)) |
| subprocess.check_call(cmd, stdout=open('a.wast', 'w'), stderr=subprocess.PIPE) |
| subprocess.check_call(WASM_OPT + ['a.wast', '-all'], |
| stdout=subprocess.PIPE, stderr=subprocess.PIPE) |
| |
| |
| # run a check with BINARYEN_PASS_DEBUG set, to do full validation |
| def with_pass_debug(check): |
| old_pass_debug = os.environ.get('BINARYEN_PASS_DEBUG') |
| try: |
| os.environ['BINARYEN_PASS_DEBUG'] = '1' |
| check() |
| finally: |
| if old_pass_debug is not None: |
| os.environ['BINARYEN_PASS_DEBUG'] = old_pass_debug |
| else: |
| if 'BINARYEN_PASS_DEBUG' in os.environ: |
| del os.environ['BINARYEN_PASS_DEBUG'] |
| |
| |
| # checks if we are on windows, and if so logs out that a test is being skipped, |
| # and returns True. This is a central location for all test skipping on |
| # windows, so that we can easily find which tests are skipped. |
| def skip_if_on_windows(name): |
| if get_platform() == 'windows': |
| print('skipping test "%s" on windows' % name) |
| return True |
| return False |