• Home
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download
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'; // &nbsp;
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('&nbsp;&nbsp;&nbsp', 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