refactor(engine): set options from any value (#837)
This diff refactors how we set options for experiments to accept in input an any value or a map[string]any, depending on which method we choose to actually set options. There should be no functional change, except that now we're not guessing the type and then attempting to set the value of the selected field: now, instead, we match the provided type and the field's type as part of the same function (i.e., SetOptionAny). This diff is functional to https://github.com/ooni/probe/issues/2184, because it will allow us to load options from a map[string]any, which will be part of the OONI Run v2 JSON descriptor. If we didn't apply this change, we would only have been to set options from a map[string]string, which is good enough as a solution for the CLI but is definitely clumsy when you have to write stuff like: ```JSON { "options": { "HTTP3Enabled": "true" } } ``` when you could instead more naturally write: ```JSON { "options": { "HTTP3Enabled": true } } ```
This commit is contained in:
parent
6019b25baf
commit
086ae43b15
|
@ -176,7 +176,7 @@ func warnOnError(err error, msg string) {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func mustMakeMap(input []string) (output map[string]string) {
|
func mustMakeMapString(input []string) (output map[string]string) {
|
||||||
output = make(map[string]string)
|
output = make(map[string]string)
|
||||||
for _, opt := range input {
|
for _, opt := range input {
|
||||||
key, value, err := split(opt)
|
key, value, err := split(opt)
|
||||||
|
@ -186,6 +186,16 @@ func mustMakeMap(input []string) (output map[string]string) {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func mustMakeMapAny(input []string) (output map[string]any) {
|
||||||
|
output = make(map[string]any)
|
||||||
|
for _, opt := range input {
|
||||||
|
key, value, err := split(opt)
|
||||||
|
fatalOnError(err, "cannot split key-value pair")
|
||||||
|
output[key] = value
|
||||||
|
}
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
func mustParseURL(URL string) *url.URL {
|
func mustParseURL(URL string) *url.URL {
|
||||||
rv, err := url.Parse(URL)
|
rv, err := url.Parse(URL)
|
||||||
fatalOnError(err, "cannot parse URL")
|
fatalOnError(err, "cannot parse URL")
|
||||||
|
@ -296,8 +306,8 @@ func MainWithConfiguration(experimentName string, currentOptions Options) {
|
||||||
|
|
||||||
ctx := context.Background()
|
ctx := context.Background()
|
||||||
|
|
||||||
extraOptions := mustMakeMap(currentOptions.ExtraOptions)
|
extraOptions := mustMakeMapAny(currentOptions.ExtraOptions)
|
||||||
annotations := mustMakeMap(currentOptions.Annotations)
|
annotations := mustMakeMapString(currentOptions.Annotations)
|
||||||
|
|
||||||
logger := &log.Logger{Level: log.InfoLevel, Handler: &logHandler{Writer: os.Stderr}}
|
logger := &log.Logger{Level: log.InfoLevel, Handler: &logHandler{Writer: os.Stderr}}
|
||||||
if currentOptions.Verbose {
|
if currentOptions.Verbose {
|
||||||
|
@ -413,7 +423,7 @@ func MainWithConfiguration(experimentName string, currentOptions Options) {
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
err = builder.SetOptionsGuessType(extraOptions)
|
err = builder.SetOptionsAny(extraOptions)
|
||||||
fatalOnError(err, "cannot parse extraOptions")
|
fatalOnError(err, "cannot parse extraOptions")
|
||||||
|
|
||||||
experiment := builder.NewExperiment()
|
experiment := builder.NewExperiment()
|
||||||
|
|
|
@ -157,7 +157,7 @@ func TestSetCallbacks(t *testing.T) {
|
||||||
if err != nil {
|
if err != nil {
|
||||||
t.Fatal(err)
|
t.Fatal(err)
|
||||||
}
|
}
|
||||||
if err := builder.SetOptionInt("SleepTime", 0); err != nil {
|
if err := builder.SetOptionAny("SleepTime", 0); err != nil {
|
||||||
t.Fatal(err)
|
t.Fatal(err)
|
||||||
}
|
}
|
||||||
register := ®isterCallbacksCalled{}
|
register := ®isterCallbacksCalled{}
|
||||||
|
@ -203,7 +203,7 @@ func TestMeasurementFailure(t *testing.T) {
|
||||||
if err != nil {
|
if err != nil {
|
||||||
t.Fatal(err)
|
t.Fatal(err)
|
||||||
}
|
}
|
||||||
if err := builder.SetOptionBool("ReturnError", true); err != nil {
|
if err := builder.SetOptionAny("ReturnError", true); err != nil {
|
||||||
t.Fatal(err)
|
t.Fatal(err)
|
||||||
}
|
}
|
||||||
measurement, err := builder.NewExperiment().Measure("")
|
measurement, err := builder.NewExperiment().Measure("")
|
||||||
|
@ -279,13 +279,13 @@ func TestUseOptions(t *testing.T) {
|
||||||
if !sleepTime {
|
if !sleepTime {
|
||||||
t.Fatal("did not find SleepTime option")
|
t.Fatal("did not find SleepTime option")
|
||||||
}
|
}
|
||||||
if err := builder.SetOptionBool("ReturnError", true); err != nil {
|
if err := builder.SetOptionAny("ReturnError", true); err != nil {
|
||||||
t.Fatal("cannot set ReturnError field")
|
t.Fatal("cannot set ReturnError field")
|
||||||
}
|
}
|
||||||
if err := builder.SetOptionInt("SleepTime", 10); err != nil {
|
if err := builder.SetOptionAny("SleepTime", 10); err != nil {
|
||||||
t.Fatal("cannot set SleepTime field")
|
t.Fatal("cannot set SleepTime field")
|
||||||
}
|
}
|
||||||
if err := builder.SetOptionString("Message", "antani"); err != nil {
|
if err := builder.SetOptionAny("Message", "antani"); err != nil {
|
||||||
t.Fatal("cannot set Message field")
|
t.Fatal("cannot set Message field")
|
||||||
}
|
}
|
||||||
config := builder.config.(*example.Config)
|
config := builder.config.(*example.Config)
|
||||||
|
|
|
@ -4,7 +4,6 @@ import (
|
||||||
"errors"
|
"errors"
|
||||||
"fmt"
|
"fmt"
|
||||||
"reflect"
|
"reflect"
|
||||||
"regexp"
|
|
||||||
"strconv"
|
"strconv"
|
||||||
|
|
||||||
"github.com/iancoleman/strcase"
|
"github.com/iancoleman/strcase"
|
||||||
|
@ -63,22 +62,50 @@ func (b *ExperimentBuilder) InputPolicy() InputPolicy {
|
||||||
return b.inputPolicy
|
return b.inputPolicy
|
||||||
}
|
}
|
||||||
|
|
||||||
// OptionInfo contains info about an option
|
// OptionInfo contains info about an option.
|
||||||
type OptionInfo struct {
|
type OptionInfo struct {
|
||||||
Doc string
|
// Doc contains the documentation.
|
||||||
|
Doc string
|
||||||
|
|
||||||
|
// Type contains the type.
|
||||||
Type string
|
Type string
|
||||||
}
|
}
|
||||||
|
|
||||||
|
var (
|
||||||
|
// ErrConfigIsNotAStructPointer indicates we expected a pointer to struct.
|
||||||
|
ErrConfigIsNotAStructPointer = errors.New("config is not a struct pointer")
|
||||||
|
|
||||||
|
// ErrNoSuchField indicates there's no field with the given name.
|
||||||
|
ErrNoSuchField = errors.New("no such field")
|
||||||
|
|
||||||
|
// ErrCannotSetIntegerOption means SetOptionAny couldn't set an integer option.
|
||||||
|
ErrCannotSetIntegerOption = errors.New("cannot set integer option")
|
||||||
|
|
||||||
|
// ErrInvalidStringRepresentationOfBool indicates the string you passed
|
||||||
|
// to SetOptionaAny is not a valid string representation of a bool.
|
||||||
|
ErrInvalidStringRepresentationOfBool = errors.New("invalid string representation of bool")
|
||||||
|
|
||||||
|
// ErrCannotSetBoolOption means SetOptionAny couldn't set a bool option.
|
||||||
|
ErrCannotSetBoolOption = errors.New("cannot set bool option")
|
||||||
|
|
||||||
|
// ErrCannotSetStringOption means SetOptionAny couldn't set a string option.
|
||||||
|
ErrCannotSetStringOption = errors.New("cannot set string option")
|
||||||
|
|
||||||
|
// ErrUnsupportedOptionType means we don't support the type passed to
|
||||||
|
// the SetOptionAny method as an opaque any type.
|
||||||
|
ErrUnsupportedOptionType = errors.New("unsupported option type")
|
||||||
|
)
|
||||||
|
|
||||||
// Options returns info about all options
|
// Options returns info about all options
|
||||||
func (b *ExperimentBuilder) Options() (map[string]OptionInfo, error) {
|
func (b *ExperimentBuilder) Options() (map[string]OptionInfo, error) {
|
||||||
result := make(map[string]OptionInfo)
|
result := make(map[string]OptionInfo)
|
||||||
ptrinfo := reflect.ValueOf(b.config)
|
ptrinfo := reflect.ValueOf(b.config)
|
||||||
if ptrinfo.Kind() != reflect.Ptr {
|
if ptrinfo.Kind() != reflect.Ptr {
|
||||||
return nil, errors.New("config is not a pointer")
|
return nil, ErrConfigIsNotAStructPointer
|
||||||
}
|
}
|
||||||
structinfo := ptrinfo.Elem().Type()
|
structinfo := ptrinfo.Elem().Type()
|
||||||
if structinfo.Kind() != reflect.Struct {
|
if structinfo.Kind() != reflect.Struct {
|
||||||
return nil, errors.New("config is not a struct")
|
return nil, ErrConfigIsNotAStructPointer
|
||||||
}
|
}
|
||||||
for i := 0; i < structinfo.NumField(); i++ {
|
for i := 0; i < structinfo.NumField(); i++ {
|
||||||
field := structinfo.Field(i)
|
field := structinfo.Field(i)
|
||||||
|
@ -90,67 +117,90 @@ func (b *ExperimentBuilder) Options() (map[string]OptionInfo, error) {
|
||||||
return result, nil
|
return result, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
// SetOptionBool sets a bool option
|
// setOptionBool sets a bool option.
|
||||||
func (b *ExperimentBuilder) SetOptionBool(key string, value bool) error {
|
func (b *ExperimentBuilder) setOptionBool(field reflect.Value, value any) error {
|
||||||
field, err := fieldbyname(b.config, key)
|
switch v := value.(type) {
|
||||||
|
case bool:
|
||||||
|
field.SetBool(v)
|
||||||
|
return nil
|
||||||
|
case string:
|
||||||
|
if v != "true" && v != "false" {
|
||||||
|
return fmt.Errorf("%w: %s", ErrInvalidStringRepresentationOfBool, v)
|
||||||
|
}
|
||||||
|
field.SetBool(v == "true")
|
||||||
|
return nil
|
||||||
|
default:
|
||||||
|
return fmt.Errorf("%w from a value of type %T", ErrCannotSetBoolOption, value)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// setOptionInt sets an int option
|
||||||
|
func (b *ExperimentBuilder) setOptionInt(field reflect.Value, value any) error {
|
||||||
|
switch v := value.(type) {
|
||||||
|
case int64:
|
||||||
|
field.SetInt(v)
|
||||||
|
return nil
|
||||||
|
case int32:
|
||||||
|
field.SetInt(int64(v))
|
||||||
|
return nil
|
||||||
|
case int16:
|
||||||
|
field.SetInt(int64(v))
|
||||||
|
return nil
|
||||||
|
case int8:
|
||||||
|
field.SetInt(int64(v))
|
||||||
|
return nil
|
||||||
|
case int:
|
||||||
|
field.SetInt(int64(v))
|
||||||
|
return nil
|
||||||
|
case string:
|
||||||
|
number, err := strconv.ParseInt(v, 10, 64)
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("%w: %s", ErrCannotSetIntegerOption, err.Error())
|
||||||
|
}
|
||||||
|
field.SetInt(number)
|
||||||
|
return nil
|
||||||
|
default:
|
||||||
|
return fmt.Errorf("%w from a value of type %T", ErrCannotSetIntegerOption, value)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// setOptionString sets a string option
|
||||||
|
func (b *ExperimentBuilder) setOptionString(field reflect.Value, value any) error {
|
||||||
|
switch v := value.(type) {
|
||||||
|
case string:
|
||||||
|
field.SetString(v)
|
||||||
|
return nil
|
||||||
|
default:
|
||||||
|
return fmt.Errorf("%w from a value of type %T", ErrCannotSetStringOption, value)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// SetOptionAny sets an option whose value is an any value. We will use reasonable
|
||||||
|
// heuristics to convert the any value to the proper type of the field whose name is
|
||||||
|
// contained by the key variable. If we cannot convert the provided any value to
|
||||||
|
// the proper type, then this function returns an error.
|
||||||
|
func (b *ExperimentBuilder) SetOptionAny(key string, value any) error {
|
||||||
|
field, err := b.fieldbyname(b.config, key)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
if field.Kind() != reflect.Bool {
|
switch field.Kind() {
|
||||||
return errors.New("field is not a bool")
|
case reflect.Int64:
|
||||||
|
return b.setOptionInt(field, value)
|
||||||
|
case reflect.Bool:
|
||||||
|
return b.setOptionBool(field, value)
|
||||||
|
case reflect.String:
|
||||||
|
return b.setOptionString(field, value)
|
||||||
|
default:
|
||||||
|
return fmt.Errorf("%w: %T", ErrUnsupportedOptionType, value)
|
||||||
}
|
}
|
||||||
field.SetBool(value)
|
|
||||||
return nil
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// SetOptionInt sets an int option
|
// SetOptionsAny sets options from a map[string]any. See the documentation of
|
||||||
func (b *ExperimentBuilder) SetOptionInt(key string, value int64) error {
|
// the SetOptionAny function for more information.
|
||||||
field, err := fieldbyname(b.config, key)
|
func (b *ExperimentBuilder) SetOptionsAny(options map[string]any) error {
|
||||||
if err != nil {
|
for key, value := range options {
|
||||||
return err
|
if err := b.SetOptionAny(key, value); err != nil {
|
||||||
}
|
|
||||||
if field.Kind() != reflect.Int64 {
|
|
||||||
return errors.New("field is not an int64")
|
|
||||||
}
|
|
||||||
field.SetInt(value)
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|
||||||
// SetOptionString sets a string option
|
|
||||||
func (b *ExperimentBuilder) SetOptionString(key, value string) error {
|
|
||||||
field, err := fieldbyname(b.config, key)
|
|
||||||
if err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
if field.Kind() != reflect.String {
|
|
||||||
return errors.New("field is not a string")
|
|
||||||
}
|
|
||||||
field.SetString(value)
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|
||||||
var intregexp = regexp.MustCompile("^[0-9]+$")
|
|
||||||
|
|
||||||
// SetOptionGuessType sets an option whose type depends on the
|
|
||||||
// option value. If the value is `"true"` or `"false"` we
|
|
||||||
// assume the option is boolean. If the value is numeric, then we
|
|
||||||
// set an integer option. Otherwise we set a string option.
|
|
||||||
func (b *ExperimentBuilder) SetOptionGuessType(key, value string) error {
|
|
||||||
if value == "true" || value == "false" {
|
|
||||||
return b.SetOptionBool(key, value == "true")
|
|
||||||
}
|
|
||||||
if !intregexp.MatchString(value) {
|
|
||||||
return b.SetOptionString(key, value)
|
|
||||||
}
|
|
||||||
number, _ := strconv.ParseInt(value, 10, 64)
|
|
||||||
return b.SetOptionInt(key, number)
|
|
||||||
}
|
|
||||||
|
|
||||||
// SetOptionsGuessType calls the SetOptionGuessType method for every
|
|
||||||
// key, value pair contained by the opts input map.
|
|
||||||
func (b *ExperimentBuilder) SetOptionsGuessType(opts map[string]string) error {
|
|
||||||
for k, v := range opts {
|
|
||||||
if err := b.SetOptionGuessType(k, v); err != nil {
|
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -162,19 +212,19 @@ func (b *ExperimentBuilder) SetCallbacks(callbacks model.ExperimentCallbacks) {
|
||||||
b.callbacks = callbacks
|
b.callbacks = callbacks
|
||||||
}
|
}
|
||||||
|
|
||||||
func fieldbyname(v interface{}, key string) (reflect.Value, error) {
|
func (b *ExperimentBuilder) fieldbyname(v interface{}, key string) (reflect.Value, error) {
|
||||||
// See https://stackoverflow.com/a/6396678/4354461
|
// See https://stackoverflow.com/a/6396678/4354461
|
||||||
ptrinfo := reflect.ValueOf(v)
|
ptrinfo := reflect.ValueOf(v)
|
||||||
if ptrinfo.Kind() != reflect.Ptr {
|
if ptrinfo.Kind() != reflect.Ptr {
|
||||||
return reflect.Value{}, errors.New("value is not a pointer")
|
return reflect.Value{}, fmt.Errorf("%w but a %T", ErrConfigIsNotAStructPointer, v)
|
||||||
}
|
}
|
||||||
structinfo := ptrinfo.Elem()
|
structinfo := ptrinfo.Elem()
|
||||||
if structinfo.Kind() != reflect.Struct {
|
if structinfo.Kind() != reflect.Struct {
|
||||||
return reflect.Value{}, errors.New("value is not a pointer to struct")
|
return reflect.Value{}, fmt.Errorf("%w but a %T", ErrConfigIsNotAStructPointer, v)
|
||||||
}
|
}
|
||||||
field := structinfo.FieldByName(key)
|
field := structinfo.FieldByName(key)
|
||||||
if !field.IsValid() || !field.CanSet() {
|
if !field.IsValid() || !field.CanSet() {
|
||||||
return reflect.Value{}, errors.New("no such field")
|
return reflect.Value{}, fmt.Errorf("%w: %s", ErrNoSuchField, key)
|
||||||
}
|
}
|
||||||
return field, nil
|
return field, nil
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,170 +1,326 @@
|
||||||
package engine
|
package engine
|
||||||
|
|
||||||
import (
|
import (
|
||||||
|
"errors"
|
||||||
"testing"
|
"testing"
|
||||||
|
|
||||||
"github.com/ooni/probe-cli/v3/internal/engine/experiment/example"
|
"github.com/google/go-cmp/cmp"
|
||||||
)
|
)
|
||||||
|
|
||||||
|
type fakeExperimentConfig struct {
|
||||||
|
Chan chan any `ooni:"we cannot set this"`
|
||||||
|
String string `ooni:"a string"`
|
||||||
|
Truth bool `ooni:"something that no-one knows"`
|
||||||
|
Value int64 `ooni:"a number"`
|
||||||
|
}
|
||||||
|
|
||||||
func TestExperimentBuilderOptions(t *testing.T) {
|
func TestExperimentBuilderOptions(t *testing.T) {
|
||||||
t.Run("when config is not a pointer", func(t *testing.T) {
|
t.Run("when config is not a pointer", func(t *testing.T) {
|
||||||
b := &ExperimentBuilder{
|
b := &ExperimentBuilder{
|
||||||
config: 17,
|
config: 17,
|
||||||
}
|
}
|
||||||
options, err := b.Options()
|
options, err := b.Options()
|
||||||
if err == nil {
|
if !errors.Is(err, ErrConfigIsNotAStructPointer) {
|
||||||
t.Fatal("expected an error here")
|
t.Fatal("expected an error here")
|
||||||
}
|
}
|
||||||
if options != nil {
|
if options != nil {
|
||||||
t.Fatal("expected nil here")
|
t.Fatal("expected nil here")
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
|
|
||||||
t.Run("when config is not a struct", func(t *testing.T) {
|
t.Run("when config is not a struct", func(t *testing.T) {
|
||||||
number := 17
|
number := 17
|
||||||
b := &ExperimentBuilder{
|
b := &ExperimentBuilder{
|
||||||
config: &number,
|
config: &number,
|
||||||
}
|
}
|
||||||
options, err := b.Options()
|
options, err := b.Options()
|
||||||
if err == nil {
|
if !errors.Is(err, ErrConfigIsNotAStructPointer) {
|
||||||
t.Fatal("expected an error here")
|
t.Fatal("expected an error here")
|
||||||
}
|
}
|
||||||
if options != nil {
|
if options != nil {
|
||||||
t.Fatal("expected nil here")
|
t.Fatal("expected nil here")
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
}
|
|
||||||
|
|
||||||
func TestExperimentBuilderSetOption(t *testing.T) {
|
t.Run("when config is a pointer to struct", func(t *testing.T) {
|
||||||
t.Run("when config is not a pointer", func(t *testing.T) {
|
config := &fakeExperimentConfig{}
|
||||||
b := &ExperimentBuilder{
|
b := &ExperimentBuilder{
|
||||||
config: 17,
|
config: config,
|
||||||
}
|
}
|
||||||
if err := b.SetOptionBool("antani", false); err == nil {
|
options, err := b.Options()
|
||||||
t.Fatal("expected an error here")
|
if err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
}
|
}
|
||||||
})
|
for name, value := range options {
|
||||||
t.Run("when config is not a struct", func(t *testing.T) {
|
switch name {
|
||||||
number := 17
|
case "Chan":
|
||||||
b := &ExperimentBuilder{
|
if value.Doc != "we cannot set this" {
|
||||||
config: &number,
|
t.Fatal("invalid doc")
|
||||||
}
|
}
|
||||||
if err := b.SetOptionBool("antani", false); err == nil {
|
if value.Type != "chan interface {}" {
|
||||||
t.Fatal("expected an error here")
|
t.Fatal("invalid type", value.Type)
|
||||||
}
|
}
|
||||||
})
|
case "String":
|
||||||
t.Run("when field is not valid", func(t *testing.T) {
|
if value.Doc != "a string" {
|
||||||
b := &ExperimentBuilder{
|
t.Fatal("invalid doc")
|
||||||
config: &ExperimentBuilder{},
|
}
|
||||||
}
|
if value.Type != "string" {
|
||||||
if err := b.SetOptionBool("antani", false); err == nil {
|
t.Fatal("invalid type", value.Type)
|
||||||
t.Fatal("expected an error here")
|
}
|
||||||
}
|
case "Truth":
|
||||||
})
|
if value.Doc != "something that no-one knows" {
|
||||||
t.Run("when field is not bool", func(t *testing.T) {
|
t.Fatal("invalid doc")
|
||||||
b := &ExperimentBuilder{
|
}
|
||||||
config: new(example.Config),
|
if value.Type != "bool" {
|
||||||
}
|
t.Fatal("invalid type", value.Type)
|
||||||
if err := b.SetOptionBool("Message", false); err == nil {
|
}
|
||||||
t.Fatal("expected an error here")
|
case "Value":
|
||||||
}
|
if value.Doc != "a number" {
|
||||||
})
|
t.Fatal("invalid doc")
|
||||||
t.Run("when field is not string", func(t *testing.T) {
|
}
|
||||||
b := &ExperimentBuilder{
|
if value.Type != "int64" {
|
||||||
config: new(example.Config),
|
t.Fatal("invalid type", value.Type)
|
||||||
}
|
}
|
||||||
if err := b.SetOptionString("ReturnError", "xx"); err == nil {
|
default:
|
||||||
t.Fatal("expected an error here")
|
t.Fatal("unknown name", name)
|
||||||
}
|
}
|
||||||
})
|
|
||||||
t.Run("when field is not int", func(t *testing.T) {
|
|
||||||
b := &ExperimentBuilder{
|
|
||||||
config: new(example.Config),
|
|
||||||
}
|
|
||||||
if err := b.SetOptionInt("ReturnError", 17); err == nil {
|
|
||||||
t.Fatal("expected an error here")
|
|
||||||
}
|
|
||||||
})
|
|
||||||
t.Run("when int field does not exist", func(t *testing.T) {
|
|
||||||
b := &ExperimentBuilder{
|
|
||||||
config: new(example.Config),
|
|
||||||
}
|
|
||||||
if err := b.SetOptionInt("antani", 17); err == nil {
|
|
||||||
t.Fatal("expected an error here")
|
|
||||||
}
|
|
||||||
})
|
|
||||||
t.Run("when string field does not exist", func(t *testing.T) {
|
|
||||||
b := &ExperimentBuilder{
|
|
||||||
config: new(example.Config),
|
|
||||||
}
|
|
||||||
if err := b.SetOptionString("antani", "xx"); err == nil {
|
|
||||||
t.Fatal("expected an error here")
|
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
func TestExperimentBuilderSetOptionGuessType(t *testing.T) {
|
func TestExperimentBuilderSetOptionAny(t *testing.T) {
|
||||||
type fiction struct {
|
var inputs = []struct {
|
||||||
String string
|
TestCaseName string
|
||||||
Truth bool
|
InitialConfig any
|
||||||
Value int64
|
FieldName string
|
||||||
|
FieldValue any
|
||||||
|
ExpectErr error
|
||||||
|
ExpectConfig any
|
||||||
|
}{{
|
||||||
|
TestCaseName: "config is not a pointer",
|
||||||
|
InitialConfig: fakeExperimentConfig{},
|
||||||
|
FieldName: "Antani",
|
||||||
|
FieldValue: true,
|
||||||
|
ExpectErr: ErrConfigIsNotAStructPointer,
|
||||||
|
ExpectConfig: fakeExperimentConfig{},
|
||||||
|
}, {
|
||||||
|
TestCaseName: "config is not a pointer to struct",
|
||||||
|
InitialConfig: func() *int {
|
||||||
|
v := 17
|
||||||
|
return &v
|
||||||
|
}(),
|
||||||
|
FieldName: "Antani",
|
||||||
|
FieldValue: true,
|
||||||
|
ExpectErr: ErrConfigIsNotAStructPointer,
|
||||||
|
ExpectConfig: func() *int {
|
||||||
|
v := 17
|
||||||
|
return &v
|
||||||
|
}(),
|
||||||
|
}, {
|
||||||
|
TestCaseName: "for missing field",
|
||||||
|
InitialConfig: &fakeExperimentConfig{},
|
||||||
|
FieldName: "Antani",
|
||||||
|
FieldValue: true,
|
||||||
|
ExpectErr: ErrNoSuchField,
|
||||||
|
ExpectConfig: &fakeExperimentConfig{},
|
||||||
|
}, {
|
||||||
|
TestCaseName: "[bool] for true value represented as string",
|
||||||
|
InitialConfig: &fakeExperimentConfig{},
|
||||||
|
FieldName: "Truth",
|
||||||
|
FieldValue: "true",
|
||||||
|
ExpectErr: nil,
|
||||||
|
ExpectConfig: &fakeExperimentConfig{
|
||||||
|
Truth: true,
|
||||||
|
},
|
||||||
|
}, {
|
||||||
|
TestCaseName: "[bool] for false value represented as string",
|
||||||
|
InitialConfig: &fakeExperimentConfig{
|
||||||
|
Truth: true,
|
||||||
|
},
|
||||||
|
FieldName: "Truth",
|
||||||
|
FieldValue: "false",
|
||||||
|
ExpectErr: nil,
|
||||||
|
ExpectConfig: &fakeExperimentConfig{
|
||||||
|
Truth: false, // must have been flipped
|
||||||
|
},
|
||||||
|
}, {
|
||||||
|
TestCaseName: "[bool] for true value",
|
||||||
|
InitialConfig: &fakeExperimentConfig{},
|
||||||
|
FieldName: "Truth",
|
||||||
|
FieldValue: true,
|
||||||
|
ExpectErr: nil,
|
||||||
|
ExpectConfig: &fakeExperimentConfig{
|
||||||
|
Truth: true,
|
||||||
|
},
|
||||||
|
}, {
|
||||||
|
TestCaseName: "[bool] for false value",
|
||||||
|
InitialConfig: &fakeExperimentConfig{
|
||||||
|
Truth: true,
|
||||||
|
},
|
||||||
|
FieldName: "Truth",
|
||||||
|
FieldValue: false,
|
||||||
|
ExpectErr: nil,
|
||||||
|
ExpectConfig: &fakeExperimentConfig{
|
||||||
|
Truth: false, // must have been flipped
|
||||||
|
},
|
||||||
|
}, {
|
||||||
|
TestCaseName: "[bool] for invalid string representation of bool",
|
||||||
|
InitialConfig: &fakeExperimentConfig{},
|
||||||
|
FieldName: "Truth",
|
||||||
|
FieldValue: "xxx",
|
||||||
|
ExpectErr: ErrInvalidStringRepresentationOfBool,
|
||||||
|
ExpectConfig: &fakeExperimentConfig{},
|
||||||
|
}, {
|
||||||
|
TestCaseName: "[bool] for value we don't know how to convert to bool",
|
||||||
|
InitialConfig: &fakeExperimentConfig{},
|
||||||
|
FieldName: "Truth",
|
||||||
|
FieldValue: make(chan any),
|
||||||
|
ExpectErr: ErrCannotSetBoolOption,
|
||||||
|
ExpectConfig: &fakeExperimentConfig{},
|
||||||
|
}, {
|
||||||
|
TestCaseName: "[int] for int",
|
||||||
|
InitialConfig: &fakeExperimentConfig{},
|
||||||
|
FieldName: "Value",
|
||||||
|
FieldValue: 17,
|
||||||
|
ExpectErr: nil,
|
||||||
|
ExpectConfig: &fakeExperimentConfig{
|
||||||
|
Value: 17,
|
||||||
|
},
|
||||||
|
}, {
|
||||||
|
TestCaseName: "[int] for int64",
|
||||||
|
InitialConfig: &fakeExperimentConfig{},
|
||||||
|
FieldName: "Value",
|
||||||
|
FieldValue: int64(17),
|
||||||
|
ExpectErr: nil,
|
||||||
|
ExpectConfig: &fakeExperimentConfig{
|
||||||
|
Value: 17,
|
||||||
|
},
|
||||||
|
}, {
|
||||||
|
TestCaseName: "[int] for int32",
|
||||||
|
InitialConfig: &fakeExperimentConfig{},
|
||||||
|
FieldName: "Value",
|
||||||
|
FieldValue: int32(17),
|
||||||
|
ExpectErr: nil,
|
||||||
|
ExpectConfig: &fakeExperimentConfig{
|
||||||
|
Value: 17,
|
||||||
|
},
|
||||||
|
}, {
|
||||||
|
TestCaseName: "[int] for int16",
|
||||||
|
InitialConfig: &fakeExperimentConfig{},
|
||||||
|
FieldName: "Value",
|
||||||
|
FieldValue: int16(17),
|
||||||
|
ExpectErr: nil,
|
||||||
|
ExpectConfig: &fakeExperimentConfig{
|
||||||
|
Value: 17,
|
||||||
|
},
|
||||||
|
}, {
|
||||||
|
TestCaseName: "[int] for int8",
|
||||||
|
InitialConfig: &fakeExperimentConfig{},
|
||||||
|
FieldName: "Value",
|
||||||
|
FieldValue: int8(17),
|
||||||
|
ExpectErr: nil,
|
||||||
|
ExpectConfig: &fakeExperimentConfig{
|
||||||
|
Value: 17,
|
||||||
|
},
|
||||||
|
}, {
|
||||||
|
TestCaseName: "[int] for string representation of int",
|
||||||
|
InitialConfig: &fakeExperimentConfig{},
|
||||||
|
FieldName: "Value",
|
||||||
|
FieldValue: "17",
|
||||||
|
ExpectErr: nil,
|
||||||
|
ExpectConfig: &fakeExperimentConfig{
|
||||||
|
Value: 17,
|
||||||
|
},
|
||||||
|
}, {
|
||||||
|
TestCaseName: "[int] for invalid string representation of int",
|
||||||
|
InitialConfig: &fakeExperimentConfig{},
|
||||||
|
FieldName: "Value",
|
||||||
|
FieldValue: "xx",
|
||||||
|
ExpectErr: ErrCannotSetIntegerOption,
|
||||||
|
ExpectConfig: &fakeExperimentConfig{},
|
||||||
|
}, {
|
||||||
|
TestCaseName: "[int] for type we don't know how to convert to int",
|
||||||
|
InitialConfig: &fakeExperimentConfig{},
|
||||||
|
FieldName: "Value",
|
||||||
|
FieldValue: make(chan any),
|
||||||
|
ExpectErr: ErrCannotSetIntegerOption,
|
||||||
|
ExpectConfig: &fakeExperimentConfig{},
|
||||||
|
}, {
|
||||||
|
TestCaseName: "[string] for serialized bool value while setting a string value",
|
||||||
|
InitialConfig: &fakeExperimentConfig{},
|
||||||
|
FieldName: "String",
|
||||||
|
FieldValue: "true",
|
||||||
|
ExpectErr: nil,
|
||||||
|
ExpectConfig: &fakeExperimentConfig{
|
||||||
|
String: "true",
|
||||||
|
},
|
||||||
|
}, {
|
||||||
|
TestCaseName: "[string] for serialized int value while setting a string value",
|
||||||
|
InitialConfig: &fakeExperimentConfig{},
|
||||||
|
FieldName: "String",
|
||||||
|
FieldValue: "155",
|
||||||
|
ExpectErr: nil,
|
||||||
|
ExpectConfig: &fakeExperimentConfig{
|
||||||
|
String: "155",
|
||||||
|
},
|
||||||
|
}, {
|
||||||
|
TestCaseName: "[string] for any other string",
|
||||||
|
InitialConfig: &fakeExperimentConfig{},
|
||||||
|
FieldName: "String",
|
||||||
|
FieldValue: "xxx",
|
||||||
|
ExpectErr: nil,
|
||||||
|
ExpectConfig: &fakeExperimentConfig{
|
||||||
|
String: "xxx",
|
||||||
|
},
|
||||||
|
}, {
|
||||||
|
TestCaseName: "[string] for type we don't know how to convert to string",
|
||||||
|
InitialConfig: &fakeExperimentConfig{},
|
||||||
|
FieldName: "String",
|
||||||
|
FieldValue: make(chan any),
|
||||||
|
ExpectErr: ErrCannotSetStringOption,
|
||||||
|
ExpectConfig: &fakeExperimentConfig{},
|
||||||
|
}, {
|
||||||
|
TestCaseName: "for a field that we don't know how to set",
|
||||||
|
InitialConfig: &fakeExperimentConfig{},
|
||||||
|
FieldName: "Chan",
|
||||||
|
FieldValue: make(chan any),
|
||||||
|
ExpectErr: ErrUnsupportedOptionType,
|
||||||
|
ExpectConfig: &fakeExperimentConfig{},
|
||||||
|
}}
|
||||||
|
|
||||||
|
for _, input := range inputs {
|
||||||
|
t.Run(input.TestCaseName, func(t *testing.T) {
|
||||||
|
ec := input.InitialConfig
|
||||||
|
b := &ExperimentBuilder{config: ec}
|
||||||
|
err := b.SetOptionAny(input.FieldName, input.FieldValue)
|
||||||
|
if !errors.Is(err, input.ExpectErr) {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
if diff := cmp.Diff(input.ExpectConfig, ec); diff != "" {
|
||||||
|
t.Fatal(diff)
|
||||||
|
}
|
||||||
|
})
|
||||||
}
|
}
|
||||||
b := &ExperimentBuilder{config: &fiction{}}
|
}
|
||||||
t.Run("we correctly guess a boolean", func(t *testing.T) {
|
|
||||||
if err := b.SetOptionGuessType("Truth", "true"); err != nil {
|
func TestExperimentBuilderSetOptionsAny(t *testing.T) {
|
||||||
t.Fatal(err)
|
b := &ExperimentBuilder{config: &fakeExperimentConfig{}}
|
||||||
}
|
|
||||||
if err := b.SetOptionGuessType("Truth", "false"); err != nil {
|
|
||||||
t.Fatal(err)
|
|
||||||
}
|
|
||||||
if err := b.SetOptionGuessType("Truth", "1234"); err == nil {
|
|
||||||
t.Fatal("expected an error here")
|
|
||||||
}
|
|
||||||
if err := b.SetOptionGuessType("Truth", "yoloyolo"); err == nil {
|
|
||||||
t.Fatal("expected an error here")
|
|
||||||
}
|
|
||||||
})
|
|
||||||
t.Run("we correctly guess an integer", func(t *testing.T) {
|
|
||||||
if err := b.SetOptionGuessType("Value", "true"); err == nil {
|
|
||||||
t.Fatal("expected an error here")
|
|
||||||
}
|
|
||||||
if err := b.SetOptionGuessType("Value", "false"); err == nil {
|
|
||||||
t.Fatal("expected an error here")
|
|
||||||
}
|
|
||||||
if err := b.SetOptionGuessType("Value", "1234"); err != nil {
|
|
||||||
t.Fatal(err)
|
|
||||||
}
|
|
||||||
if err := b.SetOptionGuessType("Value", "yoloyolo"); err == nil {
|
|
||||||
t.Fatal("expected an error here")
|
|
||||||
}
|
|
||||||
})
|
|
||||||
t.Run("we correctly guess a string", func(t *testing.T) {
|
|
||||||
if err := b.SetOptionGuessType("String", "true"); err == nil {
|
|
||||||
t.Fatal("expected an error here")
|
|
||||||
}
|
|
||||||
if err := b.SetOptionGuessType("String", "false"); err == nil {
|
|
||||||
t.Fatal("expected an error here")
|
|
||||||
}
|
|
||||||
if err := b.SetOptionGuessType("String", "1234"); err == nil {
|
|
||||||
t.Fatal("expected an error here")
|
|
||||||
}
|
|
||||||
if err := b.SetOptionGuessType("String", "yoloyolo"); err != nil {
|
|
||||||
t.Fatal(err)
|
|
||||||
}
|
|
||||||
})
|
|
||||||
t.Run("we correctly handle an empty map", func(t *testing.T) {
|
t.Run("we correctly handle an empty map", func(t *testing.T) {
|
||||||
if err := b.SetOptionsGuessType(nil); err != nil {
|
if err := b.SetOptionsAny(nil); err != nil {
|
||||||
t.Fatal(err)
|
t.Fatal(err)
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
|
|
||||||
t.Run("we correctly handle a map containing options", func(t *testing.T) {
|
t.Run("we correctly handle a map containing options", func(t *testing.T) {
|
||||||
f := &fiction{}
|
f := &fakeExperimentConfig{}
|
||||||
privateb := &ExperimentBuilder{config: f}
|
privateb := &ExperimentBuilder{config: f}
|
||||||
opts := map[string]string{
|
opts := map[string]any{
|
||||||
"String": "yoloyolo",
|
"String": "yoloyolo",
|
||||||
"Value": "174",
|
"Value": "174",
|
||||||
"Truth": "true",
|
"Truth": "true",
|
||||||
}
|
}
|
||||||
if err := privateb.SetOptionsGuessType(opts); err != nil {
|
if err := privateb.SetOptionsAny(opts); err != nil {
|
||||||
t.Fatal(err)
|
t.Fatal(err)
|
||||||
}
|
}
|
||||||
if f.String != "yoloyolo" {
|
if f.String != "yoloyolo" {
|
||||||
|
@ -177,14 +333,15 @@ func TestExperimentBuilderSetOptionGuessType(t *testing.T) {
|
||||||
t.Fatal("cannot set bool value")
|
t.Fatal("cannot set bool value")
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
t.Run("we handle mistakes in a map containing options", func(t *testing.T) {
|
|
||||||
opts := map[string]string{
|
t.Run("we handle mistakes in a map containing string options", func(t *testing.T) {
|
||||||
|
opts := map[string]any{
|
||||||
"String": "yoloyolo",
|
"String": "yoloyolo",
|
||||||
"Value": "antani;",
|
"Value": "xx",
|
||||||
"Truth": "true",
|
"Truth": "true",
|
||||||
}
|
}
|
||||||
if err := b.SetOptionsGuessType(opts); err == nil {
|
if err := b.SetOptionsAny(opts); !errors.Is(err, ErrCannotSetIntegerOption) {
|
||||||
t.Fatal("expected an error here")
|
t.Fatal("unexpected err", err)
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
Loading…
Reference in New Issue
Block a user