parent
194b760739
commit
7281f760a9
1 changed files with 0 additions and 211 deletions
@ -1,211 +0,0 @@ |
|||||||
#!/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; otherwise, 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 |
|
||||||
|
|
||||||
from datetime import datetime |
|
||||||
from datetime import timedelta |
|
||||||
import json |
|
||||||
import logging |
|
||||||
import pprint |
|
||||||
|
|
||||||
import urllib.request, urllib.error, urllib.parse |
|
||||||
|
|
||||||
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 = urllib.request.Request(url) |
|
||||||
if TOKEN: |
|
||||||
request.add_header('Authorization', 'token {}'.format(TOKEN)) |
|
||||||
response = urllib.request.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() |
|
Loading…
Reference in new issue