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:
Simone Basso 2022-07-08 11:51:59 +02:00 committed by GitHub
parent 6019b25baf
commit 086ae43b15
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
4 changed files with 416 additions and 199 deletions

View File

@ -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)
for _, opt := range input {
key, value, err := split(opt)
@ -186,6 +186,16 @@ func mustMakeMap(input []string) (output map[string]string) {
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 {
rv, err := url.Parse(URL)
fatalOnError(err, "cannot parse URL")
@ -296,8 +306,8 @@ func MainWithConfiguration(experimentName string, currentOptions Options) {
ctx := context.Background()
extraOptions := mustMakeMap(currentOptions.ExtraOptions)
annotations := mustMakeMap(currentOptions.Annotations)
extraOptions := mustMakeMapAny(currentOptions.ExtraOptions)
annotations := mustMakeMapString(currentOptions.Annotations)
logger := &log.Logger{Level: log.InfoLevel, Handler: &logHandler{Writer: os.Stderr}}
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")
experiment := builder.NewExperiment()

View File

@ -157,7 +157,7 @@ func TestSetCallbacks(t *testing.T) {
if err != nil {
t.Fatal(err)
}
if err := builder.SetOptionInt("SleepTime", 0); err != nil {
if err := builder.SetOptionAny("SleepTime", 0); err != nil {
t.Fatal(err)
}
register := &registerCallbacksCalled{}
@ -203,7 +203,7 @@ func TestMeasurementFailure(t *testing.T) {
if err != nil {
t.Fatal(err)
}
if err := builder.SetOptionBool("ReturnError", true); err != nil {
if err := builder.SetOptionAny("ReturnError", true); err != nil {
t.Fatal(err)
}
measurement, err := builder.NewExperiment().Measure("")
@ -279,13 +279,13 @@ func TestUseOptions(t *testing.T) {
if !sleepTime {
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")
}
if err := builder.SetOptionInt("SleepTime", 10); err != nil {
if err := builder.SetOptionAny("SleepTime", 10); err != nil {
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")
}
config := builder.config.(*example.Config)

View File

@ -4,7 +4,6 @@ import (
"errors"
"fmt"
"reflect"
"regexp"
"strconv"
"github.com/iancoleman/strcase"
@ -63,22 +62,50 @@ func (b *ExperimentBuilder) InputPolicy() InputPolicy {
return b.inputPolicy
}
// OptionInfo contains info about an option
// OptionInfo contains info about an option.
type OptionInfo struct {
Doc string
// Doc contains the documentation.
Doc string
// Type contains the type.
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
func (b *ExperimentBuilder) Options() (map[string]OptionInfo, error) {
result := make(map[string]OptionInfo)
ptrinfo := reflect.ValueOf(b.config)
if ptrinfo.Kind() != reflect.Ptr {
return nil, errors.New("config is not a pointer")
return nil, ErrConfigIsNotAStructPointer
}
structinfo := ptrinfo.Elem().Type()
if structinfo.Kind() != reflect.Struct {
return nil, errors.New("config is not a struct")
return nil, ErrConfigIsNotAStructPointer
}
for i := 0; i < structinfo.NumField(); i++ {
field := structinfo.Field(i)
@ -90,67 +117,90 @@ func (b *ExperimentBuilder) Options() (map[string]OptionInfo, error) {
return result, nil
}
// SetOptionBool sets a bool option
func (b *ExperimentBuilder) SetOptionBool(key string, value bool) error {
field, err := fieldbyname(b.config, key)
// setOptionBool sets a bool option.
func (b *ExperimentBuilder) setOptionBool(field reflect.Value, value any) error {
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 {
return err
}
if field.Kind() != reflect.Bool {
return errors.New("field is not a bool")
switch field.Kind() {
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
func (b *ExperimentBuilder) SetOptionInt(key string, value int64) error {
field, err := fieldbyname(b.config, key)
if err != nil {
return err
}
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 {
// SetOptionsAny sets options from a map[string]any. See the documentation of
// the SetOptionAny function for more information.
func (b *ExperimentBuilder) SetOptionsAny(options map[string]any) error {
for key, value := range options {
if err := b.SetOptionAny(key, value); err != nil {
return err
}
}
@ -162,19 +212,19 @@ func (b *ExperimentBuilder) SetCallbacks(callbacks model.ExperimentCallbacks) {
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
ptrinfo := reflect.ValueOf(v)
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()
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)
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
}

View File

@ -1,170 +1,326 @@
package engine
import (
"errors"
"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) {
t.Run("when config is not a pointer", func(t *testing.T) {
b := &ExperimentBuilder{
config: 17,
}
options, err := b.Options()
if err == nil {
if !errors.Is(err, ErrConfigIsNotAStructPointer) {
t.Fatal("expected an error here")
}
if options != nil {
t.Fatal("expected nil here")
}
})
t.Run("when config is not a struct", func(t *testing.T) {
number := 17
b := &ExperimentBuilder{
config: &number,
}
options, err := b.Options()
if err == nil {
if !errors.Is(err, ErrConfigIsNotAStructPointer) {
t.Fatal("expected an error here")
}
if options != nil {
t.Fatal("expected nil here")
}
})
}
func TestExperimentBuilderSetOption(t *testing.T) {
t.Run("when config is not a pointer", func(t *testing.T) {
t.Run("when config is a pointer to struct", func(t *testing.T) {
config := &fakeExperimentConfig{}
b := &ExperimentBuilder{
config: 17,
config: config,
}
if err := b.SetOptionBool("antani", false); err == nil {
t.Fatal("expected an error here")
options, err := b.Options()
if err != nil {
t.Fatal(err)
}
})
t.Run("when config is not a struct", func(t *testing.T) {
number := 17
b := &ExperimentBuilder{
config: &number,
}
if err := b.SetOptionBool("antani", false); err == nil {
t.Fatal("expected an error here")
}
})
t.Run("when field is not valid", func(t *testing.T) {
b := &ExperimentBuilder{
config: &ExperimentBuilder{},
}
if err := b.SetOptionBool("antani", false); err == nil {
t.Fatal("expected an error here")
}
})
t.Run("when field is not bool", func(t *testing.T) {
b := &ExperimentBuilder{
config: new(example.Config),
}
if err := b.SetOptionBool("Message", false); err == nil {
t.Fatal("expected an error here")
}
})
t.Run("when field is not string", func(t *testing.T) {
b := &ExperimentBuilder{
config: new(example.Config),
}
if err := b.SetOptionString("ReturnError", "xx"); err == nil {
t.Fatal("expected an error here")
}
})
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")
for name, value := range options {
switch name {
case "Chan":
if value.Doc != "we cannot set this" {
t.Fatal("invalid doc")
}
if value.Type != "chan interface {}" {
t.Fatal("invalid type", value.Type)
}
case "String":
if value.Doc != "a string" {
t.Fatal("invalid doc")
}
if value.Type != "string" {
t.Fatal("invalid type", value.Type)
}
case "Truth":
if value.Doc != "something that no-one knows" {
t.Fatal("invalid doc")
}
if value.Type != "bool" {
t.Fatal("invalid type", value.Type)
}
case "Value":
if value.Doc != "a number" {
t.Fatal("invalid doc")
}
if value.Type != "int64" {
t.Fatal("invalid type", value.Type)
}
default:
t.Fatal("unknown name", name)
}
}
})
}
func TestExperimentBuilderSetOptionGuessType(t *testing.T) {
type fiction struct {
String string
Truth bool
Value int64
func TestExperimentBuilderSetOptionAny(t *testing.T) {
var inputs = []struct {
TestCaseName string
InitialConfig any
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 {
t.Fatal(err)
}
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)
}
})
}
func TestExperimentBuilderSetOptionsAny(t *testing.T) {
b := &ExperimentBuilder{config: &fakeExperimentConfig{}}
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.Run("we correctly handle a map containing options", func(t *testing.T) {
f := &fiction{}
f := &fakeExperimentConfig{}
privateb := &ExperimentBuilder{config: f}
opts := map[string]string{
opts := map[string]any{
"String": "yoloyolo",
"Value": "174",
"Truth": "true",
}
if err := privateb.SetOptionsGuessType(opts); err != nil {
if err := privateb.SetOptionsAny(opts); err != nil {
t.Fatal(err)
}
if f.String != "yoloyolo" {
@ -177,14 +333,15 @@ func TestExperimentBuilderSetOptionGuessType(t *testing.T) {
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",
"Value": "antani;",
"Value": "xx",
"Truth": "true",
}
if err := b.SetOptionsGuessType(opts); err == nil {
t.Fatal("expected an error here")
if err := b.SetOptionsAny(opts); !errors.Is(err, ErrCannotSetIntegerOption) {
t.Fatal("unexpected err", err)
}
})
}