3 from __future__ import print_function
4 import sys, re, os.path, stat, math
6 from html import escape
8 from cgi import escape # Python 2.7
9 from optparse import OptionParser
10 from color import getColorizer, dummyColorizer
12 class tblCell(object):
13 def __init__(self, text, value = None, props = None):
18 class tblColumn(object):
19 def __init__(self, caption, title = None, props = None):
25 def __init__(self, colsNum, props = None):
26 self.cells = [None] * colsNum
30 return '<br/>'.join([escape(s) for s in str])
42 def __init__(self, caption = None, format=None):
44 self.is_markdown = self.format == 'markdown'
45 self.is_tabs = self.format == 'tabs'
49 self.caption = caption
52 def newRow(self, **properties):
53 if len(self.rows) - 1 == self.ridx:
54 self.rows.append(tblRow(len(self.columns), properties))
56 self.rows[self.ridx + 1].props = properties
58 return self.rows[self.ridx]
60 def trimLastRow(self):
63 if self.ridx >= len(self.rows):
64 self.ridx = len(self.rows) - 1
66 def newColumn(self, name, caption, title = None, **properties):
67 if name in self.columns:
68 index = self.columns[name].index
70 index = len(self.columns)
71 if isinstance(caption, tblColumn):
73 self.columns[name] = caption
76 col = tblColumn(caption, title, properties)
78 self.columns[name] = col
81 def getColumn(self, name):
82 if isinstance(name, str):
83 return self.columns.get(name, None)
85 vals = [v for v in self.columns.values() if v.index == name]
90 def newCell(self, col_name, text, value = None, **properties):
93 col = self.getColumn(col_name)
94 row = self.rows[self.ridx]
97 if isinstance(text, tblCell):
100 cl = tblCell(text, value, properties)
101 row.cells[col.index] = cl
104 def layoutTable(self):
105 columns = self.columns.values()
106 columns = sorted(columns, key=lambda c: c.index)
111 self.headerHeight = 1
115 self.measureCell(col)
116 if col.height > self.headerHeight:
117 self.headerHeight = col.height
118 col.minwidth = col.width
121 for r in range(len(self.rows)):
124 for i in range(len(row.cells)):
126 if row.cells[i] is None:
129 self.measureCell(cell)
130 colspan = int(self.getValue("colspan", cell))
131 rowspan = int(self.getValue("rowspan", cell))
133 colspanned.append((r,i))
134 if i + colspan > len(columns):
135 colspan = len(columns) - i
136 cell.colspan = colspan
138 for j in range(i+1, min(len(row.cells), i + colspan)):
140 elif columns[i].minwidth < cell.width:
141 columns[i].minwidth = cell.width
143 rowspanned.append((r,i))
144 rowsToAppend2 = r + colspan - len(self.rows)
145 if rowsToAppend2 > rowsToAppend:
146 rowsToAppend = rowsToAppend2
147 cell.rowspan = rowspan
149 for j in range(r+1, min(len(self.rows), r + rowspan)):
150 if len(self.rows[j].cells) > i:
151 self.rows[j].cells[i] = None
152 elif row.minheight < cell.height:
153 row.minheight = cell.height
155 self.ridx = len(self.rows) - 1
156 for r in range(rowsToAppend):
158 self.rows[len(self.rows) - 1].minheight = 1
162 for r, c in colspanned:
163 cell = self.rows[r].cells[c]
164 sum([col.minwidth for col in columns[c:c + cell.colspan]])
165 cell.awailable = sum([col.minwidth for col in columns[c:c + cell.colspan]]) + cell.colspan - 1
166 if cell.awailable < cell.width:
167 colspanned_new.append((r,c))
168 colspanned = colspanned_new
171 cell = self.rows[r].cells[c]
172 cols = columns[c:c + cell.colspan]
173 total = cell.awailable - cell.colspan + 1
174 budget = cell.width - cell.awailable
179 addition = s * budget / total - spent
181 col.minwidth += addition
185 for r, c in rowspanned:
186 cell = self.rows[r].cells[c]
187 cell.awailable = sum([row.minheight for row in self.rows[r:r + cell.rowspan]])
188 if cell.awailable < cell.height:
189 rowspanned_new.append((r,c))
190 rowspanned = rowspanned_new
193 cell = self.rows[r].cells[c]
194 rows = self.rows[r:r + cell.rowspan]
195 total = cell.awailable
196 budget = cell.height - cell.awailable
201 addition = s * budget / total - spent
203 row.minheight += addition
207 def measureCell(self, cell):
208 text = self.getValue("text", cell)
209 cell.text = self.reformatTextValue(text)
210 cell.height = len(cell.text)
211 cell.width = len(max(cell.text, key = lambda line: len(line)))
213 def reformatTextValue(self, value):
214 if sys.version_info >= (2,7):
216 if isinstance(value, str):
218 elif isinstance(value, unicode):
222 vstr = '\n'.join([str(v) for v in value])
225 return vstr.splitlines()
227 def adjustColWidth(self, cols, width):
228 total = sum([c.minWidth for c in cols])
229 if total + len(cols) - 1 >= width:
231 budget = width - len(cols) + 1 - total
236 addition = s * budget / total - spent
238 col.minWidth += addition
240 def getValue(self, name, *elements):
243 return getattr(el, name)
244 except AttributeError:
250 except AttributeError:
255 return getattr(self.__class__, "def_" + name)
256 except AttributeError:
259 def consolePrintTable(self, out):
260 columns = self.layoutTable()
261 colrizer = getColorizer(out) if not (self.is_markdown or self.is_tabs) else dummyColorizer(out)
264 out.write("%s%s%s" % ( os.linesep, os.linesep.join(self.reformatTextValue(self.caption)), os.linesep * 2))
266 headerRow = tblRow(len(columns), {"align": "center", "valign": "top", "bold": True, "header": True})
267 headerRow.cells = columns
268 headerRow.minheight = self.headerHeight
270 self.consolePrintRow2(colrizer, headerRow, columns)
272 for i in range(0, len(self.rows)):
273 self.consolePrintRow2(colrizer, i, columns)
275 def consolePrintRow2(self, out, r, columns):
276 if isinstance(r, tblRow):
282 #evaluate initial values for line numbers
284 while i < len(row.cells):
286 colspan = self.getValue("colspan", cell)
288 cell.wspace = sum([col.minwidth for col in columns[i:i + colspan]]) + colspan - 1
289 if cell.line is None:
293 rows = self.rows[r:r + self.getValue("rowspan", cell)]
294 cell.line = self.evalLine(cell, rows, columns[i])
304 text = ' '.join(self.getValue('text', c) or [])
305 out.write(text + "|")
306 out.write(os.linesep)
308 cols_to_join=[' '.join(self.getValue('text', c) or []) for c in row.cells]
309 out.write('\t'.join(cols_to_join))
310 out.write(os.linesep)
312 for ln in range(row.minheight):
314 while i < len(row.cells):
320 out.write(" " * column.minwidth)
323 self.consolePrintLine(cell, row, column, out)
324 i += self.getValue("colspan", cell)
327 out.write(os.linesep)
329 if self.is_markdown and row.props.get('header', False):
332 align = self.getValue("align", th)
333 if align == 'center':
335 elif align == 'right':
339 out.write(os.linesep)
341 def consolePrintLine(self, cell, row, column, out):
342 if cell.line < 0 or cell.line >= cell.height:
345 line = cell.text[cell.line]
347 align = self.getValue("align", ((None, cell)[isinstance(cell, tblCell)]), row, column)
350 pattern = "%" + str(width) + "s"
351 elif align == "center":
352 pattern = "%" + str((width - len(line)) // 2 + len(line)) + "s" + " " * (width - len(line) - (width - len(line)) // 2)
354 pattern = "%-" + str(width) + "s"
356 out.write(pattern % line, color = self.getValue("color", cell, row, column))
359 def evalLine(self, cell, rows, column):
361 valign = self.getValue("valign", cell, rows[0], column)
362 space = sum([row.minheight for row in rows])
363 if valign == "bottom":
364 return height - space
365 if valign == "middle":
366 return (height - space + 1) // 2
369 def htmlPrintTable(self, out, embeedcss = False):
370 columns = self.layoutTable()
373 out.write("<div style=\"font-family: Lucida Console, Courier New, Courier;font-size: 16px;color:#3e4758;\">\n<table style=\"background:none repeat scroll 0 0 #FFFFFF;border-collapse:collapse;font-family:'Lucida Sans Unicode','Lucida Grande',Sans-Serif;font-size:14px;margin:20px;text-align:left;width:480px;margin-left: auto;margin-right: auto;white-space:nowrap;\">\n")
375 out.write("<div class=\"tableFormatter\">\n<table class=\"tbl\">\n")
378 out.write(" <caption style=\"font:italic 16px 'Trebuchet MS',Verdana,Arial,Helvetica,sans-serif;padding:0 0 5px;text-align:right;white-space:normal;\">%s</caption>\n" % htmlEncode(self.reformatTextValue(self.caption)))
380 out.write(" <caption>%s</caption>\n" % htmlEncode(self.reformatTextValue(self.caption)))
381 out.write(" <thead>\n")
383 headerRow = tblRow(len(columns), {"align": "center", "valign": "top", "bold": True, "header": True})
384 headerRow.cells = columns
386 header_rows = [headerRow]
387 header_rows.extend([row for row in self.rows if self.getValue("header")])
388 last_row = header_rows[len(header_rows) - 1]
390 for row in header_rows:
393 align = self.getValue("align", ((None, th)[isinstance(th, tblCell)]), row, row)
394 valign = self.getValue("valign", th, row)
395 cssclass = self.getValue("cssclass", th)
398 attr += " align=\"%s\"" % align
400 attr += " valign=\"%s\"" % valign
402 attr += " class=\"%s\"" % cssclass
405 css = " style=\"border:none;color:#003399;font-size:16px;font-weight:normal;white-space:nowrap;padding:3px 10px;\""
407 css = css[:-1] + "padding-bottom:5px;\""
408 out.write(" <th%s%s>\n" % (attr, css))
410 out.write(" %s\n" % htmlEncode(th.text))
411 out.write(" </th>\n")
412 out.write(" </tr>\n")
414 out.write(" </thead>\n <tbody>\n")
416 rows = [row for row in self.rows if not self.getValue("header")]
417 for r in range(len(rows)):
420 cssclass = self.getValue("cssclass", row)
422 rowattr += " class=\"%s\"" % cssclass
423 out.write(" <tr%s>\n" % (rowattr))
425 while i < len(row.cells):
428 if isinstance(td, int):
431 colspan = self.getValue("colspan", td)
432 rowspan = self.getValue("rowspan", td)
433 align = self.getValue("align", td, row, column)
434 valign = self.getValue("valign", td, row, column)
435 color = self.getValue("color", td, row, column)
436 bold = self.getValue("bold", td, row, column)
437 italic = self.getValue("italic", td, row, column)
441 style += "color:%s;" % color
443 style += "font-weight: bold;"
445 style += "font-style: italic;"
446 if align and align != "left":
447 attr += " align=\"%s\"" % align
448 if valign and valign != "middle":
449 attr += " valign=\"%s\"" % valign
451 attr += " colspan=\"%s\"" % colspan
453 attr += " rowspan=\"%s\"" % rowspan
454 for q in range(r+1, min(r+rowspan, len(rows))):
455 rows[q].cells[i] = colspan
457 attr += " style=\"%s\"" % style
460 css = " style=\"border:none;border-bottom:1px solid #CCCCCC;color:#666699;padding:6px 8px;white-space:nowrap;\""
462 css = css[:-1] + "border-top:2px solid #6678B1;\""
463 out.write(" <td%s%s>\n" % (attr, css))
465 out.write(" %s\n" % htmlEncode(td.text))
466 out.write(" </td>\n")
468 out.write(" </tr>\n")
470 out.write(" </tbody>\n</table>\n</div>\n")
472 def htmlPrintHeader(out, title = None):
474 titletag = "<title>%s</title>\n" % htmlEncode([str(title)])
477 out.write("""<!DOCTYPE HTML>
480 <meta http-equiv="Content-Type" content="text/html; charset=us-ascii">
481 %s<style type="text/css">
482 html, body {font-family: Lucida Console, Courier New, Courier;font-size: 16px;color:#3e4758;}
483 .tbl{background:none repeat scroll 0 0 #FFFFFF;border-collapse:collapse;font-family:"Lucida Sans Unicode","Lucida Grande",Sans-Serif;font-size:14px;margin:20px;text-align:left;width:480px;margin-left: auto;margin-right: auto;white-space:nowrap;}
484 .tbl span{display:block;white-space:nowrap;}
485 .tbl thead tr:last-child th {padding-bottom:5px;}
486 .tbl tbody tr:first-child td {border-top:3px solid #6678B1;}
487 .tbl th{border:none;color:#003399;font-size:16px;font-weight:normal;white-space:nowrap;padding:3px 10px;}
488 .tbl td{border:none;border-bottom:1px solid #CCCCCC;color:#666699;padding:6px 8px;white-space:nowrap;}
489 .tbl tbody tr:hover td{color:#000099;}
490 .tbl caption{font:italic 16px "Trebuchet MS",Verdana,Arial,Helvetica,sans-serif;padding:0 0 5px;text-align:right;white-space:normal;}
491 .firstingroup {border-top:2px solid #6678B1;}
493 <script type="text/javascript" src="https://ajax.googleapis.com/ajax/libs/jquery/1.6.4/jquery.min.js"></script>
494 <script type="text/javascript">
495 function abs(val) { return val < 0 ? -val : val }
497 //generate filter rows
498 $("div.tableFormatter table.tbl").each(function(tblIdx, tbl) {
499 var head = $("thead", tbl)
500 var filters = $("<tr></tr>")
502 $("tr:first th", head).each(function(colIdx, col) {
505 var id = "t" + tblIdx + "r" + colIdx
506 if (col.hasClass("col_name")){
507 cell = $("<th><input id='" + id + "' name='" + id + "' type='text' style='width:100%%' class='filter_col_name' title='Regular expression for name filtering ("resize.*640x480" - resize tests on VGA resolution)'></input></th>")
510 else if (col.hasClass("col_rel")){
511 cell = $("<th><input id='" + id + "' name='" + id + "' type='text' style='width:100%%' class='filter_col_rel' title='Filter out lines with a x-factor of acceleration less than Nx'></input></th>")
514 else if (col.hasClass("col_cr")){
515 cell = $("<th><input id='" + id + "' name='" + id + "' type='text' style='width:100%%' class='filter_col_cr' title='Filter out lines with a percentage of acceleration less than N%%'></input></th>")
519 cell = $("<th></th>")
520 cell.appendTo(filters)
524 $(tbl).wrap("<form id='form_t" + tblIdx + "' method='get' action=''></form>")
525 $("<input it='test' type='submit' value='Apply Filters' style='margin-left:10px;'></input>")
526 .appendTo($("th:last", filters.appendTo(head)))
532 var hashes = window.location.href.slice(window.location.href.indexOf('?') + 1).split('&')
533 for(var i = 0; i < hashes.length; ++i)
535 hash = hashes[i].split('=')
536 vars.push(decodeURIComponent(hash[0]))
537 vars[decodeURIComponent(hash[0])] = decodeURIComponent(hash[1]);
541 for(var i = 0; i < vars.length; ++i)
542 $("#" + vars[i]).val(vars[vars[i]])
545 $("div.tableFormatter table.tbl").each(function(tblIdx, tbl) {
546 filters = $("input:text", tbl)
547 var predicate = function(row) {return true;}
549 $.each($("input:text", tbl), function(i, flt) {
552 var pred = predicate;
555 var colIdx = parseInt(flt.attr("id").slice(flt.attr("id").indexOf('r') + 1))
556 if(flt.hasClass("filter_col_name")) {
557 var re = new RegExp(val);
558 predicate = function(row) {
559 if (re.exec($(row.get(colIdx)).text()) == null)
563 } else if(flt.hasClass("filter_col_rel")) {
564 var percent = parseFloat(val)
566 predicate = function(row) {
567 var val = parseFloat($(row.get(colIdx)).text())
568 if (!val || val >= 1 || val > 1+percent)
573 predicate = function(row) {
574 var val = parseFloat($(row.get(colIdx)).text())
575 if (!val || val < percent)
580 } else if(flt.hasClass("filter_col_cr")) {
581 var percent = parseFloat(val)
582 predicate = function(row) {
583 var val = parseFloat($(row.get(colIdx)).text())
584 if (!val || val < percent)
592 $("tbody tr", tbl).each(function (i, tbl_row) {
593 if(!predicate($("td", tbl_row)))
596 if($("tbody tr", tbl).length == 0) {
597 $("<tr><td colspan='"+$("thead tr:first th", tbl).length+"'>No results matching your search criteria</td></tr>")
598 .appendTo($("tbody", tbl))
608 def htmlPrintFooter(out):
609 out.write("</body>\n</html>")
611 def getStdoutFilename():
614 import msvcrt, ctypes
615 handle = msvcrt.get_osfhandle(sys.stdout.fileno())
616 size = ctypes.c_ulong(1024)
617 nameBuffer = ctypes.create_string_buffer(size.value)
618 ctypes.windll.kernel32.GetFinalPathNameByHandleA(handle, nameBuffer, size, 4)
619 return nameBuffer.value
621 return os.readlink('/proc/self/fd/1')
625 def detectHtmlOutputType(requestedType):
626 if requestedType in ['txt', 'markdown']:
628 elif requestedType in ["html", "moinwiki"]:
631 if sys.stdout.isatty():
634 outname = getStdoutFilename()
636 if outname.endswith(".htm") or outname.endswith(".html"):
643 def getRelativeVal(test, test0, metric):
644 if not test or not test0:
646 val0 = test0.get(metric, "s")
649 val = test.get(metric, "s")
650 if not val or val == 0:
652 return float(val0)/val
654 def getCycleReduction(test, test0, metric):
655 if not test or not test0:
657 val0 = test0.get(metric, "s")
658 if not val0 or val0 == 0:
660 val = test.get(metric, "s")
663 return (1.0-float(val)/val0)*100
665 def getScore(test, test0, metric):
666 if not test or not test0:
668 m0 = float(test.get("gmean", None))
669 m1 = float(test0.get("gmean", None))
670 if m0 == 0 or m1 == 0:
672 s0 = float(test.get("gstddev", None))
673 s1 = float(test0.get("gstddev", None))
674 s = math.sqrt(s0*s0 + s1*s1)
683 "name": ("Name of Test", lambda test,test0,units: str(test)),
685 "samples": ("Number of\ncollected samples", lambda test,test0,units: test.get("samples", units)),
686 "outliers": ("Number of\noutliers", lambda test,test0,units: test.get("outliers", units)),
688 "gmean": ("Geometric mean", lambda test,test0,units: test.get("gmean", units)),
689 "mean": ("Mean", lambda test,test0,units: test.get("mean", units)),
690 "min": ("Min", lambda test,test0,units: test.get("min", units)),
691 "median": ("Median", lambda test,test0,units: test.get("median", units)),
692 "stddev": ("Standard deviation", lambda test,test0,units: test.get("stddev", units)),
693 "gstddev": ("Standard deviation of Ln(time)", lambda test,test0,units: test.get("gstddev")),
695 "gmean%": ("Geometric mean (relative)", lambda test,test0,units: getRelativeVal(test, test0, "gmean")),
696 "mean%": ("Mean (relative)", lambda test,test0,units: getRelativeVal(test, test0, "mean")),
697 "min%": ("Min (relative)", lambda test,test0,units: getRelativeVal(test, test0, "min")),
698 "median%": ("Median (relative)", lambda test,test0,units: getRelativeVal(test, test0, "median")),
699 "stddev%": ("Standard deviation (relative)", lambda test,test0,units: getRelativeVal(test, test0, "stddev")),
700 "gstddev%": ("Standard deviation of Ln(time) (relative)", lambda test,test0,units: getRelativeVal(test, test0, "gstddev")),
702 "gmean$": ("Geometric mean (cycle reduction)", lambda test,test0,units: getCycleReduction(test, test0, "gmean")),
703 "mean$": ("Mean (cycle reduction)", lambda test,test0,units: getCycleReduction(test, test0, "mean")),
704 "min$": ("Min (cycle reduction)", lambda test,test0,units: getCycleReduction(test, test0, "min")),
705 "median$": ("Median (cycle reduction)", lambda test,test0,units: getCycleReduction(test, test0, "median")),
706 "stddev$": ("Standard deviation (cycle reduction)", lambda test,test0,units: getCycleReduction(test, test0, "stddev")),
707 "gstddev$": ("Standard deviation of Ln(time) (cycle reduction)", lambda test,test0,units: getCycleReduction(test, test0, "gstddev")),
709 "score": ("SCORE", lambda test,test0,units: getScore(test, test0, "gstddev")),
712 def formatValue(val, metric, units = None):
715 if metric.endswith("%"):
717 if metric.endswith("$"):
718 return "%.2f%%" % val
719 if metric.endswith("S"):
724 if val > -1.5 and val < 1.5:
732 return "%.3f %s" % (val, units)
736 if __name__ == "__main__":
737 if len(sys.argv) < 2:
738 print("Usage:\n", os.path.basename(sys.argv[0]), "<log_name>.xml")
741 parser = OptionParser()
742 parser.add_option("-o", "--output", dest="format", help="output results in text format (can be 'txt', 'html', 'markdown' or 'auto' - default)", metavar="FMT", default="auto")
743 parser.add_option("-m", "--metric", dest="metric", help="output metric", metavar="NAME", default="gmean")
744 parser.add_option("-u", "--units", dest="units", help="units for output values (s, ms (default), us, ns or ticks)", metavar="UNITS", default="ms")
745 (options, args) = parser.parse_args()
747 options.generateHtml = detectHtmlOutputType(options.format)
748 if options.metric not in metrix_table:
749 options.metric = "gmean"
755 # tbl.newColumn("first", "qqqq", align = "left")
756 # tbl.newColumn("second", "wwww\nz\nx\n")
757 # tbl.newColumn("third", "wwasdas")
759 # tbl.newCell(0, "ccc111", align = "right")
760 # tbl.newCell(1, "dddd1")
761 # tbl.newCell(2, "8768756754")
763 # tbl.newCell(0, "1\n2\n3\n4\n5\n6\n7", align = "center", colspan = 2, rowspan = 2)
764 # tbl.newCell(2, "xxx\nqqq", align = "center", colspan = 1, valign = "middle")
766 # tbl.newCell(2, "+", align = "center", colspan = 1, valign = "middle")
768 # tbl.newCell(0, "vcvvbasdsadassdasdasv", align = "right", colspan = 2)
769 # tbl.newCell(2, "dddd1")
771 # tbl.newCell(0, "vcvvbv")
772 # tbl.newCell(1, "3445324", align = "right")
773 # tbl.newCell(2, None)
774 # tbl.newCell(1, "0000")
775 # if sys.stdout.isatty():
776 # tbl.consolePrintTable(sys.stdout)
778 # htmlPrintHeader(sys.stdout)
779 # tbl.htmlPrintTable(sys.stdout)
780 # htmlPrintFooter(sys.stdout)
782 import testlog_parser
784 if options.generateHtml:
785 htmlPrintHeader(sys.stdout, "Tables demo")
787 getter = metrix_table[options.metric][1]
790 tests = testlog_parser.parseLogFile(arg)
791 tbl = table(arg, format=options.format)
792 tbl.newColumn("name", "Name of Test", align = "left")
793 tbl.newColumn("value", metrix_table[options.metric][0], align = "center", bold = "true")
795 for t in sorted(tests):
797 tbl.newCell("name", str(t))
799 status = t.get("status")
801 tbl.newCell("value", status)
803 val = getter(t, None, options.units)
805 if options.metric.endswith("%"):
806 tbl.newCell("value", "%.2f" % val, val)
808 tbl.newCell("value", "%.3f %s" % (val, options.units), val)
810 tbl.newCell("value", "-")
812 if options.generateHtml:
813 tbl.htmlPrintTable(sys.stdout)
815 tbl.consolePrintTable(sys.stdout)
817 if options.generateHtml:
818 htmlPrintFooter(sys.stdout)