diff --git a/cmd/ooniprobe/internal/nettests/dnscheck.go b/cmd/ooniprobe/internal/nettests/dnscheck.go index 7fed35f..54bf283 100644 --- a/cmd/ooniprobe/internal/nettests/dnscheck.go +++ b/cmd/ooniprobe/internal/nettests/dnscheck.go @@ -16,7 +16,7 @@ func (n DNSCheck) lookupURLs(ctl *Controller) ([]string, error) { // not needed because we have default static input in the engine }, ExperimentName: "dnscheck", - InputPolicy: engine.InputOrStaticDefault, + InputPolicy: model.InputOrStaticDefault, Session: ctl.Session, SourceFiles: ctl.InputFiles, StaticInputs: ctl.Inputs, diff --git a/cmd/ooniprobe/internal/nettests/nettests.go b/cmd/ooniprobe/internal/nettests/nettests.go index 2a0a275..3809f8b 100644 --- a/cmd/ooniprobe/internal/nettests/nettests.go +++ b/cmd/ooniprobe/internal/nettests/nettests.go @@ -123,7 +123,7 @@ func (c *Controller) SetNettestIndex(i, n int) { // // This function will continue to run in most cases but will // immediately halt if something's wrong with the file system. -func (c *Controller) Run(builder engine.ExperimentBuilder, inputs []string) error { +func (c *Controller) Run(builder model.ExperimentBuilder, inputs []string) error { // This will configure the controller as handler for the callbacks // called by ooni/probe-engine/experiment.Experiment. builder.SetCallbacks(model.ExperimentCallbacks(c)) diff --git a/cmd/ooniprobe/internal/nettests/stunreachability.go b/cmd/ooniprobe/internal/nettests/stunreachability.go index 76baa29..6b8fe1c 100644 --- a/cmd/ooniprobe/internal/nettests/stunreachability.go +++ b/cmd/ooniprobe/internal/nettests/stunreachability.go @@ -16,7 +16,7 @@ func (n STUNReachability) lookupURLs(ctl *Controller) ([]string, error) { // not needed because we have default static input in the engine }, ExperimentName: "stunreachability", - InputPolicy: engine.InputOrStaticDefault, + InputPolicy: model.InputOrStaticDefault, Session: ctl.Session, SourceFiles: ctl.InputFiles, StaticInputs: ctl.Inputs, diff --git a/cmd/ooniprobe/internal/nettests/web_connectivity.go b/cmd/ooniprobe/internal/nettests/web_connectivity.go index 9a89b5d..fd681c3 100644 --- a/cmd/ooniprobe/internal/nettests/web_connectivity.go +++ b/cmd/ooniprobe/internal/nettests/web_connectivity.go @@ -22,7 +22,7 @@ func (n WebConnectivity) lookupURLs(ctl *Controller, categories []string) ([]str }, }, ExperimentName: "web_connectivity", - InputPolicy: engine.InputOrQueryBackend, + InputPolicy: model.InputOrQueryBackend, Session: ctl.Session, SourceFiles: ctl.InputFiles, StaticInputs: ctl.Inputs, diff --git a/internal/engine/allexperiments.go b/internal/engine/allexperiments.go index d3471d7..ae4cb56 100644 --- a/internal/engine/allexperiments.go +++ b/internal/engine/allexperiments.go @@ -1,378 +1,17 @@ package engine // -// List of all implemented experiments +// List of all implemented experiments. +// +// Note: if you're looking for a way to register a new experiment, we +// now use the internal/registry package for this purpose. +// +// (This comment will eventually autodestruct.) // -import ( - "time" - - "github.com/ooni/probe-cli/v3/internal/engine/experiment/dash" - "github.com/ooni/probe-cli/v3/internal/engine/experiment/dnscheck" - "github.com/ooni/probe-cli/v3/internal/engine/experiment/dnsping" - "github.com/ooni/probe-cli/v3/internal/engine/experiment/example" - "github.com/ooni/probe-cli/v3/internal/engine/experiment/fbmessenger" - "github.com/ooni/probe-cli/v3/internal/engine/experiment/hhfm" - "github.com/ooni/probe-cli/v3/internal/engine/experiment/hirl" - "github.com/ooni/probe-cli/v3/internal/engine/experiment/httphostheader" - "github.com/ooni/probe-cli/v3/internal/engine/experiment/ndt7" - "github.com/ooni/probe-cli/v3/internal/engine/experiment/psiphon" - "github.com/ooni/probe-cli/v3/internal/engine/experiment/quicping" - "github.com/ooni/probe-cli/v3/internal/engine/experiment/riseupvpn" - "github.com/ooni/probe-cli/v3/internal/engine/experiment/run" - "github.com/ooni/probe-cli/v3/internal/engine/experiment/signal" - "github.com/ooni/probe-cli/v3/internal/engine/experiment/simplequicping" - "github.com/ooni/probe-cli/v3/internal/engine/experiment/sniblocking" - "github.com/ooni/probe-cli/v3/internal/engine/experiment/stunreachability" - "github.com/ooni/probe-cli/v3/internal/engine/experiment/tcpping" - "github.com/ooni/probe-cli/v3/internal/engine/experiment/telegram" - "github.com/ooni/probe-cli/v3/internal/engine/experiment/tlsping" - "github.com/ooni/probe-cli/v3/internal/engine/experiment/tlstool" - "github.com/ooni/probe-cli/v3/internal/engine/experiment/tor" - "github.com/ooni/probe-cli/v3/internal/engine/experiment/torsf" - "github.com/ooni/probe-cli/v3/internal/engine/experiment/urlgetter" - "github.com/ooni/probe-cli/v3/internal/engine/experiment/vanillator" - "github.com/ooni/probe-cli/v3/internal/engine/experiment/webconnectivity" - "github.com/ooni/probe-cli/v3/internal/engine/experiment/whatsapp" -) - -var experimentsByName = map[string]func(*Session) *experimentBuilder{ - "dash": func(session *Session) *experimentBuilder { - return &experimentBuilder{ - build: func(config interface{}) *experiment { - return newExperiment(session, dash.NewExperimentMeasurer( - *config.(*dash.Config), - )) - }, - config: &dash.Config{}, - interruptible: true, - inputPolicy: InputNone, - } - }, - - "dnscheck": func(session *Session) *experimentBuilder { - return &experimentBuilder{ - build: func(config interface{}) *experiment { - return newExperiment(session, dnscheck.NewExperimentMeasurer( - *config.(*dnscheck.Config), - )) - }, - config: &dnscheck.Config{}, - inputPolicy: InputOrStaticDefault, - } - }, - - "dnsping": func(session *Session) *experimentBuilder { - return &experimentBuilder{ - build: func(config interface{}) *experiment { - return newExperiment(session, dnsping.NewExperimentMeasurer( - *config.(*dnsping.Config), - )) - }, - config: &dnsping.Config{}, - inputPolicy: InputOrStaticDefault, - } - }, - - "example": func(session *Session) *experimentBuilder { - return &experimentBuilder{ - build: func(config interface{}) *experiment { - return newExperiment(session, example.NewExperimentMeasurer( - *config.(*example.Config), "example", - )) - }, - config: &example.Config{ - Message: "Good day from the example experiment!", - SleepTime: int64(time.Second), - }, - interruptible: true, - inputPolicy: InputNone, - } - }, - - "facebook_messenger": func(session *Session) *experimentBuilder { - return &experimentBuilder{ - build: func(config interface{}) *experiment { - return newExperiment(session, fbmessenger.NewExperimentMeasurer( - *config.(*fbmessenger.Config), - )) - }, - config: &fbmessenger.Config{}, - inputPolicy: InputNone, - } - }, - - "http_header_field_manipulation": func(session *Session) *experimentBuilder { - return &experimentBuilder{ - build: func(config interface{}) *experiment { - return newExperiment(session, hhfm.NewExperimentMeasurer( - *config.(*hhfm.Config), - )) - }, - config: &hhfm.Config{}, - inputPolicy: InputNone, - } - }, - - "http_host_header": func(session *Session) *experimentBuilder { - return &experimentBuilder{ - build: func(config interface{}) *experiment { - return newExperiment(session, httphostheader.NewExperimentMeasurer( - *config.(*httphostheader.Config), - )) - }, - config: &httphostheader.Config{}, - inputPolicy: InputOrQueryBackend, - } - }, - - "http_invalid_request_line": func(session *Session) *experimentBuilder { - return &experimentBuilder{ - build: func(config interface{}) *experiment { - return newExperiment(session, hirl.NewExperimentMeasurer( - *config.(*hirl.Config), - )) - }, - config: &hirl.Config{}, - inputPolicy: InputNone, - } - }, - - "ndt": func(session *Session) *experimentBuilder { - return &experimentBuilder{ - build: func(config interface{}) *experiment { - return newExperiment(session, ndt7.NewExperimentMeasurer( - *config.(*ndt7.Config), - )) - }, - config: &ndt7.Config{}, - interruptible: true, - inputPolicy: InputNone, - } - }, - - "psiphon": func(session *Session) *experimentBuilder { - return &experimentBuilder{ - build: func(config interface{}) *experiment { - return newExperiment(session, psiphon.NewExperimentMeasurer( - *config.(*psiphon.Config), - )) - }, - config: &psiphon.Config{}, - inputPolicy: InputOptional, - } - }, - - "quicping": func(session *Session) *experimentBuilder { - return &experimentBuilder{ - build: func(config interface{}) *experiment { - return newExperiment(session, quicping.NewExperimentMeasurer( - *config.(*quicping.Config), - )) - }, - config: &quicping.Config{}, - inputPolicy: InputStrictlyRequired, - } - }, - - "riseupvpn": func(session *Session) *experimentBuilder { - return &experimentBuilder{ - build: func(config interface{}) *experiment { - return newExperiment(session, riseupvpn.NewExperimentMeasurer( - *config.(*riseupvpn.Config), - )) - }, - config: &riseupvpn.Config{}, - inputPolicy: InputNone, - } - }, - - "run": func(session *Session) *experimentBuilder { - return &experimentBuilder{ - build: func(config interface{}) *experiment { - return newExperiment(session, run.NewExperimentMeasurer( - *config.(*run.Config), - )) - }, - config: &run.Config{}, - inputPolicy: InputStrictlyRequired, - } - }, - - "simplequicping": func(session *Session) *experimentBuilder { - return &experimentBuilder{ - build: func(config interface{}) *experiment { - return newExperiment(session, simplequicping.NewExperimentMeasurer( - *config.(*simplequicping.Config), - )) - }, - config: &simplequicping.Config{}, - inputPolicy: InputStrictlyRequired, - } - }, - - "signal": func(session *Session) *experimentBuilder { - return &experimentBuilder{ - build: func(config interface{}) *experiment { - return newExperiment(session, signal.NewExperimentMeasurer( - *config.(*signal.Config), - )) - }, - config: &signal.Config{}, - inputPolicy: InputNone, - } - }, - - "sni_blocking": func(session *Session) *experimentBuilder { - return &experimentBuilder{ - build: func(config interface{}) *experiment { - return newExperiment(session, sniblocking.NewExperimentMeasurer( - *config.(*sniblocking.Config), - )) - }, - config: &sniblocking.Config{}, - inputPolicy: InputOrQueryBackend, - } - }, - - "stunreachability": func(session *Session) *experimentBuilder { - return &experimentBuilder{ - build: func(config interface{}) *experiment { - return newExperiment(session, stunreachability.NewExperimentMeasurer( - *config.(*stunreachability.Config), - )) - }, - config: &stunreachability.Config{}, - inputPolicy: InputOrStaticDefault, - } - }, - - "tcpping": func(session *Session) *experimentBuilder { - return &experimentBuilder{ - build: func(config interface{}) *experiment { - return newExperiment(session, tcpping.NewExperimentMeasurer( - *config.(*tcpping.Config), - )) - }, - config: &tcpping.Config{}, - inputPolicy: InputStrictlyRequired, - } - }, - - "tlsping": func(session *Session) *experimentBuilder { - return &experimentBuilder{ - build: func(config interface{}) *experiment { - return newExperiment(session, tlsping.NewExperimentMeasurer( - *config.(*tlsping.Config), - )) - }, - config: &tlsping.Config{}, - inputPolicy: InputStrictlyRequired, - } - }, - - "telegram": func(session *Session) *experimentBuilder { - return &experimentBuilder{ - build: func(config interface{}) *experiment { - return newExperiment(session, telegram.NewExperimentMeasurer( - *config.(*telegram.Config), - )) - }, - config: &telegram.Config{}, - inputPolicy: InputNone, - } - }, - - "tlstool": func(session *Session) *experimentBuilder { - return &experimentBuilder{ - build: func(config interface{}) *experiment { - return newExperiment(session, tlstool.NewExperimentMeasurer( - *config.(*tlstool.Config), - )) - }, - config: &tlstool.Config{}, - inputPolicy: InputOrQueryBackend, - } - }, - - "tor": func(session *Session) *experimentBuilder { - return &experimentBuilder{ - build: func(config interface{}) *experiment { - return newExperiment(session, tor.NewExperimentMeasurer( - *config.(*tor.Config), - )) - }, - config: &tor.Config{}, - inputPolicy: InputNone, - } - }, - - "torsf": func(session *Session) *experimentBuilder { - return &experimentBuilder{ - build: func(config interface{}) *experiment { - return newExperiment(session, torsf.NewExperimentMeasurer( - *config.(*torsf.Config), - )) - }, - config: &torsf.Config{}, - inputPolicy: InputNone, - } - }, - - "urlgetter": func(session *Session) *experimentBuilder { - return &experimentBuilder{ - build: func(config interface{}) *experiment { - return newExperiment(session, urlgetter.NewExperimentMeasurer( - *config.(*urlgetter.Config), - )) - }, - config: &urlgetter.Config{}, - inputPolicy: InputStrictlyRequired, - } - }, - - "vanilla_tor": func(session *Session) *experimentBuilder { - return &experimentBuilder{ - build: func(config interface{}) *experiment { - return newExperiment(session, vanillator.NewExperimentMeasurer( - *config.(*vanillator.Config), - )) - }, - config: &vanillator.Config{}, - inputPolicy: InputNone, - } - }, - - "web_connectivity": func(session *Session) *experimentBuilder { - return &experimentBuilder{ - build: func(config interface{}) *experiment { - return newExperiment(session, webconnectivity.NewExperimentMeasurer( - *config.(*webconnectivity.Config), - )) - }, - config: &webconnectivity.Config{}, - inputPolicy: InputOrQueryBackend, - } - }, - - "whatsapp": func(session *Session) *experimentBuilder { - return &experimentBuilder{ - build: func(config interface{}) *experiment { - return newExperiment(session, whatsapp.NewExperimentMeasurer( - *config.(*whatsapp.Config), - )) - }, - config: &whatsapp.Config{}, - inputPolicy: InputNone, - } - }, -} +import "github.com/ooni/probe-cli/v3/internal/registry" // AllExperiments returns the name of all experiments func AllExperiments() []string { - var names []string - for key := range experimentsByName { - names = append(names, key) - } - return names + return registry.ExperimentNames() } diff --git a/internal/engine/experiment.go b/internal/engine/experiment.go index be5a21a..069e5b6 100644 --- a/internal/engine/experiment.go +++ b/internal/engine/experiment.go @@ -26,76 +26,6 @@ func formatTimeNowUTC() string { return time.Now().UTC().Format(dateFormat) } -// Experiment is an experiment instance. -type Experiment interface { - // KibiBytesReceived accounts for the KibiBytes received by the experiment. - KibiBytesReceived() float64 - - // KibiBytesSent is like KibiBytesReceived but for the bytes sent. - KibiBytesSent() float64 - - // Name returns the experiment name. - Name() string - - // GetSummaryKeys returns a data structure containing a - // summary of the test keys for ooniprobe. - GetSummaryKeys(m *model.Measurement) (any, error) - - // ReportID returns the open report's ID, if we have opened a report - // successfully before, or an empty string, otherwise. - // - // Deprecated: new code should use a Submitter. - ReportID() string - - // MeasureAsync runs an async measurement. This operation could post - // one or more measurements onto the returned channel. We'll close the - // channel when we've emitted all the measurements. - // - // Arguments: - // - // - ctx is the context for deadline/cancellation/timeout; - // - // - input is the input (typically a URL but it could also be - // just an endpoint or an empty string for input-less experiments - // such as, e.g., ndt7 and dash). - // - // Return value: - // - // - on success, channel where to post measurements (the channel - // will be closed when done) and nil error; - // - // - on failure, nil channel and non-nil error. - MeasureAsync(ctx context.Context, input string) (<-chan *model.Measurement, error) - - // MeasureWithContext performs a synchronous measurement. - // - // Return value: strictly either a non-nil measurement and - // a nil error or a nil measurement and a non-nil error. - // - // CAVEAT: while this API is perfectly fine for experiments that - // return a single measurement, it will only return the first measurement - // when used with an asynchronous experiment. - MeasureWithContext(ctx context.Context, input string) (measurement *model.Measurement, err error) - - // SaveMeasurement saves a measurement on the specified file path. - // - // Deprecated: new code should use a Saver. - SaveMeasurement(measurement *model.Measurement, filePath string) error - - // SubmitAndUpdateMeasurementContext submits a measurement and updates the - // fields whose value has changed as part of the submission. - // - // Deprecated: new code should use a Submitter. - SubmitAndUpdateMeasurementContext( - ctx context.Context, measurement *model.Measurement) error - - // OpenReportContext will open a report using the given context - // to possibly limit the lifetime of this operation. - // - // Deprecated: new code should use a Submitter. - OpenReportContext(ctx context.Context) error -} - // experiment implements Experiment. type experiment struct { byteCounter *bytecounter.Counter diff --git a/internal/engine/experiment_integration_test.go b/internal/engine/experiment_integration_test.go index dd91b7f..1ba6431 100644 --- a/internal/engine/experiment_integration_test.go +++ b/internal/engine/experiment_integration_test.go @@ -12,7 +12,6 @@ import ( "strings" "testing" - "github.com/ooni/probe-cli/v3/internal/engine/experiment/example" "github.com/ooni/probe-cli/v3/internal/model" ) @@ -142,7 +141,7 @@ func TestNeedsInput(t *testing.T) { if err != nil { t.Fatal(err) } - if builder.InputPolicy() != InputOrQueryBackend { + if builder.InputPolicy() != model.InputOrQueryBackend { t.Fatal("web_connectivity certainly needs input") } } @@ -218,88 +217,6 @@ func TestMeasurementFailure(t *testing.T) { } } -func TestUseOptions(t *testing.T) { - if testing.Short() { - t.Skip("skip test in short mode") - } - sess := newSessionForTesting(t) - defer sess.Close() - builder, err := sess.NewExperimentBuilder("example") - if err != nil { - t.Fatal(err) - } - options, err := builder.Options() - if err != nil { - t.Fatal(err) - } - var ( - returnError bool - message bool - sleepTime bool - other int64 - ) - for name, option := range options { - if name == "ReturnError" { - returnError = true - if option.Type != "bool" { - t.Fatal("ReturnError is not a bool") - } - if option.Doc != "Toogle to return a mocked error" { - t.Fatal("ReturnError doc is wrong") - } - } else if name == "Message" { - message = true - if option.Type != "string" { - t.Fatal("Message is not a string") - } - if option.Doc != "Message to emit at test completion" { - t.Fatal("Message doc is wrong") - } - } else if name == "SleepTime" { - sleepTime = true - if option.Type != "int64" { - t.Fatal("SleepTime is not an int64") - } - if option.Doc != "Amount of time to sleep for" { - t.Fatal("SleepTime doc is wrong") - } - } else { - other++ - } - } - if other != 0 { - t.Fatal("found unexpected option") - } - if !returnError { - t.Fatal("did not find ReturnError option") - } - if !message { - t.Fatal("did not find Message option") - } - if !sleepTime { - t.Fatal("did not find SleepTime option") - } - if err := builder.SetOptionAny("ReturnError", true); err != nil { - t.Fatal("cannot set ReturnError field") - } - if err := builder.SetOptionAny("SleepTime", 10); err != nil { - t.Fatal("cannot set SleepTime field") - } - if err := builder.SetOptionAny("Message", "antani"); err != nil { - t.Fatal("cannot set Message field") - } - config := builder.(*experimentBuilder).config.(*example.Config) - if config.ReturnError != true { - t.Fatal("config.ReturnError was not changed") - } - if config.SleepTime != 10 { - t.Fatal("config.SleepTime was not changed") - } - if config.Message != "antani" { - t.Fatal("config.Message was not changed") - } -} - func TestRunHHFM(t *testing.T) { if testing.Short() { t.Skip("skip test in short mode") @@ -313,7 +230,7 @@ func TestRunHHFM(t *testing.T) { runexperimentflow(t, builder.NewExperiment(), "") } -func runexperimentflow(t *testing.T, experiment Experiment, input string) { +func runexperimentflow(t *testing.T, experiment model.Experiment, input string) { ctx := context.Background() err := experiment.OpenReportContext(ctx) if err != nil { diff --git a/internal/engine/experimentbuilder.go b/internal/engine/experimentbuilder.go index 64dc7c4..3cbd46f 100644 --- a/internal/engine/experimentbuilder.go +++ b/internal/engine/experimentbuilder.go @@ -5,243 +5,46 @@ package engine // import ( - "errors" - "fmt" - "reflect" - "strconv" - - "github.com/iancoleman/strcase" "github.com/ooni/probe-cli/v3/internal/model" + "github.com/ooni/probe-cli/v3/internal/registry" ) -// InputPolicy describes the experiment policy with respect to input. That is -// whether it requires input, optionally accepts input, does not want input. -type InputPolicy string - -const ( - // InputOrQueryBackend indicates that the experiment requires - // external input to run and that this kind of input is URLs - // from the citizenlab/test-lists repository. If this input - // not provided to the experiment, then the code that runs the - // experiment is supposed to fetch from URLs from OONI's backends. - InputOrQueryBackend = InputPolicy("or_query_backend") - - // InputStrictlyRequired indicates that the experiment - // requires input and we currently don't have an API for - // fetching such input. Therefore, either the user specifies - // input or the experiment will fail for the lack of input. - InputStrictlyRequired = InputPolicy("strictly_required") - - // InputOptional indicates that the experiment handles input, - // if any; otherwise it fetchs input/uses a default. - InputOptional = InputPolicy("optional") - - // InputNone indicates that the experiment does not want any - // input and ignores the input if provided with it. - InputNone = InputPolicy("none") - - // We gather input from StaticInput and SourceFiles. If there is - // input, we return it. Otherwise, we return an internal static - // list of inputs to be used with this experiment. - InputOrStaticDefault = InputPolicy("or_static_default") -) - -// 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. +// +// This type is now just a tiny wrapper around registry.Factory. type experimentBuilder struct { - // build is the constructor that build an experiment with the given config. - build func(config interface{}) *experiment + factory *registry.Factory // 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 + // session is the session + session *Session } // Interruptible implements ExperimentBuilder.Interruptible. func (b *experimentBuilder) Interruptible() bool { - return b.interruptible + return b.factory.Interruptible() } // InputPolicy implements ExperimentBuilder.InputPolicy. -func (b *experimentBuilder) InputPolicy() InputPolicy { - return b.inputPolicy +func (b *experimentBuilder) InputPolicy() model.InputPolicy { + return b.factory.InputPolicy() } -// OptionInfo contains info about an option. -type OptionInfo struct { - // 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 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 { - 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] = OptionInfo{ - Doc: field.Tag.Get("ooni"), - Type: field.Type.String(), - } - } - return result, nil -} - -// 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) - } +func (b *experimentBuilder) Options() (map[string]model.ExperimentOptionInfo, error) { + return b.factory.Options() } // 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 - } - 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) - } + return b.factory.SetOptionAny(key, value) } // 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 - } - } - return nil + return b.factory.SetOptionsAny(options) } // SetCallbacks implements ExperimentBuilder.SetCallbacks. @@ -249,57 +52,24 @@ func (b *experimentBuilder) SetCallbacks(callbacks model.ExperimentCallbacks) { b.callbacks = callbacks } -// 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 { - 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 -} - // NewExperiment creates the experiment -func (b *experimentBuilder) NewExperiment() Experiment { - experiment := b.build(b.config) +func (b *experimentBuilder) NewExperiment() model.Experiment { + measurer := b.factory.NewExperimentMeasurer() + experiment := newExperiment(b.session, measurer) experiment.callbacks = b.callbacks return experiment } -// 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 -} - // 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) + factory, err := registry.NewFactory(name) + if err != nil { + return nil, err + } + builder := &experimentBuilder{ + factory: factory, + callbacks: model.NewPrinterCallbacks(session.Logger()), + session: session, } - builder := factory(session) - builder.callbacks = model.NewPrinterCallbacks(session.Logger()) return builder, nil } diff --git a/internal/engine/experimentbuilder_test.go b/internal/engine/experimentbuilder_test.go index 9197032..00a22ef 100644 --- a/internal/engine/experimentbuilder_test.go +++ b/internal/engine/experimentbuilder_test.go @@ -1,347 +1 @@ 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) - } - }) -} diff --git a/internal/engine/inputloader.go b/internal/engine/inputloader.go index b6792bf..172800c 100644 --- a/internal/engine/inputloader.go +++ b/internal/engine/inputloader.go @@ -11,6 +11,7 @@ import ( "github.com/apex/log" "github.com/ooni/probe-cli/v3/internal/fsx" "github.com/ooni/probe-cli/v3/internal/model" + "github.com/ooni/probe-cli/v3/internal/registry" "github.com/ooni/probe-cli/v3/internal/stuninput" ) @@ -86,7 +87,7 @@ type InputLoader struct { // current experiment. We will not load any input if // the policy says we should not. You MUST fill in // this field. - InputPolicy InputPolicy + InputPolicy model.InputPolicy // Logger is the optional logger that the InputLoader // should be using. If not set, we will use the default @@ -112,13 +113,13 @@ type InputLoader struct { // return a list of URLs because this is the only input we support. func (il *InputLoader) Load(ctx context.Context) ([]model.OOAPIURLInfo, error) { switch il.InputPolicy { - case InputOptional: + case model.InputOptional: return il.loadOptional() - case InputOrQueryBackend: + case model.InputOrQueryBackend: return il.loadOrQueryBackend(ctx) - case InputStrictlyRequired: + case model.InputStrictlyRequired: return il.loadStrictlyRequired(ctx) - case InputOrStaticDefault: + case model.InputOrStaticDefault: return il.loadOrStaticDefault(ctx) default: return il.loadNone() @@ -299,7 +300,7 @@ func StaticBareInputForExperiment(name string) ([]string, error) { // Implementation note: we may be called from pkg/oonimkall // with a non-canonical experiment name, so we need to convert // the experiment name to be canonical before proceeding. - switch canonicalizeExperimentName(name) { + switch registry.CanonicalizeExperimentName(name) { case "dnscheck": return dnsCheckDefaultInput, nil case "stunreachability": diff --git a/internal/engine/inputloader_network_test.go b/internal/engine/inputloader_network_test.go index 5e82aa5..f696a01 100644 --- a/internal/engine/inputloader_network_test.go +++ b/internal/engine/inputloader_network_test.go @@ -30,7 +30,7 @@ func TestInputLoaderInputOrQueryBackendWithNoInput(t *testing.T) { } defer sess.Close() il := &engine.InputLoader{ - InputPolicy: engine.InputOrQueryBackend, + InputPolicy: model.InputOrQueryBackend, Session: sess, } ctx := context.Background() diff --git a/internal/engine/inputloader_test.go b/internal/engine/inputloader_test.go index bd0c6ce..58dfc39 100644 --- a/internal/engine/inputloader_test.go +++ b/internal/engine/inputloader_test.go @@ -19,7 +19,7 @@ import ( func TestInputLoaderInputNoneWithStaticInputs(t *testing.T) { il := &InputLoader{ StaticInputs: []string{"https://www.google.com/"}, - InputPolicy: InputNone, + InputPolicy: model.InputNone, } ctx := context.Background() out, err := il.Load(ctx) @@ -37,7 +37,7 @@ func TestInputLoaderInputNoneWithFilesInputs(t *testing.T) { "testdata/inputloader1.txt", "testdata/inputloader2.txt", }, - InputPolicy: InputNone, + InputPolicy: model.InputNone, } ctx := context.Background() out, err := il.Load(ctx) @@ -56,7 +56,7 @@ func TestInputLoaderInputNoneWithBothInputs(t *testing.T) { "testdata/inputloader1.txt", "testdata/inputloader2.txt", }, - InputPolicy: InputNone, + InputPolicy: model.InputNone, } ctx := context.Background() out, err := il.Load(ctx) @@ -70,7 +70,7 @@ func TestInputLoaderInputNoneWithBothInputs(t *testing.T) { func TestInputLoaderInputNoneWithNoInput(t *testing.T) { il := &InputLoader{ - InputPolicy: InputNone, + InputPolicy: model.InputNone, } ctx := context.Background() out, err := il.Load(ctx) @@ -84,7 +84,7 @@ func TestInputLoaderInputNoneWithNoInput(t *testing.T) { func TestInputLoaderInputOptionalWithNoInput(t *testing.T) { il := &InputLoader{ - InputPolicy: InputOptional, + InputPolicy: model.InputOptional, } ctx := context.Background() out, err := il.Load(ctx) @@ -103,7 +103,7 @@ func TestInputLoaderInputOptionalWithInput(t *testing.T) { "testdata/inputloader1.txt", "testdata/inputloader2.txt", }, - InputPolicy: InputOptional, + InputPolicy: model.InputOptional, } ctx := context.Background() out, err := il.Load(ctx) @@ -133,7 +133,7 @@ func TestInputLoaderInputOptionalNonexistentFile(t *testing.T) { "/nonexistent", "testdata/inputloader2.txt", }, - InputPolicy: InputOptional, + InputPolicy: model.InputOptional, } ctx := context.Background() out, err := il.Load(ctx) @@ -152,7 +152,7 @@ func TestInputLoaderInputStrictlyRequiredWithInput(t *testing.T) { "testdata/inputloader1.txt", "testdata/inputloader2.txt", }, - InputPolicy: InputStrictlyRequired, + InputPolicy: model.InputStrictlyRequired, } ctx := context.Background() out, err := il.Load(ctx) @@ -176,7 +176,7 @@ func TestInputLoaderInputStrictlyRequiredWithInput(t *testing.T) { func TestInputLoaderInputStrictlyRequiredWithoutInput(t *testing.T) { il := &InputLoader{ - InputPolicy: InputStrictlyRequired, + InputPolicy: model.InputStrictlyRequired, } ctx := context.Background() out, err := il.Load(ctx) @@ -190,7 +190,7 @@ func TestInputLoaderInputStrictlyRequiredWithoutInput(t *testing.T) { func TestInputLoaderInputStrictlyRequiredWithEmptyFile(t *testing.T) { il := &InputLoader{ - InputPolicy: InputStrictlyRequired, + InputPolicy: model.InputStrictlyRequired, SourceFiles: []string{ "testdata/inputloader1.txt", "testdata/inputloader3.txt", // we want it before inputloader2.txt @@ -215,7 +215,7 @@ func TestInputLoaderInputOrStaticDefaultWithInput(t *testing.T) { "testdata/inputloader1.txt", "testdata/inputloader2.txt", }, - InputPolicy: InputOrStaticDefault, + InputPolicy: model.InputOrStaticDefault, } ctx := context.Background() out, err := il.Load(ctx) @@ -240,7 +240,7 @@ func TestInputLoaderInputOrStaticDefaultWithInput(t *testing.T) { func TestInputLoaderInputOrStaticDefaultWithEmptyFile(t *testing.T) { il := &InputLoader{ ExperimentName: "dnscheck", - InputPolicy: InputOrStaticDefault, + InputPolicy: model.InputOrStaticDefault, SourceFiles: []string{ "testdata/inputloader1.txt", "testdata/inputloader3.txt", // we want it before inputloader2.txt @@ -260,7 +260,7 @@ func TestInputLoaderInputOrStaticDefaultWithEmptyFile(t *testing.T) { func TestInputLoaderInputOrStaticDefaultWithoutInputDNSCheck(t *testing.T) { il := &InputLoader{ ExperimentName: "dnscheck", - InputPolicy: InputOrStaticDefault, + InputPolicy: model.InputOrStaticDefault, } ctx := context.Background() out, err := il.Load(ctx) @@ -287,7 +287,7 @@ func TestInputLoaderInputOrStaticDefaultWithoutInputDNSCheck(t *testing.T) { func TestInputLoaderInputOrStaticDefaultWithoutInputStunReachability(t *testing.T) { il := &InputLoader{ ExperimentName: "stunreachability", - InputPolicy: InputOrStaticDefault, + InputPolicy: model.InputOrStaticDefault, } ctx := context.Background() out, err := il.Load(ctx) @@ -323,7 +323,7 @@ func TestStaticBareInputForExperimentWorksWithNonCanonicalNames(t *testing.T) { func TestInputLoaderInputOrStaticDefaultWithoutInputOtherName(t *testing.T) { il := &InputLoader{ ExperimentName: "xx", - InputPolicy: InputOrStaticDefault, + InputPolicy: model.InputOrStaticDefault, } ctx := context.Background() out, err := il.Load(ctx) @@ -342,7 +342,7 @@ func TestInputLoaderInputOrQueryBackendWithInput(t *testing.T) { "testdata/inputloader1.txt", "testdata/inputloader2.txt", }, - InputPolicy: InputOrQueryBackend, + InputPolicy: model.InputOrQueryBackend, } ctx := context.Background() out, err := il.Load(ctx) @@ -377,7 +377,7 @@ func TestInputLoaderInputOrQueryBackendWithNoInputAndCancelledContext(t *testing } defer sess.Close() il := &InputLoader{ - InputPolicy: InputOrQueryBackend, + InputPolicy: model.InputOrQueryBackend, Session: sess, } ctx, cancel := context.WithCancel(context.Background()) @@ -393,7 +393,7 @@ func TestInputLoaderInputOrQueryBackendWithNoInputAndCancelledContext(t *testing func TestInputLoaderInputOrQueryBackendWithEmptyFile(t *testing.T) { il := &InputLoader{ - InputPolicy: InputOrQueryBackend, + InputPolicy: model.InputOrQueryBackend, SourceFiles: []string{ "testdata/inputloader1.txt", "testdata/inputloader3.txt", // we want it before inputloader2.txt diff --git a/internal/engine/session.go b/internal/engine/session.go index bf0ef7d..13ec0d0 100644 --- a/internal/engine/session.go +++ b/internal/engine/session.go @@ -390,7 +390,7 @@ var ErrAlreadyUsingProxy = errors.New( // NewExperimentBuilder returns a new experiment builder // for the experiment with the given name, or an error if // there's no such experiment with the given name -func (s *Session) NewExperimentBuilder(name string) (ExperimentBuilder, error) { +func (s *Session) NewExperimentBuilder(name string) (model.ExperimentBuilder, error) { eb, err := newExperimentBuilder(s, name) if err != nil { return nil, err diff --git a/internal/model/experiment.go b/internal/model/experiment.go index 5c605ef..70aceb6 100644 --- a/internal/model/experiment.go +++ b/internal/model/experiment.go @@ -141,3 +141,143 @@ type ExperimentMeasurer interface { // GetSummaryKeys returns summary keys expected by ooni/probe-cli. GetSummaryKeys(*Measurement) (interface{}, error) } + +// Experiment is an experiment instance. +type Experiment interface { + // KibiBytesReceived accounts for the KibiBytes received by the experiment. + KibiBytesReceived() float64 + + // KibiBytesSent is like KibiBytesReceived but for the bytes sent. + KibiBytesSent() float64 + + // Name returns the experiment name. + Name() string + + // GetSummaryKeys returns a data structure containing a + // summary of the test keys for ooniprobe. + GetSummaryKeys(m *Measurement) (any, error) + + // ReportID returns the open report's ID, if we have opened a report + // successfully before, or an empty string, otherwise. + // + // Deprecated: new code should use a Submitter. + ReportID() string + + // MeasureAsync runs an async measurement. This operation could post + // one or more measurements onto the returned channel. We'll close the + // channel when we've emitted all the measurements. + // + // Arguments: + // + // - ctx is the context for deadline/cancellation/timeout; + // + // - input is the input (typically a URL but it could also be + // just an endpoint or an empty string for input-less experiments + // such as, e.g., ndt7 and dash). + // + // Return value: + // + // - on success, channel where to post measurements (the channel + // will be closed when done) and nil error; + // + // - on failure, nil channel and non-nil error. + MeasureAsync(ctx context.Context, input string) (<-chan *Measurement, error) + + // MeasureWithContext performs a synchronous measurement. + // + // Return value: strictly either a non-nil measurement and + // a nil error or a nil measurement and a non-nil error. + // + // CAVEAT: while this API is perfectly fine for experiments that + // return a single measurement, it will only return the first measurement + // when used with an asynchronous experiment. + MeasureWithContext(ctx context.Context, input string) (measurement *Measurement, err error) + + // SaveMeasurement saves a measurement on the specified file path. + // + // Deprecated: new code should use a Saver. + SaveMeasurement(measurement *Measurement, filePath string) error + + // SubmitAndUpdateMeasurementContext submits a measurement and updates the + // fields whose value has changed as part of the submission. + // + // Deprecated: new code should use a Submitter. + SubmitAndUpdateMeasurementContext( + ctx context.Context, measurement *Measurement) error + + // OpenReportContext will open a report using the given context + // to possibly limit the lifetime of this operation. + // + // Deprecated: new code should use a Submitter. + OpenReportContext(ctx context.Context) error +} + +// InputPolicy describes the experiment policy with respect to input. That is +// whether it requires input, optionally accepts input, does not want input. +type InputPolicy string + +const ( + // InputOrQueryBackend indicates that the experiment requires + // external input to run and that this kind of input is URLs + // from the citizenlab/test-lists repository. If this input + // not provided to the experiment, then the code that runs the + // experiment is supposed to fetch from URLs from OONI's backends. + InputOrQueryBackend = InputPolicy("or_query_backend") + + // InputStrictlyRequired indicates that the experiment + // requires input and we currently don't have an API for + // fetching such input. Therefore, either the user specifies + // input or the experiment will fail for the lack of input. + InputStrictlyRequired = InputPolicy("strictly_required") + + // InputOptional indicates that the experiment handles input, + // if any; otherwise it fetchs input/uses a default. + InputOptional = InputPolicy("optional") + + // InputNone indicates that the experiment does not want any + // input and ignores the input if provided with it. + InputNone = InputPolicy("none") + + // We gather input from StaticInput and SourceFiles. If there is + // input, we return it. Otherwise, we return an internal static + // list of inputs to be used with this experiment. + InputOrStaticDefault = InputPolicy("or_static_default") +) + +// 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]ExperimentOptionInfo, 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 ExperimentCallbacks) + + // NewExperiment creates the experiment instance. + NewExperiment() Experiment +} + +// ExperimentOptionInfo contains info about an experiment option. +type ExperimentOptionInfo struct { + // Doc contains the documentation. + Doc string + + // Type contains the type. + Type string +} diff --git a/internal/oonirun/experiment.go b/internal/oonirun/experiment.go index 10cfdfa..25ee396 100644 --- a/internal/oonirun/experiment.go +++ b/internal/oonirun/experiment.go @@ -117,7 +117,7 @@ type inputProcessor interface { } // newInputProcessor creates a new inputProcessor instance. -func (ed *Experiment) newInputProcessor(experiment engine.Experiment, +func (ed *Experiment) newInputProcessor(experiment model.Experiment, inputList []model.OOAPIURLInfo, saver engine.Saver, submitter engine.Submitter) inputProcessor { return &engine.InputProcessor{ Annotations: ed.Annotations, @@ -138,7 +138,7 @@ func (ed *Experiment) newInputProcessor(experiment engine.Experiment, } // newSaver creates a new engine.Saver instance. -func (ed *Experiment) newSaver(experiment engine.Experiment) (engine.Saver, error) { +func (ed *Experiment) newSaver(experiment model.Experiment) (engine.Saver, error) { return engine.NewSaver(engine.SaverConfig{ Enabled: !ed.NoJSON, Experiment: experiment, @@ -157,7 +157,7 @@ func (ed *Experiment) newSubmitter(ctx context.Context) (engine.Submitter, error } // newExperimentBuilder creates a new engine.ExperimentBuilder for the given experimentName. -func (ed *Experiment) newExperimentBuilder(experimentName string) (engine.ExperimentBuilder, error) { +func (ed *Experiment) newExperimentBuilder(experimentName string) (model.ExperimentBuilder, error) { return ed.Session.NewExperimentBuilder(ed.Name) } @@ -167,7 +167,7 @@ type inputLoader interface { } // newInputLoader creates a new inputLoader. -func (ed *Experiment) newInputLoader(inputPolicy engine.InputPolicy) inputLoader { +func (ed *Experiment) newInputLoader(inputPolicy model.InputPolicy) inputLoader { return &engine.InputLoader{ CheckInConfig: &model.OOAPICheckInConfig{ RunType: model.RunTypeManual, diff --git a/internal/oonirun/session.go b/internal/oonirun/session.go index 9346c5c..6075c8d 100644 --- a/internal/oonirun/session.go +++ b/internal/oonirun/session.go @@ -29,5 +29,5 @@ type Session interface { Logger() model.Logger // NewExperimentBuilder creates a new engine.ExperimentBuilder. - NewExperimentBuilder(name string) (engine.ExperimentBuilder, error) + NewExperimentBuilder(name string) (model.ExperimentBuilder, error) } diff --git a/internal/registry/allexperiments.go b/internal/registry/allexperiments.go new file mode 100644 index 0000000..4cb28ac --- /dev/null +++ b/internal/registry/allexperiments.go @@ -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 +} diff --git a/internal/registry/dash.go b/internal/registry/dash.go new file mode 100644 index 0000000..f56bced --- /dev/null +++ b/internal/registry/dash.go @@ -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, + } +} diff --git a/internal/registry/dnscheck.go b/internal/registry/dnscheck.go new file mode 100644 index 0000000..b4c264d --- /dev/null +++ b/internal/registry/dnscheck.go @@ -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, + } +} diff --git a/internal/registry/dnsping.go b/internal/registry/dnsping.go new file mode 100644 index 0000000..6ced01b --- /dev/null +++ b/internal/registry/dnsping.go @@ -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, + } +} diff --git a/internal/registry/doc.go b/internal/registry/doc.go new file mode 100644 index 0000000..6929580 --- /dev/null +++ b/internal/registry/doc.go @@ -0,0 +1,2 @@ +// Package registry contains a registry of all the available experiments. +package registry diff --git a/internal/registry/example.go b/internal/registry/example.go new file mode 100644 index 0000000..947715c --- /dev/null +++ b/internal/registry/example.go @@ -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, + } +} diff --git a/internal/registry/factory.go b/internal/registry/factory.go new file mode 100644 index 0000000..b0b4416 --- /dev/null +++ b/internal/registry/factory.go @@ -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 +} diff --git a/internal/registry/factory_test.go b/internal/registry/factory_test.go new file mode 100644 index 0000000..f881ffd --- /dev/null +++ b/internal/registry/factory_test.go @@ -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) + } + }) +} diff --git a/internal/registry/fbmessenger.go b/internal/registry/fbmessenger.go new file mode 100644 index 0000000..6d2cda4 --- /dev/null +++ b/internal/registry/fbmessenger.go @@ -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, + } +} diff --git a/internal/registry/hhfm.go b/internal/registry/hhfm.go new file mode 100644 index 0000000..9a2bdaf --- /dev/null +++ b/internal/registry/hhfm.go @@ -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, + } +} diff --git a/internal/registry/hirl.go b/internal/registry/hirl.go new file mode 100644 index 0000000..63cb633 --- /dev/null +++ b/internal/registry/hirl.go @@ -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, + } +} diff --git a/internal/registry/httphostheader.go b/internal/registry/httphostheader.go new file mode 100644 index 0000000..7f34614 --- /dev/null +++ b/internal/registry/httphostheader.go @@ -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, + } +} diff --git a/internal/registry/ndt.go b/internal/registry/ndt.go new file mode 100644 index 0000000..2a1a2c5 --- /dev/null +++ b/internal/registry/ndt.go @@ -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, + } +} diff --git a/internal/registry/psiphon.go b/internal/registry/psiphon.go new file mode 100644 index 0000000..7702b31 --- /dev/null +++ b/internal/registry/psiphon.go @@ -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, + } +} diff --git a/internal/registry/quicping.go b/internal/registry/quicping.go new file mode 100644 index 0000000..cc59ff0 --- /dev/null +++ b/internal/registry/quicping.go @@ -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, + } +} diff --git a/internal/registry/riseupvpn.go b/internal/registry/riseupvpn.go new file mode 100644 index 0000000..de2c88d --- /dev/null +++ b/internal/registry/riseupvpn.go @@ -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, + } +} diff --git a/internal/registry/run.go b/internal/registry/run.go new file mode 100644 index 0000000..120578c --- /dev/null +++ b/internal/registry/run.go @@ -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, + } +} diff --git a/internal/registry/signal.go b/internal/registry/signal.go new file mode 100644 index 0000000..71a919b --- /dev/null +++ b/internal/registry/signal.go @@ -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, + } +} diff --git a/internal/registry/simplequicping.go b/internal/registry/simplequicping.go new file mode 100644 index 0000000..22230b3 --- /dev/null +++ b/internal/registry/simplequicping.go @@ -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, + } +} diff --git a/internal/registry/sniblocking.go b/internal/registry/sniblocking.go new file mode 100644 index 0000000..cd3409f --- /dev/null +++ b/internal/registry/sniblocking.go @@ -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, + } +} diff --git a/internal/registry/stunreachability.go b/internal/registry/stunreachability.go new file mode 100644 index 0000000..bf6f6a4 --- /dev/null +++ b/internal/registry/stunreachability.go @@ -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, + } +} diff --git a/internal/registry/tcpping.go b/internal/registry/tcpping.go new file mode 100644 index 0000000..5ff79ea --- /dev/null +++ b/internal/registry/tcpping.go @@ -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, + } +} diff --git a/internal/registry/telegram.go b/internal/registry/telegram.go new file mode 100644 index 0000000..6881d21 --- /dev/null +++ b/internal/registry/telegram.go @@ -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, + } +} diff --git a/internal/registry/tlsping.go b/internal/registry/tlsping.go new file mode 100644 index 0000000..0f94480 --- /dev/null +++ b/internal/registry/tlsping.go @@ -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, + } +} diff --git a/internal/registry/tlstool.go b/internal/registry/tlstool.go new file mode 100644 index 0000000..4343516 --- /dev/null +++ b/internal/registry/tlstool.go @@ -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, + } +} diff --git a/internal/registry/tor.go b/internal/registry/tor.go new file mode 100644 index 0000000..5a2a2ba --- /dev/null +++ b/internal/registry/tor.go @@ -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, + } +} diff --git a/internal/registry/torsf.go b/internal/registry/torsf.go new file mode 100644 index 0000000..b31888f --- /dev/null +++ b/internal/registry/torsf.go @@ -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, + } +} diff --git a/internal/registry/urlgetter.go b/internal/registry/urlgetter.go new file mode 100644 index 0000000..84762e0 --- /dev/null +++ b/internal/registry/urlgetter.go @@ -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, + } +} diff --git a/internal/registry/vanillator.go b/internal/registry/vanillator.go new file mode 100644 index 0000000..042ae54 --- /dev/null +++ b/internal/registry/vanillator.go @@ -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, + } +} diff --git a/internal/registry/webconnectivity.go b/internal/registry/webconnectivity.go new file mode 100644 index 0000000..dc7cefe --- /dev/null +++ b/internal/registry/webconnectivity.go @@ -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, + } +} diff --git a/internal/registry/whatsapp.go b/internal/registry/whatsapp.go new file mode 100644 index 0000000..9fd8d0f --- /dev/null +++ b/internal/registry/whatsapp.go @@ -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, + } +} diff --git a/pkg/oonimkall/experiment.go b/pkg/oonimkall/experiment.go index 4f7077c..b8c58f5 100644 --- a/pkg/oonimkall/experiment.go +++ b/pkg/oonimkall/experiment.go @@ -3,7 +3,6 @@ package oonimkall import ( "context" - "github.com/ooni/probe-cli/v3/internal/engine" "github.com/ooni/probe-cli/v3/internal/model" ) @@ -67,7 +66,7 @@ type experimentBuilder interface { // experimentBuilderWrapper wraps *ExperimentBuilder type experimentBuilderWrapper struct { - eb engine.ExperimentBuilder + eb model.ExperimentBuilder } // newExperiment implements experimentBuilder.newExperiment diff --git a/pkg/oonimkall/taskmocks_test.go b/pkg/oonimkall/taskmocks_test.go index 8b551f6..af58a82 100644 --- a/pkg/oonimkall/taskmocks_test.go +++ b/pkg/oonimkall/taskmocks_test.go @@ -87,7 +87,7 @@ type MockableTaskRunnerDependencies struct { // taskExperimentBuilder: MockableSetCallbacks func(callbacks model.ExperimentCallbacks) - MockableInputPolicy func() engine.InputPolicy + MockableInputPolicy func() model.InputPolicy MockableNewExperimentInstance func() taskExperiment MockableInterruptible func() bool @@ -169,7 +169,7 @@ func (dep *MockableTaskRunnerDependencies) SetCallbacks(callbacks model.Experime dep.MockableSetCallbacks(callbacks) } -func (dep *MockableTaskRunnerDependencies) InputPolicy() engine.InputPolicy { +func (dep *MockableTaskRunnerDependencies) InputPolicy() model.InputPolicy { return dep.MockableInputPolicy() } diff --git a/pkg/oonimkall/taskmodel.go b/pkg/oonimkall/taskmodel.go index 12dfdee..cd60ee9 100644 --- a/pkg/oonimkall/taskmodel.go +++ b/pkg/oonimkall/taskmodel.go @@ -227,7 +227,7 @@ type taskExperimentBuilder interface { SetCallbacks(callbacks model.ExperimentCallbacks) // InputPolicy returns the experiment's input policy. - InputPolicy() engine.InputPolicy + InputPolicy() model.InputPolicy // NewExperiment creates the new experiment. NewExperimentInstance() taskExperiment diff --git a/pkg/oonimkall/taskrunner.go b/pkg/oonimkall/taskrunner.go index f63eda2..7ec5eb1 100644 --- a/pkg/oonimkall/taskrunner.go +++ b/pkg/oonimkall/taskrunner.go @@ -184,12 +184,12 @@ func (r *runnerForTask) Run(rootCtx context.Context) { // In fact, our current app assumes that it's its // responsibility to load the inputs, not oonimkall's. switch builder.InputPolicy() { - case engine.InputOrQueryBackend, engine.InputStrictlyRequired: + case model.InputOrQueryBackend, model.InputStrictlyRequired: if len(r.settings.Inputs) <= 0 { r.emitter.EmitFailureStartup("no input provided") return } - case engine.InputOrStaticDefault: + case model.InputOrStaticDefault: if len(r.settings.Inputs) <= 0 { inputs, err := engine.StaticBareInputForExperiment(r.settings.Name) if err != nil { @@ -198,7 +198,7 @@ func (r *runnerForTask) Run(rootCtx context.Context) { } r.settings.Inputs = inputs } - case engine.InputOptional: + case model.InputOptional: if len(r.settings.Inputs) <= 0 { r.settings.Inputs = append(r.settings.Inputs, "") } @@ -240,7 +240,7 @@ func (r *runnerForTask) Run(rootCtx context.Context) { // this policy in the future, but for now this covers in a // reasonable way web connectivity, so we should be ok. switch builder.InputPolicy() { - case engine.InputOrQueryBackend, engine.InputStrictlyRequired: + case model.InputOrQueryBackend, model.InputStrictlyRequired: var ( cancelMeas context.CancelFunc cancelSubmit context.CancelFunc diff --git a/pkg/oonimkall/taskrunner_test.go b/pkg/oonimkall/taskrunner_test.go index 5e61621..06d8fd6 100644 --- a/pkg/oonimkall/taskrunner_test.go +++ b/pkg/oonimkall/taskrunner_test.go @@ -204,8 +204,8 @@ func TestTaskRunnerRun(t *testing.T) { }, MockableSetCallbacks: func(callbacks model.ExperimentCallbacks) { }, - MockableInputPolicy: func() engine.InputPolicy { - return engine.InputNone + MockableInputPolicy: func() model.InputPolicy { + return model.InputNone }, MockableInterruptible: func() bool { return false @@ -310,8 +310,8 @@ func TestTaskRunnerRun(t *testing.T) { t.Run("with missing input and InputOrQueryBackend policy", func(t *testing.T) { runner, emitter := newRunnerForTesting() fake := fakeSuccessfulRun() - fake.MockableInputPolicy = func() engine.InputPolicy { - return engine.InputOrQueryBackend + fake.MockableInputPolicy = func() model.InputPolicy { + return model.InputOrQueryBackend } runner.sessionBuilder = fake events := runAndCollect(runner, emitter) @@ -331,8 +331,8 @@ func TestTaskRunnerRun(t *testing.T) { t.Run("with missing input and InputStrictlyRequired policy", func(t *testing.T) { runner, emitter := newRunnerForTesting() fake := fakeSuccessfulRun() - fake.MockableInputPolicy = func() engine.InputPolicy { - return engine.InputStrictlyRequired + fake.MockableInputPolicy = func() model.InputPolicy { + return model.InputStrictlyRequired } runner.sessionBuilder = fake events := runAndCollect(runner, emitter) @@ -355,8 +355,8 @@ func TestTaskRunnerRun(t *testing.T) { runner, emitter := newRunnerForTesting() runner.settings.Name = "Antani" // no input for this experiment fake := fakeSuccessfulRun() - fake.MockableInputPolicy = func() engine.InputPolicy { - return engine.InputOrStaticDefault + fake.MockableInputPolicy = func() model.InputPolicy { + return model.InputOrStaticDefault } runner.sessionBuilder = fake events := runAndCollect(runner, emitter) @@ -377,8 +377,8 @@ func TestTaskRunnerRun(t *testing.T) { runner, emitter := newRunnerForTesting() runner.settings.Inputs = append(runner.settings.Inputs, "https://x.org/") fake := fakeSuccessfulRun() - fake.MockableInputPolicy = func() engine.InputPolicy { - return engine.InputNone + fake.MockableInputPolicy = func() model.InputPolicy { + return model.InputNone } runner.sessionBuilder = fake events := runAndCollect(runner, emitter) @@ -419,8 +419,8 @@ func TestTaskRunnerRun(t *testing.T) { t.Run("with success and InputNone policy", func(t *testing.T) { runner, emitter := newRunnerForTesting() fake := fakeSuccessfulRun() - fake.MockableInputPolicy = func() engine.InputPolicy { - return engine.InputNone + fake.MockableInputPolicy = func() model.InputPolicy { + return model.InputNone } runner.sessionBuilder = fake events := runAndCollect(runner, emitter) @@ -445,8 +445,8 @@ func TestTaskRunnerRun(t *testing.T) { t.Run("with measurement failure and InputNone policy", func(t *testing.T) { runner, emitter := newRunnerForTesting() fake := fakeSuccessfulRun() - fake.MockableInputPolicy = func() engine.InputPolicy { - return engine.InputNone + fake.MockableInputPolicy = func() model.InputPolicy { + return model.InputNone } fake.MockableMeasureWithContext = func(ctx context.Context, input string) (measurement *model.Measurement, err error) { return nil, errors.New("preconditions error") @@ -475,8 +475,8 @@ func TestTaskRunnerRun(t *testing.T) { // which is what was happening in the above referenced issue. runner, emitter := newRunnerForTesting() fake := fakeSuccessfulRun() - fake.MockableInputPolicy = func() engine.InputPolicy { - return engine.InputNone + fake.MockableInputPolicy = func() model.InputPolicy { + return model.InputNone } fake.MockableMeasureWithContext = func(ctx context.Context, input string) (measurement *model.Measurement, err error) { return nil, errors.New("preconditions error") @@ -506,8 +506,8 @@ func TestTaskRunnerRun(t *testing.T) { runner, emitter := newRunnerForTesting() runner.settings.Inputs = []string{"a", "b", "c", "d"} fake := fakeSuccessfulRun() - fake.MockableInputPolicy = func() engine.InputPolicy { - return engine.InputStrictlyRequired + fake.MockableInputPolicy = func() model.InputPolicy { + return model.InputStrictlyRequired } runner.sessionBuilder = fake events := runAndCollect(runner, emitter) @@ -554,8 +554,8 @@ func TestTaskRunnerRun(t *testing.T) { runner, emitter := newRunnerForTesting() runner.settings.Inputs = []string{"a", "b", "c", "d"} fake := fakeSuccessfulRun() - fake.MockableInputPolicy = func() engine.InputPolicy { - return engine.InputOptional + fake.MockableInputPolicy = func() model.InputPolicy { + return model.InputOptional } runner.sessionBuilder = fake events := runAndCollect(runner, emitter) @@ -601,8 +601,8 @@ func TestTaskRunnerRun(t *testing.T) { t.Run("with success and InputOptional and no input", func(t *testing.T) { runner, emitter := newRunnerForTesting() fake := fakeSuccessfulRun() - fake.MockableInputPolicy = func() engine.InputPolicy { - return engine.InputOptional + fake.MockableInputPolicy = func() model.InputPolicy { + return model.InputOptional } runner.sessionBuilder = fake events := runAndCollect(runner, emitter) @@ -631,8 +631,8 @@ func TestTaskRunnerRun(t *testing.T) { runner, emitter := newRunnerForTesting() runner.settings.Name = experimentName fake := fakeSuccessfulRun() - fake.MockableInputPolicy = func() engine.InputPolicy { - return engine.InputOrStaticDefault + fake.MockableInputPolicy = func() model.InputPolicy { + return model.InputOrStaticDefault } runner.sessionBuilder = fake events := runAndCollect(runner, emitter) @@ -667,8 +667,8 @@ func TestTaskRunnerRun(t *testing.T) { runner.settings.Inputs = []string{"a", "b", "c", "d"} runner.settings.Options.MaxRuntime = 2 fake := fakeSuccessfulRun() - fake.MockableInputPolicy = func() engine.InputPolicy { - return engine.InputStrictlyRequired + fake.MockableInputPolicy = func() model.InputPolicy { + return model.InputStrictlyRequired } fake.MockableMeasureWithContext = func(ctx context.Context, input string) (measurement *model.Measurement, err error) { time.Sleep(1 * time.Second) @@ -708,8 +708,8 @@ func TestTaskRunnerRun(t *testing.T) { runner.settings.Inputs = []string{"a", "b", "c", "d"} runner.settings.Options.MaxRuntime = 2 fake := fakeSuccessfulRun() - fake.MockableInputPolicy = func() engine.InputPolicy { - return engine.InputStrictlyRequired + fake.MockableInputPolicy = func() model.InputPolicy { + return model.InputStrictlyRequired } fake.MockableInterruptible = func() bool { return true @@ -743,8 +743,8 @@ func TestTaskRunnerRun(t *testing.T) { runner, emitter := newRunnerForTesting() runner.settings.Inputs = []string{"a"} fake := fakeSuccessfulRun() - fake.MockableInputPolicy = func() engine.InputPolicy { - return engine.InputStrictlyRequired + fake.MockableInputPolicy = func() model.InputPolicy { + return model.InputStrictlyRequired } fake.MockableSubmitAndUpdateMeasurementContext = func(ctx context.Context, measurement *model.Measurement) error { return errors.New("cannot submit") diff --git a/pkg/oonimkall/tasksession.go b/pkg/oonimkall/tasksession.go index 86d2e08..27b1dd6 100644 --- a/pkg/oonimkall/tasksession.go +++ b/pkg/oonimkall/tasksession.go @@ -59,7 +59,7 @@ func (sess *taskSessionEngine) NewExperimentBuilderByName( // taskExperimentBuilderEngine wraps ./internal/engine's // ExperimentBuilder type. type taskExperimentBuilderEngine struct { - engine.ExperimentBuilder + model.ExperimentBuilder } var _ taskExperimentBuilder = &taskExperimentBuilderEngine{} @@ -72,7 +72,7 @@ func (b *taskExperimentBuilderEngine) NewExperimentInstance() taskExperiment { // taskExperimentEngine wraps ./internal/engine's Experiment. type taskExperimentEngine struct { - engine.Experiment + model.Experiment } var _ taskExperiment = &taskExperimentEngine{}