| # Copyright 2014 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 argparse | 
 | import contextlib | 
 | import io | 
 | import json | 
 | import os | 
 | import logging | 
 | import subprocess | 
 | import sys | 
 | import tempfile | 
 | import time | 
 |  | 
 |  | 
 | SCRIPT_DIR = os.path.abspath(os.path.dirname(__file__)) | 
 | SRC_DIR = os.path.abspath( | 
 |     os.path.join(SCRIPT_DIR, os.path.pardir, os.path.pardir)) | 
 |  | 
 |  | 
 | # run_web_tests.py returns the number of failures as the return | 
 | # code, but caps the return code at 101 to avoid overflow or colliding | 
 | # with reserved values from the shell. | 
 | MAX_FAILURES_EXIT_STATUS = 101 | 
 |  | 
 |  | 
 | # Exit code to indicate infrastructure issue. | 
 | INFRA_FAILURE_EXIT_CODE = 87 | 
 |  | 
 |  | 
 | def run_script(argv, funcs): | 
 |   def parse_json(path): | 
 |     with open(path) as f: | 
 |       return json.load(f) | 
 |   parser = argparse.ArgumentParser() | 
 |   # TODO(phajdan.jr): Make build-config-fs required after passing it in recipe. | 
 |   parser.add_argument('--build-config-fs') | 
 |   parser.add_argument('--paths', type=parse_json, default={}) | 
 |   # Properties describe the environment of the build, and are the same per | 
 |   # script invocation. | 
 |   parser.add_argument('--properties', type=parse_json, default={}) | 
 |   # Args contains per-invocation arguments that potentially change the | 
 |   # behavior of the script. | 
 |   parser.add_argument('--args', type=parse_json, default=[]) | 
 |  | 
 |   parser.add_argument( | 
 |       '--use-src-side-runtest-py', action='store_true', | 
 |       help='Use the src-side copy of runtest.py, as opposed to the build-side ' | 
 |            'one') | 
 |  | 
 |   subparsers = parser.add_subparsers() | 
 |  | 
 |   run_parser = subparsers.add_parser('run') | 
 |   run_parser.add_argument( | 
 |       '--output', type=argparse.FileType('w'), required=True) | 
 |   run_parser.add_argument('--filter-file', type=argparse.FileType('r')) | 
 |   run_parser.set_defaults(func=funcs['run']) | 
 |  | 
 |   run_parser = subparsers.add_parser('compile_targets') | 
 |   run_parser.add_argument( | 
 |       '--output', type=argparse.FileType('w'), required=True) | 
 |   run_parser.set_defaults(func=funcs['compile_targets']) | 
 |  | 
 |   args = parser.parse_args(argv) | 
 |   return args.func(args) | 
 |  | 
 |  | 
 | def run_command(argv, env=None, cwd=None): | 
 |   print 'Running %r in %r (env: %r)' % (argv, cwd, env) | 
 |   rc = subprocess.call(argv, env=env, cwd=cwd) | 
 |   print 'Command %r returned exit code %d' % (argv, rc) | 
 |   return rc | 
 |  | 
 |  | 
 | def run_runtest(cmd_args, runtest_args): | 
 |   env = os.environ.copy() | 
 |   env['CHROME_HEADLESS'] = '1' | 
 |  | 
 |   if cmd_args.use_src_side_runtest_py: | 
 |     cmd = [ | 
 |       sys.executable, | 
 |       os.path.join( | 
 |           cmd_args.paths['checkout'], 'infra', 'scripts', 'runtest_wrapper.py'), | 
 |       '--', | 
 |     ] | 
 |   else: | 
 |     cmd = [ | 
 |       sys.executable, | 
 |       cmd_args.paths['runit.py'], | 
 |       '--show-path', | 
 |       '--with-third-party-lib', | 
 |       sys.executable, | 
 |       cmd_args.paths['runtest.py'], | 
 |     ] | 
 |   return run_command(cmd + [ | 
 |       '--target', cmd_args.build_config_fs, | 
 |       '--xvfb', | 
 |       '--builder-name', cmd_args.properties['buildername'], | 
 |       '--slave-name', cmd_args.properties['slavename'], | 
 |       '--build-number', str(cmd_args.properties['buildnumber']), | 
 |       '--build-properties', json.dumps(cmd_args.properties), | 
 |   ] + runtest_args, env=env) | 
 |  | 
 |  | 
 | @contextlib.contextmanager | 
 | def temporary_file(): | 
 |   fd, path = tempfile.mkstemp() | 
 |   os.close(fd) | 
 |   try: | 
 |     yield path | 
 |   finally: | 
 |     os.remove(path) | 
 |  | 
 |  | 
 | def parse_common_test_results(json_results, test_separator='/'): | 
 |   def convert_trie_to_flat_paths(trie, prefix=None): | 
 |     # Also see blinkpy.web_tests.layout_package.json_results_generator | 
 |     result = {} | 
 |     for name, data in trie.iteritems(): | 
 |       if prefix: | 
 |         name = prefix + test_separator + name | 
 |       if len(data) and not 'actual' in data and not 'expected' in data: | 
 |         result.update(convert_trie_to_flat_paths(data, name)) | 
 |       else: | 
 |         result[name] = data | 
 |     return result | 
 |  | 
 |   results = { | 
 |     'passes': {}, | 
 |     'unexpected_passes': {}, | 
 |     'failures': {}, | 
 |     'unexpected_failures': {}, | 
 |     'flakes': {}, | 
 |     'unexpected_flakes': {}, | 
 |   } | 
 |  | 
 |   # TODO(dpranke): crbug.com/357866 - we should simplify the handling of | 
 |   # both the return code and parsing the actual results, below. | 
 |  | 
 |   passing_statuses = ('PASS', 'SLOW', 'NEEDSREBASELINE', | 
 |                         'NEEDSMANUALREBASELINE') | 
 |  | 
 |   for test, result in convert_trie_to_flat_paths( | 
 |       json_results['tests']).iteritems(): | 
 |     key = 'unexpected_' if result.get('is_unexpected') else '' | 
 |     data = result['actual'] | 
 |     actual_results = data.split() | 
 |     last_result = actual_results[-1] | 
 |     expected_results = result['expected'].split() | 
 |  | 
 |     if (len(actual_results) > 1 and | 
 |         (last_result in expected_results or last_result in passing_statuses)): | 
 |       key += 'flakes' | 
 |     elif last_result in passing_statuses: | 
 |       key += 'passes' | 
 |       # TODO(dpranke): crbug.com/357867 ...  Why are we assigning result | 
 |       # instead of actual_result here. Do we even need these things to be | 
 |       # hashes, or just lists? | 
 |       data = result | 
 |     else: | 
 |       key += 'failures' | 
 |     results[key][test] = data | 
 |  | 
 |   return results | 
 |  | 
 |  | 
 | def extract_filter_list(filter_list): | 
 |   """Helper for isolated script test wrappers. Parses the | 
 |   --isolated-script-test-filter command line argument. Currently, double-colon | 
 |   ('::') is used as the separator between test names, because a single colon may | 
 |   be used in the names of perf benchmarks, which contain URLs. | 
 |   """ | 
 |   return filter_list.split('::') | 
 |  | 
 |  | 
 | def run_integration_test(script_to_run, extra_args, log_file, output): | 
 |   integration_test_res = subprocess.call( | 
 |       [sys.executable, script_to_run] + extra_args) | 
 |  | 
 |   with open(log_file) as f: | 
 |     failures = json.load(f) | 
 |   json.dump({ | 
 |       'valid': integration_test_res == 0, | 
 |       'failures': failures, | 
 |   }, output) | 
 |  | 
 |   return integration_test_res |