From d19b5e0118fdf807e33999aa9e88ed10f83ebcc8 Mon Sep 17 00:00:00 2001 From: frcroth Date: Thu, 19 Dec 2024 12:59:42 +0100 Subject: [PATCH 1/4] Add ddr experiment --- internal/experiment/ddr/ddr.go | 174 ++++++++++++++++++++++++++++ internal/experiment/ddr/ddr_test.go | 15 +++ internal/registry/ddr.go | 28 +++++ 3 files changed, 217 insertions(+) create mode 100644 internal/experiment/ddr/ddr.go create mode 100644 internal/experiment/ddr/ddr_test.go create mode 100644 internal/registry/ddr.go diff --git a/internal/experiment/ddr/ddr.go b/internal/experiment/ddr/ddr.go new file mode 100644 index 000000000..c12ba5ac4 --- /dev/null +++ b/internal/experiment/ddr/ddr.go @@ -0,0 +1,174 @@ +package ddr + +import ( + "context" + "errors" + "fmt" + "net" + "time" + + "github.com/apex/log" + "github.com/miekg/dns" + "github.com/ooni/probe-cli/v3/internal/model" + "github.com/ooni/probe-cli/v3/internal/netxlite" +) + +const ( + testName = "ddr" + testVersion = "0.1.0" +) + +type Config struct { +} + +// Measurer performs the measurement. +type Measurer struct { + config Config +} + +// ExperimentName implements ExperimentMeasurer.ExperimentName. +func (m *Measurer) ExperimentName() string { + return testName +} + +// ExperimentVersion implements ExperimentMeasurer.ExperimentVersion. +func (m *Measurer) ExperimentVersion() string { + return testVersion +} + +type DDRResponse struct { + Priority int `json:"priority"` + Target string `json:"target"` + Keys map[string]string `json:"keys"` +} + +type TestKeys struct { + // DDRResponse is the DDR response. + DDRResponse []DDRResponse `json:"ddr_responses"` + + // SupportsDDR is true if DDR is supported. + SupportsDDR bool `json:"supports_ddr"` + + // Resolver is the resolver used (the system resolver of the host). + Resolver string `json:"resolver"` + + // Failure is the failure that occurred, or nil. + Failure *string `json:"failure"` +} + +func (m *Measurer) Run( + ctx context.Context, + args *model.ExperimentArgs) error { + + log.SetLevel(log.DebugLevel) + measurement := args.Measurement + + tk := &TestKeys{} + measurement.TestKeys = tk + + timeout := 60 * time.Second + ctx, cancel := context.WithTimeout(ctx, timeout) + defer cancel() + + systemResolver := getSystemResolverAddress() + if systemResolver == "" { + return errors.New("could not get system resolver") + } + log.Infof("Using system resolver: %s", systemResolver) + tk.Resolver = systemResolver + + // DDR queries are queries of the SVCB type for the _dns.resolver.arpa. domain. + + netx := &netxlite.Netx{} + dialer := netx.NewDialerWithoutResolver(log.Log) + transport := netxlite.NewUnwrappedDNSOverUDPTransport( + dialer, systemResolver) + encoder := &netxlite.DNSEncoderMiekg{} + query := encoder.Encode( + "_dns.resolver.arpa.", // As specified in RFC 9462 + dns.TypeSVCB, + true) + resp, err := transport.RoundTrip(ctx, query) + if err != nil { + failure := err.Error() + tk.Failure = &failure + return nil + } + + reply := &dns.Msg{} + err = reply.Unpack(resp.Bytes()) + if err != nil { + unpackError := err.Error() + tk.Failure = &unpackError + return nil + } + + ddrResponse, err := decodeResponse(reply.Answer) + + if err != nil { + decodingError := err.Error() + tk.Failure = &decodingError + } else { + tk.DDRResponse = ddrResponse + } + + tk.SupportsDDR = len(tk.DDRResponse) > 0 + + log.Infof("Gathered DDR Responses: %+v", tk.DDRResponse) + return nil +} + +// decodeResponse decodes the response from the DNS query. +// DDR is only concerned with SVCB records, so we only decode those. +func decodeResponse(responseFields []dns.RR) ([]DDRResponse, error) { + responses := make([]DDRResponse, 0) + for _, rr := range responseFields { + switch rr := rr.(type) { + case *dns.SVCB: + parsed, err := parseSvcb(rr) + if err != nil { + return nil, err + } + responses = append(responses, parsed) + default: + return nil, fmt.Errorf("unknown RR type: %T", rr) + } + } + return responses, nil +} + +func parseSvcb(rr *dns.SVCB) (DDRResponse, error) { + keys := make(map[string]string) + for _, kv := range rr.Value { + value := kv.String() + key := kv.Key().String() + keys[key] = value + } + + return DDRResponse{ + Priority: int(rr.Priority), + Target: rr.Target, + Keys: keys, + }, nil +} + +// Get the system resolver address from /etc/resolv.conf +// This should also be possible via querying the system resolver and checking the response +func getSystemResolverAddress() string { + resolverConfig, err := dns.ClientConfigFromFile("/etc/resolv.conf") + if err != nil { + return "" + } + + if len(resolverConfig.Servers) > 0 { + return net.JoinHostPort(resolverConfig.Servers[0], resolverConfig.Port) + } + + return "" +} + +func NewExperimentMeasurer(config Config) model.ExperimentMeasurer { + return &Measurer{ + config: config, + } +} diff --git a/internal/experiment/ddr/ddr_test.go b/internal/experiment/ddr/ddr_test.go new file mode 100644 index 000000000..ee848efeb --- /dev/null +++ b/internal/experiment/ddr/ddr_test.go @@ -0,0 +1,15 @@ +package ddr + +import ( + "testing" +) + +func TestMeasurerExperimentNameVersion(t *testing.T) { + measurer := NewExperimentMeasurer(Config{}) + if measurer.ExperimentName() != "ddr" { + t.Fatal("unexpected ExperimentName") + } + if measurer.ExperimentVersion() != "0.1.0" { + t.Fatal("unexpected ExperimentVersion") + } +} diff --git a/internal/registry/ddr.go b/internal/registry/ddr.go new file mode 100644 index 000000000..9ab7f8454 --- /dev/null +++ b/internal/registry/ddr.go @@ -0,0 +1,28 @@ +package registry + +// +// Registers the `ddr' experiment. +// + +import ( + "github.com/ooni/probe-cli/v3/internal/experiment/ddr" + "github.com/ooni/probe-cli/v3/internal/model" +) + +func init() { + const canonicalName = "ddr" + AllExperiments[canonicalName] = func() *Factory { + return &Factory{ + build: func(config interface{}) model.ExperimentMeasurer { + return ddr.NewExperimentMeasurer( + *config.(*ddr.Config), + ) + }, + canonicalName: canonicalName, + config: &ddr.Config{}, + enabledByDefault: true, + interruptible: true, + inputPolicy: model.InputNone, + } + } +} From 8ddec182caff7c1f9d49aa76a69d2fd10733e613 Mon Sep 17 00:00:00 2001 From: frcroth Date: Thu, 19 Dec 2024 15:59:47 +0100 Subject: [PATCH 2/4] Add tests to DDR experiment --- internal/experiment/ddr/ddr.go | 19 ++++--- internal/experiment/ddr/ddr_test.go | 78 +++++++++++++++++++++++++++++ 2 files changed, 91 insertions(+), 6 deletions(-) diff --git a/internal/experiment/ddr/ddr.go b/internal/experiment/ddr/ddr.go index c12ba5ac4..946d12178 100644 --- a/internal/experiment/ddr/ddr.go +++ b/internal/experiment/ddr/ddr.go @@ -19,6 +19,9 @@ const ( ) type Config struct { + // CustomResolver is the custom resolver to use. + // If empty, the system resolver is used. + CustomResolver *string } // Measurer performs the measurement. @@ -70,19 +73,23 @@ func (m *Measurer) Run( ctx, cancel := context.WithTimeout(ctx, timeout) defer cancel() - systemResolver := getSystemResolverAddress() - if systemResolver == "" { - return errors.New("could not get system resolver") + if m.config.CustomResolver == nil { + systemResolver := getSystemResolverAddress() + if systemResolver == "" { + return errors.New("could not get system resolver") + } + log.Infof("Using system resolver: %s", systemResolver) + tk.Resolver = systemResolver + } else { + tk.Resolver = *m.config.CustomResolver } - log.Infof("Using system resolver: %s", systemResolver) - tk.Resolver = systemResolver // DDR queries are queries of the SVCB type for the _dns.resolver.arpa. domain. netx := &netxlite.Netx{} dialer := netx.NewDialerWithoutResolver(log.Log) transport := netxlite.NewUnwrappedDNSOverUDPTransport( - dialer, systemResolver) + dialer, tk.Resolver) encoder := &netxlite.DNSEncoderMiekg{} query := encoder.Encode( "_dns.resolver.arpa.", // As specified in RFC 9462 diff --git a/internal/experiment/ddr/ddr_test.go b/internal/experiment/ddr/ddr_test.go index ee848efeb..28d930621 100644 --- a/internal/experiment/ddr/ddr_test.go +++ b/internal/experiment/ddr/ddr_test.go @@ -1,7 +1,12 @@ package ddr import ( + "context" "testing" + + "github.com/apex/log" + "github.com/ooni/probe-cli/v3/internal/mocks" + "github.com/ooni/probe-cli/v3/internal/model" ) func TestMeasurerExperimentNameVersion(t *testing.T) { @@ -13,3 +18,76 @@ func TestMeasurerExperimentNameVersion(t *testing.T) { t.Fatal("unexpected ExperimentVersion") } } + +func TestMeasurerRun(t *testing.T) { + if testing.Short() { + t.Skip("skip test in short mode") + } + + oneOneOneOneResolver := "1.1.1.1:53" + + measurer := NewExperimentMeasurer(Config{ + CustomResolver: &oneOneOneOneResolver, + }) + args := &model.ExperimentArgs{ + Callbacks: model.NewPrinterCallbacks(log.Log), + Measurement: new(model.Measurement), + Session: &mocks.Session{ + MockLogger: func() model.Logger { + return log.Log + }, + }, + } + if err := measurer.Run(context.Background(), args); err != nil { + t.Fatal(err) + } + tk := args.Measurement.TestKeys.(*TestKeys) + if tk.Failure != nil { + t.Fatal("unexpected Failure") + } + + if tk.Resolver != oneOneOneOneResolver { + t.Fatal("Resolver should be written to TestKeys") + } + + // 1.1.1.1 supports DDR + if tk.SupportsDDR != true { + t.Fatal("unexpected value for Supports DDR") + } +} + +// This test fails because the resolver is a domain name and not an IP address. +func TestMeasurerFailsWithDomainResolver(t *testing.T) { + invalidResolver := "invalid-resolver.example:53" + + tk, _ := runExperiment(invalidResolver) + if tk.Failure == nil { + t.Fatal("expected Failure") + } +} + +func TestMeasurerFailsWithNoPort(t *testing.T) { + invalidResolver := "1.1.1.1" + + tk, _ := runExperiment(invalidResolver) + if tk.Failure == nil { + t.Fatal("expected Failure") + } +} + +func runExperiment(resolver string) (*TestKeys, error) { + measurer := NewExperimentMeasurer(Config{ + CustomResolver: &resolver, + }) + args := &model.ExperimentArgs{ + Callbacks: model.NewPrinterCallbacks(log.Log), + Measurement: new(model.Measurement), + Session: &mocks.Session{ + MockLogger: func() model.Logger { + return log.Log + }, + }, + } + err := measurer.Run(context.Background(), args) + return args.Measurement.TestKeys.(*TestKeys), err +} From 8739b76162bb288cc1f322c420abb84ec4478bb6 Mon Sep 17 00:00:00 2001 From: frcroth Date: Fri, 31 Jan 2025 11:31:12 +0100 Subject: [PATCH 3/4] Use DNS data format in DDR experiment --- internal/experiment/ddr/ddr.go | 88 +++++++++++++++++++++-------- internal/experiment/ddr/ddr_test.go | 8 ++- internal/model/archival.go | 22 +++++--- 3 files changed, 85 insertions(+), 33 deletions(-) diff --git a/internal/experiment/ddr/ddr.go b/internal/experiment/ddr/ddr.go index 946d12178..81682f7c6 100644 --- a/internal/experiment/ddr/ddr.go +++ b/internal/experiment/ddr/ddr.go @@ -9,6 +9,7 @@ import ( "github.com/apex/log" "github.com/miekg/dns" + "github.com/ooni/probe-cli/v3/internal/geoipx" "github.com/ooni/probe-cli/v3/internal/model" "github.com/ooni/probe-cli/v3/internal/netxlite" ) @@ -39,22 +40,13 @@ func (m *Measurer) ExperimentVersion() string { return testVersion } -type DDRResponse struct { - Priority int `json:"priority"` - Target string `json:"target"` - Keys map[string]string `json:"keys"` -} - type TestKeys struct { - // DDRResponse is the DDR response. - DDRResponse []DDRResponse `json:"ddr_responses"` + // DNS Queries and results (as specified in https://github.com/ooni/spec/blob/master/data-formats/df-002-dnst.md#dns-data-format) + Queries model.ArchivalDNSLookupResult `json:"queries"` // SupportsDDR is true if DDR is supported. SupportsDDR bool `json:"supports_ddr"` - // Resolver is the resolver used (the system resolver of the host). - Resolver string `json:"resolver"` - // Failure is the failure that occurred, or nil. Failure *string `json:"failure"` } @@ -73,15 +65,16 @@ func (m *Measurer) Run( ctx, cancel := context.WithTimeout(ctx, timeout) defer cancel() + resolver := "" if m.config.CustomResolver == nil { systemResolver := getSystemResolverAddress() if systemResolver == "" { return errors.New("could not get system resolver") } log.Infof("Using system resolver: %s", systemResolver) - tk.Resolver = systemResolver + resolver = systemResolver } else { - tk.Resolver = *m.config.CustomResolver + resolver = *m.config.CustomResolver } // DDR queries are queries of the SVCB type for the _dns.resolver.arpa. domain. @@ -89,12 +82,13 @@ func (m *Measurer) Run( netx := &netxlite.Netx{} dialer := netx.NewDialerWithoutResolver(log.Log) transport := netxlite.NewUnwrappedDNSOverUDPTransport( - dialer, tk.Resolver) + dialer, resolver) encoder := &netxlite.DNSEncoderMiekg{} query := encoder.Encode( "_dns.resolver.arpa.", // As specified in RFC 9462 dns.TypeSVCB, true) + t0 := time.Since(measurement.MeasurementStartTimeSaved).Seconds() resp, err := transport.RoundTrip(ctx, query) if err != nil { failure := err.Error() @@ -115,20 +109,19 @@ func (m *Measurer) Run( if err != nil { decodingError := err.Error() tk.Failure = &decodingError - } else { - tk.DDRResponse = ddrResponse } + t := time.Since(measurement.MeasurementStartTimeSaved).Seconds() + tk.Queries = createResult(t, t0, tk.Failure, resp, resolver, ddrResponse) - tk.SupportsDDR = len(tk.DDRResponse) > 0 + tk.SupportsDDR = len(ddrResponse) > 0 - log.Infof("Gathered DDR Responses: %+v", tk.DDRResponse) return nil } // decodeResponse decodes the response from the DNS query. // DDR is only concerned with SVCB records, so we only decode those. -func decodeResponse(responseFields []dns.RR) ([]DDRResponse, error) { - responses := make([]DDRResponse, 0) +func decodeResponse(responseFields []dns.RR) ([]model.SVCBData, error) { + responses := make([]model.SVCBData, 0) for _, rr := range responseFields { switch rr := rr.(type) { case *dns.SVCB: @@ -144,7 +137,7 @@ func decodeResponse(responseFields []dns.RR) ([]DDRResponse, error) { return responses, nil } -func parseSvcb(rr *dns.SVCB) (DDRResponse, error) { +func parseSvcb(rr *dns.SVCB) (model.SVCBData, error) { keys := make(map[string]string) for _, kv := range rr.Value { value := kv.String() @@ -152,10 +145,10 @@ func parseSvcb(rr *dns.SVCB) (DDRResponse, error) { keys[key] = value } - return DDRResponse{ - Priority: int(rr.Priority), - Target: rr.Target, - Keys: keys, + return model.SVCBData{ + Priority: rr.Priority, + TargetName: rr.Target, + Params: keys, }, nil } @@ -174,6 +167,51 @@ func getSystemResolverAddress() string { return "" } +func createResult(t float64, t0 float64, failure *string, resp model.DNSResponse, resolver string, svcbRecords []model.SVCBData) model.ArchivalDNSLookupResult { + resolverHost, _, err := net.SplitHostPort(resolver) + if err != nil { + log.Warnf("Could not split resolver address %s: %s", resolver, err) + resolverHost = resolver + } + asn, org, err := geoipx.LookupASN(resolverHost) + if err != nil { + log.Warnf("Could not lookup ASN for resolver %s: %s", resolverHost, err) + asn = 0 + org = "" + } + + answers := make([]model.ArchivalDNSAnswer, 0) + for _, record := range svcbRecords { + // Create an ArchivalDNSAnswer for each SVCB record + // for this experiment, only the SVCB key is relevant. + answers = append(answers, model.ArchivalDNSAnswer{ + ASN: int64(asn), + ASOrgName: org, + AnswerType: "SVCB", + Hostname: "", + IPv4: "", + IPv6: "", + SVCB: &record, + }) + } + + return model.ArchivalDNSLookupResult{ + Answers: answers, + Engine: "udp", + Failure: failure, + GetaddrinfoError: 0, + Hostname: "_dns.resolver.arpa.", + QueryType: "SVCB", + RawResponse: resp.Bytes(), + Rcode: int64(resp.Rcode()), + ResolverAddress: resolverHost, + T0: t0, + T: t, + Tags: nil, + TransactionID: 0, + } +} + func NewExperimentMeasurer(config Config) model.ExperimentMeasurer { return &Measurer{ config: config, diff --git a/internal/experiment/ddr/ddr_test.go b/internal/experiment/ddr/ddr_test.go index 28d930621..766f27060 100644 --- a/internal/experiment/ddr/ddr_test.go +++ b/internal/experiment/ddr/ddr_test.go @@ -46,7 +46,13 @@ func TestMeasurerRun(t *testing.T) { t.Fatal("unexpected Failure") } - if tk.Resolver != oneOneOneOneResolver { + firstAnswer := tk.Queries.Answers[0] + + if firstAnswer.AnswerType != "SVCB" { + t.Fatal("unexpected AnswerType") + } + + if tk.Queries.ResolverAddress != oneOneOneOneResolver { t.Fatal("Resolver should be written to TestKeys") } diff --git a/internal/model/archival.go b/internal/model/archival.go index 1930dc53c..94032aead 100644 --- a/internal/model/archival.go +++ b/internal/model/archival.go @@ -198,13 +198,21 @@ type ArchivalDNSLookupResult struct { // ArchivalDNSAnswer is a DNS answer. type ArchivalDNSAnswer struct { - ASN int64 `json:"asn,omitempty"` - ASOrgName string `json:"as_org_name,omitempty"` - AnswerType string `json:"answer_type"` - Hostname string `json:"hostname,omitempty"` - IPv4 string `json:"ipv4,omitempty"` - IPv6 string `json:"ipv6,omitempty"` - TTL *uint32 `json:"ttl"` + ASN int64 `json:"asn,omitempty"` + ASOrgName string `json:"as_org_name,omitempty"` + AnswerType string `json:"answer_type"` + Hostname string `json:"hostname,omitempty"` + IPv4 string `json:"ipv4,omitempty"` + IPv6 string `json:"ipv6,omitempty"` + TTL *uint32 `json:"ttl"` + SVCB *SVCBData `json:"svcb,omitempty"` // SVCB-specific data +} + +// SVCBData represents details of an SVCB record. +type SVCBData struct { + Priority uint16 `json:"priority"` + TargetName string `json:"target_name"` + Params map[string]string `json:"params,omitempty"` // SvcParams key-value pairs } // From 27a2ebc715bd0f3476273e176e59f04450821665 Mon Sep 17 00:00:00 2001 From: frcroth Date: Tue, 4 Feb 2025 16:47:44 +0100 Subject: [PATCH 4/4] Add more tests for DDR experiment --- internal/experiment/ddr/ddr.go | 25 ++++----- internal/experiment/ddr/ddr_test.go | 84 ++++++++++++++++++++++++++++- 2 files changed, 94 insertions(+), 15 deletions(-) diff --git a/internal/experiment/ddr/ddr.go b/internal/experiment/ddr/ddr.go index 81682f7c6..cbf050c2e 100644 --- a/internal/experiment/ddr/ddr.go +++ b/internal/experiment/ddr/ddr.go @@ -51,10 +51,7 @@ type TestKeys struct { Failure *string `json:"failure"` } -func (m *Measurer) Run( - ctx context.Context, - args *model.ExperimentArgs) error { - +func (m *Measurer) Run(ctx context.Context, args *model.ExperimentArgs) error { log.SetLevel(log.DebugLevel) measurement := args.Measurement @@ -77,20 +74,22 @@ func (m *Measurer) Run( resolver = *m.config.CustomResolver } - // DDR queries are queries of the SVCB type for the _dns.resolver.arpa. domain. - netx := &netxlite.Netx{} dialer := netx.NewDialerWithoutResolver(log.Log) - transport := netxlite.NewUnwrappedDNSOverUDPTransport( - dialer, resolver) + transport := netxlite.NewUnwrappedDNSOverUDPTransport(dialer, resolver) encoder := &netxlite.DNSEncoderMiekg{} - query := encoder.Encode( - "_dns.resolver.arpa.", // As specified in RFC 9462 - dns.TypeSVCB, - true) + // As specified in RFC 9462 a DDR Query is a SVCB query for the _dns.resolver.arpa. domain + query := encoder.Encode("_dns.resolver.arpa.", dns.TypeSVCB, true) t0 := time.Since(measurement.MeasurementStartTimeSaved).Seconds() + resp, err := transport.RoundTrip(ctx, query) if err != nil { + // Since we are using a custom transport, we need to check for context errors manually + if errors.Is(err, context.Canceled) || errors.Is(err, context.DeadlineExceeded) { + failure := "interrupted" + tk.Failure = &failure + return nil + } failure := err.Error() tk.Failure = &failure return nil @@ -105,14 +104,12 @@ func (m *Measurer) Run( } ddrResponse, err := decodeResponse(reply.Answer) - if err != nil { decodingError := err.Error() tk.Failure = &decodingError } t := time.Since(measurement.MeasurementStartTimeSaved).Seconds() tk.Queries = createResult(t, t0, tk.Failure, resp, resolver, ddrResponse) - tk.SupportsDDR = len(ddrResponse) > 0 return nil diff --git a/internal/experiment/ddr/ddr_test.go b/internal/experiment/ddr/ddr_test.go index 766f27060..b150d1287 100644 --- a/internal/experiment/ddr/ddr_test.go +++ b/internal/experiment/ddr/ddr_test.go @@ -3,6 +3,7 @@ package ddr import ( "context" "testing" + "time" "github.com/apex/log" "github.com/ooni/probe-cli/v3/internal/mocks" @@ -52,7 +53,7 @@ func TestMeasurerRun(t *testing.T) { t.Fatal("unexpected AnswerType") } - if tk.Queries.ResolverAddress != oneOneOneOneResolver { + if tk.Queries.ResolverAddress != "1.1.1.1" { t.Fatal("Resolver should be written to TestKeys") } @@ -81,6 +82,87 @@ func TestMeasurerFailsWithNoPort(t *testing.T) { } } +func TestMeasurerFailsWithInvalidResolver(t *testing.T) { + invalidResolver := "256.256.256.256:53" + + tk, _ := runExperiment(invalidResolver) + if tk.Failure == nil { + t.Fatal("expected Failure") + } +} + +func TestMeasurerRunWithSystemResolver(t *testing.T) { + if testing.Short() { + t.Skip("skip test in short mode") + } + + measurer := NewExperimentMeasurer(Config{}) + args := &model.ExperimentArgs{ + Callbacks: model.NewPrinterCallbacks(log.Log), + Measurement: new(model.Measurement), + Session: &mocks.Session{ + MockLogger: func() model.Logger { + return log.Log + }, + }, + } + if err := measurer.Run(context.Background(), args); err != nil { + t.Fatal(err) + } + tk := args.Measurement.TestKeys.(*TestKeys) + if tk.Failure != nil { + t.Fatal("unexpected Failure") + } +} + +func TestMeasurerRunWithCancelledContext(t *testing.T) { + ctx, cancel := context.WithCancel(context.Background()) + cancel() // immediately cancel the context + + measurer := NewExperimentMeasurer(Config{}) + args := &model.ExperimentArgs{ + Callbacks: model.NewPrinterCallbacks(log.Log), + Measurement: new(model.Measurement), + Session: &mocks.Session{ + MockLogger: func() model.Logger { + return log.Log + }, + }, + } + err := measurer.Run(ctx, args) + if err != nil { + t.Fatal("expected no error due to cancelled context") + } + tk := args.Measurement.TestKeys.(*TestKeys) + if tk.Failure == nil || *tk.Failure != "interrupted" { + t.Fatal("expected interrupted failure") + } +} + +func TestMeasurerRunWithTimeout(t *testing.T) { + ctx, cancel := context.WithTimeout(context.Background(), 1*time.Nanosecond) + defer cancel() + + measurer := NewExperimentMeasurer(Config{}) + args := &model.ExperimentArgs{ + Callbacks: model.NewPrinterCallbacks(log.Log), + Measurement: new(model.Measurement), + Session: &mocks.Session{ + MockLogger: func() model.Logger { + return log.Log + }, + }, + } + err := measurer.Run(ctx, args) + if err != nil { + t.Fatal("expected no error due to context timeout") + } + tk := args.Measurement.TestKeys.(*TestKeys) + if tk.Failure == nil || *tk.Failure != "interrupted" { + t.Fatal("expected interrupted failure") + } +} + func runExperiment(resolver string) (*TestKeys, error) { measurer := NewExperimentMeasurer(Config{ CustomResolver: &resolver,