1# Copyright 2022 The Chromium Authors 2# Use of this source code is governed by a BSD-style license that can be 3# found in the LICENSE file. 4 5import argparse 6import json 7import os 8import subprocess 9import sys 10import time 11import typing 12 13CHROMIUM_SRC_DIR = os.path.realpath( 14 os.path.join(os.path.abspath(os.path.dirname(__file__)), '..', '..')) 15 16sys.path.append(os.path.join(CHROMIUM_SRC_DIR, 'build', 'util')) 17 18# pylint: disable=wrong-import-position 19from lib.results import result_sink 20from lib.results import result_types 21 22# pylint: disable=wrong-import-position 23 24 25# pylint: disable=too-many-arguments 26def report_results(test_name: str, 27 test_location: str, 28 status: str, 29 duration: float, 30 log: str, 31 output_file: typing.Optional[str], 32 sink_client: typing.Optional[result_sink.ResultSinkClient], 33 failure_reason: typing.Optional[str] = None) -> None: 34 """Report results on bots. 35 36 Args: 37 test_name: The name of the test to report. 38 test_location: The Chromium src-relative path (starting with //) of the 39 test file that will be reported in results. Usually the path to 40 whatever script is calling this function. 41 status: A string containing the test status. 42 duration: An float containing the test duration in seconds. 43 log: A string containing the log output of the test. 44 output_dir: An optional string containing a path to a file to output 45 JSON to. 46 sink_client: An optional client for reporting results to ResultDB. 47 failure_reason: An optional string containing a reason why the test 48 failed. 49 """ 50 if output_file: 51 report_json_results(output_file) 52 if sink_client: 53 sink_client.Post(test_id=test_name, 54 status=status, 55 duration=(duration * 1000), 56 test_log=log, 57 test_file=test_location, 58 failure_reason=failure_reason) 59 60 61# pylint: enable=too-many-arguments 62 63 64def report_json_results(output_file: str) -> None: 65 """'Report' results on bots. 66 67 Actually just writes an empty JSON object to a file since all we need to 68 do is make the merge scripts happy. 69 70 Args: 71 output_dir: An optional string containing a path to a file to output 72 JSON to. 73 """ 74 with open(output_file, 'w') as outfile: 75 json.dump({}, outfile) 76 77 78def parse_args() -> argparse.Namespace: 79 parser = argparse.ArgumentParser() 80 parser.add_argument('--isolated-script-test-output', 81 dest='output_file', 82 help=('Path to JSON output file.')) 83 84 args, _ = parser.parse_known_args() 85 return args 86 87 88def run_pytype(test_name: str, test_location: str, 89 files_to_check: typing.Iterable[str], 90 python_paths: typing.Iterable[str], cwd: str) -> int: 91 """Runs pytype on a given list of files/directories. 92 93 Args: 94 test_name: The name of the test that will be reported in results. 95 test_location: The Chromium src-relative path (starting with //) of the 96 test file that will be reported in results. Usually the path to 97 whatever script is calling this function. 98 files_to_check: Files and directories to run pytype on as absolute 99 paths. 100 python_paths: Any paths that should be set as PYTHONPATH when running 101 pytype. 102 cwd: The directory that pytype should be run from. 103 104 Returns: 105 0 on success, non-zero on failure. 106 """ 107 sink_client = result_sink.TryInitClient() 108 args = parse_args() 109 110 if sys.platform != 'linux': 111 print('pytype is currently only supported on Linux, see ' 112 'https://github.com/google/pytype/issues/1154') 113 report_results(test_name, test_location, result_types.SKIP, 0, 114 'Skipped due to unsupported platform.', 115 args.output_file, sink_client) 116 return 0 117 118 # Strangely, pytype won't complain if you tell it to analyze a directory 119 # that 120 # doesn't exist, which could potentially lead to code not being analyzed if 121 # it's added here but not added to the isolate. So, ensure that everything 122 # we expect to analyze actually exists. 123 for f in files_to_check: 124 if not os.path.exists(f): 125 raise RuntimeError( 126 'Requested file or directory %s does not exist.' % f) 127 128 # pytype looks for a 'python' or 'python3' executable in PATH, so make sure 129 # that the Python 3 executable from vpython is in the path. 130 executable_dir = os.path.dirname(sys.executable) 131 os.environ['PATH'] = executable_dir + os.pathsep + os.environ['PATH'] 132 133 # pytype specifies that the provided PYTHONPATH is :-separated. 134 pythonpath = ':'.join(python_paths) 135 pytype_cmd = [ 136 sys.executable, 137 '-m', 138 'pytype', 139 '--pythonpath', 140 pythonpath, 141 '--keep-going', 142 '--jobs', 143 'auto', 144 ] 145 pytype_cmd.extend(files_to_check) 146 147 if sink_client: 148 stdout_handle = subprocess.PIPE 149 stderr_handle = subprocess.STDOUT 150 else: 151 stdout_handle = None 152 stderr_handle = None 153 154 start_time = time.time() 155 try: 156 proc = subprocess.run(pytype_cmd, 157 check=True, 158 cwd=cwd, 159 stdout=stdout_handle, 160 stderr=stderr_handle, 161 text=True) 162 stdout = proc.stdout 163 status = result_types.PASS 164 failure_reason = None 165 except subprocess.CalledProcessError as e: 166 stdout = e.stdout 167 status = result_types.FAIL 168 failure_reason = 'Checking Python 3 type hinting failed.' 169 duration = (time.time() - start_time) 170 171 if stdout: 172 print(stdout) 173 report_results(test_name, test_location, status, duration, stdout or '', 174 args.output_file, sink_client, failure_reason) 175 176 if status == result_types.FAIL: 177 return 1 178 return 0 179