From d0da224a2ac51085b6c802f58578c6d66883884c Mon Sep 17 00:00:00 2001 From: Simone Basso Date: Wed, 31 Aug 2022 18:40:27 +0200 Subject: [PATCH] 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...) --- internal/cmd/miniooni/consent.go | 2 +- internal/cmd/miniooni/main.go | 2 +- internal/engine/mockable/mockable.go | 2 + internal/engine/saver.go | 6 +- internal/engine/submitter.go | 8 +- internal/measurexlite/failure.go | 2 +- internal/model/experiment.go | 22 ++ internal/model/mocks/experiment.go | 75 ++++ internal/model/mocks/experiment_test.go | 146 +++++++ internal/model/mocks/experimentbuilder.go | 48 +++ .../model/mocks/experimentbuilder_test.go | 99 +++++ internal/model/mocks/experimentinputloader.go | 19 + .../model/mocks/experimentinputloader_test.go | 27 ++ .../model/mocks/experimentinputprocessor.go | 12 + .../mocks/experimentinputprocessor_test.go | 22 ++ internal/model/mocks/saver.go | 12 + internal/model/mocks/saver_test.go | 23 ++ internal/model/mocks/session.go | 161 ++++++++ internal/model/mocks/session_test.go | 357 +++++++++++++++++ internal/model/mocks/submitter.go | 17 + internal/model/mocks/submitter_test.go | 24 ++ internal/netxlite/dnsdecoder_test.go | 12 +- internal/netxlite/tls.go | 2 +- internal/oonirun/experiment.go | 48 ++- internal/oonirun/experiment_test.go | 366 ++++++++++++++++-- internal/oonirun/v1.go | 60 ++- internal/oonirun/v1_test.go | 238 +++++++++++- internal/oonirun/v2.go | 32 +- internal/oonirun/v2_test.go | 91 ++++- internal/runtimex/runtimex.go | 6 +- internal/runtimex/runtimex_test.go | 6 +- internal/tracex/event.go | 2 +- 32 files changed, 1837 insertions(+), 112 deletions(-) create mode 100644 internal/model/mocks/experiment.go create mode 100644 internal/model/mocks/experiment_test.go create mode 100644 internal/model/mocks/experimentbuilder.go create mode 100644 internal/model/mocks/experimentbuilder_test.go create mode 100644 internal/model/mocks/experimentinputloader.go create mode 100644 internal/model/mocks/experimentinputloader_test.go create mode 100644 internal/model/mocks/experimentinputprocessor.go create mode 100644 internal/model/mocks/experimentinputprocessor_test.go create mode 100644 internal/model/mocks/saver.go create mode 100644 internal/model/mocks/saver_test.go create mode 100644 internal/model/mocks/session.go create mode 100644 internal/model/mocks/session_test.go create mode 100644 internal/model/mocks/submitter.go create mode 100644 internal/model/mocks/submitter_test.go diff --git a/internal/cmd/miniooni/consent.go b/internal/cmd/miniooni/consent.go index 70a6626..50f205a 100644 --- a/internal/cmd/miniooni/consent.go +++ b/internal/cmd/miniooni/consent.go @@ -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, ) diff --git a/internal/cmd/miniooni/main.go b/internal/cmd/miniooni/main.go index ab5d081..eb7f9f8 100644 --- a/internal/cmd/miniooni/main.go +++ b/internal/cmd/miniooni/main.go @@ -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") diff --git a/internal/engine/mockable/mockable.go b/internal/engine/mockable/mockable.go index 4c98639..a873383 100644 --- a/internal/engine/mockable/mockable.go +++ b/internal/engine/mockable/mockable.go @@ -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 diff --git a/internal/engine/saver.go b/internal/engine/saver.go index 471be32..1ae104b 100644 --- a/internal/engine/saver.go +++ b/internal/engine/saver.go @@ -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 { diff --git a/internal/engine/submitter.go b/internal/engine/submitter.go index 074b820..4c39916 100644 --- a/internal/engine/submitter.go +++ b/internal/engine/submitter.go @@ -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 { diff --git a/internal/measurexlite/failure.go b/internal/measurexlite/failure.go index ab83a2b..3e750b5 100644 --- a/internal/measurexlite/failure.go +++ b/internal/measurexlite/failure.go @@ -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 == "" { diff --git a/internal/model/experiment.go b/internal/model/experiment.go index 70aceb6..acaadde 100644 --- a/internal/model/experiment.go +++ b/internal/model/experiment.go @@ -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 +} diff --git a/internal/model/mocks/experiment.go b/internal/model/mocks/experiment.go new file mode 100644 index 0000000..bf8c1f4 --- /dev/null +++ b/internal/model/mocks/experiment.go @@ -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) +} diff --git a/internal/model/mocks/experiment_test.go b/internal/model/mocks/experiment_test.go new file mode 100644 index 0000000..bb16b15 --- /dev/null +++ b/internal/model/mocks/experiment_test.go @@ -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) + } + }) +} diff --git a/internal/model/mocks/experimentbuilder.go b/internal/model/mocks/experimentbuilder.go new file mode 100644 index 0000000..16763d4 --- /dev/null +++ b/internal/model/mocks/experimentbuilder.go @@ -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() +} diff --git a/internal/model/mocks/experimentbuilder_test.go b/internal/model/mocks/experimentbuilder_test.go new file mode 100644 index 0000000..6fd1ce5 --- /dev/null +++ b/internal/model/mocks/experimentbuilder_test.go @@ -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") + } + }) +} diff --git a/internal/model/mocks/experimentinputloader.go b/internal/model/mocks/experimentinputloader.go new file mode 100644 index 0000000..5c72343 --- /dev/null +++ b/internal/model/mocks/experimentinputloader.go @@ -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) +} diff --git a/internal/model/mocks/experimentinputloader_test.go b/internal/model/mocks/experimentinputloader_test.go new file mode 100644 index 0000000..a22f456 --- /dev/null +++ b/internal/model/mocks/experimentinputloader_test.go @@ -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") + } + }) +} diff --git a/internal/model/mocks/experimentinputprocessor.go b/internal/model/mocks/experimentinputprocessor.go new file mode 100644 index 0000000..fa8b721 --- /dev/null +++ b/internal/model/mocks/experimentinputprocessor.go @@ -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) +} diff --git a/internal/model/mocks/experimentinputprocessor_test.go b/internal/model/mocks/experimentinputprocessor_test.go new file mode 100644 index 0000000..b3c1eee --- /dev/null +++ b/internal/model/mocks/experimentinputprocessor_test.go @@ -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") + } + }) +} diff --git a/internal/model/mocks/saver.go b/internal/model/mocks/saver.go new file mode 100644 index 0000000..bd7bf02 --- /dev/null +++ b/internal/model/mocks/saver.go @@ -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) +} diff --git a/internal/model/mocks/saver_test.go b/internal/model/mocks/saver_test.go new file mode 100644 index 0000000..a63facb --- /dev/null +++ b/internal/model/mocks/saver_test.go @@ -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) + } + }) +} diff --git a/internal/model/mocks/session.go b/internal/model/mocks/session.go new file mode 100644 index 0000000..576e08b --- /dev/null +++ b/internal/model/mocks/session.go @@ -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) +} diff --git a/internal/model/mocks/session_test.go b/internal/model/mocks/session_test.go new file mode 100644 index 0000000..1889eb8 --- /dev/null +++ b/internal/model/mocks/session_test.go @@ -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") + } + }) +} diff --git a/internal/model/mocks/submitter.go b/internal/model/mocks/submitter.go new file mode 100644 index 0000000..e6af646 --- /dev/null +++ b/internal/model/mocks/submitter.go @@ -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) +} diff --git a/internal/model/mocks/submitter_test.go b/internal/model/mocks/submitter_test.go new file mode 100644 index 0000000..a7928e9 --- /dev/null +++ b/internal/model/mocks/submitter_test.go @@ -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) + } + }) +} diff --git a/internal/netxlite/dnsdecoder_test.go b/internal/netxlite/dnsdecoder_test.go index 9b490df..0d1d48b 100644 --- a/internal/netxlite/dnsdecoder_test.go +++ b/internal/netxlite/dnsdecoder_test.go @@ -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 diff --git a/internal/netxlite/tls.go b/internal/netxlite/tls.go index 824fbd9..dc87743 100644 --- a/internal/netxlite/tls.go +++ b/internal/netxlite/tls.go @@ -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 } diff --git a/internal/oonirun/experiment.go b/internal/oonirun/experiment.go index 6dcd68b..1bc04f8 100644 --- a/internal/oonirun/experiment.go +++ b/internal/oonirun/experiment.go @@ -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, diff --git a/internal/oonirun/experiment_test.go b/internal/oonirun/experiment_test.go index 862c2ef..5aaba50 100644 --- a/internal/oonirun/experiment_test.go +++ b/internal/oonirun/experiment_test.go @@ -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) + } + }) + } +} diff --git a/internal/oonirun/v1.go b/internal/oonirun/v1.go index f82d6e6..48bf894 100644 --- a/internal/oonirun/v1.go +++ b/internal/oonirun/v1.go @@ -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 +} diff --git a/internal/oonirun/v1_test.go b/internal/oonirun/v1_test.go index 6abc80e..67d8404 100644 --- a/internal/oonirun/v1_test.go +++ b/internal/oonirun/v1_test.go @@ -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) + } + }) +} diff --git a/internal/oonirun/v2.go b/internal/oonirun/v2.go index 5ad6789..0cd68fc 100644 --- a/internal/oonirun/v2.go +++ b/internal/oonirun/v2.go @@ -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 } } diff --git a/internal/oonirun/v2_test.go b/internal/oonirun/v2_test.go index 172e1e0..012358b 100644 --- a/internal/oonirun/v2_test.go +++ b/internal/oonirun/v2_test.go @@ -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) { diff --git a/internal/runtimex/runtimex.go b/internal/runtimex/runtimex.go index 140a15e..691e33a 100644 --- a/internal/runtimex/runtimex.go +++ b/internal/runtimex/runtimex.go @@ -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. diff --git a/internal/runtimex/runtimex_test.go b/internal/runtimex/runtimex_test.go index 9977622..97dfded 100644 --- a/internal/runtimex/runtimex_test.go +++ b/internal/runtimex/runtimex_test.go @@ -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) { diff --git a/internal/tracex/event.go b/internal/tracex/event.go index 76a1d0e..f593928 100644 --- a/internal/tracex/event.go +++ b/internal/tracex/event.go @@ -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 == "" {