diff --git a/internal/cmd/miniooni/libminiooni.go b/internal/cmd/miniooni/libminiooni.go index 08c7d82..9de2456 100644 --- a/internal/cmd/miniooni/libminiooni.go +++ b/internal/cmd/miniooni/libminiooni.go @@ -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") @@ -233,15 +243,15 @@ Do you consent to OONI Probe data collection? OONI Probe collects evidence of internet censorship and measures network performance: - + - OONI Probe will likely test objectionable sites and services; - + - Anyone monitoring your internet activity (such as a government or Internet provider) may be able to tell that you are using OONI Probe; - + - The network data you collect will be published automatically unless you use miniooni's -n command line flag. - + To learn more, see https://ooni.org/about/risks/. If you're onboard, re-run the same command and add the --yes flag, to @@ -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() diff --git a/internal/engine/experiment_integration_test.go b/internal/engine/experiment_integration_test.go index 21701b8..2114d6d 100644 --- a/internal/engine/experiment_integration_test.go +++ b/internal/engine/experiment_integration_test.go @@ -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 := ®isterCallbacksCalled{} @@ -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) diff --git a/internal/engine/experimentbuilder.go b/internal/engine/experimentbuilder.go index 26fe32b..2b5f651 100644 --- a/internal/engine/experimentbuilder.go +++ b/internal/engine/experimentbuilder.go @@ -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 } diff --git a/internal/engine/experimentbuilder_test.go b/internal/engine/experimentbuilder_test.go index 6762a63..39a439e 100644 --- a/internal/engine/experimentbuilder_test.go +++ b/internal/engine/experimentbuilder_test.go @@ -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) } }) }