diff options
Diffstat (limited to 'tools/regression/xsl_reports/email_maintainers.py')
-rw-r--r-- | tools/regression/xsl_reports/email_maintainers.py | 840 |
1 files changed, 840 insertions, 0 deletions
diff --git a/tools/regression/xsl_reports/email_maintainers.py b/tools/regression/xsl_reports/email_maintainers.py new file mode 100644 index 0000000000..308ab688f5 --- /dev/null +++ b/tools/regression/xsl_reports/email_maintainers.py @@ -0,0 +1,840 @@ +# +# Copyright (C) 2005, 2007 The Trustees of Indiana University +# Author: Douglas Gregor +# +# Distributed under the Boost Software License, Version 1.0. (See +# accompanying file LICENSE_1_0.txt or copy at +# http://www.boost.org/LICENSE_1_0.txt) +# +import re +import smtplib +import os +import time +import string +import datetime +import sys + +report_author = "Douglas Gregor <dgregor@osl.iu.edu>" +boost_dev_list = "Boost Developer List <boost@lists.boost.org>" +boost_testing_list = "Boost Testing List <boost-testing@lists.boost.org>" + +def sorted_keys( dict ): + result = dict.keys() + result.sort() + return result + + +class Platform: + """ + All of the failures for a particular platform. + """ + def __init__(self, name): + self.name = name + self.failures = list() + self.maintainers = list() + return + + def addFailure(self, failure): + self.failures.append(failure) + return + + def isBroken(self): + return len(self.failures) > 300 + + def addMaintainer(self, maintainer): + """ + Add a new maintainer for this platform. + """ + self.maintainers.append(maintainer) + return + +class Failure: + """ + A single test case failure in the report. + """ + def __init__(self, test, platform): + self.test = test + self.platform = platform + return + +class Test: + """ + All of the failures for a single test name within a library. + """ + def __init__(self, library, name): + self.library = library + self.name = name + self.failures = list() + return + + def addFailure(self, failure): + self.failures.append(failure) + return + + def numFailures(self): + return len(self.failures) + + def numReportableFailures(self): + """ + Returns the number of failures that we will report to the + maintainers of the library. This doesn't count failures on + broken platforms. + """ + count = 0 + for failure in self.failures: + if not failure.platform.isBroken(): + count += 1 + pass + pass + return count + +class Library: + """ + All of the information about the failures in a single library. + """ + def __init__(self, name): + self.name = name + self.maintainers = list() + self.tests = list() + return + + def addTest(self, test): + """ + Add another test to the library. + """ + self.tests.append(test) + return + + def addMaintainer(self, maintainer): + """ + Add a new maintainer for this library. + """ + self.maintainers.append(maintainer) + return + + def numFailures(self): + count = 0 + for test in self.tests: + count += test.numFailures() + pass + return count + + def numReportableFailures(self): + count = 0 + for test in self.tests: + count += test.numReportableFailures() + pass + return count + +class Maintainer: + """ + Information about the maintainer of a library + """ + def __init__(self, name, email): + self.name = name + self.email = email + self.libraries = list() + return + + def addLibrary(self, library): + self.libraries.append(library) + return + + def composeEmail(self, report): + """ + Composes an e-mail to this maintainer with information about + the failures in his or her libraries, omitting those that come + from "broken" platforms. Returns the e-mail text if a message + needs to be sent, or None otherwise. + """ + + # Determine if we need to send a message to this developer. + requires_message = False + for library in self.libraries: + if library.numReportableFailures() > 0: + requires_message = True + break + + if not requires_message: + return None + + # Build the message header + message = """From: Douglas Gregor <dgregor@osl.iu.edu> +To: """ + message += self.name + ' <' + self.email + '>' + message += """ +Reply-To: boost@lists.boost.org +Subject: Failures in your Boost libraries as of """ + message += str(datetime.date.today()) + " [" + report.branch + "]" + message += """ + +You are receiving this report because one or more of the libraries you +maintain has regression test failures that are not accounted for. +A full version of the report is sent to the Boost developer's mailing +list. + +Detailed report: +""" + message += ' ' + report.url + """ + +There are failures in these libraries you maintain: +""" + + # List the libraries this maintainer is responsible for and + # the number of reportable failures in that library. + for library in self.libraries: + num_failures = library.numReportableFailures() + if num_failures > 0: + message += ' ' + library.name + ' (' + str(num_failures) + ')\n' + pass + pass + + # Provide the details for the failures in each library. + for library in self.libraries: + if library.numReportableFailures() > 0: + message += '\n|' + library.name + '|\n' + for test in library.tests: + if test.numReportableFailures() > 0: + message += ' ' + test.name + ':' + for failure in test.failures: + if not failure.platform.isBroken(): + message += ' ' + failure.platform.name + pass + pass + message += '\n' + pass + pass + pass + pass + + return message + +class PlatformMaintainer: + """ + Information about the platform maintainer of a library + """ + def __init__(self, name, email): + self.name = name + self.email = email + self.platforms = list() + return + + def addPlatform(self, runner, platform): + self.platforms.append(platform) + return + + def composeEmail(self, report): + """ + Composes an e-mail to this platform maintainer if one or more of + the platforms s/he maintains has a large number of failures. + Returns the e-mail text if a message needs to be sent, or None + otherwise. + """ + + # Determine if we need to send a message to this developer. + requires_message = False + for platform in self.platforms: + if platform.isBroken(): + requires_message = True + break + + if not requires_message: + return None + + # Build the message header + message = """From: Douglas Gregor <dgregor@osl.iu.edu> +To: """ + message += self.name + ' <' + self.email + '>' + message += """ +Reply-To: boost@lists.boost.org +Subject: Large number of Boost failures on a platform you maintain as of """ + message += str(datetime.date.today()) + " [" + report.branch + "]" + message += """ + +You are receiving this report because one or more of the testing +platforms that you maintain has a large number of Boost failures that +are not accounted for. A full version of the report is sent to the +Boost developer's mailing list. + +Detailed report: +""" + message += ' ' + report.url + """ + +The following platforms have a large number of failures: +""" + + for platform in self.platforms: + if platform.isBroken(): + message += (' ' + platform.name + ' (' + + str(len(platform.failures)) + ' failures)\n') + + return message + +class Report: + """ + The complete report of all failing test cases. + """ + def __init__(self, branch = 'trunk'): + self.branch = branch + self.date = None + self.url = None + self.libraries = dict() + self.platforms = dict() + self.maintainers = dict() + self.platform_maintainers = dict() + return + + def getPlatform(self, name): + """ + Retrieve the platform with the given name. + """ + if self.platforms.has_key(name): + return self.platforms[name] + else: + self.platforms[name] = Platform(name) + return self.platforms[name] + + def getMaintainer(self, name, email): + """ + Retrieve the maintainer with the given name and e-mail address. + """ + if self.maintainers.has_key(name): + return self.maintainers[name] + else: + self.maintainers[name] = Maintainer(name, email) + return self.maintainers[name] + + def getPlatformMaintainer(self, name, email): + """ + Retrieve the platform maintainer with the given name and + e-mail address. + """ + if self.platform_maintainers.has_key(name): + return self.platform_maintainers[name] + else: + self.platform_maintainers[name] = PlatformMaintainer(name, email) + return self.platform_maintainers[name] + + def parseIssuesEmail(self): + """ + Try to parse the issues e-mail file. Returns True if everything was + successful, false otherwise. + """ + # See if we actually got the file + if not os.path.isfile('issues-email.txt'): + return False + + # Determine the set of libraries that have unresolved failures + date_regex = re.compile('Report time: (.*)') + url_regex = re.compile(' (http://.*)') + library_regex = re.compile('\|(.*)\|') + failure_regex = re.compile(' ([^:]*): (.*)') + current_library = None + for line in file('issues-email.txt', 'r'): + # Check for the report time line + m = date_regex.match(line) + if m: + self.date = m.group(1) + continue + + # Check for the detailed report URL + m = url_regex.match(line) + if m: + self.url = m.group(1) + continue + + # Check for a library header + m = library_regex.match(line) + if m: + current_library = Library(m.group(1)) + self.libraries[m.group(1)] = current_library + continue + + # Check for a library test and its failures + m = failure_regex.match(line) + if m: + test = Test(current_library, m.group(1)) + for platform_name in re.split('\s*', m.group(2)): + if platform_name != '': + platform = self.getPlatform(platform_name) + failure = Failure(test, platform) + test.addFailure(failure) + platform.addFailure(failure) + pass + current_library.addTest(test) + continue + pass + + return True + + def getIssuesEmail(self): + """ + Retrieve the issues email from beta.boost.org, trying a few + times in case something wonky is happening. If we can retrieve + the file, calls parseIssuesEmail and return True; otherwise, + return False. + """ + base_url = "http://beta.boost.org/development/tests/" + base_url += self.branch + base_url += "/developer/"; + got_issues = False + + # Ping the server by looking for an HTML file + print "Pinging the server to initiate extraction..." + ping_url = base_url + "issues.html" + os.system('curl -O ' + ping_url) + os.system('rm -f issues.html') + + for x in range(30): + # Update issues-email.txt + url = base_url + "issues-email.txt" + print 'Retrieving issues email from ' + url + os.system('rm -f issues-email.txt') + os.system('curl -O ' + url) + + if self.parseIssuesEmail(): + return True + + print 'Failed to fetch issues email. ' + time.sleep (30) + + return False + + # Parses the file $BOOST_ROOT/libs/maintainers.txt to create a hash + # mapping from the library name to the list of maintainers. + def parseLibraryMaintainersFile(self): + """ + Parse the maintainers file in ../../../libs/maintainers.txt to + collect information about the maintainers of broken libraries. + """ + lib_maintainer_regex = re.compile('(\S+)\s*(.*)') + name_email_regex = re.compile('\s*(\w*(\s*\w+)+)\s*<\s*(\S*(\s*\S+)+)\S*>') + at_regex = re.compile('\s*-\s*at\s*-\s*') + for line in file('../../../libs/maintainers.txt', 'r'): + if line.startswith('#'): + continue + m = lib_maintainer_regex.match (line) + if m: + libname = m.group(1) + if self.libraries.has_key(m.group(1)): + library = self.libraries[m.group(1)] + for person in re.split('\s*,\s*', m.group(2)): + nmm = name_email_regex.match(person) + if nmm: + name = nmm.group(1) + email = nmm.group(3) + email = at_regex.sub('@', email) + maintainer = self.getMaintainer(name, email) + maintainer.addLibrary(library) + library.addMaintainer(maintainer) + pass + pass + pass + pass + pass + pass + + # Parses the file $BOOST_ROOT/libs/platform_maintainers.txt to + # create a hash mapping from the platform name to the list of + # maintainers. + def parsePlatformMaintainersFile(self): + """ + Parse the platform maintainers file in + ../../../libs/platform_maintainers.txt to collect information + about the maintainers of the various platforms. + """ + platform_maintainer_regex = re.compile('([A-Za-z0-9_.-]*|"[^"]*")\s+(\S+)\s+(.*)') + name_email_regex = re.compile('\s*(\w*(\s*\w+)+)\s*<\s*(\S*(\s*\S+)+)\S*>') + at_regex = re.compile('\s*-\s*at\s*-\s*') + for line in file('../../../libs/platform_maintainers.txt', 'r'): + if line.startswith('#'): + continue + m = platform_maintainer_regex.match (line) + if m: + platformname = m.group(2) + if self.platforms.has_key(platformname): + platform = self.platforms[platformname] + for person in re.split('\s*,\s*', m.group(3)): + nmm = name_email_regex.match(person) + if nmm: + name = nmm.group(1) + email = nmm.group(3) + email = at_regex.sub('@', email) + maintainer = self.getPlatformMaintainer(name, email) + maintainer.addPlatform(m.group(1), platform) + platform.addMaintainer(maintainer) + pass + pass + pass + pass + pass + + def numFailures(self): + count = 0 + for library in self.libraries: + count += self.libraries[library].numFailures() + pass + return count + + def numReportableFailures(self): + count = 0 + for library in self.libraries: + count += self.libraries[library].numReportableFailures() + pass + return count + + def composeSummaryEmail(self): + """ + Compose a message to send to the Boost developer's + list. Return the message and return it. + """ + message = """From: Douglas Gregor <dgregor@osl.iu.edu> +To: boost@lists.boost.org +Reply-To: boost@lists.boost.org +Subject: [Report] """ + message += str(self.numFailures()) + " failures on " + branch + if branch != 'trunk': + message += ' branch' + message += " (" + str(datetime.date.today()) + ")" + message += """ + +Boost regression test failures +""" + message += "Report time: " + self.date + """ + +This report lists all regression test failures on high-priority platforms. + +Detailed report: +""" + + message += ' ' + self.url + '\n\n' + + if self.numFailures() == 0: + message += "No failures! Yay!\n" + return message + + # List the platforms that are broken + any_broken_platforms = self.numReportableFailures() < self.numFailures() + if any_broken_platforms: + message += """The following platforms have a large number of failures: +""" + for platform in sorted_keys( self.platforms ): + if self.platforms[platform].isBroken(): + message += (' ' + platform + ' (' + + str(len(self.platforms[platform].failures)) + + ' failures)\n') + + message += """ +Failures on these "broken" platforms will be omitted from the results below. +Please see the full report for information about these failures. + +""" + + # Display the number of failures + message += (str(self.numReportableFailures()) + ' failures in ' + + str(len(self.libraries)) + ' libraries') + if any_broken_platforms: + message += (' (plus ' + str(self.numFailures() - self.numReportableFailures()) + + ' from broken platforms)') + + message += '\n' + + # Display the number of failures per library + for k in sorted_keys( self.libraries ): + library = self.libraries[k] + num_failures = library.numFailures() + message += ' ' + library.name + ' (' + + if library.numReportableFailures() > 0: + message += (str(library.numReportableFailures()) + + " failures") + + if library.numReportableFailures() < num_failures: + if library.numReportableFailures() > 0: + message += ', plus ' + + message += (str(num_failures-library.numReportableFailures()) + + ' failures on broken platforms') + message += ')\n' + pass + + message += '\n' + + # Provide the details for the failures in each library. + for k in sorted_keys( self.libraries ): + library = self.libraries[k] + if library.numReportableFailures() > 0: + message += '\n|' + library.name + '|\n' + for test in library.tests: + if test.numReportableFailures() > 0: + message += ' ' + test.name + ':' + for failure in test.failures: + platform = failure.platform + if not platform.isBroken(): + message += ' ' + platform.name + message += '\n' + + return message + + def composeTestingSummaryEmail(self): + """ + Compose a message to send to the Boost Testing list. Returns + the message text if a message is needed, returns None + otherwise. + """ + brokenPlatforms = 0 + for platform in sorted_keys( self.platforms ): + if self.platforms[platform].isBroken(): + brokenPlatforms = brokenPlatforms + 1 + + if brokenPlatforms == 0: + return None; + + message = """From: Douglas Gregor <dgregor@osl.iu.edu> +To: boost-testing@lists.boost.org +Reply-To: boost-testing@lists.boost.org +Subject: [Report] """ + message += str(brokenPlatforms) + " potentially broken platforms on " + branch + if branch != 'trunk': + message += ' branch' + message += " (" + str(datetime.date.today()) + ")" + message += """ + +Potentially broken platforms for Boost regression testing +""" + message += "Report time: " + self.date + """ + +This report lists the high-priority platforms that are exhibiting a +large number of regression test failures, which might indicate a problem +with the test machines or testing harness. + +Detailed report: +""" + + message += ' ' + self.url + '\n' + + message += """ +Platforms with a large number of failures: +""" + for platform in sorted_keys( self.platforms ): + if self.platforms[platform].isBroken(): + message += (' ' + platform + ' (' + + str(len(self.platforms[platform].failures)) + + ' failures)\n') + + return message + +# Send a message to "person" (a maintainer of a library that is +# failing). +# maintainers is the result of get_library_maintainers() +def send_individualized_message (branch, person, maintainers): + # There are several states we could be in: + # 0 Initial state. Eat everything up to the "NNN failures in MMM + # libraries" line + # 1 Suppress output within this library + # 2 Forward output within this library + state = 0 + + failures_in_lib_regex = re.compile('\d+ failur.*\d+ librar') + lib_failures_regex = re.compile(' (\S+) \((\d+)\)') + lib_start_regex = re.compile('\|(\S+)\|') + general_pass_regex = re.compile(' http://') + for line in file('issues-email.txt', 'r'): + if state == 0: + lfm = lib_failures_regex.match(line) + if lfm: + # Pass the line through if the current person is a + # maintainer of this library + if lfm.group(1) in maintainers and person in maintainers[lfm.group(1)]: + message += line + print line, + + elif failures_in_lib_regex.match(line): + message += "\nThere are failures in these libraries you maintain:\n" + elif general_pass_regex.match(line): + message += line + + lib_start = lib_start_regex.match(line) + if lib_start: + if state == 0: + message += '\n' + + if lib_start.group(1) in maintainers and person in maintainers[lib_start.group(1)]: + message += line + state = 2 + else: + state = 1 + else: + if state == 1: + pass + elif state == 2: + message += line + + if '--debug' in sys.argv: + print '-----------------Message text----------------' + print message + else: + print + + if '--send' in sys.argv: + print "Sending..." + smtp = smtplib.SMTP('milliways.osl.iu.edu') + smtp.sendmail(from_addr = 'Douglas Gregor <dgregor@osl.iu.edu>', + to_addrs = person[1], + msg = message) + print "Done." + + +# Send a message to the developer's list +def send_boost_developers_message(branch, maintainers, failing_libraries): + to_line = 'boost@lists.boost.org' + from_line = 'Douglas Gregor <dgregor@osl.iu.edu>' + + message = """From: Douglas Gregor <dgregor@osl.iu.edu> +To: boost@lists.boost.org +Reply-To: boost@lists.boost.org +Subject: Boost regression testing notification (""" + + message += str(datetime.date.today()) + " [" + branch + "]" + message += ")" + + message += """ + +""" + + for line in file('issues-email.txt', 'r'): + # Right before the detailed report, put out a warning message if + # any libraries with failures to not have maintainers listed. + if line.startswith('Detailed report:'): + missing_maintainers = False + for lib in failing_libraries: + if not(lib in maintainers) or maintainers[lib] == list(): + missing_maintainers = True + + if missing_maintainers: + message += """WARNING: The following libraries have failing regression tests but do +not have a maintainer on file. Once a maintainer is found, add an +entry to libs/maintainers.txt to eliminate this message. +""" + + for lib in failing_libraries: + if not(lib in maintainers) or maintainers[lib] == list(): + message += ' ' + lib + '\n' + message += '\n' + + message += line + + if '--send' in sys.argv: + print 'Sending notification email...' + smtp = smtplib.SMTP('milliways.osl.iu.edu') + smtp.sendmail(from_addr = from_line, to_addrs = to_line, msg = message) + print 'Done.' + + if '--debug' in sys.argv: + print "----------Boost developer's message text----------" + print message + +############################################################################### +# Main program # +############################################################################### + +# Parse command-line options +branch = "trunk" +for arg in sys.argv: + if arg.startswith("--branch="): + branch = arg[len("--branch="):] + +report = Report(branch) + +# Try to parse the issues e-mail +if '--no-get' in sys.argv: + okay = report.parseIssuesEmail() +else: + okay = report.getIssuesEmail() + +if not okay: + print 'Aborting.' + if '--send' in sys.argv: + message = """From: Douglas Gregor <dgregor@osl.iu.edu> + To: Douglas Gregor <dgregor@osl.iu.edu> + Reply-To: boost@lists.boost.org + Subject: Regression status script failed on """ + message += str(datetime.date.today()) + " [" + branch + "]" + smtp = smtplib.SMTP('milliways.osl.iu.edu') + smtp.sendmail(from_addr = 'Douglas Gregor <dgregor@osl.iu.edu>', + to_addrs = 'dgregor@osl.iu.edu', + msg = message) + sys.exit(1) + +# Try to parse maintainers information +report.parseLibraryMaintainersFile() +report.parsePlatformMaintainersFile() + +# Generate individualized e-mail for library maintainers +for maintainer_name in report.maintainers: + maintainer = report.maintainers[maintainer_name] + + email = maintainer.composeEmail(report) + if email: + if '--send' in sys.argv: + print ('Sending notification email to ' + maintainer.name + '...') + smtp = smtplib.SMTP('milliways.osl.iu.edu') + smtp.sendmail(from_addr = report_author, + to_addrs = maintainer.email, + msg = email) + print 'done.\n' + else: + print 'Would send a notification e-mail to',maintainer.name + + if '--debug' in sys.argv: + print ('Message text for ' + maintainer.name + ':\n') + print email + +# Generate individualized e-mail for platform maintainers +for maintainer_name in report.platform_maintainers: + maintainer = report.platform_maintainers[maintainer_name] + + email = maintainer.composeEmail(report) + if email: + if '--send' in sys.argv: + print ('Sending notification email to ' + maintainer.name + '...') + smtp = smtplib.SMTP('milliways.osl.iu.edu') + smtp.sendmail(from_addr = report_author, + to_addrs = maintainer.email, + msg = email) + print 'done.\n' + else: + print 'Would send a notification e-mail to',maintainer.name + + if '--debug' in sys.argv: + print ('Message text for ' + maintainer.name + ':\n') + print email + +email = report.composeSummaryEmail() +if '--send' in sys.argv: + print 'Sending summary email to Boost developer list...' + smtp = smtplib.SMTP('milliways.osl.iu.edu') + smtp.sendmail(from_addr = report_author, + to_addrs = boost_dev_list, + msg = email) + print 'done.\n' +if '--debug' in sys.argv: + print 'Message text for summary:\n' + print email + +email = report.composeTestingSummaryEmail() +if email: + if '--send' in sys.argv: + print 'Sending summary email to Boost testing list...' + smtp = smtplib.SMTP('milliways.osl.iu.edu') + smtp.sendmail(from_addr = report_author, + to_addrs = boost_testing_list, + msg = email) + print 'done.\n' + if '--debug' in sys.argv: + print 'Message text for testing summary:\n' + print email + +if not ('--send' in sys.argv): + print 'Chickening out and not sending any e-mail.' + print 'Use --send to actually send e-mail, --debug to see e-mails.' |