tester/constraint.go (100 lines of code) (raw):
/*
Copyright (c) Facebook, Inc. and its affiliates.
All rights reserved.
This source code is licensed under the BSD-style license found in the
LICENSE file in the root directory of this source tree.
*/
package tester
import (
"errors"
"fmt"
"strconv"
"time"
"github.com/facebookincubator/fbender/utils"
)
// Constraint represents a constraint tests should meet to be considered
// successful.
type Constraint struct {
Metric Metric
Aggregator Aggregator
Comparator Comparator
Threshold float64
}
func (c *Constraint) String() string {
return fmt.Sprintf("%s(%s) %s %.2f",
c.Aggregator.Name(), c.Metric.Name(), c.Comparator.Name(), c.Threshold)
}
// ErrNoDataPoints is raised when no data points are found.
var ErrNoDataPoints = errors.New("no data points")
// ErrNotSatisfied is raised when a condition is not met.
var ErrNotSatisfied = errors.New("unsatisfied condition")
// Check fetches metric and checks if the constraint has been satisfied.
func (c *Constraint) Check(start time.Time, duration time.Duration) error {
points, err := c.Metric.Fetch(start, duration)
if err != nil {
//nolint:wrapcheck
return err
}
if points == nil {
return ErrNoDataPoints
}
value := c.Aggregator.Aggregate(points)
if !c.Comparator.Compare(value, c.Threshold) {
return fmt.Errorf("%w: %.4f %s %.4f", ErrNotSatisfied, value, c.Comparator.Name(), c.Threshold)
}
return nil
}
// ConstraintsHelp is an help message on how to use constraints.
const ConstraintsHelp = `
Constraints follow the syntax:
Constraint ::= <Aggregator>(<Metric>)<Cmp><Threshold>
Aggregator ::= "MIN" | "MAX"
Metric ::= <string>
Cmp ::= "<" | ">"
Threshold ::= <float>
Constraints examples:
MIN(metric) < 20.5
MAX(metric) > 0.45
MIN(metric) < 123
` + GrowthHelp
// ErrNotParsed should be returned when a parser did not parse a constraint.
var ErrNotParsed = errors.New("constraint could not be parsed")
// ErrInvalidFormat is raised when the constraint format is not correct.
var ErrInvalidFormat = errors.New("invalid constraint format")
// MetricParser is used to parse string values to a metric.
// Parsers should return a metric and error if it successfully parsed
// a metric string, or a fatal error occurred. Otherwise it should return
// ErrNotParsed which will result in trying next parser from the list.
type MetricParser func(string) (Metric, error)
// Named capture groups of the constraints matching regexp.
const (
aggregatorMatch = `(?P<aggregator>\w+)`
metricMatch = `(?P<metric>\S+)`
comparatorMatch = `(?P<comparator>[<>=~!@#$%^&?]+)`
thresholdMatch = `(?P<threshold>[-+]?\d*\.?\d+)`
)
//nolint:gochecknoglobals
var constraintRegexp = utils.MustCompile(
fmt.Sprintf(
`^\s*%s\(%s\)\s*%s\s*%s\s*$`,
aggregatorMatch, metricMatch, comparatorMatch, thresholdMatch,
),
)
// ParseConstraint creates a constraint from a string representation.
func ParseConstraint(s string, parsers ...MetricParser) (*Constraint, error) {
if !constraintRegexp.MatchString(s) {
return nil, ErrInvalidFormat
}
match := constraintRegexp.FindStringSubmatchMap(s)
aggregator, err := ParseAggregator(match["aggregator"])
if err != nil {
return nil, err
}
metric, err := parseMetric(match["metric"], parsers...)
if err != nil {
return nil, err
}
comparator, err := ParseComparator(match["comparator"])
if err != nil {
return nil, err
}
threshold, err := strconv.ParseFloat(match["threshold"], 64)
if err != nil {
//nolint:wrapcheck
return nil, err
}
return &Constraint{
Metric: metric,
Aggregator: aggregator,
Comparator: comparator,
Threshold: threshold,
}, nil
}
func parseMetric(name string, parsers ...MetricParser) (Metric, error) {
for _, parser := range parsers {
metric, err := parser(name)
if err == nil {
return metric, nil
} else if !errors.Is(err, ErrNotParsed) {
return nil, err
}
}
return nil, ErrNotParsed
}