tester/growth.go (97 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"
"strings"
)
// Growth is used to determine what test should be ran next.
type Growth interface {
OnSuccess(test int) int
OnFail(test int) int
String() string
}
// LinearGrowth increases test by a specified amount with every successful test.
type LinearGrowth struct {
Increase int
}
// LinearGrowthPrefix prefix used in linear growth string representation.
const LinearGrowthPrefix = "+"
func (g *LinearGrowth) String() string {
return fmt.Sprintf("%s%d", LinearGrowthPrefix, g.Increase)
}
// OnSuccess increases test by a specified amount.
func (g *LinearGrowth) OnSuccess(test int) int {
return test + g.Increase
}
// OnFail stops the tests.
func (g *LinearGrowth) OnFail(test int) int {
return 0
}
// PercentageGrowth increases test by a specified percentage with every successful test.
type PercentageGrowth struct {
Increase float64
}
// PercentageGrowthPrefix prefix used in percentage growth string representation.
const PercentageGrowthPrefix = "%"
func (g *PercentageGrowth) String() string {
return fmt.Sprintf("%s%.2f", PercentageGrowthPrefix, g.Increase)
}
// OnSuccess increases test by a specified percentage.
func (g *PercentageGrowth) OnSuccess(test int) int {
return int((100. + g.Increase) / 100. * float64(test))
}
// OnFail stops the tests.
func (g *PercentageGrowth) OnFail(test int) int {
return 0
}
// ExponentialGrowth performs binary search up to a given precision.
type ExponentialGrowth struct {
Precision int
left, right int
bound bool
}
// ExponentialGrowthPrefix prefix used in exponential growth string representation.
const ExponentialGrowthPrefix = "^"
func (g *ExponentialGrowth) String() string {
return fmt.Sprintf("%s%d", ExponentialGrowthPrefix, g.Precision)
}
// OnSuccess sets the lower bound to the last test and returns (left+right) / 2
// unless the precision has been achieved.
func (g *ExponentialGrowth) OnSuccess(test int) int {
g.left = test
if !g.bound {
return test * 2
}
if g.right-g.left <= g.Precision {
return 0
}
return int(float64(g.right+g.left) / 2)
}
// OnFail sets the upper bound to the last test and returns (left+right) / 2
// unless the precision has been achieved.
func (g *ExponentialGrowth) OnFail(test int) int {
g.right = test
g.bound = true
if g.right-g.left <= g.Precision {
return 0
}
return int(float64(g.right+g.left) / 2)
}
// GrowthHelp provides usage help about the growth.
const GrowthHelp = `Growth determines what will be the next value used for a test.
* linear growth (+int) increases test value by a fixed amount after each success,
stops immediately after the first failure
* percentage growth (%float) increases test value by a fixed percentage after
each success, stops immediately after the first failure
* exponential growth (^int) first doubles the test value after each success to
find an upper bound, then performs a binary search up to a given precision`
// ErrInvalidGrowth is returned when a growth cannot be found.
var ErrInvalidGrowth = errors.New("unknown growth, want +int, %%flaot, ^int")
// ParseGrowth creates a growth from its string representation.
func ParseGrowth(value string) (Growth, error) {
switch {
case strings.HasPrefix(value, LinearGrowthPrefix):
inc, err := strconv.Atoi(strings.TrimPrefix(value, LinearGrowthPrefix))
if err != nil {
//nolint:wrapcheck
return nil, err
}
return &LinearGrowth{Increase: inc}, nil
case strings.HasPrefix(value, PercentageGrowthPrefix):
inc, err := strconv.ParseFloat(strings.TrimPrefix(value, PercentageGrowthPrefix), 64)
if err != nil {
//nolint:wrapcheck
return nil, err
}
return &PercentageGrowth{Increase: inc}, nil
case strings.HasPrefix(value, ExponentialGrowthPrefix):
prec, err := strconv.Atoi(strings.TrimPrefix(value, ExponentialGrowthPrefix))
if err != nil {
//nolint:wrapcheck
return nil, err
}
return &ExponentialGrowth{Precision: prec}, nil
default:
return nil, ErrInvalidGrowth
}
}