scripts/contrib: add oe-build-perf-report-email

Script for sending build perf test reports as an email. Mangles an html
report, generated by oe-build-perf-report, into a format suitable for
html emails. Supports multipart emails where a plaintext alternative can
be included in the same email.

Dependencies required to be installed on the host:
- phantomjs
- optipng

[YOCTO #10931]

(From OE-Core rev: 9e97ff174458f7245fc27a4c407f21a9d2e317ab)

Signed-off-by: Markus Lehtonen <markus.lehtonen@linux.intel.com>
Signed-off-by: Richard Purdie <richard.purdie@linuxfoundation.org>
This commit is contained in:
Markus Lehtonen 2017-03-31 17:07:30 +03:00 committed by Richard Purdie
parent 9f299876f7
commit 034702f520
2 changed files with 322 additions and 0 deletions

View File

@ -0,0 +1,266 @@
#!/usr/bin/python3
#
# Send build performance test report emails
#
# Copyright (c) 2017, Intel Corporation.
#
# This program is free software; you can redistribute it and/or modify it
# under the terms and conditions of the GNU General Public License,
# version 2, as published by the Free Software Foundation.
#
# This program is distributed in the hope it will be useful, but WITHOUT
# ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or
# FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for
# more details.
#
import argparse
import base64
import logging
import os
import pwd
import re
import shutil
import smtplib
import subprocess
import sys
import tempfile
from email.mime.multipart import MIMEMultipart
from email.mime.text import MIMEText
# Setup logging
logging.basicConfig(level=logging.INFO, format="%(levelname)s: %(message)s")
log = logging.getLogger('oe-build-perf-report')
# Find js scaper script
SCRAPE_JS = os.path.join(os.path.dirname(__file__), '..', 'lib', 'build_perf',
'scrape-html-report.js')
if not os.path.isfile(SCRAPE_JS):
log.error("Unableto find oe-build-perf-report-scrape.js")
sys.exit(1)
class ReportError(Exception):
"""Local errors"""
pass
def check_utils():
"""Check that all needed utils are installed in the system"""
missing = []
for cmd in ('phantomjs', 'optipng'):
if not shutil.which(cmd):
missing.append(cmd)
if missing:
log.error("The following tools are missing: %s", ' '.join(missing))
sys.exit(1)
def parse_args(argv):
"""Parse command line arguments"""
description = """Email build perf test report"""
parser = argparse.ArgumentParser(
formatter_class=argparse.ArgumentDefaultsHelpFormatter,
description=description)
parser.add_argument('--debug', '-d', action='store_true',
help="Verbose logging")
parser.add_argument('--quiet', '-q', action='store_true',
help="Only print errors")
parser.add_argument('--to', action='append',
help="Recipients of the email")
parser.add_argument('--subject', default="Yocto build perf test report",
help="Email subject")
parser.add_argument('--outdir', '-o',
help="Store files in OUTDIR. Can be used to preserve "
"the email parts")
parser.add_argument('--text',
help="Plain text message")
parser.add_argument('--html',
help="HTML peport generated by oe-build-perf-report")
parser.add_argument('--phantomjs-args', action='append',
help="Extra command line arguments passed to PhantomJS")
args = parser.parse_args(argv)
if not args.html and not args.text:
parser.error("Please specify --html and/or --text")
return args
def decode_png(infile, outfile):
"""Parse/decode/optimize png data from a html element"""
with open(infile) as f:
raw_data = f.read()
# Grab raw base64 data
b64_data = re.sub('^.*href="data:image/png;base64,', '', raw_data, 1)
b64_data = re.sub('">.+$', '', b64_data, 1)
# Replace file with proper decoded png
with open(outfile, 'wb') as f:
f.write(base64.b64decode(b64_data))
subprocess.check_output(['optipng', outfile], stderr=subprocess.STDOUT)
def encode_png(pngfile):
"""Encode png into a <img> html element"""
with open(pngfile, 'rb') as f:
data = f.read()
b64_data = base64.b64encode(data)
return '<img src="data:image/png;base64,' + b64_data.decode('utf-8') + '">\n'
def mangle_html_report(infile, outfile, pngs):
"""Mangle html file into a email compatible format"""
paste = True
png_dir = os.path.dirname(outfile)
with open(infile) as f_in:
with open(outfile, 'w') as f_out:
for line in f_in.readlines():
stripped = line.strip()
# Strip out scripts
if stripped == '<!--START-OF-SCRIPTS-->':
paste = False
elif stripped == '<!--END-OF-SCRIPTS-->':
paste = True
elif paste:
if re.match('^.+href="data:image/png;base64', stripped):
# Strip out encoded pngs (as they're huge in size)
continue
elif 'www.gstatic.com' in stripped:
# HACK: drop references to external static pages
continue
# Replace charts with <img> elements
match = re.match('<div id="(?P<id>\w+)"', stripped)
if match and match.group('id') in pngs:
#f_out.write('<img src="{}">\n'.format(match.group('id') + '.png'))
png_file = os.path.join(png_dir, match.group('id') + '.png')
f_out.write(encode_png(png_file))
else:
f_out.write(line)
def scrape_html_report(report, outdir, phantomjs_extra_args=None):
"""Scrape html report into a format sendable by email"""
tmpdir = tempfile.mkdtemp(dir='.')
log.debug("Using tmpdir %s for phantomjs output", tmpdir)
if not os.path.isdir(outdir):
os.mkdir(outdir)
if os.path.splitext(report)[1] not in ('.html', '.htm'):
raise ReportError("Invalid file extension for report, needs to be "
"'.html' or '.htm'")
try:
log.info("Scraping HTML report with PhangomJS")
extra_args = phantomjs_extra_args if phantomjs_extra_args else []
subprocess.check_output(['phantomjs', '--debug=true'] + extra_args +
[SCRAPE_JS, report, tmpdir],
stderr=subprocess.STDOUT)
pngs = []
attachments = []
for fname in os.listdir(tmpdir):
base, ext = os.path.splitext(fname)
if ext == '.png':
log.debug("Decoding %s", fname)
decode_png(os.path.join(tmpdir, fname),
os.path.join(outdir, fname))
pngs.append(base)
attachments.append(fname)
elif ext in ('.html', '.htm'):
report_file = fname
else:
log.warning("Unknown file extension: '%s'", ext)
#shutil.move(os.path.join(tmpdir, fname), outdir)
log.debug("Mangling html report file %s", report_file)
mangle_html_report(os.path.join(tmpdir, report_file),
os.path.join(outdir, report_file), pngs)
return report_file, attachments
finally:
shutil.rmtree(tmpdir)
def send_email(text_fn, html_fn, subject, recipients):
"""Send email"""
# Generate email message
text_msg = html_msg = None
if text_fn:
with open(text_fn) as f:
text_msg = MIMEText("Yocto build performance test report.\n" +
f.read(), 'plain')
if html_fn:
with open(html_fn) as f:
html_msg = MIMEText(f.read(), 'html')
if text_msg and html_msg:
msg = MIMEMultipart('alternative')
msg.attach(text_msg)
msg.attach(html_msg)
elif text_msg:
msg = text_msg
elif html_msg:
msg = html_msg
else:
raise ReportError("Neither plain text nor html body specified")
full_name = pwd.getpwuid(os.getuid()).pw_gecos.split(',')[0]
email = os.environ.get('EMAIL', os.getlogin())
msg['From'] = "{} <{}>".format(full_name, email)
msg['To'] = ', '.join(recipients)
msg['Subject'] = subject
# Send email
with smtplib.SMTP('localhost') as smtp:
smtp.send_message(msg)
def main(argv=None):
"""Script entry point"""
args = parse_args(argv)
if args.quiet:
log.setLevel(logging.ERROR)
if args.debug:
log.setLevel(logging.DEBUG)
check_utils()
if args.outdir:
outdir = args.outdir
if not os.path.exists(outdir):
os.mkdir(outdir)
else:
outdir = tempfile.mkdtemp(dir='.')
try:
log.debug("Storing email parts in %s", outdir)
html_report = None
if args.html:
scrape_html_report(args.html, outdir, args.phantomjs_args)
html_report = os.path.join(outdir, args.html)
if args.to:
log.info("Sending email to %s", ', '.join(args.to))
send_email(args.text, html_report, args.subject, args.to)
except subprocess.CalledProcessError as err:
log.error("%s, with output:\n%s", str(err), err.output.decode())
return 1
except ReportError as err:
log.error(err)
return 1
finally:
if not args.outdir:
log.debug("Wiping %s", outdir)
shutil.rmtree(outdir)
return 0
if __name__ == "__main__":
sys.exit(main())

View File

@ -0,0 +1,56 @@
var fs = require('fs');
var system = require('system');
var page = require('webpage').create();
// Examine console log for message from chart drawing
page.onConsoleMessage = function(msg) {
console.log(msg);
if (msg === "ALL CHARTS READY") {
window.charts_ready = true;
}
else if (msg.slice(0, 11) === "CHART READY") {
var chart_id = msg.split(" ")[2];
console.log('grabbing ' + chart_id);
var png_data = page.evaluate(function (chart_id) {
var chart_div = document.getElementById(chart_id + '_png');
return chart_div.outerHTML;
}, chart_id);
fs.write(args[2] + '/' + chart_id + '.png', png_data, 'w');
}
};
// Check command line arguments
var args = system.args;
if (args.length != 3) {
console.log("USAGE: " + args[0] + " REPORT_HTML OUT_DIR\n");
phantom.exit(1);
}
// Open the web page
page.open(args[1], function(status) {
if (status == 'fail') {
console.log("Failed to open file '" + args[1] + "'");
phantom.exit(1);
}
});
// Check status every 100 ms
interval = window.setInterval(function () {
//console.log('waiting');
if (window.charts_ready) {
clearTimeout(timer);
clearInterval(interval);
var fname = args[1].replace(/\/+$/, "").split("/").pop()
console.log("saving " + fname);
fs.write(args[2] + '/' + fname, page.content, 'w');
phantom.exit(0);
}
}, 100);
// Time-out after 10 seconds
timer = window.setTimeout(function () {
clearInterval(interval);
console.log("ERROR: timeout");
phantom.exit(1);
}, 10000);