| # -*- coding: utf-8 -*- |
| # Copyright 2017 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. |
| |
| """Contains classes used among bisect modules. |
| |
| * Score: stores a list of scores and its statistics. |
| * CommitInfo: stores commit info. |
| * OptionsChecker: performs sanity check that every required option are given. |
| """ |
| |
| from __future__ import division |
| from __future__ import print_function |
| |
| import math |
| |
| from chromite.lib import cros_logging as logging |
| |
| class Score(object): |
| """Stores a list of scores and calculates its statistics. |
| |
| Scores can be assigned by constructor or Update(). |
| |
| Attributes: |
| values: A list of scores. |
| mean: Mean of the scores. |
| variance: Variance of the scores. |
| std: Standard deviation of the scores. |
| """ |
| |
| def __init__(self, values=None): |
| """Constructor. |
| |
| If values is not assigned, e.g. Score(), it returns a default Score object. |
| And bool(Score()) == False. |
| |
| Args: |
| values: A list of scores. |
| """ |
| self.values = [] |
| self.mean = 0.0 |
| self.variance = 0.0 |
| self.std = 0.0 |
| if values is not None: |
| self.Update(values) |
| |
| def __repr__(self): |
| return 'Score(values=%r)' % self.values |
| |
| def __str__(self): |
| return 'Score(values=%r, mean=%.3f, var=%.3f, std=%.3f)' % ( |
| self.values, self.mean, self.variance, self.std) |
| |
| def __len__(self): |
| """Returns number of scores. |
| |
| Returns: |
| Number of values. |
| """ |
| return len(self.values) |
| |
| def __eq__(self, other): |
| return sorted(self.values) == sorted(other.values) |
| |
| def __ne__(self, other): |
| return not self.__eq__(other) |
| |
| def Clear(self): |
| """Clears values and statistics.""" |
| self.values = [] |
| self.mean = 0.0 |
| self.variance = 0.0 |
| self.std = 0.0 |
| |
| def Update(self, values): |
| """Updates scores and their corresponding statistics. |
| |
| If values is empty or ill-formed, it calls Clear(). |
| |
| Args: |
| values: a list of scores. |
| """ |
| if not values: |
| self.Clear() |
| return |
| try: |
| values = [float(x) for x in values] |
| except ValueError as e: |
| logging.error('Invalid literal of score: %s', e) |
| self.Clear() |
| return |
| except TypeError: |
| logging.error('Invalid type of score: %s', values) |
| self.Clear() |
| return |
| |
| self.values = values |
| num_values = len(values) |
| if num_values == 1: |
| self.mean = values[0] |
| self.variance = 0.0 |
| self.std = 0.0 |
| else: |
| self.mean = sum(values) / num_values |
| differences_from_mean = [x - self.mean for x in values] |
| squared_differences = [x * x for x in differences_from_mean] |
| self.variance = sum(squared_differences) / (num_values - 1) |
| self.std = math.sqrt(self.variance) |
| |
| |
| class CommitInfo(object): |
| """Stores commit info. |
| |
| Attributes: |
| sha1: Commit SHA1. |
| title: Commit title. |
| score: Evaluation score of the commit. |
| label: 'good' or 'bad'. |
| timestamp: Commit timestamp. |
| """ |
| def __init__(self, sha1=None, title=None, score=None, label=None, |
| timestamp=None): |
| """Constructor. |
| |
| All arguments are optional. CommitInfo() creates default CommitInfo object |
| and bool(CommitInfo()) == False. |
| |
| Args: |
| sha1: Commit SHA1. |
| title: Commit title. |
| score: Evaluation score of the commit. |
| label: 'good' or 'bad'. |
| timestamp: Commit timestamp. |
| """ |
| self.sha1 = '' if sha1 is None else sha1 |
| self.title = '' if title is None else title |
| self.score = Score() if score is None else score |
| self.label = '' if label is None else label |
| self.timestamp = 0 if timestamp is None else timestamp |
| |
| def __repr__(self): |
| return 'CommitInfo(sha1=%r, title=%r, score=%r, label=%r, timestamp=%r)' % ( |
| self.sha1, self.title, self.score, self.label, self.timestamp) |
| |
| def __eq__(self, other): |
| return (self.sha1 == other.sha1 and self.title == other.title and |
| self.score == other.score and self.label == other.label and |
| self.timestamp == other.timestamp) |
| |
| def __ne__(self, other): |
| return not self.__eq__(other) |
| |
| def __bool__(self): |
| return bool(self.sha1 or self.timestamp or self.title or self.label or |
| self.score) |
| |
| # Python 2 glue. |
| __nonzero__ = __bool__ |
| |
| |
| class MissingRequiredOptionsException(Exception): |
| """Exception raised for missing required options.""" |
| |
| |
| class OptionsChecker(object): |
| """Makes sure that __init__'s 'options' contains all required arguments. |
| |
| Its derived class should just update class attribute "REQUIRED_ARGS" and |
| invokes __init__ to get options checked. For example: |
| class ClassA(OptionsChecker): |
| REQUIRED_ARGS = ['a'] |
| def __init__(self, options): |
| super(ClassA, self).__init__(options) |
| |
| class ClassB(ClassA): |
| REQUIRED_ARGS = ClassA.REQUIRED_ARGS + ['b'] |
| def __init__(self, options): |
| super(ClassB, self).__init__(options) |
| |
| then calling |
| ClassA(argparse.Namespace(a='a')) |
| ClassB(argparse.Namespace(a='a', b='b')) |
| is okay, but calling |
| ClassA(argparse.Namespace(c='c')) |
| ClassB(argparse.Namespace(b='b')) |
| raises MissingRequiredOptionsException telling you that argument a is |
| missing. |
| """ |
| REQUIRED_ARGS = () |
| |
| def __init__(self, options): |
| """Constructor. |
| |
| Args: |
| options: An argparse.Namespace to hold command line arguments. |
| |
| Raises: |
| MissingRequiredOptionsException if any required argument is missing. |
| """ |
| self.SanityCheckOptions(options) |
| |
| @classmethod |
| def SanityCheckOptions(cls, options): |
| """Performs sanity check on command line arguments. |
| |
| Args: |
| options: An argparse.Namespace to hold command line arguments. |
| |
| Returns: |
| True if sanity check passed. |
| |
| Raises: |
| MissingRequiredOptionsException if any required argument is missing. |
| """ |
| missing_args = [arg for arg in cls.REQUIRED_ARGS if arg not in options] |
| if missing_args: |
| raise MissingRequiredOptionsException( |
| 'Missing command line argument(s) %s required for class %s' % |
| (missing_args, cls.__name__)) |
| return True |