普通文本  |  1271行  |  36.73 KB

# Copyright (c) 2013 The Chromium OS Authors. All rights reserved.
# Use of this source code is governed by a BSD-style license that can be
# found in the LICENSE file.
"""Table generating, analyzing and printing functions.

This defines several classes that are used to generate, analyze and print
tables.

Example usage:

  from cros_utils import tabulator

  data = [["benchmark1", "33", "44"],["benchmark2", "44", "33"]]
  tabulator.GetSimpleTable(data)

You could also use it to generate more complex tables with analysis such as
p-values, custom colors, etc. Tables are generated by TableGenerator and
analyzed/formatted by TableFormatter. TableFormatter can take in a list of
columns with custom result computation and coloring, and will compare values in
each row according to taht scheme. Here is a complex example on printing a
table:

  from cros_utils import tabulator

  runs = [[{"k1": "10", "k2": "12", "k5": "40", "k6": "40",
            "ms_1": "20", "k7": "FAIL", "k8": "PASS", "k9": "PASS",
            "k10": "0"},
           {"k1": "13", "k2": "14", "k3": "15", "ms_1": "10", "k8": "PASS",
            "k9": "FAIL", "k10": "0"}],
          [{"k1": "50", "k2": "51", "k3": "52", "k4": "53", "k5": "35", "k6":
            "45", "ms_1": "200", "ms_2": "20", "k7": "FAIL", "k8": "PASS", "k9":
            "PASS"}]]
  labels = ["vanilla", "modified"]
  tg = TableGenerator(runs, labels, TableGenerator.SORT_BY_VALUES_DESC)
  table = tg.GetTable()
  columns = [Column(LiteralResult(),
                    Format(),
                    "Literal"),
             Column(AmeanResult(),
                    Format()),
             Column(StdResult(),
                    Format()),
             Column(CoeffVarResult(),
                    CoeffVarFormat()),
             Column(NonEmptyCountResult(),
                    Format()),
             Column(AmeanRatioResult(),
                    PercentFormat()),
             Column(AmeanRatioResult(),
                    RatioFormat()),
             Column(GmeanRatioResult(),
                    RatioFormat()),
             Column(PValueResult(),
                    PValueFormat()),
            ]
  tf = TableFormatter(table, columns)
  cell_table = tf.GetCellTable()
  tp = TablePrinter(cell_table, out_to)
  print tp.Print()
"""

from __future__ import print_function

import getpass
import math
import sys
import numpy

from email_sender import EmailSender
import misc


def _AllFloat(values):
  return all([misc.IsFloat(v) for v in values])


def _GetFloats(values):
  return [float(v) for v in values]


def _StripNone(results):
  res = []
  for result in results:
    if result is not None:
      res.append(result)
  return res


class TableGenerator(object):
  """Creates a table from a list of list of dicts.

  The main public function is called GetTable().
  """
  SORT_BY_KEYS = 0
  SORT_BY_KEYS_DESC = 1
  SORT_BY_VALUES = 2
  SORT_BY_VALUES_DESC = 3

  MISSING_VALUE = 'x'

  def __init__(self, d, l, sort=SORT_BY_KEYS, key_name='keys'):
    self._runs = d
    self._labels = l
    self._sort = sort
    self._key_name = key_name

  def _AggregateKeys(self):
    keys = set([])
    for run_list in self._runs:
      for run in run_list:
        keys = keys.union(run.keys())
    return keys

  def _GetHighestValue(self, key):
    values = []
    for run_list in self._runs:
      for run in run_list:
        if key in run:
          values.append(run[key])
    values = _StripNone(values)
    if _AllFloat(values):
      values = _GetFloats(values)
    return max(values)

  def _GetLowestValue(self, key):
    values = []
    for run_list in self._runs:
      for run in run_list:
        if key in run:
          values.append(run[key])
    values = _StripNone(values)
    if _AllFloat(values):
      values = _GetFloats(values)
    return min(values)

  def _SortKeys(self, keys):
    if self._sort == self.SORT_BY_KEYS:
      return sorted(keys)
    elif self._sort == self.SORT_BY_VALUES:
      # pylint: disable=unnecessary-lambda
      return sorted(keys, key=lambda x: self._GetLowestValue(x))
    elif self._sort == self.SORT_BY_VALUES_DESC:
      # pylint: disable=unnecessary-lambda
      return sorted(keys, key=lambda x: self._GetHighestValue(x), reverse=True)
    else:
      assert 0, 'Unimplemented sort %s' % self._sort

  def _GetKeys(self):
    keys = self._AggregateKeys()
    return self._SortKeys(keys)

  def GetTable(self, number_of_rows=sys.maxint):
    """Returns a table from a list of list of dicts.

    The list of list of dicts is passed into the constructor of TableGenerator.
    This method converts that into a canonical list of lists which represents a
    table of values.

    Args:
      number_of_rows: Maximum number of rows to return from the table.

    Returns:
      A list of lists which is the table.

    Example:
      We have the following runs:
        [[{"k1": "v1", "k2": "v2"}, {"k1": "v3"}],
         [{"k1": "v4", "k4": "v5"}]]
      and the following labels:
        ["vanilla", "modified"]
      it will return:
        [["Key", "vanilla", "modified"]
         ["k1", ["v1", "v3"], ["v4"]]
         ["k2", ["v2"], []]
         ["k4", [], ["v5"]]]
      The returned table can then be processed further by other classes in this
      module.
    """
    keys = self._GetKeys()
    header = [self._key_name] + self._labels
    table = [header]
    rows = 0
    for k in keys:
      row = [k]
      unit = None
      for run_list in self._runs:
        v = []
        for run in run_list:
          if k in run:
            if type(run[k]) is list:
              val = run[k][0]
              unit = run[k][1]
            else:
              val = run[k]
            v.append(val)
          else:
            v.append(None)
        row.append(v)
      # If we got a 'unit' value, append the units name to the key name.
      if unit:
        keyname = row[0] + ' (%s) ' % unit
        row[0] = keyname
      table.append(row)
      rows += 1
      if rows == number_of_rows:
        break
    return table


class Result(object):
  """A class that respresents a single result.

  This single result is obtained by condensing the information from a list of
  runs and a list of baseline runs.
  """

  def __init__(self):
    pass

  def _AllStringsSame(self, values):
    values_set = set(values)
    return len(values_set) == 1

  def NeedsBaseline(self):
    return False

  # pylint: disable=unused-argument
  def _Literal(self, cell, values, baseline_values):
    cell.value = ' '.join([str(v) for v in values])

  def _ComputeFloat(self, cell, values, baseline_values):
    self._Literal(cell, values, baseline_values)

  def _ComputeString(self, cell, values, baseline_values):
    self._Literal(cell, values, baseline_values)

  def _InvertIfLowerIsBetter(self, cell):
    pass

  def _GetGmean(self, values):
    if not values:
      return float('nan')
    if any([v < 0 for v in values]):
      return float('nan')
    if any([v == 0 for v in values]):
      return 0.0
    log_list = [math.log(v) for v in values]
    gmean_log = sum(log_list) / len(log_list)
    return math.exp(gmean_log)

  def Compute(self, cell, values, baseline_values):
    """Compute the result given a list of values and baseline values.

    Args:
      cell: A cell data structure to populate.
      values: List of values.
      baseline_values: List of baseline values. Can be none if this is the
      baseline itself.
    """
    all_floats = True
    values = _StripNone(values)
    if not values:
      cell.value = ''
      return
    if _AllFloat(values):
      float_values = _GetFloats(values)
    else:
      all_floats = False
    if baseline_values:
      baseline_values = _StripNone(baseline_values)
    if baseline_values:
      if _AllFloat(baseline_values):
        float_baseline_values = _GetFloats(baseline_values)
      else:
        all_floats = False
    else:
      if self.NeedsBaseline():
        cell.value = ''
        return
      float_baseline_values = None
    if all_floats:
      self._ComputeFloat(cell, float_values, float_baseline_values)
      self._InvertIfLowerIsBetter(cell)
    else:
      self._ComputeString(cell, values, baseline_values)


class LiteralResult(Result):
  """A literal result."""

  def __init__(self, iteration=0):
    super(LiteralResult, self).__init__()
    self.iteration = iteration

  def Compute(self, cell, values, baseline_values):
    try:
      cell.value = values[self.iteration]
    except IndexError:
      cell.value = '-'


class NonEmptyCountResult(Result):
  """A class that counts the number of non-empty results.

  The number of non-empty values will be stored in the cell.
  """

  def Compute(self, cell, values, baseline_values):
    """Put the number of non-empty values in the cell result.

    Args:
      cell: Put the result in cell.value.
      values: A list of values for the row.
      baseline_values: A list of baseline values for the row.
    """
    cell.value = len(_StripNone(values))
    if not baseline_values:
      return
    base_value = len(_StripNone(baseline_values))
    if cell.value == base_value:
      return
    f = ColorBoxFormat()
    len_values = len(values)
    len_baseline_values = len(baseline_values)
    tmp_cell = Cell()
    tmp_cell.value = 1.0 + (float(cell.value - base_value) /
                            (max(len_values, len_baseline_values)))
    f.Compute(tmp_cell)
    cell.bgcolor = tmp_cell.bgcolor


class StringMeanResult(Result):
  """Mean of string values."""

  def _ComputeString(self, cell, values, baseline_values):
    if self._AllStringsSame(values):
      cell.value = str(values[0])
    else:
      cell.value = '?'


class AmeanResult(StringMeanResult):
  """Arithmetic mean."""

  def _ComputeFloat(self, cell, values, baseline_values):
    cell.value = numpy.mean(values)


class RawResult(Result):
  """Raw result."""
  pass


class MinResult(Result):
  """Minimum."""

  def _ComputeFloat(self, cell, values, baseline_values):
    cell.value = min(values)

  def _ComputeString(self, cell, values, baseline_values):
    if values:
      cell.value = min(values)
    else:
      cell.value = ''


class MaxResult(Result):
  """Maximum."""

  def _ComputeFloat(self, cell, values, baseline_values):
    cell.value = max(values)

  def _ComputeString(self, cell, values, baseline_values):
    if values:
      cell.value = max(values)
    else:
      cell.value = ''


class NumericalResult(Result):
  """Numerical result."""

  def _ComputeString(self, cell, values, baseline_values):
    cell.value = '?'


class StdResult(NumericalResult):
  """Standard deviation."""

  def _ComputeFloat(self, cell, values, baseline_values):
    cell.value = numpy.std(values)


class CoeffVarResult(NumericalResult):
  """Standard deviation / Mean"""

  def _ComputeFloat(self, cell, values, baseline_values):
    if numpy.mean(values) != 0.0:
      noise = numpy.abs(numpy.std(values) / numpy.mean(values))
    else:
      noise = 0.0
    cell.value = noise


class ComparisonResult(Result):
  """Same or Different."""

  def NeedsBaseline(self):
    return True

  def _ComputeString(self, cell, values, baseline_values):
    value = None
    baseline_value = None
    if self._AllStringsSame(values):
      value = values[0]
    if self._AllStringsSame(baseline_values):
      baseline_value = baseline_values[0]
    if value is not None and baseline_value is not None:
      if value == baseline_value:
        cell.value = 'SAME'
      else:
        cell.value = 'DIFFERENT'
    else:
      cell.value = '?'


class PValueResult(ComparisonResult):
  """P-value."""

  def _ComputeFloat(self, cell, values, baseline_values):
    if len(values) < 2 or len(baseline_values) < 2:
      cell.value = float('nan')
      return
    import stats
    _, cell.value = stats.lttest_ind(values, baseline_values)

  def _ComputeString(self, cell, values, baseline_values):
    return float('nan')


class KeyAwareComparisonResult(ComparisonResult):
  """Automatic key aware comparison."""

  def _IsLowerBetter(self, key):
    # TODO(llozano): Trying to guess direction by looking at the name of the
    # test does not seem like a good idea. Test frameworks should provide this
    # info explicitly. I believe Telemetry has this info. Need to find it out.
    #
    # Below are some test names for which we are not sure what the
    # direction is.
    #
    # For these we dont know what the direction is. But, since we dont
    # specify anything, crosperf will assume higher is better:
    # --percent_impl_scrolled--percent_impl_scrolled--percent
    # --solid_color_tiles_analyzed--solid_color_tiles_analyzed--count
    # --total_image_cache_hit_count--total_image_cache_hit_count--count
    # --total_texture_upload_time_by_url
    #
    # About these we are doubtful but we made a guess:
    # --average_num_missing_tiles_by_url--*--units (low is good)
    # --experimental_mean_frame_time_by_url--*--units (low is good)
    # --experimental_median_frame_time_by_url--*--units (low is good)
    # --texture_upload_count--texture_upload_count--count (high is good)
    # --total_deferred_image_decode_count--count (low is good)
    # --total_tiles_analyzed--total_tiles_analyzed--count (high is good)
    lower_is_better_keys = [
        'milliseconds', 'ms_', 'seconds_', 'KB', 'rdbytes', 'wrbytes',
        'dropped_percent', '(ms)', '(seconds)', '--ms',
        '--average_num_missing_tiles', '--experimental_jank',
        '--experimental_mean_frame', '--experimental_median_frame_time',
        '--total_deferred_image_decode_count', '--seconds'
    ]

    return any([l in key for l in lower_is_better_keys])

  def _InvertIfLowerIsBetter(self, cell):
    if self._IsLowerBetter(cell.name):
      if cell.value:
        cell.value = 1.0 / cell.value


class AmeanRatioResult(KeyAwareComparisonResult):
  """Ratio of arithmetic means of values vs. baseline values."""

  def _ComputeFloat(self, cell, values, baseline_values):
    if numpy.mean(baseline_values) != 0:
      cell.value = numpy.mean(values) / numpy.mean(baseline_values)
    elif numpy.mean(values) != 0:
      cell.value = 0.00
      # cell.value = 0 means the values and baseline_values have big difference
    else:
      cell.value = 1.00
      # no difference if both values and baseline_values are 0


class GmeanRatioResult(KeyAwareComparisonResult):
  """Ratio of geometric means of values vs. baseline values."""

  def _ComputeFloat(self, cell, values, baseline_values):
    if self._GetGmean(baseline_values) != 0:
      cell.value = self._GetGmean(values) / self._GetGmean(baseline_values)
    elif self._GetGmean(values) != 0:
      cell.value = 0.00
    else:
      cell.value = 1.00


class Color(object):
  """Class that represents color in RGBA format."""

  def __init__(self, r=0, g=0, b=0, a=0):
    self.r = r
    self.g = g
    self.b = b
    self.a = a

  def __str__(self):
    return 'r: %s g: %s: b: %s: a: %s' % (self.r, self.g, self.b, self.a)

  def Round(self):
    """Round RGBA values to the nearest integer."""
    self.r = int(self.r)
    self.g = int(self.g)
    self.b = int(self.b)
    self.a = int(self.a)

  def GetRGB(self):
    """Get a hex representation of the color."""
    return '%02x%02x%02x' % (self.r, self.g, self.b)

  @classmethod
  def Lerp(cls, ratio, a, b):
    """Perform linear interpolation between two colors.

    Args:
      ratio: The ratio to use for linear polation.
      a: The first color object (used when ratio is 0).
      b: The second color object (used when ratio is 1).

    Returns:
      Linearly interpolated color.
    """
    ret = cls()
    ret.r = (b.r - a.r) * ratio + a.r
    ret.g = (b.g - a.g) * ratio + a.g
    ret.b = (b.b - a.b) * ratio + a.b
    ret.a = (b.a - a.a) * ratio + a.a
    return ret


class Format(object):
  """A class that represents the format of a column."""

  def __init__(self):
    pass

  def Compute(self, cell):
    """Computes the attributes of a cell based on its value.

    Attributes typically are color, width, etc.

    Args:
      cell: The cell whose attributes are to be populated.
    """
    if cell.value is None:
      cell.string_value = ''
    if isinstance(cell.value, float):
      self._ComputeFloat(cell)
    else:
      self._ComputeString(cell)

  def _ComputeFloat(self, cell):
    cell.string_value = '{0:.2f}'.format(cell.value)

  def _ComputeString(self, cell):
    cell.string_value = str(cell.value)

  def _GetColor(self, value, low, mid, high, power=6, mid_value=1.0):
    min_value = 0.0
    max_value = 2.0
    if math.isnan(value):
      return mid
    if value > mid_value:
      value = max_value - mid_value / value

    return self._GetColorBetweenRange(value, min_value, mid_value, max_value,
                                      low, mid, high, power)

  def _GetColorBetweenRange(self, value, min_value, mid_value, max_value,
                            low_color, mid_color, high_color, power):
    assert value <= max_value
    assert value >= min_value
    if value > mid_value:
      value = (max_value - value) / (max_value - mid_value)
      value **= power
      ret = Color.Lerp(value, high_color, mid_color)
    else:
      value = (value - min_value) / (mid_value - min_value)
      value **= power
      ret = Color.Lerp(value, low_color, mid_color)
    ret.Round()
    return ret


class PValueFormat(Format):
  """Formatting for p-value."""

  def _ComputeFloat(self, cell):
    cell.string_value = '%0.2f' % float(cell.value)
    if float(cell.value) < 0.05:
      cell.bgcolor = self._GetColor(
          cell.value,
          Color(255, 255, 0, 0),
          Color(255, 255, 255, 0),
          Color(255, 255, 255, 0),
          mid_value=0.05,
          power=1)


class StorageFormat(Format):
  """Format the cell as a storage number.

  Example:
    If the cell contains a value of 1024, the string_value will be 1.0K.
  """

  def _ComputeFloat(self, cell):
    base = 1024
    suffices = ['K', 'M', 'G']
    v = float(cell.value)
    current = 0
    while v >= base**(current + 1) and current < len(suffices):
      current += 1

    if current:
      divisor = base**current
      cell.string_value = '%1.1f%s' % ((v / divisor), suffices[current - 1])
    else:
      cell.string_value = str(cell.value)


class CoeffVarFormat(Format):
  """Format the cell as a percent.

  Example:
    If the cell contains a value of 1.5, the string_value will be +150%.
  """

  def _ComputeFloat(self, cell):
    cell.string_value = '%1.1f%%' % (float(cell.value) * 100)
    cell.color = self._GetColor(
        cell.value,
        Color(0, 255, 0, 0),
        Color(0, 0, 0, 0),
        Color(255, 0, 0, 0),
        mid_value=0.02,
        power=1)


class PercentFormat(Format):
  """Format the cell as a percent.

  Example:
    If the cell contains a value of 1.5, the string_value will be +50%.
  """

  def _ComputeFloat(self, cell):
    cell.string_value = '%+1.1f%%' % ((float(cell.value) - 1) * 100)
    cell.color = self._GetColor(cell.value,
                                Color(255, 0, 0, 0),
                                Color(0, 0, 0, 0), Color(0, 255, 0, 0))


class RatioFormat(Format):
  """Format the cell as a ratio.

  Example:
    If the cell contains a value of 1.5642, the string_value will be 1.56.
  """

  def _ComputeFloat(self, cell):
    cell.string_value = '%+1.1f%%' % ((cell.value - 1) * 100)
    cell.color = self._GetColor(cell.value,
                                Color(255, 0, 0, 0),
                                Color(0, 0, 0, 0), Color(0, 255, 0, 0))


class ColorBoxFormat(Format):
  """Format the cell as a color box.

  Example:
    If the cell contains a value of 1.5, it will get a green color.
    If the cell contains a value of 0.5, it will get a red color.
    The intensity of the green/red will be determined by how much above or below
    1.0 the value is.
  """

  def _ComputeFloat(self, cell):
    cell.string_value = '--'
    bgcolor = self._GetColor(cell.value,
                             Color(255, 0, 0, 0),
                             Color(255, 255, 255, 0), Color(0, 255, 0, 0))
    cell.bgcolor = bgcolor
    cell.color = bgcolor


class Cell(object):
  """A class to represent a cell in a table.

  Attributes:
    value: The raw value of the cell.
    color: The color of the cell.
    bgcolor: The background color of the cell.
    string_value: The string value of the cell.
    suffix: A string suffix to be attached to the value when displaying.
    prefix: A string prefix to be attached to the value when displaying.
    color_row: Indicates whether the whole row is to inherit this cell's color.
    bgcolor_row: Indicates whether the whole row is to inherit this cell's
    bgcolor.
    width: Optional specifier to make a column narrower than the usual width.
    The usual width of a column is the max of all its cells widths.
    colspan: Set the colspan of the cell in the HTML table, this is used for
    table headers. Default value is 1.
    name: the test name of the cell.
    header: Whether this is a header in html.
  """

  def __init__(self):
    self.value = None
    self.color = None
    self.bgcolor = None
    self.string_value = None
    self.suffix = None
    self.prefix = None
    # Entire row inherits this color.
    self.color_row = False
    self.bgcolor_row = False
    self.width = None
    self.colspan = 1
    self.name = None
    self.header = False

  def __str__(self):
    l = []
    l.append('value: %s' % self.value)
    l.append('string_value: %s' % self.string_value)
    return ' '.join(l)


class Column(object):
  """Class representing a column in a table.

  Attributes:
    result: an object of the Result class.
    fmt: an object of the Format class.
  """

  def __init__(self, result, fmt, name=''):
    self.result = result
    self.fmt = fmt
    self.name = name


# Takes in:
# ["Key", "Label1", "Label2"]
# ["k", ["v", "v2"], [v3]]
# etc.
# Also takes in a format string.
# Returns a table like:
# ["Key", "Label1", "Label2"]
# ["k", avg("v", "v2"), stddev("v", "v2"), etc.]]
# according to format string
class TableFormatter(object):
  """Class to convert a plain table into a cell-table.

  This class takes in a table generated by TableGenerator and a list of column
  formats to apply to the table and returns a table of cells.
  """

  def __init__(self, table, columns):
    """The constructor takes in a table and a list of columns.

    Args:
      table: A list of lists of values.
      columns: A list of column containing what to produce and how to format it.
    """
    self._table = table
    self._columns = columns
    self._table_columns = []
    self._out_table = []

  def GenerateCellTable(self, table_type):
    row_index = 0
    all_failed = False

    for row in self._table[1:]:
      # It does not make sense to put retval in the summary table.
      if str(row[0]) == 'retval' and table_type == 'summary':
        # Check to see if any runs passed, and update all_failed.
        all_failed = True
        for values in row[1:]:
          if 0 in values:
            all_failed = False
        continue
      key = Cell()
      key.string_value = str(row[0])
      out_row = [key]
      baseline = None
      for values in row[1:]:
        for column in self._columns:
          cell = Cell()
          cell.name = key.string_value
          if column.result.NeedsBaseline():
            if baseline is not None:
              column.result.Compute(cell, values, baseline)
              column.fmt.Compute(cell)
              out_row.append(cell)
              if not row_index:
                self._table_columns.append(column)
          else:
            column.result.Compute(cell, values, baseline)
            column.fmt.Compute(cell)
            out_row.append(cell)
            if not row_index:
              self._table_columns.append(column)

        if baseline is None:
          baseline = values
      self._out_table.append(out_row)
      row_index += 1

    # If this is a summary table, and the only row in it is 'retval', and
    # all the test runs failed, we need to a 'Results' row to the output
    # table.
    if table_type == 'summary' and all_failed and len(self._table) == 2:
      labels_row = self._table[0]
      key = Cell()
      key.string_value = 'Results'
      out_row = [key]
      baseline = None
      for _ in labels_row[1:]:
        for column in self._columns:
          cell = Cell()
          cell.name = key.string_value
          column.result.Compute(cell, ['Fail'], baseline)
          column.fmt.Compute(cell)
          out_row.append(cell)
          if not row_index:
            self._table_columns.append(column)
      self._out_table.append(out_row)

  def AddColumnName(self):
    """Generate Column name at the top of table."""
    key = Cell()
    key.header = True
    key.string_value = 'Keys'
    header = [key]
    for column in self._table_columns:
      cell = Cell()
      cell.header = True
      if column.name:
        cell.string_value = column.name
      else:
        result_name = column.result.__class__.__name__
        format_name = column.fmt.__class__.__name__

        cell.string_value = '%s %s' % (result_name.replace('Result', ''),
                                       format_name.replace('Format', ''))

      header.append(cell)

    self._out_table = [header] + self._out_table

  def AddHeader(self, s):
    """Put additional string on the top of the table."""
    cell = Cell()
    cell.header = True
    cell.string_value = str(s)
    header = [cell]
    colspan = max(1, max(len(row) for row in self._table))
    cell.colspan = colspan
    self._out_table = [header] + self._out_table

  def GetPassesAndFails(self, values):
    passes = 0
    fails = 0
    for val in values:
      if val == 0:
        passes = passes + 1
      else:
        fails = fails + 1
    return passes, fails

  def AddLabelName(self):
    """Put label on the top of the table."""
    top_header = []
    base_colspan = len(
        [c for c in self._columns if not c.result.NeedsBaseline()])
    compare_colspan = len(self._columns)
    # Find the row with the key 'retval', if it exists.  This
    # will be used to calculate the number of iterations that passed and
    # failed for each image label.
    retval_row = None
    for row in self._table:
      if row[0] == 'retval':
        retval_row = row
    # The label is organized as follows
    # "keys" label_base, label_comparison1, label_comparison2
    # The first cell has colspan 1, the second is base_colspan
    # The others are compare_colspan
    column_position = 0
    for label in self._table[0]:
      cell = Cell()
      cell.header = True
      # Put the number of pass/fail iterations in the image label header.
      if column_position > 0 and retval_row:
        retval_values = retval_row[column_position]
        if type(retval_values) is list:
          passes, fails = self.GetPassesAndFails(retval_values)
          cell.string_value = str(label) + '  (pass:%d fail:%d)' % (passes,
                                                                    fails)
        else:
          cell.string_value = str(label)
      else:
        cell.string_value = str(label)
      if top_header:
        cell.colspan = base_colspan
      if len(top_header) > 1:
        cell.colspan = compare_colspan
      top_header.append(cell)
      column_position = column_position + 1
    self._out_table = [top_header] + self._out_table

  def _PrintOutTable(self):
    o = ''
    for row in self._out_table:
      for cell in row:
        o += str(cell) + ' '
      o += '\n'
    print(o)

  def GetCellTable(self, table_type='full', headers=True):
    """Function to return a table of cells.

    The table (list of lists) is converted into a table of cells by this
    function.

    Args:
      table_type: Can be 'full' or 'summary'
      headers: A boolean saying whether we want default headers

    Returns:
      A table of cells with each cell having the properties and string values as
      requiested by the columns passed in the constructor.
    """
    # Generate the cell table, creating a list of dynamic columns on the fly.
    if not self._out_table:
      self.GenerateCellTable(table_type)
    if headers:
      self.AddColumnName()
      self.AddLabelName()
    return self._out_table


class TablePrinter(object):
  """Class to print a cell table to the console, file or html."""
  PLAIN = 0
  CONSOLE = 1
  HTML = 2
  TSV = 3
  EMAIL = 4

  def __init__(self, table, output_type):
    """Constructor that stores the cell table and output type."""
    self._table = table
    self._output_type = output_type
    self._row_styles = []
    self._column_styles = []

  # Compute whole-table properties like max-size, etc.
  def _ComputeStyle(self):
    self._row_styles = []
    for row in self._table:
      row_style = Cell()
      for cell in row:
        if cell.color_row:
          assert cell.color, 'Cell color not set but color_row set!'
          assert not row_style.color, 'Multiple row_style.colors found!'
          row_style.color = cell.color
        if cell.bgcolor_row:
          assert cell.bgcolor, 'Cell bgcolor not set but bgcolor_row set!'
          assert not row_style.bgcolor, 'Multiple row_style.bgcolors found!'
          row_style.bgcolor = cell.bgcolor
      self._row_styles.append(row_style)

    self._column_styles = []
    if len(self._table) < 2:
      return

    for i in range(max(len(row) for row in self._table)):
      column_style = Cell()
      for row in self._table:
        if not any([cell.colspan != 1 for cell in row]):
          column_style.width = max(column_style.width, len(row[i].string_value))
      self._column_styles.append(column_style)

  def _GetBGColorFix(self, color):
    if self._output_type == self.CONSOLE:
      prefix = misc.rgb2short(color.r, color.g, color.b)
      # pylint: disable=anomalous-backslash-in-string
      prefix = '\033[48;5;%sm' % prefix
      suffix = '\033[0m'
    elif self._output_type in [self.EMAIL, self.HTML]:
      rgb = color.GetRGB()
      prefix = ("<FONT style=\"BACKGROUND-COLOR:#{0}\">".format(rgb))
      suffix = '</FONT>'
    elif self._output_type in [self.PLAIN, self.TSV]:
      prefix = ''
      suffix = ''
    return prefix, suffix

  def _GetColorFix(self, color):
    if self._output_type == self.CONSOLE:
      prefix = misc.rgb2short(color.r, color.g, color.b)
      # pylint: disable=anomalous-backslash-in-string
      prefix = '\033[38;5;%sm' % prefix
      suffix = '\033[0m'
    elif self._output_type in [self.EMAIL, self.HTML]:
      rgb = color.GetRGB()
      prefix = '<FONT COLOR=#{0}>'.format(rgb)
      suffix = '</FONT>'
    elif self._output_type in [self.PLAIN, self.TSV]:
      prefix = ''
      suffix = ''
    return prefix, suffix

  def Print(self):
    """Print the table to a console, html, etc.

    Returns:
      A string that contains the desired representation of the table.
    """
    self._ComputeStyle()
    return self._GetStringValue()

  def _GetCellValue(self, i, j):
    cell = self._table[i][j]
    out = cell.string_value
    raw_width = len(out)

    if cell.color:
      p, s = self._GetColorFix(cell.color)
      out = '%s%s%s' % (p, out, s)

    if cell.bgcolor:
      p, s = self._GetBGColorFix(cell.bgcolor)
      out = '%s%s%s' % (p, out, s)

    if self._output_type in [self.PLAIN, self.CONSOLE, self.EMAIL]:
      if cell.width:
        width = cell.width
      else:
        if self._column_styles:
          width = self._column_styles[j].width
        else:
          width = len(cell.string_value)
      if cell.colspan > 1:
        width = 0
        start = 0
        for k in range(j):
          start += self._table[i][k].colspan
        for k in range(cell.colspan):
          width += self._column_styles[start + k].width
      if width > raw_width:
        padding = ('%' + str(width - raw_width) + 's') % ''
        out = padding + out

    if self._output_type == self.HTML:
      if cell.header:
        tag = 'th'
      else:
        tag = 'td'
      out = "<{0} colspan = \"{2}\"> {1} </{0}>".format(tag, out, cell.colspan)

    return out

  def _GetHorizontalSeparator(self):
    if self._output_type in [self.CONSOLE, self.PLAIN, self.EMAIL]:
      return ' '
    if self._output_type == self.HTML:
      return ''
    if self._output_type == self.TSV:
      return '\t'

  def _GetVerticalSeparator(self):
    if self._output_type in [self.PLAIN, self.CONSOLE, self.TSV, self.EMAIL]:
      return '\n'
    if self._output_type == self.HTML:
      return '</tr>\n<tr>'

  def _GetPrefix(self):
    if self._output_type in [self.PLAIN, self.CONSOLE, self.TSV, self.EMAIL]:
      return ''
    if self._output_type == self.HTML:
      return "<p></p><table id=\"box-table-a\">\n<tr>"

  def _GetSuffix(self):
    if self._output_type in [self.PLAIN, self.CONSOLE, self.TSV, self.EMAIL]:
      return ''
    if self._output_type == self.HTML:
      return '</tr>\n</table>'

  def _GetStringValue(self):
    o = ''
    o += self._GetPrefix()
    for i in range(len(self._table)):
      row = self._table[i]
      # Apply row color and bgcolor.
      p = s = bgp = bgs = ''
      if self._row_styles[i].bgcolor:
        bgp, bgs = self._GetBGColorFix(self._row_styles[i].bgcolor)
      if self._row_styles[i].color:
        p, s = self._GetColorFix(self._row_styles[i].color)
      o += p + bgp
      for j in range(len(row)):
        out = self._GetCellValue(i, j)
        o += out + self._GetHorizontalSeparator()
      o += s + bgs
      o += self._GetVerticalSeparator()
    o += self._GetSuffix()
    return o


# Some common drivers
def GetSimpleTable(table, out_to=TablePrinter.CONSOLE):
  """Prints a simple table.

  This is used by code that has a very simple list-of-lists and wants to produce
  a table with ameans, a percentage ratio of ameans and a colorbox.

  Args:
    table: a list of lists.
    out_to: specify the fomat of output. Currently it supports HTML and CONSOLE.

  Returns:
    A string version of the table that can be printed to the console.

  Example:
    GetSimpleConsoleTable([["binary", "b1", "b2"],["size", "300", "400"]])
    will produce a colored table that can be printed to the console.
  """
  columns = [
      Column(AmeanResult(), Format()),
      Column(AmeanRatioResult(), PercentFormat()),
      Column(AmeanRatioResult(), ColorBoxFormat()),
  ]
  our_table = [table[0]]
  for row in table[1:]:
    our_row = [row[0]]
    for v in row[1:]:
      our_row.append([v])
    our_table.append(our_row)

  tf = TableFormatter(our_table, columns)
  cell_table = tf.GetCellTable()
  tp = TablePrinter(cell_table, out_to)
  return tp.Print()


# pylint: disable=redefined-outer-name
def GetComplexTable(runs, labels, out_to=TablePrinter.CONSOLE):
  """Prints a complex table.

  This can be used to generate a table with arithmetic mean, standard deviation,
  coefficient of variation, p-values, etc.

  Args:
    runs: A list of lists with data to tabulate.
    labels: A list of labels that correspond to the runs.
    out_to: specifies the format of the table (example CONSOLE or HTML).

  Returns:
    A string table that can be printed to the console or put in an HTML file.
  """
  tg = TableGenerator(runs, labels, TableGenerator.SORT_BY_VALUES_DESC)
  table = tg.GetTable()
  columns = [
      Column(LiteralResult(), Format(), 'Literal'), Column(
          AmeanResult(), Format()), Column(StdResult(), Format()), Column(
              CoeffVarResult(), CoeffVarFormat()), Column(
                  NonEmptyCountResult(), Format()),
      Column(AmeanRatioResult(), PercentFormat()), Column(
          AmeanRatioResult(), RatioFormat()), Column(GmeanRatioResult(),
                                                     RatioFormat()), Column(
                                                         PValueResult(),
                                                         PValueFormat())
  ]
  tf = TableFormatter(table, columns)
  cell_table = tf.GetCellTable()
  tp = TablePrinter(cell_table, out_to)
  return tp.Print()


if __name__ == '__main__':
  # Run a few small tests here.
  runs = [[{
      'k1': '10',
      'k2': '12',
      'k5': '40',
      'k6': '40',
      'ms_1': '20',
      'k7': 'FAIL',
      'k8': 'PASS',
      'k9': 'PASS',
      'k10': '0'
  }, {
      'k1': '13',
      'k2': '14',
      'k3': '15',
      'ms_1': '10',
      'k8': 'PASS',
      'k9': 'FAIL',
      'k10': '0'
  }], [{
      'k1': '50',
      'k2': '51',
      'k3': '52',
      'k4': '53',
      'k5': '35',
      'k6': '45',
      'ms_1': '200',
      'ms_2': '20',
      'k7': 'FAIL',
      'k8': 'PASS',
      'k9': 'PASS'
  }]]
  labels = ['vanilla', 'modified']
  t = GetComplexTable(runs, labels, TablePrinter.CONSOLE)
  print(t)
  email = GetComplexTable(runs, labels, TablePrinter.EMAIL)

  runs = [[{
      'k1': '1'
  }, {
      'k1': '1.1'
  }, {
      'k1': '1.2'
  }], [{
      'k1': '5'
  }, {
      'k1': '5.1'
  }, {
      'k1': '5.2'
  }]]
  t = GetComplexTable(runs, labels, TablePrinter.CONSOLE)
  print(t)

  simple_table = [
      ['binary', 'b1', 'b2', 'b3'],
      ['size', 100, 105, 108],
      ['rodata', 100, 80, 70],
      ['data', 100, 100, 100],
      ['debug', 100, 140, 60],
  ]
  t = GetSimpleTable(simple_table)
  print(t)
  email += GetSimpleTable(simple_table, TablePrinter.HTML)
  email_to = [getpass.getuser()]
  email = "<pre style='font-size: 13px'>%s</pre>" % email
  EmailSender().SendEmail(email_to, 'SimpleTableTest', email, msg_type='html')