refactor(engine): allow scripts to register experiments (#860)
See https://github.com/ooni/probe/issues/2216
This commit is contained in:
@@ -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
|
||||
|
||||
Reference in New Issue
Block a user