mirror of
https://github.com/tailscale/tailscale.git
synced 2024-11-29 04:55:31 +00:00
cmd/tailscale: change formatting of "tailscale status"
* show DNS name over hostname, removing domain's common MagicDNS suffix. only show hostname if there's no DNS name. but still show shared devices' MagicDNS FQDN. * remove nerdy low-level details by default: endpoints, DERP relay, public key. They're available in JSON mode still for those who need them. * only show endpoint or DERP relay when it's active with the goal of making debugging easier. (so it's easier for users to understand what's happening) The asterisks are gone. * remove Tx/Rx numbers by default for idle peers; only show them when there's traffic. * include peers' owner login names * add CLI option to not show peers (matching --self=true, --peers= also defaults to true) * sort by DNS/host name, not public key * reorder columns
This commit is contained in:
parent
c09d5a9e28
commit
5efb0a8bca
@ -14,6 +14,8 @@
|
|||||||
"net"
|
"net"
|
||||||
"net/http"
|
"net/http"
|
||||||
"os"
|
"os"
|
||||||
|
"sort"
|
||||||
|
"strings"
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
"github.com/peterbourgon/ff/v2/ffcli"
|
"github.com/peterbourgon/ff/v2/ffcli"
|
||||||
@ -21,6 +23,7 @@
|
|||||||
"tailscale.com/ipn"
|
"tailscale.com/ipn"
|
||||||
"tailscale.com/ipn/ipnstate"
|
"tailscale.com/ipn/ipnstate"
|
||||||
"tailscale.com/net/interfaces"
|
"tailscale.com/net/interfaces"
|
||||||
|
"tailscale.com/util/dnsname"
|
||||||
)
|
)
|
||||||
|
|
||||||
var statusCmd = &ffcli.Command{
|
var statusCmd = &ffcli.Command{
|
||||||
@ -34,6 +37,7 @@
|
|||||||
fs.BoolVar(&statusArgs.web, "web", false, "run webserver with HTML showing status")
|
fs.BoolVar(&statusArgs.web, "web", false, "run webserver with HTML showing status")
|
||||||
fs.BoolVar(&statusArgs.active, "active", false, "filter output to only peers with active sessions (not applicable to web mode)")
|
fs.BoolVar(&statusArgs.active, "active", false, "filter output to only peers with active sessions (not applicable to web mode)")
|
||||||
fs.BoolVar(&statusArgs.self, "self", true, "show status of local machine")
|
fs.BoolVar(&statusArgs.self, "self", true, "show status of local machine")
|
||||||
|
fs.BoolVar(&statusArgs.peers, "peers", true, "show status of peers")
|
||||||
fs.StringVar(&statusArgs.listen, "listen", "127.0.0.1:8384", "listen address; use port 0 for automatic")
|
fs.StringVar(&statusArgs.listen, "listen", "127.0.0.1:8384", "listen address; use port 0 for automatic")
|
||||||
fs.BoolVar(&statusArgs.browser, "browser", true, "Open a browser in web mode")
|
fs.BoolVar(&statusArgs.browser, "browser", true, "Open a browser in web mode")
|
||||||
return fs
|
return fs
|
||||||
@ -47,6 +51,7 @@
|
|||||||
browser bool // in web mode, whether to open browser
|
browser bool // in web mode, whether to open browser
|
||||||
active bool // in CLI mode, filter output to only peers with active sessions
|
active bool // in CLI mode, filter output to only peers with active sessions
|
||||||
self bool // in CLI mode, show status of local machine
|
self bool // in CLI mode, show status of local machine
|
||||||
|
peers bool // in CLI mode, show status of peer machines
|
||||||
}
|
}
|
||||||
|
|
||||||
func runStatus(ctx context.Context, args []string) error {
|
func runStatus(ctx context.Context, args []string) error {
|
||||||
@ -136,30 +141,30 @@ func runStatus(ctx context.Context, args []string) error {
|
|||||||
f := func(format string, a ...interface{}) { fmt.Fprintf(&buf, format, a...) }
|
f := func(format string, a ...interface{}) { fmt.Fprintf(&buf, format, a...) }
|
||||||
printPS := func(ps *ipnstate.PeerStatus) {
|
printPS := func(ps *ipnstate.PeerStatus) {
|
||||||
active := peerActive(ps)
|
active := peerActive(ps)
|
||||||
f("%s %-7s %-15s %-18s tx=%8d rx=%8d ",
|
f("%-15s %-20s %-12s %-7s ",
|
||||||
ps.PublicKey.ShortString(),
|
|
||||||
ps.OS,
|
|
||||||
ps.TailAddr,
|
ps.TailAddr,
|
||||||
ps.SimpleHostName(),
|
dnsOrQuoteHostname(st, ps),
|
||||||
ps.TxBytes,
|
ownerLogin(st, ps),
|
||||||
ps.RxBytes,
|
ps.OS,
|
||||||
)
|
)
|
||||||
relay := ps.Relay
|
relay := ps.Relay
|
||||||
if active && relay != "" && ps.CurAddr == "" {
|
anyTraffic := ps.TxBytes != 0 || ps.RxBytes != 0
|
||||||
relay = "*" + relay + "*"
|
if !active {
|
||||||
} else {
|
if anyTraffic {
|
||||||
relay = " " + relay
|
f("idle")
|
||||||
}
|
|
||||||
f("%-6s", relay)
|
|
||||||
for i, addr := range ps.Addrs {
|
|
||||||
if i != 0 {
|
|
||||||
f(", ")
|
|
||||||
}
|
|
||||||
if addr == ps.CurAddr {
|
|
||||||
f("*%s*", addr)
|
|
||||||
} else {
|
} else {
|
||||||
f("%s", addr)
|
f("-")
|
||||||
}
|
}
|
||||||
|
} else {
|
||||||
|
f("active; ")
|
||||||
|
if relay != "" && ps.CurAddr == "" {
|
||||||
|
f("relay %q", relay)
|
||||||
|
} else if ps.CurAddr != "" {
|
||||||
|
f("direct %s", ps.CurAddr)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if anyTraffic {
|
||||||
|
f(", tx %d rx %d", ps.TxBytes, ps.RxBytes)
|
||||||
}
|
}
|
||||||
f("\n")
|
f("\n")
|
||||||
}
|
}
|
||||||
@ -167,16 +172,23 @@ func runStatus(ctx context.Context, args []string) error {
|
|||||||
if statusArgs.self && st.Self != nil {
|
if statusArgs.self && st.Self != nil {
|
||||||
printPS(st.Self)
|
printPS(st.Self)
|
||||||
}
|
}
|
||||||
for _, peer := range st.Peers() {
|
if statusArgs.peers {
|
||||||
ps := st.Peer[peer]
|
var peers []*ipnstate.PeerStatus
|
||||||
if ps.ShareeNode {
|
for _, peer := range st.Peers() {
|
||||||
continue
|
ps := st.Peer[peer]
|
||||||
|
if ps.ShareeNode {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
peers = append(peers, ps)
|
||||||
}
|
}
|
||||||
active := peerActive(ps)
|
sort.Slice(peers, func(i, j int) bool { return sortKey(peers[i]) < sortKey(peers[j]) })
|
||||||
if statusArgs.active && !active {
|
for _, ps := range peers {
|
||||||
continue
|
active := peerActive(ps)
|
||||||
|
if statusArgs.active && !active {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
printPS(ps)
|
||||||
}
|
}
|
||||||
printPS(ps)
|
|
||||||
}
|
}
|
||||||
os.Stdout.Write(buf.Bytes())
|
os.Stdout.Write(buf.Bytes())
|
||||||
return nil
|
return nil
|
||||||
@ -188,3 +200,37 @@ func runStatus(ctx context.Context, args []string) error {
|
|||||||
func peerActive(ps *ipnstate.PeerStatus) bool {
|
func peerActive(ps *ipnstate.PeerStatus) bool {
|
||||||
return !ps.LastWrite.IsZero() && time.Since(ps.LastWrite) < 2*time.Minute
|
return !ps.LastWrite.IsZero() && time.Since(ps.LastWrite) < 2*time.Minute
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func dnsOrQuoteHostname(st *ipnstate.Status, ps *ipnstate.PeerStatus) string {
|
||||||
|
if i := strings.Index(ps.DNSName, "."); i != -1 && dnsname.HasSuffix(ps.DNSName, st.MagicDNSSuffix) {
|
||||||
|
return ps.DNSName[:i]
|
||||||
|
}
|
||||||
|
if ps.DNSName != "" {
|
||||||
|
return ps.DNSName
|
||||||
|
}
|
||||||
|
return fmt.Sprintf("- (%q)", ps.SimpleHostName())
|
||||||
|
}
|
||||||
|
|
||||||
|
func sortKey(ps *ipnstate.PeerStatus) string {
|
||||||
|
if ps.DNSName != "" {
|
||||||
|
return ps.DNSName
|
||||||
|
}
|
||||||
|
if ps.HostName != "" {
|
||||||
|
return ps.HostName
|
||||||
|
}
|
||||||
|
return ps.TailAddr
|
||||||
|
}
|
||||||
|
|
||||||
|
func ownerLogin(st *ipnstate.Status, ps *ipnstate.PeerStatus) string {
|
||||||
|
if ps.UserID.IsZero() {
|
||||||
|
return "-"
|
||||||
|
}
|
||||||
|
u, ok := st.User[ps.UserID]
|
||||||
|
if !ok {
|
||||||
|
return fmt.Sprint(ps.UserID)
|
||||||
|
}
|
||||||
|
if i := strings.Index(u.LoginName, "@"); i != -1 {
|
||||||
|
return u.LoginName[:i+1]
|
||||||
|
}
|
||||||
|
return u.LoginName
|
||||||
|
}
|
||||||
|
@ -74,6 +74,7 @@ tailscale.com/cmd/tailscale dependencies: (generated by github.com/tailscale/dep
|
|||||||
tailscale.com/types/strbuilder from tailscale.com/net/packet
|
tailscale.com/types/strbuilder from tailscale.com/net/packet
|
||||||
tailscale.com/types/structs from tailscale.com/control/controlclient+
|
tailscale.com/types/structs from tailscale.com/control/controlclient+
|
||||||
tailscale.com/types/wgkey from tailscale.com/control/controlclient+
|
tailscale.com/types/wgkey from tailscale.com/control/controlclient+
|
||||||
|
tailscale.com/util/dnsname from tailscale.com/cmd/tailscale/cli+
|
||||||
LW tailscale.com/util/endian from tailscale.com/net/netns+
|
LW tailscale.com/util/endian from tailscale.com/net/netns+
|
||||||
tailscale.com/util/lineread from tailscale.com/control/controlclient+
|
tailscale.com/util/lineread from tailscale.com/control/controlclient+
|
||||||
tailscale.com/util/systemd from tailscale.com/control/controlclient+
|
tailscale.com/util/systemd from tailscale.com/control/controlclient+
|
||||||
|
@ -82,6 +82,7 @@ tailscale.com/cmd/tailscaled dependencies: (generated by github.com/tailscale/de
|
|||||||
tailscale.com/types/strbuilder from tailscale.com/net/packet
|
tailscale.com/types/strbuilder from tailscale.com/net/packet
|
||||||
tailscale.com/types/structs from tailscale.com/control/controlclient+
|
tailscale.com/types/structs from tailscale.com/control/controlclient+
|
||||||
tailscale.com/types/wgkey from tailscale.com/control/controlclient+
|
tailscale.com/types/wgkey from tailscale.com/control/controlclient+
|
||||||
|
tailscale.com/util/dnsname from tailscale.com/control/controlclient+
|
||||||
LW tailscale.com/util/endian from tailscale.com/net/netns+
|
LW tailscale.com/util/endian from tailscale.com/net/netns+
|
||||||
tailscale.com/util/lineread from tailscale.com/control/controlclient+
|
tailscale.com/util/lineread from tailscale.com/control/controlclient+
|
||||||
tailscale.com/util/pidowner from tailscale.com/ipn/ipnserver
|
tailscale.com/util/pidowner from tailscale.com/ipn/ipnserver
|
||||||
|
@ -18,6 +18,7 @@
|
|||||||
"tailscale.com/tailcfg"
|
"tailscale.com/tailcfg"
|
||||||
"tailscale.com/types/logger"
|
"tailscale.com/types/logger"
|
||||||
"tailscale.com/types/wgkey"
|
"tailscale.com/types/wgkey"
|
||||||
|
"tailscale.com/util/dnsname"
|
||||||
"tailscale.com/wgengine/filter"
|
"tailscale.com/wgengine/filter"
|
||||||
)
|
)
|
||||||
|
|
||||||
@ -56,7 +57,30 @@ type NetworkMap struct {
|
|||||||
// TODO(crawshaw): Capabilities []tailcfg.Capability
|
// TODO(crawshaw): Capabilities []tailcfg.Capability
|
||||||
}
|
}
|
||||||
|
|
||||||
func (nm NetworkMap) String() string {
|
// MagicDNSSuffix returns the domain's MagicDNS suffix, or empty if none.
|
||||||
|
// If non-empty, it will neither start nor end with a period.
|
||||||
|
func (nm *NetworkMap) MagicDNSSuffix() string {
|
||||||
|
searchPathUsedAsDNSSuffix := func(suffix string) bool {
|
||||||
|
if dnsname.HasSuffix(nm.Name, suffix) {
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
for _, p := range nm.Peers {
|
||||||
|
if dnsname.HasSuffix(p.Name, suffix) {
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, d := range nm.DNS.Domains {
|
||||||
|
if searchPathUsedAsDNSSuffix(d) {
|
||||||
|
return strings.Trim(d, ".")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return ""
|
||||||
|
}
|
||||||
|
|
||||||
|
func (nm *NetworkMap) String() string {
|
||||||
return nm.Concise()
|
return nm.Concise()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -25,9 +25,10 @@
|
|||||||
|
|
||||||
// Status represents the entire state of the IPN network.
|
// Status represents the entire state of the IPN network.
|
||||||
type Status struct {
|
type Status struct {
|
||||||
BackendState string
|
BackendState string
|
||||||
TailscaleIPs []netaddr.IP // Tailscale IP(s) assigned to this node
|
TailscaleIPs []netaddr.IP // Tailscale IP(s) assigned to this node
|
||||||
Self *PeerStatus
|
Self *PeerStatus
|
||||||
|
MagicDNSSuffix string // e.g. "userfoo.tailscale.net" (no surrounding dots)
|
||||||
|
|
||||||
Peer map[key.Public]*PeerStatus
|
Peer map[key.Public]*PeerStatus
|
||||||
User map[tailcfg.UserID]tailcfg.UserProfile
|
User map[tailcfg.UserID]tailcfg.UserProfile
|
||||||
@ -103,6 +104,12 @@ func (sb *StatusBuilder) SetBackendState(v string) {
|
|||||||
sb.st.BackendState = v
|
sb.st.BackendState = v
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (sb *StatusBuilder) SetMagicDNSSuffix(v string) {
|
||||||
|
sb.mu.Lock()
|
||||||
|
defer sb.mu.Unlock()
|
||||||
|
sb.st.MagicDNSSuffix = v
|
||||||
|
}
|
||||||
|
|
||||||
func (sb *StatusBuilder) Status() *Status {
|
func (sb *StatusBuilder) Status() *Status {
|
||||||
sb.mu.Lock()
|
sb.mu.Lock()
|
||||||
defer sb.mu.Unlock()
|
defer sb.mu.Unlock()
|
||||||
|
25
ipn/local.go
25
ipn/local.go
@ -201,6 +201,7 @@ func (b *LocalBackend) UpdateStatus(sb *ipnstate.StatusBuilder) {
|
|||||||
// TODO: hostinfo, and its networkinfo
|
// TODO: hostinfo, and its networkinfo
|
||||||
// TODO: EngineStatus copy (and deprecate it?)
|
// TODO: EngineStatus copy (and deprecate it?)
|
||||||
if b.netMap != nil {
|
if b.netMap != nil {
|
||||||
|
sb.SetMagicDNSSuffix(b.netMap.MagicDNSSuffix())
|
||||||
for id, up := range b.netMap.UserProfiles {
|
for id, up := range b.netMap.UserProfiles {
|
||||||
sb.AddUser(id, up)
|
sb.AddUser(id, up)
|
||||||
}
|
}
|
||||||
@ -1232,28 +1233,10 @@ func (b *LocalBackend) authReconfig() {
|
|||||||
// magicDNSRootDomains returns the subset of nm.DNS.Domains that are the search domains for MagicDNS.
|
// magicDNSRootDomains returns the subset of nm.DNS.Domains that are the search domains for MagicDNS.
|
||||||
// Each entry has a trailing period.
|
// Each entry has a trailing period.
|
||||||
func magicDNSRootDomains(nm *controlclient.NetworkMap) []string {
|
func magicDNSRootDomains(nm *controlclient.NetworkMap) []string {
|
||||||
searchPathUsedAsDNSSuffix := func(suffix string) bool {
|
if v := nm.MagicDNSSuffix(); v != "" {
|
||||||
if tsdns.NameHasSuffix(nm.Name, suffix) {
|
return []string{strings.Trim(v, ".") + "."}
|
||||||
return true
|
|
||||||
}
|
|
||||||
for _, p := range nm.Peers {
|
|
||||||
if tsdns.NameHasSuffix(p.Name, suffix) {
|
|
||||||
return true
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return false
|
|
||||||
}
|
}
|
||||||
|
return nil
|
||||||
var ret []string
|
|
||||||
for _, d := range nm.DNS.Domains {
|
|
||||||
if searchPathUsedAsDNSSuffix(d) {
|
|
||||||
if !strings.HasSuffix(d, ".") {
|
|
||||||
d += "."
|
|
||||||
}
|
|
||||||
ret = append(ret, d)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return ret
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// routerConfig produces a router.Config from a wireguard config and IPN prefs.
|
// routerConfig produces a router.Config from a wireguard config and IPN prefs.
|
||||||
|
19
util/dnsname/dnsname.go
Normal file
19
util/dnsname/dnsname.go
Normal file
@ -0,0 +1,19 @@
|
|||||||
|
// Copyright (c) 2021 Tailscale Inc & AUTHORS All rights reserved.
|
||||||
|
// Use of this source code is governed by a BSD-style
|
||||||
|
// license that can be found in the LICENSE file.
|
||||||
|
|
||||||
|
// Package dnsname contains string functions for working with DNS names.
|
||||||
|
package dnsname
|
||||||
|
|
||||||
|
import "strings"
|
||||||
|
|
||||||
|
// HasSuffix reports whether the provided DNS name ends with the
|
||||||
|
// component(s) in suffix, ignoring any trailing dots.
|
||||||
|
//
|
||||||
|
// If suffix is the empty string, HasSuffix always reports false.
|
||||||
|
func HasSuffix(name, suffix string) bool {
|
||||||
|
name = strings.TrimSuffix(name, ".")
|
||||||
|
suffix = strings.TrimSuffix(suffix, ".")
|
||||||
|
nameBase := strings.TrimSuffix(name, suffix)
|
||||||
|
return len(nameBase) < len(name) && strings.HasSuffix(nameBase, ".")
|
||||||
|
}
|
28
util/dnsname/dnsname_test.go
Normal file
28
util/dnsname/dnsname_test.go
Normal file
@ -0,0 +1,28 @@
|
|||||||
|
// Copyright (c) 2021 Tailscale Inc & AUTHORS All rights reserved.
|
||||||
|
// Use of this source code is governed by a BSD-style
|
||||||
|
// license that can be found in the LICENSE file.
|
||||||
|
|
||||||
|
package dnsname
|
||||||
|
|
||||||
|
import "testing"
|
||||||
|
|
||||||
|
func TestHasSuffix(t *testing.T) {
|
||||||
|
tests := []struct {
|
||||||
|
name, suffix string
|
||||||
|
want bool
|
||||||
|
}{
|
||||||
|
{"foo.com", "com", true},
|
||||||
|
{"foo.com.", "com", true},
|
||||||
|
{"foo.com.", "com.", true},
|
||||||
|
|
||||||
|
{"", "", false},
|
||||||
|
{"foo.com.", "", false},
|
||||||
|
{"foo.com.", "o.com", false},
|
||||||
|
}
|
||||||
|
for _, tt := range tests {
|
||||||
|
got := HasSuffix(tt.name, tt.suffix)
|
||||||
|
if got != tt.want {
|
||||||
|
t.Errorf("HasSuffix(%q, %q) = %v; want %v", tt.name, tt.suffix, got, tt.want)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
@ -2710,11 +2710,14 @@ func (c *Conn) UpdateStatus(sb *ipnstate.StatusBuilder) {
|
|||||||
ss := &ipnstate.PeerStatus{
|
ss := &ipnstate.PeerStatus{
|
||||||
PublicKey: c.privateKey.Public(),
|
PublicKey: c.privateKey.Public(),
|
||||||
Addrs: c.lastEndpoints,
|
Addrs: c.lastEndpoints,
|
||||||
|
OS: version.OS(),
|
||||||
}
|
}
|
||||||
if c.netMap != nil {
|
if c.netMap != nil {
|
||||||
ss.HostName = c.netMap.Hostinfo.Hostname
|
ss.HostName = c.netMap.Hostinfo.Hostname
|
||||||
ss.OS = version.OS()
|
|
||||||
ss.DNSName = c.netMap.Name
|
ss.DNSName = c.netMap.Name
|
||||||
|
ss.UserID = c.netMap.User
|
||||||
|
} else {
|
||||||
|
ss.HostName, _ = os.Hostname()
|
||||||
}
|
}
|
||||||
if c.derpMap != nil {
|
if c.derpMap != nil {
|
||||||
derpRegion, ok := c.derpMap.Regions[c.myDerp]
|
derpRegion, ok := c.derpMap.Regions[c.myDerp]
|
||||||
|
@ -17,6 +17,7 @@
|
|||||||
dns "golang.org/x/net/dns/dnsmessage"
|
dns "golang.org/x/net/dns/dnsmessage"
|
||||||
"inet.af/netaddr"
|
"inet.af/netaddr"
|
||||||
"tailscale.com/types/logger"
|
"tailscale.com/types/logger"
|
||||||
|
"tailscale.com/util/dnsname"
|
||||||
)
|
)
|
||||||
|
|
||||||
// maxResponseBytes is the maximum size of a response from a Resolver.
|
// maxResponseBytes is the maximum size of a response from a Resolver.
|
||||||
@ -195,7 +196,7 @@ func (r *Resolver) Resolve(domain string, tp dns.Type) (netaddr.IP, dns.RCode, e
|
|||||||
|
|
||||||
anyHasSuffix := false
|
anyHasSuffix := false
|
||||||
for _, suffix := range dnsMap.rootDomains {
|
for _, suffix := range dnsMap.rootDomains {
|
||||||
if NameHasSuffix(domain, suffix) {
|
if dnsname.HasSuffix(domain, suffix) {
|
||||||
anyHasSuffix = true
|
anyHasSuffix = true
|
||||||
break
|
break
|
||||||
}
|
}
|
||||||
@ -616,12 +617,3 @@ func (r *Resolver) respond(query []byte) ([]byte, error) {
|
|||||||
|
|
||||||
return marshalResponse(resp)
|
return marshalResponse(resp)
|
||||||
}
|
}
|
||||||
|
|
||||||
// NameHasSuffix reports whether the provided DNS name ends with the
|
|
||||||
// component(s) in suffix, ignoring any trailing dots.
|
|
||||||
func NameHasSuffix(name, suffix string) bool {
|
|
||||||
name = strings.TrimSuffix(name, ".")
|
|
||||||
suffix = strings.TrimSuffix(suffix, ".")
|
|
||||||
nameBase := strings.TrimSuffix(name, suffix)
|
|
||||||
return len(nameBase) < len(name) && strings.HasSuffix(nameBase, ".")
|
|
||||||
}
|
|
||||||
|
@ -797,24 +797,3 @@ func TestMarshalResponseFormatError(t *testing.T) {
|
|||||||
}
|
}
|
||||||
t.Logf("response: %q", v)
|
t.Logf("response: %q", v)
|
||||||
}
|
}
|
||||||
|
|
||||||
func TestNameHasSuffix(t *testing.T) {
|
|
||||||
tests := []struct {
|
|
||||||
name, suffix string
|
|
||||||
want bool
|
|
||||||
}{
|
|
||||||
{"foo.com", "com", true},
|
|
||||||
{"foo.com.", "com", true},
|
|
||||||
{"foo.com.", "com.", true},
|
|
||||||
|
|
||||||
{"", "", false},
|
|
||||||
{"foo.com.", "", false},
|
|
||||||
{"foo.com.", "o.com", false},
|
|
||||||
}
|
|
||||||
for _, tt := range tests {
|
|
||||||
got := NameHasSuffix(tt.name, tt.suffix)
|
|
||||||
if got != tt.want {
|
|
||||||
t.Errorf("NameHasSuffix(%q, %q) = %v; want %v", tt.name, tt.suffix, got, tt.want)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
Loading…
Reference in New Issue
Block a user