| # Copyright 2017 The PDFium Authors. All rights reserved. | 
 | # Use of this source code is governed by a BSD-style license that can be | 
 | # found in the LICENSE file. | 
 | """Classes that draw conclusions out of a comparison and represent them.""" | 
 |  | 
 | from collections import Counter | 
 |  | 
 | FORMAT_RED = '\033[01;31m{0}\033[00m' | 
 | FORMAT_GREEN = '\033[01;32m{0}\033[00m' | 
 | FORMAT_MAGENTA = '\033[01;35m{0}\033[00m' | 
 | FORMAT_CYAN = '\033[01;36m{0}\033[00m' | 
 | FORMAT_NORMAL = '{0}' | 
 |  | 
 | RATING_FAILURE = 'failure' | 
 | RATING_REGRESSION = 'regression' | 
 | RATING_IMPROVEMENT = 'improvement' | 
 | RATING_NO_CHANGE = 'no_change' | 
 | RATING_SMALL_CHANGE = 'small_change' | 
 |  | 
 | RATINGS = [ | 
 |     RATING_FAILURE, RATING_REGRESSION, RATING_IMPROVEMENT, RATING_NO_CHANGE, | 
 |     RATING_SMALL_CHANGE | 
 | ] | 
 |  | 
 | RATING_TO_COLOR = { | 
 |     RATING_FAILURE: FORMAT_MAGENTA, | 
 |     RATING_REGRESSION: FORMAT_RED, | 
 |     RATING_IMPROVEMENT: FORMAT_CYAN, | 
 |     RATING_NO_CHANGE: FORMAT_GREEN, | 
 |     RATING_SMALL_CHANGE: FORMAT_NORMAL, | 
 | } | 
 |  | 
 |  | 
 | class ComparisonConclusions(object): | 
 |   """All conclusions drawn from a comparison. | 
 |  | 
 |   This is initialized empty and then processes pairs of results for each test | 
 |   case, determining the rating for that case, which can be: | 
 |   "failure" if either or both runs for the case failed. | 
 |   "regression" if there is a significant increase in time for the test case. | 
 |   "improvement" if there is a significant decrease in time for the test case. | 
 |   "no_change" if the time for the test case did not change at all. | 
 |   "small_change" if the time for the test case changed but within the threshold. | 
 |   """ | 
 |  | 
 |   def __init__(self, threshold_significant): | 
 |     """Initializes an empty ComparisonConclusions. | 
 |  | 
 |     Args: | 
 |       threshold_significant: Float with the tolerance beyond which changes in | 
 |           measurements are considered significant. | 
 |  | 
 |           The change is considered as a multiplication rather than an addition | 
 |           of a fraction of the previous measurement, that is, a | 
 |           threshold_significant of 1.0 will flag test cases that became over | 
 |           100% slower (> 200% of the previous time measured) or over 100% faster | 
 |           (< 50% of the previous time measured). | 
 |  | 
 |           threshold_significant 0.02 -> 98.04% to 102% is not significant | 
 |           threshold_significant 0.1 -> 90.9% to 110% is not significant | 
 |           threshold_significant 0.25 -> 80% to 125% is not significant | 
 |           threshold_significant 1 -> 50% to 200% is not significant | 
 |           threshold_significant 4 -> 20% to 500% is not significant | 
 |  | 
 |     """ | 
 |     self.threshold_significant = threshold_significant | 
 |     self.threshold_significant_negative = (1 / (1 + threshold_significant)) - 1 | 
 |  | 
 |     self.params = {'threshold': threshold_significant} | 
 |     self.summary = ComparisonSummary() | 
 |     self.case_results = {} | 
 |  | 
 |   def ProcessCase(self, case_name, before, after): | 
 |     """Feeds a test case results to the ComparisonConclusions. | 
 |  | 
 |     Args: | 
 |       case_name: String identifying the case. | 
 |       before: Measurement for the "before" version of the code. | 
 |       after: Measurement for the "after" version of the code. | 
 |     """ | 
 |  | 
 |     # Switch 0 to None to simplify the json dict output. All zeros are | 
 |     # considered failed runs, so they will be represented by "null". | 
 |     if not before: | 
 |       before = None | 
 |     if not after: | 
 |       after = None | 
 |  | 
 |     if not before or not after: | 
 |       ratio = None | 
 |       rating = RATING_FAILURE | 
 |     else: | 
 |       ratio = (float(after) / before) - 1.0 | 
 |       if ratio > self.threshold_significant: | 
 |         rating = RATING_REGRESSION | 
 |       elif ratio < self.threshold_significant_negative: | 
 |         rating = RATING_IMPROVEMENT | 
 |       elif ratio == 0: | 
 |         rating = RATING_NO_CHANGE | 
 |       else: | 
 |         rating = RATING_SMALL_CHANGE | 
 |  | 
 |     case_result = CaseResult(case_name, before, after, ratio, rating) | 
 |  | 
 |     self.summary.ProcessCaseResult(case_result) | 
 |     self.case_results[case_name] = case_result | 
 |  | 
 |   def GetSummary(self): | 
 |     """Gets the ComparisonSummary with consolidated totals.""" | 
 |     return self.summary | 
 |  | 
 |   def GetCaseResults(self): | 
 |     """Gets a dict mapping each test case identifier to its CaseResult.""" | 
 |     return self.case_results | 
 |  | 
 |   def GetOutputDict(self): | 
 |     """Returns a conclusions dict with all the conclusions drawn. | 
 |  | 
 |     Returns: | 
 |       A serializable dict with the format illustrated below: | 
 |       { | 
 |         "version": 1, | 
 |         "params": { | 
 |           "threshold": 0.02 | 
 |         }, | 
 |         "summary": { | 
 |           "total": 123, | 
 |           "failure": 1, | 
 |           "regression": 2, | 
 |           "improvement": 1, | 
 |           "no_change": 100, | 
 |           "small_change": 19 | 
 |         }, | 
 |         "comparison_by_case": { | 
 |           "testing/resources/new_test.pdf": { | 
 |             "before": None, | 
 |             "after": 1000, | 
 |             "ratio": None, | 
 |             "rating": "failure" | 
 |           }, | 
 |           "testing/resources/test1.pdf": { | 
 |             "before": 100, | 
 |             "after": 120, | 
 |             "ratio": 0.2, | 
 |             "rating": "regression" | 
 |           }, | 
 |           "testing/resources/test2.pdf": { | 
 |             "before": 100, | 
 |             "after": 2000, | 
 |             "ratio": 19.0, | 
 |             "rating": "regression" | 
 |           }, | 
 |           "testing/resources/test3.pdf": { | 
 |             "before": 1000, | 
 |             "after": 1005, | 
 |             "ratio": 0.005, | 
 |             "rating": "small_change" | 
 |           }, | 
 |           "testing/resources/test4.pdf": { | 
 |             "before": 1000, | 
 |             "after": 1000, | 
 |             "ratio": 0.0, | 
 |             "rating": "no_change" | 
 |           }, | 
 |           "testing/resources/test5.pdf": { | 
 |             "before": 1000, | 
 |             "after": 600, | 
 |             "ratio": -0.4, | 
 |             "rating": "improvement" | 
 |           } | 
 |         } | 
 |       } | 
 |     """ | 
 |     output_dict = {} | 
 |     output_dict['version'] = 1 | 
 |     output_dict['params'] = {'threshold': self.threshold_significant} | 
 |     output_dict['summary'] = self.summary.GetOutputDict() | 
 |     output_dict['comparison_by_case'] = { | 
 |         cr.case_name.decode('utf-8'): cr.GetOutputDict() | 
 |         for cr in self.GetCaseResults().values() | 
 |     } | 
 |     return output_dict | 
 |  | 
 |  | 
 | class ComparisonSummary(object): | 
 |   """Totals computed for a comparison.""" | 
 |  | 
 |   def __init__(self): | 
 |     self.rating_counter = Counter() | 
 |  | 
 |   def ProcessCaseResult(self, case_result): | 
 |     self.rating_counter[case_result.rating] += 1 | 
 |  | 
 |   def GetTotal(self): | 
 |     """Gets the number of test cases processed.""" | 
 |     return sum(self.rating_counter.values()) | 
 |  | 
 |   def GetCount(self, rating): | 
 |     """Gets the number of test cases processed with a given rating.""" | 
 |     return self.rating_counter[rating] | 
 |  | 
 |   def GetOutputDict(self): | 
 |     """Returns a dict that can be serialized with all the totals.""" | 
 |     result = {'total': self.GetTotal()} | 
 |     for rating in RATINGS: | 
 |       result[rating] = self.GetCount(rating) | 
 |     return result | 
 |  | 
 |  | 
 | class CaseResult(object): | 
 |   """The conclusion for the comparison of a single test case.""" | 
 |  | 
 |   def __init__(self, case_name, before, after, ratio, rating): | 
 |     """Initializes an empty ComparisonConclusions. | 
 |  | 
 |     Args: | 
 |       case_name: String identifying the case. | 
 |       before: Measurement for the "before" version of the code. | 
 |       after: Measurement for the "after" version of the code. | 
 |       ratio: Difference between |after| and |before| as a fraction of |before|. | 
 |       rating: Rating for this test case. | 
 |     """ | 
 |     self.case_name = case_name | 
 |     self.before = before | 
 |     self.after = after | 
 |     self.ratio = ratio | 
 |     self.rating = rating | 
 |  | 
 |   def GetOutputDict(self): | 
 |     """Returns a dict with the test case's conclusions.""" | 
 |     return { | 
 |         'before': self.before, | 
 |         'after': self.after, | 
 |         'ratio': self.ratio, | 
 |         'rating': self.rating | 
 |     } | 
 |  | 
 |  | 
 | def PrintConclusionsDictHumanReadable(conclusions_dict, colored, key=None): | 
 |   """Prints a conclusions dict in a human-readable way. | 
 |  | 
 |   Args: | 
 |     conclusions_dict: Dict to print. | 
 |     colored: Whether to color the output to highlight significant changes. | 
 |     key: String with the CaseResult dictionary key to sort the cases. | 
 |   """ | 
 |   # Print header | 
 |   print '=' * 80 | 
 |   print '{0:>11s} {1:>15s}  {2}'.format('% Change', 'Time after', 'Test case') | 
 |   print '-' * 80 | 
 |  | 
 |   color = FORMAT_NORMAL | 
 |  | 
 |   # Print cases | 
 |   if key is not None: | 
 |     case_pairs = sorted( | 
 |         conclusions_dict['comparison_by_case'].iteritems(), | 
 |         key=lambda kv: kv[1][key]) | 
 |   else: | 
 |     case_pairs = sorted(conclusions_dict['comparison_by_case'].iteritems()) | 
 |  | 
 |   for case_name, case_dict in case_pairs: | 
 |     if colored: | 
 |       color = RATING_TO_COLOR[case_dict['rating']] | 
 |  | 
 |     if case_dict['rating'] == RATING_FAILURE: | 
 |       print u'{} to measure time for {}'.format( | 
 |           color.format('Failed'), case_name).encode('utf-8') | 
 |       continue | 
 |  | 
 |     print u'{0} {1:15,d}  {2}'.format( | 
 |         color.format('{:+11.4%}'.format(case_dict['ratio'])), | 
 |         case_dict['after'], case_name).encode('utf-8') | 
 |  | 
 |   # Print totals | 
 |   totals = conclusions_dict['summary'] | 
 |   print '=' * 80 | 
 |   print 'Test cases run: %d' % totals['total'] | 
 |  | 
 |   if colored: | 
 |     color = FORMAT_MAGENTA if totals[RATING_FAILURE] else FORMAT_GREEN | 
 |   print('Failed to measure: %s' % color.format(totals[RATING_FAILURE])) | 
 |  | 
 |   if colored: | 
 |     color = FORMAT_RED if totals[RATING_REGRESSION] else FORMAT_GREEN | 
 |   print('Regressions: %s' % color.format(totals[RATING_REGRESSION])) | 
 |  | 
 |   if colored: | 
 |     color = FORMAT_CYAN if totals[RATING_IMPROVEMENT] else FORMAT_GREEN | 
 |   print('Improvements: %s' % color.format(totals[RATING_IMPROVEMENT])) |