The programs cd_hash, html_report, scan_results can be used by other users of autotest, so move them to the tools directory inside the client directory. This patch contains bugfixes suggested by Amos Kong. Signed-off-by: Lucas Meneghel Rodrigues <lmr@xxxxxxxxxx> Signed-off-by: Amos Kong <akong@xxxxxxxxxx> --- client/tools/cd_hash.py | 48 ++ client/tools/common.py | 8 + client/tools/html_report.py | 1727 ++++++++++++++++++++++++++++++++++++++++++ client/tools/scan_results.py | 97 +++ 4 files changed, 1880 insertions(+), 0 deletions(-) create mode 100644 client/tools/__init__.py create mode 100755 client/tools/cd_hash.py create mode 100644 client/tools/common.py create mode 100755 client/tools/html_report.py create mode 100755 client/tools/scan_results.py diff --git a/client/tools/__init__.py b/client/tools/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/client/tools/cd_hash.py b/client/tools/cd_hash.py new file mode 100755 index 0000000..3db1e47 --- /dev/null +++ b/client/tools/cd_hash.py @@ -0,0 +1,48 @@ +#!/usr/bin/python +""" +Program that calculates several hashes for a given CD image. + +@copyright: Red Hat 2008-2009 +""" + +import os, sys, optparse, logging +import common +from autotest_lib.client.common_lib import logging_manager +from autotest_lib.client.bin import utils +from autotest_lib.client.virt import virt_utils + + +if __name__ == "__main__": + parser = optparse.OptionParser("usage: %prog [options] [filenames]") + options, args = parser.parse_args() + + logging_manager.configure_logging(virt_utils.VirtLoggingConfig()) + + if args: + filenames = args + else: + parser.print_help() + sys.exit(1) + + for filename in filenames: + filename = os.path.abspath(filename) + + file_exists = os.path.isfile(filename) + can_read_file = os.access(filename, os.R_OK) + if not file_exists: + logging.critical("File %s does not exist!", filename) + continue + if not can_read_file: + logging.critical("File %s does not have read permissions!", + filename) + continue + + logging.info("Hash values for file %s", os.path.basename(filename)) + logging.info("md5 (1m): %s", utils.hash_file(filename, 1024*1024, + method="md5")) + logging.info("sha1 (1m): %s", utils.hash_file(filename, 1024*1024, + method="sha1")) + logging.info("md5 (full): %s", utils.hash_file(filename, method="md5")) + logging.info("sha1 (full): %s", utils.hash_file(filename, + method="sha1")) + logging.info("") diff --git a/client/tools/common.py b/client/tools/common.py new file mode 100644 index 0000000..6881386 --- /dev/null +++ b/client/tools/common.py @@ -0,0 +1,8 @@ +import os, sys +dirname = os.path.dirname(sys.modules[__name__].__file__) +client_dir = os.path.abspath(os.path.join(dirname, "..")) +sys.path.insert(0, client_dir) +import setup_modules +sys.path.pop(0) +setup_modules.setup(base_path=client_dir, + root_module_name="autotest_lib.client") diff --git a/client/tools/html_report.py b/client/tools/html_report.py new file mode 100755 index 0000000..8b4b109 --- /dev/null +++ b/client/tools/html_report.py @@ -0,0 +1,1727 @@ +#!/usr/bin/python +""" +Script used to parse the test results and generate an HTML report. + +@copyright: (c)2005-2007 Matt Kruse (javascripttoolbox.com) +@copyright: Red Hat 2008-2009 +@author: Dror Russo (drusso@xxxxxxxxxx) +""" + +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-shadow: 2px 2px 2px #555; + 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-shadow: 2px 2px 2px #555; + 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:(a<b)?-1:1; + }; + sort.alphanumeric_rev = function(a,b) { + return (a==b)?0:(a<b)?1:-1; + }; + sort['default'] = sort.alphanumeric; // IE chokes on sort.default + + // This conversion is generalized to work for either a decimal separator of , or . + sort.numeric_converter = function(separator) { + return function(val) { + if (typeof(val)=="string") { + val = parseFloat(val.replace(/^[^\d\.]*([\d., ]+).*/g,"$1").replace(new RegExp("[^\\\d"+separator+"]","g"),'').replace(/,/,'.')) || 0; + } + return val || 0; + }; + }; + + // Numeric Reversed Sort + // ------------ + sort.numeric_rev = function(a,b) { + if (sort.numeric.convert(a)>sort.numeric.convert(b)) { + return (-1); + } + if (sort.numeric.convert(a)==sort.numeric.convert(b)) { + return 0; + } + if (sort.numeric.convert(a)<sort.numeric.convert(b)) { + return 1; + } + }; + + + // Numeric Sort + // ------------ + sort.numeric = function(a,b) { + return sort.numeric.convert(a)-sort.numeric.convert(b); + }; + sort.numeric.convert = sort.numeric_converter("."); + + // Numeric Sort - comma decimal separator + // -------------------------------------- + sort.numeric_comma = function(a,b) { + return sort.numeric_comma.convert(a)-sort.numeric_comma.convert(b); + }; + sort.numeric_comma.convert = sort.numeric_converter(","); + + // Case-insensitive Sort + // --------------------- + sort.ignorecase = function(a,b) { + return sort.alphanumeric(sort.ignorecase.convert(a),sort.ignorecase.convert(b)); + }; + sort.ignorecase.convert = function(val) { + if (val==null) { return ""; } + return (""+val).toLowerCase(); + }; + + // Currency Sort + // ------------- + sort.currency = sort.numeric; // Just treat it as numeric! + sort.currency_comma = sort.numeric_comma; + + // Date sort + // --------- + sort.date = function(a,b) { + return sort.numeric(sort.date.convert(a),sort.date.convert(b)); + }; + // Convert 2-digit years to 4 + sort.date.fixYear=function(yr) { + yr = +yr; + if (yr<50) { yr += 2000; } + else if (yr<100) { yr += 1900; } + return yr; + }; + sort.date.formats = [ + // YY[YY]-MM-DD + { re:/(\d{2,4})-(\d{1,2})-(\d{1,2})/ , f:function(x){ return (new Date(sort.date.fixYear(x[1]),+x[2],+x[3])).getTime(); } } + // MM/DD/YY[YY] or MM-DD-YY[YY] + ,{ re:/(\d{1,2})[\/-](\d{1,2})[\/-](\d{2,4})/ , f:function(x){ return (new Date(sort.date.fixYear(x[3]),+x[1],+x[2])).getTime(); } } + // Any catch-all format that new Date() can handle. This is not reliable except for long formats, for example: 31 Jan 2000 01:23:45 GMT + ,{ re:/(.*\d{4}.*\d+:\d+\d+.*)/, f:function(x){ var d=new Date(x[1]); if(d){return d.getTime();} } } + ]; + sort.date.convert = function(val) { + var m,v, f = sort.date.formats; + for (var i=0,L=f.length; i<L; i++) { + if (m=val.match(f[i].re)) { + v=f[i].f(m); + if (typeof(v)!="undefined") { return v; } + } + } + return 9999999999999; // So non-parsed dates will be last, not first + }; + + return sort; +})(); + +/** + * The main Table namespace + */ +var Table = (function(){ + + /** + * Determine if a reference is defined + */ + function def(o) {return (typeof o!="undefined");}; + + /** + * Determine if an object or class string contains a given class. + */ + function hasClass(o,name) { + return new RegExp("(^|\\\s)"+name+"(\\\s|$)").test(o.className); + }; + + /** + * Add a class to an object + */ + function addClass(o,name) { + var c = o.className || ""; + if (def(c) && !hasClass(o,name)) { + o.className += (c?" ":"") + name; + } + }; + + /** + * Remove a class from an object + */ + function removeClass(o,name) { + var c = o.className || ""; + o.className = c.replace(new RegExp("(^|\\\s)"+name+"(\\\s|$)"),"$1"); + }; + + /** + * For classes that match a given substring, return the rest + */ + function classValue(o,prefix) { + var c = o.className; + if (c.match(new RegExp("(^|\\\s)"+prefix+"([^ ]+)"))) { + return RegExp.$2; + } + return null; + }; + + /** + * Return true if an object is hidden. + * This uses the "russian doll" technique to unwrap itself to the most efficient + * function after the first pass. This avoids repeated feature detection that + * would always fall into the same block of code. + */ + function isHidden(o) { + if (window.getComputedStyle) { + var cs = window.getComputedStyle; + return (isHidden = function(o) { + return 'none'==cs(o,null).getPropertyValue('display'); + })(o); + } + else if (window.currentStyle) { + return(isHidden = function(o) { + return 'none'==o.currentStyle['display']; + })(o); + } + return (isHidden = function(o) { + return 'none'==o.style['display']; + })(o); + }; + + /** + * Get a parent element by tag name, or the original element if it is of the tag type + */ + function getParent(o,a,b) { + if (o!=null && o.nodeName) { + if (o.nodeName==a || (b && o.nodeName==b)) { + return o; + } + while (o=o.parentNode) { + if (o.nodeName && (o.nodeName==a || (b && o.nodeName==b))) { + return o; + } + } + } + return null; + }; + + /** + * Utility function to copy properties from one object to another + */ + function copy(o1,o2) { + for (var i=2;i<arguments.length; i++) { + var a = arguments[i]; + if (def(o1[a])) { + o2[a] = o1[a]; + } + } + } + + // The table object itself + var table = { + //Class names used in the code + AutoStripeClassName:"table-autostripe", + StripeClassNamePrefix:"table-stripeclass:", + + AutoSortClassName:"table-autosort", + AutoSortColumnPrefix:"table-autosort:", + AutoSortTitle:"Click to sort", + SortedAscendingClassName:"table-sorted-asc", + SortedDescendingClassName:"table-sorted-desc", + SortableClassName:"table-sortable", + SortableColumnPrefix:"table-sortable:", + NoSortClassName:"table-nosort", + + AutoFilterClassName:"table-autofilter", + FilteredClassName:"table-filtered", + FilterableClassName:"table-filterable", + FilteredRowcountPrefix:"table-filtered-rowcount:", + RowcountPrefix:"table-rowcount:", + FilterAllLabel:"Filter: All", + + AutoPageSizePrefix:"table-autopage:", + AutoPageJumpPrefix:"table-page:", + PageNumberPrefix:"table-page-number:", + PageCountPrefix:"table-page-count:" + }; + + /** + * A place to store misc table information, rather than in the table objects themselves + */ + table.tabledata = {}; + + /** + * Resolve a table given an element reference, and make sure it has a unique ID + */ + table.uniqueId=1; + table.resolve = function(o,args) { + if (o!=null && o.nodeName && o.nodeName!="TABLE") { + o = getParent(o,"TABLE"); + } + if (o==null) { return null; } + if (!o.id) { + var id = null; + do { var id = "TABLE_"+(table.uniqueId++); } + while (document.getElementById(id)!=null); + o.id = id; + } + this.tabledata[o.id] = this.tabledata[o.id] || {}; + if (args) { + copy(args,this.tabledata[o.id],"stripeclass","ignorehiddenrows","useinnertext","sorttype","col","desc","page","pagesize"); + } + return o; + }; + + + /** + * Run a function against each cell in a table header or footer, usually + * to add or remove css classes based on sorting, filtering, etc. + */ + table.processTableCells = function(t, type, func, arg) { + t = this.resolve(t); + if (t==null) { return; } + if (type!="TFOOT") { + this.processCells(t.tHead, func, arg); + } + if (type!="THEAD") { + this.processCells(t.tFoot, func, arg); + } + }; + + /** + * Internal method used to process an arbitrary collection of cells. + * Referenced by processTableCells. + * It's done this way to avoid getElementsByTagName() which would also return nested table cells. + */ + table.processCells = function(section,func,arg) { + if (section!=null) { + if (section.rows && section.rows.length && section.rows.length>0) { + var rows = section.rows; + for (var j=0,L2=rows.length; j<L2; j++) { + var row = rows[j]; + if (row.cells && row.cells.length && row.cells.length>0) { + var cells = row.cells; + for (var k=0,L3=cells.length; k<L3; k++) { + var cellsK = cells[k]; + func.call(this,cellsK,arg); + } + } + } + } + } + }; + + /** + * Get the cellIndex value for a cell. This is only needed because of a Safari + * bug that causes cellIndex to exist but always be 0. + * Rather than feature-detecting each time it is called, the function will + * re-write itself the first time it is called. + */ + table.getCellIndex = function(td) { + var tr = td.parentNode; + var cells = tr.cells; + if (cells && cells.length) { + if (cells.length>1 && 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<L; i++) { + if (tr.cells[i]==td) { + return i; + } + } + } + return 0; + }; + + /** + * A map of node names and how to convert them into their "value" for sorting, filtering, etc. + * These are put here so it is extensible. + */ + table.nodeValue = { + 'INPUT':function(node) { + if (def(node.value) && node.type && ((node.type!="checkbox" && node.type!="radio") || node.checked)) { + return node.value; + } + return ""; + }, + 'SELECT':function(node) { + if (node.selectedIndex>=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<L; i++) { + var node = childNodes[i]; + var type = node.nodeType; + // In order to get realistic sort results, we need to treat some elements in a special way. + // These behaviors are defined in the nodeValue() object, keyed by node name + if (type==1) { + var nname = node.nodeName; + if (this.nodeValue[nname]) { + ret += this.nodeValue[nname](node); + } + else { + ret += this.getCellValue(node); + } + } + else if (type==3) { + if (def(node.innerText)) { + ret += node.innerText; + } + else if (def(node.nodeValue)) { + ret += node.nodeValue; + } + } + } + return ret; + }; + + /** + * Consider colspan and rowspan values in table header cells to calculate the actual cellIndex + * of a given cell. This is necessary because if the first cell in row 0 has a rowspan of 2, + * then the first cell in row 1 will have a cellIndex of 0 rather than 1, even though it really + * starts in the second column rather than the first. + * See: http://www.javascripttoolbox.com/temp/table_cellindex.html + */ + table.tableHeaderIndexes = {}; + table.getActualCellIndex = function(tableCellObj) { + if (!def(tableCellObj.cellIndex)) { return null; } + var tableObj = getParent(tableCellObj,"TABLE"); + var cellCoordinates = tableCellObj.parentNode.rowIndex+"-"+this.getCellIndex(tableCellObj); + + // If it has already been computed, return the answer from the lookup table + if (def(this.tableHeaderIndexes[tableObj.id])) { + return this.tableHeaderIndexes[tableObj.id][cellCoordinates]; + } + + var matrix = []; + this.tableHeaderIndexes[tableObj.id] = {}; + var thead = getParent(tableCellObj,"THEAD"); + var trs = thead.getElementsByTagName('TR'); + + // Loop thru every tr and every cell in the tr, building up a 2-d array "grid" that gets + // populated with an "x" for each space that a cell takes up. If the first cell is colspan + // 2, it will fill in values [0] and [1] in the first array, so that the second cell will + // find the first empty cell in the first row (which will be [2]) and know that this is + // where it sits, rather than its internal .cellIndex value of [1]. + for (var i=0; i<trs.length; i++) { + var cells = trs[i].cells; + for (var j=0; j<cells.length; j++) { + var c = cells[j]; + var rowIndex = c.parentNode.rowIndex; + var cellId = rowIndex+"-"+this.getCellIndex(c); + var rowSpan = c.rowSpan || 1; + var colSpan = c.colSpan || 1; + var firstAvailCol; + if(!def(matrix[rowIndex])) { + matrix[rowIndex] = []; + } + var m = matrix[rowIndex]; + // Find first available column in the first row + for (var k=0; k<m.length+1; k++) { + if (!def(m[k])) { + firstAvailCol = k; + break; + } + } + this.tableHeaderIndexes[tableObj.id][cellId] = firstAvailCol; + for (var k=rowIndex; k<rowIndex+rowSpan; k++) { + if(!def(matrix[k])) { + matrix[k] = []; + } + var matrixrow = matrix[k]; + for (var l=firstAvailCol; l<firstAvailCol+colSpan; l++) { + matrixrow[l] = "x"; + } + } + } + } + // Store the map so future lookups are fast. + return this.tableHeaderIndexes[tableObj.id][cellCoordinates]; + }; + + /** + * Sort all rows in each TBODY (tbodies are sorted independent of each other) + */ + table.sort = function(o,args) { + var t, tdata, sortconvert=null; + // Allow for a simple passing of sort type as second parameter + if (typeof(args)=="function") { + args={sorttype:args}; + } + args = args || {}; + + // If no col is specified, deduce it from the object sent in + if (!def(args.col)) { + args.col = this.getActualCellIndex(o) || 0; + } + // If no sort type is specified, default to the default sort + args.sorttype = args.sorttype || Sort['default']; + + // Resolve the table + t = this.resolve(o,args); + tdata = this.tabledata[t.id]; + + // If we are sorting on the same column as last time, flip the sort direction + if (def(tdata.lastcol) && tdata.lastcol==tdata.col && def(tdata.lastdesc)) { + tdata.desc = !tdata.lastdesc; + } + else { + tdata.desc = !!args.desc; + } + + // Store the last sorted column so clicking again will reverse the sort order + tdata.lastcol=tdata.col; + tdata.lastdesc=!!tdata.desc; + + // If a sort conversion function exists, pre-convert cell values and then use a plain alphanumeric sort + var sorttype = tdata.sorttype; + if (typeof(sorttype.convert)=="function") { + sortconvert=tdata.sorttype.convert; + sorttype=Sort.alphanumeric; + } + + // Loop through all THEADs and remove sorted class names, then re-add them for the col + // that is being sorted + this.processTableCells(t,"THEAD", + function(cell) { + if (hasClass(cell,this.SortableClassName)) { + removeClass(cell,this.SortedAscendingClassName); + removeClass(cell,this.SortedDescendingClassName); + // If the computed colIndex of the cell equals the sorted colIndex, flag it as sorted + if (tdata.col==table.getActualCellIndex(cell) && (classValue(cell,table.SortableClassName))) { + addClass(cell,tdata.desc?this.SortedAscendingClassName:this.SortedDescendingClassName); + } + } + } + ); + + // Sort each tbody independently + var bodies = t.tBodies; + if (bodies==null || bodies.length==0) { return; } + + // Define a new sort function to be called to consider descending or not + var newSortFunc = (tdata.desc)? + function(a,b){return sorttype(b[0],a[0]);} + :function(a,b){return sorttype(a[0],b[0]);}; + + var useinnertext=!!tdata.useinnertext; + var col = tdata.col; + + for (var i=0,L=bodies.length; i<L; i++) { + var tb = bodies[i], tbrows = tb.rows, rows = []; + + // Allow tbodies to request that they not be sorted + if(!hasClass(tb,table.NoSortClassName)) { + // Create a separate array which will store the converted values and refs to the + // actual rows. This is the array that will be sorted. + var cRow, cRowIndex=0; + if (cRow=tbrows[cRowIndex]){ + // Funky loop style because it's considerably faster in IE + do { + if (rowCells = cRow.cells) { + var cellValue = (col<rowCells.length)?this.getCellValue(rowCells[col],useinnertext):null; + if (sortconvert) cellValue = sortconvert(cellValue); + rows[cRowIndex] = [cellValue,tbrows[cRowIndex]]; + } + } while (cRow=tbrows[++cRowIndex]) + } + + // Do the actual sorting + rows.sort(newSortFunc); + + // Move the rows to the correctly sorted order. Appending an existing DOM object just moves it! + cRowIndex=0; + var displayedCount=0; + var f=[removeClass,addClass]; + if (cRow=rows[cRowIndex]){ + do { + tb.appendChild(cRow[1]); + } while (cRow=rows[++cRowIndex]) + } + } + } + + // If paging is enabled on the table, then we need to re-page because the order of rows has changed! + if (tdata.pagesize) { + this.page(t); // This will internally do the striping + } + else { + // Re-stripe if a class name was supplied + if (tdata.stripeclass) { + this.stripe(t,tdata.stripeclass,!!tdata.ignorehiddenrows); + } + } + }; + + /** + * Apply a filter to rows in a table and hide those that do not match. + */ + table.filter = function(o,filters,args) { + var cell; + args = args || {}; + + var t = this.resolve(o,args); + var tdata = this.tabledata[t.id]; + + // If new filters were passed in, apply them to the table's list of filters + if (!filters) { + // If a null or blank value was sent in for 'filters' then that means reset the table to no filters + tdata.filters = null; + } + else { + // Allow for passing a select list in as the filter, since this is common design + if (filters.nodeName=="SELECT" && filters.type=="select-one" && filters.selectedIndex>-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; i<L; i++) { + var filter = filters[i]; + if (typeof(filter.filter)=="string") { + // If a filter string is like "/expr/" then turn it into a Regex + if (filter.filter.match(/^\/(.*)\/$/)) { + filter.filter = new RegExp(RegExp.$1); + filter.filter.regex=true; + } + // If filter string is like "function (x) { ... }" then turn it into a function + else if (filter.filter.match(/^function\s*\(([^\)]*)\)\s*\{(.*)}\s*$/)) { + filter.filter = Function(RegExp.$1,RegExp.$2); + } + } + // If some non-table object was passed in rather than a 'col' value, resolve it + // and assign it's column index to the filter if it doesn't have one. This way, + // passing in a cell reference or a select object etc instead of a table object + // will automatically set the correct column to filter. + if (filter && !def(filter.col) && (cell=getParent(o,"TD","TH"))) { + filter.col = this.getCellIndex(cell); + } + + // Apply the passed-in filters to the existing list of filters for the table, removing those that have a filter of null or "" + if ((!filter || !filter.filter) && tdata.filters) { + delete tdata.filters[filter.col]; + } + else { + tdata.filters = tdata.filters || {}; + tdata.filters[filter.col] = filter.filter; + } + } + // If no more filters are left, then make sure to empty out the filters object + for (var j in tdata.filters) { var keep = true; } + if (!keep) { + tdata.filters = null; + } + } + // Everything's been setup, so now scrape the table rows + return table.scrape(o); + }; + + /** + * "Page" a table by showing only a subset of the rows + */ + table.page = function(t,page,args) { + args = args || {}; + if (def(page)) { args.page = page; } + return table.scrape(t,args); + }; + + /** + * Jump forward or back any number of pages + */ + table.pageJump = function(t,count,args) { + t = this.resolve(t,args); + return this.page(t,(table.tabledata[t.id].page||0)+count,args); + }; + + /** + * Go to the next page of a paged table + */ + table.pageNext = function(t,args) { + return this.pageJump(t,1,args); + }; + + /** + * Go to the previous page of a paged table + */ + table.pagePrevious = function(t,args) { + return this.pageJump(t,-1,args); + }; + + /** + * Scrape a table to either hide or show each row based on filters and paging + */ + table.scrape = function(o,args) { + var col,cell,filterList,filterReset=false,filter; + var page,pagesize,pagestart,pageend; + var unfilteredrows=[],unfilteredrowcount=0,totalrows=0; + var t,tdata,row,hideRow; + args = args || {}; + + // Resolve the table object + t = this.resolve(o,args); + tdata = this.tabledata[t.id]; + + // Setup for Paging + var page = tdata.page; + if (def(page)) { + // Don't let the page go before the beginning + if (page<0) { tdata.page=page=0; } + pagesize = tdata.pagesize || 25; // 25=arbitrary default + pagestart = page*pagesize+1; + pageend = pagestart + pagesize - 1; + } + + // Scrape each row of each tbody + var bodies = t.tBodies; + if (bodies==null || bodies.length==0) { return; } + for (var i=0,L=bodies.length; i<L; i++) { + var tb = bodies[i]; + for (var j=0,L2=tb.rows.length; j<L2; j++) { + row = tb.rows[j]; + hideRow = false; + + // Test if filters will hide the row + if (tdata.filters && row.cells) { + var cells = row.cells; + var cellsLength = cells.length; + // Test each filter + for (col in tdata.filters) { + if (!hideRow) { + filter = tdata.filters[col]; + if (filter && col<cellsLength) { + var val = this.getCellValue(cells[col]); + if (filter.regex && val.search) { + hideRow=(val.search(filter)<0); + } + else if (typeof(filter)=="function") { + hideRow=!filter(val,cells[col]); + } + else { + hideRow = (val!=filter); + } + } + } + } + } + + // Keep track of the total rows scanned and the total runs _not_ filtered out + totalrows++; + if (!hideRow) { + unfilteredrowcount++; + if (def(page)) { + // Temporarily keep an array of unfiltered rows in case the page we're on goes past + // the last page and we need to back up. Don't want to filter again! + unfilteredrows.push(row); + if (unfilteredrowcount<pagestart || unfilteredrowcount>pageend) { + 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; i<L; i++) { + unfilteredrows[i].style.display=""; + } + } + } + + // Loop through all THEADs and add/remove filtered class names + this.processTableCells(t,"THEAD", + function(c) { + ((tdata.filters && def(tdata.filters[table.getCellIndex(c)]) && hasClass(c,table.FilterableClassName))?addClass:removeClass)(c,table.FilteredClassName); + } + ); + + // Stripe the table if necessary + if (tdata.stripeclass) { + this.stripe(t); + } + + // Calculate some values to be returned for info and updating purposes + var pagecount = Math.floor(unfilteredrowcount/pagesize)+1; + if (def(page)) { + // Update the page number/total containers if they exist + if (tdata.container_number) { + tdata.container_number.innerHTML = page+1; + } + if (tdata.container_count) { + tdata.container_count.innerHTML = pagecount; + } + } + + // Update the row count containers if they exist + if (tdata.container_filtered_count) { + tdata.container_filtered_count.innerHTML = unfilteredrowcount; + } + if (tdata.container_all_count) { + tdata.container_all_count.innerHTML = totalrows; + } + return { 'data':tdata, 'unfilteredcount':unfilteredrowcount, 'total':totalrows, 'pagecount':pagecount, 'page':page, 'pagesize':pagesize }; + }; + + /** + * Shade alternate rows, aka Stripe the table. + */ + table.stripe = function(t,className,args) { + args = args || {}; + args.stripeclass = className; + + t = this.resolve(t,args); + var tdata = this.tabledata[t.id]; + + var bodies = t.tBodies; + if (bodies==null || bodies.length==0) { + return; + } + + className = tdata.stripeclass; + // Cache a shorter, quicker reference to either the remove or add class methods + var f=[removeClass,addClass]; + for (var i=0,L=bodies.length; i<L; i++) { + var tb = bodies[i], tbrows = tb.rows, cRowIndex=0, cRow, displayedCount=0; + if (cRow=tbrows[cRowIndex]){ + // The ignorehiddenrows test is pulled out of the loop for a slight speed increase. + // Makes a bigger difference in FF than in IE. + // In this case, speed always wins over brevity! + if (tdata.ignoreHiddenRows) { + do { + f[displayedCount++%2](cRow,className); + } while (cRow=tbrows[++cRowIndex]) + } + else { + do { + if (!isHidden(cRow)) { + f[displayedCount++%2](cRow,className); + } + } while (cRow=tbrows[++cRowIndex]) + } + } + } + }; + + /** + * Build up a list of unique values in a table column + */ + table.getUniqueColValues = function(t,col) { + var values={}, bodies = this.resolve(t).tBodies; + for (var i=0,L=bodies.length; i<L; i++) { + var tbody = bodies[i]; + for (var r=0,L2=tbody.rows.length; r<L2; r++) { + values[this.getCellValue(tbody.rows[r].cells[col])] = true; + } + } + var valArray = []; + for (var val in values) { + valArray.push(val); + } + return valArray.sort(); + }; + + /** + * Scan the document on load and add sorting, filtering, paging etc ability automatically + * based on existence of class names on the table and cells. + */ + table.auto = function(args) { + var cells = [], tables = document.getElementsByTagName("TABLE"); + var val,tdata; + if (tables!=null) { + for (var i=0,L=tables.length; i<L; i++) { + var t = table.resolve(tables[i]); + tdata = table.tabledata[t.id]; + if (val=classValue(t,table.StripeClassNamePrefix)) { + tdata.stripeclass=val; + } + // Do auto-filter if necessary + if (hasClass(t,table.AutoFilterClassName)) { + table.autofilter(t); + } + // Do auto-page if necessary + if (val = classValue(t,table.AutoPageSizePrefix)) { + table.autopage(t,{'pagesize':+val}); + } + // Do auto-sort if necessary + if ((val = classValue(t,table.AutoSortColumnPrefix)) || (hasClass(t,table.AutoSortClassName))) { + table.autosort(t,{'col':(val==null)?null:+val}); + } + // Do auto-stripe if necessary + if (tdata.stripeclass && hasClass(t,table.AutoStripeClassName)) { + table.stripe(t); + } + } + } + }; + + /** + * Add sorting functionality to a table header cell + */ + table.autosort = function(t,args) { + t = this.resolve(t,args); + var tdata = this.tabledata[t.id]; + this.processTableCells(t, "THEAD", function(c) { + var type = classValue(c,table.SortableColumnPrefix); + if (type!=null) { + type = type || "default"; + c.title =c.title || table.AutoSortTitle; + addClass(c,table.SortableClassName); + c.onclick = Function("","Table.sort(this,{'sorttype':Sort['"+type+"']})"); + // If we are going to auto sort on a column, we need to keep track of what kind of sort it will be + if (args.col!=null) { + if (args.col==table.getActualCellIndex(c)) { + tdata.sorttype=Sort['"+type+"']; + } + } + } + } ); + if (args.col!=null) { + table.sort(t,args); + } + }; + + /** + * Add paging functionality to a table + */ + table.autopage = function(t,args) { + t = this.resolve(t,args); + var tdata = this.tabledata[t.id]; + if (tdata.pagesize) { + this.processTableCells(t, "THEAD,TFOOT", function(c) { + var type = classValue(c,table.AutoPageJumpPrefix); + if (type=="next") { type = 1; } + else if (type=="previous") { type = -1; } + if (type!=null) { + c.onclick = Function("","Table.pageJump(this,"+type+")"); + } + } ); + if (val = classValue(t,table.PageNumberPrefix)) { + tdata.container_number = document.getElementById(val); + } + if (val = classValue(t,table.PageCountPrefix)) { + tdata.container_count = document.getElementById(val); + } + return table.page(t,0,args); + } + }; + + /** + * A util function to cancel bubbling of clicks on filter dropdowns + */ + table.cancelBubble = function(e) { + e = e || window.event; + if (typeof(e.stopPropagation)=="function") { e.stopPropagation(); } + if (def(e.cancelBubble)) { e.cancelBubble = true; } + }; + + /** + * Auto-filter a table + */ + table.autofilter = function(t,args) { + args = args || {}; + t = this.resolve(t,args); + var tdata = this.tabledata[t.id],val; + table.processTableCells(t, "THEAD", function(cell) { + if (hasClass(cell,table.FilterableClassName)) { + var cellIndex = table.getCellIndex(cell); + var colValues = table.getUniqueColValues(t,cellIndex); + if (colValues.length>0) { + if (typeof(args.insert)=="function") { + func.insert(cell,colValues); + } + else { + var sel = '<select onchange="Table.filter(this,this)" onclick="Table.cancelBubble(event)" class="'+table.AutoFilterClassName+'"><option value="">'+table.FilterAllLabel+'</option>'; + for (var i=0; i<colValues.length; i++) { + sel += '<option value="'+colValues[i]+'">'+colValues[i]+'</option>'; + } + sel += '</select>'; + cell.innerHTML += "<br>"+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@xxxxxxxxxxxxx + 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<ul.childNodes.length;itemi++) { + var item = ul.childNodes[itemi]; + if (itemId!=null && item.id==itemId) { return true; } + if (item.nodeName == "LI") { + // Iterate things in this LI + var subLists = false; + for (var sitemi=0;sitemi<item.childNodes.length;sitemi++) { + var sitem = item.childNodes[sitemi]; + if (sitem.nodeName=="UL") { + subLists = true; + var ret = expandCollapseList(sitem,cName,itemId); + if (itemId!=null && ret) { + item.className=cName; + return true; + } + } + } + if (subLists && itemId==null) { + item.className = cName; + } + } + } +} + +// Search the document for UL elements with the correct CLASS name, then process them +function convertTrees() { + setDefault("treeClass","mktree"); + setDefault("nodeClosedClass","liClosed"); + setDefault("nodeOpenClass","liOpen"); + setDefault("nodeBulletClass","liBullet"); + setDefault("nodeLinkClass","bullet"); + setDefault("preProcessTrees",true); + if (preProcessTrees) { + if (!document.createElement) { return; } // Without createElement, we can't do anything + var uls = document.getElementsByTagName("ul"); + if (uls==null) { return; } + var uls_length = uls.length; + for (var uli=0;uli<uls_length;uli++) { + var ul=uls[uli]; + if (ul.nodeName=="UL" && ul.className==treeClass) { + processList(ul); + } + } + } +} + +function treeNodeOnclick() { + this.parentNode.className = (this.parentNode.className==nodeOpenClass) ? nodeClosedClass : nodeOpenClass; + return false; +} +function retFalse() { + return false; +} +// Process a UL tag and all its children, to convert to a tree +function processList(ul) { + if (!ul.childNodes || ul.childNodes.length==0) { return; } + // Iterate LIs + var childNodesLength = ul.childNodes.length; + for (var itemi=0;itemi<childNodesLength;itemi++) { + var item = ul.childNodes[itemi]; + if (item.nodeName == "LI") { + // Iterate things in this LI + var subLists = false; + var itemChildNodesLength = item.childNodes.length; + for (var sitemi=0;sitemi<itemChildNodesLength;sitemi++) { + var sitem = item.childNodes[sitemi]; + if (sitem.nodeName=="UL") { + subLists = true; + processList(sitem); + } + } + var s= document.createElement("SPAN"); + var t= '\u00A0'; // + s.className = nodeLinkClass; + if (subLists) { + // This LI has UL's in it, so it's a +/- node + if (item.className==null || item.className=="") { + item.className = nodeClosedClass; + } + // If it's just text, make the text work as the link also + if (item.firstChild.nodeName=="#text") { + t = t+item.firstChild.nodeValue; + item.removeChild(item.firstChild); + } + s.onclick = treeNodeOnclick; + } + else { + // No sublists, so it's just a bullet node + item.className = nodeBulletClass; + s.onclick = retFalse; + } + s.appendChild(document.createTextNode(t)); + item.insertBefore(s,item.firstChild); + } + } +} +""" + + +################################################################# +## This script gets kvm autotest results directory path as an ## +## input and create a single html formatted result page. ## +################################################################# + +stimelist = [] + + +def make_html_file(metadata, results, tag, host, output_file_name, dirname): + html_prefix = """ +<!DOCTYPE HTML PUBLIC "-//W3C//DTD HTML 4.01//EN" "http://www.w3.org/TR/html4/strict.dtd"> +<html> +<head> +<title>KVM Autotest Results</title> +<style type="text/css"> +%s +</style> +<script type="text/javascript"> +%s +%s +function popup(tag,text) { +var w = window.open('', tag, 'toolbar=no,location=no,directories=no,status=no,menubar=no,scrollbars=yes,resizable=yes, copyhistory=no,width=600,height=300,top=20,left=100'); +w.document.open("text/html", "replace"); +w.document.write(text); +w.document.close(); +return true; +} +</script> +</head> +<body> +""" % (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, '<h2 id=\"page_title\">KVM Autotest Execution Report</h2>' + + # 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: + total_executed += 1 + if res['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['kvmver'] + + print >> output, '<table class="stats2">' + print >> output, '<tr><td>HOST</td><td>:</td><td>%s</td></tr>' % host + print >> output, '<tr><td>RESULTS DIR</td><td>:</td><td>%s</td></tr>' % tag + print >> output, '<tr><td>DATE</td><td>:</td><td>%s</td></tr>' % now.ctime() + print >> output, '<tr><td>STATS</td><td>:</td><td>%s</td></tr>'% stat_str + print >> output, '<tr><td></td><td></td><td></td></tr>' + print >> output, '<tr><td>KVM VERSION</td><td>:</td><td>%s</td></tr>' % kvm_ver_str + print >> output, '</table>' + + + ## print test results + print >> output, '<br>' + print >> output, '<h2 id=\"page_sub_title\">Test Results</h2>' + print >> output, '<h2 id=\"comment\">click on table headers to asc/desc sort</h2>' + result_table_prefix = """<table +id="t1" class="stats table-autosort:4 table-autofilter table-stripeclass:alternate table-page-number:t1page table-page-count:t1pages table-filtered-rowcount:t1filtercount table-rowcount:t1allcount"> +<thead class="th table-sorted-asc table-sorted-desc"> +<tr> +<th align="left" class="table-sortable:alphanumeric">Date/Time</th> +<th align="left" class="filterable table-sortable:alphanumeric">Test Case<br><input name="tc_filter" size="10" onkeyup="Table.filter(this,this)" onclick="Table.cancelBubble(event)"></th> +<th align="left" class="table-filterable table-sortable:alphanumeric">Status</th> +<th align="left">Time (sec)</th> +<th align="left">Info</th> +<th align="left">Debug</th> +</tr></thead> +<tbody> +""" + print >> output, result_table_prefix + for res in results: + print >> output, '<tr>' + print >> output, '<td align="left">%s</td>' % res['time'] + print >> output, '<td align="left">%s</td>' % res['testcase'] + if res['status'] == 'GOOD': + print >> output, '<td align=\"left\"><b><font color="#00CC00">PASS</font></b></td>' + elif res['status'] == 'FAIL': + print >> output, '<td align=\"left\"><b><font color="red">FAIL</font></b></td>' + elif res['status'] == 'ERROR': + print >> output, '<td align=\"left\"><b><font color="red">ERROR!</font></b></td>' + else: + print >> output, '<td align=\"left\">%s</td>' % res['status'] + # print exec time (seconds) + print >> output, '<td align="left">%s</td>' % 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 = '<html><head><title>%s</title></head><body>%s</body></html>' % (str(updated_tag), log_text) + print >> output, '<td align=\"left\"><A HREF=\"#\" onClick=\"popup(\'%s\',\'%s\')\">Info</A></td>' % (str(updated_tag), str(html_body_text)) + else: + print >> output, '<td align=\"left\"></td>' + # print execution time + print >> output, '<td align="left"><A HREF=\"%s\">Debug</A></td>' % os.path.join(dirname, res['title'], "debug") + + print >> output, '</tr>' + print >> output, "</tbody></table>" + + + print >> output, '<h2 id=\"page_sub_title\">Host Info</h2>' + print >> output, '<h2 id=\"comment\">click on each item to expend/collapse</h2>' + ## Meta list comes here.. + print >> output, '<p>' + print >> output, '<A href="#" class="button" onClick="expandTree(\'meta_tree\');return false;">Expand All</A>' + print >> output, '  ' + print >> output, '<A class="button" href="#" onClick="collapseTree(\'meta_tree\'); return false;">Collapse All</A>' + print >> output, '</p>' + + print >> output, '<ul class="mktree" id="meta_tree">' + counter = 0 + keys = metadata.keys() + keys.sort() + for key in keys: + val = metadata[key] + print >> output, '<li id=\"meta_headline\">%s' % key + print >> output, '<ul><table class="meta_table"><tr><td align="left">%s</td></tr></table></ul></li>' % val + print >> output, '</ul>' + + print >> output, "</body></html>" + if output_file_name: + output.close() + + +def parse_result(dirname, line): + parts = line.split() + if len(parts) < 4: + return None + global stimelist + if parts[0] == 'START': + pair = parts[3].split('=') + stime = int(pair[1]) + stimelist.append(stime) + + 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] + + # assign actual values + rx = re.compile('^(\w+)\.(.*)$') + m1 = rx.findall(parts[3]) + result['testcase'] = m1[0][1] + result['title'] = str(tag) + result['status'] = parts[1] + if result['status'] != 'GOOD': + result['log'] = get_exec_log(dirname, tag) + if len(stimelist)>0: + pair = parts[4].split('=') + etime = int(pair[1]) + stime = stimelist.pop() + total_exec_time_sec = etime - stime + result['exec_time_sec'] = total_exec_time_sec + return result + return None + + +def get_exec_log(resdir, 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 += '<br><b>STDERR:</b><br>' + log += get_info_file(stderr_file) + log += '<br><b>STDOUT:</b><br>' + log += get_info_file(stdout_file) + log += '<br><b>STATUS:</b><br>' + log += get_info_file(status_file) + log += '<br><b>DMESG:</b><br>' + log += get_info_file(dmesg_file) + return log + + +def get_info_file(filename): + 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 += '<font color=red>%s</font><br>' % str(new_line) + else: + data += '%s<br>' % str(new_line) + if not data: + data = 'No Information Found.<br>' + else: + data = 'File not found.<br>' + return data + + + +def usage(): + print 'usage:', + print 'make_html_report.py -r <result_directory> [-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'. + """ + 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") + return "Kernel: %s<br>Userspace: %s" % (kvm_version, kvm_userspace_version) + + +def main(argv): + 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 + res_dir = os.path.abspath(dirname) + tag = res_dir + status_file_name = dirname + '/status' + sysinfo_dir = dirname + '/sysinfo' + host = get_info_file('%s/hostname' % sysinfo_dir) + rx = re.compile('^\s+[END|START].*$') + # create the results set dict + results_data = [] + 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): + result_dict = parse_result(dirname, line) + if result_dict: + results_data.append(result_dict) + # create the meta info dict + metalist = { + 'uname': get_info_file('%s/uname' % sysinfo_dir), + 'cpuinfo':get_info_file('%s/cpuinfo' % sysinfo_dir), + 'meminfo':get_info_file('%s/meminfo' % sysinfo_dir), + 'df':get_info_file('%s/df' % sysinfo_dir), + 'modules':get_info_file('%s/modules' % sysinfo_dir), + 'gcc':get_info_file('%s/gcc_--version' % sysinfo_dir), + 'dmidecode':get_info_file('%s/dmidecode' % sysinfo_dir), + 'dmesg':get_info_file('%s/dmesg' % sysinfo_dir), + 'kvmver':get_kvm_version(dirname) + } + + make_html_file(metalist, results_data, tag, host, output_file_name, + html_path) + 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:]) diff --git a/client/tools/scan_results.py b/client/tools/scan_results.py new file mode 100755 index 0000000..562a05b --- /dev/null +++ b/client/tools/scan_results.py @@ -0,0 +1,97 @@ +#!/usr/bin/python +""" +Program that parses the autotest results and return a nicely printed final test +result. + +@copyright: Red Hat 2008-2009 +""" + +def parse_results(text): + """ + Parse text containing Autotest results. + + @return: A list of result 4-tuples. + """ + result_list = [] + start_time_list = [] + info_list = [] + + lines = text.splitlines() + for line in lines: + line = line.strip() + parts = line.split("\t") + + # Found a START line -- get start time + if (line.startswith("START") and len(parts) >= 5 and + parts[3].startswith("timestamp")): + start_time = float(parts[3].split("=")[1]) + start_time_list.append(start_time) + info_list.append("") + + # Found an END line -- get end time, name and status + elif (line.startswith("END") and len(parts) >= 5 and + parts[3].startswith("timestamp")): + end_time = float(parts[3].split("=")[1]) + start_time = start_time_list.pop() + info = info_list.pop() + test_name = parts[2] + test_status = parts[0].split()[1] + # Remove "kvm." prefix + if test_name.startswith("kvm."): + test_name = test_name[4:] + result_list.append((test_name, test_status, + int(end_time - start_time), info)) + + # Found a FAIL/ERROR/GOOD line -- get failure/success info + elif (len(parts) >= 6 and parts[3].startswith("timestamp") and + parts[4].startswith("localtime")): + info_list[-1] = parts[5] + + return result_list + + +def print_result(result, name_width): + """ + Nicely print a single Autotest result. + + @param result: a 4-tuple + @param name_width: test name maximum width + """ + if result: + format = "%%-%ds %%-10s %%-8s %%s" % name_width + print format % result + + +def main(resfiles): + result_lists = [] + name_width = 40 + + for resfile in resfiles: + try: + text = open(resfile).read() + except IOError: + print "Bad result file: %s" % resfile + continue + results = parse_results(text) + result_lists.append((resfile, results)) + name_width = max([name_width] + [len(r[0]) for r in results]) + + print_result(("Test", "Status", "Seconds", "Info"), name_width) + print_result(("----", "------", "-------", "----"), name_width) + + for resfile, results in result_lists: + print " (Result file: %s)" % resfile + for r in results: + print_result(r, name_width) + + +if __name__ == "__main__": + import sys, glob + + resfiles = glob.glob("../results/default/status*") + if len(sys.argv) > 1: + if sys.argv[1] == "-h" or sys.argv[1] == "--help": + print "Usage: %s [result files]" % sys.argv[0] + sys.exit(0) + resfiles = sys.argv[1:] + main(resfiles) -- 1.7.4 -- To unsubscribe from this list: send the line "unsubscribe kvm" in the body of a message to majordomo@xxxxxxxxxxxxxxx More majordomo info at http://vger.kernel.org/majordomo-info.html