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