tools/html_report: Make html report generation autotest generic
[autotest-zwu.git] / client / tools / html_report.py
blob7b17a75dfb3b78eb33e87433d7c27a69908a54d3
1 #!/usr/bin/python
2 """
3 Module used to parse the autotest job results and generate an HTML report.
5 @copyright: (c)2005-2007 Matt Kruse (javascripttoolbox.com)
6 @copyright: Red Hat 2008-2009
7 @author: Dror Russo (drusso@redhat.com)
8 """
10 import os, sys, re, getopt, time, datetime, commands
11 import common
14 format_css = """
15 html,body {
16 padding:0;
17 color:#222;
18 background:#FFFFFF;
21 body {
22 padding:0px;
23 font:76%/150% "Lucida Grande", "Lucida Sans Unicode", Lucida, Verdana, Geneva, Arial, Helvetica, sans-serif;
26 #page_title{
27 text-decoration:none;
28 font:bold 2em/2em Arial, Helvetica, sans-serif;
29 text-transform:none;
30 text-align: left;
31 color:#555555;
32 border-bottom: 1px solid #555555;
35 #page_sub_title{
36 text-decoration:none;
37 font:bold 16px Arial, Helvetica, sans-serif;
38 text-transform:uppercase;
39 text-align: left;
40 color:#555555;
41 margin-bottom:0;
44 #comment{
45 text-decoration:none;
46 font:bold 10px Arial, Helvetica, sans-serif;
47 text-transform:none;
48 text-align: left;
49 color:#999999;
50 margin-top:0;
54 #meta_headline{
55 text-decoration:none;
56 font-family: Verdana, Geneva, Arial, Helvetica, sans-serif ;
57 text-align: left;
58 color:black;
59 font-weight: bold;
60 font-size: 14px;
64 table.meta_table
65 {text-align: center;
66 font-family: Verdana, Geneva, Arial, Helvetica, sans-serif ;
67 width: 90%;
68 background-color: #FFFFFF;
69 border: 0px;
70 border-top: 1px #003377 solid;
71 border-bottom: 1px #003377 solid;
72 border-right: 1px #003377 solid;
73 border-left: 1px #003377 solid;
74 border-collapse: collapse;
75 border-spacing: 0px;}
77 table.meta_table td
78 {background-color: #FFFFFF;
79 color: #000;
80 padding: 4px;
81 border-top: 1px #BBBBBB solid;
82 border-bottom: 1px #BBBBBB solid;
83 font-weight: normal;
84 font-size: 13px;}
87 table.stats
88 {text-align: center;
89 font-family: Verdana, Geneva, Arial, Helvetica, sans-serif ;
90 width: 100%;
91 background-color: #FFFFFF;
92 border: 0px;
93 border-top: 1px #003377 solid;
94 border-bottom: 1px #003377 solid;
95 border-right: 1px #003377 solid;
96 border-left: 1px #003377 solid;
97 border-collapse: collapse;
98 border-spacing: 0px;}
100 table.stats td{
101 background-color: #FFFFFF;
102 color: #000;
103 padding: 4px;
104 border-top: 1px #BBBBBB solid;
105 border-bottom: 1px #BBBBBB solid;
106 font-weight: normal;
107 font-size: 11px;}
109 table.stats th{
110 background: #dcdcdc;
111 color: #000;
112 padding: 6px;
113 font-size: 12px;
114 border-bottom: 1px #003377 solid;
115 font-weight: bold;}
117 table.stats td.top{
118 background-color: #dcdcdc;
119 color: #000;
120 padding: 6px;
121 text-align: center;
122 border: 0px;
123 border-bottom: 1px #003377 solid;
124 font-size: 10px;
125 font-weight: bold;}
127 table.stats th.table-sorted-asc{
128 background-image: url(ascending.gif);
129 background-position: top left ;
130 background-repeat: no-repeat;
133 table.stats th.table-sorted-desc{
134 background-image: url(descending.gif);
135 background-position: top left;
136 background-repeat: no-repeat;
139 table.stats2
140 {text-align: left;
141 font-family: Verdana, Geneva, Arial, Helvetica, sans-serif ;
142 width: 100%;
143 background-color: #FFFFFF;
144 border: 0px;
147 table.stats2 td{
148 background-color: #FFFFFF;
149 color: #000;
150 padding: 0px;
151 font-weight: bold;
152 font-size: 13px;}
156 /* Put this inside a @media qualifier so Netscape 4 ignores it */
157 @media screen, print {
158 /* Turn off list bullets */
159 ul.mktree li { list-style: none; }
160 /* Control how "spaced out" the tree is */
161 ul.mktree, ul.mktree ul , ul.mktree li { margin-left:10px; padding:0px; }
162 /* Provide space for our own "bullet" inside the LI */
163 ul.mktree li .bullet { padding-left: 15px; }
164 /* Show "bullets" in the links, depending on the class of the LI that the link's in */
165 ul.mktree li.liOpen .bullet { cursor: pointer; }
166 ul.mktree li.liClosed .bullet { cursor: pointer; }
167 ul.mktree li.liBullet .bullet { cursor: default; }
168 /* Sublists are visible or not based on class of parent LI */
169 ul.mktree li.liOpen ul { display: block; }
170 ul.mktree li.liClosed ul { display: none; }
172 /* Format menu items differently depending on what level of the tree they are in */
173 /* Uncomment this if you want your fonts to decrease in size the deeper they are in the tree */
175 ul.mktree li ul li { font-size: 90% }
181 table_js = """
183 * Copyright (c)2005-2007 Matt Kruse (javascripttoolbox.com)
185 * Dual licensed under the MIT and GPL licenses.
186 * This basically means you can use this code however you want for
187 * free, but don't claim to have written it yourself!
188 * Donations always accepted: http://www.JavascriptToolbox.com/donate/
190 * Please do not link to the .js files on javascripttoolbox.com from
191 * your site. Copy the files locally to your server instead.
195 * Table.js
196 * Functions for interactive Tables
198 * Copyright (c) 2007 Matt Kruse (javascripttoolbox.com)
199 * Dual licensed under the MIT and GPL licenses.
201 * @version 0.981
203 * @history 0.981 2007-03-19 Added Sort.numeric_comma, additional date parsing formats
204 * @history 0.980 2007-03-18 Release new BETA release pending some testing. Todo: Additional docs, examples, plus jQuery plugin.
205 * @history 0.959 2007-03-05 Added more "auto" functionality, couple bug fixes
206 * @history 0.958 2007-02-28 Added auto functionality based on class names
207 * @history 0.957 2007-02-21 Speed increases, more code cleanup, added Auto Sort functionality
208 * @history 0.956 2007-02-16 Cleaned up the code and added Auto Filter functionality.
209 * @history 0.950 2006-11-15 First BETA release.
211 * @todo Add more date format parsers
212 * @todo Add style classes to colgroup tags after sorting/filtering in case the user wants to highlight the whole column
213 * @todo Correct for colspans in data rows (this may slow it down)
214 * @todo Fix for IE losing form control values after sort?
218 * Sort Functions
220 var Sort = (function(){
221 var sort = {};
222 // Default alpha-numeric sort
223 // --------------------------
224 sort.alphanumeric = function(a,b) {
225 return (a==b)?0:(a<b)?-1:1;
227 sort.alphanumeric_rev = function(a,b) {
228 return (a==b)?0:(a<b)?1:-1;
230 sort['default'] = sort.alphanumeric; // IE chokes on sort.default
232 // This conversion is generalized to work for either a decimal separator of , or .
233 sort.numeric_converter = function(separator) {
234 return function(val) {
235 if (typeof(val)=="string") {
236 val = parseFloat(val.replace(/^[^\d\.]*([\d., ]+).*/g,"$1").replace(new RegExp("[^\\\d"+separator+"]","g"),'').replace(/,/,'.')) || 0;
238 return val || 0;
242 // Numeric Reversed Sort
243 // ------------
244 sort.numeric_rev = function(a,b) {
245 if (sort.numeric.convert(a)>sort.numeric.convert(b)) {
246 return (-1);
248 if (sort.numeric.convert(a)==sort.numeric.convert(b)) {
249 return 0;
251 if (sort.numeric.convert(a)<sort.numeric.convert(b)) {
252 return 1;
257 // Numeric Sort
258 // ------------
259 sort.numeric = function(a,b) {
260 return sort.numeric.convert(a)-sort.numeric.convert(b);
262 sort.numeric.convert = sort.numeric_converter(".");
264 // Numeric Sort - comma decimal separator
265 // --------------------------------------
266 sort.numeric_comma = function(a,b) {
267 return sort.numeric_comma.convert(a)-sort.numeric_comma.convert(b);
269 sort.numeric_comma.convert = sort.numeric_converter(",");
271 // Case-insensitive Sort
272 // ---------------------
273 sort.ignorecase = function(a,b) {
274 return sort.alphanumeric(sort.ignorecase.convert(a),sort.ignorecase.convert(b));
276 sort.ignorecase.convert = function(val) {
277 if (val==null) { return ""; }
278 return (""+val).toLowerCase();
281 // Currency Sort
282 // -------------
283 sort.currency = sort.numeric; // Just treat it as numeric!
284 sort.currency_comma = sort.numeric_comma;
286 // Date sort
287 // ---------
288 sort.date = function(a,b) {
289 return sort.numeric(sort.date.convert(a),sort.date.convert(b));
291 // Convert 2-digit years to 4
292 sort.date.fixYear=function(yr) {
293 yr = +yr;
294 if (yr<50) { yr += 2000; }
295 else if (yr<100) { yr += 1900; }
296 return yr;
298 sort.date.formats = [
299 // YY[YY]-MM-DD
300 { 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(); } }
301 // MM/DD/YY[YY] or MM-DD-YY[YY]
302 ,{ 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(); } }
303 // 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
304 ,{ re:/(.*\d{4}.*\d+:\d+\d+.*)/, f:function(x){ var d=new Date(x[1]); if(d){return d.getTime();} } }
306 sort.date.convert = function(val) {
307 var m,v, f = sort.date.formats;
308 for (var i=0,L=f.length; i<L; i++) {
309 if (m=val.match(f[i].re)) {
310 v=f[i].f(m);
311 if (typeof(v)!="undefined") { return v; }
314 return 9999999999999; // So non-parsed dates will be last, not first
317 return sort;
318 })();
321 * The main Table namespace
323 var Table = (function(){
326 * Determine if a reference is defined
328 function def(o) {return (typeof o!="undefined");};
331 * Determine if an object or class string contains a given class.
333 function hasClass(o,name) {
334 return new RegExp("(^|\\\s)"+name+"(\\\s|$)").test(o.className);
338 * Add a class to an object
340 function addClass(o,name) {
341 var c = o.className || "";
342 if (def(c) && !hasClass(o,name)) {
343 o.className += (c?" ":"") + name;
348 * Remove a class from an object
350 function removeClass(o,name) {
351 var c = o.className || "";
352 o.className = c.replace(new RegExp("(^|\\\s)"+name+"(\\\s|$)"),"$1");
356 * For classes that match a given substring, return the rest
358 function classValue(o,prefix) {
359 var c = o.className;
360 if (c.match(new RegExp("(^|\\\s)"+prefix+"([^ ]+)"))) {
361 return RegExp.$2;
363 return null;
367 * Return true if an object is hidden.
368 * This uses the "russian doll" technique to unwrap itself to the most efficient
369 * function after the first pass. This avoids repeated feature detection that
370 * would always fall into the same block of code.
372 function isHidden(o) {
373 if (window.getComputedStyle) {
374 var cs = window.getComputedStyle;
375 return (isHidden = function(o) {
376 return 'none'==cs(o,null).getPropertyValue('display');
377 })(o);
379 else if (window.currentStyle) {
380 return(isHidden = function(o) {
381 return 'none'==o.currentStyle['display'];
382 })(o);
384 return (isHidden = function(o) {
385 return 'none'==o.style['display'];
386 })(o);
390 * Get a parent element by tag name, or the original element if it is of the tag type
392 function getParent(o,a,b) {
393 if (o!=null && o.nodeName) {
394 if (o.nodeName==a || (b && o.nodeName==b)) {
395 return o;
397 while (o=o.parentNode) {
398 if (o.nodeName && (o.nodeName==a || (b && o.nodeName==b))) {
399 return o;
403 return null;
407 * Utility function to copy properties from one object to another
409 function copy(o1,o2) {
410 for (var i=2;i<arguments.length; i++) {
411 var a = arguments[i];
412 if (def(o1[a])) {
413 o2[a] = o1[a];
418 // The table object itself
419 var table = {
420 //Class names used in the code
421 AutoStripeClassName:"table-autostripe",
422 StripeClassNamePrefix:"table-stripeclass:",
424 AutoSortClassName:"table-autosort",
425 AutoSortColumnPrefix:"table-autosort:",
426 AutoSortTitle:"Click to sort",
427 SortedAscendingClassName:"table-sorted-asc",
428 SortedDescendingClassName:"table-sorted-desc",
429 SortableClassName:"table-sortable",
430 SortableColumnPrefix:"table-sortable:",
431 NoSortClassName:"table-nosort",
433 AutoFilterClassName:"table-autofilter",
434 FilteredClassName:"table-filtered",
435 FilterableClassName:"table-filterable",
436 FilteredRowcountPrefix:"table-filtered-rowcount:",
437 RowcountPrefix:"table-rowcount:",
438 FilterAllLabel:"Filter: All",
440 AutoPageSizePrefix:"table-autopage:",
441 AutoPageJumpPrefix:"table-page:",
442 PageNumberPrefix:"table-page-number:",
443 PageCountPrefix:"table-page-count:"
447 * A place to store misc table information, rather than in the table objects themselves
449 table.tabledata = {};
452 * Resolve a table given an element reference, and make sure it has a unique ID
454 table.uniqueId=1;
455 table.resolve = function(o,args) {
456 if (o!=null && o.nodeName && o.nodeName!="TABLE") {
457 o = getParent(o,"TABLE");
459 if (o==null) { return null; }
460 if (!o.id) {
461 var id = null;
462 do { var id = "TABLE_"+(table.uniqueId++); }
463 while (document.getElementById(id)!=null);
464 o.id = id;
466 this.tabledata[o.id] = this.tabledata[o.id] || {};
467 if (args) {
468 copy(args,this.tabledata[o.id],"stripeclass","ignorehiddenrows","useinnertext","sorttype","col","desc","page","pagesize");
470 return o;
475 * Run a function against each cell in a table header or footer, usually
476 * to add or remove css classes based on sorting, filtering, etc.
478 table.processTableCells = function(t, type, func, arg) {
479 t = this.resolve(t);
480 if (t==null) { return; }
481 if (type!="TFOOT") {
482 this.processCells(t.tHead, func, arg);
484 if (type!="THEAD") {
485 this.processCells(t.tFoot, func, arg);
490 * Internal method used to process an arbitrary collection of cells.
491 * Referenced by processTableCells.
492 * It's done this way to avoid getElementsByTagName() which would also return nested table cells.
494 table.processCells = function(section,func,arg) {
495 if (section!=null) {
496 if (section.rows && section.rows.length && section.rows.length>0) {
497 var rows = section.rows;
498 for (var j=0,L2=rows.length; j<L2; j++) {
499 var row = rows[j];
500 if (row.cells && row.cells.length && row.cells.length>0) {
501 var cells = row.cells;
502 for (var k=0,L3=cells.length; k<L3; k++) {
503 var cellsK = cells[k];
504 func.call(this,cellsK,arg);
513 * Get the cellIndex value for a cell. This is only needed because of a Safari
514 * bug that causes cellIndex to exist but always be 0.
515 * Rather than feature-detecting each time it is called, the function will
516 * re-write itself the first time it is called.
518 table.getCellIndex = function(td) {
519 var tr = td.parentNode;
520 var cells = tr.cells;
521 if (cells && cells.length) {
522 if (cells.length>1 && cells[cells.length-1].cellIndex>0) {
523 // Define the new function, overwrite the one we're running now, and then run the new one
524 (this.getCellIndex = function(td) {
525 return td.cellIndex;
526 })(td);
528 // Safari will always go through this slower block every time. Oh well.
529 for (var i=0,L=cells.length; i<L; i++) {
530 if (tr.cells[i]==td) {
531 return i;
535 return 0;
539 * A map of node names and how to convert them into their "value" for sorting, filtering, etc.
540 * These are put here so it is extensible.
542 table.nodeValue = {
543 'INPUT':function(node) {
544 if (def(node.value) && node.type && ((node.type!="checkbox" && node.type!="radio") || node.checked)) {
545 return node.value;
547 return "";
549 'SELECT':function(node) {
550 if (node.selectedIndex>=0 && node.options) {
551 // Sort select elements by the visible text
552 return node.options[node.selectedIndex].text;
554 return "";
556 'IMG':function(node) {
557 return node.name || "";
562 * Get the text value of a cell. Only use innerText if explicitly told to, because
563 * otherwise we want to be able to handle sorting on inputs and other types
565 table.getCellValue = function(td,useInnerText) {
566 if (useInnerText && def(td.innerText)) {
567 return td.innerText;
569 if (!td.childNodes) {
570 return "";
572 var childNodes=td.childNodes;
573 var ret = "";
574 for (var i=0,L=childNodes.length; i<L; i++) {
575 var node = childNodes[i];
576 var type = node.nodeType;
577 // In order to get realistic sort results, we need to treat some elements in a special way.
578 // These behaviors are defined in the nodeValue() object, keyed by node name
579 if (type==1) {
580 var nname = node.nodeName;
581 if (this.nodeValue[nname]) {
582 ret += this.nodeValue[nname](node);
584 else {
585 ret += this.getCellValue(node);
588 else if (type==3) {
589 if (def(node.innerText)) {
590 ret += node.innerText;
592 else if (def(node.nodeValue)) {
593 ret += node.nodeValue;
597 return ret;
601 * Consider colspan and rowspan values in table header cells to calculate the actual cellIndex
602 * of a given cell. This is necessary because if the first cell in row 0 has a rowspan of 2,
603 * then the first cell in row 1 will have a cellIndex of 0 rather than 1, even though it really
604 * starts in the second column rather than the first.
605 * See: http://www.javascripttoolbox.com/temp/table_cellindex.html
607 table.tableHeaderIndexes = {};
608 table.getActualCellIndex = function(tableCellObj) {
609 if (!def(tableCellObj.cellIndex)) { return null; }
610 var tableObj = getParent(tableCellObj,"TABLE");
611 var cellCoordinates = tableCellObj.parentNode.rowIndex+"-"+this.getCellIndex(tableCellObj);
613 // If it has already been computed, return the answer from the lookup table
614 if (def(this.tableHeaderIndexes[tableObj.id])) {
615 return this.tableHeaderIndexes[tableObj.id][cellCoordinates];
618 var matrix = [];
619 this.tableHeaderIndexes[tableObj.id] = {};
620 var thead = getParent(tableCellObj,"THEAD");
621 var trs = thead.getElementsByTagName('TR');
623 // Loop thru every tr and every cell in the tr, building up a 2-d array "grid" that gets
624 // populated with an "x" for each space that a cell takes up. If the first cell is colspan
625 // 2, it will fill in values [0] and [1] in the first array, so that the second cell will
626 // find the first empty cell in the first row (which will be [2]) and know that this is
627 // where it sits, rather than its internal .cellIndex value of [1].
628 for (var i=0; i<trs.length; i++) {
629 var cells = trs[i].cells;
630 for (var j=0; j<cells.length; j++) {
631 var c = cells[j];
632 var rowIndex = c.parentNode.rowIndex;
633 var cellId = rowIndex+"-"+this.getCellIndex(c);
634 var rowSpan = c.rowSpan || 1;
635 var colSpan = c.colSpan || 1;
636 var firstAvailCol;
637 if(!def(matrix[rowIndex])) {
638 matrix[rowIndex] = [];
640 var m = matrix[rowIndex];
641 // Find first available column in the first row
642 for (var k=0; k<m.length+1; k++) {
643 if (!def(m[k])) {
644 firstAvailCol = k;
645 break;
648 this.tableHeaderIndexes[tableObj.id][cellId] = firstAvailCol;
649 for (var k=rowIndex; k<rowIndex+rowSpan; k++) {
650 if(!def(matrix[k])) {
651 matrix[k] = [];
653 var matrixrow = matrix[k];
654 for (var l=firstAvailCol; l<firstAvailCol+colSpan; l++) {
655 matrixrow[l] = "x";
660 // Store the map so future lookups are fast.
661 return this.tableHeaderIndexes[tableObj.id][cellCoordinates];
665 * Sort all rows in each TBODY (tbodies are sorted independent of each other)
667 table.sort = function(o,args) {
668 var t, tdata, sortconvert=null;
669 // Allow for a simple passing of sort type as second parameter
670 if (typeof(args)=="function") {
671 args={sorttype:args};
673 args = args || {};
675 // If no col is specified, deduce it from the object sent in
676 if (!def(args.col)) {
677 args.col = this.getActualCellIndex(o) || 0;
679 // If no sort type is specified, default to the default sort
680 args.sorttype = args.sorttype || Sort['default'];
682 // Resolve the table
683 t = this.resolve(o,args);
684 tdata = this.tabledata[t.id];
686 // If we are sorting on the same column as last time, flip the sort direction
687 if (def(tdata.lastcol) && tdata.lastcol==tdata.col && def(tdata.lastdesc)) {
688 tdata.desc = !tdata.lastdesc;
690 else {
691 tdata.desc = !!args.desc;
694 // Store the last sorted column so clicking again will reverse the sort order
695 tdata.lastcol=tdata.col;
696 tdata.lastdesc=!!tdata.desc;
698 // If a sort conversion function exists, pre-convert cell values and then use a plain alphanumeric sort
699 var sorttype = tdata.sorttype;
700 if (typeof(sorttype.convert)=="function") {
701 sortconvert=tdata.sorttype.convert;
702 sorttype=Sort.alphanumeric;
705 // Loop through all THEADs and remove sorted class names, then re-add them for the col
706 // that is being sorted
707 this.processTableCells(t,"THEAD",
708 function(cell) {
709 if (hasClass(cell,this.SortableClassName)) {
710 removeClass(cell,this.SortedAscendingClassName);
711 removeClass(cell,this.SortedDescendingClassName);
712 // If the computed colIndex of the cell equals the sorted colIndex, flag it as sorted
713 if (tdata.col==table.getActualCellIndex(cell) && (classValue(cell,table.SortableClassName))) {
714 addClass(cell,tdata.desc?this.SortedAscendingClassName:this.SortedDescendingClassName);
720 // Sort each tbody independently
721 var bodies = t.tBodies;
722 if (bodies==null || bodies.length==0) { return; }
724 // Define a new sort function to be called to consider descending or not
725 var newSortFunc = (tdata.desc)?
726 function(a,b){return sorttype(b[0],a[0]);}
727 :function(a,b){return sorttype(a[0],b[0]);};
729 var useinnertext=!!tdata.useinnertext;
730 var col = tdata.col;
732 for (var i=0,L=bodies.length; i<L; i++) {
733 var tb = bodies[i], tbrows = tb.rows, rows = [];
735 // Allow tbodies to request that they not be sorted
736 if(!hasClass(tb,table.NoSortClassName)) {
737 // Create a separate array which will store the converted values and refs to the
738 // actual rows. This is the array that will be sorted.
739 var cRow, cRowIndex=0;
740 if (cRow=tbrows[cRowIndex]){
741 // Funky loop style because it's considerably faster in IE
742 do {
743 if (rowCells = cRow.cells) {
744 var cellValue = (col<rowCells.length)?this.getCellValue(rowCells[col],useinnertext):null;
745 if (sortconvert) cellValue = sortconvert(cellValue);
746 rows[cRowIndex] = [cellValue,tbrows[cRowIndex]];
748 } while (cRow=tbrows[++cRowIndex])
751 // Do the actual sorting
752 rows.sort(newSortFunc);
754 // Move the rows to the correctly sorted order. Appending an existing DOM object just moves it!
755 cRowIndex=0;
756 var displayedCount=0;
757 var f=[removeClass,addClass];
758 if (cRow=rows[cRowIndex]){
759 do {
760 tb.appendChild(cRow[1]);
761 } while (cRow=rows[++cRowIndex])
766 // If paging is enabled on the table, then we need to re-page because the order of rows has changed!
767 if (tdata.pagesize) {
768 this.page(t); // This will internally do the striping
770 else {
771 // Re-stripe if a class name was supplied
772 if (tdata.stripeclass) {
773 this.stripe(t,tdata.stripeclass,!!tdata.ignorehiddenrows);
779 * Apply a filter to rows in a table and hide those that do not match.
781 table.filter = function(o,filters,args) {
782 var cell;
783 args = args || {};
785 var t = this.resolve(o,args);
786 var tdata = this.tabledata[t.id];
788 // If new filters were passed in, apply them to the table's list of filters
789 if (!filters) {
790 // If a null or blank value was sent in for 'filters' then that means reset the table to no filters
791 tdata.filters = null;
793 else {
794 // Allow for passing a select list in as the filter, since this is common design
795 if (filters.nodeName=="SELECT" && filters.type=="select-one" && filters.selectedIndex>-1) {
796 filters={ 'filter':filters.options[filters.selectedIndex].value };
798 // Also allow for a regular input
799 if (filters.nodeName=="INPUT" && filters.type=="text") {
800 filters={ 'filter':"/"+filters.value+"/" };
802 // Force filters to be an array
803 if (typeof(filters)=="object" && !filters.length) {
804 filters = [filters];
807 // Convert regular expression strings to RegExp objects and function strings to function objects
808 for (var i=0,L=filters.length; i<L; i++) {
809 var filter = filters[i];
810 if (typeof(filter.filter)=="string") {
811 // If a filter string is like "/expr/" then turn it into a Regex
812 if (filter.filter.match(/^\/(.*)\/$/)) {
813 filter.filter = new RegExp(RegExp.$1);
814 filter.filter.regex=true;
816 // If filter string is like "function (x) { ... }" then turn it into a function
817 else if (filter.filter.match(/^function\s*\(([^\)]*)\)\s*\{(.*)}\s*$/)) {
818 filter.filter = Function(RegExp.$1,RegExp.$2);
821 // If some non-table object was passed in rather than a 'col' value, resolve it
822 // and assign it's column index to the filter if it doesn't have one. This way,
823 // passing in a cell reference or a select object etc instead of a table object
824 // will automatically set the correct column to filter.
825 if (filter && !def(filter.col) && (cell=getParent(o,"TD","TH"))) {
826 filter.col = this.getCellIndex(cell);
829 // Apply the passed-in filters to the existing list of filters for the table, removing those that have a filter of null or ""
830 if ((!filter || !filter.filter) && tdata.filters) {
831 delete tdata.filters[filter.col];
833 else {
834 tdata.filters = tdata.filters || {};
835 tdata.filters[filter.col] = filter.filter;
838 // If no more filters are left, then make sure to empty out the filters object
839 for (var j in tdata.filters) { var keep = true; }
840 if (!keep) {
841 tdata.filters = null;
844 // Everything's been setup, so now scrape the table rows
845 return table.scrape(o);
849 * "Page" a table by showing only a subset of the rows
851 table.page = function(t,page,args) {
852 args = args || {};
853 if (def(page)) { args.page = page; }
854 return table.scrape(t,args);
858 * Jump forward or back any number of pages
860 table.pageJump = function(t,count,args) {
861 t = this.resolve(t,args);
862 return this.page(t,(table.tabledata[t.id].page||0)+count,args);
866 * Go to the next page of a paged table
868 table.pageNext = function(t,args) {
869 return this.pageJump(t,1,args);
873 * Go to the previous page of a paged table
875 table.pagePrevious = function(t,args) {
876 return this.pageJump(t,-1,args);
880 * Scrape a table to either hide or show each row based on filters and paging
882 table.scrape = function(o,args) {
883 var col,cell,filterList,filterReset=false,filter;
884 var page,pagesize,pagestart,pageend;
885 var unfilteredrows=[],unfilteredrowcount=0,totalrows=0;
886 var t,tdata,row,hideRow;
887 args = args || {};
889 // Resolve the table object
890 t = this.resolve(o,args);
891 tdata = this.tabledata[t.id];
893 // Setup for Paging
894 var page = tdata.page;
895 if (def(page)) {
896 // Don't let the page go before the beginning
897 if (page<0) { tdata.page=page=0; }
898 pagesize = tdata.pagesize || 25; // 25=arbitrary default
899 pagestart = page*pagesize+1;
900 pageend = pagestart + pagesize - 1;
903 // Scrape each row of each tbody
904 var bodies = t.tBodies;
905 if (bodies==null || bodies.length==0) { return; }
906 for (var i=0,L=bodies.length; i<L; i++) {
907 var tb = bodies[i];
908 for (var j=0,L2=tb.rows.length; j<L2; j++) {
909 row = tb.rows[j];
910 hideRow = false;
912 // Test if filters will hide the row
913 if (tdata.filters && row.cells) {
914 var cells = row.cells;
915 var cellsLength = cells.length;
916 // Test each filter
917 for (col in tdata.filters) {
918 if (!hideRow) {
919 filter = tdata.filters[col];
920 if (filter && col<cellsLength) {
921 var val = this.getCellValue(cells[col]);
922 if (filter.regex && val.search) {
923 hideRow=(val.search(filter)<0);
925 else if (typeof(filter)=="function") {
926 hideRow=!filter(val,cells[col]);
928 else {
929 hideRow = (val!=filter);
936 // Keep track of the total rows scanned and the total runs _not_ filtered out
937 totalrows++;
938 if (!hideRow) {
939 unfilteredrowcount++;
940 if (def(page)) {
941 // Temporarily keep an array of unfiltered rows in case the page we're on goes past
942 // the last page and we need to back up. Don't want to filter again!
943 unfilteredrows.push(row);
944 if (unfilteredrowcount<pagestart || unfilteredrowcount>pageend) {
945 hideRow = true;
950 row.style.display = hideRow?"none":"";
954 if (def(page)) {
955 // Check to see if filtering has put us past the requested page index. If it has,
956 // then go back to the last page and show it.
957 if (pagestart>=unfilteredrowcount) {
958 pagestart = unfilteredrowcount-(unfilteredrowcount%pagesize);
959 tdata.page = page = pagestart/pagesize;
960 for (var i=pagestart,L=unfilteredrows.length; i<L; i++) {
961 unfilteredrows[i].style.display="";
966 // Loop through all THEADs and add/remove filtered class names
967 this.processTableCells(t,"THEAD",
968 function(c) {
969 ((tdata.filters && def(tdata.filters[table.getCellIndex(c)]) && hasClass(c,table.FilterableClassName))?addClass:removeClass)(c,table.FilteredClassName);
973 // Stripe the table if necessary
974 if (tdata.stripeclass) {
975 this.stripe(t);
978 // Calculate some values to be returned for info and updating purposes
979 var pagecount = Math.floor(unfilteredrowcount/pagesize)+1;
980 if (def(page)) {
981 // Update the page number/total containers if they exist
982 if (tdata.container_number) {
983 tdata.container_number.innerHTML = page+1;
985 if (tdata.container_count) {
986 tdata.container_count.innerHTML = pagecount;
990 // Update the row count containers if they exist
991 if (tdata.container_filtered_count) {
992 tdata.container_filtered_count.innerHTML = unfilteredrowcount;
994 if (tdata.container_all_count) {
995 tdata.container_all_count.innerHTML = totalrows;
997 return { 'data':tdata, 'unfilteredcount':unfilteredrowcount, 'total':totalrows, 'pagecount':pagecount, 'page':page, 'pagesize':pagesize };
1001 * Shade alternate rows, aka Stripe the table.
1003 table.stripe = function(t,className,args) {
1004 args = args || {};
1005 args.stripeclass = className;
1007 t = this.resolve(t,args);
1008 var tdata = this.tabledata[t.id];
1010 var bodies = t.tBodies;
1011 if (bodies==null || bodies.length==0) {
1012 return;
1015 className = tdata.stripeclass;
1016 // Cache a shorter, quicker reference to either the remove or add class methods
1017 var f=[removeClass,addClass];
1018 for (var i=0,L=bodies.length; i<L; i++) {
1019 var tb = bodies[i], tbrows = tb.rows, cRowIndex=0, cRow, displayedCount=0;
1020 if (cRow=tbrows[cRowIndex]){
1021 // The ignorehiddenrows test is pulled out of the loop for a slight speed increase.
1022 // Makes a bigger difference in FF than in IE.
1023 // In this case, speed always wins over brevity!
1024 if (tdata.ignoreHiddenRows) {
1025 do {
1026 f[displayedCount++%2](cRow,className);
1027 } while (cRow=tbrows[++cRowIndex])
1029 else {
1030 do {
1031 if (!isHidden(cRow)) {
1032 f[displayedCount++%2](cRow,className);
1034 } while (cRow=tbrows[++cRowIndex])
1041 * Build up a list of unique values in a table column
1043 table.getUniqueColValues = function(t,col) {
1044 var values={}, bodies = this.resolve(t).tBodies;
1045 for (var i=0,L=bodies.length; i<L; i++) {
1046 var tbody = bodies[i];
1047 for (var r=0,L2=tbody.rows.length; r<L2; r++) {
1048 values[this.getCellValue(tbody.rows[r].cells[col])] = true;
1051 var valArray = [];
1052 for (var val in values) {
1053 valArray.push(val);
1055 return valArray.sort();
1059 * Scan the document on load and add sorting, filtering, paging etc ability automatically
1060 * based on existence of class names on the table and cells.
1062 table.auto = function(args) {
1063 var cells = [], tables = document.getElementsByTagName("TABLE");
1064 var val,tdata;
1065 if (tables!=null) {
1066 for (var i=0,L=tables.length; i<L; i++) {
1067 var t = table.resolve(tables[i]);
1068 tdata = table.tabledata[t.id];
1069 if (val=classValue(t,table.StripeClassNamePrefix)) {
1070 tdata.stripeclass=val;
1072 // Do auto-filter if necessary
1073 if (hasClass(t,table.AutoFilterClassName)) {
1074 table.autofilter(t);
1076 // Do auto-page if necessary
1077 if (val = classValue(t,table.AutoPageSizePrefix)) {
1078 table.autopage(t,{'pagesize':+val});
1080 // Do auto-sort if necessary
1081 if ((val = classValue(t,table.AutoSortColumnPrefix)) || (hasClass(t,table.AutoSortClassName))) {
1082 table.autosort(t,{'col':(val==null)?null:+val});
1084 // Do auto-stripe if necessary
1085 if (tdata.stripeclass && hasClass(t,table.AutoStripeClassName)) {
1086 table.stripe(t);
1093 * Add sorting functionality to a table header cell
1095 table.autosort = function(t,args) {
1096 t = this.resolve(t,args);
1097 var tdata = this.tabledata[t.id];
1098 this.processTableCells(t, "THEAD", function(c) {
1099 var type = classValue(c,table.SortableColumnPrefix);
1100 if (type!=null) {
1101 type = type || "default";
1102 c.title =c.title || table.AutoSortTitle;
1103 addClass(c,table.SortableClassName);
1104 c.onclick = Function("","Table.sort(this,{'sorttype':Sort['"+type+"']})");
1105 // If we are going to auto sort on a column, we need to keep track of what kind of sort it will be
1106 if (args.col!=null) {
1107 if (args.col==table.getActualCellIndex(c)) {
1108 tdata.sorttype=Sort['"+type+"'];
1112 } );
1113 if (args.col!=null) {
1114 table.sort(t,args);
1119 * Add paging functionality to a table
1121 table.autopage = function(t,args) {
1122 t = this.resolve(t,args);
1123 var tdata = this.tabledata[t.id];
1124 if (tdata.pagesize) {
1125 this.processTableCells(t, "THEAD,TFOOT", function(c) {
1126 var type = classValue(c,table.AutoPageJumpPrefix);
1127 if (type=="next") { type = 1; }
1128 else if (type=="previous") { type = -1; }
1129 if (type!=null) {
1130 c.onclick = Function("","Table.pageJump(this,"+type+")");
1132 } );
1133 if (val = classValue(t,table.PageNumberPrefix)) {
1134 tdata.container_number = document.getElementById(val);
1136 if (val = classValue(t,table.PageCountPrefix)) {
1137 tdata.container_count = document.getElementById(val);
1139 return table.page(t,0,args);
1144 * A util function to cancel bubbling of clicks on filter dropdowns
1146 table.cancelBubble = function(e) {
1147 e = e || window.event;
1148 if (typeof(e.stopPropagation)=="function") { e.stopPropagation(); }
1149 if (def(e.cancelBubble)) { e.cancelBubble = true; }
1153 * Auto-filter a table
1155 table.autofilter = function(t,args) {
1156 args = args || {};
1157 t = this.resolve(t,args);
1158 var tdata = this.tabledata[t.id],val;
1159 table.processTableCells(t, "THEAD", function(cell) {
1160 if (hasClass(cell,table.FilterableClassName)) {
1161 var cellIndex = table.getCellIndex(cell);
1162 var colValues = table.getUniqueColValues(t,cellIndex);
1163 if (colValues.length>0) {
1164 if (typeof(args.insert)=="function") {
1165 func.insert(cell,colValues);
1167 else {
1168 var sel = '<select onchange="Table.filter(this,this)" onclick="Table.cancelBubble(event)" class="'+table.AutoFilterClassName+'"><option value="">'+table.FilterAllLabel+'</option>';
1169 for (var i=0; i<colValues.length; i++) {
1170 sel += '<option value="'+colValues[i]+'">'+colValues[i]+'</option>';
1172 sel += '</select>';
1173 cell.innerHTML += "<br>"+sel;
1178 if (val = classValue(t,table.FilteredRowcountPrefix)) {
1179 tdata.container_filtered_count = document.getElementById(val);
1181 if (val = classValue(t,table.RowcountPrefix)) {
1182 tdata.container_all_count = document.getElementById(val);
1187 * Attach the auto event so it happens on load.
1188 * use jQuery's ready() function if available
1190 if (typeof(jQuery)!="undefined") {
1191 jQuery(table.auto);
1193 else if (window.addEventListener) {
1194 window.addEventListener( "load", table.auto, false );
1196 else if (window.attachEvent) {
1197 window.attachEvent( "onload", table.auto );
1200 return table;
1201 })();
1205 maketree_js = """/**
1206 * Copyright (c)2005-2007 Matt Kruse (javascripttoolbox.com)
1208 * Dual licensed under the MIT and GPL licenses.
1209 * This basically means you can use this code however you want for
1210 * free, but don't claim to have written it yourself!
1211 * Donations always accepted: http://www.JavascriptToolbox.com/donate/
1213 * Please do not link to the .js files on javascripttoolbox.com from
1214 * your site. Copy the files locally to your server instead.
1218 This code is inspired by and extended from Stuart Langridge's aqlist code:
1219 http://www.kryogenix.org/code/browser/aqlists/
1220 Stuart Langridge, November 2002
1221 sil@kryogenix.org
1222 Inspired by Aaron's labels.js (http://youngpup.net/demos/labels/)
1223 and Dave Lindquist's menuDropDown.js (http://www.gazingus.org/dhtml/?id=109)
1226 // Automatically attach a listener to the window onload, to convert the trees
1227 addEvent(window,"load",convertTrees);
1229 // Utility function to add an event listener
1230 function addEvent(o,e,f){
1231 if (o.addEventListener){ o.addEventListener(e,f,false); return true; }
1232 else if (o.attachEvent){ return o.attachEvent("on"+e,f); }
1233 else { return false; }
1236 // utility function to set a global variable if it is not already set
1237 function setDefault(name,val) {
1238 if (typeof(window[name])=="undefined" || window[name]==null) {
1239 window[name]=val;
1243 // Full expands a tree with a given ID
1244 function expandTree(treeId) {
1245 var ul = document.getElementById(treeId);
1246 if (ul == null) { return false; }
1247 expandCollapseList(ul,nodeOpenClass);
1250 // Fully collapses a tree with a given ID
1251 function collapseTree(treeId) {
1252 var ul = document.getElementById(treeId);
1253 if (ul == null) { return false; }
1254 expandCollapseList(ul,nodeClosedClass);
1257 // Expands enough nodes to expose an LI with a given ID
1258 function expandToItem(treeId,itemId) {
1259 var ul = document.getElementById(treeId);
1260 if (ul == null) { return false; }
1261 var ret = expandCollapseList(ul,nodeOpenClass,itemId);
1262 if (ret) {
1263 var o = document.getElementById(itemId);
1264 if (o.scrollIntoView) {
1265 o.scrollIntoView(false);
1270 // Performs 3 functions:
1271 // a) Expand all nodes
1272 // b) Collapse all nodes
1273 // c) Expand all nodes to reach a certain ID
1274 function expandCollapseList(ul,cName,itemId) {
1275 if (!ul.childNodes || ul.childNodes.length==0) { return false; }
1276 // Iterate LIs
1277 for (var itemi=0;itemi<ul.childNodes.length;itemi++) {
1278 var item = ul.childNodes[itemi];
1279 if (itemId!=null && item.id==itemId) { return true; }
1280 if (item.nodeName == "LI") {
1281 // Iterate things in this LI
1282 var subLists = false;
1283 for (var sitemi=0;sitemi<item.childNodes.length;sitemi++) {
1284 var sitem = item.childNodes[sitemi];
1285 if (sitem.nodeName=="UL") {
1286 subLists = true;
1287 var ret = expandCollapseList(sitem,cName,itemId);
1288 if (itemId!=null && ret) {
1289 item.className=cName;
1290 return true;
1294 if (subLists && itemId==null) {
1295 item.className = cName;
1301 // Search the document for UL elements with the correct CLASS name, then process them
1302 function convertTrees() {
1303 setDefault("treeClass","mktree");
1304 setDefault("nodeClosedClass","liClosed");
1305 setDefault("nodeOpenClass","liOpen");
1306 setDefault("nodeBulletClass","liBullet");
1307 setDefault("nodeLinkClass","bullet");
1308 setDefault("preProcessTrees",true);
1309 if (preProcessTrees) {
1310 if (!document.createElement) { return; } // Without createElement, we can't do anything
1311 var uls = document.getElementsByTagName("ul");
1312 if (uls==null) { return; }
1313 var uls_length = uls.length;
1314 for (var uli=0;uli<uls_length;uli++) {
1315 var ul=uls[uli];
1316 if (ul.nodeName=="UL" && ul.className==treeClass) {
1317 processList(ul);
1323 function treeNodeOnclick() {
1324 this.parentNode.className = (this.parentNode.className==nodeOpenClass) ? nodeClosedClass : nodeOpenClass;
1325 return false;
1327 function retFalse() {
1328 return false;
1330 // Process a UL tag and all its children, to convert to a tree
1331 function processList(ul) {
1332 if (!ul.childNodes || ul.childNodes.length==0) { return; }
1333 // Iterate LIs
1334 var childNodesLength = ul.childNodes.length;
1335 for (var itemi=0;itemi<childNodesLength;itemi++) {
1336 var item = ul.childNodes[itemi];
1337 if (item.nodeName == "LI") {
1338 // Iterate things in this LI
1339 var subLists = false;
1340 var itemChildNodesLength = item.childNodes.length;
1341 for (var sitemi=0;sitemi<itemChildNodesLength;sitemi++) {
1342 var sitem = item.childNodes[sitemi];
1343 if (sitem.nodeName=="UL") {
1344 subLists = true;
1345 processList(sitem);
1348 var s= document.createElement("SPAN");
1349 var t= '\u00A0'; // &nbsp;
1350 s.className = nodeLinkClass;
1351 if (subLists) {
1352 // This LI has UL's in it, so it's a +/- node
1353 if (item.className==null || item.className=="") {
1354 item.className = nodeClosedClass;
1356 // If it's just text, make the text work as the link also
1357 if (item.firstChild.nodeName=="#text") {
1358 t = t+item.firstChild.nodeValue;
1359 item.removeChild(item.firstChild);
1361 s.onclick = treeNodeOnclick;
1363 else {
1364 // No sublists, so it's just a bullet node
1365 item.className = nodeBulletClass;
1366 s.onclick = retFalse;
1368 s.appendChild(document.createTextNode(t));
1369 item.insertBefore(s,item.firstChild);
1375 stimelist = []
1378 def make_html_file(metadata, results, tag, host, output_file_name, dirname):
1380 Create HTML file contents for the job report, to stdout or filesystem.
1382 @param metadata: Dictionary with Job metadata (tests, exec time, etc).
1383 @param results: List with testcase results.
1384 @param tag: Job tag.
1385 @param host: Client hostname.
1386 @param output_file_name: Output file name. If empty string, prints to
1387 stdout.
1388 @param dirname: Prefix for HTML links. If empty string, the HTML links
1389 will be relative to the results dir.
1391 html_prefix = """
1392 <!DOCTYPE HTML PUBLIC "-//W3C//DTD HTML 4.01//EN" "http://www.w3.org/TR/html4/strict.dtd">
1393 <html>
1394 <head>
1395 <title>Autotest job execution results</title>
1396 <style type="text/css">
1398 </style>
1399 <script type="text/javascript">
1402 function popup(tag,text) {
1403 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');
1404 w.document.open("text/html", "replace");
1405 w.document.write(text);
1406 w.document.close();
1407 return true;
1409 </script>
1410 </head>
1411 <body>
1412 """ % (format_css, table_js, maketree_js)
1414 if output_file_name:
1415 output = open(output_file_name, "w")
1416 else: #if no output file defined, print html file to console
1417 output = sys.stdout
1418 # create html page
1419 print >> output, html_prefix
1420 print >> output, '<h2 id=\"page_title\">Autotest job execution report</h2>'
1422 # formating date and time to print
1423 t = datetime.datetime.now()
1425 epoch_sec = time.mktime(t.timetuple())
1426 now = datetime.datetime.fromtimestamp(epoch_sec)
1428 # basic statistics
1429 total_executed = 0
1430 total_failed = 0
1431 total_passed = 0
1432 for res in results:
1433 total_executed += 1
1434 if res['status'] == 'GOOD':
1435 total_passed += 1
1436 else:
1437 total_failed += 1
1438 stat_str = 'No test cases executed'
1439 if total_executed > 0:
1440 failed_perct = int(float(total_failed)/float(total_executed)*100)
1441 stat_str = ('From %d tests executed, %d have passed (%d%% failures)' %
1442 (total_executed, total_passed, failed_perct))
1444 kvm_ver_str = metadata.get('kvmver', None)
1446 print >> output, '<table class="stats2">'
1447 print >> output, '<tr><td>HOST</td><td>:</td><td>%s</td></tr>' % host
1448 print >> output, '<tr><td>RESULTS DIR</td><td>:</td><td>%s</td></tr>' % tag
1449 print >> output, '<tr><td>DATE</td><td>:</td><td>%s</td></tr>' % now.ctime()
1450 print >> output, '<tr><td>STATS</td><td>:</td><td>%s</td></tr>'% stat_str
1451 print >> output, '<tr><td></td><td></td><td></td></tr>'
1452 if kvm_ver_str is not None:
1453 print >> output, '<tr><td>KVM VERSION</td><td>:</td><td>%s</td></tr>' % kvm_ver_str
1454 print >> output, '</table>'
1456 ## print test results
1457 print >> output, '<br>'
1458 print >> output, '<h2 id=\"page_sub_title\">Test Results</h2>'
1459 print >> output, '<h2 id=\"comment\">click on table headers to asc/desc sort</h2>'
1460 result_table_prefix = """<table
1461 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">
1462 <thead class="th table-sorted-asc table-sorted-desc">
1463 <tr>
1464 <th align="left" class="table-sortable:alphanumeric">Date/Time</th>
1465 <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>
1466 <th align="left" class="table-filterable table-sortable:alphanumeric">Status</th>
1467 <th align="left">Time (sec)</th>
1468 <th align="left">Info</th>
1469 <th align="left">Debug</th>
1470 </tr></thead>
1471 <tbody>
1473 print >> output, result_table_prefix
1474 for res in results:
1475 print >> output, '<tr>'
1476 print >> output, '<td align="left">%s</td>' % res['time']
1477 print >> output, '<td align="left">%s</td>' % res['testcase']
1478 if res['status'] == 'GOOD':
1479 print >> output, '<td align=\"left\"><b><font color="#00CC00">PASS</font></b></td>'
1480 elif res['status'] == 'FAIL':
1481 print >> output, '<td align=\"left\"><b><font color="red">FAIL</font></b></td>'
1482 elif res['status'] == 'ERROR':
1483 print >> output, '<td align=\"left\"><b><font color="red">ERROR!</font></b></td>'
1484 else:
1485 print >> output, '<td align=\"left\">%s</td>' % res['status']
1486 # print exec time (seconds)
1487 print >> output, '<td align="left">%s</td>' % res['exec_time_sec']
1488 # print log only if test failed..
1489 if res['log']:
1490 #chop all '\n' from log text (to prevent html errors)
1491 rx1 = re.compile('(\s+)')
1492 log_text = rx1.sub(' ', res['log'])
1494 # allow only a-zA-Z0-9_ in html title name
1495 # (due to bug in MS-explorer)
1496 rx2 = re.compile('([^a-zA-Z_0-9])')
1497 updated_tag = rx2.sub('_', res['title'])
1499 html_body_text = '<html><head><title>%s</title></head><body>%s</body></html>' % (str(updated_tag), log_text)
1500 print >> output, '<td align=\"left\"><A HREF=\"#\" onClick=\"popup(\'%s\',\'%s\')\">Info</A></td>' % (str(updated_tag), str(html_body_text))
1501 else:
1502 print >> output, '<td align=\"left\"></td>'
1503 # print execution time
1504 print >> output, '<td align="left"><A HREF=\"%s\">Debug</A></td>' % os.path.join(dirname, res['title'], "debug")
1506 print >> output, '</tr>'
1507 print >> output, "</tbody></table>"
1510 print >> output, '<h2 id=\"page_sub_title\">Host Info</h2>'
1511 print >> output, '<h2 id=\"comment\">click on each item to expend/collapse</h2>'
1512 ## Meta list comes here..
1513 print >> output, '<p>'
1514 print >> output, '<A href="#" class="button" onClick="expandTree(\'meta_tree\');return false;">Expand All</A>'
1515 print >> output, '&nbsp;&nbsp;&nbsp'
1516 print >> output, '<A class="button" href="#" onClick="collapseTree(\'meta_tree\'); return false;">Collapse All</A>'
1517 print >> output, '</p>'
1519 print >> output, '<ul class="mktree" id="meta_tree">'
1520 counter = 0
1521 keys = metadata.keys()
1522 keys.sort()
1523 for key in keys:
1524 val = metadata[key]
1525 print >> output, '<li id=\"meta_headline\">%s' % key
1526 print >> output, '<ul><table class="meta_table"><tr><td align="left">%s</td></tr></table></ul></li>' % val
1527 print >> output, '</ul>'
1529 print >> output, "</body></html>"
1530 if output_file_name:
1531 output.close()
1534 def parse_result(dirname, line):
1536 Parse job status log line.
1538 @param dirname: Job results dir
1539 @param line: Status log line.
1541 parts = line.split()
1542 if len(parts) < 4:
1543 return None
1544 global stimelist
1545 if parts[0] == 'START':
1546 pair = parts[3].split('=')
1547 stime = int(pair[1])
1548 stimelist.append(stime)
1550 elif (parts[0] == 'END'):
1551 result = {}
1552 exec_time = ''
1553 # fetch time stamp
1554 if len(parts) > 7:
1555 temp = parts[5].split('=')
1556 exec_time = temp[1] + ' ' + parts[6] + ' ' + parts[7]
1557 # assign default values
1558 result['time'] = exec_time
1559 result['testcase'] = 'na'
1560 result['status'] = 'na'
1561 result['log'] = None
1562 result['exec_time_sec'] = 'na'
1563 tag = parts[3]
1565 # assign actual values
1566 rx = re.compile('^(\w+)\.(.*)$')
1567 m1 = rx.findall(parts[3])
1568 result['testcase'] = str(tag)
1569 result['title'] = str(tag)
1570 result['status'] = parts[1]
1571 if result['status'] != 'GOOD':
1572 result['log'] = get_exec_log(dirname, tag)
1573 if len(stimelist)>0:
1574 pair = parts[4].split('=')
1575 etime = int(pair[1])
1576 stime = stimelist.pop()
1577 total_exec_time_sec = etime - stime
1578 result['exec_time_sec'] = total_exec_time_sec
1579 return result
1580 return None
1583 def get_exec_log(resdir, tag):
1585 Get job execution summary.
1587 @param resdir: Job results dir.
1588 @param tag: Job tag.
1590 stdout_file = os.path.join(resdir, tag, 'debug', 'stdout')
1591 stderr_file = os.path.join(resdir, tag, 'debug', 'stderr')
1592 status_file = os.path.join(resdir, tag, 'status')
1593 dmesg_file = os.path.join(resdir, tag, 'sysinfo', 'dmesg')
1594 log = ''
1595 log += '<br><b>STDERR:</b><br>'
1596 log += get_info_file(stderr_file)
1597 log += '<br><b>STDOUT:</b><br>'
1598 log += get_info_file(stdout_file)
1599 log += '<br><b>STATUS:</b><br>'
1600 log += get_info_file(status_file)
1601 log += '<br><b>DMESG:</b><br>'
1602 log += get_info_file(dmesg_file)
1603 return log
1606 def get_info_file(filename):
1608 Gets the contents of an autotest info file.
1610 It also and highlights the file contents with possible problems.
1612 @param filename: Info file path.
1614 data = ''
1615 errors = re.compile(r"\b(error|fail|failed)\b", re.IGNORECASE)
1616 if os.path.isfile(filename):
1617 f = open('%s' % filename, "r")
1618 lines = f.readlines()
1619 f.close()
1620 rx = re.compile('(\'|\")')
1621 for line in lines:
1622 new_line = rx.sub('', line)
1623 errors_found = errors.findall(new_line)
1624 if len(errors_found) > 0:
1625 data += '<font color=red>%s</font><br>' % str(new_line)
1626 else:
1627 data += '%s<br>' % str(new_line)
1628 if not data:
1629 data = 'No Information Found.<br>'
1630 else:
1631 data = 'File not found.<br>'
1632 return data
1635 def usage():
1637 Print stand alone program usage.
1639 print 'usage:',
1640 print 'make_html_report.py -r <result_directory> [-f output_file] [-R]'
1641 print '(e.g. make_html_reporter.py -r '\
1642 '/usr/local/autotest/client/results/default -f /tmp/myreport.html)'
1643 print 'add "-R" for an html report with relative-paths (relative '\
1644 'to results directory)'
1645 print ''
1646 sys.exit(1)
1649 def get_keyval_value(result_dir, key):
1651 Return the value of the first appearance of key in any keyval file in
1652 result_dir. If no appropriate line is found, return 'Unknown'.
1654 @param result_dir: Path that holds the keyval files.
1655 @param key: Specific key we're retrieving.
1657 keyval_pattern = os.path.join(result_dir, "kvm.*", "keyval")
1658 keyval_lines = commands.getoutput(r"grep -h '\b%s\b.*=' %s"
1659 % (key, keyval_pattern))
1660 if not keyval_lines:
1661 return "Unknown"
1662 keyval_line = keyval_lines.splitlines()[0]
1663 if key in keyval_line and "=" in keyval_line:
1664 return keyval_line.split("=")[1].strip()
1665 else:
1666 return "Unknown"
1669 def get_kvm_version(result_dir):
1671 Return an HTML string describing the KVM version.
1673 @param result_dir: An Autotest job result dir.
1675 kvm_version = get_keyval_value(result_dir, "kvm_version")
1676 kvm_userspace_version = get_keyval_value(result_dir,
1677 "kvm_userspace_version")
1678 if kvm_version == "Unknown" or kvm_userspace_version == "Unknown":
1679 return None
1680 return "Kernel: %s<br>Userspace: %s" % (kvm_version, kvm_userspace_version)
1683 def create_report(dirname, html_path='', output_file_name=None):
1685 Create an HTML report with info about an autotest client job.
1687 If no relative path (html_path) or output file name provided, an HTML
1688 file in the toplevel job results dir called 'job_report.html' will be
1689 created, with relative links.
1691 @param html_path: Prefix for the HTML links. Useful to specify absolute
1692 in the report (not wanted most of the time).
1693 @param output_file_name: Path to the report file.
1695 res_dir = os.path.abspath(dirname)
1696 tag = res_dir
1697 status_file_name = os.path.join(dirname, 'status')
1698 sysinfo_dir = os.path.join(dirname, 'sysinfo')
1699 host = get_info_file(os.path.join(sysinfo_dir, 'hostname'))
1700 rx = re.compile('^\s+[END|START].*$')
1701 # create the results set dict
1702 results_data = []
1703 if os.path.exists(status_file_name):
1704 f = open(status_file_name, "r")
1705 lines = f.readlines()
1706 f.close()
1707 for line in lines:
1708 if rx.match(line):
1709 result_dict = parse_result(dirname, line)
1710 if result_dict:
1711 results_data.append(result_dict)
1712 # create the meta info dict
1713 metalist = {
1714 'uname': get_info_file(os.path.join(sysinfo_dir, 'uname')),
1715 'cpuinfo':get_info_file(os.path.join(sysinfo_dir, 'cpuinfo')),
1716 'meminfo':get_info_file(os.path.join(sysinfo_dir, 'meminfo')),
1717 'df':get_info_file(os.path.join(sysinfo_dir, 'df')),
1718 'modules':get_info_file(os.path.join(sysinfo_dir, 'modules')),
1719 'gcc':get_info_file(os.path.join(sysinfo_dir, 'gcc_--version')),
1720 'dmidecode':get_info_file(os.path.join(sysinfo_dir, 'dmidecode')),
1721 'dmesg':get_info_file(os.path.join(sysinfo_dir, 'dmesg')),
1723 if get_kvm_version(dirname) is not None:
1724 metalist['kvm_ver'] = get_kvm_version(dirname)
1726 if output_file_name is None:
1727 output_file_name = os.path.join(dirname, 'job_report.html')
1728 make_html_file(metalist, results_data, tag, host, output_file_name,
1729 html_path)
1732 def main(argv):
1734 Parses the arguments and executes the stand alone program.
1736 dirname = None
1737 output_file_name = None
1738 relative_path = False
1739 try:
1740 opts, args = getopt.getopt(argv, "r:f:h:R", ['help'])
1741 except getopt.GetoptError:
1742 usage()
1743 sys.exit(2)
1744 for opt, arg in opts:
1745 if opt in ("-h", "--help"):
1746 usage()
1747 sys.exit()
1748 elif opt == '-r':
1749 dirname = arg
1750 elif opt == '-f':
1751 output_file_name = arg
1752 elif opt == '-R':
1753 relative_path = True
1754 else:
1755 usage()
1756 sys.exit(1)
1758 html_path = dirname
1759 # don't use absolute path in html output if relative flag passed
1760 if relative_path:
1761 html_path = ''
1763 if dirname:
1764 if os.path.isdir(dirname): # TBD: replace it with a validation of
1765 # autotest result dir
1766 create_report(dirname, html_path, output_file_name)
1767 sys.exit(0)
1768 else:
1769 print 'Invalid result directory <%s>' % dirname
1770 sys.exit(1)
1771 else:
1772 usage()
1773 sys.exit(1)
1776 if __name__ == "__main__":
1777 main(sys.argv[1:])