| #!/usr/bin/env fuchsia-vendored-python |
| # Copyright 2019 The Fuchsia Authors. All rights reserved. |
| # Use of this source code is governed by a BSD-style license that can be |
| # found in the LICENSE file. |
| |
| # This script downloads information about changes landed in the Fuchsia tree |
| # and computes the dates at which those changes receive Code, API, and |
| # Testability reviews. It produces four fields into a CSV file: |
| # |
| # * Change-ID |
| # * Time at which the change first received Code-Review+2 and API-Review+1. |
| # If a change never received API-Review+1, the time the change first received |
| # Code-Review+2 is used. This is an approximation of when the change is first |
| # eligible to receive a Testability Review. |
| # * Time at which the change first received a Testability-Review vote, either |
| # +1 or -1. |
| # * Email of the first reviewer to set Testability-Review |
| # |
| # It caches information from Gerrit into //local/change_id_cache/. |
| |
| import argparse |
| import datetime |
| import json |
| import os |
| import sys |
| import urllib |
| |
| FUCHSIA_DIR = os.path.abspath(os.path.join(__file__, os.pardir, os.pardir)) |
| |
| |
| def parse_date_from_gerrit(date_str): |
| # Gerrit returns timestamps with 9 digits of precision, but the %f |
| # formatter only expects 6 digits of precision, so truncate to parse. |
| return datetime.datetime.strptime(date_str[:-3], |
| "%Y-%m-%d %H:%M:%S.%f") |
| |
| |
| def cache_dir(): |
| dirname = os.path.join(FUCHSIA_DIR, 'local', 'change_id_cache') |
| if not os.path.exists(dirname): |
| os.makedirs(dirname) |
| return dirname |
| |
| |
| def cache_filename(c): |
| return os.path.join(cache_dir(), c) |
| |
| |
| def cache_change_data(c): |
| filename = cache_filename(c) |
| if os.path.exists(filename): |
| return |
| print('Data for %s not found in cache, fetching from gerrit' % c) |
| url = 'https://fuchsia-review.googlesource.com/changes/%s/detail' % c |
| r = urllib.request.urlopen(url) |
| if r.status != 200: |
| print('Gerrit returned error for %s: %d' % (c, r.status_code)) |
| return |
| response_json = r.read().decode('utf-8')[5:] # skip preamble |
| data = json.loads(response_json) |
| with open(cache_filename(c), 'w') as f: |
| json.dump(data, f, indent=2) |
| |
| |
| def read_change_data(c): |
| with open(cache_filename(c)) as f: |
| return json.load(f) |
| |
| |
| def process_change(c): |
| cache_change_data(c) |
| data = read_change_data(c) |
| testability_date = None |
| code_review_date = None |
| api_review_date = None |
| for message in data['messages']: |
| m = message['message'] |
| date = parse_date_from_gerrit(message['date']) |
| if testability_date is None: |
| if 'Testability-Review+1' in m or 'Testability-Review-1' in m: |
| testability_reviewer = message['real_author']['email'] |
| testability_date = parse_date_from_gerrit(message['date']) |
| if 'Code-Review+2' in message['message']: |
| if code_review_date is None: |
| code_review_date = date |
| if 'API-Review+1' in message['message']: |
| if api_review_date is None: |
| api_review_date = date |
| if testability_date is None or code_review_date is None: |
| return |
| last_approval_date = code_review_date |
| if api_review_date is not None: |
| if api_review_date > last_approval_date: |
| last_approval_date = api_review_date |
| return (c, testability_date, |
| last_approval_date, testability_reviewer) |
| |
| |
| header = 'ChangeID, First Testability-Review Date, First {Code + API}-Review date, Testability Reviewer' |
| |
| |
| def main(): |
| parser = argparse.ArgumentParser( |
| description='Compute Testability-Review stats for set of changes.') |
| parser.add_argument('--output', '-o', default='output.txt', |
| help='Name of output file') |
| parser.add_argument('changes', default='last_week_change_ids.txt', |
| help='File containing Change-Id values to analyze') |
| args = parser.parse_args() |
| |
| with open(args.changes) as f: |
| changes = f.readlines() |
| with open(args.output, 'w') as out: |
| out.write(header + '\n') |
| for c in changes: |
| c = c.strip() |
| try: |
| out.write('%s, %s, %s, %s\n' % process_change(c)) |
| except KeyboardInterrupt: |
| raise |
| except: |
| sys.stderr.write('Could not process %s, skipping\n' % c) |
| |
| |
| if __name__ == '__main__': |
| sys.exit(main()) |