| #!/usr/bin/python3 |
| """ |
| 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, subprocess |
| 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:(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@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<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); |
| } |
| } |
| } |
| """ |
| |
| |
| |
| |
| def make_html_file(metadata, results, tag, host, output_file_name, dirname): |
| """ |
| Create HTML file contents for the job report, to stdout or filesystem. |
| |
| @param metadata: Dictionary with Job metadata (tests, exec time, etc). |
| @param results: List with testcase results. |
| @param tag: Job tag. |
| @param host: Client hostname. |
| @param output_file_name: Output file name. If empty string, prints to |
| stdout. |
| @param dirname: Prefix for HTML links. If empty string, the HTML links |
| will be relative to the results dir. |
| """ |
| html_prefix = """ |
| <!DOCTYPE HTML PUBLIC "-//W3C//DTD HTML 4.01//EN" "http://www.w3.org/TR/html4/strict.dtd"> |
| <html> |
| <head> |
| <title>Autotest job execution 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(html_prefix, file=output) |
| print('<h2 id=\"page_title\">Autotest job execution report</h2>', file=output) |
| |
| # formating date and time to print |
| t = datetime.datetime.utcnow() |
| |
| 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('<table class="stats2">', file=output) |
| print('<tr><td>HOST</td><td>:</td><td>%s</td></tr>' % host, file=output) |
| print('<tr><td>RESULTS DIR</td><td>:</td><td>%s</td></tr>' % tag, file=output) |
| print('<tr><td>DATE</td><td>:</td><td>%s</td></tr>' % now.ctime(), file=output) |
| print('<tr><td>STATS</td><td>:</td><td>%s</td></tr>'% stat_str, file=output) |
| print('<tr><td></td><td></td><td></td></tr>', file=output) |
| if kvm_ver_str is not None: |
| print('<tr><td>KVM VERSION</td><td>:</td><td>%s</td></tr>' % kvm_ver_str, file=output) |
| print('</table>', file=output) |
| |
| ## print test results |
| print('<br>', file=output) |
| print('<h2 id=\"page_sub_title\">Test Results</h2>', file=output) |
| print('<h2 id=\"comment\">click on table headers to asc/desc sort</h2>', file=output) |
| 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(result_table_prefix, file=output) |
| def print_result(result, indent): |
| while result != []: |
| r = result.pop(0) |
| res = results[r][2] |
| print('<tr>', file=output) |
| print('<td align="left">%s</td>' % res['time'], file=output) |
| print('<td align="left" style="padding-left:%dpx">%s</td>' % (indent * 20, res['title']), file=output) |
| if res['status'] == 'GOOD': |
| print('<td align=\"left\"><b><font color="#00CC00">PASS</font></b></td>', file=output) |
| elif res['status'] == 'FAIL': |
| print('<td align=\"left\"><b><font color="red">FAIL</font></b></td>', file=output) |
| elif res['status'] == 'ERROR': |
| print('<td align=\"left\"><b><font color="red">ERROR!</font></b></td>', file=output) |
| else: |
| print('<td align=\"left\">%s</td>' % res['status'], file=output) |
| # print exec time (seconds) |
| print('<td align="left">%s</td>' % res['exec_time_sec'], file=output) |
| # 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('<td align=\"left\"><A HREF=\"#\" onClick=\"popup(\'%s\',\'%s\')\">Info</A></td>' % (str(updated_tag), str(html_body_text)), file=output) |
| else: |
| print('<td align=\"left\"></td>', file=output) |
| # print execution time |
| print('<td align="left"><A HREF=\"%s\">Debug</A></td>' % os.path.join(dirname, res['subdir'], "debug"), file=output) |
| |
| print('</tr>', file=output) |
| print_result(results[r][1], indent + 1) |
| |
| print_result(results[""][1], 0) |
| print("</tbody></table>", file=output) |
| |
| |
| print('<h2 id=\"page_sub_title\">Host Info</h2>', file=output) |
| print('<h2 id=\"comment\">click on each item to expend/collapse</h2>', file=output) |
| ## Meta list comes here.. |
| print('<p>', file=output) |
| print('<A href="#" class="button" onClick="expandTree(\'meta_tree\');return false;">Expand All</A>', file=output) |
| print('  ', file=output) |
| print('<A class="button" href="#" onClick="collapseTree(\'meta_tree\'); return false;">Collapse All</A>', file=output) |
| print('</p>', file=output) |
| |
| print('<ul class="mktree" id="meta_tree">', file=output) |
| counter = 0 |
| keys = list(metadata.keys()) |
| keys.sort() |
| for key in keys: |
| val = metadata[key] |
| print('<li id=\"meta_headline\">%s' % key, file=output) |
| print('<ul><table class="meta_table"><tr><td align="left">%s</td></tr></table></ul></li>' % val, file=output) |
| print('</ul>', file=output) |
| |
| print("</body></html>", file=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 += '<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): |
| """ |
| 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 += '<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 stand alone program 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'. |
| |
| @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 = subprocess.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<br>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:]) |