refactor(engine): allow scripts to register experiments (#860)
See https://github.com/ooni/probe/issues/2216
This commit is contained in:
parent
69602abe8a
commit
6a0ae5c70b
|
@ -16,7 +16,7 @@ func (n DNSCheck) lookupURLs(ctl *Controller) ([]string, error) {
|
||||||
// not needed because we have default static input in the engine
|
// not needed because we have default static input in the engine
|
||||||
},
|
},
|
||||||
ExperimentName: "dnscheck",
|
ExperimentName: "dnscheck",
|
||||||
InputPolicy: engine.InputOrStaticDefault,
|
InputPolicy: model.InputOrStaticDefault,
|
||||||
Session: ctl.Session,
|
Session: ctl.Session,
|
||||||
SourceFiles: ctl.InputFiles,
|
SourceFiles: ctl.InputFiles,
|
||||||
StaticInputs: ctl.Inputs,
|
StaticInputs: ctl.Inputs,
|
||||||
|
|
|
@ -123,7 +123,7 @@ func (c *Controller) SetNettestIndex(i, n int) {
|
||||||
//
|
//
|
||||||
// This function will continue to run in most cases but will
|
// This function will continue to run in most cases but will
|
||||||
// immediately halt if something's wrong with the file system.
|
// immediately halt if something's wrong with the file system.
|
||||||
func (c *Controller) Run(builder engine.ExperimentBuilder, inputs []string) error {
|
func (c *Controller) Run(builder model.ExperimentBuilder, inputs []string) error {
|
||||||
// This will configure the controller as handler for the callbacks
|
// This will configure the controller as handler for the callbacks
|
||||||
// called by ooni/probe-engine/experiment.Experiment.
|
// called by ooni/probe-engine/experiment.Experiment.
|
||||||
builder.SetCallbacks(model.ExperimentCallbacks(c))
|
builder.SetCallbacks(model.ExperimentCallbacks(c))
|
||||||
|
|
|
@ -16,7 +16,7 @@ func (n STUNReachability) lookupURLs(ctl *Controller) ([]string, error) {
|
||||||
// not needed because we have default static input in the engine
|
// not needed because we have default static input in the engine
|
||||||
},
|
},
|
||||||
ExperimentName: "stunreachability",
|
ExperimentName: "stunreachability",
|
||||||
InputPolicy: engine.InputOrStaticDefault,
|
InputPolicy: model.InputOrStaticDefault,
|
||||||
Session: ctl.Session,
|
Session: ctl.Session,
|
||||||
SourceFiles: ctl.InputFiles,
|
SourceFiles: ctl.InputFiles,
|
||||||
StaticInputs: ctl.Inputs,
|
StaticInputs: ctl.Inputs,
|
||||||
|
|
|
@ -22,7 +22,7 @@ func (n WebConnectivity) lookupURLs(ctl *Controller, categories []string) ([]str
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
ExperimentName: "web_connectivity",
|
ExperimentName: "web_connectivity",
|
||||||
InputPolicy: engine.InputOrQueryBackend,
|
InputPolicy: model.InputOrQueryBackend,
|
||||||
Session: ctl.Session,
|
Session: ctl.Session,
|
||||||
SourceFiles: ctl.InputFiles,
|
SourceFiles: ctl.InputFiles,
|
||||||
StaticInputs: ctl.Inputs,
|
StaticInputs: ctl.Inputs,
|
||||||
|
|
|
@ -1,378 +1,17 @@
|
||||||
package engine
|
package engine
|
||||||
|
|
||||||
//
|
//
|
||||||
// List of all implemented experiments
|
// List of all implemented experiments.
|
||||||
|
//
|
||||||
|
// Note: if you're looking for a way to register a new experiment, we
|
||||||
|
// now use the internal/registry package for this purpose.
|
||||||
|
//
|
||||||
|
// (This comment will eventually autodestruct.)
|
||||||
//
|
//
|
||||||
|
|
||||||
import (
|
import "github.com/ooni/probe-cli/v3/internal/registry"
|
||||||
"time"
|
|
||||||
|
|
||||||
"github.com/ooni/probe-cli/v3/internal/engine/experiment/dash"
|
|
||||||
"github.com/ooni/probe-cli/v3/internal/engine/experiment/dnscheck"
|
|
||||||
"github.com/ooni/probe-cli/v3/internal/engine/experiment/dnsping"
|
|
||||||
"github.com/ooni/probe-cli/v3/internal/engine/experiment/example"
|
|
||||||
"github.com/ooni/probe-cli/v3/internal/engine/experiment/fbmessenger"
|
|
||||||
"github.com/ooni/probe-cli/v3/internal/engine/experiment/hhfm"
|
|
||||||
"github.com/ooni/probe-cli/v3/internal/engine/experiment/hirl"
|
|
||||||
"github.com/ooni/probe-cli/v3/internal/engine/experiment/httphostheader"
|
|
||||||
"github.com/ooni/probe-cli/v3/internal/engine/experiment/ndt7"
|
|
||||||
"github.com/ooni/probe-cli/v3/internal/engine/experiment/psiphon"
|
|
||||||
"github.com/ooni/probe-cli/v3/internal/engine/experiment/quicping"
|
|
||||||
"github.com/ooni/probe-cli/v3/internal/engine/experiment/riseupvpn"
|
|
||||||
"github.com/ooni/probe-cli/v3/internal/engine/experiment/run"
|
|
||||||
"github.com/ooni/probe-cli/v3/internal/engine/experiment/signal"
|
|
||||||
"github.com/ooni/probe-cli/v3/internal/engine/experiment/simplequicping"
|
|
||||||
"github.com/ooni/probe-cli/v3/internal/engine/experiment/sniblocking"
|
|
||||||
"github.com/ooni/probe-cli/v3/internal/engine/experiment/stunreachability"
|
|
||||||
"github.com/ooni/probe-cli/v3/internal/engine/experiment/tcpping"
|
|
||||||
"github.com/ooni/probe-cli/v3/internal/engine/experiment/telegram"
|
|
||||||
"github.com/ooni/probe-cli/v3/internal/engine/experiment/tlsping"
|
|
||||||
"github.com/ooni/probe-cli/v3/internal/engine/experiment/tlstool"
|
|
||||||
"github.com/ooni/probe-cli/v3/internal/engine/experiment/tor"
|
|
||||||
"github.com/ooni/probe-cli/v3/internal/engine/experiment/torsf"
|
|
||||||
"github.com/ooni/probe-cli/v3/internal/engine/experiment/urlgetter"
|
|
||||||
"github.com/ooni/probe-cli/v3/internal/engine/experiment/vanillator"
|
|
||||||
"github.com/ooni/probe-cli/v3/internal/engine/experiment/webconnectivity"
|
|
||||||
"github.com/ooni/probe-cli/v3/internal/engine/experiment/whatsapp"
|
|
||||||
)
|
|
||||||
|
|
||||||
var experimentsByName = map[string]func(*Session) *experimentBuilder{
|
|
||||||
"dash": func(session *Session) *experimentBuilder {
|
|
||||||
return &experimentBuilder{
|
|
||||||
build: func(config interface{}) *experiment {
|
|
||||||
return newExperiment(session, dash.NewExperimentMeasurer(
|
|
||||||
*config.(*dash.Config),
|
|
||||||
))
|
|
||||||
},
|
|
||||||
config: &dash.Config{},
|
|
||||||
interruptible: true,
|
|
||||||
inputPolicy: InputNone,
|
|
||||||
}
|
|
||||||
},
|
|
||||||
|
|
||||||
"dnscheck": func(session *Session) *experimentBuilder {
|
|
||||||
return &experimentBuilder{
|
|
||||||
build: func(config interface{}) *experiment {
|
|
||||||
return newExperiment(session, dnscheck.NewExperimentMeasurer(
|
|
||||||
*config.(*dnscheck.Config),
|
|
||||||
))
|
|
||||||
},
|
|
||||||
config: &dnscheck.Config{},
|
|
||||||
inputPolicy: InputOrStaticDefault,
|
|
||||||
}
|
|
||||||
},
|
|
||||||
|
|
||||||
"dnsping": func(session *Session) *experimentBuilder {
|
|
||||||
return &experimentBuilder{
|
|
||||||
build: func(config interface{}) *experiment {
|
|
||||||
return newExperiment(session, dnsping.NewExperimentMeasurer(
|
|
||||||
*config.(*dnsping.Config),
|
|
||||||
))
|
|
||||||
},
|
|
||||||
config: &dnsping.Config{},
|
|
||||||
inputPolicy: InputOrStaticDefault,
|
|
||||||
}
|
|
||||||
},
|
|
||||||
|
|
||||||
"example": func(session *Session) *experimentBuilder {
|
|
||||||
return &experimentBuilder{
|
|
||||||
build: func(config interface{}) *experiment {
|
|
||||||
return newExperiment(session, example.NewExperimentMeasurer(
|
|
||||||
*config.(*example.Config), "example",
|
|
||||||
))
|
|
||||||
},
|
|
||||||
config: &example.Config{
|
|
||||||
Message: "Good day from the example experiment!",
|
|
||||||
SleepTime: int64(time.Second),
|
|
||||||
},
|
|
||||||
interruptible: true,
|
|
||||||
inputPolicy: InputNone,
|
|
||||||
}
|
|
||||||
},
|
|
||||||
|
|
||||||
"facebook_messenger": func(session *Session) *experimentBuilder {
|
|
||||||
return &experimentBuilder{
|
|
||||||
build: func(config interface{}) *experiment {
|
|
||||||
return newExperiment(session, fbmessenger.NewExperimentMeasurer(
|
|
||||||
*config.(*fbmessenger.Config),
|
|
||||||
))
|
|
||||||
},
|
|
||||||
config: &fbmessenger.Config{},
|
|
||||||
inputPolicy: InputNone,
|
|
||||||
}
|
|
||||||
},
|
|
||||||
|
|
||||||
"http_header_field_manipulation": func(session *Session) *experimentBuilder {
|
|
||||||
return &experimentBuilder{
|
|
||||||
build: func(config interface{}) *experiment {
|
|
||||||
return newExperiment(session, hhfm.NewExperimentMeasurer(
|
|
||||||
*config.(*hhfm.Config),
|
|
||||||
))
|
|
||||||
},
|
|
||||||
config: &hhfm.Config{},
|
|
||||||
inputPolicy: InputNone,
|
|
||||||
}
|
|
||||||
},
|
|
||||||
|
|
||||||
"http_host_header": func(session *Session) *experimentBuilder {
|
|
||||||
return &experimentBuilder{
|
|
||||||
build: func(config interface{}) *experiment {
|
|
||||||
return newExperiment(session, httphostheader.NewExperimentMeasurer(
|
|
||||||
*config.(*httphostheader.Config),
|
|
||||||
))
|
|
||||||
},
|
|
||||||
config: &httphostheader.Config{},
|
|
||||||
inputPolicy: InputOrQueryBackend,
|
|
||||||
}
|
|
||||||
},
|
|
||||||
|
|
||||||
"http_invalid_request_line": func(session *Session) *experimentBuilder {
|
|
||||||
return &experimentBuilder{
|
|
||||||
build: func(config interface{}) *experiment {
|
|
||||||
return newExperiment(session, hirl.NewExperimentMeasurer(
|
|
||||||
*config.(*hirl.Config),
|
|
||||||
))
|
|
||||||
},
|
|
||||||
config: &hirl.Config{},
|
|
||||||
inputPolicy: InputNone,
|
|
||||||
}
|
|
||||||
},
|
|
||||||
|
|
||||||
"ndt": func(session *Session) *experimentBuilder {
|
|
||||||
return &experimentBuilder{
|
|
||||||
build: func(config interface{}) *experiment {
|
|
||||||
return newExperiment(session, ndt7.NewExperimentMeasurer(
|
|
||||||
*config.(*ndt7.Config),
|
|
||||||
))
|
|
||||||
},
|
|
||||||
config: &ndt7.Config{},
|
|
||||||
interruptible: true,
|
|
||||||
inputPolicy: InputNone,
|
|
||||||
}
|
|
||||||
},
|
|
||||||
|
|
||||||
"psiphon": func(session *Session) *experimentBuilder {
|
|
||||||
return &experimentBuilder{
|
|
||||||
build: func(config interface{}) *experiment {
|
|
||||||
return newExperiment(session, psiphon.NewExperimentMeasurer(
|
|
||||||
*config.(*psiphon.Config),
|
|
||||||
))
|
|
||||||
},
|
|
||||||
config: &psiphon.Config{},
|
|
||||||
inputPolicy: InputOptional,
|
|
||||||
}
|
|
||||||
},
|
|
||||||
|
|
||||||
"quicping": func(session *Session) *experimentBuilder {
|
|
||||||
return &experimentBuilder{
|
|
||||||
build: func(config interface{}) *experiment {
|
|
||||||
return newExperiment(session, quicping.NewExperimentMeasurer(
|
|
||||||
*config.(*quicping.Config),
|
|
||||||
))
|
|
||||||
},
|
|
||||||
config: &quicping.Config{},
|
|
||||||
inputPolicy: InputStrictlyRequired,
|
|
||||||
}
|
|
||||||
},
|
|
||||||
|
|
||||||
"riseupvpn": func(session *Session) *experimentBuilder {
|
|
||||||
return &experimentBuilder{
|
|
||||||
build: func(config interface{}) *experiment {
|
|
||||||
return newExperiment(session, riseupvpn.NewExperimentMeasurer(
|
|
||||||
*config.(*riseupvpn.Config),
|
|
||||||
))
|
|
||||||
},
|
|
||||||
config: &riseupvpn.Config{},
|
|
||||||
inputPolicy: InputNone,
|
|
||||||
}
|
|
||||||
},
|
|
||||||
|
|
||||||
"run": func(session *Session) *experimentBuilder {
|
|
||||||
return &experimentBuilder{
|
|
||||||
build: func(config interface{}) *experiment {
|
|
||||||
return newExperiment(session, run.NewExperimentMeasurer(
|
|
||||||
*config.(*run.Config),
|
|
||||||
))
|
|
||||||
},
|
|
||||||
config: &run.Config{},
|
|
||||||
inputPolicy: InputStrictlyRequired,
|
|
||||||
}
|
|
||||||
},
|
|
||||||
|
|
||||||
"simplequicping": func(session *Session) *experimentBuilder {
|
|
||||||
return &experimentBuilder{
|
|
||||||
build: func(config interface{}) *experiment {
|
|
||||||
return newExperiment(session, simplequicping.NewExperimentMeasurer(
|
|
||||||
*config.(*simplequicping.Config),
|
|
||||||
))
|
|
||||||
},
|
|
||||||
config: &simplequicping.Config{},
|
|
||||||
inputPolicy: InputStrictlyRequired,
|
|
||||||
}
|
|
||||||
},
|
|
||||||
|
|
||||||
"signal": func(session *Session) *experimentBuilder {
|
|
||||||
return &experimentBuilder{
|
|
||||||
build: func(config interface{}) *experiment {
|
|
||||||
return newExperiment(session, signal.NewExperimentMeasurer(
|
|
||||||
*config.(*signal.Config),
|
|
||||||
))
|
|
||||||
},
|
|
||||||
config: &signal.Config{},
|
|
||||||
inputPolicy: InputNone,
|
|
||||||
}
|
|
||||||
},
|
|
||||||
|
|
||||||
"sni_blocking": func(session *Session) *experimentBuilder {
|
|
||||||
return &experimentBuilder{
|
|
||||||
build: func(config interface{}) *experiment {
|
|
||||||
return newExperiment(session, sniblocking.NewExperimentMeasurer(
|
|
||||||
*config.(*sniblocking.Config),
|
|
||||||
))
|
|
||||||
},
|
|
||||||
config: &sniblocking.Config{},
|
|
||||||
inputPolicy: InputOrQueryBackend,
|
|
||||||
}
|
|
||||||
},
|
|
||||||
|
|
||||||
"stunreachability": func(session *Session) *experimentBuilder {
|
|
||||||
return &experimentBuilder{
|
|
||||||
build: func(config interface{}) *experiment {
|
|
||||||
return newExperiment(session, stunreachability.NewExperimentMeasurer(
|
|
||||||
*config.(*stunreachability.Config),
|
|
||||||
))
|
|
||||||
},
|
|
||||||
config: &stunreachability.Config{},
|
|
||||||
inputPolicy: InputOrStaticDefault,
|
|
||||||
}
|
|
||||||
},
|
|
||||||
|
|
||||||
"tcpping": func(session *Session) *experimentBuilder {
|
|
||||||
return &experimentBuilder{
|
|
||||||
build: func(config interface{}) *experiment {
|
|
||||||
return newExperiment(session, tcpping.NewExperimentMeasurer(
|
|
||||||
*config.(*tcpping.Config),
|
|
||||||
))
|
|
||||||
},
|
|
||||||
config: &tcpping.Config{},
|
|
||||||
inputPolicy: InputStrictlyRequired,
|
|
||||||
}
|
|
||||||
},
|
|
||||||
|
|
||||||
"tlsping": func(session *Session) *experimentBuilder {
|
|
||||||
return &experimentBuilder{
|
|
||||||
build: func(config interface{}) *experiment {
|
|
||||||
return newExperiment(session, tlsping.NewExperimentMeasurer(
|
|
||||||
*config.(*tlsping.Config),
|
|
||||||
))
|
|
||||||
},
|
|
||||||
config: &tlsping.Config{},
|
|
||||||
inputPolicy: InputStrictlyRequired,
|
|
||||||
}
|
|
||||||
},
|
|
||||||
|
|
||||||
"telegram": func(session *Session) *experimentBuilder {
|
|
||||||
return &experimentBuilder{
|
|
||||||
build: func(config interface{}) *experiment {
|
|
||||||
return newExperiment(session, telegram.NewExperimentMeasurer(
|
|
||||||
*config.(*telegram.Config),
|
|
||||||
))
|
|
||||||
},
|
|
||||||
config: &telegram.Config{},
|
|
||||||
inputPolicy: InputNone,
|
|
||||||
}
|
|
||||||
},
|
|
||||||
|
|
||||||
"tlstool": func(session *Session) *experimentBuilder {
|
|
||||||
return &experimentBuilder{
|
|
||||||
build: func(config interface{}) *experiment {
|
|
||||||
return newExperiment(session, tlstool.NewExperimentMeasurer(
|
|
||||||
*config.(*tlstool.Config),
|
|
||||||
))
|
|
||||||
},
|
|
||||||
config: &tlstool.Config{},
|
|
||||||
inputPolicy: InputOrQueryBackend,
|
|
||||||
}
|
|
||||||
},
|
|
||||||
|
|
||||||
"tor": func(session *Session) *experimentBuilder {
|
|
||||||
return &experimentBuilder{
|
|
||||||
build: func(config interface{}) *experiment {
|
|
||||||
return newExperiment(session, tor.NewExperimentMeasurer(
|
|
||||||
*config.(*tor.Config),
|
|
||||||
))
|
|
||||||
},
|
|
||||||
config: &tor.Config{},
|
|
||||||
inputPolicy: InputNone,
|
|
||||||
}
|
|
||||||
},
|
|
||||||
|
|
||||||
"torsf": func(session *Session) *experimentBuilder {
|
|
||||||
return &experimentBuilder{
|
|
||||||
build: func(config interface{}) *experiment {
|
|
||||||
return newExperiment(session, torsf.NewExperimentMeasurer(
|
|
||||||
*config.(*torsf.Config),
|
|
||||||
))
|
|
||||||
},
|
|
||||||
config: &torsf.Config{},
|
|
||||||
inputPolicy: InputNone,
|
|
||||||
}
|
|
||||||
},
|
|
||||||
|
|
||||||
"urlgetter": func(session *Session) *experimentBuilder {
|
|
||||||
return &experimentBuilder{
|
|
||||||
build: func(config interface{}) *experiment {
|
|
||||||
return newExperiment(session, urlgetter.NewExperimentMeasurer(
|
|
||||||
*config.(*urlgetter.Config),
|
|
||||||
))
|
|
||||||
},
|
|
||||||
config: &urlgetter.Config{},
|
|
||||||
inputPolicy: InputStrictlyRequired,
|
|
||||||
}
|
|
||||||
},
|
|
||||||
|
|
||||||
"vanilla_tor": func(session *Session) *experimentBuilder {
|
|
||||||
return &experimentBuilder{
|
|
||||||
build: func(config interface{}) *experiment {
|
|
||||||
return newExperiment(session, vanillator.NewExperimentMeasurer(
|
|
||||||
*config.(*vanillator.Config),
|
|
||||||
))
|
|
||||||
},
|
|
||||||
config: &vanillator.Config{},
|
|
||||||
inputPolicy: InputNone,
|
|
||||||
}
|
|
||||||
},
|
|
||||||
|
|
||||||
"web_connectivity": func(session *Session) *experimentBuilder {
|
|
||||||
return &experimentBuilder{
|
|
||||||
build: func(config interface{}) *experiment {
|
|
||||||
return newExperiment(session, webconnectivity.NewExperimentMeasurer(
|
|
||||||
*config.(*webconnectivity.Config),
|
|
||||||
))
|
|
||||||
},
|
|
||||||
config: &webconnectivity.Config{},
|
|
||||||
inputPolicy: InputOrQueryBackend,
|
|
||||||
}
|
|
||||||
},
|
|
||||||
|
|
||||||
"whatsapp": func(session *Session) *experimentBuilder {
|
|
||||||
return &experimentBuilder{
|
|
||||||
build: func(config interface{}) *experiment {
|
|
||||||
return newExperiment(session, whatsapp.NewExperimentMeasurer(
|
|
||||||
*config.(*whatsapp.Config),
|
|
||||||
))
|
|
||||||
},
|
|
||||||
config: &whatsapp.Config{},
|
|
||||||
inputPolicy: InputNone,
|
|
||||||
}
|
|
||||||
},
|
|
||||||
}
|
|
||||||
|
|
||||||
// AllExperiments returns the name of all experiments
|
// AllExperiments returns the name of all experiments
|
||||||
func AllExperiments() []string {
|
func AllExperiments() []string {
|
||||||
var names []string
|
return registry.ExperimentNames()
|
||||||
for key := range experimentsByName {
|
|
||||||
names = append(names, key)
|
|
||||||
}
|
|
||||||
return names
|
|
||||||
}
|
}
|
||||||
|
|
|
@ -26,76 +26,6 @@ func formatTimeNowUTC() string {
|
||||||
return time.Now().UTC().Format(dateFormat)
|
return time.Now().UTC().Format(dateFormat)
|
||||||
}
|
}
|
||||||
|
|
||||||
// Experiment is an experiment instance.
|
|
||||||
type Experiment interface {
|
|
||||||
// KibiBytesReceived accounts for the KibiBytes received by the experiment.
|
|
||||||
KibiBytesReceived() float64
|
|
||||||
|
|
||||||
// KibiBytesSent is like KibiBytesReceived but for the bytes sent.
|
|
||||||
KibiBytesSent() float64
|
|
||||||
|
|
||||||
// Name returns the experiment name.
|
|
||||||
Name() string
|
|
||||||
|
|
||||||
// GetSummaryKeys returns a data structure containing a
|
|
||||||
// summary of the test keys for ooniprobe.
|
|
||||||
GetSummaryKeys(m *model.Measurement) (any, error)
|
|
||||||
|
|
||||||
// ReportID returns the open report's ID, if we have opened a report
|
|
||||||
// successfully before, or an empty string, otherwise.
|
|
||||||
//
|
|
||||||
// Deprecated: new code should use a Submitter.
|
|
||||||
ReportID() string
|
|
||||||
|
|
||||||
// MeasureAsync runs an async measurement. This operation could post
|
|
||||||
// one or more measurements onto the returned channel. We'll close the
|
|
||||||
// channel when we've emitted all the measurements.
|
|
||||||
//
|
|
||||||
// Arguments:
|
|
||||||
//
|
|
||||||
// - ctx is the context for deadline/cancellation/timeout;
|
|
||||||
//
|
|
||||||
// - input is the input (typically a URL but it could also be
|
|
||||||
// just an endpoint or an empty string for input-less experiments
|
|
||||||
// such as, e.g., ndt7 and dash).
|
|
||||||
//
|
|
||||||
// Return value:
|
|
||||||
//
|
|
||||||
// - on success, channel where to post measurements (the channel
|
|
||||||
// will be closed when done) and nil error;
|
|
||||||
//
|
|
||||||
// - on failure, nil channel and non-nil error.
|
|
||||||
MeasureAsync(ctx context.Context, input string) (<-chan *model.Measurement, error)
|
|
||||||
|
|
||||||
// MeasureWithContext performs a synchronous measurement.
|
|
||||||
//
|
|
||||||
// Return value: strictly either a non-nil measurement and
|
|
||||||
// a nil error or a nil measurement and a non-nil error.
|
|
||||||
//
|
|
||||||
// CAVEAT: while this API is perfectly fine for experiments that
|
|
||||||
// return a single measurement, it will only return the first measurement
|
|
||||||
// when used with an asynchronous experiment.
|
|
||||||
MeasureWithContext(ctx context.Context, input string) (measurement *model.Measurement, err error)
|
|
||||||
|
|
||||||
// SaveMeasurement saves a measurement on the specified file path.
|
|
||||||
//
|
|
||||||
// Deprecated: new code should use a Saver.
|
|
||||||
SaveMeasurement(measurement *model.Measurement, filePath string) error
|
|
||||||
|
|
||||||
// SubmitAndUpdateMeasurementContext submits a measurement and updates the
|
|
||||||
// fields whose value has changed as part of the submission.
|
|
||||||
//
|
|
||||||
// Deprecated: new code should use a Submitter.
|
|
||||||
SubmitAndUpdateMeasurementContext(
|
|
||||||
ctx context.Context, measurement *model.Measurement) error
|
|
||||||
|
|
||||||
// OpenReportContext will open a report using the given context
|
|
||||||
// to possibly limit the lifetime of this operation.
|
|
||||||
//
|
|
||||||
// Deprecated: new code should use a Submitter.
|
|
||||||
OpenReportContext(ctx context.Context) error
|
|
||||||
}
|
|
||||||
|
|
||||||
// experiment implements Experiment.
|
// experiment implements Experiment.
|
||||||
type experiment struct {
|
type experiment struct {
|
||||||
byteCounter *bytecounter.Counter
|
byteCounter *bytecounter.Counter
|
||||||
|
|
|
@ -12,7 +12,6 @@ import (
|
||||||
"strings"
|
"strings"
|
||||||
"testing"
|
"testing"
|
||||||
|
|
||||||
"github.com/ooni/probe-cli/v3/internal/engine/experiment/example"
|
|
||||||
"github.com/ooni/probe-cli/v3/internal/model"
|
"github.com/ooni/probe-cli/v3/internal/model"
|
||||||
)
|
)
|
||||||
|
|
||||||
|
@ -142,7 +141,7 @@ func TestNeedsInput(t *testing.T) {
|
||||||
if err != nil {
|
if err != nil {
|
||||||
t.Fatal(err)
|
t.Fatal(err)
|
||||||
}
|
}
|
||||||
if builder.InputPolicy() != InputOrQueryBackend {
|
if builder.InputPolicy() != model.InputOrQueryBackend {
|
||||||
t.Fatal("web_connectivity certainly needs input")
|
t.Fatal("web_connectivity certainly needs input")
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -218,88 +217,6 @@ func TestMeasurementFailure(t *testing.T) {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func TestUseOptions(t *testing.T) {
|
|
||||||
if testing.Short() {
|
|
||||||
t.Skip("skip test in short mode")
|
|
||||||
}
|
|
||||||
sess := newSessionForTesting(t)
|
|
||||||
defer sess.Close()
|
|
||||||
builder, err := sess.NewExperimentBuilder("example")
|
|
||||||
if err != nil {
|
|
||||||
t.Fatal(err)
|
|
||||||
}
|
|
||||||
options, err := builder.Options()
|
|
||||||
if err != nil {
|
|
||||||
t.Fatal(err)
|
|
||||||
}
|
|
||||||
var (
|
|
||||||
returnError bool
|
|
||||||
message bool
|
|
||||||
sleepTime bool
|
|
||||||
other int64
|
|
||||||
)
|
|
||||||
for name, option := range options {
|
|
||||||
if name == "ReturnError" {
|
|
||||||
returnError = true
|
|
||||||
if option.Type != "bool" {
|
|
||||||
t.Fatal("ReturnError is not a bool")
|
|
||||||
}
|
|
||||||
if option.Doc != "Toogle to return a mocked error" {
|
|
||||||
t.Fatal("ReturnError doc is wrong")
|
|
||||||
}
|
|
||||||
} else if name == "Message" {
|
|
||||||
message = true
|
|
||||||
if option.Type != "string" {
|
|
||||||
t.Fatal("Message is not a string")
|
|
||||||
}
|
|
||||||
if option.Doc != "Message to emit at test completion" {
|
|
||||||
t.Fatal("Message doc is wrong")
|
|
||||||
}
|
|
||||||
} else if name == "SleepTime" {
|
|
||||||
sleepTime = true
|
|
||||||
if option.Type != "int64" {
|
|
||||||
t.Fatal("SleepTime is not an int64")
|
|
||||||
}
|
|
||||||
if option.Doc != "Amount of time to sleep for" {
|
|
||||||
t.Fatal("SleepTime doc is wrong")
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
other++
|
|
||||||
}
|
|
||||||
}
|
|
||||||
if other != 0 {
|
|
||||||
t.Fatal("found unexpected option")
|
|
||||||
}
|
|
||||||
if !returnError {
|
|
||||||
t.Fatal("did not find ReturnError option")
|
|
||||||
}
|
|
||||||
if !message {
|
|
||||||
t.Fatal("did not find Message option")
|
|
||||||
}
|
|
||||||
if !sleepTime {
|
|
||||||
t.Fatal("did not find SleepTime option")
|
|
||||||
}
|
|
||||||
if err := builder.SetOptionAny("ReturnError", true); err != nil {
|
|
||||||
t.Fatal("cannot set ReturnError field")
|
|
||||||
}
|
|
||||||
if err := builder.SetOptionAny("SleepTime", 10); err != nil {
|
|
||||||
t.Fatal("cannot set SleepTime field")
|
|
||||||
}
|
|
||||||
if err := builder.SetOptionAny("Message", "antani"); err != nil {
|
|
||||||
t.Fatal("cannot set Message field")
|
|
||||||
}
|
|
||||||
config := builder.(*experimentBuilder).config.(*example.Config)
|
|
||||||
if config.ReturnError != true {
|
|
||||||
t.Fatal("config.ReturnError was not changed")
|
|
||||||
}
|
|
||||||
if config.SleepTime != 10 {
|
|
||||||
t.Fatal("config.SleepTime was not changed")
|
|
||||||
}
|
|
||||||
if config.Message != "antani" {
|
|
||||||
t.Fatal("config.Message was not changed")
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func TestRunHHFM(t *testing.T) {
|
func TestRunHHFM(t *testing.T) {
|
||||||
if testing.Short() {
|
if testing.Short() {
|
||||||
t.Skip("skip test in short mode")
|
t.Skip("skip test in short mode")
|
||||||
|
@ -313,7 +230,7 @@ func TestRunHHFM(t *testing.T) {
|
||||||
runexperimentflow(t, builder.NewExperiment(), "")
|
runexperimentflow(t, builder.NewExperiment(), "")
|
||||||
}
|
}
|
||||||
|
|
||||||
func runexperimentflow(t *testing.T, experiment Experiment, input string) {
|
func runexperimentflow(t *testing.T, experiment model.Experiment, input string) {
|
||||||
ctx := context.Background()
|
ctx := context.Background()
|
||||||
err := experiment.OpenReportContext(ctx)
|
err := experiment.OpenReportContext(ctx)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
|
|
|
@ -5,243 +5,46 @@ package engine
|
||||||
//
|
//
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"errors"
|
|
||||||
"fmt"
|
|
||||||
"reflect"
|
|
||||||
"strconv"
|
|
||||||
|
|
||||||
"github.com/iancoleman/strcase"
|
|
||||||
"github.com/ooni/probe-cli/v3/internal/model"
|
"github.com/ooni/probe-cli/v3/internal/model"
|
||||||
|
"github.com/ooni/probe-cli/v3/internal/registry"
|
||||||
)
|
)
|
||||||
|
|
||||||
// InputPolicy describes the experiment policy with respect to input. That is
|
|
||||||
// whether it requires input, optionally accepts input, does not want input.
|
|
||||||
type InputPolicy string
|
|
||||||
|
|
||||||
const (
|
|
||||||
// InputOrQueryBackend indicates that the experiment requires
|
|
||||||
// external input to run and that this kind of input is URLs
|
|
||||||
// from the citizenlab/test-lists repository. If this input
|
|
||||||
// not provided to the experiment, then the code that runs the
|
|
||||||
// experiment is supposed to fetch from URLs from OONI's backends.
|
|
||||||
InputOrQueryBackend = InputPolicy("or_query_backend")
|
|
||||||
|
|
||||||
// InputStrictlyRequired indicates that the experiment
|
|
||||||
// requires input and we currently don't have an API for
|
|
||||||
// fetching such input. Therefore, either the user specifies
|
|
||||||
// input or the experiment will fail for the lack of input.
|
|
||||||
InputStrictlyRequired = InputPolicy("strictly_required")
|
|
||||||
|
|
||||||
// InputOptional indicates that the experiment handles input,
|
|
||||||
// if any; otherwise it fetchs input/uses a default.
|
|
||||||
InputOptional = InputPolicy("optional")
|
|
||||||
|
|
||||||
// InputNone indicates that the experiment does not want any
|
|
||||||
// input and ignores the input if provided with it.
|
|
||||||
InputNone = InputPolicy("none")
|
|
||||||
|
|
||||||
// We gather input from StaticInput and SourceFiles. If there is
|
|
||||||
// input, we return it. Otherwise, we return an internal static
|
|
||||||
// list of inputs to be used with this experiment.
|
|
||||||
InputOrStaticDefault = InputPolicy("or_static_default")
|
|
||||||
)
|
|
||||||
|
|
||||||
// ExperimentBuilder builds an experiment.
|
|
||||||
type ExperimentBuilder interface {
|
|
||||||
// Interruptible tells you whether this is an interruptible experiment. This kind
|
|
||||||
// of experiments (e.g. ndt7) may be interrupted mid way.
|
|
||||||
Interruptible() bool
|
|
||||||
|
|
||||||
// InputPolicy returns the experiment input policy.
|
|
||||||
InputPolicy() InputPolicy
|
|
||||||
|
|
||||||
// Options returns information about the experiment's options.
|
|
||||||
Options() (map[string]OptionInfo, error)
|
|
||||||
|
|
||||||
// SetOptionAny sets an option whose value is an any value. We will use reasonable
|
|
||||||
// heuristics to convert the any value to the proper type of the field whose name is
|
|
||||||
// contained by the key variable. If we cannot convert the provided any value to
|
|
||||||
// the proper type, then this function returns an error.
|
|
||||||
SetOptionAny(key string, value any) error
|
|
||||||
|
|
||||||
// SetOptionsAny sets options from a map[string]any. See the documentation of
|
|
||||||
// the SetOptionAny method for more information.
|
|
||||||
SetOptionsAny(options map[string]any) error
|
|
||||||
|
|
||||||
// SetCallbacks sets the experiment's interactive callbacks.
|
|
||||||
SetCallbacks(callbacks model.ExperimentCallbacks)
|
|
||||||
|
|
||||||
// NewExperiment creates the experiment instance.
|
|
||||||
NewExperiment() Experiment
|
|
||||||
}
|
|
||||||
|
|
||||||
// experimentBuilder implements ExperimentBuilder.
|
// experimentBuilder implements ExperimentBuilder.
|
||||||
|
//
|
||||||
|
// This type is now just a tiny wrapper around registry.Factory.
|
||||||
type experimentBuilder struct {
|
type experimentBuilder struct {
|
||||||
// build is the constructor that build an experiment with the given config.
|
factory *registry.Factory
|
||||||
build func(config interface{}) *experiment
|
|
||||||
|
|
||||||
// callbacks contains callbacks for the new experiment.
|
// callbacks contains callbacks for the new experiment.
|
||||||
callbacks model.ExperimentCallbacks
|
callbacks model.ExperimentCallbacks
|
||||||
|
|
||||||
// config contains the experiment's config.
|
// session is the session
|
||||||
config interface{}
|
session *Session
|
||||||
|
|
||||||
// inputPolicy contains the experiment's InputPolicy.
|
|
||||||
inputPolicy InputPolicy
|
|
||||||
|
|
||||||
// interruptible indicates whether the experiment is interruptible.
|
|
||||||
interruptible bool
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// Interruptible implements ExperimentBuilder.Interruptible.
|
// Interruptible implements ExperimentBuilder.Interruptible.
|
||||||
func (b *experimentBuilder) Interruptible() bool {
|
func (b *experimentBuilder) Interruptible() bool {
|
||||||
return b.interruptible
|
return b.factory.Interruptible()
|
||||||
}
|
}
|
||||||
|
|
||||||
// InputPolicy implements ExperimentBuilder.InputPolicy.
|
// InputPolicy implements ExperimentBuilder.InputPolicy.
|
||||||
func (b *experimentBuilder) InputPolicy() InputPolicy {
|
func (b *experimentBuilder) InputPolicy() model.InputPolicy {
|
||||||
return b.inputPolicy
|
return b.factory.InputPolicy()
|
||||||
}
|
}
|
||||||
|
|
||||||
// OptionInfo contains info about an option.
|
|
||||||
type OptionInfo struct {
|
|
||||||
// Doc contains the documentation.
|
|
||||||
Doc string
|
|
||||||
|
|
||||||
// Type contains the type.
|
|
||||||
Type string
|
|
||||||
}
|
|
||||||
|
|
||||||
var (
|
|
||||||
// ErrConfigIsNotAStructPointer indicates we expected a pointer to struct.
|
|
||||||
ErrConfigIsNotAStructPointer = errors.New("config is not a struct pointer")
|
|
||||||
|
|
||||||
// ErrNoSuchField indicates there's no field with the given name.
|
|
||||||
ErrNoSuchField = errors.New("no such field")
|
|
||||||
|
|
||||||
// ErrCannotSetIntegerOption means SetOptionAny couldn't set an integer option.
|
|
||||||
ErrCannotSetIntegerOption = errors.New("cannot set integer option")
|
|
||||||
|
|
||||||
// ErrInvalidStringRepresentationOfBool indicates the string you passed
|
|
||||||
// to SetOptionaAny is not a valid string representation of a bool.
|
|
||||||
ErrInvalidStringRepresentationOfBool = errors.New("invalid string representation of bool")
|
|
||||||
|
|
||||||
// ErrCannotSetBoolOption means SetOptionAny couldn't set a bool option.
|
|
||||||
ErrCannotSetBoolOption = errors.New("cannot set bool option")
|
|
||||||
|
|
||||||
// ErrCannotSetStringOption means SetOptionAny couldn't set a string option.
|
|
||||||
ErrCannotSetStringOption = errors.New("cannot set string option")
|
|
||||||
|
|
||||||
// ErrUnsupportedOptionType means we don't support the type passed to
|
|
||||||
// the SetOptionAny method as an opaque any type.
|
|
||||||
ErrUnsupportedOptionType = errors.New("unsupported option type")
|
|
||||||
)
|
|
||||||
|
|
||||||
// Options implements ExperimentBuilder.Options.
|
// Options implements ExperimentBuilder.Options.
|
||||||
func (b *experimentBuilder) Options() (map[string]OptionInfo, error) {
|
func (b *experimentBuilder) Options() (map[string]model.ExperimentOptionInfo, error) {
|
||||||
result := make(map[string]OptionInfo)
|
return b.factory.Options()
|
||||||
ptrinfo := reflect.ValueOf(b.config)
|
|
||||||
if ptrinfo.Kind() != reflect.Ptr {
|
|
||||||
return nil, ErrConfigIsNotAStructPointer
|
|
||||||
}
|
|
||||||
structinfo := ptrinfo.Elem().Type()
|
|
||||||
if structinfo.Kind() != reflect.Struct {
|
|
||||||
return nil, ErrConfigIsNotAStructPointer
|
|
||||||
}
|
|
||||||
for i := 0; i < structinfo.NumField(); i++ {
|
|
||||||
field := structinfo.Field(i)
|
|
||||||
result[field.Name] = OptionInfo{
|
|
||||||
Doc: field.Tag.Get("ooni"),
|
|
||||||
Type: field.Type.String(),
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return result, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
// setOptionBool sets a bool option.
|
|
||||||
func (b *experimentBuilder) setOptionBool(field reflect.Value, value any) error {
|
|
||||||
switch v := value.(type) {
|
|
||||||
case bool:
|
|
||||||
field.SetBool(v)
|
|
||||||
return nil
|
|
||||||
case string:
|
|
||||||
if v != "true" && v != "false" {
|
|
||||||
return fmt.Errorf("%w: %s", ErrInvalidStringRepresentationOfBool, v)
|
|
||||||
}
|
|
||||||
field.SetBool(v == "true")
|
|
||||||
return nil
|
|
||||||
default:
|
|
||||||
return fmt.Errorf("%w from a value of type %T", ErrCannotSetBoolOption, value)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// setOptionInt sets an int option
|
|
||||||
func (b *experimentBuilder) setOptionInt(field reflect.Value, value any) error {
|
|
||||||
switch v := value.(type) {
|
|
||||||
case int64:
|
|
||||||
field.SetInt(v)
|
|
||||||
return nil
|
|
||||||
case int32:
|
|
||||||
field.SetInt(int64(v))
|
|
||||||
return nil
|
|
||||||
case int16:
|
|
||||||
field.SetInt(int64(v))
|
|
||||||
return nil
|
|
||||||
case int8:
|
|
||||||
field.SetInt(int64(v))
|
|
||||||
return nil
|
|
||||||
case int:
|
|
||||||
field.SetInt(int64(v))
|
|
||||||
return nil
|
|
||||||
case string:
|
|
||||||
number, err := strconv.ParseInt(v, 10, 64)
|
|
||||||
if err != nil {
|
|
||||||
return fmt.Errorf("%w: %s", ErrCannotSetIntegerOption, err.Error())
|
|
||||||
}
|
|
||||||
field.SetInt(number)
|
|
||||||
return nil
|
|
||||||
default:
|
|
||||||
return fmt.Errorf("%w from a value of type %T", ErrCannotSetIntegerOption, value)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// setOptionString sets a string option
|
|
||||||
func (b *experimentBuilder) setOptionString(field reflect.Value, value any) error {
|
|
||||||
switch v := value.(type) {
|
|
||||||
case string:
|
|
||||||
field.SetString(v)
|
|
||||||
return nil
|
|
||||||
default:
|
|
||||||
return fmt.Errorf("%w from a value of type %T", ErrCannotSetStringOption, value)
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// SetOptionAny implements ExperimentBuilder.SetOptionAny.
|
// SetOptionAny implements ExperimentBuilder.SetOptionAny.
|
||||||
func (b *experimentBuilder) SetOptionAny(key string, value any) error {
|
func (b *experimentBuilder) SetOptionAny(key string, value any) error {
|
||||||
field, err := b.fieldbyname(b.config, key)
|
return b.factory.SetOptionAny(key, value)
|
||||||
if err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
switch field.Kind() {
|
|
||||||
case reflect.Int64:
|
|
||||||
return b.setOptionInt(field, value)
|
|
||||||
case reflect.Bool:
|
|
||||||
return b.setOptionBool(field, value)
|
|
||||||
case reflect.String:
|
|
||||||
return b.setOptionString(field, value)
|
|
||||||
default:
|
|
||||||
return fmt.Errorf("%w: %T", ErrUnsupportedOptionType, value)
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// SetOptionsAny implements ExperimentBuilder.SetOptionsAny.
|
// SetOptionsAny implements ExperimentBuilder.SetOptionsAny.
|
||||||
func (b *experimentBuilder) SetOptionsAny(options map[string]any) error {
|
func (b *experimentBuilder) SetOptionsAny(options map[string]any) error {
|
||||||
for key, value := range options {
|
return b.factory.SetOptionsAny(options)
|
||||||
if err := b.SetOptionAny(key, value); err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return nil
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// SetCallbacks implements ExperimentBuilder.SetCallbacks.
|
// SetCallbacks implements ExperimentBuilder.SetCallbacks.
|
||||||
|
@ -249,57 +52,24 @@ func (b *experimentBuilder) SetCallbacks(callbacks model.ExperimentCallbacks) {
|
||||||
b.callbacks = callbacks
|
b.callbacks = callbacks
|
||||||
}
|
}
|
||||||
|
|
||||||
// fieldbyname return v's field whose name is equal to the given key.
|
|
||||||
func (b *experimentBuilder) fieldbyname(v interface{}, key string) (reflect.Value, error) {
|
|
||||||
// See https://stackoverflow.com/a/6396678/4354461
|
|
||||||
ptrinfo := reflect.ValueOf(v)
|
|
||||||
if ptrinfo.Kind() != reflect.Ptr {
|
|
||||||
return reflect.Value{}, fmt.Errorf("%w but a %T", ErrConfigIsNotAStructPointer, v)
|
|
||||||
}
|
|
||||||
structinfo := ptrinfo.Elem()
|
|
||||||
if structinfo.Kind() != reflect.Struct {
|
|
||||||
return reflect.Value{}, fmt.Errorf("%w but a %T", ErrConfigIsNotAStructPointer, v)
|
|
||||||
}
|
|
||||||
field := structinfo.FieldByName(key)
|
|
||||||
if !field.IsValid() || !field.CanSet() {
|
|
||||||
return reflect.Value{}, fmt.Errorf("%w: %s", ErrNoSuchField, key)
|
|
||||||
}
|
|
||||||
return field, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
// NewExperiment creates the experiment
|
// NewExperiment creates the experiment
|
||||||
func (b *experimentBuilder) NewExperiment() Experiment {
|
func (b *experimentBuilder) NewExperiment() model.Experiment {
|
||||||
experiment := b.build(b.config)
|
measurer := b.factory.NewExperimentMeasurer()
|
||||||
|
experiment := newExperiment(b.session, measurer)
|
||||||
experiment.callbacks = b.callbacks
|
experiment.callbacks = b.callbacks
|
||||||
return experiment
|
return experiment
|
||||||
}
|
}
|
||||||
|
|
||||||
// canonicalizeExperimentName allows code to provide experiment names
|
|
||||||
// in a more flexible way, where we have aliases.
|
|
||||||
//
|
|
||||||
// Because we allow for uppercase experiment names for backwards
|
|
||||||
// compatibility with MK, we need to add some exceptions here when
|
|
||||||
// mapping (e.g., DNSCheck => dnscheck).
|
|
||||||
func canonicalizeExperimentName(name string) string {
|
|
||||||
switch name = strcase.ToSnake(name); name {
|
|
||||||
case "ndt_7":
|
|
||||||
name = "ndt" // since 2020-03-18, we use ndt7 to implement ndt by default
|
|
||||||
case "dns_check":
|
|
||||||
name = "dnscheck"
|
|
||||||
case "stun_reachability":
|
|
||||||
name = "stunreachability"
|
|
||||||
default:
|
|
||||||
}
|
|
||||||
return name
|
|
||||||
}
|
|
||||||
|
|
||||||
// newExperimentBuilder creates a new experimentBuilder instance.
|
// newExperimentBuilder creates a new experimentBuilder instance.
|
||||||
func newExperimentBuilder(session *Session, name string) (*experimentBuilder, error) {
|
func newExperimentBuilder(session *Session, name string) (*experimentBuilder, error) {
|
||||||
factory := experimentsByName[canonicalizeExperimentName(name)]
|
factory, err := registry.NewFactory(name)
|
||||||
if factory == nil {
|
if err != nil {
|
||||||
return nil, fmt.Errorf("no such experiment: %s", name)
|
return nil, err
|
||||||
|
}
|
||||||
|
builder := &experimentBuilder{
|
||||||
|
factory: factory,
|
||||||
|
callbacks: model.NewPrinterCallbacks(session.Logger()),
|
||||||
|
session: session,
|
||||||
}
|
}
|
||||||
builder := factory(session)
|
|
||||||
builder.callbacks = model.NewPrinterCallbacks(session.Logger())
|
|
||||||
return builder, nil
|
return builder, nil
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,347 +1 @@
|
||||||
package engine
|
package engine
|
||||||
|
|
||||||
import (
|
|
||||||
"errors"
|
|
||||||
"testing"
|
|
||||||
|
|
||||||
"github.com/google/go-cmp/cmp"
|
|
||||||
)
|
|
||||||
|
|
||||||
type fakeExperimentConfig struct {
|
|
||||||
Chan chan any `ooni:"we cannot set this"`
|
|
||||||
String string `ooni:"a string"`
|
|
||||||
Truth bool `ooni:"something that no-one knows"`
|
|
||||||
Value int64 `ooni:"a number"`
|
|
||||||
}
|
|
||||||
|
|
||||||
func TestExperimentBuilderOptions(t *testing.T) {
|
|
||||||
t.Run("when config is not a pointer", func(t *testing.T) {
|
|
||||||
b := &experimentBuilder{
|
|
||||||
config: 17,
|
|
||||||
}
|
|
||||||
options, err := b.Options()
|
|
||||||
if !errors.Is(err, ErrConfigIsNotAStructPointer) {
|
|
||||||
t.Fatal("expected an error here")
|
|
||||||
}
|
|
||||||
if options != nil {
|
|
||||||
t.Fatal("expected nil here")
|
|
||||||
}
|
|
||||||
})
|
|
||||||
|
|
||||||
t.Run("when config is not a struct", func(t *testing.T) {
|
|
||||||
number := 17
|
|
||||||
b := &experimentBuilder{
|
|
||||||
config: &number,
|
|
||||||
}
|
|
||||||
options, err := b.Options()
|
|
||||||
if !errors.Is(err, ErrConfigIsNotAStructPointer) {
|
|
||||||
t.Fatal("expected an error here")
|
|
||||||
}
|
|
||||||
if options != nil {
|
|
||||||
t.Fatal("expected nil here")
|
|
||||||
}
|
|
||||||
})
|
|
||||||
|
|
||||||
t.Run("when config is a pointer to struct", func(t *testing.T) {
|
|
||||||
config := &fakeExperimentConfig{}
|
|
||||||
b := &experimentBuilder{
|
|
||||||
config: config,
|
|
||||||
}
|
|
||||||
options, err := b.Options()
|
|
||||||
if err != nil {
|
|
||||||
t.Fatal(err)
|
|
||||||
}
|
|
||||||
for name, value := range options {
|
|
||||||
switch name {
|
|
||||||
case "Chan":
|
|
||||||
if value.Doc != "we cannot set this" {
|
|
||||||
t.Fatal("invalid doc")
|
|
||||||
}
|
|
||||||
if value.Type != "chan interface {}" {
|
|
||||||
t.Fatal("invalid type", value.Type)
|
|
||||||
}
|
|
||||||
case "String":
|
|
||||||
if value.Doc != "a string" {
|
|
||||||
t.Fatal("invalid doc")
|
|
||||||
}
|
|
||||||
if value.Type != "string" {
|
|
||||||
t.Fatal("invalid type", value.Type)
|
|
||||||
}
|
|
||||||
case "Truth":
|
|
||||||
if value.Doc != "something that no-one knows" {
|
|
||||||
t.Fatal("invalid doc")
|
|
||||||
}
|
|
||||||
if value.Type != "bool" {
|
|
||||||
t.Fatal("invalid type", value.Type)
|
|
||||||
}
|
|
||||||
case "Value":
|
|
||||||
if value.Doc != "a number" {
|
|
||||||
t.Fatal("invalid doc")
|
|
||||||
}
|
|
||||||
if value.Type != "int64" {
|
|
||||||
t.Fatal("invalid type", value.Type)
|
|
||||||
}
|
|
||||||
default:
|
|
||||||
t.Fatal("unknown name", name)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
func TestExperimentBuilderSetOptionAny(t *testing.T) {
|
|
||||||
var inputs = []struct {
|
|
||||||
TestCaseName string
|
|
||||||
InitialConfig any
|
|
||||||
FieldName string
|
|
||||||
FieldValue any
|
|
||||||
ExpectErr error
|
|
||||||
ExpectConfig any
|
|
||||||
}{{
|
|
||||||
TestCaseName: "config is not a pointer",
|
|
||||||
InitialConfig: fakeExperimentConfig{},
|
|
||||||
FieldName: "Antani",
|
|
||||||
FieldValue: true,
|
|
||||||
ExpectErr: ErrConfigIsNotAStructPointer,
|
|
||||||
ExpectConfig: fakeExperimentConfig{},
|
|
||||||
}, {
|
|
||||||
TestCaseName: "config is not a pointer to struct",
|
|
||||||
InitialConfig: func() *int {
|
|
||||||
v := 17
|
|
||||||
return &v
|
|
||||||
}(),
|
|
||||||
FieldName: "Antani",
|
|
||||||
FieldValue: true,
|
|
||||||
ExpectErr: ErrConfigIsNotAStructPointer,
|
|
||||||
ExpectConfig: func() *int {
|
|
||||||
v := 17
|
|
||||||
return &v
|
|
||||||
}(),
|
|
||||||
}, {
|
|
||||||
TestCaseName: "for missing field",
|
|
||||||
InitialConfig: &fakeExperimentConfig{},
|
|
||||||
FieldName: "Antani",
|
|
||||||
FieldValue: true,
|
|
||||||
ExpectErr: ErrNoSuchField,
|
|
||||||
ExpectConfig: &fakeExperimentConfig{},
|
|
||||||
}, {
|
|
||||||
TestCaseName: "[bool] for true value represented as string",
|
|
||||||
InitialConfig: &fakeExperimentConfig{},
|
|
||||||
FieldName: "Truth",
|
|
||||||
FieldValue: "true",
|
|
||||||
ExpectErr: nil,
|
|
||||||
ExpectConfig: &fakeExperimentConfig{
|
|
||||||
Truth: true,
|
|
||||||
},
|
|
||||||
}, {
|
|
||||||
TestCaseName: "[bool] for false value represented as string",
|
|
||||||
InitialConfig: &fakeExperimentConfig{
|
|
||||||
Truth: true,
|
|
||||||
},
|
|
||||||
FieldName: "Truth",
|
|
||||||
FieldValue: "false",
|
|
||||||
ExpectErr: nil,
|
|
||||||
ExpectConfig: &fakeExperimentConfig{
|
|
||||||
Truth: false, // must have been flipped
|
|
||||||
},
|
|
||||||
}, {
|
|
||||||
TestCaseName: "[bool] for true value",
|
|
||||||
InitialConfig: &fakeExperimentConfig{},
|
|
||||||
FieldName: "Truth",
|
|
||||||
FieldValue: true,
|
|
||||||
ExpectErr: nil,
|
|
||||||
ExpectConfig: &fakeExperimentConfig{
|
|
||||||
Truth: true,
|
|
||||||
},
|
|
||||||
}, {
|
|
||||||
TestCaseName: "[bool] for false value",
|
|
||||||
InitialConfig: &fakeExperimentConfig{
|
|
||||||
Truth: true,
|
|
||||||
},
|
|
||||||
FieldName: "Truth",
|
|
||||||
FieldValue: false,
|
|
||||||
ExpectErr: nil,
|
|
||||||
ExpectConfig: &fakeExperimentConfig{
|
|
||||||
Truth: false, // must have been flipped
|
|
||||||
},
|
|
||||||
}, {
|
|
||||||
TestCaseName: "[bool] for invalid string representation of bool",
|
|
||||||
InitialConfig: &fakeExperimentConfig{},
|
|
||||||
FieldName: "Truth",
|
|
||||||
FieldValue: "xxx",
|
|
||||||
ExpectErr: ErrInvalidStringRepresentationOfBool,
|
|
||||||
ExpectConfig: &fakeExperimentConfig{},
|
|
||||||
}, {
|
|
||||||
TestCaseName: "[bool] for value we don't know how to convert to bool",
|
|
||||||
InitialConfig: &fakeExperimentConfig{},
|
|
||||||
FieldName: "Truth",
|
|
||||||
FieldValue: make(chan any),
|
|
||||||
ExpectErr: ErrCannotSetBoolOption,
|
|
||||||
ExpectConfig: &fakeExperimentConfig{},
|
|
||||||
}, {
|
|
||||||
TestCaseName: "[int] for int",
|
|
||||||
InitialConfig: &fakeExperimentConfig{},
|
|
||||||
FieldName: "Value",
|
|
||||||
FieldValue: 17,
|
|
||||||
ExpectErr: nil,
|
|
||||||
ExpectConfig: &fakeExperimentConfig{
|
|
||||||
Value: 17,
|
|
||||||
},
|
|
||||||
}, {
|
|
||||||
TestCaseName: "[int] for int64",
|
|
||||||
InitialConfig: &fakeExperimentConfig{},
|
|
||||||
FieldName: "Value",
|
|
||||||
FieldValue: int64(17),
|
|
||||||
ExpectErr: nil,
|
|
||||||
ExpectConfig: &fakeExperimentConfig{
|
|
||||||
Value: 17,
|
|
||||||
},
|
|
||||||
}, {
|
|
||||||
TestCaseName: "[int] for int32",
|
|
||||||
InitialConfig: &fakeExperimentConfig{},
|
|
||||||
FieldName: "Value",
|
|
||||||
FieldValue: int32(17),
|
|
||||||
ExpectErr: nil,
|
|
||||||
ExpectConfig: &fakeExperimentConfig{
|
|
||||||
Value: 17,
|
|
||||||
},
|
|
||||||
}, {
|
|
||||||
TestCaseName: "[int] for int16",
|
|
||||||
InitialConfig: &fakeExperimentConfig{},
|
|
||||||
FieldName: "Value",
|
|
||||||
FieldValue: int16(17),
|
|
||||||
ExpectErr: nil,
|
|
||||||
ExpectConfig: &fakeExperimentConfig{
|
|
||||||
Value: 17,
|
|
||||||
},
|
|
||||||
}, {
|
|
||||||
TestCaseName: "[int] for int8",
|
|
||||||
InitialConfig: &fakeExperimentConfig{},
|
|
||||||
FieldName: "Value",
|
|
||||||
FieldValue: int8(17),
|
|
||||||
ExpectErr: nil,
|
|
||||||
ExpectConfig: &fakeExperimentConfig{
|
|
||||||
Value: 17,
|
|
||||||
},
|
|
||||||
}, {
|
|
||||||
TestCaseName: "[int] for string representation of int",
|
|
||||||
InitialConfig: &fakeExperimentConfig{},
|
|
||||||
FieldName: "Value",
|
|
||||||
FieldValue: "17",
|
|
||||||
ExpectErr: nil,
|
|
||||||
ExpectConfig: &fakeExperimentConfig{
|
|
||||||
Value: 17,
|
|
||||||
},
|
|
||||||
}, {
|
|
||||||
TestCaseName: "[int] for invalid string representation of int",
|
|
||||||
InitialConfig: &fakeExperimentConfig{},
|
|
||||||
FieldName: "Value",
|
|
||||||
FieldValue: "xx",
|
|
||||||
ExpectErr: ErrCannotSetIntegerOption,
|
|
||||||
ExpectConfig: &fakeExperimentConfig{},
|
|
||||||
}, {
|
|
||||||
TestCaseName: "[int] for type we don't know how to convert to int",
|
|
||||||
InitialConfig: &fakeExperimentConfig{},
|
|
||||||
FieldName: "Value",
|
|
||||||
FieldValue: make(chan any),
|
|
||||||
ExpectErr: ErrCannotSetIntegerOption,
|
|
||||||
ExpectConfig: &fakeExperimentConfig{},
|
|
||||||
}, {
|
|
||||||
TestCaseName: "[string] for serialized bool value while setting a string value",
|
|
||||||
InitialConfig: &fakeExperimentConfig{},
|
|
||||||
FieldName: "String",
|
|
||||||
FieldValue: "true",
|
|
||||||
ExpectErr: nil,
|
|
||||||
ExpectConfig: &fakeExperimentConfig{
|
|
||||||
String: "true",
|
|
||||||
},
|
|
||||||
}, {
|
|
||||||
TestCaseName: "[string] for serialized int value while setting a string value",
|
|
||||||
InitialConfig: &fakeExperimentConfig{},
|
|
||||||
FieldName: "String",
|
|
||||||
FieldValue: "155",
|
|
||||||
ExpectErr: nil,
|
|
||||||
ExpectConfig: &fakeExperimentConfig{
|
|
||||||
String: "155",
|
|
||||||
},
|
|
||||||
}, {
|
|
||||||
TestCaseName: "[string] for any other string",
|
|
||||||
InitialConfig: &fakeExperimentConfig{},
|
|
||||||
FieldName: "String",
|
|
||||||
FieldValue: "xxx",
|
|
||||||
ExpectErr: nil,
|
|
||||||
ExpectConfig: &fakeExperimentConfig{
|
|
||||||
String: "xxx",
|
|
||||||
},
|
|
||||||
}, {
|
|
||||||
TestCaseName: "[string] for type we don't know how to convert to string",
|
|
||||||
InitialConfig: &fakeExperimentConfig{},
|
|
||||||
FieldName: "String",
|
|
||||||
FieldValue: make(chan any),
|
|
||||||
ExpectErr: ErrCannotSetStringOption,
|
|
||||||
ExpectConfig: &fakeExperimentConfig{},
|
|
||||||
}, {
|
|
||||||
TestCaseName: "for a field that we don't know how to set",
|
|
||||||
InitialConfig: &fakeExperimentConfig{},
|
|
||||||
FieldName: "Chan",
|
|
||||||
FieldValue: make(chan any),
|
|
||||||
ExpectErr: ErrUnsupportedOptionType,
|
|
||||||
ExpectConfig: &fakeExperimentConfig{},
|
|
||||||
}}
|
|
||||||
|
|
||||||
for _, input := range inputs {
|
|
||||||
t.Run(input.TestCaseName, func(t *testing.T) {
|
|
||||||
ec := input.InitialConfig
|
|
||||||
b := &experimentBuilder{config: ec}
|
|
||||||
err := b.SetOptionAny(input.FieldName, input.FieldValue)
|
|
||||||
if !errors.Is(err, input.ExpectErr) {
|
|
||||||
t.Fatal(err)
|
|
||||||
}
|
|
||||||
if diff := cmp.Diff(input.ExpectConfig, ec); diff != "" {
|
|
||||||
t.Fatal(diff)
|
|
||||||
}
|
|
||||||
})
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func TestExperimentBuilderSetOptionsAny(t *testing.T) {
|
|
||||||
b := &experimentBuilder{config: &fakeExperimentConfig{}}
|
|
||||||
|
|
||||||
t.Run("we correctly handle an empty map", func(t *testing.T) {
|
|
||||||
if err := b.SetOptionsAny(nil); err != nil {
|
|
||||||
t.Fatal(err)
|
|
||||||
}
|
|
||||||
})
|
|
||||||
|
|
||||||
t.Run("we correctly handle a map containing options", func(t *testing.T) {
|
|
||||||
f := &fakeExperimentConfig{}
|
|
||||||
privateb := &experimentBuilder{config: f}
|
|
||||||
opts := map[string]any{
|
|
||||||
"String": "yoloyolo",
|
|
||||||
"Value": "174",
|
|
||||||
"Truth": "true",
|
|
||||||
}
|
|
||||||
if err := privateb.SetOptionsAny(opts); err != nil {
|
|
||||||
t.Fatal(err)
|
|
||||||
}
|
|
||||||
if f.String != "yoloyolo" {
|
|
||||||
t.Fatal("cannot set string value")
|
|
||||||
}
|
|
||||||
if f.Value != 174 {
|
|
||||||
t.Fatal("cannot set integer value")
|
|
||||||
}
|
|
||||||
if f.Truth != true {
|
|
||||||
t.Fatal("cannot set bool value")
|
|
||||||
}
|
|
||||||
})
|
|
||||||
|
|
||||||
t.Run("we handle mistakes in a map containing string options", func(t *testing.T) {
|
|
||||||
opts := map[string]any{
|
|
||||||
"String": "yoloyolo",
|
|
||||||
"Value": "xx",
|
|
||||||
"Truth": "true",
|
|
||||||
}
|
|
||||||
if err := b.SetOptionsAny(opts); !errors.Is(err, ErrCannotSetIntegerOption) {
|
|
||||||
t.Fatal("unexpected err", err)
|
|
||||||
}
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
|
@ -11,6 +11,7 @@ import (
|
||||||
"github.com/apex/log"
|
"github.com/apex/log"
|
||||||
"github.com/ooni/probe-cli/v3/internal/fsx"
|
"github.com/ooni/probe-cli/v3/internal/fsx"
|
||||||
"github.com/ooni/probe-cli/v3/internal/model"
|
"github.com/ooni/probe-cli/v3/internal/model"
|
||||||
|
"github.com/ooni/probe-cli/v3/internal/registry"
|
||||||
"github.com/ooni/probe-cli/v3/internal/stuninput"
|
"github.com/ooni/probe-cli/v3/internal/stuninput"
|
||||||
)
|
)
|
||||||
|
|
||||||
|
@ -86,7 +87,7 @@ type InputLoader struct {
|
||||||
// current experiment. We will not load any input if
|
// current experiment. We will not load any input if
|
||||||
// the policy says we should not. You MUST fill in
|
// the policy says we should not. You MUST fill in
|
||||||
// this field.
|
// this field.
|
||||||
InputPolicy InputPolicy
|
InputPolicy model.InputPolicy
|
||||||
|
|
||||||
// Logger is the optional logger that the InputLoader
|
// Logger is the optional logger that the InputLoader
|
||||||
// should be using. If not set, we will use the default
|
// should be using. If not set, we will use the default
|
||||||
|
@ -112,13 +113,13 @@ type InputLoader struct {
|
||||||
// return a list of URLs because this is the only input we support.
|
// return a list of URLs because this is the only input we support.
|
||||||
func (il *InputLoader) Load(ctx context.Context) ([]model.OOAPIURLInfo, error) {
|
func (il *InputLoader) Load(ctx context.Context) ([]model.OOAPIURLInfo, error) {
|
||||||
switch il.InputPolicy {
|
switch il.InputPolicy {
|
||||||
case InputOptional:
|
case model.InputOptional:
|
||||||
return il.loadOptional()
|
return il.loadOptional()
|
||||||
case InputOrQueryBackend:
|
case model.InputOrQueryBackend:
|
||||||
return il.loadOrQueryBackend(ctx)
|
return il.loadOrQueryBackend(ctx)
|
||||||
case InputStrictlyRequired:
|
case model.InputStrictlyRequired:
|
||||||
return il.loadStrictlyRequired(ctx)
|
return il.loadStrictlyRequired(ctx)
|
||||||
case InputOrStaticDefault:
|
case model.InputOrStaticDefault:
|
||||||
return il.loadOrStaticDefault(ctx)
|
return il.loadOrStaticDefault(ctx)
|
||||||
default:
|
default:
|
||||||
return il.loadNone()
|
return il.loadNone()
|
||||||
|
@ -299,7 +300,7 @@ func StaticBareInputForExperiment(name string) ([]string, error) {
|
||||||
// Implementation note: we may be called from pkg/oonimkall
|
// Implementation note: we may be called from pkg/oonimkall
|
||||||
// with a non-canonical experiment name, so we need to convert
|
// with a non-canonical experiment name, so we need to convert
|
||||||
// the experiment name to be canonical before proceeding.
|
// the experiment name to be canonical before proceeding.
|
||||||
switch canonicalizeExperimentName(name) {
|
switch registry.CanonicalizeExperimentName(name) {
|
||||||
case "dnscheck":
|
case "dnscheck":
|
||||||
return dnsCheckDefaultInput, nil
|
return dnsCheckDefaultInput, nil
|
||||||
case "stunreachability":
|
case "stunreachability":
|
||||||
|
|
|
@ -30,7 +30,7 @@ func TestInputLoaderInputOrQueryBackendWithNoInput(t *testing.T) {
|
||||||
}
|
}
|
||||||
defer sess.Close()
|
defer sess.Close()
|
||||||
il := &engine.InputLoader{
|
il := &engine.InputLoader{
|
||||||
InputPolicy: engine.InputOrQueryBackend,
|
InputPolicy: model.InputOrQueryBackend,
|
||||||
Session: sess,
|
Session: sess,
|
||||||
}
|
}
|
||||||
ctx := context.Background()
|
ctx := context.Background()
|
||||||
|
|
|
@ -19,7 +19,7 @@ import (
|
||||||
func TestInputLoaderInputNoneWithStaticInputs(t *testing.T) {
|
func TestInputLoaderInputNoneWithStaticInputs(t *testing.T) {
|
||||||
il := &InputLoader{
|
il := &InputLoader{
|
||||||
StaticInputs: []string{"https://www.google.com/"},
|
StaticInputs: []string{"https://www.google.com/"},
|
||||||
InputPolicy: InputNone,
|
InputPolicy: model.InputNone,
|
||||||
}
|
}
|
||||||
ctx := context.Background()
|
ctx := context.Background()
|
||||||
out, err := il.Load(ctx)
|
out, err := il.Load(ctx)
|
||||||
|
@ -37,7 +37,7 @@ func TestInputLoaderInputNoneWithFilesInputs(t *testing.T) {
|
||||||
"testdata/inputloader1.txt",
|
"testdata/inputloader1.txt",
|
||||||
"testdata/inputloader2.txt",
|
"testdata/inputloader2.txt",
|
||||||
},
|
},
|
||||||
InputPolicy: InputNone,
|
InputPolicy: model.InputNone,
|
||||||
}
|
}
|
||||||
ctx := context.Background()
|
ctx := context.Background()
|
||||||
out, err := il.Load(ctx)
|
out, err := il.Load(ctx)
|
||||||
|
@ -56,7 +56,7 @@ func TestInputLoaderInputNoneWithBothInputs(t *testing.T) {
|
||||||
"testdata/inputloader1.txt",
|
"testdata/inputloader1.txt",
|
||||||
"testdata/inputloader2.txt",
|
"testdata/inputloader2.txt",
|
||||||
},
|
},
|
||||||
InputPolicy: InputNone,
|
InputPolicy: model.InputNone,
|
||||||
}
|
}
|
||||||
ctx := context.Background()
|
ctx := context.Background()
|
||||||
out, err := il.Load(ctx)
|
out, err := il.Load(ctx)
|
||||||
|
@ -70,7 +70,7 @@ func TestInputLoaderInputNoneWithBothInputs(t *testing.T) {
|
||||||
|
|
||||||
func TestInputLoaderInputNoneWithNoInput(t *testing.T) {
|
func TestInputLoaderInputNoneWithNoInput(t *testing.T) {
|
||||||
il := &InputLoader{
|
il := &InputLoader{
|
||||||
InputPolicy: InputNone,
|
InputPolicy: model.InputNone,
|
||||||
}
|
}
|
||||||
ctx := context.Background()
|
ctx := context.Background()
|
||||||
out, err := il.Load(ctx)
|
out, err := il.Load(ctx)
|
||||||
|
@ -84,7 +84,7 @@ func TestInputLoaderInputNoneWithNoInput(t *testing.T) {
|
||||||
|
|
||||||
func TestInputLoaderInputOptionalWithNoInput(t *testing.T) {
|
func TestInputLoaderInputOptionalWithNoInput(t *testing.T) {
|
||||||
il := &InputLoader{
|
il := &InputLoader{
|
||||||
InputPolicy: InputOptional,
|
InputPolicy: model.InputOptional,
|
||||||
}
|
}
|
||||||
ctx := context.Background()
|
ctx := context.Background()
|
||||||
out, err := il.Load(ctx)
|
out, err := il.Load(ctx)
|
||||||
|
@ -103,7 +103,7 @@ func TestInputLoaderInputOptionalWithInput(t *testing.T) {
|
||||||
"testdata/inputloader1.txt",
|
"testdata/inputloader1.txt",
|
||||||
"testdata/inputloader2.txt",
|
"testdata/inputloader2.txt",
|
||||||
},
|
},
|
||||||
InputPolicy: InputOptional,
|
InputPolicy: model.InputOptional,
|
||||||
}
|
}
|
||||||
ctx := context.Background()
|
ctx := context.Background()
|
||||||
out, err := il.Load(ctx)
|
out, err := il.Load(ctx)
|
||||||
|
@ -133,7 +133,7 @@ func TestInputLoaderInputOptionalNonexistentFile(t *testing.T) {
|
||||||
"/nonexistent",
|
"/nonexistent",
|
||||||
"testdata/inputloader2.txt",
|
"testdata/inputloader2.txt",
|
||||||
},
|
},
|
||||||
InputPolicy: InputOptional,
|
InputPolicy: model.InputOptional,
|
||||||
}
|
}
|
||||||
ctx := context.Background()
|
ctx := context.Background()
|
||||||
out, err := il.Load(ctx)
|
out, err := il.Load(ctx)
|
||||||
|
@ -152,7 +152,7 @@ func TestInputLoaderInputStrictlyRequiredWithInput(t *testing.T) {
|
||||||
"testdata/inputloader1.txt",
|
"testdata/inputloader1.txt",
|
||||||
"testdata/inputloader2.txt",
|
"testdata/inputloader2.txt",
|
||||||
},
|
},
|
||||||
InputPolicy: InputStrictlyRequired,
|
InputPolicy: model.InputStrictlyRequired,
|
||||||
}
|
}
|
||||||
ctx := context.Background()
|
ctx := context.Background()
|
||||||
out, err := il.Load(ctx)
|
out, err := il.Load(ctx)
|
||||||
|
@ -176,7 +176,7 @@ func TestInputLoaderInputStrictlyRequiredWithInput(t *testing.T) {
|
||||||
|
|
||||||
func TestInputLoaderInputStrictlyRequiredWithoutInput(t *testing.T) {
|
func TestInputLoaderInputStrictlyRequiredWithoutInput(t *testing.T) {
|
||||||
il := &InputLoader{
|
il := &InputLoader{
|
||||||
InputPolicy: InputStrictlyRequired,
|
InputPolicy: model.InputStrictlyRequired,
|
||||||
}
|
}
|
||||||
ctx := context.Background()
|
ctx := context.Background()
|
||||||
out, err := il.Load(ctx)
|
out, err := il.Load(ctx)
|
||||||
|
@ -190,7 +190,7 @@ func TestInputLoaderInputStrictlyRequiredWithoutInput(t *testing.T) {
|
||||||
|
|
||||||
func TestInputLoaderInputStrictlyRequiredWithEmptyFile(t *testing.T) {
|
func TestInputLoaderInputStrictlyRequiredWithEmptyFile(t *testing.T) {
|
||||||
il := &InputLoader{
|
il := &InputLoader{
|
||||||
InputPolicy: InputStrictlyRequired,
|
InputPolicy: model.InputStrictlyRequired,
|
||||||
SourceFiles: []string{
|
SourceFiles: []string{
|
||||||
"testdata/inputloader1.txt",
|
"testdata/inputloader1.txt",
|
||||||
"testdata/inputloader3.txt", // we want it before inputloader2.txt
|
"testdata/inputloader3.txt", // we want it before inputloader2.txt
|
||||||
|
@ -215,7 +215,7 @@ func TestInputLoaderInputOrStaticDefaultWithInput(t *testing.T) {
|
||||||
"testdata/inputloader1.txt",
|
"testdata/inputloader1.txt",
|
||||||
"testdata/inputloader2.txt",
|
"testdata/inputloader2.txt",
|
||||||
},
|
},
|
||||||
InputPolicy: InputOrStaticDefault,
|
InputPolicy: model.InputOrStaticDefault,
|
||||||
}
|
}
|
||||||
ctx := context.Background()
|
ctx := context.Background()
|
||||||
out, err := il.Load(ctx)
|
out, err := il.Load(ctx)
|
||||||
|
@ -240,7 +240,7 @@ func TestInputLoaderInputOrStaticDefaultWithInput(t *testing.T) {
|
||||||
func TestInputLoaderInputOrStaticDefaultWithEmptyFile(t *testing.T) {
|
func TestInputLoaderInputOrStaticDefaultWithEmptyFile(t *testing.T) {
|
||||||
il := &InputLoader{
|
il := &InputLoader{
|
||||||
ExperimentName: "dnscheck",
|
ExperimentName: "dnscheck",
|
||||||
InputPolicy: InputOrStaticDefault,
|
InputPolicy: model.InputOrStaticDefault,
|
||||||
SourceFiles: []string{
|
SourceFiles: []string{
|
||||||
"testdata/inputloader1.txt",
|
"testdata/inputloader1.txt",
|
||||||
"testdata/inputloader3.txt", // we want it before inputloader2.txt
|
"testdata/inputloader3.txt", // we want it before inputloader2.txt
|
||||||
|
@ -260,7 +260,7 @@ func TestInputLoaderInputOrStaticDefaultWithEmptyFile(t *testing.T) {
|
||||||
func TestInputLoaderInputOrStaticDefaultWithoutInputDNSCheck(t *testing.T) {
|
func TestInputLoaderInputOrStaticDefaultWithoutInputDNSCheck(t *testing.T) {
|
||||||
il := &InputLoader{
|
il := &InputLoader{
|
||||||
ExperimentName: "dnscheck",
|
ExperimentName: "dnscheck",
|
||||||
InputPolicy: InputOrStaticDefault,
|
InputPolicy: model.InputOrStaticDefault,
|
||||||
}
|
}
|
||||||
ctx := context.Background()
|
ctx := context.Background()
|
||||||
out, err := il.Load(ctx)
|
out, err := il.Load(ctx)
|
||||||
|
@ -287,7 +287,7 @@ func TestInputLoaderInputOrStaticDefaultWithoutInputDNSCheck(t *testing.T) {
|
||||||
func TestInputLoaderInputOrStaticDefaultWithoutInputStunReachability(t *testing.T) {
|
func TestInputLoaderInputOrStaticDefaultWithoutInputStunReachability(t *testing.T) {
|
||||||
il := &InputLoader{
|
il := &InputLoader{
|
||||||
ExperimentName: "stunreachability",
|
ExperimentName: "stunreachability",
|
||||||
InputPolicy: InputOrStaticDefault,
|
InputPolicy: model.InputOrStaticDefault,
|
||||||
}
|
}
|
||||||
ctx := context.Background()
|
ctx := context.Background()
|
||||||
out, err := il.Load(ctx)
|
out, err := il.Load(ctx)
|
||||||
|
@ -323,7 +323,7 @@ func TestStaticBareInputForExperimentWorksWithNonCanonicalNames(t *testing.T) {
|
||||||
func TestInputLoaderInputOrStaticDefaultWithoutInputOtherName(t *testing.T) {
|
func TestInputLoaderInputOrStaticDefaultWithoutInputOtherName(t *testing.T) {
|
||||||
il := &InputLoader{
|
il := &InputLoader{
|
||||||
ExperimentName: "xx",
|
ExperimentName: "xx",
|
||||||
InputPolicy: InputOrStaticDefault,
|
InputPolicy: model.InputOrStaticDefault,
|
||||||
}
|
}
|
||||||
ctx := context.Background()
|
ctx := context.Background()
|
||||||
out, err := il.Load(ctx)
|
out, err := il.Load(ctx)
|
||||||
|
@ -342,7 +342,7 @@ func TestInputLoaderInputOrQueryBackendWithInput(t *testing.T) {
|
||||||
"testdata/inputloader1.txt",
|
"testdata/inputloader1.txt",
|
||||||
"testdata/inputloader2.txt",
|
"testdata/inputloader2.txt",
|
||||||
},
|
},
|
||||||
InputPolicy: InputOrQueryBackend,
|
InputPolicy: model.InputOrQueryBackend,
|
||||||
}
|
}
|
||||||
ctx := context.Background()
|
ctx := context.Background()
|
||||||
out, err := il.Load(ctx)
|
out, err := il.Load(ctx)
|
||||||
|
@ -377,7 +377,7 @@ func TestInputLoaderInputOrQueryBackendWithNoInputAndCancelledContext(t *testing
|
||||||
}
|
}
|
||||||
defer sess.Close()
|
defer sess.Close()
|
||||||
il := &InputLoader{
|
il := &InputLoader{
|
||||||
InputPolicy: InputOrQueryBackend,
|
InputPolicy: model.InputOrQueryBackend,
|
||||||
Session: sess,
|
Session: sess,
|
||||||
}
|
}
|
||||||
ctx, cancel := context.WithCancel(context.Background())
|
ctx, cancel := context.WithCancel(context.Background())
|
||||||
|
@ -393,7 +393,7 @@ func TestInputLoaderInputOrQueryBackendWithNoInputAndCancelledContext(t *testing
|
||||||
|
|
||||||
func TestInputLoaderInputOrQueryBackendWithEmptyFile(t *testing.T) {
|
func TestInputLoaderInputOrQueryBackendWithEmptyFile(t *testing.T) {
|
||||||
il := &InputLoader{
|
il := &InputLoader{
|
||||||
InputPolicy: InputOrQueryBackend,
|
InputPolicy: model.InputOrQueryBackend,
|
||||||
SourceFiles: []string{
|
SourceFiles: []string{
|
||||||
"testdata/inputloader1.txt",
|
"testdata/inputloader1.txt",
|
||||||
"testdata/inputloader3.txt", // we want it before inputloader2.txt
|
"testdata/inputloader3.txt", // we want it before inputloader2.txt
|
||||||
|
|
|
@ -390,7 +390,7 @@ var ErrAlreadyUsingProxy = errors.New(
|
||||||
// NewExperimentBuilder returns a new experiment builder
|
// NewExperimentBuilder returns a new experiment builder
|
||||||
// for the experiment with the given name, or an error if
|
// for the experiment with the given name, or an error if
|
||||||
// there's no such experiment with the given name
|
// there's no such experiment with the given name
|
||||||
func (s *Session) NewExperimentBuilder(name string) (ExperimentBuilder, error) {
|
func (s *Session) NewExperimentBuilder(name string) (model.ExperimentBuilder, error) {
|
||||||
eb, err := newExperimentBuilder(s, name)
|
eb, err := newExperimentBuilder(s, name)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
|
|
|
@ -141,3 +141,143 @@ type ExperimentMeasurer interface {
|
||||||
// GetSummaryKeys returns summary keys expected by ooni/probe-cli.
|
// GetSummaryKeys returns summary keys expected by ooni/probe-cli.
|
||||||
GetSummaryKeys(*Measurement) (interface{}, error)
|
GetSummaryKeys(*Measurement) (interface{}, error)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Experiment is an experiment instance.
|
||||||
|
type Experiment interface {
|
||||||
|
// KibiBytesReceived accounts for the KibiBytes received by the experiment.
|
||||||
|
KibiBytesReceived() float64
|
||||||
|
|
||||||
|
// KibiBytesSent is like KibiBytesReceived but for the bytes sent.
|
||||||
|
KibiBytesSent() float64
|
||||||
|
|
||||||
|
// Name returns the experiment name.
|
||||||
|
Name() string
|
||||||
|
|
||||||
|
// GetSummaryKeys returns a data structure containing a
|
||||||
|
// summary of the test keys for ooniprobe.
|
||||||
|
GetSummaryKeys(m *Measurement) (any, error)
|
||||||
|
|
||||||
|
// ReportID returns the open report's ID, if we have opened a report
|
||||||
|
// successfully before, or an empty string, otherwise.
|
||||||
|
//
|
||||||
|
// Deprecated: new code should use a Submitter.
|
||||||
|
ReportID() string
|
||||||
|
|
||||||
|
// MeasureAsync runs an async measurement. This operation could post
|
||||||
|
// one or more measurements onto the returned channel. We'll close the
|
||||||
|
// channel when we've emitted all the measurements.
|
||||||
|
//
|
||||||
|
// Arguments:
|
||||||
|
//
|
||||||
|
// - ctx is the context for deadline/cancellation/timeout;
|
||||||
|
//
|
||||||
|
// - input is the input (typically a URL but it could also be
|
||||||
|
// just an endpoint or an empty string for input-less experiments
|
||||||
|
// such as, e.g., ndt7 and dash).
|
||||||
|
//
|
||||||
|
// Return value:
|
||||||
|
//
|
||||||
|
// - on success, channel where to post measurements (the channel
|
||||||
|
// will be closed when done) and nil error;
|
||||||
|
//
|
||||||
|
// - on failure, nil channel and non-nil error.
|
||||||
|
MeasureAsync(ctx context.Context, input string) (<-chan *Measurement, error)
|
||||||
|
|
||||||
|
// MeasureWithContext performs a synchronous measurement.
|
||||||
|
//
|
||||||
|
// Return value: strictly either a non-nil measurement and
|
||||||
|
// a nil error or a nil measurement and a non-nil error.
|
||||||
|
//
|
||||||
|
// CAVEAT: while this API is perfectly fine for experiments that
|
||||||
|
// return a single measurement, it will only return the first measurement
|
||||||
|
// when used with an asynchronous experiment.
|
||||||
|
MeasureWithContext(ctx context.Context, input string) (measurement *Measurement, err error)
|
||||||
|
|
||||||
|
// SaveMeasurement saves a measurement on the specified file path.
|
||||||
|
//
|
||||||
|
// Deprecated: new code should use a Saver.
|
||||||
|
SaveMeasurement(measurement *Measurement, filePath string) error
|
||||||
|
|
||||||
|
// SubmitAndUpdateMeasurementContext submits a measurement and updates the
|
||||||
|
// fields whose value has changed as part of the submission.
|
||||||
|
//
|
||||||
|
// Deprecated: new code should use a Submitter.
|
||||||
|
SubmitAndUpdateMeasurementContext(
|
||||||
|
ctx context.Context, measurement *Measurement) error
|
||||||
|
|
||||||
|
// OpenReportContext will open a report using the given context
|
||||||
|
// to possibly limit the lifetime of this operation.
|
||||||
|
//
|
||||||
|
// Deprecated: new code should use a Submitter.
|
||||||
|
OpenReportContext(ctx context.Context) error
|
||||||
|
}
|
||||||
|
|
||||||
|
// InputPolicy describes the experiment policy with respect to input. That is
|
||||||
|
// whether it requires input, optionally accepts input, does not want input.
|
||||||
|
type InputPolicy string
|
||||||
|
|
||||||
|
const (
|
||||||
|
// InputOrQueryBackend indicates that the experiment requires
|
||||||
|
// external input to run and that this kind of input is URLs
|
||||||
|
// from the citizenlab/test-lists repository. If this input
|
||||||
|
// not provided to the experiment, then the code that runs the
|
||||||
|
// experiment is supposed to fetch from URLs from OONI's backends.
|
||||||
|
InputOrQueryBackend = InputPolicy("or_query_backend")
|
||||||
|
|
||||||
|
// InputStrictlyRequired indicates that the experiment
|
||||||
|
// requires input and we currently don't have an API for
|
||||||
|
// fetching such input. Therefore, either the user specifies
|
||||||
|
// input or the experiment will fail for the lack of input.
|
||||||
|
InputStrictlyRequired = InputPolicy("strictly_required")
|
||||||
|
|
||||||
|
// InputOptional indicates that the experiment handles input,
|
||||||
|
// if any; otherwise it fetchs input/uses a default.
|
||||||
|
InputOptional = InputPolicy("optional")
|
||||||
|
|
||||||
|
// InputNone indicates that the experiment does not want any
|
||||||
|
// input and ignores the input if provided with it.
|
||||||
|
InputNone = InputPolicy("none")
|
||||||
|
|
||||||
|
// We gather input from StaticInput and SourceFiles. If there is
|
||||||
|
// input, we return it. Otherwise, we return an internal static
|
||||||
|
// list of inputs to be used with this experiment.
|
||||||
|
InputOrStaticDefault = InputPolicy("or_static_default")
|
||||||
|
)
|
||||||
|
|
||||||
|
// ExperimentBuilder builds an experiment.
|
||||||
|
type ExperimentBuilder interface {
|
||||||
|
// Interruptible tells you whether this is an interruptible experiment. This kind
|
||||||
|
// of experiments (e.g. ndt7) may be interrupted mid way.
|
||||||
|
Interruptible() bool
|
||||||
|
|
||||||
|
// InputPolicy returns the experiment input policy.
|
||||||
|
InputPolicy() InputPolicy
|
||||||
|
|
||||||
|
// Options returns information about the experiment's options.
|
||||||
|
Options() (map[string]ExperimentOptionInfo, error)
|
||||||
|
|
||||||
|
// SetOptionAny sets an option whose value is an any value. We will use reasonable
|
||||||
|
// heuristics to convert the any value to the proper type of the field whose name is
|
||||||
|
// contained by the key variable. If we cannot convert the provided any value to
|
||||||
|
// the proper type, then this function returns an error.
|
||||||
|
SetOptionAny(key string, value any) error
|
||||||
|
|
||||||
|
// SetOptionsAny sets options from a map[string]any. See the documentation of
|
||||||
|
// the SetOptionAny method for more information.
|
||||||
|
SetOptionsAny(options map[string]any) error
|
||||||
|
|
||||||
|
// SetCallbacks sets the experiment's interactive callbacks.
|
||||||
|
SetCallbacks(callbacks ExperimentCallbacks)
|
||||||
|
|
||||||
|
// NewExperiment creates the experiment instance.
|
||||||
|
NewExperiment() Experiment
|
||||||
|
}
|
||||||
|
|
||||||
|
// ExperimentOptionInfo contains info about an experiment option.
|
||||||
|
type ExperimentOptionInfo struct {
|
||||||
|
// Doc contains the documentation.
|
||||||
|
Doc string
|
||||||
|
|
||||||
|
// Type contains the type.
|
||||||
|
Type string
|
||||||
|
}
|
||||||
|
|
|
@ -117,7 +117,7 @@ type inputProcessor interface {
|
||||||
}
|
}
|
||||||
|
|
||||||
// newInputProcessor creates a new inputProcessor instance.
|
// newInputProcessor creates a new inputProcessor instance.
|
||||||
func (ed *Experiment) newInputProcessor(experiment engine.Experiment,
|
func (ed *Experiment) newInputProcessor(experiment model.Experiment,
|
||||||
inputList []model.OOAPIURLInfo, saver engine.Saver, submitter engine.Submitter) inputProcessor {
|
inputList []model.OOAPIURLInfo, saver engine.Saver, submitter engine.Submitter) inputProcessor {
|
||||||
return &engine.InputProcessor{
|
return &engine.InputProcessor{
|
||||||
Annotations: ed.Annotations,
|
Annotations: ed.Annotations,
|
||||||
|
@ -138,7 +138,7 @@ func (ed *Experiment) newInputProcessor(experiment engine.Experiment,
|
||||||
}
|
}
|
||||||
|
|
||||||
// newSaver creates a new engine.Saver instance.
|
// newSaver creates a new engine.Saver instance.
|
||||||
func (ed *Experiment) newSaver(experiment engine.Experiment) (engine.Saver, error) {
|
func (ed *Experiment) newSaver(experiment model.Experiment) (engine.Saver, error) {
|
||||||
return engine.NewSaver(engine.SaverConfig{
|
return engine.NewSaver(engine.SaverConfig{
|
||||||
Enabled: !ed.NoJSON,
|
Enabled: !ed.NoJSON,
|
||||||
Experiment: experiment,
|
Experiment: experiment,
|
||||||
|
@ -157,7 +157,7 @@ func (ed *Experiment) newSubmitter(ctx context.Context) (engine.Submitter, error
|
||||||
}
|
}
|
||||||
|
|
||||||
// newExperimentBuilder creates a new engine.ExperimentBuilder for the given experimentName.
|
// newExperimentBuilder creates a new engine.ExperimentBuilder for the given experimentName.
|
||||||
func (ed *Experiment) newExperimentBuilder(experimentName string) (engine.ExperimentBuilder, error) {
|
func (ed *Experiment) newExperimentBuilder(experimentName string) (model.ExperimentBuilder, error) {
|
||||||
return ed.Session.NewExperimentBuilder(ed.Name)
|
return ed.Session.NewExperimentBuilder(ed.Name)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -167,7 +167,7 @@ type inputLoader interface {
|
||||||
}
|
}
|
||||||
|
|
||||||
// newInputLoader creates a new inputLoader.
|
// newInputLoader creates a new inputLoader.
|
||||||
func (ed *Experiment) newInputLoader(inputPolicy engine.InputPolicy) inputLoader {
|
func (ed *Experiment) newInputLoader(inputPolicy model.InputPolicy) inputLoader {
|
||||||
return &engine.InputLoader{
|
return &engine.InputLoader{
|
||||||
CheckInConfig: &model.OOAPICheckInConfig{
|
CheckInConfig: &model.OOAPICheckInConfig{
|
||||||
RunType: model.RunTypeManual,
|
RunType: model.RunTypeManual,
|
||||||
|
|
|
@ -29,5 +29,5 @@ type Session interface {
|
||||||
Logger() model.Logger
|
Logger() model.Logger
|
||||||
|
|
||||||
// NewExperimentBuilder creates a new engine.ExperimentBuilder.
|
// NewExperimentBuilder creates a new engine.ExperimentBuilder.
|
||||||
NewExperimentBuilder(name string) (engine.ExperimentBuilder, error)
|
NewExperimentBuilder(name string) (model.ExperimentBuilder, error)
|
||||||
}
|
}
|
||||||
|
|
12
internal/registry/allexperiments.go
Normal file
12
internal/registry/allexperiments.go
Normal file
|
@ -0,0 +1,12 @@
|
||||||
|
package registry
|
||||||
|
|
||||||
|
// Where we register all the available experiments.
|
||||||
|
var allexperiments = map[string]*Factory{}
|
||||||
|
|
||||||
|
// ExperimentNames returns the name of all experiments
|
||||||
|
func ExperimentNames() (names []string) {
|
||||||
|
for key := range allexperiments {
|
||||||
|
names = append(names, key)
|
||||||
|
}
|
||||||
|
return
|
||||||
|
}
|
23
internal/registry/dash.go
Normal file
23
internal/registry/dash.go
Normal file
|
@ -0,0 +1,23 @@
|
||||||
|
package registry
|
||||||
|
|
||||||
|
//
|
||||||
|
// Registers the `dash' experiment.
|
||||||
|
//
|
||||||
|
|
||||||
|
import (
|
||||||
|
"github.com/ooni/probe-cli/v3/internal/engine/experiment/dash"
|
||||||
|
"github.com/ooni/probe-cli/v3/internal/model"
|
||||||
|
)
|
||||||
|
|
||||||
|
func init() {
|
||||||
|
allexperiments["dash"] = &Factory{
|
||||||
|
build: func(config interface{}) model.ExperimentMeasurer {
|
||||||
|
return dash.NewExperimentMeasurer(
|
||||||
|
*config.(*dash.Config),
|
||||||
|
)
|
||||||
|
},
|
||||||
|
config: &dash.Config{},
|
||||||
|
interruptible: true,
|
||||||
|
inputPolicy: model.InputNone,
|
||||||
|
}
|
||||||
|
}
|
22
internal/registry/dnscheck.go
Normal file
22
internal/registry/dnscheck.go
Normal file
|
@ -0,0 +1,22 @@
|
||||||
|
package registry
|
||||||
|
|
||||||
|
//
|
||||||
|
// Registers the `dnscheck' experiment.
|
||||||
|
//
|
||||||
|
|
||||||
|
import (
|
||||||
|
"github.com/ooni/probe-cli/v3/internal/engine/experiment/dnscheck"
|
||||||
|
"github.com/ooni/probe-cli/v3/internal/model"
|
||||||
|
)
|
||||||
|
|
||||||
|
func init() {
|
||||||
|
allexperiments["dnscheck"] = &Factory{
|
||||||
|
build: func(config interface{}) model.ExperimentMeasurer {
|
||||||
|
return dnscheck.NewExperimentMeasurer(
|
||||||
|
*config.(*dnscheck.Config),
|
||||||
|
)
|
||||||
|
},
|
||||||
|
config: &dnscheck.Config{},
|
||||||
|
inputPolicy: model.InputOrStaticDefault,
|
||||||
|
}
|
||||||
|
}
|
22
internal/registry/dnsping.go
Normal file
22
internal/registry/dnsping.go
Normal file
|
@ -0,0 +1,22 @@
|
||||||
|
package registry
|
||||||
|
|
||||||
|
//
|
||||||
|
// Registers the `dnsping' experiment.
|
||||||
|
//
|
||||||
|
|
||||||
|
import (
|
||||||
|
"github.com/ooni/probe-cli/v3/internal/engine/experiment/dnsping"
|
||||||
|
"github.com/ooni/probe-cli/v3/internal/model"
|
||||||
|
)
|
||||||
|
|
||||||
|
func init() {
|
||||||
|
allexperiments["dnsping"] = &Factory{
|
||||||
|
build: func(config interface{}) model.ExperimentMeasurer {
|
||||||
|
return dnsping.NewExperimentMeasurer(
|
||||||
|
*config.(*dnsping.Config),
|
||||||
|
)
|
||||||
|
},
|
||||||
|
config: &dnsping.Config{},
|
||||||
|
inputPolicy: model.InputOrStaticDefault,
|
||||||
|
}
|
||||||
|
}
|
2
internal/registry/doc.go
Normal file
2
internal/registry/doc.go
Normal file
|
@ -0,0 +1,2 @@
|
||||||
|
// Package registry contains a registry of all the available experiments.
|
||||||
|
package registry
|
28
internal/registry/example.go
Normal file
28
internal/registry/example.go
Normal file
|
@ -0,0 +1,28 @@
|
||||||
|
package registry
|
||||||
|
|
||||||
|
//
|
||||||
|
// Registers the `example' experiment.
|
||||||
|
//
|
||||||
|
|
||||||
|
import (
|
||||||
|
"time"
|
||||||
|
|
||||||
|
"github.com/ooni/probe-cli/v3/internal/engine/experiment/example"
|
||||||
|
"github.com/ooni/probe-cli/v3/internal/model"
|
||||||
|
)
|
||||||
|
|
||||||
|
func init() {
|
||||||
|
allexperiments["example"] = &Factory{
|
||||||
|
build: func(config interface{}) model.ExperimentMeasurer {
|
||||||
|
return example.NewExperimentMeasurer(
|
||||||
|
*config.(*example.Config), "example",
|
||||||
|
)
|
||||||
|
},
|
||||||
|
config: &example.Config{
|
||||||
|
Message: "Good day from the example experiment!",
|
||||||
|
SleepTime: int64(time.Second),
|
||||||
|
},
|
||||||
|
interruptible: true,
|
||||||
|
inputPolicy: model.InputNone,
|
||||||
|
}
|
||||||
|
}
|
223
internal/registry/factory.go
Normal file
223
internal/registry/factory.go
Normal file
|
@ -0,0 +1,223 @@
|
||||||
|
package registry
|
||||||
|
|
||||||
|
//
|
||||||
|
// Factory for constructing experiments.
|
||||||
|
//
|
||||||
|
|
||||||
|
import (
|
||||||
|
"errors"
|
||||||
|
"fmt"
|
||||||
|
"reflect"
|
||||||
|
"strconv"
|
||||||
|
|
||||||
|
"github.com/iancoleman/strcase"
|
||||||
|
"github.com/ooni/probe-cli/v3/internal/model"
|
||||||
|
)
|
||||||
|
|
||||||
|
// Factory allows to construct an experiment measurer.
|
||||||
|
type Factory struct {
|
||||||
|
// build is the constructor that build an experiment with the given config.
|
||||||
|
build func(config interface{}) model.ExperimentMeasurer
|
||||||
|
|
||||||
|
// config contains the experiment's config.
|
||||||
|
config any
|
||||||
|
|
||||||
|
// inputPolicy contains the experiment's InputPolicy.
|
||||||
|
inputPolicy model.InputPolicy
|
||||||
|
|
||||||
|
// interruptible indicates whether the experiment is interruptible.
|
||||||
|
interruptible bool
|
||||||
|
}
|
||||||
|
|
||||||
|
// Interruptible returns whether the experiment is interruptible.
|
||||||
|
func (b *Factory) Interruptible() bool {
|
||||||
|
return b.interruptible
|
||||||
|
}
|
||||||
|
|
||||||
|
// InputPolicy returns the experiment's InputPolicy.
|
||||||
|
func (b *Factory) InputPolicy() model.InputPolicy {
|
||||||
|
return b.inputPolicy
|
||||||
|
}
|
||||||
|
|
||||||
|
var (
|
||||||
|
// ErrConfigIsNotAStructPointer indicates we expected a pointer to struct.
|
||||||
|
ErrConfigIsNotAStructPointer = errors.New("config is not a struct pointer")
|
||||||
|
|
||||||
|
// ErrNoSuchField indicates there's no field with the given name.
|
||||||
|
ErrNoSuchField = errors.New("no such field")
|
||||||
|
|
||||||
|
// ErrCannotSetIntegerOption means SetOptionAny couldn't set an integer option.
|
||||||
|
ErrCannotSetIntegerOption = errors.New("cannot set integer option")
|
||||||
|
|
||||||
|
// ErrInvalidStringRepresentationOfBool indicates the string you passed
|
||||||
|
// to SetOptionaAny is not a valid string representation of a bool.
|
||||||
|
ErrInvalidStringRepresentationOfBool = errors.New("invalid string representation of bool")
|
||||||
|
|
||||||
|
// ErrCannotSetBoolOption means SetOptionAny couldn't set a bool option.
|
||||||
|
ErrCannotSetBoolOption = errors.New("cannot set bool option")
|
||||||
|
|
||||||
|
// ErrCannotSetStringOption means SetOptionAny couldn't set a string option.
|
||||||
|
ErrCannotSetStringOption = errors.New("cannot set string option")
|
||||||
|
|
||||||
|
// ErrUnsupportedOptionType means we don't support the type passed to
|
||||||
|
// the SetOptionAny method as an opaque any type.
|
||||||
|
ErrUnsupportedOptionType = errors.New("unsupported option type")
|
||||||
|
)
|
||||||
|
|
||||||
|
// Options returns the options exposed by this experiment.
|
||||||
|
func (b *Factory) Options() (map[string]model.ExperimentOptionInfo, error) {
|
||||||
|
result := make(map[string]model.ExperimentOptionInfo)
|
||||||
|
ptrinfo := reflect.ValueOf(b.config)
|
||||||
|
if ptrinfo.Kind() != reflect.Ptr {
|
||||||
|
return nil, ErrConfigIsNotAStructPointer
|
||||||
|
}
|
||||||
|
structinfo := ptrinfo.Elem().Type()
|
||||||
|
if structinfo.Kind() != reflect.Struct {
|
||||||
|
return nil, ErrConfigIsNotAStructPointer
|
||||||
|
}
|
||||||
|
for i := 0; i < structinfo.NumField(); i++ {
|
||||||
|
field := structinfo.Field(i)
|
||||||
|
result[field.Name] = model.ExperimentOptionInfo{
|
||||||
|
Doc: field.Tag.Get("ooni"),
|
||||||
|
Type: field.Type.String(),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return result, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// setOptionBool sets a bool option.
|
||||||
|
func (b *Factory) setOptionBool(field reflect.Value, value any) error {
|
||||||
|
switch v := value.(type) {
|
||||||
|
case bool:
|
||||||
|
field.SetBool(v)
|
||||||
|
return nil
|
||||||
|
case string:
|
||||||
|
if v != "true" && v != "false" {
|
||||||
|
return fmt.Errorf("%w: %s", ErrInvalidStringRepresentationOfBool, v)
|
||||||
|
}
|
||||||
|
field.SetBool(v == "true")
|
||||||
|
return nil
|
||||||
|
default:
|
||||||
|
return fmt.Errorf("%w from a value of type %T", ErrCannotSetBoolOption, value)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// setOptionInt sets an int option
|
||||||
|
func (b *Factory) setOptionInt(field reflect.Value, value any) error {
|
||||||
|
switch v := value.(type) {
|
||||||
|
case int64:
|
||||||
|
field.SetInt(v)
|
||||||
|
return nil
|
||||||
|
case int32:
|
||||||
|
field.SetInt(int64(v))
|
||||||
|
return nil
|
||||||
|
case int16:
|
||||||
|
field.SetInt(int64(v))
|
||||||
|
return nil
|
||||||
|
case int8:
|
||||||
|
field.SetInt(int64(v))
|
||||||
|
return nil
|
||||||
|
case int:
|
||||||
|
field.SetInt(int64(v))
|
||||||
|
return nil
|
||||||
|
case string:
|
||||||
|
number, err := strconv.ParseInt(v, 10, 64)
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("%w: %s", ErrCannotSetIntegerOption, err.Error())
|
||||||
|
}
|
||||||
|
field.SetInt(number)
|
||||||
|
return nil
|
||||||
|
default:
|
||||||
|
return fmt.Errorf("%w from a value of type %T", ErrCannotSetIntegerOption, value)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// setOptionString sets a string option
|
||||||
|
func (b *Factory) setOptionString(field reflect.Value, value any) error {
|
||||||
|
switch v := value.(type) {
|
||||||
|
case string:
|
||||||
|
field.SetString(v)
|
||||||
|
return nil
|
||||||
|
default:
|
||||||
|
return fmt.Errorf("%w from a value of type %T", ErrCannotSetStringOption, value)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// SetOptionAny sets an option given any value.
|
||||||
|
func (b *Factory) SetOptionAny(key string, value any) error {
|
||||||
|
field, err := b.fieldbyname(b.config, key)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
switch field.Kind() {
|
||||||
|
case reflect.Int64:
|
||||||
|
return b.setOptionInt(field, value)
|
||||||
|
case reflect.Bool:
|
||||||
|
return b.setOptionBool(field, value)
|
||||||
|
case reflect.String:
|
||||||
|
return b.setOptionString(field, value)
|
||||||
|
default:
|
||||||
|
return fmt.Errorf("%w: %T", ErrUnsupportedOptionType, value)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// SetOptionsAny calls SetOptionAny for each entry inside [options].
|
||||||
|
func (b *Factory) SetOptionsAny(options map[string]any) error {
|
||||||
|
for key, value := range options {
|
||||||
|
if err := b.SetOptionAny(key, value); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// fieldbyname return v's field whose name is equal to the given key.
|
||||||
|
func (b *Factory) fieldbyname(v interface{}, key string) (reflect.Value, error) {
|
||||||
|
// See https://stackoverflow.com/a/6396678/4354461
|
||||||
|
ptrinfo := reflect.ValueOf(v)
|
||||||
|
if ptrinfo.Kind() != reflect.Ptr {
|
||||||
|
return reflect.Value{}, fmt.Errorf("%w but a %T", ErrConfigIsNotAStructPointer, v)
|
||||||
|
}
|
||||||
|
structinfo := ptrinfo.Elem()
|
||||||
|
if structinfo.Kind() != reflect.Struct {
|
||||||
|
return reflect.Value{}, fmt.Errorf("%w but a %T", ErrConfigIsNotAStructPointer, v)
|
||||||
|
}
|
||||||
|
field := structinfo.FieldByName(key)
|
||||||
|
if !field.IsValid() || !field.CanSet() {
|
||||||
|
return reflect.Value{}, fmt.Errorf("%w: %s", ErrNoSuchField, key)
|
||||||
|
}
|
||||||
|
return field, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// NewExperimentMeasurer creates the experiment
|
||||||
|
func (b *Factory) NewExperimentMeasurer() model.ExperimentMeasurer {
|
||||||
|
return b.build(b.config)
|
||||||
|
}
|
||||||
|
|
||||||
|
// CanonicalizeExperimentName allows code to provide experiment names
|
||||||
|
// in a more flexible way, where we have aliases.
|
||||||
|
//
|
||||||
|
// Because we allow for uppercase experiment names for backwards
|
||||||
|
// compatibility with MK, we need to add some exceptions here when
|
||||||
|
// mapping (e.g., DNSCheck => dnscheck).
|
||||||
|
func CanonicalizeExperimentName(name string) string {
|
||||||
|
switch name = strcase.ToSnake(name); name {
|
||||||
|
case "ndt_7":
|
||||||
|
name = "ndt" // since 2020-03-18, we use ndt7 to implement ndt by default
|
||||||
|
case "dns_check":
|
||||||
|
name = "dnscheck"
|
||||||
|
case "stun_reachability":
|
||||||
|
name = "stunreachability"
|
||||||
|
default:
|
||||||
|
}
|
||||||
|
return name
|
||||||
|
}
|
||||||
|
|
||||||
|
// NewFactory creates a new Factory instance.
|
||||||
|
func NewFactory(name string) (*Factory, error) {
|
||||||
|
factory := allexperiments[CanonicalizeExperimentName(name)]
|
||||||
|
if factory == nil {
|
||||||
|
return nil, fmt.Errorf("no such experiment: %s", name)
|
||||||
|
}
|
||||||
|
return factory, nil
|
||||||
|
}
|
347
internal/registry/factory_test.go
Normal file
347
internal/registry/factory_test.go
Normal file
|
@ -0,0 +1,347 @@
|
||||||
|
package registry
|
||||||
|
|
||||||
|
import (
|
||||||
|
"errors"
|
||||||
|
"testing"
|
||||||
|
|
||||||
|
"github.com/google/go-cmp/cmp"
|
||||||
|
)
|
||||||
|
|
||||||
|
type fakeExperimentConfig struct {
|
||||||
|
Chan chan any `ooni:"we cannot set this"`
|
||||||
|
String string `ooni:"a string"`
|
||||||
|
Truth bool `ooni:"something that no-one knows"`
|
||||||
|
Value int64 `ooni:"a number"`
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestExperimentBuilderOptions(t *testing.T) {
|
||||||
|
t.Run("when config is not a pointer", func(t *testing.T) {
|
||||||
|
b := &Factory{
|
||||||
|
config: 17,
|
||||||
|
}
|
||||||
|
options, err := b.Options()
|
||||||
|
if !errors.Is(err, ErrConfigIsNotAStructPointer) {
|
||||||
|
t.Fatal("expected an error here")
|
||||||
|
}
|
||||||
|
if options != nil {
|
||||||
|
t.Fatal("expected nil here")
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
t.Run("when config is not a struct", func(t *testing.T) {
|
||||||
|
number := 17
|
||||||
|
b := &Factory{
|
||||||
|
config: &number,
|
||||||
|
}
|
||||||
|
options, err := b.Options()
|
||||||
|
if !errors.Is(err, ErrConfigIsNotAStructPointer) {
|
||||||
|
t.Fatal("expected an error here")
|
||||||
|
}
|
||||||
|
if options != nil {
|
||||||
|
t.Fatal("expected nil here")
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
t.Run("when config is a pointer to struct", func(t *testing.T) {
|
||||||
|
config := &fakeExperimentConfig{}
|
||||||
|
b := &Factory{
|
||||||
|
config: config,
|
||||||
|
}
|
||||||
|
options, err := b.Options()
|
||||||
|
if err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
for name, value := range options {
|
||||||
|
switch name {
|
||||||
|
case "Chan":
|
||||||
|
if value.Doc != "we cannot set this" {
|
||||||
|
t.Fatal("invalid doc")
|
||||||
|
}
|
||||||
|
if value.Type != "chan interface {}" {
|
||||||
|
t.Fatal("invalid type", value.Type)
|
||||||
|
}
|
||||||
|
case "String":
|
||||||
|
if value.Doc != "a string" {
|
||||||
|
t.Fatal("invalid doc")
|
||||||
|
}
|
||||||
|
if value.Type != "string" {
|
||||||
|
t.Fatal("invalid type", value.Type)
|
||||||
|
}
|
||||||
|
case "Truth":
|
||||||
|
if value.Doc != "something that no-one knows" {
|
||||||
|
t.Fatal("invalid doc")
|
||||||
|
}
|
||||||
|
if value.Type != "bool" {
|
||||||
|
t.Fatal("invalid type", value.Type)
|
||||||
|
}
|
||||||
|
case "Value":
|
||||||
|
if value.Doc != "a number" {
|
||||||
|
t.Fatal("invalid doc")
|
||||||
|
}
|
||||||
|
if value.Type != "int64" {
|
||||||
|
t.Fatal("invalid type", value.Type)
|
||||||
|
}
|
||||||
|
default:
|
||||||
|
t.Fatal("unknown name", name)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestExperimentBuilderSetOptionAny(t *testing.T) {
|
||||||
|
var inputs = []struct {
|
||||||
|
TestCaseName string
|
||||||
|
InitialConfig any
|
||||||
|
FieldName string
|
||||||
|
FieldValue any
|
||||||
|
ExpectErr error
|
||||||
|
ExpectConfig any
|
||||||
|
}{{
|
||||||
|
TestCaseName: "config is not a pointer",
|
||||||
|
InitialConfig: fakeExperimentConfig{},
|
||||||
|
FieldName: "Antani",
|
||||||
|
FieldValue: true,
|
||||||
|
ExpectErr: ErrConfigIsNotAStructPointer,
|
||||||
|
ExpectConfig: fakeExperimentConfig{},
|
||||||
|
}, {
|
||||||
|
TestCaseName: "config is not a pointer to struct",
|
||||||
|
InitialConfig: func() *int {
|
||||||
|
v := 17
|
||||||
|
return &v
|
||||||
|
}(),
|
||||||
|
FieldName: "Antani",
|
||||||
|
FieldValue: true,
|
||||||
|
ExpectErr: ErrConfigIsNotAStructPointer,
|
||||||
|
ExpectConfig: func() *int {
|
||||||
|
v := 17
|
||||||
|
return &v
|
||||||
|
}(),
|
||||||
|
}, {
|
||||||
|
TestCaseName: "for missing field",
|
||||||
|
InitialConfig: &fakeExperimentConfig{},
|
||||||
|
FieldName: "Antani",
|
||||||
|
FieldValue: true,
|
||||||
|
ExpectErr: ErrNoSuchField,
|
||||||
|
ExpectConfig: &fakeExperimentConfig{},
|
||||||
|
}, {
|
||||||
|
TestCaseName: "[bool] for true value represented as string",
|
||||||
|
InitialConfig: &fakeExperimentConfig{},
|
||||||
|
FieldName: "Truth",
|
||||||
|
FieldValue: "true",
|
||||||
|
ExpectErr: nil,
|
||||||
|
ExpectConfig: &fakeExperimentConfig{
|
||||||
|
Truth: true,
|
||||||
|
},
|
||||||
|
}, {
|
||||||
|
TestCaseName: "[bool] for false value represented as string",
|
||||||
|
InitialConfig: &fakeExperimentConfig{
|
||||||
|
Truth: true,
|
||||||
|
},
|
||||||
|
FieldName: "Truth",
|
||||||
|
FieldValue: "false",
|
||||||
|
ExpectErr: nil,
|
||||||
|
ExpectConfig: &fakeExperimentConfig{
|
||||||
|
Truth: false, // must have been flipped
|
||||||
|
},
|
||||||
|
}, {
|
||||||
|
TestCaseName: "[bool] for true value",
|
||||||
|
InitialConfig: &fakeExperimentConfig{},
|
||||||
|
FieldName: "Truth",
|
||||||
|
FieldValue: true,
|
||||||
|
ExpectErr: nil,
|
||||||
|
ExpectConfig: &fakeExperimentConfig{
|
||||||
|
Truth: true,
|
||||||
|
},
|
||||||
|
}, {
|
||||||
|
TestCaseName: "[bool] for false value",
|
||||||
|
InitialConfig: &fakeExperimentConfig{
|
||||||
|
Truth: true,
|
||||||
|
},
|
||||||
|
FieldName: "Truth",
|
||||||
|
FieldValue: false,
|
||||||
|
ExpectErr: nil,
|
||||||
|
ExpectConfig: &fakeExperimentConfig{
|
||||||
|
Truth: false, // must have been flipped
|
||||||
|
},
|
||||||
|
}, {
|
||||||
|
TestCaseName: "[bool] for invalid string representation of bool",
|
||||||
|
InitialConfig: &fakeExperimentConfig{},
|
||||||
|
FieldName: "Truth",
|
||||||
|
FieldValue: "xxx",
|
||||||
|
ExpectErr: ErrInvalidStringRepresentationOfBool,
|
||||||
|
ExpectConfig: &fakeExperimentConfig{},
|
||||||
|
}, {
|
||||||
|
TestCaseName: "[bool] for value we don't know how to convert to bool",
|
||||||
|
InitialConfig: &fakeExperimentConfig{},
|
||||||
|
FieldName: "Truth",
|
||||||
|
FieldValue: make(chan any),
|
||||||
|
ExpectErr: ErrCannotSetBoolOption,
|
||||||
|
ExpectConfig: &fakeExperimentConfig{},
|
||||||
|
}, {
|
||||||
|
TestCaseName: "[int] for int",
|
||||||
|
InitialConfig: &fakeExperimentConfig{},
|
||||||
|
FieldName: "Value",
|
||||||
|
FieldValue: 17,
|
||||||
|
ExpectErr: nil,
|
||||||
|
ExpectConfig: &fakeExperimentConfig{
|
||||||
|
Value: 17,
|
||||||
|
},
|
||||||
|
}, {
|
||||||
|
TestCaseName: "[int] for int64",
|
||||||
|
InitialConfig: &fakeExperimentConfig{},
|
||||||
|
FieldName: "Value",
|
||||||
|
FieldValue: int64(17),
|
||||||
|
ExpectErr: nil,
|
||||||
|
ExpectConfig: &fakeExperimentConfig{
|
||||||
|
Value: 17,
|
||||||
|
},
|
||||||
|
}, {
|
||||||
|
TestCaseName: "[int] for int32",
|
||||||
|
InitialConfig: &fakeExperimentConfig{},
|
||||||
|
FieldName: "Value",
|
||||||
|
FieldValue: int32(17),
|
||||||
|
ExpectErr: nil,
|
||||||
|
ExpectConfig: &fakeExperimentConfig{
|
||||||
|
Value: 17,
|
||||||
|
},
|
||||||
|
}, {
|
||||||
|
TestCaseName: "[int] for int16",
|
||||||
|
InitialConfig: &fakeExperimentConfig{},
|
||||||
|
FieldName: "Value",
|
||||||
|
FieldValue: int16(17),
|
||||||
|
ExpectErr: nil,
|
||||||
|
ExpectConfig: &fakeExperimentConfig{
|
||||||
|
Value: 17,
|
||||||
|
},
|
||||||
|
}, {
|
||||||
|
TestCaseName: "[int] for int8",
|
||||||
|
InitialConfig: &fakeExperimentConfig{},
|
||||||
|
FieldName: "Value",
|
||||||
|
FieldValue: int8(17),
|
||||||
|
ExpectErr: nil,
|
||||||
|
ExpectConfig: &fakeExperimentConfig{
|
||||||
|
Value: 17,
|
||||||
|
},
|
||||||
|
}, {
|
||||||
|
TestCaseName: "[int] for string representation of int",
|
||||||
|
InitialConfig: &fakeExperimentConfig{},
|
||||||
|
FieldName: "Value",
|
||||||
|
FieldValue: "17",
|
||||||
|
ExpectErr: nil,
|
||||||
|
ExpectConfig: &fakeExperimentConfig{
|
||||||
|
Value: 17,
|
||||||
|
},
|
||||||
|
}, {
|
||||||
|
TestCaseName: "[int] for invalid string representation of int",
|
||||||
|
InitialConfig: &fakeExperimentConfig{},
|
||||||
|
FieldName: "Value",
|
||||||
|
FieldValue: "xx",
|
||||||
|
ExpectErr: ErrCannotSetIntegerOption,
|
||||||
|
ExpectConfig: &fakeExperimentConfig{},
|
||||||
|
}, {
|
||||||
|
TestCaseName: "[int] for type we don't know how to convert to int",
|
||||||
|
InitialConfig: &fakeExperimentConfig{},
|
||||||
|
FieldName: "Value",
|
||||||
|
FieldValue: make(chan any),
|
||||||
|
ExpectErr: ErrCannotSetIntegerOption,
|
||||||
|
ExpectConfig: &fakeExperimentConfig{},
|
||||||
|
}, {
|
||||||
|
TestCaseName: "[string] for serialized bool value while setting a string value",
|
||||||
|
InitialConfig: &fakeExperimentConfig{},
|
||||||
|
FieldName: "String",
|
||||||
|
FieldValue: "true",
|
||||||
|
ExpectErr: nil,
|
||||||
|
ExpectConfig: &fakeExperimentConfig{
|
||||||
|
String: "true",
|
||||||
|
},
|
||||||
|
}, {
|
||||||
|
TestCaseName: "[string] for serialized int value while setting a string value",
|
||||||
|
InitialConfig: &fakeExperimentConfig{},
|
||||||
|
FieldName: "String",
|
||||||
|
FieldValue: "155",
|
||||||
|
ExpectErr: nil,
|
||||||
|
ExpectConfig: &fakeExperimentConfig{
|
||||||
|
String: "155",
|
||||||
|
},
|
||||||
|
}, {
|
||||||
|
TestCaseName: "[string] for any other string",
|
||||||
|
InitialConfig: &fakeExperimentConfig{},
|
||||||
|
FieldName: "String",
|
||||||
|
FieldValue: "xxx",
|
||||||
|
ExpectErr: nil,
|
||||||
|
ExpectConfig: &fakeExperimentConfig{
|
||||||
|
String: "xxx",
|
||||||
|
},
|
||||||
|
}, {
|
||||||
|
TestCaseName: "[string] for type we don't know how to convert to string",
|
||||||
|
InitialConfig: &fakeExperimentConfig{},
|
||||||
|
FieldName: "String",
|
||||||
|
FieldValue: make(chan any),
|
||||||
|
ExpectErr: ErrCannotSetStringOption,
|
||||||
|
ExpectConfig: &fakeExperimentConfig{},
|
||||||
|
}, {
|
||||||
|
TestCaseName: "for a field that we don't know how to set",
|
||||||
|
InitialConfig: &fakeExperimentConfig{},
|
||||||
|
FieldName: "Chan",
|
||||||
|
FieldValue: make(chan any),
|
||||||
|
ExpectErr: ErrUnsupportedOptionType,
|
||||||
|
ExpectConfig: &fakeExperimentConfig{},
|
||||||
|
}}
|
||||||
|
|
||||||
|
for _, input := range inputs {
|
||||||
|
t.Run(input.TestCaseName, func(t *testing.T) {
|
||||||
|
ec := input.InitialConfig
|
||||||
|
b := &Factory{config: ec}
|
||||||
|
err := b.SetOptionAny(input.FieldName, input.FieldValue)
|
||||||
|
if !errors.Is(err, input.ExpectErr) {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
if diff := cmp.Diff(input.ExpectConfig, ec); diff != "" {
|
||||||
|
t.Fatal(diff)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestExperimentBuilderSetOptionsAny(t *testing.T) {
|
||||||
|
b := &Factory{config: &fakeExperimentConfig{}}
|
||||||
|
|
||||||
|
t.Run("we correctly handle an empty map", func(t *testing.T) {
|
||||||
|
if err := b.SetOptionsAny(nil); err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
t.Run("we correctly handle a map containing options", func(t *testing.T) {
|
||||||
|
f := &fakeExperimentConfig{}
|
||||||
|
privateb := &Factory{config: f}
|
||||||
|
opts := map[string]any{
|
||||||
|
"String": "yoloyolo",
|
||||||
|
"Value": "174",
|
||||||
|
"Truth": "true",
|
||||||
|
}
|
||||||
|
if err := privateb.SetOptionsAny(opts); err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
if f.String != "yoloyolo" {
|
||||||
|
t.Fatal("cannot set string value")
|
||||||
|
}
|
||||||
|
if f.Value != 174 {
|
||||||
|
t.Fatal("cannot set integer value")
|
||||||
|
}
|
||||||
|
if f.Truth != true {
|
||||||
|
t.Fatal("cannot set bool value")
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
t.Run("we handle mistakes in a map containing string options", func(t *testing.T) {
|
||||||
|
opts := map[string]any{
|
||||||
|
"String": "yoloyolo",
|
||||||
|
"Value": "xx",
|
||||||
|
"Truth": "true",
|
||||||
|
}
|
||||||
|
if err := b.SetOptionsAny(opts); !errors.Is(err, ErrCannotSetIntegerOption) {
|
||||||
|
t.Fatal("unexpected err", err)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
22
internal/registry/fbmessenger.go
Normal file
22
internal/registry/fbmessenger.go
Normal file
|
@ -0,0 +1,22 @@
|
||||||
|
package registry
|
||||||
|
|
||||||
|
//
|
||||||
|
// Registers the `fbmessenger' experiment.
|
||||||
|
//
|
||||||
|
|
||||||
|
import (
|
||||||
|
"github.com/ooni/probe-cli/v3/internal/engine/experiment/fbmessenger"
|
||||||
|
"github.com/ooni/probe-cli/v3/internal/model"
|
||||||
|
)
|
||||||
|
|
||||||
|
func init() {
|
||||||
|
allexperiments["facebook_messenger"] = &Factory{
|
||||||
|
build: func(config interface{}) model.ExperimentMeasurer {
|
||||||
|
return fbmessenger.NewExperimentMeasurer(
|
||||||
|
*config.(*fbmessenger.Config),
|
||||||
|
)
|
||||||
|
},
|
||||||
|
config: &fbmessenger.Config{},
|
||||||
|
inputPolicy: model.InputNone,
|
||||||
|
}
|
||||||
|
}
|
22
internal/registry/hhfm.go
Normal file
22
internal/registry/hhfm.go
Normal file
|
@ -0,0 +1,22 @@
|
||||||
|
package registry
|
||||||
|
|
||||||
|
//
|
||||||
|
// Registers the `hhfm' experiment.
|
||||||
|
//
|
||||||
|
|
||||||
|
import (
|
||||||
|
"github.com/ooni/probe-cli/v3/internal/engine/experiment/hhfm"
|
||||||
|
"github.com/ooni/probe-cli/v3/internal/model"
|
||||||
|
)
|
||||||
|
|
||||||
|
func init() {
|
||||||
|
allexperiments["http_header_field_manipulation"] = &Factory{
|
||||||
|
build: func(config interface{}) model.ExperimentMeasurer {
|
||||||
|
return hhfm.NewExperimentMeasurer(
|
||||||
|
*config.(*hhfm.Config),
|
||||||
|
)
|
||||||
|
},
|
||||||
|
config: &hhfm.Config{},
|
||||||
|
inputPolicy: model.InputNone,
|
||||||
|
}
|
||||||
|
}
|
22
internal/registry/hirl.go
Normal file
22
internal/registry/hirl.go
Normal file
|
@ -0,0 +1,22 @@
|
||||||
|
package registry
|
||||||
|
|
||||||
|
//
|
||||||
|
// Registers the `hirl' experiment.
|
||||||
|
//
|
||||||
|
|
||||||
|
import (
|
||||||
|
"github.com/ooni/probe-cli/v3/internal/engine/experiment/hirl"
|
||||||
|
"github.com/ooni/probe-cli/v3/internal/model"
|
||||||
|
)
|
||||||
|
|
||||||
|
func init() {
|
||||||
|
allexperiments["http_invalid_request_line"] = &Factory{
|
||||||
|
build: func(config interface{}) model.ExperimentMeasurer {
|
||||||
|
return hirl.NewExperimentMeasurer(
|
||||||
|
*config.(*hirl.Config),
|
||||||
|
)
|
||||||
|
},
|
||||||
|
config: &hirl.Config{},
|
||||||
|
inputPolicy: model.InputNone,
|
||||||
|
}
|
||||||
|
}
|
22
internal/registry/httphostheader.go
Normal file
22
internal/registry/httphostheader.go
Normal file
|
@ -0,0 +1,22 @@
|
||||||
|
package registry
|
||||||
|
|
||||||
|
//
|
||||||
|
// Registers the `httphostheader' experiment.
|
||||||
|
//
|
||||||
|
|
||||||
|
import (
|
||||||
|
"github.com/ooni/probe-cli/v3/internal/engine/experiment/httphostheader"
|
||||||
|
"github.com/ooni/probe-cli/v3/internal/model"
|
||||||
|
)
|
||||||
|
|
||||||
|
func init() {
|
||||||
|
allexperiments["http_host_header"] = &Factory{
|
||||||
|
build: func(config interface{}) model.ExperimentMeasurer {
|
||||||
|
return httphostheader.NewExperimentMeasurer(
|
||||||
|
*config.(*httphostheader.Config),
|
||||||
|
)
|
||||||
|
},
|
||||||
|
config: &httphostheader.Config{},
|
||||||
|
inputPolicy: model.InputOrQueryBackend,
|
||||||
|
}
|
||||||
|
}
|
23
internal/registry/ndt.go
Normal file
23
internal/registry/ndt.go
Normal file
|
@ -0,0 +1,23 @@
|
||||||
|
package registry
|
||||||
|
|
||||||
|
//
|
||||||
|
// Registers the `ndt' experiment.
|
||||||
|
//
|
||||||
|
|
||||||
|
import (
|
||||||
|
"github.com/ooni/probe-cli/v3/internal/engine/experiment/ndt7"
|
||||||
|
"github.com/ooni/probe-cli/v3/internal/model"
|
||||||
|
)
|
||||||
|
|
||||||
|
func init() {
|
||||||
|
allexperiments["ndt"] = &Factory{
|
||||||
|
build: func(config interface{}) model.ExperimentMeasurer {
|
||||||
|
return ndt7.NewExperimentMeasurer(
|
||||||
|
*config.(*ndt7.Config),
|
||||||
|
)
|
||||||
|
},
|
||||||
|
config: &ndt7.Config{},
|
||||||
|
interruptible: true,
|
||||||
|
inputPolicy: model.InputNone,
|
||||||
|
}
|
||||||
|
}
|
22
internal/registry/psiphon.go
Normal file
22
internal/registry/psiphon.go
Normal file
|
@ -0,0 +1,22 @@
|
||||||
|
package registry
|
||||||
|
|
||||||
|
//
|
||||||
|
// Registers the `psiphon' experiment.
|
||||||
|
//
|
||||||
|
|
||||||
|
import (
|
||||||
|
"github.com/ooni/probe-cli/v3/internal/engine/experiment/psiphon"
|
||||||
|
"github.com/ooni/probe-cli/v3/internal/model"
|
||||||
|
)
|
||||||
|
|
||||||
|
func init() {
|
||||||
|
allexperiments["psiphon"] = &Factory{
|
||||||
|
build: func(config interface{}) model.ExperimentMeasurer {
|
||||||
|
return psiphon.NewExperimentMeasurer(
|
||||||
|
*config.(*psiphon.Config),
|
||||||
|
)
|
||||||
|
},
|
||||||
|
config: &psiphon.Config{},
|
||||||
|
inputPolicy: model.InputOptional,
|
||||||
|
}
|
||||||
|
}
|
22
internal/registry/quicping.go
Normal file
22
internal/registry/quicping.go
Normal file
|
@ -0,0 +1,22 @@
|
||||||
|
package registry
|
||||||
|
|
||||||
|
//
|
||||||
|
// Registers the `quicping' experiment.
|
||||||
|
//
|
||||||
|
|
||||||
|
import (
|
||||||
|
"github.com/ooni/probe-cli/v3/internal/engine/experiment/quicping"
|
||||||
|
"github.com/ooni/probe-cli/v3/internal/model"
|
||||||
|
)
|
||||||
|
|
||||||
|
func init() {
|
||||||
|
allexperiments["quicping"] = &Factory{
|
||||||
|
build: func(config interface{}) model.ExperimentMeasurer {
|
||||||
|
return quicping.NewExperimentMeasurer(
|
||||||
|
*config.(*quicping.Config),
|
||||||
|
)
|
||||||
|
},
|
||||||
|
config: &quicping.Config{},
|
||||||
|
inputPolicy: model.InputStrictlyRequired,
|
||||||
|
}
|
||||||
|
}
|
22
internal/registry/riseupvpn.go
Normal file
22
internal/registry/riseupvpn.go
Normal file
|
@ -0,0 +1,22 @@
|
||||||
|
package registry
|
||||||
|
|
||||||
|
//
|
||||||
|
// Registers the `riseupvpn' experiment.
|
||||||
|
//
|
||||||
|
|
||||||
|
import (
|
||||||
|
"github.com/ooni/probe-cli/v3/internal/engine/experiment/riseupvpn"
|
||||||
|
"github.com/ooni/probe-cli/v3/internal/model"
|
||||||
|
)
|
||||||
|
|
||||||
|
func init() {
|
||||||
|
allexperiments["riseupvpn"] = &Factory{
|
||||||
|
build: func(config interface{}) model.ExperimentMeasurer {
|
||||||
|
return riseupvpn.NewExperimentMeasurer(
|
||||||
|
*config.(*riseupvpn.Config),
|
||||||
|
)
|
||||||
|
},
|
||||||
|
config: &riseupvpn.Config{},
|
||||||
|
inputPolicy: model.InputNone,
|
||||||
|
}
|
||||||
|
}
|
22
internal/registry/run.go
Normal file
22
internal/registry/run.go
Normal file
|
@ -0,0 +1,22 @@
|
||||||
|
package registry
|
||||||
|
|
||||||
|
//
|
||||||
|
// Registers the `run' experiment.
|
||||||
|
//
|
||||||
|
|
||||||
|
import (
|
||||||
|
"github.com/ooni/probe-cli/v3/internal/engine/experiment/run"
|
||||||
|
"github.com/ooni/probe-cli/v3/internal/model"
|
||||||
|
)
|
||||||
|
|
||||||
|
func init() {
|
||||||
|
allexperiments["run"] = &Factory{
|
||||||
|
build: func(config interface{}) model.ExperimentMeasurer {
|
||||||
|
return run.NewExperimentMeasurer(
|
||||||
|
*config.(*run.Config),
|
||||||
|
)
|
||||||
|
},
|
||||||
|
config: &run.Config{},
|
||||||
|
inputPolicy: model.InputStrictlyRequired,
|
||||||
|
}
|
||||||
|
}
|
22
internal/registry/signal.go
Normal file
22
internal/registry/signal.go
Normal file
|
@ -0,0 +1,22 @@
|
||||||
|
package registry
|
||||||
|
|
||||||
|
//
|
||||||
|
// Registers the `signal' experiment.
|
||||||
|
//
|
||||||
|
|
||||||
|
import (
|
||||||
|
"github.com/ooni/probe-cli/v3/internal/engine/experiment/signal"
|
||||||
|
"github.com/ooni/probe-cli/v3/internal/model"
|
||||||
|
)
|
||||||
|
|
||||||
|
func init() {
|
||||||
|
allexperiments["signal"] = &Factory{
|
||||||
|
build: func(config interface{}) model.ExperimentMeasurer {
|
||||||
|
return signal.NewExperimentMeasurer(
|
||||||
|
*config.(*signal.Config),
|
||||||
|
)
|
||||||
|
},
|
||||||
|
config: &signal.Config{},
|
||||||
|
inputPolicy: model.InputNone,
|
||||||
|
}
|
||||||
|
}
|
22
internal/registry/simplequicping.go
Normal file
22
internal/registry/simplequicping.go
Normal file
|
@ -0,0 +1,22 @@
|
||||||
|
package registry
|
||||||
|
|
||||||
|
//
|
||||||
|
// Registers the `simplequicping' experiment.
|
||||||
|
//
|
||||||
|
|
||||||
|
import (
|
||||||
|
"github.com/ooni/probe-cli/v3/internal/engine/experiment/simplequicping"
|
||||||
|
"github.com/ooni/probe-cli/v3/internal/model"
|
||||||
|
)
|
||||||
|
|
||||||
|
func init() {
|
||||||
|
allexperiments["simplequicping"] = &Factory{
|
||||||
|
build: func(config interface{}) model.ExperimentMeasurer {
|
||||||
|
return simplequicping.NewExperimentMeasurer(
|
||||||
|
*config.(*simplequicping.Config),
|
||||||
|
)
|
||||||
|
},
|
||||||
|
config: &simplequicping.Config{},
|
||||||
|
inputPolicy: model.InputStrictlyRequired,
|
||||||
|
}
|
||||||
|
}
|
22
internal/registry/sniblocking.go
Normal file
22
internal/registry/sniblocking.go
Normal file
|
@ -0,0 +1,22 @@
|
||||||
|
package registry
|
||||||
|
|
||||||
|
//
|
||||||
|
// Registers the `sniblocking' experiment.
|
||||||
|
//
|
||||||
|
|
||||||
|
import (
|
||||||
|
"github.com/ooni/probe-cli/v3/internal/engine/experiment/sniblocking"
|
||||||
|
"github.com/ooni/probe-cli/v3/internal/model"
|
||||||
|
)
|
||||||
|
|
||||||
|
func init() {
|
||||||
|
allexperiments["sni_blocking"] = &Factory{
|
||||||
|
build: func(config interface{}) model.ExperimentMeasurer {
|
||||||
|
return sniblocking.NewExperimentMeasurer(
|
||||||
|
*config.(*sniblocking.Config),
|
||||||
|
)
|
||||||
|
},
|
||||||
|
config: &sniblocking.Config{},
|
||||||
|
inputPolicy: model.InputOrQueryBackend,
|
||||||
|
}
|
||||||
|
}
|
22
internal/registry/stunreachability.go
Normal file
22
internal/registry/stunreachability.go
Normal file
|
@ -0,0 +1,22 @@
|
||||||
|
package registry
|
||||||
|
|
||||||
|
//
|
||||||
|
// Registers the `stunreachability' experiment.
|
||||||
|
//
|
||||||
|
|
||||||
|
import (
|
||||||
|
"github.com/ooni/probe-cli/v3/internal/engine/experiment/stunreachability"
|
||||||
|
"github.com/ooni/probe-cli/v3/internal/model"
|
||||||
|
)
|
||||||
|
|
||||||
|
func init() {
|
||||||
|
allexperiments["stunreachability"] = &Factory{
|
||||||
|
build: func(config interface{}) model.ExperimentMeasurer {
|
||||||
|
return stunreachability.NewExperimentMeasurer(
|
||||||
|
*config.(*stunreachability.Config),
|
||||||
|
)
|
||||||
|
},
|
||||||
|
config: &stunreachability.Config{},
|
||||||
|
inputPolicy: model.InputOrStaticDefault,
|
||||||
|
}
|
||||||
|
}
|
22
internal/registry/tcpping.go
Normal file
22
internal/registry/tcpping.go
Normal file
|
@ -0,0 +1,22 @@
|
||||||
|
package registry
|
||||||
|
|
||||||
|
//
|
||||||
|
// Registers the `tcpping' experiment.
|
||||||
|
//
|
||||||
|
|
||||||
|
import (
|
||||||
|
"github.com/ooni/probe-cli/v3/internal/engine/experiment/tcpping"
|
||||||
|
"github.com/ooni/probe-cli/v3/internal/model"
|
||||||
|
)
|
||||||
|
|
||||||
|
func init() {
|
||||||
|
allexperiments["tcpping"] = &Factory{
|
||||||
|
build: func(config interface{}) model.ExperimentMeasurer {
|
||||||
|
return tcpping.NewExperimentMeasurer(
|
||||||
|
*config.(*tcpping.Config),
|
||||||
|
)
|
||||||
|
},
|
||||||
|
config: &tcpping.Config{},
|
||||||
|
inputPolicy: model.InputStrictlyRequired,
|
||||||
|
}
|
||||||
|
}
|
23
internal/registry/telegram.go
Normal file
23
internal/registry/telegram.go
Normal file
|
@ -0,0 +1,23 @@
|
||||||
|
package registry
|
||||||
|
|
||||||
|
//
|
||||||
|
// Registers the `telegram' experiment.
|
||||||
|
//
|
||||||
|
|
||||||
|
import (
|
||||||
|
"github.com/ooni/probe-cli/v3/internal/engine/experiment/telegram"
|
||||||
|
"github.com/ooni/probe-cli/v3/internal/model"
|
||||||
|
)
|
||||||
|
|
||||||
|
func init() {
|
||||||
|
allexperiments["telegram"] = &Factory{
|
||||||
|
build: func(config any) model.ExperimentMeasurer {
|
||||||
|
return telegram.NewExperimentMeasurer(
|
||||||
|
config.(telegram.Config),
|
||||||
|
)
|
||||||
|
},
|
||||||
|
config: &telegram.Config{},
|
||||||
|
interruptible: false,
|
||||||
|
inputPolicy: model.InputNone,
|
||||||
|
}
|
||||||
|
}
|
22
internal/registry/tlsping.go
Normal file
22
internal/registry/tlsping.go
Normal file
|
@ -0,0 +1,22 @@
|
||||||
|
package registry
|
||||||
|
|
||||||
|
//
|
||||||
|
// Registers the `tlsping' experiment.
|
||||||
|
//
|
||||||
|
|
||||||
|
import (
|
||||||
|
"github.com/ooni/probe-cli/v3/internal/engine/experiment/tlsping"
|
||||||
|
"github.com/ooni/probe-cli/v3/internal/model"
|
||||||
|
)
|
||||||
|
|
||||||
|
func init() {
|
||||||
|
allexperiments["tlsping"] = &Factory{
|
||||||
|
build: func(config interface{}) model.ExperimentMeasurer {
|
||||||
|
return tlsping.NewExperimentMeasurer(
|
||||||
|
*config.(*tlsping.Config),
|
||||||
|
)
|
||||||
|
},
|
||||||
|
config: &tlsping.Config{},
|
||||||
|
inputPolicy: model.InputStrictlyRequired,
|
||||||
|
}
|
||||||
|
}
|
22
internal/registry/tlstool.go
Normal file
22
internal/registry/tlstool.go
Normal file
|
@ -0,0 +1,22 @@
|
||||||
|
package registry
|
||||||
|
|
||||||
|
//
|
||||||
|
// Registers the `tlstool' experiment.
|
||||||
|
//
|
||||||
|
|
||||||
|
import (
|
||||||
|
"github.com/ooni/probe-cli/v3/internal/engine/experiment/tlstool"
|
||||||
|
"github.com/ooni/probe-cli/v3/internal/model"
|
||||||
|
)
|
||||||
|
|
||||||
|
func init() {
|
||||||
|
allexperiments["tlstool"] = &Factory{
|
||||||
|
build: func(config interface{}) model.ExperimentMeasurer {
|
||||||
|
return tlstool.NewExperimentMeasurer(
|
||||||
|
*config.(*tlstool.Config),
|
||||||
|
)
|
||||||
|
},
|
||||||
|
config: &tlstool.Config{},
|
||||||
|
inputPolicy: model.InputOrQueryBackend,
|
||||||
|
}
|
||||||
|
}
|
22
internal/registry/tor.go
Normal file
22
internal/registry/tor.go
Normal file
|
@ -0,0 +1,22 @@
|
||||||
|
package registry
|
||||||
|
|
||||||
|
//
|
||||||
|
// Registers the `tor' experiment.
|
||||||
|
//
|
||||||
|
|
||||||
|
import (
|
||||||
|
"github.com/ooni/probe-cli/v3/internal/engine/experiment/tor"
|
||||||
|
"github.com/ooni/probe-cli/v3/internal/model"
|
||||||
|
)
|
||||||
|
|
||||||
|
func init() {
|
||||||
|
allexperiments["tor"] = &Factory{
|
||||||
|
build: func(config interface{}) model.ExperimentMeasurer {
|
||||||
|
return tor.NewExperimentMeasurer(
|
||||||
|
*config.(*tor.Config),
|
||||||
|
)
|
||||||
|
},
|
||||||
|
config: &tor.Config{},
|
||||||
|
inputPolicy: model.InputNone,
|
||||||
|
}
|
||||||
|
}
|
22
internal/registry/torsf.go
Normal file
22
internal/registry/torsf.go
Normal file
|
@ -0,0 +1,22 @@
|
||||||
|
package registry
|
||||||
|
|
||||||
|
//
|
||||||
|
// Registers the `torsf' experiment.
|
||||||
|
//
|
||||||
|
|
||||||
|
import (
|
||||||
|
"github.com/ooni/probe-cli/v3/internal/engine/experiment/torsf"
|
||||||
|
"github.com/ooni/probe-cli/v3/internal/model"
|
||||||
|
)
|
||||||
|
|
||||||
|
func init() {
|
||||||
|
allexperiments["torsf"] = &Factory{
|
||||||
|
build: func(config interface{}) model.ExperimentMeasurer {
|
||||||
|
return torsf.NewExperimentMeasurer(
|
||||||
|
*config.(*torsf.Config),
|
||||||
|
)
|
||||||
|
},
|
||||||
|
config: &torsf.Config{},
|
||||||
|
inputPolicy: model.InputNone,
|
||||||
|
}
|
||||||
|
}
|
22
internal/registry/urlgetter.go
Normal file
22
internal/registry/urlgetter.go
Normal file
|
@ -0,0 +1,22 @@
|
||||||
|
package registry
|
||||||
|
|
||||||
|
//
|
||||||
|
// Registers the `urlgetter' experiment.
|
||||||
|
//
|
||||||
|
|
||||||
|
import (
|
||||||
|
"github.com/ooni/probe-cli/v3/internal/engine/experiment/urlgetter"
|
||||||
|
"github.com/ooni/probe-cli/v3/internal/model"
|
||||||
|
)
|
||||||
|
|
||||||
|
func init() {
|
||||||
|
allexperiments["urlgetter"] = &Factory{
|
||||||
|
build: func(config interface{}) model.ExperimentMeasurer {
|
||||||
|
return urlgetter.NewExperimentMeasurer(
|
||||||
|
*config.(*urlgetter.Config),
|
||||||
|
)
|
||||||
|
},
|
||||||
|
config: &urlgetter.Config{},
|
||||||
|
inputPolicy: model.InputStrictlyRequired,
|
||||||
|
}
|
||||||
|
}
|
22
internal/registry/vanillator.go
Normal file
22
internal/registry/vanillator.go
Normal file
|
@ -0,0 +1,22 @@
|
||||||
|
package registry
|
||||||
|
|
||||||
|
//
|
||||||
|
// Registers the `vanilla_tor' experiment.
|
||||||
|
//
|
||||||
|
|
||||||
|
import (
|
||||||
|
"github.com/ooni/probe-cli/v3/internal/engine/experiment/vanillator"
|
||||||
|
"github.com/ooni/probe-cli/v3/internal/model"
|
||||||
|
)
|
||||||
|
|
||||||
|
func init() {
|
||||||
|
allexperiments["vanilla_tor"] = &Factory{
|
||||||
|
build: func(config interface{}) model.ExperimentMeasurer {
|
||||||
|
return vanillator.NewExperimentMeasurer(
|
||||||
|
*config.(*vanillator.Config),
|
||||||
|
)
|
||||||
|
},
|
||||||
|
config: &vanillator.Config{},
|
||||||
|
inputPolicy: model.InputNone,
|
||||||
|
}
|
||||||
|
}
|
23
internal/registry/webconnectivity.go
Normal file
23
internal/registry/webconnectivity.go
Normal file
|
@ -0,0 +1,23 @@
|
||||||
|
package registry
|
||||||
|
|
||||||
|
//
|
||||||
|
// Registers the `web_connectivity' experiment.
|
||||||
|
//
|
||||||
|
|
||||||
|
import (
|
||||||
|
"github.com/ooni/probe-cli/v3/internal/engine/experiment/webconnectivity"
|
||||||
|
"github.com/ooni/probe-cli/v3/internal/model"
|
||||||
|
)
|
||||||
|
|
||||||
|
func init() {
|
||||||
|
allexperiments["web_connectivity"] = &Factory{
|
||||||
|
build: func(config any) model.ExperimentMeasurer {
|
||||||
|
return webconnectivity.NewExperimentMeasurer(
|
||||||
|
config.(webconnectivity.Config),
|
||||||
|
)
|
||||||
|
},
|
||||||
|
config: &webconnectivity.Config{},
|
||||||
|
interruptible: false,
|
||||||
|
inputPolicy: model.InputOrQueryBackend,
|
||||||
|
}
|
||||||
|
}
|
22
internal/registry/whatsapp.go
Normal file
22
internal/registry/whatsapp.go
Normal file
|
@ -0,0 +1,22 @@
|
||||||
|
package registry
|
||||||
|
|
||||||
|
//
|
||||||
|
// Registers the `whatsapp' experiment.
|
||||||
|
//
|
||||||
|
|
||||||
|
import (
|
||||||
|
"github.com/ooni/probe-cli/v3/internal/engine/experiment/whatsapp"
|
||||||
|
"github.com/ooni/probe-cli/v3/internal/model"
|
||||||
|
)
|
||||||
|
|
||||||
|
func init() {
|
||||||
|
allexperiments["whatsapp"] = &Factory{
|
||||||
|
build: func(config interface{}) model.ExperimentMeasurer {
|
||||||
|
return whatsapp.NewExperimentMeasurer(
|
||||||
|
*config.(*whatsapp.Config),
|
||||||
|
)
|
||||||
|
},
|
||||||
|
config: &whatsapp.Config{},
|
||||||
|
inputPolicy: model.InputNone,
|
||||||
|
}
|
||||||
|
}
|
|
@ -3,7 +3,6 @@ package oonimkall
|
||||||
import (
|
import (
|
||||||
"context"
|
"context"
|
||||||
|
|
||||||
"github.com/ooni/probe-cli/v3/internal/engine"
|
|
||||||
"github.com/ooni/probe-cli/v3/internal/model"
|
"github.com/ooni/probe-cli/v3/internal/model"
|
||||||
)
|
)
|
||||||
|
|
||||||
|
@ -67,7 +66,7 @@ type experimentBuilder interface {
|
||||||
|
|
||||||
// experimentBuilderWrapper wraps *ExperimentBuilder
|
// experimentBuilderWrapper wraps *ExperimentBuilder
|
||||||
type experimentBuilderWrapper struct {
|
type experimentBuilderWrapper struct {
|
||||||
eb engine.ExperimentBuilder
|
eb model.ExperimentBuilder
|
||||||
}
|
}
|
||||||
|
|
||||||
// newExperiment implements experimentBuilder.newExperiment
|
// newExperiment implements experimentBuilder.newExperiment
|
||||||
|
|
|
@ -87,7 +87,7 @@ type MockableTaskRunnerDependencies struct {
|
||||||
// taskExperimentBuilder:
|
// taskExperimentBuilder:
|
||||||
|
|
||||||
MockableSetCallbacks func(callbacks model.ExperimentCallbacks)
|
MockableSetCallbacks func(callbacks model.ExperimentCallbacks)
|
||||||
MockableInputPolicy func() engine.InputPolicy
|
MockableInputPolicy func() model.InputPolicy
|
||||||
MockableNewExperimentInstance func() taskExperiment
|
MockableNewExperimentInstance func() taskExperiment
|
||||||
MockableInterruptible func() bool
|
MockableInterruptible func() bool
|
||||||
|
|
||||||
|
@ -169,7 +169,7 @@ func (dep *MockableTaskRunnerDependencies) SetCallbacks(callbacks model.Experime
|
||||||
dep.MockableSetCallbacks(callbacks)
|
dep.MockableSetCallbacks(callbacks)
|
||||||
}
|
}
|
||||||
|
|
||||||
func (dep *MockableTaskRunnerDependencies) InputPolicy() engine.InputPolicy {
|
func (dep *MockableTaskRunnerDependencies) InputPolicy() model.InputPolicy {
|
||||||
return dep.MockableInputPolicy()
|
return dep.MockableInputPolicy()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -227,7 +227,7 @@ type taskExperimentBuilder interface {
|
||||||
SetCallbacks(callbacks model.ExperimentCallbacks)
|
SetCallbacks(callbacks model.ExperimentCallbacks)
|
||||||
|
|
||||||
// InputPolicy returns the experiment's input policy.
|
// InputPolicy returns the experiment's input policy.
|
||||||
InputPolicy() engine.InputPolicy
|
InputPolicy() model.InputPolicy
|
||||||
|
|
||||||
// NewExperiment creates the new experiment.
|
// NewExperiment creates the new experiment.
|
||||||
NewExperimentInstance() taskExperiment
|
NewExperimentInstance() taskExperiment
|
||||||
|
|
|
@ -184,12 +184,12 @@ func (r *runnerForTask) Run(rootCtx context.Context) {
|
||||||
// In fact, our current app assumes that it's its
|
// In fact, our current app assumes that it's its
|
||||||
// responsibility to load the inputs, not oonimkall's.
|
// responsibility to load the inputs, not oonimkall's.
|
||||||
switch builder.InputPolicy() {
|
switch builder.InputPolicy() {
|
||||||
case engine.InputOrQueryBackend, engine.InputStrictlyRequired:
|
case model.InputOrQueryBackend, model.InputStrictlyRequired:
|
||||||
if len(r.settings.Inputs) <= 0 {
|
if len(r.settings.Inputs) <= 0 {
|
||||||
r.emitter.EmitFailureStartup("no input provided")
|
r.emitter.EmitFailureStartup("no input provided")
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
case engine.InputOrStaticDefault:
|
case model.InputOrStaticDefault:
|
||||||
if len(r.settings.Inputs) <= 0 {
|
if len(r.settings.Inputs) <= 0 {
|
||||||
inputs, err := engine.StaticBareInputForExperiment(r.settings.Name)
|
inputs, err := engine.StaticBareInputForExperiment(r.settings.Name)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
|
@ -198,7 +198,7 @@ func (r *runnerForTask) Run(rootCtx context.Context) {
|
||||||
}
|
}
|
||||||
r.settings.Inputs = inputs
|
r.settings.Inputs = inputs
|
||||||
}
|
}
|
||||||
case engine.InputOptional:
|
case model.InputOptional:
|
||||||
if len(r.settings.Inputs) <= 0 {
|
if len(r.settings.Inputs) <= 0 {
|
||||||
r.settings.Inputs = append(r.settings.Inputs, "")
|
r.settings.Inputs = append(r.settings.Inputs, "")
|
||||||
}
|
}
|
||||||
|
@ -240,7 +240,7 @@ func (r *runnerForTask) Run(rootCtx context.Context) {
|
||||||
// this policy in the future, but for now this covers in a
|
// this policy in the future, but for now this covers in a
|
||||||
// reasonable way web connectivity, so we should be ok.
|
// reasonable way web connectivity, so we should be ok.
|
||||||
switch builder.InputPolicy() {
|
switch builder.InputPolicy() {
|
||||||
case engine.InputOrQueryBackend, engine.InputStrictlyRequired:
|
case model.InputOrQueryBackend, model.InputStrictlyRequired:
|
||||||
var (
|
var (
|
||||||
cancelMeas context.CancelFunc
|
cancelMeas context.CancelFunc
|
||||||
cancelSubmit context.CancelFunc
|
cancelSubmit context.CancelFunc
|
||||||
|
|
|
@ -204,8 +204,8 @@ func TestTaskRunnerRun(t *testing.T) {
|
||||||
},
|
},
|
||||||
MockableSetCallbacks: func(callbacks model.ExperimentCallbacks) {
|
MockableSetCallbacks: func(callbacks model.ExperimentCallbacks) {
|
||||||
},
|
},
|
||||||
MockableInputPolicy: func() engine.InputPolicy {
|
MockableInputPolicy: func() model.InputPolicy {
|
||||||
return engine.InputNone
|
return model.InputNone
|
||||||
},
|
},
|
||||||
MockableInterruptible: func() bool {
|
MockableInterruptible: func() bool {
|
||||||
return false
|
return false
|
||||||
|
@ -310,8 +310,8 @@ func TestTaskRunnerRun(t *testing.T) {
|
||||||
t.Run("with missing input and InputOrQueryBackend policy", func(t *testing.T) {
|
t.Run("with missing input and InputOrQueryBackend policy", func(t *testing.T) {
|
||||||
runner, emitter := newRunnerForTesting()
|
runner, emitter := newRunnerForTesting()
|
||||||
fake := fakeSuccessfulRun()
|
fake := fakeSuccessfulRun()
|
||||||
fake.MockableInputPolicy = func() engine.InputPolicy {
|
fake.MockableInputPolicy = func() model.InputPolicy {
|
||||||
return engine.InputOrQueryBackend
|
return model.InputOrQueryBackend
|
||||||
}
|
}
|
||||||
runner.sessionBuilder = fake
|
runner.sessionBuilder = fake
|
||||||
events := runAndCollect(runner, emitter)
|
events := runAndCollect(runner, emitter)
|
||||||
|
@ -331,8 +331,8 @@ func TestTaskRunnerRun(t *testing.T) {
|
||||||
t.Run("with missing input and InputStrictlyRequired policy", func(t *testing.T) {
|
t.Run("with missing input and InputStrictlyRequired policy", func(t *testing.T) {
|
||||||
runner, emitter := newRunnerForTesting()
|
runner, emitter := newRunnerForTesting()
|
||||||
fake := fakeSuccessfulRun()
|
fake := fakeSuccessfulRun()
|
||||||
fake.MockableInputPolicy = func() engine.InputPolicy {
|
fake.MockableInputPolicy = func() model.InputPolicy {
|
||||||
return engine.InputStrictlyRequired
|
return model.InputStrictlyRequired
|
||||||
}
|
}
|
||||||
runner.sessionBuilder = fake
|
runner.sessionBuilder = fake
|
||||||
events := runAndCollect(runner, emitter)
|
events := runAndCollect(runner, emitter)
|
||||||
|
@ -355,8 +355,8 @@ func TestTaskRunnerRun(t *testing.T) {
|
||||||
runner, emitter := newRunnerForTesting()
|
runner, emitter := newRunnerForTesting()
|
||||||
runner.settings.Name = "Antani" // no input for this experiment
|
runner.settings.Name = "Antani" // no input for this experiment
|
||||||
fake := fakeSuccessfulRun()
|
fake := fakeSuccessfulRun()
|
||||||
fake.MockableInputPolicy = func() engine.InputPolicy {
|
fake.MockableInputPolicy = func() model.InputPolicy {
|
||||||
return engine.InputOrStaticDefault
|
return model.InputOrStaticDefault
|
||||||
}
|
}
|
||||||
runner.sessionBuilder = fake
|
runner.sessionBuilder = fake
|
||||||
events := runAndCollect(runner, emitter)
|
events := runAndCollect(runner, emitter)
|
||||||
|
@ -377,8 +377,8 @@ func TestTaskRunnerRun(t *testing.T) {
|
||||||
runner, emitter := newRunnerForTesting()
|
runner, emitter := newRunnerForTesting()
|
||||||
runner.settings.Inputs = append(runner.settings.Inputs, "https://x.org/")
|
runner.settings.Inputs = append(runner.settings.Inputs, "https://x.org/")
|
||||||
fake := fakeSuccessfulRun()
|
fake := fakeSuccessfulRun()
|
||||||
fake.MockableInputPolicy = func() engine.InputPolicy {
|
fake.MockableInputPolicy = func() model.InputPolicy {
|
||||||
return engine.InputNone
|
return model.InputNone
|
||||||
}
|
}
|
||||||
runner.sessionBuilder = fake
|
runner.sessionBuilder = fake
|
||||||
events := runAndCollect(runner, emitter)
|
events := runAndCollect(runner, emitter)
|
||||||
|
@ -419,8 +419,8 @@ func TestTaskRunnerRun(t *testing.T) {
|
||||||
t.Run("with success and InputNone policy", func(t *testing.T) {
|
t.Run("with success and InputNone policy", func(t *testing.T) {
|
||||||
runner, emitter := newRunnerForTesting()
|
runner, emitter := newRunnerForTesting()
|
||||||
fake := fakeSuccessfulRun()
|
fake := fakeSuccessfulRun()
|
||||||
fake.MockableInputPolicy = func() engine.InputPolicy {
|
fake.MockableInputPolicy = func() model.InputPolicy {
|
||||||
return engine.InputNone
|
return model.InputNone
|
||||||
}
|
}
|
||||||
runner.sessionBuilder = fake
|
runner.sessionBuilder = fake
|
||||||
events := runAndCollect(runner, emitter)
|
events := runAndCollect(runner, emitter)
|
||||||
|
@ -445,8 +445,8 @@ func TestTaskRunnerRun(t *testing.T) {
|
||||||
t.Run("with measurement failure and InputNone policy", func(t *testing.T) {
|
t.Run("with measurement failure and InputNone policy", func(t *testing.T) {
|
||||||
runner, emitter := newRunnerForTesting()
|
runner, emitter := newRunnerForTesting()
|
||||||
fake := fakeSuccessfulRun()
|
fake := fakeSuccessfulRun()
|
||||||
fake.MockableInputPolicy = func() engine.InputPolicy {
|
fake.MockableInputPolicy = func() model.InputPolicy {
|
||||||
return engine.InputNone
|
return model.InputNone
|
||||||
}
|
}
|
||||||
fake.MockableMeasureWithContext = func(ctx context.Context, input string) (measurement *model.Measurement, err error) {
|
fake.MockableMeasureWithContext = func(ctx context.Context, input string) (measurement *model.Measurement, err error) {
|
||||||
return nil, errors.New("preconditions error")
|
return nil, errors.New("preconditions error")
|
||||||
|
@ -475,8 +475,8 @@ func TestTaskRunnerRun(t *testing.T) {
|
||||||
// which is what was happening in the above referenced issue.
|
// which is what was happening in the above referenced issue.
|
||||||
runner, emitter := newRunnerForTesting()
|
runner, emitter := newRunnerForTesting()
|
||||||
fake := fakeSuccessfulRun()
|
fake := fakeSuccessfulRun()
|
||||||
fake.MockableInputPolicy = func() engine.InputPolicy {
|
fake.MockableInputPolicy = func() model.InputPolicy {
|
||||||
return engine.InputNone
|
return model.InputNone
|
||||||
}
|
}
|
||||||
fake.MockableMeasureWithContext = func(ctx context.Context, input string) (measurement *model.Measurement, err error) {
|
fake.MockableMeasureWithContext = func(ctx context.Context, input string) (measurement *model.Measurement, err error) {
|
||||||
return nil, errors.New("preconditions error")
|
return nil, errors.New("preconditions error")
|
||||||
|
@ -506,8 +506,8 @@ func TestTaskRunnerRun(t *testing.T) {
|
||||||
runner, emitter := newRunnerForTesting()
|
runner, emitter := newRunnerForTesting()
|
||||||
runner.settings.Inputs = []string{"a", "b", "c", "d"}
|
runner.settings.Inputs = []string{"a", "b", "c", "d"}
|
||||||
fake := fakeSuccessfulRun()
|
fake := fakeSuccessfulRun()
|
||||||
fake.MockableInputPolicy = func() engine.InputPolicy {
|
fake.MockableInputPolicy = func() model.InputPolicy {
|
||||||
return engine.InputStrictlyRequired
|
return model.InputStrictlyRequired
|
||||||
}
|
}
|
||||||
runner.sessionBuilder = fake
|
runner.sessionBuilder = fake
|
||||||
events := runAndCollect(runner, emitter)
|
events := runAndCollect(runner, emitter)
|
||||||
|
@ -554,8 +554,8 @@ func TestTaskRunnerRun(t *testing.T) {
|
||||||
runner, emitter := newRunnerForTesting()
|
runner, emitter := newRunnerForTesting()
|
||||||
runner.settings.Inputs = []string{"a", "b", "c", "d"}
|
runner.settings.Inputs = []string{"a", "b", "c", "d"}
|
||||||
fake := fakeSuccessfulRun()
|
fake := fakeSuccessfulRun()
|
||||||
fake.MockableInputPolicy = func() engine.InputPolicy {
|
fake.MockableInputPolicy = func() model.InputPolicy {
|
||||||
return engine.InputOptional
|
return model.InputOptional
|
||||||
}
|
}
|
||||||
runner.sessionBuilder = fake
|
runner.sessionBuilder = fake
|
||||||
events := runAndCollect(runner, emitter)
|
events := runAndCollect(runner, emitter)
|
||||||
|
@ -601,8 +601,8 @@ func TestTaskRunnerRun(t *testing.T) {
|
||||||
t.Run("with success and InputOptional and no input", func(t *testing.T) {
|
t.Run("with success and InputOptional and no input", func(t *testing.T) {
|
||||||
runner, emitter := newRunnerForTesting()
|
runner, emitter := newRunnerForTesting()
|
||||||
fake := fakeSuccessfulRun()
|
fake := fakeSuccessfulRun()
|
||||||
fake.MockableInputPolicy = func() engine.InputPolicy {
|
fake.MockableInputPolicy = func() model.InputPolicy {
|
||||||
return engine.InputOptional
|
return model.InputOptional
|
||||||
}
|
}
|
||||||
runner.sessionBuilder = fake
|
runner.sessionBuilder = fake
|
||||||
events := runAndCollect(runner, emitter)
|
events := runAndCollect(runner, emitter)
|
||||||
|
@ -631,8 +631,8 @@ func TestTaskRunnerRun(t *testing.T) {
|
||||||
runner, emitter := newRunnerForTesting()
|
runner, emitter := newRunnerForTesting()
|
||||||
runner.settings.Name = experimentName
|
runner.settings.Name = experimentName
|
||||||
fake := fakeSuccessfulRun()
|
fake := fakeSuccessfulRun()
|
||||||
fake.MockableInputPolicy = func() engine.InputPolicy {
|
fake.MockableInputPolicy = func() model.InputPolicy {
|
||||||
return engine.InputOrStaticDefault
|
return model.InputOrStaticDefault
|
||||||
}
|
}
|
||||||
runner.sessionBuilder = fake
|
runner.sessionBuilder = fake
|
||||||
events := runAndCollect(runner, emitter)
|
events := runAndCollect(runner, emitter)
|
||||||
|
@ -667,8 +667,8 @@ func TestTaskRunnerRun(t *testing.T) {
|
||||||
runner.settings.Inputs = []string{"a", "b", "c", "d"}
|
runner.settings.Inputs = []string{"a", "b", "c", "d"}
|
||||||
runner.settings.Options.MaxRuntime = 2
|
runner.settings.Options.MaxRuntime = 2
|
||||||
fake := fakeSuccessfulRun()
|
fake := fakeSuccessfulRun()
|
||||||
fake.MockableInputPolicy = func() engine.InputPolicy {
|
fake.MockableInputPolicy = func() model.InputPolicy {
|
||||||
return engine.InputStrictlyRequired
|
return model.InputStrictlyRequired
|
||||||
}
|
}
|
||||||
fake.MockableMeasureWithContext = func(ctx context.Context, input string) (measurement *model.Measurement, err error) {
|
fake.MockableMeasureWithContext = func(ctx context.Context, input string) (measurement *model.Measurement, err error) {
|
||||||
time.Sleep(1 * time.Second)
|
time.Sleep(1 * time.Second)
|
||||||
|
@ -708,8 +708,8 @@ func TestTaskRunnerRun(t *testing.T) {
|
||||||
runner.settings.Inputs = []string{"a", "b", "c", "d"}
|
runner.settings.Inputs = []string{"a", "b", "c", "d"}
|
||||||
runner.settings.Options.MaxRuntime = 2
|
runner.settings.Options.MaxRuntime = 2
|
||||||
fake := fakeSuccessfulRun()
|
fake := fakeSuccessfulRun()
|
||||||
fake.MockableInputPolicy = func() engine.InputPolicy {
|
fake.MockableInputPolicy = func() model.InputPolicy {
|
||||||
return engine.InputStrictlyRequired
|
return model.InputStrictlyRequired
|
||||||
}
|
}
|
||||||
fake.MockableInterruptible = func() bool {
|
fake.MockableInterruptible = func() bool {
|
||||||
return true
|
return true
|
||||||
|
@ -743,8 +743,8 @@ func TestTaskRunnerRun(t *testing.T) {
|
||||||
runner, emitter := newRunnerForTesting()
|
runner, emitter := newRunnerForTesting()
|
||||||
runner.settings.Inputs = []string{"a"}
|
runner.settings.Inputs = []string{"a"}
|
||||||
fake := fakeSuccessfulRun()
|
fake := fakeSuccessfulRun()
|
||||||
fake.MockableInputPolicy = func() engine.InputPolicy {
|
fake.MockableInputPolicy = func() model.InputPolicy {
|
||||||
return engine.InputStrictlyRequired
|
return model.InputStrictlyRequired
|
||||||
}
|
}
|
||||||
fake.MockableSubmitAndUpdateMeasurementContext = func(ctx context.Context, measurement *model.Measurement) error {
|
fake.MockableSubmitAndUpdateMeasurementContext = func(ctx context.Context, measurement *model.Measurement) error {
|
||||||
return errors.New("cannot submit")
|
return errors.New("cannot submit")
|
||||||
|
|
|
@ -59,7 +59,7 @@ func (sess *taskSessionEngine) NewExperimentBuilderByName(
|
||||||
// taskExperimentBuilderEngine wraps ./internal/engine's
|
// taskExperimentBuilderEngine wraps ./internal/engine's
|
||||||
// ExperimentBuilder type.
|
// ExperimentBuilder type.
|
||||||
type taskExperimentBuilderEngine struct {
|
type taskExperimentBuilderEngine struct {
|
||||||
engine.ExperimentBuilder
|
model.ExperimentBuilder
|
||||||
}
|
}
|
||||||
|
|
||||||
var _ taskExperimentBuilder = &taskExperimentBuilderEngine{}
|
var _ taskExperimentBuilder = &taskExperimentBuilderEngine{}
|
||||||
|
@ -72,7 +72,7 @@ func (b *taskExperimentBuilderEngine) NewExperimentInstance() taskExperiment {
|
||||||
|
|
||||||
// taskExperimentEngine wraps ./internal/engine's Experiment.
|
// taskExperimentEngine wraps ./internal/engine's Experiment.
|
||||||
type taskExperimentEngine struct {
|
type taskExperimentEngine struct {
|
||||||
engine.Experiment
|
model.Experiment
|
||||||
}
|
}
|
||||||
|
|
||||||
var _ taskExperiment = &taskExperimentEngine{}
|
var _ taskExperiment = &taskExperimentEngine{}
|
||||||
|
|
Loading…
Reference in New Issue
Block a user