woo_commenter.py
author Ed Morley <emorley@mozilla.com>
Tue, 23 May 2017 17:29:17 +0100
changeset 323 98d6754d43f6
parent 319 5c74a2f26b23
permissions -rwxr-xr-x
Bug 1367107 - Validate bug list passed to /bugdetails/
#!/usr/bin/env python

# This Source Code is subject to the terms of the Mozilla Public License
# version 2.0 (the "License"). You can obtain a copy of the License at
# http://mozilla.org/MPL/2.0/.

import ConfigParser
import datetime
import os
import sys
from operator import itemgetter
from optparse import OptionParser

import requests
import tempita
from requests.exceptions import ConnectionError, HTTPError, Timeout

from woo_client import TopBugs

CONF_FILE = 'woo_cron.conf'
TEMPLATE_FILE = os.path.join('templates', 'bug_comment.template')

# The minimum number of failure classifications a bug must receive
# (in the specified time window) for a bug comment to be posted.
DAILY_THRESHOLD = 15
WEEKLY_THRESHOLD = 5
# Include rank for top 50 bugs
RANK_THRESHOLD = 50
# Include call-to-action message for bugs with more than 30 failures/week
# and a more urgent message if more than 75 failures/week
PRIORITY1_THRESHOLD = 75
PRIORITY2_THRESHOLD = 30

COMMENT_API_URL = 'https://bugzilla.mozilla.org/rest/bug/%s/comment'


def calculate_date_strings(weekly_mode):
    """Returns a tuple of start and end date strings in YYYY-MM-DD format."""
    yesterday = datetime.date.today() - datetime.timedelta(days=1)
    end_date = yesterday.isoformat()
    # The start/end dates are inclusive.
    if weekly_mode:
        start_date = (yesterday - datetime.timedelta(days=6)).isoformat()
    else:
        # Daily mode.
        start_date = end_date
    return start_date, end_date


def dict_to_sorted_list(d):
    """Convert a dict into a list of tuples, in descending order of value (then key)."""
    return sorted(d.iteritems(), key=itemgetter(1, 0), reverse=True)


def submit_bug_comment(bmo_session, bug_id, text):
    """Submits a comment to a Bugzilla bug but fails gracefully in the case of errors,
       to avoid breaking the whole batch if only one or two requests fail to succeed.
       This is particularly important given bug numbers might have been typoed, or be
       for non-public bugs, on which this script will not be able to leave comments."""
    try:
        r = bmo_session.post(COMMENT_API_URL % bug_id, json={'comment': text}, timeout=30)
        r.raise_for_status()
    except (ConnectionError, Timeout) as e:
        print "%s: %s" % (e.__class__.__name__, str(e))
    except HTTPError:
        print "HTTPError %s: %s" % (r.status_code, r.text)


def main():
    """Posts a bug comment containing stats to each bug whose total number of
       occurrences (in the chosen time window) met the appropriate threshold."""
    parser = OptionParser()
    parser.add_option('--weekly', action='store_true', dest='weekly_mode', default=False,
                      help='generate weekly summaries instead of the default daily summaries')
    parser.add_option('--test', action='store_true', dest='test_mode', default=False,
                      help='output bug comments to stdout rather than submitting to Bugzilla')
    options, _ = parser.parse_args()

    try:
        cfg = ConfigParser.ConfigParser()
        cfg.read(CONF_FILE)
        local_server_url = cfg.get('woo', 'local_server_url')
        bugzilla_api_key = cfg.get('bugzilla', 'api_key')
    except ConfigParser.Error as e:
        sys.stderr.write('Error reading %s: %s\n' % (CONF_FILE, e))
        sys.exit(1)

    start_date, end_date = calculate_date_strings(options.weekly_mode)
    threshold = WEEKLY_THRESHOLD if options.weekly_mode else DAILY_THRESHOLD

    template_defaults = {'weekly_mode': options.weekly_mode,
                         'start_date': start_date,
                         'end_date': end_date}
    tmpl = tempita.Template.from_filename(TEMPLATE_FILE, namespace=template_defaults)

    bmo_session = requests.Session()
    bmo_session.headers['User-Agent'] = 'orangefactor-commenter'
    bmo_session.headers['X_BUGZILLA_API_KEY'] = bugzilla_api_key
    # Use a custom HTTP adapter, so we can set a non-zero max_retries value.
    bmo_session.mount("https://", requests.adapters.HTTPAdapter(max_retries=3))

    # Fetch per-repository, per-platform and total failure counts for each bug.
    tb = TopBugs(local_server_url, start_date, end_date, tree='all')
    stats = tb.stats()
    bug_stats = tb.stats_by_bug()
    if options.weekly_mode:
        top = tb.top_bugs()
        top = top[:RANK_THRESHOLD]
    else:
        top = []

    testruncount = stats['testruncount']
    for bug_id, counts in bug_stats.iteritems():
        if counts['total'] < threshold:
            continue
        rank = None
        if (bug_id, counts['total']) in top:
            rank = top.index((bug_id, counts['total']))+1
        if options.weekly_mode and counts['total'] >= PRIORITY1_THRESHOLD:
            priority = 1
        elif options.weekly_mode and counts['total'] >= PRIORITY2_THRESHOLD:
            priority = 2
        else:
            priority = 0
        text = tmpl.substitute(bug_id=bug_id,
                               total=counts['total'],
                               testruncount=testruncount,
                               rank=rank,
                               priority=priority,
                               failure_rate=round(counts['total']/float(testruncount), 3),
                               repositories=dict_to_sorted_list(counts['per_repository']),
                               platforms=dict_to_sorted_list(counts['per_platform']))
        if options.test_mode:
            print "\n# Bug %s:" % bug_id
            print text
            continue
        print "Submitting comment to bug %s (%d occurrences)" % (bug_id, counts['total'])
        submit_bug_comment(bmo_session, bug_id, text)

    print "Complete!"


if __name__ == '__main__':
    main()