1# Copyright (c) 2012 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 5"""A module providing the summary for multiple test results. 6 7This firmware_summary module is used to collect the test results of 8multiple rounds from the logs generated by different firmware versions. 9The test results of the various validators of every gesture are displayed. 10In addition, the test results of every validator across all gestures are 11also summarized. 12 13Usage: 14$ python firmware_summary log_directory 15 16 17A typical summary output looks like 18 19Test Summary (by gesture) : fw_2.41 fw_2.42 count 20--------------------------------------------------------------------- 21one_finger_tracking 22 CountTrackingIDValidator : 1.00 0.90 12 23 LinearityBothEndsValidator : 0.97 0.89 12 24 LinearityMiddleValidator : 1.00 1.00 12 25 NoGapValidator : 0.74 0.24 12 26 NoReversedMotionBothEndsValidator : 0.68 0.34 12 27 NoReversedMotionMiddleValidator : 1.00 1.00 12 28 ReportRateValidator : 1.00 1.00 12 29one_finger_to_edge 30 CountTrackingIDValidator : 1.00 1.00 4 31 LinearityBothEndsValidator : 0.88 0.89 4 32 LinearityMiddleValidator : 1.00 1.00 4 33 NoGapValidator : 0.50 0.00 4 34 NoReversedMotionMiddleValidator : 1.00 1.00 4 35 RangeValidator : 1.00 1.00 4 36 37 ... 38 39 40Test Summary (by validator) : fw_2.4 fw_2.4.a count 41--------------------------------------------------------------------- 42 CountPacketsValidator : 1.00 0.82 6 43 CountTrackingIDValidator : 0.92 0.88 84 44 45 ... 46 47""" 48 49 50import getopt 51import os 52import sys 53 54import firmware_log 55import test_conf as conf 56 57from collections import defaultdict 58 59from common_util import print_and_exit 60from firmware_constants import OPTIONS 61from test_conf import (log_root_dir, merged_validators, segment_weights, 62 validator_weights) 63from validators import BaseValidator, get_parent_validators 64 65 66class OptionsDisplayMetrics: 67 """The options of displaying metrics.""" 68 # Defining the options of displaying metrics 69 HIDE_SOME_METRICS_STATS = '0' 70 DISPLAY_ALL_METRICS_STATS = '1' 71 DISPLAY_ALL_METRICS_WITH_RAW_VALUES = '2' 72 DISPLAY_METRICS_OPTIONS = [HIDE_SOME_METRICS_STATS, 73 DISPLAY_ALL_METRICS_STATS, 74 DISPLAY_ALL_METRICS_WITH_RAW_VALUES] 75 DISPLAY_METRICS_DEFAULT = DISPLAY_ALL_METRICS_WITH_RAW_VALUES 76 77 def __init__(self, option): 78 """Initialize with the level value. 79 80 @param option: the option of display metrics 81 """ 82 if option not in self.DISPLAY_METRICS_OPTIONS: 83 option = self.DISPLAY_METRICS_DEFAULT 84 85 # To display all metrics statistics grouped by validators? 86 self.display_all_stats = ( 87 option == self.DISPLAY_ALL_METRICS_STATS or 88 option == self.DISPLAY_ALL_METRICS_WITH_RAW_VALUES) 89 90 # To display the raw metrics values in details on file basis? 91 self.display_raw_values = ( 92 option == self.DISPLAY_ALL_METRICS_WITH_RAW_VALUES) 93 94 95class FirmwareSummary: 96 """Summary for touch device firmware tests.""" 97 98 def __init__(self, log_dir, display_metrics=False, debug_flag=False, 99 display_scores=False, individual_round_flag=False, 100 segment_weights=segment_weights, 101 validator_weights=validator_weights): 102 """ segment_weights and validator_weights are passed as arguments 103 so that it is possible to assign arbitrary weights in unit tests. 104 """ 105 if os.path.isdir(log_dir): 106 self.log_dir = log_dir 107 else: 108 error_msg = 'Error: The test result directory does not exist: %s' 109 print error_msg % log_dir 110 sys.exit(1) 111 112 self.display_metrics = display_metrics 113 self.display_scores = display_scores 114 self.slog = firmware_log.SummaryLog(log_dir, 115 segment_weights, 116 validator_weights, 117 individual_round_flag, 118 debug_flag) 119 120 def _print_summary_title(self, summary_title_str): 121 """Print the summary of the test results by gesture.""" 122 # Create a flexible column title format according to the number of 123 # firmware versions which could be 1, 2, or more. 124 # 125 # A typical summary title looks like 126 # Test Summary () : fw_11.26 fw_11.23 127 # mean ssd count mean ssd count 128 # ---------------------------------------------------------------------- 129 # 130 # The 1st line above is called title_fw. 131 # The 2nd line above is called title_statistics. 132 # 133 # As an example for 2 firmwares, title_fw_format looks like: 134 # '{0:<37}: {1:>12} {2:>21}' 135 title_fw_format_list = ['{0:<37}:',] 136 for i in range(len(self.slog.fws)): 137 format_space = 12 if i == 0 else (12 + 9) 138 title_fw_format_list.append('{%d:>%d}' % (i + 1, format_space)) 139 title_fw_format = ' '.join(title_fw_format_list) 140 141 # As an example for 2 firmwares, title_statistics_format looks like: 142 # '{0:>47} {1:>6} {2:>5} {3:>8} {4:>6} {5:>5}' 143 title_statistics_format_list = [] 144 for i in range(len(self.slog.fws)): 145 format_space = (12 + 35) if i == 0 else 8 146 title_statistics_format_list.append('{%d:>%d}' % (3 * i, 147 format_space)) 148 title_statistics_format_list.append('{%d:>%d}' % (3 * i + 1 , 6)) 149 title_statistics_format_list.append('{%d:>%d}' % (3 * i + 2 , 5)) 150 title_statistics_format = ' '.join(title_statistics_format_list) 151 152 # Create title_fw_list 153 # As an example for two firmware versions, it looks like 154 # ['Test Summary (by gesture)', 'fw_2.4', 'fw_2.5'] 155 title_fw_list = [summary_title_str,] + self.slog.fws 156 157 # Create title_statistics_list 158 # As an example for two firmware versions, it looks like 159 # ['mean', 'ssd', 'count', 'mean', 'ssd', 'count', ] 160 title_statistics_list = ['mean', 'ssd', 'count'] * len(self.slog.fws) 161 162 # Print the title. 163 title_fw = title_fw_format.format(*title_fw_list) 164 title_statistics = title_statistics_format.format( 165 *title_statistics_list) 166 print '\n\n', title_fw 167 print title_statistics 168 print '-' * len(title_statistics) 169 170 def _print_result_stats(self, gesture=None): 171 """Print the result statistics of validators.""" 172 for validator in self.slog.validators: 173 stat_scores_data = [] 174 statistics_format_list = [] 175 for fw in self.slog.fws: 176 result = self.slog.get_result(fw=fw, gesture=gesture, 177 validators=validator) 178 scores_data = result.stat_scores.all_data 179 if scores_data: 180 stat_scores_data += scores_data 181 statistics_format_list.append('{:>8.2f} {:>6.2f} {:>5}') 182 else: 183 stat_scores_data.append('') 184 statistics_format_list.append('{:>21}') 185 186 # Print the score statistics of all firmwares on the same row. 187 if any(stat_scores_data): 188 stat_scores_data.insert(0, validator) 189 statistics_format_list.insert(0,' {:<35}:') 190 statistics_format = ' '.join(statistics_format_list) 191 print statistics_format.format(*tuple(stat_scores_data)) 192 193 def _print_result_stats_by_gesture(self): 194 """Print the summary of the test results by gesture.""" 195 self._print_summary_title('Test Summary (by gesture)') 196 for gesture in self.slog.gestures: 197 print gesture 198 self._print_result_stats(gesture=gesture) 199 200 def _print_result_stats_by_validator(self): 201 """Print the summary of the test results by validator. The validator 202 results of all gestures are combined to compute the statistics. 203 """ 204 self._print_summary_title('Test Summary (by validator)') 205 self._print_result_stats() 206 207 def _get_metric_name_for_display(self, metric_name): 208 """Get the metric name for display. 209 We would like to shorten the metric name when displayed. 210 211 @param metric_name: a metric name 212 """ 213 return metric_name.split('--')[0] 214 215 def _get_merged_validators(self): 216 merged = defaultdict(list) 217 for validator_name in self.slog.validators: 218 parents = get_parent_validators(validator_name) 219 for parent in parents: 220 if parent in merged_validators: 221 merged[parent].append(validator_name) 222 break 223 else: 224 merged[validator_name] = [validator_name,] 225 return sorted(merged.values()) 226 227 def _print_statistics_of_metrics(self, detailed=True, gesture=None): 228 """Print the statistics of metrics by gesture or by validator. 229 230 @param gesture: print the statistics grouped by gesture 231 if this argument is specified; otherwise, by validator. 232 @param detailed: print statistics for all derived validators if True; 233 otherwise, print the merged statistics, e.g., 234 both StationaryFingerValidator and StationaryTapValidator 235 are merged into StationaryValidator. 236 """ 237 # Print the complete title which looks like: 238 # <title_str> <fw1> <fw2> ... <description> 239 fws = self.slog.fws 240 num_fws = len(fws) 241 fws_str_max_width = max(map(len, fws)) 242 fws_str_width = max(fws_str_max_width + 1, 10) 243 table_name = ('Detailed table (for debugging)' if detailed else 244 'Summary table') 245 title_str = ('Metrics statistics by gesture: ' + gesture if gesture else 246 'Metrics statistics by validator') 247 description_str = 'description (lower is better)' 248 fw_format = '{:>%d}' % fws_str_width 249 complete_title = ('{:<37}: '.format(title_str) + 250 (fw_format * num_fws).format(*fws) + 251 ' {:<40}'.format(description_str)) 252 print '\n' * 2 253 print table_name 254 print complete_title 255 print '-' * len(complete_title) 256 257 # Print the metric name and the metric stats values of every firmwares 258 name_format = ' ' * 6 + '{:<31}:' 259 description_format = ' {:<40}' 260 float_format = '{:>%d.2f}' % fws_str_width 261 blank_format = '{:>%d}' % fws_str_width 262 263 validators = (self.slog.validators if detailed else 264 self._get_merged_validators()) 265 266 for validator in validators: 267 fw_stats_values = defaultdict(dict) 268 for fw in fws: 269 result = self.slog.get_result(fw=fw, gesture=gesture, 270 validators=validator) 271 stat_metrics = result.stat_metrics 272 273 for metric_name in stat_metrics.metrics_values: 274 fw_stats_values[metric_name][fw] = \ 275 stat_metrics.stats_values[metric_name] 276 277 fw_stats_values_printed = False 278 for metric_name, fw_values_dict in sorted(fw_stats_values.items()): 279 values = [] 280 values_format = '' 281 for fw in fws: 282 value = fw_values_dict.get(fw, '') 283 values.append(value) 284 values_format += float_format if value else blank_format 285 286 # The metrics of some special validators will not be shown 287 # unless the display_all_stats flag is True or any stats values 288 # are non-zero. 289 if (validator not in conf.validators_hidden_when_no_failures or 290 self.display_metrics.display_all_stats or any(values)): 291 if not fw_stats_values_printed: 292 fw_stats_values_printed = True 293 if isinstance(validator, list): 294 print (' ' + ' {}' * len(validator)).format(*validator) 295 else: 296 print ' ' + validator 297 disp_name = self._get_metric_name_for_display(metric_name) 298 print name_format.format(disp_name), 299 print values_format.format(*values), 300 print description_format.format( 301 stat_metrics.metrics_props[metric_name].description) 302 303 def _print_raw_metrics_values(self): 304 """Print the raw metrics values.""" 305 # The subkey() below extracts (gesture, variation, round) from 306 # metric.key which is (fw, round, gesture, variation, validator) 307 subkey = lambda key: (key[2], key[3], key[1]) 308 309 # The sum_len() below is used to calculate the sum of the length 310 # of the elements in the subkey. 311 sum_len = lambda lst: sum([len(str(l)) if l else 0 for l in lst]) 312 313 mnprops = firmware_log.MetricNameProps() 314 print '\n\nRaw metrics values' 315 print '-' * 80 316 for fw in self.slog.fws: 317 print '\n', fw 318 for validator in self.slog.validators: 319 result = self.slog.get_result(fw=fw, validators=validator) 320 metrics_dict = result.stat_metrics.metrics_dict 321 if metrics_dict: 322 print '\n' + ' ' * 3 + validator 323 for metric_name, metrics in sorted(metrics_dict.items()): 324 disp_name = self._get_metric_name_for_display(metric_name) 325 print ' ' * 6 + disp_name 326 327 metric_note = mnprops.metrics_props[metric_name].note 328 if metric_note: 329 msg = '** Note: value below represents ' 330 print ' ' * 9 + msg + metric_note 331 332 # Make a metric value list sorted by 333 # (gesture, variation, round) 334 value_list = sorted([(subkey(metric.key), metric.value) 335 for metric in metrics]) 336 337 max_len = max([sum_len(value[0]) for value in value_list]) 338 template_prefix = ' ' * 9 + '{:<%d}: ' % (max_len + 5) 339 for (gesture, variation, round), value in value_list: 340 template = template_prefix + ( 341 '{}' if isinstance(value, tuple) else '{:.2f}') 342 gvr_str = '%s.%s (%s)' % (gesture, variation, round) 343 print template.format(gvr_str, value) 344 345 def _print_final_weighted_averages(self): 346 """Print the final weighted averages of all validators.""" 347 title_str = 'Test Summary (final weighted averages)' 348 print '\n\n' + title_str 349 print '-' * len(title_str) 350 weighted_average = self.slog.get_final_weighted_average() 351 for fw in self.slog.fws: 352 print '%s: %4.3f' % (fw, weighted_average[fw]) 353 354 def print_result_summary(self): 355 """Print the summary of the test results.""" 356 print self.slog.test_version 357 if self.display_metrics: 358 self._print_statistics_of_metrics(detailed=False) 359 self._print_statistics_of_metrics(detailed=True) 360 if self.display_metrics.display_raw_values: 361 self._print_raw_metrics_values() 362 if self.display_scores: 363 self._print_result_stats_by_gesture() 364 self._print_result_stats_by_validator() 365 self._print_final_weighted_averages() 366 367 368def _usage_and_exit(): 369 """Print the usage message and exit.""" 370 prog = sys.argv[0] 371 print 'Usage: $ python %s [options]\n' % prog 372 print 'options:' 373 print ' -D, --%s' % OPTIONS.DEBUG 374 print ' enable debug flag' 375 print ' -d, --%s <directory>' % OPTIONS.DIR 376 print ' specify which log directory to derive the summary' 377 print ' -h, --%s' % OPTIONS.HELP 378 print ' show this help' 379 print ' -i, --%s' % OPTIONS.INDIVIDUAL 380 print ' Calculate statistics of every individual round separately' 381 print ' -m, --%s <verbose_level>' % OPTIONS.METRICS 382 print ' display the summary metrics.' 383 print ' verbose_level:' 384 print ' 0: hide some metrics statistics if they passed' 385 print ' 1: display all metrics statistics' 386 print ' 2: display all metrics statistics and ' \ 387 'the detailed raw metrics values (default)' 388 print ' -s, --%s' % OPTIONS.SCORES 389 print ' display the scores (0.0 ~ 1.0)' 390 print 391 print 'Examples:' 392 print ' Specify the log root directory.' 393 print ' $ python %s -d /tmp' % prog 394 print ' Hide some metrics statistics.' 395 print ' $ python %s -m 0' % prog 396 print ' Display all metrics statistics.' 397 print ' $ python %s -m 1' % prog 398 print ' Display all metrics statistics with detailed raw metrics values.' 399 print ' $ python %s # or' % prog 400 print ' $ python %s -m 2' % prog 401 sys.exit(1) 402 403 404def _parsing_error(msg): 405 """Print the usage and exit when encountering parsing error.""" 406 print 'Error: %s' % msg 407 _usage_and_exit() 408 409 410def _parse_options(): 411 """Parse the options.""" 412 # Set the default values of options. 413 options = {OPTIONS.DEBUG: False, 414 OPTIONS.DIR: log_root_dir, 415 OPTIONS.INDIVIDUAL: False, 416 OPTIONS.METRICS: OptionsDisplayMetrics(None), 417 OPTIONS.SCORES: False, 418 } 419 420 try: 421 short_opt = 'Dd:him:s' 422 long_opt = [OPTIONS.DEBUG, 423 OPTIONS.DIR + '=', 424 OPTIONS.HELP, 425 OPTIONS.INDIVIDUAL, 426 OPTIONS.METRICS + '=', 427 OPTIONS.SCORES, 428 ] 429 opts, args = getopt.getopt(sys.argv[1:], short_opt, long_opt) 430 except getopt.GetoptError, err: 431 _parsing_error(str(err)) 432 433 for opt, arg in opts: 434 if opt in ('-h', '--%s' % OPTIONS.HELP): 435 _usage_and_exit() 436 elif opt in ('-D', '--%s' % OPTIONS.DEBUG): 437 options[OPTIONS.DEBUG] = True 438 elif opt in ('-d', '--%s' % OPTIONS.DIR): 439 options[OPTIONS.DIR] = arg 440 if not os.path.isdir(arg): 441 print 'Error: the log directory %s does not exist.' % arg 442 _usage_and_exit() 443 elif opt in ('-i', '--%s' % OPTIONS.INDIVIDUAL): 444 options[OPTIONS.INDIVIDUAL] = True 445 elif opt in ('-m', '--%s' % OPTIONS.METRICS): 446 options[OPTIONS.METRICS] = OptionsDisplayMetrics(arg) 447 elif opt in ('-s', '--%s' % OPTIONS.SCORES): 448 options[OPTIONS.SCORES] = True 449 else: 450 msg = 'This option "%s" is not supported.' % opt 451 _parsing_error(opt) 452 453 return options 454 455 456if __name__ == '__main__': 457 options = _parse_options() 458 summary = FirmwareSummary(options[OPTIONS.DIR], 459 display_metrics=options[OPTIONS.METRICS], 460 individual_round_flag=options[OPTIONS.INDIVIDUAL], 461 display_scores=options[OPTIONS.SCORES], 462 debug_flag=options[OPTIONS.DEBUG]) 463 summary.print_result_summary() 464