diff --git a/internal/minipipeline/analysis.go b/internal/minipipeline/analysis.go index 4c5b36187..0906a4e02 100644 --- a/internal/minipipeline/analysis.go +++ b/internal/minipipeline/analysis.go @@ -51,7 +51,7 @@ func NewLinearWebAnalysis(input *WebObservationsContainer) (output []*WebObserva output = append(output, entry) } - // sort in descending order + // sort using complex sorting rule sort.SliceStable(output, func(i, j int) bool { left, right := output[i], output[j] diff --git a/internal/minipipeline/sorting.go b/internal/minipipeline/sorting.go index fad2388a9..414ba96d1 100644 --- a/internal/minipipeline/sorting.go +++ b/internal/minipipeline/sorting.go @@ -10,13 +10,13 @@ import ( // timing dependent churn when generating testcases for the minipipeline. func SortDNSLookupResults(inputs []*model.ArchivalDNSLookupResult) (outputs []*model.ArchivalDNSLookupResult) { // copy the original slice - outputs = append(outputs, inputs...) + outputs = append([]*model.ArchivalDNSLookupResult{}, inputs...) // sort using complex sorting rule sort.SliceStable(outputs, func(i, j int) bool { left, right := outputs[i], outputs[j] - // we sort groups by resolver type to avoid the churn caused by parallel runs. + // we sort groups by resolver type to avoid the churn caused by parallel runs if left.Engine < right.Engine { return true } @@ -48,12 +48,13 @@ func SortDNSLookupResults(inputs []*model.ArchivalDNSLookupResult) (outputs []*m // SortNetworkEvents is like [SortDNSLookupResults] but for network events. func SortNetworkEvents(inputs []*model.ArchivalNetworkEvent) (outputs []*model.ArchivalNetworkEvent) { // copy the original slice - outputs = append(outputs, inputs...) + outputs = append([]*model.ArchivalNetworkEvent{}, inputs...) // sort using complex sorting rule sort.SliceStable(outputs, func(i, j int) bool { left, right := outputs[i], outputs[j] + // we sort by endpoint address to significantly reduce the churn if left.Address < right.Address { return true } @@ -61,7 +62,16 @@ func SortNetworkEvents(inputs []*model.ArchivalNetworkEvent) (outputs []*model.A return false } - return left.TransactionID < right.TransactionID + // if the address is the same, then we group by transaction + if left.TransactionID < right.TransactionID { + return true + } + if left.TransactionID > right.TransactionID { + return false + } + + // with same transaction, we sort by increasing time + return left.T < right.T }) return @@ -71,19 +81,19 @@ func SortNetworkEvents(inputs []*model.ArchivalNetworkEvent) (outputs []*model.A func SortTCPConnectResults( inputs []*model.ArchivalTCPConnectResult) (outputs []*model.ArchivalTCPConnectResult) { // copy the original slice - outputs = append(outputs, inputs...) + outputs = append([]*model.ArchivalTCPConnectResult{}, inputs...) // sort using complex sorting rule sort.SliceStable(outputs, func(i, j int) bool { left, right := outputs[i], outputs[j] + // we sort by endpoint address to significantly reduce the churn if left.IP < right.IP { return true } if left.IP > right.IP { return false } - if left.Port < right.Port { return true } @@ -91,7 +101,16 @@ func SortTCPConnectResults( return false } - return left.TransactionID < right.TransactionID + // if the address is the same, then we group by transaction + if left.TransactionID < right.TransactionID { + return true + } + if left.TransactionID > right.TransactionID { + return false + } + + // with same transaction, we sort by increasing time + return left.T < right.T }) return @@ -101,7 +120,7 @@ func SortTCPConnectResults( func SortTLSHandshakeResults( inputs []*model.ArchivalTLSOrQUICHandshakeResult) (outputs []*model.ArchivalTLSOrQUICHandshakeResult) { // copy the original slice - outputs = append(outputs, inputs...) + outputs = append([]*model.ArchivalTLSOrQUICHandshakeResult{}, inputs...) // sort using complex sorting rule sort.SliceStable(outputs, func(i, j int) bool { @@ -114,7 +133,16 @@ func SortTLSHandshakeResults( return false } - return left.TransactionID < right.TransactionID + // if the address is the same, then we group by transaction + if left.TransactionID < right.TransactionID { + return true + } + if left.TransactionID > right.TransactionID { + return false + } + + // with same transaction, we sort by increasing time + return left.T < right.T }) return diff --git a/internal/minipipeline/sorting_test.go b/internal/minipipeline/sorting_test.go index f2c205620..ec45c8ed9 100644 --- a/internal/minipipeline/sorting_test.go +++ b/internal/minipipeline/sorting_test.go @@ -12,21 +12,117 @@ func TestSortDNSLookupResults(t *testing.T) { return &s } - inputGen := func() []*model.ArchivalDNSLookupResult { - return []*model.ArchivalDNSLookupResult{ + type testcase struct { + name string + inputGen func() []*model.ArchivalDNSLookupResult + expect []*model.ArchivalDNSLookupResult + } + + cases := []testcase{{ + name: "with nil input", + inputGen: func() []*model.ArchivalDNSLookupResult { + return nil + }, + expect: []*model.ArchivalDNSLookupResult{}, + }, { + name: "with empty input", + inputGen: func() []*model.ArchivalDNSLookupResult { + return []*model.ArchivalDNSLookupResult{} + }, + expect: []*model.ArchivalDNSLookupResult{}, + }, { + name: "with good input", + inputGen: func() []*model.ArchivalDNSLookupResult { + return []*model.ArchivalDNSLookupResult{ + { + Engine: "udp", + Failure: newfailurestring("dns_no_answer"), + QueryType: "AAAA", + ResolverAddress: "1.1.1.1:53", + TransactionID: 5, + }, + { + Engine: "udp", + Failure: nil, + QueryType: "A", + ResolverAddress: "1.1.1.1:53", + TransactionID: 5, + }, + { + Engine: "udp", + Failure: newfailurestring("dns_no_answer"), + QueryType: "AAAA", + ResolverAddress: "8.8.8.8:53", + TransactionID: 3, + }, + { + Engine: "udp", + Failure: nil, + QueryType: "A", + ResolverAddress: "8.8.8.8:53", + TransactionID: 3, + }, + { + Engine: "doh", + Failure: newfailurestring("dns_no_answer"), + QueryType: "AAAA", + ResolverAddress: "https://dns.google/dns-query", + TransactionID: 2, + }, + { + Engine: "doh", + Failure: nil, + QueryType: "A", + ResolverAddress: "https://dns.google/dns-query", + TransactionID: 2, + }, + { + Engine: "getaddrinfo", + QueryType: "ANY", + Failure: nil, + TransactionID: 1, + }, + { + Engine: "getaddrinfo", + QueryType: "ANY", + Failure: nil, + TransactionID: 7, + }, + } + }, + expect: []*model.ArchivalDNSLookupResult{ { - Engine: "udp", + Engine: "doh", + Failure: nil, + QueryType: "A", + ResolverAddress: "https://dns.google/dns-query", + TransactionID: 2, + }, + { + Engine: "doh", Failure: newfailurestring("dns_no_answer"), QueryType: "AAAA", - ResolverAddress: "1.1.1.1:53", - TransactionID: 5, + ResolverAddress: "https://dns.google/dns-query", + TransactionID: 2, + }, + { + Engine: "getaddrinfo", + QueryType: "ANY", + Failure: nil, + TransactionID: 1, + }, + { + Engine: "getaddrinfo", + QueryType: "ANY", + Failure: nil, + TransactionID: 7, }, { Engine: "udp", Failure: nil, QueryType: "A", - ResolverAddress: "1.1.1.1:53", - TransactionID: 5, + ResolverAddress: "8.8.8.8:53", + TransactionID: 3, }, { Engine: "udp", @@ -39,95 +135,528 @@ func TestSortDNSLookupResults(t *testing.T) { Engine: "udp", Failure: nil, QueryType: "A", - ResolverAddress: "8.8.8.8:53", - TransactionID: 3, + ResolverAddress: "1.1.1.1:53", + TransactionID: 5, }, { - Engine: "doh", + Engine: "udp", Failure: newfailurestring("dns_no_answer"), QueryType: "AAAA", - ResolverAddress: "https://dns.google/dns-query", - TransactionID: 2, + ResolverAddress: "1.1.1.1:53", + TransactionID: 5, }, + }, + }} + + for _, tc := range cases { + t.Run(tc.name, func(t *testing.T) { + input := tc.inputGen() + output := SortDNSLookupResults(input) + + t.Run("the input should not have mutated", func(t *testing.T) { + if diff := cmp.Diff(tc.inputGen(), input); diff != "" { + t.Fatal(diff) + } + }) + + t.Run("the output should be consistent with expectations", func(t *testing.T) { + if diff := cmp.Diff(tc.expect, output); diff != "" { + t.Fatal(diff) + } + }) + }) + } +} + +func TestSortNetworkEvents(t *testing.T) { + newfailurestring := func(s string) *string { + return &s + } + + type testcase struct { + name string + inputGen func() []*model.ArchivalNetworkEvent + expect []*model.ArchivalNetworkEvent + } + + cases := []testcase{{ + name: "with nil input", + inputGen: func() []*model.ArchivalNetworkEvent { + return nil + }, + expect: []*model.ArchivalNetworkEvent{}, + }, { + name: "with empty input", + inputGen: func() []*model.ArchivalNetworkEvent { + return []*model.ArchivalNetworkEvent{} + }, + expect: []*model.ArchivalNetworkEvent{}, + }, { + name: "with good input", + inputGen: func() []*model.ArchivalNetworkEvent { + return []*model.ArchivalNetworkEvent{ + { + Address: "8.8.8.8:443", + Failure: newfailurestring("connection_reset"), + Operation: "read", + T: 1.1, + TransactionID: 5, + }, + { + Address: "8.8.8.8:443", + Failure: nil, + Operation: "write", + T: 1.0, + TransactionID: 5, + }, + { + Address: "1.1.1.1:443", + Failure: newfailurestring("eof_error"), + Operation: "read", + T: 0.9, + TransactionID: 3, + }, + { + Address: "1.1.1.1:443", + Failure: nil, + Operation: "write", + T: 0.4, + TransactionID: 3, + }, + { + Address: "8.8.8.8:443", + Failure: nil, + Operation: "write", + T: 1.4, + TransactionID: 2, + }, + { + Address: "8.8.8.8:443", + Failure: nil, + Operation: "read", + T: 1.5, + TransactionID: 2, + }, + { + Address: "8.8.8.8:443", + Failure: nil, + Operation: "write", + T: 1.4, + TransactionID: 3, + }, + { + Address: "8.8.8.8:443", + Failure: nil, + Operation: "read", + T: 1.5, + TransactionID: 3, + }, + } + }, + expect: []*model.ArchivalNetworkEvent{ { - Engine: "doh", - Failure: nil, - QueryType: "A", - ResolverAddress: "https://dns.google/dns-query", - TransactionID: 2, + Address: "1.1.1.1:443", + Failure: nil, + Operation: "write", + T: 0.4, + TransactionID: 3, }, { - Engine: "getaddrinfo", - QueryType: "ANY", + Address: "1.1.1.1:443", + Failure: newfailurestring("eof_error"), + Operation: "read", + T: 0.9, + TransactionID: 3, + }, + { + Address: "8.8.8.8:443", Failure: nil, - TransactionID: 1, + Operation: "write", + T: 1.4, + TransactionID: 2, + }, + { + Address: "8.8.8.8:443", + Failure: nil, + Operation: "read", + T: 1.5, + TransactionID: 2, + }, + { + Address: "8.8.8.8:443", + Failure: nil, + Operation: "write", + T: 1.4, + TransactionID: 3, + }, + { + Address: "8.8.8.8:443", + Failure: nil, + Operation: "read", + T: 1.5, + TransactionID: 3, + }, + { + Address: "8.8.8.8:443", + Failure: nil, + Operation: "write", + T: 1.0, + TransactionID: 5, }, - } + { + Address: "8.8.8.8:443", + Failure: newfailurestring("connection_reset"), + Operation: "read", + T: 1.1, + TransactionID: 5, + }, + }, + }} + + for _, tc := range cases { + t.Run(tc.name, func(t *testing.T) { + input := tc.inputGen() + output := SortNetworkEvents(input) + + t.Run("the input should not have mutated", func(t *testing.T) { + if diff := cmp.Diff(tc.inputGen(), input); diff != "" { + t.Fatal(diff) + } + }) + + t.Run("the output should be consistent with expectations", func(t *testing.T) { + if diff := cmp.Diff(tc.expect, output); diff != "" { + t.Fatal(diff) + } + }) + }) + } +} + +func TestSortTCPConnectResults(t *testing.T) { + newfailurestring := func(s string) *string { + return &s + } + + type testcase struct { + name string + inputGen func() []*model.ArchivalTCPConnectResult + expect []*model.ArchivalTCPConnectResult } - expect := []*model.ArchivalDNSLookupResult{ - { - Engine: "doh", - Failure: nil, - QueryType: "A", - ResolverAddress: "https://dns.google/dns-query", - TransactionID: 2, - }, - { - Engine: "doh", - Failure: newfailurestring("dns_no_answer"), - QueryType: "AAAA", - ResolverAddress: "https://dns.google/dns-query", - TransactionID: 2, - }, - { - Engine: "getaddrinfo", - QueryType: "ANY", - Failure: nil, - TransactionID: 1, - }, - { - Engine: "udp", - Failure: nil, - QueryType: "A", - ResolverAddress: "8.8.8.8:53", - TransactionID: 3, - }, - { - Engine: "udp", - Failure: newfailurestring("dns_no_answer"), - QueryType: "AAAA", - ResolverAddress: "8.8.8.8:53", - TransactionID: 3, - }, - { - Engine: "udp", - Failure: nil, - QueryType: "A", - ResolverAddress: "1.1.1.1:53", - TransactionID: 5, - }, - { - Engine: "udp", - Failure: newfailurestring("dns_no_answer"), - QueryType: "AAAA", - ResolverAddress: "1.1.1.1:53", - TransactionID: 5, + cases := []testcase{{ + name: "with nil input", + inputGen: func() []*model.ArchivalTCPConnectResult { + return nil + }, + expect: []*model.ArchivalTCPConnectResult{}, + }, { + name: "with empty input", + inputGen: func() []*model.ArchivalTCPConnectResult { + return []*model.ArchivalTCPConnectResult{} + }, + expect: []*model.ArchivalTCPConnectResult{}, + }, { + name: "with good input", + inputGen: func() []*model.ArchivalTCPConnectResult { + return []*model.ArchivalTCPConnectResult{ + { + IP: "1.1.1.1", + Port: 443, + Status: model.ArchivalTCPConnectStatus{ + Failure: newfailurestring("connection_reset"), + }, + T: 0.9, + TransactionID: 7, + }, + { + IP: "8.8.8.8", + Port: 443, + Status: model.ArchivalTCPConnectStatus{ + Failure: newfailurestring("connection_reset"), + }, + T: 1.1, + TransactionID: 5, + }, + { + IP: "8.8.8.8", + Port: 80, + Status: model.ArchivalTCPConnectStatus{ + Failure: newfailurestring("connection_reset"), + }, + T: 1.1, + TransactionID: 5, + }, + { + IP: "1.1.1.1", + Port: 443, + Status: model.ArchivalTCPConnectStatus{ + Failure: newfailurestring("connection_reset"), + }, + T: 0.9, + TransactionID: 3, + }, + { + IP: "8.8.8.8", + Port: 443, + Status: model.ArchivalTCPConnectStatus{ + Failure: nil, + }, + T: 1.4, + TransactionID: 2, + }, + { + IP: "8.8.8.8", + Port: 443, + Status: model.ArchivalTCPConnectStatus{ + Failure: nil, + }, + T: 1.4, + TransactionID: 3, + }, + { + IP: "8.8.8.8", + Port: 80, + Status: model.ArchivalTCPConnectStatus{ + Failure: newfailurestring("connection_reset"), + }, + T: 5.1, + TransactionID: 5, + }, + } }, + expect: []*model.ArchivalTCPConnectResult{ + { + IP: "1.1.1.1", + Port: 443, + Status: model.ArchivalTCPConnectStatus{ + Failure: newfailurestring("connection_reset"), + }, + T: 0.9, + TransactionID: 3, + }, + { + IP: "1.1.1.1", + Port: 443, + Status: model.ArchivalTCPConnectStatus{ + Failure: newfailurestring("connection_reset"), + }, + T: 0.9, + TransactionID: 7, + }, + { + IP: "8.8.8.8", + Port: 80, + Status: model.ArchivalTCPConnectStatus{ + Failure: newfailurestring("connection_reset"), + }, + T: 1.1, + TransactionID: 5, + }, + { + IP: "8.8.8.8", + Port: 80, + Status: model.ArchivalTCPConnectStatus{ + Failure: newfailurestring("connection_reset"), + }, + T: 5.1, + TransactionID: 5, + }, + { + IP: "8.8.8.8", + Port: 443, + Status: model.ArchivalTCPConnectStatus{ + Failure: nil, + }, + T: 1.4, + TransactionID: 2, + }, + { + IP: "8.8.8.8", + Port: 443, + Status: model.ArchivalTCPConnectStatus{ + Failure: nil, + }, + T: 1.4, + TransactionID: 3, + }, + { + IP: "8.8.8.8", + Port: 443, + Status: model.ArchivalTCPConnectStatus{ + Failure: newfailurestring("connection_reset"), + }, + T: 1.1, + TransactionID: 5, + }, + }, + }} + + for _, tc := range cases { + t.Run(tc.name, func(t *testing.T) { + input := tc.inputGen() + output := SortTCPConnectResults(input) + + t.Run("the input should not have mutated", func(t *testing.T) { + if diff := cmp.Diff(tc.inputGen(), input); diff != "" { + t.Fatal(diff) + } + }) + + t.Run("the output should be consistent with expectations", func(t *testing.T) { + if diff := cmp.Diff(tc.expect, output); diff != "" { + t.Fatal(diff) + } + }) + }) } +} - input := inputGen() - output := SortDNSLookupResults(input) +func TestSortQUICTLSHandshakeResults(t *testing.T) { + newfailurestring := func(s string) *string { + return &s + } - t.Run("the input should not have mutated", func(t *testing.T) { - if diff := cmp.Diff(inputGen(), input); diff != "" { - t.Fatal(diff) - } - }) + type testcase struct { + name string + inputGen func() []*model.ArchivalTLSOrQUICHandshakeResult + expect []*model.ArchivalTLSOrQUICHandshakeResult + } - t.Run("the output should be consistent with expectations", func(t *testing.T) { - if diff := cmp.Diff(expect, output); diff != "" { - t.Fatal(diff) - } - }) + cases := []testcase{{ + name: "with nil input", + inputGen: func() []*model.ArchivalTLSOrQUICHandshakeResult { + return nil + }, + expect: []*model.ArchivalTLSOrQUICHandshakeResult{}, + }, { + name: "with empty input", + inputGen: func() []*model.ArchivalTLSOrQUICHandshakeResult { + return []*model.ArchivalTLSOrQUICHandshakeResult{} + }, + expect: []*model.ArchivalTLSOrQUICHandshakeResult{}, + }, { + name: "with good input", + inputGen: func() []*model.ArchivalTLSOrQUICHandshakeResult { + return []*model.ArchivalTLSOrQUICHandshakeResult{ + { + Address: "8.8.8.8:443", + Failure: newfailurestring("connection_reset"), + T: 1.1, + TransactionID: 5, + }, + { + Address: "8.8.8.8:443", + Failure: nil, + T: 1.0, + TransactionID: 5, + }, + { + Address: "1.1.1.1:443", + Failure: newfailurestring("eof_error"), + T: 0.9, + TransactionID: 3, + }, + { + Address: "1.1.1.1:443", + Failure: nil, + T: 0.4, + TransactionID: 3, + }, + { + Address: "8.8.8.8:443", + Failure: nil, + T: 1.4, + TransactionID: 2, + }, + { + Address: "8.8.8.8:443", + Failure: nil, + T: 1.5, + TransactionID: 2, + }, + { + Address: "8.8.8.8:443", + Failure: nil, + T: 1.4, + TransactionID: 3, + }, + { + Address: "8.8.8.8:443", + Failure: nil, + T: 1.5, + TransactionID: 3, + }, + } + }, + expect: []*model.ArchivalTLSOrQUICHandshakeResult{ + { + Address: "1.1.1.1:443", + Failure: nil, + T: 0.4, + TransactionID: 3, + }, + { + Address: "1.1.1.1:443", + Failure: newfailurestring("eof_error"), + T: 0.9, + TransactionID: 3, + }, + { + Address: "8.8.8.8:443", + Failure: nil, + T: 1.4, + TransactionID: 2, + }, + { + Address: "8.8.8.8:443", + Failure: nil, + T: 1.5, + TransactionID: 2, + }, + { + Address: "8.8.8.8:443", + Failure: nil, + T: 1.4, + TransactionID: 3, + }, + { + Address: "8.8.8.8:443", + Failure: nil, + T: 1.5, + TransactionID: 3, + }, + { + Address: "8.8.8.8:443", + Failure: nil, + T: 1.0, + TransactionID: 5, + }, + { + Address: "8.8.8.8:443", + Failure: newfailurestring("connection_reset"), + T: 1.1, + TransactionID: 5, + }, + }, + }} + + for _, tc := range cases { + t.Run(tc.name, func(t *testing.T) { + input := tc.inputGen() + output := SortTLSHandshakeResults(input) + + t.Run("the input should not have mutated", func(t *testing.T) { + if diff := cmp.Diff(tc.inputGen(), input); diff != "" { + t.Fatal(diff) + } + }) + + t.Run("the output should be consistent with expectations", func(t *testing.T) { + if diff := cmp.Diff(tc.expect, output); diff != "" { + t.Fatal(diff) + } + }) + }) + } }