From 52212f432342dc7338dbb8e05d4037884619055a Mon Sep 17 00:00:00 2001 From: David Anderson Date: Fri, 28 Jul 2023 10:39:04 -0700 Subject: [PATCH] all: update exp/slices and fix call sites slices.SortFunc suffered a late-in-cycle API breakage. Updates #cleanup Signed-off-by: David Anderson --- cmd/derper/depaware.txt | 2 +- cmd/netlogfmt/main.go | 5 ++-- cmd/tailscale/cli/exitnode.go | 9 ++++--- cmd/tailscale/depaware.txt | 2 +- go.mod | 6 ++--- go.sum | 12 ++++----- ipn/ipnlocal/peerapi.go | 4 +-- ipn/ipnlocal/profiles.go | 4 +-- net/dns/manager.go | 16 ++++++------ net/dns/recursive/recursive_test.go | 12 ++++----- net/dnsfallback/dnsfallback.go | 5 ++-- net/tsaddr/tsaddr.go | 8 ++---- util/cmpx/cmpx.go | 37 ++++++++++++++++++++++++++++ wgengine/magicsock/magicsock_test.go | 8 +++--- wgengine/router/ifconfig_windows.go | 6 ++--- wgengine/router/router_linux_test.go | 5 ++-- 16 files changed, 91 insertions(+), 50 deletions(-) diff --git a/cmd/derper/depaware.txt b/cmd/derper/depaware.txt index 7dad61739..1ee5a4757 100644 --- a/cmd/derper/depaware.txt +++ b/cmd/derper/depaware.txt @@ -47,7 +47,7 @@ tailscale.com/cmd/derper dependencies: (generated by github.com/tailscale/depawa L github.com/vishvananda/netns from github.com/tailscale/netlink+ github.com/x448/float16 from github.com/fxamacker/cbor/v2 💣 go4.org/mem from tailscale.com/client/tailscale+ - go4.org/netipx from tailscale.com/wgengine/filter + go4.org/netipx from tailscale.com/wgengine/filter+ W 💣 golang.zx2c4.com/wireguard/windows/tunnel/winipcfg from tailscale.com/net/interfaces+ google.golang.org/protobuf/encoding/prototext from github.com/golang/protobuf/proto+ google.golang.org/protobuf/encoding/protowire from github.com/golang/protobuf/proto+ diff --git a/cmd/netlogfmt/main.go b/cmd/netlogfmt/main.go index 0d1b1667e..296469327 100644 --- a/cmd/netlogfmt/main.go +++ b/cmd/netlogfmt/main.go @@ -45,6 +45,7 @@ import ( "golang.org/x/exp/slices" "tailscale.com/types/logid" "tailscale.com/types/netlogtype" + "tailscale.com/util/cmpx" "tailscale.com/util/must" ) @@ -151,10 +152,10 @@ func printMessage(msg message) { if len(traffic) == 0 { return } - slices.SortFunc(traffic, func(x, y netlogtype.ConnectionCounts) bool { + slices.SortFunc(traffic, func(x, y netlogtype.ConnectionCounts) int { nx := x.TxPackets + x.TxBytes + x.RxPackets + x.RxBytes ny := y.TxPackets + y.TxBytes + y.RxPackets + y.RxBytes - return nx > ny + return cmpx.Compare(ny, nx) }) var sum netlogtype.Counts for _, cc := range traffic { diff --git a/cmd/tailscale/cli/exitnode.go b/cmd/tailscale/cli/exitnode.go index 39a9cd5de..3ad5b93b3 100644 --- a/cmd/tailscale/cli/exitnode.go +++ b/cmd/tailscale/cli/exitnode.go @@ -18,6 +18,7 @@ import ( "golang.org/x/exp/slices" "tailscale.com/ipn/ipnstate" "tailscale.com/tailcfg" + "tailscale.com/util/cmpx" ) var exitNodeCmd = &ffcli.Command{ @@ -227,19 +228,21 @@ func filterFormatAndSortExitNodes(peers []*ipnstate.PeerStatus, filterBy string) // sortPeersByPriority sorts a slice of PeerStatus // by location.Priority, in order of highest priority. func sortPeersByPriority(peers []*ipnstate.PeerStatus) { - slices.SortFunc(peers, func(a, b *ipnstate.PeerStatus) bool { return a.Location.Priority > b.Location.Priority }) + slices.SortStableFunc(peers, func(a, b *ipnstate.PeerStatus) int { + return cmpx.Compare(b.Location.Priority, a.Location.Priority) + }) } // sortByCityName sorts a slice of filteredCity alphabetically // by name. The '-' used to indicate no location data will always // be sorted to the front of the slice. func sortByCityName(cities []*filteredCity) { - slices.SortFunc(cities, func(a, b *filteredCity) bool { return a.Name < b.Name }) + slices.SortStableFunc(cities, func(a, b *filteredCity) int { return strings.Compare(a.Name, b.Name) }) } // sortByCountryName sorts a slice of filteredCountry alphabetically // by name. The '-' used to indicate no location data will always // be sorted to the front of the slice. func sortByCountryName(countries []*filteredCountry) { - slices.SortFunc(countries, func(a, b *filteredCountry) bool { return a.Name < b.Name }) + slices.SortStableFunc(countries, func(a, b *filteredCountry) int { return strings.Compare(a.Name, b.Name) }) } diff --git a/cmd/tailscale/depaware.txt b/cmd/tailscale/depaware.txt index d22a34bde..c7dc13eab 100644 --- a/cmd/tailscale/depaware.txt +++ b/cmd/tailscale/depaware.txt @@ -52,7 +52,7 @@ tailscale.com/cmd/tailscale dependencies: (generated by github.com/tailscale/dep L github.com/vishvananda/netns from github.com/tailscale/netlink+ github.com/x448/float16 from github.com/fxamacker/cbor/v2 💣 go4.org/mem from tailscale.com/derp+ - go4.org/netipx from tailscale.com/wgengine/filter + go4.org/netipx from tailscale.com/wgengine/filter+ W 💣 golang.zx2c4.com/wireguard/windows/tunnel/winipcfg from tailscale.com/net/interfaces+ gopkg.in/yaml.v2 from sigs.k8s.io/yaml k8s.io/client-go/util/homedir from tailscale.com/cmd/tailscale/cli diff --git a/go.mod b/go.mod index 97314f32a..0b8593230 100644 --- a/go.mod +++ b/go.mod @@ -73,10 +73,10 @@ require ( github.com/vishvananda/netns v0.0.4 go.uber.org/zap v1.24.0 go4.org/mem v0.0.0-20220726221520-4f986261bf13 - go4.org/netipx v0.0.0-20230303233057-f1b76eb4bb35 + go4.org/netipx v0.0.0-20230728180743-ad4cb58a6516 golang.org/x/crypto v0.11.0 - golang.org/x/exp v0.0.0-20230425010034-47ecfdc1ba53 - golang.org/x/mod v0.10.0 + golang.org/x/exp v0.0.0-20230725093048-515e97ebf090 + golang.org/x/mod v0.11.0 golang.org/x/net v0.10.0 golang.org/x/oauth2 v0.7.0 golang.org/x/sync v0.2.0 diff --git a/go.sum b/go.sum index ba3507e27..2de14ce05 100644 --- a/go.sum +++ b/go.sum @@ -1183,8 +1183,8 @@ go.uber.org/zap v1.24.0 h1:FiJd5l1UOLj0wCgbSE0rwwXHzEdAZS6hiiSnxJN/D60= go.uber.org/zap v1.24.0/go.mod h1:2kMP+WWQ8aoFoedH3T2sq6iJ2yDWpHbP0f6MQbS9Gkg= go4.org/mem v0.0.0-20220726221520-4f986261bf13 h1:CbZeCBZ0aZj8EfVgnqQcYZgf0lpZ3H9rmp5nkDTAst8= go4.org/mem v0.0.0-20220726221520-4f986261bf13/go.mod h1:reUoABIJ9ikfM5sgtSF3Wushcza7+WeD01VB9Lirh3g= -go4.org/netipx v0.0.0-20230303233057-f1b76eb4bb35 h1:nJAwRlGWZZDOD+6wni9KVUNHMpHko/OnRwsrCYeAzPo= -go4.org/netipx v0.0.0-20230303233057-f1b76eb4bb35/go.mod h1:TQvodOM+hJTioNQJilmLXu08JNb8i+ccq418+KWu1/Y= +go4.org/netipx v0.0.0-20230728180743-ad4cb58a6516 h1:X66ZEoMN2SuaoI/dfZVYobB6E5zjZyyHUMWlCA7MgGE= +go4.org/netipx v0.0.0-20230728180743-ad4cb58a6516/go.mod h1:TQvodOM+hJTioNQJilmLXu08JNb8i+ccq418+KWu1/Y= golang.org/x/arch v0.1.0/go.mod h1:5om86z9Hs0C8fWVUuoMHwpExlXzs5Tkyp9hOrfG7pp8= golang.org/x/crypto v0.0.0-20180904163835-0709b304e793/go.mod h1:6SG95UA2DQfeDnfUPMdvaQW0Q7yPrPDi9nlGo2tz2b4= golang.org/x/crypto v0.0.0-20181029021203-45a5f77698d3/go.mod h1:6SG95UA2DQfeDnfUPMdvaQW0Q7yPrPDi9nlGo2tz2b4= @@ -1222,8 +1222,8 @@ golang.org/x/exp v0.0.0-20191227195350-da58074b4299/go.mod h1:2RIsYlXP63K8oxa1u0 golang.org/x/exp v0.0.0-20200119233911-0405dc783f0a/go.mod h1:2RIsYlXP63K8oxa1u096TMicItID8zy7Y6sNkU49FU4= golang.org/x/exp v0.0.0-20200207192155-f17229e696bd/go.mod h1:J/WKrq2StrnmMY6+EHIKF9dgMWnmCNThgcyBT1FY9mM= golang.org/x/exp v0.0.0-20200224162631-6cc2880d07d6/go.mod h1:3jZMyOhIsHpP37uCMkUooju7aAi5cS1Q23tOzKc+0MU= -golang.org/x/exp v0.0.0-20230425010034-47ecfdc1ba53 h1:5llv2sWeaMSnA3w2kS57ouQQ4pudlXrR0dCgw51QK9o= -golang.org/x/exp v0.0.0-20230425010034-47ecfdc1ba53/go.mod h1:V1LtkGg67GoY2N1AnLN78QLrzxkLyJw7RJb1gzOOz9w= +golang.org/x/exp v0.0.0-20230725093048-515e97ebf090 h1:Di6/M8l0O2lCLc6VVRWhgCiApHV8MnQurBnFSHsQtNY= +golang.org/x/exp v0.0.0-20230725093048-515e97ebf090/go.mod h1:FXUEEKJgO7OQYeo8N01OfiKP8RXMtf6e8aTskBGqWdc= golang.org/x/exp/typeparams v0.0.0-20220428152302-39d4317da171/go.mod h1:AbB0pIl9nAr9wVwH+Z2ZpaocVmF5I4GyWCDIsVjR0bk= golang.org/x/exp/typeparams v0.0.0-20230203172020-98cc5a0785f9/go.mod h1:AbB0pIl9nAr9wVwH+Z2ZpaocVmF5I4GyWCDIsVjR0bk= golang.org/x/exp/typeparams v0.0.0-20230425010034-47ecfdc1ba53 h1:w/MOPdQ1IoYoDou3L55ZbTx2Nhn7JAhX1BBZor8qChU= @@ -1261,8 +1261,8 @@ golang.org/x/mod v0.6.0-dev.0.20220419223038-86c51ed26bb4/go.mod h1:jJ57K6gSWd91 golang.org/x/mod v0.6.0/go.mod h1:4mET923SAdbXp2ki8ey+zGs1SLqsuM2Y0uvdZR/fUNI= golang.org/x/mod v0.7.0/go.mod h1:iBbtSCu2XBx23ZKBPSOrRkjjQPZFPuis4dIYUhu/chs= golang.org/x/mod v0.8.0/go.mod h1:iBbtSCu2XBx23ZKBPSOrRkjjQPZFPuis4dIYUhu/chs= -golang.org/x/mod v0.10.0 h1:lFO9qtOdlre5W1jxS3r/4szv2/6iXxScdzjoBMXNhYk= -golang.org/x/mod v0.10.0/go.mod h1:iBbtSCu2XBx23ZKBPSOrRkjjQPZFPuis4dIYUhu/chs= +golang.org/x/mod v0.11.0 h1:bUO06HqtnRcc/7l71XBe4WcqTZ+3AH1J59zWDDwLKgU= +golang.org/x/mod v0.11.0/go.mod h1:iBbtSCu2XBx23ZKBPSOrRkjjQPZFPuis4dIYUhu/chs= golang.org/x/net v0.0.0-20180724234803-3673e40ba225/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= golang.org/x/net v0.0.0-20180826012351-8a410e7b638d/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= golang.org/x/net v0.0.0-20180906233101-161cd47e91fd/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= diff --git a/ipn/ipnlocal/peerapi.go b/ipn/ipnlocal/peerapi.go index 5c5436a19..40314da38 100644 --- a/ipn/ipnlocal/peerapi.go +++ b/ipn/ipnlocal/peerapi.go @@ -902,8 +902,8 @@ func (h *peerAPIHandler) handleServeSockStats(w http.ResponseWriter, r *http.Req for label := range stats.Stats { labels = append(labels, label) } - slices.SortFunc(labels, func(a, b sockstats.Label) bool { - return a.String() < b.String() + slices.SortFunc(labels, func(a, b sockstats.Label) int { + return strings.Compare(a.String(), b.String()) }) txTotal := uint64(0) diff --git a/ipn/ipnlocal/profiles.go b/ipn/ipnlocal/profiles.go index f831a4093..a38bccab0 100644 --- a/ipn/ipnlocal/profiles.go +++ b/ipn/ipnlocal/profiles.go @@ -298,8 +298,8 @@ func (pm *profileManager) writePrefsToStore(key ipn.StateKey, prefs ipn.PrefsVie // Profiles returns the list of known profiles. func (pm *profileManager) Profiles() []ipn.LoginProfile { profiles := pm.matchingProfiles(func(*ipn.LoginProfile) bool { return true }) - slices.SortFunc(profiles, func(a, b *ipn.LoginProfile) bool { - return a.Name < b.Name + slices.SortFunc(profiles, func(a, b *ipn.LoginProfile) int { + return strings.Compare(a.Name, b.Name) }) out := make([]ipn.LoginProfile, 0, len(profiles)) for _, p := range profiles { diff --git a/net/dns/manager.go b/net/dns/manager.go index d1aa73ca6..95f0908ca 100644 --- a/net/dns/manager.go +++ b/net/dns/manager.go @@ -12,6 +12,7 @@ import ( "net" "net/netip" "runtime" + "strings" "sync/atomic" "time" @@ -139,14 +140,15 @@ func compileHostEntries(cfg Config) (hosts []*HostEntry) { } } } - slices.SortFunc(hosts, func(a, b *HostEntry) bool { - if len(a.Hosts) == 0 { - return false + slices.SortFunc(hosts, func(a, b *HostEntry) int { + if len(a.Hosts) == 0 && len(b.Hosts) == 0 { + return 0 + } else if len(a.Hosts) == 0 { + return -1 + } else if len(b.Hosts) == 0 { + return 1 } - if len(b.Hosts) == 0 { - return true - } - return a.Hosts[0] < b.Hosts[0] + return strings.Compare(a.Hosts[0], b.Hosts[0]) }) return hosts } diff --git a/net/dns/recursive/recursive_test.go b/net/dns/recursive/recursive_test.go index 04f765249..681252997 100644 --- a/net/dns/recursive/recursive_test.go +++ b/net/dns/recursive/recursive_test.go @@ -366,8 +366,8 @@ func TestBasicRecursion(t *testing.T) { netip.MustParseAddr("2600:9000:a602:b1e6:86d:8165:5e8c:295b"), netip.MustParseAddr("2600:9000:a51d:27c1:1530:b9ef:2a6:b9e5"), } - slices.SortFunc(addrs, func(x, y netip.Addr) bool { return x.String() < y.String() }) - slices.SortFunc(wantAddrs, func(x, y netip.Addr) bool { return x.String() < y.String() }) + slices.SortFunc(addrs, func(x, y netip.Addr) int { return strings.Compare(x.String(), y.String()) }) + slices.SortFunc(wantAddrs, func(x, y netip.Addr) int { return strings.Compare(x.String(), y.String()) }) if !reflect.DeepEqual(addrs, wantAddrs) { t.Errorf("got addrs=%+v; want %+v", addrs, wantAddrs) @@ -485,8 +485,8 @@ func TestRecursionCNAME(t *testing.T) { netip.MustParseAddr("13.248.141.131"), netip.MustParseAddr("2600:9000:a602:b1e6:86d:8165:5e8c:295b"), } - slices.SortFunc(addrs, func(x, y netip.Addr) bool { return x.String() < y.String() }) - slices.SortFunc(wantAddrs, func(x, y netip.Addr) bool { return x.String() < y.String() }) + slices.SortFunc(addrs, func(x, y netip.Addr) int { return strings.Compare(x.String(), y.String()) }) + slices.SortFunc(wantAddrs, func(x, y netip.Addr) int { return strings.Compare(x.String(), y.String()) }) if !reflect.DeepEqual(addrs, wantAddrs) { t.Errorf("got addrs=%+v; want %+v", addrs, wantAddrs) @@ -590,8 +590,8 @@ func TestRecursionNoGlue(t *testing.T) { netip.MustParseAddr("13.248.141.131"), netip.MustParseAddr("2600:9000:a602:b1e6:86d:8165:5e8c:295b"), } - slices.SortFunc(addrs, func(x, y netip.Addr) bool { return x.String() < y.String() }) - slices.SortFunc(wantAddrs, func(x, y netip.Addr) bool { return x.String() < y.String() }) + slices.SortFunc(addrs, func(x, y netip.Addr) int { return strings.Compare(x.String(), y.String()) }) + slices.SortFunc(wantAddrs, func(x, y netip.Addr) int { return strings.Compare(x.String(), y.String()) }) if !reflect.DeepEqual(addrs, wantAddrs) { t.Errorf("got addrs=%+v; want %+v", addrs, wantAddrs) diff --git a/net/dnsfallback/dnsfallback.go b/net/dnsfallback/dnsfallback.go index fcb2d1367..3393f5e10 100644 --- a/net/dnsfallback/dnsfallback.go +++ b/net/dnsfallback/dnsfallback.go @@ -22,6 +22,7 @@ import ( "sync/atomic" "time" + "go4.org/netipx" "golang.org/x/exp/slices" "tailscale.com/atomicfile" "tailscale.com/envknob" @@ -76,11 +77,11 @@ func MakeLookupFunc(logf logger.Logf, netMon *netmon.Monitor) func(ctx context.C metricRecursiveErrors.Add(1) return } - slices.SortFunc(addrs, func(a, b netip.Addr) bool { return a.Less(b) }) + slices.SortFunc(addrs, netipx.CompareAddr) // Wait for a response from the main function oldAddrs := <-addrsCh - slices.SortFunc(oldAddrs, func(a, b netip.Addr) bool { return a.Less(b) }) + slices.SortFunc(oldAddrs, netipx.CompareAddr) matches := slices.Equal(addrs, oldAddrs) diff --git a/net/tsaddr/tsaddr.go b/net/tsaddr/tsaddr.go index 34259b690..566e9716c 100644 --- a/net/tsaddr/tsaddr.go +++ b/net/tsaddr/tsaddr.go @@ -10,6 +10,7 @@ import ( "net/netip" "sync" + "go4.org/netipx" "golang.org/x/exp/slices" "tailscale.com/net/netaddr" ) @@ -252,12 +253,7 @@ func ExitRoutes() []netip.Prefix { return []netip.Prefix{allIPv4, allIPv6} } // SortPrefixes sorts the prefixes in place. func SortPrefixes(p []netip.Prefix) { - slices.SortFunc(p, func(ri, rj netip.Prefix) bool { - if ri.Addr() == rj.Addr() { - return ri.Bits() < rj.Bits() - } - return ri.Addr().Less(rj.Addr()) - }) + slices.SortFunc(p, netipx.ComparePrefix) } // FilterPrefixes returns a new slice, not aliasing in, containing elements of diff --git a/util/cmpx/cmpx.go b/util/cmpx/cmpx.go index d747f0a1d..007d9096a 100644 --- a/util/cmpx/cmpx.go +++ b/util/cmpx/cmpx.go @@ -20,3 +20,40 @@ func Or[T comparable](list ...T) T { } return zero } + +// Ordered is cmp.Ordered from Go 1.21. +type Ordered interface { + ~int | ~int8 | ~int16 | ~int32 | ~int64 | + ~uint | ~uint8 | ~uint16 | ~uint32 | ~uint64 | ~uintptr | + ~float32 | ~float64 | + ~string +} + +// Compare returns +// +// -1 if x is less than y, +// 0 if x equals y, +// +1 if x is greater than y. +// +// For floating-point types, a NaN is considered less than any non-NaN, +// a NaN is considered equal to a NaN, and -0.0 is equal to 0.0. +func Compare[T Ordered](x, y T) int { + xNaN := isNaN(x) + yNaN := isNaN(y) + if xNaN && yNaN { + return 0 + } + if xNaN || x < y { + return -1 + } + if yNaN || x > y { + return +1 + } + return 0 +} + +// isNaN reports whether x is a NaN without requiring the math package. +// This will always return false if T is not floating-point. +func isNaN[T Ordered](x T) bool { + return x != x +} diff --git a/wgengine/magicsock/magicsock_test.go b/wgengine/magicsock/magicsock_test.go index ff3a25ab4..b6bfef107 100644 --- a/wgengine/magicsock/magicsock_test.go +++ b/wgengine/magicsock/magicsock_test.go @@ -2438,11 +2438,11 @@ func TestEndpointTracker(t *testing.T) { got := et.update(tt.now, tt.eps) // Sort both arrays for comparison - slices.SortFunc(got, func(a, b tailcfg.Endpoint) bool { - return a.Addr.String() < b.Addr.String() + slices.SortFunc(got, func(a, b tailcfg.Endpoint) int { + return strings.Compare(a.Addr.String(), b.Addr.String()) }) - slices.SortFunc(tt.want, func(a, b tailcfg.Endpoint) bool { - return a.Addr.String() < b.Addr.String() + slices.SortFunc(tt.want, func(a, b tailcfg.Endpoint) int { + return strings.Compare(a.Addr.String(), b.Addr.String()) }) if !reflect.DeepEqual(got, tt.want) { diff --git a/wgengine/router/ifconfig_windows.go b/wgengine/router/ifconfig_windows.go index 1cd01eee1..76d3e82be 100644 --- a/wgengine/router/ifconfig_windows.go +++ b/wgengine/router/ifconfig_windows.go @@ -396,7 +396,7 @@ func configureInterface(cfg *Config, tun *tun.NativeTun) (retErr error) { return fmt.Errorf("syncAddresses: %w", err) } - slices.SortFunc(routes, routeDataLess) + slices.SortFunc(routes, routeDataCompare) deduplicatedRoutes := []*winipcfg.RouteData{} for i := 0; i < len(routes); i++ { @@ -652,8 +652,8 @@ func routeDataCompare(a, b *winipcfg.RouteData) int { func deltaRouteData(a, b []*winipcfg.RouteData) (add, del []*winipcfg.RouteData) { add = make([]*winipcfg.RouteData, 0, len(b)) del = make([]*winipcfg.RouteData, 0, len(a)) - slices.SortFunc(a, routeDataLess) - slices.SortFunc(b, routeDataLess) + slices.SortFunc(a, routeDataCompare) + slices.SortFunc(b, routeDataCompare) i := 0 j := 0 diff --git a/wgengine/router/router_linux_test.go b/wgengine/router/router_linux_test.go index 5d0263993..e7998f9b9 100644 --- a/wgengine/router/router_linux_test.go +++ b/wgengine/router/router_linux_test.go @@ -20,6 +20,7 @@ import ( "github.com/google/go-cmp/cmp" "github.com/tailscale/wireguard-go/tun" "github.com/vishvananda/netlink" + "go4.org/netipx" "golang.org/x/exp/slices" "tailscale.com/net/netmon" "tailscale.com/net/tsaddr" @@ -1022,8 +1023,8 @@ func TestCIDRDiff(t *testing.T) { if err != nil { t.Fatal(err) } - slices.SortFunc(added, func(a, b netip.Prefix) bool { return a.Addr().Less(b.Addr()) }) - slices.SortFunc(deleted, func(a, b netip.Prefix) bool { return a.Addr().Less(b.Addr()) }) + slices.SortFunc(added, netipx.ComparePrefix) + slices.SortFunc(deleted, netipx.ComparePrefix) if !reflect.DeepEqual(added, tc.wantAdd) { t.Errorf("added = %v, want %v", added, tc.wantAdd) }