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