1# Copyright (c) 2013 The Chromium OS Authors. All rights reserved.
2# Use of this source code is governed by a BSD-style license that can be
3# found in the LICENSE file.
4"""Table generating, analyzing and printing functions.
5
6This defines several classes that are used to generate, analyze and print
7tables.
8
9Example usage:
10
11  from cros_utils import tabulator
12
13  data = [["benchmark1", "33", "44"],["benchmark2", "44", "33"]]
14  tabulator.GetSimpleTable(data)
15
16You could also use it to generate more complex tables with analysis such as
17p-values, custom colors, etc. Tables are generated by TableGenerator and
18analyzed/formatted by TableFormatter. TableFormatter can take in a list of
19columns with custom result computation and coloring, and will compare values in
20each row according to taht scheme. Here is a complex example on printing a
21table:
22
23  from cros_utils import tabulator
24
25  runs = [[{"k1": "10", "k2": "12", "k5": "40", "k6": "40",
26            "ms_1": "20", "k7": "FAIL", "k8": "PASS", "k9": "PASS",
27            "k10": "0"},
28           {"k1": "13", "k2": "14", "k3": "15", "ms_1": "10", "k8": "PASS",
29            "k9": "FAIL", "k10": "0"}],
30          [{"k1": "50", "k2": "51", "k3": "52", "k4": "53", "k5": "35", "k6":
31            "45", "ms_1": "200", "ms_2": "20", "k7": "FAIL", "k8": "PASS", "k9":
32            "PASS"}]]
33  labels = ["vanilla", "modified"]
34  tg = TableGenerator(runs, labels, TableGenerator.SORT_BY_VALUES_DESC)
35  table = tg.GetTable()
36  columns = [Column(LiteralResult(),
37                    Format(),
38                    "Literal"),
39             Column(AmeanResult(),
40                    Format()),
41             Column(StdResult(),
42                    Format()),
43             Column(CoeffVarResult(),
44                    CoeffVarFormat()),
45             Column(NonEmptyCountResult(),
46                    Format()),
47             Column(AmeanRatioResult(),
48                    PercentFormat()),
49             Column(AmeanRatioResult(),
50                    RatioFormat()),
51             Column(GmeanRatioResult(),
52                    RatioFormat()),
53             Column(PValueResult(),
54                    PValueFormat()),
55            ]
56  tf = TableFormatter(table, columns)
57  cell_table = tf.GetCellTable()
58  tp = TablePrinter(cell_table, out_to)
59  print tp.Print()
60"""
61
62from __future__ import print_function
63
64import getpass
65import math
66import sys
67import numpy
68
69from email_sender import EmailSender
70import misc
71
72
73def _AllFloat(values):
74  return all([misc.IsFloat(v) for v in values])
75
76
77def _GetFloats(values):
78  return [float(v) for v in values]
79
80
81def _StripNone(results):
82  res = []
83  for result in results:
84    if result is not None:
85      res.append(result)
86  return res
87
88
89class TableGenerator(object):
90  """Creates a table from a list of list of dicts.
91
92  The main public function is called GetTable().
93  """
94  SORT_BY_KEYS = 0
95  SORT_BY_KEYS_DESC = 1
96  SORT_BY_VALUES = 2
97  SORT_BY_VALUES_DESC = 3
98
99  MISSING_VALUE = 'x'
100
101  def __init__(self, d, l, sort=SORT_BY_KEYS, key_name='keys'):
102    self._runs = d
103    self._labels = l
104    self._sort = sort
105    self._key_name = key_name
106
107  def _AggregateKeys(self):
108    keys = set([])
109    for run_list in self._runs:
110      for run in run_list:
111        keys = keys.union(run.keys())
112    return keys
113
114  def _GetHighestValue(self, key):
115    values = []
116    for run_list in self._runs:
117      for run in run_list:
118        if key in run:
119          values.append(run[key])
120    values = _StripNone(values)
121    if _AllFloat(values):
122      values = _GetFloats(values)
123    return max(values)
124
125  def _GetLowestValue(self, key):
126    values = []
127    for run_list in self._runs:
128      for run in run_list:
129        if key in run:
130          values.append(run[key])
131    values = _StripNone(values)
132    if _AllFloat(values):
133      values = _GetFloats(values)
134    return min(values)
135
136  def _SortKeys(self, keys):
137    if self._sort == self.SORT_BY_KEYS:
138      return sorted(keys)
139    elif self._sort == self.SORT_BY_VALUES:
140      # pylint: disable=unnecessary-lambda
141      return sorted(keys, key=lambda x: self._GetLowestValue(x))
142    elif self._sort == self.SORT_BY_VALUES_DESC:
143      # pylint: disable=unnecessary-lambda
144      return sorted(keys, key=lambda x: self._GetHighestValue(x), reverse=True)
145    else:
146      assert 0, 'Unimplemented sort %s' % self._sort
147
148  def _GetKeys(self):
149    keys = self._AggregateKeys()
150    return self._SortKeys(keys)
151
152  def GetTable(self, number_of_rows=sys.maxint):
153    """Returns a table from a list of list of dicts.
154
155    The list of list of dicts is passed into the constructor of TableGenerator.
156    This method converts that into a canonical list of lists which represents a
157    table of values.
158
159    Args:
160      number_of_rows: Maximum number of rows to return from the table.
161
162    Returns:
163      A list of lists which is the table.
164
165    Example:
166      We have the following runs:
167        [[{"k1": "v1", "k2": "v2"}, {"k1": "v3"}],
168         [{"k1": "v4", "k4": "v5"}]]
169      and the following labels:
170        ["vanilla", "modified"]
171      it will return:
172        [["Key", "vanilla", "modified"]
173         ["k1", ["v1", "v3"], ["v4"]]
174         ["k2", ["v2"], []]
175         ["k4", [], ["v5"]]]
176      The returned table can then be processed further by other classes in this
177      module.
178    """
179    keys = self._GetKeys()
180    header = [self._key_name] + self._labels
181    table = [header]
182    rows = 0
183    for k in keys:
184      row = [k]
185      unit = None
186      for run_list in self._runs:
187        v = []
188        for run in run_list:
189          if k in run:
190            if type(run[k]) is list:
191              val = run[k][0]
192              unit = run[k][1]
193            else:
194              val = run[k]
195            v.append(val)
196          else:
197            v.append(None)
198        row.append(v)
199      # If we got a 'unit' value, append the units name to the key name.
200      if unit:
201        keyname = row[0] + ' (%s) ' % unit
202        row[0] = keyname
203      table.append(row)
204      rows += 1
205      if rows == number_of_rows:
206        break
207    return table
208
209
210class Result(object):
211  """A class that respresents a single result.
212
213  This single result is obtained by condensing the information from a list of
214  runs and a list of baseline runs.
215  """
216
217  def __init__(self):
218    pass
219
220  def _AllStringsSame(self, values):
221    values_set = set(values)
222    return len(values_set) == 1
223
224  def NeedsBaseline(self):
225    return False
226
227  # pylint: disable=unused-argument
228  def _Literal(self, cell, values, baseline_values):
229    cell.value = ' '.join([str(v) for v in values])
230
231  def _ComputeFloat(self, cell, values, baseline_values):
232    self._Literal(cell, values, baseline_values)
233
234  def _ComputeString(self, cell, values, baseline_values):
235    self._Literal(cell, values, baseline_values)
236
237  def _InvertIfLowerIsBetter(self, cell):
238    pass
239
240  def _GetGmean(self, values):
241    if not values:
242      return float('nan')
243    if any([v < 0 for v in values]):
244      return float('nan')
245    if any([v == 0 for v in values]):
246      return 0.0
247    log_list = [math.log(v) for v in values]
248    gmean_log = sum(log_list) / len(log_list)
249    return math.exp(gmean_log)
250
251  def Compute(self, cell, values, baseline_values):
252    """Compute the result given a list of values and baseline values.
253
254    Args:
255      cell: A cell data structure to populate.
256      values: List of values.
257      baseline_values: List of baseline values. Can be none if this is the
258      baseline itself.
259    """
260    all_floats = True
261    values = _StripNone(values)
262    if not values:
263      cell.value = ''
264      return
265    if _AllFloat(values):
266      float_values = _GetFloats(values)
267    else:
268      all_floats = False
269    if baseline_values:
270      baseline_values = _StripNone(baseline_values)
271    if baseline_values:
272      if _AllFloat(baseline_values):
273        float_baseline_values = _GetFloats(baseline_values)
274      else:
275        all_floats = False
276    else:
277      if self.NeedsBaseline():
278        cell.value = ''
279        return
280      float_baseline_values = None
281    if all_floats:
282      self._ComputeFloat(cell, float_values, float_baseline_values)
283      self._InvertIfLowerIsBetter(cell)
284    else:
285      self._ComputeString(cell, values, baseline_values)
286
287
288class LiteralResult(Result):
289  """A literal result."""
290
291  def __init__(self, iteration=0):
292    super(LiteralResult, self).__init__()
293    self.iteration = iteration
294
295  def Compute(self, cell, values, baseline_values):
296    try:
297      cell.value = values[self.iteration]
298    except IndexError:
299      cell.value = '-'
300
301
302class NonEmptyCountResult(Result):
303  """A class that counts the number of non-empty results.
304
305  The number of non-empty values will be stored in the cell.
306  """
307
308  def Compute(self, cell, values, baseline_values):
309    """Put the number of non-empty values in the cell result.
310
311    Args:
312      cell: Put the result in cell.value.
313      values: A list of values for the row.
314      baseline_values: A list of baseline values for the row.
315    """
316    cell.value = len(_StripNone(values))
317    if not baseline_values:
318      return
319    base_value = len(_StripNone(baseline_values))
320    if cell.value == base_value:
321      return
322    f = ColorBoxFormat()
323    len_values = len(values)
324    len_baseline_values = len(baseline_values)
325    tmp_cell = Cell()
326    tmp_cell.value = 1.0 + (float(cell.value - base_value) /
327                            (max(len_values, len_baseline_values)))
328    f.Compute(tmp_cell)
329    cell.bgcolor = tmp_cell.bgcolor
330
331
332class StringMeanResult(Result):
333  """Mean of string values."""
334
335  def _ComputeString(self, cell, values, baseline_values):
336    if self._AllStringsSame(values):
337      cell.value = str(values[0])
338    else:
339      cell.value = '?'
340
341
342class AmeanResult(StringMeanResult):
343  """Arithmetic mean."""
344
345  def _ComputeFloat(self, cell, values, baseline_values):
346    cell.value = numpy.mean(values)
347
348
349class RawResult(Result):
350  """Raw result."""
351  pass
352
353
354class MinResult(Result):
355  """Minimum."""
356
357  def _ComputeFloat(self, cell, values, baseline_values):
358    cell.value = min(values)
359
360  def _ComputeString(self, cell, values, baseline_values):
361    if values:
362      cell.value = min(values)
363    else:
364      cell.value = ''
365
366
367class MaxResult(Result):
368  """Maximum."""
369
370  def _ComputeFloat(self, cell, values, baseline_values):
371    cell.value = max(values)
372
373  def _ComputeString(self, cell, values, baseline_values):
374    if values:
375      cell.value = max(values)
376    else:
377      cell.value = ''
378
379
380class NumericalResult(Result):
381  """Numerical result."""
382
383  def _ComputeString(self, cell, values, baseline_values):
384    cell.value = '?'
385
386
387class StdResult(NumericalResult):
388  """Standard deviation."""
389
390  def _ComputeFloat(self, cell, values, baseline_values):
391    cell.value = numpy.std(values)
392
393
394class CoeffVarResult(NumericalResult):
395  """Standard deviation / Mean"""
396
397  def _ComputeFloat(self, cell, values, baseline_values):
398    if numpy.mean(values) != 0.0:
399      noise = numpy.abs(numpy.std(values) / numpy.mean(values))
400    else:
401      noise = 0.0
402    cell.value = noise
403
404
405class ComparisonResult(Result):
406  """Same or Different."""
407
408  def NeedsBaseline(self):
409    return True
410
411  def _ComputeString(self, cell, values, baseline_values):
412    value = None
413    baseline_value = None
414    if self._AllStringsSame(values):
415      value = values[0]
416    if self._AllStringsSame(baseline_values):
417      baseline_value = baseline_values[0]
418    if value is not None and baseline_value is not None:
419      if value == baseline_value:
420        cell.value = 'SAME'
421      else:
422        cell.value = 'DIFFERENT'
423    else:
424      cell.value = '?'
425
426
427class PValueResult(ComparisonResult):
428  """P-value."""
429
430  def _ComputeFloat(self, cell, values, baseline_values):
431    if len(values) < 2 or len(baseline_values) < 2:
432      cell.value = float('nan')
433      return
434    import stats
435    _, cell.value = stats.lttest_ind(values, baseline_values)
436
437  def _ComputeString(self, cell, values, baseline_values):
438    return float('nan')
439
440
441class KeyAwareComparisonResult(ComparisonResult):
442  """Automatic key aware comparison."""
443
444  def _IsLowerBetter(self, key):
445    # TODO(llozano): Trying to guess direction by looking at the name of the
446    # test does not seem like a good idea. Test frameworks should provide this
447    # info explicitly. I believe Telemetry has this info. Need to find it out.
448    #
449    # Below are some test names for which we are not sure what the
450    # direction is.
451    #
452    # For these we dont know what the direction is. But, since we dont
453    # specify anything, crosperf will assume higher is better:
454    # --percent_impl_scrolled--percent_impl_scrolled--percent
455    # --solid_color_tiles_analyzed--solid_color_tiles_analyzed--count
456    # --total_image_cache_hit_count--total_image_cache_hit_count--count
457    # --total_texture_upload_time_by_url
458    #
459    # About these we are doubtful but we made a guess:
460    # --average_num_missing_tiles_by_url--*--units (low is good)
461    # --experimental_mean_frame_time_by_url--*--units (low is good)
462    # --experimental_median_frame_time_by_url--*--units (low is good)
463    # --texture_upload_count--texture_upload_count--count (high is good)
464    # --total_deferred_image_decode_count--count (low is good)
465    # --total_tiles_analyzed--total_tiles_analyzed--count (high is good)
466    lower_is_better_keys = [
467        'milliseconds', 'ms_', 'seconds_', 'KB', 'rdbytes', 'wrbytes',
468        'dropped_percent', '(ms)', '(seconds)', '--ms',
469        '--average_num_missing_tiles', '--experimental_jank',
470        '--experimental_mean_frame', '--experimental_median_frame_time',
471        '--total_deferred_image_decode_count', '--seconds'
472    ]
473
474    return any([l in key for l in lower_is_better_keys])
475
476  def _InvertIfLowerIsBetter(self, cell):
477    if self._IsLowerBetter(cell.name):
478      if cell.value:
479        cell.value = 1.0 / cell.value
480
481
482class AmeanRatioResult(KeyAwareComparisonResult):
483  """Ratio of arithmetic means of values vs. baseline values."""
484
485  def _ComputeFloat(self, cell, values, baseline_values):
486    if numpy.mean(baseline_values) != 0:
487      cell.value = numpy.mean(values) / numpy.mean(baseline_values)
488    elif numpy.mean(values) != 0:
489      cell.value = 0.00
490      # cell.value = 0 means the values and baseline_values have big difference
491    else:
492      cell.value = 1.00
493      # no difference if both values and baseline_values are 0
494
495
496class GmeanRatioResult(KeyAwareComparisonResult):
497  """Ratio of geometric means of values vs. baseline values."""
498
499  def _ComputeFloat(self, cell, values, baseline_values):
500    if self._GetGmean(baseline_values) != 0:
501      cell.value = self._GetGmean(values) / self._GetGmean(baseline_values)
502    elif self._GetGmean(values) != 0:
503      cell.value = 0.00
504    else:
505      cell.value = 1.00
506
507
508class Color(object):
509  """Class that represents color in RGBA format."""
510
511  def __init__(self, r=0, g=0, b=0, a=0):
512    self.r = r
513    self.g = g
514    self.b = b
515    self.a = a
516
517  def __str__(self):
518    return 'r: %s g: %s: b: %s: a: %s' % (self.r, self.g, self.b, self.a)
519
520  def Round(self):
521    """Round RGBA values to the nearest integer."""
522    self.r = int(self.r)
523    self.g = int(self.g)
524    self.b = int(self.b)
525    self.a = int(self.a)
526
527  def GetRGB(self):
528    """Get a hex representation of the color."""
529    return '%02x%02x%02x' % (self.r, self.g, self.b)
530
531  @classmethod
532  def Lerp(cls, ratio, a, b):
533    """Perform linear interpolation between two colors.
534
535    Args:
536      ratio: The ratio to use for linear polation.
537      a: The first color object (used when ratio is 0).
538      b: The second color object (used when ratio is 1).
539
540    Returns:
541      Linearly interpolated color.
542    """
543    ret = cls()
544    ret.r = (b.r - a.r) * ratio + a.r
545    ret.g = (b.g - a.g) * ratio + a.g
546    ret.b = (b.b - a.b) * ratio + a.b
547    ret.a = (b.a - a.a) * ratio + a.a
548    return ret
549
550
551class Format(object):
552  """A class that represents the format of a column."""
553
554  def __init__(self):
555    pass
556
557  def Compute(self, cell):
558    """Computes the attributes of a cell based on its value.
559
560    Attributes typically are color, width, etc.
561
562    Args:
563      cell: The cell whose attributes are to be populated.
564    """
565    if cell.value is None:
566      cell.string_value = ''
567    if isinstance(cell.value, float):
568      self._ComputeFloat(cell)
569    else:
570      self._ComputeString(cell)
571
572  def _ComputeFloat(self, cell):
573    cell.string_value = '{0:.2f}'.format(cell.value)
574
575  def _ComputeString(self, cell):
576    cell.string_value = str(cell.value)
577
578  def _GetColor(self, value, low, mid, high, power=6, mid_value=1.0):
579    min_value = 0.0
580    max_value = 2.0
581    if math.isnan(value):
582      return mid
583    if value > mid_value:
584      value = max_value - mid_value / value
585
586    return self._GetColorBetweenRange(value, min_value, mid_value, max_value,
587                                      low, mid, high, power)
588
589  def _GetColorBetweenRange(self, value, min_value, mid_value, max_value,
590                            low_color, mid_color, high_color, power):
591    assert value <= max_value
592    assert value >= min_value
593    if value > mid_value:
594      value = (max_value - value) / (max_value - mid_value)
595      value **= power
596      ret = Color.Lerp(value, high_color, mid_color)
597    else:
598      value = (value - min_value) / (mid_value - min_value)
599      value **= power
600      ret = Color.Lerp(value, low_color, mid_color)
601    ret.Round()
602    return ret
603
604
605class PValueFormat(Format):
606  """Formatting for p-value."""
607
608  def _ComputeFloat(self, cell):
609    cell.string_value = '%0.2f' % float(cell.value)
610    if float(cell.value) < 0.05:
611      cell.bgcolor = self._GetColor(
612          cell.value,
613          Color(255, 255, 0, 0),
614          Color(255, 255, 255, 0),
615          Color(255, 255, 255, 0),
616          mid_value=0.05,
617          power=1)
618
619
620class StorageFormat(Format):
621  """Format the cell as a storage number.
622
623  Example:
624    If the cell contains a value of 1024, the string_value will be 1.0K.
625  """
626
627  def _ComputeFloat(self, cell):
628    base = 1024
629    suffices = ['K', 'M', 'G']
630    v = float(cell.value)
631    current = 0
632    while v >= base**(current + 1) and current < len(suffices):
633      current += 1
634
635    if current:
636      divisor = base**current
637      cell.string_value = '%1.1f%s' % ((v / divisor), suffices[current - 1])
638    else:
639      cell.string_value = str(cell.value)
640
641
642class CoeffVarFormat(Format):
643  """Format the cell as a percent.
644
645  Example:
646    If the cell contains a value of 1.5, the string_value will be +150%.
647  """
648
649  def _ComputeFloat(self, cell):
650    cell.string_value = '%1.1f%%' % (float(cell.value) * 100)
651    cell.color = self._GetColor(
652        cell.value,
653        Color(0, 255, 0, 0),
654        Color(0, 0, 0, 0),
655        Color(255, 0, 0, 0),
656        mid_value=0.02,
657        power=1)
658
659
660class PercentFormat(Format):
661  """Format the cell as a percent.
662
663  Example:
664    If the cell contains a value of 1.5, the string_value will be +50%.
665  """
666
667  def _ComputeFloat(self, cell):
668    cell.string_value = '%+1.1f%%' % ((float(cell.value) - 1) * 100)
669    cell.color = self._GetColor(cell.value,
670                                Color(255, 0, 0, 0),
671                                Color(0, 0, 0, 0), Color(0, 255, 0, 0))
672
673
674class RatioFormat(Format):
675  """Format the cell as a ratio.
676
677  Example:
678    If the cell contains a value of 1.5642, the string_value will be 1.56.
679  """
680
681  def _ComputeFloat(self, cell):
682    cell.string_value = '%+1.1f%%' % ((cell.value - 1) * 100)
683    cell.color = self._GetColor(cell.value,
684                                Color(255, 0, 0, 0),
685                                Color(0, 0, 0, 0), Color(0, 255, 0, 0))
686
687
688class ColorBoxFormat(Format):
689  """Format the cell as a color box.
690
691  Example:
692    If the cell contains a value of 1.5, it will get a green color.
693    If the cell contains a value of 0.5, it will get a red color.
694    The intensity of the green/red will be determined by how much above or below
695    1.0 the value is.
696  """
697
698  def _ComputeFloat(self, cell):
699    cell.string_value = '--'
700    bgcolor = self._GetColor(cell.value,
701                             Color(255, 0, 0, 0),
702                             Color(255, 255, 255, 0), Color(0, 255, 0, 0))
703    cell.bgcolor = bgcolor
704    cell.color = bgcolor
705
706
707class Cell(object):
708  """A class to represent a cell in a table.
709
710  Attributes:
711    value: The raw value of the cell.
712    color: The color of the cell.
713    bgcolor: The background color of the cell.
714    string_value: The string value of the cell.
715    suffix: A string suffix to be attached to the value when displaying.
716    prefix: A string prefix to be attached to the value when displaying.
717    color_row: Indicates whether the whole row is to inherit this cell's color.
718    bgcolor_row: Indicates whether the whole row is to inherit this cell's
719    bgcolor.
720    width: Optional specifier to make a column narrower than the usual width.
721    The usual width of a column is the max of all its cells widths.
722    colspan: Set the colspan of the cell in the HTML table, this is used for
723    table headers. Default value is 1.
724    name: the test name of the cell.
725    header: Whether this is a header in html.
726  """
727
728  def __init__(self):
729    self.value = None
730    self.color = None
731    self.bgcolor = None
732    self.string_value = None
733    self.suffix = None
734    self.prefix = None
735    # Entire row inherits this color.
736    self.color_row = False
737    self.bgcolor_row = False
738    self.width = None
739    self.colspan = 1
740    self.name = None
741    self.header = False
742
743  def __str__(self):
744    l = []
745    l.append('value: %s' % self.value)
746    l.append('string_value: %s' % self.string_value)
747    return ' '.join(l)
748
749
750class Column(object):
751  """Class representing a column in a table.
752
753  Attributes:
754    result: an object of the Result class.
755    fmt: an object of the Format class.
756  """
757
758  def __init__(self, result, fmt, name=''):
759    self.result = result
760    self.fmt = fmt
761    self.name = name
762
763
764# Takes in:
765# ["Key", "Label1", "Label2"]
766# ["k", ["v", "v2"], [v3]]
767# etc.
768# Also takes in a format string.
769# Returns a table like:
770# ["Key", "Label1", "Label2"]
771# ["k", avg("v", "v2"), stddev("v", "v2"), etc.]]
772# according to format string
773class TableFormatter(object):
774  """Class to convert a plain table into a cell-table.
775
776  This class takes in a table generated by TableGenerator and a list of column
777  formats to apply to the table and returns a table of cells.
778  """
779
780  def __init__(self, table, columns):
781    """The constructor takes in a table and a list of columns.
782
783    Args:
784      table: A list of lists of values.
785      columns: A list of column containing what to produce and how to format it.
786    """
787    self._table = table
788    self._columns = columns
789    self._table_columns = []
790    self._out_table = []
791
792  def GenerateCellTable(self, table_type):
793    row_index = 0
794    all_failed = False
795
796    for row in self._table[1:]:
797      # It does not make sense to put retval in the summary table.
798      if str(row[0]) == 'retval' and table_type == 'summary':
799        # Check to see if any runs passed, and update all_failed.
800        all_failed = True
801        for values in row[1:]:
802          if 0 in values:
803            all_failed = False
804        continue
805      key = Cell()
806      key.string_value = str(row[0])
807      out_row = [key]
808      baseline = None
809      for values in row[1:]:
810        for column in self._columns:
811          cell = Cell()
812          cell.name = key.string_value
813          if column.result.NeedsBaseline():
814            if baseline is not None:
815              column.result.Compute(cell, values, baseline)
816              column.fmt.Compute(cell)
817              out_row.append(cell)
818              if not row_index:
819                self._table_columns.append(column)
820          else:
821            column.result.Compute(cell, values, baseline)
822            column.fmt.Compute(cell)
823            out_row.append(cell)
824            if not row_index:
825              self._table_columns.append(column)
826
827        if baseline is None:
828          baseline = values
829      self._out_table.append(out_row)
830      row_index += 1
831
832    # If this is a summary table, and the only row in it is 'retval', and
833    # all the test runs failed, we need to a 'Results' row to the output
834    # table.
835    if table_type == 'summary' and all_failed and len(self._table) == 2:
836      labels_row = self._table[0]
837      key = Cell()
838      key.string_value = 'Results'
839      out_row = [key]
840      baseline = None
841      for _ in labels_row[1:]:
842        for column in self._columns:
843          cell = Cell()
844          cell.name = key.string_value
845          column.result.Compute(cell, ['Fail'], baseline)
846          column.fmt.Compute(cell)
847          out_row.append(cell)
848          if not row_index:
849            self._table_columns.append(column)
850      self._out_table.append(out_row)
851
852  def AddColumnName(self):
853    """Generate Column name at the top of table."""
854    key = Cell()
855    key.header = True
856    key.string_value = 'Keys'
857    header = [key]
858    for column in self._table_columns:
859      cell = Cell()
860      cell.header = True
861      if column.name:
862        cell.string_value = column.name
863      else:
864        result_name = column.result.__class__.__name__
865        format_name = column.fmt.__class__.__name__
866
867        cell.string_value = '%s %s' % (result_name.replace('Result', ''),
868                                       format_name.replace('Format', ''))
869
870      header.append(cell)
871
872    self._out_table = [header] + self._out_table
873
874  def AddHeader(self, s):
875    """Put additional string on the top of the table."""
876    cell = Cell()
877    cell.header = True
878    cell.string_value = str(s)
879    header = [cell]
880    colspan = max(1, max(len(row) for row in self._table))
881    cell.colspan = colspan
882    self._out_table = [header] + self._out_table
883
884  def GetPassesAndFails(self, values):
885    passes = 0
886    fails = 0
887    for val in values:
888      if val == 0:
889        passes = passes + 1
890      else:
891        fails = fails + 1
892    return passes, fails
893
894  def AddLabelName(self):
895    """Put label on the top of the table."""
896    top_header = []
897    base_colspan = len(
898        [c for c in self._columns if not c.result.NeedsBaseline()])
899    compare_colspan = len(self._columns)
900    # Find the row with the key 'retval', if it exists.  This
901    # will be used to calculate the number of iterations that passed and
902    # failed for each image label.
903    retval_row = None
904    for row in self._table:
905      if row[0] == 'retval':
906        retval_row = row
907    # The label is organized as follows
908    # "keys" label_base, label_comparison1, label_comparison2
909    # The first cell has colspan 1, the second is base_colspan
910    # The others are compare_colspan
911    column_position = 0
912    for label in self._table[0]:
913      cell = Cell()
914      cell.header = True
915      # Put the number of pass/fail iterations in the image label header.
916      if column_position > 0 and retval_row:
917        retval_values = retval_row[column_position]
918        if type(retval_values) is list:
919          passes, fails = self.GetPassesAndFails(retval_values)
920          cell.string_value = str(label) + '  (pass:%d fail:%d)' % (passes,
921                                                                    fails)
922        else:
923          cell.string_value = str(label)
924      else:
925        cell.string_value = str(label)
926      if top_header:
927        cell.colspan = base_colspan
928      if len(top_header) > 1:
929        cell.colspan = compare_colspan
930      top_header.append(cell)
931      column_position = column_position + 1
932    self._out_table = [top_header] + self._out_table
933
934  def _PrintOutTable(self):
935    o = ''
936    for row in self._out_table:
937      for cell in row:
938        o += str(cell) + ' '
939      o += '\n'
940    print(o)
941
942  def GetCellTable(self, table_type='full', headers=True):
943    """Function to return a table of cells.
944
945    The table (list of lists) is converted into a table of cells by this
946    function.
947
948    Args:
949      table_type: Can be 'full' or 'summary'
950      headers: A boolean saying whether we want default headers
951
952    Returns:
953      A table of cells with each cell having the properties and string values as
954      requiested by the columns passed in the constructor.
955    """
956    # Generate the cell table, creating a list of dynamic columns on the fly.
957    if not self._out_table:
958      self.GenerateCellTable(table_type)
959    if headers:
960      self.AddColumnName()
961      self.AddLabelName()
962    return self._out_table
963
964
965class TablePrinter(object):
966  """Class to print a cell table to the console, file or html."""
967  PLAIN = 0
968  CONSOLE = 1
969  HTML = 2
970  TSV = 3
971  EMAIL = 4
972
973  def __init__(self, table, output_type):
974    """Constructor that stores the cell table and output type."""
975    self._table = table
976    self._output_type = output_type
977    self._row_styles = []
978    self._column_styles = []
979
980  # Compute whole-table properties like max-size, etc.
981  def _ComputeStyle(self):
982    self._row_styles = []
983    for row in self._table:
984      row_style = Cell()
985      for cell in row:
986        if cell.color_row:
987          assert cell.color, 'Cell color not set but color_row set!'
988          assert not row_style.color, 'Multiple row_style.colors found!'
989          row_style.color = cell.color
990        if cell.bgcolor_row:
991          assert cell.bgcolor, 'Cell bgcolor not set but bgcolor_row set!'
992          assert not row_style.bgcolor, 'Multiple row_style.bgcolors found!'
993          row_style.bgcolor = cell.bgcolor
994      self._row_styles.append(row_style)
995
996    self._column_styles = []
997    if len(self._table) < 2:
998      return
999
1000    for i in range(max(len(row) for row in self._table)):
1001      column_style = Cell()
1002      for row in self._table:
1003        if not any([cell.colspan != 1 for cell in row]):
1004          column_style.width = max(column_style.width, len(row[i].string_value))
1005      self._column_styles.append(column_style)
1006
1007  def _GetBGColorFix(self, color):
1008    if self._output_type == self.CONSOLE:
1009      prefix = misc.rgb2short(color.r, color.g, color.b)
1010      # pylint: disable=anomalous-backslash-in-string
1011      prefix = '\033[48;5;%sm' % prefix
1012      suffix = '\033[0m'
1013    elif self._output_type in [self.EMAIL, self.HTML]:
1014      rgb = color.GetRGB()
1015      prefix = ("<FONT style=\"BACKGROUND-COLOR:#{0}\">".format(rgb))
1016      suffix = '</FONT>'
1017    elif self._output_type in [self.PLAIN, self.TSV]:
1018      prefix = ''
1019      suffix = ''
1020    return prefix, suffix
1021
1022  def _GetColorFix(self, color):
1023    if self._output_type == self.CONSOLE:
1024      prefix = misc.rgb2short(color.r, color.g, color.b)
1025      # pylint: disable=anomalous-backslash-in-string
1026      prefix = '\033[38;5;%sm' % prefix
1027      suffix = '\033[0m'
1028    elif self._output_type in [self.EMAIL, self.HTML]:
1029      rgb = color.GetRGB()
1030      prefix = '<FONT COLOR=#{0}>'.format(rgb)
1031      suffix = '</FONT>'
1032    elif self._output_type in [self.PLAIN, self.TSV]:
1033      prefix = ''
1034      suffix = ''
1035    return prefix, suffix
1036
1037  def Print(self):
1038    """Print the table to a console, html, etc.
1039
1040    Returns:
1041      A string that contains the desired representation of the table.
1042    """
1043    self._ComputeStyle()
1044    return self._GetStringValue()
1045
1046  def _GetCellValue(self, i, j):
1047    cell = self._table[i][j]
1048    out = cell.string_value
1049    raw_width = len(out)
1050
1051    if cell.color:
1052      p, s = self._GetColorFix(cell.color)
1053      out = '%s%s%s' % (p, out, s)
1054
1055    if cell.bgcolor:
1056      p, s = self._GetBGColorFix(cell.bgcolor)
1057      out = '%s%s%s' % (p, out, s)
1058
1059    if self._output_type in [self.PLAIN, self.CONSOLE, self.EMAIL]:
1060      if cell.width:
1061        width = cell.width
1062      else:
1063        if self._column_styles:
1064          width = self._column_styles[j].width
1065        else:
1066          width = len(cell.string_value)
1067      if cell.colspan > 1:
1068        width = 0
1069        start = 0
1070        for k in range(j):
1071          start += self._table[i][k].colspan
1072        for k in range(cell.colspan):
1073          width += self._column_styles[start + k].width
1074      if width > raw_width:
1075        padding = ('%' + str(width - raw_width) + 's') % ''
1076        out = padding + out
1077
1078    if self._output_type == self.HTML:
1079      if cell.header:
1080        tag = 'th'
1081      else:
1082        tag = 'td'
1083      out = "<{0} colspan = \"{2}\"> {1} </{0}>".format(tag, out, cell.colspan)
1084
1085    return out
1086
1087  def _GetHorizontalSeparator(self):
1088    if self._output_type in [self.CONSOLE, self.PLAIN, self.EMAIL]:
1089      return ' '
1090    if self._output_type == self.HTML:
1091      return ''
1092    if self._output_type == self.TSV:
1093      return '\t'
1094
1095  def _GetVerticalSeparator(self):
1096    if self._output_type in [self.PLAIN, self.CONSOLE, self.TSV, self.EMAIL]:
1097      return '\n'
1098    if self._output_type == self.HTML:
1099      return '</tr>\n<tr>'
1100
1101  def _GetPrefix(self):
1102    if self._output_type in [self.PLAIN, self.CONSOLE, self.TSV, self.EMAIL]:
1103      return ''
1104    if self._output_type == self.HTML:
1105      return "<p></p><table id=\"box-table-a\">\n<tr>"
1106
1107  def _GetSuffix(self):
1108    if self._output_type in [self.PLAIN, self.CONSOLE, self.TSV, self.EMAIL]:
1109      return ''
1110    if self._output_type == self.HTML:
1111      return '</tr>\n</table>'
1112
1113  def _GetStringValue(self):
1114    o = ''
1115    o += self._GetPrefix()
1116    for i in range(len(self._table)):
1117      row = self._table[i]
1118      # Apply row color and bgcolor.
1119      p = s = bgp = bgs = ''
1120      if self._row_styles[i].bgcolor:
1121        bgp, bgs = self._GetBGColorFix(self._row_styles[i].bgcolor)
1122      if self._row_styles[i].color:
1123        p, s = self._GetColorFix(self._row_styles[i].color)
1124      o += p + bgp
1125      for j in range(len(row)):
1126        out = self._GetCellValue(i, j)
1127        o += out + self._GetHorizontalSeparator()
1128      o += s + bgs
1129      o += self._GetVerticalSeparator()
1130    o += self._GetSuffix()
1131    return o
1132
1133
1134# Some common drivers
1135def GetSimpleTable(table, out_to=TablePrinter.CONSOLE):
1136  """Prints a simple table.
1137
1138  This is used by code that has a very simple list-of-lists and wants to produce
1139  a table with ameans, a percentage ratio of ameans and a colorbox.
1140
1141  Args:
1142    table: a list of lists.
1143    out_to: specify the fomat of output. Currently it supports HTML and CONSOLE.
1144
1145  Returns:
1146    A string version of the table that can be printed to the console.
1147
1148  Example:
1149    GetSimpleConsoleTable([["binary", "b1", "b2"],["size", "300", "400"]])
1150    will produce a colored table that can be printed to the console.
1151  """
1152  columns = [
1153      Column(AmeanResult(), Format()),
1154      Column(AmeanRatioResult(), PercentFormat()),
1155      Column(AmeanRatioResult(), ColorBoxFormat()),
1156  ]
1157  our_table = [table[0]]
1158  for row in table[1:]:
1159    our_row = [row[0]]
1160    for v in row[1:]:
1161      our_row.append([v])
1162    our_table.append(our_row)
1163
1164  tf = TableFormatter(our_table, columns)
1165  cell_table = tf.GetCellTable()
1166  tp = TablePrinter(cell_table, out_to)
1167  return tp.Print()
1168
1169
1170# pylint: disable=redefined-outer-name
1171def GetComplexTable(runs, labels, out_to=TablePrinter.CONSOLE):
1172  """Prints a complex table.
1173
1174  This can be used to generate a table with arithmetic mean, standard deviation,
1175  coefficient of variation, p-values, etc.
1176
1177  Args:
1178    runs: A list of lists with data to tabulate.
1179    labels: A list of labels that correspond to the runs.
1180    out_to: specifies the format of the table (example CONSOLE or HTML).
1181
1182  Returns:
1183    A string table that can be printed to the console or put in an HTML file.
1184  """
1185  tg = TableGenerator(runs, labels, TableGenerator.SORT_BY_VALUES_DESC)
1186  table = tg.GetTable()
1187  columns = [
1188      Column(LiteralResult(), Format(), 'Literal'), Column(
1189          AmeanResult(), Format()), Column(StdResult(), Format()), Column(
1190              CoeffVarResult(), CoeffVarFormat()), Column(
1191                  NonEmptyCountResult(), Format()),
1192      Column(AmeanRatioResult(), PercentFormat()), Column(
1193          AmeanRatioResult(), RatioFormat()), Column(GmeanRatioResult(),
1194                                                     RatioFormat()), Column(
1195                                                         PValueResult(),
1196                                                         PValueFormat())
1197  ]
1198  tf = TableFormatter(table, columns)
1199  cell_table = tf.GetCellTable()
1200  tp = TablePrinter(cell_table, out_to)
1201  return tp.Print()
1202
1203
1204if __name__ == '__main__':
1205  # Run a few small tests here.
1206  runs = [[{
1207      'k1': '10',
1208      'k2': '12',
1209      'k5': '40',
1210      'k6': '40',
1211      'ms_1': '20',
1212      'k7': 'FAIL',
1213      'k8': 'PASS',
1214      'k9': 'PASS',
1215      'k10': '0'
1216  }, {
1217      'k1': '13',
1218      'k2': '14',
1219      'k3': '15',
1220      'ms_1': '10',
1221      'k8': 'PASS',
1222      'k9': 'FAIL',
1223      'k10': '0'
1224  }], [{
1225      'k1': '50',
1226      'k2': '51',
1227      'k3': '52',
1228      'k4': '53',
1229      'k5': '35',
1230      'k6': '45',
1231      'ms_1': '200',
1232      'ms_2': '20',
1233      'k7': 'FAIL',
1234      'k8': 'PASS',
1235      'k9': 'PASS'
1236  }]]
1237  labels = ['vanilla', 'modified']
1238  t = GetComplexTable(runs, labels, TablePrinter.CONSOLE)
1239  print(t)
1240  email = GetComplexTable(runs, labels, TablePrinter.EMAIL)
1241
1242  runs = [[{
1243      'k1': '1'
1244  }, {
1245      'k1': '1.1'
1246  }, {
1247      'k1': '1.2'
1248  }], [{
1249      'k1': '5'
1250  }, {
1251      'k1': '5.1'
1252  }, {
1253      'k1': '5.2'
1254  }]]
1255  t = GetComplexTable(runs, labels, TablePrinter.CONSOLE)
1256  print(t)
1257
1258  simple_table = [
1259      ['binary', 'b1', 'b2', 'b3'],
1260      ['size', 100, 105, 108],
1261      ['rodata', 100, 80, 70],
1262      ['data', 100, 100, 100],
1263      ['debug', 100, 140, 60],
1264  ]
1265  t = GetSimpleTable(simple_table)
1266  print(t)
1267  email += GetSimpleTable(simple_table, TablePrinter.HTML)
1268  email_to = [getpass.getuser()]
1269  email = "<pre style='font-size: 13px'>%s</pre>" % email
1270  EmailSender().SendEmail(email_to, 'SimpleTableTest', email, msg_type='html')
1271