| #!/usr/bin/env python |
| # Copyright 2017 gRPC authors. |
| # |
| # Licensed under the Apache License, Version 2.0 (the "License"); |
| # you may not use this file except in compliance with the License. |
| # You may obtain a copy of the License at |
| # |
| # http://www.apache.org/licenses/LICENSE-2.0 |
| # |
| # Unless required by applicable law or agreed to in writing, software |
| # distributed under the License is distributed on an "AS IS" BASIS, |
| # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. |
| # See the License for the specific language governing permissions and |
| # limitations under the License. |
| """Measure the time between PR creation and completion of all tests. |
| |
| You'll need a github API token to avoid being rate-limited. See |
| https://help.github.com/articles/creating-a-personal-access-token-for-the-command-line/ |
| |
| This script goes over the most recent 100 pull requests. For PRs with a single |
| commit, it uses the PR's creation as the initial time; othewise, it uses the |
| date of the last commit. This is somewhat fragile, and imposed by the fact that |
| GitHub reports a PR's updated timestamp for any event that modifies the PR (e.g. |
| comments), not just the addition of new commits. |
| |
| In addition, it ignores latencies greater than five hours, as that's likely due |
| to a manual re-run of tests. |
| """ |
| |
| from __future__ import absolute_import |
| from __future__ import division |
| from __future__ import print_function |
| |
| import json |
| import logging |
| import pprint |
| import urllib2 |
| |
| from datetime import datetime, timedelta |
| |
| logging.basicConfig(format='%(asctime)s %(message)s') |
| |
| PRS = 'https://api.github.com/repos/grpc/grpc/pulls?state=open&per_page=100' |
| COMMITS = 'https://api.github.com/repos/grpc/grpc/pulls/{pr_number}/commits' |
| |
| |
| def gh(url): |
| request = urllib2.Request(url) |
| if TOKEN: |
| request.add_header('Authorization', 'token {}'.format(TOKEN)) |
| response = urllib2.urlopen(request) |
| return response.read() |
| |
| |
| def print_csv_header(): |
| print('pr,base_time,test_time,latency_seconds,successes,failures,errors') |
| |
| |
| def output(pr, |
| base_time, |
| test_time, |
| diff_time, |
| successes, |
| failures, |
| errors, |
| mode='human'): |
| if mode == 'human': |
| print( |
| "PR #{} base time: {} UTC, Tests completed at: {} UTC. Latency: {}." |
| "\n\tSuccesses: {}, Failures: {}, Errors: {}".format( |
| pr, base_time, test_time, diff_time, successes, failures, |
| errors)) |
| elif mode == 'csv': |
| print(','.join([ |
| str(pr), str(base_time), str(test_time), str( |
| int((test_time - base_time).total_seconds())), str(successes), |
| str(failures), str(errors) |
| ])) |
| |
| |
| def parse_timestamp(datetime_str): |
| return datetime.strptime(datetime_str, '%Y-%m-%dT%H:%M:%SZ') |
| |
| |
| def to_posix_timestamp(dt): |
| return str((dt - datetime(1970, 1, 1)).total_seconds()) |
| |
| |
| def get_pr_data(): |
| latest_prs = json.loads(gh(PRS)) |
| res = [{ |
| 'number': pr['number'], |
| 'created_at': parse_timestamp(pr['created_at']), |
| 'updated_at': parse_timestamp(pr['updated_at']), |
| 'statuses_url': pr['statuses_url'] |
| } for pr in latest_prs] |
| return res |
| |
| |
| def get_commits_data(pr_number): |
| commits = json.loads(gh(COMMITS.format(pr_number=pr_number))) |
| return { |
| 'num_commits': len(commits), |
| 'most_recent_date': |
| parse_timestamp(commits[-1]['commit']['author']['date']) |
| } |
| |
| |
| def get_status_data(statuses_url, system): |
| status_url = statuses_url.replace('statuses', 'status') |
| statuses = json.loads(gh(status_url + '?per_page=100')) |
| successes = 0 |
| failures = 0 |
| errors = 0 |
| latest_datetime = None |
| if not statuses: return None |
| if system == 'kokoro': string_in_target_url = 'kokoro' |
| elif system == 'jenkins': string_in_target_url = 'grpc-testing' |
| for status in statuses['statuses']: |
| if not status['target_url'] or string_in_target_url not in status[ |
| 'target_url']: |
| continue # Ignore jenkins |
| if status['state'] == 'pending': return None |
| elif status['state'] == 'success': successes += 1 |
| elif status['state'] == 'failure': failures += 1 |
| elif status['state'] == 'error': errors += 1 |
| if not latest_datetime: |
| latest_datetime = parse_timestamp(status['updated_at']) |
| else: |
| latest_datetime = max(latest_datetime, |
| parse_timestamp(status['updated_at'])) |
| # First status is the most recent one. |
| if any([successes, failures, errors]) and sum( |
| [successes, failures, errors]) > 15: |
| return { |
| 'latest_datetime': latest_datetime, |
| 'successes': successes, |
| 'failures': failures, |
| 'errors': errors |
| } |
| else: |
| return None |
| |
| |
| def build_args_parser(): |
| import argparse |
| parser = argparse.ArgumentParser() |
| parser.add_argument( |
| '--format', |
| type=str, |
| choices=['human', 'csv'], |
| default='human', |
| help='Output format: are you a human or a machine?') |
| parser.add_argument( |
| '--system', |
| type=str, |
| choices=['jenkins', 'kokoro'], |
| required=True, |
| help='Consider only the given CI system') |
| parser.add_argument( |
| '--token', |
| type=str, |
| default='', |
| help='GitHub token to use its API with a higher rate limit') |
| return parser |
| |
| |
| def main(): |
| import sys |
| global TOKEN |
| args_parser = build_args_parser() |
| args = args_parser.parse_args() |
| TOKEN = args.token |
| if args.format == 'csv': print_csv_header() |
| for pr_data in get_pr_data(): |
| commit_data = get_commits_data(pr_data['number']) |
| # PR with a single commit -> use the PRs creation time. |
| # else -> use the latest commit's date. |
| base_timestamp = pr_data['updated_at'] |
| if commit_data['num_commits'] > 1: |
| base_timestamp = commit_data['most_recent_date'] |
| else: |
| base_timestamp = pr_data['created_at'] |
| last_status = get_status_data(pr_data['statuses_url'], args.system) |
| if last_status: |
| diff = last_status['latest_datetime'] - base_timestamp |
| if diff < timedelta(hours=5): |
| output( |
| pr_data['number'], |
| base_timestamp, |
| last_status['latest_datetime'], |
| diff, |
| last_status['successes'], |
| last_status['failures'], |
| last_status['errors'], |
| mode=args.format) |
| |
| |
| if __name__ == '__main__': |
| main() |