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...)
This commit is contained in:
parent
a8a29cc0dd
commit
d0da224a2a
|
@ -17,7 +17,7 @@ func acquireUserConsent(miniooniDir string, currentOptions *Options) {
|
|||
consentFile := path.Join(miniooniDir, "informed")
|
||||
err := maybeWriteConsentFile(currentOptions.Yes, consentFile)
|
||||
runtimex.PanicOnError(err, "cannot write informed consent file")
|
||||
runtimex.PanicIfFalse(
|
||||
runtimex.Assert(
|
||||
regularFileExists(consentFile),
|
||||
riskOfRunningOONI,
|
||||
)
|
||||
|
|
|
@ -308,7 +308,7 @@ func mainSingleIteration(logger model.Logger, experimentName string, currentOpti
|
|||
log.Infof("Current time: %s", time.Now().Format("2006-01-02 15:04:05 MST"))
|
||||
|
||||
homeDir := gethomedir(currentOptions.HomeDir)
|
||||
runtimex.PanicIfFalse(homeDir != "", "home directory is empty")
|
||||
runtimex.Assert(homeDir != "", "home directory is empty")
|
||||
miniooniDir := path.Join(homeDir, ".miniooni")
|
||||
err := os.MkdirAll(miniooniDir, 0700)
|
||||
runtimex.PanicOnError(err, "cannot create $HOME/.miniooni directory")
|
||||
|
|
|
@ -11,6 +11,8 @@ import (
|
|||
)
|
||||
|
||||
// Session allows to mock sessions.
|
||||
//
|
||||
// Deprecated: use ./internal/model/mocks.Session instead.
|
||||
type Session struct {
|
||||
MockableTestHelpers map[string][]model.OOAPIService
|
||||
MockableHTTPClient model.HTTPClient
|
||||
|
|
|
@ -6,10 +6,8 @@ import (
|
|||
"github.com/ooni/probe-cli/v3/internal/model"
|
||||
)
|
||||
|
||||
// Saver saves a measurement on some persistent storage.
|
||||
type Saver interface {
|
||||
SaveMeasurement(m *model.Measurement) error
|
||||
}
|
||||
// Saver is an alias for model.Saver.
|
||||
type Saver = model.Saver
|
||||
|
||||
// SaverConfig is the configuration for creating a new Saver.
|
||||
type SaverConfig struct {
|
||||
|
|
|
@ -9,12 +9,8 @@ import (
|
|||
// TODO(bassosimone): maybe keep track of which measurements
|
||||
// could not be submitted by a specific submitter?
|
||||
|
||||
// Submitter submits a measurement to the OONI collector.
|
||||
type Submitter interface {
|
||||
// Submit submits the measurement and updates its
|
||||
// report ID field in case of success.
|
||||
Submit(ctx context.Context, m *model.Measurement) error
|
||||
}
|
||||
// Submitter is an alias for model.Submitter
|
||||
type Submitter = model.Submitter
|
||||
|
||||
// SubmitterSession is the Submitter's view of the Session.
|
||||
type SubmitterSession interface {
|
||||
|
|
|
@ -27,7 +27,7 @@ func NewFailure(err error) *string {
|
|||
if !errors.As(err, &errWrapper) {
|
||||
err := netxlite.NewTopLevelGenericErrWrapper(err)
|
||||
couldConvert := errors.As(err, &errWrapper)
|
||||
runtimex.PanicIfFalse(couldConvert, "we should have an ErrWrapper here")
|
||||
runtimex.Assert(couldConvert, "we should have an ErrWrapper here")
|
||||
}
|
||||
s := errWrapper.Failure
|
||||
if s == "" {
|
||||
|
|
|
@ -281,3 +281,25 @@ type ExperimentOptionInfo struct {
|
|||
// Type contains the type.
|
||||
Type string
|
||||
}
|
||||
|
||||
// ExperimentInputLoader loads inputs from local or remote sources.
|
||||
type ExperimentInputLoader interface {
|
||||
Load(ctx context.Context) ([]OOAPIURLInfo, error)
|
||||
}
|
||||
|
||||
// Submitter submits a measurement to the OONI collector.
|
||||
type Submitter interface {
|
||||
// Submit submits the measurement and updates its
|
||||
// report ID field in case of success.
|
||||
Submit(ctx context.Context, m *Measurement) error
|
||||
}
|
||||
|
||||
// Saver saves a measurement on some persistent storage.
|
||||
type Saver interface {
|
||||
SaveMeasurement(m *Measurement) error
|
||||
}
|
||||
|
||||
// ExperimentInputProcessor processes inputs for an experiment.
|
||||
type ExperimentInputProcessor interface {
|
||||
Run(ctx context.Context) error
|
||||
}
|
||||
|
|
75
internal/model/mocks/experiment.go
Normal file
75
internal/model/mocks/experiment.go
Normal file
|
@ -0,0 +1,75 @@
|
|||
package mocks
|
||||
|
||||
import (
|
||||
"context"
|
||||
|
||||
"github.com/ooni/probe-cli/v3/internal/model"
|
||||
)
|
||||
|
||||
// Experiment mocks model.Experiment
|
||||
type Experiment struct {
|
||||
MockKibiBytesReceived func() float64
|
||||
|
||||
MockKibiBytesSent func() float64
|
||||
|
||||
MockName func() string
|
||||
|
||||
MockGetSummaryKeys func(m *model.Measurement) (any, error)
|
||||
|
||||
MockReportID func() string
|
||||
|
||||
MockMeasureAsync func(ctx context.Context, input string) (<-chan *model.Measurement, error)
|
||||
|
||||
MockMeasureWithContext func(
|
||||
ctx context.Context, input string) (measurement *model.Measurement, err error)
|
||||
|
||||
MockSaveMeasurement func(measurement *model.Measurement, filePath string) error
|
||||
|
||||
MockSubmitAndUpdateMeasurementContext func(
|
||||
ctx context.Context, measurement *model.Measurement) error
|
||||
|
||||
MockOpenReportContext func(ctx context.Context) error
|
||||
}
|
||||
|
||||
func (e *Experiment) KibiBytesReceived() float64 {
|
||||
return e.MockKibiBytesReceived()
|
||||
}
|
||||
|
||||
func (e *Experiment) KibiBytesSent() float64 {
|
||||
return e.MockKibiBytesSent()
|
||||
}
|
||||
|
||||
func (e *Experiment) Name() string {
|
||||
return e.MockName()
|
||||
}
|
||||
|
||||
func (e *Experiment) GetSummaryKeys(m *model.Measurement) (any, error) {
|
||||
return e.MockGetSummaryKeys(m)
|
||||
}
|
||||
|
||||
func (e *Experiment) ReportID() string {
|
||||
return e.MockReportID()
|
||||
}
|
||||
|
||||
func (e *Experiment) MeasureAsync(
|
||||
ctx context.Context, input string) (<-chan *model.Measurement, error) {
|
||||
return e.MockMeasureAsync(ctx, input)
|
||||
}
|
||||
|
||||
func (e *Experiment) MeasureWithContext(
|
||||
ctx context.Context, input string) (measurement *model.Measurement, err error) {
|
||||
return e.MockMeasureWithContext(ctx, input)
|
||||
}
|
||||
|
||||
func (e *Experiment) SaveMeasurement(measurement *model.Measurement, filePath string) error {
|
||||
return e.MockSaveMeasurement(measurement, filePath)
|
||||
}
|
||||
|
||||
func (e *Experiment) SubmitAndUpdateMeasurementContext(
|
||||
ctx context.Context, measurement *model.Measurement) error {
|
||||
return e.MockSubmitAndUpdateMeasurementContext(ctx, measurement)
|
||||
}
|
||||
|
||||
func (e *Experiment) OpenReportContext(ctx context.Context) error {
|
||||
return e.MockOpenReportContext(ctx)
|
||||
}
|
146
internal/model/mocks/experiment_test.go
Normal file
146
internal/model/mocks/experiment_test.go
Normal file
|
@ -0,0 +1,146 @@
|
|||
package mocks
|
||||
|
||||
import (
|
||||
"context"
|
||||
"errors"
|
||||
"testing"
|
||||
|
||||
"github.com/ooni/probe-cli/v3/internal/model"
|
||||
)
|
||||
|
||||
func TestExperiment(t *testing.T) {
|
||||
t.Run("KibiBytesReceived", func(t *testing.T) {
|
||||
expected := 1.0
|
||||
e := &Experiment{
|
||||
MockKibiBytesReceived: func() float64 {
|
||||
return expected
|
||||
},
|
||||
}
|
||||
if e.KibiBytesReceived() != expected {
|
||||
t.Fatal("unexpected result")
|
||||
}
|
||||
})
|
||||
|
||||
t.Run("KibiBytesSent", func(t *testing.T) {
|
||||
expected := 1.0
|
||||
e := &Experiment{
|
||||
MockKibiBytesSent: func() float64 {
|
||||
return expected
|
||||
},
|
||||
}
|
||||
if e.KibiBytesSent() != expected {
|
||||
t.Fatal("unexpected result")
|
||||
}
|
||||
})
|
||||
|
||||
t.Run("Name", func(t *testing.T) {
|
||||
expected := "antani"
|
||||
e := &Experiment{
|
||||
MockName: func() string {
|
||||
return expected
|
||||
},
|
||||
}
|
||||
if e.Name() != expected {
|
||||
t.Fatal("unexpected result")
|
||||
}
|
||||
})
|
||||
|
||||
t.Run("GetSummaryKeys", func(t *testing.T) {
|
||||
expected := errors.New("mocked err")
|
||||
e := &Experiment{
|
||||
MockGetSummaryKeys: func(m *model.Measurement) (any, error) {
|
||||
return nil, expected
|
||||
},
|
||||
}
|
||||
out, err := e.GetSummaryKeys(&model.Measurement{})
|
||||
if !errors.Is(err, expected) {
|
||||
t.Fatal("unexpected err", err)
|
||||
}
|
||||
if out != nil {
|
||||
t.Fatal("invalid out")
|
||||
}
|
||||
})
|
||||
|
||||
t.Run("ReportID", func(t *testing.T) {
|
||||
expect := "xyz"
|
||||
e := &Experiment{
|
||||
MockReportID: func() string {
|
||||
return expect
|
||||
},
|
||||
}
|
||||
if e.ReportID() != expect {
|
||||
t.Fatal("invalid value")
|
||||
}
|
||||
})
|
||||
|
||||
t.Run("MeasureAsync", func(t *testing.T) {
|
||||
expected := errors.New("mocked err")
|
||||
e := &Experiment{
|
||||
MockMeasureAsync: func(ctx context.Context, input string) (<-chan *model.Measurement, error) {
|
||||
return nil, expected
|
||||
},
|
||||
}
|
||||
out, err := e.MeasureAsync(context.Background(), "xo")
|
||||
if !errors.Is(err, expected) {
|
||||
t.Fatal("unexpected err", err)
|
||||
}
|
||||
if out != nil {
|
||||
t.Fatal("expected nil")
|
||||
}
|
||||
})
|
||||
|
||||
t.Run("MeasureWithContext", func(t *testing.T) {
|
||||
expected := errors.New("mocked err")
|
||||
e := &Experiment{
|
||||
MockMeasureWithContext: func(ctx context.Context, input string) (measurement *model.Measurement, err error) {
|
||||
return nil, expected
|
||||
},
|
||||
}
|
||||
out, err := e.MeasureWithContext(context.Background(), "xo")
|
||||
if !errors.Is(err, expected) {
|
||||
t.Fatal("unexpected err", err)
|
||||
}
|
||||
if out != nil {
|
||||
t.Fatal("expected nil")
|
||||
}
|
||||
})
|
||||
|
||||
t.Run("SaveMeasurement", func(t *testing.T) {
|
||||
expected := errors.New("mocked err")
|
||||
e := &Experiment{
|
||||
MockSaveMeasurement: func(measurement *model.Measurement, filePath string) error {
|
||||
return expected
|
||||
},
|
||||
}
|
||||
err := e.SaveMeasurement(&model.Measurement{}, "x")
|
||||
if !errors.Is(err, expected) {
|
||||
t.Fatal("unexpected err", err)
|
||||
}
|
||||
})
|
||||
|
||||
t.Run("SubmitAndUpdateMeasurementContext", func(t *testing.T) {
|
||||
expected := errors.New("mocked err")
|
||||
e := &Experiment{
|
||||
MockSubmitAndUpdateMeasurementContext: func(ctx context.Context, measurement *model.Measurement) error {
|
||||
return expected
|
||||
},
|
||||
}
|
||||
err := e.SubmitAndUpdateMeasurementContext(context.Background(), &model.Measurement{})
|
||||
if !errors.Is(err, expected) {
|
||||
t.Fatal("unexpected err", err)
|
||||
}
|
||||
})
|
||||
|
||||
t.Run("OpenReportContext", func(t *testing.T) {
|
||||
expected := errors.New("mocked err")
|
||||
e := &Experiment{
|
||||
MockOpenReportContext: func(ctx context.Context) error {
|
||||
return expected
|
||||
},
|
||||
}
|
||||
err := e.OpenReportContext(context.Background())
|
||||
if !errors.Is(err, expected) {
|
||||
t.Fatal("unexpected err", err)
|
||||
}
|
||||
})
|
||||
}
|
48
internal/model/mocks/experimentbuilder.go
Normal file
48
internal/model/mocks/experimentbuilder.go
Normal file
|
@ -0,0 +1,48 @@
|
|||
package mocks
|
||||
|
||||
import "github.com/ooni/probe-cli/v3/internal/model"
|
||||
|
||||
// ExperimentBuilder mocks model.ExperimentBuilder.
|
||||
type ExperimentBuilder struct {
|
||||
MockInterruptible func() bool
|
||||
|
||||
MockInputPolicy func() model.InputPolicy
|
||||
|
||||
MockOptions func() (map[string]model.ExperimentOptionInfo, error)
|
||||
|
||||
MockSetOptionAny func(key string, value any) error
|
||||
|
||||
MockSetOptionsAny func(options map[string]any) error
|
||||
|
||||
MockSetCallbacks func(callbacks model.ExperimentCallbacks)
|
||||
|
||||
MockNewExperiment func() model.Experiment
|
||||
}
|
||||
|
||||
func (eb *ExperimentBuilder) Interruptible() bool {
|
||||
return eb.MockInterruptible()
|
||||
}
|
||||
|
||||
func (eb *ExperimentBuilder) InputPolicy() model.InputPolicy {
|
||||
return eb.MockInputPolicy()
|
||||
}
|
||||
|
||||
func (eb *ExperimentBuilder) Options() (map[string]model.ExperimentOptionInfo, error) {
|
||||
return eb.MockOptions()
|
||||
}
|
||||
|
||||
func (eb *ExperimentBuilder) SetOptionAny(key string, value any) error {
|
||||
return eb.MockSetOptionAny(key, value)
|
||||
}
|
||||
|
||||
func (eb *ExperimentBuilder) SetOptionsAny(options map[string]any) error {
|
||||
return eb.MockSetOptionsAny(options)
|
||||
}
|
||||
|
||||
func (eb *ExperimentBuilder) SetCallbacks(callbacks model.ExperimentCallbacks) {
|
||||
eb.MockSetCallbacks(callbacks)
|
||||
}
|
||||
|
||||
func (eb *ExperimentBuilder) NewExperiment() model.Experiment {
|
||||
return eb.MockNewExperiment()
|
||||
}
|
99
internal/model/mocks/experimentbuilder_test.go
Normal file
99
internal/model/mocks/experimentbuilder_test.go
Normal file
|
@ -0,0 +1,99 @@
|
|||
package mocks
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"testing"
|
||||
|
||||
"github.com/ooni/probe-cli/v3/internal/model"
|
||||
)
|
||||
|
||||
func TestExperimentBuilder(t *testing.T) {
|
||||
t.Run("Interruptible", func(t *testing.T) {
|
||||
eb := &ExperimentBuilder{
|
||||
MockInterruptible: func() bool {
|
||||
return true
|
||||
},
|
||||
}
|
||||
if !eb.Interruptible() {
|
||||
t.Fatal("unexpected value")
|
||||
}
|
||||
})
|
||||
|
||||
t.Run("InputPolicy", func(t *testing.T) {
|
||||
eb := &ExperimentBuilder{
|
||||
MockInputPolicy: func() model.InputPolicy {
|
||||
return model.InputOrQueryBackend
|
||||
},
|
||||
}
|
||||
if eb.InputPolicy() != model.InputOrQueryBackend {
|
||||
t.Fatal("unexpected value")
|
||||
}
|
||||
})
|
||||
|
||||
t.Run("Options", func(t *testing.T) {
|
||||
expected := errors.New("mocked error")
|
||||
eb := &ExperimentBuilder{
|
||||
MockOptions: func() (map[string]model.ExperimentOptionInfo, error) {
|
||||
return nil, expected
|
||||
},
|
||||
}
|
||||
out, err := eb.Options()
|
||||
if !errors.Is(err, expected) {
|
||||
t.Fatal("unexpected value")
|
||||
}
|
||||
if len(out) > 0 {
|
||||
t.Fatal("unexpected value")
|
||||
}
|
||||
})
|
||||
|
||||
t.Run("SetOptionAny", func(t *testing.T) {
|
||||
expected := errors.New("mocked error")
|
||||
eb := &ExperimentBuilder{
|
||||
MockSetOptionAny: func(key string, value any) error {
|
||||
return expected
|
||||
},
|
||||
}
|
||||
err := eb.SetOptionAny("antani", 1245678)
|
||||
if !errors.Is(err, expected) {
|
||||
t.Fatal("unexpected value")
|
||||
}
|
||||
})
|
||||
|
||||
t.Run("SetOptionsAny", func(t *testing.T) {
|
||||
expected := errors.New("mocked error")
|
||||
eb := &ExperimentBuilder{
|
||||
MockSetOptionsAny: func(options map[string]any) error {
|
||||
return expected
|
||||
},
|
||||
}
|
||||
err := eb.SetOptionsAny(make(map[string]any))
|
||||
if !errors.Is(err, expected) {
|
||||
t.Fatal("unexpected value")
|
||||
}
|
||||
})
|
||||
|
||||
t.Run("SetCallbacks", func(t *testing.T) {
|
||||
var called bool
|
||||
eb := &ExperimentBuilder{
|
||||
MockSetCallbacks: func(callbacks model.ExperimentCallbacks) {
|
||||
called = true
|
||||
},
|
||||
}
|
||||
eb.SetCallbacks(model.NewPrinterCallbacks(model.DiscardLogger))
|
||||
if !called {
|
||||
t.Fatal("not called")
|
||||
}
|
||||
})
|
||||
|
||||
t.Run("NewExperiment", func(t *testing.T) {
|
||||
exp := &Experiment{}
|
||||
eb := &ExperimentBuilder{
|
||||
MockNewExperiment: func() model.Experiment {
|
||||
return exp
|
||||
},
|
||||
}
|
||||
if out := eb.NewExperiment(); out != exp {
|
||||
t.Fatal("invalid result")
|
||||
}
|
||||
})
|
||||
}
|
19
internal/model/mocks/experimentinputloader.go
Normal file
19
internal/model/mocks/experimentinputloader.go
Normal file
|
@ -0,0 +1,19 @@
|
|||
package mocks
|
||||
|
||||
import (
|
||||
"context"
|
||||
|
||||
"github.com/ooni/probe-cli/v3/internal/model"
|
||||
)
|
||||
|
||||
// ExperimentInputLoader mocks model.ExperimentInputLoader
|
||||
type ExperimentInputLoader struct {
|
||||
MockLoad func(ctx context.Context) ([]model.OOAPIURLInfo, error)
|
||||
}
|
||||
|
||||
var _ model.ExperimentInputLoader = &ExperimentInputLoader{}
|
||||
|
||||
// Load calls MockLoad
|
||||
func (eil *ExperimentInputLoader) Load(ctx context.Context) ([]model.OOAPIURLInfo, error) {
|
||||
return eil.MockLoad(ctx)
|
||||
}
|
27
internal/model/mocks/experimentinputloader_test.go
Normal file
27
internal/model/mocks/experimentinputloader_test.go
Normal file
|
@ -0,0 +1,27 @@
|
|||
package mocks
|
||||
|
||||
import (
|
||||
"context"
|
||||
"errors"
|
||||
"testing"
|
||||
|
||||
"github.com/ooni/probe-cli/v3/internal/model"
|
||||
)
|
||||
|
||||
func TestExperimentInputLoader(t *testing.T) {
|
||||
t.Run("Load", func(t *testing.T) {
|
||||
expected := errors.New("mocked error")
|
||||
eil := &ExperimentInputLoader{
|
||||
MockLoad: func(ctx context.Context) ([]model.OOAPIURLInfo, error) {
|
||||
return nil, expected
|
||||
},
|
||||
}
|
||||
out, err := eil.Load(context.Background())
|
||||
if !errors.Is(err, expected) {
|
||||
t.Fatal("unexpected err", err)
|
||||
}
|
||||
if len(out) > 0 {
|
||||
t.Fatal("unexpected length")
|
||||
}
|
||||
})
|
||||
}
|
12
internal/model/mocks/experimentinputprocessor.go
Normal file
12
internal/model/mocks/experimentinputprocessor.go
Normal file
|
@ -0,0 +1,12 @@
|
|||
package mocks
|
||||
|
||||
import "context"
|
||||
|
||||
// ExperimentInputProcessor processes inputs running the given experiment.
|
||||
type ExperimentInputProcessor struct {
|
||||
MockRun func(ctx context.Context) error
|
||||
}
|
||||
|
||||
func (eip *ExperimentInputProcessor) Run(ctx context.Context) error {
|
||||
return eip.MockRun(ctx)
|
||||
}
|
22
internal/model/mocks/experimentinputprocessor_test.go
Normal file
22
internal/model/mocks/experimentinputprocessor_test.go
Normal file
|
@ -0,0 +1,22 @@
|
|||
package mocks
|
||||
|
||||
import (
|
||||
"context"
|
||||
"errors"
|
||||
"testing"
|
||||
)
|
||||
|
||||
func TestExperimentInputProcessor(t *testing.T) {
|
||||
t.Run("Run", func(t *testing.T) {
|
||||
expected := errors.New("mocked error")
|
||||
eip := &ExperimentInputProcessor{
|
||||
MockRun: func(ctx context.Context) error {
|
||||
return expected
|
||||
},
|
||||
}
|
||||
err := eip.Run(context.Background())
|
||||
if !errors.Is(err, expected) {
|
||||
t.Fatal("unexpected result")
|
||||
}
|
||||
})
|
||||
}
|
12
internal/model/mocks/saver.go
Normal file
12
internal/model/mocks/saver.go
Normal file
|
@ -0,0 +1,12 @@
|
|||
package mocks
|
||||
|
||||
import "github.com/ooni/probe-cli/v3/internal/model"
|
||||
|
||||
// Saver saves a measurement on some persistent storage.
|
||||
type Saver struct {
|
||||
MockSaveMeasurement func(m *model.Measurement) error
|
||||
}
|
||||
|
||||
func (s *Saver) SaveMeasurement(m *model.Measurement) error {
|
||||
return s.MockSaveMeasurement(m)
|
||||
}
|
23
internal/model/mocks/saver_test.go
Normal file
23
internal/model/mocks/saver_test.go
Normal file
|
@ -0,0 +1,23 @@
|
|||
package mocks
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"testing"
|
||||
|
||||
"github.com/ooni/probe-cli/v3/internal/model"
|
||||
)
|
||||
|
||||
func TestSaver(t *testing.T) {
|
||||
t.Run("SaveMeasurement", func(t *testing.T) {
|
||||
expected := errors.New("mocked error")
|
||||
s := &Saver{
|
||||
MockSaveMeasurement: func(m *model.Measurement) error {
|
||||
return expected
|
||||
},
|
||||
}
|
||||
err := s.SaveMeasurement(&model.Measurement{})
|
||||
if !errors.Is(err, expected) {
|
||||
t.Fatal("unexpected err", err)
|
||||
}
|
||||
})
|
||||
}
|
161
internal/model/mocks/session.go
Normal file
161
internal/model/mocks/session.go
Normal file
|
@ -0,0 +1,161 @@
|
|||
package mocks
|
||||
|
||||
import (
|
||||
"context"
|
||||
"net/url"
|
||||
|
||||
"github.com/ooni/probe-cli/v3/internal/model"
|
||||
)
|
||||
|
||||
// Session allows to mock sessions.
|
||||
type Session struct {
|
||||
MockGetTestHelpersByName func(name string) ([]model.OOAPIService, bool)
|
||||
|
||||
MockDefaultHTTPClient func() model.HTTPClient
|
||||
|
||||
MockFetchPsiphonConfig func(ctx context.Context) ([]byte, error)
|
||||
|
||||
MockFetchTorTargets func(
|
||||
ctx context.Context, cc string) (map[string]model.OOAPITorTarget, error)
|
||||
|
||||
MockFetchURLList func(
|
||||
ctx context.Context, config model.OOAPIURLListConfig) ([]model.OOAPIURLInfo, error)
|
||||
|
||||
MockKeyValueStore func() model.KeyValueStore
|
||||
|
||||
MockLogger func() model.Logger
|
||||
|
||||
MockMaybeResolverIP func() string
|
||||
|
||||
MockProbeASNString func() string
|
||||
|
||||
MockProbeCC func() string
|
||||
|
||||
MockProbeIP func() string
|
||||
|
||||
MockProbeNetworkName func() string
|
||||
|
||||
MockProxyURL func() *url.URL
|
||||
|
||||
MockResolverIP func() string
|
||||
|
||||
MockSoftwareName func() string
|
||||
|
||||
MockSoftwareVersion func() string
|
||||
|
||||
MockTempDir func() string
|
||||
|
||||
MockTorArgs func() []string
|
||||
|
||||
MockTorBinary func() string
|
||||
|
||||
MockTunnelDir func() string
|
||||
|
||||
MockUserAgent func() string
|
||||
|
||||
MockNewExperimentBuilder func(name string) (model.ExperimentBuilder, error)
|
||||
|
||||
MockNewSubmitter func(ctx context.Context) (model.Submitter, error)
|
||||
|
||||
MockCheckIn func(ctx context.Context,
|
||||
config *model.OOAPICheckInConfig) (*model.OOAPICheckInInfo, error)
|
||||
}
|
||||
|
||||
func (sess *Session) GetTestHelpersByName(name string) ([]model.OOAPIService, bool) {
|
||||
return sess.MockGetTestHelpersByName(name)
|
||||
}
|
||||
|
||||
func (sess *Session) DefaultHTTPClient() model.HTTPClient {
|
||||
return sess.MockDefaultHTTPClient()
|
||||
}
|
||||
|
||||
func (sess *Session) FetchPsiphonConfig(ctx context.Context) ([]byte, error) {
|
||||
return sess.MockFetchPsiphonConfig(ctx)
|
||||
}
|
||||
|
||||
func (sess *Session) FetchTorTargets(
|
||||
ctx context.Context, cc string) (map[string]model.OOAPITorTarget, error) {
|
||||
return sess.MockFetchTorTargets(ctx, cc)
|
||||
}
|
||||
|
||||
func (sess *Session) FetchURLList(
|
||||
ctx context.Context, config model.OOAPIURLListConfig) ([]model.OOAPIURLInfo, error) {
|
||||
return sess.MockFetchURLList(ctx, config)
|
||||
}
|
||||
|
||||
func (sess *Session) KeyValueStore() model.KeyValueStore {
|
||||
return sess.MockKeyValueStore()
|
||||
}
|
||||
|
||||
func (sess *Session) Logger() model.Logger {
|
||||
return sess.MockLogger()
|
||||
}
|
||||
|
||||
func (sess *Session) MaybeResolverIP() string {
|
||||
return sess.MockMaybeResolverIP()
|
||||
}
|
||||
|
||||
func (sess *Session) ProbeASNString() string {
|
||||
return sess.MockProbeASNString()
|
||||
}
|
||||
|
||||
func (sess *Session) ProbeCC() string {
|
||||
return sess.MockProbeCC()
|
||||
}
|
||||
|
||||
func (sess *Session) ProbeIP() string {
|
||||
return sess.MockProbeIP()
|
||||
}
|
||||
|
||||
func (sess *Session) ProbeNetworkName() string {
|
||||
return sess.MockProbeNetworkName()
|
||||
}
|
||||
|
||||
func (sess *Session) ProxyURL() *url.URL {
|
||||
return sess.MockProxyURL()
|
||||
}
|
||||
|
||||
func (sess *Session) ResolverIP() string {
|
||||
return sess.MockResolverIP()
|
||||
}
|
||||
|
||||
func (sess *Session) SoftwareName() string {
|
||||
return sess.MockSoftwareName()
|
||||
}
|
||||
|
||||
func (sess *Session) SoftwareVersion() string {
|
||||
return sess.MockSoftwareVersion()
|
||||
}
|
||||
|
||||
func (sess *Session) TempDir() string {
|
||||
return sess.MockTempDir()
|
||||
}
|
||||
|
||||
func (sess *Session) TorArgs() []string {
|
||||
return sess.MockTorArgs()
|
||||
}
|
||||
|
||||
func (sess *Session) TorBinary() string {
|
||||
return sess.MockTorBinary()
|
||||
}
|
||||
|
||||
func (sess *Session) TunnelDir() string {
|
||||
return sess.MockTunnelDir()
|
||||
}
|
||||
|
||||
func (sess *Session) UserAgent() string {
|
||||
return sess.MockUserAgent()
|
||||
}
|
||||
|
||||
func (sess *Session) NewExperimentBuilder(name string) (model.ExperimentBuilder, error) {
|
||||
return sess.MockNewExperimentBuilder(name)
|
||||
}
|
||||
|
||||
func (sess *Session) NewSubmitter(ctx context.Context) (model.Submitter, error) {
|
||||
return sess.MockNewSubmitter(ctx)
|
||||
}
|
||||
|
||||
func (sess *Session) CheckIn(ctx context.Context,
|
||||
config *model.OOAPICheckInConfig) (*model.OOAPICheckInInfo, error) {
|
||||
return sess.MockCheckIn(ctx, config)
|
||||
}
|
357
internal/model/mocks/session_test.go
Normal file
357
internal/model/mocks/session_test.go
Normal file
|
@ -0,0 +1,357 @@
|
|||
package mocks
|
||||
|
||||
import (
|
||||
"context"
|
||||
"errors"
|
||||
"net/url"
|
||||
"testing"
|
||||
|
||||
"github.com/google/go-cmp/cmp"
|
||||
"github.com/ooni/probe-cli/v3/internal/model"
|
||||
"github.com/ooni/probe-cli/v3/internal/runtimex"
|
||||
"github.com/ooni/probe-cli/v3/internal/testingx"
|
||||
)
|
||||
|
||||
func TestSession(t *testing.T) {
|
||||
t.Run("GetTestHelpersByName", func(t *testing.T) {
|
||||
var expect []model.OOAPIService
|
||||
ff := &testingx.FakeFiller{}
|
||||
ff.Fill(&expect)
|
||||
runtimex.Assert(len(expect) > 0, "expected non-empty array")
|
||||
s := &Session{
|
||||
MockGetTestHelpersByName: func(name string) ([]model.OOAPIService, bool) {
|
||||
return expect, len(expect) > 0
|
||||
},
|
||||
}
|
||||
out, good := s.GetTestHelpersByName("xx")
|
||||
if !good {
|
||||
t.Fatal("not good")
|
||||
}
|
||||
if diff := cmp.Diff(expect, out); diff != "" {
|
||||
t.Fatal(diff)
|
||||
}
|
||||
})
|
||||
|
||||
t.Run("DefaultHTTPClient", func(t *testing.T) {
|
||||
expected := &HTTPClient{}
|
||||
s := &Session{
|
||||
MockDefaultHTTPClient: func() model.HTTPClient {
|
||||
return expected
|
||||
},
|
||||
}
|
||||
out := s.DefaultHTTPClient()
|
||||
if expected != out {
|
||||
t.Fatal("unexpected result")
|
||||
}
|
||||
})
|
||||
|
||||
t.Run("FetchPsiphonConfig", func(t *testing.T) {
|
||||
var expected []byte
|
||||
ff := &testingx.FakeFiller{}
|
||||
ff.Fill(&expected)
|
||||
runtimex.Assert(len(expected) > 0, "expected nonempty list")
|
||||
s := &Session{
|
||||
MockFetchPsiphonConfig: func(ctx context.Context) ([]byte, error) {
|
||||
return expected, nil
|
||||
},
|
||||
}
|
||||
out, err := s.FetchPsiphonConfig(context.Background())
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if diff := cmp.Diff(expected, out); diff != "" {
|
||||
t.Fatal(diff)
|
||||
}
|
||||
})
|
||||
|
||||
t.Run("FetchTorTargets", func(t *testing.T) {
|
||||
expected := errors.New("mocked err")
|
||||
s := &Session{
|
||||
MockFetchTorTargets: func(ctx context.Context, cc string) (map[string]model.OOAPITorTarget, error) {
|
||||
return nil, expected
|
||||
},
|
||||
}
|
||||
out, err := s.FetchTorTargets(context.Background(), "IT")
|
||||
if !errors.Is(err, expected) {
|
||||
t.Fatal("unexpected err", err)
|
||||
}
|
||||
if len(out) > 0 {
|
||||
t.Fatal("expected empty out")
|
||||
}
|
||||
})
|
||||
|
||||
t.Run("FetchURLList", func(t *testing.T) {
|
||||
expected := errors.New("mocked err")
|
||||
s := &Session{
|
||||
MockFetchURLList: func(ctx context.Context, config model.OOAPIURLListConfig) ([]model.OOAPIURLInfo, error) {
|
||||
return nil, expected
|
||||
},
|
||||
}
|
||||
out, err := s.FetchURLList(context.Background(), model.OOAPIURLListConfig{})
|
||||
if !errors.Is(err, expected) {
|
||||
t.Fatal("unexpected err", err)
|
||||
}
|
||||
if len(out) > 0 {
|
||||
t.Fatal("expected empty out")
|
||||
}
|
||||
})
|
||||
|
||||
t.Run("KeyValueStore", func(t *testing.T) {
|
||||
expect := &KeyValueStore{}
|
||||
s := &Session{
|
||||
MockKeyValueStore: func() model.KeyValueStore {
|
||||
return expect
|
||||
},
|
||||
}
|
||||
out := s.KeyValueStore()
|
||||
if out != expect {
|
||||
t.Fatal("invalid output")
|
||||
}
|
||||
})
|
||||
|
||||
t.Run("Logger", func(t *testing.T) {
|
||||
expect := &Logger{}
|
||||
s := &Session{
|
||||
MockLogger: func() model.Logger {
|
||||
return expect
|
||||
},
|
||||
}
|
||||
out := s.Logger()
|
||||
if out != expect {
|
||||
t.Fatal("invalid output")
|
||||
}
|
||||
})
|
||||
|
||||
t.Run("MaybeResolverIP", func(t *testing.T) {
|
||||
expect := "xx"
|
||||
s := &Session{
|
||||
MockMaybeResolverIP: func() string {
|
||||
return expect
|
||||
},
|
||||
}
|
||||
out := s.MaybeResolverIP()
|
||||
if out != expect {
|
||||
t.Fatal("invalid output")
|
||||
}
|
||||
})
|
||||
|
||||
t.Run("ProbeASNString", func(t *testing.T) {
|
||||
expect := "xx"
|
||||
s := &Session{
|
||||
MockProbeASNString: func() string {
|
||||
return expect
|
||||
},
|
||||
}
|
||||
out := s.ProbeASNString()
|
||||
if out != expect {
|
||||
t.Fatal("invalid output")
|
||||
}
|
||||
})
|
||||
|
||||
t.Run("ProbeCC", func(t *testing.T) {
|
||||
expect := "xx"
|
||||
s := &Session{
|
||||
MockProbeCC: func() string {
|
||||
return expect
|
||||
},
|
||||
}
|
||||
out := s.ProbeCC()
|
||||
if out != expect {
|
||||
t.Fatal("invalid output")
|
||||
}
|
||||
})
|
||||
|
||||
t.Run("ProbeIP", func(t *testing.T) {
|
||||
expect := "xx"
|
||||
s := &Session{
|
||||
MockProbeIP: func() string {
|
||||
return expect
|
||||
},
|
||||
}
|
||||
out := s.ProbeIP()
|
||||
if out != expect {
|
||||
t.Fatal("invalid output")
|
||||
}
|
||||
})
|
||||
|
||||
t.Run("ProbeNetworkName", func(t *testing.T) {
|
||||
expect := "xx"
|
||||
s := &Session{
|
||||
MockProbeNetworkName: func() string {
|
||||
return expect
|
||||
},
|
||||
}
|
||||
out := s.ProbeNetworkName()
|
||||
if out != expect {
|
||||
t.Fatal("invalid output")
|
||||
}
|
||||
})
|
||||
|
||||
t.Run("ProxyURL", func(t *testing.T) {
|
||||
expect := &url.URL{Scheme: "xx"}
|
||||
s := &Session{
|
||||
MockProxyURL: func() *url.URL {
|
||||
return expect
|
||||
},
|
||||
}
|
||||
out := s.ProxyURL()
|
||||
if out != expect {
|
||||
t.Fatal("invalid output")
|
||||
}
|
||||
})
|
||||
|
||||
t.Run("ResolverIP", func(t *testing.T) {
|
||||
expect := "xx"
|
||||
s := &Session{
|
||||
MockResolverIP: func() string {
|
||||
return expect
|
||||
},
|
||||
}
|
||||
out := s.ResolverIP()
|
||||
if out != expect {
|
||||
t.Fatal("invalid output")
|
||||
}
|
||||
})
|
||||
|
||||
t.Run("SoftwareName", func(t *testing.T) {
|
||||
expect := "xx"
|
||||
s := &Session{
|
||||
MockSoftwareName: func() string {
|
||||
return expect
|
||||
},
|
||||
}
|
||||
out := s.SoftwareName()
|
||||
if out != expect {
|
||||
t.Fatal("invalid output")
|
||||
}
|
||||
})
|
||||
|
||||
t.Run("SoftwareVersion", func(t *testing.T) {
|
||||
expect := "xx"
|
||||
s := &Session{
|
||||
MockSoftwareVersion: func() string {
|
||||
return expect
|
||||
},
|
||||
}
|
||||
out := s.SoftwareVersion()
|
||||
if out != expect {
|
||||
t.Fatal("invalid output")
|
||||
}
|
||||
})
|
||||
|
||||
t.Run("TempDir", func(t *testing.T) {
|
||||
expect := "xx"
|
||||
s := &Session{
|
||||
MockTempDir: func() string {
|
||||
return expect
|
||||
},
|
||||
}
|
||||
out := s.TempDir()
|
||||
if out != expect {
|
||||
t.Fatal("invalid output")
|
||||
}
|
||||
})
|
||||
|
||||
t.Run("TorArgs", func(t *testing.T) {
|
||||
var expect []string
|
||||
ff := &testingx.FakeFiller{}
|
||||
ff.Fill(&expect)
|
||||
runtimex.Assert(len(expect) > 0, "expected non empty slice")
|
||||
s := &Session{
|
||||
MockTorArgs: func() []string {
|
||||
return expect
|
||||
},
|
||||
}
|
||||
out := s.TorArgs()
|
||||
if diff := cmp.Diff(expect, out); diff != "" {
|
||||
t.Fatal(diff)
|
||||
}
|
||||
})
|
||||
|
||||
t.Run("TorBinary", func(t *testing.T) {
|
||||
expect := "xx"
|
||||
s := &Session{
|
||||
MockTorBinary: func() string {
|
||||
return expect
|
||||
},
|
||||
}
|
||||
out := s.TorBinary()
|
||||
if out != expect {
|
||||
t.Fatal("invalid output")
|
||||
}
|
||||
})
|
||||
|
||||
t.Run("TunnelDir", func(t *testing.T) {
|
||||
expect := "xx"
|
||||
s := &Session{
|
||||
MockTunnelDir: func() string {
|
||||
return expect
|
||||
},
|
||||
}
|
||||
out := s.TunnelDir()
|
||||
if out != expect {
|
||||
t.Fatal("invalid output")
|
||||
}
|
||||
})
|
||||
|
||||
t.Run("UserAgent", func(t *testing.T) {
|
||||
expect := "xx"
|
||||
s := &Session{
|
||||
MockUserAgent: func() string {
|
||||
return expect
|
||||
},
|
||||
}
|
||||
out := s.UserAgent()
|
||||
if out != expect {
|
||||
t.Fatal("invalid output")
|
||||
}
|
||||
})
|
||||
|
||||
t.Run("NewExperimentBuilder", func(t *testing.T) {
|
||||
eb := &ExperimentBuilder{}
|
||||
s := &Session{
|
||||
MockNewExperimentBuilder: func(name string) (model.ExperimentBuilder, error) {
|
||||
return eb, nil
|
||||
},
|
||||
}
|
||||
out, err := s.NewExperimentBuilder("x")
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if out != eb {
|
||||
t.Fatal("invalid output")
|
||||
}
|
||||
})
|
||||
|
||||
t.Run("NewSubmitter", func(t *testing.T) {
|
||||
expected := errors.New("mocked err")
|
||||
s := &Session{
|
||||
MockNewSubmitter: func(ctx context.Context) (model.Submitter, error) {
|
||||
return nil, expected
|
||||
},
|
||||
}
|
||||
out, err := s.NewSubmitter(context.Background())
|
||||
if !errors.Is(err, expected) {
|
||||
t.Fatal("unexpected err")
|
||||
}
|
||||
if out != nil {
|
||||
t.Fatal("unexpected out")
|
||||
}
|
||||
})
|
||||
|
||||
t.Run("CheckIn", func(t *testing.T) {
|
||||
expected := errors.New("mocked err")
|
||||
s := &Session{
|
||||
MockCheckIn: func(ctx context.Context, config *model.OOAPICheckInConfig) (*model.OOAPICheckInInfo, error) {
|
||||
return nil, expected
|
||||
},
|
||||
}
|
||||
out, err := s.CheckIn(context.Background(), &model.OOAPICheckInConfig{})
|
||||
if !errors.Is(err, expected) {
|
||||
t.Fatal("unexpected err")
|
||||
}
|
||||
if out != nil {
|
||||
t.Fatal("unexpected out")
|
||||
}
|
||||
})
|
||||
}
|
17
internal/model/mocks/submitter.go
Normal file
17
internal/model/mocks/submitter.go
Normal file
|
@ -0,0 +1,17 @@
|
|||
package mocks
|
||||
|
||||
import (
|
||||
"context"
|
||||
|
||||
"github.com/ooni/probe-cli/v3/internal/model"
|
||||
)
|
||||
|
||||
// Submitter mocks model.Submitter.
|
||||
type Submitter struct {
|
||||
MockSubmit func(ctx context.Context, m *model.Measurement) error
|
||||
}
|
||||
|
||||
// Submit calls MockSubmit
|
||||
func (s *Submitter) Submit(ctx context.Context, m *model.Measurement) error {
|
||||
return s.MockSubmit(ctx, m)
|
||||
}
|
24
internal/model/mocks/submitter_test.go
Normal file
24
internal/model/mocks/submitter_test.go
Normal file
|
@ -0,0 +1,24 @@
|
|||
package mocks
|
||||
|
||||
import (
|
||||
"context"
|
||||
"errors"
|
||||
"testing"
|
||||
|
||||
"github.com/ooni/probe-cli/v3/internal/model"
|
||||
)
|
||||
|
||||
func TestSubmitter(t *testing.T) {
|
||||
t.Run("Submit", func(t *testing.T) {
|
||||
expect := errors.New("mocked error")
|
||||
s := &Submitter{
|
||||
MockSubmit: func(ctx context.Context, m *model.Measurement) error {
|
||||
return expect
|
||||
},
|
||||
}
|
||||
err := s.Submit(context.Background(), &model.Measurement{})
|
||||
if !errors.Is(err, expect) {
|
||||
t.Fatal("unexpected err", err)
|
||||
}
|
||||
})
|
||||
}
|
|
@ -613,9 +613,9 @@ func dnsGenLookupHostReplySuccess(rawQuery []byte, cname *dnsCNAMEAnswer, ips ..
|
|||
query := new(dns.Msg)
|
||||
err := query.Unpack(rawQuery)
|
||||
runtimex.PanicOnError(err, "query.Unpack failed")
|
||||
runtimex.PanicIfFalse(len(query.Question) == 1, "more than one question")
|
||||
runtimex.Assert(len(query.Question) == 1, "more than one question")
|
||||
question := query.Question[0]
|
||||
runtimex.PanicIfFalse(
|
||||
runtimex.Assert(
|
||||
question.Qtype == dns.TypeA || question.Qtype == dns.TypeAAAA,
|
||||
"invalid query type (expected A or AAAA)",
|
||||
)
|
||||
|
@ -669,9 +669,9 @@ func dnsGenHTTPSReplySuccess(rawQuery []byte, alpns, ipv4s, ipv6s []string) []by
|
|||
query := new(dns.Msg)
|
||||
err := query.Unpack(rawQuery)
|
||||
runtimex.PanicOnError(err, "query.Unpack failed")
|
||||
runtimex.PanicIfFalse(len(query.Question) == 1, "expected just a single question")
|
||||
runtimex.Assert(len(query.Question) == 1, "expected just a single question")
|
||||
question := query.Question[0]
|
||||
runtimex.PanicIfFalse(question.Qtype == dns.TypeHTTPS, "expected HTTPS query")
|
||||
runtimex.Assert(question.Qtype == dns.TypeHTTPS, "expected HTTPS query")
|
||||
reply := new(dns.Msg)
|
||||
reply.Compress = true
|
||||
reply.MsgHdr.RecursionAvailable = true
|
||||
|
@ -716,9 +716,9 @@ func dnsGenNSReplySuccess(rawQuery []byte, names ...string) []byte {
|
|||
query := new(dns.Msg)
|
||||
err := query.Unpack(rawQuery)
|
||||
runtimex.PanicOnError(err, "query.Unpack failed")
|
||||
runtimex.PanicIfFalse(len(query.Question) == 1, "more than one question")
|
||||
runtimex.Assert(len(query.Question) == 1, "more than one question")
|
||||
question := query.Question[0]
|
||||
runtimex.PanicIfFalse(question.Qtype == dns.TypeNS, "expected NS query")
|
||||
runtimex.Assert(question.Qtype == dns.TypeNS, "expected NS query")
|
||||
reply := new(dns.Msg)
|
||||
reply.Compress = true
|
||||
reply.MsgHdr.RecursionAvailable = true
|
||||
|
|
|
@ -104,7 +104,7 @@ func NewDefaultCertPool() *x509.CertPool {
|
|||
// Assumption: AppendCertsFromPEM cannot fail because we
|
||||
// have a test in certify_test.go that guarantees that
|
||||
ok := pool.AppendCertsFromPEM([]byte(pemcerts))
|
||||
runtimex.PanicIfFalse(ok, "pool.AppendCertsFromPEM failed")
|
||||
runtimex.Assert(ok, "pool.AppendCertsFromPEM failed")
|
||||
return pool
|
||||
}
|
||||
|
||||
|
|
|
@ -11,11 +11,15 @@ import (
|
|||
"strings"
|
||||
"time"
|
||||
|
||||
"github.com/ooni/probe-cli/v3/internal/atomicx"
|
||||
"github.com/ooni/probe-cli/v3/internal/engine"
|
||||
"github.com/ooni/probe-cli/v3/internal/humanize"
|
||||
"github.com/ooni/probe-cli/v3/internal/model"
|
||||
)
|
||||
|
||||
// experimentShuffledInputs counts how many times we shuffled inputs
|
||||
var experimentShuffledInputs = &atomicx.Int64{}
|
||||
|
||||
// Experiment describes an experiment to run. You MUST fill all the fields that
|
||||
// are marked as MANDATORY, otherwise Experiment.Run will cause panics.
|
||||
type Experiment struct {
|
||||
|
@ -52,6 +56,22 @@ type Experiment struct {
|
|||
|
||||
// Session is the MANDATORY session.
|
||||
Session Session
|
||||
|
||||
// newExperimentBuilderFn is OPTIONAL and used for testing.
|
||||
newExperimentBuilderFn func(experimentName string) (model.ExperimentBuilder, error)
|
||||
|
||||
// newInputLoaderFn is OPTIONAL and used for testing.
|
||||
newInputLoaderFn func(inputPolicy model.InputPolicy) inputLoader
|
||||
|
||||
// newSubmitterFn is OPTIONAL and used for testing.
|
||||
newSubmitterFn func(ctx context.Context) (engine.Submitter, error)
|
||||
|
||||
// newSaverFn is OPTIONAL and used for testing.
|
||||
newSaverFn func(experiment model.Experiment) (engine.Saver, error)
|
||||
|
||||
// newInputProcessorFn is OPTIONAL and used for testing.
|
||||
newInputProcessorFn func(experiment model.Experiment, inputList []model.OOAPIURLInfo,
|
||||
saver engine.Saver, submitter engine.Submitter) inputProcessor
|
||||
}
|
||||
|
||||
// Run runs the given experiment.
|
||||
|
@ -76,6 +96,7 @@ func (ed *Experiment) Run(ctx context.Context) error {
|
|||
rnd.Shuffle(len(inputList), func(i, j int) {
|
||||
inputList[i], inputList[j] = inputList[j], inputList[i]
|
||||
})
|
||||
experimentShuffledInputs.Add(1)
|
||||
}
|
||||
|
||||
// 4. configure experiment's options
|
||||
|
@ -112,14 +133,15 @@ func (ed *Experiment) Run(ctx context.Context) error {
|
|||
return inputProcessor.Run(ctx)
|
||||
}
|
||||
|
||||
// inputProcessor processes inputs running the given experiment.
|
||||
type inputProcessor interface {
|
||||
Run(ctx context.Context) error
|
||||
}
|
||||
// inputProcessor is an alias for model.ExperimentInputProcessor
|
||||
type inputProcessor = model.ExperimentInputProcessor
|
||||
|
||||
// newInputProcessor creates a new inputProcessor instance.
|
||||
func (ed *Experiment) newInputProcessor(experiment model.Experiment,
|
||||
inputList []model.OOAPIURLInfo, saver engine.Saver, submitter engine.Submitter) inputProcessor {
|
||||
if ed.newInputProcessorFn != nil {
|
||||
return ed.newInputProcessorFn(experiment, inputList, saver, submitter)
|
||||
}
|
||||
return &engine.InputProcessor{
|
||||
Annotations: ed.Annotations,
|
||||
Experiment: &experimentWrapper{
|
||||
|
@ -140,6 +162,9 @@ func (ed *Experiment) newInputProcessor(experiment model.Experiment,
|
|||
|
||||
// newSaver creates a new engine.Saver instance.
|
||||
func (ed *Experiment) newSaver(experiment model.Experiment) (engine.Saver, error) {
|
||||
if ed.newSaverFn != nil {
|
||||
return ed.newSaverFn(experiment)
|
||||
}
|
||||
return engine.NewSaver(engine.SaverConfig{
|
||||
Enabled: !ed.NoJSON,
|
||||
Experiment: experiment,
|
||||
|
@ -150,6 +175,9 @@ func (ed *Experiment) newSaver(experiment model.Experiment) (engine.Saver, error
|
|||
|
||||
// newSubmitter creates a new engine.Submitter instance.
|
||||
func (ed *Experiment) newSubmitter(ctx context.Context) (engine.Submitter, error) {
|
||||
if ed.newSubmitterFn != nil {
|
||||
return ed.newSubmitterFn(ctx)
|
||||
}
|
||||
return engine.NewSubmitter(ctx, engine.SubmitterConfig{
|
||||
Enabled: !ed.NoCollector,
|
||||
Session: ed.Session,
|
||||
|
@ -159,16 +187,20 @@ 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) (model.ExperimentBuilder, error) {
|
||||
if ed.newExperimentBuilderFn != nil {
|
||||
return ed.newExperimentBuilderFn(experimentName)
|
||||
}
|
||||
return ed.Session.NewExperimentBuilder(ed.Name)
|
||||
}
|
||||
|
||||
// inputLoader loads inputs from local or remote sources.
|
||||
type inputLoader interface {
|
||||
Load(ctx context.Context) ([]model.OOAPIURLInfo, error)
|
||||
}
|
||||
// inputLoader is an alias for model.ExperimentInputLoader
|
||||
type inputLoader = model.ExperimentInputLoader
|
||||
|
||||
// newInputLoader creates a new inputLoader.
|
||||
func (ed *Experiment) newInputLoader(inputPolicy model.InputPolicy) inputLoader {
|
||||
if ed.newInputLoaderFn != nil {
|
||||
return ed.newInputLoaderFn(inputPolicy)
|
||||
}
|
||||
return &engine.InputLoader{
|
||||
CheckInConfig: &model.OOAPICheckInConfig{
|
||||
RunType: model.RunTypeManual,
|
||||
|
|
|
@ -2,42 +2,24 @@ package oonirun
|
|||
|
||||
import (
|
||||
"context"
|
||||
"os"
|
||||
"errors"
|
||||
"reflect"
|
||||
"sort"
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
"github.com/ooni/probe-cli/v3/internal/engine"
|
||||
"github.com/ooni/probe-cli/v3/internal/kvstore"
|
||||
"github.com/ooni/probe-cli/v3/internal/model"
|
||||
"github.com/ooni/probe-cli/v3/internal/version"
|
||||
"github.com/ooni/probe-cli/v3/internal/model/mocks"
|
||||
"github.com/ooni/probe-cli/v3/internal/testingx"
|
||||
)
|
||||
|
||||
// TODO(bassosimone): it would be cool to write unit tests. However, to do that
|
||||
// we need to ~redesign the engine package for unit-testability.
|
||||
|
||||
func newSession(ctx context.Context, t *testing.T) *engine.Session {
|
||||
config := engine.SessionConfig{
|
||||
AvailableProbeServices: []model.OOAPIService{},
|
||||
KVStore: &kvstore.Memory{},
|
||||
Logger: model.DiscardLogger,
|
||||
ProxyURL: nil,
|
||||
SoftwareName: "miniooni",
|
||||
SoftwareVersion: version.Version,
|
||||
TempDir: os.TempDir(),
|
||||
TorArgs: []string{},
|
||||
TorBinary: "",
|
||||
TunnelDir: "",
|
||||
}
|
||||
sess, err := engine.NewSession(ctx, config)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
return sess
|
||||
}
|
||||
|
||||
func TestExperimentRunWithExample(t *testing.T) {
|
||||
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{
|
||||
|
@ -46,19 +28,89 @@ func TestExperimentRunWithExample(t *testing.T) {
|
|||
ExtraOptions: map[string]any{
|
||||
"SleepTime": int64(10 * time.Millisecond),
|
||||
},
|
||||
Inputs: []string{},
|
||||
Inputs: []string{
|
||||
"a", "b", "c",
|
||||
},
|
||||
InputFilePaths: []string{},
|
||||
MaxRuntime: 0,
|
||||
Name: "example",
|
||||
NoCollector: true,
|
||||
NoJSON: true,
|
||||
Random: false,
|
||||
Random: true, // to test randomness
|
||||
ReportFile: "",
|
||||
Session: newSession(ctx, t),
|
||||
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) {
|
||||
|
@ -102,3 +154,257 @@ func Test_experimentOptionsToStringList(t *testing.T) {
|
|||
})
|
||||
}
|
||||
}
|
||||
|
||||
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)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
|
|
@ -8,6 +8,7 @@ import (
|
|||
"context"
|
||||
"encoding/json"
|
||||
"errors"
|
||||
"fmt"
|
||||
"net/url"
|
||||
)
|
||||
|
||||
|
@ -50,7 +51,7 @@ func v1Measure(ctx context.Context, config *LinkConfig, URL string) error {
|
|||
if pu.Host != "nettest" {
|
||||
return ErrInvalidV1URLHost
|
||||
}
|
||||
if pu.Path != "" {
|
||||
if pu.Path != "" && pu.Path != "/" {
|
||||
return ErrInvalidV1URLPath
|
||||
}
|
||||
default:
|
||||
|
@ -58,33 +59,50 @@ func v1Measure(ctx context.Context, config *LinkConfig, URL string) error {
|
|||
}
|
||||
name := pu.Query().Get("tn")
|
||||
if name == "" {
|
||||
return ErrInvalidV1URLQueryArgument
|
||||
return fmt.Errorf("%w: empty test name", ErrInvalidV1URLQueryArgument)
|
||||
}
|
||||
var inputs []string
|
||||
if ra := pu.Query().Get("ta"); ra != "" {
|
||||
pa, err := url.QueryUnescape(ra)
|
||||
if ta := pu.Query().Get("ta"); ta != "" {
|
||||
inputs, err = v1ParseArguments(ta)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
var arguments v1Arguments
|
||||
if err := json.Unmarshal([]byte(pa), &arguments); err != nil {
|
||||
return err
|
||||
}
|
||||
inputs = arguments.URLs
|
||||
}
|
||||
// TODO(bassosimone): reject mv < 1.2.0
|
||||
if mv := pu.Query().Get("mv"); mv != "1.2.0" {
|
||||
return fmt.Errorf("%w: unknown minimum version", ErrInvalidV1URLQueryArgument)
|
||||
}
|
||||
exp := &Experiment{
|
||||
Annotations: config.Annotations,
|
||||
ExtraOptions: nil, // no way to specify with v1 URLs
|
||||
Inputs: inputs,
|
||||
InputFilePaths: nil,
|
||||
MaxRuntime: config.MaxRuntime,
|
||||
Name: name,
|
||||
NoCollector: config.NoCollector,
|
||||
NoJSON: config.NoJSON,
|
||||
Random: config.Random,
|
||||
ReportFile: config.ReportFile,
|
||||
Session: config.Session,
|
||||
Annotations: config.Annotations,
|
||||
ExtraOptions: nil, // no way to specify with v1 URLs
|
||||
Inputs: inputs,
|
||||
InputFilePaths: nil,
|
||||
MaxRuntime: config.MaxRuntime,
|
||||
Name: name,
|
||||
NoCollector: config.NoCollector,
|
||||
NoJSON: config.NoJSON,
|
||||
Random: config.Random,
|
||||
ReportFile: config.ReportFile,
|
||||
Session: config.Session,
|
||||
newExperimentBuilderFn: nil,
|
||||
newInputLoaderFn: nil,
|
||||
newSubmitterFn: nil,
|
||||
newSaverFn: nil,
|
||||
newInputProcessorFn: nil,
|
||||
}
|
||||
return exp.Run(ctx)
|
||||
}
|
||||
|
||||
// v1ParseArguments parses the `ta` field of the query string.
|
||||
func v1ParseArguments(ta string) ([]string, error) {
|
||||
var inputs []string
|
||||
pa, err := url.QueryUnescape(ta)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("%w: %s", ErrInvalidV1URLQueryArgument, err.Error())
|
||||
}
|
||||
var arguments v1Arguments
|
||||
if err := json.Unmarshal([]byte(pa), &arguments); err != nil {
|
||||
return nil, fmt.Errorf("%w: %s", ErrInvalidV1URLQueryArgument, err.Error())
|
||||
}
|
||||
inputs = arguments.URLs
|
||||
return inputs, nil
|
||||
}
|
||||
|
|
|
@ -2,13 +2,60 @@ package oonirun
|
|||
|
||||
import (
|
||||
"context"
|
||||
"errors"
|
||||
"net/http"
|
||||
"strings"
|
||||
"testing"
|
||||
|
||||
"github.com/ooni/probe-cli/v3/internal/kvstore"
|
||||
"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"
|
||||
)
|
||||
|
||||
// TODO(bassosimone): it would be cool to write unit tests. However, to do that
|
||||
// we need to ~redesign the engine package for unit-testability.
|
||||
func newMinimalFakeSession() *mocks.Session {
|
||||
return &mocks.Session{
|
||||
MockLogger: func() model.Logger {
|
||||
return model.DiscardLogger
|
||||
},
|
||||
MockNewExperimentBuilder: func(name string) (model.ExperimentBuilder, error) {
|
||||
eb := &mocks.ExperimentBuilder{
|
||||
MockInputPolicy: func() model.InputPolicy {
|
||||
return model.InputNone
|
||||
},
|
||||
MockSetOptionsAny: func(options map[string]any) error {
|
||||
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 {
|
||||
return 1.1
|
||||
},
|
||||
MockKibiBytesSent: func() float64 {
|
||||
return 0.1
|
||||
},
|
||||
}
|
||||
return exp
|
||||
},
|
||||
}
|
||||
return eb, nil
|
||||
},
|
||||
MockDefaultHTTPClient: func() model.HTTPClient {
|
||||
return http.DefaultClient
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
func TestOONIRunV1Link(t *testing.T) {
|
||||
ctx := context.Background()
|
||||
|
@ -23,7 +70,7 @@ func TestOONIRunV1Link(t *testing.T) {
|
|||
NoJSON: true,
|
||||
Random: false,
|
||||
ReportFile: "",
|
||||
Session: newSession(ctx, t),
|
||||
Session: newMinimalFakeSession(),
|
||||
}
|
||||
r := NewLinkRunner(config, "https://run.ooni.io/nettest?tn=example&mv=1.2.0")
|
||||
if err := r.Run(ctx); err != nil {
|
||||
|
@ -34,3 +81,188 @@ func TestOONIRunV1Link(t *testing.T) {
|
|||
t.Fatal(err)
|
||||
}
|
||||
}
|
||||
|
||||
func TestV1MeasureInvalidURL(t *testing.T) {
|
||||
t.Run("URL does not parse", func(t *testing.T) {
|
||||
ctx := context.Background()
|
||||
config := &LinkConfig{
|
||||
Session: &mocks.Session{
|
||||
MockLogger: func() model.Logger {
|
||||
return model.DiscardLogger
|
||||
},
|
||||
},
|
||||
}
|
||||
URL := "\t"
|
||||
err := v1Measure(ctx, config, URL)
|
||||
if err == nil || !strings.HasSuffix(err.Error(), "invalid control character in URL") {
|
||||
t.Fatal("unexpected err", err)
|
||||
}
|
||||
})
|
||||
|
||||
t.Run("with https:// URL and invalid hostname", func(t *testing.T) {
|
||||
ctx := context.Background()
|
||||
config := &LinkConfig{
|
||||
Session: &mocks.Session{
|
||||
MockLogger: func() model.Logger {
|
||||
return model.DiscardLogger
|
||||
},
|
||||
},
|
||||
}
|
||||
URL := "https://run.ooni.nu/nettest"
|
||||
err := v1Measure(ctx, config, URL)
|
||||
if !errors.Is(err, ErrInvalidV1URLHost) {
|
||||
t.Fatal("unexpected err", err)
|
||||
}
|
||||
})
|
||||
|
||||
t.Run("with https:// URL and invalid path", func(t *testing.T) {
|
||||
ctx := context.Background()
|
||||
config := &LinkConfig{
|
||||
Session: &mocks.Session{
|
||||
MockLogger: func() model.Logger {
|
||||
return model.DiscardLogger
|
||||
},
|
||||
},
|
||||
}
|
||||
URL := "https://run.ooni.io/antani"
|
||||
err := v1Measure(ctx, config, URL)
|
||||
if !errors.Is(err, ErrInvalidV1URLPath) {
|
||||
t.Fatal("unexpected err", err)
|
||||
}
|
||||
})
|
||||
|
||||
t.Run("with ooni:// URL and invalid host", func(t *testing.T) {
|
||||
ctx := context.Background()
|
||||
config := &LinkConfig{
|
||||
Session: &mocks.Session{
|
||||
MockLogger: func() model.Logger {
|
||||
return model.DiscardLogger
|
||||
},
|
||||
},
|
||||
}
|
||||
URL := "ooni://antani"
|
||||
err := v1Measure(ctx, config, URL)
|
||||
if !errors.Is(err, ErrInvalidV1URLHost) {
|
||||
t.Fatal("unexpected err", err)
|
||||
}
|
||||
})
|
||||
|
||||
t.Run("with ooni:// URL and path", func(t *testing.T) {
|
||||
ctx := context.Background()
|
||||
config := &LinkConfig{
|
||||
Session: &mocks.Session{
|
||||
MockLogger: func() model.Logger {
|
||||
return model.DiscardLogger
|
||||
},
|
||||
},
|
||||
}
|
||||
URL := "ooni://nettest/x"
|
||||
err := v1Measure(ctx, config, URL)
|
||||
if !errors.Is(err, ErrInvalidV1URLPath) {
|
||||
t.Fatal("unexpected err", err)
|
||||
}
|
||||
})
|
||||
|
||||
t.Run("with invalid URL scheme", func(t *testing.T) {
|
||||
ctx := context.Background()
|
||||
config := &LinkConfig{
|
||||
Session: &mocks.Session{
|
||||
MockLogger: func() model.Logger {
|
||||
return model.DiscardLogger
|
||||
},
|
||||
},
|
||||
}
|
||||
URL := "antani://nettest"
|
||||
err := v1Measure(ctx, config, URL)
|
||||
if !errors.Is(err, ErrInvalidV1URLScheme) {
|
||||
t.Fatal("unexpected err", err)
|
||||
}
|
||||
})
|
||||
|
||||
t.Run("with empty test name", func(t *testing.T) {
|
||||
ctx := context.Background()
|
||||
config := &LinkConfig{
|
||||
Session: &mocks.Session{
|
||||
MockLogger: func() model.Logger {
|
||||
return model.DiscardLogger
|
||||
},
|
||||
},
|
||||
}
|
||||
URL := "ooni://nettest/"
|
||||
err := v1Measure(ctx, config, URL)
|
||||
if !errors.Is(err, ErrInvalidV1URLQueryArgument) {
|
||||
t.Fatal("unexpected err", err)
|
||||
}
|
||||
})
|
||||
|
||||
t.Run("with invalid JSON and explicit / as path", func(t *testing.T) {
|
||||
ctx := context.Background()
|
||||
config := &LinkConfig{
|
||||
Session: &mocks.Session{
|
||||
MockLogger: func() model.Logger {
|
||||
return model.DiscardLogger
|
||||
},
|
||||
},
|
||||
}
|
||||
URL := "ooni://nettest/?tn=web_connectivity&ta=123x"
|
||||
err := v1Measure(ctx, config, URL)
|
||||
if !errors.Is(err, ErrInvalidV1URLQueryArgument) {
|
||||
t.Fatal("unexpected err", err)
|
||||
}
|
||||
})
|
||||
|
||||
t.Run("with invalid JSON and empty path", func(t *testing.T) {
|
||||
ctx := context.Background()
|
||||
config := &LinkConfig{
|
||||
Session: &mocks.Session{
|
||||
MockLogger: func() model.Logger {
|
||||
return model.DiscardLogger
|
||||
},
|
||||
},
|
||||
}
|
||||
URL := "ooni://nettest?tn=web_connectivity&ta=123x"
|
||||
err := v1Measure(ctx, config, URL)
|
||||
if !errors.Is(err, ErrInvalidV1URLQueryArgument) {
|
||||
t.Fatal("unexpected err", err)
|
||||
}
|
||||
})
|
||||
|
||||
t.Run("with missing minimum version", func(t *testing.T) {
|
||||
ctx := context.Background()
|
||||
config := &LinkConfig{
|
||||
Session: &mocks.Session{
|
||||
MockLogger: func() model.Logger {
|
||||
return model.DiscardLogger
|
||||
},
|
||||
},
|
||||
}
|
||||
URL := "ooni://nettest?tn=example"
|
||||
err := v1Measure(ctx, config, URL)
|
||||
if !errors.Is(err, ErrInvalidV1URLQueryArgument) {
|
||||
t.Fatal("unexpected err", err)
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
func TestV1ParseArguments(t *testing.T) {
|
||||
t.Run("with invalid test arguments", func(t *testing.T) {
|
||||
// "[QueryUnescape] returns an error if any % is not followed by two hexadecimal digits."
|
||||
out, err := v1ParseArguments("%KK")
|
||||
if !errors.Is(err, ErrInvalidV1URLQueryArgument) {
|
||||
t.Fatal("unexpected err", err)
|
||||
}
|
||||
if len(out) > 0 {
|
||||
t.Fatal("expected no output")
|
||||
}
|
||||
})
|
||||
|
||||
t.Run("with valid arguments", func(t *testing.T) {
|
||||
out, err := v1ParseArguments("%7B%22urls%22%3A%5B%22https%3A%2F%2Fexample.com%2F%22%5D%7D")
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if len(out) != 1 || out[0] != "https://example.com/" {
|
||||
t.Fatal("unexpected out", out)
|
||||
}
|
||||
})
|
||||
}
|
||||
|
|
|
@ -24,6 +24,10 @@ var (
|
|||
// v2CountEmptyNettestNames counts the number of cases in which we have been
|
||||
// given an empty nettest name, which is useful for testing.
|
||||
v2CountEmptyNettestNames = &atomicx.Int64{}
|
||||
|
||||
// v2CountFailedExperiments countes the number of failed experiments
|
||||
// and is useful when testing this package
|
||||
v2CountFailedExperiments = &atomicx.Int64{}
|
||||
)
|
||||
|
||||
// v2Descriptor describes a single nettest to run.
|
||||
|
@ -172,20 +176,26 @@ func v2MeasureDescriptor(ctx context.Context, config *LinkConfig, desc *v2Descri
|
|||
continue
|
||||
}
|
||||
exp := &Experiment{
|
||||
Annotations: config.Annotations,
|
||||
ExtraOptions: nettest.Options,
|
||||
Inputs: nettest.Inputs,
|
||||
InputFilePaths: nil,
|
||||
MaxRuntime: config.MaxRuntime,
|
||||
Name: nettest.TestName,
|
||||
NoCollector: config.NoCollector,
|
||||
NoJSON: config.NoJSON,
|
||||
Random: config.Random,
|
||||
ReportFile: config.ReportFile,
|
||||
Session: config.Session,
|
||||
Annotations: config.Annotations,
|
||||
ExtraOptions: nettest.Options,
|
||||
Inputs: nettest.Inputs,
|
||||
InputFilePaths: nil,
|
||||
MaxRuntime: config.MaxRuntime,
|
||||
Name: nettest.TestName,
|
||||
NoCollector: config.NoCollector,
|
||||
NoJSON: config.NoJSON,
|
||||
Random: config.Random,
|
||||
ReportFile: config.ReportFile,
|
||||
Session: config.Session,
|
||||
newExperimentBuilderFn: nil,
|
||||
newInputLoaderFn: nil,
|
||||
newSubmitterFn: nil,
|
||||
newSaverFn: nil,
|
||||
newInputProcessorFn: nil,
|
||||
}
|
||||
if err := exp.Run(ctx); err != nil {
|
||||
logger.Warnf("cannot run experiment: %s", err.Error())
|
||||
v2CountFailedExperiments.Add(1)
|
||||
continue
|
||||
}
|
||||
}
|
||||
|
|
|
@ -10,13 +10,11 @@ import (
|
|||
"time"
|
||||
|
||||
"github.com/ooni/probe-cli/v3/internal/kvstore"
|
||||
"github.com/ooni/probe-cli/v3/internal/model"
|
||||
"github.com/ooni/probe-cli/v3/internal/model/mocks"
|
||||
"github.com/ooni/probe-cli/v3/internal/runtimex"
|
||||
)
|
||||
|
||||
// TODO(bassosimone): it would be cool to write unit tests. However, to do that
|
||||
// we need to ~redesign the engine package for unit-testability.
|
||||
|
||||
func TestOONIRunV2LinkCommonCase(t *testing.T) {
|
||||
server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||
descriptor := &v2Descriptor{
|
||||
|
@ -48,7 +46,7 @@ func TestOONIRunV2LinkCommonCase(t *testing.T) {
|
|||
NoJSON: true,
|
||||
Random: false,
|
||||
ReportFile: "",
|
||||
Session: newSession(ctx, t),
|
||||
Session: newMinimalFakeSession(),
|
||||
}
|
||||
r := NewLinkRunner(config, server.URL)
|
||||
if err := r.Run(ctx); err != nil {
|
||||
|
@ -95,7 +93,7 @@ func TestOONIRunV2LinkCannotUpdateCache(t *testing.T) {
|
|||
NoJSON: true,
|
||||
Random: false,
|
||||
ReportFile: "",
|
||||
Session: newSession(ctx, t),
|
||||
Session: newMinimalFakeSession(),
|
||||
}
|
||||
r := NewLinkRunner(config, server.URL)
|
||||
err := r.Run(ctx)
|
||||
|
@ -135,7 +133,7 @@ func TestOONIRunV2LinkWithoutAcceptChanges(t *testing.T) {
|
|||
NoJSON: true,
|
||||
Random: false,
|
||||
ReportFile: "",
|
||||
Session: newSession(ctx, t),
|
||||
Session: newMinimalFakeSession(),
|
||||
}
|
||||
r := NewLinkRunner(config, server.URL)
|
||||
err := r.Run(ctx)
|
||||
|
@ -161,7 +159,7 @@ func TestOONIRunV2LinkNilDescriptor(t *testing.T) {
|
|||
NoJSON: true,
|
||||
Random: false,
|
||||
ReportFile: "",
|
||||
Session: newSession(ctx, t),
|
||||
Session: newMinimalFakeSession(),
|
||||
}
|
||||
r := NewLinkRunner(config, server.URL)
|
||||
if err := r.Run(ctx); err != nil {
|
||||
|
@ -170,6 +168,7 @@ func TestOONIRunV2LinkNilDescriptor(t *testing.T) {
|
|||
}
|
||||
|
||||
func TestOONIRunV2LinkEmptyTestName(t *testing.T) {
|
||||
emptyTestNamesPrev := v2CountEmptyNettestNames.Load()
|
||||
server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||
descriptor := &v2Descriptor{
|
||||
Name: "",
|
||||
|
@ -200,14 +199,14 @@ func TestOONIRunV2LinkEmptyTestName(t *testing.T) {
|
|||
NoJSON: true,
|
||||
Random: false,
|
||||
ReportFile: "",
|
||||
Session: newSession(ctx, t),
|
||||
Session: newMinimalFakeSession(),
|
||||
}
|
||||
r := NewLinkRunner(config, server.URL)
|
||||
if err := r.Run(ctx); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if v2CountEmptyNettestNames.Load() != 1 {
|
||||
t.Fatal("expected to see 1 instance of empty nettest names")
|
||||
if v2CountEmptyNettestNames.Load() != emptyTestNamesPrev+1 {
|
||||
t.Fatal("expected to see 1 more instance of empty nettest names")
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -220,6 +219,74 @@ func TestV2MeasureDescriptor(t *testing.T) {
|
|||
t.Fatal("unexpected err", err)
|
||||
}
|
||||
})
|
||||
|
||||
t.Run("with failing experiment", func(t *testing.T) {
|
||||
previousFailedExperiments := v2CountFailedExperiments.Load()
|
||||
expected := errors.New("mocked error")
|
||||
ctx := context.Background()
|
||||
sess := newMinimalFakeSession()
|
||||
sess.MockNewSubmitter = func(ctx context.Context) (model.Submitter, error) {
|
||||
subm := &mocks.Submitter{
|
||||
MockSubmit: func(ctx context.Context, m *model.Measurement) error {
|
||||
panic("should not be called")
|
||||
},
|
||||
}
|
||||
return subm, nil
|
||||
}
|
||||
sess.MockNewExperimentBuilder = func(name string) (model.ExperimentBuilder, error) {
|
||||
eb := &mocks.ExperimentBuilder{
|
||||
MockInputPolicy: func() model.InputPolicy {
|
||||
return model.InputNone
|
||||
},
|
||||
MockSetOptionsAny: func(options map[string]any) error {
|
||||
return nil
|
||||
},
|
||||
MockNewExperiment: func() model.Experiment {
|
||||
exp := &mocks.Experiment{
|
||||
MockMeasureAsync: func(ctx context.Context, input string) (<-chan *model.Measurement, error) {
|
||||
return nil, expected
|
||||
},
|
||||
MockKibiBytesReceived: func() float64 {
|
||||
return 1.1
|
||||
},
|
||||
MockKibiBytesSent: func() float64 {
|
||||
return 0.1
|
||||
},
|
||||
}
|
||||
return exp
|
||||
},
|
||||
}
|
||||
return eb, nil
|
||||
}
|
||||
config := &LinkConfig{
|
||||
AcceptChanges: false,
|
||||
Annotations: map[string]string{},
|
||||
KVStore: nil,
|
||||
MaxRuntime: 0,
|
||||
NoCollector: false,
|
||||
NoJSON: false,
|
||||
Random: false,
|
||||
ReportFile: "",
|
||||
Session: sess,
|
||||
}
|
||||
descr := &v2Descriptor{
|
||||
Name: "",
|
||||
Description: "",
|
||||
Author: "",
|
||||
Nettests: []v2Nettest{{
|
||||
Inputs: []string{},
|
||||
Options: map[string]any{},
|
||||
TestName: "example",
|
||||
}},
|
||||
}
|
||||
err := v2MeasureDescriptor(ctx, config, descr)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if v2CountFailedExperiments.Load() != previousFailedExperiments+1 {
|
||||
t.Fatal("expected to see a failed experiment")
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
func TestV2MeasureHTTPS(t *testing.T) {
|
||||
|
@ -239,7 +306,7 @@ func TestV2MeasureHTTPS(t *testing.T) {
|
|||
NoJSON: false,
|
||||
Random: false,
|
||||
ReportFile: "",
|
||||
Session: newSession(ctx, t),
|
||||
Session: newMinimalFakeSession(),
|
||||
}
|
||||
err := v2MeasureHTTPS(ctx, config, "")
|
||||
if !errors.Is(err, expected) {
|
||||
|
@ -259,7 +326,7 @@ func TestV2MeasureHTTPS(t *testing.T) {
|
|||
NoJSON: false,
|
||||
Random: false,
|
||||
ReportFile: "",
|
||||
Session: newSession(ctx, t),
|
||||
Session: newMinimalFakeSession(),
|
||||
}
|
||||
err := v2MeasureHTTPS(ctx, config, "https://example.com") // should not use URL
|
||||
if !errors.Is(err, context.Canceled) {
|
||||
|
|
|
@ -11,8 +11,8 @@ func PanicOnError(err error, message string) {
|
|||
}
|
||||
}
|
||||
|
||||
// PanicIfFalse calls panic if assertion is false.
|
||||
func PanicIfFalse(assertion bool, message string) {
|
||||
// Assert calls panic if assertion is false.
|
||||
func Assert(assertion bool, message string) {
|
||||
if !assertion {
|
||||
panic(message)
|
||||
}
|
||||
|
@ -20,7 +20,7 @@ func PanicIfFalse(assertion bool, message string) {
|
|||
|
||||
// PanicIfTrue calls panic if assertion is true.
|
||||
func PanicIfTrue(assertion bool, message string) {
|
||||
PanicIfFalse(!assertion, message)
|
||||
Assert(!assertion, message)
|
||||
}
|
||||
|
||||
// PanicIfNil calls panic if the given interface is nil.
|
||||
|
|
|
@ -28,17 +28,17 @@ func TestPanicOnError(t *testing.T) {
|
|||
})
|
||||
}
|
||||
|
||||
func TestPanicIfFalse(t *testing.T) {
|
||||
func TestAssert(t *testing.T) {
|
||||
badfunc := func(in bool, message string) (out error) {
|
||||
defer func() {
|
||||
out = errors.New(recover().(string))
|
||||
}()
|
||||
runtimex.PanicIfFalse(in, message)
|
||||
runtimex.Assert(in, message)
|
||||
return
|
||||
}
|
||||
|
||||
t.Run("assertion is true", func(t *testing.T) {
|
||||
runtimex.PanicIfFalse(true, "this assertion should not fail")
|
||||
runtimex.Assert(true, "this assertion should not fail")
|
||||
})
|
||||
|
||||
t.Run("assertion is false", func(t *testing.T) {
|
||||
|
|
|
@ -33,7 +33,7 @@ func NewFailureStr(err error) FailureStr {
|
|||
if !errors.As(err, &errWrapper) {
|
||||
err := netxlite.NewTopLevelGenericErrWrapper(err)
|
||||
couldConvert := errors.As(err, &errWrapper)
|
||||
runtimex.PanicIfFalse(couldConvert, "we should have an ErrWrapper here")
|
||||
runtimex.Assert(couldConvert, "we should have an ErrWrapper here")
|
||||
}
|
||||
s := errWrapper.Failure
|
||||
if s == "" {
|
||||
|
|
Loading…
Reference in New Issue
Block a user