#!/usr/bin/env python """ Prints data in a table format. This module serves as utility for other scripts. """ import sys, re, os.path, stat, math from html import escape from optparse import OptionParser from color import getColorizer, dummyColorizer class tblCell(object): def __init__(self, text, value = None, props = None): self.text = text self.value = value self.props = props class tblColumn(object): def __init__(self, caption, title = None, props = None): self.text = caption self.title = title self.props = props class tblRow(object): def __init__(self, colsNum, props = None): self.cells = [None] * colsNum self.props = props def htmlEncode(str): return '
'.join([escape(s) for s in str]) class table(object): def_align = "left" def_valign = "middle" def_color = None def_colspan = 1 def_rowspan = 1 def_bold = False def_italic = False def_text="-" def __init__(self, caption = None, format=None): self.format = format self.is_markdown = self.format == 'markdown' self.is_tabs = self.format == 'tabs' self.columns = {} self.rows = [] self.ridx = -1; self.caption = caption pass def newRow(self, **properties): if len(self.rows) - 1 == self.ridx: self.rows.append(tblRow(len(self.columns), properties)) else: self.rows[self.ridx + 1].props = properties self.ridx += 1 return self.rows[self.ridx] def trimLastRow(self): if self.rows: self.rows.pop() if self.ridx >= len(self.rows): self.ridx = len(self.rows) - 1 def newColumn(self, name, caption, title = None, **properties): if name in self.columns: index = self.columns[name].index else: index = len(self.columns) if isinstance(caption, tblColumn): caption.index = index self.columns[name] = caption return caption else: col = tblColumn(caption, title, properties) col.index = index self.columns[name] = col return col def getColumn(self, name): if isinstance(name, str): return self.columns.get(name, None) else: vals = [v for v in self.columns.values() if v.index == name] if vals: return vals[0] return None def newCell(self, col_name, text, value = None, **properties): if self.ridx < 0: self.newRow() col = self.getColumn(col_name) row = self.rows[self.ridx] if not col: return None if isinstance(text, tblCell): cl = text else: cl = tblCell(text, value, properties) row.cells[col.index] = cl return cl def layoutTable(self): columns = self.columns.values() columns = sorted(columns, key=lambda c: c.index) colspanned = [] rowspanned = [] self.headerHeight = 1 rowsToAppend = 0 for col in columns: self.measureCell(col) if col.height > self.headerHeight: self.headerHeight = col.height col.minwidth = col.width col.line = None for r in range(len(self.rows)): row = self.rows[r] row.minheight = 1 for i in range(len(row.cells)): cell = row.cells[i] if row.cells[i] is None: continue cell.line = None self.measureCell(cell) colspan = int(self.getValue("colspan", cell)) rowspan = int(self.getValue("rowspan", cell)) if colspan > 1: colspanned.append((r,i)) if i + colspan > len(columns): colspan = len(columns) - i cell.colspan = colspan #clear spanned cells for j in range(i+1, min(len(row.cells), i + colspan)): row.cells[j] = None elif columns[i].minwidth < cell.width: columns[i].minwidth = cell.width if rowspan > 1: rowspanned.append((r,i)) rowsToAppend2 = r + colspan - len(self.rows) if rowsToAppend2 > rowsToAppend: rowsToAppend = rowsToAppend2 cell.rowspan = rowspan #clear spanned cells for j in range(r+1, min(len(self.rows), r + rowspan)): if len(self.rows[j].cells) > i: self.rows[j].cells[i] = None elif row.minheight < cell.height: row.minheight = cell.height self.ridx = len(self.rows) - 1 for r in range(rowsToAppend): self.newRow() self.rows[len(self.rows) - 1].minheight = 1 while colspanned: colspanned_new = [] for r, c in colspanned: cell = self.rows[r].cells[c] sum([col.minwidth for col in columns[c:c + cell.colspan]]) cell.awailable = sum([col.minwidth for col in columns[c:c + cell.colspan]]) + cell.colspan - 1 if cell.awailable < cell.width: colspanned_new.append((r,c)) colspanned = colspanned_new if colspanned: r,c = colspanned[0] cell = self.rows[r].cells[c] cols = columns[c:c + cell.colspan] total = cell.awailable - cell.colspan + 1 budget = cell.width - cell.awailable spent = 0 s = 0 for col in cols: s += col.minwidth addition = s * budget / total - spent spent += addition col.minwidth += addition while rowspanned: rowspanned_new = [] for r, c in rowspanned: cell = self.rows[r].cells[c] cell.awailable = sum([row.minheight for row in self.rows[r:r + cell.rowspan]]) if cell.awailable < cell.height: rowspanned_new.append((r,c)) rowspanned = rowspanned_new if rowspanned: r,c = rowspanned[0] cell = self.rows[r].cells[c] rows = self.rows[r:r + cell.rowspan] total = cell.awailable budget = cell.height - cell.awailable spent = 0 s = 0 for row in rows: s += row.minheight addition = s * budget / total - spent spent += addition row.minheight += addition return columns def measureCell(self, cell): text = self.getValue("text", cell) cell.text = self.reformatTextValue(text) cell.height = len(cell.text) cell.width = len(max(cell.text, key = lambda line: len(line))) def reformatTextValue(self, value): if isinstance(value, str): vstr = value else: try: vstr = '\n'.join([str(v) for v in value]) except TypeError: vstr = str(value) return vstr.splitlines() def adjustColWidth(self, cols, width): total = sum([c.minWidth for c in cols]) if total + len(cols) - 1 >= width: return budget = width - len(cols) + 1 - total spent = 0 s = 0 for col in cols: s += col.minWidth addition = s * budget / total - spent spent += addition col.minWidth += addition def getValue(self, name, *elements): for el in elements: try: return getattr(el, name) except AttributeError: pass try: val = el.props[name] if val: return val except AttributeError: pass except KeyError: pass try: return getattr(self.__class__, "def_" + name) except AttributeError: return None def consolePrintTable(self, out): columns = self.layoutTable() colrizer = getColorizer(out) if not (self.is_markdown or self.is_tabs) else dummyColorizer(out) if self.caption: out.write("%s%s%s" % ( os.linesep, os.linesep.join(self.reformatTextValue(self.caption)), os.linesep * 2)) headerRow = tblRow(len(columns), {"align": "center", "valign": "top", "bold": True, "header": True}) headerRow.cells = columns headerRow.minheight = self.headerHeight self.consolePrintRow2(colrizer, headerRow, columns) for i in range(0, len(self.rows)): self.consolePrintRow2(colrizer, i, columns) def consolePrintRow2(self, out, r, columns): if isinstance(r, tblRow): row = r r = -1 else: row = self.rows[r] #evaluate initial values for line numbers i = 0 while i < len(row.cells): cell = row.cells[i] colspan = self.getValue("colspan", cell) if cell is not None: cell.wspace = sum([col.minwidth for col in columns[i:i + colspan]]) + colspan - 1 if cell.line is None: if r < 0: rows = [row] else: rows = self.rows[r:r + self.getValue("rowspan", cell)] cell.line = self.evalLine(cell, rows, columns[i]) if len(rows) > 1: for rw in rows: rw.cells[i] = cell i += colspan #print content if self.is_markdown: out.write("|") for c in row.cells: text = ' '.join(self.getValue('text', c) or []) out.write(text + "|") out.write(os.linesep) elif self.is_tabs: cols_to_join=[' '.join(self.getValue('text', c) or []) for c in row.cells] out.write('\t'.join(cols_to_join)) out.write(os.linesep) else: for ln in range(row.minheight): i = 0 while i < len(row.cells): if i > 0: out.write(" ") cell = row.cells[i] column = columns[i] if cell is None: out.write(" " * column.minwidth) i += 1 else: self.consolePrintLine(cell, row, column, out) i += self.getValue("colspan", cell) if self.is_markdown: out.write("|") out.write(os.linesep) if self.is_markdown and row.props.get('header', False): out.write("|") for th in row.cells: align = self.getValue("align", th) if align == 'center': out.write(":-:|") elif align == 'right': out.write("--:|") else: out.write("---|") out.write(os.linesep) def consolePrintLine(self, cell, row, column, out): if cell.line < 0 or cell.line >= cell.height: line = "" else: line = cell.text[cell.line] width = cell.wspace align = self.getValue("align", ((None, cell)[isinstance(cell, tblCell)]), row, column) if align == "right": pattern = "%" + str(width) + "s" elif align == "center": pattern = "%" + str((width - len(line)) // 2 + len(line)) + "s" + " " * (width - len(line) - (width - len(line)) // 2) else: pattern = "%-" + str(width) + "s" out.write(pattern % line, color = self.getValue("color", cell, row, column)) cell.line += 1 def evalLine(self, cell, rows, column): height = cell.height valign = self.getValue("valign", cell, rows[0], column) space = sum([row.minheight for row in rows]) if valign == "bottom": return height - space if valign == "middle": return (height - space + 1) // 2 return 0 def htmlPrintTable(self, out, embeedcss = False): columns = self.layoutTable() if embeedcss: out.write("
\n\n") else: out.write("
\n
\n") if self.caption: if embeedcss: out.write(" \n" % htmlEncode(self.reformatTextValue(self.caption))) else: out.write(" \n" % htmlEncode(self.reformatTextValue(self.caption))) out.write(" \n") headerRow = tblRow(len(columns), {"align": "center", "valign": "top", "bold": True, "header": True}) headerRow.cells = columns header_rows = [headerRow] header_rows.extend([row for row in self.rows if self.getValue("header")]) last_row = header_rows[len(header_rows) - 1] for row in header_rows: out.write(" \n") for th in row.cells: align = self.getValue("align", ((None, th)[isinstance(th, tblCell)]), row, row) valign = self.getValue("valign", th, row) cssclass = self.getValue("cssclass", th) attr = "" if align: attr += " align=\"%s\"" % align if valign: attr += " valign=\"%s\"" % valign if cssclass: attr += " class=\"%s\"" % cssclass css = "" if embeedcss: css = " style=\"border:none;color:#003399;font-size:16px;font-weight:normal;white-space:nowrap;padding:3px 10px;\"" if row == last_row: css = css[:-1] + "padding-bottom:5px;\"" out.write(" \n" % (attr, css)) if th is not None: out.write(" %s\n" % htmlEncode(th.text)) out.write(" \n") out.write(" \n") out.write(" \n \n") rows = [row for row in self.rows if not self.getValue("header")] for r in range(len(rows)): row = rows[r] rowattr = "" cssclass = self.getValue("cssclass", row) if cssclass: rowattr += " class=\"%s\"" % cssclass out.write(" \n" % (rowattr)) i = 0 while i < len(row.cells): column = columns[i] td = row.cells[i] if isinstance(td, int): i += td continue colspan = self.getValue("colspan", td) rowspan = self.getValue("rowspan", td) align = self.getValue("align", td, row, column) valign = self.getValue("valign", td, row, column) color = self.getValue("color", td, row, column) bold = self.getValue("bold", td, row, column) italic = self.getValue("italic", td, row, column) style = "" attr = "" if color: style += "color:%s;" % color if bold: style += "font-weight: bold;" if italic: style += "font-style: italic;" if align and align != "left": attr += " align=\"%s\"" % align if valign and valign != "middle": attr += " valign=\"%s\"" % valign if colspan > 1: attr += " colspan=\"%s\"" % colspan if rowspan > 1: attr += " rowspan=\"%s\"" % rowspan for q in range(r+1, min(r+rowspan, len(rows))): rows[q].cells[i] = colspan if style: attr += " style=\"%s\"" % style css = "" if embeedcss: css = " style=\"border:none;border-bottom:1px solid #CCCCCC;color:#666699;padding:6px 8px;white-space:nowrap;\"" if r == 0: css = css[:-1] + "border-top:2px solid #6678B1;\"" out.write(" \n" % (attr, css)) if td is not None: out.write(" %s\n" % htmlEncode(td.text)) out.write(" \n") i += colspan out.write(" \n") out.write(" \n
%s%s
\n
\n") def htmlPrintHeader(out, title = None): if title: titletag = "%s\n" % htmlEncode([str(title)]) else: titletag = "" out.write(""" %s """ % titletag) def htmlPrintFooter(out): out.write("\n") def getStdoutFilename(): try: if os.name == "nt": import msvcrt, ctypes handle = msvcrt.get_osfhandle(sys.stdout.fileno()) size = ctypes.c_ulong(1024) nameBuffer = ctypes.create_string_buffer(size.value) ctypes.windll.kernel32.GetFinalPathNameByHandleA(handle, nameBuffer, size, 4) return nameBuffer.value else: return os.readlink('/proc/self/fd/1') except: return "" def detectHtmlOutputType(requestedType): if requestedType in ['txt', 'markdown']: return False elif requestedType in ["html", "moinwiki"]: return True else: if sys.stdout.isatty(): return False else: outname = getStdoutFilename() if outname: if outname.endswith(".htm") or outname.endswith(".html"): return True else: return False else: return False def getRelativeVal(test, test0, metric): if not test or not test0: return None val0 = test0.get(metric, "s") if not val0: return None val = test.get(metric, "s") if not val or val == 0: return None return float(val0)/val def getCycleReduction(test, test0, metric): if not test or not test0: return None val0 = test0.get(metric, "s") if not val0 or val0 == 0: return None val = test.get(metric, "s") if not val: return None return (1.0-float(val)/val0)*100 def getScore(test, test0, metric): if not test or not test0: return None m0 = float(test.get("gmean", None)) m1 = float(test0.get("gmean", None)) if m0 == 0 or m1 == 0: return None s0 = float(test.get("gstddev", None)) s1 = float(test0.get("gstddev", None)) s = math.sqrt(s0*s0 + s1*s1) m0 = math.log(m0) m1 = math.log(m1) if s == 0: return None return (m0-m1)/s metrix_table = \ { "name": ("Name of Test", lambda test,test0,units: str(test)), "samples": ("Number of\ncollected samples", lambda test,test0,units: test.get("samples", units)), "outliers": ("Number of\noutliers", lambda test,test0,units: test.get("outliers", units)), "gmean": ("Geometric mean", lambda test,test0,units: test.get("gmean", units)), "mean": ("Mean", lambda test,test0,units: test.get("mean", units)), "min": ("Min", lambda test,test0,units: test.get("min", units)), "median": ("Median", lambda test,test0,units: test.get("median", units)), "stddev": ("Standard deviation", lambda test,test0,units: test.get("stddev", units)), "gstddev": ("Standard deviation of Ln(time)", lambda test,test0,units: test.get("gstddev")), "gmean%": ("Geometric mean (relative)", lambda test,test0,units: getRelativeVal(test, test0, "gmean")), "mean%": ("Mean (relative)", lambda test,test0,units: getRelativeVal(test, test0, "mean")), "min%": ("Min (relative)", lambda test,test0,units: getRelativeVal(test, test0, "min")), "median%": ("Median (relative)", lambda test,test0,units: getRelativeVal(test, test0, "median")), "stddev%": ("Standard deviation (relative)", lambda test,test0,units: getRelativeVal(test, test0, "stddev")), "gstddev%": ("Standard deviation of Ln(time) (relative)", lambda test,test0,units: getRelativeVal(test, test0, "gstddev")), "gmean$": ("Geometric mean (cycle reduction)", lambda test,test0,units: getCycleReduction(test, test0, "gmean")), "mean$": ("Mean (cycle reduction)", lambda test,test0,units: getCycleReduction(test, test0, "mean")), "min$": ("Min (cycle reduction)", lambda test,test0,units: getCycleReduction(test, test0, "min")), "median$": ("Median (cycle reduction)", lambda test,test0,units: getCycleReduction(test, test0, "median")), "stddev$": ("Standard deviation (cycle reduction)", lambda test,test0,units: getCycleReduction(test, test0, "stddev")), "gstddev$": ("Standard deviation of Ln(time) (cycle reduction)", lambda test,test0,units: getCycleReduction(test, test0, "gstddev")), "score": ("SCORE", lambda test,test0,units: getScore(test, test0, "gstddev")), } def formatValue(val, metric, units = None): if val is None: return "-" if metric.endswith("%"): return "%.2f" % val if metric.endswith("$"): return "%.2f%%" % val if metric.endswith("S"): if val > 3.5: return "SLOWER" if val < -3.5: return "FASTER" if val > -1.5 and val < 1.5: return " " if val < 0: return "faster" if val > 0: return "slower" #return "%.4f" % val if units: return "%.3f %s" % (val, units) else: return "%.3f" % val if __name__ == "__main__": if len(sys.argv) < 2: print("Usage:\n", os.path.basename(sys.argv[0]), ".xml") exit(0) parser = OptionParser() 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") parser.add_option("-m", "--metric", dest="metric", help="output metric", metavar="NAME", default="gmean") parser.add_option("-u", "--units", dest="units", help="units for output values (s, ms (default), us, ns or ticks)", metavar="UNITS", default="ms") (options, args) = parser.parse_args() options.generateHtml = detectHtmlOutputType(options.format) if options.metric not in metrix_table: options.metric = "gmean" #print options #print args # tbl = table() # tbl.newColumn("first", "qqqq", align = "left") # tbl.newColumn("second", "wwww\nz\nx\n") # tbl.newColumn("third", "wwasdas") # # tbl.newCell(0, "ccc111", align = "right") # tbl.newCell(1, "dddd1") # tbl.newCell(2, "8768756754") # tbl.newRow() # tbl.newCell(0, "1\n2\n3\n4\n5\n6\n7", align = "center", colspan = 2, rowspan = 2) # tbl.newCell(2, "xxx\nqqq", align = "center", colspan = 1, valign = "middle") # tbl.newRow() # tbl.newCell(2, "+", align = "center", colspan = 1, valign = "middle") # tbl.newRow() # tbl.newCell(0, "vcvvbasdsadassdasdasv", align = "right", colspan = 2) # tbl.newCell(2, "dddd1") # tbl.newRow() # tbl.newCell(0, "vcvvbv") # tbl.newCell(1, "3445324", align = "right") # tbl.newCell(2, None) # tbl.newCell(1, "0000") # if sys.stdout.isatty(): # tbl.consolePrintTable(sys.stdout) # else: # htmlPrintHeader(sys.stdout) # tbl.htmlPrintTable(sys.stdout) # htmlPrintFooter(sys.stdout) import testlog_parser if options.generateHtml: htmlPrintHeader(sys.stdout, "Tables demo") getter = metrix_table[options.metric][1] for arg in args: tests = testlog_parser.parseLogFile(arg) tbl = table(arg, format=options.format) tbl.newColumn("name", "Name of Test", align = "left") tbl.newColumn("value", metrix_table[options.metric][0], align = "center", bold = "true") for t in sorted(tests): tbl.newRow() tbl.newCell("name", str(t)) status = t.get("status") if status != "run": tbl.newCell("value", status) else: val = getter(t, None, options.units) if val: if options.metric.endswith("%"): tbl.newCell("value", "%.2f" % val, val) else: tbl.newCell("value", "%.3f %s" % (val, options.units), val) else: tbl.newCell("value", "-") if options.generateHtml: tbl.htmlPrintTable(sys.stdout) else: tbl.consolePrintTable(sys.stdout) if options.generateHtml: htmlPrintFooter(sys.stdout)