#!/usr/bin/env ruby # Copyright (C) 2013-2021 Apple Inc. All rights reserved. # # Redistribution and use in source and binary forms, with or without # modification, are permitted provided that the following conditions # are met: # # 1. Redistributions of source code must retain the above copyright # notice, this list of conditions and the following disclaimer. # 2. Redistributions in binary form must reproduce the above copyright # notice, this list of conditions and the following disclaimer in the # documentation and/or other materials provided with the distribution. # # THIS SOFTWARE IS PROVIDED BY APPLE AND ITS CONTRIBUTORS "AS IS" AND ANY # EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED # WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE # DISCLAIMED. IN NO EVENT SHALL APPLE OR ITS CONTRIBUTORS BE LIABLE FOR ANY # DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES # (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; # LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND # ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT # (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF # THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. require 'fileutils' require 'getoptlong' require 'ostruct' require 'pathname' require 'rbconfig' require 'set' require 'tempfile' require 'uri' require 'yaml' require_relative "webkitruby/jsc-stress-test/test-result-evaluator" require_relative "webkitruby/jsc-stress-test/executor" module URI class SSH < Generic DEFAULT_PORT = 22 end unless defined?(self.register_scheme) def self.register_scheme(scheme, klass) @@schemes[scheme] = klass end end register_scheme 'SSH', SSH end class String def scrub encode("UTF-16be", :invalid=>:replace, :replace=>"?").encode('UTF-8') end end Signal.trap("TERM") { # Occasionally, the jscore-test step fails to produce any output for 10' at # which point it gets killed by the buildbot code. Try to diagnose. puts("Received SIGTERM") puts(Thread.current.backtrace) } RemoteHost = Struct.new(:name, :user, :host, :port, :remoteDirectory, :identity_file_path) THIS_SCRIPT_PATH = Pathname.new(__FILE__).realpath SCRIPTS_PATH = THIS_SCRIPT_PATH.dirname WEBKIT_PATH = SCRIPTS_PATH.dirname.dirname LAYOUTTESTS_PATH = WEBKIT_PATH + "LayoutTests" WASMTESTS_PATH = WEBKIT_PATH + "JSTests/wasm" JETSTREAM2_PATH = WEBKIT_PATH + "PerformanceTests/JetStream2" CHAKRATESTS_PATH = WEBKIT_PATH + "JSTests/ChakraCore/test" raise unless SCRIPTS_PATH.basename.to_s == "Scripts" raise unless SCRIPTS_PATH.dirname.basename.to_s == "Tools" HELPERS_PATH = SCRIPTS_PATH + "jsc-stress-test-helpers" STATUS_FILE_PREFIX = "test_status_" STATUS_FILE_PASS = "P" STATUS_FILE_FAIL = "F" # There are all random and adequately large to be unlikely to appear # in practice, except as used in this file. PARALLEL_REMOTE_WRAPPER_MARK_BEGIN = "5d65329bd1a3" PARALLEL_REMOTE_WRAPPER_MARK_END = "a9aea5c3b843" PARALLEL_REMOTE_STATE_LOST_MARKER = "709fb7a77c45231918eb118a" ITERATION_LIMITS = OpenStruct.new(:infraIterationsFloor => 3, :iterationsCeiling => 50) REMOTE_TIMEOUT = 120 SSH_OPTIONS_DEFAULT = [ "-o", "NoHostAuthenticationForLocalhost=yes", "-o", "ServerAliveInterval=30", ] begin require 'shellwords' rescue Exception => e $stderr.puts "Warning: did not find shellwords, not running any tests." exit 0 end $canRunDisplayProfilerOutput = false begin require 'rubygems' require 'json' require 'highline' $canRunDisplayProfilerOutput = true rescue Exception => e $stderr.puts "Warning: did not find json or highline; some features will be disabled." $stderr.puts "Run \"sudo gem install json highline\" to fix the issue." $stderr.puts "Error: #{e.inspect}" end def printCommandArray(*cmd) begin commandArray = cmd.each{|value| Shellwords.shellescape(value.to_s)}.join(' ') rescue commandArray = cmd.join(' ') end $stderr.puts ">> #{commandArray}" end class CommandExecutionFailed < Exception end def mysys(commandArray, options={}) printCommandArray(commandArray) if $verbosity >= 1 successful = system(*commandArray) status = $? if successful or options[:ignoreFailure] return end raise CommandExecutionFailed, "Command failed: #{status.inspect}" end def escapeAll(array) array.map { | v | raise "Detected a non-string in #{inspect}" unless v.is_a? String Shellwords.shellescape(v) }.join(' ') end $jscPath = nil $doNotMessWithVMPath = false $jitTests = true $memoryLimited = false $outputDir = Pathname.new("results") $verbosity = 0 $bundle = nil $tarball = false $tarFileName = "payload.tar.gz" $copyVM = false $testRunnerType = nil $testWriter = "default" $remoteHosts = [] $architecture = nil $forceArchitecture = nil $hostOS = nil $model = nil $filter = nil $envVars = [] $mode = "full" $buildType = "release" $forceCollectContinuously = false $reportExecutionTime = false $ldd = nil $artifact_exec_wrapper = nil $numChildProcessesSetByUser = false $runUniqueId = Random.new.bytes(16).unpack("H*")[0] $gnuParallelChunkSize = 1 $testsDebugStream = nil # set to $stderr for debugging def putd(s) $testsDebugStream.puts(s) unless $testsDebugStream.nil? end def usage puts "run-jsc-stress-tests -j [ ...]" puts puts "--jsc (-j) Path to JavaScriptCore build product. This option is required." puts "--no-copy Do not copy the JavaScriptCore build product before testing." puts " --jsc specifies an already present JavaScriptCore to test." puts "--memory-limited Indicate that we are targeting the test for a memory limited device." puts " Skip tests tagged with //@skip if $memoryLimited" puts "--no-jit Do not run JIT specific tests." puts "--force-collectContinuously Enable the collectContinuously mode even if disabled on this" puts " platform." puts "--output-dir (-o) Path where to put results. Default is #{$outputDir}." puts "--verbose (-v) Print more things while running." puts "--run-bundle Runs a bundle previously created by run-jsc-stress-tests." puts "--tarball [fileName] Creates a tarball of the final bundle. Use name if supplied for tar file." puts "--arch Specify architecture instead of determining from JavaScriptCore build." puts "--force-architecture Override the architecture to run tests with." puts " e.g. x86, x86_64, arm." puts "--ldd Use alternate ldd" puts "--artifact-exec-wrapper Wrapper for executing a build artifact" puts "--os Specify os instead of determining from JavaScriptCore build." puts " e.g. darwin, linux & windows." puts "--shell-runner Uses the shell-based test runner instead of the default make-based runner." puts " In general the shell runner is slower than the make runner." puts "--make-runner Uses the faster make-based runner." puts "--ruby-runner Uses the ruby runner for machines without unix shell or make." puts "--test-writer [writer] Specifies the test script format." puts " default is to use shell scripts to run the tests" puts " \"ruby\" to use ruby scripts for systems without a unix shell." puts "--remote Specify a remote host on which to run tests from command line argument." puts "--remote-config-file Specify a remote host on which to run tests from JSON file." puts "--report-execution-time Print execution time for each test." puts "--child-processes (-c) Specify the number of child processes." puts "--filter Only run tests whose name matches the given regular expression." puts "--help (-h) Print this message." puts "--env-vars Add a list of environment variables to set before running jsc." puts " Each environment variable should be separated by a space." puts " e.g. \"foo=bar x=y\" (no quotes). Note, if you pass DYLD_FRAMEWORK_PATH" puts " it will override the default value." puts "--quick (-q) Only run with the default and no-cjit-validate modes." puts "--basic Run with default and these additional modes: no-llint," puts " no-cjit-validate-phases, no-cjit-collect-continuously, dfg-eager" puts " and for FTL platforms: no-ftl, ftl-eager-no-cjit and" puts " ftl-no-cjit-small-pool." exit 1 end jscArg = nil GetoptLong.new(['--help', '-h', GetoptLong::NO_ARGUMENT], ['--jsc', '-j', GetoptLong::REQUIRED_ARGUMENT], ['--no-copy', GetoptLong::NO_ARGUMENT], ['--memory-limited', GetoptLong::NO_ARGUMENT], ['--no-jit', GetoptLong::NO_ARGUMENT], ['--force-collectContinuously', GetoptLong::NO_ARGUMENT], ['--output-dir', '-o', GetoptLong::REQUIRED_ARGUMENT], ['--run-bundle', GetoptLong::REQUIRED_ARGUMENT], ['--tarball', GetoptLong::OPTIONAL_ARGUMENT], ['--force-vm-copy', GetoptLong::NO_ARGUMENT], ['--arch', GetoptLong::REQUIRED_ARGUMENT], ['--force-architecture', GetoptLong::REQUIRED_ARGUMENT], ['--ldd', GetoptLong::REQUIRED_ARGUMENT], ['--artifact-exec-wrapper', GetoptLong::REQUIRED_ARGUMENT], ['--os', GetoptLong::REQUIRED_ARGUMENT], ['--shell-runner', GetoptLong::NO_ARGUMENT], ['--make-runner', GetoptLong::NO_ARGUMENT], ['--ruby-runner', GetoptLong::NO_ARGUMENT], ['--gnu-parallel-runner', GetoptLong::NO_ARGUMENT], ['--gnu-parallel-chunk-size', GetoptLong::REQUIRED_ARGUMENT], ['--test-writer', GetoptLong::REQUIRED_ARGUMENT], ['--treat-failing-as-flaky', GetoptLong::REQUIRED_ARGUMENT], ['--remote', GetoptLong::REQUIRED_ARGUMENT], ['--remote-config-file', GetoptLong::REQUIRED_ARGUMENT], ['--report-execution-time', GetoptLong::NO_ARGUMENT], ['--model', GetoptLong::REQUIRED_ARGUMENT], ['--child-processes', '-c', GetoptLong::REQUIRED_ARGUMENT], ['--filter', GetoptLong::REQUIRED_ARGUMENT], ['--verbose', '-v', GetoptLong::NO_ARGUMENT], ['--env-vars', GetoptLong::REQUIRED_ARGUMENT], ['--debug', GetoptLong::NO_ARGUMENT], ['--release', GetoptLong::NO_ARGUMENT], ['--quick', '-q', GetoptLong::NO_ARGUMENT], ['--basic', GetoptLong::NO_ARGUMENT]).each { | opt, arg | case opt when '--help' usage when '--jsc' jscArg = arg when '--no-copy' $doNotMessWithVMPath = true when '--output-dir' $outputDir = Pathname.new(arg) when '--memory-limited' $memoryLimited = true when '--no-jit' $jitTests = false when '--force-collectContinuously' $forceCollectContinuously = true; when '--verbose' $verbosity += 1 when '--run-bundle' $bundle = Pathname.new(arg) when '--tarball' $tarball = true $copyVM = true $tarFileName = arg unless arg == '' when '--force-vm-copy' $copyVM = true when '--shell-runner' $testRunnerType = :shell when '--make-runner' $testRunnerType = :make when '--ruby-runner' $testRunnerType = :ruby when '--gnu-parallel-runner' $testRunnerType = :gnuparallel when '--gnu-parallel-chunk-size' $gnuParallelChunkSize = arg.to_i when '--test-writer' $testWriter = arg when '--treat-failing-as-flaky' md = /^([^,]+),(\d+),(\d+)$/.match(arg) if md.nil? $stderr.puts("Could not parse argument to `--treat-failing-as-flaky`; expected `passPercentage,maxTries,maxFailing`") exit(1) end passPercentage = md[1].to_f if passPercentage.zero? $stderr.puts("Invalid passPercentage `#{md[1]}`") exit(1) end maxTries = md[2].to_i if maxTries == 0 $stderr.puts("Invalid maxTries `#{md[2]}`") exit(1) end maxFailing = md[3].to_i if maxFailing == 0 $stderr.puts("Invalid maxFailing `#{md[3]}`") exit(1) end $treatFailingAsFlaky = OpenStruct.new(:passPercentage => passPercentage, :maxTries => maxTries, :maxFailing => maxFailing) when '--remote' $copyVM = true $tarball = true $remote = true uri = URI("ssh://" + arg) $remoteHosts << RemoteHost.new("default-#{$remoteHosts.length}", uri.user, uri.host, uri.port) when '--remote-config-file' $remoteConfigFile = arg when '--report-execution-time' $reportExecutionTime = true when '--child-processes' $numChildProcesses = arg.to_i $numChildProcessesSetByUser = true when '--filter' $filter = Regexp.new(arg) when '--arch' $architecture = arg when '--force-architecture' $architecture = arg unless $architecture $forceArchitecture = arg when '--ldd' $ldd = arg when '--artifact-exec-wrapper' $artifact_exec_wrapper = arg when '--os' $hostOS = arg when '--model' $model = arg.gsub(/\A['"]+|['"]+\Z/, '') when '--env-vars' $envVars = arg.gsub(/\s+/, ' ').split(' ') when '--quick' $mode = "quick" when '--basic' $mode = "basic" when '--debug' $buildType = "debug" when '--release' $buildType = "release" end } if $remoteConfigFile file = File.read($remoteConfigFile) config = JSON.parse(file) # old style config allowing for only one remote if !$remote and config['remote'] $copyVM = true $tarball = true $remote = true uri = URI("ssh://" + config['remote']) $remoteHosts = [ RemoteHost.new("default", uri.user, uri.host, uri.port) ] if config['remoteDirectory'] $remoteHosts[0].remoteDirectory = config['remoteDirectory'] end if config['idFilePath'] $remoteHosts[0].identity_file_path = config['idFilePath'] end end # we can combine --remote and a new style config if config['remotes'] $copyVM = true $tarball = true $remote = true $remoteHosts += config['remotes'].map { | remote | uri = URI("ssh://" + remote['address']) host = RemoteHost.new(remote['name'], uri.user, uri.host, uri.port) if remote['remoteDirectory'] host.remoteDirectory = remote['remoteDirectory'] end if remote['idFilePath'] host.identity_file_path = remote['idFilePath'] print('Using identity file: ' + host.identity_file_path + "\r") end host } end end unless jscArg # If we're not provided a JSC path, try to come up with a sensible JSC path automagically. command = SCRIPTS_PATH.join("webkit-build-directory").to_s command += ($buildType == "release") ? " --release" : " --debug" command += " --executablePath" output = `#{command}`.split("\n") if !output.length $stderr.puts "Error: must specify --jsc " exit 1 end output = output[0] jscArg = Pathname.new(output).join("jsc") jscArg = Pathname.new(output).join("JavaScriptCore.framework", "Helpers", "jsc") if !File.file?(jscArg) jscArg = Pathname.new(output).join("bin", "jsc") if !File.file?(jscArg) # Support CMake build. if !File.file?(jscArg) $stderr.puts "Error: must specify --jsc " exit 1 end puts "Using the following jsc path: #{jscArg}" end if $doNotMessWithVMPath $jscPath = Pathname.new(jscArg) else $jscPath = Pathname.new(jscArg).realpath end $progressMeter = ($verbosity == 0 and $stdout.tty? and $remoteHosts.length <= 1) if $bundle $jscPath = $bundle + ".vm" + "JavaScriptCore.framework" + "Helpers" + "jsc" $outputDir = $bundle end # Try to determine architecture. Return nil on failure. def machOArchitectureCode begin otoolLines = `otool -afh #{Shellwords.shellescape($jscPath.to_s)}`.split("\n") otoolLines.each_with_index { | value, index | if value =~ /magic/ and value =~ /cputype/ return otoolLines[index + 1].split[1].to_i end } rescue $stderr.puts "Warning: unable to execute otool." end $stderr.puts "Warning: unable to determine architecture." nil end def determineArchitectureFromMachOBinary code = machOArchitectureCode return nil unless code is64BitFlag = 0x01000000 case code when 7 "x86" when 7 | is64BitFlag "x86-64" when 12 "arm" when 12 | is64BitFlag "arm64" else $stderr.puts "Warning: unable to determine architecture from code: #{code}" nil end end def determineArchitectureFromELFBinary f = File.open($jscPath.to_s) data = f.read(20) if !(data[0,4] == "\x7F\x45\x4C\x46") $stderr.puts "Warning: Missing ELF magic in file #{Shellwords.shellescape($jscPath.to_s)}" return nil end # MIPS and PowerPC may be either big- or little-endian. S390 (which includes # S390x) is big-endian. The rest are little-endian. # For RISC-V, to avoid encoding problems, construct the comparison string # by packing a char array matching the ELF's RISC-V machine value. code = data[18, 20] case code when "\x03\0" "x86" when "\x08\0" "mips" when "\0\x08" "mips" when "\x14\0" "powerpc" when "\0\x14" "powerpc" when "\x15\0" "powerpc64" when "\0\x15" "powerpc64" when "\0\x16" "s390" when "\x28\0" "arm" when "\x3E\0" "x86-64" when "\xB7\0" "arm64" when [243, 0].pack("cc") "riscv64" else $stderr.puts "Warning: unable to determine architecture from code: #{code}" nil end end def determineArchitectureFromPEBinary f = File.open($jscPath.to_s) data = f.read(1024) if !(data[0, 2] == "MZ") $stderr.puts "Warning: Missing PE magic in file #{Shellwords.shellescape($jscPath.to_s)}" return nil end peHeaderAddr = data[0x3c, 4].unpack('V').first # 32-bit unsigned int little endian if !(data[peHeaderAddr, 4] == "PE\0\0") $stderr.puts "Warning: Incorrect PE header in file #{Shellwords.shellescape($jscPath.to_s)}" return nil end machine = data[peHeaderAddr + 4, 2].unpack('v').first # 16-bit unsigned short, little endian case machine when 0x014c "x86" when 0x8664 "x86-64" else $stderr.puts "Warning: unsupported machine type: #{machine}" nil end end def determineArchitecture case $hostOS when "darwin" determineArchitectureFromMachOBinary when "linux" determineArchitectureFromELFBinary when "windows" determineArchitectureFromPEBinary when "playstation" "x86-64" else $stderr.puts "Warning: unable to determine architecture on this platform." nil end end def determineOS case RbConfig::CONFIG["host_os"] when /darwin/i "darwin" when /linux/i "linux" when /mswin|mingw|cygwin/ "windows" else $stderr.puts "Warning: unable to determine host operating system" nil end end $hostOS = determineOS unless $hostOS $architecture = determineArchitecture unless $architecture $isFTLPlatform = !($architecture == "x86" || $architecture == "arm" || $architecture == "mips" || $architecture == "riscv64" || $hostOS == "windows" || $hostOS == "playstation") # Special case armv7, we want to run the wasm tests temporarily without B3/Air support $isWasmPlatform = $isFTLPlatform || $architecture == "arm" if $architecture == "x86" # The JIT is temporarily disabled on this platform since # https://trac.webkit.org/changeset/237547 $jitTests = false end def isFTLEnabled $jitTests && $isFTLPlatform end if !$testRunnerType if $remote and $hostOS == "darwin" $testRunnerType = :shell else $testRunnerType = :make end end if $remoteHosts.length > 1 and ($testRunnerType != :make) and ($testRunnerType != :gnuparallel) raise "Multiple remote hosts only supported with the make or gnu-parallel runners" end if $hostOS == "playstation" && $testWriter == "default" $testWriter = "playstation" end if $testWriter if /[^-a-zA-Z0-9_]/.match($testWriter) raise "Invalid test writer #{$testWriter} given" end end # We force all tests to use a smaller (1.5M) stack so that stack overflow tests can run faster. BASE_OPTIONS = ["--useFTLJIT=false", "--useFunctionDotArguments=true", "--validateExceptionChecks=true", "--useDollarVM=true", "--maxPerThreadStackUsage=1572864"] EAGER_OPTIONS = ["--thresholdForJITAfterWarmUp=10", "--thresholdForJITSoon=10", "--thresholdForOptimizeAfterWarmUp=20", "--thresholdForOptimizeAfterLongWarmUp=20", "--thresholdForOptimizeSoon=20", "--thresholdForFTLOptimizeAfterWarmUp=20", "--thresholdForFTLOptimizeSoon=20", "--thresholdForOMGOptimizeAfterWarmUp=20", "--thresholdForOMGOptimizeSoon=20", "--maximumEvalCacheableSourceLength=150000", "--useEagerCodeBlockJettisonTiming=true", "--repatchBufferingCountdown=0"] # NOTE: Tests rely on this using scribbleFreeCells. NO_CJIT_OPTIONS = ["--useConcurrentJIT=false", "--thresholdForJITAfterWarmUp=100", "--scribbleFreeCells=true"] B3O1_OPTIONS = ["--defaultB3OptLevel=1", "--useDataICInFTL=1", "--forceUnlinkedDFG=1"] B3O0_OPTIONS = ["--maxDFGNodesInBasicBlockForPreciseAnalysis=100", "--defaultB3OptLevel=0"] FTL_OPTIONS = ["--useFTLJIT=true"] FORCE_LLINT_EXIT_OPTIONS = ["--forceOSRExitToLLInt=true"] EXECUTABLE_FUZZER_OPTIONS = ["--useExecutableAllocationFuzz=true", "--fireExecutableAllocationFuzzRandomly=true"] class BasePlan attr_reader :directory, :arguments, :family, :name, :outputHandler, :errorHandler, :additionalEnv, :index attr_accessor :retryParameters @@index = 0 def initialize(directory, arguments, family, name, outputHandler, errorHandler, retryParameters) @directory = directory @arguments = argumentsMapper(arguments) @family = family @name = name @outputHandler = outputHandler @errorHandler = errorHandler # A plan for which @retryParameters is not nil is being # treated as potentially flaky. @retryParameters = retryParameters @additionalEnv = [] @index = @@index @@index += 1 end def self.mock(family, name, retryParameters=nil) self.new("/none", [], family, name, nil, nil, retryParameters) end def self.create(directory, arguments, family, name, outputHandler, errorHandler) if $runCommandOptions[:crashOK] outputHandler = noisyOutputHandler end self.new(directory, arguments, family, name, outputHandler, errorHandler, $runCommandOptions[:flaky]) end def argumentsMapper(args) args end # We regularly place Plans in containers, but may modify @retryParameters # after the fact; only hash on @index instead. def hash @index end def to_s "#{@index}" end end class TestRunner def initialize(testRunnerType, runnerDir) @testRunnerType = testRunnerType @runnerDir = runnerDir end def prepare(runlist, serialPlans, completedPlans, remoteHosts) prepareScripts(runlist) prepareRunner(runlist, serialPlans, completedPlans, remoteHosts) end def prepareScripts(runlist) Dir.mkdir(@runnerDir) unless @runnerDir.directory? toDelete = [] Dir.foreach(@runnerDir) { | filename | if filename =~ /^test_/ toDelete << filename end } toDelete.each { | filename | File.unlink(@runnerDir + filename) } # Write test scripts in parallel as this is both an expensive and a # highly IO intensive operation, but each script is independent and # the operation is pure other than writing the unique run script. parallelEach(runlist) do | plan | plan.writeRunScript(@runnerDir + "test_script_#{plan.index}") end end def self.create(testRunnerType, runnerDir) cls = nil case testRunnerType when :shell cls = TestRunnerShell when :make cls = TestRunnerMake when :ruby cls = TestRunnerRuby when :gnuparallel cls = TestRunnerGnuParallel else raise "Unknown test runner type: #{testRunnerType.to_s}" end return cls.new(testRunnerType, runnerDir) end end require_relative "webkitruby/jsc-stress-test-writer-#{$testWriter}" def shouldCollectContinuously? $buildType == "release" or $forceCollectContinuously end COLLECT_CONTINUOUSLY_OPTIONS = shouldCollectContinuously? ? ["--collectContinuously=true", "--useGenerationalGC=false", "--verifyGC=true"] : [] $serialPlans = Set.new $runlist = [] def frameworkFromJSCPath(jscPath) parentDirectory = jscPath.dirname if (parentDirectory.basename.to_s == "Resources" or parentDirectory.basename.to_s == "Helpers") and parentDirectory.dirname.basename.to_s == "JavaScriptCore.framework" parentDirectory.dirname elsif $hostOS == "playstation" jscPath.dirname elsif parentDirectory.basename.to_s =~ /^Debug/ or parentDirectory.basename.to_s =~ /^Release/ jscPath.dirname + "JavaScriptCore.framework" else $stderr.puts "Warning: cannot identify JSC framework, doing generic VM copy." nil end end # Frequently repeated path computations. Cache results for speed. $bundleResourcePathCache = Hash.new do |h, key| resourcePath, benchmarkDirectory, dir = key benchmarkDirectory.each_filename { | pathComponent | dir = dir.parent } h[key] = dir + resourcePath end def pathToBundleResourceFromBenchmarkDirectory(resourcePath) $bundleResourcePathCache[[resourcePath, $benchmarkDirectory, Pathname.new(".")]] end def pathToVM pathToBundleResourceFromBenchmarkDirectory($jscPath) end def vmCommand cmd = [pathToVM.to_s] if not $artifact_exec_wrapper.nil? cmd.unshift($artifact_exec_wrapper) end if ($forceArchitecture) cmd = ["/usr/bin/arch", "-" + $forceArchitecture] + cmd end return cmd end def pathToHelpers pathToBundleResourceFromBenchmarkDirectory(".helpers") end $runCommandOptions = {} $testSpecificRequiredOptions = [] $uniqueFilenameCounter = 0 def uniqueFilename(extension) payloadDir = $outputDir + "_payload" Dir.mkdir payloadDir unless payloadDir.directory? result = payloadDir.realpath + "temp-#{$uniqueFilenameCounter}#{extension}" $uniqueFilenameCounter += 1 result end def baseOutputName(kind) "#{$collectionName}/#{$benchmark}.#{kind}" end def addRunCommandCfg(cfg, *additionalEnv) [:kind, :command, :outputHandler, :errorHandler].each { |key| if not cfg.has_key?(key) raise "Missing #{key} in #{cfg}" end } $didAddRunCommand = true name = baseOutputName(cfg[:kind]) if $filter and name !~ $filter return end plan = Plan.create( $benchmarkDirectory, cfg[:command], "#{$collectionName}/#{$benchmark}", name, cfg[:outputHandler], cfg[:errorHandler]) if cfg.has_key?(:additionalEnv) plan.additionalEnv.push(*(cfg[:additionalEnv])) end if $runCommandOptions[:serial] # Add this to the list of tests to be run on their own, so # that we can treat them specially when scheduling, but keep # it in the $runlist for code that dosn't care about # scheduling. $serialPlans.add(plan) end if $numChildProcesses > 1 and $runCommandOptions[:isSlow] $runlist.unshift plan else $runlist << plan end end def addRunCommand(kind, command, outputHandler, errorHandler, *additionalEnv) cfg = { :kind => kind, :command => command, :outputHandler => outputHandler, :errorHandler => errorHandler, :additionalEnv => additionalEnv, } addRunCommandCfg(cfg) end # Returns true if there were run commands found in the file ($benchmarkDirectory + # $benchmark), in which case those run commands have already been executed. Otherwise # returns false, in which case you're supposed to add your own run commands. def parseRunCommands oldDidAddRunCommand = $didAddRunCommand $didAddRunCommand = false $skipped = false Dir.chdir($outputDir) { File.open($benchmarkDirectory + $benchmark) { | inp | inp.each_line { | line | begin doesMatch = line =~ /^\/\/@/ rescue Exception => e # Apparently this happens in the case of some UTF8 stuff in some files, where # Ruby tries to be strict and throw exceptions. next end next unless doesMatch eval $~.post_match if $skipped break end } } } result = $didAddRunCommand $didAddRunCommand = result or oldDidAddRunCommand result end def slow! $runCommandOptions[:isSlow] = true skip() if ($mode == "quick") end def crashOK! $testSpecificRequiredOptions += ["-s"] $runCommandOptions[:crashOK] = true end def serial! $runCommandOptions[:serial] = true end # Retry parameters for tests that we treat as flaky, either because # they've been explicitly marked so (via flaky!) or because we were # asked to --treat-failing-as-flaky. class RetryParameters attr_reader :passPercentage, :maxTries def initialize(passPercentage, maxTries) @passPercentage = passPercentage @maxTries = maxTries end def to_s "RetryParameters(#{@passPercentage}, #{@maxTries})" end def result(statuses) if statuses.length > @maxTries # This could happen if (because of remotes disappearing # and reappearing) there are multiple runs for the last # try for a flaky test. Just ignore any extra statuses. statuses = statuses.take(@maxTries) end successes = successfulStatuses(statuses) remaining = @maxTries - statuses.length requiredSuccesses = (@passPercentage * @maxTries).ceil ret = nil # "has not completed yet" if successes >= requiredSuccesses ret = true elsif (successes + remaining) < requiredSuccesses ret = false end putd("#{self}.result(#{statuses}) => #{ret}") ret end private def successfulStatuses(statuses) statuses.count { |status| status == STATUS_FILE_PASS } end end def flaky!(passPercentage, maxTries) $runCommandOptions[:flaky] = RetryParameters.new(passPercentage, maxTries) end def requireOptions(*options) $testSpecificRequiredOptions += options end def runWithOptions(cfg, *options) baseOptions = BASE_OPTIONS if cfg.has_key?(:no_base_options) baseOptions = [] end commandPrefix = cfg.fetch(:command_prefix, []) if cfg.has_key?(:place_benchmark_early) cfg[:command] = commandPrefix + vmCommand + [$benchmark.to_s] + baseOptions + options + $testSpecificRequiredOptions else cfg[:command] = commandPrefix + vmCommand + baseOptions + options + $testSpecificRequiredOptions + [$benchmark.to_s] end addRunCommandCfg(cfg) end def runWithOutputHandler(kind, outputHandler, *options) cfg = { :kind => kind, :outputHandler => outputHandler, :errorHandler => simpleErrorHandler, } runWithOptions(cfg, *options) end def runWithOutputHandlerWithoutBaseOption(kind, outputHandler, *options) cfg = { :kind => kind, :outputHandler => outputHandler, :errorHandler => simpleErrorHandler, :no_base_options => true, } runWithOptions(cfg, *options) end def run(kind, *options) runWithOutputHandler(kind, silentOutputHandler, *options) end def runInner(cfg, *options) cfg = cfg.dup if not cfg.has_key?(:outputHandler) cfg[:outputHandler] = silentOutputHandler end if not cfg.has_key?(:errorHandler) cfg[:errorHandler] = simpleErrorHandler end runWithOptions(cfg, *options) end def runWithoutBaseOptionCfg(cfg, *options) cfg = cfg.dup cfg[:no_base_options] = true runWithOptions(cfg, *options) end def runWithoutBaseOption(kind, *options) runWithOutputHandlerWithoutBaseOption(kind, silentOutputHandler, *options) end def runOneLargeHeap(*optionalTestSpecificOptions) if $memoryLimited $didAddRunCommand = true puts "Skipping #{$collectionName}/#{$benchmark}" else run("default", *optionalTestSpecificOptions) end end def bytecodeCacheTemplate if ($hostOS == "darwin") return "bytecode-cache" elsif ($hostOS == "linux") return "bytecode-cacheXXXXXX" end nil end def runBytecodeCacheImpl(optionalTestSpecificOptions, *additionalEnv) fileTemplate = bytecodeCacheTemplate if fileTemplate.nil? return nil end { :cfg => { :command_prefix => [ "sh", (pathToHelpers + "bytecode-cache-test-helper.sh").to_s, fileTemplate.to_s, ], :place_benchmark_early => true, :additionalEnv => additionalEnv, }, :testSpecificOptions => FTL_OPTIONS + optionalTestSpecificOptions, } end def cfgInitializerPlain Proc.new { |cfg, kind| { :kind => kind} } end def cfgInitializerCfg Proc.new { |cfg, kind| cfg = cfg.dup cfg[:kind] = kind cfg } end # For each base mode (defined below) we generate two kinds of functions: # # - a version which takes a cfg argument and passes it along, only # setting the kind field # - a "plain" version which starts out with an empty cfg # # The plain version is intended for use in the testcase definitions # (in `//@` comments and the like). # # The former version is used for plumbing. The caller may set various # fields in the cfg which will be respected. # # This way, we can # - define a set of test modes in defaultRunCfg # - have defaultRunCfg propagate the cfg argument to the run*Cfg # functions it calls # - call defaultRunCfg from e.g. defaultRunNoisyTest with the output # handlers appropriately set, in order to make sure we're running # the exact same of tests. CfgKind = Struct.new(:extension, :expectCfg, :initializer) cfgKinds = [ CfgKind.new("", false, cfgInitializerPlain), CfgKind.new("Cfg", true, cfgInitializerCfg), ] # Define base test modes. Each mode is an array of [name, kind, # options]. The name is used to derive the ruby method names, the kind # is used for reporting (i.e. what you'd see in this script's # output). In the common case, options is a static array; if not, it's # a Proc that returns a dict that needs to be unpacked (see its use # site for a more detailed description). BASE_MODES = [ [ "NoCJIT", "ftl-no-cjit", [ "--validateBytecode=true", "--validateGraphAtEachPhase=true" ] + FTL_OPTIONS + NO_CJIT_OPTIONS + COLLECT_CONTINUOUSLY_OPTIONS ], [ "FTLNoCJIT", "misc-ftl-no-cjit", [ "--useDataICInFTL=true", ] + FTL_OPTIONS + NO_CJIT_OPTIONS ], [ "FTLNoCJITB3O0", "ftl-no-cjit-b3o0", [ "--useArrayAllocationProfiling=false", "--forcePolyProto=true", "--useRandomizingExecutableIslandAllocation=true", ] + FTL_OPTIONS + NO_CJIT_OPTIONS + B3O0_OPTIONS + FORCE_LLINT_EXIT_OPTIONS ], [ "FTLNoCJITValidate", "ftl-no-cjit-validate-sampling-profiler", [ "--validateGraph=true", "--validateBCE=true", "--useSamplingProfiler=true", "--airForceIRCAllocator=true", "--useDataICInFTL=true", "--forceUnlinkedDFG=true", ] + FTL_OPTIONS + NO_CJIT_OPTIONS ], [ "FTLNoCJITNoPutStackValidate", "ftl-no-cjit-no-put-stack-validate", [ "--validateGraph=true", "--usePutStackSinking=false", "--airForceIRCAllocator=true", ] + FTL_OPTIONS + NO_CJIT_OPTIONS ], [ "FTLNoCJITNoInlineValidate", "ftl-no-cjit-no-inline-validate", [ "--validateGraph=true", "--maximumInliningDepth=1", "--airForceBriggsAllocator=true", "--useB3HoistLoopInvariantValues=true", ] + FTL_OPTIONS + NO_CJIT_OPTIONS ], [ "FTLNoCJITOSRValidation", "ftl-no-cjit-osr-validation", [ "--validateFTLOSRExitLiveness=true", ] + FTL_OPTIONS + NO_CJIT_OPTIONS ], [ "DFGEager", "dfg-eager", EAGER_OPTIONS + COLLECT_CONTINUOUSLY_OPTIONS + FORCE_LLINT_EXIT_OPTIONS ], [ "DFGEagerNoCJITValidate", "dfg-eager-no-cjit-validate", [ "--validateGraph=true", ] + NO_CJIT_OPTIONS + EAGER_OPTIONS + COLLECT_CONTINUOUSLY_OPTIONS ], [ "FTLEager", "ftl-eager", [ "--airForceBriggsAllocator=true", "--useRandomizingExecutableIslandAllocation=true", "--forcePolyProto=true", "--useDataICInFTL=true", ] + FTL_OPTIONS + EAGER_OPTIONS + COLLECT_CONTINUOUSLY_OPTIONS ], [ "FTLEagerNoCJITValidate", "ftl-eager-no-cjit", [ "--validateGraph=true", "--validateBCE=true", "--airForceIRCAllocator=true", ] + FTL_OPTIONS + NO_CJIT_OPTIONS + EAGER_OPTIONS + COLLECT_CONTINUOUSLY_OPTIONS + FORCE_LLINT_EXIT_OPTIONS + EXECUTABLE_FUZZER_OPTIONS ], [ "FTLEagerNoCJITB3O1", "ftl-eager-no-cjit-b3o1", [ "--validateGraph=true", ] + FTL_OPTIONS + NO_CJIT_OPTIONS + EAGER_OPTIONS + B3O1_OPTIONS ], [ "FTLEagerNoCJITOSRValidation", "ftl-eager-no-cjit-osr-validation", [ "--validateFTLOSRExitLiveness=true", ] + FTL_OPTIONS + NO_CJIT_OPTIONS + EAGER_OPTIONS + COLLECT_CONTINUOUSLY_OPTIONS ], [ "NoCJITNoASO", "no-cjit-no-aso", [ "--useArchitectureSpecificOptimizations=false", ] + NO_CJIT_OPTIONS ], [ "NoCJITNoAccessInlining", "no-cjit-no-access-inlining", [ "--useAccessInlining=false", ] + NO_CJIT_OPTIONS ], [ "FTLNoCJITNoAccessInlining", "ftl-no-cjit-no-access-inlining", [ "--useAccessInlining=false", ] + FTL_OPTIONS + NO_CJIT_OPTIONS ], [ "FTLNoCJITSmallPool", "ftl-no-cjit-small-pool", [ "--jitMemoryReservationSize=202400", ] + FTL_OPTIONS + NO_CJIT_OPTIONS ], [ "NoCJIT", "no-cjit", NO_CJIT_OPTIONS ], [ "EagerJettisonNoCJIT", "eager-jettison-no-cjit", [ "--useRandomizingExecutableIslandAllocation=true", "--forceCodeBlockToJettisonDueToOldAge=true", "--verifyGC=true", ] + NO_CJIT_OPTIONS ], [ "ShadowChicken", "shadow-chicken", [ "--useDFGJIT=false", "--alwaysUseShadowChicken=true", ] ], [ "MiniMode", "mini-mode", [ "--forceMiniVMMode=true", ] ], [ "LogicalAssignmentOperatorsEnabled", "logical-assignment-operators-enabled", [ "--useLogicalAssignmentOperators=true", ] + FTL_OPTIONS ], [ "NoJIT", "no-jit", [ "--useJIT=false", ] ], [ # NOTE: Tests rely on this using scribbleFreeCells. "NoCJITValidate", "no-cjit", [ "--validateBytecode=true", "--validateGraph=true", ] + NO_CJIT_OPTIONS ], [ "NoCJITValidatePhases", "no-cjit-validate-phases", [ "--validateBytecode=true", "--validateGraphAtEachPhase=true", "--useSourceProviderCache=false", "--useRandomizingExecutableIslandAllocation=true", "--useLLIntICs=false", ] + NO_CJIT_OPTIONS ], [ "NoCJITCollectContinuously", "no-cjit-collect-continuously", NO_CJIT_OPTIONS + COLLECT_CONTINUOUSLY_OPTIONS ], [ "Default", "default", FTL_OPTIONS ], [ "NoFTL", "no-ftl", [] ], [ "WithRAMSize", nil, # Not used Proc.new { |size, *optionalTestSpecificOptions| { :cfg => { :kind => "ram-size-#{size}", }, :testSpecificOptions => [ "--forceRAMSize=#{size}", ] + optionalTestSpecificOptions } } ], [ "BytecodeCache", "bytecode-cache", Proc.new { |*optionalTestSpecificOptions| runBytecodeCacheImpl(optionalTestSpecificOptions) } ], [ "BytecodeCacheNoAssertion", "bytecode-cache", Proc.new { |*optionalTestSpecificOptions| runBytecodeCacheImpl(optionalTestSpecificOptions, "JSC_forceDiskCache=false") } ], [ "FTLEagerWatchdog", nil, Proc.new { |*optionalTestSpecificOptions| timeout = rand(100) { :cfg => { :kind => "ftl-eager-watchdog-#{timeout}", }, :testSpecificOptions => [ "--watchdog=#{timeout}", "--watchdog-exception-ok", ] + FTL_OPTIONS + EAGER_OPTIONS + COLLECT_CONTINUOUSLY_OPTIONS + optionalTestSpecificOptions } } ], [ "NoLLInt", "no-llint", Proc.new { |*optionalTestSpecificOptions| if $jitTests { :cfg => { }, :testSpecificOptions => [ "--useLLInt=false", ] + optionalTestSpecificOptions } else nil end } ], [ "OneLangeHeap", "default", Proc.new { |*optionalTestSpecificOptions| if $memoryLimited nil else { :testSpecificOptions => optionalTestSpecificOptions } end } ] ] BASE_MODES.each { |mode| name = "run#{mode[0]}" kind = mode[1] options = mode[2] # We need to define two variants, one expecting a cfg as the first # argument, one not. cfgKinds.each { |cfgKind| methodName = "#{name}#{cfgKind.extension}".to_sym define_method(methodName) { |*args| cfg = nil if cfgKind.expectCfg # If we're defining a method that expects a cfg # argument, pick it out of the args to pass to the # initializer. cfg = args.shift end # The cfg is initialized differently depending on whether # we're in a run*Cfg method or not. cfg = cfgKind.initializer.call(cfg, kind) finalOptions = nil if options.respond_to?(:call) dynamicOptions = options.call(*args) if dynamicOptions.nil? skip return end # The Proc object may override any cfg option passed # in. This is used e.g. for dynamic test names as used # by WithRAMSize and FTLEagerWatchdog. cfg.merge!(dynamicOptions[:cfg]) # As the Proc may consume arguments, it's responsible # for returning the final option list. Needed e.g. by # WithRAMSize. finalOptions = dynamicOptions[:testSpecificOptions] else finalOptions = options + args end runInner(cfg, *finalOptions) } } } CFG_NOISY = { :outputHandler => noisyOutputHandler, :errorHandler => noisyErrorHandler, }.freeze BASE_MODES.each { |mode| name = "runNoisyTest#{mode[0]}".to_sym define_method(name) { |*args| # For each base mode, define the "noisy" variant which simply # calls the respective run#{name}Cfg, passing in the "noisy" # cfg. send("run#{mode[0]}Cfg", CFG_NOISY, "--validateBytecode=true", "--validateGraphAtEachPhase=true", *args) } } # Default set of tests to run; propagates the cfg to every callee. def defaultRunCfg(cfg, subsetOptions, *optionalTestSpecificOptions) cfg.freeze if not subsetOptions.has_key?(:ignoreQuickMode) and $mode == "quick" defaultQuickRunCfg(cfg, *optionalTestSpecificOptions) else runDefaultCfg(cfg, *optionalTestSpecificOptions) runBytecodeCacheCfg(cfg, *optionalTestSpecificOptions) runMiniModeCfg(cfg, *optionalTestSpecificOptions) if $jitTests if not subsetOptions.has_key?(:skipNoLLInt) runNoLLIntCfg(cfg, *optionalTestSpecificOptions) end runNoCJITValidatePhasesCfg(cfg, *optionalTestSpecificOptions) runNoCJITCollectContinuouslyCfg(cfg, *optionalTestSpecificOptions) if shouldCollectContinuously? if not subsetOptions.has_key?(:skipEager) runDFGEagerCfg(cfg) if $mode != "basic" runDFGEagerNoCJITValidateCfg(cfg, *optionalTestSpecificOptions) runEagerJettisonNoCJITCfg(cfg, *optionalTestSpecificOptions) end end return if !$isFTLPlatform runNoFTLCfg(cfg, *optionalTestSpecificOptions) if not subsetOptions.has_key?(:skipEager) runFTLEagerCfg(cfg, *optionalTestSpecificOptions) runFTLEagerNoCJITValidateCfg(cfg, *optionalTestSpecificOptions) if $buildType == "release" end runFTLNoCJITSmallPoolCfg(cfg, *optionalTestSpecificOptions) return if $mode == "basic" runFTLNoCJITValidateCfg(cfg, *optionalTestSpecificOptions) runFTLNoCJITB3O0Cfg(cfg, *optionalTestSpecificOptions) runFTLNoCJITNoPutStackValidateCfg(cfg, *optionalTestSpecificOptions) runFTLNoCJITNoInlineValidateCfg(cfg, *optionalTestSpecificOptions) if not subsetOptions.has_key?(:skipEager) runFTLEagerNoCJITB3O1Cfg(cfg, *optionalTestSpecificOptions) end end end end def defaultRun defaultRunCfg({}, {}) end def defaultNoNoLLIntRun defaultRunCfg({}, {:skipNoLLInt => true}) end def defaultQuickRunCfg(cfg, *optionalTestSpecificOptions) runDefaultCfg(cfg, *optionalTestSpecificOptions) if $jitTests runNoCJITValidateCfg(cfg, *optionalTestSpecificOptions) return if !$isFTLPlatform runNoFTLCfg(cfg, *optionalTestSpecificOptions) runFTLNoCJITValidateCfg(cfg, *optionalTestSpecificOptions) end end def defaultQuickRun defaultQuickRunCfg({}) end def defaultSpotCheckNoMaximalFlush defaultQuickRun runNoCJITNoAccessInlining return if !$isFTLPlatform runFTLNoCJITOSRValidation runFTLNoCJITNoAccessInlining runFTLNoCJITB3O0 end def defaultSpotCheck defaultSpotCheckNoMaximalFlush runEagerJettisonNoCJIT end # This is expected to not do eager runs because eager runs can have a lot of recompilations # for reasons that don't arise in the real world. It's used for tests that assert convergence # by counting recompilations. def defaultNoEagerRun(*optionalTestSpecificOptions) defaultRunCfg({}, { :skipEager => true }, *optionalTestSpecificOptions) end def defaultNoSamplingProfilerRun defaultRunCfg({}, { :ignoreQuickMode => true }) end def runProfiler if $remote or $memoryLimited or ($hostOS == "windows") or ($hostOS == "playstation") skip return end profilerOutput = uniqueFilename(".json") if $canRunDisplayProfilerOutput addRunCommand("profiler", ["ruby", (pathToHelpers + "profiler-test-helper").to_s, (SCRIPTS_PATH + "display-profiler-output").to_s, profilerOutput.to_s, *vmCommand, "--useConcurrentJIT=false", "-p", profilerOutput.to_s, $benchmark.to_s], silentOutputHandler, simpleErrorHandler) else puts "Running simple version of #{$collectionName}/#{$benchmark} because some required Ruby features are unavailable." run("profiler-simple", "--useConcurrentJIT=false", "-p", profilerOutput.to_s) end end def runExceptionFuzz subCommand = escapeAll(vmCommand + ["--useDollarVM=true", "--useExceptionFuzz=true", $benchmark.to_s]) addRunCommand("exception-fuzz", ["perl", (pathToHelpers + "js-exception-fuzz").to_s, subCommand], silentOutputHandler, simpleErrorHandler) end def runExecutableAllocationFuzz(name, *options) subCommand = escapeAll(vmCommand + ["--useDollarVM=true", $benchmark.to_s] + options) addRunCommand("executable-allocation-fuzz-" + name, ["perl", (pathToHelpers + "js-executable-allocation-fuzz").to_s, subCommand], silentOutputHandler, simpleErrorHandler) end def runTypeProfiler if !$jitTests return end run("ftl-type-profiler", "--useTypeProfiler=true", *(FTL_OPTIONS)) run("ftl-no-cjit-type-profiler-force-poly-proto", "--useTypeProfiler=true", "--forcePolyProto=true", *(FTL_OPTIONS + NO_CJIT_OPTIONS)) return if !$isFTLPlatform run("ftl-type-profiler-ftl-eager", "--useTypeProfiler=true", *(FTL_OPTIONS + EAGER_OPTIONS)) end def runControlFlowProfiler if !$jitTests return end return if !$isFTLPlatform run("ftl-no-cjit-type-profiler", "--useControlFlowProfiler=true", *(FTL_OPTIONS + NO_CJIT_OPTIONS)) end def runTest262(mode, exception, includeFiles, flags) failsWithException = exception != "NoException" isStrict = false isModule = false isAsync = false flags.each { | flag | case flag when :strict isStrict = true when :module isModule = true when :async isAsync = true else raise "Invalid flag for runTest262, #{flag}" end } prepareExtraRelativeFiles(includeFiles.map { |f| "../" + f }, $collection) args = vmCommand + BASE_OPTIONS args << "--exception=" + exception if failsWithException args << "--test262-async" if isAsync args += $testSpecificRequiredOptions args += includeFiles case mode when :normal errorHandler = simpleErrorHandler outputHandler = silentOutputHandler when :fail errorHandler = expectedFailErrorHandler outputHandler = noisyOutputHandler when :failDueToOutdatedOrBadTest errorHandler = expectedFailErrorHandler outputHandler = noisyOutputHandler when :skip return else raise "Invalid mode: #{mode}" end if isStrict kind = "default-strict" args << "--strict-file=#{$benchmark}" else kind = "default" if isModule args << "--module-file=#{$benchmark}" else args << $benchmark.to_s end end addRunCommand(kind, args, outputHandler, errorHandler) end def prepareTest262Fixture # This function is used to add the files used by Test262 modules tests. prepareExtraRelativeFiles([""], $collection) end def runES6(mode) args = vmCommand + BASE_OPTIONS + $testSpecificRequiredOptions + [$benchmark.to_s] case mode when :normal errorHandler = simpleErrorHandler when :fail errorHandler = expectedFailErrorHandler when :failDueToOutdatedOrBadTest errorHandler = expectedFailErrorHandler when :skip return else raise "Invalid mode: #{mode}" end addRunCommand("default", args, noisyOutputHandler, errorHandler) end def defaultRunModules(noLLInt: true) run("default-modules", "-m") if !$jitTests return end run("no-llint-modules", "-m", "--useLLInt=false") if noLLInt run("no-cjit-validate-phases-modules", "-m", "--validateBytecode=true", "--validateGraphAtEachPhase=true", *NO_CJIT_OPTIONS) run("dfg-eager-modules", "-m", *EAGER_OPTIONS) run("dfg-eager-no-cjit-validate-modules", "-m", "--validateGraph=true", *(NO_CJIT_OPTIONS + EAGER_OPTIONS)) return if !$isFTLPlatform run("default-ftl-modules", "-m", *FTL_OPTIONS) run("ftl-no-cjit-validate-modules", "-m", "--validateGraph=true", *(FTL_OPTIONS + NO_CJIT_OPTIONS)) run("ftl-no-cjit-no-inline-validate-modules", "-m", "--validateGraph=true", "--maximumInliningDepth=1", *(FTL_OPTIONS + NO_CJIT_OPTIONS)) run("ftl-eager-modules", "-m", *(FTL_OPTIONS + EAGER_OPTIONS)) run("ftl-eager-no-cjit-modules", "-m", "--validateGraph=true", *(FTL_OPTIONS + NO_CJIT_OPTIONS + EAGER_OPTIONS)) run("ftl-no-cjit-small-pool-modules", "-m", "--jitMemoryReservationSize=202400", *(FTL_OPTIONS + NO_CJIT_OPTIONS)) end def noNoLLIntRunModules defaultRunModules(noLLInt: false) end def runWebAssembly return if !$jitTests return if !$isWasmPlatform run("default-wasm", "-m", *FTL_OPTIONS) if $mode != "quick" run("wasm-no-cjit-yes-tls-context", "-m", "--useFastTLSForWasmContext=true", *(FTL_OPTIONS + NO_CJIT_OPTIONS)) run("wasm-eager", "-m", "--useRandomizingExecutableIslandAllocation=true", *(FTL_OPTIONS + EAGER_OPTIONS)) run("wasm-eager-jettison", "-m", "--forceCodeBlockToJettisonDueToOldAge=true", "--useRandomizingExecutableIslandAllocation=true", "--verifyGC=true", *FTL_OPTIONS) run("wasm-no-tls-context", "-m", "--useFastTLSForWasmContext=false", *FTL_OPTIONS) run("wasm-slow-memory", "-m", "--useWebAssemblyFastMemory=false", *FTL_OPTIONS) run("wasm-collect-continuously", "-m", "--collectContinuously=true", "--verifyGC=true", *FTL_OPTIONS) if shouldCollectContinuously? if $isFTLPlatform run("wasm-b3", "-m", "--useWasmLLInt=false", "--wasmBBQUsesAir=false", *FTL_OPTIONS) run("wasm-air", "-m", "--useWasmLLInt=false", "--useRandomizingExecutableIslandAllocation=true", *FTL_OPTIONS) end end end def runWebAssemblyJetStream2 return if !$jitTests return if !$isWasmPlatform if $memoryLimited skip return end prepareExtraAbsoluteFiles(JETSTREAM2_PATH, ["JetStreamDriver.js"]) prepareExtraRelativeFilesWithBaseDirectory(Dir[JETSTREAM2_PATH + "wasm" + "*.js"].map { |f| "wasm/" + File.basename(f) }, $collection.dirname, $extraFilesBaseDir.dirname) prepareExtraRelativeFilesWithBaseDirectory(Dir[JETSTREAM2_PATH + "wasm" + "*.wasm"].map { |f| "wasm/" + File.basename(f) }, $collection.dirname, $extraFilesBaseDir.dirname) run("default-wasm", *FTL_OPTIONS) if $mode != "quick" run("wasm-no-cjit-yes-tls-context", "--useFastTLSForWasmContext=true", *(FTL_OPTIONS + NO_CJIT_OPTIONS)) run("wasm-eager", "--useRandomizingExecutableIslandAllocation=true", *(FTL_OPTIONS + EAGER_OPTIONS)) run("wasm-eager-jettison", "--forceCodeBlockToJettisonDueToOldAge=true", "--verifyGC=true", *FTL_OPTIONS) run("wasm-no-tls-context", "--useFastTLSForWasmContext=false", *FTL_OPTIONS) run("wasm-slow-memory", "--useWebAssemblyFastMemory=false", *FTL_OPTIONS) run("wasm-collect-continuously", "--collectContinuously=true", "--verifyGC=true", *FTL_OPTIONS) if shouldCollectContinuously? if $isFTLPlatform run("wasm-b3", "--useWasmLLInt=false", "--wasmBBQUsesAir=false", *FTL_OPTIONS) run("wasm-air", "--useWasmLLInt=false", "--useRandomizingExecutableIslandAllocation=true", *FTL_OPTIONS) end end end def runWebAssemblySuite(*optionalTestSpecificOptions) return if !$jitTests return if !$isWasmPlatform modules = Dir[WASMTESTS_PATH + "*.js"].map { |f| File.basename(f) } prepareExtraAbsoluteFiles(WASMTESTS_PATH, ["wasm.json"]) prepareExtraRelativeFiles(modules.map { |f| "../" + f }, $collection) if optionalTestSpecificOptions[0] == :no_module optionalTestSpecificOptions.shift else optionalTestSpecificOptions.unshift "-m" end run("default-wasm", *(FTL_OPTIONS + optionalTestSpecificOptions)) if $mode != "quick" run("wasm-no-cjit-yes-tls-context", "--useFastTLSForWasmContext=true", *(FTL_OPTIONS + NO_CJIT_OPTIONS + optionalTestSpecificOptions)) run("wasm-eager", *(FTL_OPTIONS + EAGER_OPTIONS + optionalTestSpecificOptions)) run("wasm-eager-jettison", "--forceCodeBlockToJettisonDueToOldAge=true", "--useRandomizingExecutableIslandAllocation=true", "--verifyGC=true", *(FTL_OPTIONS + optionalTestSpecificOptions)) run("wasm-no-tls-context", "--useFastTLSForWasmContext=false", *(FTL_OPTIONS + optionalTestSpecificOptions)) run("wasm-slow-memory", "--useWebAssemblyFastMemory=false", *(FTL_OPTIONS + optionalTestSpecificOptions)) run("wasm-collect-continuously", "--collectContinuously=true", "--verifyGC=true", *(FTL_OPTIONS + optionalTestSpecificOptions)) if shouldCollectContinuously? if $isFTLPlatform run("wasm-b3", "--useWasmLLInt=false", "--wasmBBQUsesAir=false", *(FTL_OPTIONS + optionalTestSpecificOptions)) run("wasm-air", "--useWasmLLInt=false", "--useRandomizingExecutableIslandAllocation=true", *(FTL_OPTIONS + optionalTestSpecificOptions)) end end end def runHarnessTest(kind, *options) wasmFiles = allWasmFiles($collection) wasmFiles.each { | file | basename = file.basename.to_s addRunCommand("(" + basename + ")-" + kind, vmCommand + options + $testSpecificRequiredOptions + [$benchmark.to_s, "--", basename], silentOutputHandler, simpleErrorHandler) } end def runWebAssemblyWithHarness(*optionalTestSpecificOptions) raise unless $benchmark.to_s =~ /harness\.m?js/ return if !$jitTests return if !$isWasmPlatform # Skip this completely on armv7 for now, since most of the tests are related to OSR and it's a bit hairy to skip them manually return if $architecture == "arm" wasmFiles = allWasmFiles($collection) prepareExtraRelativeFiles(wasmFiles.map { |f| f.basename }, $collection) runHarnessTest("default-wasm", *(FTL_OPTIONS + optionalTestSpecificOptions)) if $mode != "quick" runHarnessTest("wasm-no-cjit-yes-tls-context", "--useFastTLSForWasmContext=true", *(FTL_OPTIONS + NO_CJIT_OPTIONS + optionalTestSpecificOptions)) runHarnessTest("wasm-eager", *(FTL_OPTIONS + EAGER_OPTIONS + optionalTestSpecificOptions)) runHarnessTest("wasm-eager-jettison", "--forceCodeBlockToJettisonDueToOldAge=true", "--useRandomizingExecutableIslandAllocation=true", "--verifyGC=true", *(FTL_OPTIONS + optionalTestSpecificOptions)) runHarnessTest("wasm-no-tls-context", "--useFastTLSForWasmContext=false", *(FTL_OPTIONS + optionalTestSpecificOptions)) runHarnessTest("wasm-slow-memory", "--useWebAssemblyFastMemory=false", *(FTL_OPTIONS + optionalTestSpecificOptions)) runHarnessTest("wasm-collect-continuously", "--collectContinuously=true", "--verifyGC=true", *(FTL_OPTIONS + optionalTestSpecificOptions)) if shouldCollectContinuously? if $isFTLPlatform runHarnessTest("wasm-b3", "--useWasmLLInt=false", "--wasmBBQUsesAir=false", *(FTL_OPTIONS + optionalTestSpecificOptions)) runHarnessTest("wasm-no-air", "--useWasmLLInt=false", "--useRandomizingExecutableIslandAllocation=true", *(FTL_OPTIONS + optionalTestSpecificOptions)) end end end def runWebAssemblyEmscripten(mode) case mode when :skip return end return if !$jitTests return if !$isWasmPlatform wasm = $benchmark.to_s.sub! '.js', '.wasm' prepareExtraRelativeFiles([Pathname('..') + wasm], $collection) run("default-wasm", *FTL_OPTIONS) if $mode != "quick" run("wasm-no-cjit-yes-tls-context", "--useFastTLSForWasmContext=true", *(FTL_OPTIONS + NO_CJIT_OPTIONS)) run("wasm-eager-jettison", "--forceCodeBlockToJettisonDueToOldAge=true", "--useRandomizingExecutableIslandAllocation=true", "--verifyGC=true", *FTL_OPTIONS) run("wasm-no-tls-context", "--useFastTLSForWasmContext=false", *FTL_OPTIONS) run("wasm-collect-continuously", "--collectContinuously=true", "--verifyGC=true", *FTL_OPTIONS) if shouldCollectContinuously? if $isFTLPlatform run("wasm-b3", "--useWasmLLInt=false", "--wasmBBQUsesAir=false", *FTL_OPTIONS) run("wasm-air", "--useWasmLLInt=false", "--useRandomizingExecutableIslandAllocation=true", *FTL_OPTIONS) end end end def runWebAssemblySpecTestBase(mode, specHarnessPath, *optionalTestSpecificOptions) case mode when :skip return end return if !$jitTests return if !$isWasmPlatform prepareExtraAbsoluteFiles(WASMTESTS_PATH, ["wasm.json"]) modules = Dir[WASMTESTS_PATH + "*.js"].map { |f| File.basename(f) } prepareExtraRelativeFiles(modules.map { |f| "../../" + f }, $collection) harness = Dir[WASMTESTS_PATH + (specHarnessPath + "/") + "*.js"].map { |f| File.basename(f) } prepareExtraRelativeFiles(harness.map { |f| ("../../" + specHarnessPath + "/") + f }, $collection) specHarnessJsPath = "../" + specHarnessPath + ".js" runWithOutputHandler("default-wasm", noisyOutputHandler, specHarnessJsPath, *(FTL_OPTIONS + optionalTestSpecificOptions)) if $mode != "quick" runWithOutputHandler("wasm-no-cjit-yes-tls-context", noisyOutputHandler, specHarnessJsPath, "--useFastTLSForWasmContext=true", *(FTL_OPTIONS + NO_CJIT_OPTIONS + optionalTestSpecificOptions)) runWithOutputHandler("wasm-eager-jettison", noisyOutputHandler, specHarnessJsPath, "--forceCodeBlockToJettisonDueToOldAge=true", "--useRandomizingExecutableIslandAllocation=true", "--verifyGC=true", *(FTL_OPTIONS + optionalTestSpecificOptions)) runWithOutputHandler("wasm-no-tls-context", noisyOutputHandler, specHarnessJsPath, "--useFastTLSForWasmContext=false", *(FTL_OPTIONS + optionalTestSpecificOptions)) runWithOutputHandler("wasm-collect-continuously", noisyOutputHandler, specHarnessJsPath, "--collectContinuously=true", "--verifyGC=true", *(FTL_OPTIONS + optionalTestSpecificOptions)) if shouldCollectContinuously? if $isFTLPlatform runWithOutputHandler("wasm-b3", noisyOutputHandler, specHarnessJsPath, "--useWasmLLInt=false", "--wasmBBQUsesAir=false", *(FTL_OPTIONS + optionalTestSpecificOptions)) runWithOutputHandler("wasm-air", noisyOutputHandler, specHarnessJsPath, "--useWasmLLInt=false", "--useRandomizingExecutableIslandAllocation=true", *(FTL_OPTIONS + optionalTestSpecificOptions)) end end end def runWebAssemblySpecTest(mode) runWebAssemblySpecTestBase(mode, "spec-harness") end def runWebAssemblyReferenceSpecTest(mode) runWebAssemblySpecTestBase(mode, "ref-spec-harness") end def runWebAssemblyFunctionReferenceSpecTest(mode) runWebAssemblySpecTestBase(mode, "spec-harness", "--useWebAssemblyTypedFunctionReferences=true") end def runWebAssemblyLowExecutableMemory(*optionalTestSpecificOptions) return if !$jitTests return if !$isWasmPlatform modules = Dir[WASMTESTS_PATH + "*.js"].map { |f| File.basename(f) } prepareExtraAbsoluteFiles(WASMTESTS_PATH, ["wasm.json"]) prepareExtraRelativeFiles(modules.map { |f| "../" + f }, $collection) # Only let WebAssembly get executable memory. run("default-wasm", "--useConcurrentGC=0" , "--useConcurrentJIT=0", "--jitMemoryReservationSize=20000", "--useBaselineJIT=0", "--useDFGJIT=0", "--useFTLJIT=0", "-m") end def runChakra(mode, exception, baselineFile, extraFiles) raise unless $benchmark.to_s =~ /\.js$/ failsWithException = exception != "NoException" testName = $~.pre_match prepareExtraAbsoluteFiles(CHAKRATESTS_PATH, ["jsc-lib.js"]) prepareExtraRelativeFiles(extraFiles.map { |f| "../" + f }, $collection) args = vmCommand + BASE_OPTIONS args += FTL_OPTIONS if $isFTLPlatform args += EAGER_OPTIONS args << "--exception=" + exception if failsWithException args << "--dumpException" if failsWithException args += $testSpecificRequiredOptions args += ["jsc-lib.js"] case mode when :baseline prepareExtraRelativeFiles([(Pathname("..") + baselineFile).to_s], $collection) errorHandler = diffErrorHandler(($benchmarkDirectory + baselineFile).to_s) outputHandler = noisyOutputHandler when :pass errorHandler = chakraPassFailErrorHandler outputHandler = noisyOutputHandler when :skipDueToOutdatedOrBadTest return when :skip return else raise "Invalid mode: #{mode}" end kind = "default" args << $benchmark.to_s addRunCommand(kind, args, outputHandler, errorHandler) end def runLayoutTest(kind, *options) raise unless $benchmark.to_s =~ /\.js$/ testName = $~.pre_match if kind kind = "layout-" + kind else kind = "layout" end prepareExtraRelativeFiles(["../#{testName}-expected.txt"], $benchmarkDirectory) prepareExtraAbsoluteFiles(LAYOUTTESTS_PATH, ["resources/standalone-pre.js", "resources/standalone-post.js"]) args = vmCommand + BASE_OPTIONS + options + $testSpecificRequiredOptions + [(Pathname.new("resources") + "standalone-pre.js").to_s, $benchmark.to_s, (Pathname.new("resources") + "standalone-post.js").to_s] addRunCommand(kind, args, noisyOutputHandler, diffErrorHandler(($benchmarkDirectory + "../#{testName}-expected.txt").to_s)) end def runLayoutTestNoFTL runLayoutTest("no-ftl") end def runLayoutTestNoLLInt runLayoutTest("no-llint", "--useLLInt=false") end def runLayoutTestNoCJIT runLayoutTest("no-cjit", *NO_CJIT_OPTIONS) end def runLayoutTestDFGEagerNoCJIT runLayoutTest("dfg-eager-no-cjit", *(NO_CJIT_OPTIONS + EAGER_OPTIONS)) end def runLayoutTestDefault runLayoutTest(nil, "--testTheFTL=true", *FTL_OPTIONS) end def runLayoutTestFTLNoCJIT runLayoutTest("ftl-no-cjit", "--testTheFTL=true", *(FTL_OPTIONS + NO_CJIT_OPTIONS)) end def runLayoutTestFTLEagerNoCJIT runLayoutTest("ftl-eager-no-cjit", "--testTheFTL=true", *(FTL_OPTIONS + NO_CJIT_OPTIONS + EAGER_OPTIONS)) end def runLayoutTestFTLEagerNoCJITB3O1 runLayoutTest("ftl-eager-no-cjit-b3o1", "--testTheFTL=true", *(FTL_OPTIONS + NO_CJIT_OPTIONS + EAGER_OPTIONS + B3O1_OPTIONS)) end def noFTLRunLayoutTest if !$jitTests return end runLayoutTestNoLLInt runLayoutTestNoCJIT runLayoutTestDFGEagerNoCJIT end def defaultQuickRunLayoutTest runLayoutTestDefault if $jitTests if $isFTLPlatform runLayoutTestNoFTL runLayoutTestFTLNoCJIT runLayoutTestFTLEagerNoCJIT else noFTLRunLayoutTest end end end def defaultRunLayoutTest if $mode == "quick" defaultQuickRunLayoutTest else runLayoutTestDefault if $jitTests noFTLRunLayoutTest return if !$isFTLPlatform runLayoutTestNoFTL runLayoutTestFTLNoCJIT runLayoutTestFTLEagerNoCJIT end end end def noEagerNoNoLLIntTestsRunLayoutTest runLayoutTestDefault if $jitTests runLayoutTestNoCJIT return if !$isFTLPlatform runLayoutTestNoFTL runLayoutTestFTLNoCJIT end end def noNoLLIntRunLayoutTest runLayoutTestDefault if $jitTests runLayoutTestNoCJIT runLayoutTestDFGEagerNoCJIT return if !$isFTLPlatform runLayoutTestNoFTL runLayoutTestFTLNoCJIT runLayoutTestFTLEagerNoCJIT end end def prepareExtraRelativeFilesWithBaseDirectory(extraFiles, destination, baseDirectory) Dir.chdir($outputDir) { extraFiles.each { | file | dest = destination + file FileUtils.mkdir_p(dest.dirname) FileUtils.cp baseDirectory + file, dest } } end def prepareExtraRelativeFiles(extraFiles, destination) prepareExtraRelativeFilesWithBaseDirectory(extraFiles, destination, $extraFilesBaseDir) end def baseDirForCollection(collectionName) Pathname(".tests") + collectionName end def prepareExtraAbsoluteFiles(absoluteBase, extraFiles) raise unless absoluteBase.absolute? Dir.chdir($outputDir) { collectionBaseDir = baseDirForCollection($collectionName) extraFiles.each { | file | destination = collectionBaseDir + file FileUtils.mkdir_p destination.dirname unless destination.directory? FileUtils.cp absoluteBase + file, destination } } end def runComplexTest(before, after, additionalEnv, *options) prepareExtraRelativeFiles(before.map{|v| (Pathname("..") + v).to_s}, $collection) prepareExtraRelativeFiles(after.map{|v| (Pathname("..") + v).to_s}, $collection) args = vmCommand + BASE_OPTIONS + options + $testSpecificRequiredOptions + before.map{|v| v.to_s} + [$benchmark.to_s] + after.map{|v| v.to_s} addRunCommand("complex", args, noisyOutputHandler, simpleErrorHandler, *additionalEnv) end def runMozillaTest(kind, mode, extraFiles, *options) if kind kind = "mozilla-" + kind else kind = "mozilla" end prepareExtraRelativeFiles(extraFiles.map{|v| (Pathname("..") + v).to_s}, $collection) args = vmCommand + BASE_OPTIONS + options + $testSpecificRequiredOptions + extraFiles.map{|v| v.to_s} + [$benchmark.to_s] case mode when :normal errorHandler = mozillaErrorHandler when :negative errorHandler = mozillaExit3ErrorHandler when :fail errorHandler = mozillaFailErrorHandler when :failDueToOutdatedOrBadTest errorHandler = mozillaFailErrorHandler when :skip return else raise "Invalid mode: #{mode}" end addRunCommand(kind, args, noisyOutputHandler, errorHandler) end def runMozillaTestDefault(mode, *extraFiles) runMozillaTest(nil, mode, extraFiles, *FTL_OPTIONS) end def runMozillaTestNoFTL(mode, *extraFiles) runMozillaTest("no-ftl", mode, extraFiles) end def runMozillaTestLLInt(mode, *extraFiles) runMozillaTest("llint", mode, extraFiles, "--useJIT=false") end def runMozillaTestBaselineJIT(mode, *extraFiles) runMozillaTest("baseline", mode, extraFiles, "--useLLInt=false", "--useDFGJIT=false") end def runMozillaTestDFGEagerNoCJITValidatePhases(mode, *extraFiles) runMozillaTest("dfg-eager-no-cjit-validate-phases", mode, extraFiles, "--validateBytecode=true", "--validateGraphAtEachPhase=true", *(NO_CJIT_OPTIONS + EAGER_OPTIONS)) end def runMozillaTestFTLEagerNoCJITValidatePhases(mode, *extraFiles) runMozillaTest("ftl-eager-no-cjit-validate-phases", mode, extraFiles, "--validateBytecode=true", "--validateGraphAtEachPhase=true", *(FTL_OPTIONS + NO_CJIT_OPTIONS + EAGER_OPTIONS)) end def defaultQuickRunMozillaTest(mode, *extraFiles) if $jitTests runMozillaTestDefault(mode, *extraFiles) runMozillaTestFTLEagerNoCJITValidatePhases(mode, *extraFiles) else runMozillaTestNoFTL(mode, *extraFiles) if $jitTests runMozillaTestDFGEagerNoCJITValidatePhases(mode, *extraFiles) end end end def defaultRunMozillaTest(mode, *extraFiles) if $mode == "quick" defaultQuickRunMozillaTest(mode, *extraFiles) else runMozillaTestNoFTL(mode, *extraFiles) if $jitTests runMozillaTestLLInt(mode, *extraFiles) runMozillaTestBaselineJIT(mode, *extraFiles) runMozillaTestDFGEagerNoCJITValidatePhases(mode, *extraFiles) runMozillaTestDefault(mode, *extraFiles) runMozillaTestFTLEagerNoCJITValidatePhases(mode, *extraFiles) if $isFTLPlatform end end end def runNoisyTestWithEnv(kind, *additionalEnv) cfg = CFG_NOISY.dup cfg[:kind] = kind cfg[:additionalEnv] = additionalEnv runDefaultCfg(cfg) end def defaultRunNoisyTest cfg = { :outputHandler => noisyOutputHandler, :errorHandler => noisyErrorHandler, } defaultRunCfg(cfg, {}) end def skip $didAddRunCommand = true $skipped = true puts "Skipping #{$collectionName}/#{$benchmark}" end def allWasmFiles(path) if path.file? [path] else result = [] Dir.foreach(path) { | filename | next unless filename =~ /\.m?wasm$/ next unless (path + filename).file? result << path + filename } result end end def allJSFiles(path) if path.file? [path] else result = [] Dir.foreach(path) { | filename | next unless filename =~ /\.m?js$/ next unless (path + filename).file? result << path + filename } result end end def uniqueifyName(names, name) result = name.to_s toAdd = 1 while names[result] result = "#{name}-#{toAdd}" toAdd += 1 end names[result] = true result end def simplifyCollectionName(collectionPath) outerDir = collectionPath.dirname name = collectionPath.basename lastName = name if collectionPath.directory? while lastName.to_s =~ /test/ lastName = outerDir.basename name = lastName + name outerDir = outerDir.dirname end end uniqueifyName($collectionNames, name) end def prepareCollection(name) FileUtils.mkdir_p $outputDir + name absoluteCollection = $collection.realpath Dir.chdir($outputDir) { bundleDir = baseDirForCollection(name) # Create the proper directory structures. FileUtils.mkdir_p bundleDir if bundleDir.basename == $collection.basename FileUtils.cp_r absoluteCollection, bundleDir.dirname $collection = bundleDir else FileUtils.cp_r absoluteCollection, bundleDir $collection = bundleDir + $collection.basename end $extraFilesBaseDir = absoluteCollection } end $collectionNames = {} def handleCollectionFile(collection) collectionName = simplifyCollectionName(collection) paths = {} subCollections = [] YAML::load(IO::read(collection)).each { | entry | if entry["collection"] subCollections << entry["collection"] next end if Pathname.new(entry["path"]).absolute? raise "Absolute path: " + entry["path"] + " in #{collection}" end if paths[entry["path"]] raise "Duplicate path: " + entry["path"] + " in #{collection}" end subCollection = collection.dirname + entry["path"] if subCollection.file? subCollectionName = Pathname.new(entry["path"]).dirname else subCollectionName = entry["path"] end $collection = subCollection $collectionName = Pathname.new(collectionName) Pathname.new(subCollectionName).each_filename { | filename | next if filename =~ /^\./ $collectionName += filename } $collectionName = $collectionName.to_s prepareCollection($collectionName) Dir.chdir($outputDir) { pathsToSearch = [$collection] if entry["tests"] if entry["tests"].is_a? Array pathsToSearch = entry["tests"].map { | testName | pathsToSearch[0] + testName } else pathsToSearch[0] += entry["tests"] end end pathsToSearch.each { | pathToSearch | allJSFiles(pathToSearch).each { | path | $benchmark = path.basename $benchmarkDirectory = path.dirname $runCommandOptions = {} $testSpecificRequiredOptions = [] eval entry["cmd"] } } } } subCollections.each { | subCollection | handleCollection(collection.dirname + subCollection) } end def handleCollectionDirectory(collection) collectionName = simplifyCollectionName(collection) $collection = collection $collectionName = collectionName prepareCollection(collectionName) Dir.chdir($outputDir) { $benchmarkDirectory = $collection allJSFiles($collection).each { | path | $benchmark = path.basename $runCommandOptions = {} $testSpecificRequiredOptions = [] defaultRun unless parseRunCommands } } end def handleCollection(collection) collection = Pathname.new(collection) if collection.file? handleCollectionFile(collection) else handleCollectionDirectory(collection) end end def prepareBundle raise if $bundle if $doNotMessWithVMPath if !$remote and !$tarball $testingFrameworkPath = (frameworkFromJSCPath($jscPath) || $jscPath.dirname).realpath $jscPath = Pathname.new($jscPath).realpath else $testingFrameworkPath = frameworkFromJSCPath($jscPath) end else originalJSCPath = $jscPath vmDir = $outputDir + ".vm" FileUtils.mkdir_p vmDir frameworkPath = frameworkFromJSCPath($jscPath) destinationFrameworkPath = Pathname.new(".vm") + "JavaScriptCore.framework" $jscPath = destinationFrameworkPath + "Helpers" + "jsc" $testingFrameworkPath = Pathname.new("..") + destinationFrameworkPath if frameworkPath source = frameworkPath destination = Pathname.new(".vm") elsif $hostOS == "windows" # Make sure to copy dll along with jsc on Windows originalJSCDir = File.dirname(originalJSCPath) source = [originalJSCPath] + [originalJSCDir + "/jscLib.dll"] # Check for and copy JavaScriptCore.dll and WTF.dll for dynamic builds javaScriptCoreDLLPath = File.join(originalJSCDir, "JavaScriptCore.dll") wtfDLLPath = File.join(originalJSCDir, "WTF.dll") if (File.exists?(javaScriptCoreDLLPath)) source = source + [javaScriptCoreDLLPath] end if (File.exists?(wtfDLLPath)) source = source + [wtfDLLPath] end destination = $jscPath.dirname Dir.chdir($outputDir) { FileUtils.mkdir_p destination } else source = originalJSCPath destination = $jscPath Dir.chdir($outputDir) { FileUtils.mkdir_p $jscPath.dirname } end Dir.chdir($outputDir) { if $copyVM FileUtils.cp_r source, destination else begin FileUtils.ln_s source, destination rescue Exception $stderr.puts "Warning: unable to create soft link, trying to copy." FileUtils.cp_r source, destination end end if $remote and $hostOS == "linux" bundle_binary = (Pathname.new(THIS_SCRIPT_PATH).dirname + 'bundle-binary').realpath Dir.mktmpdir { | tmpdir | # Generate bundle in a temporary directory so that # we can safely pick it up regardless of its name # (it's the only zip file there). cmdline = [ bundle_binary.to_s, "--dest-dir=#{$jscPath.dirname}", "--log-level=debug", $jscPath.to_s ] if not $ldd.nil? cmdline << "--ldd=#{$ldd}" end mysys(cmdline) } end } end Dir.chdir($outputDir) { FileUtils.cp_r HELPERS_PATH, ".helpers" } ARGV.each { | collection | handleCollection(collection) } if $runlist.size == 0 $stderr.puts("No tests selected for execution (overzealous --filter?)") exit(2) end puts end def cleanOldResults raise unless $bundle eachResultFile($outputDir) { | path | FileUtils.rm_f path } end def cleanEmptyResultFiles eachResultFile($outputDir) { | path | next unless path.basename.to_s =~ /\.out$/ next unless FileTest.size(path) == 0 FileUtils.rm_f path } end def eachResultFile(startingDir, &block) dirsToClean = [startingDir] until dirsToClean.empty? nextDir = dirsToClean.pop Dir.foreach(nextDir) { | entry | next if entry =~ /^\./ path = nextDir + entry if path.directory? dirsToClean.push(path) else block.call(path) end } end end def parallelEach(array, &block) # Short of using 'require "parallel"' we use a simple statically # partitioned multiprocess dispatch for processing fixed chunks of the # given array in parallel. We use Process rather than Thread to # parallelise CPU load as well as IO (due to the GIL). # Some platforms (notably Windows) do not support Process.fork, so work # serially on these. nWorkers = Process.respond_to?(:fork) ? $numChildProcesses : 1 # Chunk size is rounded up chunkSize = (array.size + (nWorkers - 1)) / nWorkers # If chunk size is too small, work serially if chunkSize <= 1 nWorkers = 1 chunkSize = array.size end childPIDs = [] # Chunks 1 to nWorkers-1 run in the worker processes for i in 1...nWorkers do chunkStart = i*chunkSize break if chunkStart >= array.size pid = Process.fork if pid.nil? # Worker process. Process chunk i. array.slice(chunkStart, chunkSize).each(&block) Process.exit!(true) else childPIDs << pid end end # Main process. Process chunk 0. array.slice(0, chunkSize).each(&block) # Wait for workers for pid in childPIDs do _, status = Process.waitpid2(pid) raise "Child process still running" unless status.exited? if status.exitstatus != 0 STDERR.puts "Child process failed with status: #{status.exitstatus}" exit(status.exitstatus) end end end def cleanRunnerDirectory raise unless $bundle Dir.foreach($runnerDir) { | filename | next unless filename =~ /^#{STATUS_FILE_PREFIX}/ FileUtils.rm_f $runnerDir + filename } end def sshRead(cmd, remoteHost, options={}) raise unless $remote result = "" ssh_cmd = ["ssh"] + SSH_OPTIONS_DEFAULT + ["-p", remoteHost.port.to_s] + (remoteHost.identity_file_path ? ["-i", remoteHost.identity_file_path] : []) + ["#{remoteHost.user}@#{remoteHost.host}", cmd] IO.popen(ssh_cmd, "r") { |inp| result = inp.read } raise "#{$?}" unless $?.success? or options[:ignoreFailure] result end def runCommandOnTester(cmd) if $remote $remoteHosts.each { |remoteHost| begin # Return first successful value. Obviously, this # assumes the remotes are homogeneous. return sshRead(cmd, remoteHost) rescue Exception => e $stderr.puts("Error running `#{cmd}` on #{remoteHost.host}: #{e}") end } return "0" end `#{cmd}` end def commandToGetNumberOfProcessors if $hostOS == "windows" "cmd /c echo %NUMBER_OF_PROCESSORS%" else "sysctl -n hw.activecpu 2>/dev/null || nproc --all 2>/dev/null || getconf _NPROCESSORS_ONLN" end end def numberOfProcessors begin numProcessors = runCommandOnTester(commandToGetNumberOfProcessors).to_i rescue numProcessors = 0 end if numProcessors == 0 $stderr.puts("Warning: could not determine the number of remote CPUs, defaulting to 1") numProcessors = 1 end return numProcessors end def runAndMonitorCommandOutput(cmd, &blk) cmd = cmd.collect { |a| a.to_s } IO.popen(cmd, "r") { | p | p.each_line { | line | blk.call(p.pid, line) puts(line) $stdout.flush } } end $runnerDirMutex = Mutex.new def runAndMonitorTestRunnerCommand(cmd, options={}) numberOfTests = 0 $runnerDirMutex.synchronize { Dir.chdir($runnerDir) { # -1 for the runscript, and -2 for '..' and '.' numberOfTests = Dir.entries(".").count - 3 } } unless $progressMeter mysys(cmd.join(' '), options) else running = {} didRun = {} didFail = {} blankLine = true prevStringLength = 0 IO.popen(cmd.join(' '), mode="r") { | inp | inp.each_line { | line | line = line.scrub.chomp if line =~ /^Running / running[$~.post_match] = true elsif line =~ /^PASS: / didRun[$~.post_match] = true elsif line =~ /^FAIL: / didRun[$~.post_match] = true didFail[$~.post_match] = true else unless blankLine print("\r" + " " * prevStringLength + "\r") end puts line blankLine = true end def lpad(str, chars) str = str.to_s if str.length > chars str else "%#{chars}s"%(str) end end string = "" string += "\r#{lpad(didRun.size, numberOfTests.to_s.size)}/#{numberOfTests}" unless didFail.empty? string += " (failed #{didFail.size})" end string += " " (running.size - didRun.size).times { string += "." } if string.length < prevStringLength print string print(" " * (prevStringLength - string.length)) end print string prevStringLength = string.length blankLine = false $stdout.flush } } puts if not $?.success? and not options[:ignoreFailure] raise "Failed to run #{cmd}: #{$?.inspect}" end end end def getRemoteDirectoryIfNeeded(remoteHost) if !remoteHost.remoteDirectory remoteHost.remoteDirectory = JSON::parse(sshRead("cat ~/.bencher", remoteHost))["tempPath"] end end def copyBundleToRemote(remoteHost) mysys(["ssh"] + SSH_OPTIONS_DEFAULT + (remoteHost.identity_file_path ? ["-i", remoteHost.identity_file_path] : []) + ["-p", remoteHost.port.to_s, "#{remoteHost.user}@#{remoteHost.host}", "mkdir -p #{remoteHost.remoteDirectory}"]) mysys(["scp"] + SSH_OPTIONS_DEFAULT + (remoteHost.identity_file_path ? ["-i", remoteHost.identity_file_path] : []) + ["-P", remoteHost.port.to_s, ($outputDir.dirname + $tarFileName).to_s, "#{remoteHost.user}@#{remoteHost.host}:#{remoteHost.remoteDirectory}"]) end def exportBaseEnvironmentVariables(escape) if escape dyldFrameworkPath = "\\$(cd #{$testingFrameworkPath.dirname}; pwd)" ldLibraryPath = "\\$(cd #{$testingFrameworkPath.dirname}/..; pwd)/#{$jscPath.dirname}" else dyldFrameworkPath = "\$(cd #{$testingFrameworkPath.dirname}; pwd)" ldLibraryPath = "\$(cd #{$testingFrameworkPath.dirname}/..; pwd)/#{$jscPath.dirname}" end [ "export DYLD_FRAMEWORK_PATH=#{dyldFrameworkPath} && ", "export LD_LIBRARY_PATH=#{ldLibraryPath} &&", "export JSCTEST_timeout=#{Shellwords.shellescape(ENV['JSCTEST_timeout'])} && ", "export JSCTEST_hardTimeout=#{Shellwords.shellescape(ENV['JSCTEST_hardTimeout'])} && ", "export JSCTEST_memoryLimit=#{Shellwords.shellescape(ENV['JSCTEST_memoryLimit'])} && ", "export TZ=#{Shellwords.shellescape(ENV['TZ'])} && ", ].join("") end def runTestRunner(testRunner, remoteHosts, remoteIndex=0) if not remoteHosts.nil? remoteHost = remoteHosts[remoteIndex] getRemoteDirectoryIfNeeded(remoteHost) remoteScript = "\"" remoteScript += "cd #{remoteHost.remoteDirectory}/#{$outputDir.basename}/.runner && " remoteScript += exportBaseEnvironmentVariables(true) $envVars.each { |var| remoteScript += "export " << var << "\n" } remoteScript += "#{testRunner.command(remoteIndex)}\"" runAndMonitorTestRunnerCommand(["ssh"] + SSH_OPTIONS_DEFAULT + (remoteHost.identity_file_path ? ["-i", remoteHost.identity_file_path] : []) + ["-p", remoteHost.port.to_s, "#{remoteHost.user}@#{remoteHost.host}", remoteScript]) else Dir.chdir($runnerDir) { runAndMonitorTestRunnerCommand(Shellwords.shellsplit(testRunner.command)) } end end STATUS_RE = /^[.]\/#{STATUS_FILE_PREFIX}(?\d+)\s(?\h+)\s(?\d+)\s(?#{STATUS_FILE_PASS}|#{STATUS_FILE_FAIL})$/ def processStatusLine(map, line) md = STATUS_RE.match(line) if md.nil? # Malfromed lines can legitimately occur when the remote fails at an # inopportune moment. Ignore those lines but print them out, so that we # can easily diagnose actual issues with the remote output. $stderr.puts("Ignoring malformed status line `#{line}`") return end index = md[:index].to_i runId = md[:runId] result = md[:result] if runId != $runUniqueId if $bundle # The scripts in the bundle have their own runId embedded, # just ignore the value. else # This may conceivably happen if a remote goes # away in the middle of a run and comes back # online in the middle of a different run. $stderr.puts("Ignoring stale status file for #{index} (ID #{runId} but current ID is #{$runUniqueId})") return end end if map.has_key?(index) map[index].push(result) else map[index] = [result] end end def getStatusMap(map={}) find_cmd = "find . -maxdepth 1 -name \"#{STATUS_FILE_PREFIX}*\" -a -size +0c -exec sh -c \"printf \\\"%s \\\" {}; cat {}\" \\;" if $remote # Note: here we're using $remoteHosts (instead of getting the # list of live remoteHosts from the caller, because there may # well be test results on a remoteHost that got rebooted # (note, the test results are tagged with a run ID, so we'll # ignore any stale results from a previous run). forEachRemote($remoteHosts, :dropOnFailure => true, :timeout => 8 * REMOTE_TIMEOUT) { |_, host| runnerDir = "#{host.remoteDirectory}/#{$outputDir.basename}/.runner" output = sshRead("if test -d #{runnerDir}; then cd #{runnerDir}; else false; fi && " + find_cmd, host, :ignoreFailure => true) output.split(/\n/).each { | line | processStatusLine(map, line) } } else Dir.chdir($runnerDir) { Dir.glob("#{STATUS_FILE_PREFIX}*").each do |name| if File.size(name) > 0 File.open(name, 'r') { |f| line = f.first processStatusLine(map, "./#{name} #{line}") } end end } end map end def detectFailures(statusMap={}) raise if $bundle if statusMap.size == 0 statusMap = getStatusMap end evaluator = TestResultEvaluatorFinal.new($runlist, statusMap) evaluator.visit!({:treatIncompleteAsFailed => true}) evaluator.validate if evaluator.noresult > 0 $stderr.puts("Could not get the exit status for #{evaluator.noresult} tests") # We can't change our exit code, as run-javascriptcore-tests # expects 0 even when there are failures. end familyMap = evaluator.familyMap File.open($outputDir + "resultsByFamily", "w") { | outp | first = true familyMap.keys.sort.each { | familyName | if first first = false else outp.puts end outp.print "#{familyName}:" numPassed = 0 familyMap[familyName].each { | entry | if entry[:result] == "PASS" numPassed += 1 end } if numPassed == familyMap[familyName].size outp.puts " PASSED" elsif numPassed == 0 outp.puts " FAILED" else outp.puts familyMap[familyName].each { | entry | outp.puts " #{entry[:plan].name}: #{entry[:result]}" } end } } end def compressBundle cmd = "cd #{$outputDir}/.. && tar -czf #{$tarFileName} #{$outputDir.basename}" $stderr.puts ">> #{cmd}" if $verbosity >= 2 raise unless system(cmd) end def clean(file) FileUtils.rm_rf file unless $bundle end clean($outputDir + "failed") clean($outputDir + "passed") clean($outputDir + "noresult") clean($outputDir + "flaky") clean($outputDir + "results") clean($outputDir + "resultsByFamily") clean($outputDir + ".vm") clean($outputDir + ".helpers") clean($outputDir + ".runner") clean($outputDir + ".tests") clean($outputDir + "_payload") Dir.mkdir($outputDir) unless $outputDir.directory? $outputDir = $outputDir.realpath $runnerDir = $outputDir + ".runner" if !$numChildProcesses if ENV["WEBKIT_TEST_CHILD_PROCESSES"] $numChildProcesses = ENV["WEBKIT_TEST_CHILD_PROCESSES"].to_i $numChildProcessesSetByUser = true else $numChildProcesses = numberOfProcessors end end if ENV["JSCTEST_timeout"] # In the worst case, the processors just interfere with each other. # Increase the timeout proportionally to the number of processors. ENV["JSCTEST_timeout"] = (ENV["JSCTEST_timeout"].to_i.to_f * Math.sqrt($numChildProcesses)).to_i.to_s end # We do not adjust hardTimeout. If we are not producing any results during 1200 seconds, buildbot terminates the tests. So we should terminate hung tests. if !ENV["JSCTEST_memoryLimit"] && $memoryLimited ENV["JSCTEST_memoryLimit"] = (600 * 1024 * 1024).to_s end # Some tests fail if the time zone is not set to US/Pacific # https://webkit.org/b/136363 # Set as done in run-javascript-tests ENV["TZ"] = "US/Pacific"; def runBundle raise unless $bundle testRunner = TestRunner.create($testRunnerType, $runnerDir) evaluator = BundleTestsExecutor.new($runlist, $serialPlans, ITERATION_LIMITS, $treatFailingAsFlaky, testRunner) evaluator.loop end def runTarball raise unless $tarball prepareBundle testRunner = TestRunner.create($testRunnerType, $runnerDir) testRunner.prepare($runlist, $serialPlans, Set.new, nil) compressBundle end def forEachRemote(remoteHosts, options={}, &blk) threads = [] remoteHosts.each_index { | index | remoteHost = remoteHosts[index] threads << Thread.new { blk.call(index, remoteHost) } } etime = nil if options.has_key?(:timeout) etime = Time.now + options[:timeout] end liveRemotes = [] threads.each_index { | index | thread = threads[index] begin if options.has_key?(:timeout) if etime.nil? # If a timeout has been requested and etime is nil, # that means the timeout has expired and we shouldn't # wait at all. timeout = 0 else timeout = etime - Time.now if timeout < 0 timeout = 0 end end if thread.join(timeout).nil? if $verbosity > 0 $stderr.puts("Timeout joining thread for remote #{remoteHosts[index]}") end # Timeout expired, so we can't block waiting for # any other threads. Either they're done or # they've also timed out. etime = nil raise CommandExecutionFailed end else thread.join # No timeout requested, just block. end liveRemotes << remoteHosts[index] rescue CommandExecutionFailed if options[:dropOnFailure] if $verbosity > 0 $stderr.puts("Dropping failed remote #{remoteHosts[index]}") end else raise end end } liveRemotes end def each_chunk(e, chunkSize) accumulator = [] e.each { |element| accumulator << element if accumulator.size == chunkSize yield accumulator accumulator = [] end } if accumulator.size > 0 yield accumulator end end class TestRunnerGnuParallel < TestRunner def prepareGnuParallelRunnerJobs(name, runlist, completedPlans) path = @runnerDir + name FileUtils.mkdir_p(@runnerDir) File.open(path, "w") { | outp | runlist = runlist.select { |plan| not completedPlans.include?(plan) } each_chunk(runlist, $gnuParallelChunkSize) { |plans| job = plans.collect { |plan| "sh ./test_script_#{plan.index}" }.join("; ") outp.puts(job) } } end def prepareRunner(runlist, serialPlans, completedPlans, remoteHosts) prepareGnuParallelRunnerJobs("parallel-tests", runlist, completedPlans | serialPlans) prepareGnuParallelRunnerJobs("serial-tests", serialPlans, completedPlans) end end def withGnuParallelSshWrapper(&blk) Tempfile.open('ssh-wrapper', $runnerDir) { | wrapper | head = <<'EOF' #!/bin/sh remotedir="$1" shift remoteport="$1" shift remoteuser="$1" shift remotehost="$1" shift if test "x$1" != "x--"; then echo "Expected '--' at this position, instead got $1" 1>&2 exit 3 fi shift EOF extraOptions = [ # Many of our jobs are short, ensure GNU parallel won't need to set # up an ssh connection from scratch. "ControlPath=./%C", "ControlMaster=auto", "ControlPersist=10m", # Treat remote boards as volatile. Don't check the host keys, don't # save them locally. "StrictHostKeyChecking=no", "UserKnownHostsFile=/dev/null" ].collect { |opt| ["-o", opt] }.flatten sshOptions = (SSH_OPTIONS_DEFAULT + extraOptions).join(" ") wrapper.puts(head + "echo \"$@\" | ssh #{sshOptions} -p \"$remoteport\" -l \"$remoteuser\" -o RemoteCommand=\"if test -d '$remotedir'; then cd '$remotedir'; else echo '#{PARALLEL_REMOTE_WRAPPER_MARK_BEGIN}${remotehost}#{PARALLEL_REMOTE_WRAPPER_MARK_END}'; false; fi && sh -s\" \"$remotehost\"" ) FileUtils.chmod("ugo=rx", wrapper.path) wrapper.close # Avoid ETXTBUSY blk.call(wrapper.path) } end def withGnuParallelSshLoginFile(remoteHosts, &blk) withGnuParallelSshWrapper { | wrapper | Tempfile.open('slf', $runnerDir) { | tf | remoteHosts.each { | remoteHost | tf.puts("#{wrapper} #{remoteHost.remoteDirectory} #{remoteHost.port} #{remoteHost.user} #{remoteHost.host}") } tf.flush blk.call(tf.path) } } end def unpackBundleOnRemoteHosts(remoteHosts) forEachRemote(remoteHosts, :dropOnFailure => true, :timeout => REMOTE_TIMEOUT) { | _, remoteHost | mysys(["ssh"] + SSH_OPTIONS_DEFAULT + (remoteHost.identity_file_path ? ["-i", remoteHost.identity_file_path] : []) + ["-p", remoteHost.port.to_s, "#{remoteHost.user}@#{remoteHost.host}", "cd #{Shellwords.shellescape(remoteHost.remoteDirectory)} && rm -rf #{$outputDir.basename} && tar xzf #{$tarFileName}"]) } end def prepareRemotes(remoteHosts) # If the preparatory steps fail, drop the remote host from our # list. Otherwise, if it comes back online in the middle of an # iteration, we'll try to run test jobs on it, possibly using # an unrelated bundle from a previous run. remoteHosts = forEachRemote(remoteHosts, {:dropOnFailure => true, :timeout => REMOTE_TIMEOUT}) { | _, remoteHost | getRemoteDirectoryIfNeeded(remoteHost) copyBundleToRemote(remoteHost) } return unpackBundleOnRemoteHosts(remoteHosts) end def runGnuParallelRunner(remoteHosts, inputs, options={}) timeout = 300 if ENV["JSCTEST_timeout"] timeout = ENV["JSCTEST_timeout"].to_f.ceil.to_i end # Keep ncpus + 1 jobs running by default to avoid any stalls due # to ssh latency. parallelJobsOnEachHost = "+1" if $numChildProcessesSetByUser parallelJobsOnEachHost = $numChildProcesses end if options[:parallelJobsOnEachHost] parallelJobsOnEachHost = options[:parallelJobsOnEachHost] end markerWithHost = Regexp.new(".*#{PARALLEL_REMOTE_WRAPPER_MARK_BEGIN}(.*)#{PARALLEL_REMOTE_WRAPPER_MARK_END}.*") markerWithoutHost = Regexp.new(".*#{PARALLEL_REMOTE_STATE_LOST_MARKER}.*") withGnuParallelSshLoginFile(remoteHosts) { | slf | cmd = [ "parallel", "-j", "#{parallelJobsOnEachHost}", # NB: the tests exit with 0 regardless of whether they # passed or failed, so this will only retry tests that we # weren't able to get a result for, likely because the # connection went down or the remote OOM'd/crashed). "--retries", "5", "--line-buffer", # we know our output is line-oriented "--slf", slf, "--timeout", timeout.to_s, "-a", inputs, "if test -e #{$outputDir.basename}/.runner; then cd #{$outputDir.basename}/.runner; else echo #{PARALLEL_REMOTE_STATE_LOST_MARKER}; false; fi && " + exportBaseEnvironmentVariables(false) + $envVars.collect { |var | "export #{var} &&"}.join("") + "sh -c " ] runAndMonitorCommandOutput(cmd) { | pid, line | host = "" md = markerWithoutHost.match(line) if md.nil? md = markerWithHost.match(line) host = md[1] unless md.nil? end if not md.nil? if $verbosity > 0 $stderr.puts("Remote host lost state, triggering high-level retry: #{host}") end # We could try to reprovision this specific remote # host, but that seems needlessly complicated (we # don't expect the remotes to go down every # minute...). Simply kill the GNU parallel process. Process.kill("TERM", pid) end } } end # Unconditionally run the selftests on every execution, they're pretty cheap. ExecutorSelfTests.run TestResultEvaluatorSelfTests.run class NonRemoteTestsExecutor < BaseTestsExecutor def initialize(runlist, serialPlans, maxIterationsBounds, treatFailingAsFlaky, testRunner) super(runlist, serialPlans, nil, maxIterationsBounds, treatFailingAsFlaky ) @testRunner = testRunner end def prepareExecution(remoteHosts) remoteHosts end def refreshExecution(runlist, serialPlans, completedTests, remoteHosts) if not @remoteHosts.nil? raise "remoteHosts #{@remoteHosts}, expected nil" end @testRunner.prepare(runlist, serialPlans, completedTests, remoteHosts) end end class NormalTestsExecutor < NonRemoteTestsExecutor def prepareArtifacts(remoteHosts) raise "remoteHosts not nil" unless remoteHosts.nil? prepareBundle @testRunner.prepare(@runlist, @serialPlans, Set.new, nil) end def executeTests(remoteHosts) runTestRunner(@testRunner, nil, nil) cleanEmptyResultFiles end def updateStatusMap(iteration, statusMap) return getStatusMap(statusMap) end end class BundleTestsExecutor < NonRemoteTestsExecutor def prepareArtifacts(remoteHosts) raise "remoteHosts not nil" unless remoteHosts.nil? cleanRunnerDirectory cleanOldResults end def executeTests(remoteHosts) runTestRunner(@testRunner, nil, nil) cleanEmptyResultFiles end def updateStatusMap(iteration, statusMap) return getStatusMap(statusMap) end end class BaseRemoteTestsExecutor < BaseTestsExecutor def initialize(runlist, serialPlans, remoteHosts, iterationLimits, treatFailingAsFlaky, testRunner) super(runlist, serialPlans, remoteHosts, iterationLimits, treatFailingAsFlaky) @testRunner = testRunner end def updateStatusMap(iteration, statusMap) return getStatusMap(statusMap) end def refreshExecution(runlist, serialPlans, completedTests, remoteHosts) # Note, when running tests remotely, we always prepare tests # for all the hosts when refreshing execution, as even hosts # that went away during testing may have come back online in # the meantime -- so we don't expect a list of live remote # hosts to be passed in. @testRunner.prepare(runlist, serialPlans, completedTests, remoteHosts) end end class RemoteTestsExecutor < BaseRemoteTestsExecutor def prepareArtifacts(remoteHosts) raise "remoteHosts nil" if remoteHosts.nil? prepareBundle # The make runner statically shards the tests, so do a # liveness check before @testRunner.prepare. Otherwise, we'd # always end up scheduling tests on dead remotes and hamper # our progress. remoteHosts = getLiveRemoteHosts(remoteHosts) @testRunner.prepare(@runlist, @serialPlans, Set.new, remoteHosts) compressBundle remoteHosts end def prepareExecution(remoteHosts) return prepareRemotes(remoteHosts) end def refreshExecution(runlist, serialPlans, completedTests, remoteHosts) # Don't assign tests to remotes that are down. remoteHosts = getLiveRemoteHosts(remoteHosts) super(runlist, serialPlans, completedTests, remoteHosts) end def executeTests(remoteHosts) forEachRemote(remoteHosts, :dropOnFailure => true) { | index | runTestRunner(@testRunner, remoteHosts, index) } end private def getLiveRemoteHosts(remoteHosts) # This command is supposed to work on all remote hosts, so # reuse it for the liveness check. cmd = commandToGetNumberOfProcessors forEachRemote(remoteHosts, :dropOnFailure => true, :timeout => REMOTE_TIMEOUT) { |_, remoteHost| begin sshRead(cmd, remoteHost) rescue # It's what forEachRemote considers an expected failure. raise CommandExecutionFailed end } end end class GnuParallelTestsExecutor < BaseRemoteTestsExecutor def prepareArtifacts(remoteHosts) prepareBundle @testRunner.prepare(@runlist, @serialPlans, Set.new, remoteHosts) compressBundle end def prepareExecution(remoteHosts) return prepareRemotes(remoteHosts) end def executeTests(remoteHosts) runGnuParallelRunner(remoteHosts, $runnerDir + "serial-tests", { :parallelJobsOnEachHost => 1}) runGnuParallelRunner(remoteHosts, $runnerDir + "parallel-tests") end end def runNormal raise if $bundle or $tarball testRunner = TestRunner.create($testRunnerType, $runnerDir) evaluator = NormalTestsExecutor.new($runlist, $serialPlans, ITERATION_LIMITS, $treatFailingAsFlaky, testRunner) statusMap = evaluator.loop detectFailures(statusMap) end def runRemote raise unless $remote testRunner = TestRunner.create($testRunnerType, $runnerDir) executor = RemoteTestsExecutor.new($runlist, $serialPlans, $remoteHosts, ITERATION_LIMITS, $treatFailingAsFlaky, testRunner) statusMap = executor.loop detectFailures(statusMap) end def runGnuParallel raise "Can only use --gnu-parallel-runner with --remote*" unless $remote raise "Can only use --gnu-parallel-runner with the default writer" unless $testWriter == "default" testRunner = TestRunner.create($testRunnerType, $runnerDir) executor = GnuParallelTestsExecutor.new($runlist, $serialPlans, $remoteHosts, ITERATION_LIMITS, $treatFailingAsFlaky, testRunner) statusMap = executor.loop detectFailures(statusMap) end puts if $testRunnerType == :gnuparallel raise unless $remote end if $bundle runBundle elsif $remote if $testRunnerType == :gnuparallel runGnuParallel else runRemote end elsif $tarball runTarball else runNormal end