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