| #!/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()) |