utils/testsuite/testsuite.py (836 lines of code) (raw):

#!/usr/bin/env python3 # Copyright (c) Meta Platforms, Inc. and affiliates. # # This source code is licensed under the MIT license found in the # LICENSE file in the root directory of this source tree. import argparse import enum import os import re import subprocess import sys import tempfile import textwrap import time from collections import namedtuple from multiprocessing import Pool, Value from os.path import basename, isdir, isfile, join, splitext try: import testsuite.esprima_test_runner as esprima from testsuite.testsuite_skiplist import ( SKIP_LIST, HANDLESAN_SKIP_LIST, LAZY_SKIP_LIST, PERMANENT_SKIP_LIST, UNSUPPORTED_FEATURES, PERMANENT_UNSUPPORTED_FEATURES, INTL_TESTS, ) except ImportError: import esprima_test_runner as esprima # Hacky way to handle non-buck builds that call the file immediately. from testsuite_skiplist import ( SKIP_LIST, HANDLESAN_SKIP_LIST, LAZY_SKIP_LIST, PERMANENT_SKIP_LIST, UNSUPPORTED_FEATURES, PERMANENT_UNSUPPORTED_FEATURES, INTL_TESTS, ) ## This is a simple script that runs the hermes compiler on ## external test suites. The script expects to find the hermes compiler under ## ./bin/hermes. The script adds ## some basic test built-ins such as assertTrue, assertEquals that are used in ## the V8 test suite. ## How results are computed: ## If a test is on the skip list or contains unsupported ES6 features, ## it is skipped, and thus not executed at all. ## Result classes: ## Compile fail: The Hermes compiler failed when it should have succeeded, ## or vice versa. ## Compile timeout: The Hermes compiler timed out. ## Execute fail: Bytecode execution with the chosen backend ## failed when it should have succeeded, or vice versa. ## Execute timeout: Bytecode execution with the chosen backend timed out. ## Strictness: ## The default strictness mode is currently non-strict. ## For the test suites themselves: ## - test262: Require the test to pass in both strictness modes, ## generating source code for both modes automatically. ## - mjsunit: Run the tests in non-strict mode, ## because the test suite adds its own "use strict" directives. ## The content of this string is prepended to the test files and is used to ## provide the basic test built-ins. test_builtins_content = """ // v8 test harness: function internal_arraysEqual(a, b) { if (a === b) return true; if (a.length != b.length) return false; for (var i = 0; i < a.length; ++i) { if (a[i] !== b[i]) return false; } return true; } function builtin_nop(x) { return x; } function builtin_false() { return false; } var nopSentinel = {}; function v8pragma_HaveSameMap(obj1, obj2) { // This function doesn't work for all tests, but works for many. var keysAreSubset = function(lhs, rhs) { for (var property in lhs) { if (lhs[property] !== rhs[property]) { return false; } } return true; } return keysAreSubset(obj1, obj2) && keysAreSubset(obj2, obj1); } function v8pragma_FunctionSetPrototype(f, p) { // Set f.prototype. f.prototype = p; } function v8pragma_ClassOf(obj) { // Turn "[object ClassName]" into just "ClassName". return Object.prototype.toString.call(obj).slice(8, -1); } function v8pragma_Call(f, thisVal) { return f.apply(thisVal, Array.prototype.slice.call(arguments, 2)); } function v8pragma_StringCharFromCode(i) { return String.fromCharCode(i); } function v8pragma_StringCharCodeAt(s, i) { return s.charCodeAt(i); } // debug variable sometimes used in mjsunit. // Implemented the same way JSC does. var debug = function(s) { print('-->', s); }; // The idea here is that some pragmas are meaningless for our JS interpreter, // but we don't want to throw out the whole test case. In those cases, just // throw out the assertions in those test cases resulting from checking the // results of those pragmas. function v8pragma_NopSentinel() { return nopSentinel; } // test262 requirements. // Leave the unimplemented features unset in $262. var $262 = {}; $262.global = this; $262.evalScript = eval; if (typeof HermesInternal === 'object') { $262.detachArrayBuffer = HermesInternal.detachArrayBuffer; } // Browser functions: var alert = print; """ # Colors for stdout. @enum.unique class Color(enum.Enum): RESET = enum.auto() RED = enum.auto() GREEN = enum.auto() def __str__(self): if not sys.stdout.isatty(): return "" return { Color.RESET.value: "\033[0m", Color.RED.value: "\033[31m", Color.GREEN.value: "\033[32m", }[self.value] # These flags indicate the status of a job. @enum.unique class TestFlag(enum.Enum): TEST_FAILED = enum.auto() TEST_PASSED = enum.auto() TEST_SKIPPED = enum.auto() TEST_PERMANENTLY_SKIPPED = enum.auto() TEST_UNEXPECTED_PASSED = enum.auto() COMPILE_FAILED = enum.auto() COMPILE_TIMEOUT = enum.auto() EXECUTE_FAILED = enum.auto() EXECUTE_TIMEOUT = enum.auto() def __str__(self): return { TestFlag.TEST_FAILED.value: "TEST_FAILED", TestFlag.TEST_PASSED.value: "TEST_PASSED", TestFlag.TEST_SKIPPED.value: "TEST_SKIPPED", TestFlag.TEST_PERMANENTLY_SKIPPED.value: "TEST_PERMANENTLY_SKIPPED", TestFlag.TEST_UNEXPECTED_PASSED.value: "TEST_UNEXPECTED_PASSED", TestFlag.COMPILE_FAILED.value: "COMPILE_FAILED", TestFlag.COMPILE_TIMEOUT.value: "COMPILE_TIMEOUT", TestFlag.EXECUTE_FAILED.value: "EXECUTE_FAILED", TestFlag.EXECUTE_TIMEOUT.value: "EXECUTE_TIMEOUT", }[self.value] TIMEOUT_COMPILER = 200 TIMEOUT_VM = 200 includesMatcher = re.compile(r"includes:\s*\[(.*)\]") # This matches a special case in which the includes looks like: # includes: # - foo.js # This regex works only because the few cases which use this pattern # only include one file. specialIncludesMatcher = re.compile( "includes:\n" r".*-\s*(.*\.js)" "\n", re.MULTILINE | re.DOTALL ) def generateSource(content, strict, suite, flags): """ Generate the source code for a test case resulting from resolving pragmas in the given file and adding a use-strict directive, if necessary. Return a tuple: (source, includes) """ # The raw flag specifies that the source code shouldn't be modified. if "raw" in flags: return (content, []) v8_pragmas = { "%OptimizeObjectForAddingMultipleProperties": "builtin_nop", "%ClearFunctionTypeFeedback": "builtin_nop", "%OptimizeFunctionOnNextCall": "builtin_nop", "%DeoptimizeFunction": "builtin_nop", "%DeoptimizeNow": "builtin_nop", "%_DeoptimizeNow": "builtin_nop", "%NeverOptimizeFunction": "builtin_nop", "%OptimizeOsr": "builtin_nop", "%ClearFunctionTypeFeedback": "builtin_nop", "%BaselineFunctionOnNextCall": "builtin_nop", "%SetForceInlineFlag": "builtin_nop", "%OptimizeObjectForAddingMultipleProperties": "builtin_nop", "%ToFastProperties": "builtin_nop", "%NormalizeElements": "builtin_nop", "%ArrayBufferNeuter": "HermesInternal.detachArrayBuffer", # ArrayBufferDetach is the more modern version of ArrayBufferNeuter. "%ArrayBufferDetach": "HermesInternal.detachArrayBuffer", "%RunMicrotasks": "builtin_nop", "%SetAllocationTimeout": "builtin_nop", "%UnblockConcurrentRecompilation": "builtin_nop", "%DebugPrint": "builtin_nop", "%HaveSameMap": "v8pragma_HaveSameMap", "%HasFastDoubleElements": "v8pragma_NopSentinel", "%HasFastSmiElements": "v8pragma_NopSentinel", "%HasFastObjectElements": "v8pragma_NopSentinel", "%HasFastHoleyElements": "v8pragma_NopSentinel", "%HasFastProperties": "v8pragma_NopSentinel", "%IsAsmWasmCode": "v8pragma_NopSentinel", "%IsNotAsmWasmCode": "v8pragma_NopSentinel", "%NotifyContextDisposed": "v8pragma_NopSentinel", "%FunctionSetPrototype": "v8pragma_FunctionSetPrototype", "%_ClassOf": "v8pragma_ClassOf", "%_Call": "v8pragma_Call", "%RunningInSimulator": "builtin_false", "%IsConcurrentRecompilationSupported": "builtin_false", "%_StringCharFromCode": "v8pragma_StringCharFromCode", "%_StringCharCodeAt": "v8pragma_StringCharCodeAt", } for pragma, replacement in v8_pragmas.items(): content = content.replace(pragma, replacement) source = "" if strict: source += "'use strict';\n" includes = [] if suite: if "test262" in suite: match = includesMatcher.search(content) includes = ["assert.js", "sta.js"] if match: includes += [i.strip() for i in match.group(1).split(",")] match = specialIncludesMatcher.search(content) if match: includes.append(match.group(1)) for i in includes: filepath = join(suite, "harness", i) with open(filepath, "rb") as f: source += f.read().decode("utf-8") + "\n" if "mjsunit" in suite: filepath = join(suite, "mjsunit.js") with open(filepath, "rb") as f: source += f.read().decode("utf-8") + "\n" source += test_builtins_content source += content return (source, includes) evalMatcher = re.compile(r"\beval\s*\(") indirectEvalMatcher = re.compile(r"\(.*,\s*eval\)\s*\(") assignEvalMatcher = re.compile(r"=\s*eval\s*;") withMatcher = re.compile(r"\bwith\s*\(") constMatcher = re.compile(r"\bconst\b") negativeMatcher = re.compile( r""" /\*---.* negative:.*\n \s*phase:\s*(\S+).*\n \s*type:\s*(\S+).*\n ---\*/ """, re.MULTILINE | re.DOTALL | re.VERBOSE, ) negativeMatcher2 = re.compile( r""" /\*---.* negative:.*\n \s*type:\s*(\S+).*\n \s*phase:\s*(\S+).*\n ---\*/ """, re.MULTILINE | re.DOTALL | re.VERBOSE, ) flagsMatcher = re.compile(r"\s*flags:\s*\[(.*)\]") featuresMatcher = re.compile(r"\s*features:\s*\[(.*)\]") # Alternate features syntax has "features:" and then bullet points using "-". featuresMatcher2 = re.compile(r"\s*features:\s*\n(.*)\*\/", re.MULTILINE | re.DOTALL) def getSuite(filename): suite = None # Try all possible test suites to see which one we're in. for s in ["test262", "mjsunit", "CVEs", "esprima", "flow"]: if (s + "/") in filename: suite = filename[: filename.find(s) + len(s)] break return suite verbose = False def printVerbose(s): global verbose if verbose: print(s) istty = sys.stdout.isatty() completed = Value("i", 0) ttyWidth = os.get_terminal_size().columns if istty else 0 count = -1 def showStatus(filename): global completed, istty, verbose, count if istty and not verbose and count > 0: with completed.get_lock(): record = ("\r{:" + str(ttyWidth) + "s}\n").format("Testing " + filename) status = "{:06.2f}% ({:d} / {:d})".format( 100.0 * completed.value / count, completed.value, count ) sys.stdout.write(record + status) sys.stdout.flush() completed.value += 1 else: print("Testing " + filename) es6_args = ["-Xes6-promise", "-Xes6-proxy"] extra_run_args = ["-Xhermes-internal-test-methods"] extra_compile_flags = ["-fno-static-builtins"] def fileInSkiplist(filename, skiplist): for blName in skiplist: if isinstance(blName, str): if blName in filename: return True else: # Assume it's a regex if it's not a string. if blName.search(filename): return True return False # should_run: bool, If the test should run # skip_reason: str, Reason for skipping, if the test shouldn't be run. # Empty if the test should be run (str) # permanent: bool, If the test shouldn't be run, whether that condition is permanent # flags: Set[str], The flags that were found for the file # strict_modes: List[str], The strict modes that this file should be run with TestContentParameters = namedtuple( "TestContentFlags", ["should_run", "skip_reason", "permanent", "flags", "strict_modes"], ) def testShouldRun(filename, content): suite = getSuite(filename) # Determine flags and strict modes before deciding to skip a test case. flags = set() strictModes = [] if not suite: strictModes = [False] else: if "test262" in suite: match = flagsMatcher.search(content) if match: flags = {flag.strip() for flag in match.group(1).split(",")} if "onlyStrict" in flags: strictModes = [True] elif "noStrict" in flags or "raw" in flags: strictModes = [False] else: strictModes = [True, False] else: strictModes = [True, False] elif "mjsunit" in suite: strictModes = [False] elif "CVEs" in suite: strictModes = [False] else: raise Exception("Unknown suite") # Now find if this test case should be skipped. if "async" in flags: # We don't support async operations. return TestContentParameters( False, "Skipping test with async", False, flags, strictModes ) if "module" in flags: # We don't support module code. return TestContentParameters( False, "Skipping test with modules", False, flags, strictModes ) # Be picky about which tests to run unless we are running the CVEs suite runAll = "CVEs" in suite if not runAll: # Skip tests that use 'eval'. if evalMatcher.search(content): return TestContentParameters( False, "Skipping test with eval()", True, flags, strictModes ) # Skip tests that use indirect 'eval' that look like (1, eval)(...). if indirectEvalMatcher.search(content): return TestContentParameters( False, "Skipping test with indirect eval()", True, flags, strictModes ) # Skip tests that use indirect 'eval' by assigning a variable to eval. if assignEvalMatcher.search(content): return TestContentParameters( False, "Skipping test with alias to eval()", True, flags, strictModes ) # Skip tests that use 'with'. if withMatcher.search(content): return TestContentParameters( False, "Skipping test with with()", True, flags, strictModes ) if constMatcher.search(content): return TestContentParameters( False, "Skipping test with 'const'", False, flags, strictModes ) if suite and "test262" in suite: # Skip unsupported features. match = featuresMatcher.search(content) match2 = featuresMatcher2.search(content) features = set() if match: features.update(feature.strip() for feature in match.group(1).split(",")) if match2: features.update( feature.strip(" \t\n\r-") for feature in match2.group(1).split("\n") ) features.discard("") for f in features: if f in UNSUPPORTED_FEATURES + PERMANENT_UNSUPPORTED_FEATURES: return TestContentParameters( False, "Skipping unsupported feature: " + f, f in PERMANENT_UNSUPPORTED_FEATURES, flags, strictModes, ) return TestContentParameters(True, "", False, flags, strictModes) ESPRIMA_TEST_STATUS_MAP = { esprima.TestStatus.TEST_PASSED: TestFlag.TEST_PASSED, esprima.TestStatus.TEST_FAILED: TestFlag.COMPILE_FAILED, esprima.TestStatus.TEST_SKIPPED: TestFlag.TEST_SKIPPED, esprima.TestStatus.TEST_TIMEOUT: TestFlag.COMPILE_TIMEOUT, } def runTest( filename, test_skiplist, keep_tmp, binary_path, hvm, esprima_runner, lazy, test_intl ): """ Runs a single js test pointed by filename """ baseFileName = basename(filename) suite = getSuite(filename) skiplisted = fileInSkiplist(filename, SKIP_LIST + PERMANENT_SKIP_LIST) if lazy: skiplisted = skiplisted or fileInSkiplist(filename, LAZY_SKIP_LIST) if not test_intl: skiplisted = skiplisted or fileInSkiplist(filename, INTL_TESTS) skippedType = ( TestFlag.TEST_PERMANENTLY_SKIPPED if fileInSkiplist(filename, PERMANENT_SKIP_LIST) else TestFlag.TEST_SKIPPED ) if skiplisted and not test_skiplist: printVerbose( "Skipping test in skiplist{}: {}".format( " (permanently)" if skippedType is TestFlag.TEST_PERMANENTLY_SKIPPED else "", filename, ) ) return (skippedType, "", 0) showStatus(filename) if "esprima" in suite or "flow" in suite: hermes_path = os.path.join(binary_path, "hermes") test_res = esprima_runner.run_test(suite, filename, hermes_path) if test_res[0] == esprima.TestStatus.TEST_PASSED and skiplisted: printVerbose("FAIL: A skiplisted test completed successfully") return (TestFlag.TEST_UNEXPECTED_PASSED, "", 0) return ( ESPRIMA_TEST_STATUS_MAP[test_res[0]], "" if test_res[0] == esprima.TestStatus.TEST_PASSED else test_res[1], 0, ) content = open(filename, "rb").read().decode("utf-8") shouldRun, skipReason, permanent, flags, strictModes = testShouldRun( filename, content ) if not shouldRun: skippedType = ( TestFlag.TEST_SKIPPED if not permanent else TestFlag.TEST_PERMANENTLY_SKIPPED ) if not test_skiplist: printVerbose( skipReason + "{}: ".format(" (permanently)" if permanent else "") + filename ) return (skippedType, "", 0) # Check if the test is expected to fail, and how. negativePhase = "" m = negativeMatcher.search(content) if m: negativePhase = m.group(1) else: m = negativeMatcher2.search(content) if m: negativePhase = m.group(2) # Report the max duration of any successful run for the variants of a test. # Unsuccessful runs are ignored for simplicity. max_duration = 0 for strictEnabled in strictModes: temp = tempfile.NamedTemporaryFile( prefix=splitext(baseFileName)[0] + "-", suffix=".js", delete=False ) source, includes = generateSource(content, strictEnabled, suite, flags) source = source.encode("utf-8") if "testIntl.js" in includes: # No support for multiple Intl constructors in that file. return (TestFlag.TEST_SKIPPED, "", 0) temp.write(source) temp.close() printVerbose("\n==============") printVerbose("Strict Mode: {}".format(str(strictEnabled))) printVerbose("Temp js file name: " + temp.name) if lazy: run_vm = True fileToRun = temp.name start = time.time() else: errString = "" binfile = tempfile.NamedTemporaryFile( prefix=splitext(baseFileName)[0] + "-", suffix=".hbc", delete=False ) binfile.close() fileToRun = binfile.name for optEnabled in (True, False): printVerbose("\nRunning with Hermes...") printVerbose("Optimization: {}".format(str(optEnabled))) run_vm = True start = time.time() # Compile to bytecode with Hermes. try: printVerbose("Compiling: {} to {}".format(filename, binfile.name)) args = [ os.path.join(binary_path, "hermesc"), temp.name, "-hermes-parser", "-emit-binary", "-out", binfile.name, ] + extra_compile_flags if optEnabled: args.append("-O") else: args.append("-O0") if strictEnabled: args.append("-strict") else: args.append("-non-strict") subprocess.check_output( args, timeout=TIMEOUT_COMPILER, stderr=subprocess.STDOUT ) if negativePhase == "early" or negativePhase == "parse": run_vm = False printVerbose( "FAIL: Compilation failure expected on {} with Hermes".format( baseFileName ) ) # If the test was in the skiplist, it was possible a # compiler failure was expected. Else, it is unexpected and # will return a failure. return ( (skippedType, "", 0) if skiplisted else (TestFlag.COMPILE_FAILED, "", 0) ) except subprocess.CalledProcessError as e: run_vm = False if negativePhase != "early" and negativePhase != "parse": printVerbose( "FAIL: Compilation failed on {} with Hermes".format( baseFileName ) ) errString = e.output.decode("utf-8").strip() printVerbose(textwrap.indent(errString, "\t")) return ( (skippedType, "", 0) if skiplisted else (TestFlag.COMPILE_FAILED, errString, 0) ) printVerbose("PASS: Hermes correctly failed to compile") except subprocess.TimeoutExpired: printVerbose( "FAIL: Compilation timed out on {}".format(baseFileName) ) return ( (skippedType, "", 0) if skiplisted else (TestFlag.COMPILE_TIMEOUT, "", 0) ) # If the compilation succeeded, run the bytecode with the specified VM. if run_vm: try: printVerbose("Running with HBC VM: {}".format(filename)) # Run the hermes vm. if lazy: binary = os.path.join(binary_path, "hermes") else: binary = os.path.join(binary_path, hvm) disableHandleSanFlag = ( ["-gc-sanitize-handles=0"] if fileInSkiplist(filename, HANDLESAN_SKIP_LIST) else [] ) args = ( [binary, fileToRun] + es6_args + extra_run_args + disableHandleSanFlag ) if lazy: args.append("-lazy") if strictEnabled: args.append("-strict") else: args.append("-non-strict") env = {"LC_ALL": "en_US.UTF-8"} if sys.platform == "linux": env["ICU_DATA"] = binary_path printVerbose(" ".join(args)) subprocess.check_output( args, timeout=TIMEOUT_VM, stderr=subprocess.STDOUT, env=env ) if (not lazy and negativePhase == "runtime") or ( lazy and negativePhase != "" ): printVerbose("FAIL: Expected execution to throw") return ( (skippedType, "", 0) if skiplisted else (TestFlag.EXECUTE_FAILED, "", 0) ) else: printVerbose("PASS: Execution completed successfully") except subprocess.CalledProcessError as e: if (not lazy and negativePhase != "runtime") or ( lazy and negativePhase == "" ): printVerbose( "FAIL: Execution of {} threw unexpected error".format(filename) ) printVerbose("Return code: {}".format(e.returncode)) if e.output: printVerbose("Output:") errString = e.output.decode("utf-8").strip() printVerbose(textwrap.indent(errString, "\t")) else: printVerbose("No output received from process") return ( (skippedType, "", 0) if skiplisted else (TestFlag.EXECUTE_FAILED, errString, 0) ) else: printVerbose("PASS: Execution of binary threw an error as expected") except subprocess.TimeoutExpired: printVerbose("FAIL: Execution of binary timed out") return ( (skippedType, "", 0) if skiplisted else (TestFlag.EXECUTE_TIMEOUT, "", 0) ) max_duration = max(max_duration, time.time() - start) if not keep_tmp: os.unlink(temp.name) if not lazy: os.unlink(binfile.name) if skiplisted: # If the test was skiplisted, but it passed successfully, consider that # an error case. printVerbose("FAIL: A skiplisted test completed successfully") return (TestFlag.TEST_UNEXPECTED_PASSED, "", max_duration) else: printVerbose("PASS: Test completed successfully") return (TestFlag.TEST_PASSED, "", max_duration) def makeCalls(params, onlyfiles, rangeLeft, rangeRight): global count # Store all test parameters in calls[]. calls = [] count = -1 for f in onlyfiles: count += 1 if count < rangeLeft or count > rangeRight: continue calls.append((f,) + params) return calls def calcParams(params): return (params[0], runTest(*params)) def testLoop(calls, jobs, fail_fast, num_slowest_tests): results = [] # Histogram for results from the Hermes compiler. resultsHist = { TestFlag.COMPILE_FAILED: 0, TestFlag.COMPILE_TIMEOUT: 0, TestFlag.EXECUTE_FAILED: 0, TestFlag.EXECUTE_TIMEOUT: 0, TestFlag.TEST_PASSED: 0, TestFlag.TEST_SKIPPED: 0, TestFlag.TEST_PERMANENTLY_SKIPPED: 0, TestFlag.TEST_UNEXPECTED_PASSED: 0, } slowest_tests = [("", 0)] * num_slowest_tests with Pool(processes=jobs) as pool: for res in pool.imap_unordered(calcParams, calls, 1): testname = res[0] results.append(res) (hermesStatus, errString, duration) = res[1] resultsHist[hermesStatus] += 1 insert_pos = len(slowest_tests) for i, (_, other_duration) in reversed(list(enumerate(slowest_tests))): if duration < other_duration: break else: insert_pos = i if insert_pos < len(slowest_tests): # If this was one of the slowest tests, push it into the list # and drop the bottom of the list. slowest_tests = ( slowest_tests[:insert_pos] + [(testname, duration)] + slowest_tests[insert_pos:-1] ) if ( fail_fast and hermesStatus != TestFlag.TEST_PASSED and hermesStatus != TestFlag.TEST_SKIPPED and hermesStatus != TestFlag.TEST_PERMANENTLY_SKIPPED ): break # Filter out missing test names in case there were fewer tests run than the top slowest tests. slowest_tests = [ (testName, duration) for testName, duration in slowest_tests if testName ] return results, resultsHist, slowest_tests def get_arg_parser(): parser = argparse.ArgumentParser(description="Run javascript tests with Hermes.") parser.add_argument( "paths", type=str, nargs="+", help="Paths to test suite, can be either dir or file name", ) parser.add_argument( "-c", "--chunk", dest="chunk", default=-1, type=int, help="Chunk ID (0, 1, 2), to only process 1/3 of all tests", ) parser.add_argument( "-f", "--fast-fail", dest="fail_fast", action="store_true", help="Exit script immediately when a test failed.", ) parser.add_argument( "-k", "--keep-tmp", dest="keep_tmp", action="store_true", help="Keep temporary files of successful tests.", ) parser.add_argument( "--test-skiplist", dest="test_skiplist", action="store_true", help="Also test if tests in the skiplist fail", ) parser.add_argument( "-a", "--show-all", dest="show_all", action="store_true", help="show results of successful tests.", ) parser.add_argument( "--hvm-filename", dest="hvm_filename", default="hvm", help="Filename for hvm binary (e.g., hvm-lean)", ) parser.add_argument( "-j", "--jobs", dest="jobs", default=None, type=int, help="Number of jobs to run simultaneously. By default " + "equal to the number of CPUs.", ) parser.add_argument( "-m", "--match", dest="match", default=None, type=str, help="Optional. Substring that the test filename must " "contain in order to run.", ) parser.add_argument( "-s", "--source", dest="source", action="store_true", help="Instead of running any tests, print the source of " "the matched test case (use -m/--match) to standard " "output, including any generated use-strict " "directives or stubbed pragmas. (You could then " "pipe this to hermes.)", ) parser.add_argument( "--num-slowest-tests", dest="num_slowest_tests", type=int, default=10, help="Print the top N tests that take the longest time to execute on " "average, where N is the option value", ) parser.add_argument( "-v", "--verbose", dest="verbose", default=False, action="store_true", help="Show intermediate output", ) parser.add_argument( "--lazy", dest="lazy", default=False, action="store_true", help="Force lazy evaluation", ) parser.add_argument( "--test-intl", dest="test_intl", default=False, action="store_true", help="Run supported Intl tests.", ) return parser def run( paths, chunk, fail_fast, binary_path, hvm, jobs, is_verbose, match, source, test_skiplist, num_slowest_tests, keep_tmp, show_all, lazy, test_intl, ): global count global verbose verbose = is_verbose onlyfiles = [] for path in paths: if isdir(path): for root, _dirnames, filenames in os.walk(path): for filename in filenames: onlyfiles.append(os.path.join(root, filename)) elif isfile(path): onlyfiles.append(path) else: print("Invalid path: " + path) sys.exit(1) def isTest(f): isFixture = "test262" in f and f.endswith("_FIXTURE.js") return f.endswith(".js") and not isFixture onlyfiles = [f for f in onlyfiles if isTest(f) if not match or match in f] # Generates the source for the single provided file, # without an extra "use strict" directive prepended to the file. # Handles [noStrict] and [onlyStrict] flags. if source: if len(onlyfiles) != 1: print("Need exactly one file matched by the -m/--match option.") print("Got these files: " + ", ".join(onlyfiles)) sys.exit(1) with open(onlyfiles[0], "rb") as f: content = f.read().decode("utf-8") match = flagsMatcher.search(content) flags = set() if match: flags = {flag.strip() for flag in match.group(1).split(",")} strict = False if "noStrict" in flags or "raw" in flags: strict = False if "onlyStrict" in flags: strict = True print(generateSource(content, strict, getSuite(onlyfiles[0]), flags)[0]) sys.exit(0) rangeLeft = 0 rangeRight = len(onlyfiles) - 1 if chunk != -1: if chunk == 0: rangeRight = rangeRight // 3 - 1 elif chunk == 1: rangeLeft = rangeRight // 3 rangeRight = rangeRight - rangeLeft elif chunk == 2: rangeLeft = rangeRight - rangeRight // 3 + 1 else: print("Invalid chunk ID") sys.exit(1) if not os.path.isfile(join(binary_path, "hermes")): print("{} not found.".format(join(binary_path, "hermes"))) sys.exit(1) if not os.path.isfile(join(binary_path, hvm)): print("{} not found.".format(join(binary_path, hvm))) sys.exit(1) esprima_runner = esprima.EsprimaTestRunner(verbose) calls = makeCalls( (test_skiplist, keep_tmp, binary_path, hvm, esprima_runner, lazy, test_intl), onlyfiles, rangeLeft, rangeRight, ) results, resultsHist, slowest_tests = testLoop( calls, jobs, fail_fast, num_slowest_tests ) # Sort the results for easier reading of failed tests. results.sort(key=lambda f: f[1][0].value) if results: print("") for testName, (hermesStatus, errString, _) in results: if show_all or ( (hermesStatus != TestFlag.TEST_PASSED) and (hermesStatus != TestFlag.TEST_SKIPPED) and (hermesStatus != TestFlag.TEST_PERMANENTLY_SKIPPED) ): print("{} {}".format(str(hermesStatus), testName)) if errString: print("{}".format(textwrap.indent(errString, "\t"))) if slowest_tests: print() print("Top {:d} slowest tests".format(len(slowest_tests))) maxNameWidth = 0 maxNumWidth = 0 for testName, duration in slowest_tests: maxNameWidth = max(maxNameWidth, len(testName)) maxNumWidth = max(maxNumWidth, len("{:.3f}".format(duration))) for testName, duration in slowest_tests: print( "{:<{testNameWidth}} {:>{durationWidth}.3f}".format( testName, duration, # Add 3 just in case it's right at the borderline testNameWidth=maxNameWidth + 3, durationWidth=maxNumWidth, ) ) print() total = sum(resultsHist.values()) failed = ( resultsHist[TestFlag.COMPILE_FAILED] + resultsHist[TestFlag.COMPILE_TIMEOUT] + resultsHist[TestFlag.EXECUTE_FAILED] + resultsHist[TestFlag.EXECUTE_TIMEOUT] + resultsHist[TestFlag.TEST_UNEXPECTED_PASSED] ) eligible = ( sum(resultsHist.values()) - resultsHist[TestFlag.TEST_SKIPPED] - resultsHist[TestFlag.TEST_PERMANENTLY_SKIPPED] ) if eligible > 0: passRate = "{0:.2%}".format(resultsHist[TestFlag.TEST_PASSED] / eligible) else: passRate = "--" if (eligible - resultsHist[TestFlag.TEST_PASSED]) > 0: resultStr = "{}FAIL{}".format(Color.RED, Color.RESET) else: resultStr = "{}PASS{}".format(Color.GREEN, Color.RESET) # Turn off formatting so that the table looks nice in source code. # fmt: off print("-----------------------------------") print("| Results | {} |".format(resultStr)) print("|----------------------+----------|") print("| Total | {:>8} |".format(total)) print("| Pass | {:>8} |".format(resultsHist[TestFlag.TEST_PASSED])) print("| Fail | {:>8} |".format(failed)) print("| Skipped | {:>8} |".format(resultsHist[TestFlag.TEST_SKIPPED])) print("| Permanently Skipped | {:>8} |".format(resultsHist[TestFlag.TEST_PERMANENTLY_SKIPPED])) print("| Pass Rate | {:>8} |".format(passRate)) print("-----------------------------------") print("| Failures | |") print("|----------------------+----------|") print("| Compile fail | {:>8} |".format(resultsHist[TestFlag.COMPILE_FAILED])) print("| Compile timeout | {:>8} |".format(resultsHist[TestFlag.COMPILE_TIMEOUT])) print("| Execute fail | {:>8} |".format(resultsHist[TestFlag.EXECUTE_FAILED])) print("| Execute timeout | {:>8} |".format(resultsHist[TestFlag.EXECUTE_TIMEOUT])) if test_skiplist: print("| Skiplisted passes | {:>8} |".format(resultsHist[TestFlag.TEST_UNEXPECTED_PASSED])) print("-----------------------------------") # fmt: on return (eligible - resultsHist[TestFlag.TEST_PASSED]) > 0