d0da224a2a
See https://github.com/ooni/probe/issues/2184 While there, rename `runtimex.PanicIfFalse` to `runtimex.Assert` (it was about time...)
411 lines
11 KiB
Go
411 lines
11 KiB
Go
package oonirun
|
|
|
|
import (
|
|
"context"
|
|
"errors"
|
|
"reflect"
|
|
"sort"
|
|
"testing"
|
|
"time"
|
|
|
|
"github.com/ooni/probe-cli/v3/internal/engine"
|
|
"github.com/ooni/probe-cli/v3/internal/model"
|
|
"github.com/ooni/probe-cli/v3/internal/model/mocks"
|
|
"github.com/ooni/probe-cli/v3/internal/testingx"
|
|
)
|
|
|
|
func TestExperimentRunWithFailureToSubmitAndShuffle(t *testing.T) {
|
|
shuffledInputsPrev := experimentShuffledInputs.Load()
|
|
var calledSetOptionsAny int
|
|
var failedToSubmit int
|
|
var calledKibiBytesReceived int
|
|
var calledKibiBytesSent int
|
|
ctx := context.Background()
|
|
desc := &Experiment{
|
|
Annotations: map[string]string{
|
|
"platform": "linux",
|
|
},
|
|
ExtraOptions: map[string]any{
|
|
"SleepTime": int64(10 * time.Millisecond),
|
|
},
|
|
Inputs: []string{
|
|
"a", "b", "c",
|
|
},
|
|
InputFilePaths: []string{},
|
|
MaxRuntime: 0,
|
|
Name: "example",
|
|
NoCollector: true,
|
|
NoJSON: true,
|
|
Random: true, // to test randomness
|
|
ReportFile: "",
|
|
Session: &mocks.Session{
|
|
MockNewExperimentBuilder: func(name string) (model.ExperimentBuilder, error) {
|
|
eb := &mocks.ExperimentBuilder{
|
|
MockInputPolicy: func() model.InputPolicy {
|
|
return model.InputOptional
|
|
},
|
|
MockSetOptionsAny: func(options map[string]any) error {
|
|
calledSetOptionsAny++
|
|
return nil
|
|
},
|
|
MockNewExperiment: func() model.Experiment {
|
|
exp := &mocks.Experiment{
|
|
MockMeasureAsync: func(ctx context.Context, input string) (<-chan *model.Measurement, error) {
|
|
out := make(chan *model.Measurement)
|
|
go func() {
|
|
defer close(out)
|
|
ff := &testingx.FakeFiller{}
|
|
var meas model.Measurement
|
|
ff.Fill(&meas)
|
|
out <- &meas
|
|
}()
|
|
return out, nil
|
|
},
|
|
MockKibiBytesReceived: func() float64 {
|
|
calledKibiBytesReceived++
|
|
return 1.453
|
|
},
|
|
MockKibiBytesSent: func() float64 {
|
|
calledKibiBytesSent++
|
|
return 1.648
|
|
},
|
|
}
|
|
return exp
|
|
},
|
|
}
|
|
return eb, nil
|
|
},
|
|
MockLogger: func() model.Logger {
|
|
return model.DiscardLogger
|
|
},
|
|
},
|
|
newExperimentBuilderFn: nil,
|
|
newInputLoaderFn: nil,
|
|
newSubmitterFn: func(ctx context.Context) (engine.Submitter, error) {
|
|
subm := &mocks.Submitter{
|
|
MockSubmit: func(ctx context.Context, m *model.Measurement) error {
|
|
failedToSubmit++
|
|
return errors.New("mocked error")
|
|
},
|
|
}
|
|
return subm, nil
|
|
},
|
|
newSaverFn: nil,
|
|
newInputProcessorFn: nil,
|
|
}
|
|
if err := desc.Run(ctx); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
if failedToSubmit < 1 {
|
|
t.Fatal("expected to see failure to submit")
|
|
}
|
|
if experimentShuffledInputs.Load() != shuffledInputsPrev+1 {
|
|
t.Fatal("did not shuffle inputs")
|
|
}
|
|
if calledSetOptionsAny < 1 {
|
|
t.Fatal("should have called SetOptionsAny")
|
|
}
|
|
if calledKibiBytesReceived < 1 {
|
|
t.Fatal("did not call KibiBytesReceived")
|
|
}
|
|
if calledKibiBytesSent < 1 {
|
|
t.Fatal("did not call KibiBytesSent")
|
|
}
|
|
}
|
|
|
|
func Test_experimentOptionsToStringList(t *testing.T) {
|
|
type args struct {
|
|
options map[string]any
|
|
}
|
|
tests := []struct {
|
|
name string
|
|
args args
|
|
wantOut []string
|
|
}{
|
|
{
|
|
name: "happy path: a map with three entries returns three items",
|
|
args: args{
|
|
map[string]any{
|
|
"foo": 1,
|
|
"bar": 2,
|
|
"baaz": 3,
|
|
},
|
|
},
|
|
wantOut: []string{"baaz=3", "bar=2", "foo=1"},
|
|
},
|
|
{
|
|
name: "an option beginning with `Safe` is skipped from the output",
|
|
args: args{
|
|
map[string]any{
|
|
"foo": 1,
|
|
"Safefoo": 42,
|
|
},
|
|
},
|
|
wantOut: []string{"foo=1"},
|
|
},
|
|
}
|
|
for _, tt := range tests {
|
|
t.Run(tt.name, func(t *testing.T) {
|
|
gotOut := experimentOptionsToStringList(tt.args.options)
|
|
sort.Strings(gotOut)
|
|
if !reflect.DeepEqual(gotOut, tt.wantOut) {
|
|
t.Errorf("experimentOptionsToStringList() = %v, want %v", gotOut, tt.wantOut)
|
|
}
|
|
})
|
|
}
|
|
}
|
|
|
|
func TestExperimentRun(t *testing.T) {
|
|
errMocked := errors.New("mocked error")
|
|
type fields struct {
|
|
Annotations map[string]string
|
|
ExtraOptions map[string]any
|
|
Inputs []string
|
|
InputFilePaths []string
|
|
MaxRuntime int64
|
|
Name string
|
|
NoCollector bool
|
|
NoJSON bool
|
|
Random bool
|
|
ReportFile string
|
|
Session Session
|
|
newExperimentBuilderFn func(experimentName string) (model.ExperimentBuilder, error)
|
|
newInputLoaderFn func(inputPolicy model.InputPolicy) inputLoader
|
|
newSubmitterFn func(ctx context.Context) (engine.Submitter, error)
|
|
newSaverFn func(experiment model.Experiment) (engine.Saver, error)
|
|
newInputProcessorFn func(experiment model.Experiment, inputList []model.OOAPIURLInfo, saver engine.Saver, submitter engine.Submitter) inputProcessor
|
|
}
|
|
type args struct {
|
|
ctx context.Context
|
|
}
|
|
tests := []struct {
|
|
name string
|
|
fields fields
|
|
args args
|
|
expectErr error
|
|
}{{
|
|
name: "cannot construct an experiment builder",
|
|
fields: fields{
|
|
newExperimentBuilderFn: func(experimentName string) (model.ExperimentBuilder, error) {
|
|
return nil, errMocked
|
|
},
|
|
},
|
|
args: args{},
|
|
expectErr: errMocked,
|
|
}, {
|
|
name: "cannot load input",
|
|
fields: fields{
|
|
newExperimentBuilderFn: func(experimentName string) (model.ExperimentBuilder, error) {
|
|
eb := &mocks.ExperimentBuilder{
|
|
MockInputPolicy: func() model.InputPolicy {
|
|
return model.InputOptional
|
|
},
|
|
}
|
|
return eb, nil
|
|
},
|
|
newInputLoaderFn: func(inputPolicy model.InputPolicy) inputLoader {
|
|
return &mocks.ExperimentInputLoader{
|
|
MockLoad: func(ctx context.Context) ([]model.OOAPIURLInfo, error) {
|
|
return nil, errMocked
|
|
},
|
|
}
|
|
},
|
|
},
|
|
args: args{},
|
|
expectErr: errMocked,
|
|
}, {
|
|
name: "cannot set options",
|
|
fields: fields{
|
|
newExperimentBuilderFn: func(experimentName string) (model.ExperimentBuilder, error) {
|
|
eb := &mocks.ExperimentBuilder{
|
|
MockInputPolicy: func() model.InputPolicy {
|
|
return model.InputOptional
|
|
},
|
|
MockSetOptionsAny: func(options map[string]any) error {
|
|
return errMocked
|
|
},
|
|
}
|
|
return eb, nil
|
|
},
|
|
newInputLoaderFn: func(inputPolicy model.InputPolicy) inputLoader {
|
|
return &mocks.ExperimentInputLoader{
|
|
MockLoad: func(ctx context.Context) ([]model.OOAPIURLInfo, error) {
|
|
return []model.OOAPIURLInfo{}, nil
|
|
},
|
|
}
|
|
},
|
|
},
|
|
args: args{},
|
|
expectErr: errMocked,
|
|
}, {
|
|
name: "cannot create new submitter",
|
|
fields: fields{
|
|
Session: &mocks.Session{
|
|
MockLogger: func() model.Logger {
|
|
return model.DiscardLogger
|
|
},
|
|
},
|
|
newExperimentBuilderFn: func(experimentName string) (model.ExperimentBuilder, error) {
|
|
eb := &mocks.ExperimentBuilder{
|
|
MockInputPolicy: func() model.InputPolicy {
|
|
return model.InputOptional
|
|
},
|
|
MockSetOptionsAny: func(options map[string]any) error {
|
|
return nil
|
|
},
|
|
MockNewExperiment: func() model.Experiment {
|
|
exp := &mocks.Experiment{
|
|
MockKibiBytesReceived: func() float64 {
|
|
return 0
|
|
},
|
|
MockKibiBytesSent: func() float64 {
|
|
return 0
|
|
},
|
|
}
|
|
return exp
|
|
},
|
|
}
|
|
return eb, nil
|
|
},
|
|
newInputLoaderFn: func(inputPolicy model.InputPolicy) inputLoader {
|
|
return &mocks.ExperimentInputLoader{
|
|
MockLoad: func(ctx context.Context) ([]model.OOAPIURLInfo, error) {
|
|
return []model.OOAPIURLInfo{}, nil
|
|
},
|
|
}
|
|
},
|
|
newSubmitterFn: func(ctx context.Context) (engine.Submitter, error) {
|
|
return nil, errMocked
|
|
},
|
|
},
|
|
args: args{},
|
|
expectErr: errMocked,
|
|
}, {
|
|
name: "cannot create new saver",
|
|
fields: fields{
|
|
Session: &mocks.Session{
|
|
MockLogger: func() model.Logger {
|
|
return model.DiscardLogger
|
|
},
|
|
},
|
|
newExperimentBuilderFn: func(experimentName string) (model.ExperimentBuilder, error) {
|
|
eb := &mocks.ExperimentBuilder{
|
|
MockInputPolicy: func() model.InputPolicy {
|
|
return model.InputOptional
|
|
},
|
|
MockSetOptionsAny: func(options map[string]any) error {
|
|
return nil
|
|
},
|
|
MockNewExperiment: func() model.Experiment {
|
|
exp := &mocks.Experiment{
|
|
MockKibiBytesReceived: func() float64 {
|
|
return 0
|
|
},
|
|
MockKibiBytesSent: func() float64 {
|
|
return 0
|
|
},
|
|
}
|
|
return exp
|
|
},
|
|
}
|
|
return eb, nil
|
|
},
|
|
newInputLoaderFn: func(inputPolicy model.InputPolicy) inputLoader {
|
|
return &mocks.ExperimentInputLoader{
|
|
MockLoad: func(ctx context.Context) ([]model.OOAPIURLInfo, error) {
|
|
return []model.OOAPIURLInfo{}, nil
|
|
},
|
|
}
|
|
},
|
|
newSubmitterFn: func(ctx context.Context) (engine.Submitter, error) {
|
|
return &mocks.Submitter{}, nil
|
|
},
|
|
newSaverFn: func(experiment model.Experiment) (engine.Saver, error) {
|
|
return nil, errMocked
|
|
},
|
|
},
|
|
args: args{},
|
|
expectErr: errMocked,
|
|
}, {
|
|
name: "input processor fails",
|
|
fields: fields{
|
|
Session: &mocks.Session{
|
|
MockLogger: func() model.Logger {
|
|
return model.DiscardLogger
|
|
},
|
|
},
|
|
newExperimentBuilderFn: func(experimentName string) (model.ExperimentBuilder, error) {
|
|
eb := &mocks.ExperimentBuilder{
|
|
MockInputPolicy: func() model.InputPolicy {
|
|
return model.InputOptional
|
|
},
|
|
MockSetOptionsAny: func(options map[string]any) error {
|
|
return nil
|
|
},
|
|
MockNewExperiment: func() model.Experiment {
|
|
exp := &mocks.Experiment{
|
|
MockKibiBytesReceived: func() float64 {
|
|
return 0
|
|
},
|
|
MockKibiBytesSent: func() float64 {
|
|
return 0
|
|
},
|
|
}
|
|
return exp
|
|
},
|
|
}
|
|
return eb, nil
|
|
},
|
|
newInputLoaderFn: func(inputPolicy model.InputPolicy) inputLoader {
|
|
return &mocks.ExperimentInputLoader{
|
|
MockLoad: func(ctx context.Context) ([]model.OOAPIURLInfo, error) {
|
|
return []model.OOAPIURLInfo{}, nil
|
|
},
|
|
}
|
|
},
|
|
newSubmitterFn: func(ctx context.Context) (engine.Submitter, error) {
|
|
return &mocks.Submitter{}, nil
|
|
},
|
|
newSaverFn: func(experiment model.Experiment) (engine.Saver, error) {
|
|
return &mocks.Saver{}, nil
|
|
},
|
|
newInputProcessorFn: func(experiment model.Experiment, inputList []model.OOAPIURLInfo,
|
|
saver engine.Saver, submitter engine.Submitter) inputProcessor {
|
|
return &mocks.ExperimentInputProcessor{
|
|
MockRun: func(ctx context.Context) error {
|
|
return errMocked
|
|
},
|
|
}
|
|
},
|
|
},
|
|
args: args{},
|
|
expectErr: errMocked,
|
|
}}
|
|
for _, tt := range tests {
|
|
t.Run(tt.name, func(t *testing.T) {
|
|
ed := &Experiment{
|
|
Annotations: tt.fields.Annotations,
|
|
ExtraOptions: tt.fields.ExtraOptions,
|
|
Inputs: tt.fields.Inputs,
|
|
InputFilePaths: tt.fields.InputFilePaths,
|
|
MaxRuntime: tt.fields.MaxRuntime,
|
|
Name: tt.fields.Name,
|
|
NoCollector: tt.fields.NoCollector,
|
|
NoJSON: tt.fields.NoJSON,
|
|
Random: tt.fields.Random,
|
|
ReportFile: tt.fields.ReportFile,
|
|
Session: tt.fields.Session,
|
|
newExperimentBuilderFn: tt.fields.newExperimentBuilderFn,
|
|
newInputLoaderFn: tt.fields.newInputLoaderFn,
|
|
newSubmitterFn: tt.fields.newSubmitterFn,
|
|
newSaverFn: tt.fields.newSaverFn,
|
|
newInputProcessorFn: tt.fields.newInputProcessorFn,
|
|
}
|
|
err := ed.Run(tt.args.ctx)
|
|
if !errors.Is(err, tt.expectErr) {
|
|
t.Fatalf("Experiment.Run() error = %v, expectErr %v", err, tt.expectErr)
|
|
}
|
|
})
|
|
}
|
|
}
|