214 lines
8.3 KiB
Go
214 lines
8.3 KiB
Go
package main
|
|
|
|
import (
|
|
"go/ast"
|
|
"go/parser"
|
|
"go/token"
|
|
"path/filepath"
|
|
"strconv"
|
|
"strings"
|
|
"testing"
|
|
)
|
|
|
|
// The schemas the controller serves, verb by verb, as the console receives them: from the
|
|
// announcement, not the table — what is checked is what a caller is shown (novox/hq issue 244).
|
|
|
|
// subsetsOf is every subset of the names, the empty one included.
|
|
func subsetsOf(names []string) [][]string {
|
|
var out [][]string
|
|
for mask := 0; mask < 1<<len(names); mask++ {
|
|
var s []string
|
|
for i, n := range names {
|
|
if mask&(1<<i) != 0 {
|
|
s = append(s, n)
|
|
}
|
|
}
|
|
out = append(out, s)
|
|
}
|
|
return out
|
|
}
|
|
|
|
func argumentsFor(subset []string, switches map[string]bool) map[string]any {
|
|
args := map[string]any{}
|
|
for _, n := range subset {
|
|
if switches[n] {
|
|
args[n] = "true"
|
|
} else {
|
|
args[n] = "x-" + n
|
|
}
|
|
}
|
|
return args
|
|
}
|
|
|
|
// saidOutright are the switches that say the default aloud, so the line is the same without them —
|
|
// on purpose, and only these.
|
|
var saidOutright = map[string]string{
|
|
"push": "behind", // the whole mesh is what no machine means; behind lets a caller say they meant it
|
|
}
|
|
|
|
// **No argument a caller gives is passed over** (novox/hq issue 244). For every verb served and
|
|
// every combination of the arguments its schema declares, the verb either refuses the call, or
|
|
// composes a command line that each given argument changed: taking any one away changes the line
|
|
// or makes it refused. An argument the line is the same without is one the verb ignored — the
|
|
// shape of the push that named a machine and pushed every machine behind.
|
|
|
|
// **An argument a verb does not declare is refused, naming it — never dropped.** Every verb, with
|
|
// what it requires and one argument more; and `node` in particular, for every verb whose schema
|
|
// does not take a machine.
|
|
|
|
// The push that was the cause: a machine named is that machine; none named is the whole mesh, and
|
|
// naming one beside behind is refused rather than one of the two guessed.
|
|
func TestAPushIsOneMachineOrSaysItIsTheWholeMesh(t *testing.T) {
|
|
argv, err := argvFor("push", map[string]any{"node": "g1", "why": "w"})
|
|
if err != nil || strings.Join(argv, " ") != "push g1 --wait 0s --why w" {
|
|
t.Fatalf("a named push: %v %v", argv, err)
|
|
}
|
|
for _, args := range []map[string]any{{"why": "w"}, {"behind": "true", "why": "w"}} {
|
|
argv, err := argvFor("push", args)
|
|
if err != nil || strings.Join(argv, " ") != "push --behind --wait 0 --why w" {
|
|
t.Fatalf("a push of the whole mesh %v: %v %v", args, argv, err)
|
|
}
|
|
}
|
|
if _, err := argvFor("push", map[string]any{"node": "g1", "behind": "true", "why": "w"}); err == nil ||
|
|
!strings.Contains(err.Error(), `"behind"`) {
|
|
t.Fatalf("a named push with behind was taken: %v", err)
|
|
}
|
|
if _, err := argvFor("push", map[string]any{"machine": "g1"}); err == nil || !strings.Contains(err.Error(), `"machine"`) {
|
|
t.Fatalf("a push given the machine under another name ran as a push of every machine: %v", err)
|
|
}
|
|
}
|
|
|
|
// commandFlags is every flag set this package's commands parse, by the name the set is made with,
|
|
// and the flags defined on it — read from the source, so a flag added to a command is seen here
|
|
// without anyone remembering to.
|
|
func commandFlags(t *testing.T) map[string][]string {
|
|
t.Helper()
|
|
files, err := filepath.Glob("*.go")
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
fset := token.NewFileSet()
|
|
out := map[string][]string{}
|
|
for _, f := range files {
|
|
if strings.HasSuffix(f, "_test.go") {
|
|
continue
|
|
}
|
|
file, err := parser.ParseFile(fset, f, nil, 0)
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
for _, decl := range file.Decls {
|
|
fn, ok := decl.(*ast.FuncDecl)
|
|
if !ok || fn.Body == nil {
|
|
continue
|
|
}
|
|
sets := map[string]string{} // variable → the set's name
|
|
ast.Inspect(fn.Body, func(n ast.Node) bool {
|
|
if assign, ok := n.(*ast.AssignStmt); ok && len(assign.Lhs) == 1 && len(assign.Rhs) == 1 {
|
|
if call, ok := assign.Rhs[0].(*ast.CallExpr); ok && isSelector(call.Fun, "flag", "NewFlagSet") {
|
|
if id, ok := assign.Lhs[0].(*ast.Ident); ok {
|
|
if name, ok := stringLit(call.Args[0]); ok {
|
|
sets[id.Name] = name
|
|
out[name] = append(out[name], []string{}...)
|
|
}
|
|
}
|
|
}
|
|
}
|
|
call, ok := n.(*ast.CallExpr)
|
|
if !ok {
|
|
return true
|
|
}
|
|
sel, ok := call.Fun.(*ast.SelectorExpr)
|
|
if !ok {
|
|
return true
|
|
}
|
|
recv, ok := sel.X.(*ast.Ident)
|
|
if !ok || sets[recv.Name] == "" {
|
|
return true
|
|
}
|
|
arg := 0
|
|
switch sel.Sel.Name {
|
|
case "Bool", "String", "Int", "Int64", "Uint", "Uint64", "Float64", "Duration", "Func", "BoolFunc", "TextVar":
|
|
case "BoolVar", "StringVar", "IntVar", "Int64Var", "UintVar", "Uint64Var", "Float64Var", "DurationVar", "Var":
|
|
arg = 1
|
|
default:
|
|
return true
|
|
}
|
|
if arg < len(call.Args) {
|
|
if flagName, ok := stringLit(call.Args[arg]); ok {
|
|
out[sets[recv.Name]] = append(out[sets[recv.Name]], flagName)
|
|
}
|
|
}
|
|
return true
|
|
})
|
|
}
|
|
}
|
|
return out
|
|
}
|
|
|
|
func isSelector(e ast.Expr, pkg, name string) bool {
|
|
sel, ok := e.(*ast.SelectorExpr)
|
|
if !ok {
|
|
return false
|
|
}
|
|
id, ok := sel.X.(*ast.Ident)
|
|
return ok && id.Name == pkg && sel.Sel.Name == name
|
|
}
|
|
|
|
func stringLit(e ast.Expr) (string, bool) {
|
|
lit, ok := e.(*ast.BasicLit)
|
|
if !ok || lit.Kind != token.STRING {
|
|
return "", false
|
|
}
|
|
s, err := strconv.Unquote(lit.Value)
|
|
return s, err == nil
|
|
}
|
|
|
|
// accountedFlags are the flags of a verb's command that are not an argument of the same name:
|
|
// carried by an argument named otherwise ("=argument"), or set by the verb itself, or withheld from
|
|
// the named verb on purpose — each with why. `command` reaches every flag of the binary regardless.
|
|
var accountedFlags = map[string]map[string]string{
|
|
"status": {"json": "set by the verb: the answer is data"},
|
|
"seats": {"json": "set by the verb: the answer is data"},
|
|
"queue": {"json": "set by the verb: the answer is data, its output the table (novox/hq issue 455)"},
|
|
"plan": {"json": "set by the verb unless files is asked"},
|
|
"push": {
|
|
"wait": "set by the verb to 0: a tool call cannot hold a connection for a whole apply",
|
|
"all": "withheld: the verb naming no machine is --behind, and a push of every machine is said at a " +
|
|
"shell (novox/hq ADR 0217); `command` reaches it",
|
|
},
|
|
// Kept only to be refused by name, naming what replaced it (novox/hq ADR 0501 §5, ADR 0429 §18).
|
|
"replay": {"register": "withheld: refused by name, a registered replay being removed (ADR 0501 §5)"},
|
|
"builds": {"n": "=limit"},
|
|
"plans": {"n": "=limit", "what-if": "=repository"},
|
|
// The machine's tunnel key, named as the verb's other arguments are (novox/hq ADR 0169).
|
|
"durations": {
|
|
"json": "set by the verb: the answer is data",
|
|
"all": "withheld: every measurement of a fortnight is more than a call should carry; `command` reaches it",
|
|
},
|
|
// The desk path of `secret accept` (novox/hq ADR 0259 §10): a value is never an argument of a call.
|
|
"hand-acts": {"json": "set by the verb: the answer is data"},
|
|
"conditions": {"json": "set by the verb: the answer is data"},
|
|
"retire": {"json": "set by the verb: the answer is data"},
|
|
"cleanup": {"json": "set by the verb: the answer is data"},
|
|
"data": {"json": "set by the verb: the answer is data"},
|
|
"artifacts": {"json": "set by the verb: the answer is data"},
|
|
"collect": {"json": "set by the verb: the answer is data"},
|
|
"images": {"json": "set by the verb: the answer is data"},
|
|
"mirrors": {"json": "set by the verb: the answer is data"},
|
|
"move-placement": {"json": "set by the verb: the answer is data"},
|
|
"move-config": {"json": "set by the verb: the answer is data"},
|
|
"conditions history": {"json": "set by the verb: the answer is data"},
|
|
"conditions show": {"json": "set by the verb: the answer is data"},
|
|
"healers": {"json": "set by the verb: the answer is data"},
|
|
}
|
|
|
|
// **Every flag of the command a verb runs is in the verb's schema, or accounted for here.** Derived
|
|
// from the source, so a flag added to a command — or a verb added whose command takes flags —
|
|
// fails this until somebody decides, in writing, how a caller reaches it (novox/hq issue 244). And
|
|
// a flag accounted for that no longer exists fails too, so the table cannot rot into a list nobody
|
|
// reads.
|
|
|
|
// Every verb's required arguments are properties of its schema: a schema that requires what it
|
|
// does not describe is the uncallable verb of issue 244 from the other side.
|