Ver Fonte

cmd/viewer, types/views, all: un-special case slice of netip.Prefix

Make it just a views.Slice[netip.Prefix] instead of its own named type.

Having the special case led to circular dependencies in another WIP PR
of mine.

Updates #8948

Signed-off-by: Brad Fitzpatrick <[email protected]>
Brad Fitzpatrick há 2 anos atrás
pai
commit
6e57dee7eb

+ 2 - 1
cmd/tailscale/cli/set.go

@@ -15,6 +15,7 @@ import (
 	"tailscale.com/net/netutil"
 	"tailscale.com/net/tsaddr"
 	"tailscale.com/safesocket"
+	"tailscale.com/types/views"
 )
 
 var setCmd = &ffcli.Command{
@@ -171,7 +172,7 @@ func calcAdvertiseRoutesForSet(advertiseExitNodeSet, advertiseRoutesSet bool, cu
 		if alreadyAdvertisesExitNode == setArgs.advertiseDefaultRoute {
 			return curPrefs.AdvertiseRoutes, nil
 		}
-		routes = tsaddr.FilterPrefixesCopy(curPrefs.AdvertiseRoutes, func(p netip.Prefix) bool {
+		routes = tsaddr.FilterPrefixesCopy(views.SliceOf(curPrefs.AdvertiseRoutes), func(p netip.Prefix) bool {
 			return p.Bits() != 0
 		})
 		if setArgs.advertiseDefaultRoute {

+ 2 - 2
cmd/viewer/tests/tests_view.go

@@ -309,8 +309,8 @@ func (v StructWithSlicesView) StructPointers() views.SliceView[*StructWithPtrs,
 func (v StructWithSlicesView) Structs() StructWithPtrs    { panic("unsupported") }
 func (v StructWithSlicesView) Ints() *int                 { panic("unsupported") }
 func (v StructWithSlicesView) Slice() views.Slice[string] { return views.SliceOf(v.ж.Slice) }
-func (v StructWithSlicesView) Prefixes() views.IPPrefixSlice {
-	return views.IPPrefixSliceOf(v.ж.Prefixes)
+func (v StructWithSlicesView) Prefixes() views.Slice[netip.Prefix] {
+	return views.SliceOf(v.ж.Prefixes)
 }
 func (v StructWithSlicesView) Data() mem.RO { return mem.B(v.ж.Data) }
 

+ 0 - 5
cmd/viewer/viewer.go

@@ -69,8 +69,6 @@ func (v *{{.ViewName}}) UnmarshalJSON(b []byte) error {
 {{end}}
 {{define "byteSliceField"}}func (v {{.ViewName}}) {{.FieldName}}() mem.RO { return mem.B(v.ж.{{.FieldName}}) }
 {{end}}
-{{define "ipPrefixSliceField"}}func (v {{.ViewName}}) {{.FieldName}}() views.IPPrefixSlice { return views.IPPrefixSliceOf(v.ж.{{.FieldName}}) }
-{{end}}
 {{define "sliceField"}}func (v {{.ViewName}}) {{.FieldName}}() views.Slice[{{.FieldType}}] { return views.SliceOf(v.ж.{{.FieldName}}) }
 {{end}}
 {{define "viewSliceField"}}func (v {{.ViewName}}) {{.FieldName}}() views.SliceView[{{.FieldType}},{{.FieldViewName}}] { return views.SliceOfViews[{{.FieldType}},{{.FieldViewName}}](v.ж.{{.FieldName}}) }
@@ -176,9 +174,6 @@ func genView(buf *bytes.Buffer, it *codegen.ImportTracker, typ *types.Named, thi
 			case "byte":
 				it.Import("go4.org/mem")
 				writeTemplate("byteSliceField")
-			case "inet.af/netip.Prefix", "net/netip.Prefix":
-				it.Import("tailscale.com/types/views")
-				writeTemplate("ipPrefixSliceField")
 			default:
 				it.Import("tailscale.com/types/views")
 				shallow, deep, base := requiresCloning(elem)

+ 2 - 2
ipn/ipn_view.go

@@ -79,8 +79,8 @@ func (v PrefsView) Hostname() string                   { return v.ж.Hostname }
 func (v PrefsView) NotepadURLs() bool                  { return v.ж.NotepadURLs }
 func (v PrefsView) ForceDaemon() bool                  { return v.ж.ForceDaemon }
 func (v PrefsView) Egg() bool                          { return v.ж.Egg }
-func (v PrefsView) AdvertiseRoutes() views.IPPrefixSlice {
-	return views.IPPrefixSliceOf(v.ж.AdvertiseRoutes)
+func (v PrefsView) AdvertiseRoutes() views.Slice[netip.Prefix] {
+	return views.SliceOf(v.ж.AdvertiseRoutes)
 }
 func (v PrefsView) NoSNAT() bool                          { return v.ж.NoSNAT }
 func (v PrefsView) NetfilterMode() preftype.NetfilterMode { return v.ж.NetfilterMode }

+ 4 - 3
ipn/ipnlocal/local.go

@@ -776,13 +776,13 @@ func (b *LocalBackend) populatePeerStatusLocked(sb *ipnstate.StatusBuilder) {
 func peerStatusFromNode(ps *ipnstate.PeerStatus, n *tailcfg.Node) {
 	ps.ID = n.StableID
 	ps.Created = n.Created
-	ps.ExitNodeOption = tsaddr.ContainsExitRoutes(n.AllowedIPs)
+	ps.ExitNodeOption = tsaddr.ContainsExitRoutes(views.SliceOf(n.AllowedIPs))
 	if n.Tags != nil {
 		v := views.SliceOf(n.Tags)
 		ps.Tags = &v
 	}
 	if n.PrimaryRoutes != nil {
-		v := views.IPPrefixSliceOf(n.PrimaryRoutes)
+		v := views.SliceOf(n.PrimaryRoutes)
 		ps.PrimaryRoutes = &v
 	}
 
@@ -2301,7 +2301,8 @@ func (b *LocalBackend) setAtomicValuesFromPrefsLocked(p ipn.PrefsView) {
 		b.lastServeConfJSON = mem.B(nil)
 		b.serveConfig = ipn.ServeConfigView{}
 	} else {
-		b.containsViaIPFuncAtomic.Store(tsaddr.NewContainsIPFunc(p.AdvertiseRoutes().Filter(tsaddr.IsViaPrefix)))
+		filtered := tsaddr.FilterPrefixesCopy(p.AdvertiseRoutes(), tsaddr.IsViaPrefix)
+		b.containsViaIPFuncAtomic.Store(tsaddr.NewContainsIPFunc(filtered))
 		b.setTCPPortsInterceptedFromNetmapAndPrefsLocked(p)
 	}
 }

+ 1 - 1
ipn/ipnstate/ipnstate.go

@@ -209,7 +209,7 @@ type PeerStatus struct {
 	// PrimaryRoutes are the routes this node is currently the primary
 	// subnet router for, as determined by the control plane. It does
 	// not include the IPs in TailscaleIPs.
-	PrimaryRoutes *views.IPPrefixSlice `json:",omitempty"`
+	PrimaryRoutes *views.Slice[netip.Prefix] `json:",omitempty"`
 
 	// Endpoints:
 	Addrs   []string

+ 2 - 1
ipn/prefs.go

@@ -23,6 +23,7 @@ import (
 	"tailscale.com/tailcfg"
 	"tailscale.com/types/persist"
 	"tailscale.com/types/preftype"
+	"tailscale.com/types/views"
 	"tailscale.com/util/dnsname"
 )
 
@@ -506,7 +507,7 @@ func (p *Prefs) AdvertisesExitNode() bool {
 	if p == nil {
 		return false
 	}
-	return tsaddr.ContainsExitRoutes(p.AdvertiseRoutes)
+	return tsaddr.ContainsExitRoutes(views.SliceOf(p.AdvertiseRoutes))
 }
 
 // SetAdvertiseExitNode mutates p (if non-nil) to add or remove the two

+ 18 - 5
net/tsaddr/tsaddr.go

@@ -13,6 +13,7 @@ import (
 
 	"go4.org/netipx"
 	"tailscale.com/net/netaddr"
+	"tailscale.com/types/views"
 )
 
 // ChromeOSVMRange returns the subset of the CGNAT IPv4 range used by
@@ -225,9 +226,10 @@ func PrefixIs6(p netip.Prefix) bool { return p.Addr().Is6() }
 
 // ContainsExitRoutes reports whether rr contains both the IPv4 and
 // IPv6 /0 route.
-func ContainsExitRoutes(rr []netip.Prefix) bool {
+func ContainsExitRoutes(rr views.Slice[netip.Prefix]) bool {
 	var v4, v6 bool
-	for _, r := range rr {
+	for i := range rr.LenIter() {
+		r := rr.At(i)
 		if r == allIPv4 {
 			v4 = true
 		} else if r == allIPv6 {
@@ -237,6 +239,17 @@ func ContainsExitRoutes(rr []netip.Prefix) bool {
 	return v4 && v6
 }
 
+// ContainsNonExitSubnetRoutes reports whether v contains Subnet
+// Routes other than ExitNode Routes.
+func ContainsNonExitSubnetRoutes(rr views.Slice[netip.Prefix]) bool {
+	for i := range rr.LenIter() {
+		if rr.At(i).Bits() != 0 {
+			return true
+		}
+	}
+	return false
+}
+
 var (
 	allIPv4 = netip.MustParsePrefix("0.0.0.0/0")
 	allIPv6 = netip.MustParsePrefix("::/0")
@@ -258,10 +271,10 @@ func SortPrefixes(p []netip.Prefix) {
 
 // FilterPrefixes returns a new slice, not aliasing in, containing elements of
 // in that match f.
-func FilterPrefixesCopy(in []netip.Prefix, f func(netip.Prefix) bool) []netip.Prefix {
+func FilterPrefixesCopy(in views.Slice[netip.Prefix], f func(netip.Prefix) bool) []netip.Prefix {
 	var out []netip.Prefix
-	for _, v := range in {
-		if f(v) {
+	for i := range in.LenIter() {
+		if v := in.At(i); f(v) {
 			out = append(out, v)
 		}
 	}

+ 52 - 56
tailcfg/tailcfg_view.go

@@ -131,27 +131,25 @@ func (v *NodeView) UnmarshalJSON(b []byte) error {
 	return nil
 }
 
-func (v NodeView) ID() NodeID                      { return v.ж.ID }
-func (v NodeView) StableID() StableNodeID          { return v.ж.StableID }
-func (v NodeView) Name() string                    { return v.ж.Name }
-func (v NodeView) User() UserID                    { return v.ж.User }
-func (v NodeView) Sharer() UserID                  { return v.ж.Sharer }
-func (v NodeView) Key() key.NodePublic             { return v.ж.Key }
-func (v NodeView) KeyExpiry() time.Time            { return v.ж.KeyExpiry }
-func (v NodeView) KeySignature() mem.RO            { return mem.B(v.ж.KeySignature) }
-func (v NodeView) Machine() key.MachinePublic      { return v.ж.Machine }
-func (v NodeView) DiscoKey() key.DiscoPublic       { return v.ж.DiscoKey }
-func (v NodeView) Addresses() views.IPPrefixSlice  { return views.IPPrefixSliceOf(v.ж.Addresses) }
-func (v NodeView) AllowedIPs() views.IPPrefixSlice { return views.IPPrefixSliceOf(v.ж.AllowedIPs) }
-func (v NodeView) Endpoints() views.Slice[string]  { return views.SliceOf(v.ж.Endpoints) }
-func (v NodeView) DERP() string                    { return v.ж.DERP }
-func (v NodeView) Hostinfo() HostinfoView          { return v.ж.Hostinfo }
-func (v NodeView) Created() time.Time              { return v.ж.Created }
-func (v NodeView) Cap() CapabilityVersion          { return v.ж.Cap }
-func (v NodeView) Tags() views.Slice[string]       { return views.SliceOf(v.ж.Tags) }
-func (v NodeView) PrimaryRoutes() views.IPPrefixSlice {
-	return views.IPPrefixSliceOf(v.ж.PrimaryRoutes)
-}
+func (v NodeView) ID() NodeID                               { return v.ж.ID }
+func (v NodeView) StableID() StableNodeID                   { return v.ж.StableID }
+func (v NodeView) Name() string                             { return v.ж.Name }
+func (v NodeView) User() UserID                             { return v.ж.User }
+func (v NodeView) Sharer() UserID                           { return v.ж.Sharer }
+func (v NodeView) Key() key.NodePublic                      { return v.ж.Key }
+func (v NodeView) KeyExpiry() time.Time                     { return v.ж.KeyExpiry }
+func (v NodeView) KeySignature() mem.RO                     { return mem.B(v.ж.KeySignature) }
+func (v NodeView) Machine() key.MachinePublic               { return v.ж.Machine }
+func (v NodeView) DiscoKey() key.DiscoPublic                { return v.ж.DiscoKey }
+func (v NodeView) Addresses() views.Slice[netip.Prefix]     { return views.SliceOf(v.ж.Addresses) }
+func (v NodeView) AllowedIPs() views.Slice[netip.Prefix]    { return views.SliceOf(v.ж.AllowedIPs) }
+func (v NodeView) Endpoints() views.Slice[string]           { return views.SliceOf(v.ж.Endpoints) }
+func (v NodeView) DERP() string                             { return v.ж.DERP }
+func (v NodeView) Hostinfo() HostinfoView                   { return v.ж.Hostinfo }
+func (v NodeView) Created() time.Time                       { return v.ж.Created }
+func (v NodeView) Cap() CapabilityVersion                   { return v.ж.Cap }
+func (v NodeView) Tags() views.Slice[string]                { return views.SliceOf(v.ж.Tags) }
+func (v NodeView) PrimaryRoutes() views.Slice[netip.Prefix] { return views.SliceOf(v.ж.PrimaryRoutes) }
 func (v NodeView) LastSeen() *time.Time {
 	if v.ж.LastSeen == nil {
 		return nil
@@ -266,41 +264,39 @@ func (v *HostinfoView) UnmarshalJSON(b []byte) error {
 	return nil
 }
 
-func (v HostinfoView) IPNVersion() string      { return v.ж.IPNVersion }
-func (v HostinfoView) FrontendLogID() string   { return v.ж.FrontendLogID }
-func (v HostinfoView) BackendLogID() string    { return v.ж.BackendLogID }
-func (v HostinfoView) OS() string              { return v.ж.OS }
-func (v HostinfoView) OSVersion() string       { return v.ж.OSVersion }
-func (v HostinfoView) Container() opt.Bool     { return v.ж.Container }
-func (v HostinfoView) Env() string             { return v.ж.Env }
-func (v HostinfoView) Distro() string          { return v.ж.Distro }
-func (v HostinfoView) DistroVersion() string   { return v.ж.DistroVersion }
-func (v HostinfoView) DistroCodeName() string  { return v.ж.DistroCodeName }
-func (v HostinfoView) App() string             { return v.ж.App }
-func (v HostinfoView) Desktop() opt.Bool       { return v.ж.Desktop }
-func (v HostinfoView) Package() string         { return v.ж.Package }
-func (v HostinfoView) DeviceModel() string     { return v.ж.DeviceModel }
-func (v HostinfoView) PushDeviceToken() string { return v.ж.PushDeviceToken }
-func (v HostinfoView) Hostname() string        { return v.ж.Hostname }
-func (v HostinfoView) ShieldsUp() bool         { return v.ж.ShieldsUp }
-func (v HostinfoView) ShareeNode() bool        { return v.ж.ShareeNode }
-func (v HostinfoView) NoLogsNoSupport() bool   { return v.ж.NoLogsNoSupport }
-func (v HostinfoView) WireIngress() bool       { return v.ж.WireIngress }
-func (v HostinfoView) AllowsUpdate() bool      { return v.ж.AllowsUpdate }
-func (v HostinfoView) Machine() string         { return v.ж.Machine }
-func (v HostinfoView) GoArch() string          { return v.ж.GoArch }
-func (v HostinfoView) GoArchVar() string       { return v.ж.GoArchVar }
-func (v HostinfoView) GoVersion() string       { return v.ж.GoVersion }
-func (v HostinfoView) RoutableIPs() views.IPPrefixSlice {
-	return views.IPPrefixSliceOf(v.ж.RoutableIPs)
-}
-func (v HostinfoView) RequestTags() views.Slice[string]  { return views.SliceOf(v.ж.RequestTags) }
-func (v HostinfoView) Services() views.Slice[Service]    { return views.SliceOf(v.ж.Services) }
-func (v HostinfoView) NetInfo() NetInfoView              { return v.ж.NetInfo.View() }
-func (v HostinfoView) SSH_HostKeys() views.Slice[string] { return views.SliceOf(v.ж.SSH_HostKeys) }
-func (v HostinfoView) Cloud() string                     { return v.ж.Cloud }
-func (v HostinfoView) Userspace() opt.Bool               { return v.ж.Userspace }
-func (v HostinfoView) UserspaceRouter() opt.Bool         { return v.ж.UserspaceRouter }
+func (v HostinfoView) IPNVersion() string                     { return v.ж.IPNVersion }
+func (v HostinfoView) FrontendLogID() string                  { return v.ж.FrontendLogID }
+func (v HostinfoView) BackendLogID() string                   { return v.ж.BackendLogID }
+func (v HostinfoView) OS() string                             { return v.ж.OS }
+func (v HostinfoView) OSVersion() string                      { return v.ж.OSVersion }
+func (v HostinfoView) Container() opt.Bool                    { return v.ж.Container }
+func (v HostinfoView) Env() string                            { return v.ж.Env }
+func (v HostinfoView) Distro() string                         { return v.ж.Distro }
+func (v HostinfoView) DistroVersion() string                  { return v.ж.DistroVersion }
+func (v HostinfoView) DistroCodeName() string                 { return v.ж.DistroCodeName }
+func (v HostinfoView) App() string                            { return v.ж.App }
+func (v HostinfoView) Desktop() opt.Bool                      { return v.ж.Desktop }
+func (v HostinfoView) Package() string                        { return v.ж.Package }
+func (v HostinfoView) DeviceModel() string                    { return v.ж.DeviceModel }
+func (v HostinfoView) PushDeviceToken() string                { return v.ж.PushDeviceToken }
+func (v HostinfoView) Hostname() string                       { return v.ж.Hostname }
+func (v HostinfoView) ShieldsUp() bool                        { return v.ж.ShieldsUp }
+func (v HostinfoView) ShareeNode() bool                       { return v.ж.ShareeNode }
+func (v HostinfoView) NoLogsNoSupport() bool                  { return v.ж.NoLogsNoSupport }
+func (v HostinfoView) WireIngress() bool                      { return v.ж.WireIngress }
+func (v HostinfoView) AllowsUpdate() bool                     { return v.ж.AllowsUpdate }
+func (v HostinfoView) Machine() string                        { return v.ж.Machine }
+func (v HostinfoView) GoArch() string                         { return v.ж.GoArch }
+func (v HostinfoView) GoArchVar() string                      { return v.ж.GoArchVar }
+func (v HostinfoView) GoVersion() string                      { return v.ж.GoVersion }
+func (v HostinfoView) RoutableIPs() views.Slice[netip.Prefix] { return views.SliceOf(v.ж.RoutableIPs) }
+func (v HostinfoView) RequestTags() views.Slice[string]       { return views.SliceOf(v.ж.RequestTags) }
+func (v HostinfoView) Services() views.Slice[Service]         { return views.SliceOf(v.ж.Services) }
+func (v HostinfoView) NetInfo() NetInfoView                   { return v.ж.NetInfo.View() }
+func (v HostinfoView) SSH_HostKeys() views.Slice[string]      { return views.SliceOf(v.ж.SSH_HostKeys) }
+func (v HostinfoView) Cloud() string                          { return v.ж.Cloud }
+func (v HostinfoView) Userspace() opt.Bool                    { return v.ж.Userspace }
+func (v HostinfoView) UserspaceRouter() opt.Bool              { return v.ж.UserspaceRouter }
 func (v HostinfoView) Location() *Location {
 	if v.ж.Location == nil {
 		return nil

+ 0 - 82
types/views/views.go

@@ -9,10 +9,6 @@ import (
 	"encoding/json"
 	"errors"
 	"maps"
-	"net/netip"
-	"slices"
-
-	"tailscale.com/net/tsaddr"
 )
 
 func unmarshalSliceFromJSON[T any](b []byte, x *[]T) error {
@@ -229,84 +225,6 @@ func SliceEqualAnyOrder[T comparable](a, b Slice[T]) bool {
 	return true
 }
 
-// IPPrefixSlice is a read-only accessor for a slice of netip.Prefix.
-type IPPrefixSlice struct {
-	ж Slice[netip.Prefix]
-}
-
-// IPPrefixSliceOf returns a IPPrefixSlice for the provided slice.
-func IPPrefixSliceOf(x []netip.Prefix) IPPrefixSlice { return IPPrefixSlice{SliceOf(x)} }
-
-// IsNil reports whether the underlying slice is nil.
-func (v IPPrefixSlice) IsNil() bool { return v.ж.IsNil() }
-
-// Len returns the length of the slice.
-func (v IPPrefixSlice) Len() int { return v.ж.Len() }
-
-// LenIter returns a slice the same length as the v.Len().
-// The caller can then range over it to get the valid indexes.
-// It does not allocate.
-func (v IPPrefixSlice) LenIter() []struct{} { return make([]struct{}, v.ж.Len()) }
-
-// At returns the IPPrefix at index `i` of the slice.
-func (v IPPrefixSlice) At(i int) netip.Prefix { return v.ж.At(i) }
-
-// AppendTo appends the underlying slice values to dst.
-func (v IPPrefixSlice) AppendTo(dst []netip.Prefix) []netip.Prefix {
-	return v.ж.AppendTo(dst)
-}
-
-// Unwrap returns the underlying Slice[netip.Prefix].
-func (v IPPrefixSlice) Unwrap() Slice[netip.Prefix] {
-	return v.ж
-}
-
-// AsSlice returns a copy of underlying slice.
-func (v IPPrefixSlice) AsSlice() []netip.Prefix {
-	return v.ж.AsSlice()
-}
-
-// Filter returns a new slice, containing elements of v that match f.
-func (v IPPrefixSlice) Filter(f func(netip.Prefix) bool) []netip.Prefix {
-	return tsaddr.FilterPrefixesCopy(v.ж.ж, f)
-}
-
-// PrefixesContainsIP reports whether any IPPrefix contains IP.
-func (v IPPrefixSlice) ContainsIP(ip netip.Addr) bool {
-	return tsaddr.PrefixesContainsIP(v.ж.ж, ip)
-}
-
-// PrefixesContainsFunc reports whether f is true for any IPPrefix in the slice.
-func (v IPPrefixSlice) ContainsFunc(f func(netip.Prefix) bool) bool {
-	return slices.ContainsFunc(v.ж.ж, f)
-}
-
-// ContainsExitRoutes reports whether v contains ExitNode Routes.
-func (v IPPrefixSlice) ContainsExitRoutes() bool {
-	return tsaddr.ContainsExitRoutes(v.ж.ж)
-}
-
-// ContainsNonExitSubnetRoutes reports whether v contains Subnet
-// Routes other than ExitNode Routes.
-func (v IPPrefixSlice) ContainsNonExitSubnetRoutes() bool {
-	for i := 0; i < v.Len(); i++ {
-		if v.At(i).Bits() != 0 {
-			return true
-		}
-	}
-	return false
-}
-
-// MarshalJSON implements json.Marshaler.
-func (v IPPrefixSlice) MarshalJSON() ([]byte, error) {
-	return v.ж.MarshalJSON()
-}
-
-// UnmarshalJSON implements json.Unmarshaler.
-func (v *IPPrefixSlice) UnmarshalJSON(b []byte) error {
-	return v.ж.UnmarshalJSON(b)
-}
-
 // MapOf returns a view over m. It is the caller's responsibility to make sure K
 // and V is immutable, if this is being used to provide a read-only view over m.
 func MapOf[K comparable, V comparable](m map[K]V) Map[K, V] {

+ 4 - 4
types/views/views_test.go

@@ -16,10 +16,10 @@ import (
 
 type viewStruct struct {
 	Int        int
-	Addrs      IPPrefixSlice
+	Addrs      Slice[netip.Prefix]
 	Strings    Slice[string]
-	AddrsPtr   *IPPrefixSlice `json:",omitempty"`
-	StringsPtr *Slice[string] `json:",omitempty"`
+	AddrsPtr   *Slice[netip.Prefix] `json:",omitempty"`
+	StringsPtr *Slice[string]       `json:",omitempty"`
 }
 
 func BenchmarkSliceIteration(b *testing.B) {
@@ -66,7 +66,7 @@ func TestViewsJSON(t *testing.T) {
 		}
 		return
 	}
-	ipp := IPPrefixSliceOf(mustCIDR("192.168.0.0/24"))
+	ipp := SliceOf(mustCIDR("192.168.0.0/24"))
 	ss := SliceOf([]string{"bar"})
 	tests := []struct {
 		name     string