161 lines
6.0 KiB
Python
Executable File
161 lines
6.0 KiB
Python
Executable File
#!/usr/bin/python
|
|
|
|
import datetime
|
|
import json
|
|
import sys
|
|
import time
|
|
|
|
import conf
|
|
|
|
def patch_list_as_html(l):
|
|
out = []
|
|
for p in sorted(l):
|
|
number, patch = p.split(',')
|
|
out.append('<a href="http://review.openstack.org/#/c/%s/%s">%s,%s</a>'
|
|
% (number, patch, number, patch))
|
|
return ', '.join(out)
|
|
|
|
|
|
def report(project_filter, user_filter, prefix):
|
|
with open('patchsets.json') as f:
|
|
patchsets = json.loads(f.read())
|
|
|
|
if not user_filter:
|
|
user_filter = conf.CI_SYSTEM[prefix]
|
|
elif user_filter and not 'Jenkins' in user_filter:
|
|
user_filter_new = ['Jenkins']
|
|
user_filter_new.extend(user_filter)
|
|
user_filter = user_filter_new
|
|
|
|
# This is more complicated than it looks because we need to handle
|
|
# patchsets which are uploaded so rapidly that older patchsets aren't
|
|
# finished testing.
|
|
total_patches = 0
|
|
total_votes = {}
|
|
missed_votes = {}
|
|
sentiments = {}
|
|
passed_votes = {}
|
|
failed_votes = {}
|
|
unparsed_votes = {}
|
|
|
|
for number in patchsets:
|
|
if patchsets[number].get('__exemption__'):
|
|
continue
|
|
|
|
if project_filter != '*':
|
|
if patchsets[number].get('__project__') != project_filter:
|
|
continue
|
|
|
|
patches = sorted(patchsets[number].keys())
|
|
valid_patches = []
|
|
|
|
# Determine how long a patch was valid for. If it wasn't valid for
|
|
# at least three hours, disgard.
|
|
for patch in patches:
|
|
if not '__created__' in patchsets[number][patch]:
|
|
continue
|
|
|
|
uploaded = datetime.datetime.fromtimestamp(
|
|
patchsets[number][patch]['__created__'])
|
|
obsoleted = datetime.datetime.fromtimestamp(
|
|
patchsets[number].get(str(int(patch) + 1), {}).get(
|
|
'__created__', time.time()))
|
|
valid_for = obsoleted - uploaded
|
|
|
|
if valid_for < datetime.timedelta(hours=3):
|
|
continue
|
|
|
|
valid_patches.append(patch)
|
|
|
|
total_patches += len(valid_patches)
|
|
|
|
for patch in valid_patches:
|
|
for author in patchsets[number][patch]:
|
|
if author == '__created__':
|
|
continue
|
|
|
|
if author not in user_filter:
|
|
continue
|
|
|
|
total_votes.setdefault(author, 0)
|
|
total_votes[author] += 1
|
|
|
|
for vote, msg, sentiment in patchsets[number][patch][author]:
|
|
if sentiment.startswith('Positive'):
|
|
passed_votes.setdefault(author, 0)
|
|
passed_votes[author] += 1
|
|
elif sentiment.startswith('Negative'):
|
|
failed_votes.setdefault(author, 0)
|
|
failed_votes[author] += 1
|
|
else:
|
|
unparsed_votes.setdefault(author, 0)
|
|
unparsed_votes[author] += 1
|
|
|
|
sentiments.setdefault(author, {})
|
|
sentiments[author].setdefault(sentiment, [])
|
|
sentiments[author][sentiment].append(
|
|
'%s,%s' % (number, patch))
|
|
|
|
for author in user_filter:
|
|
if not author in patchsets[number][patch]:
|
|
missed_votes.setdefault(author, [])
|
|
missed_votes[author].append('%s,%s' % (number, patch))
|
|
|
|
with open('%s-cireport.html' % prefix, 'w') as f:
|
|
f.write('<b>Valid patches in report period: %d</b><ul>'
|
|
% total_patches)
|
|
for author in user_filter:
|
|
if not author in total_votes:
|
|
f.write('<li><font color=blue>No votes recorded for '
|
|
'<b>%s</b></font></li>'
|
|
% author)
|
|
continue
|
|
|
|
percentage = (total_votes[author] * 100.0 / total_patches)
|
|
|
|
if percentage < 95.0:
|
|
f.write('<font color=red>')
|
|
|
|
passed = passed_votes.get(author, 0)
|
|
failed = failed_votes.get(author, 0)
|
|
unparsed = unparsed_votes.get(author, 0)
|
|
total = passed + failed + unparsed
|
|
pass_percentage = passed * 100.0 / total
|
|
fail_percentage = failed * 100.0 / total
|
|
unparsed_percentage = unparsed * 100.0 / total
|
|
f.write('<li><b>%s</b> voted on %d patchsets (%.02f%%), '
|
|
'passing %d (%.02f%%), failing %s (%.02f%%) and '
|
|
'unparsed %d (%.02f%%)'
|
|
% (author, total_votes[author], percentage, passed,
|
|
pass_percentage, failed, fail_percentage, unparsed,
|
|
unparsed_percentage))
|
|
|
|
if percentage < 95.0:
|
|
f.write('</font>')
|
|
|
|
f.write('</li><ul><li>Missed %d: %s</li>'
|
|
'<li>Sentiment:</li><ul>'
|
|
% (len(missed_votes.get(author, [])),
|
|
patch_list_as_html(
|
|
missed_votes.get(author, []))))
|
|
for sentiment in conf.SENTIMENTS:
|
|
count = len(sentiments.get(author, {}).get(
|
|
sentiment, []))
|
|
if count > 0:
|
|
f.write('<li>%s: %d' % (sentiment, count ))
|
|
if sentiment != 'Positive':
|
|
f.write('(%s)</li>'
|
|
% patch_list_as_html(
|
|
sentiments[author][sentiment]))
|
|
f.write('</li>')
|
|
f.write('</ul></ul>')
|
|
f.write('</ul><p>What is this report? Why is it so wrong? This report is a quick hack done by Michael Still to visualize the performance of CI systems voting on OpenStack changes. For help, please email him at mikal@stillhq.com.</p>')
|
|
|
|
|
|
if __name__ == '__main__':
|
|
report('openstack/nova', None, 'nova')
|
|
report('openstack/neutron', None, 'neutron')
|
|
|
|
for user in conf.CI_USERS:
|
|
report('*', [user], user.replace(' ', '_'))
|