- Docker:新增只读、生命周期、清理、Compose 共 32 条命令,Linux 自动适配 podman - 危险命令 clearall/prunevolumes 未带 --yes 时仅提示并以退出码 2 结束 - 需要容器/镜像名的命令做必填参数校验,-- 之后参数计入位置参数 - diagnose:自动识别 Docker/Podman 容器或 K8s Pod,输出状态、根因、依据、建议并附日志与事件 - diagnose 中 -n 表示 namespace,其余命令 -n 仍为 --dry-run - 新增内置动作桥接与平台变体 helper,补充单元/集成/CLI 测试与 README
294 lines
8.7 KiB
Go
294 lines
8.7 KiB
Go
package registry
|
|
|
|
import (
|
|
"reflect"
|
|
"strings"
|
|
"testing"
|
|
)
|
|
|
|
func TestBuiltinIsValid(t *testing.T) {
|
|
reg := Builtin()
|
|
if err := reg.Validate(); err != nil {
|
|
t.Fatalf("Builtin() is invalid: %v", err)
|
|
}
|
|
}
|
|
|
|
func TestBuiltinIDsStable(t *testing.T) {
|
|
reg := Builtin()
|
|
want := []string{
|
|
"docker.ps", "docker.psall", "docker.images", "docker.stats", "docker.df",
|
|
"docker.info", "docker.version", "docker.logs", "docker.inspect",
|
|
"docker.networks", "docker.volumes",
|
|
"docker.start", "docker.stop", "docker.restart", "docker.rm", "docker.rmi",
|
|
"docker.exec", "docker.pull", "docker.tag", "docker.save", "docker.load", "docker.cp",
|
|
"docker.clearsafe", "docker.pruneimages", "docker.prunecontainers", "docker.prunenets",
|
|
"docker.prunebuilder", "docker.prunevolumes", "docker.clearall",
|
|
"docker.compose.up", "docker.compose.down", "docker.compose.ps", "docker.compose.logs",
|
|
"diagnose",
|
|
"disk.usage", "net.ports", "pkg.refreshindex",
|
|
}
|
|
if got := reg.IDs(); !reflect.DeepEqual(got, want) {
|
|
t.Fatalf("IDs() =\n%v\nwant\n%v", got, want)
|
|
}
|
|
}
|
|
|
|
func TestBuiltinClearsafeIsSafe(t *testing.T) {
|
|
reg := Builtin()
|
|
cmd, ok := reg.Lookup("docker.clearsafe")
|
|
if !ok {
|
|
t.Fatal("docker.clearsafe not found")
|
|
}
|
|
if len(cmd.Variants) < 4 {
|
|
t.Fatalf("expected a variant per platform, got %d", len(cmd.Variants))
|
|
}
|
|
|
|
for _, variant := range cmd.Variants {
|
|
if len(variant.Argv) == 0 || (variant.Argv[0] != "docker" && variant.Argv[0] != "podman") {
|
|
t.Errorf("unexpected program %v", variant.Argv)
|
|
}
|
|
if strings.Join(variant.Argv, " ") != variant.Argv[0]+" system prune -f" {
|
|
t.Errorf("clearsafe must stay scoped to `system prune -f`, got %v", variant.Argv)
|
|
}
|
|
for _, forbidden := range []string{"-a", "--all", "--volumes", "-v", "--force=false"} {
|
|
for _, arg := range variant.Argv {
|
|
if arg == forbidden {
|
|
t.Errorf("variant %v must not pass %q", variant.Argv, forbidden)
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
func TestLookupAlias(t *testing.T) {
|
|
reg := Builtin()
|
|
cmd, ok := reg.Lookup("docker.prunesafe")
|
|
if !ok {
|
|
t.Fatal("alias lookup failed")
|
|
}
|
|
if cmd.ID != "docker.clearsafe" {
|
|
t.Fatalf("alias resolved to %q", cmd.ID)
|
|
}
|
|
if _, ok := reg.Lookup("does.not.exist"); ok {
|
|
t.Fatal("unexpected lookup hit")
|
|
}
|
|
}
|
|
|
|
func TestVariantPlatformsExplicit(t *testing.T) {
|
|
for _, cmd := range Builtin().Commands {
|
|
for _, variant := range cmd.Variants {
|
|
if variant.Match.OS == "" {
|
|
t.Errorf("command %q has a variant without an OS constraint: %v", cmd.ID, variant.Argv)
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
func plainCommand(id string) Command {
|
|
return Command{
|
|
ID: id,
|
|
Summary: "summary",
|
|
Variants: []Variant{{Argv: []string{"x"}}},
|
|
}
|
|
}
|
|
|
|
func TestValidateErrors(t *testing.T) {
|
|
withBlankDistroLike := plainCommand("a")
|
|
withBlankDistroLike.Variants = []Variant{{
|
|
Match: Match{DistroLike: []string{" "}},
|
|
Argv: []string{"x"},
|
|
}}
|
|
|
|
withEmptyArgv := plainCommand("a")
|
|
withEmptyArgv.Variants = []Variant{{}}
|
|
|
|
noVariants := plainCommand("a")
|
|
noVariants.Variants = nil
|
|
|
|
noSummary := plainCommand("a")
|
|
noSummary.Summary = ""
|
|
|
|
noID := plainCommand("")
|
|
noID.ID = ""
|
|
|
|
aliased := func(first, second []string) Registry {
|
|
a, b := plainCommand("a"), plainCommand("b")
|
|
a.Aliases, b.Aliases = first, second
|
|
return Registry{Commands: []Command{a, b}}
|
|
}
|
|
|
|
tests := []struct {
|
|
name string
|
|
registry Registry
|
|
wantErr string
|
|
}{
|
|
{"empty ID", Registry{Commands: []Command{noID}}, "empty ID"},
|
|
{"empty summary", Registry{Commands: []Command{noSummary}}, "empty summary"},
|
|
{"duplicate ID", Registry{Commands: []Command{plainCommand("a"), plainCommand("a")}}, "duplicate ID"},
|
|
{"duplicate alias", aliased([]string{"z"}, []string{"z"}), "already used"},
|
|
{"no variants", Registry{Commands: []Command{noVariants}}, "no variants"},
|
|
{"empty argv", Registry{Commands: []Command{withEmptyArgv}}, "empty argv"},
|
|
{"blank DistroLike", Registry{Commands: []Command{withBlankDistroLike}}, "empty DistroLike"},
|
|
}
|
|
|
|
for _, tc := range tests {
|
|
t.Run(tc.name, func(t *testing.T) {
|
|
err := tc.registry.Validate()
|
|
if err == nil {
|
|
t.Fatal("expected an error")
|
|
}
|
|
if !strings.Contains(err.Error(), tc.wantErr) {
|
|
t.Fatalf("error %q does not contain %q", err, tc.wantErr)
|
|
}
|
|
})
|
|
}
|
|
}
|
|
|
|
func TestValidateBuiltinVariants(t *testing.T) {
|
|
builtinOnly := plainCommand("a")
|
|
builtinOnly.Variants = []Variant{{Match: Match{OS: "linux"}, Builtin: "demo.action"}}
|
|
|
|
if err := (&Registry{Commands: []Command{builtinOnly}}).Validate(); err != nil {
|
|
t.Fatalf("builtin-only variant should be valid: %v", err)
|
|
}
|
|
|
|
both := plainCommand("a")
|
|
both.Variants = []Variant{{Match: Match{OS: "linux"}, Argv: []string{"x"}, Builtin: "demo.action"}}
|
|
err := (&Registry{Commands: []Command{both}}).Validate()
|
|
if err == nil || !strings.Contains(err.Error(), "mutually exclusive") {
|
|
t.Fatalf("err = %v, want mutual-exclusion error", err)
|
|
}
|
|
}
|
|
|
|
func TestDiskUsageIsBuiltinEverywhere(t *testing.T) {
|
|
reg := Builtin()
|
|
cmd, ok := reg.Lookup("disk.usage")
|
|
if !ok {
|
|
t.Fatal("disk.usage not found")
|
|
}
|
|
seen := map[string]bool{}
|
|
for _, variant := range cmd.Variants {
|
|
if variant.Builtin != "disk.usage" {
|
|
t.Errorf("variant for %s should use the disk.usage builtin, got %q", variant.Match.OS, variant.Builtin)
|
|
}
|
|
if len(variant.Argv) != 0 {
|
|
t.Errorf("builtin variant must not carry argv: %v", variant.Argv)
|
|
}
|
|
seen[variant.Match.OS] = true
|
|
}
|
|
for _, osName := range []string{"linux", "darwin", "windows"} {
|
|
if !seen[osName] {
|
|
t.Errorf("disk.usage lacks a %s variant", osName)
|
|
}
|
|
}
|
|
}
|
|
|
|
func TestDockerCommandsWellFormed(t *testing.T) {
|
|
reg := Builtin()
|
|
destructive := map[string]bool{
|
|
"docker.clearall": true,
|
|
"docker.prunevolumes": true,
|
|
}
|
|
needsArgs := map[string]int{
|
|
"docker.logs": 1,
|
|
"docker.inspect": 1,
|
|
"docker.start": 1,
|
|
"docker.stop": 1,
|
|
"docker.restart": 1,
|
|
"docker.rm": 1,
|
|
"docker.rmi": 1,
|
|
"docker.exec": 1,
|
|
"docker.pull": 1,
|
|
"docker.tag": 2,
|
|
"docker.save": 1,
|
|
"docker.cp": 2,
|
|
}
|
|
|
|
for i := range reg.Commands {
|
|
cmd := ®.Commands[i]
|
|
if cmd.Group != "docker" {
|
|
continue
|
|
}
|
|
|
|
if destructive[cmd.ID] != cmd.RequiresYes {
|
|
t.Errorf("%s: RequiresYes = %v, want %v", cmd.ID, cmd.RequiresYes, destructive[cmd.ID])
|
|
}
|
|
if want, ok := needsArgs[cmd.ID]; ok && cmd.MinArgs != want {
|
|
t.Errorf("%s: MinArgs = %d, want %d", cmd.ID, cmd.MinArgs, want)
|
|
}
|
|
if cmd.MinArgs > 0 && strings.TrimSpace(cmd.Usage) == "" {
|
|
t.Errorf("%s: missing Usage for MinArgs=%d", cmd.ID, cmd.MinArgs)
|
|
}
|
|
|
|
var podman, docker int
|
|
for _, v := range cmd.Variants {
|
|
if len(v.Argv) == 0 {
|
|
t.Errorf("%s: docker command must have an argv variant", cmd.ID)
|
|
continue
|
|
}
|
|
switch v.Argv[0] {
|
|
case "podman":
|
|
podman++
|
|
case "docker":
|
|
docker++
|
|
}
|
|
if v.Match.Runtime != v.Argv[0] {
|
|
t.Errorf("%s: runtime %q does not match program %q", cmd.ID, v.Match.Runtime, v.Argv[0])
|
|
}
|
|
}
|
|
if podman != 1 || docker != 3 {
|
|
t.Errorf("%s: variants = %d podman / %d docker, want 1/3", cmd.ID, podman, docker)
|
|
}
|
|
}
|
|
}
|
|
|
|
func TestValidateArgs(t *testing.T) {
|
|
logs := &Command{ID: "docker.logs", Usage: "<容器>", MinArgs: 1}
|
|
tag := &Command{ID: "docker.tag", Usage: "<源> <目标>", MinArgs: 2}
|
|
|
|
cases := []struct {
|
|
name string
|
|
cmd *Command
|
|
args []string
|
|
wantErr bool
|
|
}{
|
|
{"no args", logs, nil, true},
|
|
{"only option", logs, []string{"-f"}, true},
|
|
{"positional", logs, []string{"web"}, false},
|
|
{"option then positional", logs, []string{"-f", "web"}, false},
|
|
{"option value counted loosely", logs, []string{"--tail", "100"}, false},
|
|
{"interactive option then args", logs, []string{"-it", "web", "sh"}, false},
|
|
{"path flag", &Command{ID: "disk.usage", Usage: "<路径>", MinArgs: 1}, []string{"--path", "/var"}, false},
|
|
{"after double dash", logs, []string{"--", "-weird-name"}, false},
|
|
{"two required", tag, []string{"old:1", "new:1"}, false},
|
|
{"two required short", tag, []string{"old:1"}, true},
|
|
{"option does not satisfy requirement", tag, []string{"-f", "old:1"}, true},
|
|
{"no requirement", &Command{ID: "docker.ps"}, nil, false},
|
|
}
|
|
|
|
for _, tc := range cases {
|
|
t.Run(tc.name, func(t *testing.T) {
|
|
err := tc.cmd.ValidateArgs(tc.args)
|
|
if tc.wantErr && err == nil {
|
|
t.Fatalf("expected an error for %v", tc.args)
|
|
}
|
|
if !tc.wantErr && err != nil {
|
|
t.Fatalf("unexpected error: %v", err)
|
|
}
|
|
if err != nil {
|
|
msg := err.Error()
|
|
if !strings.Contains(msg, "kopsc "+strings.ReplaceAll(tc.cmd.ID, ".", " ")) {
|
|
t.Errorf("error lacks usage hint: %q", msg)
|
|
}
|
|
}
|
|
})
|
|
}
|
|
}
|
|
|
|
func TestMatchIsComparable(t *testing.T) {
|
|
a := Match{OS: "linux", Distro: "centos", DistroLike: []string{"rhel"}, Runtime: "docker"}
|
|
b := Match{OS: "linux", Distro: "centos", DistroLike: []string{"rhel"}, Runtime: "docker"}
|
|
if !reflect.DeepEqual(a, b) {
|
|
t.Fatal("Match should be a comparable plain struct value")
|
|
}
|
|
}
|