• Home
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download
1#!/usr/bin/python
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
10import os, sys, re, getopt, time, datetime, commands
11import common
12
13
14format_css = """
15html,body {
16    padding:0;
17    color:#222;
18    background:#FFFFFF;
19}
20
21body {
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
64table.meta_table
65{text-align: center;
66font-family: Verdana, Geneva, Arial, Helvetica, sans-serif ;
67width: 90%;
68background-color: #FFFFFF;
69border: 0px;
70border-top: 1px #003377 solid;
71border-bottom: 1px #003377 solid;
72border-right: 1px #003377 solid;
73border-left: 1px #003377 solid;
74border-collapse: collapse;
75border-spacing: 0px;}
76
77table.meta_table td
78{background-color: #FFFFFF;
79color: #000;
80padding: 4px;
81border-top: 1px #BBBBBB solid;
82border-bottom: 1px #BBBBBB solid;
83font-weight: normal;
84font-size: 13px;}
85
86
87table.stats
88{text-align: center;
89font-family: Verdana, Geneva, Arial, Helvetica, sans-serif ;
90width: 100%;
91background-color: #FFFFFF;
92border: 0px;
93border-top: 1px #003377 solid;
94border-bottom: 1px #003377 solid;
95border-right: 1px #003377 solid;
96border-left: 1px #003377 solid;
97border-collapse: collapse;
98border-spacing: 0px;}
99
100table.stats td{
101background-color: #FFFFFF;
102color: #000;
103padding: 4px;
104border-top: 1px #BBBBBB solid;
105border-bottom: 1px #BBBBBB solid;
106font-weight: normal;
107font-size: 11px;}
108
109table.stats th{
110background: #dcdcdc;
111color: #000;
112padding: 6px;
113font-size: 12px;
114border-bottom: 1px #003377 solid;
115font-weight: bold;}
116
117table.stats td.top{
118background-color: #dcdcdc;
119color: #000;
120padding: 6px;
121text-align: center;
122border: 0px;
123border-bottom: 1px #003377 solid;
124font-size: 10px;
125font-weight: bold;}
126
127table.stats th.table-sorted-asc{
128        background-image: url(ascending.gif);
129        background-position: top left  ;
130        background-repeat: no-repeat;
131}
132
133table.stats th.table-sorted-desc{
134        background-image: url(descending.gif);
135        background-position: top left;
136        background-repeat: no-repeat;
137}
138
139table.stats2
140{text-align: left;
141font-family: Verdana, Geneva, Arial, Helvetica, sans-serif ;
142width: 100%;
143background-color: #FFFFFF;
144border: 0px;
145}
146
147table.stats2 td{
148background-color: #FFFFFF;
149color: #000;
150padding: 0px;
151font-weight: bold;
152font-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
181table_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 */
220var 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 */
323var 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
1205maketree_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/*
1218This code is inspired by and extended from Stuart Langridge's aqlist code:
1219    http://www.kryogenix.org/code/browser/aqlists/
1220    Stuart Langridge, November 2002
1221    sil@kryogenix.org
1222    Inspired by Aaron's labels.js (http://youngpup.net/demos/labels/)
1223    and Dave Lindquist's menuDropDown.js (http://www.gazingus.org/dhtml/?id=109)
1224*/
1225
1226// Automatically attach a listener to the window onload, to convert the trees
1227addEvent(window,"load",convertTrees);
1228
1229// Utility function to add an event listener
1230function 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
1237function 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
1244function 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
1251function 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
1258function 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
1274function 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
1302function 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
1323function treeNodeOnclick() {
1324  this.parentNode.className = (this.parentNode.className==nodeOpenClass) ? nodeClosedClass : nodeOpenClass;
1325  return false;
1326}
1327function retFalse() {
1328  return false;
1329}
1330// Process a UL tag and all its children, to convert to a tree
1331function 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'; // &nbsp;
1350      s.className = nodeLinkClass;
1351      if (subLists) {
1352        // This LI has UL's in it, so it's a +/- node
1353        if (item.className==null || item.className=="") {
1354          item.className = nodeClosedClass;
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
1378def 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
1402function popup(tag,text) {
1403var 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');
1404w.document.open("text/html", "replace");
1405w.document.write(text);
1406w.document.close();
1407return 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
1462id="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, '&nbsp;&nbsp;&nbsp'
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
1541def 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
1600def 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
1623def 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
1652def 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
1666def 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
1686def 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
1700def 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
1748def 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
1792if __name__ == "__main__":
1793    main(sys.argv[1:])
1794