1 #!/usr/bin/env python 2 # 3 # Copyright 2012 the V8 project authors. All rights reserved. 4 # Redistribution and use in source and binary forms, with or without 5 # modification, are permitted provided that the following conditions are 6 # met: 7 # 8 # * Redistributions of source code must retain the above copyright 9 # notice, this list of conditions and the following disclaimer. 10 # * Redistributions in binary form must reproduce the above 11 # copyright notice, this list of conditions and the following 12 # disclaimer in the documentation and/or other materials provided 13 # with the distribution. 14 # * Neither the name of Google Inc. nor the names of its 15 # contributors may be used to endorse or promote products derived 16 # from this software without specific prior written permission. 17 # 18 # THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS 19 # "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT 20 # LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR 21 # A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT 22 # OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, 23 # SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT 24 # LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, 25 # DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY 26 # THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT 27 # (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE 28 # OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. 29 30 31 import json 32 import math 33 import multiprocessing 34 import optparse 35 import os 36 from os.path import join 37 import random 38 import shlex 39 import subprocess 40 import sys 41 import time 42 43 from testrunner.local import execution 44 from testrunner.local import progress 45 from testrunner.local import testsuite 46 from testrunner.local import utils 47 from testrunner.local import verbose 48 from testrunner.objects import context 49 50 51 ARCH_GUESS = utils.DefaultArch() 52 DEFAULT_TESTS = ["mjsunit", "webkit"] 53 TIMEOUT_DEFAULT = 60 54 TIMEOUT_SCALEFACTOR = {"debug" : 4, 55 "release" : 1 } 56 57 MODE_FLAGS = { 58 "debug" : ["--nobreak-on-abort", "--nodead-code-elimination", 59 "--nofold-constants", "--enable-slow-asserts", 60 "--debug-code", "--verify-heap", 61 "--noparallel-recompilation"], 62 "release" : ["--nobreak-on-abort", "--nodead-code-elimination", 63 "--nofold-constants", "--noparallel-recompilation"]} 64 65 SUPPORTED_ARCHS = ["android_arm", 66 "android_ia32", 67 "arm", 68 "ia32", 69 "mipsel", 70 "nacl_ia32", 71 "nacl_x64", 72 "x64"] 73 # Double the timeout for these: 74 SLOW_ARCHS = ["android_arm", 75 "android_ia32", 76 "arm", 77 "mipsel", 78 "nacl_ia32", 79 "nacl_x64"] 80 MAX_DEOPT = 1000000000 81 DISTRIBUTION_MODES = ["smooth", "random"] 82 83 84 class RandomDistribution: 85 def __init__(self, seed=None): 86 seed = seed or random.randint(1, sys.maxint) 87 print "Using random distribution with seed %d" % seed 88 self._random = random.Random(seed) 89 90 def Distribute(self, n, m): 91 if n > m: 92 n = m 93 return self._random.sample(xrange(1, m + 1), n) 94 95 96 class SmoothDistribution: 97 """Distribute n numbers into the interval [1:m]. 98 F1: Factor of the first derivation of the distribution function. 99 F2: Factor of the second derivation of the distribution function. 100 With F1 and F2 set to 0, the distribution will be equal. 101 """ 102 def __init__(self, factor1=2.0, factor2=0.2): 103 self._factor1 = factor1 104 self._factor2 = factor2 105 106 def Distribute(self, n, m): 107 if n > m: 108 n = m 109 if n <= 1: 110 return [ 1 ] 111 112 result = [] 113 x = 0.0 114 dx = 1.0 115 ddx = self._factor1 116 dddx = self._factor2 117 for i in range(0, n): 118 result += [ x ] 119 x += dx 120 dx += ddx 121 ddx += dddx 122 123 # Project the distribution into the interval [0:M]. 124 result = [ x * m / result[-1] for x in result ] 125 126 # Equalize by n. The closer n is to m, the more equal will be the 127 # distribution. 128 for (i, x) in enumerate(result): 129 # The value of x if it was equally distributed. 130 equal_x = i / float(n - 1) * float(m - 1) + 1 131 132 # Difference factor between actual and equal distribution. 133 diff = 1 - (x / equal_x) 134 135 # Equalize x dependent on the number of values to distribute. 136 result[i] = int(x + (i + 1) * diff) 137 return result 138 139 140 def Distribution(options): 141 if options.distribution_mode == "random": 142 return RandomDistribution(options.seed) 143 if options.distribution_mode == "smooth": 144 return SmoothDistribution(options.distribution_factor1, 145 options.distribution_factor2) 146 147 148 def BuildOptions(): 149 result = optparse.OptionParser() 150 result.add_option("--arch", 151 help=("The architecture to run tests for, " 152 "'auto' or 'native' for auto-detect"), 153 default="ia32,x64,arm") 154 result.add_option("--arch-and-mode", 155 help="Architecture and mode in the format 'arch.mode'", 156 default=None) 157 result.add_option("--buildbot", 158 help="Adapt to path structure used on buildbots", 159 default=False, action="store_true") 160 result.add_option("--command-prefix", 161 help="Prepended to each shell command used to run a test", 162 default="") 163 result.add_option("--coverage", help=("Exponential test coverage " 164 "(range 0.0, 1.0) -- 0.0: one test, 1.0 all tests (slow)"), 165 default=0.4, type="float") 166 result.add_option("--coverage-lift", help=("Lifts test coverage for tests " 167 "with a small number of deopt points (range 0, inf)"), 168 default=20, type="int") 169 result.add_option("--download-data", help="Download missing test suite data", 170 default=False, action="store_true") 171 result.add_option("--distribution-factor1", help=("Factor of the first " 172 "derivation of the distribution function"), default=2.0, 173 type="float") 174 result.add_option("--distribution-factor2", help=("Factor of the second " 175 "derivation of the distribution function"), default=0.7, 176 type="float") 177 result.add_option("--distribution-mode", help=("How to select deopt points " 178 "for a given test (smooth|random)"), 179 default="smooth") 180 result.add_option("--dump-results-file", help=("Dump maximum number of " 181 "deopt points per test to a file")) 182 result.add_option("--extra-flags", 183 help="Additional flags to pass to each test command", 184 default="") 185 result.add_option("--isolates", help="Whether to test isolates", 186 default=False, action="store_true") 187 result.add_option("-j", help="The number of parallel tasks to run", 188 default=0, type="int") 189 result.add_option("-m", "--mode", 190 help="The test modes in which to run (comma-separated)", 191 default="release,debug") 192 result.add_option("--outdir", help="Base directory with compile output", 193 default="out") 194 result.add_option("-p", "--progress", 195 help=("The style of progress indicator" 196 " (verbose, dots, color, mono)"), 197 choices=progress.PROGRESS_INDICATORS.keys(), 198 default="mono") 199 result.add_option("--shard-count", 200 help="Split testsuites into this number of shards", 201 default=1, type="int") 202 result.add_option("--shard-run", 203 help="Run this shard from the split up tests.", 204 default=1, type="int") 205 result.add_option("--shell-dir", help="Directory containing executables", 206 default="") 207 result.add_option("--seed", help="The seed for the random distribution", 208 type="int") 209 result.add_option("-t", "--timeout", help="Timeout in seconds", 210 default= -1, type="int") 211 result.add_option("-v", "--verbose", help="Verbose output", 212 default=False, action="store_true") 213 return result 214 215 216 def ProcessOptions(options): 217 global VARIANT_FLAGS 218 219 # Architecture and mode related stuff. 220 if options.arch_and_mode: 221 tokens = options.arch_and_mode.split(".") 222 options.arch = tokens[0] 223 options.mode = tokens[1] 224 options.mode = options.mode.split(",") 225 for mode in options.mode: 226 if not mode.lower() in ["debug", "release"]: 227 print "Unknown mode %s" % mode 228 return False 229 if options.arch in ["auto", "native"]: 230 options.arch = ARCH_GUESS 231 options.arch = options.arch.split(",") 232 for arch in options.arch: 233 if not arch in SUPPORTED_ARCHS: 234 print "Unknown architecture %s" % arch 235 return False 236 237 # Special processing of other options, sorted alphabetically. 238 options.command_prefix = shlex.split(options.command_prefix) 239 options.extra_flags = shlex.split(options.extra_flags) 240 if options.j == 0: 241 options.j = multiprocessing.cpu_count() 242 if not options.distribution_mode in DISTRIBUTION_MODES: 243 print "Unknown distribution mode %s" % options.distribution_mode 244 return False 245 if options.distribution_factor1 < 0.0: 246 print ("Distribution factor1 %s is out of range. Defaulting to 0.0" 247 % options.distribution_factor1) 248 options.distribution_factor1 = 0.0 249 if options.distribution_factor2 < 0.0: 250 print ("Distribution factor2 %s is out of range. Defaulting to 0.0" 251 % options.distribution_factor2) 252 options.distribution_factor2 = 0.0 253 if options.coverage < 0.0 or options.coverage > 1.0: 254 print ("Coverage %s is out of range. Defaulting to 0.4" 255 % options.coverage) 256 options.coverage = 0.4 257 if options.coverage_lift < 0: 258 print ("Coverage lift %s is out of range. Defaulting to 0" 259 % options.coverage_lift) 260 options.coverage_lift = 0 261 return True 262 263 264 def ShardTests(tests, shard_count, shard_run): 265 if shard_count < 2: 266 return tests 267 if shard_run < 1 or shard_run > shard_count: 268 print "shard-run not a valid number, should be in [1:shard-count]" 269 print "defaulting back to running all tests" 270 return tests 271 count = 0 272 shard = [] 273 for test in tests: 274 if count % shard_count == shard_run - 1: 275 shard.append(test) 276 count += 1 277 return shard 278 279 280 def Main(): 281 parser = BuildOptions() 282 (options, args) = parser.parse_args() 283 if not ProcessOptions(options): 284 parser.print_help() 285 return 1 286 287 exit_code = 0 288 workspace = os.path.abspath(join(os.path.dirname(sys.argv[0]), "..")) 289 290 suite_paths = utils.GetSuitePaths(join(workspace, "test")) 291 292 if len(args) == 0: 293 suite_paths = [ s for s in suite_paths if s in DEFAULT_TESTS ] 294 else: 295 args_suites = set() 296 for arg in args: 297 suite = arg.split(os.path.sep)[0] 298 if not suite in args_suites: 299 args_suites.add(suite) 300 suite_paths = [ s for s in suite_paths if s in args_suites ] 301 302 suites = [] 303 for root in suite_paths: 304 suite = testsuite.TestSuite.LoadTestSuite( 305 os.path.join(workspace, "test", root)) 306 if suite: 307 suites.append(suite) 308 309 if options.download_data: 310 for s in suites: 311 s.DownloadData() 312 313 for mode in options.mode: 314 for arch in options.arch: 315 code = Execute(arch, mode, args, options, suites, workspace) 316 exit_code = exit_code or code 317 return exit_code 318 319 320 def CalculateNTests(m, options): 321 """Calculates the number of tests from m deopt points with exponential 322 coverage. 323 The coverage is expected to be between 0.0 and 1.0. 324 The 'coverage lift' lifts the coverage for tests with smaller m values. 325 """ 326 c = float(options.coverage) 327 l = float(options.coverage_lift) 328 return int(math.pow(m, (m * c + l) / (m + l))) 329 330 331 def Execute(arch, mode, args, options, suites, workspace): 332 print(">>> Running tests for %s.%s" % (arch, mode)) 333 334 dist = Distribution(options) 335 336 shell_dir = options.shell_dir 337 if not shell_dir: 338 if options.buildbot: 339 shell_dir = os.path.join(workspace, options.outdir, mode) 340 mode = mode.lower() 341 else: 342 shell_dir = os.path.join(workspace, options.outdir, 343 "%s.%s" % (arch, mode)) 344 shell_dir = os.path.relpath(shell_dir) 345 346 # Populate context object. 347 mode_flags = MODE_FLAGS[mode] 348 timeout = options.timeout 349 if timeout == -1: 350 # Simulators are slow, therefore allow a longer default timeout. 351 if arch in SLOW_ARCHS: 352 timeout = 2 * TIMEOUT_DEFAULT; 353 else: 354 timeout = TIMEOUT_DEFAULT; 355 356 timeout *= TIMEOUT_SCALEFACTOR[mode] 357 ctx = context.Context(arch, mode, shell_dir, 358 mode_flags, options.verbose, 359 timeout, options.isolates, 360 options.command_prefix, 361 options.extra_flags) 362 363 # Find available test suites and read test cases from them. 364 variables = { 365 "mode": mode, 366 "arch": arch, 367 "system": utils.GuessOS(), 368 "isolates": options.isolates, 369 "deopt_fuzzer": True, 370 } 371 all_tests = [] 372 num_tests = 0 373 test_id = 0 374 375 # Remember test case prototypes for the fuzzing phase. 376 test_backup = dict((s, []) for s in suites) 377 378 for s in suites: 379 s.ReadStatusFile(variables) 380 s.ReadTestCases(ctx) 381 if len(args) > 0: 382 s.FilterTestCasesByArgs(args) 383 all_tests += s.tests 384 s.FilterTestCasesByStatus(False) 385 test_backup[s] = s.tests 386 analysis_flags = ["--deopt-every-n-times", "%d" % MAX_DEOPT, 387 "--print-deopt-stress"] 388 s.tests = [ t.CopyAddingFlags(analysis_flags) for t in s.tests ] 389 num_tests += len(s.tests) 390 for t in s.tests: 391 t.id = test_id 392 test_id += 1 393 394 if num_tests == 0: 395 print "No tests to run." 396 return 0 397 398 try: 399 print(">>> Collection phase") 400 progress_indicator = progress.PROGRESS_INDICATORS[options.progress]() 401 runner = execution.Runner(suites, progress_indicator, ctx) 402 403 exit_code = runner.Run(options.j) 404 if runner.terminate: 405 return exit_code 406 407 except KeyboardInterrupt: 408 return 1 409 410 print(">>> Analysis phase") 411 num_tests = 0 412 test_id = 0 413 for s in suites: 414 test_results = {} 415 for t in s.tests: 416 for line in t.output.stdout.splitlines(): 417 if line.startswith("=== Stress deopt counter: "): 418 test_results[t.path] = MAX_DEOPT - int(line.split(" ")[-1]) 419 for t in s.tests: 420 if t.path not in test_results: 421 print "Missing results for %s" % t.path 422 if options.dump_results_file: 423 results_dict = dict((t.path, n) for (t, n) in test_results.iteritems()) 424 with file("%s.%d.txt" % (dump_results_file, time.time()), "w") as f: 425 f.write(json.dumps(results_dict)) 426 427 # Reset tests and redistribute the prototypes from the collection phase. 428 s.tests = [] 429 if options.verbose: 430 print "Test distributions:" 431 for t in test_backup[s]: 432 max_deopt = test_results.get(t.path, 0) 433 if max_deopt == 0: 434 continue 435 n_deopt = CalculateNTests(max_deopt, options) 436 distribution = dist.Distribute(n_deopt, max_deopt) 437 if options.verbose: 438 print "%s %s" % (t.path, distribution) 439 for i in distribution: 440 fuzzing_flags = ["--deopt-every-n-times", "%d" % i] 441 s.tests.append(t.CopyAddingFlags(fuzzing_flags)) 442 num_tests += len(s.tests) 443 for t in s.tests: 444 t.id = test_id 445 test_id += 1 446 447 if num_tests == 0: 448 print "No tests to run." 449 return 0 450 451 try: 452 print(">>> Deopt fuzzing phase (%d test cases)" % num_tests) 453 progress_indicator = progress.PROGRESS_INDICATORS[options.progress]() 454 runner = execution.Runner(suites, progress_indicator, ctx) 455 456 exit_code = runner.Run(options.j) 457 if runner.terminate: 458 return exit_code 459 460 except KeyboardInterrupt: 461 return 1 462 463 return exit_code 464 465 466 if __name__ == "__main__": 467 sys.exit(Main()) 468