From 4ea29ff336e489eda529744a9f1eeabef56c45ec Mon Sep 17 00:00:00 2001 From: Simone Basso Date: Thu, 6 Jun 2024 13:51:37 +0200 Subject: [PATCH 01/63] refactor: make TargetLoader using ExperimentBuilder This diff completes the set of preliminary richer input diffs. We build the TargetLoader using the ExperimentBuilder, which in turn uses a registry.Factory under the hood. This means that we can load targets for each experiment. Part of https://github.com/ooni/probe/issues/2607 --- cmd/ooniprobe/internal/nettests/dnscheck.go | 16 ++++---- .../internal/nettests/stunreachability.go | 16 ++++---- .../internal/nettests/web_connectivity.go | 21 +++++----- internal/engine/experimentbuilder.go | 7 ++++ internal/mocks/experimentbuilder.go | 8 ++++ internal/model/experiment.go | 40 ++++++++++++++++++- internal/oonirun/experiment.go | 21 +++++----- internal/oonirun/experiment_test.go | 12 +++--- internal/registry/dash.go | 4 +- internal/registry/dnscheck.go | 4 +- internal/registry/dnsping.go | 4 +- internal/registry/dslxtutorial.go | 8 ++-- internal/registry/echcheck.go | 8 ++-- internal/registry/example.go | 4 +- internal/registry/factory.go | 20 ++++++++++ internal/registry/fbmessenger.go | 4 +- internal/registry/hhfm.go | 4 +- internal/registry/hirl.go | 4 +- internal/registry/httphostheader.go | 4 +- internal/registry/ndt.go | 4 +- internal/registry/openvpn.go | 4 +- internal/registry/portfiltering.go | 4 +- internal/registry/psiphon.go | 4 +- internal/registry/quicping.go | 4 +- internal/registry/riseupvpn.go | 8 ++-- internal/registry/signal.go | 4 +- internal/registry/simplequicping.go | 4 +- internal/registry/sniblocking.go | 4 +- internal/registry/stunreachability.go | 4 +- internal/registry/tcpping.go | 4 +- internal/registry/telegram.go | 4 +- internal/registry/tlsmiddlebox.go | 4 +- internal/registry/tlsping.go | 4 +- internal/registry/tlstool.go | 4 +- internal/registry/tor.go | 4 +- internal/registry/torsf.go | 4 +- internal/registry/urlgetter.go | 4 +- internal/registry/vanillator.go | 6 ++- internal/registry/webconnectivity.go | 4 +- internal/registry/webconnectivityv05.go | 4 +- internal/registry/whatsapp.go | 4 +- internal/targetloading/targetloading.go | 13 ++---- internal/targetloading/targetloading_test.go | 11 ++++- 43 files changed, 229 insertions(+), 98 deletions(-) diff --git a/cmd/ooniprobe/internal/nettests/dnscheck.go b/cmd/ooniprobe/internal/nettests/dnscheck.go index cb53560155..2c8e798622 100644 --- a/cmd/ooniprobe/internal/nettests/dnscheck.go +++ b/cmd/ooniprobe/internal/nettests/dnscheck.go @@ -4,23 +4,21 @@ import ( "context" "github.com/ooni/probe-cli/v3/internal/model" - "github.com/ooni/probe-cli/v3/internal/targetloading" ) // DNSCheck nettest implementation. type DNSCheck struct{} -func (n DNSCheck) lookupURLs(ctl *Controller) ([]model.ExperimentTarget, error) { - targetloader := &targetloading.Loader{ +func (n DNSCheck) lookupURLs(ctl *Controller, builder model.ExperimentBuilder) ([]model.ExperimentTarget, error) { + config := &model.ExperimentTargetLoaderConfig{ CheckInConfig: &model.OOAPICheckInConfig{ // not needed because we have default static input in the engine }, - ExperimentName: "dnscheck", - InputPolicy: model.InputOrStaticDefault, - Session: ctl.Session, - SourceFiles: ctl.InputFiles, - StaticInputs: ctl.Inputs, + Session: ctl.Session, + SourceFiles: ctl.InputFiles, + StaticInputs: ctl.Inputs, } + targetloader := builder.NewTargetLoader(config) testlist, err := targetloader.Load(context.Background()) if err != nil { return nil, err @@ -34,7 +32,7 @@ func (n DNSCheck) Run(ctl *Controller) error { if err != nil { return err } - urls, err := n.lookupURLs(ctl) + urls, err := n.lookupURLs(ctl, builder) if err != nil { return err } diff --git a/cmd/ooniprobe/internal/nettests/stunreachability.go b/cmd/ooniprobe/internal/nettests/stunreachability.go index 743d53e8f2..c6313d96c8 100644 --- a/cmd/ooniprobe/internal/nettests/stunreachability.go +++ b/cmd/ooniprobe/internal/nettests/stunreachability.go @@ -4,23 +4,21 @@ import ( "context" "github.com/ooni/probe-cli/v3/internal/model" - "github.com/ooni/probe-cli/v3/internal/targetloading" ) // STUNReachability nettest implementation. type STUNReachability struct{} -func (n STUNReachability) lookupURLs(ctl *Controller) ([]model.ExperimentTarget, error) { - targetloader := &targetloading.Loader{ +func (n STUNReachability) lookupURLs(ctl *Controller, builder model.ExperimentBuilder) ([]model.ExperimentTarget, error) { + config := &model.ExperimentTargetLoaderConfig{ CheckInConfig: &model.OOAPICheckInConfig{ // not needed because we have default static input in the engine }, - ExperimentName: "stunreachability", - InputPolicy: model.InputOrStaticDefault, - Session: ctl.Session, - SourceFiles: ctl.InputFiles, - StaticInputs: ctl.Inputs, + Session: ctl.Session, + SourceFiles: ctl.InputFiles, + StaticInputs: ctl.Inputs, } + targetloader := builder.NewTargetLoader(config) testlist, err := targetloader.Load(context.Background()) if err != nil { return nil, err @@ -34,7 +32,7 @@ func (n STUNReachability) Run(ctl *Controller) error { if err != nil { return err } - urls, err := n.lookupURLs(ctl) + urls, err := n.lookupURLs(ctl, builder) if err != nil { return err } diff --git a/cmd/ooniprobe/internal/nettests/web_connectivity.go b/cmd/ooniprobe/internal/nettests/web_connectivity.go index 7e4bcad996..0da3be56d1 100644 --- a/cmd/ooniprobe/internal/nettests/web_connectivity.go +++ b/cmd/ooniprobe/internal/nettests/web_connectivity.go @@ -5,11 +5,11 @@ import ( "github.com/apex/log" "github.com/ooni/probe-cli/v3/internal/model" - "github.com/ooni/probe-cli/v3/internal/targetloading" ) -func (n WebConnectivity) lookupURLs(ctl *Controller, categories []string) ([]model.ExperimentTarget, error) { - targetloader := &targetloading.Loader{ +func (n WebConnectivity) lookupURLs( + ctl *Controller, builder model.ExperimentBuilder, categories []string) ([]model.ExperimentTarget, error) { + config := &model.ExperimentTargetLoaderConfig{ CheckInConfig: &model.OOAPICheckInConfig{ // Setting Charging and OnWiFi to true causes the CheckIn // API to return to us as much URL as possible with the @@ -21,12 +21,11 @@ func (n WebConnectivity) lookupURLs(ctl *Controller, categories []string) ([]mod CategoryCodes: categories, }, }, - ExperimentName: "web_connectivity", - InputPolicy: model.InputOrQueryBackend, - Session: ctl.Session, - SourceFiles: ctl.InputFiles, - StaticInputs: ctl.Inputs, + Session: ctl.Session, + SourceFiles: ctl.InputFiles, + StaticInputs: ctl.Inputs, } + targetloader := builder.NewTargetLoader(config) testlist, err := targetloader.Load(context.Background()) if err != nil { return nil, err @@ -39,12 +38,12 @@ type WebConnectivity struct{} // Run starts the test func (n WebConnectivity) Run(ctl *Controller) error { - log.Debugf("Enabled category codes are the following %v", ctl.Probe.Config().Nettests.WebsitesEnabledCategoryCodes) - urls, err := n.lookupURLs(ctl, ctl.Probe.Config().Nettests.WebsitesEnabledCategoryCodes) + builder, err := ctl.Session.NewExperimentBuilder("web_connectivity") if err != nil { return err } - builder, err := ctl.Session.NewExperimentBuilder("web_connectivity") + log.Debugf("Enabled category codes are the following %v", ctl.Probe.Config().Nettests.WebsitesEnabledCategoryCodes) + urls, err := n.lookupURLs(ctl, builder, ctl.Probe.Config().Nettests.WebsitesEnabledCategoryCodes) if err != nil { return err } diff --git a/internal/engine/experimentbuilder.go b/internal/engine/experimentbuilder.go index a60d0a50d5..330777957b 100644 --- a/internal/engine/experimentbuilder.go +++ b/internal/engine/experimentbuilder.go @@ -22,6 +22,8 @@ type experimentBuilder struct { session *Session } +var _ model.ExperimentBuilder = &experimentBuilder{} + // Interruptible implements ExperimentBuilder.Interruptible. func (b *experimentBuilder) Interruptible() bool { return b.factory.Interruptible() @@ -60,6 +62,11 @@ func (b *experimentBuilder) NewExperiment() model.Experiment { return experiment } +// NewTargetLoader creates a new [model.ExperimentTargetLoader] instance. +func (b *experimentBuilder) NewTargetLoader(config *model.ExperimentTargetLoaderConfig) model.ExperimentTargetLoader { + return b.factory.NewTargetLoader(config) +} + // newExperimentBuilder creates a new experimentBuilder instance. func newExperimentBuilder(session *Session, name string) (*experimentBuilder, error) { factory, err := registry.NewFactory(name, session.kvStore, session.logger) diff --git a/internal/mocks/experimentbuilder.go b/internal/mocks/experimentbuilder.go index 16763d471d..1f6a27187f 100644 --- a/internal/mocks/experimentbuilder.go +++ b/internal/mocks/experimentbuilder.go @@ -17,8 +17,12 @@ type ExperimentBuilder struct { MockSetCallbacks func(callbacks model.ExperimentCallbacks) MockNewExperiment func() model.Experiment + + MockNewTargetLoader func(config *model.ExperimentTargetLoaderConfig) model.ExperimentTargetLoader } +var _ model.ExperimentBuilder = &ExperimentBuilder{} + func (eb *ExperimentBuilder) Interruptible() bool { return eb.MockInterruptible() } @@ -46,3 +50,7 @@ func (eb *ExperimentBuilder) SetCallbacks(callbacks model.ExperimentCallbacks) { func (eb *ExperimentBuilder) NewExperiment() model.Experiment { return eb.MockNewExperiment() } + +func (eb *ExperimentBuilder) NewTargetLoader(config *model.ExperimentTargetLoaderConfig) model.ExperimentTargetLoader { + return eb.MockNewTargetLoader(config) +} diff --git a/internal/model/experiment.go b/internal/model/experiment.go index 9ae3dbcce6..8bd309f326 100644 --- a/internal/model/experiment.go +++ b/internal/model/experiment.go @@ -232,8 +232,46 @@ type ExperimentBuilder interface { // SetCallbacks sets the experiment's interactive callbacks. SetCallbacks(callbacks ExperimentCallbacks) - // NewExperiment creates the experiment instance. + // NewExperiment creates the [Experiment] instance. NewExperiment() Experiment + + // NewTargetLoader creates the [ExperimentTargetLoader] instance. + NewTargetLoader(config *ExperimentTargetLoaderConfig) ExperimentTargetLoader +} + +// ExperimentTargetLoaderConfig is the configuration to create a new [ExperimentTargetLoader]. +// +// The zero value is not ready to use; please, init the MANDATORY fields. +type ExperimentTargetLoaderConfig struct { + // CheckInConfig contains OPTIONAL options for the CheckIn API. If not set, then we'll create a + // default config. If set but there are fields inside it that are not set, then we will set them + // to a default value. + CheckInConfig *OOAPICheckInConfig + + // Session is the MANDATORY current measurement session. + Session ExperimentTargetLoaderSession + + // StaticInputs contains OPTIONAL input to be added + // to the resulting input list if possible. + StaticInputs []string + + // SourceFiles contains OPTIONAL files to read input + // from. Each file should contain a single input string + // per line. We will fail if any file is unreadable + // as well as if any file is empty. + SourceFiles []string +} + +// ExperimentTargetLoaderSession is the session according to [ExperimentTargetLoader]. +type ExperimentTargetLoaderSession interface { + // CheckIn invokes the check-in API. + CheckIn(ctx context.Context, config *OOAPICheckInConfig) (*OOAPICheckInResult, error) + + // FetchOpenVPNConfig fetches the OpenVPN experiment configuration. + FetchOpenVPNConfig(ctx context.Context, provider, cc string) (*OOAPIVPNProviderConfig, error) + + // Logger returns the logger to use. + Logger() Logger } // ExperimentOptionInfo contains info about an experiment option. diff --git a/internal/oonirun/experiment.go b/internal/oonirun/experiment.go index 09deedaedc..758db9e0c5 100644 --- a/internal/oonirun/experiment.go +++ b/internal/oonirun/experiment.go @@ -14,7 +14,6 @@ import ( "github.com/ooni/probe-cli/v3/internal/humanize" "github.com/ooni/probe-cli/v3/internal/model" - "github.com/ooni/probe-cli/v3/internal/targetloading" ) // experimentShuffledInputs counts how many times we shuffled inputs @@ -61,7 +60,7 @@ type Experiment struct { newExperimentBuilderFn func(experimentName string) (model.ExperimentBuilder, error) // newTargetLoaderFn is OPTIONAL and used for testing. - newTargetLoaderFn func(inputPolicy model.InputPolicy) targetLoader + newTargetLoaderFn func(builder model.ExperimentBuilder) targetLoader // newSubmitterFn is OPTIONAL and used for testing. newSubmitterFn func(ctx context.Context) (model.Submitter, error) @@ -84,7 +83,7 @@ func (ed *Experiment) Run(ctx context.Context) error { } // 2. create input loader and load input for this experiment - targetLoader := ed.newTargetLoader(builder.InputPolicy()) + targetLoader := ed.newTargetLoader(builder) targetList, err := targetLoader.Load(ctx) if err != nil { return err @@ -196,22 +195,20 @@ func (ed *Experiment) newExperimentBuilder(experimentName string) (model.Experim type targetLoader = model.ExperimentTargetLoader // newTargetLoader creates a new [model.ExperimentTargetLoader]. -func (ed *Experiment) newTargetLoader(inputPolicy model.InputPolicy) targetLoader { +func (ed *Experiment) newTargetLoader(builder model.ExperimentBuilder) targetLoader { if ed.newTargetLoaderFn != nil { - return ed.newTargetLoaderFn(inputPolicy) + return ed.newTargetLoaderFn(builder) } - return &targetloading.Loader{ + return builder.NewTargetLoader(&model.ExperimentTargetLoaderConfig{ CheckInConfig: &model.OOAPICheckInConfig{ RunType: model.RunTypeManual, OnWiFi: true, // meaning: not on 4G Charging: true, }, - ExperimentName: ed.Name, - InputPolicy: inputPolicy, - StaticInputs: ed.Inputs, - SourceFiles: ed.InputFilePaths, - Session: ed.Session, - } + StaticInputs: ed.Inputs, + SourceFiles: ed.InputFilePaths, + Session: ed.Session, + }) } // experimentOptionsToStringList convers the options to []string, which is diff --git a/internal/oonirun/experiment_test.go b/internal/oonirun/experiment_test.go index 29764d9c8c..b93fd25ae2 100644 --- a/internal/oonirun/experiment_test.go +++ b/internal/oonirun/experiment_test.go @@ -165,7 +165,7 @@ func TestExperimentRun(t *testing.T) { ReportFile string Session Session newExperimentBuilderFn func(experimentName string) (model.ExperimentBuilder, error) - newTargetLoaderFn func(inputPolicy model.InputPolicy) targetLoader + newTargetLoaderFn func(builder model.ExperimentBuilder) targetLoader newSubmitterFn func(ctx context.Context) (model.Submitter, error) newSaverFn func() (model.Saver, error) newInputProcessorFn func(experiment model.Experiment, @@ -199,7 +199,7 @@ func TestExperimentRun(t *testing.T) { } return eb, nil }, - newTargetLoaderFn: func(inputPolicy model.InputPolicy) targetLoader { + newTargetLoaderFn: func(builder model.ExperimentBuilder) targetLoader { return &mocks.ExperimentTargetLoader{ MockLoad: func(ctx context.Context) ([]model.ExperimentTarget, error) { return nil, errMocked @@ -223,7 +223,7 @@ func TestExperimentRun(t *testing.T) { } return eb, nil }, - newTargetLoaderFn: func(inputPolicy model.InputPolicy) targetLoader { + newTargetLoaderFn: func(builder model.ExperimentBuilder) targetLoader { return &mocks.ExperimentTargetLoader{ MockLoad: func(ctx context.Context) ([]model.ExperimentTarget, error) { return []model.ExperimentTarget{}, nil @@ -263,7 +263,7 @@ func TestExperimentRun(t *testing.T) { } return eb, nil }, - newTargetLoaderFn: func(inputPolicy model.InputPolicy) targetLoader { + newTargetLoaderFn: func(builder model.ExperimentBuilder) targetLoader { return &mocks.ExperimentTargetLoader{ MockLoad: func(ctx context.Context) ([]model.ExperimentTarget, error) { return []model.ExperimentTarget{}, nil @@ -306,7 +306,7 @@ func TestExperimentRun(t *testing.T) { } return eb, nil }, - newTargetLoaderFn: func(inputPolicy model.InputPolicy) targetLoader { + newTargetLoaderFn: func(builder model.ExperimentBuilder) targetLoader { return &mocks.ExperimentTargetLoader{ MockLoad: func(ctx context.Context) ([]model.ExperimentTarget, error) { return []model.ExperimentTarget{}, nil @@ -352,7 +352,7 @@ func TestExperimentRun(t *testing.T) { } return eb, nil }, - newTargetLoaderFn: func(inputPolicy model.InputPolicy) targetLoader { + newTargetLoaderFn: func(builder model.ExperimentBuilder) targetLoader { return &mocks.ExperimentTargetLoader{ MockLoad: func(ctx context.Context) ([]model.ExperimentTarget, error) { return []model.ExperimentTarget{}, nil diff --git a/internal/registry/dash.go b/internal/registry/dash.go index 812619361b..e8a2ad1165 100644 --- a/internal/registry/dash.go +++ b/internal/registry/dash.go @@ -10,13 +10,15 @@ import ( ) func init() { - AllExperiments["dash"] = func() *Factory { + const canonicalName = "dash" + AllExperiments[canonicalName] = func() *Factory { return &Factory{ build: func(config interface{}) model.ExperimentMeasurer { return dash.NewExperimentMeasurer( *config.(*dash.Config), ) }, + canonicalName: canonicalName, config: &dash.Config{}, enabledByDefault: true, interruptible: true, diff --git a/internal/registry/dnscheck.go b/internal/registry/dnscheck.go index b9355773fc..2890ca19a5 100644 --- a/internal/registry/dnscheck.go +++ b/internal/registry/dnscheck.go @@ -10,13 +10,15 @@ import ( ) func init() { - AllExperiments["dnscheck"] = func() *Factory { + const canonicalName = "dnscheck" + AllExperiments[canonicalName] = func() *Factory { return &Factory{ build: func(config interface{}) model.ExperimentMeasurer { return dnscheck.NewExperimentMeasurer( *config.(*dnscheck.Config), ) }, + canonicalName: canonicalName, config: &dnscheck.Config{}, enabledByDefault: true, inputPolicy: model.InputOrStaticDefault, diff --git a/internal/registry/dnsping.go b/internal/registry/dnsping.go index 92d52456ed..fb01d1ab80 100644 --- a/internal/registry/dnsping.go +++ b/internal/registry/dnsping.go @@ -10,13 +10,15 @@ import ( ) func init() { - AllExperiments["dnsping"] = func() *Factory { + const canonicalName = "dnsping" + AllExperiments[canonicalName] = func() *Factory { return &Factory{ build: func(config interface{}) model.ExperimentMeasurer { return dnsping.NewExperimentMeasurer( *config.(*dnsping.Config), ) }, + canonicalName: canonicalName, config: &dnsping.Config{}, enabledByDefault: true, inputPolicy: model.InputOrStaticDefault, diff --git a/internal/registry/dslxtutorial.go b/internal/registry/dslxtutorial.go index 41aef7d40c..199e36077c 100644 --- a/internal/registry/dslxtutorial.go +++ b/internal/registry/dslxtutorial.go @@ -10,15 +10,17 @@ import ( ) func init() { - AllExperiments["simple_sni"] = func() *Factory { + const canonicalName = "simple_sni" + AllExperiments[canonicalName] = func() *Factory { return &Factory{ build: func(config interface{}) model.ExperimentMeasurer { return chapter02.NewExperimentMeasurer( *config.(*chapter02.Config), ) }, - config: &chapter02.Config{}, - inputPolicy: model.InputOrQueryBackend, + canonicalName: canonicalName, + config: &chapter02.Config{}, + inputPolicy: model.InputOrQueryBackend, } } } diff --git a/internal/registry/echcheck.go b/internal/registry/echcheck.go index b091d58af9..939cf3d15d 100644 --- a/internal/registry/echcheck.go +++ b/internal/registry/echcheck.go @@ -10,15 +10,17 @@ import ( ) func init() { - AllExperiments["echcheck"] = func() *Factory { + const canonicalName = "echcheck" + AllExperiments[canonicalName] = func() *Factory { return &Factory{ build: func(config interface{}) model.ExperimentMeasurer { return echcheck.NewExperimentMeasurer( *config.(*echcheck.Config), ) }, - config: &echcheck.Config{}, - inputPolicy: model.InputOptional, + canonicalName: canonicalName, + config: &echcheck.Config{}, + inputPolicy: model.InputOptional, } } } diff --git a/internal/registry/example.go b/internal/registry/example.go index aa2d13ee11..94c55ca59f 100644 --- a/internal/registry/example.go +++ b/internal/registry/example.go @@ -12,13 +12,15 @@ import ( ) func init() { - AllExperiments["example"] = func() *Factory { + const canonicalName = "example" + AllExperiments[canonicalName] = func() *Factory { return &Factory{ build: func(config interface{}) model.ExperimentMeasurer { return example.NewExperimentMeasurer( *config.(*example.Config), "example", ) }, + canonicalName: canonicalName, config: &example.Config{ Message: "Good day from the example experiment!", SleepTime: int64(time.Second), diff --git a/internal/registry/factory.go b/internal/registry/factory.go index f0baa34806..aba0946d64 100644 --- a/internal/registry/factory.go +++ b/internal/registry/factory.go @@ -15,6 +15,7 @@ import ( "github.com/ooni/probe-cli/v3/internal/checkincache" "github.com/ooni/probe-cli/v3/internal/model" "github.com/ooni/probe-cli/v3/internal/strcasex" + "github.com/ooni/probe-cli/v3/internal/targetloading" ) // Factory allows to construct an experiment measurer. @@ -22,6 +23,9 @@ type Factory struct { // build is the constructor that build an experiment with the given config. build func(config interface{}) model.ExperimentMeasurer + // canonicalName is the canonical name of the experiment. + canonicalName string + // config contains the experiment's config. config any @@ -218,6 +222,22 @@ func (b *Factory) NewExperimentMeasurer() model.ExperimentMeasurer { return b.build(b.config) } +// Session is the session definition according to this package. +type Session = model.ExperimentTargetLoaderSession + +// NewTargetLoader creates a new [model.ExperimentTargetLoader] instance. +func (b *Factory) NewTargetLoader(config *model.ExperimentTargetLoaderConfig) model.ExperimentTargetLoader { + return &targetloading.Loader{ + CheckInConfig: config.CheckInConfig, // OPTIONAL + ExperimentName: b.canonicalName, + InputPolicy: b.inputPolicy, + Logger: config.Session.Logger(), + Session: config.Session, + StaticInputs: config.StaticInputs, + SourceFiles: config.SourceFiles, + } +} + // CanonicalizeExperimentName allows code to provide experiment names // in a more flexible way, where we have aliases. // diff --git a/internal/registry/fbmessenger.go b/internal/registry/fbmessenger.go index d3f3233e3a..cd46519826 100644 --- a/internal/registry/fbmessenger.go +++ b/internal/registry/fbmessenger.go @@ -10,13 +10,15 @@ import ( ) func init() { - AllExperiments["facebook_messenger"] = func() *Factory { + const canonicalName = "facebook_messenger" + AllExperiments[canonicalName] = func() *Factory { return &Factory{ build: func(config interface{}) model.ExperimentMeasurer { return fbmessenger.NewExperimentMeasurer( *config.(*fbmessenger.Config), ) }, + canonicalName: canonicalName, config: &fbmessenger.Config{}, enabledByDefault: true, inputPolicy: model.InputNone, diff --git a/internal/registry/hhfm.go b/internal/registry/hhfm.go index 0820a86476..a86ce98192 100644 --- a/internal/registry/hhfm.go +++ b/internal/registry/hhfm.go @@ -10,13 +10,15 @@ import ( ) func init() { - AllExperiments["http_header_field_manipulation"] = func() *Factory { + const canonicalName = "http_header_field_manipulation" + AllExperiments[canonicalName] = func() *Factory { return &Factory{ build: func(config interface{}) model.ExperimentMeasurer { return hhfm.NewExperimentMeasurer( *config.(*hhfm.Config), ) }, + canonicalName: canonicalName, config: &hhfm.Config{}, enabledByDefault: true, inputPolicy: model.InputNone, diff --git a/internal/registry/hirl.go b/internal/registry/hirl.go index 846493bc8f..84dbea7ccd 100644 --- a/internal/registry/hirl.go +++ b/internal/registry/hirl.go @@ -10,13 +10,15 @@ import ( ) func init() { - AllExperiments["http_invalid_request_line"] = func() *Factory { + const canonicalName = "http_invalid_request_line" + AllExperiments[canonicalName] = func() *Factory { return &Factory{ build: func(config interface{}) model.ExperimentMeasurer { return hirl.NewExperimentMeasurer( *config.(*hirl.Config), ) }, + canonicalName: canonicalName, config: &hirl.Config{}, enabledByDefault: true, inputPolicy: model.InputNone, diff --git a/internal/registry/httphostheader.go b/internal/registry/httphostheader.go index c1ecffd76f..c3a02b655c 100644 --- a/internal/registry/httphostheader.go +++ b/internal/registry/httphostheader.go @@ -10,13 +10,15 @@ import ( ) func init() { - AllExperiments["http_host_header"] = func() *Factory { + const canonicalName = "http_host_header" + AllExperiments[canonicalName] = func() *Factory { return &Factory{ build: func(config interface{}) model.ExperimentMeasurer { return httphostheader.NewExperimentMeasurer( *config.(*httphostheader.Config), ) }, + canonicalName: canonicalName, config: &httphostheader.Config{}, enabledByDefault: true, inputPolicy: model.InputOrQueryBackend, diff --git a/internal/registry/ndt.go b/internal/registry/ndt.go index e6891c7c7b..71ea5562a6 100644 --- a/internal/registry/ndt.go +++ b/internal/registry/ndt.go @@ -10,13 +10,15 @@ import ( ) func init() { - AllExperiments["ndt"] = func() *Factory { + const canonicalName = "ndt" + AllExperiments[canonicalName] = func() *Factory { return &Factory{ build: func(config interface{}) model.ExperimentMeasurer { return ndt7.NewExperimentMeasurer( *config.(*ndt7.Config), ) }, + canonicalName: canonicalName, config: &ndt7.Config{}, enabledByDefault: true, interruptible: true, diff --git a/internal/registry/openvpn.go b/internal/registry/openvpn.go index cf9244786f..8bf107630c 100644 --- a/internal/registry/openvpn.go +++ b/internal/registry/openvpn.go @@ -10,13 +10,15 @@ import ( ) func init() { - AllExperiments["openvpn"] = func() *Factory { + const canonicalName = "openvpn" + AllExperiments[canonicalName] = func() *Factory { return &Factory{ build: func(config interface{}) model.ExperimentMeasurer { return openvpn.NewExperimentMeasurer( *config.(*openvpn.Config), "openvpn", ) }, + canonicalName: canonicalName, config: &openvpn.Config{}, enabledByDefault: true, interruptible: true, diff --git a/internal/registry/portfiltering.go b/internal/registry/portfiltering.go index 56937eaca1..8c6a857df8 100644 --- a/internal/registry/portfiltering.go +++ b/internal/registry/portfiltering.go @@ -10,13 +10,15 @@ import ( ) func init() { - AllExperiments["portfiltering"] = func() *Factory { + const canonicalName = "portfiltering" + AllExperiments[canonicalName] = func() *Factory { return &Factory{ build: func(config any) model.ExperimentMeasurer { return portfiltering.NewExperimentMeasurer( config.(portfiltering.Config), ) }, + canonicalName: canonicalName, config: portfiltering.Config{}, enabledByDefault: true, interruptible: false, diff --git a/internal/registry/psiphon.go b/internal/registry/psiphon.go index 48dc7888a2..517bb22053 100644 --- a/internal/registry/psiphon.go +++ b/internal/registry/psiphon.go @@ -10,13 +10,15 @@ import ( ) func init() { - AllExperiments["psiphon"] = func() *Factory { + const canonicalName = "psiphon" + AllExperiments[canonicalName] = func() *Factory { return &Factory{ build: func(config interface{}) model.ExperimentMeasurer { return psiphon.NewExperimentMeasurer( *config.(*psiphon.Config), ) }, + canonicalName: canonicalName, config: &psiphon.Config{}, enabledByDefault: true, inputPolicy: model.InputOptional, diff --git a/internal/registry/quicping.go b/internal/registry/quicping.go index 090037ad78..77a1d3ebf2 100644 --- a/internal/registry/quicping.go +++ b/internal/registry/quicping.go @@ -10,13 +10,15 @@ import ( ) func init() { - AllExperiments["quicping"] = func() *Factory { + const canonicalName = "quicping" + AllExperiments[canonicalName] = func() *Factory { return &Factory{ build: func(config interface{}) model.ExperimentMeasurer { return quicping.NewExperimentMeasurer( *config.(*quicping.Config), ) }, + canonicalName: canonicalName, config: &quicping.Config{}, enabledByDefault: true, inputPolicy: model.InputStrictlyRequired, diff --git a/internal/registry/riseupvpn.go b/internal/registry/riseupvpn.go index 950a86b79e..6527f3a04f 100644 --- a/internal/registry/riseupvpn.go +++ b/internal/registry/riseupvpn.go @@ -10,15 +10,17 @@ import ( ) func init() { - AllExperiments["riseupvpn"] = func() *Factory { + const canonicalName = "riseupvpn" + AllExperiments[canonicalName] = func() *Factory { return &Factory{ build: func(config interface{}) model.ExperimentMeasurer { return riseupvpn.NewExperimentMeasurer( *config.(*riseupvpn.Config), ) }, - config: &riseupvpn.Config{}, - inputPolicy: model.InputNone, + canonicalName: canonicalName, + config: &riseupvpn.Config{}, + inputPolicy: model.InputNone, } } } diff --git a/internal/registry/signal.go b/internal/registry/signal.go index 615d44b732..5890936b31 100644 --- a/internal/registry/signal.go +++ b/internal/registry/signal.go @@ -10,13 +10,15 @@ import ( ) func init() { - AllExperiments["signal"] = func() *Factory { + const canonicalName = "signal" + AllExperiments[canonicalName] = func() *Factory { return &Factory{ build: func(config interface{}) model.ExperimentMeasurer { return signal.NewExperimentMeasurer( *config.(*signal.Config), ) }, + canonicalName: canonicalName, config: &signal.Config{}, enabledByDefault: true, inputPolicy: model.InputNone, diff --git a/internal/registry/simplequicping.go b/internal/registry/simplequicping.go index 8eb3e7c54e..c83b8cec88 100644 --- a/internal/registry/simplequicping.go +++ b/internal/registry/simplequicping.go @@ -10,13 +10,15 @@ import ( ) func init() { - AllExperiments["simplequicping"] = func() *Factory { + const canonicalName = "simplequicping" + AllExperiments[canonicalName] = func() *Factory { return &Factory{ build: func(config interface{}) model.ExperimentMeasurer { return simplequicping.NewExperimentMeasurer( *config.(*simplequicping.Config), ) }, + canonicalName: canonicalName, config: &simplequicping.Config{}, enabledByDefault: true, inputPolicy: model.InputStrictlyRequired, diff --git a/internal/registry/sniblocking.go b/internal/registry/sniblocking.go index 8af8214d68..a433de3dc2 100644 --- a/internal/registry/sniblocking.go +++ b/internal/registry/sniblocking.go @@ -10,13 +10,15 @@ import ( ) func init() { - AllExperiments["sni_blocking"] = func() *Factory { + const canonicalName = "sni_blocking" + AllExperiments[canonicalName] = func() *Factory { return &Factory{ build: func(config interface{}) model.ExperimentMeasurer { return sniblocking.NewExperimentMeasurer( *config.(*sniblocking.Config), ) }, + canonicalName: canonicalName, config: &sniblocking.Config{}, enabledByDefault: true, inputPolicy: model.InputOrQueryBackend, diff --git a/internal/registry/stunreachability.go b/internal/registry/stunreachability.go index dfa331c3fc..db35b59845 100644 --- a/internal/registry/stunreachability.go +++ b/internal/registry/stunreachability.go @@ -10,13 +10,15 @@ import ( ) func init() { - AllExperiments["stunreachability"] = func() *Factory { + const canonicalName = "stunreachability" + AllExperiments[canonicalName] = func() *Factory { return &Factory{ build: func(config interface{}) model.ExperimentMeasurer { return stunreachability.NewExperimentMeasurer( *config.(*stunreachability.Config), ) }, + canonicalName: canonicalName, config: &stunreachability.Config{}, enabledByDefault: true, inputPolicy: model.InputOrStaticDefault, diff --git a/internal/registry/tcpping.go b/internal/registry/tcpping.go index d0ca932494..029e6e2d1c 100644 --- a/internal/registry/tcpping.go +++ b/internal/registry/tcpping.go @@ -10,13 +10,15 @@ import ( ) func init() { - AllExperiments["tcpping"] = func() *Factory { + const canonicalName = "tcpping" + AllExperiments[canonicalName] = func() *Factory { return &Factory{ build: func(config interface{}) model.ExperimentMeasurer { return tcpping.NewExperimentMeasurer( *config.(*tcpping.Config), ) }, + canonicalName: canonicalName, config: &tcpping.Config{}, enabledByDefault: true, inputPolicy: model.InputStrictlyRequired, diff --git a/internal/registry/telegram.go b/internal/registry/telegram.go index 8f61d78baa..987e640935 100644 --- a/internal/registry/telegram.go +++ b/internal/registry/telegram.go @@ -10,13 +10,15 @@ import ( ) func init() { - AllExperiments["telegram"] = func() *Factory { + const canonicalName = "telegram" + AllExperiments[canonicalName] = func() *Factory { return &Factory{ build: func(config any) model.ExperimentMeasurer { return telegram.NewExperimentMeasurer( config.(telegram.Config), ) }, + canonicalName: canonicalName, config: telegram.Config{}, enabledByDefault: true, interruptible: false, diff --git a/internal/registry/tlsmiddlebox.go b/internal/registry/tlsmiddlebox.go index a97de82c89..73e1ecfa33 100644 --- a/internal/registry/tlsmiddlebox.go +++ b/internal/registry/tlsmiddlebox.go @@ -10,13 +10,15 @@ import ( ) func init() { - AllExperiments["tlsmiddlebox"] = func() *Factory { + const canonicalName = "tlsmiddlebox" + AllExperiments[canonicalName] = func() *Factory { return &Factory{ build: func(config interface{}) model.ExperimentMeasurer { return tlsmiddlebox.NewExperimentMeasurer( *config.(*tlsmiddlebox.Config), ) }, + canonicalName: canonicalName, config: &tlsmiddlebox.Config{}, enabledByDefault: true, inputPolicy: model.InputStrictlyRequired, diff --git a/internal/registry/tlsping.go b/internal/registry/tlsping.go index a40723d20b..6ec5048d52 100644 --- a/internal/registry/tlsping.go +++ b/internal/registry/tlsping.go @@ -10,13 +10,15 @@ import ( ) func init() { - AllExperiments["tlsping"] = func() *Factory { + const canonicalName = "tlsping" + AllExperiments[canonicalName] = func() *Factory { return &Factory{ build: func(config interface{}) model.ExperimentMeasurer { return tlsping.NewExperimentMeasurer( *config.(*tlsping.Config), ) }, + canonicalName: canonicalName, config: &tlsping.Config{}, enabledByDefault: true, inputPolicy: model.InputStrictlyRequired, diff --git a/internal/registry/tlstool.go b/internal/registry/tlstool.go index 8bb70983f2..0fe2625a55 100644 --- a/internal/registry/tlstool.go +++ b/internal/registry/tlstool.go @@ -10,13 +10,15 @@ import ( ) func init() { - AllExperiments["tlstool"] = func() *Factory { + const canonicalName = "tlstool" + AllExperiments[canonicalName] = func() *Factory { return &Factory{ build: func(config interface{}) model.ExperimentMeasurer { return tlstool.NewExperimentMeasurer( *config.(*tlstool.Config), ) }, + canonicalName: canonicalName, config: &tlstool.Config{}, enabledByDefault: true, inputPolicy: model.InputOrQueryBackend, diff --git a/internal/registry/tor.go b/internal/registry/tor.go index 73098bf770..2e2a613265 100644 --- a/internal/registry/tor.go +++ b/internal/registry/tor.go @@ -10,13 +10,15 @@ import ( ) func init() { - AllExperiments["tor"] = func() *Factory { + const canonicalName = "tor" + AllExperiments[canonicalName] = func() *Factory { return &Factory{ build: func(config interface{}) model.ExperimentMeasurer { return tor.NewExperimentMeasurer( *config.(*tor.Config), ) }, + canonicalName: canonicalName, config: &tor.Config{}, enabledByDefault: true, inputPolicy: model.InputNone, diff --git a/internal/registry/torsf.go b/internal/registry/torsf.go index 178f01d5e4..3090900445 100644 --- a/internal/registry/torsf.go +++ b/internal/registry/torsf.go @@ -10,13 +10,15 @@ import ( ) func init() { - AllExperiments["torsf"] = func() *Factory { + const canonicalName = "torsf" + AllExperiments[canonicalName] = func() *Factory { return &Factory{ build: func(config interface{}) model.ExperimentMeasurer { return torsf.NewExperimentMeasurer( *config.(*torsf.Config), ) }, + canonicalName: canonicalName, config: &torsf.Config{}, enabledByDefault: false, inputPolicy: model.InputNone, diff --git a/internal/registry/urlgetter.go b/internal/registry/urlgetter.go index 63dba20f9e..ae3391bd67 100644 --- a/internal/registry/urlgetter.go +++ b/internal/registry/urlgetter.go @@ -10,13 +10,15 @@ import ( ) func init() { - AllExperiments["urlgetter"] = func() *Factory { + const canonicalName = "urlgetter" + AllExperiments[canonicalName] = func() *Factory { return &Factory{ build: func(config interface{}) model.ExperimentMeasurer { return urlgetter.NewExperimentMeasurer( *config.(*urlgetter.Config), ) }, + canonicalName: canonicalName, config: &urlgetter.Config{}, enabledByDefault: true, inputPolicy: model.InputStrictlyRequired, diff --git a/internal/registry/vanillator.go b/internal/registry/vanillator.go index 1af548660a..a1835e22ce 100644 --- a/internal/registry/vanillator.go +++ b/internal/registry/vanillator.go @@ -10,14 +10,16 @@ import ( ) func init() { - AllExperiments["vanilla_tor"] = func() *Factory { + const canonicalName = "vanilla_tor" + AllExperiments[canonicalName] = func() *Factory { return &Factory{ build: func(config interface{}) model.ExperimentMeasurer { return vanillator.NewExperimentMeasurer( *config.(*vanillator.Config), ) }, - config: &vanillator.Config{}, + canonicalName: canonicalName, + config: &vanillator.Config{}, // We discussed this topic with @aanorbel. On Android this experiment crashes // frequently because of https://github.com/ooni/probe/issues/2406. So, it seems // more cautious to disable it by default and let the check-in API decide. diff --git a/internal/registry/webconnectivity.go b/internal/registry/webconnectivity.go index 1ea720d581..470bad802b 100644 --- a/internal/registry/webconnectivity.go +++ b/internal/registry/webconnectivity.go @@ -10,13 +10,15 @@ import ( ) func init() { - AllExperiments["web_connectivity"] = func() *Factory { + const canonicalName = "web_connectivity" + AllExperiments[canonicalName] = func() *Factory { return &Factory{ build: func(config any) model.ExperimentMeasurer { return webconnectivity.NewExperimentMeasurer( config.(webconnectivity.Config), ) }, + canonicalName: canonicalName, config: webconnectivity.Config{}, enabledByDefault: true, interruptible: false, diff --git a/internal/registry/webconnectivityv05.go b/internal/registry/webconnectivityv05.go index 0881b4448f..3a56511a69 100644 --- a/internal/registry/webconnectivityv05.go +++ b/internal/registry/webconnectivityv05.go @@ -12,13 +12,15 @@ import ( ) func init() { - AllExperiments["web_connectivity@v0.5"] = func() *Factory { + const canonicalName = "web_connectivity@v0.5" + AllExperiments[canonicalName] = func() *Factory { return &Factory{ build: func(config any) model.ExperimentMeasurer { return webconnectivitylte.NewExperimentMeasurer( config.(*webconnectivitylte.Config), ) }, + canonicalName: canonicalName, config: &webconnectivitylte.Config{}, enabledByDefault: true, interruptible: false, diff --git a/internal/registry/whatsapp.go b/internal/registry/whatsapp.go index 84acb57898..24a27a2acc 100644 --- a/internal/registry/whatsapp.go +++ b/internal/registry/whatsapp.go @@ -10,13 +10,15 @@ import ( ) func init() { - AllExperiments["whatsapp"] = func() *Factory { + const canonicalName = "whatsapp" + AllExperiments[canonicalName] = func() *Factory { return &Factory{ build: func(config interface{}) model.ExperimentMeasurer { return whatsapp.NewExperimentMeasurer( *config.(*whatsapp.Config), ) }, + canonicalName: canonicalName, config: &whatsapp.Config{}, enabledByDefault: true, inputPolicy: model.InputNone, diff --git a/internal/targetloading/targetloading.go b/internal/targetloading/targetloading.go index 6590c2c4a7..66a22bc88e 100644 --- a/internal/targetloading/targetloading.go +++ b/internal/targetloading/targetloading.go @@ -13,7 +13,6 @@ import ( "github.com/ooni/probe-cli/v3/internal/experiment/openvpn" "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" ) @@ -26,12 +25,8 @@ var ( ErrNoStaticInput = errors.New("no static input for this experiment") ) -// Session is the session according to a [*Loader]. -type Session interface { - CheckIn(ctx context.Context, config *model.OOAPICheckInConfig) (*model.OOAPICheckInResult, error) - FetchOpenVPNConfig(ctx context.Context, - provider, cc string) (*model.OOAPIVPNProviderConfig, error) -} +// Session is the session according to a [*Loader] instance. +type Session = model.ExperimentTargetLoaderSession // Logger is the [model.Logger] according to a [*Loader]. type Logger interface { @@ -230,7 +225,7 @@ func StaticBareInputForExperiment(name string) ([]string, error) { // // TODO(https://github.com/ooni/probe/issues/2557): server STUNReachability // inputs using richer input (aka check-in v2). - switch registry.CanonicalizeExperimentName(name) { + switch name { case "dnscheck": return dnsCheckDefaultInput, nil case "stunreachability": @@ -305,7 +300,7 @@ func (il *Loader) readfile(filepath string, open openFunc) ([]model.ExperimentTa // loadRemote loads inputs from a remote source. func (il *Loader) loadRemote(ctx context.Context) ([]model.ExperimentTarget, error) { - switch registry.CanonicalizeExperimentName(il.ExperimentName) { + switch il.ExperimentName { case "openvpn": // TODO(ainghazal): given the semantics of the current API call, in an ideal world we'd need to pass // the desired provider here, if known. We only have one provider for now, so I'm acting like YAGNI. Another diff --git a/internal/targetloading/targetloading_test.go b/internal/targetloading/targetloading_test.go index 9710ce1b34..82684a8205 100644 --- a/internal/targetloading/targetloading_test.go +++ b/internal/targetloading/targetloading_test.go @@ -11,6 +11,7 @@ import ( "testing" "time" + "github.com/apex/log" "github.com/google/go-cmp/cmp" "github.com/ooni/probe-cli/v3/internal/mocks" "github.com/ooni/probe-cli/v3/internal/model" @@ -535,7 +536,7 @@ type TargetLoaderMockableSession struct { Error error } -// CheckIn implements TargetLoaderSession.CheckIn. +// CheckIn implements [Session]. func (sess *TargetLoaderMockableSession) CheckIn( ctx context.Context, config *model.OOAPICheckInConfig) (*model.OOAPICheckInResult, error) { if sess.Output == nil && sess.Error == nil { @@ -544,13 +545,19 @@ func (sess *TargetLoaderMockableSession) CheckIn( return sess.Output, sess.Error } -// FetchOpenVPNConfig implements TargetLoaderSession.FetchOpenVPNConfig. +// FetchOpenVPNConfig implements [Session]. func (sess *TargetLoaderMockableSession) FetchOpenVPNConfig( ctx context.Context, provider, cc string) (*model.OOAPIVPNProviderConfig, error) { runtimex.Assert(!(sess.Error == nil && sess.FetchOpenVPNConfigOutput == nil), "both FetchOpenVPNConfig and Error are nil") return sess.FetchOpenVPNConfigOutput, sess.Error } +// Logger implements [Session]. +func (sess *TargetLoaderMockableSession) Logger() model.Logger { + // Such that we see some logs when running tests + return log.Log +} + func TestTargetLoaderCheckInFailure(t *testing.T) { il := &Loader{ Session: &TargetLoaderMockableSession{ From 4beba3721aa446c0475894fb127f458e2524397f Mon Sep 17 00:00:00 2001 From: Simone Basso Date: Thu, 6 Jun 2024 15:34:27 +0200 Subject: [PATCH 02/63] x --- internal/experimentname/experimentname.go | 25 +++++++++++++++++++++++ internal/registry/factory.go | 25 ++--------------------- internal/registry/factory_test.go | 3 ++- internal/targetloading/targetloading.go | 5 +++-- 4 files changed, 32 insertions(+), 26 deletions(-) create mode 100644 internal/experimentname/experimentname.go diff --git a/internal/experimentname/experimentname.go b/internal/experimentname/experimentname.go new file mode 100644 index 0000000000..193edc6135 --- /dev/null +++ b/internal/experimentname/experimentname.go @@ -0,0 +1,25 @@ +// Package experimentname contains code to manipulate experiment names. +package experimentname + +import "github.com/ooni/probe-cli/v3/internal/strcasex" + +// Canonicalize 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 Canonicalize(name string) string { + switch name = strcasex.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" + case "web_connectivity@v_0_5": + name = "web_connectivity@v0.5" + default: + } + return name +} diff --git a/internal/registry/factory.go b/internal/registry/factory.go index aba0946d64..eca08b2094 100644 --- a/internal/registry/factory.go +++ b/internal/registry/factory.go @@ -13,8 +13,8 @@ import ( "strconv" "github.com/ooni/probe-cli/v3/internal/checkincache" + "github.com/ooni/probe-cli/v3/internal/experimentname" "github.com/ooni/probe-cli/v3/internal/model" - "github.com/ooni/probe-cli/v3/internal/strcasex" "github.com/ooni/probe-cli/v3/internal/targetloading" ) @@ -238,27 +238,6 @@ func (b *Factory) NewTargetLoader(config *model.ExperimentTargetLoaderConfig) mo } } -// 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 = strcasex.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" - case "web_connectivity@v_0_5": - name = "web_connectivity@v0.5" - default: - } - return name -} - // ErrNoSuchExperiment indicates a given experiment does not exist. var ErrNoSuchExperiment = errors.New("no such experiment") @@ -305,7 +284,7 @@ const OONI_FORCE_ENABLE_EXPERIMENT = "OONI_FORCE_ENABLE_EXPERIMENT" func NewFactory(name string, kvStore model.KeyValueStore, logger model.Logger) (*Factory, error) { // Make sure we are deadling with the canonical experiment name. Historically MK used // names such as WebConnectivity and we want to continue supporting this use case. - name = CanonicalizeExperimentName(name) + name = experimentname.Canonicalize(name) // Handle A/B testing where we dynamically choose LTE for some users. The current policy // only relates to a few users to collect data. diff --git a/internal/registry/factory_test.go b/internal/registry/factory_test.go index dd32ddafb3..a309400945 100644 --- a/internal/registry/factory_test.go +++ b/internal/registry/factory_test.go @@ -10,6 +10,7 @@ import ( "github.com/google/go-cmp/cmp" "github.com/ooni/probe-cli/v3/internal/checkincache" "github.com/ooni/probe-cli/v3/internal/experiment/webconnectivitylte" + "github.com/ooni/probe-cli/v3/internal/experimentname" "github.com/ooni/probe-cli/v3/internal/kvstore" "github.com/ooni/probe-cli/v3/internal/model" ) @@ -691,7 +692,7 @@ func TestNewFactory(t *testing.T) { // get experiment expectations -- note that here we must canonicalize the // experiment name otherwise we won't find it into the map when testing non-canonical names - expectations := expectationsMap[CanonicalizeExperimentName(tc.experimentName)] + expectations := expectationsMap[experimentname.Canonicalize(tc.experimentName)] if expectations == nil { t.Fatal("no expectations for", tc.experimentName) } diff --git a/internal/targetloading/targetloading.go b/internal/targetloading/targetloading.go index 66a22bc88e..92e6a3b99c 100644 --- a/internal/targetloading/targetloading.go +++ b/internal/targetloading/targetloading.go @@ -11,6 +11,7 @@ import ( "github.com/apex/log" "github.com/ooni/probe-cli/v3/internal/experiment/openvpn" + "github.com/ooni/probe-cli/v3/internal/experimentname" "github.com/ooni/probe-cli/v3/internal/fsx" "github.com/ooni/probe-cli/v3/internal/model" "github.com/ooni/probe-cli/v3/internal/stuninput" @@ -225,7 +226,7 @@ func StaticBareInputForExperiment(name string) ([]string, error) { // // TODO(https://github.com/ooni/probe/issues/2557): server STUNReachability // inputs using richer input (aka check-in v2). - switch name { + switch experimentname.Canonicalize(name) { case "dnscheck": return dnsCheckDefaultInput, nil case "stunreachability": @@ -300,7 +301,7 @@ func (il *Loader) readfile(filepath string, open openFunc) ([]model.ExperimentTa // loadRemote loads inputs from a remote source. func (il *Loader) loadRemote(ctx context.Context) ([]model.ExperimentTarget, error) { - switch il.ExperimentName { + switch experimentname.Canonicalize(il.ExperimentName) { case "openvpn": // TODO(ainghazal): given the semantics of the current API call, in an ideal world we'd need to pass // the desired provider here, if known. We only have one provider for now, so I'm acting like YAGNI. Another From 775c836f1660c65fbcaee7e7b3ed85d5b00f4860 Mon Sep 17 00:00:00 2001 From: Simone Basso Date: Thu, 6 Jun 2024 15:50:34 +0200 Subject: [PATCH 03/63] x --- .../experimentname/experimentname_test.go | 55 +++++++++++++++++++ internal/mocks/experimentbuilder_test.go | 12 ++++ 2 files changed, 67 insertions(+) create mode 100644 internal/experimentname/experimentname_test.go diff --git a/internal/experimentname/experimentname_test.go b/internal/experimentname/experimentname_test.go new file mode 100644 index 0000000000..df191d768d --- /dev/null +++ b/internal/experimentname/experimentname_test.go @@ -0,0 +1,55 @@ +// Package experimentname contains code to manipulate experiment names. +package experimentname + +import "testing" + +func TestCanonicalize(t *testing.T) { + tests := []struct { + input string + expect string + }{ + { + input: "example", + expect: "example", + }, + { + input: "Example", + expect: "example", + }, + { + input: "ndt7", + expect: "ndt", + }, + { + input: "Ndt7", + expect: "ndt", + }, + { + input: "DNSCheck", + expect: "dnscheck", + }, + { + input: "dns_check", + expect: "dnscheck", + }, + { + input: "STUNReachability", + expect: "stunreachability", + }, + { + input: "stun_reachability", + expect: "stunreachability", + }, + { + input: "WebConnectivity@v0.5", + expect: "web_connectivity@v0.5", + }, + } + for _, tt := range tests { + t.Run(tt.input, func(t *testing.T) { + if got := Canonicalize(tt.input); got != tt.expect { + t.Errorf("Canonicalize() = %v, want %v", got, tt.expect) + } + }) + } +} diff --git a/internal/mocks/experimentbuilder_test.go b/internal/mocks/experimentbuilder_test.go index 6fd1ce532c..55ba1783f9 100644 --- a/internal/mocks/experimentbuilder_test.go +++ b/internal/mocks/experimentbuilder_test.go @@ -96,4 +96,16 @@ func TestExperimentBuilder(t *testing.T) { t.Fatal("invalid result") } }) + + t.Run("NewTargetLoader", func(t *testing.T) { + tloader := &ExperimentTargetLoader{} + eb := &ExperimentBuilder{ + MockNewTargetLoader: func(*model.ExperimentTargetLoaderConfig) model.ExperimentTargetLoader { + return tloader + }, + } + if out := eb.NewTargetLoader(&model.ExperimentTargetLoaderConfig{}); out != tloader { + t.Fatal("invalid result") + } + }) } From 4eb8147518a7c169907a17dbcd666baa234e91d3 Mon Sep 17 00:00:00 2001 From: Simone Basso Date: Thu, 6 Jun 2024 15:52:40 +0200 Subject: [PATCH 04/63] x --- internal/registry/factory.go | 32 ++++++++++++++++---------------- 1 file changed, 16 insertions(+), 16 deletions(-) diff --git a/internal/registry/factory.go b/internal/registry/factory.go index eca08b2094..0b37a79ee2 100644 --- a/internal/registry/factory.go +++ b/internal/registry/factory.go @@ -39,6 +39,22 @@ type Factory struct { interruptible bool } +// Session is the session definition according to this package. +type Session = model.ExperimentTargetLoaderSession + +// NewTargetLoader creates a new [model.ExperimentTargetLoader] instance. +func (b *Factory) NewTargetLoader(config *model.ExperimentTargetLoaderConfig) model.ExperimentTargetLoader { + return &targetloading.Loader{ + CheckInConfig: config.CheckInConfig, // OPTIONAL + ExperimentName: b.canonicalName, + InputPolicy: b.inputPolicy, + Logger: config.Session.Logger(), + Session: config.Session, + StaticInputs: config.StaticInputs, + SourceFiles: config.SourceFiles, + } +} + // Interruptible returns whether the experiment is interruptible. func (b *Factory) Interruptible() bool { return b.interruptible @@ -222,22 +238,6 @@ func (b *Factory) NewExperimentMeasurer() model.ExperimentMeasurer { return b.build(b.config) } -// Session is the session definition according to this package. -type Session = model.ExperimentTargetLoaderSession - -// NewTargetLoader creates a new [model.ExperimentTargetLoader] instance. -func (b *Factory) NewTargetLoader(config *model.ExperimentTargetLoaderConfig) model.ExperimentTargetLoader { - return &targetloading.Loader{ - CheckInConfig: config.CheckInConfig, // OPTIONAL - ExperimentName: b.canonicalName, - InputPolicy: b.inputPolicy, - Logger: config.Session.Logger(), - Session: config.Session, - StaticInputs: config.StaticInputs, - SourceFiles: config.SourceFiles, - } -} - // ErrNoSuchExperiment indicates a given experiment does not exist. var ErrNoSuchExperiment = errors.New("no such experiment") From e38678d6be8f0138025816e55f82d73498169422 Mon Sep 17 00:00:00 2001 From: Simone Basso Date: Thu, 6 Jun 2024 16:02:38 +0200 Subject: [PATCH 05/63] x --- internal/registry/factory_test.go | 58 +++++++++++++++++++++++++++++++ 1 file changed, 58 insertions(+) diff --git a/internal/registry/factory_test.go b/internal/registry/factory_test.go index a309400945..60105d1354 100644 --- a/internal/registry/factory_test.go +++ b/internal/registry/factory_test.go @@ -1,17 +1,20 @@ package registry import ( + "context" "errors" "fmt" "math" "os" "testing" + "github.com/apex/log" "github.com/google/go-cmp/cmp" "github.com/ooni/probe-cli/v3/internal/checkincache" "github.com/ooni/probe-cli/v3/internal/experiment/webconnectivitylte" "github.com/ooni/probe-cli/v3/internal/experimentname" "github.com/ooni/probe-cli/v3/internal/kvstore" + "github.com/ooni/probe-cli/v3/internal/mocks" "github.com/ooni/probe-cli/v3/internal/model" ) @@ -295,6 +298,15 @@ func TestExperimentBuilderSetOptionAny(t *testing.T) { FieldValue: 1.11, ExpectErr: ErrCannotSetIntegerOption, ExpectConfig: &fakeExperimentConfig{}, + }, { + TestCaseName: "[int] for float64 with zero fractional value", + InitialConfig: &fakeExperimentConfig{}, + FieldName: "Value", + FieldValue: float64(16.0), + ExpectErr: nil, + ExpectConfig: &fakeExperimentConfig{ + Value: 16, + }, }, { TestCaseName: "[string] for serialized bool value while setting a string value", InitialConfig: &fakeExperimentConfig{}, @@ -802,3 +814,49 @@ func TestNewFactory(t *testing.T) { } }) } + +// Make sure the target loader for web connectivity is WAI when using no static inputs. +func TestFactoryNewTargetLoaderWebConnectivity(t *testing.T) { + // construct the proper factory instance + store := &kvstore.Memory{} + factory, err := NewFactory("web_connectivity", store, log.Log) + if err != nil { + t.Fatal(err) + } + + // define the expected error. + expected := errors.New("antani") + + // create suitable loader config. + config := &model.ExperimentTargetLoaderConfig{ + CheckInConfig: &model.OOAPICheckInConfig{ + // nothing + }, + Session: &mocks.Session{ + MockCheckIn: func(ctx context.Context, config *model.OOAPICheckInConfig) (*model.OOAPICheckInResult, error) { + return nil, expected + }, + MockLogger: func() model.Logger { + return log.Log + }, + }, + StaticInputs: nil, + SourceFiles: nil, + } + + // obtain the loader + loader := factory.NewTargetLoader(config) + + // attempt to load targets + targets, err := loader.Load(context.Background()) + + // make sure we've got the expected error + if !errors.Is(err, expected) { + t.Fatal("unexpected error", err) + } + + // make sure there are no targets + if len(targets) != 0 { + t.Fatal("expected zero length targets") + } +} From 06d300f927172a4d4634093e778a1d4c470dbbba Mon Sep 17 00:00:00 2001 From: Simone Basso Date: Thu, 6 Jun 2024 16:08:50 +0200 Subject: [PATCH 06/63] x --- internal/engine/experimentbuilder_test.go | 50 +++++++++++++++++++++++ 1 file changed, 50 insertions(+) diff --git a/internal/engine/experimentbuilder_test.go b/internal/engine/experimentbuilder_test.go index 00a22ef62d..d81e0bb7bb 100644 --- a/internal/engine/experimentbuilder_test.go +++ b/internal/engine/experimentbuilder_test.go @@ -1 +1,51 @@ package engine + +import ( + "context" + "errors" + "testing" + + "github.com/ooni/probe-cli/v3/internal/model" +) + +func TestExperimentBuilderEngineWebConnectivity(t *testing.T) { + // create a session for testing that does not use the network at all + sess := newSessionForTestingNoLookups(t) + + // create an experiment builder for Web Connectivity + builder, err := sess.NewExperimentBuilder("WebConnectivity") + if err != nil { + t.Fatal(err) + } + + // create suitable loader config + config := &model.ExperimentTargetLoaderConfig{ + CheckInConfig: &model.OOAPICheckInConfig{ + // nothing + }, + Session: sess, + StaticInputs: nil, + SourceFiles: nil, + } + + // create the loader + loader := builder.NewTargetLoader(config) + + // create cancelled context to interrupt immediately so that we + // don't use the network when running this test + ctx, cancel := context.WithCancel(context.Background()) + cancel() + + // attempt to load targets + targets, err := loader.Load(ctx) + + // make sure we've got the expected error + if !errors.Is(err, context.Canceled) { + t.Fatal("unexpected err", err) + } + + // make sure there are no targets + if len(targets) != 0 { + t.Fatal("expected zero length targets") + } +} From d4904486b5b73931a254995eca9f1defd53ce7b2 Mon Sep 17 00:00:00 2001 From: Simone Basso Date: Thu, 6 Jun 2024 16:14:58 +0200 Subject: [PATCH 07/63] x --- internal/oonirun/experiment_test.go | 10 ++++++++++ internal/oonirun/v1_test.go | 10 ++++++++++ internal/oonirun/v2_test.go | 10 ++++++++++ 3 files changed, 30 insertions(+) diff --git a/internal/oonirun/experiment_test.go b/internal/oonirun/experiment_test.go index b93fd25ae2..d438f47ce1 100644 --- a/internal/oonirun/experiment_test.go +++ b/internal/oonirun/experiment_test.go @@ -67,6 +67,16 @@ func TestExperimentRunWithFailureToSubmitAndShuffle(t *testing.T) { } return exp }, + MockNewTargetLoader: func(config *model.ExperimentTargetLoaderConfig) model.ExperimentTargetLoader { + return &mocks.ExperimentTargetLoader{ + MockLoad: func(ctx context.Context) ([]model.ExperimentTarget, error) { + // Implementation note: the convention for input-less experiments is that + // they require a single entry containing an empty input. + entry := model.NewOOAPIURLInfoWithDefaultCategoryAndCountry("") + return []model.ExperimentTarget{entry}, nil + }, + } + }, } return eb, nil }, diff --git a/internal/oonirun/v1_test.go b/internal/oonirun/v1_test.go index c23455a9fc..910d910b0c 100644 --- a/internal/oonirun/v1_test.go +++ b/internal/oonirun/v1_test.go @@ -44,6 +44,16 @@ func newMinimalFakeSession() *mocks.Session { } return exp }, + MockNewTargetLoader: func(config *model.ExperimentTargetLoaderConfig) model.ExperimentTargetLoader { + return &mocks.ExperimentTargetLoader{ + MockLoad: func(ctx context.Context) ([]model.ExperimentTarget, error) { + // Implementation note: the convention for input-less experiments is that + // they require a single entry containing an empty input. + entry := model.NewOOAPIURLInfoWithDefaultCategoryAndCountry("") + return []model.ExperimentTarget{entry}, nil + }, + } + }, } return eb, nil }, diff --git a/internal/oonirun/v2_test.go b/internal/oonirun/v2_test.go index c4afa60e96..d849c6d088 100644 --- a/internal/oonirun/v2_test.go +++ b/internal/oonirun/v2_test.go @@ -392,6 +392,16 @@ func TestV2MeasureDescriptor(t *testing.T) { } return exp }, + MockNewTargetLoader: func(config *model.ExperimentTargetLoaderConfig) model.ExperimentTargetLoader { + return &mocks.ExperimentTargetLoader{ + MockLoad: func(ctx context.Context) ([]model.ExperimentTarget, error) { + // Implementation note: the convention for input-less experiments is that + // they require a single entry containing an empty input. + entry := model.NewOOAPIURLInfoWithDefaultCategoryAndCountry("") + return []model.ExperimentTarget{entry}, nil + }, + } + }, } return eb, nil } From a97bcd8f2a6b4dcf975659b21f1e34362538f418 Mon Sep 17 00:00:00 2001 From: Simone Basso Date: Thu, 6 Jun 2024 18:54:39 +0200 Subject: [PATCH 08/63] dnscheck --- internal/engine/experiment.go | 19 ++- internal/engine/experiment_test.go | 2 +- internal/experiment/dnscheck/dnscheck.go | 43 ++++--- internal/experiment/dnscheck/dnscheck_test.go | 74 +++++++---- internal/experiment/dnscheck/richerinput.go | 120 ++++++++++++++++++ internal/model/experiment.go | 3 + internal/oonirun/experiment.go | 14 +- internal/registry/dnscheck.go | 7 +- internal/registry/factory.go | 9 +- internal/targetloading/targetloading.go | 56 ++++---- internal/targetloading/targetloading_test.go | 5 +- 11 files changed, 262 insertions(+), 90 deletions(-) create mode 100644 internal/experiment/dnscheck/richerinput.go diff --git a/internal/engine/experiment.go b/internal/engine/experiment.go index 61197c0d8d..f2934ce4fa 100644 --- a/internal/engine/experiment.go +++ b/internal/engine/experiment.go @@ -109,11 +109,12 @@ func (e *experiment) SubmitAndUpdateMeasurementContext( } // newMeasurement creates a new measurement for this experiment with the given input. -func (e *experiment) newMeasurement(input string) *model.Measurement { +func (e *experiment) newMeasurement(target model.ExperimentTarget) *model.Measurement { utctimenow := time.Now().UTC() + // TODO(bassosimone,DecFox): add support for unmarshaling options. m := &model.Measurement{ DataFormatVersion: model.OOAPIReportDefaultDataFormatVersion, - Input: model.MeasurementInput(input), + Input: model.MeasurementInput(target.Input()), MeasurementStartTime: utctimenow.Format(model.MeasurementDateFormat), MeasurementStartTimeSaved: utctimenow, ProbeIP: model.DefaultProbeIP, @@ -204,10 +205,15 @@ func (e *experiment) MeasureWithContext( ctx = bytecounter.WithExperimentByteCounter(ctx, e.byteCounter) // Create a new measurement that the experiment measurer will finish filling - // by adding the test keys etc. Please, note that, as of 2024-06-05, we're using - // the measurement Input to provide input to an experiment. We'll probably - // change this, when we'll have finished implementing richer input. - measurement := e.newMeasurement(target.Input()) + // by adding the test keys etc. Please, note that, as of 2024-06-06: + // + // 1. experiments using richer input receive input via the Target field; + // + // 2. other experiments use (*Measurement).Input. + // + // Here we're passing the whole target to newMeasurement such that we're able + // to record options values in addition to the input value. + measurement := e.newMeasurement(target) // Record when we started the experiment, to compute the runtime. start := time.Now() @@ -217,6 +223,7 @@ func (e *experiment) MeasureWithContext( Callbacks: e.callbacks, Measurement: measurement, Session: e.session, + Target: target, } // Invoke the measurer. Conventionally, an error being returned here diff --git a/internal/engine/experiment_test.go b/internal/engine/experiment_test.go index e2444306af..ceab79d7bb 100644 --- a/internal/engine/experiment_test.go +++ b/internal/engine/experiment_test.go @@ -17,7 +17,7 @@ func TestExperimentHonoursSharingDefaults(t *testing.T) { t.Fatal(err) } exp := builder.NewExperiment().(*experiment) - return exp.newMeasurement("") + return exp.newMeasurement(model.NewOOAPIURLInfoWithDefaultCategoryAndCountry("")) } type spec struct { name string diff --git a/internal/experiment/dnscheck/dnscheck.go b/internal/experiment/dnscheck/dnscheck.go index 4c9d5fd17c..3b6c93787b 100644 --- a/internal/experiment/dnscheck/dnscheck.go +++ b/internal/experiment/dnscheck/dnscheck.go @@ -96,7 +96,6 @@ type TestKeys struct { // Measurer performs the measurement. type Measurer struct { - Config Endpoints *Endpoints } @@ -125,6 +124,10 @@ func (m *Measurer) Run(ctx context.Context, args *model.ExperimentArgs) error { measurement := args.Measurement sess := args.Session + // 0. obtain the richer input target, config, and input or panic + target := args.Target.(*Target) + config, input := target.options, target.input + // 1. fill the measurement with test keys tk := new(TestKeys) tk.Lookups = make(map[string]urlgetter.TestKeys) @@ -133,20 +136,19 @@ func (m *Measurer) Run(ctx context.Context, args *model.ExperimentArgs) error { // 2. select the domain to resolve or use default and, while there, also // ensure that we register all the other options we're using. - domain := m.Config.Domain + domain := config.Domain if domain == "" { domain = defaultDomain } - tk.DefaultAddrs = m.Config.DefaultAddrs + tk.DefaultAddrs = config.DefaultAddrs tk.Domain = domain - tk.HTTP3Enabled = m.Config.HTTP3Enabled - tk.HTTPHost = m.Config.HTTPHost - tk.TLSServerName = m.Config.TLSServerName - tk.TLSVersion = m.Config.TLSVersion + tk.HTTP3Enabled = config.HTTP3Enabled + tk.HTTPHost = config.HTTPHost + tk.TLSServerName = config.TLSServerName + tk.TLSVersion = config.TLSVersion tk.Residual = m.Endpoints != nil // 3. parse the input URL describing the resolver to use - input := string(measurement.Input) if input == "" { return ErrInputRequired } @@ -191,7 +193,7 @@ func (m *Measurer) Run(ctx context.Context, args *model.ExperimentArgs) error { for _, addr := range addrs { allAddrs[addr] = true } - for _, addr := range strings.Split(m.Config.DefaultAddrs, " ") { + for _, addr := range strings.Split(config.DefaultAddrs, " ") { if addr != "" { allAddrs[addr] = true } @@ -208,10 +210,10 @@ func (m *Measurer) Run(ctx context.Context, args *model.ExperimentArgs) error { for addr := range allAddrs { inputs = append(inputs, urlgetter.MultiInput{ Config: urlgetter.Config{ - DNSHTTPHost: m.httpHost(URL.Host), - DNSTLSServerName: m.tlsServerName(URL.Hostname()), - DNSTLSVersion: m.Config.TLSVersion, - HTTP3Enabled: m.Config.HTTP3Enabled, + DNSHTTPHost: config.httpHost(URL.Host), + DNSTLSServerName: config.tlsServerName(URL.Hostname()), + DNSTLSVersion: config.TLSVersion, + HTTP3Enabled: config.HTTP3Enabled, RejectDNSBogons: true, // bogons are errors in this context ResolverURL: makeResolverURL(URL, addr), Timeout: 15 * time.Second, @@ -244,17 +246,17 @@ func (m *Measurer) lookupHost(ctx context.Context, hostname string, r model.Reso // httpHost returns the configured HTTP host, if set, otherwise // it will return the host provide as argument. -func (m *Measurer) httpHost(httpHost string) string { - if m.Config.HTTPHost != "" { - return m.Config.HTTPHost +func (c *Config) httpHost(httpHost string) string { + if c.HTTPHost != "" { + return c.HTTPHost } return httpHost } // tlsServerName is like httpHost for the TLS server name. -func (m *Measurer) tlsServerName(tlsServerName string) string { - if m.Config.TLSServerName != "" { - return m.Config.TLSServerName +func (c *Config) tlsServerName(tlsServerName string) string { + if c.TLSServerName != "" { + return c.TLSServerName } return tlsServerName } @@ -311,9 +313,8 @@ func makeResolverURL(URL *url.URL, addr string) string { } // NewExperimentMeasurer creates a new ExperimentMeasurer. -func NewExperimentMeasurer(config Config) model.ExperimentMeasurer { +func NewExperimentMeasurer() model.ExperimentMeasurer { return &Measurer{ - Config: config, Endpoints: nil, // disabled by default } } diff --git a/internal/experiment/dnscheck/dnscheck_test.go b/internal/experiment/dnscheck/dnscheck_test.go index 2fd2c295c8..bfe91eb767 100644 --- a/internal/experiment/dnscheck/dnscheck_test.go +++ b/internal/experiment/dnscheck/dnscheck_test.go @@ -8,44 +8,40 @@ import ( "time" "github.com/apex/log" - "github.com/ooni/probe-cli/v3/internal/legacy/mockable" + "github.com/ooni/probe-cli/v3/internal/mocks" "github.com/ooni/probe-cli/v3/internal/model" ) func TestHTTPHostWithOverride(t *testing.T) { - m := Measurer{Config: Config{HTTPHost: "antani"}} - result := m.httpHost("mascetti") - if result != "antani" { + c := &Config{HTTPHost: "antani"} + if result := c.httpHost("mascetti"); result != "antani" { t.Fatal("not the result we expected") } } func TestHTTPHostWithoutOverride(t *testing.T) { - m := Measurer{Config: Config{}} - result := m.httpHost("mascetti") - if result != "mascetti" { + c := &Config{} + if result := c.httpHost("mascetti"); result != "mascetti" { t.Fatal("not the result we expected") } } func TestTLSServerNameWithOverride(t *testing.T) { - m := Measurer{Config: Config{TLSServerName: "antani"}} - result := m.tlsServerName("mascetti") - if result != "antani" { + c := &Config{TLSServerName: "antani"} + if result := c.tlsServerName("mascetti"); result != "antani" { t.Fatal("not the result we expected") } } func TestTLSServerNameWithoutOverride(t *testing.T) { - m := Measurer{Config: Config{}} - result := m.tlsServerName("mascetti") - if result != "mascetti" { + c := &Config{} + if result := c.tlsServerName("mascetti"); result != "mascetti" { t.Fatal("not the result we expected") } } func TestExperimentNameAndVersion(t *testing.T) { - measurer := NewExperimentMeasurer(Config{Domain: "example.com"}) + measurer := NewExperimentMeasurer() if measurer.ExperimentName() != "dnscheck" { t.Error("unexpected experiment name") } @@ -55,11 +51,17 @@ func TestExperimentNameAndVersion(t *testing.T) { } func TestDNSCheckFailsWithoutInput(t *testing.T) { - measurer := NewExperimentMeasurer(Config{Domain: "example.com"}) + measurer := NewExperimentMeasurer() args := &model.ExperimentArgs{ Callbacks: model.NewPrinterCallbacks(log.Log), Measurement: new(model.Measurement), Session: newsession(), + Target: &Target{ + input: "", // explicitly empty + options: &Config{ + Domain: "example.com", + }, + }, } err := measurer.Run(context.Background(), args) if !errors.Is(err, ErrInputRequired) { @@ -68,11 +70,15 @@ func TestDNSCheckFailsWithoutInput(t *testing.T) { } func TestDNSCheckFailsWithInvalidURL(t *testing.T) { - measurer := NewExperimentMeasurer(Config{}) + measurer := NewExperimentMeasurer() args := &model.ExperimentArgs{ Callbacks: model.NewPrinterCallbacks(log.Log), Measurement: &model.Measurement{Input: "Not a valid URL \x7f"}, Session: newsession(), + Target: &Target{ + input: "Not a valid URL \x7f", + options: &Config{}, + }, } err := measurer.Run(context.Background(), args) if !errors.Is(err, ErrInvalidURL) { @@ -81,11 +87,15 @@ func TestDNSCheckFailsWithInvalidURL(t *testing.T) { } func TestDNSCheckFailsWithUnsupportedProtocol(t *testing.T) { - measurer := NewExperimentMeasurer(Config{}) + measurer := NewExperimentMeasurer() args := &model.ExperimentArgs{ Callbacks: model.NewPrinterCallbacks(log.Log), Measurement: &model.Measurement{Input: "file://1.1.1.1"}, Session: newsession(), + Target: &Target{ + input: "file://1.1.1.1", + options: &Config{}, + }, } err := measurer.Run(context.Background(), args) if !errors.Is(err, ErrUnsupportedURLScheme) { @@ -96,14 +106,18 @@ func TestDNSCheckFailsWithUnsupportedProtocol(t *testing.T) { func TestWithCancelledContext(t *testing.T) { ctx, cancel := context.WithCancel(context.Background()) cancel() // immediately cancel the context - measurer := NewExperimentMeasurer(Config{ - DefaultAddrs: "1.1.1.1 1.0.0.1", - }) + measurer := NewExperimentMeasurer() measurement := &model.Measurement{Input: "dot://one.one.one.one"} args := &model.ExperimentArgs{ Callbacks: model.NewPrinterCallbacks(log.Log), Measurement: measurement, Session: newsession(), + Target: &Target{ + input: "dot://one.one.one.one", + options: &Config{ + DefaultAddrs: "1.1.1.1 1.0.0.1", + }, + }, } err := measurer.Run(ctx, args) if err != nil { @@ -140,14 +154,18 @@ func TestDNSCheckValid(t *testing.T) { t.Skip("skip test in short mode") } - measurer := NewExperimentMeasurer(Config{ - DefaultAddrs: "1.1.1.1 1.0.0.1", - }) + measurer := NewExperimentMeasurer() measurement := model.Measurement{Input: "dot://one.one.one.one:853"} args := &model.ExperimentArgs{ Callbacks: model.NewPrinterCallbacks(log.Log), Measurement: &measurement, Session: newsession(), + Target: &Target{ + input: "dot://one.one.one.one:853", + options: &Config{ + DefaultAddrs: "1.1.1.1 1.0.0.1", + }, + }, } err := measurer.Run(context.Background(), args) if err != nil { @@ -169,7 +187,11 @@ func TestDNSCheckValid(t *testing.T) { } func newsession() model.ExperimentSession { - return &mockable.Session{MockableLogger: log.Log} + return &mocks.Session{ + MockLogger: func() model.Logger { + return log.Log + }, + } } func TestDNSCheckWait(t *testing.T) { @@ -187,6 +209,10 @@ func TestDNSCheckWait(t *testing.T) { Callbacks: model.NewPrinterCallbacks(log.Log), Measurement: &measurement, Session: newsession(), + Target: &Target{ + input: input, + options: &Config{}, + }, } err := measurer.Run(context.Background(), args) if err != nil { diff --git a/internal/experiment/dnscheck/richerinput.go b/internal/experiment/dnscheck/richerinput.go new file mode 100644 index 0000000000..758b014319 --- /dev/null +++ b/internal/experiment/dnscheck/richerinput.go @@ -0,0 +1,120 @@ +package dnscheck + +import ( + "context" + + "github.com/ooni/probe-cli/v3/internal/model" + "github.com/ooni/probe-cli/v3/internal/targetloading" +) + +// Target is a richer-input target that this experiment should measure. +type Target struct { + // input is the input. + input string + + // options is the configuration. + options *Config +} + +var _ model.ExperimentTarget = &Target{} + +// Category implements [model.ExperimentTarget]. +func (t *Target) Category() string { + return model.DefaultCategoryCode +} + +// Country implements [model.ExperimentTarget]. +func (t *Target) Country() string { + return model.DefaultCountryCode +} + +// Input implements [model.ExperimentTarget]. +func (t *Target) Input() string { + return t.input +} + +// String implements [model.ExperimentTarget]. +func (t *Target) String() string { + return t.input +} + +// NewLoader constructs a new [model.ExperimentTargerLoader] instance. +// +// This function PANICS if options is not an instance of [*dnscheck.Config]. +func NewLoader(loader *targetloading.Loader, gopts any) model.ExperimentTargetLoader { + // Panic if we cannot convert the options to the expected type. + // + // We do not expect a panic here because the type is managed by the registry package. + options := gopts.(*Config) + + // Construct the proper loader instance. + return &targetLoader{ + loader: loader, + options: options, + } +} + +type targetLoader struct { + loader *targetloading.Loader + options *Config +} + +// Load implements model.ExperimentTargetLoader. +func (tl *targetLoader) Load(ctx context.Context) ([]model.ExperimentTarget, error) { + // TODO(bassosimone): we need a way to know whether the options are empty!!! + + // If there's nothing to statically load fallback to the API + if len(tl.loader.StaticInputs) <= 0 && len(tl.loader.SourceFiles) <= 0 { + return tl.loadFromBackend(ctx) + } + + // Otherwise, attempt to load the static inputs from CLI and files + inputs, err := targetloading.LoadStatic(tl.loader) + + // Handle the case where we couldn't + if err != nil { + return nil, err + } + + // Build the list of targets that we should measure. + var targets []model.ExperimentTarget + for _, input := range inputs { + targets = append(targets, &Target{ + options: tl.options, + input: input, + }) + } + return targets, nil +} + +var defaultInput = []model.ExperimentTarget{ + // + // https://dns.google/dns-query + // + // Measure HTTP/3 first and then HTTP/2 (see https://github.com/ooni/probe/issues/2675). + // + // Make sure we include the typical IP addresses for the domain. + // + &Target{ + input: "https://dns.google/dns-query", + options: &Config{ + HTTP3Enabled: true, + DefaultAddrs: "8.8.8.8 8.8.4.4", + }, + }, + &Target{ + input: "https://dns.google/dns-query", + options: &Config{ + DefaultAddrs: "8.8.8.8 8.8.4.4", + }, + }, + + // TODO(bassosimone): before merging, we need to reinstate the + // whole list that we previously had in tree +} + +func (tl *targetLoader) loadFromBackend(_ context.Context) ([]model.ExperimentTarget, error) { + // TODO(https://github.com/ooni/probe/issues/1390): serve DNSCheck + // inputs using richer input (aka check-in v2). + return defaultInput, nil +} diff --git a/internal/model/experiment.go b/internal/model/experiment.go index 8bd309f326..4aac4a0147 100644 --- a/internal/model/experiment.go +++ b/internal/model/experiment.go @@ -123,6 +123,9 @@ type ExperimentArgs struct { // Session is the MANDATORY session the experiment can use. Session ExperimentSession + + // Target is the MANDATORY target we're measuring. + Target ExperimentTarget } // ExperimentMeasurer is the interface that allows to run a diff --git a/internal/oonirun/experiment.go b/internal/oonirun/experiment.go index 758db9e0c5..3136e32c66 100644 --- a/internal/oonirun/experiment.go +++ b/internal/oonirun/experiment.go @@ -82,14 +82,19 @@ func (ed *Experiment) Run(ctx context.Context) error { return err } - // 2. create input loader and load input for this experiment + // 2. configure experiment's options + if err := builder.SetOptionsAny(ed.ExtraOptions); err != nil { + return err + } + + // 3. create input loader and load input for this experiment targetLoader := ed.newTargetLoader(builder) targetList, err := targetLoader.Load(ctx) if err != nil { return err } - // 3. randomize input, if needed + // 4. randomize input, if needed if ed.Random { rnd := rand.New(rand.NewSource(time.Now().UnixNano())) // #nosec G404 -- not really important rnd.Shuffle(len(targetList), func(i, j int) { @@ -98,11 +103,6 @@ func (ed *Experiment) Run(ctx context.Context) error { experimentShuffledInputs.Add(1) } - // 4. configure experiment's options - if err := builder.SetOptionsAny(ed.ExtraOptions); err != nil { - return err - } - // 5. construct the experiment instance experiment := builder.NewExperiment() logger := ed.Session.Logger() diff --git a/internal/registry/dnscheck.go b/internal/registry/dnscheck.go index 2890ca19a5..2ed902c38e 100644 --- a/internal/registry/dnscheck.go +++ b/internal/registry/dnscheck.go @@ -12,16 +12,17 @@ import ( func init() { const canonicalName = "dnscheck" AllExperiments[canonicalName] = func() *Factory { + // TODO(bassosimone): for now, we MUST keep the InputOrStaticDefault + // policy because otherwise ./pkg/oonimkall should break. return &Factory{ build: func(config interface{}) model.ExperimentMeasurer { - return dnscheck.NewExperimentMeasurer( - *config.(*dnscheck.Config), - ) + return dnscheck.NewExperimentMeasurer() }, canonicalName: canonicalName, config: &dnscheck.Config{}, enabledByDefault: true, inputPolicy: model.InputOrStaticDefault, + newLoader: dnscheck.NewLoader, } } } diff --git a/internal/registry/factory.go b/internal/registry/factory.go index 0b37a79ee2..2bb6dd3fe2 100644 --- a/internal/registry/factory.go +++ b/internal/registry/factory.go @@ -37,6 +37,9 @@ type Factory struct { // interruptible indicates whether the experiment is interruptible. interruptible bool + + // newLoader is the OPTIONAL function to create a new loader. + newLoader func(config *targetloading.Loader, options any) model.ExperimentTargetLoader } // Session is the session definition according to this package. @@ -44,7 +47,7 @@ type Session = model.ExperimentTargetLoaderSession // NewTargetLoader creates a new [model.ExperimentTargetLoader] instance. func (b *Factory) NewTargetLoader(config *model.ExperimentTargetLoaderConfig) model.ExperimentTargetLoader { - return &targetloading.Loader{ + loader := &targetloading.Loader{ CheckInConfig: config.CheckInConfig, // OPTIONAL ExperimentName: b.canonicalName, InputPolicy: b.inputPolicy, @@ -53,6 +56,10 @@ func (b *Factory) NewTargetLoader(config *model.ExperimentTargetLoaderConfig) mo StaticInputs: config.StaticInputs, SourceFiles: config.SourceFiles, } + if b.newLoader != nil { + return b.newLoader(loader, b.config) + } + return loader } // Interruptible returns whether the experiment is interruptible. diff --git a/internal/targetloading/targetloading.go b/internal/targetloading/targetloading.go index 92e6a3b99c..c6c91162c5 100644 --- a/internal/targetloading/targetloading.go +++ b/internal/targetloading/targetloading.go @@ -220,16 +220,14 @@ 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. - // - // TODO(https://github.com/ooni/probe/issues/1390): serve DNSCheck - // inputs using richer input (aka check-in v2). - // - // TODO(https://github.com/ooni/probe/issues/2557): server STUNReachability - // inputs using richer input (aka check-in v2). switch experimentname.Canonicalize(name) { case "dnscheck": + // TODO(https://github.com/ooni/probe/issues/1390): serve DNSCheck + // inputs using richer input (aka check-in v2). return dnsCheckDefaultInput, nil case "stunreachability": + // TODO(https://github.com/ooni/probe/issues/2557): server STUNReachability + // inputs using richer input (aka check-in v2). return stunReachabilityDefaultInput, nil default: return nil, ErrNoStaticInput @@ -251,24 +249,17 @@ func (il *Loader) loadOrStaticDefault(_ context.Context) ([]model.ExperimentTarg return staticInputForExperiment(il.ExperimentName) } -// loadLocal loads inputs from StaticInputs and SourceFiles. +// loadLocal loads inputs from the [*Loader] StaticInputs and SourceFiles. func (il *Loader) loadLocal() ([]model.ExperimentTarget, error) { - inputs := []model.ExperimentTarget{} - for _, input := range il.StaticInputs { - inputs = append(inputs, model.NewOOAPIURLInfoWithDefaultCategoryAndCountry(input)) + inputs, err := LoadStatic(il) + if err != nil { + return nil, err } - for _, filepath := range il.SourceFiles { - extra, err := il.readfile(filepath, fsx.OpenFile) - if err != nil { - return nil, err - } - // See https://github.com/ooni/probe-engine/issues/1123. - if len(extra) <= 0 { - return nil, fmt.Errorf("%w: %s", ErrDetectedEmptyFile, filepath) - } - inputs = append(inputs, extra...) + var targets []model.ExperimentTarget + for _, input := range inputs { + targets = append(targets, model.NewOOAPIURLInfoWithDefaultCategoryAndCountry(input)) } - return inputs, nil + return targets, nil } // openFunc is the type of the function to open a file. @@ -276,8 +267,8 @@ type openFunc func(filepath string) (fs.File, error) // readfile reads inputs from the specified file. The open argument should be // compatible with stdlib's fs.Open and helps us with unit testing. -func (il *Loader) readfile(filepath string, open openFunc) ([]model.ExperimentTarget, error) { - inputs := []model.ExperimentTarget{} +func readfile(filepath string, open openFunc) ([]string, error) { + inputs := []string{} filep, err := open(filepath) if err != nil { return nil, err @@ -290,7 +281,7 @@ func (il *Loader) readfile(filepath string, open openFunc) ([]model.ExperimentTa for scanner.Scan() { line := scanner.Text() if line != "" { - inputs = append(inputs, model.NewOOAPIURLInfoWithDefaultCategoryAndCountry(line)) + inputs = append(inputs, line) } } if scanner.Err() != nil { @@ -299,6 +290,23 @@ func (il *Loader) readfile(filepath string, open openFunc) ([]model.ExperimentTa return inputs, nil } +// LoadStatic loads inputs from the [*Loader] StaticInputs and SourceFiles. +func LoadStatic(config *Loader) ([]string, error) { + inputs := append([]string{}, config.StaticInputs...) + for _, filepath := range config.SourceFiles { + extra, err := readfile(filepath, fsx.OpenFile) + if err != nil { + return nil, err + } + // See https://github.com/ooni/probe-engine/issues/1123. + if len(extra) <= 0 { + return nil, fmt.Errorf("%w: %s", ErrDetectedEmptyFile, filepath) + } + inputs = append(inputs, extra...) + } + return inputs, nil +} + // loadRemote loads inputs from a remote source. func (il *Loader) loadRemote(ctx context.Context) ([]model.ExperimentTarget, error) { switch experimentname.Canonicalize(il.ExperimentName) { diff --git a/internal/targetloading/targetloading_test.go b/internal/targetloading/targetloading_test.go index 82684a8205..0147ddb5ba 100644 --- a/internal/targetloading/targetloading_test.go +++ b/internal/targetloading/targetloading_test.go @@ -509,9 +509,8 @@ func (TargetLoaderBrokenFile) Close() error { return nil } -func TestTargetLoaderReadfileScannerFailure(t *testing.T) { - il := &Loader{} - out, err := il.readfile("", TargetLoaderBrokenFS{}.Open) +func TestReadfileScannerFailure(t *testing.T) { + out, err := readfile("", TargetLoaderBrokenFS{}.Open) if !errors.Is(err, syscall.EFAULT) { t.Fatal("not the error we expected") } From 1a57fd330fea0067de389a19cb8d919ba4067e5f Mon Sep 17 00:00:00 2001 From: Simone Basso Date: Thu, 6 Jun 2024 19:22:15 +0200 Subject: [PATCH 09/63] x --- internal/engine/experiment.go | 13 ++++++++++--- internal/experiment/dnscheck/dnscheck.go | 6 +++++- internal/model/experiment.go | 5 ++++- internal/oonirun/experiment.go | 5 ++++- internal/registry/dnscheck.go | 2 +- internal/registry/factory.go | 6 ++++++ 6 files changed, 30 insertions(+), 7 deletions(-) diff --git a/internal/engine/experiment.go b/internal/engine/experiment.go index f2934ce4fa..9183a48d99 100644 --- a/internal/engine/experiment.go +++ b/internal/engine/experiment.go @@ -111,7 +111,11 @@ func (e *experiment) SubmitAndUpdateMeasurementContext( // newMeasurement creates a new measurement for this experiment with the given input. func (e *experiment) newMeasurement(target model.ExperimentTarget) *model.Measurement { utctimenow := time.Now().UTC() - // TODO(bassosimone,DecFox): add support for unmarshaling options. + // TODO(bassosimone,DecFox): move here code that supports unmarshaling options + // when there is richer input, which currently is inside ./internal/oonirun. + // + // We MUST do this because the current solution only works for OONI Run and when + // there are command line options but does not work for API/static targets. m := &model.Measurement{ DataFormatVersion: model.OOAPIReportDefaultDataFormatVersion, Input: model.MeasurementInput(target.Input()), @@ -207,7 +211,8 @@ func (e *experiment) MeasureWithContext( // Create a new measurement that the experiment measurer will finish filling // by adding the test keys etc. Please, note that, as of 2024-06-06: // - // 1. experiments using richer input receive input via the Target field; + // 1. experiments using richer input receive input via the Target field + // and ignore (*Measurement).Input field. // // 2. other experiments use (*Measurement).Input. // @@ -218,7 +223,9 @@ func (e *experiment) MeasureWithContext( // Record when we started the experiment, to compute the runtime. start := time.Now() - // Prepare the arguments for the experiment measurer + // Prepare the arguments for the experiment measurer. + // + // Only richer-input-aware experiments honour the Target field. args := &model.ExperimentArgs{ Callbacks: e.callbacks, Measurement: measurement, diff --git a/internal/experiment/dnscheck/dnscheck.go b/internal/experiment/dnscheck/dnscheck.go index 3b6c93787b..af788cea02 100644 --- a/internal/experiment/dnscheck/dnscheck.go +++ b/internal/experiment/dnscheck/dnscheck.go @@ -19,6 +19,7 @@ import ( "github.com/ooni/probe-cli/v3/internal/legacy/tracex" "github.com/ooni/probe-cli/v3/internal/model" "github.com/ooni/probe-cli/v3/internal/runtimex" + "github.com/ooni/probe-cli/v3/internal/targetloading" ) const ( @@ -113,7 +114,7 @@ func (m *Measurer) ExperimentVersion() string { // errors are in addition to any other errors returned by the low level packages // that are used by this experiment to implement its functionality. var ( - ErrInputRequired = errors.New("this experiment needs input") + ErrInputRequired = targetloading.ErrInputRequired ErrInvalidURL = errors.New("the input URL is invalid") ErrUnsupportedURLScheme = errors.New("unsupported URL scheme") ) @@ -125,6 +126,9 @@ func (m *Measurer) Run(ctx context.Context, args *model.ExperimentArgs) error { sess := args.Session // 0. obtain the richer input target, config, and input or panic + if args.Target == nil { + return ErrInputRequired + } target := args.Target.(*Target) config, input := target.options, target.input diff --git a/internal/model/experiment.go b/internal/model/experiment.go index 4aac4a0147..c15bc3ee2d 100644 --- a/internal/model/experiment.go +++ b/internal/model/experiment.go @@ -124,7 +124,10 @@ type ExperimentArgs struct { // Session is the MANDATORY session the experiment can use. Session ExperimentSession - // Target is the MANDATORY target we're measuring. + // Target is the OPTIONAL target we're measuring. + // + // Only richer-input-aware experiments use this field. These experiments + // SHOULD be defensive and handle the case where this field is nil. Target ExperimentTarget } diff --git a/internal/oonirun/experiment.go b/internal/oonirun/experiment.go index 3136e32c66..5665a13fb0 100644 --- a/internal/oonirun/experiment.go +++ b/internal/oonirun/experiment.go @@ -83,11 +83,14 @@ func (ed *Experiment) Run(ctx context.Context) error { } // 2. configure experiment's options + // + // This MUST happen before loading targets because the options will + // possibly be used to produce richer input targets. if err := builder.SetOptionsAny(ed.ExtraOptions); err != nil { return err } - // 3. create input loader and load input for this experiment + // 3. create target loader and load targets for this experiment targetLoader := ed.newTargetLoader(builder) targetList, err := targetLoader.Load(ctx) if err != nil { diff --git a/internal/registry/dnscheck.go b/internal/registry/dnscheck.go index 2ed902c38e..009078aa6b 100644 --- a/internal/registry/dnscheck.go +++ b/internal/registry/dnscheck.go @@ -12,7 +12,7 @@ import ( func init() { const canonicalName = "dnscheck" AllExperiments[canonicalName] = func() *Factory { - // TODO(bassosimone): for now, we MUST keep the InputOrStaticDefault + // TODO(bassosimone,DecFox): for now, we MUST keep the InputOrStaticDefault // policy because otherwise ./pkg/oonimkall should break. return &Factory{ build: func(config interface{}) model.ExperimentMeasurer { diff --git a/internal/registry/factory.go b/internal/registry/factory.go index 2bb6dd3fe2..ee73e95a8f 100644 --- a/internal/registry/factory.go +++ b/internal/registry/factory.go @@ -47,6 +47,7 @@ type Session = model.ExperimentTargetLoaderSession // NewTargetLoader creates a new [model.ExperimentTargetLoader] instance. func (b *Factory) NewTargetLoader(config *model.ExperimentTargetLoaderConfig) model.ExperimentTargetLoader { + // Construct the default loader used in the non-richer input case. loader := &targetloading.Loader{ CheckInConfig: config.CheckInConfig, // OPTIONAL ExperimentName: b.canonicalName, @@ -56,9 +57,14 @@ func (b *Factory) NewTargetLoader(config *model.ExperimentTargetLoaderConfig) mo StaticInputs: config.StaticInputs, SourceFiles: config.SourceFiles, } + + // If an experiment implements richer input, it will use its custom loader + // that will use experiment specific policy for loading targets. if b.newLoader != nil { return b.newLoader(loader, b.config) } + + // Otherwise just return the default loader. return loader } From 399cf788a9e817a4ccfdb63ea67d34524e5ae8a8 Mon Sep 17 00:00:00 2001 From: Simone Basso Date: Thu, 6 Jun 2024 19:26:17 +0200 Subject: [PATCH 10/63] x --- internal/oonirun/experiment.go | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/internal/oonirun/experiment.go b/internal/oonirun/experiment.go index 5665a13fb0..428bf19ba2 100644 --- a/internal/oonirun/experiment.go +++ b/internal/oonirun/experiment.go @@ -82,6 +82,11 @@ func (ed *Experiment) Run(ctx context.Context) error { return err } + // TODO(bassosimone,DecFox): when we're executed by OONI Run v2, it probably makes + // slightly more sense to set options from a json.RawMessage because the current + // command line limitation is that it's hard to set non scalar parameters and instead + // with using OONI Run v2 we can completely bypass such a limitation. + // 2. configure experiment's options // // This MUST happen before loading targets because the options will From 34fe88f861324f9d73b5d3a94c9226743dd13134 Mon Sep 17 00:00:00 2001 From: Simone Basso Date: Thu, 6 Jun 2024 19:45:31 +0200 Subject: [PATCH 11/63] x --- internal/engine/experiment.go | 9 +++++---- internal/oonirun/experiment.go | 6 ++++-- internal/oonirun/experiment_test.go | 27 +++++++++++++-------------- 3 files changed, 22 insertions(+), 20 deletions(-) diff --git a/internal/engine/experiment.go b/internal/engine/experiment.go index 9183a48d99..5f6ac5d874 100644 --- a/internal/engine/experiment.go +++ b/internal/engine/experiment.go @@ -111,7 +111,7 @@ func (e *experiment) SubmitAndUpdateMeasurementContext( // newMeasurement creates a new measurement for this experiment with the given input. func (e *experiment) newMeasurement(target model.ExperimentTarget) *model.Measurement { utctimenow := time.Now().UTC() - // TODO(bassosimone,DecFox): move here code that supports unmarshaling options + // TODO(bassosimone,DecFox): move here code that supports filling the options field // when there is richer input, which currently is inside ./internal/oonirun. // // We MUST do this because the current solution only works for OONI Run and when @@ -211,10 +211,11 @@ func (e *experiment) MeasureWithContext( // Create a new measurement that the experiment measurer will finish filling // by adding the test keys etc. Please, note that, as of 2024-06-06: // - // 1. experiments using richer input receive input via the Target field - // and ignore (*Measurement).Input field. + // 1. Experiments using richer input receive input via the Target field + // and ignore (*Measurement).Input, which however contains the same value + // that would be returned by the Target.Input method. // - // 2. other experiments use (*Measurement).Input. + // 2. Other experiments use (*Measurement).Input. // // Here we're passing the whole target to newMeasurement such that we're able // to record options values in addition to the input value. diff --git a/internal/oonirun/experiment.go b/internal/oonirun/experiment.go index 428bf19ba2..c4613107d7 100644 --- a/internal/oonirun/experiment.go +++ b/internal/oonirun/experiment.go @@ -104,8 +104,10 @@ func (ed *Experiment) Run(ctx context.Context) error { // 4. randomize input, if needed if ed.Random { - rnd := rand.New(rand.NewSource(time.Now().UnixNano())) // #nosec G404 -- not really important - rnd.Shuffle(len(targetList), func(i, j int) { + // Note: since go1.20 the default random generated is random seeded + // + // See https://tip.golang.org/doc/go1.20 + rand.Shuffle(len(targetList), func(i, j int) { targetList[i], targetList[j] = targetList[j], targetList[i] }) experimentShuffledInputs.Add(1) diff --git a/internal/oonirun/experiment_test.go b/internal/oonirun/experiment_test.go index d438f47ce1..1fd38abb0b 100644 --- a/internal/oonirun/experiment_test.go +++ b/internal/oonirun/experiment_test.go @@ -27,9 +27,6 @@ func TestExperimentRunWithFailureToSubmitAndShuffle(t *testing.T) { ExtraOptions: map[string]any{ "SleepTime": int64(10 * time.Millisecond), }, - Inputs: []string{ - "a", "b", "c", - }, InputFilePaths: []string{}, MaxRuntime: 0, Name: "example", @@ -70,10 +67,12 @@ func TestExperimentRunWithFailureToSubmitAndShuffle(t *testing.T) { MockNewTargetLoader: func(config *model.ExperimentTargetLoaderConfig) model.ExperimentTargetLoader { return &mocks.ExperimentTargetLoader{ MockLoad: func(ctx context.Context) ([]model.ExperimentTarget, error) { - // Implementation note: the convention for input-less experiments is that - // they require a single entry containing an empty input. - entry := model.NewOOAPIURLInfoWithDefaultCategoryAndCountry("") - return []model.ExperimentTarget{entry}, nil + results := []model.ExperimentTarget{ + model.NewOOAPIURLInfoWithDefaultCategoryAndCountry("a"), + model.NewOOAPIURLInfoWithDefaultCategoryAndCountry("b"), + model.NewOOAPIURLInfoWithDefaultCategoryAndCountry("c"), + } + return results, nil }, } }, @@ -199,12 +198,12 @@ func TestExperimentRun(t *testing.T) { args: args{}, expectErr: errMocked, }, { - name: "cannot load input", + name: "cannot set options", fields: fields{ newExperimentBuilderFn: func(experimentName string) (model.ExperimentBuilder, error) { eb := &mocks.ExperimentBuilder{ - MockInputPolicy: func() model.InputPolicy { - return model.InputOptional + MockSetOptionsAny: func(options map[string]any) error { + return errMocked }, } return eb, nil @@ -212,7 +211,7 @@ func TestExperimentRun(t *testing.T) { newTargetLoaderFn: func(builder model.ExperimentBuilder) targetLoader { return &mocks.ExperimentTargetLoader{ MockLoad: func(ctx context.Context) ([]model.ExperimentTarget, error) { - return nil, errMocked + return []model.ExperimentTarget{}, nil }, } }, @@ -220,7 +219,7 @@ func TestExperimentRun(t *testing.T) { args: args{}, expectErr: errMocked, }, { - name: "cannot set options", + name: "cannot load input", fields: fields{ newExperimentBuilderFn: func(experimentName string) (model.ExperimentBuilder, error) { eb := &mocks.ExperimentBuilder{ @@ -228,7 +227,7 @@ func TestExperimentRun(t *testing.T) { return model.InputOptional }, MockSetOptionsAny: func(options map[string]any) error { - return errMocked + return nil }, } return eb, nil @@ -236,7 +235,7 @@ func TestExperimentRun(t *testing.T) { newTargetLoaderFn: func(builder model.ExperimentBuilder) targetLoader { return &mocks.ExperimentTargetLoader{ MockLoad: func(ctx context.Context) ([]model.ExperimentTarget, error) { - return []model.ExperimentTarget{}, nil + return nil, errMocked }, } }, From ed895617f8afa408c3aca1855fb657dabe0a3271 Mon Sep 17 00:00:00 2001 From: Simone Basso Date: Thu, 6 Jun 2024 19:57:59 +0200 Subject: [PATCH 12/63] x --- internal/registry/example.go | 5 ++++ internal/registry/factory_test.go | 47 +++++++++++++++++++++++++++++++ 2 files changed, 52 insertions(+) diff --git a/internal/registry/example.go b/internal/registry/example.go index 94c55ca59f..9bcb5459bb 100644 --- a/internal/registry/example.go +++ b/internal/registry/example.go @@ -14,6 +14,11 @@ import ( func init() { const canonicalName = "example" AllExperiments[canonicalName] = func() *Factory { + // TODO(bassosimone,DecFox): as pointed out by @ainghazal, this experiment + // should be the one that people modify to start out new experiments, so it's + // kind of suboptimal that it has a constructor with explicit experiment + // name to ease writing some tests that ./pkg/oonimkall needs given that no + // other experiment ever sets the experiment name externally! return &Factory{ build: func(config interface{}) model.ExperimentMeasurer { return example.NewExperimentMeasurer( diff --git a/internal/registry/factory_test.go b/internal/registry/factory_test.go index 60105d1354..20a7ffe74b 100644 --- a/internal/registry/factory_test.go +++ b/internal/registry/factory_test.go @@ -16,6 +16,7 @@ import ( "github.com/ooni/probe-cli/v3/internal/kvstore" "github.com/ooni/probe-cli/v3/internal/mocks" "github.com/ooni/probe-cli/v3/internal/model" + "github.com/ooni/probe-cli/v3/internal/targetloading" ) type fakeExperimentConfig struct { @@ -860,3 +861,49 @@ func TestFactoryNewTargetLoaderWebConnectivity(t *testing.T) { t.Fatal("expected zero length targets") } } + +// customConfig is a custom config for [TestFactoryCustomTargetLoaderForRicherInput]. +type customConfig struct{} + +// customTargetLoader is a custom target loader for [TestFactoryCustomTargetLoaderForRicherInput]. +type customTargetLoader struct{} + +// Load implements [model.ExperimentTargetLoader]. +func (c *customTargetLoader) Load(ctx context.Context) ([]model.ExperimentTarget, error) { + panic("should not be called") +} + +func TestFactoryCustomTargetLoaderForRicherInput(t *testing.T) { + // create factory creating a custom target loader + factory := &Factory{ + build: nil, + canonicalName: "", + config: &customConfig{}, + enabledByDefault: false, + inputPolicy: "", + interruptible: false, + newLoader: func(config *targetloading.Loader, options any) model.ExperimentTargetLoader { + return &customTargetLoader{} + }, + } + + // create config for creating a new target loader + config := &model.ExperimentTargetLoaderConfig{ + CheckInConfig: &model.OOAPICheckInConfig{ /* nothing */ }, + Session: &mocks.Session{ + MockLogger: func() model.Logger { + return model.DiscardLogger + }, + }, + StaticInputs: []string{}, + SourceFiles: []string{}, + } + + // create the loader + loader := factory.NewTargetLoader(config) + + // make sure the type is the one we expected + if _, good := loader.(*customTargetLoader); !good { + t.Fatalf("expected a *customTargetLoader, got %T", loader) + } +} From c9e55e7eded48ae666abd3e7c341cc9042129cba Mon Sep 17 00:00:00 2001 From: Simone Basso Date: Thu, 6 Jun 2024 21:55:18 +0200 Subject: [PATCH 13/63] x --- internal/experiment/dnscheck/dnscheck.go | 2 +- internal/experiment/dnscheck/dnscheck_test.go | 41 ++- internal/experiment/dnscheck/richerinput.go | 43 +-- .../experiment/dnscheck/richerinput_test.go | 312 ++++++++++++++++++ .../experiment/dnscheck/testdata/input.txt | 2 + internal/reflectx/reflectx.go | 31 ++ internal/reflectx/reflectx_test.go | 70 ++++ 7 files changed, 468 insertions(+), 33 deletions(-) create mode 100644 internal/experiment/dnscheck/richerinput_test.go create mode 100644 internal/experiment/dnscheck/testdata/input.txt create mode 100644 internal/reflectx/reflectx.go create mode 100644 internal/reflectx/reflectx_test.go diff --git a/internal/experiment/dnscheck/dnscheck.go b/internal/experiment/dnscheck/dnscheck.go index af788cea02..e932e7c61f 100644 --- a/internal/experiment/dnscheck/dnscheck.go +++ b/internal/experiment/dnscheck/dnscheck.go @@ -130,7 +130,7 @@ func (m *Measurer) Run(ctx context.Context, args *model.ExperimentArgs) error { return ErrInputRequired } target := args.Target.(*Target) - config, input := target.options, target.input + config, input := target.Options, target.URL // 1. fill the measurement with test keys tk := new(TestKeys) diff --git a/internal/experiment/dnscheck/dnscheck_test.go b/internal/experiment/dnscheck/dnscheck_test.go index bfe91eb767..75d7a3f37c 100644 --- a/internal/experiment/dnscheck/dnscheck_test.go +++ b/internal/experiment/dnscheck/dnscheck_test.go @@ -57,8 +57,8 @@ func TestDNSCheckFailsWithoutInput(t *testing.T) { Measurement: new(model.Measurement), Session: newsession(), Target: &Target{ - input: "", // explicitly empty - options: &Config{ + URL: "", // explicitly empty + Options: &Config{ Domain: "example.com", }, }, @@ -76,8 +76,8 @@ func TestDNSCheckFailsWithInvalidURL(t *testing.T) { Measurement: &model.Measurement{Input: "Not a valid URL \x7f"}, Session: newsession(), Target: &Target{ - input: "Not a valid URL \x7f", - options: &Config{}, + URL: "Not a valid URL \x7f", + Options: &Config{}, }, } err := measurer.Run(context.Background(), args) @@ -93,8 +93,8 @@ func TestDNSCheckFailsWithUnsupportedProtocol(t *testing.T) { Measurement: &model.Measurement{Input: "file://1.1.1.1"}, Session: newsession(), Target: &Target{ - input: "file://1.1.1.1", - options: &Config{}, + URL: "file://1.1.1.1", + Options: &Config{}, }, } err := measurer.Run(context.Background(), args) @@ -113,8 +113,8 @@ func TestWithCancelledContext(t *testing.T) { Measurement: measurement, Session: newsession(), Target: &Target{ - input: "dot://one.one.one.one", - options: &Config{ + URL: "dot://one.one.one.one", + Options: &Config{ DefaultAddrs: "1.1.1.1 1.0.0.1", }, }, @@ -125,6 +125,23 @@ func TestWithCancelledContext(t *testing.T) { } } +func TestWithNilTarget(t *testing.T) { + ctx, cancel := context.WithCancel(context.Background()) + cancel() // immediately cancel the context + measurer := NewExperimentMeasurer() + measurement := &model.Measurement{Input: "dot://one.one.one.one"} + args := &model.ExperimentArgs{ + Callbacks: model.NewPrinterCallbacks(log.Log), + Measurement: measurement, + Session: newsession(), + Target: nil, // explicitly nil + } + err := measurer.Run(ctx, args) + if !errors.Is(err, ErrInputRequired) { + t.Fatal("unexpected err", err) + } +} + func TestMakeResolverURL(t *testing.T) { // test address substitution addr := "255.255.255.0" @@ -161,8 +178,8 @@ func TestDNSCheckValid(t *testing.T) { Measurement: &measurement, Session: newsession(), Target: &Target{ - input: "dot://one.one.one.one:853", - options: &Config{ + URL: "dot://one.one.one.one:853", + Options: &Config{ DefaultAddrs: "1.1.1.1 1.0.0.1", }, }, @@ -210,8 +227,8 @@ func TestDNSCheckWait(t *testing.T) { Measurement: &measurement, Session: newsession(), Target: &Target{ - input: input, - options: &Config{}, + URL: input, + Options: &Config{}, }, } err := measurer.Run(context.Background(), args) diff --git a/internal/experiment/dnscheck/richerinput.go b/internal/experiment/dnscheck/richerinput.go index 758b014319..d512a511a9 100644 --- a/internal/experiment/dnscheck/richerinput.go +++ b/internal/experiment/dnscheck/richerinput.go @@ -4,16 +4,17 @@ import ( "context" "github.com/ooni/probe-cli/v3/internal/model" + "github.com/ooni/probe-cli/v3/internal/reflectx" "github.com/ooni/probe-cli/v3/internal/targetloading" ) // Target is a richer-input target that this experiment should measure. type Target struct { - // input is the input. - input string + // Options contains the configuration. + Options *Config - // options is the configuration. - options *Config + // URL is the input URL. + URL string } var _ model.ExperimentTarget = &Target{} @@ -30,12 +31,12 @@ func (t *Target) Country() string { // Input implements [model.ExperimentTarget]. func (t *Target) Input() string { - return t.input + return t.URL } // String implements [model.ExperimentTarget]. func (t *Target) String() string { - return t.input + return t.URL } // NewLoader constructs a new [model.ExperimentTargerLoader] instance. @@ -49,22 +50,24 @@ func NewLoader(loader *targetloading.Loader, gopts any) model.ExperimentTargetLo // Construct the proper loader instance. return &targetLoader{ - loader: loader, - options: options, + defaultInput: defaultInput, + loader: loader, + options: options, } } +// targetLoader loads targets for this experiment. type targetLoader struct { - loader *targetloading.Loader - options *Config + defaultInput []model.ExperimentTarget + loader *targetloading.Loader + options *Config } // Load implements model.ExperimentTargetLoader. func (tl *targetLoader) Load(ctx context.Context) ([]model.ExperimentTarget, error) { - // TODO(bassosimone): we need a way to know whether the options are empty!!! - - // If there's nothing to statically load fallback to the API - if len(tl.loader.StaticInputs) <= 0 && len(tl.loader.SourceFiles) <= 0 { + // If inputs and files are all empty and there are no options, let's use the backend + if len(tl.loader.StaticInputs) <= 0 && len(tl.loader.SourceFiles) <= 0 && + reflectx.StructOrStructPtrIsZero(tl.options) { return tl.loadFromBackend(ctx) } @@ -80,8 +83,8 @@ func (tl *targetLoader) Load(ctx context.Context) ([]model.ExperimentTarget, err var targets []model.ExperimentTarget for _, input := range inputs { targets = append(targets, &Target{ - options: tl.options, - input: input, + Options: tl.options, + URL: input, }) } return targets, nil @@ -96,15 +99,15 @@ var defaultInput = []model.ExperimentTarget{ // Make sure we include the typical IP addresses for the domain. // &Target{ - input: "https://dns.google/dns-query", - options: &Config{ + URL: "https://dns.google/dns-query", + Options: &Config{ HTTP3Enabled: true, DefaultAddrs: "8.8.8.8 8.8.4.4", }, }, &Target{ - input: "https://dns.google/dns-query", - options: &Config{ + URL: "https://dns.google/dns-query", + Options: &Config{ DefaultAddrs: "8.8.8.8 8.8.4.4", }, }, diff --git a/internal/experiment/dnscheck/richerinput_test.go b/internal/experiment/dnscheck/richerinput_test.go new file mode 100644 index 0000000000..2d9e5dd53d --- /dev/null +++ b/internal/experiment/dnscheck/richerinput_test.go @@ -0,0 +1,312 @@ +package dnscheck + +import ( + "context" + "errors" + "io/fs" + "path/filepath" + "testing" + + "github.com/google/go-cmp/cmp" + "github.com/ooni/probe-cli/v3/internal/mocks" + "github.com/ooni/probe-cli/v3/internal/model" + "github.com/ooni/probe-cli/v3/internal/targetloading" +) + +func TestTarget(t *testing.T) { + target := &Target{ + URL: "https://dns.google/dns-query", + Options: &Config{ + DefaultAddrs: "8.8.8.8 8.8.4.4", + Domain: "example.com", + HTTP3Enabled: false, + HTTPHost: "dns.google", + TLSServerName: "dns.google.com", + TLSVersion: "TLSv1.3", + }, + } + + t.Run("Category", func(t *testing.T) { + if target.Category() != model.DefaultCategoryCode { + t.Fatal("invalid Category") + } + }) + + t.Run("Country", func(t *testing.T) { + if target.Country() != model.DefaultCountryCode { + t.Fatal("invalid Country") + } + }) + + t.Run("Input", func(t *testing.T) { + if target.Input() != "https://dns.google/dns-query" { + t.Fatal("invalid Input") + } + }) + + t.Run("String", func(t *testing.T) { + if target.String() != "https://dns.google/dns-query" { + t.Fatal("invalid String") + } + }) +} + +func TestNewLoader(t *testing.T) { + // create the pointers we expect to see + child := &targetloading.Loader{} + options := &Config{} + + // create the loader and cast it to its private type + loader := NewLoader(child, options).(*targetLoader) + + // make sure the default input is okay + if diff := cmp.Diff(defaultInput, loader.defaultInput); diff != "" { + t.Fatal(diff) + } + + // make sure the loader is okay + if child != loader.loader { + t.Fatal("invalid loader pointer") + } + + // make sure the options are okay + if options != loader.options { + t.Fatal("invalid options pointer") + } +} + +// testDefaultInput is the default input used by [TestTargetLoaderLoad]. +var testDefaultInput = []model.ExperimentTarget{ + &Target{ + URL: "https://dns.google/dns-query", + Options: &Config{ + HTTP3Enabled: true, + DefaultAddrs: "8.8.8.8 8.8.4.4", + }, + }, + &Target{ + URL: "https://dns.google/dns-query", + Options: &Config{ + DefaultAddrs: "8.8.8.8 8.8.4.4", + }, + }, +} + +func TestTargetLoaderLoad(t *testing.T) { + // testcase is a test case implemented by this function + type testcase struct { + // name is the test case name + name string + + // options contains the options to use + options *Config + + // loader is the loader to use + loader *targetloading.Loader + + // expectErr is the error we expect + expectErr error + + // expectResults contains the expected results + expectTargets []model.ExperimentTarget + } + + cases := []testcase{ + + { + name: "with options, inputs, and files", + options: &Config{ + DefaultAddrs: "1.1.1.1 1.0.0.1", + }, + loader: &targetloading.Loader{ + CheckInConfig: &model.OOAPICheckInConfig{ /* nothing */ }, + ExperimentName: "dnscheck", + InputPolicy: model.InputOrStaticDefault, + Logger: model.DiscardLogger, + Session: &mocks.Session{}, + StaticInputs: []string{ + "https://dns.cloudflare.com/dns-query", + "https://one.one.one.one/dns-query", + }, + SourceFiles: []string{ + filepath.Join("testdata", "input.txt"), + }, + }, + expectErr: nil, + expectTargets: []model.ExperimentTarget{ + &Target{ + URL: "https://dns.cloudflare.com/dns-query", + Options: &Config{ + DefaultAddrs: "1.1.1.1 1.0.0.1", + }, + }, + &Target{ + URL: "https://one.one.one.one/dns-query", + Options: &Config{ + DefaultAddrs: "1.1.1.1 1.0.0.1", + }, + }, + &Target{ + URL: "https://1dot1dot1dot1dot.com/dns-query", + Options: &Config{ + DefaultAddrs: "1.1.1.1 1.0.0.1", + }, + }, + &Target{ + URL: "https://dns.cloudflare/dns-query", + Options: &Config{ + DefaultAddrs: "1.1.1.1 1.0.0.1", + }, + }, + }, + }, + + { + name: "with an unreadable file", + options: &Config{ + DefaultAddrs: "1.1.1.1 1.0.0.1", + }, + loader: &targetloading.Loader{ + CheckInConfig: &model.OOAPICheckInConfig{ /* nothing */ }, + ExperimentName: "dnscheck", + InputPolicy: model.InputOrStaticDefault, + Logger: model.DiscardLogger, + Session: &mocks.Session{}, + StaticInputs: []string{ + "https://dns.cloudflare.com/dns-query", + "https://one.one.one.one/dns-query", + }, + SourceFiles: []string{ + filepath.Join("testdata", "nonexistent.txt"), + }, + }, + expectErr: fs.ErrNotExist, + expectTargets: nil, + }, + + { + name: "with just inputs", + options: &Config{}, + loader: &targetloading.Loader{ + CheckInConfig: &model.OOAPICheckInConfig{ /* nothing */ }, + ExperimentName: "dnscheck", + InputPolicy: model.InputOrStaticDefault, + Logger: model.DiscardLogger, + Session: &mocks.Session{}, + StaticInputs: []string{ + "https://dns.cloudflare.com/dns-query", + "https://one.one.one.one/dns-query", + }, + SourceFiles: []string{}, + }, + expectErr: nil, + expectTargets: []model.ExperimentTarget{ + &Target{ + URL: "https://dns.cloudflare.com/dns-query", + Options: &Config{}, + }, + &Target{ + URL: "https://one.one.one.one/dns-query", + Options: &Config{}, + }, + }, + }, + + { + name: "with just files", + options: &Config{}, + loader: &targetloading.Loader{ + CheckInConfig: &model.OOAPICheckInConfig{ /* nothing */ }, + ExperimentName: "dnscheck", + InputPolicy: model.InputOrStaticDefault, + Logger: model.DiscardLogger, + Session: &mocks.Session{}, + StaticInputs: []string{}, + SourceFiles: []string{ + filepath.Join("testdata", "input.txt"), + }, + }, + expectErr: nil, + expectTargets: []model.ExperimentTarget{ + &Target{ + URL: "https://1dot1dot1dot1dot.com/dns-query", + Options: &Config{}, + }, + &Target{ + URL: "https://dns.cloudflare/dns-query", + Options: &Config{}, + }, + }, + }, + + { + name: "with just options", + options: &Config{ + DefaultAddrs: "1.1.1.1 1.0.0.1", + }, + loader: &targetloading.Loader{ + CheckInConfig: &model.OOAPICheckInConfig{ /* nothing */ }, + ExperimentName: "dnscheck", + InputPolicy: model.InputOrStaticDefault, + Logger: model.DiscardLogger, + Session: &mocks.Session{}, + StaticInputs: []string{}, + SourceFiles: []string{}, + }, + expectErr: nil, + expectTargets: nil, + }, + + { + name: "with no options, not inputs, no files", + options: &Config{}, + loader: &targetloading.Loader{ + CheckInConfig: &model.OOAPICheckInConfig{ /* nothing */ }, + ExperimentName: "dnscheck", + InputPolicy: model.InputOrStaticDefault, + Logger: model.DiscardLogger, + Session: &mocks.Session{}, + StaticInputs: []string{}, + SourceFiles: []string{}, + }, + expectErr: nil, + expectTargets: testDefaultInput, + }} + + for _, tc := range cases { + t.Run(tc.name, func(t *testing.T) { + // create a target loader using the given config + // + // note that we use a default test input for results predictability + // since the static list may change over time + tl := &targetLoader{ + defaultInput: testDefaultInput, + loader: tc.loader, + options: tc.options, + } + + // load targets + targets, err := tl.Load(context.Background()) + + // make sure error is consistent + switch { + case err == nil && tc.expectErr == nil: + // fallthrough + + case err != nil && tc.expectErr != nil: + if !errors.Is(err, tc.expectErr) { + t.Fatal("unexpected error", err) + } + // fallthrough + + default: + t.Fatal("expected", tc.expectErr, "got", err) + } + + // make sure the targets are consistent + if diff := cmp.Diff(tc.expectTargets, targets); diff != "" { + t.Fatal(diff) + } + }) + } +} diff --git a/internal/experiment/dnscheck/testdata/input.txt b/internal/experiment/dnscheck/testdata/input.txt new file mode 100644 index 0000000000..42a1896f10 --- /dev/null +++ b/internal/experiment/dnscheck/testdata/input.txt @@ -0,0 +1,2 @@ +https://1dot1dot1dot1dot.com/dns-query +https://dns.cloudflare/dns-query diff --git a/internal/reflectx/reflectx.go b/internal/reflectx/reflectx.go new file mode 100644 index 0000000000..ace8b464d9 --- /dev/null +++ b/internal/reflectx/reflectx.go @@ -0,0 +1,31 @@ +// Package reflectx contains [reflect] extensions. +package reflectx + +import ( + "reflect" + + "github.com/ooni/probe-cli/v3/internal/runtimex" +) + +// StructOrStructPtrIsZero returns whether a given struct or struct pointer +// only contains zero value public fields. This function panic if passed a value +// that is neither a pointer to struct nor a struct. This function panics if +// passed a nil struct pointer. +func StructOrStructPtrIsZero(vop any) bool { + vx := reflect.ValueOf(vop) + if vx.Kind() == reflect.Pointer { + vx = vx.Elem() + } + runtimex.Assert(vx.Kind() == reflect.Struct, "not a struct") + tx := vx.Type() + for idx := 0; idx < tx.NumField(); idx++ { + fvx, ftx := vx.Field(idx), tx.Field(idx) + if !ftx.IsExported() { + continue + } + if !fvx.IsZero() { + return false + } + } + return true +} diff --git a/internal/reflectx/reflectx_test.go b/internal/reflectx/reflectx_test.go new file mode 100644 index 0000000000..c8b74c5bbd --- /dev/null +++ b/internal/reflectx/reflectx_test.go @@ -0,0 +1,70 @@ +package reflectx + +import ( + "testing" + + "github.com/ooni/probe-cli/v3/internal/testingx" +) + +type example struct { + _ struct{} + Age int64 + Ch chan int64 + F bool + Fmp map[string]*bool + Fp *bool + Fv []bool + Fvp []*bool + Name string + Ptr *int64 + V []int64 +} + +var nonzero example + +func init() { + ff := &testingx.FakeFiller{} + ff.Fill(&nonzero) +} + +func TestStructOrStructPtrIsZero(t *testing.T) { + + // testcase is a test case implemented by this function + type testcase struct { + // name is the name of the test case + name string + + // input is the input + input any + + // expect is the expected result + expect bool + } + + cases := []testcase{{ + name: "[struct] with zero value", + input: example{}, + expect: true, + }, { + name: "[ptr] with zero value", + input: &example{}, + expect: true, + }, { + name: "[struct] with nonzero value", + input: nonzero, + expect: false, + }, { + name: "[ptr] with nonzero value", + input: &nonzero, + expect: false, + }} + + for _, tc := range cases { + t.Run(tc.name, func(t *testing.T) { + t.Logf("input: %#v", tc.input) + if got := StructOrStructPtrIsZero(tc.input); got != tc.expect { + t.Fatal("expected", tc.expect, "got", got) + } + }) + } +} From 68f806ddcf402b63811d18162a655fd910e21507 Mon Sep 17 00:00:00 2001 From: Simone Basso Date: Thu, 6 Jun 2024 22:01:41 +0200 Subject: [PATCH 14/63] x --- internal/experiment/dnscheck/dnscheck_test.go | 6 ++---- 1 file changed, 2 insertions(+), 4 deletions(-) diff --git a/internal/experiment/dnscheck/dnscheck_test.go b/internal/experiment/dnscheck/dnscheck_test.go index 75d7a3f37c..1deaf3d92a 100644 --- a/internal/experiment/dnscheck/dnscheck_test.go +++ b/internal/experiment/dnscheck/dnscheck_test.go @@ -125,9 +125,7 @@ func TestWithCancelledContext(t *testing.T) { } } -func TestWithNilTarget(t *testing.T) { - ctx, cancel := context.WithCancel(context.Background()) - cancel() // immediately cancel the context +func TestDNSCheckFailsWithNilTarget(t *testing.T) { measurer := NewExperimentMeasurer() measurement := &model.Measurement{Input: "dot://one.one.one.one"} args := &model.ExperimentArgs{ @@ -136,7 +134,7 @@ func TestWithNilTarget(t *testing.T) { Session: newsession(), Target: nil, // explicitly nil } - err := measurer.Run(ctx, args) + err := measurer.Run(context.Background(), args) if !errors.Is(err, ErrInputRequired) { t.Fatal("unexpected err", err) } From 21baba002fa42d165c88427dd0aae784d272fd3a Mon Sep 17 00:00:00 2001 From: Simone Basso Date: Thu, 6 Jun 2024 22:05:20 +0200 Subject: [PATCH 15/63] x --- internal/experiment/dnscheck/richerinput.go | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/internal/experiment/dnscheck/richerinput.go b/internal/experiment/dnscheck/richerinput.go index d512a511a9..4d4a7ce528 100644 --- a/internal/experiment/dnscheck/richerinput.go +++ b/internal/experiment/dnscheck/richerinput.go @@ -112,8 +112,8 @@ var defaultInput = []model.ExperimentTarget{ }, }, - // TODO(bassosimone): before merging, we need to reinstate the - // whole list that we previously had in tree + // TODO(bassosimone,DecFox): before releasing, we need to either sync up + // this list with ./internal/targetloader or implement a backend API. } func (tl *targetLoader) loadFromBackend(_ context.Context) ([]model.ExperimentTarget, error) { From 6a86d436dce2cae80deb7c40402882be6d09bd86 Mon Sep 17 00:00:00 2001 From: Simone Basso Date: Thu, 6 Jun 2024 22:19:57 +0200 Subject: [PATCH 16/63] x --- internal/experiment/dnscheck/dnscheck.go | 1 + 1 file changed, 1 insertion(+) diff --git a/internal/experiment/dnscheck/dnscheck.go b/internal/experiment/dnscheck/dnscheck.go index e932e7c61f..49eed47d5c 100644 --- a/internal/experiment/dnscheck/dnscheck.go +++ b/internal/experiment/dnscheck/dnscheck.go @@ -131,6 +131,7 @@ func (m *Measurer) Run(ctx context.Context, args *model.ExperimentArgs) error { } target := args.Target.(*Target) config, input := target.Options, target.URL + sess.Logger().Infof("dnscheck: using richer input: %+v %+v", config, input) // 1. fill the measurement with test keys tk := new(TestKeys) From 8b710f6a2bfbef6a6badcad4a8cb6d6fb38dd1ec Mon Sep 17 00:00:00 2001 From: Simone Basso Date: Fri, 7 Jun 2024 07:47:08 +0200 Subject: [PATCH 17/63] cleanup(pkg/oonimkall): simplify test code This diff simplifies test code in pkg/oonimkall in preparation for further richer-input related changes. Part of https://github.com/ooni/probe/issues/2607 --- internal/mocks/session.go | 30 ++++ internal/mocks/session_test.go | 61 +++++++ pkg/oonimkall/taskmocks_test.go | 187 +-------------------- pkg/oonimkall/taskmodel.go | 83 ++------- pkg/oonimkall/taskrunner.go | 37 ++-- pkg/oonimkall/taskrunner_test.go | 270 ++++++++++++++++++------------ pkg/oonimkall/tasksession.go | 78 --------- pkg/oonimkall/tasksession_test.go | 128 -------------- 8 files changed, 292 insertions(+), 582 deletions(-) delete mode 100644 pkg/oonimkall/tasksession.go delete mode 100644 pkg/oonimkall/tasksession_test.go diff --git a/internal/mocks/session.go b/internal/mocks/session.go index b26611268c..00b9a0a71b 100644 --- a/internal/mocks/session.go +++ b/internal/mocks/session.go @@ -59,6 +59,16 @@ type Session struct { MockCheckIn func(ctx context.Context, config *model.OOAPICheckInConfig) (*model.OOAPICheckInResult, error) + + MockClose func() error + + MockMaybeLookupBackendsContext func(ctx context.Context) error + + MockMaybeLookupLocationContext func(ctx context.Context) error + + MockResolverASNString func() string + + MockResolverNetworkName func() string } func (sess *Session) GetTestHelpersByName(name string) ([]model.OOAPIService, bool) { @@ -159,3 +169,23 @@ func (sess *Session) CheckIn(ctx context.Context, config *model.OOAPICheckInConfig) (*model.OOAPICheckInResult, error) { return sess.MockCheckIn(ctx, config) } + +func (sess *Session) Close() error { + return sess.MockClose() +} + +func (sess *Session) MaybeLookupBackendsContext(ctx context.Context) error { + return sess.MockMaybeLookupBackendsContext(ctx) +} + +func (sess *Session) MaybeLookupLocationContext(ctx context.Context) error { + return sess.MockMaybeLookupLocationContext(ctx) +} + +func (sess *Session) ResolverASNString() string { + return sess.MockResolverASNString() +} + +func (sess *Session) ResolverNetworkName() string { + return sess.MockResolverNetworkName() +} diff --git a/internal/mocks/session_test.go b/internal/mocks/session_test.go index e26f67430d..3a98ee5ff9 100644 --- a/internal/mocks/session_test.go +++ b/internal/mocks/session_test.go @@ -354,4 +354,65 @@ func TestSession(t *testing.T) { t.Fatal("unexpected out") } }) + + t.Run("Close", func(t *testing.T) { + expected := errors.New("mocked err") + s := &Session{ + MockClose: func() error { + return expected + }, + } + err := s.Close() + if !errors.Is(err, expected) { + t.Fatal("unexpected err") + } + }) + + t.Run("MaybeLookupBackendsContext", func(t *testing.T) { + expected := errors.New("mocked err") + s := &Session{ + MockMaybeLookupBackendsContext: func(ctx context.Context) error { + return expected + }, + } + err := s.MaybeLookupBackendsContext(context.Background()) + if !errors.Is(err, expected) { + t.Fatal("unexpected err") + } + }) + + t.Run("MaybeLookupLocationContext", func(t *testing.T) { + expected := errors.New("mocked err") + s := &Session{ + MockMaybeLookupLocationContext: func(ctx context.Context) error { + return expected + }, + } + err := s.MaybeLookupLocationContext(context.Background()) + if !errors.Is(err, expected) { + t.Fatal("unexpected err") + } + }) + + t.Run("ResolverASNString", func(t *testing.T) { + s := &Session{ + MockResolverASNString: func() string { + return "xx" + }, + } + if s.ResolverASNString() != "xx" { + t.Fatal("unexpected result") + } + }) + + t.Run("ResolverNetworkName", func(t *testing.T) { + s := &Session{ + MockResolverNetworkName: func() string { + return "xx" + }, + } + if s.ResolverNetworkName() != "xx" { + t.Fatal("unexpected result") + } + }) } diff --git a/pkg/oonimkall/taskmocks_test.go b/pkg/oonimkall/taskmocks_test.go index 839f519e36..9b03386b85 100644 --- a/pkg/oonimkall/taskmocks_test.go +++ b/pkg/oonimkall/taskmocks_test.go @@ -1,19 +1,14 @@ package oonimkall -import ( - "context" - "errors" - "sync" - - "github.com/ooni/probe-cli/v3/internal/engine" - "github.com/ooni/probe-cli/v3/internal/model" -) - // // This file contains mocks for types used by tasks. Because // we only use mocks when testing, this file is a `_test.go` file. // +import ( + "sync" +) + // CollectorTaskEmitter is a thread-safe taskEmitter // that stores all the events inside itself. type CollectorTaskEmitter struct { @@ -46,177 +41,3 @@ func (e *CollectorTaskEmitter) Collect() (out []*event) { e.mu.Unlock() return } - -// SessionBuilderConfigSaver is a session builder that -// saves the received config and returns an error. -type SessionBuilderConfigSaver struct { - Config engine.SessionConfig -} - -var _ taskSessionBuilder = &SessionBuilderConfigSaver{} - -func (b *SessionBuilderConfigSaver) NewSession( - ctx context.Context, config engine.SessionConfig) (taskSession, error) { - b.Config = config - return nil, errors.New("mocked error") -} - -// MockableTaskRunnerDependencies allows to mock all the -// dependencies of taskRunner using a single structure. -type MockableTaskRunnerDependencies struct { - - // taskSessionBuilder: - - MockNewSession func(ctx context.Context, - config engine.SessionConfig) (taskSession, error) - - // taskSession: - - MockClose func() error - MockNewExperimentBuilderByName func(name string) (taskExperimentBuilder, error) - MockMaybeLookupBackendsContext func(ctx context.Context) error - MockMaybeLookupLocationContext func(ctx context.Context) error - MockProbeIP func() string - MockProbeASNString func() string - MockProbeCC func() string - MockProbeNetworkName func() string - MockResolverASNString func() string - MockResolverIP func() string - MockResolverNetworkName func() string - - // taskExperimentBuilder: - - MockableSetCallbacks func(callbacks model.ExperimentCallbacks) - MockableInputPolicy func() model.InputPolicy - MockableNewExperimentInstance func() taskExperiment - MockableInterruptible func() bool - - // taskExperiment: - - MockableKibiBytesReceived func() float64 - MockableKibiBytesSent func() float64 - MockableOpenReportContext func(ctx context.Context) error - MockableReportID func() string - MockableMeasureWithContext func(ctx context.Context, target model.ExperimentTarget) ( - measurement *model.Measurement, err error) - MockableSubmitAndUpdateMeasurementContext func( - ctx context.Context, measurement *model.Measurement) error -} - -var ( - _ taskSessionBuilder = &MockableTaskRunnerDependencies{} - _ taskSession = &MockableTaskRunnerDependencies{} - _ taskExperimentBuilder = &MockableTaskRunnerDependencies{} - _ taskExperiment = &MockableTaskRunnerDependencies{} -) - -func (dep *MockableTaskRunnerDependencies) NewSession( - ctx context.Context, config engine.SessionConfig) (taskSession, error) { - if f := dep.MockNewSession; f != nil { - return f(ctx, config) - } - return dep, nil -} - -func (dep *MockableTaskRunnerDependencies) Close() error { - return dep.MockClose() -} - -func (dep *MockableTaskRunnerDependencies) NewExperimentBuilderByName(name string) (taskExperimentBuilder, error) { - if f := dep.MockNewExperimentBuilderByName; f != nil { - return f(name) - } - return dep, nil -} - -func (dep *MockableTaskRunnerDependencies) MaybeLookupBackendsContext(ctx context.Context) error { - return dep.MockMaybeLookupBackendsContext(ctx) -} - -func (dep *MockableTaskRunnerDependencies) MaybeLookupLocationContext(ctx context.Context) error { - return dep.MockMaybeLookupLocationContext(ctx) -} - -func (dep *MockableTaskRunnerDependencies) ProbeIP() string { - return dep.MockProbeIP() -} - -func (dep *MockableTaskRunnerDependencies) ProbeASNString() string { - return dep.MockProbeASNString() -} - -func (dep *MockableTaskRunnerDependencies) ProbeCC() string { - return dep.MockProbeCC() -} - -func (dep *MockableTaskRunnerDependencies) ProbeNetworkName() string { - return dep.MockProbeNetworkName() -} - -func (dep *MockableTaskRunnerDependencies) ResolverASNString() string { - return dep.MockResolverASNString() -} - -func (dep *MockableTaskRunnerDependencies) ResolverIP() string { - return dep.MockResolverIP() -} - -func (dep *MockableTaskRunnerDependencies) ResolverNetworkName() string { - return dep.MockResolverNetworkName() -} - -func (dep *MockableTaskRunnerDependencies) SetCallbacks(callbacks model.ExperimentCallbacks) { - dep.MockableSetCallbacks(callbacks) -} - -func (dep *MockableTaskRunnerDependencies) InputPolicy() model.InputPolicy { - return dep.MockableInputPolicy() -} - -func (dep *MockableTaskRunnerDependencies) NewExperimentInstance() taskExperiment { - if f := dep.MockableNewExperimentInstance; f != nil { - return f() - } - return dep -} - -func (dep *MockableTaskRunnerDependencies) Interruptible() bool { - return dep.MockableInterruptible() -} - -func (dep *MockableTaskRunnerDependencies) KibiBytesReceived() float64 { - return dep.MockableKibiBytesReceived() -} - -func (dep *MockableTaskRunnerDependencies) KibiBytesSent() float64 { - return dep.MockableKibiBytesSent() -} - -func (dep *MockableTaskRunnerDependencies) OpenReportContext(ctx context.Context) error { - return dep.MockableOpenReportContext(ctx) -} - -func (dep *MockableTaskRunnerDependencies) ReportID() string { - return dep.MockableReportID() -} - -func (dep *MockableTaskRunnerDependencies) MeasureWithContext( - ctx context.Context, target model.ExperimentTarget) (measurement *model.Measurement, err error) { - return dep.MockableMeasureWithContext(ctx, target) -} - -func (dep *MockableTaskRunnerDependencies) SubmitAndUpdateMeasurementContext( - ctx context.Context, measurement *model.Measurement) error { - return dep.MockableSubmitAndUpdateMeasurementContext(ctx, measurement) -} - -// MockableKVStoreFSBuilder is a mockable taskKVStoreFSBuilder. -type MockableKVStoreFSBuilder struct { - MockNewFS func(path string) (model.KeyValueStore, error) -} - -var _ taskKVStoreFSBuilder = &MockableKVStoreFSBuilder{} - -func (m *MockableKVStoreFSBuilder) NewFS(path string) (model.KeyValueStore, error) { - return m.MockNewFS(path) -} diff --git a/pkg/oonimkall/taskmodel.go b/pkg/oonimkall/taskmodel.go index 86f534e01b..3bd6ccfee5 100644 --- a/pkg/oonimkall/taskmodel.go +++ b/pkg/oonimkall/taskmodel.go @@ -1,13 +1,5 @@ package oonimkall -import ( - "context" - "io" - - "github.com/ooni/probe-cli/v3/internal/engine" - "github.com/ooni/probe-cli/v3/internal/model" -) - // // Task Model // @@ -25,6 +17,13 @@ import ( // ooni/probe-cli and so it's not defined in this file. // +import ( + "context" + "io" + + "github.com/ooni/probe-cli/v3/internal/model" +) + const taskABIVersion = 1 // Running tasks emit logs using different log levels. We @@ -161,28 +160,14 @@ type event struct { // The abstraction representing a OONI session is taskSession. // -// taskKVStoreFSBuilder constructs a KVStore with -// filesystem backing for running tests. -type taskKVStoreFSBuilder interface { - // NewFS creates a new KVStore using the filesystem. - NewFS(path string) (model.KeyValueStore, error) -} - -// taskSessionBuilder constructs a new Session. -type taskSessionBuilder interface { - // NewSession creates a new taskSession. - NewSession(ctx context.Context, - config engine.SessionConfig) (taskSession, error) -} - // taskSession abstracts a OONI session. type taskSession interface { // A session can be closed. io.Closer - // NewExperimentBuilderByName creates the builder for constructing + // NewExperimentBuilder creates the builder for constructing // a new experiment given the experiment's name. - NewExperimentBuilderByName(name string) (taskExperimentBuilder, error) + NewExperimentBuilder(name string) (model.ExperimentBuilder, error) // MaybeLookupBackendsContext lookups the OONI backend unless // this operation has already been performed. @@ -200,10 +185,6 @@ type taskSession interface { // and returns the resolved probe ASN as a string. ProbeASNString() string - // ProbeCC must be called after MaybeLookupLocationContext - // and returns the resolved probe country code. - ProbeCC() string - // ProbeNetworkName must be called after MaybeLookupLocationContext // and returns the resolved probe country code. ProbeNetworkName() string @@ -212,53 +193,15 @@ type taskSession interface { // and returns the resolved resolver's ASN as a string. ResolverASNString() string - // ResolverIP must be called after MaybeLookupLocationContext - // and returns the resolved resolver's IP. - ResolverIP() string - // ResolverNetworkName must be called after MaybeLookupLocationContext // and returns the resolved resolver's network name. ResolverNetworkName() string -} - -// taskExperimentBuilder builds a taskExperiment. -type taskExperimentBuilder interface { - // SetCallbacks sets the experiment callbacks. - SetCallbacks(callbacks model.ExperimentCallbacks) - - // InputPolicy returns the experiment's input policy. - InputPolicy() model.InputPolicy - - // NewExperiment creates the new experiment. - NewExperimentInstance() taskExperiment - - // Interruptible returns whether this experiment is interruptible. - Interruptible() bool -} - -// taskExperiment is a runnable experiment. -type taskExperiment interface { - // KibiBytesReceived returns the KiB received by the experiment. - KibiBytesReceived() float64 - - // KibiBytesSent returns the KiB sent by the experiment. - KibiBytesSent() float64 - - // OpenReportContext opens a new report. - OpenReportContext(ctx context.Context) error - - // ReportID must be called after a successful OpenReportContext - // and returns the report ID for this measurement. - ReportID() string - // MeasureWithContext runs the measurement. - MeasureWithContext(ctx context.Context, target model.ExperimentTarget) ( - measurement *model.Measurement, err error) + // A session should be used by an experiment. + model.ExperimentSession - // SubmitAndUpdateMeasurementContext submits the measurement - // and updates its report ID on success. - SubmitAndUpdateMeasurementContext( - ctx context.Context, measurement *model.Measurement) error + // A session should be used when loading targets. + model.ExperimentTargetLoaderSession } // diff --git a/pkg/oonimkall/taskrunner.go b/pkg/oonimkall/taskrunner.go index d5b70947c0..000532e25a 100644 --- a/pkg/oonimkall/taskrunner.go +++ b/pkg/oonimkall/taskrunner.go @@ -8,6 +8,7 @@ import ( "time" "github.com/ooni/probe-cli/v3/internal/engine" + "github.com/ooni/probe-cli/v3/internal/kvstore" "github.com/ooni/probe-cli/v3/internal/model" "github.com/ooni/probe-cli/v3/internal/runtimex" "github.com/ooni/probe-cli/v3/internal/targetloading" @@ -15,10 +16,10 @@ import ( // runnerForTask runs a specific task type runnerForTask struct { - emitter *taskEmitterWrapper - kvStoreBuilder taskKVStoreFSBuilder - sessionBuilder taskSessionBuilder - settings *settings + emitter *taskEmitterWrapper + newKVStore func(path string) (model.KeyValueStore, error) + newSession func(ctx context.Context, config engine.SessionConfig) (taskSession, error) + settings *settings } var _ taskRunner = &runnerForTask{} @@ -26,10 +27,20 @@ var _ taskRunner = &runnerForTask{} // newRunner creates a new task runner func newRunner(settings *settings, emitter taskEmitter) *runnerForTask { return &runnerForTask{ - emitter: &taskEmitterWrapper{emitter}, - kvStoreBuilder: &taskKVStoreFSBuilderEngine{}, - sessionBuilder: &taskSessionBuilderEngine{}, - settings: settings, + emitter: &taskEmitterWrapper{emitter}, + newKVStore: func(path string) (model.KeyValueStore, error) { + // Note that we will return a non-nil model.KeyValueStore even if the + // kvstore.NewFS factory returns a nil *kvstore.FS because of how golang + // converts between nil types. Because we're checking the error and + // acting upon it, it is not a big deal. + return kvstore.NewFS(path) + }, + newSession: func(ctx context.Context, config engine.SessionConfig) (taskSession, error) { + // Same note as above: the returned session is not nil even when the + // factory returns a nil *engine.Session because of golang nil conversion. + return engine.NewSession(ctx, config) + }, + settings: settings, } } @@ -45,7 +56,7 @@ func (r *runnerForTask) hasUnsupportedSettings() bool { } func (r *runnerForTask) newsession(ctx context.Context, logger model.Logger) (taskSession, error) { - kvstore, err := r.kvStoreBuilder.NewFS(r.settings.StateDir) + kvstore, err := r.newKVStore(r.settings.StateDir) if err != nil { return nil, err } @@ -74,13 +85,13 @@ func (r *runnerForTask) newsession(ctx context.Context, logger model.Logger) (ta Address: r.settings.Options.ProbeServicesBaseURL, }} } - return r.sessionBuilder.NewSession(ctx, config) + return r.newSession(ctx, config) } // contextForExperiment ensurs that for measuring we only use an // interruptible context when we can interrupt the experiment func (r *runnerForTask) contextForExperiment( - ctx context.Context, builder taskExperimentBuilder, + ctx context.Context, builder model.ExperimentBuilder, ) context.Context { if builder.Interruptible() { return ctx @@ -132,7 +143,7 @@ func (r *runnerForTask) Run(rootCtx context.Context) { r.emitter.Emit(eventTypeStatusEnd, endEvent) }() - builder, err := sess.NewExperimentBuilderByName(r.settings.Name) + builder, err := sess.NewExperimentBuilder(r.settings.Name) if err != nil { r.emitter.EmitFailureStartup(err.Error()) return @@ -210,7 +221,7 @@ func (r *runnerForTask) Run(rootCtx context.Context) { } r.settings.Inputs = append(r.settings.Inputs, "") } - experiment := builder.NewExperimentInstance() + experiment := builder.NewExperiment() defer func() { endEvent.DownloadedKB = experiment.KibiBytesReceived() endEvent.UploadedKB = experiment.KibiBytesSent() diff --git a/pkg/oonimkall/taskrunner_test.go b/pkg/oonimkall/taskrunner_test.go index d7840e8f12..f3fc57f4a5 100644 --- a/pkg/oonimkall/taskrunner_test.go +++ b/pkg/oonimkall/taskrunner_test.go @@ -7,8 +7,10 @@ import ( "time" "github.com/google/go-cmp/cmp" - "github.com/ooni/probe-cli/v3/internal/targetloading" + "github.com/ooni/probe-cli/v3/internal/engine" + "github.com/ooni/probe-cli/v3/internal/mocks" "github.com/ooni/probe-cli/v3/internal/model" + "github.com/ooni/probe-cli/v3/internal/targetloading" ) func TestMeasurementSubmissionEventName(t *testing.T) { @@ -29,6 +31,19 @@ func TestMeasurementSubmissionFailure(t *testing.T) { } } +// MockableTaskRunnerDependencies is the mockable struct used by [TestTaskRunnerRun]. +type MockableTaskRunnerDependencies struct { + Builder *mocks.ExperimentBuilder + Experiment *mocks.Experiment + Loader *mocks.ExperimentTargetLoader + Session *mocks.Session +} + +// NewSession is the method that returns the new fake session. +func (dep *MockableTaskRunnerDependencies) NewSession(ctx context.Context, config engine.SessionConfig) (taskSession, error) { + return dep.Session, nil +} + func TestTaskRunnerRun(t *testing.T) { if testing.Short() { t.Skip("skip test in short mode") @@ -95,10 +110,8 @@ func TestTaskRunnerRun(t *testing.T) { t.Run("with failure when creating a new kvstore", func(t *testing.T) { runner, emitter := newRunnerForTesting() // override the kvstore builder to provoke an error - runner.kvStoreBuilder = &MockableKVStoreFSBuilder{ - MockNewFS: func(path string) (model.KeyValueStore, error) { - return nil, errors.New("generic error") - }, + runner.newKVStore = func(path string) (model.KeyValueStore, error) { + return nil, errors.New("generic error") } events := runAndCollect(runner, emitter) assertCountEventsByKey(events, eventTypeFailureStartup, 1) @@ -117,11 +130,14 @@ func TestTaskRunnerRun(t *testing.T) { runner.settings.Proxy = "https://127.0.0.1/" // set a fake session builder that causes the startup to // fail but records the config passed to NewSession - saver := &SessionBuilderConfigSaver{} - runner.sessionBuilder = saver + var savedConfig engine.SessionConfig + runner.newSession = func(ctx context.Context, config engine.SessionConfig) (taskSession, error) { + savedConfig = config + return nil, errors.New("generic error") + } events := runAndCollect(runner, emitter) assertCountEventsByKey(events, eventTypeFailureStartup, 1) - if saver.Config.ProxyURL.String() != runner.settings.Proxy { + if savedConfig.ProxyURL.String() != runner.settings.Proxy { t.Fatal("invalid proxy URL") } }) @@ -132,11 +148,14 @@ func TestTaskRunnerRun(t *testing.T) { runner.settings.Options.ProbeServicesBaseURL = "https://127.0.0.1" // set a fake session builder that causes the startup to // fail but records the config passed to NewSession - saver := &SessionBuilderConfigSaver{} - runner.sessionBuilder = saver + var savedConfig engine.SessionConfig + runner.newSession = func(ctx context.Context, config engine.SessionConfig) (taskSession, error) { + savedConfig = config + return nil, errors.New("generic error") + } events := runAndCollect(runner, emitter) assertCountEventsByKey(events, eventTypeFailureStartup, 1) - psu := saver.Config.AvailableProbeServices + psu := savedConfig.AvailableProbeServices if len(psu) != 1 { t.Fatal("invalid length") } @@ -182,65 +201,96 @@ func TestTaskRunnerRun(t *testing.T) { // fakeSuccessfulRun returns a new set of dependencies that // will perform a fully successful, but fake, run. + // + // You MAY override some functions to provoke specific errors + // or generally change the operating conditions. fakeSuccessfulRun := func() *MockableTaskRunnerDependencies { - return &MockableTaskRunnerDependencies{ - MockableKibiBytesReceived: func() float64 { - return 10 - }, - MockableKibiBytesSent: func() float64 { - return 4 - }, - MockableOpenReportContext: func(ctx context.Context) error { - return nil - }, - MockableReportID: func() string { - return "20211202T074907Z_example_IT_30722_n1_axDLHNUfJaV1IbuU" - }, - MockableMeasureWithContext: func(ctx context.Context, target model.ExperimentTarget) (*model.Measurement, error) { - return &model.Measurement{}, nil - }, - MockableSubmitAndUpdateMeasurementContext: func(ctx context.Context, measurement *model.Measurement) error { - return nil - }, - MockableSetCallbacks: func(callbacks model.ExperimentCallbacks) { - }, - MockableInputPolicy: func() model.InputPolicy { - return model.InputNone - }, - MockableInterruptible: func() bool { - return false + deps := &MockableTaskRunnerDependencies{ + + // Configure the fake experiment + Experiment: &mocks.Experiment{ + MockKibiBytesReceived: func() float64 { + return 10 + }, + MockKibiBytesSent: func() float64 { + return 4 + }, + MockOpenReportContext: func(ctx context.Context) error { + return nil + }, + MockReportID: func() string { + return "20211202T074907Z_example_IT_30722_n1_axDLHNUfJaV1IbuU" + }, + MockMeasureWithContext: func(ctx context.Context, target model.ExperimentTarget) (*model.Measurement, error) { + return &model.Measurement{}, nil + }, + MockSubmitAndUpdateMeasurementContext: func(ctx context.Context, measurement *model.Measurement) error { + return nil + }, }, - MockClose: func() error { - return nil - }, - MockMaybeLookupBackendsContext: func(ctx context.Context) error { - return nil - }, - MockMaybeLookupLocationContext: func(ctx context.Context) error { - return nil - }, - MockProbeIP: func() string { - return "130.192.91.211" - }, - MockProbeASNString: func() string { - return "AS137" - }, - MockProbeCC: func() string { - return "IT" - }, - MockProbeNetworkName: func() string { - return "GARR" - }, - MockResolverASNString: func() string { - return "AS137" - }, - MockResolverIP: func() string { - return "130.192.3.24" + + // Configure the fake experiment builder + Builder: &mocks.ExperimentBuilder{ + MockSetCallbacks: func(callbacks model.ExperimentCallbacks) {}, + MockInputPolicy: func() model.InputPolicy { + return model.InputNone + }, + MockInterruptible: func() bool { + return false + }, }, - MockResolverNetworkName: func() string { - return "GARR" + + // Configure the fake session + Session: &mocks.Session{ + MockClose: func() error { + return nil + }, + MockMaybeLookupBackendsContext: func(ctx context.Context) error { + return nil + }, + MockMaybeLookupLocationContext: func(ctx context.Context) error { + return nil + }, + MockProbeIP: func() string { + return "130.192.91.211" + }, + MockProbeASNString: func() string { + return "AS137" + }, + MockProbeCC: func() string { + return "IT" + }, + MockProbeNetworkName: func() string { + return "GARR" + }, + MockResolverASNString: func() string { + return "AS137" + }, + MockResolverIP: func() string { + return "130.192.3.24" + }, + MockResolverNetworkName: func() string { + return "GARR" + }, }, } + + // The fake session MUST return the fake experiment builder + deps.Session.MockNewExperimentBuilder = func(name string) (model.ExperimentBuilder, error) { + return deps.Builder, nil + } + + // The fake experiment builder MUST return the fake target loader + deps.Builder.MockNewTargetLoader = func(config *model.ExperimentTargetLoaderConfig) model.ExperimentTargetLoader { + return deps.Loader + } + + // The fake experiment builder MUST return the fake experiment + deps.Builder.MockNewExperiment = func() model.Experiment { + return deps.Experiment + } + + return deps } assertReducedEventsLike := func(t *testing.T, expected, got []eventKeyCount) { @@ -252,10 +302,10 @@ func TestTaskRunnerRun(t *testing.T) { t.Run("with invalid experiment name", func(t *testing.T) { runner, emitter := newRunnerForTesting() fake := fakeSuccessfulRun() - fake.MockNewExperimentBuilderByName = func(name string) (taskExperimentBuilder, error) { + fake.Session.MockNewExperimentBuilder = func(name string) (model.ExperimentBuilder, error) { return nil, errors.New("invalid experiment name") } - runner.sessionBuilder = fake + runner.newSession = fake.NewSession events := runAndCollect(runner, emitter) reduced := reduceEventsKeysIgnoreLog(events) expect := []eventKeyCount{ @@ -270,10 +320,10 @@ func TestTaskRunnerRun(t *testing.T) { t.Run("with error during backends lookup", func(t *testing.T) { runner, emitter := newRunnerForTesting() fake := fakeSuccessfulRun() - fake.MockMaybeLookupBackendsContext = func(ctx context.Context) error { + fake.Session.MockMaybeLookupBackendsContext = func(ctx context.Context) error { return errors.New("mocked error") } - runner.sessionBuilder = fake + runner.newSession = fake.NewSession events := runAndCollect(runner, emitter) reduced := reduceEventsKeysIgnoreLog(events) expect := []eventKeyCount{ @@ -288,10 +338,10 @@ func TestTaskRunnerRun(t *testing.T) { t.Run("with error during location lookup", func(t *testing.T) { runner, emitter := newRunnerForTesting() fake := fakeSuccessfulRun() - fake.MockMaybeLookupLocationContext = func(ctx context.Context) error { + fake.Session.MockMaybeLookupLocationContext = func(ctx context.Context) error { return errors.New("mocked error") } - runner.sessionBuilder = fake + runner.newSession = fake.NewSession events := runAndCollect(runner, emitter) reduced := reduceEventsKeysIgnoreLog(events) expect := []eventKeyCount{ @@ -310,10 +360,10 @@ 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() model.InputPolicy { + fake.Builder.MockInputPolicy = func() model.InputPolicy { return model.InputOrQueryBackend } - runner.sessionBuilder = fake + runner.newSession = fake.NewSession events := runAndCollect(runner, emitter) reduced := reduceEventsKeysIgnoreLog(events) expect := []eventKeyCount{ @@ -331,10 +381,10 @@ 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() model.InputPolicy { + fake.Builder.MockInputPolicy = func() model.InputPolicy { return model.InputStrictlyRequired } - runner.sessionBuilder = fake + runner.newSession = fake.NewSession events := runAndCollect(runner, emitter) reduced := reduceEventsKeysIgnoreLog(events) expect := []eventKeyCount{ @@ -354,10 +404,10 @@ func TestTaskRunnerRun(t *testing.T) { runner, emitter := newRunnerForTesting() runner.settings.Name = "Antani" // no input for this experiment fake := fakeSuccessfulRun() - fake.MockableInputPolicy = func() model.InputPolicy { + fake.Builder.MockInputPolicy = func() model.InputPolicy { return model.InputOrStaticDefault } - runner.sessionBuilder = fake + runner.newSession = fake.NewSession events := runAndCollect(runner, emitter) reduced := reduceEventsKeysIgnoreLog(events) expect := []eventKeyCount{ @@ -376,10 +426,10 @@ func TestTaskRunnerRun(t *testing.T) { runner, emitter := newRunnerForTesting() runner.settings.Inputs = append(runner.settings.Inputs, "https://x.org/") fake := fakeSuccessfulRun() - fake.MockableInputPolicy = func() model.InputPolicy { + fake.Builder.MockInputPolicy = func() model.InputPolicy { return model.InputNone } - runner.sessionBuilder = fake + runner.newSession = fake.NewSession events := runAndCollect(runner, emitter) reduced := reduceEventsKeysIgnoreLog(events) expect := []eventKeyCount{ @@ -397,10 +447,10 @@ func TestTaskRunnerRun(t *testing.T) { t.Run("with failure opening report", func(t *testing.T) { runner, emitter := newRunnerForTesting() fake := fakeSuccessfulRun() - fake.MockableOpenReportContext = func(ctx context.Context) error { + fake.Experiment.MockOpenReportContext = func(ctx context.Context) error { return errors.New("mocked error") } - runner.sessionBuilder = fake + runner.newSession = fake.NewSession events := runAndCollect(runner, emitter) reduced := reduceEventsKeysIgnoreLog(events) expect := []eventKeyCount{ @@ -418,10 +468,10 @@ func TestTaskRunnerRun(t *testing.T) { t.Run("with success and InputNone policy", func(t *testing.T) { runner, emitter := newRunnerForTesting() fake := fakeSuccessfulRun() - fake.MockableInputPolicy = func() model.InputPolicy { + fake.Builder.MockInputPolicy = func() model.InputPolicy { return model.InputNone } - runner.sessionBuilder = fake + runner.newSession = fake.NewSession events := runAndCollect(runner, emitter) reduced := reduceEventsKeysIgnoreLog(events) expect := []eventKeyCount{ @@ -444,13 +494,13 @@ 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() model.InputPolicy { + fake.Builder.MockInputPolicy = func() model.InputPolicy { return model.InputNone } - fake.MockableMeasureWithContext = func(ctx context.Context, target model.ExperimentTarget) (measurement *model.Measurement, err error) { + fake.Experiment.MockMeasureWithContext = func(ctx context.Context, target model.ExperimentTarget) (measurement *model.Measurement, err error) { return nil, errors.New("preconditions error") } - runner.sessionBuilder = fake + runner.newSession = fake.NewSession events := runAndCollect(runner, emitter) reduced := reduceEventsKeysIgnoreLog(events) expect := []eventKeyCount{ @@ -474,13 +524,13 @@ func TestTaskRunnerRun(t *testing.T) { // which is what was happening in the above referenced issue. runner, emitter := newRunnerForTesting() fake := fakeSuccessfulRun() - fake.MockableInputPolicy = func() model.InputPolicy { + fake.Builder.MockInputPolicy = func() model.InputPolicy { return model.InputNone } - fake.MockableMeasureWithContext = func(ctx context.Context, target model.ExperimentTarget) (measurement *model.Measurement, err error) { + fake.Experiment.MockMeasureWithContext = func(ctx context.Context, target model.ExperimentTarget) (measurement *model.Measurement, err error) { return nil, errors.New("preconditions error") } - runner.sessionBuilder = fake + runner.newSession = fake.NewSession runner.settings.Annotations = map[string]string{ "architecture": "arm64", } @@ -505,10 +555,10 @@ func TestTaskRunnerRun(t *testing.T) { runner, emitter := newRunnerForTesting() runner.settings.Inputs = []string{"a", "b", "c", "d"} fake := fakeSuccessfulRun() - fake.MockableInputPolicy = func() model.InputPolicy { + fake.Builder.MockInputPolicy = func() model.InputPolicy { return model.InputStrictlyRequired } - runner.sessionBuilder = fake + runner.newSession = fake.NewSession events := runAndCollect(runner, emitter) reduced := reduceEventsKeysIgnoreLog(events) expect := []eventKeyCount{ @@ -553,10 +603,10 @@ func TestTaskRunnerRun(t *testing.T) { runner, emitter := newRunnerForTesting() runner.settings.Inputs = []string{"a", "b", "c", "d"} fake := fakeSuccessfulRun() - fake.MockableInputPolicy = func() model.InputPolicy { + fake.Builder.MockInputPolicy = func() model.InputPolicy { return model.InputOptional } - runner.sessionBuilder = fake + runner.newSession = fake.NewSession events := runAndCollect(runner, emitter) reduced := reduceEventsKeysIgnoreLog(events) expect := []eventKeyCount{ @@ -600,10 +650,10 @@ 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() model.InputPolicy { + fake.Builder.MockInputPolicy = func() model.InputPolicy { return model.InputOptional } - runner.sessionBuilder = fake + runner.newSession = fake.NewSession events := runAndCollect(runner, emitter) reduced := reduceEventsKeysIgnoreLog(events) expect := []eventKeyCount{ @@ -630,10 +680,10 @@ func TestTaskRunnerRun(t *testing.T) { runner, emitter := newRunnerForTesting() runner.settings.Name = experimentName fake := fakeSuccessfulRun() - fake.MockableInputPolicy = func() model.InputPolicy { + fake.Builder.MockInputPolicy = func() model.InputPolicy { return model.InputOrStaticDefault } - runner.sessionBuilder = fake + runner.newSession = fake.NewSession events := runAndCollect(runner, emitter) reduced := reduceEventsKeysIgnoreLog(events) expect := []eventKeyCount{ @@ -666,14 +716,14 @@ func TestTaskRunnerRun(t *testing.T) { runner.settings.Inputs = []string{"a", "b", "c", "d"} runner.settings.Options.MaxRuntime = 2 fake := fakeSuccessfulRun() - fake.MockableInputPolicy = func() model.InputPolicy { + fake.Builder.MockInputPolicy = func() model.InputPolicy { return model.InputStrictlyRequired } - fake.MockableMeasureWithContext = func(ctx context.Context, target model.ExperimentTarget) (measurement *model.Measurement, err error) { + fake.Experiment.MockMeasureWithContext = func(ctx context.Context, target model.ExperimentTarget) (measurement *model.Measurement, err error) { time.Sleep(1 * time.Second) return &model.Measurement{}, nil } - runner.sessionBuilder = fake + runner.newSession = fake.NewSession events := runAndCollect(runner, emitter) reduced := reduceEventsKeysIgnoreLog(events) expect := []eventKeyCount{ @@ -707,18 +757,18 @@ func TestTaskRunnerRun(t *testing.T) { runner.settings.Inputs = []string{"a", "b", "c", "d"} runner.settings.Options.MaxRuntime = 2 fake := fakeSuccessfulRun() - fake.MockableInputPolicy = func() model.InputPolicy { + fake.Builder.MockInputPolicy = func() model.InputPolicy { return model.InputStrictlyRequired } - fake.MockableInterruptible = func() bool { + fake.Builder.MockInterruptible = func() bool { return true } ctx, cancel := context.WithCancel(context.Background()) - fake.MockableMeasureWithContext = func(ctx context.Context, target model.ExperimentTarget) (measurement *model.Measurement, err error) { + fake.Experiment.MockMeasureWithContext = func(ctx context.Context, target model.ExperimentTarget) (measurement *model.Measurement, err error) { cancel() return &model.Measurement{}, nil } - runner.sessionBuilder = fake + runner.newSession = fake.NewSession events := runAndCollectContext(ctx, runner, emitter) reduced := reduceEventsKeysIgnoreLog(events) expect := []eventKeyCount{ @@ -742,13 +792,13 @@ func TestTaskRunnerRun(t *testing.T) { runner, emitter := newRunnerForTesting() runner.settings.Inputs = []string{"a"} fake := fakeSuccessfulRun() - fake.MockableInputPolicy = func() model.InputPolicy { + fake.Builder.MockInputPolicy = func() model.InputPolicy { return model.InputStrictlyRequired } - fake.MockableSubmitAndUpdateMeasurementContext = func(ctx context.Context, measurement *model.Measurement) error { + fake.Experiment.MockSubmitAndUpdateMeasurementContext = func(ctx context.Context, measurement *model.Measurement) error { return errors.New("cannot submit") } - runner.sessionBuilder = fake + runner.newSession = fake.NewSession events := runAndCollect(runner, emitter) reduced := reduceEventsKeysIgnoreLog(events) expect := []eventKeyCount{ @@ -775,14 +825,14 @@ func TestTaskRunnerRun(t *testing.T) { runner, emitter := newRunnerForTesting() fake := fakeSuccessfulRun() var callbacks model.ExperimentCallbacks - fake.MockableSetCallbacks = func(cbs model.ExperimentCallbacks) { + fake.Builder.MockSetCallbacks = func(cbs model.ExperimentCallbacks) { callbacks = cbs } - fake.MockableMeasureWithContext = func(ctx context.Context, target model.ExperimentTarget) (measurement *model.Measurement, err error) { + fake.Experiment.MockMeasureWithContext = func(ctx context.Context, target model.ExperimentTarget) (measurement *model.Measurement, err error) { callbacks.OnProgress(1, "hello from the fake experiment") return &model.Measurement{}, nil } - runner.sessionBuilder = fake + runner.newSession = fake.NewSession events := runAndCollect(runner, emitter) reduced := reduceEventsKeysIgnoreLog(events) expect := []eventKeyCount{ diff --git a/pkg/oonimkall/tasksession.go b/pkg/oonimkall/tasksession.go deleted file mode 100644 index 27b1dd6192..0000000000 --- a/pkg/oonimkall/tasksession.go +++ /dev/null @@ -1,78 +0,0 @@ -package oonimkall - -import ( - "context" - - "github.com/ooni/probe-cli/v3/internal/engine" - "github.com/ooni/probe-cli/v3/internal/kvstore" - "github.com/ooni/probe-cli/v3/internal/model" -) - -// -// This file implements taskSession and derived types. -// - -// taskKVStoreFSBuilderEngine creates a new KVStore -// using the ./internal/engine package. -type taskKVStoreFSBuilderEngine struct{} - -var _ taskKVStoreFSBuilder = &taskKVStoreFSBuilderEngine{} - -func (*taskKVStoreFSBuilderEngine) NewFS(path string) (model.KeyValueStore, error) { - return kvstore.NewFS(path) -} - -// taskSessionBuilderEngine builds a new session -// using the ./internal/engine package. -type taskSessionBuilderEngine struct{} - -var _ taskSessionBuilder = &taskSessionBuilderEngine{} - -// NewSession implements taskSessionBuilder.NewSession. -func (b *taskSessionBuilderEngine) NewSession(ctx context.Context, - config engine.SessionConfig) (taskSession, error) { - sess, err := engine.NewSession(ctx, config) - if err != nil { - return nil, err - } - return &taskSessionEngine{sess}, nil -} - -// taskSessionEngine wraps ./internal/engine's Session. -type taskSessionEngine struct { - *engine.Session -} - -var _ taskSession = &taskSessionEngine{} - -// NewExperimentBuilderByName implements -// taskSessionEngine.NewExperimentBuilderByName. -func (sess *taskSessionEngine) NewExperimentBuilderByName( - name string) (taskExperimentBuilder, error) { - builder, err := sess.NewExperimentBuilder(name) - if err != nil { - return nil, err - } - return &taskExperimentBuilderEngine{builder}, err -} - -// taskExperimentBuilderEngine wraps ./internal/engine's -// ExperimentBuilder type. -type taskExperimentBuilderEngine struct { - model.ExperimentBuilder -} - -var _ taskExperimentBuilder = &taskExperimentBuilderEngine{} - -// NewExperimentInstance implements -// taskExperimentBuilder.NewExperimentInstance. -func (b *taskExperimentBuilderEngine) NewExperimentInstance() taskExperiment { - return &taskExperimentEngine{b.NewExperiment()} -} - -// taskExperimentEngine wraps ./internal/engine's Experiment. -type taskExperimentEngine struct { - model.Experiment -} - -var _ taskExperiment = &taskExperimentEngine{} diff --git a/pkg/oonimkall/tasksession_test.go b/pkg/oonimkall/tasksession_test.go deleted file mode 100644 index f52549105d..0000000000 --- a/pkg/oonimkall/tasksession_test.go +++ /dev/null @@ -1,128 +0,0 @@ -package oonimkall - -import ( - "context" - "testing" - - "github.com/apex/log" - "github.com/ooni/probe-cli/v3/internal/engine" - "github.com/ooni/probe-cli/v3/internal/version" -) - -func TestTaskKVSToreFSBuilderEngine(t *testing.T) { - b := &taskKVStoreFSBuilderEngine{} - store, err := b.NewFS("testdata/state") - if err != nil { - t.Fatal(err) - } - if store == nil { - t.Fatal("expected non-nil store here") - } -} - -func TestTaskSessionBuilderEngine(t *testing.T) { - t.Run("NewSession", func(t *testing.T) { - t.Run("on success", func(t *testing.T) { - builder := &taskSessionBuilderEngine{} - ctx := context.Background() - config := engine.SessionConfig{ - Logger: log.Log, - SoftwareName: "ooniprobe-cli", - SoftwareVersion: version.Version, - } - sess, err := builder.NewSession(ctx, config) - if err != nil { - t.Fatal(err) - } - sess.Close() - }) - - t.Run("on failure", func(t *testing.T) { - builder := &taskSessionBuilderEngine{} - ctx := context.Background() - config := engine.SessionConfig{} - sess, err := builder.NewSession(ctx, config) - if err == nil { - t.Fatal("expected an error here") - } - if sess != nil { - t.Fatal("expected nil session here") - } - }) - }) -} - -func TestTaskSessionEngine(t *testing.T) { - - // newSession is a helper function for creating a new session. - newSession := func(t *testing.T) taskSession { - builder := &taskSessionBuilderEngine{} - ctx := context.Background() - config := engine.SessionConfig{ - Logger: log.Log, - SoftwareName: "ooniprobe-cli", - SoftwareVersion: version.Version, - } - sess, err := builder.NewSession(ctx, config) - if err != nil { - t.Fatal(err) - } - return sess - } - - t.Run("NewExperimentBuilderByName", func(t *testing.T) { - t.Run("on success", func(t *testing.T) { - sess := newSession(t) - builder, err := sess.NewExperimentBuilderByName("ndt") - if err != nil { - t.Fatal(err) - } - if builder == nil { - t.Fatal("expected non-nil builder") - } - }) - - t.Run("on failure", func(t *testing.T) { - sess := newSession(t) - builder, err := sess.NewExperimentBuilderByName("antani") - if err == nil { - t.Fatal("expected an error here") - } - if builder != nil { - t.Fatal("expected nil builder") - } - }) - }) -} - -func TestTaskExperimentBuilderEngine(t *testing.T) { - - // newBuilder is a helper function for creating a new session - // as well as a new experiment builder - newBuilder := func(t *testing.T) (taskSession, taskExperimentBuilder) { - builder := &taskSessionBuilderEngine{} - ctx := context.Background() - config := engine.SessionConfig{ - Logger: log.Log, - SoftwareName: "ooniprobe-cli", - SoftwareVersion: version.Version, - } - sess, err := builder.NewSession(ctx, config) - if err != nil { - t.Fatal(err) - } - expBuilder, err := sess.NewExperimentBuilderByName("ndt") - if err != nil { - t.Fatal(err) - } - return sess, expBuilder - } - - t.Run("NewExperiment", func(t *testing.T) { - _, builder := newBuilder(t) - exp := builder.NewExperimentInstance() - if exp == nil { - t.Fatal("expected non-nil experiment here") - } - }) -} From c4f28d2b796a1ff032810f47eb1116b89cb37db6 Mon Sep 17 00:00:00 2001 From: Simone Basso Date: Fri, 7 Jun 2024 07:59:27 +0200 Subject: [PATCH 18/63] x --- internal/experiment/example/example.go | 13 +++++++------ internal/experiment/example/example_test.go | 18 +++++++++++++----- internal/registry/example.go | 7 +------ pkg/oonimkall/taskmocks_test.go | 4 +--- pkg/oonimkall/taskrunner_test.go | 4 ---- 5 files changed, 22 insertions(+), 24 deletions(-) diff --git a/internal/experiment/example/example.go b/internal/experiment/example/example.go index 83ffbe90d4..9fcd03d80d 100644 --- a/internal/experiment/example/example.go +++ b/internal/experiment/example/example.go @@ -14,6 +14,8 @@ import ( const testVersion = "0.1.0" +const testName = "example" + // Config contains the experiment config. // // This contains all the settings that user can set to modify the behaviour @@ -22,7 +24,7 @@ const testVersion = "0.1.0" type Config struct { Message string `ooni:"Message to emit at test completion"` ReturnError bool `ooni:"Toogle to return a mocked error"` - SleepTime int64 `ooni:"Amount of time to sleep for"` + SleepTime int64 `ooni:"Amount of time to sleep for in nanosecond"` } // TestKeys contains the experiment's result. @@ -38,13 +40,12 @@ type TestKeys struct { // Measurer performs the measurement. type Measurer struct { - config Config - testName string + config Config } // ExperimentName implements model.ExperimentMeasurer.ExperimentName. func (m Measurer) ExperimentName() string { - return m.testName + return testName } // ExperimentVersion implements model.ExperimentMeasurer.ExperimentVersion. @@ -81,6 +82,6 @@ func (m Measurer) Run(ctx context.Context, args *model.ExperimentArgs) error { } // NewExperimentMeasurer creates a new ExperimentMeasurer. -func NewExperimentMeasurer(config Config, testName string) model.ExperimentMeasurer { - return Measurer{config: config, testName: testName} +func NewExperimentMeasurer(config Config) model.ExperimentMeasurer { + return Measurer{config: config} } diff --git a/internal/experiment/example/example_test.go b/internal/experiment/example/example_test.go index b4a66aafc1..364fb3491b 100644 --- a/internal/experiment/example/example_test.go +++ b/internal/experiment/example/example_test.go @@ -8,14 +8,14 @@ import ( "github.com/apex/log" "github.com/ooni/probe-cli/v3/internal/experiment/example" - "github.com/ooni/probe-cli/v3/internal/legacy/mockable" + "github.com/ooni/probe-cli/v3/internal/mocks" "github.com/ooni/probe-cli/v3/internal/model" ) func TestSuccess(t *testing.T) { m := example.NewExperimentMeasurer(example.Config{ SleepTime: int64(2 * time.Millisecond), - }, "example") + }) if m.ExperimentName() != "example" { t.Fatal("invalid ExperimentName") } @@ -23,7 +23,11 @@ func TestSuccess(t *testing.T) { t.Fatal("invalid ExperimentVersion") } ctx := context.Background() - sess := &mockable.Session{MockableLogger: log.Log} + sess := &mocks.Session{ + MockLogger: func() model.Logger { + return log.Log + }, + } callbacks := model.NewPrinterCallbacks(sess.Logger()) measurement := new(model.Measurement) args := &model.ExperimentArgs{ @@ -41,9 +45,13 @@ func TestFailure(t *testing.T) { m := example.NewExperimentMeasurer(example.Config{ SleepTime: int64(2 * time.Millisecond), ReturnError: true, - }, "example") + }) ctx := context.Background() - sess := &mockable.Session{MockableLogger: log.Log} + sess := &mocks.Session{ + MockLogger: func() model.Logger { + return log.Log + }, + } callbacks := model.NewPrinterCallbacks(sess.Logger()) args := &model.ExperimentArgs{ Callbacks: callbacks, diff --git a/internal/registry/example.go b/internal/registry/example.go index 9bcb5459bb..4d1db07896 100644 --- a/internal/registry/example.go +++ b/internal/registry/example.go @@ -14,15 +14,10 @@ import ( func init() { const canonicalName = "example" AllExperiments[canonicalName] = func() *Factory { - // TODO(bassosimone,DecFox): as pointed out by @ainghazal, this experiment - // should be the one that people modify to start out new experiments, so it's - // kind of suboptimal that it has a constructor with explicit experiment - // name to ease writing some tests that ./pkg/oonimkall needs given that no - // other experiment ever sets the experiment name externally! return &Factory{ build: func(config interface{}) model.ExperimentMeasurer { return example.NewExperimentMeasurer( - *config.(*example.Config), "example", + *config.(*example.Config), ) }, canonicalName: canonicalName, diff --git a/pkg/oonimkall/taskmocks_test.go b/pkg/oonimkall/taskmocks_test.go index 9b03386b85..b91e64097c 100644 --- a/pkg/oonimkall/taskmocks_test.go +++ b/pkg/oonimkall/taskmocks_test.go @@ -5,9 +5,7 @@ package oonimkall // we only use mocks when testing, this file is a `_test.go` file. // -import ( - "sync" -) +import "sync" // CollectorTaskEmitter is a thread-safe taskEmitter // that stores all the events inside itself. diff --git a/pkg/oonimkall/taskrunner_test.go b/pkg/oonimkall/taskrunner_test.go index f3fc57f4a5..4d503f6f54 100644 --- a/pkg/oonimkall/taskrunner_test.go +++ b/pkg/oonimkall/taskrunner_test.go @@ -45,10 +45,6 @@ func (dep *MockableTaskRunnerDependencies) NewSession(ctx context.Context, confi } func TestTaskRunnerRun(t *testing.T) { - if testing.Short() { - t.Skip("skip test in short mode") - } - // newRunnerForTesting is a factory for creating a new // runner that wraps newRunner and also sets a specific // taskSessionBuilder for testing purposes. From a6bdeb792c732ef24c380e42c81f39c404d5b49e Mon Sep 17 00:00:00 2001 From: Simone Basso Date: Fri, 7 Jun 2024 08:45:10 +0200 Subject: [PATCH 19/63] x --- pkg/oonimkall/taskrunner.go | 116 +++++------- pkg/oonimkall/taskrunner_test.go | 297 ++++++++----------------------- 2 files changed, 118 insertions(+), 295 deletions(-) diff --git a/pkg/oonimkall/taskrunner.go b/pkg/oonimkall/taskrunner.go index 000532e25a..c53b6458d9 100644 --- a/pkg/oonimkall/taskrunner.go +++ b/pkg/oonimkall/taskrunner.go @@ -11,7 +11,6 @@ import ( "github.com/ooni/probe-cli/v3/internal/kvstore" "github.com/ooni/probe-cli/v3/internal/model" "github.com/ooni/probe-cli/v3/internal/runtimex" - "github.com/ooni/probe-cli/v3/internal/targetloading" ) // runnerForTask runs a specific task @@ -180,47 +179,23 @@ func (r *runnerForTask) Run(rootCtx context.Context) { builder.SetCallbacks(&runnerCallbacks{emitter: r.emitter}) - // TODO(bassosimone): replace the following code with an - // invocation of the targetloading.Loader. Since I am making these - // changes before a release and I've already changed the - // code a lot, I'd rather avoid changing it even more, - // for the following reason: - // - // If we add and call targetloading.Loader here, this code will - // magically invoke check-in for InputOrQueryBackend, - // which we need to make sure the app can handle. This is - // the main reason why now I don't fill like properly - // fixing this code and use targetloading.Loader: too much work - // in too little time, so mistakes more likely. - // - // In fact, our current app assumes that it's its - // responsibility to load the inputs, not oonimkall's. - switch builder.InputPolicy() { - case model.InputOrQueryBackend, model.InputStrictlyRequired: - if len(r.settings.Inputs) <= 0 { - r.emitter.EmitFailureStartup("no input provided") - return - } - case model.InputOrStaticDefault: - if len(r.settings.Inputs) <= 0 { - inputs, err := targetloading.StaticBareInputForExperiment(r.settings.Name) - if err != nil { - r.emitter.EmitFailureStartup("no default static input for this experiment") - return - } - r.settings.Inputs = inputs - } - case model.InputOptional: - if len(r.settings.Inputs) <= 0 { - r.settings.Inputs = append(r.settings.Inputs, "") - } - default: // treat this case as engine.InputNone. - if len(r.settings.Inputs) > 0 { - r.emitter.EmitFailureStartup("experiment does not accept input") - return - } - r.settings.Inputs = append(r.settings.Inputs, "") + // Load targets. Note that, for Web Connectivity, the mobile app has + // already loaded inputs and provides them as r.settings.Inputs. + loader := builder.NewTargetLoader(&model.ExperimentTargetLoaderConfig{ + CheckInConfig: &model.OOAPICheckInConfig{ + // Not needed since the app already provides the + // inputs to use for Web Connectivity. + }, + Session: sess, + StaticInputs: r.settings.Inputs, + SourceFiles: []string{}, + }) + targets, err := loader.Load(rootCtx) + if err != nil { + r.emitter.EmitFailureStartup(err.Error()) + return } + experiment := builder.NewExperiment() defer func() { endEvent.DownloadedKB = experiment.KibiBytesReceived() @@ -241,48 +216,43 @@ func (r *runnerForTask) Run(rootCtx context.Context) { defer measCancel() submitCtx, submitCancel := context.WithCancel(rootCtx) defer submitCancel() + // This deviates a little bit from measurement-kit, for which // a zero timeout is actually valid. Since it does not make much // sense, here we're changing the behaviour. // // See https://github.com/measurement-kit/measurement-kit/issues/1922 - if r.settings.Options.MaxRuntime > 0 { - // We want to honour max_runtime only when we're running an - // experiment that clearly wants specific input. We could refine - // 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 model.InputOrQueryBackend, model.InputStrictlyRequired: - var ( - cancelMeas context.CancelFunc - cancelSubmit context.CancelFunc - ) - // We give the context used for submitting extra time so that - // it's possible to submit the last measurement. - // - // See https://github.com/ooni/probe/issues/2037 for more info. - maxRuntime := time.Duration(r.settings.Options.MaxRuntime) * time.Second - measCtx, cancelMeas = context.WithTimeout(measCtx, maxRuntime) - defer cancelMeas() - maxRuntime += 30 * time.Second - submitCtx, cancelSubmit = context.WithTimeout(submitCtx, maxRuntime) - defer cancelSubmit() - } + if r.settings.Options.MaxRuntime > 0 && len(targets) > 1 { + var ( + cancelMeas context.CancelFunc + cancelSubmit context.CancelFunc + ) + // We give the context used for submitting extra time so that + // it's possible to submit the last measurement. + // + // See https://github.com/ooni/probe/issues/2037 for more info. + maxRuntime := time.Duration(r.settings.Options.MaxRuntime) * time.Second + measCtx, cancelMeas = context.WithTimeout(measCtx, maxRuntime) + defer cancelMeas() + maxRuntime += 30 * time.Second + submitCtx, cancelSubmit = context.WithTimeout(submitCtx, maxRuntime) + defer cancelSubmit() } - inputCount := len(r.settings.Inputs) + + inputCount := len(targets) start := time.Now() inflatedMaxRuntime := r.settings.Options.MaxRuntime + r.settings.Options.MaxRuntime/10 eta := start.Add(time.Duration(inflatedMaxRuntime) * time.Second) - for idx, input := range r.settings.Inputs { + for idx, target := range targets { if measCtx.Err() != nil { break } logger.Infof("Starting measurement with index %d", idx) r.emitter.Emit(eventTypeStatusMeasurementStart, eventMeasurementGeneric{ Idx: int64(idx), - Input: input, + Input: target.Input(), }) - if input != "" && inputCount > 0 { + if target.Input() != "" && inputCount > 0 { var percentage float64 if r.settings.Options.MaxRuntime > 0 { now := time.Now() @@ -291,7 +261,7 @@ func (r *runnerForTask) Run(rootCtx context.Context) { percentage = (float64(idx)/float64(inputCount))*0.6 + 0.4 } r.emitter.EmitStatusProgress(percentage, fmt.Sprintf( - "processing %s", input, + "processing %s", target, )) } @@ -305,7 +275,7 @@ func (r *runnerForTask) Run(rootCtx context.Context) { // we can always do, since it only has string accessors. m, err := experiment.MeasureWithContext( r.contextForExperiment(measCtx, builder), - model.NewOOAPIURLInfoWithDefaultCategoryAndCountry(input), + target, ) if builder.Interruptible() && measCtx.Err() != nil { @@ -317,7 +287,7 @@ func (r *runnerForTask) Run(rootCtx context.Context) { r.emitter.Emit(eventTypeFailureMeasurement, eventMeasurementGeneric{ Failure: err.Error(), Idx: int64(idx), - Input: input, + Input: target.Input(), }) // Historical note: here we used to fallthrough but, since we have // implemented async measurements, the case where there is an error @@ -330,7 +300,7 @@ func (r *runnerForTask) Run(rootCtx context.Context) { runtimex.PanicOnError(err, "measurement.MarshalJSON failed") r.emitter.Emit(eventTypeMeasurement, eventMeasurementGeneric{ Idx: int64(idx), - Input: input, + Input: target.Input(), JSONStr: string(data), }) if !r.settings.Options.NoCollector { @@ -339,14 +309,14 @@ func (r *runnerForTask) Run(rootCtx context.Context) { warnOnFailure(logger, "cannot submit measurement", err) r.emitter.Emit(measurementSubmissionEventName(err), eventMeasurementGeneric{ Idx: int64(idx), - Input: input, + Input: target.Input(), JSONStr: string(data), Failure: measurementSubmissionFailure(err), }) } r.emitter.Emit(eventTypeStatusMeasurementDone, eventMeasurementGeneric{ Idx: int64(idx), - Input: input, + Input: target.Input(), }) } } diff --git a/pkg/oonimkall/taskrunner_test.go b/pkg/oonimkall/taskrunner_test.go index 4d503f6f54..920b7b71ad 100644 --- a/pkg/oonimkall/taskrunner_test.go +++ b/pkg/oonimkall/taskrunner_test.go @@ -10,7 +10,6 @@ import ( "github.com/ooni/probe-cli/v3/internal/engine" "github.com/ooni/probe-cli/v3/internal/mocks" "github.com/ooni/probe-cli/v3/internal/model" - "github.com/ooni/probe-cli/v3/internal/targetloading" ) func TestMeasurementSubmissionEventName(t *testing.T) { @@ -173,9 +172,10 @@ func TestTaskRunnerRun(t *testing.T) { // reduceEventsKeysIgnoreLog reduces the list of event keys // counting equal subsequent keys and ignoring log events - reduceEventsKeysIgnoreLog := func(events []*event) (out []eventKeyCount) { + reduceEventsKeysIgnoreLog := func(t *testing.T, events []*event) (out []eventKeyCount) { var current eventKeyCount for _, ev := range events { + t.Log(ev) if ev.Key == eventTypeLog { continue } @@ -195,12 +195,12 @@ func TestTaskRunnerRun(t *testing.T) { return } - // fakeSuccessfulRun returns a new set of dependencies that + // fakeSuccessfulDeps returns a new set of dependencies that // will perform a fully successful, but fake, run. // // You MAY override some functions to provoke specific errors // or generally change the operating conditions. - fakeSuccessfulRun := func() *MockableTaskRunnerDependencies { + fakeSuccessfulDeps := func() *MockableTaskRunnerDependencies { deps := &MockableTaskRunnerDependencies{ // Configure the fake experiment @@ -269,6 +269,15 @@ func TestTaskRunnerRun(t *testing.T) { return "GARR" }, }, + + Loader: &mocks.ExperimentTargetLoader{ + MockLoad: func(ctx context.Context) ([]model.ExperimentTarget, error) { + targets := []model.ExperimentTarget{ + model.NewOOAPIURLInfoWithDefaultCategoryAndCountry(""), + } + return targets, nil + }, + }, } // The fake session MUST return the fake experiment builder @@ -297,13 +306,13 @@ func TestTaskRunnerRun(t *testing.T) { t.Run("with invalid experiment name", func(t *testing.T) { runner, emitter := newRunnerForTesting() - fake := fakeSuccessfulRun() + fake := fakeSuccessfulDeps() fake.Session.MockNewExperimentBuilder = func(name string) (model.ExperimentBuilder, error) { return nil, errors.New("invalid experiment name") } runner.newSession = fake.NewSession events := runAndCollect(runner, emitter) - reduced := reduceEventsKeysIgnoreLog(events) + reduced := reduceEventsKeysIgnoreLog(t, events) expect := []eventKeyCount{ {Key: eventTypeStatusQueued, Count: 1}, {Key: eventTypeStatusStarted, Count: 1}, @@ -315,13 +324,13 @@ func TestTaskRunnerRun(t *testing.T) { t.Run("with error during backends lookup", func(t *testing.T) { runner, emitter := newRunnerForTesting() - fake := fakeSuccessfulRun() + fake := fakeSuccessfulDeps() fake.Session.MockMaybeLookupBackendsContext = func(ctx context.Context) error { return errors.New("mocked error") } runner.newSession = fake.NewSession events := runAndCollect(runner, emitter) - reduced := reduceEventsKeysIgnoreLog(events) + reduced := reduceEventsKeysIgnoreLog(t, events) expect := []eventKeyCount{ {Key: eventTypeStatusQueued, Count: 1}, {Key: eventTypeStatusStarted, Count: 1}, @@ -333,13 +342,13 @@ func TestTaskRunnerRun(t *testing.T) { t.Run("with error during location lookup", func(t *testing.T) { runner, emitter := newRunnerForTesting() - fake := fakeSuccessfulRun() + fake := fakeSuccessfulDeps() fake.Session.MockMaybeLookupLocationContext = func(ctx context.Context) error { return errors.New("mocked error") } runner.newSession = fake.NewSession events := runAndCollect(runner, emitter) - reduced := reduceEventsKeysIgnoreLog(events) + reduced := reduceEventsKeysIgnoreLog(t, events) expect := []eventKeyCount{ {Key: eventTypeStatusQueued, Count: 1}, {Key: eventTypeStatusStarted, Count: 1}, @@ -353,81 +362,15 @@ func TestTaskRunnerRun(t *testing.T) { assertReducedEventsLike(t, expect, reduced) }) - t.Run("with missing input and InputOrQueryBackend policy", func(t *testing.T) { - runner, emitter := newRunnerForTesting() - fake := fakeSuccessfulRun() - fake.Builder.MockInputPolicy = func() model.InputPolicy { - return model.InputOrQueryBackend - } - runner.newSession = fake.NewSession - events := runAndCollect(runner, emitter) - reduced := reduceEventsKeysIgnoreLog(events) - expect := []eventKeyCount{ - {Key: eventTypeStatusQueued, Count: 1}, - {Key: eventTypeStatusStarted, Count: 1}, - {Key: eventTypeStatusProgress, Count: 3}, - {Key: eventTypeStatusGeoIPLookup, Count: 1}, - {Key: eventTypeStatusResolverLookup, Count: 1}, - {Key: eventTypeFailureStartup, Count: 1}, - {Key: eventTypeStatusEnd, Count: 1}, - } - assertReducedEventsLike(t, expect, reduced) - }) - - t.Run("with missing input and InputStrictlyRequired policy", func(t *testing.T) { + t.Run("with error during target loading", func(t *testing.T) { runner, emitter := newRunnerForTesting() - fake := fakeSuccessfulRun() - fake.Builder.MockInputPolicy = func() model.InputPolicy { - return model.InputStrictlyRequired + fake := fakeSuccessfulDeps() + fake.Loader.MockLoad = func(ctx context.Context) ([]model.ExperimentTarget, error) { + return nil, errors.New("mocked error") } runner.newSession = fake.NewSession events := runAndCollect(runner, emitter) - reduced := reduceEventsKeysIgnoreLog(events) - expect := []eventKeyCount{ - {Key: eventTypeStatusQueued, Count: 1}, - {Key: eventTypeStatusStarted, Count: 1}, - {Key: eventTypeStatusProgress, Count: 3}, - {Key: eventTypeStatusGeoIPLookup, Count: 1}, - {Key: eventTypeStatusResolverLookup, Count: 1}, - {Key: eventTypeFailureStartup, Count: 1}, - {Key: eventTypeStatusEnd, Count: 1}, - } - assertReducedEventsLike(t, expect, reduced) - }) - - t.Run("with InputOrStaticDefault policy and experiment with no static input", - func(t *testing.T) { - runner, emitter := newRunnerForTesting() - runner.settings.Name = "Antani" // no input for this experiment - fake := fakeSuccessfulRun() - fake.Builder.MockInputPolicy = func() model.InputPolicy { - return model.InputOrStaticDefault - } - runner.newSession = fake.NewSession - events := runAndCollect(runner, emitter) - reduced := reduceEventsKeysIgnoreLog(events) - expect := []eventKeyCount{ - {Key: eventTypeStatusQueued, Count: 1}, - {Key: eventTypeStatusStarted, Count: 1}, - {Key: eventTypeStatusProgress, Count: 3}, - {Key: eventTypeStatusGeoIPLookup, Count: 1}, - {Key: eventTypeStatusResolverLookup, Count: 1}, - {Key: eventTypeFailureStartup, Count: 1}, - {Key: eventTypeStatusEnd, Count: 1}, - } - assertReducedEventsLike(t, expect, reduced) - }) - - t.Run("with InputNone policy and provided input", func(t *testing.T) { - runner, emitter := newRunnerForTesting() - runner.settings.Inputs = append(runner.settings.Inputs, "https://x.org/") - fake := fakeSuccessfulRun() - fake.Builder.MockInputPolicy = func() model.InputPolicy { - return model.InputNone - } - runner.newSession = fake.NewSession - events := runAndCollect(runner, emitter) - reduced := reduceEventsKeysIgnoreLog(events) + reduced := reduceEventsKeysIgnoreLog(t, events) expect := []eventKeyCount{ {Key: eventTypeStatusQueued, Count: 1}, {Key: eventTypeStatusStarted, Count: 1}, @@ -442,13 +385,13 @@ func TestTaskRunnerRun(t *testing.T) { t.Run("with failure opening report", func(t *testing.T) { runner, emitter := newRunnerForTesting() - fake := fakeSuccessfulRun() + fake := fakeSuccessfulDeps() fake.Experiment.MockOpenReportContext = func(ctx context.Context) error { return errors.New("mocked error") } runner.newSession = fake.NewSession events := runAndCollect(runner, emitter) - reduced := reduceEventsKeysIgnoreLog(events) + reduced := reduceEventsKeysIgnoreLog(t, events) expect := []eventKeyCount{ {Key: eventTypeStatusQueued, Count: 1}, {Key: eventTypeStatusStarted, Count: 1}, @@ -461,15 +404,13 @@ func TestTaskRunnerRun(t *testing.T) { assertReducedEventsLike(t, expect, reduced) }) - t.Run("with success and InputNone policy", func(t *testing.T) { + t.Run("with success and just a single entry", func(t *testing.T) { runner, emitter := newRunnerForTesting() - fake := fakeSuccessfulRun() - fake.Builder.MockInputPolicy = func() model.InputPolicy { - return model.InputNone - } + fake := fakeSuccessfulDeps() + // Note that a single entry is the default we use runner.newSession = fake.NewSession events := runAndCollect(runner, emitter) - reduced := reduceEventsKeysIgnoreLog(events) + reduced := reduceEventsKeysIgnoreLog(t, events) expect := []eventKeyCount{ {Key: eventTypeStatusQueued, Count: 1}, {Key: eventTypeStatusStarted, Count: 1}, @@ -487,18 +428,16 @@ func TestTaskRunnerRun(t *testing.T) { assertReducedEventsLike(t, expect, reduced) }) - t.Run("with measurement failure and InputNone policy", func(t *testing.T) { + t.Run("with failure and just a single entry", func(t *testing.T) { runner, emitter := newRunnerForTesting() - fake := fakeSuccessfulRun() - fake.Builder.MockInputPolicy = func() model.InputPolicy { - return model.InputNone - } + fake := fakeSuccessfulDeps() + // Note that a single entry is the default we use fake.Experiment.MockMeasureWithContext = func(ctx context.Context, target model.ExperimentTarget) (measurement *model.Measurement, err error) { return nil, errors.New("preconditions error") } runner.newSession = fake.NewSession events := runAndCollect(runner, emitter) - reduced := reduceEventsKeysIgnoreLog(events) + reduced := reduceEventsKeysIgnoreLog(t, events) expect := []eventKeyCount{ {Key: eventTypeStatusQueued, Count: 1}, {Key: eventTypeStatusStarted, Count: 1}, @@ -519,10 +458,8 @@ func TestTaskRunnerRun(t *testing.T) { // we are not crashing when the measurement fails and there are annotations, // which is what was happening in the above referenced issue. runner, emitter := newRunnerForTesting() - fake := fakeSuccessfulRun() - fake.Builder.MockInputPolicy = func() model.InputPolicy { - return model.InputNone - } + fake := fakeSuccessfulDeps() + // Note that a single entry is the default we use fake.Experiment.MockMeasureWithContext = func(ctx context.Context, target model.ExperimentTarget) (measurement *model.Measurement, err error) { return nil, errors.New("preconditions error") } @@ -531,7 +468,7 @@ func TestTaskRunnerRun(t *testing.T) { "architecture": "arm64", } events := runAndCollect(runner, emitter) - reduced := reduceEventsKeysIgnoreLog(events) + reduced := reduceEventsKeysIgnoreLog(t, events) expect := []eventKeyCount{ {Key: eventTypeStatusQueued, Count: 1}, {Key: eventTypeStatusStarted, Count: 1}, @@ -547,64 +484,23 @@ func TestTaskRunnerRun(t *testing.T) { assertReducedEventsLike(t, expect, reduced) }) - t.Run("with success and InputStrictlyRequired", func(t *testing.T) { + t.Run("with success and explicit input provided", func(t *testing.T) { runner, emitter := newRunnerForTesting() runner.settings.Inputs = []string{"a", "b", "c", "d"} - fake := fakeSuccessfulRun() + fake := fakeSuccessfulDeps() fake.Builder.MockInputPolicy = func() model.InputPolicy { return model.InputStrictlyRequired } - runner.newSession = fake.NewSession - events := runAndCollect(runner, emitter) - reduced := reduceEventsKeysIgnoreLog(events) - expect := []eventKeyCount{ - {Key: eventTypeStatusQueued, Count: 1}, - {Key: eventTypeStatusStarted, Count: 1}, - {Key: eventTypeStatusProgress, Count: 3}, - {Key: eventTypeStatusGeoIPLookup, Count: 1}, - {Key: eventTypeStatusResolverLookup, Count: 1}, - {Key: eventTypeStatusProgress, Count: 1}, - {Key: eventTypeStatusReportCreate, Count: 1}, - // - {Key: eventTypeStatusMeasurementStart, Count: 1}, - {Key: eventTypeStatusProgress, Count: 1}, - {Key: eventTypeMeasurement, Count: 1}, - {Key: eventTypeStatusMeasurementSubmission, Count: 1}, - {Key: eventTypeStatusMeasurementDone, Count: 1}, - // - {Key: eventTypeStatusMeasurementStart, Count: 1}, - {Key: eventTypeStatusProgress, Count: 1}, - {Key: eventTypeMeasurement, Count: 1}, - {Key: eventTypeStatusMeasurementSubmission, Count: 1}, - {Key: eventTypeStatusMeasurementDone, Count: 1}, - // - {Key: eventTypeStatusMeasurementStart, Count: 1}, - {Key: eventTypeStatusProgress, Count: 1}, - {Key: eventTypeMeasurement, Count: 1}, - {Key: eventTypeStatusMeasurementSubmission, Count: 1}, - {Key: eventTypeStatusMeasurementDone, Count: 1}, - // - {Key: eventTypeStatusMeasurementStart, Count: 1}, - {Key: eventTypeStatusProgress, Count: 1}, - {Key: eventTypeMeasurement, Count: 1}, - {Key: eventTypeStatusMeasurementSubmission, Count: 1}, - {Key: eventTypeStatusMeasurementDone, Count: 1}, - // - {Key: eventTypeStatusEnd, Count: 1}, - } - assertReducedEventsLike(t, expect, reduced) - }) - - t.Run("with success and InputOptional and input", func(t *testing.T) { - runner, emitter := newRunnerForTesting() - runner.settings.Inputs = []string{"a", "b", "c", "d"} - fake := fakeSuccessfulRun() - fake.Builder.MockInputPolicy = func() model.InputPolicy { - return model.InputOptional + fake.Loader.MockLoad = func(ctx context.Context) ([]model.ExperimentTarget, error) { + targets := []model.ExperimentTarget{} + for _, input := range runner.settings.Inputs { + targets = append(targets, model.NewOOAPIURLInfoWithDefaultCategoryAndCountry(input)) + } + return targets, nil } runner.newSession = fake.NewSession events := runAndCollect(runner, emitter) - reduced := reduceEventsKeysIgnoreLog(events) + reduced := reduceEventsKeysIgnoreLog(t, events) expect := []eventKeyCount{ {Key: eventTypeStatusQueued, Count: 1}, {Key: eventTypeStatusStarted, Count: 1}, @@ -643,85 +539,28 @@ func TestTaskRunnerRun(t *testing.T) { assertReducedEventsLike(t, expect, reduced) }) - t.Run("with success and InputOptional and no input", func(t *testing.T) { - runner, emitter := newRunnerForTesting() - fake := fakeSuccessfulRun() - fake.Builder.MockInputPolicy = func() model.InputPolicy { - return model.InputOptional - } - runner.newSession = fake.NewSession - events := runAndCollect(runner, emitter) - reduced := reduceEventsKeysIgnoreLog(events) - expect := []eventKeyCount{ - {Key: eventTypeStatusQueued, Count: 1}, - {Key: eventTypeStatusStarted, Count: 1}, - {Key: eventTypeStatusProgress, Count: 3}, - {Key: eventTypeStatusGeoIPLookup, Count: 1}, - {Key: eventTypeStatusResolverLookup, Count: 1}, - {Key: eventTypeStatusProgress, Count: 1}, - {Key: eventTypeStatusReportCreate, Count: 1}, - // - {Key: eventTypeStatusMeasurementStart, Count: 1}, - {Key: eventTypeMeasurement, Count: 1}, - {Key: eventTypeStatusMeasurementSubmission, Count: 1}, - {Key: eventTypeStatusMeasurementDone, Count: 1}, - // - {Key: eventTypeStatusEnd, Count: 1}, - } - assertReducedEventsLike(t, expect, reduced) - }) - - t.Run("with success and InputOrStaticDefault", func(t *testing.T) { - experimentName := "DNSCheck" - runner, emitter := newRunnerForTesting() - runner.settings.Name = experimentName - fake := fakeSuccessfulRun() - fake.Builder.MockInputPolicy = func() model.InputPolicy { - return model.InputOrStaticDefault - } - runner.newSession = fake.NewSession - events := runAndCollect(runner, emitter) - reduced := reduceEventsKeysIgnoreLog(events) - expect := []eventKeyCount{ - {Key: eventTypeStatusQueued, Count: 1}, - {Key: eventTypeStatusStarted, Count: 1}, - {Key: eventTypeStatusProgress, Count: 3}, - {Key: eventTypeStatusGeoIPLookup, Count: 1}, - {Key: eventTypeStatusResolverLookup, Count: 1}, - {Key: eventTypeStatusProgress, Count: 1}, - {Key: eventTypeStatusReportCreate, Count: 1}, - } - allEntries, err := targetloading.StaticBareInputForExperiment(experimentName) - if err != nil { - t.Fatal(err) - } - // write the correct entries for each expected measurement. - for idx := 0; idx < len(allEntries); idx++ { - expect = append(expect, eventKeyCount{Key: eventTypeStatusMeasurementStart, Count: 1}) - expect = append(expect, eventKeyCount{Key: eventTypeStatusProgress, Count: 1}) - expect = append(expect, eventKeyCount{Key: eventTypeMeasurement, Count: 1}) - expect = append(expect, eventKeyCount{Key: eventTypeStatusMeasurementSubmission, Count: 1}) - expect = append(expect, eventKeyCount{Key: eventTypeStatusMeasurementDone, Count: 1}) - } - expect = append(expect, eventKeyCount{Key: eventTypeStatusEnd, Count: 1}) - assertReducedEventsLike(t, expect, reduced) - }) - t.Run("with success and max runtime", func(t *testing.T) { runner, emitter := newRunnerForTesting() runner.settings.Inputs = []string{"a", "b", "c", "d"} runner.settings.Options.MaxRuntime = 2 - fake := fakeSuccessfulRun() + fake := fakeSuccessfulDeps() fake.Builder.MockInputPolicy = func() model.InputPolicy { return model.InputStrictlyRequired } + fake.Loader.MockLoad = func(ctx context.Context) ([]model.ExperimentTarget, error) { + targets := []model.ExperimentTarget{} + for _, input := range runner.settings.Inputs { + targets = append(targets, model.NewOOAPIURLInfoWithDefaultCategoryAndCountry(input)) + } + return targets, nil + } fake.Experiment.MockMeasureWithContext = func(ctx context.Context, target model.ExperimentTarget) (measurement *model.Measurement, err error) { time.Sleep(1 * time.Second) return &model.Measurement{}, nil } runner.newSession = fake.NewSession events := runAndCollect(runner, emitter) - reduced := reduceEventsKeysIgnoreLog(events) + reduced := reduceEventsKeysIgnoreLog(t, events) expect := []eventKeyCount{ {Key: eventTypeStatusQueued, Count: 1}, {Key: eventTypeStatusStarted, Count: 1}, @@ -752,13 +591,20 @@ func TestTaskRunnerRun(t *testing.T) { runner, emitter := newRunnerForTesting() runner.settings.Inputs = []string{"a", "b", "c", "d"} runner.settings.Options.MaxRuntime = 2 - fake := fakeSuccessfulRun() + fake := fakeSuccessfulDeps() fake.Builder.MockInputPolicy = func() model.InputPolicy { return model.InputStrictlyRequired } fake.Builder.MockInterruptible = func() bool { return true } + fake.Loader.MockLoad = func(ctx context.Context) ([]model.ExperimentTarget, error) { + targets := []model.ExperimentTarget{} + for _, input := range runner.settings.Inputs { + targets = append(targets, model.NewOOAPIURLInfoWithDefaultCategoryAndCountry(input)) + } + return targets, nil + } ctx, cancel := context.WithCancel(context.Background()) fake.Experiment.MockMeasureWithContext = func(ctx context.Context, target model.ExperimentTarget) (measurement *model.Measurement, err error) { cancel() @@ -766,7 +612,7 @@ func TestTaskRunnerRun(t *testing.T) { } runner.newSession = fake.NewSession events := runAndCollectContext(ctx, runner, emitter) - reduced := reduceEventsKeysIgnoreLog(events) + reduced := reduceEventsKeysIgnoreLog(t, events) expect := []eventKeyCount{ {Key: eventTypeStatusQueued, Count: 1}, {Key: eventTypeStatusStarted, Count: 1}, @@ -787,16 +633,23 @@ func TestTaskRunnerRun(t *testing.T) { t.Run("with measurement submission failure", func(t *testing.T) { runner, emitter := newRunnerForTesting() runner.settings.Inputs = []string{"a"} - fake := fakeSuccessfulRun() + fake := fakeSuccessfulDeps() fake.Builder.MockInputPolicy = func() model.InputPolicy { return model.InputStrictlyRequired } fake.Experiment.MockSubmitAndUpdateMeasurementContext = func(ctx context.Context, measurement *model.Measurement) error { return errors.New("cannot submit") } + fake.Loader.MockLoad = func(ctx context.Context) ([]model.ExperimentTarget, error) { + targets := []model.ExperimentTarget{} + for _, input := range runner.settings.Inputs { + targets = append(targets, model.NewOOAPIURLInfoWithDefaultCategoryAndCountry(input)) + } + return targets, nil + } runner.newSession = fake.NewSession events := runAndCollect(runner, emitter) - reduced := reduceEventsKeysIgnoreLog(events) + reduced := reduceEventsKeysIgnoreLog(t, events) expect := []eventKeyCount{ {Key: eventTypeStatusQueued, Count: 1}, {Key: eventTypeStatusStarted, Count: 1}, @@ -819,7 +672,7 @@ func TestTaskRunnerRun(t *testing.T) { t.Run("with success and progress", func(t *testing.T) { runner, emitter := newRunnerForTesting() - fake := fakeSuccessfulRun() + fake := fakeSuccessfulDeps() var callbacks model.ExperimentCallbacks fake.Builder.MockSetCallbacks = func(cbs model.ExperimentCallbacks) { callbacks = cbs @@ -830,7 +683,7 @@ func TestTaskRunnerRun(t *testing.T) { } runner.newSession = fake.NewSession events := runAndCollect(runner, emitter) - reduced := reduceEventsKeysIgnoreLog(events) + reduced := reduceEventsKeysIgnoreLog(t, events) expect := []eventKeyCount{ {Key: eventTypeStatusQueued, Count: 1}, {Key: eventTypeStatusStarted, Count: 1}, From 9c169e416eaaed982fa8004564220da3334ec861 Mon Sep 17 00:00:00 2001 From: Simone Basso Date: Fri, 7 Jun 2024 09:24:12 +0200 Subject: [PATCH 20/63] x --- pkg/oonimkall/taskmodel.go | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/pkg/oonimkall/taskmodel.go b/pkg/oonimkall/taskmodel.go index 3bd6ccfee5..e030cbc8f7 100644 --- a/pkg/oonimkall/taskmodel.go +++ b/pkg/oonimkall/taskmodel.go @@ -162,6 +162,12 @@ type event struct { // taskSession abstracts a OONI session. type taskSession interface { + // A session should be used by an experiment. + model.ExperimentSession + + // A session should be used when loading targets. + model.ExperimentTargetLoaderSession + // A session can be closed. io.Closer @@ -196,12 +202,6 @@ type taskSession interface { // ResolverNetworkName must be called after MaybeLookupLocationContext // and returns the resolved resolver's network name. ResolverNetworkName() string - - // A session should be used by an experiment. - model.ExperimentSession - - // A session should be used when loading targets. - model.ExperimentTargetLoaderSession } // From f3c41bba67b0634a115d8e5faebe72e3c987f9ba Mon Sep 17 00:00:00 2001 From: Simone Basso Date: Wed, 26 Jun 2024 07:02:59 +0200 Subject: [PATCH 21/63] feat(oonirun): allow true JSON richer input --- internal/engine/experimentbuilder.go | 25 ++++++++++++++-------- internal/mocks/experimentbuilder.go | 13 +++++++++++- internal/model/experiment.go | 6 ++++++ internal/oonirun/experiment.go | 27 +++++++++++++++++++----- internal/oonirun/experiment_test.go | 27 +++++++++++++++++++++++- internal/oonirun/v1_test.go | 4 ++++ internal/oonirun/v2.go | 5 +++-- internal/oonirun/v2_test.go | 31 ++++++++++++++-------------- internal/registry/factory.go | 15 ++++++++++++++ internal/registry/portfiltering.go | 4 ++-- internal/registry/telegram.go | 4 ++-- 11 files changed, 124 insertions(+), 37 deletions(-) diff --git a/internal/engine/experimentbuilder.go b/internal/engine/experimentbuilder.go index 330777957b..591197f0c5 100644 --- a/internal/engine/experimentbuilder.go +++ b/internal/engine/experimentbuilder.go @@ -5,11 +5,13 @@ package engine // import ( + "encoding/json" + "github.com/ooni/probe-cli/v3/internal/model" "github.com/ooni/probe-cli/v3/internal/registry" ) -// experimentBuilder implements ExperimentBuilder. +// experimentBuilder implements [model.ExperimentBuilder]. // // This type is now just a tiny wrapper around registry.Factory. type experimentBuilder struct { @@ -24,37 +26,42 @@ type experimentBuilder struct { var _ model.ExperimentBuilder = &experimentBuilder{} -// Interruptible implements ExperimentBuilder.Interruptible. +// Interruptible implements [model.ExperimentBuilder]. func (b *experimentBuilder) Interruptible() bool { return b.factory.Interruptible() } -// InputPolicy implements ExperimentBuilder.InputPolicy. +// InputPolicy implements [model.ExperimentBuilder]. func (b *experimentBuilder) InputPolicy() model.InputPolicy { return b.factory.InputPolicy() } -// Options implements ExperimentBuilder.Options. +// Options implements [model.ExperimentBuilder]. func (b *experimentBuilder) Options() (map[string]model.ExperimentOptionInfo, error) { return b.factory.Options() } -// SetOptionAny implements ExperimentBuilder.SetOptionAny. +// SetOptionAny implements [model.ExperimentBuilder]. func (b *experimentBuilder) SetOptionAny(key string, value any) error { return b.factory.SetOptionAny(key, value) } -// SetOptionsAny implements ExperimentBuilder.SetOptionsAny. +// SetOptionsAny implements [model.ExperimentBuilder]. func (b *experimentBuilder) SetOptionsAny(options map[string]any) error { return b.factory.SetOptionsAny(options) } -// SetCallbacks implements ExperimentBuilder.SetCallbacks. +// SetOptionsJSON implements [model.ExperimentBuilder]. +func (b *experimentBuilder) SetOptionsJSON(value json.RawMessage) error { + return b.factory.SetOptionsJSON(value) +} + +// SetCallbacks implements [model.ExperimentBuilder]. func (b *experimentBuilder) SetCallbacks(callbacks model.ExperimentCallbacks) { b.callbacks = callbacks } -// NewExperiment creates the experiment +// NewExperiment creates a new [model.Experiment] instance. func (b *experimentBuilder) NewExperiment() model.Experiment { measurer := b.factory.NewExperimentMeasurer() experiment := newExperiment(b.session, measurer) @@ -67,7 +74,7 @@ func (b *experimentBuilder) NewTargetLoader(config *model.ExperimentTargetLoader return b.factory.NewTargetLoader(config) } -// newExperimentBuilder creates a new experimentBuilder instance. +// newExperimentBuilder creates a new [*experimentBuilder] instance. func newExperimentBuilder(session *Session, name string) (*experimentBuilder, error) { factory, err := registry.NewFactory(name, session.kvStore, session.logger) if err != nil { diff --git a/internal/mocks/experimentbuilder.go b/internal/mocks/experimentbuilder.go index 1f6a27187f..a9cd880ba5 100644 --- a/internal/mocks/experimentbuilder.go +++ b/internal/mocks/experimentbuilder.go @@ -1,6 +1,10 @@ package mocks -import "github.com/ooni/probe-cli/v3/internal/model" +import ( + "encoding/json" + + "github.com/ooni/probe-cli/v3/internal/model" +) // ExperimentBuilder mocks model.ExperimentBuilder. type ExperimentBuilder struct { @@ -14,6 +18,8 @@ type ExperimentBuilder struct { MockSetOptionsAny func(options map[string]any) error + MockSetOptionsJSON func(value json.RawMessage) error + MockSetCallbacks func(callbacks model.ExperimentCallbacks) MockNewExperiment func() model.Experiment @@ -43,6 +49,11 @@ func (eb *ExperimentBuilder) SetOptionsAny(options map[string]any) error { return eb.MockSetOptionsAny(options) } +func (eb *ExperimentBuilder) SetOptionsJSON(value json.RawMessage) error { + // TODO(bassosimone): write unit tests for this method + return eb.MockSetOptionsJSON(value) +} + func (eb *ExperimentBuilder) SetCallbacks(callbacks model.ExperimentCallbacks) { eb.MockSetCallbacks(callbacks) } diff --git a/internal/model/experiment.go b/internal/model/experiment.go index 5aada21ff4..95848242c2 100644 --- a/internal/model/experiment.go +++ b/internal/model/experiment.go @@ -7,6 +7,7 @@ package model import ( "context" + "encoding/json" "errors" "fmt" ) @@ -235,6 +236,11 @@ type ExperimentBuilder interface { // the SetOptionAny method for more information. SetOptionsAny(options map[string]any) error + // SetOptionsJSON uses the given [json.RawMessage] to initialize fields + // of the configuration for running the experiment. The [json.RawMessage] + // MUST contain a serialization of the experiment config's type. + SetOptionsJSON(value json.RawMessage) error + // SetCallbacks sets the experiment's interactive callbacks. SetCallbacks(callbacks ExperimentCallbacks) diff --git a/internal/oonirun/experiment.go b/internal/oonirun/experiment.go index c4613107d7..a660935244 100644 --- a/internal/oonirun/experiment.go +++ b/internal/oonirun/experiment.go @@ -6,6 +6,7 @@ package oonirun import ( "context" + "encoding/json" "fmt" "math/rand" "strings" @@ -25,9 +26,18 @@ type Experiment struct { // Annotations contains OPTIONAL Annotations for the experiment. Annotations map[string]string - // ExtraOptions contains OPTIONAL extra options for the experiment. + // ExtraOptions contains OPTIONAL extra options that modify the + // default-empty experiment-specific configuration. We apply + // the changes described by this field after using the InitialOptions + // field to initialize the experiment-specific configuration. ExtraOptions map[string]any + // InitialOptions contains an OPTIONAL [json.RawMessage] object + // used to initialize the default-empty experiment-specific + // configuration. After we have initialized the configuration + // as such, we then apply the changes described by the ExtraOptions. + InitialOptions json.RawMessage + // Inputs contains the OPTIONAL experiment Inputs Inputs []string @@ -82,15 +92,22 @@ func (ed *Experiment) Run(ctx context.Context) error { return err } - // TODO(bassosimone,DecFox): when we're executed by OONI Run v2, it probably makes - // slightly more sense to set options from a json.RawMessage because the current - // command line limitation is that it's hard to set non scalar parameters and instead - // with using OONI Run v2 we can completely bypass such a limitation. + // TODO(bassosimone): we need another patch after the current one + // to correctly serialize the options as configured using InitialOptions + // and ExtraOptions otherwise the Measurement.Options field turns out + // to always be empty and this is highly suboptimal for us. // 2. configure experiment's options // + // We first unmarshal the InitialOptions into the experiment + // configuration and afterwards we modify the configuration using + // the values contained inside the ExtraOptions field. + // // This MUST happen before loading targets because the options will // possibly be used to produce richer input targets. + if err := builder.SetOptionsJSON(ed.InitialOptions); err != nil { + return err + } if err := builder.SetOptionsAny(ed.ExtraOptions); err != nil { return err } diff --git a/internal/oonirun/experiment_test.go b/internal/oonirun/experiment_test.go index 1fd38abb0b..2fe651119e 100644 --- a/internal/oonirun/experiment_test.go +++ b/internal/oonirun/experiment_test.go @@ -2,6 +2,7 @@ package oonirun import ( "context" + "encoding/json" "errors" "reflect" "sort" @@ -16,6 +17,7 @@ import ( func TestExperimentRunWithFailureToSubmitAndShuffle(t *testing.T) { shuffledInputsPrev := experimentShuffledInputs.Load() var calledSetOptionsAny int + var calledSetOptionsJSON int var failedToSubmit int var calledKibiBytesReceived int var calledKibiBytesSent int @@ -44,6 +46,10 @@ func TestExperimentRunWithFailureToSubmitAndShuffle(t *testing.T) { calledSetOptionsAny++ return nil }, + MockSetOptionsJSON: func(value json.RawMessage) error { + calledSetOptionsJSON++ + return nil + }, MockNewExperiment: func() model.Experiment { exp := &mocks.Experiment{ MockMeasureWithContext: func( @@ -109,6 +115,9 @@ func TestExperimentRunWithFailureToSubmitAndShuffle(t *testing.T) { if calledSetOptionsAny < 1 { t.Fatal("should have called SetOptionsAny") } + if calledSetOptionsJSON < 1 { + t.Fatal("should have called SetOptionsJSON") + } if calledKibiBytesReceived < 1 { t.Fatal("did not call KibiBytesReceived") } @@ -198,10 +207,14 @@ func TestExperimentRun(t *testing.T) { args: args{}, expectErr: errMocked, }, { - name: "cannot set options", + name: "cannot set ExtraOptions", fields: fields{ newExperimentBuilderFn: func(experimentName string) (model.ExperimentBuilder, error) { eb := &mocks.ExperimentBuilder{ + MockSetOptionsJSON: func(value json.RawMessage) error { + // TODO(bassosimone): need a test case before this one + return nil + }, MockSetOptionsAny: func(options map[string]any) error { return errMocked }, @@ -226,6 +239,9 @@ func TestExperimentRun(t *testing.T) { MockInputPolicy: func() model.InputPolicy { return model.InputOptional }, + MockSetOptionsJSON: func(value json.RawMessage) error { + return nil + }, MockSetOptionsAny: func(options map[string]any) error { return nil }, @@ -255,6 +271,9 @@ func TestExperimentRun(t *testing.T) { MockInputPolicy: func() model.InputPolicy { return model.InputOptional }, + MockSetOptionsJSON: func(value json.RawMessage) error { + return nil + }, MockSetOptionsAny: func(options map[string]any) error { return nil }, @@ -298,6 +317,9 @@ func TestExperimentRun(t *testing.T) { MockInputPolicy: func() model.InputPolicy { return model.InputOptional }, + MockSetOptionsJSON: func(value json.RawMessage) error { + return nil + }, MockSetOptionsAny: func(options map[string]any) error { return nil }, @@ -344,6 +366,9 @@ func TestExperimentRun(t *testing.T) { MockInputPolicy: func() model.InputPolicy { return model.InputOptional }, + MockSetOptionsJSON: func(value json.RawMessage) error { + return nil + }, MockSetOptionsAny: func(options map[string]any) error { return nil }, diff --git a/internal/oonirun/v1_test.go b/internal/oonirun/v1_test.go index 910d910b0c..8ea9b05139 100644 --- a/internal/oonirun/v1_test.go +++ b/internal/oonirun/v1_test.go @@ -2,6 +2,7 @@ package oonirun import ( "context" + "encoding/json" "errors" "net/http" "strings" @@ -26,6 +27,9 @@ func newMinimalFakeSession() *mocks.Session { MockSetOptionsAny: func(options map[string]any) error { return nil }, + MockSetOptionsJSON: func(value json.RawMessage) error { + return nil + }, MockNewExperiment: func() model.Experiment { exp := &mocks.Experiment{ MockMeasureWithContext: func( diff --git a/internal/oonirun/v2.go b/internal/oonirun/v2.go index 4330b07b76..6552ad582e 100644 --- a/internal/oonirun/v2.go +++ b/internal/oonirun/v2.go @@ -54,7 +54,7 @@ type V2Nettest struct { // `Safe` will be available for the experiment run, but omitted from // the serialized Measurement that the experiment builder will submit // to the OONI backend. - Options map[string]any `json:"options"` + Options json.RawMessage `json:"options"` // TestName contains the nettest name. TestName string `json:"test_name"` @@ -183,7 +183,8 @@ func V2MeasureDescriptor(ctx context.Context, config *LinkConfig, desc *V2Descri // construct an experiment from the current nettest exp := &Experiment{ Annotations: config.Annotations, - ExtraOptions: nettest.Options, + ExtraOptions: make(map[string]any), + InitialOptions: nettest.Options, Inputs: nettest.Inputs, InputFilePaths: nil, MaxRuntime: config.MaxRuntime, diff --git a/internal/oonirun/v2_test.go b/internal/oonirun/v2_test.go index d849c6d088..b77b9b7bf7 100644 --- a/internal/oonirun/v2_test.go +++ b/internal/oonirun/v2_test.go @@ -7,7 +7,6 @@ import ( "net/http" "net/http/httptest" "testing" - "time" "github.com/ooni/probe-cli/v3/internal/httpclientx" "github.com/ooni/probe-cli/v3/internal/kvstore" @@ -27,9 +26,9 @@ func TestOONIRunV2LinkCommonCase(t *testing.T) { Author: "", Nettests: []V2Nettest{{ Inputs: []string{}, - Options: map[string]any{ - "SleepTime": int64(10 * time.Millisecond), - }, + Options: json.RawMessage(`{ + "SleepTime": 10000000 + }`), TestName: "example", }}, } @@ -73,9 +72,9 @@ func TestOONIRunV2LinkCannotUpdateCache(t *testing.T) { Author: "", Nettests: []V2Nettest{{ Inputs: []string{}, - Options: map[string]any{ - "SleepTime": int64(10 * time.Millisecond), - }, + Options: json.RawMessage(`{ + "SleepTime": 10000000 + }`), TestName: "example", }}, } @@ -132,9 +131,9 @@ func TestOONIRunV2LinkWithoutAcceptChanges(t *testing.T) { Author: "", Nettests: []V2Nettest{{ Inputs: []string{}, - Options: map[string]any{ - "SleepTime": int64(10 * time.Millisecond), - }, + Options: json.RawMessage(`{ + "SleepTime": 10000000 + }`), TestName: "example", }}, } @@ -220,9 +219,9 @@ func TestOONIRunV2LinkEmptyTestName(t *testing.T) { Author: "", Nettests: []V2Nettest{{ Inputs: []string{}, - Options: map[string]any{ - "SleepTime": int64(10 * time.Millisecond), - }, + Options: json.RawMessage(`{ + "SleepTime": 10000000 + }`), TestName: "", // empty! }}, } @@ -374,6 +373,9 @@ func TestV2MeasureDescriptor(t *testing.T) { MockInputPolicy: func() model.InputPolicy { return model.InputNone }, + MockSetOptionsJSON: func(value json.RawMessage) error { + return nil + }, MockSetOptionsAny: func(options map[string]any) error { return nil }, @@ -426,7 +428,7 @@ func TestV2MeasureDescriptor(t *testing.T) { Author: "", Nettests: []V2Nettest{{ Inputs: []string{}, - Options: map[string]any{}, + Options: json.RawMessage(`{}`), TestName: "example", }}, } @@ -532,5 +534,4 @@ func TestV2DescriptorCacheLoad(t *testing.T) { t.Fatal("expected nil cache") } }) - } diff --git a/internal/registry/factory.go b/internal/registry/factory.go index ee73e95a8f..9c5ffac30a 100644 --- a/internal/registry/factory.go +++ b/internal/registry/factory.go @@ -5,6 +5,7 @@ package registry // import ( + "encoding/json" "errors" "fmt" "math" @@ -228,6 +229,20 @@ func (b *Factory) SetOptionsAny(options map[string]any) error { return nil } +// SetOptionsJSON unmarshals the given [json.RawMessage] inside +// the experiment specific configuration. +func (b *Factory) SetOptionsJSON(value json.RawMessage) error { + // handle the case where the options are empty + if len(value) <= 0 { + return nil + } + + // otherwise unmarshal into the configuration, which we assume + // to be a pointer to a structure. + // TODO(bassosimone): make sure with testing that b.config is always a pointer. + return json.Unmarshal(value, b.config) +} + // 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 diff --git a/internal/registry/portfiltering.go b/internal/registry/portfiltering.go index 8c6a857df8..2d7c67f250 100644 --- a/internal/registry/portfiltering.go +++ b/internal/registry/portfiltering.go @@ -15,11 +15,11 @@ func init() { return &Factory{ build: func(config any) model.ExperimentMeasurer { return portfiltering.NewExperimentMeasurer( - config.(portfiltering.Config), + *config.(*portfiltering.Config), ) }, canonicalName: canonicalName, - config: portfiltering.Config{}, + config: &portfiltering.Config{}, enabledByDefault: true, interruptible: false, inputPolicy: model.InputNone, diff --git a/internal/registry/telegram.go b/internal/registry/telegram.go index 987e640935..bf4f71aed1 100644 --- a/internal/registry/telegram.go +++ b/internal/registry/telegram.go @@ -15,11 +15,11 @@ func init() { return &Factory{ build: func(config any) model.ExperimentMeasurer { return telegram.NewExperimentMeasurer( - config.(telegram.Config), + *config.(*telegram.Config), ) }, canonicalName: canonicalName, - config: telegram.Config{}, + config: &telegram.Config{}, enabledByDefault: true, interruptible: false, inputPolicy: model.InputNone, From 4f183bda80a6edefbe8845f11618fd22b855b82d Mon Sep 17 00:00:00 2001 From: Simone Basso Date: Wed, 26 Jun 2024 07:07:11 +0200 Subject: [PATCH 22/63] x --- internal/model/experiment.go | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/internal/model/experiment.go b/internal/model/experiment.go index 95848242c2..ad4e0dfcfc 100644 --- a/internal/model/experiment.go +++ b/internal/model/experiment.go @@ -237,8 +237,9 @@ type ExperimentBuilder interface { SetOptionsAny(options map[string]any) error // SetOptionsJSON uses the given [json.RawMessage] to initialize fields - // of the configuration for running the experiment. The [json.RawMessage] - // MUST contain a serialization of the experiment config's type. + // of the configuration for running the experiment. The [json.RawMessage], if + // not empty, MUST contain a serialization of the experiment config's + // type. An empty [json.RawMessage] will silently be ignored. SetOptionsJSON(value json.RawMessage) error // SetCallbacks sets the experiment's interactive callbacks. From 52fe2975183172d8582c4f27b6fb6b0a5469cbdd Mon Sep 17 00:00:00 2001 From: Simone Basso Date: Wed, 26 Jun 2024 08:57:58 +0200 Subject: [PATCH 23/63] x --- pkg/oonimkall/taskmodel.go | 10 ++++++---- pkg/oonimkall/taskrunner.go | 6 ++++-- pkg/oonimkall/taskrunner_test.go | 2 +- 3 files changed, 11 insertions(+), 7 deletions(-) diff --git a/pkg/oonimkall/taskmodel.go b/pkg/oonimkall/taskmodel.go index e030cbc8f7..71faea7f6d 100644 --- a/pkg/oonimkall/taskmodel.go +++ b/pkg/oonimkall/taskmodel.go @@ -106,10 +106,12 @@ type eventLog struct { } type eventMeasurementGeneric struct { - Failure string `json:"failure,omitempty"` - Idx int64 `json:"idx"` - Input string `json:"input"` - JSONStr string `json:"json_str,omitempty"` + CategoryCode string `json:"category_code,omitempty"` + CountryCode string `json:"country_code,omitempty"` + Failure string `json:"failure,omitempty"` + Idx int64 `json:"idx"` + Input string `json:"input"` + JSONStr string `json:"json_str,omitempty"` } type eventStatusEnd struct { diff --git a/pkg/oonimkall/taskrunner.go b/pkg/oonimkall/taskrunner.go index c53b6458d9..4536ce7a0c 100644 --- a/pkg/oonimkall/taskrunner.go +++ b/pkg/oonimkall/taskrunner.go @@ -249,8 +249,10 @@ func (r *runnerForTask) Run(rootCtx context.Context) { } logger.Infof("Starting measurement with index %d", idx) r.emitter.Emit(eventTypeStatusMeasurementStart, eventMeasurementGeneric{ - Idx: int64(idx), - Input: target.Input(), + CategoryCode: target.Category(), + CountryCode: target.Country(), + Idx: int64(idx), + Input: target.Input(), }) if target.Input() != "" && inputCount > 0 { var percentage float64 diff --git a/pkg/oonimkall/taskrunner_test.go b/pkg/oonimkall/taskrunner_test.go index de7b166ace..2ca4391e92 100644 --- a/pkg/oonimkall/taskrunner_test.go +++ b/pkg/oonimkall/taskrunner_test.go @@ -843,7 +843,7 @@ func TestTaskRunnerRun(t *testing.T) { t.Run("with success and progress", func(t *testing.T) { runner, emitter := newRunnerForTesting() - fake := fakeSuccessfulDeps() + fake := fakeSuccessfulRun() var callbacks model.ExperimentCallbacks fake.Builder.MockSetCallbacks = func(cbs model.ExperimentCallbacks) { callbacks = cbs From b9390cf1120f3c03f83b527eb1e75adfc13d981d Mon Sep 17 00:00:00 2001 From: Simone Basso Date: Wed, 26 Jun 2024 09:00:54 +0200 Subject: [PATCH 24/63] x --- pkg/oonimkall/taskrunner_test.go | 40 ++++++++++++++++---------------- 1 file changed, 20 insertions(+), 20 deletions(-) diff --git a/pkg/oonimkall/taskrunner_test.go b/pkg/oonimkall/taskrunner_test.go index 2ca4391e92..77f2634d71 100644 --- a/pkg/oonimkall/taskrunner_test.go +++ b/pkg/oonimkall/taskrunner_test.go @@ -201,7 +201,7 @@ func TestTaskRunnerRun(t *testing.T) { // // You MAY override some functions to provoke specific errors // or generally change the operating conditions. - fakeSuccessfulRun := func() *MockableTaskRunnerDependencies { + fakeSuccessfulDeps := func() *MockableTaskRunnerDependencies { deps := &MockableTaskRunnerDependencies{ // Configure the fake experiment @@ -307,7 +307,7 @@ func TestTaskRunnerRun(t *testing.T) { t.Run("with invalid experiment name", func(t *testing.T) { runner, emitter := newRunnerForTesting() - fake := fakeSuccessfulRun() + fake := fakeSuccessfulDeps() fake.Session.MockNewExperimentBuilder = func(name string) (model.ExperimentBuilder, error) { return nil, errors.New("invalid experiment name") } @@ -325,7 +325,7 @@ func TestTaskRunnerRun(t *testing.T) { t.Run("with error during backends lookup", func(t *testing.T) { runner, emitter := newRunnerForTesting() - fake := fakeSuccessfulRun() + fake := fakeSuccessfulDeps() fake.Session.MockMaybeLookupBackendsContext = func(ctx context.Context) error { return errors.New("mocked error") } @@ -343,7 +343,7 @@ func TestTaskRunnerRun(t *testing.T) { t.Run("with error during location lookup", func(t *testing.T) { runner, emitter := newRunnerForTesting() - fake := fakeSuccessfulRun() + fake := fakeSuccessfulDeps() fake.Session.MockMaybeLookupLocationContext = func(ctx context.Context) error { return errors.New("mocked error") } @@ -365,7 +365,7 @@ func TestTaskRunnerRun(t *testing.T) { t.Run("with error during target loading", func(t *testing.T) { runner, emitter := newRunnerForTesting() - fake := fakeSuccessfulRun() + fake := fakeSuccessfulDeps() fake.Builder.MockInputPolicy = func() model.InputPolicy { return model.InputOrQueryBackend } @@ -386,7 +386,7 @@ func TestTaskRunnerRun(t *testing.T) { t.Run("with missing input and InputStrictlyRequired policy", func(t *testing.T) { runner, emitter := newRunnerForTesting() - fake := fakeSuccessfulRun() + fake := fakeSuccessfulDeps() fake.Builder.MockInputPolicy = func() model.InputPolicy { return model.InputStrictlyRequired } @@ -409,7 +409,7 @@ func TestTaskRunnerRun(t *testing.T) { func(t *testing.T) { runner, emitter := newRunnerForTesting() runner.settings.Name = "Antani" // no input for this experiment - fake := fakeSuccessfulRun() + fake := fakeSuccessfulDeps() fake.Builder.MockInputPolicy = func() model.InputPolicy { return model.InputOrStaticDefault } @@ -431,7 +431,7 @@ func TestTaskRunnerRun(t *testing.T) { t.Run("with InputNone policy and provided input", func(t *testing.T) { runner, emitter := newRunnerForTesting() runner.settings.Inputs = append(runner.settings.Inputs, "https://x.org/") - fake := fakeSuccessfulRun() + fake := fakeSuccessfulDeps() fake.Builder.MockInputPolicy = func() model.InputPolicy { return model.InputNone } @@ -452,7 +452,7 @@ func TestTaskRunnerRun(t *testing.T) { t.Run("with failure opening report", func(t *testing.T) { runner, emitter := newRunnerForTesting() - fake := fakeSuccessfulRun() + fake := fakeSuccessfulDeps() fake.Experiment.MockOpenReportContext = func(ctx context.Context) error { return errors.New("mocked error") } @@ -473,7 +473,7 @@ func TestTaskRunnerRun(t *testing.T) { t.Run("with success and just a single entry", func(t *testing.T) { runner, emitter := newRunnerForTesting() - fake := fakeSuccessfulRun() + fake := fakeSuccessfulDeps() fake.Builder.MockInputPolicy = func() model.InputPolicy { return model.InputNone } @@ -499,7 +499,7 @@ func TestTaskRunnerRun(t *testing.T) { t.Run("with failure and just a single entry", func(t *testing.T) { runner, emitter := newRunnerForTesting() - fake := fakeSuccessfulRun() + fake := fakeSuccessfulDeps() fake.Builder.MockInputPolicy = func() model.InputPolicy { return model.InputNone } @@ -529,7 +529,7 @@ func TestTaskRunnerRun(t *testing.T) { // we are not crashing when the measurement fails and there are annotations, // which is what was happening in the above referenced issue. runner, emitter := newRunnerForTesting() - fake := fakeSuccessfulRun() + fake := fakeSuccessfulDeps() fake.Builder.MockInputPolicy = func() model.InputPolicy { return model.InputNone } @@ -560,7 +560,7 @@ func TestTaskRunnerRun(t *testing.T) { t.Run("with success and explicit input provided", func(t *testing.T) { runner, emitter := newRunnerForTesting() runner.settings.Inputs = []string{"a", "b", "c", "d"} - fake := fakeSuccessfulRun() + fake := fakeSuccessfulDeps() fake.Builder.MockInputPolicy = func() model.InputPolicy { return model.InputStrictlyRequired } @@ -608,7 +608,7 @@ func TestTaskRunnerRun(t *testing.T) { t.Run("with success and InputOptional and input", func(t *testing.T) { runner, emitter := newRunnerForTesting() runner.settings.Inputs = []string{"a", "b", "c", "d"} - fake := fakeSuccessfulRun() + fake := fakeSuccessfulDeps() fake.Builder.MockInputPolicy = func() model.InputPolicy { return model.InputOptional } @@ -655,7 +655,7 @@ func TestTaskRunnerRun(t *testing.T) { t.Run("with success and InputOptional and no input", func(t *testing.T) { runner, emitter := newRunnerForTesting() - fake := fakeSuccessfulRun() + fake := fakeSuccessfulDeps() fake.Builder.MockInputPolicy = func() model.InputPolicy { return model.InputOptional } @@ -685,7 +685,7 @@ func TestTaskRunnerRun(t *testing.T) { experimentName := "DNSCheck" runner, emitter := newRunnerForTesting() runner.settings.Name = experimentName - fake := fakeSuccessfulRun() + fake := fakeSuccessfulDeps() fake.Builder.MockInputPolicy = func() model.InputPolicy { return model.InputOrStaticDefault } @@ -721,7 +721,7 @@ func TestTaskRunnerRun(t *testing.T) { runner, emitter := newRunnerForTesting() runner.settings.Inputs = []string{"a", "b", "c", "d"} runner.settings.Options.MaxRuntime = 2 - fake := fakeSuccessfulRun() + fake := fakeSuccessfulDeps() fake.Builder.MockInputPolicy = func() model.InputPolicy { return model.InputStrictlyRequired } @@ -762,7 +762,7 @@ func TestTaskRunnerRun(t *testing.T) { runner, emitter := newRunnerForTesting() runner.settings.Inputs = []string{"a", "b", "c", "d"} runner.settings.Options.MaxRuntime = 2 - fake := fakeSuccessfulRun() + fake := fakeSuccessfulDeps() fake.Builder.MockInputPolicy = func() model.InputPolicy { return model.InputStrictlyRequired } @@ -804,7 +804,7 @@ func TestTaskRunnerRun(t *testing.T) { t.Run("with measurement submission failure", func(t *testing.T) { runner, emitter := newRunnerForTesting() runner.settings.Inputs = []string{"a"} - fake := fakeSuccessfulRun() + fake := fakeSuccessfulDeps() fake.Builder.MockInputPolicy = func() model.InputPolicy { return model.InputStrictlyRequired } @@ -843,7 +843,7 @@ func TestTaskRunnerRun(t *testing.T) { t.Run("with success and progress", func(t *testing.T) { runner, emitter := newRunnerForTesting() - fake := fakeSuccessfulRun() + fake := fakeSuccessfulDeps() var callbacks model.ExperimentCallbacks fake.Builder.MockSetCallbacks = func(cbs model.ExperimentCallbacks) { callbacks = cbs From ca1cdc7b18c48892d4dbd08b5d1bb9eea080dcae Mon Sep 17 00:00:00 2001 From: Simone Basso Date: Wed, 26 Jun 2024 09:35:30 +0200 Subject: [PATCH 25/63] feat: correctly set options from richer input --- internal/engine/experiment.go | 9 +-- internal/experiment/dnscheck/dnscheck.go | 2 +- internal/experiment/dnscheck/dnscheck_test.go | 18 ++--- internal/experiment/dnscheck/richerinput.go | 18 +++-- .../experiment/dnscheck/richerinput_test.go | 30 ++++---- internal/experiment/openvpn/openvpn.go | 2 +- internal/experiment/openvpn/openvpn_test.go | 8 +- internal/experiment/openvpn/richerinput.go | 18 +++-- .../experiment/openvpn/richerinput_test.go | 4 +- internal/experimentconfig/experimentconfig.go | 76 +++++++++++++++++++ internal/model/experiment.go | 12 +++ internal/model/ooapi.go | 13 ++++ internal/oonirun/experiment.go | 8 +- internal/oonirun/inputprocessor.go | 6 +- internal/oonirun/inputprocessor_test.go | 4 - 15 files changed, 164 insertions(+), 64 deletions(-) create mode 100644 internal/experimentconfig/experimentconfig.go diff --git a/internal/engine/experiment.go b/internal/engine/experiment.go index 5f6ac5d874..5061ec9664 100644 --- a/internal/engine/experiment.go +++ b/internal/engine/experiment.go @@ -111,13 +111,10 @@ func (e *experiment) SubmitAndUpdateMeasurementContext( // newMeasurement creates a new measurement for this experiment with the given input. func (e *experiment) newMeasurement(target model.ExperimentTarget) *model.Measurement { utctimenow := time.Now().UTC() - // TODO(bassosimone,DecFox): move here code that supports filling the options field - // when there is richer input, which currently is inside ./internal/oonirun. - // - // We MUST do this because the current solution only works for OONI Run and when - // there are command line options but does not work for API/static targets. + m := &model.Measurement{ DataFormatVersion: model.OOAPIReportDefaultDataFormatVersion, + Options: target.Options(), Input: model.MeasurementInput(target.Input()), MeasurementStartTime: utctimenow.Format(model.MeasurementDateFormat), MeasurementStartTimeSaved: utctimenow, @@ -135,6 +132,7 @@ func (e *experiment) newMeasurement(target model.ExperimentTarget) *model.Measur TestStartTime: e.testStartTime, TestVersion: e.testVersion, } + m.AddAnnotation("architecture", runtime.GOARCH) m.AddAnnotation("engine_name", "ooniprobe-engine") m.AddAnnotation("engine_version", version.Version) @@ -144,6 +142,7 @@ func (e *experiment) newMeasurement(target model.ExperimentTarget) *model.Measur m.AddAnnotation("vcs_revision", runtimex.BuildInfo.VcsRevision) m.AddAnnotation("vcs_time", runtimex.BuildInfo.VcsTime) m.AddAnnotation("vcs_tool", runtimex.BuildInfo.VcsTool) + return m } diff --git a/internal/experiment/dnscheck/dnscheck.go b/internal/experiment/dnscheck/dnscheck.go index b5c1583b29..815ab2511a 100644 --- a/internal/experiment/dnscheck/dnscheck.go +++ b/internal/experiment/dnscheck/dnscheck.go @@ -134,7 +134,7 @@ func (m *Measurer) Run(ctx context.Context, args *model.ExperimentArgs) error { if !ok { return ErrInvalidInputType } - config, input := target.Options, target.URL + config, input := target.Config, target.URL sess.Logger().Infof("dnscheck: using richer input: %+v %+v", config, input) // 1. fill the measurement with test keys diff --git a/internal/experiment/dnscheck/dnscheck_test.go b/internal/experiment/dnscheck/dnscheck_test.go index bf8f895554..c7055930c0 100644 --- a/internal/experiment/dnscheck/dnscheck_test.go +++ b/internal/experiment/dnscheck/dnscheck_test.go @@ -72,7 +72,7 @@ func TestDNSCheckFailsWithoutInput(t *testing.T) { Session: newsession(), Target: &Target{ URL: "", // explicitly empty - Options: &Config{ + Config: &Config{ Domain: "example.com", }, }, @@ -90,8 +90,8 @@ func TestDNSCheckFailsWithInvalidURL(t *testing.T) { Measurement: &model.Measurement{Input: "Not a valid URL \x7f"}, Session: newsession(), Target: &Target{ - URL: "Not a valid URL \x7f", - Options: &Config{}, + URL: "Not a valid URL \x7f", + Config: &Config{}, }, } err := measurer.Run(context.Background(), args) @@ -107,8 +107,8 @@ func TestDNSCheckFailsWithUnsupportedProtocol(t *testing.T) { Measurement: &model.Measurement{Input: "file://1.1.1.1"}, Session: newsession(), Target: &Target{ - URL: "file://1.1.1.1", - Options: &Config{}, + URL: "file://1.1.1.1", + Config: &Config{}, }, } err := measurer.Run(context.Background(), args) @@ -128,7 +128,7 @@ func TestWithCancelledContext(t *testing.T) { Session: newsession(), Target: &Target{ URL: "dot://one.one.one.one", - Options: &Config{ + Config: &Config{ DefaultAddrs: "1.1.1.1 1.0.0.1", }, }, @@ -191,7 +191,7 @@ func TestDNSCheckValid(t *testing.T) { Session: newsession(), Target: &Target{ URL: "dot://one.one.one.one:853", - Options: &Config{ + Config: &Config{ DefaultAddrs: "1.1.1.1 1.0.0.1", }, }, @@ -239,8 +239,8 @@ func TestDNSCheckWait(t *testing.T) { Measurement: &measurement, Session: newsession(), Target: &Target{ - URL: input, - Options: &Config{}, + URL: input, + Config: &Config{}, }, } err := measurer.Run(context.Background(), args) diff --git a/internal/experiment/dnscheck/richerinput.go b/internal/experiment/dnscheck/richerinput.go index 4d4a7ce528..f42d077e23 100644 --- a/internal/experiment/dnscheck/richerinput.go +++ b/internal/experiment/dnscheck/richerinput.go @@ -3,6 +3,7 @@ package dnscheck import ( "context" + "github.com/ooni/probe-cli/v3/internal/experimentconfig" "github.com/ooni/probe-cli/v3/internal/model" "github.com/ooni/probe-cli/v3/internal/reflectx" "github.com/ooni/probe-cli/v3/internal/targetloading" @@ -10,8 +11,8 @@ import ( // Target is a richer-input target that this experiment should measure. type Target struct { - // Options contains the configuration. - Options *Config + // Config contains the configuration. + Config *Config // URL is the input URL. URL string @@ -34,6 +35,11 @@ func (t *Target) Input() string { return t.URL } +// Options implements [model.ExperimentTarget]. +func (t *Target) Options() []string { + return experimentconfig.DefaultOptionsSerializer(t.Config) +} + // String implements [model.ExperimentTarget]. func (t *Target) String() string { return t.URL @@ -83,8 +89,8 @@ func (tl *targetLoader) Load(ctx context.Context) ([]model.ExperimentTarget, err var targets []model.ExperimentTarget for _, input := range inputs { targets = append(targets, &Target{ - Options: tl.options, - URL: input, + Config: tl.options, + URL: input, }) } return targets, nil @@ -100,14 +106,14 @@ var defaultInput = []model.ExperimentTarget{ // &Target{ URL: "https://dns.google/dns-query", - Options: &Config{ + Config: &Config{ HTTP3Enabled: true, DefaultAddrs: "8.8.8.8 8.8.4.4", }, }, &Target{ URL: "https://dns.google/dns-query", - Options: &Config{ + Config: &Config{ DefaultAddrs: "8.8.8.8 8.8.4.4", }, }, diff --git a/internal/experiment/dnscheck/richerinput_test.go b/internal/experiment/dnscheck/richerinput_test.go index 2d9e5dd53d..7d5b70ed5a 100644 --- a/internal/experiment/dnscheck/richerinput_test.go +++ b/internal/experiment/dnscheck/richerinput_test.go @@ -16,7 +16,7 @@ import ( func TestTarget(t *testing.T) { target := &Target{ URL: "https://dns.google/dns-query", - Options: &Config{ + Config: &Config{ DefaultAddrs: "8.8.8.8 8.8.4.4", Domain: "example.com", HTTP3Enabled: false, @@ -79,14 +79,14 @@ func TestNewLoader(t *testing.T) { var testDefaultInput = []model.ExperimentTarget{ &Target{ URL: "https://dns.google/dns-query", - Options: &Config{ + Config: &Config{ HTTP3Enabled: true, DefaultAddrs: "8.8.8.8 8.8.4.4", }, }, &Target{ URL: "https://dns.google/dns-query", - Options: &Config{ + Config: &Config{ DefaultAddrs: "8.8.8.8 8.8.4.4", }, }, @@ -136,25 +136,25 @@ func TestTargetLoaderLoad(t *testing.T) { expectTargets: []model.ExperimentTarget{ &Target{ URL: "https://dns.cloudflare.com/dns-query", - Options: &Config{ + Config: &Config{ DefaultAddrs: "1.1.1.1 1.0.0.1", }, }, &Target{ URL: "https://one.one.one.one/dns-query", - Options: &Config{ + Config: &Config{ DefaultAddrs: "1.1.1.1 1.0.0.1", }, }, &Target{ URL: "https://1dot1dot1dot1dot.com/dns-query", - Options: &Config{ + Config: &Config{ DefaultAddrs: "1.1.1.1 1.0.0.1", }, }, &Target{ URL: "https://dns.cloudflare/dns-query", - Options: &Config{ + Config: &Config{ DefaultAddrs: "1.1.1.1 1.0.0.1", }, }, @@ -202,12 +202,12 @@ func TestTargetLoaderLoad(t *testing.T) { expectErr: nil, expectTargets: []model.ExperimentTarget{ &Target{ - URL: "https://dns.cloudflare.com/dns-query", - Options: &Config{}, + URL: "https://dns.cloudflare.com/dns-query", + Config: &Config{}, }, &Target{ - URL: "https://one.one.one.one/dns-query", - Options: &Config{}, + URL: "https://one.one.one.one/dns-query", + Config: &Config{}, }, }, }, @@ -229,12 +229,12 @@ func TestTargetLoaderLoad(t *testing.T) { expectErr: nil, expectTargets: []model.ExperimentTarget{ &Target{ - URL: "https://1dot1dot1dot1dot.com/dns-query", - Options: &Config{}, + URL: "https://1dot1dot1dot1dot.com/dns-query", + Config: &Config{}, }, &Target{ - URL: "https://dns.cloudflare/dns-query", - Options: &Config{}, + URL: "https://dns.cloudflare/dns-query", + Config: &Config{}, }, }, }, diff --git a/internal/experiment/openvpn/openvpn.go b/internal/experiment/openvpn/openvpn.go index 0cf8255f43..23e904a9e8 100644 --- a/internal/experiment/openvpn/openvpn.go +++ b/internal/experiment/openvpn/openvpn.go @@ -227,7 +227,7 @@ func (m *Measurer) Run(ctx context.Context, args *model.ExperimentArgs) error { if !ok { return targetloading.ErrInvalidInputType } - config, input := target.Options, target.URL + config, input := target.Config, target.URL // 2. obtain the endpoint representation from the input URL endpoint, err := newEndpointFromInputString(input) diff --git a/internal/experiment/openvpn/openvpn_test.go b/internal/experiment/openvpn/openvpn_test.go index e45b52ae75..f050fab3a9 100644 --- a/internal/experiment/openvpn/openvpn_test.go +++ b/internal/experiment/openvpn/openvpn_test.go @@ -202,8 +202,8 @@ func TestBadTargetURLFailure(t *testing.T) { Measurement: measurement, Session: sess, Target: &openvpn.Target{ - URL: "openvpn://badprovider/?address=aa", - Options: &openvpn.Config{}, + URL: "openvpn://badprovider/?address=aa", + Config: &openvpn.Config{}, }, } err := m.Run(ctx, args) @@ -260,8 +260,8 @@ func TestSuccess(t *testing.T) { Measurement: measurement, Session: sess, Target: &openvpn.Target{ - URL: "openvpn://riseupvpn.corp/?address=127.0.0.1:9989&transport=tcp", - Options: &openvpn.Config{}, + URL: "openvpn://riseupvpn.corp/?address=127.0.0.1:9989&transport=tcp", + Config: &openvpn.Config{}, }, } err := m.Run(ctx, args) diff --git a/internal/experiment/openvpn/richerinput.go b/internal/experiment/openvpn/richerinput.go index 050673b359..5743865e94 100644 --- a/internal/experiment/openvpn/richerinput.go +++ b/internal/experiment/openvpn/richerinput.go @@ -4,6 +4,7 @@ import ( "context" "fmt" + "github.com/ooni/probe-cli/v3/internal/experimentconfig" "github.com/ooni/probe-cli/v3/internal/model" "github.com/ooni/probe-cli/v3/internal/reflectx" "github.com/ooni/probe-cli/v3/internal/targetloading" @@ -23,8 +24,8 @@ var providerAuthentication = map[string]AuthMethod{ // Target is a richer-input target that this experiment should measure. type Target struct { - // Options contains the configuration. - Options *Config + // Config contains the configuration. + Config *Config // URL is the input URL. URL string @@ -47,6 +48,11 @@ func (t *Target) Input() string { return t.URL } +// Options implements [model.ExperimentTarget]. +func (t *Target) Options() (options []string) { + return experimentconfig.DefaultOptionsSerializer(t.Config) +} + // String implements [model.ExperimentTarget]. func (t *Target) String() string { return t.URL @@ -96,8 +102,8 @@ func (tl *targetLoader) Load(ctx context.Context) ([]model.ExperimentTarget, err var targets []model.ExperimentTarget for _, input := range inputs { targets = append(targets, &Target{ - Options: tl.options, - URL: input, + Config: tl.options, + URL: input, }) } return targets, nil @@ -140,8 +146,8 @@ func (tl *targetLoader) loadFromBackend(ctx context.Context) ([]model.Experiment // TODO(ainghazal): implement (surfshark, etc) } targets = append(targets, &Target{ - URL: input, - Options: config, + URL: input, + Config: config, }) } diff --git a/internal/experiment/openvpn/richerinput_test.go b/internal/experiment/openvpn/richerinput_test.go index 4c04696635..3d4d74ac32 100644 --- a/internal/experiment/openvpn/richerinput_test.go +++ b/internal/experiment/openvpn/richerinput_test.go @@ -16,7 +16,7 @@ import ( func TestTarget(t *testing.T) { target := &Target{ URL: "openvpn://unknown.corp?address=1.1.1.1%3A443&transport=udp", - Options: &Config{ + Config: &Config{ Auth: "SHA512", Cipher: "AES-256-GCM", Provider: "unknown", @@ -112,7 +112,7 @@ func TestTargetLoaderLoad(t *testing.T) { expectTargets: []model.ExperimentTarget{ &Target{ URL: "openvpn://unknown.corp/1.1.1.1", - Options: &Config{ + Config: &Config{ Provider: "unknown", SafeCA: "aa", SafeCert: "bb", diff --git a/internal/experimentconfig/experimentconfig.go b/internal/experimentconfig/experimentconfig.go new file mode 100644 index 0000000000..503378142f --- /dev/null +++ b/internal/experimentconfig/experimentconfig.go @@ -0,0 +1,76 @@ +// Package experimentconfig contains code to manage experiments configuration. +package experimentconfig + +import ( + "fmt" + "reflect" + "strings" +) + +// TODO(bassosimone): we should probably move here all the code inside +// of registry used to serialize existing options and to set values from +// generic map[string]any types. + +// DefaultOptionsSerializer serializes options for [model.ExperimentTarget] +// honouring its Options method contract: +// +// 1. we do not serialize options whose name starts with "Safe"; +// +// 2. we do not serialize scalar values. +// +// This method MUST be passed a pointer to a struct. Otherwise, the return +// value will be a zero-length list (either nil or empty). +func DefaultOptionsSerializer(config any) (options []string) { + // as documented, this method MUST be passed a struct pointer + stval := reflect.ValueOf(config) + if stval.Kind() != reflect.Pointer { + return + } + stval = stval.Elem() + if stval.Kind() != reflect.Struct { + return + } + + // obtain the structure type + stt := stval.Type() + + // cycle through the struct fields + for idx := 0; idx < stval.NumField(); idx++ { + // obtain the field type and value + fieldval, fieldtype := stval.Field(idx), stt.Field(idx) + + // make sure the field is public + if !fieldtype.IsExported() { + continue + } + + // make sure the file name does not start with Safe + if strings.HasPrefix(fieldtype.Name, "Safe") { + continue + } + + // add the field iff it's a scalar + switch fieldval.Kind() { + case reflect.Bool, + reflect.Int, + reflect.Int8, + reflect.Int16, + reflect.Int32, + reflect.Int64, + reflect.Uint, + reflect.Uint8, + reflect.Uint16, + reflect.Uint32, + reflect.Uint64, + reflect.Float32, + reflect.Float64, + reflect.String: + options = append(options, fmt.Sprintf("%s=%s", fieldtype.Name, fieldval.Interface())) + + default: + // nothing + } + } + + return +} diff --git a/internal/model/experiment.go b/internal/model/experiment.go index ad4e0dfcfc..666718c438 100644 --- a/internal/model/experiment.go +++ b/internal/model/experiment.go @@ -99,6 +99,18 @@ type ExperimentTarget interface { // Input returns the experiment input, which is typically a URL. Input() string + // Options transforms the options contained by this target + // into a []string containing options as they were provided + // using the command line `-O option=value` syntax. + // + // This method MUST NOT serialize all the options whose name + // starts with the "Safe" prefix. This method MAY skip serializing + // sensitive options and options we cannot serialize into a list + // of strings (e.g., objects and lists). + // + // Consider using the [experimentconfig] package to serialize. + Options() []string + // String MUST return the experiment input. // // Implementation note: previously existing code often times treated diff --git a/internal/model/ooapi.go b/internal/model/ooapi.go index 3750cece17..51cc49af5a 100644 --- a/internal/model/ooapi.go +++ b/internal/model/ooapi.go @@ -224,6 +224,19 @@ func (o *OOAPIURLInfo) Input() string { return o.URL } +// Options implements ExperimentTarget. +func (o *OOAPIURLInfo) Options() []string { + // Implementation note: we're not serializing any options for now. If/when + // we do that, remember the Options contract: + // + // 1. skip options whose name begins with "Safe"; + // + // 2. skip options that are not scalars. + // + // Consider using the [experimentconfig] package to serialize. + return nil +} + // String implements [ExperimentTarget]. func (o *OOAPIURLInfo) String() string { return o.URL diff --git a/internal/oonirun/experiment.go b/internal/oonirun/experiment.go index a660935244..505c943fc1 100644 --- a/internal/oonirun/experiment.go +++ b/internal/oonirun/experiment.go @@ -92,11 +92,6 @@ func (ed *Experiment) Run(ctx context.Context) error { return err } - // TODO(bassosimone): we need another patch after the current one - // to correctly serialize the options as configured using InitialOptions - // and ExtraOptions otherwise the Measurement.Options field turns out - // to always be empty and this is highly suboptimal for us. - // 2. configure experiment's options // // We first unmarshal the InitialOptions into the experiment @@ -121,7 +116,7 @@ func (ed *Experiment) Run(ctx context.Context) error { // 4. randomize input, if needed if ed.Random { - // Note: since go1.20 the default random generated is random seeded + // Note: since go1.20 the default random generator is randomly seeded // // See https://tip.golang.org/doc/go1.20 rand.Shuffle(len(targetList), func(i, j int) { @@ -177,7 +172,6 @@ func (ed *Experiment) newInputProcessor(experiment model.Experiment, }, Inputs: inputList, MaxRuntime: time.Duration(ed.MaxRuntime) * time.Second, - Options: experimentOptionsToStringList(ed.ExtraOptions), Saver: NewInputProcessorSaverWrapper(saver), Submitter: &experimentSubmitterWrapper{ child: NewInputProcessorSubmitterWrapper(submitter), diff --git a/internal/oonirun/inputprocessor.go b/internal/oonirun/inputprocessor.go index d4e292fd63..79ef4380e2 100644 --- a/internal/oonirun/inputprocessor.go +++ b/internal/oonirun/inputprocessor.go @@ -55,9 +55,6 @@ type InputProcessor struct { // there will be no MaxRuntime limit. MaxRuntime time.Duration - // Options contains command line options for this experiment. - Options []string - // Saver is the code that will save measurement results // on persistent storage (e.g. the file system). Saver InputProcessorSaverWrapper @@ -144,9 +141,10 @@ func (ip *InputProcessor) run(ctx context.Context) (int, error) { return 0, err } meas.AddAnnotations(ip.Annotations) - meas.Options = ip.Options err = ip.Submitter.Submit(ctx, idx, meas) if err != nil { + // TODO(bassosimone): the default submitter used by + // miniooni ignores errors. Should we make it the default? return 0, err } // Note: must be after submission because submission modifies diff --git a/internal/oonirun/inputprocessor_test.go b/internal/oonirun/inputprocessor_test.go index a427b5126f..b6f97f453b 100644 --- a/internal/oonirun/inputprocessor_test.go +++ b/internal/oonirun/inputprocessor_test.go @@ -71,7 +71,6 @@ func TestInputProcessorSubmissionFailed(t *testing.T) { Inputs: []model.ExperimentTarget{ model.NewOOAPIURLInfoWithDefaultCategoryAndCountry("https://www.kernel.org/"), }, - Options: []string{"fake=true"}, Submitter: NewInputProcessorSubmitterWrapper( &FakeInputProcessorSubmitter{Err: expected}, ), @@ -120,7 +119,6 @@ func TestInputProcessorSaveOnDiskFailed(t *testing.T) { Inputs: []model.ExperimentTarget{ model.NewOOAPIURLInfoWithDefaultCategoryAndCountry("https://www.kernel.org/"), }, - Options: []string{"fake=true"}, Saver: NewInputProcessorSaverWrapper( &FakeInputProcessorSaver{Err: expected}, ), @@ -144,7 +142,6 @@ func TestInputProcessorGood(t *testing.T) { model.NewOOAPIURLInfoWithDefaultCategoryAndCountry("https://www.kernel.org/"), model.NewOOAPIURLInfoWithDefaultCategoryAndCountry("https://www.slashdot.org/"), }, - Options: []string{"fake=true"}, Saver: NewInputProcessorSaverWrapper(saver), Submitter: NewInputProcessorSubmitterWrapper(submitter), } @@ -186,7 +183,6 @@ func TestInputProcessorMaxRuntime(t *testing.T) { model.NewOOAPIURLInfoWithDefaultCategoryAndCountry("https://www.slashdot.org/"), }, MaxRuntime: 1 * time.Nanosecond, - Options: []string{"fake=true"}, Saver: NewInputProcessorSaverWrapper(saver), Submitter: NewInputProcessorSubmitterWrapper(submitter), } From 45a18b75a4a3b7163b664bbad1f067b4c09d6460 Mon Sep 17 00:00:00 2001 From: Simone Basso Date: Wed, 26 Jun 2024 09:38:40 +0200 Subject: [PATCH 26/63] x --- internal/experimentconfig/experimentconfig.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/internal/experimentconfig/experimentconfig.go b/internal/experimentconfig/experimentconfig.go index 503378142f..eb0f9b91ea 100644 --- a/internal/experimentconfig/experimentconfig.go +++ b/internal/experimentconfig/experimentconfig.go @@ -16,7 +16,7 @@ import ( // // 1. we do not serialize options whose name starts with "Safe"; // -// 2. we do not serialize scalar values. +// 2. we only serialize scalar values. // // This method MUST be passed a pointer to a struct. Otherwise, the return // value will be a zero-length list (either nil or empty). From 50d1909cd84f74d6a85660a5dde2687f98c6f0de Mon Sep 17 00:00:00 2001 From: Simone Basso Date: Thu, 27 Jun 2024 12:01:15 +0400 Subject: [PATCH 27/63] chore: add more tests for new code I wrote --- internal/engine/experimentbuilder.go | 6 ++ internal/engine/experimentbuilder_test.go | 118 ++++++++++++++++++++++ internal/model/experiment.go | 3 + internal/registry/factory.go | 8 +- internal/registry/factory_test.go | 24 ++++- 5 files changed, 155 insertions(+), 4 deletions(-) diff --git a/internal/engine/experimentbuilder.go b/internal/engine/experimentbuilder.go index 591197f0c5..7c9469a856 100644 --- a/internal/engine/experimentbuilder.go +++ b/internal/engine/experimentbuilder.go @@ -11,6 +11,12 @@ import ( "github.com/ooni/probe-cli/v3/internal/registry" ) +// TODO(bassosimone,DecFox): we should eventually finish merging the code in +// file with the code inside the ./internal/registry package. +// +// If there's time, this could happen at the end of the current (as of 2024-06-27) +// richer input work, otherwise any time in the future is actually fine. + // experimentBuilder implements [model.ExperimentBuilder]. // // This type is now just a tiny wrapper around registry.Factory. diff --git a/internal/engine/experimentbuilder_test.go b/internal/engine/experimentbuilder_test.go index d81e0bb7bb..6d1c5e4f83 100644 --- a/internal/engine/experimentbuilder_test.go +++ b/internal/engine/experimentbuilder_test.go @@ -2,9 +2,11 @@ package engine import ( "context" + "encoding/json" "errors" "testing" + "github.com/google/go-cmp/cmp" "github.com/ooni/probe-cli/v3/internal/model" ) @@ -49,3 +51,119 @@ func TestExperimentBuilderEngineWebConnectivity(t *testing.T) { t.Fatal("expected zero length targets") } } + +func TestExperimentBuilderBasicOperations(t *testing.T) { + // create a session for testing that does not use the network at all + sess := newSessionForTestingNoLookups(t) + + // create an experiment builder for example + builder, err := sess.NewExperimentBuilder("example") + if err != nil { + t.Fatal(err) + } + + // example should be interruptible + t.Run("Interruptible", func(t *testing.T) { + if !builder.Interruptible() { + t.Fatal("example should be interruptible") + } + }) + + // we expect to see the InputNone input policy + t.Run("InputPolicy", func(t *testing.T) { + if builder.InputPolicy() != model.InputNone { + t.Fatal("unexpectyed input policy") + } + }) + + // get the options and check whether they are what we expect + t.Run("Options", func(t *testing.T) { + options, err := builder.Options() + if err != nil { + t.Fatal(err) + } + expectOptions := map[string]model.ExperimentOptionInfo{ + "Message": {Doc: "Message to emit at test completion", Type: "string", Value: "Good day from the example experiment!"}, + "ReturnError": {Doc: "Toogle to return a mocked error", Type: "bool", Value: false}, + "SleepTime": {Doc: "Amount of time to sleep for in nanosecond", Type: "int64", Value: int64(1000000000)}, + } + if diff := cmp.Diff(expectOptions, options); diff != "" { + t.Fatal(diff) + } + }) + + // we can set a specific existing option + t.Run("SetOptionAny", func(t *testing.T) { + if err := builder.SetOptionAny("Message", "foobar"); err != nil { + t.Fatal(err) + } + options, err := builder.Options() + if err != nil { + t.Fatal(err) + } + expectOptions := map[string]model.ExperimentOptionInfo{ + "Message": {Doc: "Message to emit at test completion", Type: "string", Value: "foobar"}, + "ReturnError": {Doc: "Toogle to return a mocked error", Type: "bool", Value: false}, + "SleepTime": {Doc: "Amount of time to sleep for in nanosecond", Type: "int64", Value: int64(1000000000)}, + } + if diff := cmp.Diff(expectOptions, options); diff != "" { + t.Fatal(diff) + } + }) + + // we can set all options at the same time + t.Run("SetOptions", func(t *testing.T) { + inputs := map[string]any{ + "Message": "foobar", + "ReturnError": true, + } + if err := builder.SetOptionsAny(inputs); err != nil { + t.Fatal(err) + } + options, err := builder.Options() + if err != nil { + t.Fatal(err) + } + expectOptions := map[string]model.ExperimentOptionInfo{ + "Message": {Doc: "Message to emit at test completion", Type: "string", Value: "foobar"}, + "ReturnError": {Doc: "Toogle to return a mocked error", Type: "bool", Value: true}, + "SleepTime": {Doc: "Amount of time to sleep for in nanosecond", Type: "int64", Value: int64(1000000000)}, + } + if diff := cmp.Diff(expectOptions, options); diff != "" { + t.Fatal(diff) + } + }) + + // we can set all options using JSON + t.Run("SetOptionsJSON", func(t *testing.T) { + inputs := json.RawMessage(`{ + "Message": "foobar", + "ReturnError": true + }`) + if err := builder.SetOptionsJSON(inputs); err != nil { + t.Fatal(err) + } + options, err := builder.Options() + if err != nil { + t.Fatal(err) + } + expectOptions := map[string]model.ExperimentOptionInfo{ + "Message": {Doc: "Message to emit at test completion", Type: "string", Value: "foobar"}, + "ReturnError": {Doc: "Toogle to return a mocked error", Type: "bool", Value: true}, + "SleepTime": {Doc: "Amount of time to sleep for in nanosecond", Type: "int64", Value: int64(1000000000)}, + } + if diff := cmp.Diff(expectOptions, options); diff != "" { + t.Fatal(diff) + } + }) + + // TODO(bassosimone): we could possibly add more checks here. I am not doing this + // right now, because https://github.com/ooni/probe-cli/pull/1629 mostly cares about + // providing input and the rest of the codebase did not change. + // + // Also, it would make sense to eventually merge experimentbuilder.go with the + // ./internal/registry package, which also has coverage. + // + // In conclusion, our main objective for now is to make sure we don't screw the + // pooch when setting options using the experiment builder. +} diff --git a/internal/model/experiment.go b/internal/model/experiment.go index ad4e0dfcfc..b514df3dec 100644 --- a/internal/model/experiment.go +++ b/internal/model/experiment.go @@ -297,6 +297,9 @@ type ExperimentOptionInfo struct { // Type contains the type. Type string + + // Value contains the current option value. + Value any } // ExperimentTargetLoader loads targets from local or remote sources. diff --git a/internal/registry/factory.go b/internal/registry/factory.go index 9c5ffac30a..5c54183250 100644 --- a/internal/registry/factory.go +++ b/internal/registry/factory.go @@ -111,15 +111,17 @@ func (b *Factory) Options() (map[string]model.ExperimentOptionInfo, error) { if ptrinfo.Kind() != reflect.Ptr { return nil, ErrConfigIsNotAStructPointer } - structinfo := ptrinfo.Elem().Type() + valueinfo := ptrinfo.Elem() + structinfo := valueinfo.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(), + Doc: field.Tag.Get("ooni"), + Type: field.Type.String(), + Value: valueinfo.Field(i).Interface(), } } return result, nil diff --git a/internal/registry/factory_test.go b/internal/registry/factory_test.go index c92f031577..998d10f1ac 100644 --- a/internal/registry/factory_test.go +++ b/internal/registry/factory_test.go @@ -55,7 +55,12 @@ func TestExperimentBuilderOptions(t *testing.T) { }) t.Run("when config is a pointer to struct", func(t *testing.T) { - config := &fakeExperimentConfig{} + config := &fakeExperimentConfig{ + Chan: make(chan any), + String: "foobar", + Truth: true, + Value: 177114, + } b := &Factory{ config: config, } @@ -63,6 +68,7 @@ func TestExperimentBuilderOptions(t *testing.T) { if err != nil { t.Fatal(err) } + for name, value := range options { switch name { case "Chan": @@ -72,6 +78,10 @@ func TestExperimentBuilderOptions(t *testing.T) { if value.Type != "chan interface {}" { t.Fatal("invalid type", value.Type) } + if value.Value.(chan any) == nil { + t.Fatal("expected non-nil channel here") + } + case "String": if value.Doc != "a string" { t.Fatal("invalid doc") @@ -79,6 +89,10 @@ func TestExperimentBuilderOptions(t *testing.T) { if value.Type != "string" { t.Fatal("invalid type", value.Type) } + if v := value.Value.(string); v != "foobar" { + t.Fatal("unexpected string value", v) + } + case "Truth": if value.Doc != "something that no-one knows" { t.Fatal("invalid doc") @@ -86,6 +100,10 @@ func TestExperimentBuilderOptions(t *testing.T) { if value.Type != "bool" { t.Fatal("invalid type", value.Type) } + if v := value.Value.(bool); !v { + t.Fatal("unexpected bool value", v) + } + case "Value": if value.Doc != "a number" { t.Fatal("invalid doc") @@ -93,6 +111,10 @@ func TestExperimentBuilderOptions(t *testing.T) { if value.Type != "int64" { t.Fatal("invalid type", value.Type) } + if v := value.Value.(int64); v != 177114 { + t.Fatal("unexpected int64 value", v) + } + default: t.Fatal("unknown name", name) } From cf55bfb71ea1006747e74e0153c239df910aa9d1 Mon Sep 17 00:00:00 2001 From: Simone Basso Date: Thu, 27 Jun 2024 12:04:45 +0400 Subject: [PATCH 28/63] x --- internal/mocks/experimentbuilder_test.go | 14 ++++++++++++++ 1 file changed, 14 insertions(+) diff --git a/internal/mocks/experimentbuilder_test.go b/internal/mocks/experimentbuilder_test.go index 55ba1783f9..7e90fff66b 100644 --- a/internal/mocks/experimentbuilder_test.go +++ b/internal/mocks/experimentbuilder_test.go @@ -1,6 +1,7 @@ package mocks import ( + "encoding/json" "errors" "testing" @@ -72,6 +73,19 @@ func TestExperimentBuilder(t *testing.T) { } }) + t.Run("SetOptionsJSON", func(t *testing.T) { + expected := errors.New("mocked error") + eb := &ExperimentBuilder{ + MockSetOptionsJSON: func(value json.RawMessage) error { + return expected + }, + } + err := eb.SetOptionsJSON([]byte(`{}`)) + if !errors.Is(err, expected) { + t.Fatal("unexpected value") + } + }) + t.Run("SetCallbacks", func(t *testing.T) { var called bool eb := &ExperimentBuilder{ From c6dacf7b40e3430609271edcde6f3f8f972eddb4 Mon Sep 17 00:00:00 2001 From: Simone Basso Date: Thu, 27 Jun 2024 12:05:17 +0400 Subject: [PATCH 29/63] x --- internal/mocks/experimentbuilder.go | 1 - 1 file changed, 1 deletion(-) diff --git a/internal/mocks/experimentbuilder.go b/internal/mocks/experimentbuilder.go index a9cd880ba5..bcb9652745 100644 --- a/internal/mocks/experimentbuilder.go +++ b/internal/mocks/experimentbuilder.go @@ -50,7 +50,6 @@ func (eb *ExperimentBuilder) SetOptionsAny(options map[string]any) error { } func (eb *ExperimentBuilder) SetOptionsJSON(value json.RawMessage) error { - // TODO(bassosimone): write unit tests for this method return eb.MockSetOptionsJSON(value) } From 76216433a6c4c6291c60ab7840687c91a2a307cb Mon Sep 17 00:00:00 2001 From: Simone Basso Date: Thu, 27 Jun 2024 12:10:35 +0400 Subject: [PATCH 30/63] x --- internal/oonirun/experiment.go | 2 ++ internal/oonirun/experiment_test.go | 30 ++++++++++++++++++++++++----- 2 files changed, 27 insertions(+), 5 deletions(-) diff --git a/internal/oonirun/experiment.go b/internal/oonirun/experiment.go index a660935244..ddd1b36ca6 100644 --- a/internal/oonirun/experiment.go +++ b/internal/oonirun/experiment.go @@ -96,6 +96,8 @@ func (ed *Experiment) Run(ctx context.Context) error { // to correctly serialize the options as configured using InitialOptions // and ExtraOptions otherwise the Measurement.Options field turns out // to always be empty and this is highly suboptimal for us. + // + // The next patch is https://github.com/ooni/probe-cli/pull/1630. // 2. configure experiment's options // diff --git a/internal/oonirun/experiment_test.go b/internal/oonirun/experiment_test.go index 2fe651119e..e29cb0bf70 100644 --- a/internal/oonirun/experiment_test.go +++ b/internal/oonirun/experiment_test.go @@ -42,14 +42,14 @@ func TestExperimentRunWithFailureToSubmitAndShuffle(t *testing.T) { MockInputPolicy: func() model.InputPolicy { return model.InputOptional }, - MockSetOptionsAny: func(options map[string]any) error { - calledSetOptionsAny++ - return nil - }, MockSetOptionsJSON: func(value json.RawMessage) error { calledSetOptionsJSON++ return nil }, + MockSetOptionsAny: func(options map[string]any) error { + calledSetOptionsAny++ + return nil + }, MockNewExperiment: func() model.Experiment { exp := &mocks.Experiment{ MockMeasureWithContext: func( @@ -206,13 +206,33 @@ func TestExperimentRun(t *testing.T) { }, args: args{}, expectErr: errMocked, + }, { + name: "cannot set InitialOptions", + fields: fields{ + newExperimentBuilderFn: func(experimentName string) (model.ExperimentBuilder, error) { + eb := &mocks.ExperimentBuilder{ + MockSetOptionsJSON: func(value json.RawMessage) error { + return errMocked + }, + } + return eb, nil + }, + newTargetLoaderFn: func(builder model.ExperimentBuilder) targetLoader { + return &mocks.ExperimentTargetLoader{ + MockLoad: func(ctx context.Context) ([]model.ExperimentTarget, error) { + return []model.ExperimentTarget{}, nil + }, + } + }, + }, + args: args{}, + expectErr: errMocked, }, { name: "cannot set ExtraOptions", fields: fields{ newExperimentBuilderFn: func(experimentName string) (model.ExperimentBuilder, error) { eb := &mocks.ExperimentBuilder{ MockSetOptionsJSON: func(value json.RawMessage) error { - // TODO(bassosimone): need a test case before this one return nil }, MockSetOptionsAny: func(options map[string]any) error { From 4575f5e15f6f94d778abc6bfd3e075e46fa96720 Mon Sep 17 00:00:00 2001 From: Simone Basso Date: Thu, 27 Jun 2024 12:12:18 +0400 Subject: [PATCH 31/63] x --- internal/oonirun/v1_test.go | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/internal/oonirun/v1_test.go b/internal/oonirun/v1_test.go index 8ea9b05139..b1109a8efa 100644 --- a/internal/oonirun/v1_test.go +++ b/internal/oonirun/v1_test.go @@ -24,10 +24,10 @@ func newMinimalFakeSession() *mocks.Session { MockInputPolicy: func() model.InputPolicy { return model.InputNone }, - MockSetOptionsAny: func(options map[string]any) error { + MockSetOptionsJSON: func(value json.RawMessage) error { return nil }, - MockSetOptionsJSON: func(value json.RawMessage) error { + MockSetOptionsAny: func(options map[string]any) error { return nil }, MockNewExperiment: func() model.Experiment { From d1f243db4b33af91eaa97ae81387156f9ce0a46c Mon Sep 17 00:00:00 2001 From: Simone Basso Date: Thu, 27 Jun 2024 12:20:59 +0400 Subject: [PATCH 32/63] x --- internal/registry/factory.go | 39 ++++++++++++++++++++++++++++-------- 1 file changed, 31 insertions(+), 8 deletions(-) diff --git a/internal/registry/factory.go b/internal/registry/factory.go index 5c54183250..879bfad478 100644 --- a/internal/registry/factory.go +++ b/internal/registry/factory.go @@ -106,24 +106,47 @@ var ( // Options returns the options exposed by this experiment. func (b *Factory) Options() (map[string]model.ExperimentOptionInfo, error) { + // create the result value result := make(map[string]model.ExperimentOptionInfo) + + // make sure we're dealing with a pointer ptrinfo := reflect.ValueOf(b.config) if ptrinfo.Kind() != reflect.Ptr { return nil, ErrConfigIsNotAStructPointer } + + // obtain information about the value and its type valueinfo := ptrinfo.Elem() - structinfo := valueinfo.Type() - if structinfo.Kind() != reflect.Struct { + typeinfo := valueinfo.Type() + + // make sure we're dealing with a struct + if typeinfo.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(), - Value: valueinfo.Field(i).Interface(), + + // cycle through the fields + for i := 0; i < typeinfo.NumField(); i++ { + fieldType, fieldValue := typeinfo.Field(i), valueinfo.Field(i) + + // do not include private fields into our list of fields + if !fieldType.IsExported() { + continue + } + + // skip fields that are missing an `ooni` tag + docs := fieldType.Tag.Get("ooni") + if docs == "" { + continue + } + + // create a description of this field + result[fieldType.Name] = model.ExperimentOptionInfo{ + Doc: docs, + Type: fieldType.Type.String(), + Value: fieldValue.Interface(), } } + return result, nil } From 21c62dbec37afbc50448bba8d22e240b6284e051 Mon Sep 17 00:00:00 2001 From: Simone Basso Date: Thu, 27 Jun 2024 12:25:38 +0400 Subject: [PATCH 33/63] fix: make sure with testing experiment config is always a struct --- internal/registry/factory.go | 1 - internal/registry/factory_test.go | 18 ++++++++++++++++++ internal/registry/webconnectivity.go | 4 ++-- 3 files changed, 20 insertions(+), 3 deletions(-) diff --git a/internal/registry/factory.go b/internal/registry/factory.go index 879bfad478..813e9f7a44 100644 --- a/internal/registry/factory.go +++ b/internal/registry/factory.go @@ -264,7 +264,6 @@ func (b *Factory) SetOptionsJSON(value json.RawMessage) error { // otherwise unmarshal into the configuration, which we assume // to be a pointer to a structure. - // TODO(bassosimone): make sure with testing that b.config is always a pointer. return json.Unmarshal(value, b.config) } diff --git a/internal/registry/factory_test.go b/internal/registry/factory_test.go index 998d10f1ac..bb089f7f50 100644 --- a/internal/registry/factory_test.go +++ b/internal/registry/factory_test.go @@ -6,6 +6,7 @@ import ( "fmt" "math" "os" + "reflect" "testing" "github.com/apex/log" @@ -964,3 +965,20 @@ func TestFactoryNewTargetLoader(t *testing.T) { } }) } + +func TestExperimentConfigIsAlwaysAPointerToStruct(t *testing.T) { + for name, ffunc := range AllExperiments { + t.Run(name, func(t *testing.T) { + factory := ffunc() + config := factory.config + ctype := reflect.TypeOf(config) + if ctype.Kind() != reflect.Pointer { + t.Fatal("expected a pointer") + } + ctype = ctype.Elem() + if ctype.Kind() != reflect.Struct { + t.Fatal("expected a struct") + } + }) + } +} diff --git a/internal/registry/webconnectivity.go b/internal/registry/webconnectivity.go index 470bad802b..0c2d79367d 100644 --- a/internal/registry/webconnectivity.go +++ b/internal/registry/webconnectivity.go @@ -15,11 +15,11 @@ func init() { return &Factory{ build: func(config any) model.ExperimentMeasurer { return webconnectivity.NewExperimentMeasurer( - config.(webconnectivity.Config), + *config.(*webconnectivity.Config), ) }, canonicalName: canonicalName, - config: webconnectivity.Config{}, + config: &webconnectivity.Config{}, enabledByDefault: true, interruptible: false, inputPolicy: model.InputOrQueryBackend, From b8d219ba188ef8794431f0ecde21ed96c37d0d94 Mon Sep 17 00:00:00 2001 From: Simone Basso Date: Thu, 27 Jun 2024 12:28:16 +0400 Subject: [PATCH 34/63] x --- internal/registry/factory_test.go | 19 ++++++++++++++----- 1 file changed, 14 insertions(+), 5 deletions(-) diff --git a/internal/registry/factory_test.go b/internal/registry/factory_test.go index bb089f7f50..2071f387b2 100644 --- a/internal/registry/factory_test.go +++ b/internal/registry/factory_test.go @@ -21,10 +21,17 @@ import ( ) type fakeExperimentConfig struct { + // values that should be included into the Options return value 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"` + + // values that should not be included because they're private + private int64 `ooni:"a private number"` + + // values that should not be included because they lack "ooni"'s tag + Invisible int64 } func TestExperimentBuilderOptions(t *testing.T) { @@ -57,10 +64,12 @@ func TestExperimentBuilderOptions(t *testing.T) { t.Run("when config is a pointer to struct", func(t *testing.T) { config := &fakeExperimentConfig{ - Chan: make(chan any), - String: "foobar", - Truth: true, - Value: 177114, + Chan: make(chan any), + String: "foobar", + Truth: true, + Value: 177114, + private: 55, + Invisible: 9876, } b := &Factory{ config: config, @@ -117,7 +126,7 @@ func TestExperimentBuilderOptions(t *testing.T) { } default: - t.Fatal("unknown name", name) + t.Fatal("unexpected option name", name) } } }) From d49c4db04351e29d1dbb6bfe73b8c38603727d03 Mon Sep 17 00:00:00 2001 From: Simone Basso Date: Thu, 27 Jun 2024 12:43:15 +0400 Subject: [PATCH 35/63] x --- internal/registry/factory_test.go | 109 +++++++++++++++++++++++++++++- 1 file changed, 106 insertions(+), 3 deletions(-) diff --git a/internal/registry/factory_test.go b/internal/registry/factory_test.go index 2071f387b2..ce03c15149 100644 --- a/internal/registry/factory_test.go +++ b/internal/registry/factory_test.go @@ -2,6 +2,7 @@ package registry import ( "context" + "encoding/json" "errors" "fmt" "math" @@ -34,7 +35,7 @@ type fakeExperimentConfig struct { Invisible int64 } -func TestExperimentBuilderOptions(t *testing.T) { +func TestFactoryOptions(t *testing.T) { t.Run("when config is not a pointer", func(t *testing.T) { b := &Factory{ config: 17, @@ -132,7 +133,7 @@ func TestExperimentBuilderOptions(t *testing.T) { }) } -func TestExperimentBuilderSetOptionAny(t *testing.T) { +func TestFactorySetOptionAny(t *testing.T) { var inputs = []struct { TestCaseName string InitialConfig any @@ -398,7 +399,7 @@ func TestExperimentBuilderSetOptionAny(t *testing.T) { } } -func TestExperimentBuilderSetOptionsAny(t *testing.T) { +func TestFactorySetOptionsAny(t *testing.T) { b := &Factory{config: &fakeExperimentConfig{}} t.Run("we correctly handle an empty map", func(t *testing.T) { @@ -441,6 +442,106 @@ func TestExperimentBuilderSetOptionsAny(t *testing.T) { }) } +func TestFactorySetOptionsJSON(t *testing.T) { + + // PersonRecord is a fake experiment configuration. + // + // Note how the `ooni` tag here is missing because we don't care + // about whether such a tag is present when using JSON. + type PersonRecord struct { + Name string + Age int64 + Friends []string + } + + // testcase is a test case for this function. + type testcase struct { + // name is the name of the test case + name string + + // mutableConfig is the config in which we should unmarshal the JSON + mutableConfig *PersonRecord + + // rawJSON contains the raw JSON to unmarshal into mutableConfig + rawJSON json.RawMessage + + // expectErr is the error we expect + expectErr error + + // expectRecord is what we expectRecord to see in the end + expectRecord *PersonRecord + } + + cases := []testcase{ + { + name: "we correctly accept zero-length options", + mutableConfig: &PersonRecord{ + Name: "foo", + Age: 55, + Friends: []string{"bar", "baz"}, + }, + rawJSON: []byte{}, + expectRecord: &PersonRecord{ + Name: "foo", + Age: 55, + Friends: []string{"bar", "baz"}, + }, + }, + + { + name: "we return an error on JSON parsing error", + mutableConfig: &PersonRecord{}, + rawJSON: []byte(`{`), + expectErr: errors.New("unexpected end of JSON input"), + expectRecord: &PersonRecord{}, + }, + + { + name: "we correctly unmarshal into the existing config", + mutableConfig: &PersonRecord{ + Name: "foo", + Age: 55, + Friends: []string{"bar", "baz"}, + }, + rawJSON: []byte(`{"Friends":["foo","oof"]}`), + expectErr: nil, + expectRecord: &PersonRecord{ + Name: "foo", + Age: 55, + Friends: []string{"foo", "oof"}, + }, + }, + } + + for _, tc := range cases { + t.Run(tc.name, func(t *testing.T) { + + // create the factory to use + factory := &Factory{config: tc.mutableConfig} + + // unmarshal into the mutableConfig + err := factory.SetOptionsJSON(tc.rawJSON) + + // make sure the error is the one we actually expect + switch { + case err == nil && tc.expectErr == nil: + if diff := cmp.Diff(tc.expectRecord, tc.mutableConfig); diff != "" { + t.Fatal(diff) + } + + case err != nil && tc.expectErr != nil: + if err.Error() != tc.expectErr.Error() { + t.Fatal("expected", tc.expectErr, "got", err) + } + return + + default: + t.Fatal("expected", tc.expectErr, "got", err) + } + }) + } +} + func TestNewFactory(t *testing.T) { // experimentSpecificExpectations contains expectations for an experiment type experimentSpecificExpectations struct { @@ -975,6 +1076,8 @@ func TestFactoryNewTargetLoader(t *testing.T) { }) } +// This test is important because SetOptionsJSON assumes that the experiment +// config is a struct pointer into which it is possible to write func TestExperimentConfigIsAlwaysAPointerToStruct(t *testing.T) { for name, ffunc := range AllExperiments { t.Run(name, func(t *testing.T) { From eed4d020fefa9d69856e01c08f926068d334834b Mon Sep 17 00:00:00 2001 From: Simone Basso Date: Thu, 27 Jun 2024 12:44:55 +0400 Subject: [PATCH 36/63] x --- internal/registry/factory_test.go | 1 + 1 file changed, 1 insertion(+) diff --git a/internal/registry/factory_test.go b/internal/registry/factory_test.go index ce03c15149..bd4feb84fc 100644 --- a/internal/registry/factory_test.go +++ b/internal/registry/factory_test.go @@ -528,6 +528,7 @@ func TestFactorySetOptionsJSON(t *testing.T) { if diff := cmp.Diff(tc.expectRecord, tc.mutableConfig); diff != "" { t.Fatal(diff) } + return case err != nil && tc.expectErr != nil: if err.Error() != tc.expectErr.Error() { From 0dd4e6735d56f37d26e286f15ab1f8f5b4b538ec Mon Sep 17 00:00:00 2001 From: Simone Basso Date: Thu, 27 Jun 2024 13:02:23 +0400 Subject: [PATCH 37/63] x --- internal/oonirun/experiment.go | 19 +++++---- internal/oonirun/experiment_test.go | 64 +++++++++++++++++++++++++++++ 2 files changed, 75 insertions(+), 8 deletions(-) diff --git a/internal/oonirun/experiment.go b/internal/oonirun/experiment.go index ddd1b36ca6..ff2af23c26 100644 --- a/internal/oonirun/experiment.go +++ b/internal/oonirun/experiment.go @@ -101,16 +101,9 @@ func (ed *Experiment) Run(ctx context.Context) error { // 2. configure experiment's options // - // We first unmarshal the InitialOptions into the experiment - // configuration and afterwards we modify the configuration using - // the values contained inside the ExtraOptions field. - // // This MUST happen before loading targets because the options will // possibly be used to produce richer input targets. - if err := builder.SetOptionsJSON(ed.InitialOptions); err != nil { - return err - } - if err := builder.SetOptionsAny(ed.ExtraOptions); err != nil { + if err := ed.setOptions(builder); err != nil { return err } @@ -161,6 +154,16 @@ func (ed *Experiment) Run(ctx context.Context) error { return inputProcessor.Run(ctx) } +func (ed *Experiment) setOptions(builder model.ExperimentBuilder) error { + // We first unmarshal the InitialOptions into the experiment + // configuration and afterwards we modify the configuration using + // the values contained inside the ExtraOptions field. + if err := builder.SetOptionsJSON(ed.InitialOptions); err != nil { + return err + } + return builder.SetOptionsAny(ed.ExtraOptions) +} + // inputProcessor is an alias for model.ExperimentInputProcessor type inputProcessor = model.ExperimentInputProcessor diff --git a/internal/oonirun/experiment_test.go b/internal/oonirun/experiment_test.go index e29cb0bf70..81fcdcba19 100644 --- a/internal/oonirun/experiment_test.go +++ b/internal/oonirun/experiment_test.go @@ -9,6 +9,8 @@ import ( "testing" "time" + "github.com/google/go-cmp/cmp" + "github.com/ooni/probe-cli/v3/internal/engine" "github.com/ooni/probe-cli/v3/internal/mocks" "github.com/ooni/probe-cli/v3/internal/model" "github.com/ooni/probe-cli/v3/internal/testingx" @@ -126,6 +128,68 @@ func TestExperimentRunWithFailureToSubmitAndShuffle(t *testing.T) { } } +// This test ensures that we honour InitialOptions then ExtraOptions. +func TestExperimentSetOptions(t *testing.T) { + + // create the Experiment we're using for this test + exp := &Experiment{ + ExtraOptions: map[string]any{ + "ReturnError": true, + "SleepTime": 500, + }, + InitialOptions: []byte(`{"Message": "foobar", "SleepTime": 100}`), + Name: "example", + + // TODO(bassosimone): A zero-value session works here. The proper change + // however would be to write a engine.NewExperimentBuilder factory that takes + // as input an interface for the session. This would help testing. + Session: &engine.Session{}, + } + + // create the experiment builder manually + builder, err := exp.newExperimentBuilder(exp.Name) + if err != nil { + t.Fatal(err) + } + + // invoke the method we're testing + if err := exp.setOptions(builder); err != nil { + t.Fatal(err) + } + + // obtain the options + options, err := builder.Options() + if err != nil { + t.Fatal(err) + } + + // describe what we expect to happen + // + // we basically want ExtraOptions to override InitialOptions + expect := map[string]model.ExperimentOptionInfo{ + "Message": { + Doc: "Message to emit at test completion", + Type: "string", + Value: string("foobar"), // set by InitialOptions + }, + "ReturnError": { + Doc: "Toogle to return a mocked error", + Type: "bool", + Value: bool(true), // set by ExtraOptions + }, + "SleepTime": { + Doc: "Amount of time to sleep for in nanosecond", + Type: "int64", + Value: int64(500), // set by InitialOptions, overriden by ExtraOptions + }, + } + + // make sure the result equals expectation + if diff := cmp.Diff(expect, options); diff != "" { + t.Fatal(diff) + } +} + func Test_experimentOptionsToStringList(t *testing.T) { type args struct { options map[string]any From 8f507ebb369cf6381911cc53fd2dfa95b7667ca4 Mon Sep 17 00:00:00 2001 From: Simone Basso Date: Thu, 27 Jun 2024 13:03:04 +0400 Subject: [PATCH 38/63] x --- internal/oonirun/experiment.go | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/internal/oonirun/experiment.go b/internal/oonirun/experiment.go index ff2af23c26..1fe1c70795 100644 --- a/internal/oonirun/experiment.go +++ b/internal/oonirun/experiment.go @@ -27,13 +27,13 @@ type Experiment struct { Annotations map[string]string // ExtraOptions contains OPTIONAL extra options that modify the - // default-empty experiment-specific configuration. We apply + // default experiment-specific configuration. We apply // the changes described by this field after using the InitialOptions // field to initialize the experiment-specific configuration. ExtraOptions map[string]any // InitialOptions contains an OPTIONAL [json.RawMessage] object - // used to initialize the default-empty experiment-specific + // used to initialize the default experiment-specific // configuration. After we have initialized the configuration // as such, we then apply the changes described by the ExtraOptions. InitialOptions json.RawMessage From 168e9bfecf52a49bdf422f709b8348b2cec86d2d Mon Sep 17 00:00:00 2001 From: Simone Basso Date: Thu, 27 Jun 2024 13:07:04 +0400 Subject: [PATCH 39/63] x --- internal/oonirun/experiment_test.go | 11 +++++------ 1 file changed, 5 insertions(+), 6 deletions(-) diff --git a/internal/oonirun/experiment_test.go b/internal/oonirun/experiment_test.go index 81fcdcba19..cbe24d803b 100644 --- a/internal/oonirun/experiment_test.go +++ b/internal/oonirun/experiment_test.go @@ -134,10 +134,9 @@ func TestExperimentSetOptions(t *testing.T) { // create the Experiment we're using for this test exp := &Experiment{ ExtraOptions: map[string]any{ - "ReturnError": true, - "SleepTime": 500, + "Message": "jarjarbinks", }, - InitialOptions: []byte(`{"Message": "foobar", "SleepTime": 100}`), + InitialOptions: []byte(`{"Message": "foobar", "ReturnError": true}`), Name: "example", // TODO(bassosimone): A zero-value session works here. The proper change @@ -170,17 +169,17 @@ func TestExperimentSetOptions(t *testing.T) { "Message": { Doc: "Message to emit at test completion", Type: "string", - Value: string("foobar"), // set by InitialOptions + Value: string("jarjarbinks"), // set by ExtraOptions }, "ReturnError": { Doc: "Toogle to return a mocked error", Type: "bool", - Value: bool(true), // set by ExtraOptions + Value: bool(true), // set by InitialOptions }, "SleepTime": { Doc: "Amount of time to sleep for in nanosecond", Type: "int64", - Value: int64(500), // set by InitialOptions, overriden by ExtraOptions + Value: int64(1000000000), // still the default nonzero value }, } From bfc184f3674226650b9414e63df52e789a9ecb98 Mon Sep 17 00:00:00 2001 From: Simone Basso Date: Thu, 27 Jun 2024 13:23:38 +0400 Subject: [PATCH 40/63] x --- internal/registry/factory_test.go | 49 ++++++++++++++++++++++--------- 1 file changed, 35 insertions(+), 14 deletions(-) diff --git a/internal/registry/factory_test.go b/internal/registry/factory_test.go index bd4feb84fc..d4a3d8368c 100644 --- a/internal/registry/factory_test.go +++ b/internal/registry/factory_test.go @@ -21,21 +21,23 @@ import ( "github.com/ooni/probe-cli/v3/internal/targetloading" ) -type fakeExperimentConfig struct { - // values that should be included into the Options return value - 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"` - - // values that should not be included because they're private - private int64 `ooni:"a private number"` - - // values that should not be included because they lack "ooni"'s tag - Invisible int64 -} - func TestFactoryOptions(t *testing.T) { + + // the fake configuration we're using in this test + type fakeExperimentConfig struct { + // values that should be included into the Options return value + 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"` + + // values that should not be included because they're private + private int64 `ooni:"a private number"` + + // values that should not be included because they lack "ooni"'s tag + Invisible int64 + } + t.Run("when config is not a pointer", func(t *testing.T) { b := &Factory{ config: 17, @@ -134,6 +136,15 @@ func TestFactoryOptions(t *testing.T) { } func TestFactorySetOptionAny(t *testing.T) { + + // the fake configuration we're using in this test + 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"` + } + var inputs = []struct { TestCaseName string InitialConfig any @@ -400,6 +411,16 @@ func TestFactorySetOptionAny(t *testing.T) { } func TestFactorySetOptionsAny(t *testing.T) { + + // the fake configuration we're using in this test + type fakeExperimentConfig struct { + // values that should be included into the Options return value + 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"` + } + b := &Factory{config: &fakeExperimentConfig{}} t.Run("we correctly handle an empty map", func(t *testing.T) { From 43ef9bfa87e3b1704c77591c0ca54d16f367f637 Mon Sep 17 00:00:00 2001 From: Simone Basso Date: Thu, 27 Jun 2024 14:46:28 +0400 Subject: [PATCH 41/63] x --- internal/engine/experiment.go | 4 + internal/engine/experiment_test.go | 86 +++++++++++++++++++ internal/experimentconfig/experimentconfig.go | 11 ++- internal/model/experiment.go | 5 +- 4 files changed, 100 insertions(+), 6 deletions(-) diff --git a/internal/engine/experiment.go b/internal/engine/experiment.go index 5061ec9664..42a2373579 100644 --- a/internal/engine/experiment.go +++ b/internal/engine/experiment.go @@ -46,6 +46,10 @@ func (emr *experimentMutableReport) Get() (report probeservices.ReportChannel) { return } +// TODO(bassosimone,DecFox): it would be nice if `*experiment` depended +// on an interface rather than depending on the concrete session, because +// that will allow us to write tests using mocks much more easily. + // experiment implements [model.Experiment]. type experiment struct { byteCounter *bytecounter.Counter diff --git a/internal/engine/experiment_test.go b/internal/engine/experiment_test.go index ceab79d7bb..c9be288df7 100644 --- a/internal/engine/experiment_test.go +++ b/internal/engine/experiment_test.go @@ -1,9 +1,14 @@ package engine import ( + "sync" "testing" + "time" + "github.com/google/go-cmp/cmp" + "github.com/ooni/probe-cli/v3/internal/bytecounter" "github.com/ooni/probe-cli/v3/internal/enginelocate" + "github.com/ooni/probe-cli/v3/internal/experiment/dnscheck" "github.com/ooni/probe-cli/v3/internal/experiment/example" "github.com/ooni/probe-cli/v3/internal/experiment/signal" "github.com/ooni/probe-cli/v3/internal/model" @@ -105,3 +110,84 @@ func TestExperimentMeasurementSummaryKeys(t *testing.T) { } }) } + +// This test ensures that (*experiment).newMeasurement is working as intended. +func TestExperimentNewMeasurement(t *testing.T) { + // create a session for testing that does not use the network at all + sess := newSessionForTestingNoLookups(t) + + // create a conventional time for starting the experiment + t0 := time.Date(2024, 6, 27, 10, 33, 0, 0, time.UTC) + + // create the experiment + exp := &experiment{ + byteCounter: bytecounter.New(), + callbacks: model.NewPrinterCallbacks(model.DiscardLogger), + measurer: &dnscheck.Measurer{}, + mrep: &experimentMutableReport{ + mu: sync.Mutex{}, + report: nil, + }, + session: sess, + testName: "dnscheck", + testStartTime: t0.Format(model.MeasurementDateFormat), + testVersion: "0.1.0", + } + + // create the richer input target + target := &dnscheck.Target{ + Config: &dnscheck.Config{ + DefaultAddrs: "8.8.8.8 2001:4860:4860::8888", + HTTP3Enabled: true, + }, + URL: "https://dns.google/dns-query", + } + + // create measurement + meas := exp.newMeasurement(target) + + // make sure the input is correctly serialized + t.Run("Input", func(t *testing.T) { + if meas.Input != "https://dns.google/dns-query" { + t.Fatal("unexpected meas.Input") + } + }) + + // make sure the options are correctly serialized + t.Run("Options", func(t *testing.T) { + expectOptions := []string{`DefaultAddrs=8.8.8.8 2001:4860:4860::8888`, `HTTP3Enabled=true`} + if diff := cmp.Diff(expectOptions, meas.Options); diff != "" { + t.Fatal(diff) + } + }) + + // make sure we've got the expected annotation keys + t.Run("Annotations", func(t *testing.T) { + const ( + expected = 1 << iota + got + ) + m := map[string]int{ + "architecture": expected, + "engine_name": expected, + "engine_version": expected, + "go_version": expected, + "platform": expected, + "vcs_modified": expected, + "vcs_revision": expected, + "vcs_time": expected, + "vcs_tool": expected, + } + for key := range meas.Annotations { + m[key] |= got + } + for key, value := range m { + if value != expected|got { + t.Fatal("expected", expected|got, "for", key, "got", value) + } + } + }) + + // TODO(bassosimone,DecFox): this is the correct place where to + // add more tests regarding how we create measurements. +} diff --git a/internal/experimentconfig/experimentconfig.go b/internal/experimentconfig/experimentconfig.go index eb0f9b91ea..a2e321381a 100644 --- a/internal/experimentconfig/experimentconfig.go +++ b/internal/experimentconfig/experimentconfig.go @@ -16,7 +16,9 @@ import ( // // 1. we do not serialize options whose name starts with "Safe"; // -// 2. we only serialize scalar values. +// 2. we only serialize scalar values; +// +// 3. we never serialize any zero values. // // This method MUST be passed a pointer to a struct. Otherwise, the return // value will be a zero-length list (either nil or empty). @@ -49,7 +51,7 @@ func DefaultOptionsSerializer(config any) (options []string) { continue } - // add the field iff it's a scalar + // add the field iff it's a nonzero scalar switch fieldval.Kind() { case reflect.Bool, reflect.Int, @@ -65,7 +67,10 @@ func DefaultOptionsSerializer(config any) (options []string) { reflect.Float32, reflect.Float64, reflect.String: - options = append(options, fmt.Sprintf("%s=%s", fieldtype.Name, fieldval.Interface())) + if fieldval.IsZero() { + continue + } + options = append(options, fmt.Sprintf("%s=%v", fieldtype.Name, fieldval.Interface())) default: // nothing diff --git a/internal/model/experiment.go b/internal/model/experiment.go index 202158a738..31b80f01fb 100644 --- a/internal/model/experiment.go +++ b/internal/model/experiment.go @@ -104,9 +104,8 @@ type ExperimentTarget interface { // using the command line `-O option=value` syntax. // // This method MUST NOT serialize all the options whose name - // starts with the "Safe" prefix. This method MAY skip serializing - // sensitive options and options we cannot serialize into a list - // of strings (e.g., objects and lists). + // starts with the "Safe" prefix. This method MUST skip serializing + // sensitive options, non-scalar options, and zero value options. // // Consider using the [experimentconfig] package to serialize. Options() []string From 6ca260c883d8453480938071d6e6f2952d8e8a2e Mon Sep 17 00:00:00 2001 From: Simone Basso Date: Thu, 27 Jun 2024 14:49:21 +0400 Subject: [PATCH 42/63] x --- internal/experiment/dnscheck/richerinput_test.go | 13 +++++++++++++ 1 file changed, 13 insertions(+) diff --git a/internal/experiment/dnscheck/richerinput_test.go b/internal/experiment/dnscheck/richerinput_test.go index 7d5b70ed5a..46f361cf9f 100644 --- a/internal/experiment/dnscheck/richerinput_test.go +++ b/internal/experiment/dnscheck/richerinput_test.go @@ -44,6 +44,19 @@ func TestTarget(t *testing.T) { } }) + t.Run("Options", func(t *testing.T) { + expect := []string{ + "DefaultAddrs=8.8.8.8 8.8.4.4", + "Domain=example.com", + "HTTPHost=dns.google", + "TLSServerName=dns.google.com", + "TLSVersion=TLSv1.3", + } + if diff := cmp.Diff(expect, target.Options()); diff != "" { + t.Fatal(diff) + } + }) + t.Run("String", func(t *testing.T) { if target.String() != "https://dns.google/dns-query" { t.Fatal("invalid String") From 653e54b8583a59b65c836928a6856c7e376cc7a5 Mon Sep 17 00:00:00 2001 From: Simone Basso Date: Thu, 27 Jun 2024 16:33:16 +0400 Subject: [PATCH 43/63] x --- internal/experiment/openvpn/richerinput_test.go | 11 +++++++++++ 1 file changed, 11 insertions(+) diff --git a/internal/experiment/openvpn/richerinput_test.go b/internal/experiment/openvpn/richerinput_test.go index 3d4d74ac32..6c8097f1ac 100644 --- a/internal/experiment/openvpn/richerinput_test.go +++ b/internal/experiment/openvpn/richerinput_test.go @@ -44,6 +44,17 @@ func TestTarget(t *testing.T) { } }) + t.Run("Options", func(t *testing.T) { + expect := []string{ + "Auth=SHA512", + "Cipher=AES-256-GCM", + "Provider=unknown", + } + if diff := cmp.Diff(expect, target.Options()); diff != "" { + t.Fatal(diff) + } + }) + t.Run("String", func(t *testing.T) { if target.String() != "openvpn://unknown.corp?address=1.1.1.1%3A443&transport=udp" { t.Fatal("invalid String") From c5a70e8e458c1b67ee6da37ce71597792c1f3f80 Mon Sep 17 00:00:00 2001 From: Simone Basso Date: Thu, 27 Jun 2024 17:05:40 +0400 Subject: [PATCH 44/63] x --- .../experimentconfig/experimentconfig_test.go | 175 ++++++++++++++++++ 1 file changed, 175 insertions(+) create mode 100644 internal/experimentconfig/experimentconfig_test.go diff --git a/internal/experimentconfig/experimentconfig_test.go b/internal/experimentconfig/experimentconfig_test.go new file mode 100644 index 0000000000..ad5090bcb1 --- /dev/null +++ b/internal/experimentconfig/experimentconfig_test.go @@ -0,0 +1,175 @@ +package experimentconfig + +import ( + "fmt" + "testing" + + "github.com/google/go-cmp/cmp" +) + +func TestDefaultOptionsSerializer(t *testing.T) { + // configuration is the configuration we're testing the serialization of. + // + // Note that there's no `ooni:"..."` annotation here because we have changed + // our model in https://github.com/ooni/probe-cli/pull/1629, and now this kind + // of annotations are only command-line related. + type configuration struct { + // booleans + ValBool bool + + // integers + ValInt int + ValInt8 int8 + ValInt16 int16 + ValInt32 int32 + ValInt64 int64 + + // unsigned integers + ValUint uint + ValUint8 uint8 + ValUint16 uint16 + ValUint32 uint32 + ValUint64 uint64 + + // floats + ValFloat32 float32 + ValFloat64 float64 + + // strings + ValString string + + // unexported fields we should ignore + privateInt int + privateString string + privateList []int16 + + // safe fields we should ignore + SafeBool bool + SafeInt int + SafeString string + + // non-scalar fields we should ignore + NSList []int64 + NSMap map[string]string + } + + // testcase is a test case implemented by this function + type testcase struct { + // name is the test case name + name string + + // config is the config to transform into a list of options + config any + + // expectConfigType is an extra check to make sure we're actually + // passing the correct type for the config, which is here to ensure + // that, with a nil pointer to struct, we're not crashing. We need + // some extra case here because of how the Go type system work, + // and specifically we want to be sure we're passing an any containing + // a tuple like (type=*configuration,value=nil). + // + // See https://codefibershq.com/blog/golang-why-nil-is-not-always-nil + expectConfigType string + + // expect is the expected result + expect []string + } + + cases := []testcase{ + { + name: "we return a nil list for zero values", + expectConfigType: "*experimentconfig.configuration", + config: &configuration{}, + expect: nil, + }, + + { + name: "we return a nil list for non-pointers", + expectConfigType: "experimentconfig.configuration", + config: configuration{}, + expect: nil, + }, + + { + name: "we return a nil list for non-struct pointers", + expectConfigType: "*int64", + config: func() *int64 { + v := int64(12345) + return &v + }(), + expect: nil, + }, + + { + name: "we return a nil list for a nil struct pointer", + expectConfigType: "*experimentconfig.configuration", + config: func() *configuration { + return (*configuration)(nil) + }(), + expect: nil, + }, + + { + name: "we only serialize the fields that should be exported", + expectConfigType: "*experimentconfig.configuration", + config: &configuration{ + ValBool: true, + ValInt: 1, + ValInt8: 2, + ValInt16: 3, + ValInt32: 4, + ValInt64: 5, + ValUint: 6, + ValUint8: 7, + ValUint16: 8, + ValUint32: 9, + ValUint64: 10, + ValFloat32: 11, + ValFloat64: 12, + ValString: "tredici", + privateInt: 14, + privateString: "quindici", + privateList: []int16{16}, + SafeBool: true, + SafeInt: 18, + SafeString: "diciannove", + NSList: []int64{20}, + NSMap: map[string]string{"21": "22"}, + }, + expect: []string{ + "ValBool=true", + "ValInt=1", + "ValInt8=2", + "ValInt16=3", + "ValInt32=4", + "ValInt64=5", + "ValUint=6", + "ValUint8=7", + "ValUint16=8", + "ValUint32=9", + "ValUint64=10", + "ValFloat32=11", + "ValFloat64=12", + "ValString=tredici", + }, + }, + } + + for _, tc := range cases { + t.Run(tc.name, func(t *testing.T) { + // first make sure that tc.config has really the expected + // type for the reason explained in its docstring + if actual := fmt.Sprintf("%T", tc.config); actual != tc.expectConfigType { + t.Fatal("expected", tc.expectConfigType, "got", actual) + } + + // then serialize the content of the config to a list of strings + got := DefaultOptionsSerializer(tc.config) + + // finally, make sure that the result matches expectations + if diff := cmp.Diff(tc.expect, got); diff != "" { + t.Fatal(diff) + } + }) + } +} From 7f1a0c7e2266d100036fd212c6514ed67615433d Mon Sep 17 00:00:00 2001 From: Simone Basso Date: Thu, 27 Jun 2024 17:07:29 +0400 Subject: [PATCH 45/63] x --- internal/experimentconfig/experimentconfig.go | 3 +++ 1 file changed, 3 insertions(+) diff --git a/internal/experimentconfig/experimentconfig.go b/internal/experimentconfig/experimentconfig.go index a2e321381a..6174291007 100644 --- a/internal/experimentconfig/experimentconfig.go +++ b/internal/experimentconfig/experimentconfig.go @@ -24,6 +24,9 @@ import ( // value will be a zero-length list (either nil or empty). func DefaultOptionsSerializer(config any) (options []string) { // as documented, this method MUST be passed a struct pointer + // + // Implementation note: the .Elem method converts a nil + // pointer to a zero value pointee type. stval := reflect.ValueOf(config) if stval.Kind() != reflect.Pointer { return From bae507658c7fdc32f3bbc5c675230ffc25f32c6a Mon Sep 17 00:00:00 2001 From: Simone Basso Date: Thu, 27 Jun 2024 17:25:01 +0400 Subject: [PATCH 46/63] x --- internal/experimentconfig/experimentconfig.go | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/internal/experimentconfig/experimentconfig.go b/internal/experimentconfig/experimentconfig.go index 6174291007..58932b0e97 100644 --- a/internal/experimentconfig/experimentconfig.go +++ b/internal/experimentconfig/experimentconfig.go @@ -26,7 +26,7 @@ func DefaultOptionsSerializer(config any) (options []string) { // as documented, this method MUST be passed a struct pointer // // Implementation note: the .Elem method converts a nil - // pointer to a zero value pointee type. + // pointer to a zero-value pointee type. stval := reflect.ValueOf(config) if stval.Kind() != reflect.Pointer { return @@ -49,7 +49,7 @@ func DefaultOptionsSerializer(config any) (options []string) { continue } - // make sure the file name does not start with Safe + // make sure the field name does not start with "Safe" if strings.HasPrefix(fieldtype.Name, "Safe") { continue } From 9eeb1aaf2e30e12da23957431712c943102087c8 Mon Sep 17 00:00:00 2001 From: Simone Basso Date: Thu, 27 Jun 2024 17:29:25 +0400 Subject: [PATCH 47/63] x --- internal/model/ooapi_test.go | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/internal/model/ooapi_test.go b/internal/model/ooapi_test.go index aee509e859..a029581832 100644 --- a/internal/model/ooapi_test.go +++ b/internal/model/ooapi_test.go @@ -128,6 +128,10 @@ func TestOOAPIURLInfo(t *testing.T) { t.Fatal("invalid Input") } + if info.Options() != nil { + t.Fatal("invalid Options") + } + if info.String() != "https://www.facebook.com/" { t.Fatal("invalid String") } From 0005f1ec9d42e6c4d3b7783773d1058ed19fac1b Mon Sep 17 00:00:00 2001 From: Simone Basso Date: Thu, 27 Jun 2024 17:31:48 +0400 Subject: [PATCH 48/63] x --- internal/model/ooapi.go | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/internal/model/ooapi.go b/internal/model/ooapi.go index 51cc49af5a..8a0449568f 100644 --- a/internal/model/ooapi.go +++ b/internal/model/ooapi.go @@ -231,7 +231,9 @@ func (o *OOAPIURLInfo) Options() []string { // // 1. skip options whose name begins with "Safe"; // - // 2. skip options that are not scalars. + // 2. skip options that are not scalars; + // + // 3. avoid serializing zero values. // // Consider using the [experimentconfig] package to serialize. return nil From 620d4f9605dcd7976e9e2c7c26d776f6f61d8894 Mon Sep 17 00:00:00 2001 From: Simone Basso Date: Thu, 27 Jun 2024 17:39:54 +0400 Subject: [PATCH 49/63] x --- internal/oonirun/experiment.go | 18 ------------ internal/oonirun/experiment_test.go | 44 ----------------------------- internal/oonirun/inputprocessor.go | 7 +++-- 3 files changed, 5 insertions(+), 64 deletions(-) diff --git a/internal/oonirun/experiment.go b/internal/oonirun/experiment.go index 329b57492c..0435b21d15 100644 --- a/internal/oonirun/experiment.go +++ b/internal/oonirun/experiment.go @@ -7,9 +7,7 @@ package oonirun import ( "context" "encoding/json" - "fmt" "math/rand" - "strings" "sync/atomic" "time" @@ -235,22 +233,6 @@ func (ed *Experiment) newTargetLoader(builder model.ExperimentBuilder) targetLoa }) } -// experimentOptionsToStringList convers the options to []string, which is -// the format with which we include them into a OONI Measurement. The resulting -// []string will skip any option that is named with a `Safe` prefix (case -// sensitive). -func experimentOptionsToStringList(options map[string]any) (out []string) { - // the prefix to skip inclusion in the string list - safeOptionPrefix := "Safe" - for key, value := range options { - if strings.HasPrefix(key, safeOptionPrefix) { - continue - } - out = append(out, fmt.Sprintf("%s=%v", key, value)) - } - return -} - // experimentWrapper wraps an experiment and logs progress type experimentWrapper struct { // child is the child experiment wrapper diff --git a/internal/oonirun/experiment_test.go b/internal/oonirun/experiment_test.go index cbe24d803b..c840d7a175 100644 --- a/internal/oonirun/experiment_test.go +++ b/internal/oonirun/experiment_test.go @@ -4,8 +4,6 @@ import ( "context" "encoding/json" "errors" - "reflect" - "sort" "testing" "time" @@ -189,48 +187,6 @@ func TestExperimentSetOptions(t *testing.T) { } } -func Test_experimentOptionsToStringList(t *testing.T) { - type args struct { - options map[string]any - } - tests := []struct { - name string - args args - wantOut []string - }{ - { - name: "happy path: a map with three entries returns three items", - args: args{ - map[string]any{ - "foo": 1, - "bar": 2, - "baaz": 3, - }, - }, - wantOut: []string{"baaz=3", "bar=2", "foo=1"}, - }, - { - name: "an option beginning with `Safe` is skipped from the output", - args: args{ - map[string]any{ - "foo": 1, - "Safefoo": 42, - }, - }, - wantOut: []string{"foo=1"}, - }, - } - for _, tt := range tests { - t.Run(tt.name, func(t *testing.T) { - gotOut := experimentOptionsToStringList(tt.args.options) - sort.Strings(gotOut) - if !reflect.DeepEqual(gotOut, tt.wantOut) { - t.Errorf("experimentOptionsToStringList() = %v, want %v", gotOut, tt.wantOut) - } - }) - } -} - func TestExperimentRun(t *testing.T) { errMocked := errors.New("mocked error") type fields struct { diff --git a/internal/oonirun/inputprocessor.go b/internal/oonirun/inputprocessor.go index 79ef4380e2..e8f86e9583 100644 --- a/internal/oonirun/inputprocessor.go +++ b/internal/oonirun/inputprocessor.go @@ -143,8 +143,11 @@ func (ip *InputProcessor) run(ctx context.Context) (int, error) { meas.AddAnnotations(ip.Annotations) err = ip.Submitter.Submit(ctx, idx, meas) if err != nil { - // TODO(bassosimone): the default submitter used by - // miniooni ignores errors. Should we make it the default? + // TODO(bassosimone): when re-reading this code, I find it confusing that + // we return on error because I am always like "wait, this is not the right + // thing to do here". Then, I remember that the experimentSubmitterWrapper + // ignores this error and so it's like it does not exist. Maybe we should + // rewrite the code to do the right thing here. return 0, err } // Note: must be after submission because submission modifies From 7b8200083dff0793e2d0f187be8cebf8efdc3191 Mon Sep 17 00:00:00 2001 From: Simone Basso Date: Thu, 27 Jun 2024 17:52:42 +0400 Subject: [PATCH 50/63] x --- internal/oonirun/inputprocessor.go | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/internal/oonirun/inputprocessor.go b/internal/oonirun/inputprocessor.go index e8f86e9583..57b4f44898 100644 --- a/internal/oonirun/inputprocessor.go +++ b/internal/oonirun/inputprocessor.go @@ -145,9 +145,9 @@ func (ip *InputProcessor) run(ctx context.Context) (int, error) { if err != nil { // TODO(bassosimone): when re-reading this code, I find it confusing that // we return on error because I am always like "wait, this is not the right - // thing to do here". Then, I remember that the experimentSubmitterWrapper + // thing to do here". Then, I remember that the experimentSubmitterWrapper{} // ignores this error and so it's like it does not exist. Maybe we should - // rewrite the code to do the right thing here. + // rewrite the code to do the right thing here 😬😬😬. return 0, err } // Note: must be after submission because submission modifies From ddf00a7f1e83c9df8d045ee69ba8f88a92bb7770 Mon Sep 17 00:00:00 2001 From: Simone Basso Date: Thu, 27 Jun 2024 18:10:08 +0400 Subject: [PATCH 51/63] x --- internal/oonirun/inputprocessor_test.go | 3 --- 1 file changed, 3 deletions(-) diff --git a/internal/oonirun/inputprocessor_test.go b/internal/oonirun/inputprocessor_test.go index b6f97f453b..1c8ee6ca6b 100644 --- a/internal/oonirun/inputprocessor_test.go +++ b/internal/oonirun/inputprocessor_test.go @@ -95,9 +95,6 @@ func TestInputProcessorSubmissionFailed(t *testing.T) { if m.Annotations["antani"] != "antani" { t.Fatal("invalid annotation: antani") } - if len(m.Options) != 1 || m.Options[0] != "fake=true" { - t.Fatal("options not set") - } } type FakeInputProcessorSaver struct { From 418ebbda37ed772f2a1137722dcb7f26724c6043 Mon Sep 17 00:00:00 2001 From: Simone Basso Date: Fri, 28 Jun 2024 07:59:46 +0400 Subject: [PATCH 52/63] chore: update the living design document --- docs/design/dd-008-richer-input.md | 93 +++++++++++++++++++++++++++--- 1 file changed, 84 insertions(+), 9 deletions(-) diff --git a/docs/design/dd-008-richer-input.md b/docs/design/dd-008-richer-input.md index 7927d7974b..730616747d 100644 --- a/docs/design/dd-008-richer-input.md +++ b/docs/design/dd-008-richer-input.md @@ -199,6 +199,7 @@ type ExperimentTarget struct { Category() string // equivalent to OOAPIURLInfo.CategoryCode Country() string // equivalent to OOAPIURLInfo.CountryCode Input() string // equivalent to OOAPIURLInfo.URL + String() string // serializes to the input } type InputLoader = TargetLoader // we renamed to reflect the change of purpose @@ -212,6 +213,10 @@ type Experiment interface { } ``` +The `String` method is used to reduce the `ExperimentTarget` to the input string, which +allows for backwards compatibility. We can obtain a string representation of the target's +input and use it every time where previous we used the `input` string. + Note that we also renamed the `InputLoader` to `TargetLoader` to reflect the fact that we're not loading bare input anymore, rather we're loading richer input targets. @@ -361,14 +366,90 @@ wouldn't be able to cast such an interface to the `*target` type. Therefore, unconditionally casting could lead to crashes when integrating new code and generally makes for a less robust codebase. +## Implementation: add OpenVPN + +Pull request [#1625](https://github.com/ooni/probe-cli/pull/1625) added richer +input support for the `openvpn` experiment. Because this experiment already +supports richer input through the `api.dev.ooni.io` backend, we now have the +first experiment capable of using richer input. + +## Implementation: fix serializing options + +Pull request [#1630](https://github.com/ooni/probe-cli/pull/1630) adds +support for correctly serializing options. We extend the model of a richer +input target to include the following function: + +```Go +type ExperimentTarget struct { + // ... + Options() []string +} +``` + +Then we implement `Options` for every possible experiment target. There is +a default implementation in the `experimentconfig` package implementing the +default semantics that was also available before: + +1. skip fields whose name starts with `Safe`; + +2. only serialize scalar values; + +3. do not serializes any zero value. + +Additionally, we now serialize the options inside the `newMeasurement` +constructor typical of each experiment. + +## Implementation: improve passing options to experiments + +Pull request [#1629](https://github.com/ooni/probe-cli/pull/1629) modifies +the way in which the `./internal/oonirun` package loads data for experiments +such that, when using OONI Run v2, we load its `options` field as a +`json.RawMessage` rather than using a `map[string]any`. This fact is +significant because, previously, we could only unmarshal options provided +by command line, which were always scalar. With this change, instead, we +can keep backwards compatibility with respect to the command line but it's +now also possible for experiments options specified via OONI Run v2 to +provide non-scalar options. + +The key change to enable this is to modify a `*registry.Factory` type to add: + +```Go +type Factory struct { /* ... */ } + +func (fx *Factory) SetOptionsJSON(value json.RawMessage) error +``` + +In this way, we can directly assign the raw JSON to the experiment config +that is kept inside of the `*Factory` itself. + +Additionally, constructing an experiment using `*oonirun.Experiment` now +includes two options related field: + +```Go +type Experiment struct { + InitialOptions json.RawMessage // new addition + ExtraOptions map[string]any // also present before +} +``` + +Initialization of experiment options will work as follows: + +1. the per-experiment `*Factory` constructor initializes fields to their +default value, which, in most cases, SHOULD be the zero value; + +2. we update the config using `InitialOptions` unless it is empty; + +3. we update the config using `ExtraOptions` unless it is empty. + +In practice, the code would always use either `InitialOptions` or +`ExtraOptions`, but we also wanted to specify priority in case both +of them were available. + ## Next steps This is a rough sequence of next steps that we should expand as we implement additional bits of richer input and for which we need reference issues. -* setting `(*Measurement).Options` inside `(*engine.experiment).newMeasurement` -rather than inside the `oonirun` package. - * fully convert `dnscheck`'s static list to live inside `dnscheck` instead of `targetloading` and to use the proper richer input. @@ -378,15 +459,9 @@ rather than inside the `oonirun` package. * implement backend API for serving `stunreachability` richer input. - * implement backend API for serving `openvpn` richer input. - * deliver feature flags using experiment-specific richer input rather than using the check-in API (and maybe keep the caching support?). -* deliver OONI Run v2 options as a `json.RawMessage` rather than passing -through a `map[string]any` such that we can support non-scalar options when -using OONI Run v2. - * try to eliminate `InputPolicy` and instead have each experiment define its own constructor for the proper target loader, and split the implementation inside of the `targetloader` package to have multiple target loaders. From 3aadc2ad73885b0cb8a1853972a2c38e473523f3 Mon Sep 17 00:00:00 2001 From: Simone Basso Date: Fri, 28 Jun 2024 08:44:52 +0400 Subject: [PATCH 53/63] chore: start addressing already-identified TODOs --- pkg/oonimkall/taskrunner.go | 17 ++++++++++++----- 1 file changed, 12 insertions(+), 5 deletions(-) diff --git a/pkg/oonimkall/taskrunner.go b/pkg/oonimkall/taskrunner.go index 4536ce7a0c..bf59e0477d 100644 --- a/pkg/oonimkall/taskrunner.go +++ b/pkg/oonimkall/taskrunner.go @@ -179,18 +179,19 @@ func (r *runnerForTask) Run(rootCtx context.Context) { builder.SetCallbacks(&runnerCallbacks{emitter: r.emitter}) - // Load targets. Note that, for Web Connectivity, the mobile app has - // already loaded inputs and provides them as r.settings.Inputs. + // Load targets. loader := builder.NewTargetLoader(&model.ExperimentTargetLoaderConfig{ CheckInConfig: &model.OOAPICheckInConfig{ - // Not needed since the app already provides the - // inputs to use for Web Connectivity. + // TODO(bassosimone,DecFox): to correctly load Web Connectivity targets + // here we need to honour the relevant check-in settings. }, Session: sess, StaticInputs: r.settings.Inputs, SourceFiles: []string{}, }) - targets, err := loader.Load(rootCtx) + loadCtx, loadCancel := context.WithTimeout(rootCtx, 30*time.Second) + defer loadCancel() + targets, err := loader.Load(loadCtx) if err != nil { r.emitter.EmitFailureStartup(err.Error()) return @@ -221,6 +222,12 @@ func (r *runnerForTask) Run(rootCtx context.Context) { // a zero timeout is actually valid. Since it does not make much // sense, here we're changing the behaviour. // + // Additionally, since https://github.com/ooni/probe-cli/pull/1620, + // we honour the MaxRuntime for all experiments that have more + // than one input. Previously, it was just Web Connectivity, yet, + // it seems reasonable to honour MaxRuntime everytime the whole + // experiment runtime depends on more than one input. + // // See https://github.com/measurement-kit/measurement-kit/issues/1922 if r.settings.Options.MaxRuntime > 0 && len(targets) > 1 { var ( From 4ccbce3621c63d4b92cf4fbce03c24ba6bff0ec2 Mon Sep 17 00:00:00 2001 From: Simone Basso Date: Fri, 28 Jun 2024 08:48:13 +0400 Subject: [PATCH 54/63] doc: update the living design document --- docs/design/dd-008-richer-input.md | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/docs/design/dd-008-richer-input.md b/docs/design/dd-008-richer-input.md index 730616747d..b285042b63 100644 --- a/docs/design/dd-008-richer-input.md +++ b/docs/design/dd-008-richer-input.md @@ -466,11 +466,12 @@ than using the check-in API (and maybe keep the caching support?). its own constructor for the proper target loader, and split the implementation inside of the `targetloader` package to have multiple target loaders. - * rework `pkg/oonimkall` to invoke a target loader rather than relying - on the `InputPolicy` - * make sure richer-input-enabled experiments can run with `oonimkall` after we have performed the previous change + * make sure we're passing the correct check-in settings to `oonimkall` + such that it's possible to run Web Connectivity from mobile using + the loader and we can simplify the mobile app codebase + * devise long term strategy for delivering richer input to `oonimkall` from mobile apps, which we'll need as soon as we convert the IM experiments From 325cf08044bf419b60e3e4d0df951127f9a51afd Mon Sep 17 00:00:00 2001 From: Simone Basso Date: Fri, 28 Jun 2024 08:58:04 +0400 Subject: [PATCH 55/63] doc: document changes in this pull request --- docs/design/dd-008-richer-input.md | 100 +++++++++++++++++++++++++++++ 1 file changed, 100 insertions(+) diff --git a/docs/design/dd-008-richer-input.md b/docs/design/dd-008-richer-input.md index b285042b63..2529b9ef45 100644 --- a/docs/design/dd-008-richer-input.md +++ b/docs/design/dd-008-richer-input.md @@ -445,6 +445,106 @@ In practice, the code would always use either `InitialOptions` or `ExtraOptions`, but we also wanted to specify priority in case both of them were available. +## Implementation: oonimkall changes + +In [#1620](https://github.com/ooni/probe-cli/pull/1620), we started to +modify the `./pkg/oonimkall` package to support richer input. + +Before this diff, the code was not using a loader for loading inputs +for experiments, and the code roughly looked like this: + +```Go +switch builder.InputPolicy() { + case model.InputOrQueryBackend, model.InputStrictlyRequired: + if len(r.settings.Inputs) <= 0 { + r.emitter.EmitFailureStartup("no input provided") + return + } + + case model.InputOrStaticDefault: + if len(r.settings.Inputs) <= 0 { + inputs, err := targetloading.StaticBareInputForExperiment(r.settings.Name) + if err != nil { + r.emitter.EmitFailureStartup("no default static input for this experiment") + return + } + r.settings.Inputs = inputs + } + + case model.InputOptional: + if len(r.settings.Inputs) <= 0 { + r.settings.Inputs = append(r.settings.Inputs, "") + } + + default: // treat this case as engine.InputNone. + if len(r.settings.Inputs) > 0 { + r.emitter.EmitFailureStartup("experiment does not accept input") + return + } + r.settings.Inputs = append(r.settings.Inputs, "") +} +``` + +Basically, we were switching on the experiment builder's `InputPolicy` and +checking whether input was present or absent according to policy. But, we were +not *actually* loading input when needed. + +To support richer input for experiments such as `openvpn`, instead, we must +use a loader and fetch such input, as follows: + +```Go +loader := builder.NewTargetLoader(&model.ExperimentTargetLoaderConfig{ + CheckInConfig: &model.OOAPICheckInConfig{ /* not needed for now */ }, + Session: sess, + StaticInputs: r.settings.Inputs, + SourceFiles: []string{}, +}) +loadCtx, loadCancel := context.WithTimeout(rootCtx, 30*time.Second) +defer loadCancel() +targets, err := loader.Load(loadCtx) +if err != nil { + r.emitter.EmitFailureStartup(err.Error()) + return +} +``` + +After this change, we still assume the mobile app is providing us with +inputs for Web Connectivity. Because the loader honours user-provided inputs, +there's no functional change with the previous behavior. However, if there +is no input, we're going to load it using the proper mechanisms, including +using the correct backend API for the `openvpn` experiment. + +Also, to pave the way for supporting loading for Web Connectivity as well, we +need to supply the information required to populate the URLs table as part +of the `status.measurement_start` event, as follows: + +```diff + type eventMeasurementGeneric struct { ++ CategoryCode string `json:"category_code,omitempty"` ++ CountryCode string `json:"country_code,omitempty"` + Failure string `json:"failure,omitempty"` + Idx int64 `json:"idx"` + Input string `json:"input"` + JSONStr string `json:"json_str,omitempty"` + } + + + r.emitter.Emit(eventTypeStatusMeasurementStart, eventMeasurementGeneric{ ++ CategoryCode: target.Category(), ++ CountryCode: target.Country(), + Idx: int64(idx), + Input: target.Input(), + }) +``` + +By providing the `CategoryCode` and the `CountryCode`, the mobile app is now +able to correctly populate the URLs table ahead of measuring. + +Future work will address passing the correct check-in options to the +experiment runner, so that we can actually remove the mobile app source +code that invokes the check-in API, and simplify both the codebase of +the mobile app and the one of `./pkg/oonimkall`. + ## Next steps This is a rough sequence of next steps that we should expand as we implement From c4675c55501c3c28808d456afeac80cd18dec7a7 Mon Sep 17 00:00:00 2001 From: Simone Basso Date: Fri, 28 Jun 2024 09:31:16 +0400 Subject: [PATCH 56/63] doc: document the measurement algorithm No functional changes. Only adding newlines and comments. --- pkg/oonimkall/taskrunner.go | 52 ++++++++++++++++++++++++++++++++++--- 1 file changed, 49 insertions(+), 3 deletions(-) diff --git a/pkg/oonimkall/taskrunner.go b/pkg/oonimkall/taskrunner.go index bf59e0477d..4fa5785c36 100644 --- a/pkg/oonimkall/taskrunner.go +++ b/pkg/oonimkall/taskrunner.go @@ -117,6 +117,8 @@ func (r *runnerForTask) Run(rootCtx context.Context) { // // - rootCtx is the root context and is controlled by the user; // + // - loadCtx derives from rootCtx and is used to load inputs; + // // - measCtx derives from rootCtx and is possibly tied to the // maximum runtime and is used to choose when to stop measuring; // @@ -126,28 +128,36 @@ func (r *runnerForTask) Run(rootCtx context.Context) { // See https://github.com/ooni/probe/issues/2037. var logger model.Logger = newTaskLogger(r.emitter, r.settings.LogLevel) r.emitter.Emit(eventTypeStatusQueued, eventEmpty{}) + + // check whether we support the provided settings if r.hasUnsupportedSettings() { // event failureStartup already emitted return } r.emitter.Emit(eventTypeStatusStarted, eventEmpty{}) + + // create a new measurement session sess, err := r.newsession(rootCtx, logger) if err != nil { r.emitter.EmitFailureStartup(err.Error()) return } + + // make sure we emit the status.end event when we're done endEvent := new(eventStatusEnd) defer func() { _ = sess.Close() r.emitter.Emit(eventTypeStatusEnd, endEvent) }() + // create an experiment builder for the given experiment name builder, err := sess.NewExperimentBuilder(r.settings.Name) if err != nil { r.emitter.EmitFailureStartup(err.Error()) return } + // choose the proper OONI backend to use logger.Info("Looking up OONI backends... please, be patient") if err := sess.MaybeLookupBackendsContext(rootCtx); err != nil { r.emitter.EmitFailureStartup(err.Error()) @@ -155,6 +165,7 @@ func (r *runnerForTask) Run(rootCtx context.Context) { } r.emitter.EmitStatusProgress(0.1, "contacted bouncer") + // discover the probe location logger.Info("Looking up your location... please, be patient") if err := sess.MaybeLookupLocationContext(rootCtx); err != nil { r.emitter.EmitFailureGeneric(eventTypeFailureIPLookup, err.Error()) @@ -177,9 +188,10 @@ func (r *runnerForTask) Run(rootCtx context.Context) { ResolverNetworkName: sess.ResolverNetworkName(), }) + // configure the callbacks to emit events builder.SetCallbacks(&runnerCallbacks{emitter: r.emitter}) - // Load targets. + // load targets using the experiment-specific loader loader := builder.NewTargetLoader(&model.ExperimentTargetLoaderConfig{ CheckInConfig: &model.OOAPICheckInConfig{ // TODO(bassosimone,DecFox): to correctly load Web Connectivity targets @@ -197,11 +209,16 @@ func (r *runnerForTask) Run(rootCtx context.Context) { return } + // create the new experiment experiment := builder.NewExperiment() + + // make sure we account for the bytes sent and received defer func() { endEvent.DownloadedKB = experiment.KibiBytesReceived() endEvent.UploadedKB = experiment.KibiBytesSent() }() + + // open a new report if possible if !r.settings.Options.NoCollector { logger.Info("Opening report... please, be patient") if err := experiment.OpenReportContext(rootCtx); err != nil { @@ -213,11 +230,18 @@ func (r *runnerForTask) Run(rootCtx context.Context) { ReportID: experiment.ReportID(), }) } + + // create the default context for measuring measCtx, measCancel := context.WithCancel(rootCtx) defer measCancel() + + // create the default context for submitting submitCtx, submitCancel := context.WithCancel(rootCtx) defer submitCancel() + // Update measCtx and submitCtx to be timeout bound in case there's + // more than one input/target to measure. + // // This deviates a little bit from measurement-kit, for which // a zero timeout is actually valid. Since it does not make much // sense, here we're changing the behaviour. @@ -246,14 +270,19 @@ func (r *runnerForTask) Run(rootCtx context.Context) { defer cancelSubmit() } + // prepare for cycling through the targets inputCount := len(targets) start := time.Now() inflatedMaxRuntime := r.settings.Options.MaxRuntime + r.settings.Options.MaxRuntime/10 eta := start.Add(time.Duration(inflatedMaxRuntime) * time.Second) + for idx, target := range targets { + // handle the case where the time allocated for measuring has elapsed if measCtx.Err() != nil { break } + + // notify the mobile app that we are about to measure a specific target logger.Infof("Starting measurement with index %d", idx) r.emitter.Emit(eventTypeStatusMeasurementStart, eventMeasurementGeneric{ CategoryCode: target.Category(), @@ -261,6 +290,8 @@ func (r *runnerForTask) Run(rootCtx context.Context) { Idx: int64(idx), Input: target.Input(), }) + + // emit progress when there is more than one target to measure if target.Input() != "" && inputCount > 0 { var percentage float64 if r.settings.Options.MaxRuntime > 0 { @@ -274,6 +305,8 @@ func (r *runnerForTask) Run(rootCtx context.Context) { )) } + // Perform the measurement proper. + // // Richer input implementation note: in mobile, we only observe richer input // for Web Connectivity and only store this kind of input into the database and // otherwise we ignore richer input for other experiments, which are just @@ -287,11 +320,14 @@ func (r *runnerForTask) Run(rootCtx context.Context) { target, ) + // Handle the case where our time for measuring has elapsed while + // we were measuring and assume the context interrupted the measurement + // midway, so it doesn't make sense to submit it. if builder.Interruptible() && measCtx.Err() != nil { - // We want to stop here only if interruptible otherwise we want to - // submit measurement and stop at beginning of next iteration break } + + // handle the case where the measurement has failed if err != nil { r.emitter.Emit(eventTypeFailureMeasurement, eventMeasurementGeneric{ Failure: err.Error(), @@ -304,14 +340,22 @@ func (r *runnerForTask) Run(rootCtx context.Context) { // now the only valid strategy here is to continue. continue } + + // make sure the measurement contains the user-specified annotations m.AddAnnotations(r.settings.Annotations) + + // serialize the measurement to JSON (cannot fail in practice) data, err := json.Marshal(m) runtimex.PanicOnError(err, "measurement.MarshalJSON failed") + + // let the mobile app know about this measurement r.emitter.Emit(eventTypeMeasurement, eventMeasurementGeneric{ Idx: int64(idx), Input: target.Input(), JSONStr: string(data), }) + + // if possible, submit the measurement to the OONI backend if !r.settings.Options.NoCollector { logger.Info("Submitting measurement... please, be patient") err := experiment.SubmitAndUpdateMeasurementContext(submitCtx, m) @@ -323,6 +367,8 @@ func (r *runnerForTask) Run(rootCtx context.Context) { Failure: measurementSubmissionFailure(err), }) } + + // let the app know that we're done measuring this entry r.emitter.Emit(eventTypeStatusMeasurementDone, eventMeasurementGeneric{ Idx: int64(idx), Input: target.Input(), From 6f1c8e93c822ed2c4d678bc4acceec08e9fea0d8 Mon Sep 17 00:00:00 2001 From: Simone Basso Date: Fri, 28 Jun 2024 09:47:23 +0400 Subject: [PATCH 57/63] chore: attempt to upgrade all tests --- pkg/oonimkall/taskmodel.go | 4 + pkg/oonimkall/taskrunner.go | 3 + pkg/oonimkall/taskrunner_test.go | 271 ++++++------------------------- 3 files changed, 60 insertions(+), 218 deletions(-) diff --git a/pkg/oonimkall/taskmodel.go b/pkg/oonimkall/taskmodel.go index 71faea7f6d..e3df63c207 100644 --- a/pkg/oonimkall/taskmodel.go +++ b/pkg/oonimkall/taskmodel.go @@ -316,4 +316,8 @@ type settingsOptions struct { // SoftwareVersion is the software version. If this option is not // present, then the library startup will fail. SoftwareVersion string `json:"software_version,omitempty"` + + // TODO(bassosimone,DecFox): to support OONI Run v2 descriptors with + // richer input from mobile, here we also need a string-serialization + // of the descriptor options to load. } diff --git a/pkg/oonimkall/taskrunner.go b/pkg/oonimkall/taskrunner.go index 4fa5785c36..3064745b1b 100644 --- a/pkg/oonimkall/taskrunner.go +++ b/pkg/oonimkall/taskrunner.go @@ -283,6 +283,9 @@ func (r *runnerForTask) Run(rootCtx context.Context) { } // notify the mobile app that we are about to measure a specific target + // + // note that here we provide also the CategoryCode and the CountryCode + // so that the mobile app can update its URLs table here logger.Infof("Starting measurement with index %d", idx) r.emitter.Emit(eventTypeStatusMeasurementStart, eventMeasurementGeneric{ CategoryCode: target.Category(), diff --git a/pkg/oonimkall/taskrunner_test.go b/pkg/oonimkall/taskrunner_test.go index 77f2634d71..b1b49f3dde 100644 --- a/pkg/oonimkall/taskrunner_test.go +++ b/pkg/oonimkall/taskrunner_test.go @@ -10,7 +10,6 @@ import ( "github.com/ooni/probe-cli/v3/internal/engine" "github.com/ooni/probe-cli/v3/internal/mocks" "github.com/ooni/probe-cli/v3/internal/model" - "github.com/ooni/probe-cli/v3/internal/targetloading" ) func TestMeasurementSubmissionEventName(t *testing.T) { @@ -103,6 +102,10 @@ func TestTaskRunnerRun(t *testing.T) { assertCountEventsByKey(events, eventTypeFailureStartup, 1) }) + // + // Failure in creating a new measurement session: + // + t.Run("with failure when creating a new kvstore", func(t *testing.T) { runner, emitter := newRunnerForTesting() // override the kvstore builder to provoke an error @@ -166,6 +169,10 @@ func TestTaskRunnerRun(t *testing.T) { } }) + // + // Test cases where we successfully create a new measurement session: + // + type eventKeyCount struct { Key string Count int @@ -305,7 +312,7 @@ func TestTaskRunnerRun(t *testing.T) { } } - t.Run("with invalid experiment name", func(t *testing.T) { + t.Run("with invalid experiment name causing failure to create an experiment builder", func(t *testing.T) { runner, emitter := newRunnerForTesting() fake := fakeSuccessfulDeps() fake.Session.MockNewExperimentBuilder = func(name string) (model.ExperimentBuilder, error) { @@ -366,74 +373,12 @@ func TestTaskRunnerRun(t *testing.T) { t.Run("with error during target loading", func(t *testing.T) { runner, emitter := newRunnerForTesting() fake := fakeSuccessfulDeps() - fake.Builder.MockInputPolicy = func() model.InputPolicy { - return model.InputOrQueryBackend - } - runner.newSession = fake.NewSession - events := runAndCollect(runner, emitter) - reduced := reduceEventsKeysIgnoreLog(t, events) - expect := []eventKeyCount{ - {Key: eventTypeStatusQueued, Count: 1}, - {Key: eventTypeStatusStarted, Count: 1}, - {Key: eventTypeStatusProgress, Count: 3}, - {Key: eventTypeStatusGeoIPLookup, Count: 1}, - {Key: eventTypeStatusResolverLookup, Count: 1}, - {Key: eventTypeFailureStartup, Count: 1}, - {Key: eventTypeStatusEnd, Count: 1}, - } - assertReducedEventsLike(t, expect, reduced) - }) - - t.Run("with missing input and InputStrictlyRequired policy", func(t *testing.T) { - runner, emitter := newRunnerForTesting() - fake := fakeSuccessfulDeps() - fake.Builder.MockInputPolicy = func() model.InputPolicy { - return model.InputStrictlyRequired - } - runner.newSession = fake.NewSession - events := runAndCollect(runner, emitter) - reduced := reduceEventsKeysIgnoreLog(t, events) - expect := []eventKeyCount{ - {Key: eventTypeStatusQueued, Count: 1}, - {Key: eventTypeStatusStarted, Count: 1}, - {Key: eventTypeStatusProgress, Count: 3}, - {Key: eventTypeStatusGeoIPLookup, Count: 1}, - {Key: eventTypeStatusResolverLookup, Count: 1}, - {Key: eventTypeFailureStartup, Count: 1}, - {Key: eventTypeStatusEnd, Count: 1}, - } - assertReducedEventsLike(t, expect, reduced) - }) - - t.Run("with InputOrStaticDefault policy and experiment with no static input", - func(t *testing.T) { - runner, emitter := newRunnerForTesting() - runner.settings.Name = "Antani" // no input for this experiment - fake := fakeSuccessfulDeps() - fake.Builder.MockInputPolicy = func() model.InputPolicy { - return model.InputOrStaticDefault - } - runner.newSession = fake.NewSession - events := runAndCollect(runner, emitter) - reduced := reduceEventsKeysIgnoreLog(t, events) - expect := []eventKeyCount{ - {Key: eventTypeStatusQueued, Count: 1}, - {Key: eventTypeStatusStarted, Count: 1}, - {Key: eventTypeStatusProgress, Count: 3}, - {Key: eventTypeStatusGeoIPLookup, Count: 1}, - {Key: eventTypeStatusResolverLookup, Count: 1}, - {Key: eventTypeFailureStartup, Count: 1}, - {Key: eventTypeStatusEnd, Count: 1}, + fake.Builder.MockNewTargetLoader = func(config *model.ExperimentTargetLoaderConfig) model.ExperimentTargetLoader { + return &mocks.ExperimentTargetLoader{ + MockLoad: func(ctx context.Context) ([]model.ExperimentTarget, error) { + return nil, errors.New("mocked error") + }, } - assertReducedEventsLike(t, expect, reduced) - }) - - t.Run("with InputNone policy and provided input", func(t *testing.T) { - runner, emitter := newRunnerForTesting() - runner.settings.Inputs = append(runner.settings.Inputs, "https://x.org/") - fake := fakeSuccessfulDeps() - fake.Builder.MockInputPolicy = func() model.InputPolicy { - return model.InputNone } runner.newSession = fake.NewSession events := runAndCollect(runner, emitter) @@ -474,9 +419,6 @@ func TestTaskRunnerRun(t *testing.T) { t.Run("with success and just a single entry", func(t *testing.T) { runner, emitter := newRunnerForTesting() fake := fakeSuccessfulDeps() - fake.Builder.MockInputPolicy = func() model.InputPolicy { - return model.InputNone - } runner.newSession = fake.NewSession events := runAndCollect(runner, emitter) reduced := reduceEventsKeysIgnoreLog(t, events) @@ -500,9 +442,6 @@ func TestTaskRunnerRun(t *testing.T) { t.Run("with failure and just a single entry", func(t *testing.T) { runner, emitter := newRunnerForTesting() fake := fakeSuccessfulDeps() - fake.Builder.MockInputPolicy = func() model.InputPolicy { - return model.InputNone - } fake.Experiment.MockMeasureWithContext = func(ctx context.Context, target model.ExperimentTarget) (measurement *model.Measurement, err error) { return nil, errors.New("preconditions error") } @@ -530,9 +469,6 @@ func TestTaskRunnerRun(t *testing.T) { // which is what was happening in the above referenced issue. runner, emitter := newRunnerForTesting() fake := fakeSuccessfulDeps() - fake.Builder.MockInputPolicy = func() model.InputPolicy { - return model.InputNone - } fake.Experiment.MockMeasureWithContext = func(ctx context.Context, target model.ExperimentTarget) (measurement *model.Measurement, err error) { return nil, errors.New("preconditions error") } @@ -555,62 +491,25 @@ func TestTaskRunnerRun(t *testing.T) { {Key: eventTypeStatusEnd, Count: 1}, } assertReducedEventsLike(t, expect, reduced) + // TODO(bassosimone): we should probably extend this test to + // make sure we're including the annotation as well }) - t.Run("with success and explicit input provided", func(t *testing.T) { - runner, emitter := newRunnerForTesting() - runner.settings.Inputs = []string{"a", "b", "c", "d"} - fake := fakeSuccessfulDeps() - fake.Builder.MockInputPolicy = func() model.InputPolicy { - return model.InputStrictlyRequired - } - runner.newSession = fake.NewSession - events := runAndCollect(runner, emitter) - reduced := reduceEventsKeysIgnoreLog(t, events) - expect := []eventKeyCount{ - {Key: eventTypeStatusQueued, Count: 1}, - {Key: eventTypeStatusStarted, Count: 1}, - {Key: eventTypeStatusProgress, Count: 3}, - {Key: eventTypeStatusGeoIPLookup, Count: 1}, - {Key: eventTypeStatusResolverLookup, Count: 1}, - {Key: eventTypeStatusProgress, Count: 1}, - {Key: eventTypeStatusReportCreate, Count: 1}, - // - {Key: eventTypeStatusMeasurementStart, Count: 1}, - {Key: eventTypeStatusProgress, Count: 1}, - {Key: eventTypeMeasurement, Count: 1}, - {Key: eventTypeStatusMeasurementSubmission, Count: 1}, - {Key: eventTypeStatusMeasurementDone, Count: 1}, - // - {Key: eventTypeStatusMeasurementStart, Count: 1}, - {Key: eventTypeStatusProgress, Count: 1}, - {Key: eventTypeMeasurement, Count: 1}, - {Key: eventTypeStatusMeasurementSubmission, Count: 1}, - {Key: eventTypeStatusMeasurementDone, Count: 1}, - // - {Key: eventTypeStatusMeasurementStart, Count: 1}, - {Key: eventTypeStatusProgress, Count: 1}, - {Key: eventTypeMeasurement, Count: 1}, - {Key: eventTypeStatusMeasurementSubmission, Count: 1}, - {Key: eventTypeStatusMeasurementDone, Count: 1}, - // - {Key: eventTypeStatusMeasurementStart, Count: 1}, - {Key: eventTypeStatusProgress, Count: 1}, - {Key: eventTypeMeasurement, Count: 1}, - {Key: eventTypeStatusMeasurementSubmission, Count: 1}, - {Key: eventTypeStatusMeasurementDone, Count: 1}, - // - {Key: eventTypeStatusEnd, Count: 1}, - } - assertReducedEventsLike(t, expect, reduced) - }) - - t.Run("with success and InputOptional and input", func(t *testing.T) { + t.Run("with success and more than one entry", func(t *testing.T) { + inputs := []string{"a", "b", "c", "d"} runner, emitter := newRunnerForTesting() - runner.settings.Inputs = []string{"a", "b", "c", "d"} + runner.settings.Inputs = inputs // this is basically ignored because we override MockLoad fake := fakeSuccessfulDeps() - fake.Builder.MockInputPolicy = func() model.InputPolicy { - return model.InputOptional + fake.Builder.MockNewTargetLoader = func(config *model.ExperimentTargetLoaderConfig) model.ExperimentTargetLoader { + return &mocks.ExperimentTargetLoader{ + MockLoad: func(ctx context.Context) (targets []model.ExperimentTarget, err error) { + // We need to mimic wht would happen when settings.Inputs is explicitly provided + for _, input := range inputs { + targets = append(targets, model.NewOOAPIURLInfoWithDefaultCategoryAndCountry(input)) + } + return + }, + } } runner.newSession = fake.NewSession events := runAndCollect(runner, emitter) @@ -653,77 +552,22 @@ func TestTaskRunnerRun(t *testing.T) { assertReducedEventsLike(t, expect, reduced) }) - t.Run("with success and InputOptional and no input", func(t *testing.T) { - runner, emitter := newRunnerForTesting() - fake := fakeSuccessfulDeps() - fake.Builder.MockInputPolicy = func() model.InputPolicy { - return model.InputOptional - } - runner.newSession = fake.NewSession - events := runAndCollect(runner, emitter) - reduced := reduceEventsKeysIgnoreLog(t, events) - expect := []eventKeyCount{ - {Key: eventTypeStatusQueued, Count: 1}, - {Key: eventTypeStatusStarted, Count: 1}, - {Key: eventTypeStatusProgress, Count: 3}, - {Key: eventTypeStatusGeoIPLookup, Count: 1}, - {Key: eventTypeStatusResolverLookup, Count: 1}, - {Key: eventTypeStatusProgress, Count: 1}, - {Key: eventTypeStatusReportCreate, Count: 1}, - // - {Key: eventTypeStatusMeasurementStart, Count: 1}, - {Key: eventTypeMeasurement, Count: 1}, - {Key: eventTypeStatusMeasurementSubmission, Count: 1}, - {Key: eventTypeStatusMeasurementDone, Count: 1}, - // - {Key: eventTypeStatusEnd, Count: 1}, - } - assertReducedEventsLike(t, expect, reduced) - }) - - t.Run("with success and InputOrStaticDefault", func(t *testing.T) { - experimentName := "DNSCheck" - runner, emitter := newRunnerForTesting() - runner.settings.Name = experimentName - fake := fakeSuccessfulDeps() - fake.Builder.MockInputPolicy = func() model.InputPolicy { - return model.InputOrStaticDefault - } - runner.newSession = fake.NewSession - events := runAndCollect(runner, emitter) - reduced := reduceEventsKeysIgnoreLog(t, events) - expect := []eventKeyCount{ - {Key: eventTypeStatusQueued, Count: 1}, - {Key: eventTypeStatusStarted, Count: 1}, - {Key: eventTypeStatusProgress, Count: 3}, - {Key: eventTypeStatusGeoIPLookup, Count: 1}, - {Key: eventTypeStatusResolverLookup, Count: 1}, - {Key: eventTypeStatusProgress, Count: 1}, - {Key: eventTypeStatusReportCreate, Count: 1}, - } - allEntries, err := targetloading.StaticBareInputForExperiment(experimentName) - if err != nil { - t.Fatal(err) - } - // write the correct entries for each expected measurement. - for idx := 0; idx < len(allEntries); idx++ { - expect = append(expect, eventKeyCount{Key: eventTypeStatusMeasurementStart, Count: 1}) - expect = append(expect, eventKeyCount{Key: eventTypeStatusProgress, Count: 1}) - expect = append(expect, eventKeyCount{Key: eventTypeMeasurement, Count: 1}) - expect = append(expect, eventKeyCount{Key: eventTypeStatusMeasurementSubmission, Count: 1}) - expect = append(expect, eventKeyCount{Key: eventTypeStatusMeasurementDone, Count: 1}) - } - expect = append(expect, eventKeyCount{Key: eventTypeStatusEnd, Count: 1}) - assertReducedEventsLike(t, expect, reduced) - }) - t.Run("with success and max runtime", func(t *testing.T) { + inputs := []string{"a", "b", "c", "d"} runner, emitter := newRunnerForTesting() - runner.settings.Inputs = []string{"a", "b", "c", "d"} + runner.settings.Inputs = inputs // this is basically ignored because we override MockLoad runner.settings.Options.MaxRuntime = 2 fake := fakeSuccessfulDeps() - fake.Builder.MockInputPolicy = func() model.InputPolicy { - return model.InputStrictlyRequired + fake.Builder.MockNewTargetLoader = func(config *model.ExperimentTargetLoaderConfig) model.ExperimentTargetLoader { + return &mocks.ExperimentTargetLoader{ + MockLoad: func(ctx context.Context) (targets []model.ExperimentTarget, err error) { + // We need to mimic wht would happen when settings.Inputs is explicitly provided + for _, input := range inputs { + targets = append(targets, model.NewOOAPIURLInfoWithDefaultCategoryAndCountry(input)) + } + return + }, + } } fake.Experiment.MockMeasureWithContext = func(ctx context.Context, target model.ExperimentTarget) (measurement *model.Measurement, err error) { time.Sleep(1 * time.Second) @@ -759,23 +603,25 @@ func TestTaskRunnerRun(t *testing.T) { }) t.Run("with interrupted experiment", func(t *testing.T) { + inputs := []string{"a", "b", "c", "d"} runner, emitter := newRunnerForTesting() - runner.settings.Inputs = []string{"a", "b", "c", "d"} + runner.settings.Inputs = inputs // this is basically ignored because we override MockLoad runner.settings.Options.MaxRuntime = 2 fake := fakeSuccessfulDeps() - fake.Builder.MockInputPolicy = func() model.InputPolicy { - return model.InputStrictlyRequired + fake.Builder.MockNewTargetLoader = func(config *model.ExperimentTargetLoaderConfig) model.ExperimentTargetLoader { + return &mocks.ExperimentTargetLoader{ + MockLoad: func(ctx context.Context) (targets []model.ExperimentTarget, err error) { + // We need to mimic wht would happen when settings.Inputs is explicitly provided + for _, input := range inputs { + targets = append(targets, model.NewOOAPIURLInfoWithDefaultCategoryAndCountry(input)) + } + return + }, + } } fake.Builder.MockInterruptible = func() bool { return true } - fake.Loader.MockLoad = func(ctx context.Context) ([]model.ExperimentTarget, error) { - targets := []model.ExperimentTarget{} - for _, input := range runner.settings.Inputs { - targets = append(targets, model.NewOOAPIURLInfoWithDefaultCategoryAndCountry(input)) - } - return targets, nil - } ctx, cancel := context.WithCancel(context.Background()) fake.Experiment.MockMeasureWithContext = func(ctx context.Context, target model.ExperimentTarget) (measurement *model.Measurement, err error) { cancel() @@ -803,21 +649,10 @@ func TestTaskRunnerRun(t *testing.T) { t.Run("with measurement submission failure", func(t *testing.T) { runner, emitter := newRunnerForTesting() - runner.settings.Inputs = []string{"a"} fake := fakeSuccessfulDeps() - fake.Builder.MockInputPolicy = func() model.InputPolicy { - return model.InputStrictlyRequired - } fake.Experiment.MockSubmitAndUpdateMeasurementContext = func(ctx context.Context, measurement *model.Measurement) error { return errors.New("cannot submit") } - fake.Loader.MockLoad = func(ctx context.Context) ([]model.ExperimentTarget, error) { - targets := []model.ExperimentTarget{} - for _, input := range runner.settings.Inputs { - targets = append(targets, model.NewOOAPIURLInfoWithDefaultCategoryAndCountry(input)) - } - return targets, nil - } runner.newSession = fake.NewSession events := runAndCollect(runner, emitter) reduced := reduceEventsKeysIgnoreLog(t, events) From 59ee9ab7c5857a9118b7c8b66d88149a65dfab5e Mon Sep 17 00:00:00 2001 From: Simone Basso Date: Fri, 28 Jun 2024 09:50:42 +0400 Subject: [PATCH 58/63] x --- pkg/oonimkall/taskrunner_test.go | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/pkg/oonimkall/taskrunner_test.go b/pkg/oonimkall/taskrunner_test.go index b1b49f3dde..fd0e6d6e0a 100644 --- a/pkg/oonimkall/taskrunner_test.go +++ b/pkg/oonimkall/taskrunner_test.go @@ -503,7 +503,7 @@ func TestTaskRunnerRun(t *testing.T) { fake.Builder.MockNewTargetLoader = func(config *model.ExperimentTargetLoaderConfig) model.ExperimentTargetLoader { return &mocks.ExperimentTargetLoader{ MockLoad: func(ctx context.Context) (targets []model.ExperimentTarget, err error) { - // We need to mimic wht would happen when settings.Inputs is explicitly provided + // We need to mimic what would happen when settings.Inputs is explicitly provided for _, input := range inputs { targets = append(targets, model.NewOOAPIURLInfoWithDefaultCategoryAndCountry(input)) } @@ -561,7 +561,7 @@ func TestTaskRunnerRun(t *testing.T) { fake.Builder.MockNewTargetLoader = func(config *model.ExperimentTargetLoaderConfig) model.ExperimentTargetLoader { return &mocks.ExperimentTargetLoader{ MockLoad: func(ctx context.Context) (targets []model.ExperimentTarget, err error) { - // We need to mimic wht would happen when settings.Inputs is explicitly provided + // We need to mimic what would happen when settings.Inputs is explicitly provided for _, input := range inputs { targets = append(targets, model.NewOOAPIURLInfoWithDefaultCategoryAndCountry(input)) } @@ -611,7 +611,7 @@ func TestTaskRunnerRun(t *testing.T) { fake.Builder.MockNewTargetLoader = func(config *model.ExperimentTargetLoaderConfig) model.ExperimentTargetLoader { return &mocks.ExperimentTargetLoader{ MockLoad: func(ctx context.Context) (targets []model.ExperimentTarget, err error) { - // We need to mimic wht would happen when settings.Inputs is explicitly provided + // We need to mimic what would happen when settings.Inputs is explicitly provided for _, input := range inputs { targets = append(targets, model.NewOOAPIURLInfoWithDefaultCategoryAndCountry(input)) } From 9fdc61ffc5a4a49890daf2a96b57b7aa0874268f Mon Sep 17 00:00:00 2001 From: Simone Basso Date: Fri, 28 Jun 2024 09:54:30 +0400 Subject: [PATCH 59/63] x --- docs/design/dd-008-richer-input.md | 2 +- internal/targetloading/targetloading.go | 15 +++------------ 2 files changed, 4 insertions(+), 13 deletions(-) diff --git a/docs/design/dd-008-richer-input.md b/docs/design/dd-008-richer-input.md index 2529b9ef45..6590fcbd0e 100644 --- a/docs/design/dd-008-richer-input.md +++ b/docs/design/dd-008-richer-input.md @@ -450,7 +450,7 @@ of them were available. In [#1620](https://github.com/ooni/probe-cli/pull/1620), we started to modify the `./pkg/oonimkall` package to support richer input. -Before this diff, the code was not using a loader for loading inputs +Before this diff, the code was not using a loader for loading targets for experiments, and the code roughly looked like this: ```Go diff --git a/internal/targetloading/targetloading.go b/internal/targetloading/targetloading.go index bdddb675f7..dc9162354c 100644 --- a/internal/targetloading/targetloading.go +++ b/internal/targetloading/targetloading.go @@ -215,10 +215,10 @@ var dnsCheckDefaultInput = []string{ var stunReachabilityDefaultInput = stuninput.AsnStunReachabilityInput() -// StaticBareInputForExperiment returns the list of strings an +// staticBareInputForExperiment returns the list of strings an // experiment should use as static input. In case there is no // static input for this experiment, we return an error. -func StaticBareInputForExperiment(name string) ([]string, error) { +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. @@ -239,7 +239,7 @@ func StaticBareInputForExperiment(name string) ([]string, error) { // staticInputForExperiment returns the static input for the given experiment // or an error if there's no static input for the experiment. func staticInputForExperiment(name string) ([]model.ExperimentTarget, error) { - return stringListToModelExperimentTarget(StaticBareInputForExperiment(name)) + return stringListToModelExperimentTarget(staticBareInputForExperiment(name)) } // loadOrStaticDefault implements the InputOrStaticDefault policy. @@ -364,15 +364,6 @@ func (il *Loader) checkIn( return &reply.Tests, nil } -// fetchOpenVPNConfig fetches vpn information for the configured providers -func (il *Loader) fetchOpenVPNConfig(ctx context.Context, provider string) (*model.OOAPIVPNProviderConfig, error) { - reply, err := il.Session.FetchOpenVPNConfig(ctx, provider, "XX") - if err != nil { - return nil, err - } - return reply, nil -} - // preventMistakes makes the code more robust with respect to any possible // integration issue where the backend returns to us URLs that don't // belong to the category codes we requested. From 5d34280f6cfb2ae142282139ca2dfdce812402ff Mon Sep 17 00:00:00 2001 From: Simone Basso Date: Fri, 28 Jun 2024 09:59:55 +0400 Subject: [PATCH 60/63] x --- pkg/oonimkall/taskrunner.go | 9 --------- 1 file changed, 9 deletions(-) diff --git a/pkg/oonimkall/taskrunner.go b/pkg/oonimkall/taskrunner.go index 3064745b1b..58876a28fb 100644 --- a/pkg/oonimkall/taskrunner.go +++ b/pkg/oonimkall/taskrunner.go @@ -309,15 +309,6 @@ func (r *runnerForTask) Run(rootCtx context.Context) { } // Perform the measurement proper. - // - // Richer input implementation note: in mobile, we only observe richer input - // for Web Connectivity and only store this kind of input into the database and - // otherwise we ignore richer input for other experiments, which are just - // treated as experimental. As such, the thinking here is that we do not care - // about *passing* richer input from desktop to mobile for some time. When - // we will care, it would most likely suffice to require the Inputs field to - // implement in Java the [model.ExperimentTarget] interface, which is something - // we can always do, since it only has string accessors. m, err := experiment.MeasureWithContext( r.contextForExperiment(measCtx, builder), target, From d0f680ad5b913987326256daa07926b294a6527a Mon Sep 17 00:00:00 2001 From: Simone Basso Date: Fri, 28 Jun 2024 10:04:35 +0400 Subject: [PATCH 61/63] x --- pkg/oonimkall/taskrunner_test.go | 1 + 1 file changed, 1 insertion(+) diff --git a/pkg/oonimkall/taskrunner_test.go b/pkg/oonimkall/taskrunner_test.go index fd0e6d6e0a..1e63c9f457 100644 --- a/pkg/oonimkall/taskrunner_test.go +++ b/pkg/oonimkall/taskrunner_test.go @@ -280,6 +280,7 @@ func TestTaskRunnerRun(t *testing.T) { Loader: &mocks.ExperimentTargetLoader{ MockLoad: func(ctx context.Context) ([]model.ExperimentTarget, error) { + // This returns a single entry, which is what dash, ndt, telegram, etc need targets := []model.ExperimentTarget{ model.NewOOAPIURLInfoWithDefaultCategoryAndCountry(""), } From f7367510e81c236de417af1a4c34e07f7b8720c4 Mon Sep 17 00:00:00 2001 From: Simone Basso Date: Fri, 28 Jun 2024 10:25:47 +0400 Subject: [PATCH 62/63] fix the test that was failing --- pkg/oonimkall/taskrunner_test.go | 18 +++++++++++++++++- 1 file changed, 17 insertions(+), 1 deletion(-) diff --git a/pkg/oonimkall/taskrunner_test.go b/pkg/oonimkall/taskrunner_test.go index 1e63c9f457..432a8b3073 100644 --- a/pkg/oonimkall/taskrunner_test.go +++ b/pkg/oonimkall/taskrunner_test.go @@ -183,7 +183,7 @@ func TestTaskRunnerRun(t *testing.T) { reduceEventsKeysIgnoreLog := func(t *testing.T, events []*event) (out []eventKeyCount) { var current eventKeyCount for _, ev := range events { - t.Log(ev) + t.Logf("%+v", ev) if ev.Key == eventTypeLog { continue } @@ -649,8 +649,24 @@ func TestTaskRunnerRun(t *testing.T) { }) t.Run("with measurement submission failure", func(t *testing.T) { + // Implementation note: this experiment needs a non-empty input otherwise the + // code will not emit a progress event when it finished measuring the input and + // we would be missing the eventTypeStatusProgress event. + inputs := []string{"a"} runner, emitter := newRunnerForTesting() + runner.settings.Inputs = inputs // this is basically ignored because we override MockLoad fake := fakeSuccessfulDeps() + fake.Builder.MockNewTargetLoader = func(config *model.ExperimentTargetLoaderConfig) model.ExperimentTargetLoader { + return &mocks.ExperimentTargetLoader{ + MockLoad: func(ctx context.Context) (targets []model.ExperimentTarget, err error) { + // We need to mimic what would happen when settings.Inputs is explicitly provided + for _, input := range inputs { + targets = append(targets, model.NewOOAPIURLInfoWithDefaultCategoryAndCountry(input)) + } + return + }, + } + } fake.Experiment.MockSubmitAndUpdateMeasurementContext = func(ctx context.Context, measurement *model.Measurement) error { return errors.New("cannot submit") } From 536f4239e0c09048b6b558e2f13bda6ee12a4aa7 Mon Sep 17 00:00:00 2001 From: Simone Basso Date: Fri, 28 Jun 2024 10:30:35 +0400 Subject: [PATCH 63/63] reference open issues --- pkg/oonimkall/taskmodel.go | 2 +- pkg/oonimkall/taskrunner.go | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/pkg/oonimkall/taskmodel.go b/pkg/oonimkall/taskmodel.go index e3df63c207..41e5b19ea6 100644 --- a/pkg/oonimkall/taskmodel.go +++ b/pkg/oonimkall/taskmodel.go @@ -317,7 +317,7 @@ type settingsOptions struct { // present, then the library startup will fail. SoftwareVersion string `json:"software_version,omitempty"` - // TODO(bassosimone,DecFox): to support OONI Run v2 descriptors with + // TODO(https://github.com/ooni/probe/issues/2767): to support OONI Run v2 descriptors with // richer input from mobile, here we also need a string-serialization // of the descriptor options to load. } diff --git a/pkg/oonimkall/taskrunner.go b/pkg/oonimkall/taskrunner.go index 58876a28fb..498bbc1fc8 100644 --- a/pkg/oonimkall/taskrunner.go +++ b/pkg/oonimkall/taskrunner.go @@ -194,7 +194,7 @@ func (r *runnerForTask) Run(rootCtx context.Context) { // load targets using the experiment-specific loader loader := builder.NewTargetLoader(&model.ExperimentTargetLoaderConfig{ CheckInConfig: &model.OOAPICheckInConfig{ - // TODO(bassosimone,DecFox): to correctly load Web Connectivity targets + // TODO(https://github.com/ooni/probe/issues/2766): to correctly load Web Connectivity targets // here we need to honour the relevant check-in settings. }, Session: sess,