1#!/usr/bin/env python2.7 2 3# Copyright 2015, VIXL authors 4# All rights reserved. 5# 6# Redistribution and use in source and binary forms, with or without 7# modification, are permitted provided that the following conditions are met: 8# 9# * Redistributions of source code must retain the above copyright notice, 10# this list of conditions and the following disclaimer. 11# * Redistributions in binary form must reproduce the above copyright notice, 12# this list of conditions and the following disclaimer in the documentation 13# and/or other materials provided with the distribution. 14# * Neither the name of ARM Limited nor the names of its contributors may be 15# used to endorse or promote products derived from this software without 16# specific prior written permission. 17# 18# THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS CONTRIBUTORS "AS IS" AND 19# ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED 20# WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE 21# DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE LIABLE 22# FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL 23# DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR 24# SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER 25# CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, 26# OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE 27# OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. 28 29import argparse 30import fcntl 31import git 32import itertools 33import multiprocessing 34import os 35from os.path import join 36import platform 37import re 38import subprocess 39import sys 40import time 41 42import config 43import clang_format 44import lint 45import printer 46import test 47import threaded_tests 48import util 49 50 51dir_root = config.dir_root 52 53def Optionify(name): 54 return '--' + name 55 56 57# The options that can be tested are abstracted to provide an easy way to add 58# new ones. 59# Environment options influence the environment. They can be used for example to 60# set the compiler used. 61# Build options are options passed to scons, with a syntax like `scons opt=val` 62# Runtime options are options passed to the test program. 63# See the definition of `test_options` below. 64 65# 'all' is a special value for the options. If specified, all other values of 66# the option are tested. 67class TestOption(object): 68 type_environment = 'type_environment' 69 type_build = 'type_build' 70 type_run = 'type_run' 71 72 def __init__(self, option_type, name, help, 73 val_test_choices, val_test_default = None, 74 # If unset, the user can pass any value. 75 strict_choices = True, test_independently = False): 76 self.name = name 77 self.option_type = option_type 78 self.help = help 79 self.val_test_choices = val_test_choices 80 self.strict_choices = strict_choices 81 self.test_independently = test_independently 82 if val_test_default is not None: 83 self.val_test_default = val_test_default 84 else: 85 self.val_test_default = val_test_choices[0] 86 87 def ArgList(self, to_test): 88 res = [] 89 if to_test == 'all': 90 for value in self.val_test_choices: 91 if value != 'all': 92 res.append(self.GetOptionString(value)) 93 else: 94 for value in to_test: 95 res.append(self.GetOptionString(value)) 96 return res 97 98class EnvironmentOption(TestOption): 99 option_type = TestOption.type_environment 100 def __init__(self, name, environment_variable_name, help, 101 val_test_choices, val_test_default = None, 102 strict_choices = True): 103 super(EnvironmentOption, self).__init__(EnvironmentOption.option_type, 104 name, 105 help, 106 val_test_choices, 107 val_test_default, 108 strict_choices = strict_choices) 109 self.environment_variable_name = environment_variable_name 110 111 def GetOptionString(self, value): 112 return self.environment_variable_name + '=' + value 113 114 115class BuildOption(TestOption): 116 option_type = TestOption.type_build 117 def __init__(self, name, help, 118 val_test_choices, val_test_default = None, 119 strict_choices = True, test_independently = False): 120 super(BuildOption, self).__init__(BuildOption.option_type, 121 name, 122 help, 123 val_test_choices, 124 val_test_default, 125 strict_choices = strict_choices, 126 test_independently = test_independently) 127 def GetOptionString(self, value): 128 return self.name + '=' + value 129 130 131class RuntimeOption(TestOption): 132 option_type = TestOption.type_run 133 def __init__(self, name, help, 134 val_test_choices, val_test_default = None): 135 super(RuntimeOption, self).__init__(RuntimeOption.option_type, 136 name, 137 help, 138 val_test_choices, 139 val_test_default) 140 def GetOptionString(self, value): 141 if value == 'on': 142 return Optionify(self.name) 143 else: 144 return None 145 146 147 148environment_option_compiler = \ 149 EnvironmentOption('compiler', 'CXX', 'Test for the specified compilers.', 150 val_test_choices=['all'] + config.tested_compilers, 151 strict_choices = False) 152test_environment_options = [ 153 environment_option_compiler 154] 155 156build_option_mode = \ 157 BuildOption('mode', 'Test with the specified build modes.', 158 val_test_choices=['all'] + config.build_options_modes) 159build_option_standard = \ 160 BuildOption('std', 'Test with the specified C++ standard.', 161 val_test_choices=['all'] + config.tested_cpp_standards, 162 strict_choices = False) 163build_option_target = \ 164 BuildOption('target', 'Test with the specified isa enabled.', 165 val_test_choices=['all'] + config.build_options_target, 166 strict_choices = False, test_independently = True) 167build_option_negative_testing = \ 168 BuildOption('negative_testing', 'Test with negative testing enabled.', 169 val_test_choices=['all'] + config.build_options_negative_testing, 170 strict_choices = False, test_independently = True) 171test_build_options = [ 172 build_option_mode, 173 build_option_standard, 174 build_option_target, 175 build_option_negative_testing 176] 177 178runtime_option_debugger = \ 179 RuntimeOption('debugger', 180 '''Test with the specified configurations for the debugger. 181 Note that this is only tested if we are using the simulator.''', 182 val_test_choices=['all', 'on', 'off']) 183test_runtime_options = [ 184 runtime_option_debugger 185] 186 187test_options = \ 188 test_environment_options + test_build_options + test_runtime_options 189 190 191def BuildOptions(): 192 args = argparse.ArgumentParser( 193 description = 194 '''This tool runs all tests matching the specified filters for multiple 195 environment, build options, and runtime options configurations.''', 196 # Print default values. 197 formatter_class=argparse.ArgumentDefaultsHelpFormatter) 198 199 args.add_argument('filters', metavar='filter', nargs='*', 200 help='Run tests matching all of the (regexp) filters.') 201 202 # We automatically build the script options from the options to be tested. 203 test_arguments = args.add_argument_group( 204 'Test options', 205 'These options indicate what should be tested') 206 for option in test_options: 207 choices = option.val_test_choices if option.strict_choices else None 208 help = option.help 209 if not option.strict_choices: 210 help += ' Supported values: {' + ','.join(option.val_test_choices) + '}' 211 test_arguments.add_argument(Optionify(option.name), 212 nargs='+', 213 choices=choices, 214 default=option.val_test_default, 215 help=help, 216 action='store') 217 218 general_arguments = args.add_argument_group('General options') 219 general_arguments.add_argument('--fast', action='store_true', 220 help='''Skip the lint and clang-format tests, 221 and run only with one compiler, in one mode, 222 with one C++ standard, and with an appropriate 223 default for runtime options.''') 224 general_arguments.add_argument('--dry-run', action='store_true', 225 help='''Don't actually build or run anything, 226 but print the configurations that would be 227 tested.''') 228 general_arguments.add_argument( 229 '--jobs', '-j', metavar='N', type=int, nargs='?', 230 default=multiprocessing.cpu_count(), 231 const=multiprocessing.cpu_count(), 232 help='''Runs the tests using N jobs. If the option is set but no value is 233 provided, the script will use as many jobs as it thinks useful.''') 234 general_arguments.add_argument('--nobench', action='store_true', 235 help='Do not run benchmarks.') 236 general_arguments.add_argument('--nolint', action='store_true', 237 help='Do not run the linter.') 238 general_arguments.add_argument('--noclang-format', action='store_true', 239 help='Do not run clang-format.') 240 general_arguments.add_argument('--notest', action='store_true', 241 help='Do not run tests.') 242 general_arguments.add_argument('--fail-early', action='store_true', 243 help='Exit as soon as a test fails.') 244 sim_default = 'none' if platform.machine() == 'aarch64' else 'aarch64' 245 general_arguments.add_argument( 246 '--simulator', action='store', choices=['aarch64', 'none'], 247 default=sim_default, 248 help='Explicitly enable or disable the simulator.') 249 general_arguments.add_argument( 250 '--under_valgrind', action='store_true', 251 help='''Run the test-runner commands under Valgrind. 252 Note that a few tests are known to fail because of 253 issues in Valgrind''') 254 return args.parse_args() 255 256 257def RunCommand(command, environment_options = None): 258 # Create a copy of the environment. We do not want to pollute the environment 259 # of future commands run. 260 environment = os.environ 261 # Configure the environment. 262 # TODO: We currently pass the options as strings, so we need to parse them. We 263 # should instead pass them as a data structure and build the string option 264 # later. `environment_options` looks like `['CXX=compiler', 'OPT=val']`. 265 if environment_options: 266 for option in environment_options: 267 opt, val = option.split('=') 268 environment[opt] = val 269 270 printable_command = '' 271 if environment_options: 272 printable_command += ' '.join(environment_options) + ' ' 273 printable_command += ' '.join(command) 274 275 printable_command_orange = \ 276 printer.COLOUR_ORANGE + printable_command + printer.NO_COLOUR 277 printer.PrintOverwritableLine(printable_command_orange) 278 sys.stdout.flush() 279 280 # Start a process for the command. 281 # Interleave `stderr` and `stdout`. 282 p = subprocess.Popen(command, 283 stdout=subprocess.PIPE, 284 stderr=subprocess.STDOUT, 285 env=environment) 286 287 # We want to be able to display a continuously updated 'work indicator' while 288 # the process is running. Since the process can hang if the `stdout` pipe is 289 # full, we need to pull from it regularly. We cannot do so via the 290 # `readline()` function because it is blocking, and would thus cause the 291 # indicator to not be updated properly. So use file control mechanisms 292 # instead. 293 indicator = ' (still working: %d seconds elapsed)' 294 295 # Mark the process output as non-blocking. 296 flags = fcntl.fcntl(p.stdout, fcntl.F_GETFL) 297 fcntl.fcntl(p.stdout, fcntl.F_SETFL, flags | os.O_NONBLOCK) 298 299 t_start = time.time() 300 t_last_indication = t_start 301 process_output = '' 302 303 # Keep looping as long as the process is running. 304 while p.poll() is None: 305 # Avoid polling too often. 306 time.sleep(0.1) 307 # Update the progress indicator. 308 t_current = time.time() 309 if (t_current - t_start >= 2) and (t_current - t_last_indication >= 1): 310 printer.PrintOverwritableLine( 311 printable_command_orange + indicator % int(t_current - t_start)) 312 sys.stdout.flush() 313 t_last_indication = t_current 314 # Pull from the process output. 315 while True: 316 try: 317 line = os.read(p.stdout.fileno(), 1024) 318 except OSError: 319 line = '' 320 break 321 if line == '': break 322 process_output += line 323 324 # The process has exited. Don't forget to retrieve the rest of its output. 325 out, err = p.communicate() 326 rc = p.poll() 327 process_output += out 328 329 if rc == 0: 330 printer.Print(printer.COLOUR_GREEN + printable_command + printer.NO_COLOUR) 331 else: 332 printer.Print(printer.COLOUR_RED + printable_command + printer.NO_COLOUR) 333 printer.Print(process_output) 334 return rc 335 336 337def RunLinter(): 338 rc, default_tracked_files = lint.GetDefaultFilesToLint() 339 if rc: 340 return rc 341 return lint.RunLinter(map(lambda x: join(dir_root, x), default_tracked_files), 342 jobs = args.jobs, progress_prefix = 'cpp lint: ') 343 344 345def RunClangFormat(): 346 return clang_format.ClangFormatFiles(clang_format.GetCppSourceFilesToFormat(), 347 jobs = args.jobs, 348 progress_prefix = 'clang-format: ') 349 350 351 352def BuildAll(build_options, jobs): 353 scons_command = ["scons", "-C", dir_root, 'all', '-j', str(jobs)] 354 scons_command += list(build_options) 355 return RunCommand(scons_command, list(environment_options)) 356 357 358# Work out if the given options or args allow to run on the specified arch. 359# * arches is a list of ISA/architecture (a64, aarch32, etc) 360# * options are test.py's command line options if any. 361# * args are the arguments given to the build script. 362def CanRunOn(arches, options, args): 363 # First we check in the build specific options. 364 for option in options: 365 if 'target' in option: 366 # The option format is 'target=x,y,z'. 367 for target in (option.split('='))[1].split(','): 368 if target in arches: 369 return True 370 371 # There was a target build option but it didn't include the target arch. 372 return False 373 374 # No specific build option, check the script arguments. 375 # The meaning of 'all' will depend on the platform, e.g. 32-bit compilers 376 # cannot handle Aarch64 while 64-bit compiler can handle Aarch32. To avoid 377 # any issues no benchmarks are run for target='all'. 378 if args.target == 'all': return False 379 380 for target in args.target[0].split(','): 381 if target in arches: 382 return True 383 384 return False 385 386 387def CanRunAarch64(options, args): 388 return CanRunOn(['aarch64', 'a64'], options, args) 389 390 391def CanRunAarch32(options, args): 392 return CanRunOn(['aarch32', 'a32', 't32'], options, args) 393 394 395def RunBenchmarks(options, args): 396 rc = 0 397 if CanRunAarch32(options, args): 398 benchmark_names = util.ListCCFilesWithoutExt(config.dir_aarch32_benchmarks) 399 for bench in benchmark_names: 400 rc |= RunCommand( 401 [os.path.realpath( 402 join(config.dir_build_latest, 'benchmarks/aarch32', bench))]) 403 if CanRunAarch64(options, args): 404 benchmark_names = util.ListCCFilesWithoutExt(config.dir_aarch64_benchmarks) 405 for bench in benchmark_names: 406 rc |= RunCommand( 407 [util.relrealpath( 408 join(config.dir_build_latest, 'benchmarks/aarch64', bench))]) 409 return rc 410 411 412def PrintStatus(success): 413 printer.Print('\n$ ' + ' '.join(sys.argv)) 414 if success: 415 printer.Print('SUCCESS') 416 else: 417 printer.Print('FAILURE') 418 419 420 421if __name__ == '__main__': 422 util.require_program('scons') 423 rc = 0 424 425 args = BuildOptions() 426 427 def MaybeExitEarly(rc): 428 if args.fail_early and rc != 0: 429 PrintStatus(rc == 0) 430 sys.exit(rc) 431 432 if args.under_valgrind: 433 util.require_program('valgrind') 434 435 if args.fast: 436 def SetFast(option, specified, default): 437 option.val_test_choices = \ 438 [default if specified == 'all' else specified[0]] 439 # `g++` is very slow to compile a few aarch32 test files. 440 SetFast(environment_option_compiler, args.compiler, 'clang++') 441 SetFast(build_option_standard, args.std, 'c++98') 442 SetFast(build_option_mode, args.mode, 'debug') 443 SetFast(runtime_option_debugger, args.debugger, 'on') 444 445 if not args.nolint and not (args.fast or args.dry_run): 446 rc |= RunLinter() 447 MaybeExitEarly(rc) 448 449 if not args.noclang_format and not (args.fast or args.dry_run): 450 rc |= RunClangFormat() 451 MaybeExitEarly(rc) 452 453 # Don't try to test the debugger if we are not running with the simulator. 454 if not args.simulator: 455 test_runtime_options = \ 456 filter(lambda x: x.name != 'debugger', test_runtime_options) 457 458 # List all combinations of options that will be tested. 459 def ListCombinations(args, options): 460 opts_list = [ 461 opt.ArgList(args.__dict__[opt.name]) 462 for opt in options 463 if not opt.test_independently 464 ] 465 return list(itertools.product(*opts_list)) 466 # List combinations of options that should only be tested independently. 467 def ListIndependentCombinations(args, options, base): 468 n = [] 469 for opt in options: 470 if opt.test_independently: 471 for o in opt.ArgList(args.__dict__[opt.name]): 472 n.append(base + (o,)) 473 return n 474 # TODO: We should refine the configurations we test by default, instead of 475 # always testing all possible combinations. 476 test_env_combinations = ListCombinations(args, test_environment_options) 477 test_build_combinations = ListCombinations(args, test_build_options) 478 if not args.fast: 479 test_build_combinations.extend( 480 ListIndependentCombinations(args, 481 test_build_options, 482 test_build_combinations[0])) 483 test_runtime_combinations = ListCombinations(args, test_runtime_options) 484 485 for environment_options in test_env_combinations: 486 for build_options in test_build_combinations: 487 if (args.dry_run): 488 for runtime_options in test_runtime_combinations: 489 print(' '.join(filter(None, environment_options)) + ', ' + 490 ' '.join(filter(None, build_options)) + ', ' + 491 ' '.join(filter(None, runtime_options))) 492 continue 493 494 # Avoid going through the build stage if we are not using the build 495 # result. 496 if not (args.notest and args.nobench): 497 build_rc = BuildAll(build_options, args.jobs) 498 # Don't run the tests for this configuration if the build failed. 499 if build_rc != 0: 500 rc |= build_rc 501 MaybeExitEarly(rc) 502 continue 503 504 # Use the realpath of the test executable so that the commands printed 505 # can be copy-pasted and run. 506 test_executable = util.relrealpath( 507 join(config.dir_build_latest, 'test', 'test-runner')) 508 509 if not args.notest: 510 printer.Print(test_executable) 511 512 for runtime_options in test_runtime_combinations: 513 if not args.notest: 514 runtime_options = [x for x in runtime_options if x is not None] 515 prefix = ' ' + ' '.join(runtime_options) + ' ' 516 rc |= threaded_tests.RunTests(test_executable, 517 args.filters, 518 list(runtime_options), 519 args.under_valgrind, 520 jobs = args.jobs, prefix = prefix) 521 MaybeExitEarly(rc) 522 523 if not args.nobench: 524 rc |= RunBenchmarks(build_options, args) 525 MaybeExitEarly(rc) 526 527 if not args.dry_run: 528 PrintStatus(rc == 0) 529 530 sys.exit(rc) 531