ooni-probe-cli/internal/engine/experimentbuilder_test.go
Simone Basso 086ae43b15
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
  }
}
```
2022-07-08 11:51:59 +02:00

348 lines
8.8 KiB
Go

package engine
import (
"errors"
"testing"
"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 !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 !errors.Is(err, ErrConfigIsNotAStructPointer) {
t.Fatal("expected an error here")
}
if options != nil {
t.Fatal("expected nil here")
}
})
t.Run("when config is a pointer to struct", func(t *testing.T) {
config := &fakeExperimentConfig{}
b := &ExperimentBuilder{
config: config,
}
options, err := b.Options()
if err != nil {
t.Fatal(err)
}
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 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)
}
})
}
}
func TestExperimentBuilderSetOptionsAny(t *testing.T) {
b := &ExperimentBuilder{config: &fakeExperimentConfig{}}
t.Run("we correctly handle an empty map", func(t *testing.T) {
if err := b.SetOptionsAny(nil); err != nil {
t.Fatal(err)
}
})
t.Run("we correctly handle a map containing options", func(t *testing.T) {
f := &fakeExperimentConfig{}
privateb := &ExperimentBuilder{config: f}
opts := map[string]any{
"String": "yoloyolo",
"Value": "174",
"Truth": "true",
}
if err := privateb.SetOptionsAny(opts); err != nil {
t.Fatal(err)
}
if f.String != "yoloyolo" {
t.Fatal("cannot set string value")
}
if f.Value != 174 {
t.Fatal("cannot set integer value")
}
if f.Truth != true {
t.Fatal("cannot set bool value")
}
})
t.Run("we handle mistakes in a map containing string options", func(t *testing.T) {
opts := map[string]any{
"String": "yoloyolo",
"Value": "xx",
"Truth": "true",
}
if err := b.SetOptionsAny(opts); !errors.Is(err, ErrCannotSetIntegerOption) {
t.Fatal("unexpected err", err)
}
})
}