diff --git a/internal/experiment/example/example.go b/internal/experiment/example/example.go index 83ffbe90d4..9fcd03d80d 100644 --- a/internal/experiment/example/example.go +++ b/internal/experiment/example/example.go @@ -14,6 +14,8 @@ import ( const testVersion = "0.1.0" +const testName = "example" + // Config contains the experiment config. // // This contains all the settings that user can set to modify the behaviour @@ -22,7 +24,7 @@ const testVersion = "0.1.0" type Config struct { Message string `ooni:"Message to emit at test completion"` ReturnError bool `ooni:"Toogle to return a mocked error"` - SleepTime int64 `ooni:"Amount of time to sleep for"` + SleepTime int64 `ooni:"Amount of time to sleep for in nanosecond"` } // TestKeys contains the experiment's result. @@ -38,13 +40,12 @@ type TestKeys struct { // Measurer performs the measurement. type Measurer struct { - config Config - testName string + config Config } // ExperimentName implements model.ExperimentMeasurer.ExperimentName. func (m Measurer) ExperimentName() string { - return m.testName + return testName } // ExperimentVersion implements model.ExperimentMeasurer.ExperimentVersion. @@ -81,6 +82,6 @@ func (m Measurer) Run(ctx context.Context, args *model.ExperimentArgs) error { } // NewExperimentMeasurer creates a new ExperimentMeasurer. -func NewExperimentMeasurer(config Config, testName string) model.ExperimentMeasurer { - return Measurer{config: config, testName: testName} +func NewExperimentMeasurer(config Config) model.ExperimentMeasurer { + return Measurer{config: config} } diff --git a/internal/experiment/example/example_test.go b/internal/experiment/example/example_test.go index b4a66aafc1..364fb3491b 100644 --- a/internal/experiment/example/example_test.go +++ b/internal/experiment/example/example_test.go @@ -8,14 +8,14 @@ import ( "github.com/apex/log" "github.com/ooni/probe-cli/v3/internal/experiment/example" - "github.com/ooni/probe-cli/v3/internal/legacy/mockable" + "github.com/ooni/probe-cli/v3/internal/mocks" "github.com/ooni/probe-cli/v3/internal/model" ) func TestSuccess(t *testing.T) { m := example.NewExperimentMeasurer(example.Config{ SleepTime: int64(2 * time.Millisecond), - }, "example") + }) if m.ExperimentName() != "example" { t.Fatal("invalid ExperimentName") } @@ -23,7 +23,11 @@ func TestSuccess(t *testing.T) { t.Fatal("invalid ExperimentVersion") } ctx := context.Background() - sess := &mockable.Session{MockableLogger: log.Log} + sess := &mocks.Session{ + MockLogger: func() model.Logger { + return log.Log + }, + } callbacks := model.NewPrinterCallbacks(sess.Logger()) measurement := new(model.Measurement) args := &model.ExperimentArgs{ @@ -41,9 +45,13 @@ func TestFailure(t *testing.T) { m := example.NewExperimentMeasurer(example.Config{ SleepTime: int64(2 * time.Millisecond), ReturnError: true, - }, "example") + }) ctx := context.Background() - sess := &mockable.Session{MockableLogger: log.Log} + sess := &mocks.Session{ + MockLogger: func() model.Logger { + return log.Log + }, + } callbacks := model.NewPrinterCallbacks(sess.Logger()) args := &model.ExperimentArgs{ Callbacks: callbacks, diff --git a/internal/mocks/session.go b/internal/mocks/session.go index b26611268c..00b9a0a71b 100644 --- a/internal/mocks/session.go +++ b/internal/mocks/session.go @@ -59,6 +59,16 @@ type Session struct { MockCheckIn func(ctx context.Context, config *model.OOAPICheckInConfig) (*model.OOAPICheckInResult, error) + + MockClose func() error + + MockMaybeLookupBackendsContext func(ctx context.Context) error + + MockMaybeLookupLocationContext func(ctx context.Context) error + + MockResolverASNString func() string + + MockResolverNetworkName func() string } func (sess *Session) GetTestHelpersByName(name string) ([]model.OOAPIService, bool) { @@ -159,3 +169,23 @@ func (sess *Session) CheckIn(ctx context.Context, config *model.OOAPICheckInConfig) (*model.OOAPICheckInResult, error) { return sess.MockCheckIn(ctx, config) } + +func (sess *Session) Close() error { + return sess.MockClose() +} + +func (sess *Session) MaybeLookupBackendsContext(ctx context.Context) error { + return sess.MockMaybeLookupBackendsContext(ctx) +} + +func (sess *Session) MaybeLookupLocationContext(ctx context.Context) error { + return sess.MockMaybeLookupLocationContext(ctx) +} + +func (sess *Session) ResolverASNString() string { + return sess.MockResolverASNString() +} + +func (sess *Session) ResolverNetworkName() string { + return sess.MockResolverNetworkName() +} diff --git a/internal/mocks/session_test.go b/internal/mocks/session_test.go index e26f67430d..3a98ee5ff9 100644 --- a/internal/mocks/session_test.go +++ b/internal/mocks/session_test.go @@ -354,4 +354,65 @@ func TestSession(t *testing.T) { t.Fatal("unexpected out") } }) + + t.Run("Close", func(t *testing.T) { + expected := errors.New("mocked err") + s := &Session{ + MockClose: func() error { + return expected + }, + } + err := s.Close() + if !errors.Is(err, expected) { + t.Fatal("unexpected err") + } + }) + + t.Run("MaybeLookupBackendsContext", func(t *testing.T) { + expected := errors.New("mocked err") + s := &Session{ + MockMaybeLookupBackendsContext: func(ctx context.Context) error { + return expected + }, + } + err := s.MaybeLookupBackendsContext(context.Background()) + if !errors.Is(err, expected) { + t.Fatal("unexpected err") + } + }) + + t.Run("MaybeLookupLocationContext", func(t *testing.T) { + expected := errors.New("mocked err") + s := &Session{ + MockMaybeLookupLocationContext: func(ctx context.Context) error { + return expected + }, + } + err := s.MaybeLookupLocationContext(context.Background()) + if !errors.Is(err, expected) { + t.Fatal("unexpected err") + } + }) + + t.Run("ResolverASNString", func(t *testing.T) { + s := &Session{ + MockResolverASNString: func() string { + return "xx" + }, + } + if s.ResolverASNString() != "xx" { + t.Fatal("unexpected result") + } + }) + + t.Run("ResolverNetworkName", func(t *testing.T) { + s := &Session{ + MockResolverNetworkName: func() string { + return "xx" + }, + } + if s.ResolverNetworkName() != "xx" { + t.Fatal("unexpected result") + } + }) } diff --git a/internal/registry/example.go b/internal/registry/example.go index 9bcb5459bb..498c09142c 100644 --- a/internal/registry/example.go +++ b/internal/registry/example.go @@ -22,7 +22,7 @@ func init() { return &Factory{ build: func(config interface{}) model.ExperimentMeasurer { return example.NewExperimentMeasurer( - *config.(*example.Config), "example", + *config.(*example.Config), ) }, canonicalName: canonicalName, diff --git a/pkg/oonimkall/taskmocks_test.go b/pkg/oonimkall/taskmocks_test.go deleted file mode 100644 index 839f519e36..0000000000 --- a/pkg/oonimkall/taskmocks_test.go +++ /dev/null @@ -1,222 +0,0 @@ -package oonimkall - -import ( - "context" - "errors" - "sync" - - "github.com/ooni/probe-cli/v3/internal/engine" - "github.com/ooni/probe-cli/v3/internal/model" -) - -// -// This file contains mocks for types used by tasks. Because -// we only use mocks when testing, this file is a `_test.go` file. -// - -// CollectorTaskEmitter is a thread-safe taskEmitter -// that stores all the events inside itself. -type CollectorTaskEmitter struct { - // events contains the events - events []*event - - // mu provides mutual exclusion - mu sync.Mutex -} - -// ensures that a CollectorTaskEmitter is a taskEmitter. -var _ taskEmitter = &CollectorTaskEmitter{} - -// Emit implements the taskEmitter.Emit method. -func (e *CollectorTaskEmitter) Emit(key string, value interface{}) { - e.mu.Lock() - e.events = append(e.events, &event{Key: key, Value: value}) - e.mu.Unlock() -} - -// Collect returns a copy of the collected events. It is safe -// to read the events. It's a data race to modify them. -// -// After this function has been called, the internal array -// of events will now be empty. -func (e *CollectorTaskEmitter) Collect() (out []*event) { - e.mu.Lock() - out = e.events - e.events = nil - e.mu.Unlock() - return -} - -// SessionBuilderConfigSaver is a session builder that -// saves the received config and returns an error. -type SessionBuilderConfigSaver struct { - Config engine.SessionConfig -} - -var _ taskSessionBuilder = &SessionBuilderConfigSaver{} - -func (b *SessionBuilderConfigSaver) NewSession( - ctx context.Context, config engine.SessionConfig) (taskSession, error) { - b.Config = config - return nil, errors.New("mocked error") -} - -// MockableTaskRunnerDependencies allows to mock all the -// dependencies of taskRunner using a single structure. -type MockableTaskRunnerDependencies struct { - - // taskSessionBuilder: - - MockNewSession func(ctx context.Context, - config engine.SessionConfig) (taskSession, error) - - // taskSession: - - MockClose func() error - MockNewExperimentBuilderByName func(name string) (taskExperimentBuilder, error) - MockMaybeLookupBackendsContext func(ctx context.Context) error - MockMaybeLookupLocationContext func(ctx context.Context) error - MockProbeIP func() string - MockProbeASNString func() string - MockProbeCC func() string - MockProbeNetworkName func() string - MockResolverASNString func() string - MockResolverIP func() string - MockResolverNetworkName func() string - - // taskExperimentBuilder: - - MockableSetCallbacks func(callbacks model.ExperimentCallbacks) - MockableInputPolicy func() model.InputPolicy - MockableNewExperimentInstance func() taskExperiment - MockableInterruptible func() bool - - // taskExperiment: - - MockableKibiBytesReceived func() float64 - MockableKibiBytesSent func() float64 - MockableOpenReportContext func(ctx context.Context) error - MockableReportID func() string - MockableMeasureWithContext func(ctx context.Context, target model.ExperimentTarget) ( - measurement *model.Measurement, err error) - MockableSubmitAndUpdateMeasurementContext func( - ctx context.Context, measurement *model.Measurement) error -} - -var ( - _ taskSessionBuilder = &MockableTaskRunnerDependencies{} - _ taskSession = &MockableTaskRunnerDependencies{} - _ taskExperimentBuilder = &MockableTaskRunnerDependencies{} - _ taskExperiment = &MockableTaskRunnerDependencies{} -) - -func (dep *MockableTaskRunnerDependencies) NewSession( - ctx context.Context, config engine.SessionConfig) (taskSession, error) { - if f := dep.MockNewSession; f != nil { - return f(ctx, config) - } - return dep, nil -} - -func (dep *MockableTaskRunnerDependencies) Close() error { - return dep.MockClose() -} - -func (dep *MockableTaskRunnerDependencies) NewExperimentBuilderByName(name string) (taskExperimentBuilder, error) { - if f := dep.MockNewExperimentBuilderByName; f != nil { - return f(name) - } - return dep, nil -} - -func (dep *MockableTaskRunnerDependencies) MaybeLookupBackendsContext(ctx context.Context) error { - return dep.MockMaybeLookupBackendsContext(ctx) -} - -func (dep *MockableTaskRunnerDependencies) MaybeLookupLocationContext(ctx context.Context) error { - return dep.MockMaybeLookupLocationContext(ctx) -} - -func (dep *MockableTaskRunnerDependencies) ProbeIP() string { - return dep.MockProbeIP() -} - -func (dep *MockableTaskRunnerDependencies) ProbeASNString() string { - return dep.MockProbeASNString() -} - -func (dep *MockableTaskRunnerDependencies) ProbeCC() string { - return dep.MockProbeCC() -} - -func (dep *MockableTaskRunnerDependencies) ProbeNetworkName() string { - return dep.MockProbeNetworkName() -} - -func (dep *MockableTaskRunnerDependencies) ResolverASNString() string { - return dep.MockResolverASNString() -} - -func (dep *MockableTaskRunnerDependencies) ResolverIP() string { - return dep.MockResolverIP() -} - -func (dep *MockableTaskRunnerDependencies) ResolverNetworkName() string { - return dep.MockResolverNetworkName() -} - -func (dep *MockableTaskRunnerDependencies) SetCallbacks(callbacks model.ExperimentCallbacks) { - dep.MockableSetCallbacks(callbacks) -} - -func (dep *MockableTaskRunnerDependencies) InputPolicy() model.InputPolicy { - return dep.MockableInputPolicy() -} - -func (dep *MockableTaskRunnerDependencies) NewExperimentInstance() taskExperiment { - if f := dep.MockableNewExperimentInstance; f != nil { - return f() - } - return dep -} - -func (dep *MockableTaskRunnerDependencies) Interruptible() bool { - return dep.MockableInterruptible() -} - -func (dep *MockableTaskRunnerDependencies) KibiBytesReceived() float64 { - return dep.MockableKibiBytesReceived() -} - -func (dep *MockableTaskRunnerDependencies) KibiBytesSent() float64 { - return dep.MockableKibiBytesSent() -} - -func (dep *MockableTaskRunnerDependencies) OpenReportContext(ctx context.Context) error { - return dep.MockableOpenReportContext(ctx) -} - -func (dep *MockableTaskRunnerDependencies) ReportID() string { - return dep.MockableReportID() -} - -func (dep *MockableTaskRunnerDependencies) MeasureWithContext( - ctx context.Context, target model.ExperimentTarget) (measurement *model.Measurement, err error) { - return dep.MockableMeasureWithContext(ctx, target) -} - -func (dep *MockableTaskRunnerDependencies) SubmitAndUpdateMeasurementContext( - ctx context.Context, measurement *model.Measurement) error { - return dep.MockableSubmitAndUpdateMeasurementContext(ctx, measurement) -} - -// MockableKVStoreFSBuilder is a mockable taskKVStoreFSBuilder. -type MockableKVStoreFSBuilder struct { - MockNewFS func(path string) (model.KeyValueStore, error) -} - -var _ taskKVStoreFSBuilder = &MockableKVStoreFSBuilder{} - -func (m *MockableKVStoreFSBuilder) NewFS(path string) (model.KeyValueStore, error) { - return m.MockNewFS(path) -} diff --git a/pkg/oonimkall/taskmodel.go b/pkg/oonimkall/taskmodel.go index 86f534e01b..e030cbc8f7 100644 --- a/pkg/oonimkall/taskmodel.go +++ b/pkg/oonimkall/taskmodel.go @@ -1,13 +1,5 @@ package oonimkall -import ( - "context" - "io" - - "github.com/ooni/probe-cli/v3/internal/engine" - "github.com/ooni/probe-cli/v3/internal/model" -) - // // Task Model // @@ -25,6 +17,13 @@ import ( // ooni/probe-cli and so it's not defined in this file. // +import ( + "context" + "io" + + "github.com/ooni/probe-cli/v3/internal/model" +) + const taskABIVersion = 1 // Running tasks emit logs using different log levels. We @@ -161,28 +160,20 @@ type event struct { // The abstraction representing a OONI session is taskSession. // -// taskKVStoreFSBuilder constructs a KVStore with -// filesystem backing for running tests. -type taskKVStoreFSBuilder interface { - // NewFS creates a new KVStore using the filesystem. - NewFS(path string) (model.KeyValueStore, error) -} - -// taskSessionBuilder constructs a new Session. -type taskSessionBuilder interface { - // NewSession creates a new taskSession. - NewSession(ctx context.Context, - config engine.SessionConfig) (taskSession, error) -} - // taskSession abstracts a OONI session. type taskSession interface { + // A session should be used by an experiment. + model.ExperimentSession + + // A session should be used when loading targets. + model.ExperimentTargetLoaderSession + // A session can be closed. io.Closer - // NewExperimentBuilderByName creates the builder for constructing + // NewExperimentBuilder creates the builder for constructing // a new experiment given the experiment's name. - NewExperimentBuilderByName(name string) (taskExperimentBuilder, error) + NewExperimentBuilder(name string) (model.ExperimentBuilder, error) // MaybeLookupBackendsContext lookups the OONI backend unless // this operation has already been performed. @@ -200,10 +191,6 @@ type taskSession interface { // and returns the resolved probe ASN as a string. ProbeASNString() string - // ProbeCC must be called after MaybeLookupLocationContext - // and returns the resolved probe country code. - ProbeCC() string - // ProbeNetworkName must be called after MaybeLookupLocationContext // and returns the resolved probe country code. ProbeNetworkName() string @@ -212,55 +199,11 @@ type taskSession interface { // and returns the resolved resolver's ASN as a string. ResolverASNString() string - // ResolverIP must be called after MaybeLookupLocationContext - // and returns the resolved resolver's IP. - ResolverIP() string - // ResolverNetworkName must be called after MaybeLookupLocationContext // and returns the resolved resolver's network name. ResolverNetworkName() string } -// taskExperimentBuilder builds a taskExperiment. -type taskExperimentBuilder interface { - // SetCallbacks sets the experiment callbacks. - SetCallbacks(callbacks model.ExperimentCallbacks) - - // InputPolicy returns the experiment's input policy. - InputPolicy() model.InputPolicy - - // NewExperiment creates the new experiment. - NewExperimentInstance() taskExperiment - - // Interruptible returns whether this experiment is interruptible. - Interruptible() bool -} - -// taskExperiment is a runnable experiment. -type taskExperiment interface { - // KibiBytesReceived returns the KiB received by the experiment. - KibiBytesReceived() float64 - - // KibiBytesSent returns the KiB sent by the experiment. - KibiBytesSent() float64 - - // OpenReportContext opens a new report. - OpenReportContext(ctx context.Context) error - - // ReportID must be called after a successful OpenReportContext - // and returns the report ID for this measurement. - ReportID() string - - // MeasureWithContext runs the measurement. - MeasureWithContext(ctx context.Context, target model.ExperimentTarget) ( - measurement *model.Measurement, err error) - - // SubmitAndUpdateMeasurementContext submits the measurement - // and updates its report ID on success. - SubmitAndUpdateMeasurementContext( - ctx context.Context, measurement *model.Measurement) error -} - // // Task Running // diff --git a/pkg/oonimkall/taskrunner.go b/pkg/oonimkall/taskrunner.go index d5b70947c0..000532e25a 100644 --- a/pkg/oonimkall/taskrunner.go +++ b/pkg/oonimkall/taskrunner.go @@ -8,6 +8,7 @@ import ( "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/runtimex" "github.com/ooni/probe-cli/v3/internal/targetloading" @@ -15,10 +16,10 @@ import ( // runnerForTask runs a specific task type runnerForTask struct { - emitter *taskEmitterWrapper - kvStoreBuilder taskKVStoreFSBuilder - sessionBuilder taskSessionBuilder - settings *settings + emitter *taskEmitterWrapper + newKVStore func(path string) (model.KeyValueStore, error) + newSession func(ctx context.Context, config engine.SessionConfig) (taskSession, error) + settings *settings } var _ taskRunner = &runnerForTask{} @@ -26,10 +27,20 @@ var _ taskRunner = &runnerForTask{} // newRunner creates a new task runner func newRunner(settings *settings, emitter taskEmitter) *runnerForTask { return &runnerForTask{ - emitter: &taskEmitterWrapper{emitter}, - kvStoreBuilder: &taskKVStoreFSBuilderEngine{}, - sessionBuilder: &taskSessionBuilderEngine{}, - settings: settings, + emitter: &taskEmitterWrapper{emitter}, + newKVStore: func(path string) (model.KeyValueStore, error) { + // Note that we will return a non-nil model.KeyValueStore even if the + // kvstore.NewFS factory returns a nil *kvstore.FS because of how golang + // converts between nil types. Because we're checking the error and + // acting upon it, it is not a big deal. + return kvstore.NewFS(path) + }, + newSession: func(ctx context.Context, config engine.SessionConfig) (taskSession, error) { + // Same note as above: the returned session is not nil even when the + // factory returns a nil *engine.Session because of golang nil conversion. + return engine.NewSession(ctx, config) + }, + settings: settings, } } @@ -45,7 +56,7 @@ func (r *runnerForTask) hasUnsupportedSettings() bool { } func (r *runnerForTask) newsession(ctx context.Context, logger model.Logger) (taskSession, error) { - kvstore, err := r.kvStoreBuilder.NewFS(r.settings.StateDir) + kvstore, err := r.newKVStore(r.settings.StateDir) if err != nil { return nil, err } @@ -74,13 +85,13 @@ func (r *runnerForTask) newsession(ctx context.Context, logger model.Logger) (ta Address: r.settings.Options.ProbeServicesBaseURL, }} } - return r.sessionBuilder.NewSession(ctx, config) + return r.newSession(ctx, config) } // contextForExperiment ensurs that for measuring we only use an // interruptible context when we can interrupt the experiment func (r *runnerForTask) contextForExperiment( - ctx context.Context, builder taskExperimentBuilder, + ctx context.Context, builder model.ExperimentBuilder, ) context.Context { if builder.Interruptible() { return ctx @@ -132,7 +143,7 @@ func (r *runnerForTask) Run(rootCtx context.Context) { r.emitter.Emit(eventTypeStatusEnd, endEvent) }() - builder, err := sess.NewExperimentBuilderByName(r.settings.Name) + builder, err := sess.NewExperimentBuilder(r.settings.Name) if err != nil { r.emitter.EmitFailureStartup(err.Error()) return @@ -210,7 +221,7 @@ func (r *runnerForTask) Run(rootCtx context.Context) { } r.settings.Inputs = append(r.settings.Inputs, "") } - experiment := builder.NewExperimentInstance() + experiment := builder.NewExperiment() defer func() { endEvent.DownloadedKB = experiment.KibiBytesReceived() endEvent.UploadedKB = experiment.KibiBytesSent() diff --git a/pkg/oonimkall/taskrunner_test.go b/pkg/oonimkall/taskrunner_test.go index d7840e8f12..4d503f6f54 100644 --- a/pkg/oonimkall/taskrunner_test.go +++ b/pkg/oonimkall/taskrunner_test.go @@ -7,8 +7,10 @@ import ( "time" "github.com/google/go-cmp/cmp" - "github.com/ooni/probe-cli/v3/internal/targetloading" + "github.com/ooni/probe-cli/v3/internal/engine" + "github.com/ooni/probe-cli/v3/internal/mocks" "github.com/ooni/probe-cli/v3/internal/model" + "github.com/ooni/probe-cli/v3/internal/targetloading" ) func TestMeasurementSubmissionEventName(t *testing.T) { @@ -29,11 +31,20 @@ func TestMeasurementSubmissionFailure(t *testing.T) { } } -func TestTaskRunnerRun(t *testing.T) { - if testing.Short() { - t.Skip("skip test in short mode") - } +// MockableTaskRunnerDependencies is the mockable struct used by [TestTaskRunnerRun]. +type MockableTaskRunnerDependencies struct { + Builder *mocks.ExperimentBuilder + Experiment *mocks.Experiment + Loader *mocks.ExperimentTargetLoader + Session *mocks.Session +} +// NewSession is the method that returns the new fake session. +func (dep *MockableTaskRunnerDependencies) NewSession(ctx context.Context, config engine.SessionConfig) (taskSession, error) { + return dep.Session, nil +} + +func TestTaskRunnerRun(t *testing.T) { // newRunnerForTesting is a factory for creating a new // runner that wraps newRunner and also sets a specific // taskSessionBuilder for testing purposes. @@ -95,10 +106,8 @@ func TestTaskRunnerRun(t *testing.T) { t.Run("with failure when creating a new kvstore", func(t *testing.T) { runner, emitter := newRunnerForTesting() // override the kvstore builder to provoke an error - runner.kvStoreBuilder = &MockableKVStoreFSBuilder{ - MockNewFS: func(path string) (model.KeyValueStore, error) { - return nil, errors.New("generic error") - }, + runner.newKVStore = func(path string) (model.KeyValueStore, error) { + return nil, errors.New("generic error") } events := runAndCollect(runner, emitter) assertCountEventsByKey(events, eventTypeFailureStartup, 1) @@ -117,11 +126,14 @@ func TestTaskRunnerRun(t *testing.T) { runner.settings.Proxy = "https://127.0.0.1/" // set a fake session builder that causes the startup to // fail but records the config passed to NewSession - saver := &SessionBuilderConfigSaver{} - runner.sessionBuilder = saver + var savedConfig engine.SessionConfig + runner.newSession = func(ctx context.Context, config engine.SessionConfig) (taskSession, error) { + savedConfig = config + return nil, errors.New("generic error") + } events := runAndCollect(runner, emitter) assertCountEventsByKey(events, eventTypeFailureStartup, 1) - if saver.Config.ProxyURL.String() != runner.settings.Proxy { + if savedConfig.ProxyURL.String() != runner.settings.Proxy { t.Fatal("invalid proxy URL") } }) @@ -132,11 +144,14 @@ func TestTaskRunnerRun(t *testing.T) { runner.settings.Options.ProbeServicesBaseURL = "https://127.0.0.1" // set a fake session builder that causes the startup to // fail but records the config passed to NewSession - saver := &SessionBuilderConfigSaver{} - runner.sessionBuilder = saver + var savedConfig engine.SessionConfig + runner.newSession = func(ctx context.Context, config engine.SessionConfig) (taskSession, error) { + savedConfig = config + return nil, errors.New("generic error") + } events := runAndCollect(runner, emitter) assertCountEventsByKey(events, eventTypeFailureStartup, 1) - psu := saver.Config.AvailableProbeServices + psu := savedConfig.AvailableProbeServices if len(psu) != 1 { t.Fatal("invalid length") } @@ -182,65 +197,96 @@ func TestTaskRunnerRun(t *testing.T) { // fakeSuccessfulRun returns a new set of dependencies that // will perform a fully successful, but fake, run. + // + // You MAY override some functions to provoke specific errors + // or generally change the operating conditions. fakeSuccessfulRun := func() *MockableTaskRunnerDependencies { - return &MockableTaskRunnerDependencies{ - MockableKibiBytesReceived: func() float64 { - return 10 - }, - MockableKibiBytesSent: func() float64 { - return 4 - }, - MockableOpenReportContext: func(ctx context.Context) error { - return nil - }, - MockableReportID: func() string { - return "20211202T074907Z_example_IT_30722_n1_axDLHNUfJaV1IbuU" - }, - MockableMeasureWithContext: func(ctx context.Context, target model.ExperimentTarget) (*model.Measurement, error) { - return &model.Measurement{}, nil - }, - MockableSubmitAndUpdateMeasurementContext: func(ctx context.Context, measurement *model.Measurement) error { - return nil - }, - MockableSetCallbacks: func(callbacks model.ExperimentCallbacks) { - }, - MockableInputPolicy: func() model.InputPolicy { - return model.InputNone + deps := &MockableTaskRunnerDependencies{ + + // Configure the fake experiment + Experiment: &mocks.Experiment{ + MockKibiBytesReceived: func() float64 { + return 10 + }, + MockKibiBytesSent: func() float64 { + return 4 + }, + MockOpenReportContext: func(ctx context.Context) error { + return nil + }, + MockReportID: func() string { + return "20211202T074907Z_example_IT_30722_n1_axDLHNUfJaV1IbuU" + }, + MockMeasureWithContext: func(ctx context.Context, target model.ExperimentTarget) (*model.Measurement, error) { + return &model.Measurement{}, nil + }, + MockSubmitAndUpdateMeasurementContext: func(ctx context.Context, measurement *model.Measurement) error { + return nil + }, }, - MockableInterruptible: func() bool { - return false - }, - MockClose: func() error { - return nil - }, - MockMaybeLookupBackendsContext: func(ctx context.Context) error { - return nil - }, - MockMaybeLookupLocationContext: func(ctx context.Context) error { - return nil - }, - MockProbeIP: func() string { - return "130.192.91.211" - }, - MockProbeASNString: func() string { - return "AS137" - }, - MockProbeCC: func() string { - return "IT" - }, - MockProbeNetworkName: func() string { - return "GARR" - }, - MockResolverASNString: func() string { - return "AS137" - }, - MockResolverIP: func() string { - return "130.192.3.24" + + // Configure the fake experiment builder + Builder: &mocks.ExperimentBuilder{ + MockSetCallbacks: func(callbacks model.ExperimentCallbacks) {}, + MockInputPolicy: func() model.InputPolicy { + return model.InputNone + }, + MockInterruptible: func() bool { + return false + }, }, - MockResolverNetworkName: func() string { - return "GARR" + + // Configure the fake session + Session: &mocks.Session{ + MockClose: func() error { + return nil + }, + MockMaybeLookupBackendsContext: func(ctx context.Context) error { + return nil + }, + MockMaybeLookupLocationContext: func(ctx context.Context) error { + return nil + }, + MockProbeIP: func() string { + return "130.192.91.211" + }, + MockProbeASNString: func() string { + return "AS137" + }, + MockProbeCC: func() string { + return "IT" + }, + MockProbeNetworkName: func() string { + return "GARR" + }, + MockResolverASNString: func() string { + return "AS137" + }, + MockResolverIP: func() string { + return "130.192.3.24" + }, + MockResolverNetworkName: func() string { + return "GARR" + }, }, } + + // The fake session MUST return the fake experiment builder + deps.Session.MockNewExperimentBuilder = func(name string) (model.ExperimentBuilder, error) { + return deps.Builder, nil + } + + // The fake experiment builder MUST return the fake target loader + deps.Builder.MockNewTargetLoader = func(config *model.ExperimentTargetLoaderConfig) model.ExperimentTargetLoader { + return deps.Loader + } + + // The fake experiment builder MUST return the fake experiment + deps.Builder.MockNewExperiment = func() model.Experiment { + return deps.Experiment + } + + return deps } assertReducedEventsLike := func(t *testing.T, expected, got []eventKeyCount) { @@ -252,10 +298,10 @@ func TestTaskRunnerRun(t *testing.T) { t.Run("with invalid experiment name", func(t *testing.T) { runner, emitter := newRunnerForTesting() fake := fakeSuccessfulRun() - fake.MockNewExperimentBuilderByName = func(name string) (taskExperimentBuilder, error) { + fake.Session.MockNewExperimentBuilder = func(name string) (model.ExperimentBuilder, error) { return nil, errors.New("invalid experiment name") } - runner.sessionBuilder = fake + runner.newSession = fake.NewSession events := runAndCollect(runner, emitter) reduced := reduceEventsKeysIgnoreLog(events) expect := []eventKeyCount{ @@ -270,10 +316,10 @@ func TestTaskRunnerRun(t *testing.T) { t.Run("with error during backends lookup", func(t *testing.T) { runner, emitter := newRunnerForTesting() fake := fakeSuccessfulRun() - fake.MockMaybeLookupBackendsContext = func(ctx context.Context) error { + fake.Session.MockMaybeLookupBackendsContext = func(ctx context.Context) error { return errors.New("mocked error") } - runner.sessionBuilder = fake + runner.newSession = fake.NewSession events := runAndCollect(runner, emitter) reduced := reduceEventsKeysIgnoreLog(events) expect := []eventKeyCount{ @@ -288,10 +334,10 @@ func TestTaskRunnerRun(t *testing.T) { t.Run("with error during location lookup", func(t *testing.T) { runner, emitter := newRunnerForTesting() fake := fakeSuccessfulRun() - fake.MockMaybeLookupLocationContext = func(ctx context.Context) error { + fake.Session.MockMaybeLookupLocationContext = func(ctx context.Context) error { return errors.New("mocked error") } - runner.sessionBuilder = fake + runner.newSession = fake.NewSession events := runAndCollect(runner, emitter) reduced := reduceEventsKeysIgnoreLog(events) expect := []eventKeyCount{ @@ -310,10 +356,10 @@ func TestTaskRunnerRun(t *testing.T) { t.Run("with missing input and InputOrQueryBackend policy", func(t *testing.T) { runner, emitter := newRunnerForTesting() fake := fakeSuccessfulRun() - fake.MockableInputPolicy = func() model.InputPolicy { + fake.Builder.MockInputPolicy = func() model.InputPolicy { return model.InputOrQueryBackend } - runner.sessionBuilder = fake + runner.newSession = fake.NewSession events := runAndCollect(runner, emitter) reduced := reduceEventsKeysIgnoreLog(events) expect := []eventKeyCount{ @@ -331,10 +377,10 @@ func TestTaskRunnerRun(t *testing.T) { t.Run("with missing input and InputStrictlyRequired policy", func(t *testing.T) { runner, emitter := newRunnerForTesting() fake := fakeSuccessfulRun() - fake.MockableInputPolicy = func() model.InputPolicy { + fake.Builder.MockInputPolicy = func() model.InputPolicy { return model.InputStrictlyRequired } - runner.sessionBuilder = fake + runner.newSession = fake.NewSession events := runAndCollect(runner, emitter) reduced := reduceEventsKeysIgnoreLog(events) expect := []eventKeyCount{ @@ -354,10 +400,10 @@ func TestTaskRunnerRun(t *testing.T) { runner, emitter := newRunnerForTesting() runner.settings.Name = "Antani" // no input for this experiment fake := fakeSuccessfulRun() - fake.MockableInputPolicy = func() model.InputPolicy { + fake.Builder.MockInputPolicy = func() model.InputPolicy { return model.InputOrStaticDefault } - runner.sessionBuilder = fake + runner.newSession = fake.NewSession events := runAndCollect(runner, emitter) reduced := reduceEventsKeysIgnoreLog(events) expect := []eventKeyCount{ @@ -376,10 +422,10 @@ func TestTaskRunnerRun(t *testing.T) { runner, emitter := newRunnerForTesting() runner.settings.Inputs = append(runner.settings.Inputs, "https://x.org/") fake := fakeSuccessfulRun() - fake.MockableInputPolicy = func() model.InputPolicy { + fake.Builder.MockInputPolicy = func() model.InputPolicy { return model.InputNone } - runner.sessionBuilder = fake + runner.newSession = fake.NewSession events := runAndCollect(runner, emitter) reduced := reduceEventsKeysIgnoreLog(events) expect := []eventKeyCount{ @@ -397,10 +443,10 @@ func TestTaskRunnerRun(t *testing.T) { t.Run("with failure opening report", func(t *testing.T) { runner, emitter := newRunnerForTesting() fake := fakeSuccessfulRun() - fake.MockableOpenReportContext = func(ctx context.Context) error { + fake.Experiment.MockOpenReportContext = func(ctx context.Context) error { return errors.New("mocked error") } - runner.sessionBuilder = fake + runner.newSession = fake.NewSession events := runAndCollect(runner, emitter) reduced := reduceEventsKeysIgnoreLog(events) expect := []eventKeyCount{ @@ -418,10 +464,10 @@ func TestTaskRunnerRun(t *testing.T) { t.Run("with success and InputNone policy", func(t *testing.T) { runner, emitter := newRunnerForTesting() fake := fakeSuccessfulRun() - fake.MockableInputPolicy = func() model.InputPolicy { + fake.Builder.MockInputPolicy = func() model.InputPolicy { return model.InputNone } - runner.sessionBuilder = fake + runner.newSession = fake.NewSession events := runAndCollect(runner, emitter) reduced := reduceEventsKeysIgnoreLog(events) expect := []eventKeyCount{ @@ -444,13 +490,13 @@ func TestTaskRunnerRun(t *testing.T) { t.Run("with measurement failure and InputNone policy", func(t *testing.T) { runner, emitter := newRunnerForTesting() fake := fakeSuccessfulRun() - fake.MockableInputPolicy = func() model.InputPolicy { + fake.Builder.MockInputPolicy = func() model.InputPolicy { return model.InputNone } - fake.MockableMeasureWithContext = func(ctx context.Context, target model.ExperimentTarget) (measurement *model.Measurement, err error) { + fake.Experiment.MockMeasureWithContext = func(ctx context.Context, target model.ExperimentTarget) (measurement *model.Measurement, err error) { return nil, errors.New("preconditions error") } - runner.sessionBuilder = fake + runner.newSession = fake.NewSession events := runAndCollect(runner, emitter) reduced := reduceEventsKeysIgnoreLog(events) expect := []eventKeyCount{ @@ -474,13 +520,13 @@ func TestTaskRunnerRun(t *testing.T) { // which is what was happening in the above referenced issue. runner, emitter := newRunnerForTesting() fake := fakeSuccessfulRun() - fake.MockableInputPolicy = func() model.InputPolicy { + fake.Builder.MockInputPolicy = func() model.InputPolicy { return model.InputNone } - fake.MockableMeasureWithContext = func(ctx context.Context, target model.ExperimentTarget) (measurement *model.Measurement, err error) { + fake.Experiment.MockMeasureWithContext = func(ctx context.Context, target model.ExperimentTarget) (measurement *model.Measurement, err error) { return nil, errors.New("preconditions error") } - runner.sessionBuilder = fake + runner.newSession = fake.NewSession runner.settings.Annotations = map[string]string{ "architecture": "arm64", } @@ -505,10 +551,10 @@ func TestTaskRunnerRun(t *testing.T) { runner, emitter := newRunnerForTesting() runner.settings.Inputs = []string{"a", "b", "c", "d"} fake := fakeSuccessfulRun() - fake.MockableInputPolicy = func() model.InputPolicy { + fake.Builder.MockInputPolicy = func() model.InputPolicy { return model.InputStrictlyRequired } - runner.sessionBuilder = fake + runner.newSession = fake.NewSession events := runAndCollect(runner, emitter) reduced := reduceEventsKeysIgnoreLog(events) expect := []eventKeyCount{ @@ -553,10 +599,10 @@ func TestTaskRunnerRun(t *testing.T) { runner, emitter := newRunnerForTesting() runner.settings.Inputs = []string{"a", "b", "c", "d"} fake := fakeSuccessfulRun() - fake.MockableInputPolicy = func() model.InputPolicy { + fake.Builder.MockInputPolicy = func() model.InputPolicy { return model.InputOptional } - runner.sessionBuilder = fake + runner.newSession = fake.NewSession events := runAndCollect(runner, emitter) reduced := reduceEventsKeysIgnoreLog(events) expect := []eventKeyCount{ @@ -600,10 +646,10 @@ func TestTaskRunnerRun(t *testing.T) { t.Run("with success and InputOptional and no input", func(t *testing.T) { runner, emitter := newRunnerForTesting() fake := fakeSuccessfulRun() - fake.MockableInputPolicy = func() model.InputPolicy { + fake.Builder.MockInputPolicy = func() model.InputPolicy { return model.InputOptional } - runner.sessionBuilder = fake + runner.newSession = fake.NewSession events := runAndCollect(runner, emitter) reduced := reduceEventsKeysIgnoreLog(events) expect := []eventKeyCount{ @@ -630,10 +676,10 @@ func TestTaskRunnerRun(t *testing.T) { runner, emitter := newRunnerForTesting() runner.settings.Name = experimentName fake := fakeSuccessfulRun() - fake.MockableInputPolicy = func() model.InputPolicy { + fake.Builder.MockInputPolicy = func() model.InputPolicy { return model.InputOrStaticDefault } - runner.sessionBuilder = fake + runner.newSession = fake.NewSession events := runAndCollect(runner, emitter) reduced := reduceEventsKeysIgnoreLog(events) expect := []eventKeyCount{ @@ -666,14 +712,14 @@ func TestTaskRunnerRun(t *testing.T) { runner.settings.Inputs = []string{"a", "b", "c", "d"} runner.settings.Options.MaxRuntime = 2 fake := fakeSuccessfulRun() - fake.MockableInputPolicy = func() model.InputPolicy { + fake.Builder.MockInputPolicy = func() model.InputPolicy { return model.InputStrictlyRequired } - fake.MockableMeasureWithContext = func(ctx context.Context, target model.ExperimentTarget) (measurement *model.Measurement, err error) { + fake.Experiment.MockMeasureWithContext = func(ctx context.Context, target model.ExperimentTarget) (measurement *model.Measurement, err error) { time.Sleep(1 * time.Second) return &model.Measurement{}, nil } - runner.sessionBuilder = fake + runner.newSession = fake.NewSession events := runAndCollect(runner, emitter) reduced := reduceEventsKeysIgnoreLog(events) expect := []eventKeyCount{ @@ -707,18 +753,18 @@ func TestTaskRunnerRun(t *testing.T) { runner.settings.Inputs = []string{"a", "b", "c", "d"} runner.settings.Options.MaxRuntime = 2 fake := fakeSuccessfulRun() - fake.MockableInputPolicy = func() model.InputPolicy { + fake.Builder.MockInputPolicy = func() model.InputPolicy { return model.InputStrictlyRequired } - fake.MockableInterruptible = func() bool { + fake.Builder.MockInterruptible = func() bool { return true } ctx, cancel := context.WithCancel(context.Background()) - fake.MockableMeasureWithContext = func(ctx context.Context, target model.ExperimentTarget) (measurement *model.Measurement, err error) { + fake.Experiment.MockMeasureWithContext = func(ctx context.Context, target model.ExperimentTarget) (measurement *model.Measurement, err error) { cancel() return &model.Measurement{}, nil } - runner.sessionBuilder = fake + runner.newSession = fake.NewSession events := runAndCollectContext(ctx, runner, emitter) reduced := reduceEventsKeysIgnoreLog(events) expect := []eventKeyCount{ @@ -742,13 +788,13 @@ func TestTaskRunnerRun(t *testing.T) { runner, emitter := newRunnerForTesting() runner.settings.Inputs = []string{"a"} fake := fakeSuccessfulRun() - fake.MockableInputPolicy = func() model.InputPolicy { + fake.Builder.MockInputPolicy = func() model.InputPolicy { return model.InputStrictlyRequired } - fake.MockableSubmitAndUpdateMeasurementContext = func(ctx context.Context, measurement *model.Measurement) error { + fake.Experiment.MockSubmitAndUpdateMeasurementContext = func(ctx context.Context, measurement *model.Measurement) error { return errors.New("cannot submit") } - runner.sessionBuilder = fake + runner.newSession = fake.NewSession events := runAndCollect(runner, emitter) reduced := reduceEventsKeysIgnoreLog(events) expect := []eventKeyCount{ @@ -775,14 +821,14 @@ func TestTaskRunnerRun(t *testing.T) { runner, emitter := newRunnerForTesting() fake := fakeSuccessfulRun() var callbacks model.ExperimentCallbacks - fake.MockableSetCallbacks = func(cbs model.ExperimentCallbacks) { + fake.Builder.MockSetCallbacks = func(cbs model.ExperimentCallbacks) { callbacks = cbs } - fake.MockableMeasureWithContext = func(ctx context.Context, target model.ExperimentTarget) (measurement *model.Measurement, err error) { + fake.Experiment.MockMeasureWithContext = func(ctx context.Context, target model.ExperimentTarget) (measurement *model.Measurement, err error) { callbacks.OnProgress(1, "hello from the fake experiment") return &model.Measurement{}, nil } - runner.sessionBuilder = fake + runner.newSession = fake.NewSession events := runAndCollect(runner, emitter) reduced := reduceEventsKeysIgnoreLog(events) expect := []eventKeyCount{ diff --git a/pkg/oonimkall/tasksession.go b/pkg/oonimkall/tasksession.go deleted file mode 100644 index 27b1dd6192..0000000000 --- a/pkg/oonimkall/tasksession.go +++ /dev/null @@ -1,78 +0,0 @@ -package oonimkall - -import ( - "context" - - "github.com/ooni/probe-cli/v3/internal/engine" - "github.com/ooni/probe-cli/v3/internal/kvstore" - "github.com/ooni/probe-cli/v3/internal/model" -) - -// -// This file implements taskSession and derived types. -// - -// taskKVStoreFSBuilderEngine creates a new KVStore -// using the ./internal/engine package. -type taskKVStoreFSBuilderEngine struct{} - -var _ taskKVStoreFSBuilder = &taskKVStoreFSBuilderEngine{} - -func (*taskKVStoreFSBuilderEngine) NewFS(path string) (model.KeyValueStore, error) { - return kvstore.NewFS(path) -} - -// taskSessionBuilderEngine builds a new session -// using the ./internal/engine package. -type taskSessionBuilderEngine struct{} - -var _ taskSessionBuilder = &taskSessionBuilderEngine{} - -// NewSession implements taskSessionBuilder.NewSession. -func (b *taskSessionBuilderEngine) NewSession(ctx context.Context, - config engine.SessionConfig) (taskSession, error) { - sess, err := engine.NewSession(ctx, config) - if err != nil { - return nil, err - } - return &taskSessionEngine{sess}, nil -} - -// taskSessionEngine wraps ./internal/engine's Session. -type taskSessionEngine struct { - *engine.Session -} - -var _ taskSession = &taskSessionEngine{} - -// NewExperimentBuilderByName implements -// taskSessionEngine.NewExperimentBuilderByName. -func (sess *taskSessionEngine) NewExperimentBuilderByName( - name string) (taskExperimentBuilder, error) { - builder, err := sess.NewExperimentBuilder(name) - if err != nil { - return nil, err - } - return &taskExperimentBuilderEngine{builder}, err -} - -// taskExperimentBuilderEngine wraps ./internal/engine's -// ExperimentBuilder type. -type taskExperimentBuilderEngine struct { - model.ExperimentBuilder -} - -var _ taskExperimentBuilder = &taskExperimentBuilderEngine{} - -// NewExperimentInstance implements -// taskExperimentBuilder.NewExperimentInstance. -func (b *taskExperimentBuilderEngine) NewExperimentInstance() taskExperiment { - return &taskExperimentEngine{b.NewExperiment()} -} - -// taskExperimentEngine wraps ./internal/engine's Experiment. -type taskExperimentEngine struct { - model.Experiment -} - -var _ taskExperiment = &taskExperimentEngine{} diff --git a/pkg/oonimkall/tasksession_test.go b/pkg/oonimkall/tasksession_test.go deleted file mode 100644 index f52549105d..0000000000 --- a/pkg/oonimkall/tasksession_test.go +++ /dev/null @@ -1,128 +0,0 @@ -package oonimkall - -import ( - "context" - "testing" - - "github.com/apex/log" - "github.com/ooni/probe-cli/v3/internal/engine" - "github.com/ooni/probe-cli/v3/internal/version" -) - -func TestTaskKVSToreFSBuilderEngine(t *testing.T) { - b := &taskKVStoreFSBuilderEngine{} - store, err := b.NewFS("testdata/state") - if err != nil { - t.Fatal(err) - } - if store == nil { - t.Fatal("expected non-nil store here") - } -} - -func TestTaskSessionBuilderEngine(t *testing.T) { - t.Run("NewSession", func(t *testing.T) { - t.Run("on success", func(t *testing.T) { - builder := &taskSessionBuilderEngine{} - ctx := context.Background() - config := engine.SessionConfig{ - Logger: log.Log, - SoftwareName: "ooniprobe-cli", - SoftwareVersion: version.Version, - } - sess, err := builder.NewSession(ctx, config) - if err != nil { - t.Fatal(err) - } - sess.Close() - }) - - t.Run("on failure", func(t *testing.T) { - builder := &taskSessionBuilderEngine{} - ctx := context.Background() - config := engine.SessionConfig{} - sess, err := builder.NewSession(ctx, config) - if err == nil { - t.Fatal("expected an error here") - } - if sess != nil { - t.Fatal("expected nil session here") - } - }) - }) -} - -func TestTaskSessionEngine(t *testing.T) { - - // newSession is a helper function for creating a new session. - newSession := func(t *testing.T) taskSession { - builder := &taskSessionBuilderEngine{} - ctx := context.Background() - config := engine.SessionConfig{ - Logger: log.Log, - SoftwareName: "ooniprobe-cli", - SoftwareVersion: version.Version, - } - sess, err := builder.NewSession(ctx, config) - if err != nil { - t.Fatal(err) - } - return sess - } - - t.Run("NewExperimentBuilderByName", func(t *testing.T) { - t.Run("on success", func(t *testing.T) { - sess := newSession(t) - builder, err := sess.NewExperimentBuilderByName("ndt") - if err != nil { - t.Fatal(err) - } - if builder == nil { - t.Fatal("expected non-nil builder") - } - }) - - t.Run("on failure", func(t *testing.T) { - sess := newSession(t) - builder, err := sess.NewExperimentBuilderByName("antani") - if err == nil { - t.Fatal("expected an error here") - } - if builder != nil { - t.Fatal("expected nil builder") - } - }) - }) -} - -func TestTaskExperimentBuilderEngine(t *testing.T) { - - // newBuilder is a helper function for creating a new session - // as well as a new experiment builder - newBuilder := func(t *testing.T) (taskSession, taskExperimentBuilder) { - builder := &taskSessionBuilderEngine{} - ctx := context.Background() - config := engine.SessionConfig{ - Logger: log.Log, - SoftwareName: "ooniprobe-cli", - SoftwareVersion: version.Version, - } - sess, err := builder.NewSession(ctx, config) - if err != nil { - t.Fatal(err) - } - expBuilder, err := sess.NewExperimentBuilderByName("ndt") - if err != nil { - t.Fatal(err) - } - return sess, expBuilder - } - - t.Run("NewExperiment", func(t *testing.T) { - _, builder := newBuilder(t) - exp := builder.NewExperimentInstance() - if exp == nil { - t.Fatal("expected non-nil experiment here") - } - }) -} diff --git a/pkg/oonimkall/taskutils_test.go b/pkg/oonimkall/taskutils_test.go new file mode 100644 index 0000000000..3bba84a4ed --- /dev/null +++ b/pkg/oonimkall/taskutils_test.go @@ -0,0 +1,40 @@ +package oonimkall + +// +// This file contains testing code reused by other `_test.go` files. +// + +import "sync" + +// CollectorTaskEmitter is a thread-safe taskEmitter +// that stores all the events inside itself. +type CollectorTaskEmitter struct { + // events contains the events + events []*event + + // mu provides mutual exclusion + mu sync.Mutex +} + +// ensures that a CollectorTaskEmitter is a taskEmitter. +var _ taskEmitter = &CollectorTaskEmitter{} + +// Emit implements the taskEmitter.Emit method. +func (e *CollectorTaskEmitter) Emit(key string, value interface{}) { + e.mu.Lock() + e.events = append(e.events, &event{Key: key, Value: value}) + e.mu.Unlock() +} + +// Collect returns a copy of the collected events. It is safe +// to read the events. It's a data race to modify them. +// +// After this function has been called, the internal array +// of events will now be empty. +func (e *CollectorTaskEmitter) Collect() (out []*event) { + e.mu.Lock() + out = e.events + e.events = nil + e.mu.Unlock() + return +}