refactor(engine): more abstract Experiment{,Builder} (#838)

This diff modifies the engine package to make Experiment and
ExperimentBuilder interfaces rather than structs.

The previosuly existing structs are now named experiment{,Builder}.

This diff helps https://github.com/ooni/probe/issues/2184
because it allows us to write unit tests more easily.

There should be no functional change.

While there, I removed a bunch of deprecated functions, which were
unnecessarily complicate the implementation and could be easily
replaced by passing them a context.Context or context.Background().
This commit is contained in:
Simone Basso 2022-07-08 12:29:23 +02:00 committed by GitHub
commit 97864b324f
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
11 changed files with 325 additions and 283 deletions

View file

@ -1,5 +1,9 @@
package engine
//
// ExperimentBuilder definition and implementation
//
import (
"errors"
"fmt"
@ -42,23 +46,60 @@ const (
InputOrStaticDefault = InputPolicy("or_static_default")
)
// ExperimentBuilder is an experiment builder.
type ExperimentBuilder struct {
build func(interface{}) *Experiment
callbacks model.ExperimentCallbacks
config interface{}
inputPolicy InputPolicy
// ExperimentBuilder builds an experiment.
type ExperimentBuilder interface {
// Interruptible tells you whether this is an interruptible experiment. This kind
// of experiments (e.g. ndt7) may be interrupted mid way.
Interruptible() bool
// InputPolicy returns the experiment input policy.
InputPolicy() InputPolicy
// Options returns information about the experiment's options.
Options() (map[string]OptionInfo, error)
// 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.
SetOptionAny(key string, value any) error
// SetOptionsAny sets options from a map[string]any. See the documentation of
// the SetOptionAny method for more information.
SetOptionsAny(options map[string]any) error
// SetCallbacks sets the experiment's interactive callbacks.
SetCallbacks(callbacks model.ExperimentCallbacks)
// NewExperiment creates the experiment instance.
NewExperiment() Experiment
}
// experimentBuilder implements ExperimentBuilder.
type experimentBuilder struct {
// build is the constructor that build an experiment with the given config.
build func(config interface{}) *experiment
// callbacks contains callbacks for the new experiment.
callbacks model.ExperimentCallbacks
// config contains the experiment's config.
config interface{}
// inputPolicy contains the experiment's InputPolicy.
inputPolicy InputPolicy
// interruptible indicates whether the experiment is interruptible.
interruptible bool
}
// Interruptible tells you whether this is an interruptible experiment. This kind
// of experiments (e.g. ndt7) may be interrupted mid way.
func (b *ExperimentBuilder) Interruptible() bool {
// Interruptible implements ExperimentBuilder.Interruptible.
func (b *experimentBuilder) Interruptible() bool {
return b.interruptible
}
// InputPolicy returns the experiment input policy
func (b *ExperimentBuilder) InputPolicy() InputPolicy {
// InputPolicy implements ExperimentBuilder.InputPolicy.
func (b *experimentBuilder) InputPolicy() InputPolicy {
return b.inputPolicy
}
@ -96,8 +137,8 @@ var (
ErrUnsupportedOptionType = errors.New("unsupported option type")
)
// Options returns info about all options
func (b *ExperimentBuilder) Options() (map[string]OptionInfo, error) {
// Options implements ExperimentBuilder.Options.
func (b *experimentBuilder) Options() (map[string]OptionInfo, error) {
result := make(map[string]OptionInfo)
ptrinfo := reflect.ValueOf(b.config)
if ptrinfo.Kind() != reflect.Ptr {
@ -118,7 +159,7 @@ func (b *ExperimentBuilder) Options() (map[string]OptionInfo, error) {
}
// setOptionBool sets a bool option.
func (b *ExperimentBuilder) setOptionBool(field reflect.Value, value any) error {
func (b *experimentBuilder) setOptionBool(field reflect.Value, value any) error {
switch v := value.(type) {
case bool:
field.SetBool(v)
@ -135,7 +176,7 @@ func (b *ExperimentBuilder) setOptionBool(field reflect.Value, value any) error
}
// setOptionInt sets an int option
func (b *ExperimentBuilder) setOptionInt(field reflect.Value, value any) error {
func (b *experimentBuilder) setOptionInt(field reflect.Value, value any) error {
switch v := value.(type) {
case int64:
field.SetInt(v)
@ -165,7 +206,7 @@ func (b *ExperimentBuilder) setOptionInt(field reflect.Value, value any) error {
}
// setOptionString sets a string option
func (b *ExperimentBuilder) setOptionString(field reflect.Value, value any) error {
func (b *experimentBuilder) setOptionString(field reflect.Value, value any) error {
switch v := value.(type) {
case string:
field.SetString(v)
@ -175,11 +216,8 @@ func (b *ExperimentBuilder) setOptionString(field reflect.Value, value any) erro
}
}
// 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 {
// SetOptionAny implements ExperimentBuilder.SetOptionAny.
func (b *experimentBuilder) SetOptionAny(key string, value any) error {
field, err := b.fieldbyname(b.config, key)
if err != nil {
return err
@ -196,9 +234,8 @@ func (b *ExperimentBuilder) SetOptionAny(key string, value any) error {
}
}
// 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 {
// SetOptionsAny implements ExperimentBuilder.SetOptionsAny.
func (b *experimentBuilder) SetOptionsAny(options map[string]any) error {
for key, value := range options {
if err := b.SetOptionAny(key, value); err != nil {
return err
@ -207,12 +244,13 @@ func (b *ExperimentBuilder) SetOptionsAny(options map[string]any) error {
return nil
}
// SetCallbacks sets the interactive callbacks
func (b *ExperimentBuilder) SetCallbacks(callbacks model.ExperimentCallbacks) {
// SetCallbacks implements ExperimentBuilder.SetCallbacks.
func (b *experimentBuilder) SetCallbacks(callbacks model.ExperimentCallbacks) {
b.callbacks = callbacks
}
func (b *ExperimentBuilder) fieldbyname(v interface{}, key string) (reflect.Value, error) {
// fieldbyname return v's field whose name is equal to the given key.
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 {
@ -230,7 +268,7 @@ func (b *ExperimentBuilder) fieldbyname(v interface{}, key string) (reflect.Valu
}
// NewExperiment creates the experiment
func (b *ExperimentBuilder) NewExperiment() *Experiment {
func (b *experimentBuilder) NewExperiment() Experiment {
experiment := b.build(b.config)
experiment.callbacks = b.callbacks
return experiment
@ -255,7 +293,8 @@ func canonicalizeExperimentName(name string) string {
return name
}
func newExperimentBuilder(session *Session, name string) (*ExperimentBuilder, error) {
// newExperimentBuilder creates a new experimentBuilder instance.
func newExperimentBuilder(session *Session, name string) (*experimentBuilder, error) {
factory := experimentsByName[canonicalizeExperimentName(name)]
if factory == nil {
return nil, fmt.Errorf("no such experiment: %s", name)