ooni-probe-cli/internal/oonirun/experiment_test.go
Simone Basso d0da224a2a
feat(oonirun): improve tests (#915)
See https://github.com/ooni/probe/issues/2184

While there, rename `runtimex.PanicIfFalse` to `runtimex.Assert` (it was about time...)
2022-08-31 18:40:27 +02:00

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)
}
})
}
}