email_maintainers.py 20 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633
  1. #
  2. # Copyright (C) 2005 The Trustees of Indiana University
  3. # Author: Douglas Gregor
  4. #
  5. # Distributed under the Boost Software License, Version 1.0. (See
  6. # accompanying file LICENSE_1_0.txt or copy at
  7. # http://www.boost.org/LICENSE_1_0.txt)
  8. #
  9. import re
  10. import smtplib
  11. import os
  12. import time
  13. import string
  14. import datetime
  15. import sys
  16. report_author = "Douglas Gregor <dgregor@cs.indiana.edu>"
  17. boost_dev_list = "Boost Developer List <boost@lists.boost.org>"
  18. def sorted_keys( dict ):
  19. result = dict.keys()
  20. result.sort()
  21. return result
  22. class Platform:
  23. """
  24. All of the failures for a particular platform.
  25. """
  26. def __init__(self, name):
  27. self.name = name
  28. self.failures = list()
  29. return
  30. def addFailure(self, failure):
  31. self.failures.append(failure)
  32. return
  33. def isBroken(self):
  34. return len(self.failures) > 300
  35. class Failure:
  36. """
  37. A single test case failure in the report.
  38. """
  39. def __init__(self, test, platform):
  40. self.test = test
  41. self.platform = platform
  42. return
  43. class Test:
  44. """
  45. All of the failures for a single test name within a library.
  46. """
  47. def __init__(self, library, name):
  48. self.library = library
  49. self.name = name
  50. self.failures = list()
  51. return
  52. def addFailure(self, failure):
  53. self.failures.append(failure)
  54. return
  55. def numFailures(self):
  56. return len(self.failures)
  57. def numReportableFailures(self):
  58. """
  59. Returns the number of failures that we will report to the
  60. maintainers of the library. This doesn't count failures on
  61. broken platforms.
  62. """
  63. count = 0
  64. for failure in self.failures:
  65. if not failure.platform.isBroken():
  66. count += 1
  67. pass
  68. pass
  69. return count
  70. class Library:
  71. """
  72. All of the information about the failures in a single library.
  73. """
  74. def __init__(self, name):
  75. self.name = name
  76. self.maintainers = list()
  77. self.tests = list()
  78. return
  79. def addTest(self, test):
  80. """
  81. Add another test to the library.
  82. """
  83. self.tests.append(test)
  84. return
  85. def addMaintainer(self, maintainer):
  86. """
  87. Add a new maintainer for this library.
  88. """
  89. self.maintainers.append(maintainer)
  90. return
  91. def numFailures(self):
  92. count = 0
  93. for test in self.tests:
  94. count += test.numFailures()
  95. pass
  96. return count
  97. def numReportableFailures(self):
  98. count = 0
  99. for test in self.tests:
  100. count += test.numReportableFailures()
  101. pass
  102. return count
  103. class Maintainer:
  104. """
  105. Information about the maintainer of a library
  106. """
  107. def __init__(self, name, email):
  108. self.name = name
  109. self.email = email
  110. self.libraries = list()
  111. return
  112. def addLibrary(self, library):
  113. self.libraries.append(library)
  114. return
  115. def composeEmail(self, report):
  116. """
  117. Composes an e-mail to this maintainer with information about
  118. the failures in his or her libraries, omitting those that come
  119. from "broken" platforms. Returns the e-mail text if a message
  120. needs to be sent, or None otherwise.
  121. """
  122. # Determine if we need to send a message to this developer.
  123. requires_message = False
  124. for library in self.libraries:
  125. if library.numReportableFailures() > 0:
  126. requires_message = True
  127. break
  128. if not requires_message:
  129. return None
  130. # Build the message header
  131. message = """From: Douglas Gregor <dgregor@cs.indiana.edu>
  132. To: """
  133. message += self.name + ' <' + self.email + '>'
  134. message += """
  135. Reply-To: boost@lists.boost.org
  136. Subject: Regressions in your Boost libraries as of """
  137. message += str(datetime.date.today()) + " [" + report.branch + "]"
  138. message += """
  139. You are receiving this report because one or more of the libraries you
  140. maintain has regression test failures that are not accounted for.
  141. A full version of the report is sent to the Boost developer's mailing
  142. list.
  143. Detailed report:
  144. """
  145. message += ' ' + report.url + """
  146. There are failures in these libraries you maintain:
  147. """
  148. # List the libraries this maintainer is responsible for and
  149. # the number of reportable failures in that library.
  150. for library in self.libraries:
  151. num_failures = library.numReportableFailures()
  152. if num_failures > 0:
  153. message += ' ' + library.name + ' (' + str(num_failures) + ')\n'
  154. pass
  155. pass
  156. # Provide the details for the failures in each library.
  157. for library in self.libraries:
  158. if library.numReportableFailures() > 0:
  159. message += '\n|' + library.name + '|\n'
  160. for test in library.tests:
  161. if test.numReportableFailures() > 0:
  162. message += ' ' + test.name + ':'
  163. for failure in test.failures:
  164. if not failure.platform.isBroken():
  165. message += ' ' + failure.platform.name
  166. pass
  167. pass
  168. message += '\n'
  169. pass
  170. pass
  171. pass
  172. pass
  173. return message
  174. class Report:
  175. """
  176. The complete report of all failing test cases.
  177. """
  178. def __init__(self, branch = 'HEAD'):
  179. self.branch = branch
  180. self.date = None
  181. self.url = None
  182. self.libraries = dict()
  183. self.platforms = dict()
  184. self.maintainers = dict()
  185. return
  186. def getPlatform(self, name):
  187. """
  188. Retrieve the platform with the given name.
  189. """
  190. if self.platforms.has_key(name):
  191. return self.platforms[name]
  192. else:
  193. self.platforms[name] = Platform(name)
  194. return self.platforms[name]
  195. def getMaintainer(self, name, email):
  196. """
  197. Retrieve the maintainer with the given name and e-mail address.
  198. """
  199. if self.maintainers.has_key(name):
  200. return self.maintainers[name]
  201. else:
  202. self.maintainers[name] = Maintainer(name, email)
  203. return self.maintainers[name]
  204. def parseIssuesEmail(self):
  205. """
  206. Try to parse the issues e-mail file. Returns True if everything was
  207. successful, false otherwise.
  208. """
  209. # See if we actually got the file
  210. if not os.path.isfile('issues-email.txt'):
  211. return False
  212. # Determine the set of libraries that have unresolved failures
  213. date_regex = re.compile('Report time: (.*)')
  214. url_regex = re.compile(' (http://.*)')
  215. library_regex = re.compile('\|(.*)\|')
  216. failure_regex = re.compile(' ([^:]*): (.*)')
  217. current_library = None
  218. for line in file('issues-email.txt', 'r'):
  219. # Check for the report time line
  220. m = date_regex.match(line)
  221. if m:
  222. self.date = m.group(1)
  223. continue
  224. # Check for the detailed report URL
  225. m = url_regex.match(line)
  226. if m:
  227. self.url = m.group(1)
  228. continue
  229. # Check for a library header
  230. m = library_regex.match(line)
  231. if m:
  232. current_library = Library(m.group(1))
  233. self.libraries[m.group(1)] = current_library
  234. continue
  235. # Check for a library test and its failures
  236. m = failure_regex.match(line)
  237. if m:
  238. test = Test(current_library, m.group(1))
  239. for platform_name in re.split('\s*', m.group(2)):
  240. if platform_name != '':
  241. platform = self.getPlatform(platform_name)
  242. failure = Failure(test, platform)
  243. test.addFailure(failure)
  244. platform.addFailure(failure)
  245. pass
  246. current_library.addTest(test)
  247. continue
  248. pass
  249. return True
  250. def getIssuesEmail(self):
  251. """
  252. Retrieve the issues email from MetaComm, trying a few times in
  253. case something wonky is happening. If we can retrieve the file,
  254. calls parseIssuesEmail and return True; otherwise, return False.
  255. """
  256. base_url = "http://engineering.meta-comm.com/boost-regression/CVS-"
  257. base_url += self.branch
  258. base_url += "/developer/";
  259. got_issues = False
  260. # Ping the server by looking for an HTML file
  261. print "Pinging the server to initiate extraction..."
  262. ping_url = base_url + "issues.html"
  263. os.system('curl -O ' + ping_url)
  264. os.system('rm -f issues.html')
  265. for x in range(30):
  266. # Update issues-email.txt
  267. url = base_url + "issues-email.txt"
  268. print 'Retrieving issues email from ' + url
  269. os.system('rm -f issues-email.txt')
  270. os.system('curl -O ' + url)
  271. if self.parseIssuesEmail():
  272. return True
  273. print 'Failed to fetch issues email. '
  274. time.sleep (30)
  275. return False
  276. # Parses the file $BOOST_ROOT/libs/maintainers.txt to create a hash
  277. # mapping from the library name to the list of maintainers.
  278. def parseLibraryMaintainersFile(self):
  279. """
  280. Parse the maintainers file in ../../../libs/maintainers.txt to
  281. collect information about the maintainers of broken libraries.
  282. """
  283. lib_maintainer_regex = re.compile('(\S+)\s*(.*)')
  284. name_email_regex = re.compile('\s*(\w*(\s*\w+)+)\s*<\s*(\S*(\s*\S+)+)\S*>')
  285. at_regex = re.compile('\s*-\s*at\s*-\s*')
  286. for line in file('../../../libs/maintainers.txt', 'r'):
  287. m = lib_maintainer_regex.match (line)
  288. if m:
  289. libname = m.group(1)
  290. if self.libraries.has_key(m.group(1)):
  291. library = self.libraries[m.group(1)]
  292. for person in re.split('\s*,\s*', m.group(2)):
  293. nmm = name_email_regex.match(person)
  294. if nmm:
  295. name = nmm.group(1)
  296. email = nmm.group(3)
  297. email = at_regex.sub('@', email)
  298. maintainer = self.getMaintainer(name, email)
  299. maintainer.addLibrary(library)
  300. library.addMaintainer(maintainer)
  301. pass
  302. pass
  303. pass
  304. pass
  305. pass
  306. pass
  307. def numFailures(self):
  308. count = 0
  309. for library in self.libraries:
  310. count += self.libraries[library].numFailures()
  311. pass
  312. return count
  313. def numReportableFailures(self):
  314. count = 0
  315. for library in self.libraries:
  316. count += self.libraries[library].numReportableFailures()
  317. pass
  318. return count
  319. def composeSummaryEmail(self):
  320. """
  321. Compose a message to send to the Boost developer's
  322. list. Return the message and return it.
  323. """
  324. message = """From: Douglas Gregor <dgregor@cs.indiana.edu>
  325. To: boost@lists.boost.org
  326. Reply-To: boost@lists.boost.org
  327. Subject: Boost regression notification ("""
  328. message += str(datetime.date.today()) + " [" + branch + "]"
  329. message += ")"
  330. message += """
  331. Boost Regression test failures
  332. """
  333. message += "Report time: " + self.date + """
  334. This report lists all regression test failures on release platforms.
  335. Detailed report:
  336. """
  337. message += ' ' + self.url + '\n\n'
  338. if self.numFailures() == 0:
  339. message += "No failures! Yay!\n"
  340. return message
  341. # List the platforms that are broken
  342. any_broken_platforms = self.numReportableFailures() < self.numFailures()
  343. if any_broken_platforms:
  344. message += """The following platforms have a large number of failures:
  345. """
  346. for platform in sorted_keys( self.platforms ):
  347. if self.platforms[platform].isBroken():
  348. message += ' ' + platform + '\n'
  349. message += '\n'
  350. # Display the number of failures
  351. message += (str(self.numFailures()) + ' failures in ' +
  352. str(len(self.libraries)) + ' libraries')
  353. if any_broken_platforms:
  354. diff = self.numFailures() - self.numReportableFailures()
  355. message += ' (' + str(diff) + ' are from non-broken platforms)'
  356. message += '\n'
  357. # Display the number of failures per library
  358. for k in sorted_keys( self.libraries ):
  359. library = self.libraries[k]
  360. num_failures = library.numFailures()
  361. message += (' ' + library.name + ' ('
  362. + str(library.numReportableFailures()))
  363. if library.numReportableFailures() < num_failures:
  364. message += (' of ' + str(num_failures)
  365. + ' failures are from non-broken platforms')
  366. message += ')\n'
  367. pass
  368. # If we have any broken platforms, tell the user how we're
  369. # displaying them.
  370. if any_broken_platforms:
  371. message += """
  372. Test failures marked with a (*) represent tests that failed on
  373. platforms that are considered broken. They are likely caused by
  374. misconfiguration by the regression tester or a failure in a core
  375. library such as Test or Config."""
  376. message += '\n'
  377. # Provide the details for the failures in each library.
  378. for k in sorted_keys( self.libraries ):
  379. library = self.libraries[k]
  380. message += '\n|' + library.name + '|\n'
  381. for test in library.tests:
  382. message += ' ' + test.name + ':'
  383. for failure in test.failures:
  384. platform = failure.platform
  385. message += ' ' + platform.name
  386. if platform.isBroken():
  387. message += '*'
  388. pass
  389. message += '\n'
  390. pass
  391. pass
  392. return message
  393. # Send a message to "person" (a maintainer of a library that is
  394. # failing).
  395. # maintainers is the result of get_library_maintainers()
  396. def send_individualized_message (branch, person, maintainers):
  397. # There are several states we could be in:
  398. # 0 Initial state. Eat everything up to the "NNN failures in MMM
  399. # libraries" line
  400. # 1 Suppress output within this library
  401. # 2 Forward output within this library
  402. state = 0
  403. failures_in_lib_regex = re.compile('\d+ failur.*\d+ librar')
  404. lib_failures_regex = re.compile(' (\S+) \((\d+)\)')
  405. lib_start_regex = re.compile('\|(\S+)\|')
  406. general_pass_regex = re.compile(' http://')
  407. for line in file('issues-email.txt', 'r'):
  408. if state == 0:
  409. lfm = lib_failures_regex.match(line)
  410. if lfm:
  411. # Pass the line through if the current person is a
  412. # maintainer of this library
  413. if lfm.group(1) in maintainers and person in maintainers[lfm.group(1)]:
  414. message += line
  415. print line,
  416. elif failures_in_lib_regex.match(line):
  417. message += "\nThere are failures in these libraries you maintain:\n"
  418. elif general_pass_regex.match(line):
  419. message += line
  420. lib_start = lib_start_regex.match(line)
  421. if lib_start:
  422. if state == 0:
  423. message += '\n'
  424. if lib_start.group(1) in maintainers and person in maintainers[lib_start.group(1)]:
  425. message += line
  426. state = 2
  427. else:
  428. state = 1
  429. else:
  430. if state == 1:
  431. pass
  432. elif state == 2:
  433. message += line
  434. if '--debug' in sys.argv:
  435. print '-----------------Message text----------------'
  436. print message
  437. else:
  438. print
  439. if '--send' in sys.argv:
  440. print "Sending..."
  441. smtp = smtplib.SMTP('milliways.osl.iu.edu')
  442. smtp.sendmail(from_addr = 'Douglas Gregor <dgregor@cs.indiana.edu>',
  443. to_addrs = person[1],
  444. msg = message)
  445. print "Done."
  446. # Send a message to the developer's list
  447. def send_boost_developers_message(branch, maintainers, failing_libraries):
  448. to_line = 'boost@lists.boost.org'
  449. from_line = 'Douglas Gregor <dgregor@cs.indiana.edu>'
  450. message = """From: Douglas Gregor <dgregor@cs.indiana.edu>
  451. To: boost@lists.boost.org
  452. Reply-To: boost@lists.boost.org
  453. Subject: Boost regression notification ("""
  454. message += str(datetime.date.today()) + " [" + branch + "]"
  455. message += ")"
  456. message += """
  457. """
  458. for line in file('issues-email.txt', 'r'):
  459. # Right before the detailed report, put out a warning message if
  460. # any libraries with failures to not have maintainers listed.
  461. if line.startswith('Detailed report:'):
  462. missing_maintainers = False
  463. for lib in failing_libraries:
  464. if not(lib in maintainers) or maintainers[lib] == list():
  465. missing_maintainers = True
  466. if missing_maintainers:
  467. message += """WARNING: The following libraries have failing regression tests but do
  468. not have a maintainer on file. Once a maintainer is found, add an
  469. entry to libs/maintainers.txt to eliminate this message.
  470. """
  471. for lib in failing_libraries:
  472. if not(lib in maintainers) or maintainers[lib] == list():
  473. message += ' ' + lib + '\n'
  474. message += '\n'
  475. message += line
  476. if '--send' in sys.argv:
  477. print 'Sending notification email...'
  478. smtp = smtplib.SMTP('milliways.osl.iu.edu')
  479. smtp.sendmail(from_addr = from_line, to_addrs = to_line, msg = message)
  480. print 'Done.'
  481. if '--debug' in sys.argv:
  482. print "----------Boost developer's message text----------"
  483. print message
  484. ###############################################################################
  485. # Main program #
  486. ###############################################################################
  487. # Parse command-line options
  488. branch = "HEAD"
  489. for arg in sys.argv:
  490. if arg.startswith("--branch="):
  491. branch = arg[len("--branch="):]
  492. report = Report(branch)
  493. # Try to parse the issues e-mail
  494. if '--no-get' in sys.argv:
  495. okay = report.parseIssuesEmail()
  496. else:
  497. okay = report.getIssuesEmail()
  498. if not okay:
  499. print 'Aborting.'
  500. if '--send' in sys.argv:
  501. message = """From: Douglas Gregor <dgregor@cs.indiana.edu>
  502. To: Douglas Gregor <dgregor@cs.indiana.edu>
  503. Reply-To: boost@lists.boost.org
  504. Subject: Regression status script failed on """
  505. message += str(datetime.date.today()) + " [" + branch + "]"
  506. smtp = smtplib.SMTP('milliways.osl.iu.edu')
  507. smtp.sendmail(from_addr = 'Douglas Gregor <dgregor@cs.indiana.edu>',
  508. to_addrs = 'dgregor@cs.indiana.edu',
  509. msg = message)
  510. sys.exit(1)
  511. # Try to parse maintainers information
  512. report.parseLibraryMaintainersFile()
  513. for maintainer_name in report.maintainers:
  514. maintainer = report.maintainers[maintainer_name]
  515. email = maintainer.composeEmail(report)
  516. if email:
  517. if '--send' in sys.argv:
  518. print ('Sending notification email to ' + maintainer.name + '...')
  519. smtp = smtplib.SMTP('milliways.osl.iu.edu')
  520. smtp.sendmail(from_addr = report_author,
  521. to_addrs = maintainer.email,
  522. msg = email)
  523. print 'done.\n'
  524. else:
  525. print 'Would send a notification e-mail to',maintainer.name
  526. if '--debug' in sys.argv:
  527. print ('Message text for ' + maintainer.name + ':\n')
  528. print email
  529. email = report.composeSummaryEmail()
  530. if '--send' in sys.argv:
  531. print 'Sending summary email to Boost developer list...'
  532. smtp = smtplib.SMTP('milliways.osl.iu.edu')
  533. smtp.sendmail(from_addr = report_author,
  534. to_addrs = boost_dev_list,
  535. msg = email)
  536. print 'done.\n'
  537. if '--debug' in sys.argv:
  538. print 'Message text for summary:\n'
  539. print email
  540. if not ('--send' in sys.argv):
  541. print 'Chickening out and not sending any e-mail.'
  542. print 'Use --send to actually send e-mail, --debug to see e-mails.'
粤ICP备19079148号