refactor: introduce and use InputOrStaticDefault (#632)
This commit introduces a new `InputLoader` policy by which, if no input is provided, we use a static default input list. We also modify the code to use this policy for dnscheck and stunreachability, with proper input. We also modify `miniooni` to pass the new `ExperimentName` field to the `InputLoader` to indicate which default input list to use. This diff is part of a set of diffs aiming at fixing https://github.com/ooni/probe/issues/1814 and has been extracted from https://github.com/ooni/probe-cli/pull/539. What remains to be done, after this diff has landed is to ensure things also work for ooniprobe and oonimkall.
This commit is contained in:
parent
13414e0abc
commit
2044b78a5a
|
@ -416,10 +416,11 @@ func MainWithConfiguration(experimentName string, currentOptions Options) {
|
||||||
OnWiFi: true, // meaning: not on 4G
|
OnWiFi: true, // meaning: not on 4G
|
||||||
Charging: true,
|
Charging: true,
|
||||||
},
|
},
|
||||||
InputPolicy: builder.InputPolicy(),
|
ExperimentName: experimentName,
|
||||||
StaticInputs: currentOptions.Inputs,
|
InputPolicy: builder.InputPolicy(),
|
||||||
SourceFiles: currentOptions.InputFilePaths,
|
StaticInputs: currentOptions.Inputs,
|
||||||
Session: sess,
|
SourceFiles: currentOptions.InputFilePaths,
|
||||||
|
Session: sess,
|
||||||
}
|
}
|
||||||
inputs, err := inputLoader.Load(context.Background())
|
inputs, err := inputLoader.Load(context.Background())
|
||||||
fatalOnError(err, "cannot load inputs")
|
fatalOnError(err, "cannot load inputs")
|
||||||
|
|
|
@ -49,7 +49,7 @@ var experimentsByName = map[string]func(*Session) *ExperimentBuilder{
|
||||||
))
|
))
|
||||||
},
|
},
|
||||||
config: &dnscheck.Config{},
|
config: &dnscheck.Config{},
|
||||||
inputPolicy: InputStrictlyRequired,
|
inputPolicy: InputOrStaticDefault,
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
|
||||||
|
@ -198,7 +198,7 @@ var experimentsByName = map[string]func(*Session) *ExperimentBuilder{
|
||||||
))
|
))
|
||||||
},
|
},
|
||||||
config: &stunreachability.Config{},
|
config: &stunreachability.Config{},
|
||||||
inputPolicy: InputStrictlyRequired,
|
inputPolicy: InputOrStaticDefault,
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
|
||||||
|
|
|
@ -36,6 +36,11 @@ const (
|
||||||
// InputNone indicates that the experiment does not want any
|
// InputNone indicates that the experiment does not want any
|
||||||
// input and ignores the input if provided with it.
|
// input and ignores the input if provided with it.
|
||||||
InputNone = InputPolicy("none")
|
InputNone = InputPolicy("none")
|
||||||
|
|
||||||
|
// We gather input from StaticInput and SourceFiles. If there is
|
||||||
|
// input, we return it. Otherwise, we return an internal static
|
||||||
|
// list of inputs to be used with this experiment.
|
||||||
|
InputOrStaticDefault = InputPolicy("or_static_default")
|
||||||
)
|
)
|
||||||
|
|
||||||
// ExperimentBuilder is an experiment builder.
|
// ExperimentBuilder is an experiment builder.
|
||||||
|
@ -183,10 +188,18 @@ func (b *ExperimentBuilder) NewExperiment() *Experiment {
|
||||||
|
|
||||||
// canonicalizeExperimentName allows code to provide experiment names
|
// canonicalizeExperimentName allows code to provide experiment names
|
||||||
// in a more flexible way, where we have aliases.
|
// in a more flexible way, where we have aliases.
|
||||||
|
//
|
||||||
|
// Because we allow for uppercase experiment names for backwards
|
||||||
|
// compatibility with MK, we need to add some exceptions here when
|
||||||
|
// mapping (e.g., DNSCheck => dnscheck).
|
||||||
func canonicalizeExperimentName(name string) string {
|
func canonicalizeExperimentName(name string) string {
|
||||||
switch name = strcase.ToSnake(name); name {
|
switch name = strcase.ToSnake(name); name {
|
||||||
case "ndt_7":
|
case "ndt_7":
|
||||||
name = "ndt" // since 2020-03-18, we use ndt7 to implement ndt by default
|
name = "ndt" // since 2020-03-18, we use ndt7 to implement ndt by default
|
||||||
|
case "dns_check":
|
||||||
|
name = "dnscheck"
|
||||||
|
case "stun_reachability":
|
||||||
|
name = "stunreachability"
|
||||||
default:
|
default:
|
||||||
}
|
}
|
||||||
return name
|
return name
|
||||||
|
|
|
@ -6,10 +6,12 @@ import (
|
||||||
"errors"
|
"errors"
|
||||||
"fmt"
|
"fmt"
|
||||||
"io/fs"
|
"io/fs"
|
||||||
|
"net/url"
|
||||||
|
|
||||||
"github.com/apex/log"
|
"github.com/apex/log"
|
||||||
"github.com/ooni/probe-cli/v3/internal/engine/model"
|
"github.com/ooni/probe-cli/v3/internal/engine/model"
|
||||||
"github.com/ooni/probe-cli/v3/internal/fsx"
|
"github.com/ooni/probe-cli/v3/internal/fsx"
|
||||||
|
"github.com/ooni/probe-cli/v3/internal/stuninput"
|
||||||
)
|
)
|
||||||
|
|
||||||
// These errors are returned by the InputLoader.
|
// These errors are returned by the InputLoader.
|
||||||
|
@ -18,6 +20,7 @@ var (
|
||||||
ErrDetectedEmptyFile = errors.New("file did not contain any input")
|
ErrDetectedEmptyFile = errors.New("file did not contain any input")
|
||||||
ErrInputRequired = errors.New("no input provided")
|
ErrInputRequired = errors.New("no input provided")
|
||||||
ErrNoInputExpected = errors.New("we did not expect any input")
|
ErrNoInputExpected = errors.New("we did not expect any input")
|
||||||
|
ErrNoStaticInput = errors.New("no static input for this experiment")
|
||||||
)
|
)
|
||||||
|
|
||||||
// InputLoaderSession is the session according to an InputLoader. We
|
// InputLoaderSession is the session according to an InputLoader. We
|
||||||
|
@ -58,6 +61,12 @@ type InputLoaderLogger interface {
|
||||||
// input, we return it. Otherwise, we use OONI's probe services
|
// input, we return it. Otherwise, we use OONI's probe services
|
||||||
// to gather input using the best API for the task.
|
// to gather input using the best API for the task.
|
||||||
//
|
//
|
||||||
|
// InputOrStaticDefault
|
||||||
|
//
|
||||||
|
// We gather input from StaticInput and SourceFiles. If there is
|
||||||
|
// input, we return it. Otherwise, we return an internal static
|
||||||
|
// list of inputs to be used with this experiment.
|
||||||
|
//
|
||||||
// InputStrictlyRequired
|
// InputStrictlyRequired
|
||||||
//
|
//
|
||||||
// We gather input from StaticInput and SourceFiles. If there is
|
// We gather input from StaticInput and SourceFiles. If there is
|
||||||
|
@ -69,6 +78,10 @@ type InputLoader struct {
|
||||||
// will set them to a default value.
|
// will set them to a default value.
|
||||||
CheckInConfig *model.CheckInConfig
|
CheckInConfig *model.CheckInConfig
|
||||||
|
|
||||||
|
// ExperimentName is the name of the experiment. This field
|
||||||
|
// is only used together with the InputOrStaticDefault policy.
|
||||||
|
ExperimentName string
|
||||||
|
|
||||||
// InputPolicy specifies the input policy for the
|
// InputPolicy specifies the input policy for the
|
||||||
// current experiment. We will not load any input if
|
// current experiment. We will not load any input if
|
||||||
// the policy says we should not. You MUST fill in
|
// the policy says we should not. You MUST fill in
|
||||||
|
@ -105,6 +118,8 @@ func (il *InputLoader) Load(ctx context.Context) ([]model.URLInfo, error) {
|
||||||
return il.loadOrQueryBackend(ctx)
|
return il.loadOrQueryBackend(ctx)
|
||||||
case InputStrictlyRequired:
|
case InputStrictlyRequired:
|
||||||
return il.loadStrictlyRequired(ctx)
|
return il.loadStrictlyRequired(ctx)
|
||||||
|
case InputOrStaticDefault:
|
||||||
|
return il.loadOrStaticDefault(ctx)
|
||||||
default:
|
default:
|
||||||
return il.loadNone()
|
return il.loadNone()
|
||||||
}
|
}
|
||||||
|
@ -147,6 +162,59 @@ func (il *InputLoader) loadOrQueryBackend(ctx context.Context) ([]model.URLInfo,
|
||||||
return il.loadRemote(ctx)
|
return il.loadRemote(ctx)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// TODO(https://github.com/ooni/probe/issues/1390): we need to
|
||||||
|
// implement serving DNSCheck targets from the API
|
||||||
|
var dnsCheckDefaultInput = []string{
|
||||||
|
"https://dns.google/dns-query",
|
||||||
|
"https://8.8.8.8/dns-query",
|
||||||
|
"dot://8.8.8.8:853/",
|
||||||
|
"dot://8.8.4.4:853/",
|
||||||
|
"https://8.8.4.4/dns-query",
|
||||||
|
"https://cloudflare-dns.com/dns-query",
|
||||||
|
"https://1.1.1.1/dns-query",
|
||||||
|
"https://1.0.0.1/dns-query",
|
||||||
|
"dot://1.1.1.1:853/",
|
||||||
|
"dot://1.0.0.1:853/",
|
||||||
|
"https://dns.quad9.net/dns-query",
|
||||||
|
"https://9.9.9.9/dns-query",
|
||||||
|
"dot://9.9.9.9:853/",
|
||||||
|
"dot://dns.quad9.net/",
|
||||||
|
}
|
||||||
|
|
||||||
|
var stunReachabilityDefaultInput = stuninput.AsnStunReachabilityInput()
|
||||||
|
|
||||||
|
// staticBareInputForExperiment returns the list of strings an
|
||||||
|
// experiment should use as static input. In case there is no
|
||||||
|
// static input for this experiment, we return an error.
|
||||||
|
func staticBareInputForExperiment(name string) ([]string, error) {
|
||||||
|
// Implementation note: we may be called from pkg/oonimkall
|
||||||
|
// with a non-canonical experiment name, so we need to convert
|
||||||
|
// the experiment name to be canonical before proceeding.
|
||||||
|
switch canonicalizeExperimentName(name) {
|
||||||
|
case "dnscheck":
|
||||||
|
return dnsCheckDefaultInput, nil
|
||||||
|
case "stunreachability":
|
||||||
|
return stunReachabilityDefaultInput, nil
|
||||||
|
default:
|
||||||
|
return nil, ErrNoStaticInput
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// staticInputForExperiment returns the static input for the given experiment
|
||||||
|
// or an error if there's no static input for the experiment.
|
||||||
|
func staticInputForExperiment(name string) ([]model.URLInfo, error) {
|
||||||
|
return stringListToModelURLInfo(staticBareInputForExperiment(name))
|
||||||
|
}
|
||||||
|
|
||||||
|
// loadOrStaticDefault implements the InputOrStaticDefault policy.
|
||||||
|
func (il *InputLoader) loadOrStaticDefault(ctx context.Context) ([]model.URLInfo, error) {
|
||||||
|
inputs, err := il.loadLocal()
|
||||||
|
if err != nil || len(inputs) > 0 {
|
||||||
|
return inputs, err
|
||||||
|
}
|
||||||
|
return staticInputForExperiment(il.ExperimentName)
|
||||||
|
}
|
||||||
|
|
||||||
// loadLocal loads inputs from StaticInputs and SourceFiles.
|
// loadLocal loads inputs from StaticInputs and SourceFiles.
|
||||||
func (il *InputLoader) loadLocal() ([]model.URLInfo, error) {
|
func (il *InputLoader) loadLocal() ([]model.URLInfo, error) {
|
||||||
inputs := []model.URLInfo{}
|
inputs := []model.URLInfo{}
|
||||||
|
@ -264,3 +332,26 @@ func (il *InputLoader) logger() InputLoaderLogger {
|
||||||
}
|
}
|
||||||
return log.Log
|
return log.Log
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// stringListToModelURLInfo is an utility function to convert
|
||||||
|
// a list of strings containing URLs into a list of model.URLInfo
|
||||||
|
// which would have been returned by an hypothetical backend
|
||||||
|
// API serving input for a test for which we don't have an API
|
||||||
|
// yet (e.g., stunreachability and dnscheck).
|
||||||
|
func stringListToModelURLInfo(input []string, err error) ([]model.URLInfo, error) {
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
var output []model.URLInfo
|
||||||
|
for _, URL := range input {
|
||||||
|
if _, err := url.Parse(URL); err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
output = append(output, model.URLInfo{
|
||||||
|
CategoryCode: "MISC", // hard to find a category
|
||||||
|
CountryCode: "XX", // representing no country
|
||||||
|
URL: URL,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
return output, nil
|
||||||
|
}
|
||||||
|
|
|
@ -6,6 +6,7 @@ import (
|
||||||
"io"
|
"io"
|
||||||
"io/fs"
|
"io/fs"
|
||||||
"os"
|
"os"
|
||||||
|
"strings"
|
||||||
"syscall"
|
"syscall"
|
||||||
"testing"
|
"testing"
|
||||||
|
|
||||||
|
@ -206,6 +207,134 @@ func TestInputLoaderInputStrictlyRequiredWithEmptyFile(t *testing.T) {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func TestInputLoaderInputOrStaticDefaultWithInput(t *testing.T) {
|
||||||
|
il := &InputLoader{
|
||||||
|
ExperimentName: "dnscheck",
|
||||||
|
StaticInputs: []string{"https://www.google.com/"},
|
||||||
|
SourceFiles: []string{
|
||||||
|
"testdata/inputloader1.txt",
|
||||||
|
"testdata/inputloader2.txt",
|
||||||
|
},
|
||||||
|
InputPolicy: InputOrStaticDefault,
|
||||||
|
}
|
||||||
|
ctx := context.Background()
|
||||||
|
out, err := il.Load(ctx)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
if len(out) != 5 {
|
||||||
|
t.Fatal("not the output length we expected")
|
||||||
|
}
|
||||||
|
expect := []model.URLInfo{
|
||||||
|
{URL: "https://www.google.com/"},
|
||||||
|
{URL: "https://www.x.org/"},
|
||||||
|
{URL: "https://www.slashdot.org/"},
|
||||||
|
{URL: "https://abc.xyz/"},
|
||||||
|
{URL: "https://run.ooni.io/"},
|
||||||
|
}
|
||||||
|
if diff := cmp.Diff(out, expect); diff != "" {
|
||||||
|
t.Fatal(diff)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestInputLoaderInputOrStaticDefaultWithEmptyFile(t *testing.T) {
|
||||||
|
il := &InputLoader{
|
||||||
|
ExperimentName: "dnscheck",
|
||||||
|
InputPolicy: InputOrStaticDefault,
|
||||||
|
SourceFiles: []string{
|
||||||
|
"testdata/inputloader1.txt",
|
||||||
|
"testdata/inputloader3.txt", // we want it before inputloader2.txt
|
||||||
|
"testdata/inputloader2.txt",
|
||||||
|
},
|
||||||
|
}
|
||||||
|
ctx := context.Background()
|
||||||
|
out, err := il.Load(ctx)
|
||||||
|
if !errors.Is(err, ErrDetectedEmptyFile) {
|
||||||
|
t.Fatalf("not the error we expected: %+v", err)
|
||||||
|
}
|
||||||
|
if out != nil {
|
||||||
|
t.Fatal("not the output we expected")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestInputLoaderInputOrStaticDefaultWithoutInputDNSCheck(t *testing.T) {
|
||||||
|
il := &InputLoader{
|
||||||
|
ExperimentName: "dnscheck",
|
||||||
|
InputPolicy: InputOrStaticDefault,
|
||||||
|
}
|
||||||
|
ctx := context.Background()
|
||||||
|
out, err := il.Load(ctx)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
if len(out) != len(dnsCheckDefaultInput) {
|
||||||
|
t.Fatal("invalid output length")
|
||||||
|
}
|
||||||
|
for idx := 0; idx < len(dnsCheckDefaultInput); idx++ {
|
||||||
|
e := out[idx]
|
||||||
|
if e.CategoryCode != "MISC" {
|
||||||
|
t.Fatal("invalid category code")
|
||||||
|
}
|
||||||
|
if e.CountryCode != "XX" {
|
||||||
|
t.Fatal("invalid country code")
|
||||||
|
}
|
||||||
|
if e.URL != dnsCheckDefaultInput[idx] {
|
||||||
|
t.Fatal("invalid URL")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestInputLoaderInputOrStaticDefaultWithoutInputStunReachability(t *testing.T) {
|
||||||
|
il := &InputLoader{
|
||||||
|
ExperimentName: "stunreachability",
|
||||||
|
InputPolicy: InputOrStaticDefault,
|
||||||
|
}
|
||||||
|
ctx := context.Background()
|
||||||
|
out, err := il.Load(ctx)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
if len(out) != len(stunReachabilityDefaultInput) {
|
||||||
|
t.Fatal("invalid output length")
|
||||||
|
}
|
||||||
|
for idx := 0; idx < len(stunReachabilityDefaultInput); idx++ {
|
||||||
|
e := out[idx]
|
||||||
|
if e.CategoryCode != "MISC" {
|
||||||
|
t.Fatal("invalid category code")
|
||||||
|
}
|
||||||
|
if e.CountryCode != "XX" {
|
||||||
|
t.Fatal("invalid country code")
|
||||||
|
}
|
||||||
|
if e.URL != stunReachabilityDefaultInput[idx] {
|
||||||
|
t.Fatal("invalid URL")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestStaticBareInputForExperimentWorksWithNonCanonicalNames(t *testing.T) {
|
||||||
|
names := []string{"DNSCheck", "STUNReachability"}
|
||||||
|
for _, name := range names {
|
||||||
|
if _, err := staticInputForExperiment(name); err != nil {
|
||||||
|
t.Fatal("failure for", name, ":", err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestInputLoaderInputOrStaticDefaultWithoutInputOtherName(t *testing.T) {
|
||||||
|
il := &InputLoader{
|
||||||
|
ExperimentName: "xx",
|
||||||
|
InputPolicy: InputOrStaticDefault,
|
||||||
|
}
|
||||||
|
ctx := context.Background()
|
||||||
|
out, err := il.Load(ctx)
|
||||||
|
if !errors.Is(err, ErrNoStaticInput) {
|
||||||
|
t.Fatal("not the error we expected", err)
|
||||||
|
}
|
||||||
|
if out != nil {
|
||||||
|
t.Fatal("expected nil result here")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
func TestInputLoaderInputOrQueryBackendWithInput(t *testing.T) {
|
func TestInputLoaderInputOrQueryBackendWithInput(t *testing.T) {
|
||||||
il := &InputLoader{
|
il := &InputLoader{
|
||||||
StaticInputs: []string{"https://www.google.com/"},
|
StaticInputs: []string{"https://www.google.com/"},
|
||||||
|
@ -494,3 +623,59 @@ func TestInputLoaderLoggerWorksAsIntended(t *testing.T) {
|
||||||
t.Fatal("logger not working as intended")
|
t.Fatal("logger not working as intended")
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func TestStringListToModelURLInfoWithValidInput(t *testing.T) {
|
||||||
|
input := []string{
|
||||||
|
"stun://stun.voip.blackberry.com:3478",
|
||||||
|
"stun://stun.altar.com.pl:3478",
|
||||||
|
}
|
||||||
|
output, err := stringListToModelURLInfo(input, nil)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
if len(input) != len(output) {
|
||||||
|
t.Fatal("unexpected output length")
|
||||||
|
}
|
||||||
|
for idx := 0; idx < len(input); idx++ {
|
||||||
|
if input[idx] != output[idx].URL {
|
||||||
|
t.Fatal("unexpected entry")
|
||||||
|
}
|
||||||
|
if output[idx].CategoryCode != "MISC" {
|
||||||
|
t.Fatal("unexpected category")
|
||||||
|
}
|
||||||
|
if output[idx].CountryCode != "XX" {
|
||||||
|
t.Fatal("unexpected country")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestStringListToModelURLInfoWithInvalidInput(t *testing.T) {
|
||||||
|
input := []string{
|
||||||
|
"stun://stun.voip.blackberry.com:3478",
|
||||||
|
"\t", // <- not a valid URL
|
||||||
|
"stun://stun.altar.com.pl:3478",
|
||||||
|
}
|
||||||
|
output, err := stringListToModelURLInfo(input, nil)
|
||||||
|
if err == nil || !strings.HasSuffix(err.Error(), "invalid control character in URL") {
|
||||||
|
t.Fatal("no the error we expected", err)
|
||||||
|
}
|
||||||
|
if output != nil {
|
||||||
|
t.Fatal("unexpected nil output")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestStringListToModelURLInfoWithError(t *testing.T) {
|
||||||
|
input := []string{
|
||||||
|
"stun://stun.voip.blackberry.com:3478",
|
||||||
|
"\t",
|
||||||
|
"stun://stun.altar.com.pl:3478",
|
||||||
|
}
|
||||||
|
expected := errors.New("mocked error")
|
||||||
|
output, err := stringListToModelURLInfo(input, expected)
|
||||||
|
if !errors.Is(err, expected) {
|
||||||
|
t.Fatal("no the error we expected", err)
|
||||||
|
}
|
||||||
|
if output != nil {
|
||||||
|
t.Fatal("unexpected nil output")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
Loading…
Reference in New Issue
Block a user