#!/usr/bin/python """ Module used to parse the autotest job results and generate an HTML report. @copyright: (c)2005-2007 Matt Kruse (javascripttoolbox.com) @copyright: Red Hat 2008-2009 @author: Dror Russo (drusso@redhat.com) """ import os, sys, re, getopt, time, datetime, commands import common format_css = """ html,body { padding:0; color:#222; background:#FFFFFF; } body { padding:0px; font:76%/150% "Lucida Grande", "Lucida Sans Unicode", Lucida, Verdana, Geneva, Arial, Helvetica, sans-serif; } #page_title{ text-decoration:none; font:bold 2em/2em Arial, Helvetica, sans-serif; text-transform:none; text-align: left; color:#555555; border-bottom: 1px solid #555555; } #page_sub_title{ text-decoration:none; font:bold 16px Arial, Helvetica, sans-serif; text-transform:uppercase; text-align: left; color:#555555; margin-bottom:0; } #comment{ text-decoration:none; font:bold 10px Arial, Helvetica, sans-serif; text-transform:none; text-align: left; color:#999999; margin-top:0; } #meta_headline{ text-decoration:none; font-family: Verdana, Geneva, Arial, Helvetica, sans-serif ; text-align: left; color:black; font-weight: bold; font-size: 14px; } table.meta_table {text-align: center; font-family: Verdana, Geneva, Arial, Helvetica, sans-serif ; width: 90%; background-color: #FFFFFF; border: 0px; border-top: 1px #003377 solid; border-bottom: 1px #003377 solid; border-right: 1px #003377 solid; border-left: 1px #003377 solid; border-collapse: collapse; border-spacing: 0px;} table.meta_table td {background-color: #FFFFFF; color: #000; padding: 4px; border-top: 1px #BBBBBB solid; border-bottom: 1px #BBBBBB solid; font-weight: normal; font-size: 13px;} table.stats {text-align: center; font-family: Verdana, Geneva, Arial, Helvetica, sans-serif ; width: 100%; background-color: #FFFFFF; border: 0px; border-top: 1px #003377 solid; border-bottom: 1px #003377 solid; border-right: 1px #003377 solid; border-left: 1px #003377 solid; border-collapse: collapse; border-spacing: 0px;} table.stats td{ background-color: #FFFFFF; color: #000; padding: 4px; border-top: 1px #BBBBBB solid; border-bottom: 1px #BBBBBB solid; font-weight: normal; font-size: 11px;} table.stats th{ background: #dcdcdc; color: #000; padding: 6px; font-size: 12px; border-bottom: 1px #003377 solid; font-weight: bold;} table.stats td.top{ background-color: #dcdcdc; color: #000; padding: 6px; text-align: center; border: 0px; border-bottom: 1px #003377 solid; font-size: 10px; font-weight: bold;} table.stats th.table-sorted-asc{ background-image: url(ascending.gif); background-position: top left ; background-repeat: no-repeat; } table.stats th.table-sorted-desc{ background-image: url(descending.gif); background-position: top left; background-repeat: no-repeat; } table.stats2 {text-align: left; font-family: Verdana, Geneva, Arial, Helvetica, sans-serif ; width: 100%; background-color: #FFFFFF; border: 0px; } table.stats2 td{ background-color: #FFFFFF; color: #000; padding: 0px; font-weight: bold; font-size: 13px;} /* Put this inside a @media qualifier so Netscape 4 ignores it */ @media screen, print { /* Turn off list bullets */ ul.mktree li { list-style: none; } /* Control how "spaced out" the tree is */ ul.mktree, ul.mktree ul , ul.mktree li { margin-left:10px; padding:0px; } /* Provide space for our own "bullet" inside the LI */ ul.mktree li .bullet { padding-left: 15px; } /* Show "bullets" in the links, depending on the class of the LI that the link's in */ ul.mktree li.liOpen .bullet { cursor: pointer; } ul.mktree li.liClosed .bullet { cursor: pointer; } ul.mktree li.liBullet .bullet { cursor: default; } /* Sublists are visible or not based on class of parent LI */ ul.mktree li.liOpen ul { display: block; } ul.mktree li.liClosed ul { display: none; } /* Format menu items differently depending on what level of the tree they are in */ /* Uncomment this if you want your fonts to decrease in size the deeper they are in the tree */ /* ul.mktree li ul li { font-size: 90% } */ } """ table_js = """ /** * Copyright (c)2005-2007 Matt Kruse (javascripttoolbox.com) * * Dual licensed under the MIT and GPL licenses. * This basically means you can use this code however you want for * free, but don't claim to have written it yourself! * Donations always accepted: http://www.JavascriptToolbox.com/donate/ * * Please do not link to the .js files on javascripttoolbox.com from * your site. Copy the files locally to your server instead. * */ /** * Table.js * Functions for interactive Tables * * Copyright (c) 2007 Matt Kruse (javascripttoolbox.com) * Dual licensed under the MIT and GPL licenses. * * @version 0.981 * * @history 0.981 2007-03-19 Added Sort.numeric_comma, additional date parsing formats * @history 0.980 2007-03-18 Release new BETA release pending some testing. Todo: Additional docs, examples, plus jQuery plugin. * @history 0.959 2007-03-05 Added more "auto" functionality, couple bug fixes * @history 0.958 2007-02-28 Added auto functionality based on class names * @history 0.957 2007-02-21 Speed increases, more code cleanup, added Auto Sort functionality * @history 0.956 2007-02-16 Cleaned up the code and added Auto Filter functionality. * @history 0.950 2006-11-15 First BETA release. * * @todo Add more date format parsers * @todo Add style classes to colgroup tags after sorting/filtering in case the user wants to highlight the whole column * @todo Correct for colspans in data rows (this may slow it down) * @todo Fix for IE losing form control values after sort? */ /** * Sort Functions */ var Sort = (function(){ var sort = {}; // Default alpha-numeric sort // -------------------------- sort.alphanumeric = function(a,b) { return (a==b)?0:(asort.numeric.convert(b)) { return (-1); } if (sort.numeric.convert(a)==sort.numeric.convert(b)) { return 0; } if (sort.numeric.convert(a)0) { var rows = section.rows; for (var j=0,L2=rows.length; j0) { var cells = row.cells; for (var k=0,L3=cells.length; k1 && cells[cells.length-1].cellIndex>0) { // Define the new function, overwrite the one we're running now, and then run the new one (this.getCellIndex = function(td) { return td.cellIndex; })(td); } // Safari will always go through this slower block every time. Oh well. for (var i=0,L=cells.length; i=0 && node.options) { // Sort select elements by the visible text return node.options[node.selectedIndex].text; } return ""; }, 'IMG':function(node) { return node.name || ""; } }; /** * Get the text value of a cell. Only use innerText if explicitly told to, because * otherwise we want to be able to handle sorting on inputs and other types */ table.getCellValue = function(td,useInnerText) { if (useInnerText && def(td.innerText)) { return td.innerText; } if (!td.childNodes) { return ""; } var childNodes=td.childNodes; var ret = ""; for (var i=0,L=childNodes.length; i-1) { filters={ 'filter':filters.options[filters.selectedIndex].value }; } // Also allow for a regular input if (filters.nodeName=="INPUT" && filters.type=="text") { filters={ 'filter':"/"+filters.value+"/" }; } // Force filters to be an array if (typeof(filters)=="object" && !filters.length) { filters = [filters]; } // Convert regular expression strings to RegExp objects and function strings to function objects for (var i=0,L=filters.length; ipageend) { hideRow = true; } } } row.style.display = hideRow?"none":""; } } if (def(page)) { // Check to see if filtering has put us past the requested page index. If it has, // then go back to the last page and show it. if (pagestart>=unfilteredrowcount) { pagestart = unfilteredrowcount-(unfilteredrowcount%pagesize); tdata.page = page = pagestart/pagesize; for (var i=pagestart,L=unfilteredrows.length; i0) { if (typeof(args.insert)=="function") { func.insert(cell,colValues); } else { var sel = ''; cell.innerHTML += "
"+sel; } } } }); if (val = classValue(t,table.FilteredRowcountPrefix)) { tdata.container_filtered_count = document.getElementById(val); } if (val = classValue(t,table.RowcountPrefix)) { tdata.container_all_count = document.getElementById(val); } }; /** * Attach the auto event so it happens on load. * use jQuery's ready() function if available */ if (typeof(jQuery)!="undefined") { jQuery(table.auto); } else if (window.addEventListener) { window.addEventListener( "load", table.auto, false ); } else if (window.attachEvent) { window.attachEvent( "onload", table.auto ); } return table; })(); """ maketree_js = """/** * Copyright (c)2005-2007 Matt Kruse (javascripttoolbox.com) * * Dual licensed under the MIT and GPL licenses. * This basically means you can use this code however you want for * free, but don't claim to have written it yourself! * Donations always accepted: http://www.JavascriptToolbox.com/donate/ * * Please do not link to the .js files on javascripttoolbox.com from * your site. Copy the files locally to your server instead. * */ /* This code is inspired by and extended from Stuart Langridge's aqlist code: http://www.kryogenix.org/code/browser/aqlists/ Stuart Langridge, November 2002 sil@kryogenix.org Inspired by Aaron's labels.js (http://youngpup.net/demos/labels/) and Dave Lindquist's menuDropDown.js (http://www.gazingus.org/dhtml/?id=109) */ // Automatically attach a listener to the window onload, to convert the trees addEvent(window,"load",convertTrees); // Utility function to add an event listener function addEvent(o,e,f){ if (o.addEventListener){ o.addEventListener(e,f,false); return true; } else if (o.attachEvent){ return o.attachEvent("on"+e,f); } else { return false; } } // utility function to set a global variable if it is not already set function setDefault(name,val) { if (typeof(window[name])=="undefined" || window[name]==null) { window[name]=val; } } // Full expands a tree with a given ID function expandTree(treeId) { var ul = document.getElementById(treeId); if (ul == null) { return false; } expandCollapseList(ul,nodeOpenClass); } // Fully collapses a tree with a given ID function collapseTree(treeId) { var ul = document.getElementById(treeId); if (ul == null) { return false; } expandCollapseList(ul,nodeClosedClass); } // Expands enough nodes to expose an LI with a given ID function expandToItem(treeId,itemId) { var ul = document.getElementById(treeId); if (ul == null) { return false; } var ret = expandCollapseList(ul,nodeOpenClass,itemId); if (ret) { var o = document.getElementById(itemId); if (o.scrollIntoView) { o.scrollIntoView(false); } } } // Performs 3 functions: // a) Expand all nodes // b) Collapse all nodes // c) Expand all nodes to reach a certain ID function expandCollapseList(ul,cName,itemId) { if (!ul.childNodes || ul.childNodes.length==0) { return false; } // Iterate LIs for (var itemi=0;itemi Autotest job execution results """ % (format_css, table_js, maketree_js) if output_file_name: output = open(output_file_name, "w") else: #if no output file defined, print html file to console output = sys.stdout # create html page print >> output, html_prefix print >> output, '

Autotest job execution report

' # formating date and time to print t = datetime.datetime.now() epoch_sec = time.mktime(t.timetuple()) now = datetime.datetime.fromtimestamp(epoch_sec) # basic statistics total_executed = 0 total_failed = 0 total_passed = 0 for res in results: if results[res][2] != None: total_executed += 1 if results[res][2]['status'] == 'GOOD': total_passed += 1 else: total_failed += 1 stat_str = 'No test cases executed' if total_executed > 0: failed_perct = int(float(total_failed)/float(total_executed)*100) stat_str = ('From %d tests executed, %d have passed (%d%% failures)' % (total_executed, total_passed, failed_perct)) kvm_ver_str = metadata.get('kvmver', None) print >> output, '' print >> output, '' % host print >> output, '' % tag print >> output, '' % now.ctime() print >> output, ''% stat_str print >> output, '' if kvm_ver_str is not None: print >> output, '' % kvm_ver_str print >> output, '
HOST:%s
RESULTS DIR:%s
DATE:%s
STATS:%s
KVM VERSION:%s
' ## print test results print >> output, '
' print >> output, '

Test Results

' print >> output, '

click on table headers to asc/desc sort

' result_table_prefix = """ """ print >> output, result_table_prefix def print_result(result, indent): while result != []: r = result.pop(0) res = results[r][2] print >> output, '' print >> output, '' % res['time'] print >> output, '' % (indent * 20, res['title']) if res['status'] == 'GOOD': print >> output, '' elif res['status'] == 'FAIL': print >> output, '' elif res['status'] == 'ERROR': print >> output, '' else: print >> output, '' % res['status'] # print exec time (seconds) print >> output, '' % res['exec_time_sec'] # print log only if test failed.. if res['log']: #chop all '\n' from log text (to prevent html errors) rx1 = re.compile('(\s+)') log_text = rx1.sub(' ', res['log']) # allow only a-zA-Z0-9_ in html title name # (due to bug in MS-explorer) rx2 = re.compile('([^a-zA-Z_0-9])') updated_tag = rx2.sub('_', res['title']) html_body_text = '%s%s' % (str(updated_tag), log_text) print >> output, '' % (str(updated_tag), str(html_body_text)) else: print >> output, '' # print execution time print >> output, '' % os.path.join(dirname, res['subdir'], "debug") print >> output, '' print_result(results[r][1], indent + 1) print_result(results[""][1], 0) print >> output, "
Date/Time Test Case
Status Time (sec) Info Debug
%s%sPASSFAILERROR!%s%sInfoDebug
" print >> output, '

Host Info

' print >> output, '

click on each item to expend/collapse

' ## Meta list comes here.. print >> output, '

' print >> output, 'Expand All' print >> output, '   ' print >> output, 'Collapse All' print >> output, '

' print >> output, '
    ' counter = 0 keys = metadata.keys() keys.sort() for key in keys: val = metadata[key] print >> output, '
  • %s' % key print >> output, '
      %s
  • ' % val print >> output, '
' print >> output, "" if output_file_name: output.close() def parse_result(dirname, line, results_data): """ Parse job status log line. @param dirname: Job results dir @param line: Status log line. @param results_data: Dictionary with for results. """ parts = line.split() if len(parts) < 4: return None global tests if parts[0] == 'START': pair = parts[3].split('=') stime = int(pair[1]) results_data[parts[1]] = [stime, [], None] try: parent_test = re.findall(r".*/", parts[1])[0][:-1] results_data[parent_test][1].append(parts[1]) except IndexError: results_data[""][1].append(parts[1]) elif (parts[0] == 'END'): result = {} exec_time = '' # fetch time stamp if len(parts) > 7: temp = parts[5].split('=') exec_time = temp[1] + ' ' + parts[6] + ' ' + parts[7] # assign default values result['time'] = exec_time result['testcase'] = 'na' result['status'] = 'na' result['log'] = None result['exec_time_sec'] = 'na' tag = parts[3] result['subdir'] = parts[2] # assign actual values rx = re.compile('^(\w+)\.(.*)$') m1 = rx.findall(parts[3]) if len(m1): result['testcase'] = m1[0][1] else: result['testcase'] = parts[3] result['title'] = str(tag) result['status'] = parts[1] if result['status'] != 'GOOD': result['log'] = get_exec_log(dirname, tag) if len(results_data)>0: pair = parts[4].split('=') etime = int(pair[1]) stime = results_data[parts[2]][0] total_exec_time_sec = etime - stime result['exec_time_sec'] = total_exec_time_sec results_data[parts[2]][2] = result return None def get_exec_log(resdir, tag): """ Get job execution summary. @param resdir: Job results dir. @param tag: Job tag. """ stdout_file = os.path.join(resdir, tag, 'debug', 'stdout') stderr_file = os.path.join(resdir, tag, 'debug', 'stderr') status_file = os.path.join(resdir, tag, 'status') dmesg_file = os.path.join(resdir, tag, 'sysinfo', 'dmesg') log = '' log += '
STDERR:
' log += get_info_file(stderr_file) log += '
STDOUT:
' log += get_info_file(stdout_file) log += '
STATUS:
' log += get_info_file(status_file) log += '
DMESG:
' log += get_info_file(dmesg_file) return log def get_info_file(filename): """ Gets the contents of an autotest info file. It also and highlights the file contents with possible problems. @param filename: Info file path. """ data = '' errors = re.compile(r"\b(error|fail|failed)\b", re.IGNORECASE) if os.path.isfile(filename): f = open('%s' % filename, "r") lines = f.readlines() f.close() rx = re.compile('(\'|\")') for line in lines: new_line = rx.sub('', line) errors_found = errors.findall(new_line) if len(errors_found) > 0: data += '%s
' % str(new_line) else: data += '%s
' % str(new_line) if not data: data = 'No Information Found.
' else: data = 'File not found.
' return data def usage(): """ Print stand alone program usage. """ print 'usage:', print 'make_html_report.py -r [-f output_file] [-R]' print '(e.g. make_html_reporter.py -r '\ '/usr/local/autotest/client/results/default -f /tmp/myreport.html)' print 'add "-R" for an html report with relative-paths (relative '\ 'to results directory)' print '' sys.exit(1) def get_keyval_value(result_dir, key): """ Return the value of the first appearance of key in any keyval file in result_dir. If no appropriate line is found, return 'Unknown'. @param result_dir: Path that holds the keyval files. @param key: Specific key we're retrieving. """ keyval_pattern = os.path.join(result_dir, "kvm.*", "keyval") keyval_lines = commands.getoutput(r"grep -h '\b%s\b.*=' %s" % (key, keyval_pattern)) if not keyval_lines: return "Unknown" keyval_line = keyval_lines.splitlines()[0] if key in keyval_line and "=" in keyval_line: return keyval_line.split("=")[1].strip() else: return "Unknown" def get_kvm_version(result_dir): """ Return an HTML string describing the KVM version. @param result_dir: An Autotest job result dir. """ kvm_version = get_keyval_value(result_dir, "kvm_version") kvm_userspace_version = get_keyval_value(result_dir, "kvm_userspace_version") if kvm_version == "Unknown" or kvm_userspace_version == "Unknown": return None return "Kernel: %s
Userspace: %s" % (kvm_version, kvm_userspace_version) def create_report(dirname, html_path='', output_file_name=None): """ Create an HTML report with info about an autotest client job. If no relative path (html_path) or output file name provided, an HTML file in the toplevel job results dir called 'job_report.html' will be created, with relative links. @param html_path: Prefix for the HTML links. Useful to specify absolute in the report (not wanted most of the time). @param output_file_name: Path to the report file. """ res_dir = os.path.abspath(dirname) tag = res_dir status_file_name = os.path.join(dirname, 'status') sysinfo_dir = os.path.join(dirname, 'sysinfo') host = get_info_file(os.path.join(sysinfo_dir, 'hostname')) rx = re.compile('^\s+[END|START].*$') # create the results set dict results_data = {} results_data[""] = [0, [], None] if os.path.exists(status_file_name): f = open(status_file_name, "r") lines = f.readlines() f.close() for line in lines: if rx.match(line): parse_result(dirname, line, results_data) # create the meta info dict metalist = { 'uname': get_info_file(os.path.join(sysinfo_dir, 'uname')), 'cpuinfo':get_info_file(os.path.join(sysinfo_dir, 'cpuinfo')), 'meminfo':get_info_file(os.path.join(sysinfo_dir, 'meminfo')), 'df':get_info_file(os.path.join(sysinfo_dir, 'df')), 'modules':get_info_file(os.path.join(sysinfo_dir, 'modules')), 'gcc':get_info_file(os.path.join(sysinfo_dir, 'gcc_--version')), 'dmidecode':get_info_file(os.path.join(sysinfo_dir, 'dmidecode')), 'dmesg':get_info_file(os.path.join(sysinfo_dir, 'dmesg')), } if get_kvm_version(dirname) is not None: metalist['kvm_ver'] = get_kvm_version(dirname) if output_file_name is None: output_file_name = os.path.join(dirname, 'job_report.html') make_html_file(metalist, results_data, tag, host, output_file_name, html_path) def main(argv): """ Parses the arguments and executes the stand alone program. """ dirname = None output_file_name = None relative_path = False try: opts, args = getopt.getopt(argv, "r:f:h:R", ['help']) except getopt.GetoptError: usage() sys.exit(2) for opt, arg in opts: if opt in ("-h", "--help"): usage() sys.exit() elif opt == '-r': dirname = arg elif opt == '-f': output_file_name = arg elif opt == '-R': relative_path = True else: usage() sys.exit(1) html_path = dirname # don't use absolute path in html output if relative flag passed if relative_path: html_path = '' if dirname: if os.path.isdir(dirname): # TBD: replace it with a validation of # autotest result dir create_report(dirname, html_path, output_file_name) sys.exit(0) else: print 'Invalid result directory <%s>' % dirname sys.exit(1) else: usage() sys.exit(1) if __name__ == "__main__": main(sys.argv[1:])