Siddharth Shukla | 8e64d90 | 2017-03-12 19:50:18 +0100 | [diff] [blame] | 1 | #!/usr/bin/env python |
Jan Tattermusch | 7897ae9 | 2017-06-07 22:57:36 +0200 | [diff] [blame] | 2 | # Copyright 2016 gRPC authors. |
Adele Zhou | bf3b769 | 2016-08-11 18:45:18 -0700 | [diff] [blame] | 3 | # |
Jan Tattermusch | 7897ae9 | 2017-06-07 22:57:36 +0200 | [diff] [blame] | 4 | # Licensed under the Apache License, Version 2.0 (the "License"); |
| 5 | # you may not use this file except in compliance with the License. |
| 6 | # You may obtain a copy of the License at |
Adele Zhou | bf3b769 | 2016-08-11 18:45:18 -0700 | [diff] [blame] | 7 | # |
Jan Tattermusch | 7897ae9 | 2017-06-07 22:57:36 +0200 | [diff] [blame] | 8 | # http://www.apache.org/licenses/LICENSE-2.0 |
Adele Zhou | bf3b769 | 2016-08-11 18:45:18 -0700 | [diff] [blame] | 9 | # |
Jan Tattermusch | 7897ae9 | 2017-06-07 22:57:36 +0200 | [diff] [blame] | 10 | # Unless required by applicable law or agreed to in writing, software |
| 11 | # distributed under the License is distributed on an "AS IS" BASIS, |
| 12 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. |
| 13 | # See the License for the specific language governing permissions and |
| 14 | # limitations under the License. |
Adele Zhou | bf3b769 | 2016-08-11 18:45:18 -0700 | [diff] [blame] | 15 | """Tool to get build statistics from Jenkins and upload to BigQuery.""" |
| 16 | |
Siddharth Shukla | d194f59 | 2017-03-11 19:12:43 +0100 | [diff] [blame] | 17 | from __future__ import print_function |
| 18 | |
Adele Zhou | bf3b769 | 2016-08-11 18:45:18 -0700 | [diff] [blame] | 19 | import argparse |
| 20 | import jenkinsapi |
| 21 | from jenkinsapi.custom_exceptions import JenkinsAPIException |
| 22 | from jenkinsapi.jenkins import Jenkins |
| 23 | import json |
| 24 | import os |
| 25 | import re |
| 26 | import sys |
| 27 | import urllib |
| 28 | |
ncteisen | 888093c | 2017-12-11 18:00:40 -0800 | [diff] [blame] | 29 | gcp_utils_dir = os.path.abspath( |
| 30 | os.path.join(os.path.dirname(__file__), '../gcp/utils')) |
Adele Zhou | bf3b769 | 2016-08-11 18:45:18 -0700 | [diff] [blame] | 31 | sys.path.append(gcp_utils_dir) |
| 32 | import big_query_utils |
| 33 | |
Adele Zhou | bf3b769 | 2016-08-11 18:45:18 -0700 | [diff] [blame] | 34 | _PROJECT_ID = 'grpc-testing' |
| 35 | _HAS_MATRIX = True |
ncteisen | 888093c | 2017-12-11 18:00:40 -0800 | [diff] [blame] | 36 | _BUILDS = { |
| 37 | 'gRPC_interop_master': not _HAS_MATRIX, |
| 38 | 'gRPC_master_linux': not _HAS_MATRIX, |
| 39 | 'gRPC_master_macos': not _HAS_MATRIX, |
| 40 | 'gRPC_master_windows': not _HAS_MATRIX, |
| 41 | 'gRPC_performance_master': not _HAS_MATRIX, |
| 42 | 'gRPC_portability_master_linux': not _HAS_MATRIX, |
| 43 | 'gRPC_portability_master_windows': not _HAS_MATRIX, |
| 44 | 'gRPC_master_asanitizer_c': not _HAS_MATRIX, |
| 45 | 'gRPC_master_asanitizer_cpp': not _HAS_MATRIX, |
| 46 | 'gRPC_master_msan_c': not _HAS_MATRIX, |
| 47 | 'gRPC_master_tsanitizer_c': not _HAS_MATRIX, |
| 48 | 'gRPC_master_tsan_cpp': not _HAS_MATRIX, |
| 49 | 'gRPC_interop_pull_requests': not _HAS_MATRIX, |
| 50 | 'gRPC_performance_pull_requests': not _HAS_MATRIX, |
| 51 | 'gRPC_portability_pull_requests_linux': not _HAS_MATRIX, |
| 52 | 'gRPC_portability_pr_win': not _HAS_MATRIX, |
| 53 | 'gRPC_pull_requests_linux': not _HAS_MATRIX, |
| 54 | 'gRPC_pull_requests_macos': not _HAS_MATRIX, |
| 55 | 'gRPC_pr_win': not _HAS_MATRIX, |
| 56 | 'gRPC_pull_requests_asan_c': not _HAS_MATRIX, |
| 57 | 'gRPC_pull_requests_asan_cpp': not _HAS_MATRIX, |
| 58 | 'gRPC_pull_requests_msan_c': not _HAS_MATRIX, |
| 59 | 'gRPC_pull_requests_tsan_c': not _HAS_MATRIX, |
| 60 | 'gRPC_pull_requests_tsan_cpp': not _HAS_MATRIX, |
Adele Zhou | bf3b769 | 2016-08-11 18:45:18 -0700 | [diff] [blame] | 61 | } |
| 62 | _URL_BASE = 'https://grpc-testing.appspot.com/job' |
Adele Zhou | 445534e | 2016-09-06 17:57:11 -0700 | [diff] [blame] | 63 | |
Mehrdad Afshari | 87cd994 | 2018-01-02 14:40:00 -0800 | [diff] [blame] | 64 | # This is a dynamic list where known and active issues should be added. |
Adele Zhou | 445534e | 2016-09-06 17:57:11 -0700 | [diff] [blame] | 65 | # Fixed ones should be removed. |
| 66 | # Also try not to add multiple messages from the same failure. |
Adele Zhou | bf3b769 | 2016-08-11 18:45:18 -0700 | [diff] [blame] | 67 | _KNOWN_ERRORS = [ |
| 68 | 'Failed to build workspace Tests with scheme AllTests', |
| 69 | 'Build timed out', |
Adele Zhou | 859d2a4 | 2016-09-16 15:15:44 -0700 | [diff] [blame] | 70 | 'TIMEOUT: tools/run_tests/pre_build_node.sh', |
| 71 | 'TIMEOUT: tools/run_tests/pre_build_ruby.sh', |
Adele Zhou | bf3b769 | 2016-08-11 18:45:18 -0700 | [diff] [blame] | 72 | 'FATAL: Unable to produce a script file', |
Adele Zhou | 107e51e | 2016-09-08 11:37:30 -0700 | [diff] [blame] | 73 | 'FAILED: build_docker_c\+\+', |
Adele Zhou | 445534e | 2016-09-06 17:57:11 -0700 | [diff] [blame] | 74 | 'cannot find package \"cloud.google.com/go/compute/metadata\"', |
Adele Zhou | bf3b769 | 2016-08-11 18:45:18 -0700 | [diff] [blame] | 75 | 'LLVM ERROR: IO failure on output stream.', |
| 76 | 'MSBUILD : error MSB1009: Project file does not exist.', |
Adele Zhou | 445534e | 2016-09-06 17:57:11 -0700 | [diff] [blame] | 77 | 'fatal: git fetch_pack: expected ACK/NAK', |
| 78 | 'Failed to fetch from http://github.com/grpc/grpc.git', |
| 79 | ('hudson.remoting.RemotingSystemException: java.io.IOException: ' |
| 80 | 'Backing channel is disconnected.'), |
Adele Zhou | 107e51e | 2016-09-08 11:37:30 -0700 | [diff] [blame] | 81 | 'hudson.remoting.ChannelClosedException', |
Adele Zhou | 59af64b | 2016-09-22 15:23:54 -0700 | [diff] [blame] | 82 | 'Could not initialize class hudson.Util', |
| 83 | 'Too many open files in system', |
Adele Zhou | 445534e | 2016-09-06 17:57:11 -0700 | [diff] [blame] | 84 | 'FAILED: bins/tsan/qps_openloop_test GRPC_POLL_STRATEGY=epoll', |
| 85 | 'FAILED: bins/tsan/qps_openloop_test GRPC_POLL_STRATEGY=legacy', |
| 86 | 'FAILED: bins/tsan/qps_openloop_test GRPC_POLL_STRATEGY=poll', |
| 87 | ('tests.bins/asan/h2_proxy_test streaming_error_response ' |
| 88 | 'GRPC_POLL_STRATEGY=legacy'), |
Adele Zhou | 566c4c0 | 2017-08-10 15:51:47 -0700 | [diff] [blame] | 89 | 'hudson.plugins.git.GitException', |
| 90 | 'Couldn\'t find any revision to build', |
| 91 | 'org.jenkinsci.plugin.Diskcheck.preCheckout', |
| 92 | 'Something went wrong while deleting Files', |
Adele Zhou | bf3b769 | 2016-08-11 18:45:18 -0700 | [diff] [blame] | 93 | ] |
Adele Zhou | c210147 | 2017-08-09 17:04:27 -0700 | [diff] [blame] | 94 | _NO_REPORT_FILES_FOUND_ERROR = 'No test report files were found.' |
Adele Zhou | bf3b769 | 2016-08-11 18:45:18 -0700 | [diff] [blame] | 95 | _UNKNOWN_ERROR = 'Unknown error' |
| 96 | _DATASET_ID = 'build_statistics' |
| 97 | |
| 98 | |
| 99 | def _scrape_for_known_errors(html): |
ncteisen | 888093c | 2017-12-11 18:00:40 -0800 | [diff] [blame] | 100 | error_list = [] |
| 101 | for known_error in _KNOWN_ERRORS: |
| 102 | errors = re.findall(known_error, html) |
| 103 | this_error_count = len(errors) |
| 104 | if this_error_count > 0: |
| 105 | error_list.append({ |
| 106 | 'description': known_error, |
| 107 | 'count': this_error_count |
| 108 | }) |
Mehrdad Afshari | 87cd994 | 2018-01-02 14:40:00 -0800 | [diff] [blame] | 109 | print('====> %d failures due to %s' % (this_error_count, |
| 110 | known_error)) |
ncteisen | 888093c | 2017-12-11 18:00:40 -0800 | [diff] [blame] | 111 | return error_list |
Adele Zhou | bf3b769 | 2016-08-11 18:45:18 -0700 | [diff] [blame] | 112 | |
| 113 | |
Matt Kwong | 983c2da | 2016-11-10 15:07:59 -0800 | [diff] [blame] | 114 | def _no_report_files_found(html): |
ncteisen | 888093c | 2017-12-11 18:00:40 -0800 | [diff] [blame] | 115 | return _NO_REPORT_FILES_FOUND_ERROR in html |
Matt Kwong | 983c2da | 2016-11-10 15:07:59 -0800 | [diff] [blame] | 116 | |
| 117 | |
Adele Zhou | bf3b769 | 2016-08-11 18:45:18 -0700 | [diff] [blame] | 118 | def _get_last_processed_buildnumber(build_name): |
Mehrdad Afshari | 87cd994 | 2018-01-02 14:40:00 -0800 | [diff] [blame] | 119 | query = 'SELECT max(build_number) FROM [%s:%s.%s];' % (_PROJECT_ID, |
| 120 | _DATASET_ID, |
| 121 | build_name) |
ncteisen | 888093c | 2017-12-11 18:00:40 -0800 | [diff] [blame] | 122 | query_job = big_query_utils.sync_query_job(bq, _PROJECT_ID, query) |
| 123 | page = bq.jobs().getQueryResults( |
| 124 | pageToken=None, **query_job['jobReference']).execute(num_retries=3) |
| 125 | if page['rows'][0]['f'][0]['v']: |
| 126 | return int(page['rows'][0]['f'][0]['v']) |
| 127 | return 0 |
Adele Zhou | bf3b769 | 2016-08-11 18:45:18 -0700 | [diff] [blame] | 128 | |
| 129 | |
| 130 | def _process_matrix(build, url_base): |
ncteisen | 888093c | 2017-12-11 18:00:40 -0800 | [diff] [blame] | 131 | matrix_list = [] |
| 132 | for matrix in build.get_matrix_runs(): |
| 133 | matrix_str = re.match('.*\\xc2\\xbb ((?:[^,]+,?)+) #.*', |
| 134 | matrix.name).groups()[0] |
| 135 | matrix_tuple = matrix_str.split(',') |
| 136 | json_url = '%s/config=%s,language=%s,platform=%s/testReport/api/json' % ( |
| 137 | url_base, matrix_tuple[0], matrix_tuple[1], matrix_tuple[2]) |
| 138 | console_url = '%s/config=%s,language=%s,platform=%s/consoleFull' % ( |
| 139 | url_base, matrix_tuple[0], matrix_tuple[1], matrix_tuple[2]) |
| 140 | matrix_dict = { |
| 141 | 'name': matrix_str, |
| 142 | 'duration': matrix.get_duration().total_seconds() |
| 143 | } |
| 144 | matrix_dict.update(_process_build(json_url, console_url)) |
| 145 | matrix_list.append(matrix_dict) |
Adele Zhou | bf3b769 | 2016-08-11 18:45:18 -0700 | [diff] [blame] | 146 | |
ncteisen | 888093c | 2017-12-11 18:00:40 -0800 | [diff] [blame] | 147 | return matrix_list |
Adele Zhou | bf3b769 | 2016-08-11 18:45:18 -0700 | [diff] [blame] | 148 | |
| 149 | |
| 150 | def _process_build(json_url, console_url): |
ncteisen | 888093c | 2017-12-11 18:00:40 -0800 | [diff] [blame] | 151 | build_result = {} |
| 152 | error_list = [] |
| 153 | try: |
| 154 | html = urllib.urlopen(json_url).read() |
| 155 | test_result = json.loads(html) |
| 156 | print('====> Parsing result from %s' % json_url) |
| 157 | failure_count = test_result['failCount'] |
| 158 | build_result['pass_count'] = test_result['passCount'] |
| 159 | build_result['failure_count'] = failure_count |
| 160 | # This means Jenkins failure occurred. |
| 161 | build_result['no_report_files_found'] = _no_report_files_found(html) |
| 162 | # Only check errors if Jenkins failure occurred. |
| 163 | if build_result['no_report_files_found']: |
| 164 | error_list = _scrape_for_known_errors(html) |
| 165 | except Exception as e: |
| 166 | print('====> Got exception for %s: %s.' % (json_url, str(e))) |
| 167 | print('====> Parsing errors from %s.' % console_url) |
| 168 | html = urllib.urlopen(console_url).read() |
| 169 | build_result['pass_count'] = 0 |
| 170 | build_result['failure_count'] = 1 |
Mehrdad Afshari | 87cd994 | 2018-01-02 14:40:00 -0800 | [diff] [blame] | 171 | # In this case, the string doesn't exist in the result html but the fact |
| 172 | # that we fail to parse the result html indicates Jenkins failure and hence |
ncteisen | 888093c | 2017-12-11 18:00:40 -0800 | [diff] [blame] | 173 | # no report files were generated. |
| 174 | build_result['no_report_files_found'] = True |
| 175 | error_list = _scrape_for_known_errors(html) |
Adele Zhou | c210147 | 2017-08-09 17:04:27 -0700 | [diff] [blame] | 176 | |
ncteisen | 888093c | 2017-12-11 18:00:40 -0800 | [diff] [blame] | 177 | if error_list: |
| 178 | build_result['error'] = error_list |
| 179 | elif build_result['no_report_files_found']: |
| 180 | build_result['error'] = [{'description': _UNKNOWN_ERROR, 'count': 1}] |
| 181 | else: |
| 182 | build_result['error'] = [{'description': '', 'count': 0}] |
Adele Zhou | bf3b769 | 2016-08-11 18:45:18 -0700 | [diff] [blame] | 183 | |
ncteisen | 888093c | 2017-12-11 18:00:40 -0800 | [diff] [blame] | 184 | return build_result |
Adele Zhou | bf3b769 | 2016-08-11 18:45:18 -0700 | [diff] [blame] | 185 | |
| 186 | |
| 187 | # parse command line |
| 188 | argp = argparse.ArgumentParser(description='Get build statistics.') |
| 189 | argp.add_argument('-u', '--username', default='jenkins') |
ncteisen | 888093c | 2017-12-11 18:00:40 -0800 | [diff] [blame] | 190 | argp.add_argument( |
| 191 | '-b', |
| 192 | '--builds', |
| 193 | choices=['all'] + sorted(_BUILDS.keys()), |
| 194 | nargs='+', |
| 195 | default=['all']) |
Adele Zhou | bf3b769 | 2016-08-11 18:45:18 -0700 | [diff] [blame] | 196 | args = argp.parse_args() |
| 197 | |
| 198 | J = Jenkins('https://grpc-testing.appspot.com', args.username, 'apiToken') |
| 199 | bq = big_query_utils.create_big_query() |
| 200 | |
| 201 | for build_name in _BUILDS.keys() if 'all' in args.builds else args.builds: |
ncteisen | 888093c | 2017-12-11 18:00:40 -0800 | [diff] [blame] | 202 | print('====> Build: %s' % build_name) |
| 203 | # Since get_last_completed_build() always fails due to malformatted string |
| 204 | # error, we use get_build_metadata() instead. |
| 205 | job = None |
Adele Zhou | bf3b769 | 2016-08-11 18:45:18 -0700 | [diff] [blame] | 206 | try: |
ncteisen | 888093c | 2017-12-11 18:00:40 -0800 | [diff] [blame] | 207 | job = J[build_name] |
| 208 | except Exception as e: |
| 209 | print('====> Failed to get build %s: %s.' % (build_name, str(e))) |
Adele Zhou | 566c4c0 | 2017-08-10 15:51:47 -0700 | [diff] [blame] | 210 | continue |
ncteisen | 888093c | 2017-12-11 18:00:40 -0800 | [diff] [blame] | 211 | last_processed_build_number = _get_last_processed_buildnumber(build_name) |
| 212 | last_complete_build_number = job.get_last_completed_buildnumber() |
| 213 | # To avoid processing all builds for a project never looked at. In this case, |
| 214 | # only examine 10 latest builds. |
| 215 | starting_build_number = max(last_processed_build_number + 1, |
| 216 | last_complete_build_number - 9) |
| 217 | for build_number in xrange(starting_build_number, |
| 218 | last_complete_build_number + 1): |
| 219 | print('====> Processing %s build %d.' % (build_name, build_number)) |
| 220 | build = None |
| 221 | try: |
| 222 | build = job.get_build_metadata(build_number) |
| 223 | print('====> Build status: %s.' % build.get_status()) |
| 224 | if build.get_status() == 'ABORTED': |
| 225 | continue |
| 226 | # If any build is still running, stop processing this job. Next time, we |
Mehrdad Afshari | 87cd994 | 2018-01-02 14:40:00 -0800 | [diff] [blame] | 227 | # start from where it was left so that all builds are processed |
ncteisen | 888093c | 2017-12-11 18:00:40 -0800 | [diff] [blame] | 228 | # sequentially. |
| 229 | if build.is_running(): |
| 230 | print('====> Build %d is still running.' % build_number) |
| 231 | break |
| 232 | except KeyError: |
| 233 | print('====> Build %s is missing. Skip.' % build_number) |
| 234 | continue |
| 235 | build_result = { |
| 236 | 'build_number': build_number, |
| 237 | 'timestamp': str(build.get_timestamp()) |
| 238 | } |
| 239 | url_base = json_url = '%s/%s/%d' % (_URL_BASE, build_name, build_number) |
| 240 | if _BUILDS[build_name]: # The build has matrix, such as gRPC_master. |
| 241 | build_result['matrix'] = _process_matrix(build, url_base) |
| 242 | else: |
| 243 | json_url = '%s/testReport/api/json' % url_base |
| 244 | console_url = '%s/consoleFull' % url_base |
| 245 | build_result['duration'] = build.get_duration().total_seconds() |
| 246 | build_stat = _process_build(json_url, console_url) |
| 247 | build_result.update(build_stat) |
| 248 | rows = [big_query_utils.make_row(build_number, build_result)] |
| 249 | if not big_query_utils.insert_rows(bq, _PROJECT_ID, _DATASET_ID, |
| 250 | build_name, rows): |
| 251 | print('====> Error uploading result to bigquery.') |
| 252 | sys.exit(1) |