1 #!/usr/bin/python 2 """ 3 Module used to parse the autotest job results and generate an HTML report. 4 5 @copyright: (c)2005-2007 Matt Kruse (javascripttoolbox.com) 6 @copyright: Red Hat 2008-2009 7 @author: Dror Russo (drusso (at] redhat.com) 8 """ 9 10 import os, sys, re, getopt, time, datetime, commands 11 import common 12 13 14 format_css = """ 15 html,body { 16 padding:0; 17 color:#222; 18 background:#FFFFFF; 19 } 20 21 body { 22 padding:0px; 23 font:76%/150% "Lucida Grande", "Lucida Sans Unicode", Lucida, Verdana, Geneva, Arial, Helvetica, sans-serif; 24 } 25 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; 33 } 34 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; 42 } 43 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; 51 } 52 53 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; 61 } 62 63 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;} 76 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;} 85 86 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;} 99 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;} 108 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;} 116 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;} 126 127 table.stats th.table-sorted-asc{ 128 background-image: url(ascending.gif); 129 background-position: top left ; 130 background-repeat: no-repeat; 131 } 132 133 table.stats th.table-sorted-desc{ 134 background-image: url(descending.gif); 135 background-position: top left; 136 background-repeat: no-repeat; 137 } 138 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; 145 } 146 147 table.stats2 td{ 148 background-color: #FFFFFF; 149 color: #000; 150 padding: 0px; 151 font-weight: bold; 152 font-size: 13px;} 153 154 155 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; } 171 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 */ 174 /* 175 ul.mktree li ul li { font-size: 90% } 176 */ 177 } 178 """ 179 180 181 table_js = """ 182 /** 183 * Copyright (c)2005-2007 Matt Kruse (javascripttoolbox.com) 184 * 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/ 189 * 190 * Please do not link to the .js files on javascripttoolbox.com from 191 * your site. Copy the files locally to your server instead. 192 * 193 */ 194 /** 195 * Table.js 196 * Functions for interactive Tables 197 * 198 * Copyright (c) 2007 Matt Kruse (javascripttoolbox.com) 199 * Dual licensed under the MIT and GPL licenses. 200 * 201 * @version 0.981 202 * 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. 210 * 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? 215 */ 216 217 /** 218 * Sort Functions 219 */ 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; 226 }; 227 sort.alphanumeric_rev = function(a,b) { 228 return (a==b)?0:(a<b)?1:-1; 229 }; 230 sort['default'] = sort.alphanumeric; // IE chokes on sort.default 231 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; 237 } 238 return val || 0; 239 }; 240 }; 241 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); 247 } 248 if (sort.numeric.convert(a)==sort.numeric.convert(b)) { 249 return 0; 250 } 251 if (sort.numeric.convert(a)<sort.numeric.convert(b)) { 252 return 1; 253 } 254 }; 255 256 257 // Numeric Sort 258 // ------------ 259 sort.numeric = function(a,b) { 260 return sort.numeric.convert(a)-sort.numeric.convert(b); 261 }; 262 sort.numeric.convert = sort.numeric_converter("."); 263 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); 268 }; 269 sort.numeric_comma.convert = sort.numeric_converter(","); 270 271 // Case-insensitive Sort 272 // --------------------- 273 sort.ignorecase = function(a,b) { 274 return sort.alphanumeric(sort.ignorecase.convert(a),sort.ignorecase.convert(b)); 275 }; 276 sort.ignorecase.convert = function(val) { 277 if (val==null) { return ""; } 278 return (""+val).toLowerCase(); 279 }; 280 281 // Currency Sort 282 // ------------- 283 sort.currency = sort.numeric; // Just treat it as numeric! 284 sort.currency_comma = sort.numeric_comma; 285 286 // Date sort 287 // --------- 288 sort.date = function(a,b) { 289 return sort.numeric(sort.date.convert(a),sort.date.convert(b)); 290 }; 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; 297 }; 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();} } } 305 ]; 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; } 312 } 313 } 314 return 9999999999999; // So non-parsed dates will be last, not first 315 }; 316 317 return sort; 318 })(); 319 320 /** 321 * The main Table namespace 322 */ 323 var Table = (function(){ 324 325 /** 326 * Determine if a reference is defined 327 */ 328 function def(o) {return (typeof o!="undefined");}; 329 330 /** 331 * Determine if an object or class string contains a given class. 332 */ 333 function hasClass(o,name) { 334 return new RegExp("(^|\\\s)"+name+"(\\\s|$)").test(o.className); 335 }; 336 337 /** 338 * Add a class to an object 339 */ 340 function addClass(o,name) { 341 var c = o.className || ""; 342 if (def(c) && !hasClass(o,name)) { 343 o.className += (c?" ":"") + name; 344 } 345 }; 346 347 /** 348 * Remove a class from an object 349 */ 350 function removeClass(o,name) { 351 var c = o.className || ""; 352 o.className = c.replace(new RegExp("(^|\\\s)"+name+"(\\\s|$)"),"$1"); 353 }; 354 355 /** 356 * For classes that match a given substring, return the rest 357 */ 358 function classValue(o,prefix) { 359 var c = o.className; 360 if (c.match(new RegExp("(^|\\\s)"+prefix+"([^ ]+)"))) { 361 return RegExp.$2; 362 } 363 return null; 364 }; 365 366 /** 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. 371 */ 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); 378 } 379 else if (window.currentStyle) { 380 return(isHidden = function(o) { 381 return 'none'==o.currentStyle['display']; 382 })(o); 383 } 384 return (isHidden = function(o) { 385 return 'none'==o.style['display']; 386 })(o); 387 }; 388 389 /** 390 * Get a parent element by tag name, or the original element if it is of the tag type 391 */ 392 function getParent(o,a,b) { 393 if (o!=null && o.nodeName) { 394 if (o.nodeName==a || (b && o.nodeName==b)) { 395 return o; 396 } 397 while (o=o.parentNode) { 398 if (o.nodeName && (o.nodeName==a || (b && o.nodeName==b))) { 399 return o; 400 } 401 } 402 } 403 return null; 404 }; 405 406 /** 407 * Utility function to copy properties from one object to another 408 */ 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]; 414 } 415 } 416 } 417 418 // The table object itself 419 var table = { 420 //Class names used in the code 421 AutoStripeClassName:"table-autostripe", 422 StripeClassNamePrefix:"table-stripeclass:", 423 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", 432 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", 439 440 AutoPageSizePrefix:"table-autopage:", 441 AutoPageJumpPrefix:"table-page:", 442 PageNumberPrefix:"table-page-number:", 443 PageCountPrefix:"table-page-count:" 444 }; 445 446 /** 447 * A place to store misc table information, rather than in the table objects themselves 448 */ 449 table.tabledata = {}; 450 451 /** 452 * Resolve a table given an element reference, and make sure it has a unique ID 453 */ 454 table.uniqueId=1; 455 table.resolve = function(o,args) { 456 if (o!=null && o.nodeName && o.nodeName!="TABLE") { 457 o = getParent(o,"TABLE"); 458 } 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; 465 } 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"); 469 } 470 return o; 471 }; 472 473 474 /** 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. 477 */ 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); 483 } 484 if (type!="THEAD") { 485 this.processCells(t.tFoot, func, arg); 486 } 487 }; 488 489 /** 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. 493 */ 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); 505 } 506 } 507 } 508 } 509 } 510 }; 511 512 /** 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. 517 */ 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); 527 } 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; 532 } 533 } 534 } 535 return 0; 536 }; 537 538 /** 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. 541 */ 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; 546 } 547 return ""; 548 }, 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; 553 } 554 return ""; 555 }, 556 'IMG':function(node) { 557 return node.name || ""; 558 } 559 }; 560 561 /** 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 564 */ 565 table.getCellValue = function(td,useInnerText) { 566 if (useInnerText && def(td.innerText)) { 567 return td.innerText; 568 } 569 if (!td.childNodes) { 570 return ""; 571 } 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); 583 } 584 else { 585 ret += this.getCellValue(node); 586 } 587 } 588 else if (type==3) { 589 if (def(node.innerText)) { 590 ret += node.innerText; 591 } 592 else if (def(node.nodeValue)) { 593 ret += node.nodeValue; 594 } 595 } 596 } 597 return ret; 598 }; 599 600 /** 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 606 */ 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); 612 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]; 616 } 617 618 var matrix = []; 619 this.tableHeaderIndexes[tableObj.id] = {}; 620 var thead = getParent(tableCellObj,"THEAD"); 621 var trs = thead.getElementsByTagName('TR'); 622 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] = []; 639 } 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; 646 } 647 } 648 this.tableHeaderIndexes[tableObj.id][cellId] = firstAvailCol; 649 for (var k=rowIndex; k<rowIndex+rowSpan; k++) { 650 if(!def(matrix[k])) { 651 matrix[k] = []; 652 } 653 var matrixrow = matrix[k]; 654 for (var l=firstAvailCol; l<firstAvailCol+colSpan; l++) { 655 matrixrow[l] = "x"; 656 } 657 } 658 } 659 } 660 // Store the map so future lookups are fast. 661 return this.tableHeaderIndexes[tableObj.id][cellCoordinates]; 662 }; 663 664 /** 665 * Sort all rows in each TBODY (tbodies are sorted independent of each other) 666 */ 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}; 672 } 673 args = args || {}; 674 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; 678 } 679 // If no sort type is specified, default to the default sort 680 args.sorttype = args.sorttype || Sort['default']; 681 682 // Resolve the table 683 t = this.resolve(o,args); 684 tdata = this.tabledata[t.id]; 685 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; 689 } 690 else { 691 tdata.desc = !!args.desc; 692 } 693 694 // Store the last sorted column so clicking again will reverse the sort order 695 tdata.lastcol=tdata.col; 696 tdata.lastdesc=!!tdata.desc; 697 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; 703 } 704 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); 715 } 716 } 717 } 718 ); 719 720 // Sort each tbody independently 721 var bodies = t.tBodies; 722 if (bodies==null || bodies.length==0) { return; } 723 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]);}; 728 729 var useinnertext=!!tdata.useinnertext; 730 var col = tdata.col; 731 732 for (var i=0,L=bodies.length; i<L; i++) { 733 var tb = bodies[i], tbrows = tb.rows, rows = []; 734 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]]; 747 } 748 } while (cRow=tbrows[++cRowIndex]) 749 } 750 751 // Do the actual sorting 752 rows.sort(newSortFunc); 753 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]) 762 } 763 } 764 } 765 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 769 } 770 else { 771 // Re-stripe if a class name was supplied 772 if (tdata.stripeclass) { 773 this.stripe(t,tdata.stripeclass,!!tdata.ignorehiddenrows); 774 } 775 } 776 }; 777 778 /** 779 * Apply a filter to rows in a table and hide those that do not match. 780 */ 781 table.filter = function(o,filters,args) { 782 var cell; 783 args = args || {}; 784 785 var t = this.resolve(o,args); 786 var tdata = this.tabledata[t.id]; 787 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; 792 } 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 }; 797 } 798 // Also allow for a regular input 799 if (filters.nodeName=="INPUT" && filters.type=="text") { 800 filters={ 'filter':"/"+filters.value+"/" }; 801 } 802 // Force filters to be an array 803 if (typeof(filters)=="object" && !filters.length) { 804 filters = [filters]; 805 } 806 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; 815 } 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); 819 } 820 } 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); 827 } 828 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]; 832 } 833 else { 834 tdata.filters = tdata.filters || {}; 835 tdata.filters[filter.col] = filter.filter; 836 } 837 } 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; 842 } 843 } 844 // Everything's been setup, so now scrape the table rows 845 return table.scrape(o); 846 }; 847 848 /** 849 * "Page" a table by showing only a subset of the rows 850 */ 851 table.page = function(t,page,args) { 852 args = args || {}; 853 if (def(page)) { args.page = page; } 854 return table.scrape(t,args); 855 }; 856 857 /** 858 * Jump forward or back any number of pages 859 */ 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); 863 }; 864 865 /** 866 * Go to the next page of a paged table 867 */ 868 table.pageNext = function(t,args) { 869 return this.pageJump(t,1,args); 870 }; 871 872 /** 873 * Go to the previous page of a paged table 874 */ 875 table.pagePrevious = function(t,args) { 876 return this.pageJump(t,-1,args); 877 }; 878 879 /** 880 * Scrape a table to either hide or show each row based on filters and paging 881 */ 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 || {}; 888 889 // Resolve the table object 890 t = this.resolve(o,args); 891 tdata = this.tabledata[t.id]; 892 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; 901 } 902 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; 911 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); 924 } 925 else if (typeof(filter)=="function") { 926 hideRow=!filter(val,cells[col]); 927 } 928 else { 929 hideRow = (val!=filter); 930 } 931 } 932 } 933 } 934 } 935 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; 946 } 947 } 948 } 949 950 row.style.display = hideRow?"none":""; 951 } 952 } 953 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=""; 962 } 963 } 964 } 965 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); 970 } 971 ); 972 973 // Stripe the table if necessary 974 if (tdata.stripeclass) { 975 this.stripe(t); 976 } 977 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; 984 } 985 if (tdata.container_count) { 986 tdata.container_count.innerHTML = pagecount; 987 } 988 } 989 990 // Update the row count containers if they exist 991 if (tdata.container_filtered_count) { 992 tdata.container_filtered_count.innerHTML = unfilteredrowcount; 993 } 994 if (tdata.container_all_count) { 995 tdata.container_all_count.innerHTML = totalrows; 996 } 997 return { 'data':tdata, 'unfilteredcount':unfilteredrowcount, 'total':totalrows, 'pagecount':pagecount, 'page':page, 'pagesize':pagesize }; 998 }; 999 1000 /** 1001 * Shade alternate rows, aka Stripe the table. 1002 */ 1003 table.stripe = function(t,className,args) { 1004 args = args || {}; 1005 args.stripeclass = className; 1006 1007 t = this.resolve(t,args); 1008 var tdata = this.tabledata[t.id]; 1009 1010 var bodies = t.tBodies; 1011 if (bodies==null || bodies.length==0) { 1012 return; 1013 } 1014 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]) 1028 } 1029 else { 1030 do { 1031 if (!isHidden(cRow)) { 1032 f[displayedCount++%2](cRow,className); 1033 } 1034 } while (cRow=tbrows[++cRowIndex]) 1035 } 1036 } 1037 } 1038 }; 1039 1040 /** 1041 * Build up a list of unique values in a table column 1042 */ 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; 1049 } 1050 } 1051 var valArray = []; 1052 for (var val in values) { 1053 valArray.push(val); 1054 } 1055 return valArray.sort(); 1056 }; 1057 1058 /** 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. 1061 */ 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; 1071 } 1072 // Do auto-filter if necessary 1073 if (hasClass(t,table.AutoFilterClassName)) { 1074 table.autofilter(t); 1075 } 1076 // Do auto-page if necessary 1077 if (val = classValue(t,table.AutoPageSizePrefix)) { 1078 table.autopage(t,{'pagesize':+val}); 1079 } 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}); 1083 } 1084 // Do auto-stripe if necessary 1085 if (tdata.stripeclass && hasClass(t,table.AutoStripeClassName)) { 1086 table.stripe(t); 1087 } 1088 } 1089 } 1090 }; 1091 1092 /** 1093 * Add sorting functionality to a table header cell 1094 */ 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+"']; 1109 } 1110 } 1111 } 1112 } ); 1113 if (args.col!=null) { 1114 table.sort(t,args); 1115 } 1116 }; 1117 1118 /** 1119 * Add paging functionality to a table 1120 */ 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+")"); 1131 } 1132 } ); 1133 if (val = classValue(t,table.PageNumberPrefix)) { 1134 tdata.container_number = document.getElementById(val); 1135 } 1136 if (val = classValue(t,table.PageCountPrefix)) { 1137 tdata.container_count = document.getElementById(val); 1138 } 1139 return table.page(t,0,args); 1140 } 1141 }; 1142 1143 /** 1144 * A util function to cancel bubbling of clicks on filter dropdowns 1145 */ 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; } 1150 }; 1151 1152 /** 1153 * Auto-filter a table 1154 */ 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); 1166 } 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>'; 1171 } 1172 sel += '</select>'; 1173 cell.innerHTML += "<br>"+sel; 1174 } 1175 } 1176 } 1177 }); 1178 if (val = classValue(t,table.FilteredRowcountPrefix)) { 1179 tdata.container_filtered_count = document.getElementById(val); 1180 } 1181 if (val = classValue(t,table.RowcountPrefix)) { 1182 tdata.container_all_count = document.getElementById(val); 1183 } 1184 }; 1185 1186 /** 1187 * Attach the auto event so it happens on load. 1188 * use jQuery's ready() function if available 1189 */ 1190 if (typeof(jQuery)!="undefined") { 1191 jQuery(table.auto); 1192 } 1193 else if (window.addEventListener) { 1194 window.addEventListener( "load", table.auto, false ); 1195 } 1196 else if (window.attachEvent) { 1197 window.attachEvent( "onload", table.auto ); 1198 } 1199 1200 return table; 1201 })(); 1202 """ 1203 1204 1205 maketree_js = """/** 1206 * Copyright (c)2005-2007 Matt Kruse (javascripttoolbox.com) 1207 * 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/ 1212 * 1213 * Please do not link to the .js files on javascripttoolbox.com from 1214 * your site. Copy the files locally to your server instead. 1215 * 1216 */ 1217 /* 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 (at] 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) 1224 */ 1225 1226 // Automatically attach a listener to the window onload, to convert the trees 1227 addEvent(window,"load",convertTrees); 1228 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; } 1234 } 1235 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; 1240 } 1241 } 1242 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); 1248 } 1249 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); 1255 } 1256 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); 1266 } 1267 } 1268 } 1269 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; 1291 } 1292 } 1293 } 1294 if (subLists && itemId==null) { 1295 item.className = cName; 1296 } 1297 } 1298 } 1299 } 1300 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); 1318 } 1319 } 1320 } 1321 } 1322 1323 function treeNodeOnclick() { 1324 this.parentNode.className = (this.parentNode.className==nodeOpenClass) ? nodeClosedClass : nodeOpenClass; 1325 return false; 1326 } 1327 function retFalse() { 1328 return false; 1329 } 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); 1346 } 1347 } 1348 var s= document.createElement("SPAN"); 1349 var t= '\u00A0'; // 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; 1355 } 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); 1360 } 1361 s.onclick = treeNodeOnclick; 1362 } 1363 else { 1364 // No sublists, so it's just a bullet node 1365 item.className = nodeBulletClass; 1366 s.onclick = retFalse; 1367 } 1368 s.appendChild(document.createTextNode(t)); 1369 item.insertBefore(s,item.firstChild); 1370 } 1371 } 1372 } 1373 """ 1374 1375 1376 1377 1378 def make_html_file(metadata, results, tag, host, output_file_name, dirname): 1379 """ 1380 Create HTML file contents for the job report, to stdout or filesystem. 1381 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. 1390 """ 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"> 1397 %s 1398 </style> 1399 <script type="text/javascript"> 1400 %s 1401 %s 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; 1408 } 1409 </script> 1410 </head> 1411 <body> 1412 """ % (format_css, table_js, maketree_js) 1413 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>' 1421 1422 # formating date and time to print 1423 t = datetime.datetime.now() 1424 1425 epoch_sec = time.mktime(t.timetuple()) 1426 now = datetime.datetime.fromtimestamp(epoch_sec) 1427 1428 # basic statistics 1429 total_executed = 0 1430 total_failed = 0 1431 total_passed = 0 1432 for res in results: 1433 if results[res][2] != None: 1434 total_executed += 1 1435 if results[res][2]['status'] == 'GOOD': 1436 total_passed += 1 1437 else: 1438 total_failed += 1 1439 stat_str = 'No test cases executed' 1440 if total_executed > 0: 1441 failed_perct = int(float(total_failed)/float(total_executed)*100) 1442 stat_str = ('From %d tests executed, %d have passed (%d%% failures)' % 1443 (total_executed, total_passed, failed_perct)) 1444 1445 kvm_ver_str = metadata.get('kvmver', None) 1446 1447 print >> output, '<table class="stats2">' 1448 print >> output, '<tr><td>HOST</td><td>:</td><td>%s</td></tr>' % host 1449 print >> output, '<tr><td>RESULTS DIR</td><td>:</td><td>%s</td></tr>' % tag 1450 print >> output, '<tr><td>DATE</td><td>:</td><td>%s</td></tr>' % now.ctime() 1451 print >> output, '<tr><td>STATS</td><td>:</td><td>%s</td></tr>'% stat_str 1452 print >> output, '<tr><td></td><td></td><td></td></tr>' 1453 if kvm_ver_str is not None: 1454 print >> output, '<tr><td>KVM VERSION</td><td>:</td><td>%s</td></tr>' % kvm_ver_str 1455 print >> output, '</table>' 1456 1457 ## print test results 1458 print >> output, '<br>' 1459 print >> output, '<h2 id=\"page_sub_title\">Test Results</h2>' 1460 print >> output, '<h2 id=\"comment\">click on table headers to asc/desc sort</h2>' 1461 result_table_prefix = """<table 1462 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"> 1463 <thead class="th table-sorted-asc table-sorted-desc"> 1464 <tr> 1465 <th align="left" class="table-sortable:alphanumeric">Date/Time</th> 1466 <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> 1467 <th align="left" class="table-filterable table-sortable:alphanumeric">Status</th> 1468 <th align="left">Time (sec)</th> 1469 <th align="left">Info</th> 1470 <th align="left">Debug</th> 1471 </tr></thead> 1472 <tbody> 1473 """ 1474 print >> output, result_table_prefix 1475 def print_result(result, indent): 1476 while result != []: 1477 r = result.pop(0) 1478 res = results[r][2] 1479 print >> output, '<tr>' 1480 print >> output, '<td align="left">%s</td>' % res['time'] 1481 print >> output, '<td align="left" style="padding-left:%dpx">%s</td>' % (indent * 20, res['title']) 1482 if res['status'] == 'GOOD': 1483 print >> output, '<td align=\"left\"><b><font color="#00CC00">PASS</font></b></td>' 1484 elif res['status'] == 'FAIL': 1485 print >> output, '<td align=\"left\"><b><font color="red">FAIL</font></b></td>' 1486 elif res['status'] == 'ERROR': 1487 print >> output, '<td align=\"left\"><b><font color="red">ERROR!</font></b></td>' 1488 else: 1489 print >> output, '<td align=\"left\">%s</td>' % res['status'] 1490 # print exec time (seconds) 1491 print >> output, '<td align="left">%s</td>' % res['exec_time_sec'] 1492 # print log only if test failed.. 1493 if res['log']: 1494 #chop all '\n' from log text (to prevent html errors) 1495 rx1 = re.compile('(\s+)') 1496 log_text = rx1.sub(' ', res['log']) 1497 1498 # allow only a-zA-Z0-9_ in html title name 1499 # (due to bug in MS-explorer) 1500 rx2 = re.compile('([^a-zA-Z_0-9])') 1501 updated_tag = rx2.sub('_', res['title']) 1502 1503 html_body_text = '<html><head><title>%s</title></head><body>%s</body></html>' % (str(updated_tag), log_text) 1504 print >> output, '<td align=\"left\"><A HREF=\"#\" onClick=\"popup(\'%s\',\'%s\')\">Info</A></td>' % (str(updated_tag), str(html_body_text)) 1505 else: 1506 print >> output, '<td align=\"left\"></td>' 1507 # print execution time 1508 print >> output, '<td align="left"><A HREF=\"%s\">Debug</A></td>' % os.path.join(dirname, res['subdir'], "debug") 1509 1510 print >> output, '</tr>' 1511 print_result(results[r][1], indent + 1) 1512 1513 print_result(results[""][1], 0) 1514 print >> output, "</tbody></table>" 1515 1516 1517 print >> output, '<h2 id=\"page_sub_title\">Host Info</h2>' 1518 print >> output, '<h2 id=\"comment\">click on each item to expend/collapse</h2>' 1519 ## Meta list comes here.. 1520 print >> output, '<p>' 1521 print >> output, '<A href="#" class="button" onClick="expandTree(\'meta_tree\');return false;">Expand All</A>' 1522 print >> output, '  ' 1523 print >> output, '<A class="button" href="#" onClick="collapseTree(\'meta_tree\'); return false;">Collapse All</A>' 1524 print >> output, '</p>' 1525 1526 print >> output, '<ul class="mktree" id="meta_tree">' 1527 counter = 0 1528 keys = metadata.keys() 1529 keys.sort() 1530 for key in keys: 1531 val = metadata[key] 1532 print >> output, '<li id=\"meta_headline\">%s' % key 1533 print >> output, '<ul><table class="meta_table"><tr><td align="left">%s</td></tr></table></ul></li>' % val 1534 print >> output, '</ul>' 1535 1536 print >> output, "</body></html>" 1537 if output_file_name: 1538 output.close() 1539 1540 1541 def parse_result(dirname, line, results_data): 1542 """ 1543 Parse job status log line. 1544 1545 @param dirname: Job results dir 1546 @param line: Status log line. 1547 @param results_data: Dictionary with for results. 1548 """ 1549 parts = line.split() 1550 if len(parts) < 4: 1551 return None 1552 global tests 1553 if parts[0] == 'START': 1554 pair = parts[3].split('=') 1555 stime = int(pair[1]) 1556 results_data[parts[1]] = [stime, [], None] 1557 try: 1558 parent_test = re.findall(r".*/", parts[1])[0][:-1] 1559 results_data[parent_test][1].append(parts[1]) 1560 except IndexError: 1561 results_data[""][1].append(parts[1]) 1562 1563 elif (parts[0] == 'END'): 1564 result = {} 1565 exec_time = '' 1566 # fetch time stamp 1567 if len(parts) > 7: 1568 temp = parts[5].split('=') 1569 exec_time = temp[1] + ' ' + parts[6] + ' ' + parts[7] 1570 # assign default values 1571 result['time'] = exec_time 1572 result['testcase'] = 'na' 1573 result['status'] = 'na' 1574 result['log'] = None 1575 result['exec_time_sec'] = 'na' 1576 tag = parts[3] 1577 1578 result['subdir'] = parts[2] 1579 # assign actual values 1580 rx = re.compile('^(\w+)\.(.*)$') 1581 m1 = rx.findall(parts[3]) 1582 if len(m1): 1583 result['testcase'] = m1[0][1] 1584 else: 1585 result['testcase'] = parts[3] 1586 result['title'] = str(tag) 1587 result['status'] = parts[1] 1588 if result['status'] != 'GOOD': 1589 result['log'] = get_exec_log(dirname, tag) 1590 if len(results_data)>0: 1591 pair = parts[4].split('=') 1592 etime = int(pair[1]) 1593 stime = results_data[parts[2]][0] 1594 total_exec_time_sec = etime - stime 1595 result['exec_time_sec'] = total_exec_time_sec 1596 results_data[parts[2]][2] = result 1597 return None 1598 1599 1600 def get_exec_log(resdir, tag): 1601 """ 1602 Get job execution summary. 1603 1604 @param resdir: Job results dir. 1605 @param tag: Job tag. 1606 """ 1607 stdout_file = os.path.join(resdir, tag, 'debug', 'stdout') 1608 stderr_file = os.path.join(resdir, tag, 'debug', 'stderr') 1609 status_file = os.path.join(resdir, tag, 'status') 1610 dmesg_file = os.path.join(resdir, tag, 'sysinfo', 'dmesg') 1611 log = '' 1612 log += '<br><b>STDERR:</b><br>' 1613 log += get_info_file(stderr_file) 1614 log += '<br><b>STDOUT:</b><br>' 1615 log += get_info_file(stdout_file) 1616 log += '<br><b>STATUS:</b><br>' 1617 log += get_info_file(status_file) 1618 log += '<br><b>DMESG:</b><br>' 1619 log += get_info_file(dmesg_file) 1620 return log 1621 1622 1623 def get_info_file(filename): 1624 """ 1625 Gets the contents of an autotest info file. 1626 1627 It also and highlights the file contents with possible problems. 1628 1629 @param filename: Info file path. 1630 """ 1631 data = '' 1632 errors = re.compile(r"\b(error|fail|failed)\b", re.IGNORECASE) 1633 if os.path.isfile(filename): 1634 f = open('%s' % filename, "r") 1635 lines = f.readlines() 1636 f.close() 1637 rx = re.compile('(\'|\")') 1638 for line in lines: 1639 new_line = rx.sub('', line) 1640 errors_found = errors.findall(new_line) 1641 if len(errors_found) > 0: 1642 data += '<font color=red>%s</font><br>' % str(new_line) 1643 else: 1644 data += '%s<br>' % str(new_line) 1645 if not data: 1646 data = 'No Information Found.<br>' 1647 else: 1648 data = 'File not found.<br>' 1649 return data 1650 1651 1652 def usage(): 1653 """ 1654 Print stand alone program usage. 1655 """ 1656 print 'usage:', 1657 print 'make_html_report.py -r <result_directory> [-f output_file] [-R]' 1658 print '(e.g. make_html_reporter.py -r '\ 1659 '/usr/local/autotest/client/results/default -f /tmp/myreport.html)' 1660 print 'add "-R" for an html report with relative-paths (relative '\ 1661 'to results directory)' 1662 print '' 1663 sys.exit(1) 1664 1665 1666 def get_keyval_value(result_dir, key): 1667 """ 1668 Return the value of the first appearance of key in any keyval file in 1669 result_dir. If no appropriate line is found, return 'Unknown'. 1670 1671 @param result_dir: Path that holds the keyval files. 1672 @param key: Specific key we're retrieving. 1673 """ 1674 keyval_pattern = os.path.join(result_dir, "kvm.*", "keyval") 1675 keyval_lines = commands.getoutput(r"grep -h '\b%s\b.*=' %s" 1676 % (key, keyval_pattern)) 1677 if not keyval_lines: 1678 return "Unknown" 1679 keyval_line = keyval_lines.splitlines()[0] 1680 if key in keyval_line and "=" in keyval_line: 1681 return keyval_line.split("=")[1].strip() 1682 else: 1683 return "Unknown" 1684 1685 1686 def get_kvm_version(result_dir): 1687 """ 1688 Return an HTML string describing the KVM version. 1689 1690 @param result_dir: An Autotest job result dir. 1691 """ 1692 kvm_version = get_keyval_value(result_dir, "kvm_version") 1693 kvm_userspace_version = get_keyval_value(result_dir, 1694 "kvm_userspace_version") 1695 if kvm_version == "Unknown" or kvm_userspace_version == "Unknown": 1696 return None 1697 return "Kernel: %s<br>Userspace: %s" % (kvm_version, kvm_userspace_version) 1698 1699 1700 def create_report(dirname, html_path='', output_file_name=None): 1701 """ 1702 Create an HTML report with info about an autotest client job. 1703 1704 If no relative path (html_path) or output file name provided, an HTML 1705 file in the toplevel job results dir called 'job_report.html' will be 1706 created, with relative links. 1707 1708 @param html_path: Prefix for the HTML links. Useful to specify absolute 1709 in the report (not wanted most of the time). 1710 @param output_file_name: Path to the report file. 1711 """ 1712 res_dir = os.path.abspath(dirname) 1713 tag = res_dir 1714 status_file_name = os.path.join(dirname, 'status') 1715 sysinfo_dir = os.path.join(dirname, 'sysinfo') 1716 host = get_info_file(os.path.join(sysinfo_dir, 'hostname')) 1717 rx = re.compile('^\s+[END|START].*$') 1718 # create the results set dict 1719 results_data = {} 1720 results_data[""] = [0, [], None] 1721 if os.path.exists(status_file_name): 1722 f = open(status_file_name, "r") 1723 lines = f.readlines() 1724 f.close() 1725 for line in lines: 1726 if rx.match(line): 1727 parse_result(dirname, line, results_data) 1728 # create the meta info dict 1729 metalist = { 1730 'uname': get_info_file(os.path.join(sysinfo_dir, 'uname')), 1731 'cpuinfo':get_info_file(os.path.join(sysinfo_dir, 'cpuinfo')), 1732 'meminfo':get_info_file(os.path.join(sysinfo_dir, 'meminfo')), 1733 'df':get_info_file(os.path.join(sysinfo_dir, 'df')), 1734 'modules':get_info_file(os.path.join(sysinfo_dir, 'modules')), 1735 'gcc':get_info_file(os.path.join(sysinfo_dir, 'gcc_--version')), 1736 'dmidecode':get_info_file(os.path.join(sysinfo_dir, 'dmidecode')), 1737 'dmesg':get_info_file(os.path.join(sysinfo_dir, 'dmesg')), 1738 } 1739 if get_kvm_version(dirname) is not None: 1740 metalist['kvm_ver'] = get_kvm_version(dirname) 1741 1742 if output_file_name is None: 1743 output_file_name = os.path.join(dirname, 'job_report.html') 1744 make_html_file(metalist, results_data, tag, host, output_file_name, 1745 html_path) 1746 1747 1748 def main(argv): 1749 """ 1750 Parses the arguments and executes the stand alone program. 1751 """ 1752 dirname = None 1753 output_file_name = None 1754 relative_path = False 1755 try: 1756 opts, args = getopt.getopt(argv, "r:f:h:R", ['help']) 1757 except getopt.GetoptError: 1758 usage() 1759 sys.exit(2) 1760 for opt, arg in opts: 1761 if opt in ("-h", "--help"): 1762 usage() 1763 sys.exit() 1764 elif opt == '-r': 1765 dirname = arg 1766 elif opt == '-f': 1767 output_file_name = arg 1768 elif opt == '-R': 1769 relative_path = True 1770 else: 1771 usage() 1772 sys.exit(1) 1773 1774 html_path = dirname 1775 # don't use absolute path in html output if relative flag passed 1776 if relative_path: 1777 html_path = '' 1778 1779 if dirname: 1780 if os.path.isdir(dirname): # TBD: replace it with a validation of 1781 # autotest result dir 1782 create_report(dirname, html_path, output_file_name) 1783 sys.exit(0) 1784 else: 1785 print 'Invalid result directory <%s>' % dirname 1786 sys.exit(1) 1787 else: 1788 usage() 1789 sys.exit(1) 1790 1791 1792 if __name__ == "__main__": 1793 main(sys.argv[1:]) 1794