refactor(engine): allow scripts to register experiments (#860)
See https://github.com/ooni/probe/issues/2216
This commit is contained in:
@@ -0,0 +1,12 @@
|
||||
package registry
|
||||
|
||||
// Where we register all the available experiments.
|
||||
var allexperiments = map[string]*Factory{}
|
||||
|
||||
// ExperimentNames returns the name of all experiments
|
||||
func ExperimentNames() (names []string) {
|
||||
for key := range allexperiments {
|
||||
names = append(names, key)
|
||||
}
|
||||
return
|
||||
}
|
||||
@@ -0,0 +1,23 @@
|
||||
package registry
|
||||
|
||||
//
|
||||
// Registers the `dash' experiment.
|
||||
//
|
||||
|
||||
import (
|
||||
"github.com/ooni/probe-cli/v3/internal/engine/experiment/dash"
|
||||
"github.com/ooni/probe-cli/v3/internal/model"
|
||||
)
|
||||
|
||||
func init() {
|
||||
allexperiments["dash"] = &Factory{
|
||||
build: func(config interface{}) model.ExperimentMeasurer {
|
||||
return dash.NewExperimentMeasurer(
|
||||
*config.(*dash.Config),
|
||||
)
|
||||
},
|
||||
config: &dash.Config{},
|
||||
interruptible: true,
|
||||
inputPolicy: model.InputNone,
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,22 @@
|
||||
package registry
|
||||
|
||||
//
|
||||
// Registers the `dnscheck' experiment.
|
||||
//
|
||||
|
||||
import (
|
||||
"github.com/ooni/probe-cli/v3/internal/engine/experiment/dnscheck"
|
||||
"github.com/ooni/probe-cli/v3/internal/model"
|
||||
)
|
||||
|
||||
func init() {
|
||||
allexperiments["dnscheck"] = &Factory{
|
||||
build: func(config interface{}) model.ExperimentMeasurer {
|
||||
return dnscheck.NewExperimentMeasurer(
|
||||
*config.(*dnscheck.Config),
|
||||
)
|
||||
},
|
||||
config: &dnscheck.Config{},
|
||||
inputPolicy: model.InputOrStaticDefault,
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,22 @@
|
||||
package registry
|
||||
|
||||
//
|
||||
// Registers the `dnsping' experiment.
|
||||
//
|
||||
|
||||
import (
|
||||
"github.com/ooni/probe-cli/v3/internal/engine/experiment/dnsping"
|
||||
"github.com/ooni/probe-cli/v3/internal/model"
|
||||
)
|
||||
|
||||
func init() {
|
||||
allexperiments["dnsping"] = &Factory{
|
||||
build: func(config interface{}) model.ExperimentMeasurer {
|
||||
return dnsping.NewExperimentMeasurer(
|
||||
*config.(*dnsping.Config),
|
||||
)
|
||||
},
|
||||
config: &dnsping.Config{},
|
||||
inputPolicy: model.InputOrStaticDefault,
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,2 @@
|
||||
// Package registry contains a registry of all the available experiments.
|
||||
package registry
|
||||
@@ -0,0 +1,28 @@
|
||||
package registry
|
||||
|
||||
//
|
||||
// Registers the `example' experiment.
|
||||
//
|
||||
|
||||
import (
|
||||
"time"
|
||||
|
||||
"github.com/ooni/probe-cli/v3/internal/engine/experiment/example"
|
||||
"github.com/ooni/probe-cli/v3/internal/model"
|
||||
)
|
||||
|
||||
func init() {
|
||||
allexperiments["example"] = &Factory{
|
||||
build: func(config interface{}) model.ExperimentMeasurer {
|
||||
return example.NewExperimentMeasurer(
|
||||
*config.(*example.Config), "example",
|
||||
)
|
||||
},
|
||||
config: &example.Config{
|
||||
Message: "Good day from the example experiment!",
|
||||
SleepTime: int64(time.Second),
|
||||
},
|
||||
interruptible: true,
|
||||
inputPolicy: model.InputNone,
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,223 @@
|
||||
package registry
|
||||
|
||||
//
|
||||
// Factory for constructing experiments.
|
||||
//
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"fmt"
|
||||
"reflect"
|
||||
"strconv"
|
||||
|
||||
"github.com/iancoleman/strcase"
|
||||
"github.com/ooni/probe-cli/v3/internal/model"
|
||||
)
|
||||
|
||||
// Factory allows to construct an experiment measurer.
|
||||
type Factory struct {
|
||||
// build is the constructor that build an experiment with the given config.
|
||||
build func(config interface{}) model.ExperimentMeasurer
|
||||
|
||||
// config contains the experiment's config.
|
||||
config any
|
||||
|
||||
// inputPolicy contains the experiment's InputPolicy.
|
||||
inputPolicy model.InputPolicy
|
||||
|
||||
// interruptible indicates whether the experiment is interruptible.
|
||||
interruptible bool
|
||||
}
|
||||
|
||||
// Interruptible returns whether the experiment is interruptible.
|
||||
func (b *Factory) Interruptible() bool {
|
||||
return b.interruptible
|
||||
}
|
||||
|
||||
// InputPolicy returns the experiment's InputPolicy.
|
||||
func (b *Factory) InputPolicy() model.InputPolicy {
|
||||
return b.inputPolicy
|
||||
}
|
||||
|
||||
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 the options exposed by this experiment.
|
||||
func (b *Factory) Options() (map[string]model.ExperimentOptionInfo, error) {
|
||||
result := make(map[string]model.ExperimentOptionInfo)
|
||||
ptrinfo := reflect.ValueOf(b.config)
|
||||
if ptrinfo.Kind() != reflect.Ptr {
|
||||
return nil, ErrConfigIsNotAStructPointer
|
||||
}
|
||||
structinfo := ptrinfo.Elem().Type()
|
||||
if structinfo.Kind() != reflect.Struct {
|
||||
return nil, ErrConfigIsNotAStructPointer
|
||||
}
|
||||
for i := 0; i < structinfo.NumField(); i++ {
|
||||
field := structinfo.Field(i)
|
||||
result[field.Name] = model.ExperimentOptionInfo{
|
||||
Doc: field.Tag.Get("ooni"),
|
||||
Type: field.Type.String(),
|
||||
}
|
||||
}
|
||||
return result, nil
|
||||
}
|
||||
|
||||
// setOptionBool sets a bool option.
|
||||
func (b *Factory) 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 *Factory) 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 *Factory) 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 given any value.
|
||||
func (b *Factory) SetOptionAny(key string, value any) error {
|
||||
field, err := b.fieldbyname(b.config, key)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
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)
|
||||
}
|
||||
}
|
||||
|
||||
// SetOptionsAny calls SetOptionAny for each entry inside [options].
|
||||
func (b *Factory) SetOptionsAny(options map[string]any) error {
|
||||
for key, value := range options {
|
||||
if err := b.SetOptionAny(key, value); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// fieldbyname return v's field whose name is equal to the given key.
|
||||
func (b *Factory) 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{}, fmt.Errorf("%w but a %T", ErrConfigIsNotAStructPointer, v)
|
||||
}
|
||||
structinfo := ptrinfo.Elem()
|
||||
if structinfo.Kind() != reflect.Struct {
|
||||
return reflect.Value{}, fmt.Errorf("%w but a %T", ErrConfigIsNotAStructPointer, v)
|
||||
}
|
||||
field := structinfo.FieldByName(key)
|
||||
if !field.IsValid() || !field.CanSet() {
|
||||
return reflect.Value{}, fmt.Errorf("%w: %s", ErrNoSuchField, key)
|
||||
}
|
||||
return field, nil
|
||||
}
|
||||
|
||||
// NewExperimentMeasurer creates the experiment
|
||||
func (b *Factory) NewExperimentMeasurer() model.ExperimentMeasurer {
|
||||
return b.build(b.config)
|
||||
}
|
||||
|
||||
// CanonicalizeExperimentName allows code to provide experiment names
|
||||
// in a more flexible way, where we have aliases.
|
||||
//
|
||||
// Because we allow for uppercase experiment names for backwards
|
||||
// compatibility with MK, we need to add some exceptions here when
|
||||
// mapping (e.g., DNSCheck => dnscheck).
|
||||
func CanonicalizeExperimentName(name string) string {
|
||||
switch name = strcase.ToSnake(name); name {
|
||||
case "ndt_7":
|
||||
name = "ndt" // since 2020-03-18, we use ndt7 to implement ndt by default
|
||||
case "dns_check":
|
||||
name = "dnscheck"
|
||||
case "stun_reachability":
|
||||
name = "stunreachability"
|
||||
default:
|
||||
}
|
||||
return name
|
||||
}
|
||||
|
||||
// NewFactory creates a new Factory instance.
|
||||
func NewFactory(name string) (*Factory, error) {
|
||||
factory := allexperiments[CanonicalizeExperimentName(name)]
|
||||
if factory == nil {
|
||||
return nil, fmt.Errorf("no such experiment: %s", name)
|
||||
}
|
||||
return factory, nil
|
||||
}
|
||||
@@ -0,0 +1,347 @@
|
||||
package registry
|
||||
|
||||
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 := &Factory{
|
||||
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 := &Factory{
|
||||
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 := &Factory{
|
||||
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 := &Factory{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 := &Factory{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 := &Factory{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)
|
||||
}
|
||||
})
|
||||
}
|
||||
@@ -0,0 +1,22 @@
|
||||
package registry
|
||||
|
||||
//
|
||||
// Registers the `fbmessenger' experiment.
|
||||
//
|
||||
|
||||
import (
|
||||
"github.com/ooni/probe-cli/v3/internal/engine/experiment/fbmessenger"
|
||||
"github.com/ooni/probe-cli/v3/internal/model"
|
||||
)
|
||||
|
||||
func init() {
|
||||
allexperiments["facebook_messenger"] = &Factory{
|
||||
build: func(config interface{}) model.ExperimentMeasurer {
|
||||
return fbmessenger.NewExperimentMeasurer(
|
||||
*config.(*fbmessenger.Config),
|
||||
)
|
||||
},
|
||||
config: &fbmessenger.Config{},
|
||||
inputPolicy: model.InputNone,
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,22 @@
|
||||
package registry
|
||||
|
||||
//
|
||||
// Registers the `hhfm' experiment.
|
||||
//
|
||||
|
||||
import (
|
||||
"github.com/ooni/probe-cli/v3/internal/engine/experiment/hhfm"
|
||||
"github.com/ooni/probe-cli/v3/internal/model"
|
||||
)
|
||||
|
||||
func init() {
|
||||
allexperiments["http_header_field_manipulation"] = &Factory{
|
||||
build: func(config interface{}) model.ExperimentMeasurer {
|
||||
return hhfm.NewExperimentMeasurer(
|
||||
*config.(*hhfm.Config),
|
||||
)
|
||||
},
|
||||
config: &hhfm.Config{},
|
||||
inputPolicy: model.InputNone,
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,22 @@
|
||||
package registry
|
||||
|
||||
//
|
||||
// Registers the `hirl' experiment.
|
||||
//
|
||||
|
||||
import (
|
||||
"github.com/ooni/probe-cli/v3/internal/engine/experiment/hirl"
|
||||
"github.com/ooni/probe-cli/v3/internal/model"
|
||||
)
|
||||
|
||||
func init() {
|
||||
allexperiments["http_invalid_request_line"] = &Factory{
|
||||
build: func(config interface{}) model.ExperimentMeasurer {
|
||||
return hirl.NewExperimentMeasurer(
|
||||
*config.(*hirl.Config),
|
||||
)
|
||||
},
|
||||
config: &hirl.Config{},
|
||||
inputPolicy: model.InputNone,
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,22 @@
|
||||
package registry
|
||||
|
||||
//
|
||||
// Registers the `httphostheader' experiment.
|
||||
//
|
||||
|
||||
import (
|
||||
"github.com/ooni/probe-cli/v3/internal/engine/experiment/httphostheader"
|
||||
"github.com/ooni/probe-cli/v3/internal/model"
|
||||
)
|
||||
|
||||
func init() {
|
||||
allexperiments["http_host_header"] = &Factory{
|
||||
build: func(config interface{}) model.ExperimentMeasurer {
|
||||
return httphostheader.NewExperimentMeasurer(
|
||||
*config.(*httphostheader.Config),
|
||||
)
|
||||
},
|
||||
config: &httphostheader.Config{},
|
||||
inputPolicy: model.InputOrQueryBackend,
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,23 @@
|
||||
package registry
|
||||
|
||||
//
|
||||
// Registers the `ndt' experiment.
|
||||
//
|
||||
|
||||
import (
|
||||
"github.com/ooni/probe-cli/v3/internal/engine/experiment/ndt7"
|
||||
"github.com/ooni/probe-cli/v3/internal/model"
|
||||
)
|
||||
|
||||
func init() {
|
||||
allexperiments["ndt"] = &Factory{
|
||||
build: func(config interface{}) model.ExperimentMeasurer {
|
||||
return ndt7.NewExperimentMeasurer(
|
||||
*config.(*ndt7.Config),
|
||||
)
|
||||
},
|
||||
config: &ndt7.Config{},
|
||||
interruptible: true,
|
||||
inputPolicy: model.InputNone,
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,22 @@
|
||||
package registry
|
||||
|
||||
//
|
||||
// Registers the `psiphon' experiment.
|
||||
//
|
||||
|
||||
import (
|
||||
"github.com/ooni/probe-cli/v3/internal/engine/experiment/psiphon"
|
||||
"github.com/ooni/probe-cli/v3/internal/model"
|
||||
)
|
||||
|
||||
func init() {
|
||||
allexperiments["psiphon"] = &Factory{
|
||||
build: func(config interface{}) model.ExperimentMeasurer {
|
||||
return psiphon.NewExperimentMeasurer(
|
||||
*config.(*psiphon.Config),
|
||||
)
|
||||
},
|
||||
config: &psiphon.Config{},
|
||||
inputPolicy: model.InputOptional,
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,22 @@
|
||||
package registry
|
||||
|
||||
//
|
||||
// Registers the `quicping' experiment.
|
||||
//
|
||||
|
||||
import (
|
||||
"github.com/ooni/probe-cli/v3/internal/engine/experiment/quicping"
|
||||
"github.com/ooni/probe-cli/v3/internal/model"
|
||||
)
|
||||
|
||||
func init() {
|
||||
allexperiments["quicping"] = &Factory{
|
||||
build: func(config interface{}) model.ExperimentMeasurer {
|
||||
return quicping.NewExperimentMeasurer(
|
||||
*config.(*quicping.Config),
|
||||
)
|
||||
},
|
||||
config: &quicping.Config{},
|
||||
inputPolicy: model.InputStrictlyRequired,
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,22 @@
|
||||
package registry
|
||||
|
||||
//
|
||||
// Registers the `riseupvpn' experiment.
|
||||
//
|
||||
|
||||
import (
|
||||
"github.com/ooni/probe-cli/v3/internal/engine/experiment/riseupvpn"
|
||||
"github.com/ooni/probe-cli/v3/internal/model"
|
||||
)
|
||||
|
||||
func init() {
|
||||
allexperiments["riseupvpn"] = &Factory{
|
||||
build: func(config interface{}) model.ExperimentMeasurer {
|
||||
return riseupvpn.NewExperimentMeasurer(
|
||||
*config.(*riseupvpn.Config),
|
||||
)
|
||||
},
|
||||
config: &riseupvpn.Config{},
|
||||
inputPolicy: model.InputNone,
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,22 @@
|
||||
package registry
|
||||
|
||||
//
|
||||
// Registers the `run' experiment.
|
||||
//
|
||||
|
||||
import (
|
||||
"github.com/ooni/probe-cli/v3/internal/engine/experiment/run"
|
||||
"github.com/ooni/probe-cli/v3/internal/model"
|
||||
)
|
||||
|
||||
func init() {
|
||||
allexperiments["run"] = &Factory{
|
||||
build: func(config interface{}) model.ExperimentMeasurer {
|
||||
return run.NewExperimentMeasurer(
|
||||
*config.(*run.Config),
|
||||
)
|
||||
},
|
||||
config: &run.Config{},
|
||||
inputPolicy: model.InputStrictlyRequired,
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,22 @@
|
||||
package registry
|
||||
|
||||
//
|
||||
// Registers the `signal' experiment.
|
||||
//
|
||||
|
||||
import (
|
||||
"github.com/ooni/probe-cli/v3/internal/engine/experiment/signal"
|
||||
"github.com/ooni/probe-cli/v3/internal/model"
|
||||
)
|
||||
|
||||
func init() {
|
||||
allexperiments["signal"] = &Factory{
|
||||
build: func(config interface{}) model.ExperimentMeasurer {
|
||||
return signal.NewExperimentMeasurer(
|
||||
*config.(*signal.Config),
|
||||
)
|
||||
},
|
||||
config: &signal.Config{},
|
||||
inputPolicy: model.InputNone,
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,22 @@
|
||||
package registry
|
||||
|
||||
//
|
||||
// Registers the `simplequicping' experiment.
|
||||
//
|
||||
|
||||
import (
|
||||
"github.com/ooni/probe-cli/v3/internal/engine/experiment/simplequicping"
|
||||
"github.com/ooni/probe-cli/v3/internal/model"
|
||||
)
|
||||
|
||||
func init() {
|
||||
allexperiments["simplequicping"] = &Factory{
|
||||
build: func(config interface{}) model.ExperimentMeasurer {
|
||||
return simplequicping.NewExperimentMeasurer(
|
||||
*config.(*simplequicping.Config),
|
||||
)
|
||||
},
|
||||
config: &simplequicping.Config{},
|
||||
inputPolicy: model.InputStrictlyRequired,
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,22 @@
|
||||
package registry
|
||||
|
||||
//
|
||||
// Registers the `sniblocking' experiment.
|
||||
//
|
||||
|
||||
import (
|
||||
"github.com/ooni/probe-cli/v3/internal/engine/experiment/sniblocking"
|
||||
"github.com/ooni/probe-cli/v3/internal/model"
|
||||
)
|
||||
|
||||
func init() {
|
||||
allexperiments["sni_blocking"] = &Factory{
|
||||
build: func(config interface{}) model.ExperimentMeasurer {
|
||||
return sniblocking.NewExperimentMeasurer(
|
||||
*config.(*sniblocking.Config),
|
||||
)
|
||||
},
|
||||
config: &sniblocking.Config{},
|
||||
inputPolicy: model.InputOrQueryBackend,
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,22 @@
|
||||
package registry
|
||||
|
||||
//
|
||||
// Registers the `stunreachability' experiment.
|
||||
//
|
||||
|
||||
import (
|
||||
"github.com/ooni/probe-cli/v3/internal/engine/experiment/stunreachability"
|
||||
"github.com/ooni/probe-cli/v3/internal/model"
|
||||
)
|
||||
|
||||
func init() {
|
||||
allexperiments["stunreachability"] = &Factory{
|
||||
build: func(config interface{}) model.ExperimentMeasurer {
|
||||
return stunreachability.NewExperimentMeasurer(
|
||||
*config.(*stunreachability.Config),
|
||||
)
|
||||
},
|
||||
config: &stunreachability.Config{},
|
||||
inputPolicy: model.InputOrStaticDefault,
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,22 @@
|
||||
package registry
|
||||
|
||||
//
|
||||
// Registers the `tcpping' experiment.
|
||||
//
|
||||
|
||||
import (
|
||||
"github.com/ooni/probe-cli/v3/internal/engine/experiment/tcpping"
|
||||
"github.com/ooni/probe-cli/v3/internal/model"
|
||||
)
|
||||
|
||||
func init() {
|
||||
allexperiments["tcpping"] = &Factory{
|
||||
build: func(config interface{}) model.ExperimentMeasurer {
|
||||
return tcpping.NewExperimentMeasurer(
|
||||
*config.(*tcpping.Config),
|
||||
)
|
||||
},
|
||||
config: &tcpping.Config{},
|
||||
inputPolicy: model.InputStrictlyRequired,
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,23 @@
|
||||
package registry
|
||||
|
||||
//
|
||||
// Registers the `telegram' experiment.
|
||||
//
|
||||
|
||||
import (
|
||||
"github.com/ooni/probe-cli/v3/internal/engine/experiment/telegram"
|
||||
"github.com/ooni/probe-cli/v3/internal/model"
|
||||
)
|
||||
|
||||
func init() {
|
||||
allexperiments["telegram"] = &Factory{
|
||||
build: func(config any) model.ExperimentMeasurer {
|
||||
return telegram.NewExperimentMeasurer(
|
||||
config.(telegram.Config),
|
||||
)
|
||||
},
|
||||
config: &telegram.Config{},
|
||||
interruptible: false,
|
||||
inputPolicy: model.InputNone,
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,22 @@
|
||||
package registry
|
||||
|
||||
//
|
||||
// Registers the `tlsping' experiment.
|
||||
//
|
||||
|
||||
import (
|
||||
"github.com/ooni/probe-cli/v3/internal/engine/experiment/tlsping"
|
||||
"github.com/ooni/probe-cli/v3/internal/model"
|
||||
)
|
||||
|
||||
func init() {
|
||||
allexperiments["tlsping"] = &Factory{
|
||||
build: func(config interface{}) model.ExperimentMeasurer {
|
||||
return tlsping.NewExperimentMeasurer(
|
||||
*config.(*tlsping.Config),
|
||||
)
|
||||
},
|
||||
config: &tlsping.Config{},
|
||||
inputPolicy: model.InputStrictlyRequired,
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,22 @@
|
||||
package registry
|
||||
|
||||
//
|
||||
// Registers the `tlstool' experiment.
|
||||
//
|
||||
|
||||
import (
|
||||
"github.com/ooni/probe-cli/v3/internal/engine/experiment/tlstool"
|
||||
"github.com/ooni/probe-cli/v3/internal/model"
|
||||
)
|
||||
|
||||
func init() {
|
||||
allexperiments["tlstool"] = &Factory{
|
||||
build: func(config interface{}) model.ExperimentMeasurer {
|
||||
return tlstool.NewExperimentMeasurer(
|
||||
*config.(*tlstool.Config),
|
||||
)
|
||||
},
|
||||
config: &tlstool.Config{},
|
||||
inputPolicy: model.InputOrQueryBackend,
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,22 @@
|
||||
package registry
|
||||
|
||||
//
|
||||
// Registers the `tor' experiment.
|
||||
//
|
||||
|
||||
import (
|
||||
"github.com/ooni/probe-cli/v3/internal/engine/experiment/tor"
|
||||
"github.com/ooni/probe-cli/v3/internal/model"
|
||||
)
|
||||
|
||||
func init() {
|
||||
allexperiments["tor"] = &Factory{
|
||||
build: func(config interface{}) model.ExperimentMeasurer {
|
||||
return tor.NewExperimentMeasurer(
|
||||
*config.(*tor.Config),
|
||||
)
|
||||
},
|
||||
config: &tor.Config{},
|
||||
inputPolicy: model.InputNone,
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,22 @@
|
||||
package registry
|
||||
|
||||
//
|
||||
// Registers the `torsf' experiment.
|
||||
//
|
||||
|
||||
import (
|
||||
"github.com/ooni/probe-cli/v3/internal/engine/experiment/torsf"
|
||||
"github.com/ooni/probe-cli/v3/internal/model"
|
||||
)
|
||||
|
||||
func init() {
|
||||
allexperiments["torsf"] = &Factory{
|
||||
build: func(config interface{}) model.ExperimentMeasurer {
|
||||
return torsf.NewExperimentMeasurer(
|
||||
*config.(*torsf.Config),
|
||||
)
|
||||
},
|
||||
config: &torsf.Config{},
|
||||
inputPolicy: model.InputNone,
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,22 @@
|
||||
package registry
|
||||
|
||||
//
|
||||
// Registers the `urlgetter' experiment.
|
||||
//
|
||||
|
||||
import (
|
||||
"github.com/ooni/probe-cli/v3/internal/engine/experiment/urlgetter"
|
||||
"github.com/ooni/probe-cli/v3/internal/model"
|
||||
)
|
||||
|
||||
func init() {
|
||||
allexperiments["urlgetter"] = &Factory{
|
||||
build: func(config interface{}) model.ExperimentMeasurer {
|
||||
return urlgetter.NewExperimentMeasurer(
|
||||
*config.(*urlgetter.Config),
|
||||
)
|
||||
},
|
||||
config: &urlgetter.Config{},
|
||||
inputPolicy: model.InputStrictlyRequired,
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,22 @@
|
||||
package registry
|
||||
|
||||
//
|
||||
// Registers the `vanilla_tor' experiment.
|
||||
//
|
||||
|
||||
import (
|
||||
"github.com/ooni/probe-cli/v3/internal/engine/experiment/vanillator"
|
||||
"github.com/ooni/probe-cli/v3/internal/model"
|
||||
)
|
||||
|
||||
func init() {
|
||||
allexperiments["vanilla_tor"] = &Factory{
|
||||
build: func(config interface{}) model.ExperimentMeasurer {
|
||||
return vanillator.NewExperimentMeasurer(
|
||||
*config.(*vanillator.Config),
|
||||
)
|
||||
},
|
||||
config: &vanillator.Config{},
|
||||
inputPolicy: model.InputNone,
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,23 @@
|
||||
package registry
|
||||
|
||||
//
|
||||
// Registers the `web_connectivity' experiment.
|
||||
//
|
||||
|
||||
import (
|
||||
"github.com/ooni/probe-cli/v3/internal/engine/experiment/webconnectivity"
|
||||
"github.com/ooni/probe-cli/v3/internal/model"
|
||||
)
|
||||
|
||||
func init() {
|
||||
allexperiments["web_connectivity"] = &Factory{
|
||||
build: func(config any) model.ExperimentMeasurer {
|
||||
return webconnectivity.NewExperimentMeasurer(
|
||||
config.(webconnectivity.Config),
|
||||
)
|
||||
},
|
||||
config: &webconnectivity.Config{},
|
||||
interruptible: false,
|
||||
inputPolicy: model.InputOrQueryBackend,
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,22 @@
|
||||
package registry
|
||||
|
||||
//
|
||||
// Registers the `whatsapp' experiment.
|
||||
//
|
||||
|
||||
import (
|
||||
"github.com/ooni/probe-cli/v3/internal/engine/experiment/whatsapp"
|
||||
"github.com/ooni/probe-cli/v3/internal/model"
|
||||
)
|
||||
|
||||
func init() {
|
||||
allexperiments["whatsapp"] = &Factory{
|
||||
build: func(config interface{}) model.ExperimentMeasurer {
|
||||
return whatsapp.NewExperimentMeasurer(
|
||||
*config.(*whatsapp.Config),
|
||||
)
|
||||
},
|
||||
config: &whatsapp.Config{},
|
||||
inputPolicy: model.InputNone,
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user