diff --git a/internal/cmd/miniooni/main.go b/internal/cmd/miniooni/main.go index 52b8010..cf231d4 100644 --- a/internal/cmd/miniooni/main.go +++ b/internal/cmd/miniooni/main.go @@ -196,6 +196,13 @@ func registerOONIRun(rootCmd *cobra.Command, globalOptions *Options) { []string{}, "URL of the OONI Run v2 descriptor to run (may be specified multiple times)", ) + flags.StringSliceVarP( + &globalOptions.InputFilePaths, + "input-file", + "f", + []string{}, + "Path to the OONI Run v2 descriptor to run (may be specified multiple times)", + ) } // registerAllExperiments registers a subcommand for each experiment diff --git a/internal/cmd/miniooni/oonirun.go b/internal/cmd/miniooni/oonirun.go index eb5946d..420ddcc 100644 --- a/internal/cmd/miniooni/oonirun.go +++ b/internal/cmd/miniooni/oonirun.go @@ -6,25 +6,18 @@ package main import ( "context" + "encoding/json" "errors" + "os" "github.com/ooni/probe-cli/v3/internal/engine" "github.com/ooni/probe-cli/v3/internal/oonirun" - "github.com/ooni/probe-cli/v3/internal/runtimex" ) // ooniRunMain runs the experiments described by the given OONI Run URLs. This // function works with both v1 and v2 OONI Run URLs. func ooniRunMain(ctx context.Context, sess *engine.Session, currentOptions *Options, annotations map[string]string) { - runtimex.PanicIfTrue( - len(currentOptions.Inputs) <= 0, - "in oonirun mode you need to specify at least one URL using `-i URL`", - ) - runtimex.PanicIfTrue( - len(currentOptions.InputFilePaths) > 0, - "in oonirun mode you cannot specify any `-f FILE` file", - ) logger := sess.Logger() cfg := &oonirun.LinkConfig{ AcceptChanges: currentOptions.Yes, @@ -49,4 +42,20 @@ func ooniRunMain(ctx context.Context, continue } } + for _, filename := range currentOptions.InputFilePaths { + data, err := os.ReadFile(filename) + if err != nil { + logger.Warnf("oonirun: reading OONI Run v2 descriptor failed: %s", err.Error()) + continue + } + var descr oonirun.V2Descriptor + if err := json.Unmarshal(data, &descr); err != nil { + logger.Warnf("oonirun: parsing OONI Run v2 descriptor failed: %s", err.Error()) + continue + } + if err := oonirun.V2MeasureDescriptor(ctx, cfg, &descr); err != nil { + logger.Warnf("oonirun: running link failed: %s", err.Error()) + continue + } + } } diff --git a/internal/oonirun/v2.go b/internal/oonirun/v2.go index 0cd68fc..772971c 100644 --- a/internal/oonirun/v2.go +++ b/internal/oonirun/v2.go @@ -30,8 +30,8 @@ var ( v2CountFailedExperiments = &atomicx.Int64{} ) -// v2Descriptor describes a single nettest to run. -type v2Descriptor struct { +// V2Descriptor describes a list of nettests to run together. +type V2Descriptor struct { // Name is the name of this descriptor. Name string `json:"name"` @@ -42,11 +42,11 @@ type v2Descriptor struct { Author string `json:"author"` // Nettests contains the list of nettests to run. - Nettests []v2Nettest `json:"nettests"` + Nettests []V2Nettest `json:"nettests"` } -// v2Nettest specifies how a nettest should run. -type v2Nettest struct { +// V2Nettest specifies how a nettest should run. +type V2Nettest struct { // Inputs contains inputs for the experiment. Inputs []string `json:"inputs"` @@ -66,7 +66,7 @@ var ErrHTTPRequestFailed = errors.New("oonirun: HTTP request failed") // getV2DescriptorFromHTTPSURL GETs a v2Descriptor instance from // a static URL (e.g., from a GitHub repo or from a Gist). func getV2DescriptorFromHTTPSURL(ctx context.Context, client model.HTTPClient, - logger model.Logger, URL string) (*v2Descriptor, error) { + logger model.Logger, URL string) (*V2Descriptor, error) { template := httpx.APIClientTemplate{ Accept: "", Authorization: "", @@ -77,7 +77,7 @@ func getV2DescriptorFromHTTPSURL(ctx context.Context, client model.HTTPClient, Logger: logger, UserAgent: model.HTTPHeaderUserAgent, } - var desc v2Descriptor + var desc V2Descriptor if err := template.Build().GetJSON(ctx, "", &desc); err != nil { return nil, err } @@ -87,7 +87,7 @@ func getV2DescriptorFromHTTPSURL(ctx context.Context, client model.HTTPClient, // v2DescriptorCache contains all the known v2Descriptor entries. type v2DescriptorCache struct { // Entries contains all the cached descriptors. - Entries map[string]*v2Descriptor + Entries map[string]*V2Descriptor } // v2DescriptorCacheKey is the name of the kvstore2 entry keeping @@ -100,7 +100,7 @@ func v2DescriptorCacheLoad(fsstore model.KeyValueStore) (*v2DescriptorCache, err if err != nil { if errors.Is(err, kvstore.ErrNoSuchKey) { cache := &v2DescriptorCache{ - Entries: make(map[string]*v2Descriptor), + Entries: make(map[string]*V2Descriptor), } return cache, nil } @@ -111,7 +111,7 @@ func v2DescriptorCacheLoad(fsstore model.KeyValueStore) (*v2DescriptorCache, err return nil, err } if cache.Entries == nil { - cache.Entries = make(map[string]*v2Descriptor) + cache.Entries = make(map[string]*V2Descriptor) } return &cache, nil } @@ -139,7 +139,7 @@ func v2DescriptorCacheLoad(fsstore model.KeyValueStore) (*v2DescriptorCache, err // - err is the error that occurred, or nil in case of success. func (cache *v2DescriptorCache) PullChangesWithoutSideEffects( ctx context.Context, client model.HTTPClient, logger model.Logger, - URL string) (oldValue, newValue *v2Descriptor, err error) { + URL string) (oldValue, newValue *V2Descriptor, err error) { oldValue = cache.Entries[URL] newValue, err = getV2DescriptorFromHTTPSURL(ctx, client, logger, URL) return @@ -149,7 +149,7 @@ func (cache *v2DescriptorCache) PullChangesWithoutSideEffects( // // Note: this method modifies cache and is not safe for concurrent usage. func (cache *v2DescriptorCache) Update( - fsstore model.KeyValueStore, URL string, entry *v2Descriptor) error { + fsstore model.KeyValueStore, URL string, entry *V2Descriptor) error { cache.Entries[URL] = entry data, err := json.Marshal(cache) runtimex.PanicOnError(err, "json.Marshal failed") @@ -159,9 +159,9 @@ func (cache *v2DescriptorCache) Update( // ErrNilDescriptor indicates that we have been passed a descriptor that is nil. var ErrNilDescriptor = errors.New("oonirun: descriptor is nil") -// v2MeasureDescriptor performs the measurement or measurements +// V2MeasureDescriptor performs the measurement or measurements // described by the given list of v2Descriptor. -func v2MeasureDescriptor(ctx context.Context, config *LinkConfig, desc *v2Descriptor) error { +func V2MeasureDescriptor(ctx context.Context, config *LinkConfig, desc *V2Descriptor) error { if desc == nil { // Note: we have a test checking that we can handle a nil // descriptor, yet adding also this extra safety net feels @@ -208,7 +208,7 @@ func v2MeasureDescriptor(ctx context.Context, config *LinkConfig, desc *v2Descri var ErrNeedToAcceptChanges = errors.New("oonirun: need to accept changes") // v2DescriptorDiff shows what changed between the old and the new descriptors. -func v2DescriptorDiff(oldValue, newValue *v2Descriptor, URL string) string { +func v2DescriptorDiff(oldValue, newValue *V2Descriptor, URL string) string { oldData, err := json.MarshalIndent(oldValue, "", " ") runtimex.PanicOnError(err, "json.MarshalIndent failed unexpectedly") newData, err := json.MarshalIndent(newValue, "", " ") @@ -253,5 +253,5 @@ func v2MeasureHTTPS(ctx context.Context, config *LinkConfig, URL string) error { return err } } - return v2MeasureDescriptor(ctx, config, newValue) // handles nil newValue gracefully + return V2MeasureDescriptor(ctx, config, newValue) // handles nil newValue gracefully } diff --git a/internal/oonirun/v2_test.go b/internal/oonirun/v2_test.go index 012358b..fffbb9e 100644 --- a/internal/oonirun/v2_test.go +++ b/internal/oonirun/v2_test.go @@ -17,11 +17,11 @@ import ( func TestOONIRunV2LinkCommonCase(t *testing.T) { server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { - descriptor := &v2Descriptor{ + descriptor := &V2Descriptor{ Name: "", Description: "", Author: "", - Nettests: []v2Nettest{{ + Nettests: []V2Nettest{{ Inputs: []string{}, Options: map[string]any{ "SleepTime": int64(10 * time.Millisecond), @@ -56,11 +56,11 @@ func TestOONIRunV2LinkCommonCase(t *testing.T) { func TestOONIRunV2LinkCannotUpdateCache(t *testing.T) { server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { - descriptor := &v2Descriptor{ + descriptor := &V2Descriptor{ Name: "", Description: "", Author: "", - Nettests: []v2Nettest{{ + Nettests: []V2Nettest{{ Inputs: []string{}, Options: map[string]any{ "SleepTime": int64(10 * time.Millisecond), @@ -104,11 +104,11 @@ func TestOONIRunV2LinkCannotUpdateCache(t *testing.T) { func TestOONIRunV2LinkWithoutAcceptChanges(t *testing.T) { server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { - descriptor := &v2Descriptor{ + descriptor := &V2Descriptor{ Name: "", Description: "", Author: "", - Nettests: []v2Nettest{{ + Nettests: []V2Nettest{{ Inputs: []string{}, Options: map[string]any{ "SleepTime": int64(10 * time.Millisecond), @@ -170,11 +170,11 @@ 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{ + descriptor := &V2Descriptor{ Name: "", Description: "", Author: "", - Nettests: []v2Nettest{{ + Nettests: []V2Nettest{{ Inputs: []string{}, Options: map[string]any{ "SleepTime": int64(10 * time.Millisecond), @@ -214,7 +214,7 @@ func TestV2MeasureDescriptor(t *testing.T) { t.Run("with nil descriptor", func(t *testing.T) { ctx := context.Background() config := &LinkConfig{} - err := v2MeasureDescriptor(ctx, config, nil) + err := V2MeasureDescriptor(ctx, config, nil) if !errors.Is(err, ErrNilDescriptor) { t.Fatal("unexpected err", err) } @@ -269,17 +269,17 @@ func TestV2MeasureDescriptor(t *testing.T) { ReportFile: "", Session: sess, } - descr := &v2Descriptor{ + descr := &V2Descriptor{ Name: "", Description: "", Author: "", - Nettests: []v2Nettest{{ + Nettests: []V2Nettest{{ Inputs: []string{}, Options: map[string]any{}, TestName: "example", }}, } - err := v2MeasureDescriptor(ctx, config, descr) + err := V2MeasureDescriptor(ctx, config, descr) if err != nil { t.Fatal(err) }