#!/usr/bin/env python # Copyright (c) 2013 The Chromium Authors. All rights reserved. # Use of this source code is governed by a BSD-style license that can be # found in the LICENSE file. import optparse import os import subprocess import sys import time import buildbot_common import build_version import parse_dsc from build_paths import OUT_DIR, SRC_DIR, SDK_SRC_DIR, SCRIPT_DIR sys.path.append(os.path.join(SDK_SRC_DIR, 'tools')) import getos platform = getos.GetPlatform() # TODO(binji): ugly hack -- can I get the browser in a cleaner way? sys.path.append(os.path.join(SRC_DIR, 'chrome', 'test', 'nacl_test_injection')) import find_chrome browser_path = find_chrome.FindChrome(SRC_DIR, ['Debug', 'Release']) pepper_ver = str(int(build_version.ChromeMajorVersion())) pepperdir = os.path.join(OUT_DIR, 'pepper_' + pepper_ver) browser_tester_py = os.path.join(SRC_DIR, 'ppapi', 'native_client', 'tools', 'browser_tester', 'browser_tester.py') ALL_CONFIGS = ['Debug', 'Release'] ALL_TOOLCHAINS = ['newlib', 'glibc', 'pnacl', 'win', 'linux', 'mac'] # Values you can filter by: # name: The name of the test. (e.g. "pi_generator") # config: See ALL_CONFIGS above. # toolchain: See ALL_TOOLCHAINS above. # platform: mac/win/linux. # # All keys must be matched, but any value that matches in a sequence is # considered a match for that key. For example: # # {'name': ('pi_generator', 'input_event'), 'toolchain': ('newlib', 'pnacl')} # # Will match 8 tests: # pi_generator.newlib_debug_test # pi_generator.newlib_release_test # input_event.newlib_debug_test # input_event.newlib_release_test # pi_generator.glibc_debug_test # pi_generator.glibc_release_test # input_event.glibc_debug_test # input_event.glibc_release_test DISABLED_TESTS = [ # TODO(binji): Disable 3D examples on linux/win. See # http://crbug.com/262379. {'name': 'graphics_3d', 'platform': ('win', 'linux')}, # TODO(binji): These tests timeout on the trybots because the NEXEs take # more than 40 seconds to load (!). See http://crbug.com/280753 {'name': 'nacl_io_test', 'platform': 'win', 'toolchain': 'glibc'}, ] def ValidateToolchains(toolchains): invalid_toolchains = set(toolchains) - set(ALL_TOOLCHAINS) if invalid_toolchains: buildbot_common.ErrorExit('Invalid toolchain(s): %s' % ( ', '.join(invalid_toolchains))) def GetServingDirForProject(desc): dest = desc['DEST'] path = os.path.join(pepperdir, *dest.split('/')) return os.path.join(path, desc['NAME']) def GetRepoServingDirForProject(desc): # This differs from GetServingDirForProject, because it returns the location # within the Chrome repository of the project, not the "pepperdir". return os.path.dirname(desc['FILEPATH']) def GetExecutableDirForProject(desc, toolchain, config): return os.path.join(GetServingDirForProject(desc), toolchain, config) def GetBrowserTesterCommand(desc, toolchain, config): if browser_path is None: buildbot_common.ErrorExit('Failed to find chrome browser using FindChrome.') args = [ sys.executable, browser_tester_py, '--browser_path', browser_path, '--timeout', '30.0', # seconds # Prevent the infobar that shows up when requesting filesystem quota. '--browser_flag', '--unlimited-storage', '--enable_sockets', ] args.extend(['--serving_dir', GetServingDirForProject(desc)]) # Fall back on the example directory in the Chromium repo, to find test.js. args.extend(['--serving_dir', GetRepoServingDirForProject(desc)]) # If it is not found there, fall back on the dummy one (in this directory.) args.extend(['--serving_dir', SCRIPT_DIR]) if toolchain == platform: exe_dir = GetExecutableDirForProject(desc, toolchain, config) ppapi_plugin = os.path.join(exe_dir, desc['NAME']) if platform == 'win': ppapi_plugin += '.dll' else: ppapi_plugin += '.so' args.extend(['--ppapi_plugin', ppapi_plugin]) ppapi_plugin_mimetype = 'application/x-ppapi-%s' % config.lower() args.extend(['--ppapi_plugin_mimetype', ppapi_plugin_mimetype]) if toolchain == 'pnacl': args.extend(['--browser_flag', '--enable-pnacl']) url = 'index.html' url += '?tc=%s&config=%s&test=true' % (toolchain, config) args.extend(['--url', url]) return args def GetBrowserTesterEnv(): # browser_tester imports tools/valgrind/memcheck_analyze, which imports # tools/valgrind/common. Well, it tries to, anyway, but instead imports # common from PYTHONPATH first (which on the buildbots, is a # common/__init__.py file...). # # Clear the PYTHONPATH so it imports the correct file. env = dict(os.environ) env['PYTHONPATH'] = '' return env def RunTestOnce(desc, toolchain, config): args = GetBrowserTesterCommand(desc, toolchain, config) env = GetBrowserTesterEnv() start_time = time.time() try: subprocess.check_call(args, env=env) result = True except subprocess.CalledProcessError: result = False elapsed = (time.time() - start_time) * 1000 return result, elapsed def RunTestNTimes(desc, toolchain, config, times): total_elapsed = 0 for _ in xrange(times): result, elapsed = RunTestOnce(desc, toolchain, config) total_elapsed += elapsed if result: # Success, stop retrying. break return result, total_elapsed def RunTestWithGtestOutput(desc, toolchain, config, retry_on_failure_times): test_name = GetTestName(desc, toolchain, config) WriteGtestHeader(test_name) result, elapsed = RunTestNTimes(desc, toolchain, config, retry_on_failure_times) WriteGtestFooter(result, test_name, elapsed) return result def WriteGtestHeader(test_name): print '\n[ RUN ] %s' % test_name sys.stdout.flush() sys.stderr.flush() def WriteGtestFooter(success, test_name, elapsed): sys.stdout.flush() sys.stderr.flush() if success: message = '[ OK ]' else: message = '[ FAILED ]' print '%s %s (%d ms)' % (message, test_name, elapsed) def GetTestName(desc, toolchain, config): return '%s.%s_%s_test' % (desc['NAME'], toolchain, config.lower()) def IsTestDisabled(desc, toolchain, config): def AsList(value): if type(value) not in (list, tuple): return [value] return value def TestMatchesDisabled(test_values, disabled_test): for key in test_values: if key in disabled_test: if test_values[key] not in AsList(disabled_test[key]): return False return True test_values = { 'name': desc['NAME'], 'toolchain': toolchain, 'config': config, 'platform': platform } for disabled_test in DISABLED_TESTS: if TestMatchesDisabled(test_values, disabled_test): return True return False def WriteHorizontalBar(): print '-' * 80 def WriteBanner(message): WriteHorizontalBar() print message WriteHorizontalBar() def RunAllTestsInTree(tree, toolchains, configs, retry_on_failure_times): tests_run = 0 total_tests = 0 failed = [] disabled = [] for _, desc in parse_dsc.GenerateProjects(tree): desc_configs = desc.get('CONFIGS', ALL_CONFIGS) valid_toolchains = set(toolchains) & set(desc['TOOLS']) valid_configs = set(configs) & set(desc_configs) for toolchain in sorted(valid_toolchains): for config in sorted(valid_configs): test_name = GetTestName(desc, toolchain, config) total_tests += 1 if IsTestDisabled(desc, toolchain, config): disabled.append(test_name) continue tests_run += 1 success = RunTestWithGtestOutput(desc, toolchain, config, retry_on_failure_times) if not success: failed.append(test_name) if failed: WriteBanner('FAILED TESTS') for test in failed: print ' %s failed.' % test if disabled: WriteBanner('DISABLED TESTS') for test in disabled: print ' %s disabled.' % test WriteHorizontalBar() print 'Tests run: %d/%d (%d disabled).' % ( tests_run, total_tests, len(disabled)) print 'Tests succeeded: %d/%d.' % (tests_run - len(failed), tests_run) success = len(failed) != 0 return success def GetProjectTree(include): # Everything in src is a library, and cannot be run. exclude = {'DEST': 'src'} try: return parse_dsc.LoadProjectTree(SDK_SRC_DIR, include=include, exclude=exclude) except parse_dsc.ValidationError as e: buildbot_common.ErrorExit(str(e)) def main(args): parser = optparse.OptionParser() parser.add_option('-c', '--config', help='Choose configuration to run (Debug or Release). Runs both ' 'by default', action='append') parser.add_option('-x', '--experimental', help='Run experimental projects', action='store_true') parser.add_option('-t', '--toolchain', help='Run using toolchain. Can be passed more than once.', action='append', default=[]) parser.add_option('-d', '--dest', help='Select which destinations (project types) are valid.', action='append') parser.add_option('-p', '--project', help='Select which projects are valid.', action='append') parser.add_option('--retry-times', help='Number of types to retry on failure (Default: %default)', type='int', default=1) options, args = parser.parse_args(args[1:]) if options.project: parser.error('The -p/--project option is deprecated.\n' 'Just use positional paramaters instead.') if not options.toolchain: options.toolchain = ['newlib', 'glibc', 'pnacl', 'host'] if 'host' in options.toolchain: options.toolchain.remove('host') options.toolchain.append(platform) print 'Adding platform: ' + platform ValidateToolchains(options.toolchain) include = {} if options.toolchain: include['TOOLS'] = options.toolchain print 'Filter by toolchain: ' + str(options.toolchain) if not options.experimental: include['EXPERIMENTAL'] = False if options.dest: include['DEST'] = options.dest print 'Filter by type: ' + str(options.dest) if args: include['NAME'] = args print 'Filter by name: ' + str(args) if not options.config: options.config = ALL_CONFIGS project_tree = GetProjectTree(include) return RunAllTestsInTree(project_tree, options.toolchain, options.config, options.retry_times) if __name__ == '__main__': script_name = os.path.basename(sys.argv[0]) try: sys.exit(main(sys.argv)) except parse_dsc.ValidationError as e: buildbot_common.ErrorExit('%s: %s' % (script_name, e)) except KeyboardInterrupt: buildbot_common.ErrorExit('%s: interrupted' % script_name)