Files
vpp-maglev/cmd/maglevc/tree_test.go
Pim van Pelt 3bd30b69f4 Refactor CLI: birdc-style help, collapsed nouns, ReloadConfig, bug fixes
maglevc
- Rewrite '?' handler (birdc-style): show full command paths from current
  position to every leaf, right-aligned help column, dynamic slot values
  displayed as an indented block when cursor is at a slot position.
- Collapse show frontends/frontend, backends/backend, healthchecks/healthcheck
  into single plural-noun nodes with an optional <name> slot. Allows
  'sh ba' (list all) and 'sh ba nginx0' (show one) without ambiguity.
- Add 'config reload' command.
- Fix tabwriter ANSI alignment: continuation lines in transition output
  now carry the same label() byte overhead as the header line.
- Fix broken Walk for 'set frontend' command: setFrontendPoolName and
  setWeightValue were fixed-word nodes that couldn't capture user input;
  mark them as slot nodes with dynNone.
- Add tree_test.go covering expandPaths, cycle detection, prefix matching,
  and the full weight-command walk.

gRPC / proto
- Add ReloadConfig RPC: checks config then applies it to the running
  checker, returning ok/parse_error/semantic_error/reload_error.
- Add logging to CheckConfig (config-check-start/config-check-done at
  INFO level).

maglevd
- SIGHUP handler now calls maglevServer.TriggerReload(), sharing the
  same code path as the gRPC ReloadConfig RPC.

docs
- Collapse show command documentation to use [<name>] optional syntax.
- Remove developer-facing 'Command tree and parser' section.
- Document 'config reload'.
2026-04-11 18:20:43 +02:00

154 lines
4.3 KiB
Go

// Copyright (c) 2026, Pim van Pelt <pim@ipng.ch>
package main
import (
"strings"
"testing"
)
func TestExpandPathsRoot(t *testing.T) {
root := buildTree()
lines := expandPaths(root, "", make(map[*Node]bool))
// Should include well-known leaf paths.
want := []string{
"show version",
"show frontends",
"show frontends <name>",
"show backends",
"show backends <name>",
"show healthchecks",
"show healthchecks <name>",
"set backend <name> pause",
"set backend <name> resume",
"set backend <name> disable",
"set backend <name> enable",
"set frontend <name> pool <pool> backend <backend> weight <weight>",
"watch events",
"watch events <opt>",
"config check",
"config reload",
"quit",
"exit",
}
paths := make(map[string]bool, len(lines))
for _, l := range lines {
paths[l.path] = true
}
for _, w := range want {
if !paths[w] {
t.Errorf("expandPaths(root) missing %q", w)
}
}
}
func TestExpandPathsShow(t *testing.T) {
root := buildTree()
showNode, _ := Walk(root, []string{"show"})
lines := expandPaths(showNode, "show", make(map[*Node]bool))
for _, l := range lines {
if !strings.HasPrefix(l.path, "show ") {
t.Errorf("unexpected path %q: should start with 'show '", l.path)
}
if l.help == "" {
t.Errorf("path %q has empty help", l.path)
}
}
// version, frontends, frontends <name>, backends, backends <name>,
// healthchecks, healthchecks <name> = 7 lines
if len(lines) != 7 {
t.Errorf("expected exactly 7 show subcommands, got %d", len(lines))
}
}
func TestExpandPathsNoCycles(t *testing.T) {
root := buildTree()
// watch events has a self-referencing slot; expandPaths must terminate.
watchEvents, _ := Walk(root, []string{"watch", "events"})
lines := expandPaths(watchEvents, "watch events", make(map[*Node]bool))
// Should produce exactly 2 lines: "watch events" and "watch events <opt>".
if len(lines) != 2 {
t.Errorf("watch events: expected 2 lines, got %d: %v", len(lines), lines)
}
}
func TestExpandPathsSetBackendName(t *testing.T) {
root := buildTree()
// Walk to the name slot so displayPrefix carries the actual arg.
node, _ := Walk(root, []string{"set", "backend", "mybackend"})
lines := expandPaths(node, "set backend mybackend", make(map[*Node]bool))
want := []string{
"set backend mybackend pause",
"set backend mybackend resume",
"set backend mybackend disable",
"set backend mybackend enable",
}
if len(lines) != len(want) {
t.Fatalf("expected %d lines, got %d: %v", len(want), len(lines), lines)
}
for i, w := range want {
if lines[i].path != w {
t.Errorf("line %d: got %q, want %q", i, lines[i].path, w)
}
}
}
func TestPrefixMatchCollapsedNouns(t *testing.T) {
root := buildTree()
// "sh ba" → show backends (list all) via prefix matching.
node, args := Walk(root, []string{"sh", "ba"})
if node.Run == nil {
t.Fatal("'sh ba' did not reach a Run node")
}
if len(args) != 0 {
t.Errorf("'sh ba' should have 0 args, got %v", args)
}
// "sh ba nginx0" → show backends <name> (get specific) via slot.
node, args = Walk(root, []string{"sh", "ba", "nginx0"})
if node.Run == nil {
t.Fatal("'sh ba nginx0' did not reach a Run node")
}
if len(args) != 1 || args[0] != "nginx0" {
t.Errorf("'sh ba nginx0' args: got %v, want [nginx0]", args)
}
// "sh fr" → show frontends (list all).
node, _ = Walk(root, []string{"sh", "fr"})
if node.Run == nil {
t.Fatal("'sh fr' did not reach a Run node")
}
// "sh he icmp" → show healthchecks icmp (get specific).
node, args = Walk(root, []string{"sh", "he", "icmp"})
if node.Run == nil {
t.Fatal("'sh he icmp' did not reach a Run node")
}
if len(args) != 1 || args[0] != "icmp" {
t.Errorf("'sh he icmp' args: got %v, want [icmp]", args)
}
}
func TestExpandPathsWeightSlotWalk(t *testing.T) {
// Verify the weight command is fully walkable (fixes bug: setWeightValue
// and setFrontendPoolName were non-slot nodes that couldn't capture tokens).
root := buildTree()
node, args := Walk(root, []string{"set", "frontend", "web", "pool", "primary", "backend", "be0", "weight", "42"})
if node.Run == nil {
t.Fatal("Walk did not reach a Run node for full weight command")
}
if len(args) != 4 {
t.Errorf("expected 4 args (name, pool, backend, weight), got %d: %v", len(args), args)
}
if args[3] != "42" {
t.Errorf("args[3] (weight): got %q, want 42", args[3])
}
}