| 
					
				 | 
			
			
				@@ -0,0 +1,163 @@ 
			 | 
		
	
		
			
				 | 
				 | 
			
			
				+#!/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() 
			 |