| import argparse |
| import difflib |
| import glob |
| import os |
| import shutil |
| import subprocess |
| import sys |
| import urllib2 |
| |
| |
| 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( |
| '--only-prepare', dest='only_prepare', action='store_true', default=False, |
| help='If enabled, only fetches the waterfall build. Default: false.') |
| parser.add_argument( |
| # Backwards compatibility |
| '--only_prepare', dest='only_prepare', action='store_true', default=False, |
| help='If enabled, only fetches the waterfall build. Default: false.') |
| parser.add_argument( |
| '--test-waterfall', dest='test_waterfall', action='store_true', |
| default=False, |
| help=('If enabled, fetches and tests the LLVM waterfall builds.' |
| ' Default: false.')) |
| parser.add_argument( |
| '--no-test-waterfall', dest='test_waterfall', action='store_false', |
| help='Disables downloading and testing of the LLVM waterfall builds.') |
| 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( |
| '--run-gcc-tests', dest='run_gcc_tests', action='store_true', default=True, |
| help=('Chooses whether to run the tests that require building with native' |
| ' GCC. Default: true.')) |
| parser.add_argument( |
| '--no-run-gcc-tests', dest='run_gcc_tests', action='store_false', |
| help='If set, disables the native GCC tests.') |
| |
| parser.add_argument( |
| '--interpreter', dest='interpreter', default='', |
| help='Specifies the wasm interpreter executable to run tests on.') |
| parser.add_argument( |
| '--binaryen-bin', dest='binaryen_bin', default='', |
| help=('Specifies a path to where the built Binaryen executables reside at.' |
| ' 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-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( |
| '--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( |
| 'positional_args', metavar='tests', nargs=argparse.REMAINDER, |
| help='Names specific tests to run.') |
| options = parser.parse_args() |
| requested = options.positional_args |
| |
| num_failures = 0 |
| warnings = [] |
| |
| |
| def warn(text): |
| global warnings |
| warnings.append(text) |
| print 'warning:', text |
| |
| |
| # 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' |
| |
| # ensure BINARYEN_ROOT is set up |
| os.environ['BINARYEN_ROOT'] = os.path.dirname(os.path.abspath( |
| options.binaryen_bin)) |
| |
| options.binaryen_bin = os.path.normpath(options.binaryen_bin) |
| |
| wasm_dis_filenames = ['wasm-dis', 'wasm-dis.exe'] |
| 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: |
| path_parts = os.path.abspath(__file__).split(os.path.sep) |
| options.binaryen_root = os.path.sep.join(path_parts[:-3]) |
| |
| options.binaryen_test = os.path.join(options.binaryen_root, 'test') |
| |
| |
| # 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: |
| for path in os.environ["PATH"].split(os.pathsep): |
| 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 = which('nodejs') or which('node') |
| MOZJS = which('mozjs') or which('spidermonkey') |
| EMCC = which('emcc') |
| |
| 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')] |
| ASM2WASM = [os.path.join(options.binaryen_bin, 'asm2wasm')] |
| WASM2ASM = [os.path.join(options.binaryen_bin, 'wasm2asm')] |
| WASM_CTOR_EVAL = [os.path.join(options.binaryen_bin, 'wasm-ctor-eval')] |
| WASM_SHELL = [os.path.join(options.binaryen_bin, 'wasm-shell')] |
| WASM_MERGE = [os.path.join(options.binaryen_bin, 'wasm-merge')] |
| S2WASM = [os.path.join(options.binaryen_bin, 's2wasm')] |
| 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')] |
| |
| S2WASM_EXE = S2WASM[0] |
| WASM_SHELL_EXE = WASM_SHELL[0] |
| |
| |
| 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) |
| ASM2WASM = wrap_with_valgrind(ASM2WASM) |
| WASM_SHELL = wrap_with_valgrind(WASM_SHELL) |
| S2WASM = wrap_with_valgrind(S2WASM) |
| |
| os.environ['BINARYEN'] = os.getcwd() |
| |
| |
| def get_platform(): |
| return {'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 |
| |
| |
| def fetch_waterfall(): |
| rev = open(os.path.join(options.binaryen_test, 'revision')).read().strip() |
| buildername = get_platform() |
| local_rev_path = os.path.join(WATERFALL_BUILD_DIR, 'local-revision') |
| if os.path.exists(local_rev_path): |
| with open(local_rev_path) as f: |
| local_rev = f.read().strip() |
| if local_rev == rev: |
| return |
| # fetch it |
| basename = 'wasm-binaries-' + rev + '.tbz2' |
| url = '/'.join(['https://storage.googleapis.com/wasm-llvm/builds', |
| buildername, rev, basename]) |
| print '(downloading waterfall %s: %s)' % (rev, url) |
| downloaded = urllib2.urlopen(url).read().strip() |
| fullname = os.path.join(options.binaryen_test, basename) |
| open(fullname, 'wb').write(downloaded) |
| print '(unpacking)' |
| if os.path.exists(WATERFALL_BUILD_DIR): |
| shutil.rmtree(WATERFALL_BUILD_DIR) |
| os.mkdir(WATERFALL_BUILD_DIR) |
| subprocess.check_call(['tar', '-xf', os.path.abspath(fullname)], |
| cwd=WATERFALL_BUILD_DIR) |
| print '(noting local revision)' |
| with open(local_rev_path, 'w') as o: |
| o.write(rev + '\n') |
| |
| |
| has_vanilla_llvm = False |
| |
| |
| def setup_waterfall(): |
| # if we can use the waterfall llvm, do so |
| global has_vanilla_llvm |
| CLANG = os.path.join(BIN_DIR, 'clang') |
| print 'trying waterfall clang at', CLANG |
| try: |
| subprocess.check_call([CLANG, '-v']) |
| has_vanilla_llvm = True |
| print '...success' |
| except (OSError, subprocess.CalledProcessError) as e: |
| warn('could not run vanilla LLVM from waterfall: ' + str(e) + |
| ', looked for clang at ' + CLANG) |
| |
| |
| if options.test_waterfall: |
| fetch_waterfall() |
| setup_waterfall() |
| |
| if options.only_prepare: |
| print 'waterfall is fetched and setup, exiting since --only-prepare' |
| sys.exit(0) |
| |
| # 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)') |
| |
| try: |
| if MOZJS is not None: |
| subprocess.check_call( |
| [MOZJS, '--version'], stdout=subprocess.PIPE, stderr=subprocess.PIPE) |
| except (OSError, subprocess.CalledProcessError): |
| MOZJS = None |
| if MOZJS is None: |
| warn('no mozjs found (did not check native wasm support nor asm.js' |
| ' validation)') |
| |
| try: |
| if EMCC is not None: |
| subprocess.check_call( |
| [EMCC, '--version'], stdout=subprocess.PIPE, stderr=subprocess.PIPE) |
| except (OSError, subprocess.CalledProcessError): |
| EMCC = None |
| if EMCC is None: |
| warn('no emcc found (did not check non-vanilla emscripten/binaryen' |
| ' integration)') |
| |
| has_vanilla_emcc = False |
| try: |
| subprocess.check_call( |
| [os.path.join(options.binaryen_test, 'emscripten', 'emcc'), '--version'], |
| stdout=subprocess.PIPE, stderr=subprocess.PIPE) |
| has_vanilla_emcc = True |
| except (OSError, subprocess.CalledProcessError): |
| pass |
| |
| |
| # 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 |
| |
| |
| def fail_with_error(msg): |
| global num_failures |
| try: |
| num_failures += 1 |
| raise Exception(msg) |
| except Exception, e: |
| print >> sys.stderr, 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): |
| with open(expected_file, 'rb') as f: |
| fail_if_not_identical(actual, f.read(), fromfile=expected_file) |
| |
| |
| if len(requested) == 0: |
| tests = sorted(os.listdir(os.path.join(options.binaryen_test))) |
| else: |
| tests = requested[:] |
| |
| if not options.interpreter: |
| warn('no interpreter provided (did not test spec interpreter validation)') |
| |
| if not has_vanilla_emcc: |
| warn('no functional emcc submodule found') |
| |
| |
| # check utilities |
| |
| def binary_format_check(wast, verify_final_result=True, wasm_as_args=['-g'], |
| binary_suffix='.fromBinary'): |
| # checks we can convert the wast to binary and back |
| |
| print ' (binary format check)' |
| cmd = WASM_AS + [wast, '-o', 'a.wasm'] + 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'] |
| 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'] |
| 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'] |
| print ' ', ' '.join(cmd) |
| subprocess.check_call( |
| WASM_OPT + [wast, '--print-minified'], |
| stdout=open('a.wast', 'w'), stderr=subprocess.PIPE) |
| assert os.path.exists('a.wast') |
| subprocess.check_call( |
| WASM_OPT + ['a.wast', '--print-minified'], |
| stdout=open('b.wast', 'w'), stderr=subprocess.PIPE) |
| assert os.path.exists('b.wast') |
| if verify_final_result: |
| expected = open('a.wast').read() |
| actual = open('b.wast').read() |
| if actual != expected: |
| fail(actual, expected) |
| if os.path.exists('a.wast'): |
| os.unlink('a.wast') |
| if os.path.exists('b.wast'): |
| os.unlink('b.wast') |
| |
| |
| def files_with_pattern(*path_pattern): |
| return sorted(glob.glob(os.path.join(*path_pattern))) |