#!/usr/bin/env python # # Copyright (C) 2011 - Soren Hansen # Copyright (C) 2013 - Red Hat, Inc. # # 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. import calendar import datetime import optparse import os import os.path import sys import utils def sec_to_period_string(seconds): days = seconds / (3600 * 24) hours = (seconds / 3600) - (days * 24) minutes = (seconds / 60) - (days * 24 * 60) - (hours * 60) return '%d days, %d hours, %d minutes' % (days, hours, minutes) def get_age_of_patch(patch, now_ts): approvals = patch.get('approvals', []) approvals.sort(key=lambda a:a['grantedOn']) # The createdOn timestamp on the patch isn't what we want. # It's when the patch was written, not submitted for review. # The next best thing in the data we have is the time of the # first review. When all is working well, jenkins or smokestack # will comment within the first hour or two, so that's better # than the other timestamp, which may reflect that the code # was written many weeks ago, even though it was just recently # submitted for review. if approvals: return now_ts - approvals[0]['grantedOn'] else: return now_ts - patch['createdOn'] def average_age(changes, key='age'): if not changes: return 0 total_seconds = 0 for change in changes: total_seconds += change[key] avg_age = total_seconds / len(changes) return sec_to_period_string(avg_age) def median_age(changes, key='age'): if not changes: return 0 changes = sorted(changes, key=lambda change: change[key]) median_age = changes[len(changes)/2][key] return sec_to_period_string(median_age) def number_waiting_more_than(changes, seconds, key='age'): index = 0 for change in changes: if change[key] > seconds: return len(changes) - index index += 1 return 0 def gen_stats(projects, waiting_on_reviewer, waiting_on_submitter, age_sorted, age2_sorted, age3_sorted, options): result = [] result.append(('Projects', '%s' % [project['name'] for project in projects])) stats = [] stats.append(('Total Open Reviews', '%d' % ( len(waiting_on_reviewer) + len(waiting_on_submitter)))) stats.append(('Waiting on Submitter', '%d' % len(waiting_on_submitter))) stats.append(('Waiting on Reviewer', '%d' % len(waiting_on_reviewer))) latest_rev_stats = [] latest_rev_stats.append(('Average wait time', '%s' % ( average_age(waiting_on_reviewer)))) latest_rev_stats.append(('Median wait time', '%s' % ( median_age(waiting_on_reviewer)))) latest_rev_stats.append(('Number waiting more than %i days' % options.waiting_more, '%i' % (number_waiting_more_than( age_sorted, 60 * 60 * 24 * options.waiting_more)))) stats.append(('Stats since the latest revision', latest_rev_stats)) first_rev_stats = [] first_rev_stats.append(('Average wait time', '%s' % ( average_age(waiting_on_reviewer, key='age2')))) first_rev_stats.append(('Median wait time', '%s' % ( median_age(waiting_on_reviewer, key='age2')))) stats.append(('Stats since the first revision', first_rev_stats)) last_without_nack_stats = [] last_without_nack_stats.append(('Average wait time', '%s' % ( average_age(waiting_on_reviewer, key='age3')))) last_without_nack_stats.append(('Median wait time', '%s' % ( median_age(waiting_on_reviewer, key='age3')))) stats.append(('Stats since the last revision without -1 or -2 (ignoring jenkins)', last_without_nack_stats)) changes = [] for change in age_sorted[-options.longest_waiting:]: changes.append('%s %s (%s)' % (sec_to_period_string(change['age']), change['url'], change['subject'])) stats.append(('Longest waiting reviews (based on latest revision)', changes)) changes = [] for change in age2_sorted[-options.longest_waiting:]: changes.append('%s %s (%s)' % (sec_to_period_string(change['age2']), change['url'], change['subject'])) stats.append(('Longest waiting reviews (based on first revision)', changes)) changes = [] for change in age3_sorted[-options.longest_waiting:]: changes.append('%s %s (%s)' % (sec_to_period_string(change['age3']), change['url'], change['subject'])) stats.append(('Longest waiting reviews (based on oldest rev without nack, ignoring jenkins)', changes)) result.append(stats) return result def print_stats_txt(stats, f=sys.stdout): def print_list_txt(l, level): for item in l: if not isinstance(item, list): f.write('%s> ' % ('--' * level)) print_item_txt(item, level) def print_item_txt(item, level): if isinstance(item, basestring): f.write('%s\n' % item) elif isinstance(item, list): print_list_txt(item, level + 1) elif isinstance(item, tuple): f.write('%s: ' % item[0]) if isinstance(item[1], list): f.write('\n') print_item_txt(item[1], level) else: raise Exception('Unhandled type') print_list_txt(stats, 0) def print_stats_html(stats, f=sys.stdout): def print_list_html(l, level): if level: f.write('<%s>\n' % ('ul' if level == 1 else 'ol')) for item in l: if level: f.write('%s