blob: 08c1e2f392364d9cca06aec92e5b5d60ae7e3939 [file] [log] [blame]
# -*- 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