|
|
@ -16,7 +16,6 @@ import (
|
|
|
|
|
|
|
|
|
|
|
|
qt "github.com/frankban/quicktest"
|
|
|
|
qt "github.com/frankban/quicktest"
|
|
|
|
"github.com/google/go-cmp/cmp"
|
|
|
|
"github.com/google/go-cmp/cmp"
|
|
|
|
"github.com/peterbourgon/ff/v3/ffcli"
|
|
|
|
|
|
|
|
"tailscale.com/envknob"
|
|
|
|
"tailscale.com/envknob"
|
|
|
|
"tailscale.com/health/healthmsg"
|
|
|
|
"tailscale.com/health/healthmsg"
|
|
|
|
"tailscale.com/ipn"
|
|
|
|
"tailscale.com/ipn"
|
|
|
@ -34,28 +33,106 @@ func TestPanicIfAnyEnvCheckedInInit(t *testing.T) {
|
|
|
|
envknob.PanicIfAnyEnvCheckedInInit()
|
|
|
|
envknob.PanicIfAnyEnvCheckedInInit()
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
func TestShortUsage_FullCmd(t *testing.T) {
|
|
|
|
func TestShortUsage(t *testing.T) {
|
|
|
|
t.Setenv("TAILSCALE_USE_WIP_CODE", "1")
|
|
|
|
t.Setenv("TAILSCALE_USE_WIP_CODE", "1")
|
|
|
|
if !envknob.UseWIPCode() {
|
|
|
|
if !envknob.UseWIPCode() {
|
|
|
|
t.Fatal("expected envknob.UseWIPCode() to be true")
|
|
|
|
t.Fatal("expected envknob.UseWIPCode() to be true")
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
// Some commands have more than one path from the root, so investigate all
|
|
|
|
walkCommands(newRootCmd(), func(w cmdWalk) bool {
|
|
|
|
// paths before we report errors.
|
|
|
|
c, parents := w.cmd, w.parents
|
|
|
|
ok := make(map[*ffcli.Command]bool)
|
|
|
|
|
|
|
|
root := newRootCmd()
|
|
|
|
// Words that we expect to be in the usage.
|
|
|
|
walkCommands(root, func(c *ffcli.Command) {
|
|
|
|
words := make([]string, len(parents)+1)
|
|
|
|
if !ok[c] {
|
|
|
|
for i, parent := range parents {
|
|
|
|
ok[c] = strings.HasPrefix(c.ShortUsage, "tailscale ") && (c.Name == "tailscale" || strings.Contains(c.ShortUsage, " "+c.Name+" ") || strings.HasSuffix(c.ShortUsage, " "+c.Name))
|
|
|
|
words[i] = parent.Name
|
|
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
words[len(parents)] = c.Name
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
// Check the ShortHelp starts with a capital letter.
|
|
|
|
|
|
|
|
if prefix, help := trimPrefixes(c.ShortHelp, "HIDDEN: ", "[ALPHA] ", "[BETA] "); help != "" {
|
|
|
|
|
|
|
|
if 'a' <= help[0] && help[0] <= 'z' {
|
|
|
|
|
|
|
|
if len(help) > 20 {
|
|
|
|
|
|
|
|
help = help[:20] + "…"
|
|
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
caphelp := string(help[0]-'a'+'A') + help[1:]
|
|
|
|
|
|
|
|
t.Errorf("command: %s: ShortHelp %q should start with a capital letter %q", strings.Join(words, " "), prefix+help, prefix+caphelp)
|
|
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
// Check all words appear in the usage.
|
|
|
|
|
|
|
|
usage := c.ShortUsage
|
|
|
|
|
|
|
|
for _, word := range words {
|
|
|
|
|
|
|
|
var ok bool
|
|
|
|
|
|
|
|
usage, ok = cutWord(usage, word)
|
|
|
|
|
|
|
|
if !ok {
|
|
|
|
|
|
|
|
full := strings.Join(words, " ")
|
|
|
|
|
|
|
|
t.Errorf("command: %s: usage %q should contain the full path %q", full, c.ShortUsage, full)
|
|
|
|
|
|
|
|
return true
|
|
|
|
}
|
|
|
|
}
|
|
|
|
})
|
|
|
|
|
|
|
|
walkCommands(root, func(c *ffcli.Command) {
|
|
|
|
|
|
|
|
if !ok[c] {
|
|
|
|
|
|
|
|
t.Errorf("subcommand %s should show full usage ('tailscale ... %s ...') in ShortUsage (%q)", c.Name, c.Name, c.ShortUsage)
|
|
|
|
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
return true
|
|
|
|
})
|
|
|
|
})
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
func trimPrefixes(full string, prefixes ...string) (trimmed, remaining string) {
|
|
|
|
|
|
|
|
s := full
|
|
|
|
|
|
|
|
start:
|
|
|
|
|
|
|
|
for _, p := range prefixes {
|
|
|
|
|
|
|
|
var ok bool
|
|
|
|
|
|
|
|
s, ok = strings.CutPrefix(s, p)
|
|
|
|
|
|
|
|
if ok {
|
|
|
|
|
|
|
|
goto start
|
|
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
return full[:len(full)-len(s)], s
|
|
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
// cutWord("tailscale debug scale 123", "scale") returns (" 123", true).
|
|
|
|
|
|
|
|
func cutWord(s, w string) (after string, ok bool) {
|
|
|
|
|
|
|
|
var p string
|
|
|
|
|
|
|
|
for {
|
|
|
|
|
|
|
|
p, s, ok = strings.Cut(s, w)
|
|
|
|
|
|
|
|
if !ok {
|
|
|
|
|
|
|
|
return "", false
|
|
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
if p != "" && isWordChar(p[len(p)-1]) {
|
|
|
|
|
|
|
|
continue
|
|
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
if s != "" && isWordChar(s[0]) {
|
|
|
|
|
|
|
|
continue
|
|
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
return s, true
|
|
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
func isWordChar(r byte) bool {
|
|
|
|
|
|
|
|
return r == '_' ||
|
|
|
|
|
|
|
|
('0' <= r && r <= '9') ||
|
|
|
|
|
|
|
|
('A' <= r && r <= 'Z') ||
|
|
|
|
|
|
|
|
('a' <= r && r <= 'z')
|
|
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
func TestCutWord(t *testing.T) {
|
|
|
|
|
|
|
|
tests := []struct {
|
|
|
|
|
|
|
|
in string
|
|
|
|
|
|
|
|
word string
|
|
|
|
|
|
|
|
out string
|
|
|
|
|
|
|
|
ok bool
|
|
|
|
|
|
|
|
}{
|
|
|
|
|
|
|
|
{"tailscale debug", "debug", "", true},
|
|
|
|
|
|
|
|
{"tailscale debug", "bug", "", false},
|
|
|
|
|
|
|
|
{"tailscale debug", "tail", "", false},
|
|
|
|
|
|
|
|
{"tailscale debug scaley scale 123", "scale", " 123", true},
|
|
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
for _, test := range tests {
|
|
|
|
|
|
|
|
out, ok := cutWord(test.in, test.word)
|
|
|
|
|
|
|
|
if out != test.out || ok != test.ok {
|
|
|
|
|
|
|
|
t.Errorf("cutWord(%q, %q) = (%q, %t), wanted (%q, %t)", test.in, test.word, out, ok, test.out, test.ok)
|
|
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
// geese is a collection of gooses. It need not be complete.
|
|
|
|
// geese is a collection of gooses. It need not be complete.
|
|
|
|
// But it should include anything handled specially (e.g. linux, windows)
|
|
|
|
// But it should include anything handled specially (e.g. linux, windows)
|
|
|
|
// and at least one thing that's not (darwin, freebsd).
|
|
|
|
// and at least one thing that's not (darwin, freebsd).
|
|
|
|