coverage: easy-wins batch across cli, system, paths, vmdns, toolingplan
Pure-Go tests for formatters, layout resolution, and validators — no fixtures, no external processes. Targets previously-zero functions the triage scan flagged as low-hanging fruit. cli 55% -> 65% paths 64% -> 91% system 65% -> 75% vmdns 72% -> 86% toolingplan 73% -> 78% Total 52.6% -> 54.0%.
This commit is contained in:
parent
a3cc296523
commit
f8979de58a
5 changed files with 740 additions and 0 deletions
355
internal/cli/formatters_test.go
Normal file
355
internal/cli/formatters_test.go
Normal file
|
|
@ -0,0 +1,355 @@
|
|||
package cli
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"errors"
|
||||
"fmt"
|
||||
"reflect"
|
||||
"strings"
|
||||
"testing"
|
||||
|
||||
"banger/internal/api"
|
||||
"banger/internal/model"
|
||||
|
||||
"github.com/spf13/cobra"
|
||||
)
|
||||
|
||||
func TestHumanSize(t *testing.T) {
|
||||
cases := []struct {
|
||||
bytes int64
|
||||
want string
|
||||
}{
|
||||
{-1, "-"},
|
||||
{0, "-"},
|
||||
{1, "1B"},
|
||||
{1023, "1023B"},
|
||||
{1024, "1.0KiB"},
|
||||
{2048, "2.0KiB"},
|
||||
{1024 * 1024, "1.0MiB"},
|
||||
{5 * 1024 * 1024, "5.0MiB"},
|
||||
{1024 * 1024 * 1024, "1.0GiB"},
|
||||
{3 * 1024 * 1024 * 1024, "3.0GiB"},
|
||||
}
|
||||
for _, tc := range cases {
|
||||
if got := humanSize(tc.bytes); got != tc.want {
|
||||
t.Errorf("humanSize(%d) = %q, want %q", tc.bytes, got, tc.want)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func TestDashIfEmpty(t *testing.T) {
|
||||
cases := map[string]string{
|
||||
"": "-",
|
||||
" ": "-",
|
||||
"\t\n": "-",
|
||||
"value": "value",
|
||||
" hello ": " hello ",
|
||||
}
|
||||
for in, want := range cases {
|
||||
if got := dashIfEmpty(in); got != want {
|
||||
t.Errorf("dashIfEmpty(%q) = %q, want %q", in, got, want)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func TestParseKeyValuePairs(t *testing.T) {
|
||||
t.Run("nil when empty", func(t *testing.T) {
|
||||
got, err := parseKeyValuePairs(nil)
|
||||
if err != nil {
|
||||
t.Fatalf("unexpected error: %v", err)
|
||||
}
|
||||
if got != nil {
|
||||
t.Fatalf("got %v, want nil", got)
|
||||
}
|
||||
})
|
||||
|
||||
t.Run("parses entries", func(t *testing.T) {
|
||||
got, err := parseKeyValuePairs([]string{"a=1", " b = two", "c=x=y"})
|
||||
if err != nil {
|
||||
t.Fatalf("unexpected error: %v", err)
|
||||
}
|
||||
want := map[string]string{"a": "1", "b": " two", "c": "x=y"}
|
||||
if !reflect.DeepEqual(got, want) {
|
||||
t.Fatalf("got %v, want %v", got, want)
|
||||
}
|
||||
})
|
||||
|
||||
t.Run("rejects malformed entries", func(t *testing.T) {
|
||||
for _, bad := range []string{"noequals", "=noKey", " =v"} {
|
||||
if _, err := parseKeyValuePairs([]string{bad}); err == nil {
|
||||
t.Errorf("expected error for %q", bad)
|
||||
}
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
func TestExitCodeErrorError(t *testing.T) {
|
||||
e := ExitCodeError{Code: 42}
|
||||
got := e.Error()
|
||||
if !strings.Contains(got, "42") {
|
||||
t.Fatalf("error %q missing code", got)
|
||||
}
|
||||
|
||||
var target ExitCodeError
|
||||
if !errors.As(error(e), &target) {
|
||||
t.Fatal("errors.As failed to match ExitCodeError")
|
||||
}
|
||||
if target.Code != 42 {
|
||||
t.Fatalf("target.Code = %d, want 42", target.Code)
|
||||
}
|
||||
}
|
||||
|
||||
func TestShortID(t *testing.T) {
|
||||
cases := map[string]string{
|
||||
"": "",
|
||||
"abc": "abc",
|
||||
"0123456789ab": "0123456789ab",
|
||||
"0123456789abcd": "0123456789ab",
|
||||
"0123456789abcdefghij": "0123456789ab",
|
||||
}
|
||||
for in, want := range cases {
|
||||
if got := shortID(in); got != want {
|
||||
t.Errorf("shortID(%q) = %q, want %q", in, got, want)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func TestImageNameIndex(t *testing.T) {
|
||||
images := []model.Image{
|
||||
{ID: "id-a", Name: "alpha"},
|
||||
{ID: "id-b", Name: "beta"},
|
||||
}
|
||||
idx := imageNameIndex(images)
|
||||
if len(idx) != 2 {
|
||||
t.Fatalf("len = %d, want 2", len(idx))
|
||||
}
|
||||
if idx["id-a"] != "alpha" || idx["id-b"] != "beta" {
|
||||
t.Fatalf("unexpected index %v", idx)
|
||||
}
|
||||
|
||||
empty := imageNameIndex(nil)
|
||||
if empty == nil || len(empty) != 0 {
|
||||
t.Fatalf("expected empty non-nil map, got %v", empty)
|
||||
}
|
||||
}
|
||||
|
||||
func TestHelpNoArgs(t *testing.T) {
|
||||
called := false
|
||||
cmd := &cobra.Command{
|
||||
Use: "x",
|
||||
RunE: func(cmd *cobra.Command, args []string) error {
|
||||
called = true
|
||||
return nil
|
||||
},
|
||||
}
|
||||
cmd.SetOut(&bytes.Buffer{})
|
||||
cmd.SetErr(&bytes.Buffer{})
|
||||
|
||||
if err := helpNoArgs(cmd, nil); err != nil {
|
||||
t.Fatalf("helpNoArgs(nil): %v", err)
|
||||
}
|
||||
if called {
|
||||
t.Fatal("helpNoArgs should not invoke Run")
|
||||
}
|
||||
|
||||
if err := helpNoArgs(cmd, []string{"bogus"}); err == nil {
|
||||
t.Fatal("expected error for unexpected args")
|
||||
}
|
||||
}
|
||||
|
||||
func TestArgsValidators(t *testing.T) {
|
||||
cmd := &cobra.Command{Use: "x"}
|
||||
|
||||
exact := exactArgsUsage(2, "need exactly two")
|
||||
if err := exact(cmd, []string{"a", "b"}); err != nil {
|
||||
t.Fatalf("exact(2 args): %v", err)
|
||||
}
|
||||
if err := exact(cmd, []string{"a"}); err == nil {
|
||||
t.Fatal("expected error for 1 arg with exactArgsUsage(2)")
|
||||
}
|
||||
|
||||
minArgs := minArgsUsage(1, "need at least one")
|
||||
if err := minArgs(cmd, []string{"a"}); err != nil {
|
||||
t.Fatalf("min(1 arg): %v", err)
|
||||
}
|
||||
if err := minArgs(cmd, nil); err == nil {
|
||||
t.Fatal("expected error for 0 args with minArgsUsage(1)")
|
||||
}
|
||||
|
||||
maxArgs := maxArgsUsage(1, "at most one")
|
||||
if err := maxArgs(cmd, []string{"a"}); err != nil {
|
||||
t.Fatalf("max(1 arg): %v", err)
|
||||
}
|
||||
if err := maxArgs(cmd, []string{"a", "b"}); err == nil {
|
||||
t.Fatal("expected error for 2 args with maxArgsUsage(1)")
|
||||
}
|
||||
|
||||
noArgs := noArgsUsage("none allowed")
|
||||
if err := noArgs(cmd, nil); err != nil {
|
||||
t.Fatalf("no args: %v", err)
|
||||
}
|
||||
if err := noArgs(cmd, []string{"a"}); err == nil {
|
||||
t.Fatal("expected error for args with noArgsUsage")
|
||||
}
|
||||
}
|
||||
|
||||
func TestPrintKernelListTable(t *testing.T) {
|
||||
var buf bytes.Buffer
|
||||
entries := []api.KernelEntry{
|
||||
{Name: "generic-6.12", Distro: "debian", Arch: "x86_64", KernelVersion: "6.12", ImportedAt: "2026-01-01"},
|
||||
{Name: "bare"},
|
||||
}
|
||||
if err := printKernelListTable(&buf, entries); err != nil {
|
||||
t.Fatalf("printKernelListTable: %v", err)
|
||||
}
|
||||
got := buf.String()
|
||||
for _, want := range []string{"NAME", "DISTRO", "generic-6.12", "bare"} {
|
||||
if !strings.Contains(got, want) {
|
||||
t.Errorf("output missing %q:\n%s", want, got)
|
||||
}
|
||||
}
|
||||
// Empty fields render as "-".
|
||||
if !strings.Contains(got, "-") {
|
||||
t.Errorf("expected dash for empty fields, got:\n%s", got)
|
||||
}
|
||||
}
|
||||
|
||||
func TestPrintKernelCatalogTable(t *testing.T) {
|
||||
var buf bytes.Buffer
|
||||
entries := []api.KernelCatalogEntry{
|
||||
{Name: "generic-6.12", Arch: "x86_64", KernelVersion: "6.12", SizeBytes: 2 * 1024 * 1024, Pulled: true},
|
||||
{Name: "new-kernel", SizeBytes: 0, Pulled: false},
|
||||
}
|
||||
if err := printKernelCatalogTable(&buf, entries); err != nil {
|
||||
t.Fatalf("printKernelCatalogTable: %v", err)
|
||||
}
|
||||
got := buf.String()
|
||||
for _, want := range []string{"generic-6.12", "pulled", "available", "new-kernel"} {
|
||||
if !strings.Contains(got, want) {
|
||||
t.Errorf("output missing %q:\n%s", want, got)
|
||||
}
|
||||
}
|
||||
if !strings.Contains(got, "2.0MiB") {
|
||||
t.Errorf("expected humanSize(2 MiB), got:\n%s", got)
|
||||
}
|
||||
}
|
||||
|
||||
func TestPrintGuestSessionTable(t *testing.T) {
|
||||
var buf bytes.Buffer
|
||||
sessions := []model.GuestSession{
|
||||
{ID: "abcdef0123456789", Name: "planner", Status: "running", Command: "pi", CWD: "/root/repo", Attachable: true},
|
||||
{ID: "short", Name: "once", Status: "exited", Command: "true", CWD: "/tmp", Attachable: false},
|
||||
}
|
||||
if err := printGuestSessionTable(&buf, sessions); err != nil {
|
||||
t.Fatalf("printGuestSessionTable: %v", err)
|
||||
}
|
||||
got := buf.String()
|
||||
for _, want := range []string{"ID", "NAME", "planner", "once", "yes", "no", "pi"} {
|
||||
if !strings.Contains(got, want) {
|
||||
t.Errorf("output missing %q:\n%s", want, got)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func TestPrintGuestSessionSummary(t *testing.T) {
|
||||
var buf bytes.Buffer
|
||||
session := model.GuestSession{
|
||||
ID: "id1", Name: "s", Status: "exited", Command: "true", CWD: "/root",
|
||||
}
|
||||
if err := printGuestSessionSummary(&buf, session); err != nil {
|
||||
t.Fatalf("printGuestSessionSummary: %v", err)
|
||||
}
|
||||
got := buf.String()
|
||||
fields := strings.Split(strings.TrimRight(got, "\n"), "\t")
|
||||
if len(fields) != 5 {
|
||||
t.Fatalf("expected 5 tab-separated fields, got %d: %q", len(fields), got)
|
||||
}
|
||||
}
|
||||
|
||||
func TestPrintJSON(t *testing.T) {
|
||||
var buf bytes.Buffer
|
||||
if err := printJSON(&buf, map[string]int{"a": 1, "b": 2}); err != nil {
|
||||
t.Fatalf("printJSON: %v", err)
|
||||
}
|
||||
got := buf.String()
|
||||
if !strings.Contains(got, `"a": 1`) || !strings.Contains(got, `"b": 2`) {
|
||||
t.Errorf("unexpected JSON output:\n%s", got)
|
||||
}
|
||||
if !strings.HasSuffix(got, "\n") {
|
||||
t.Error("printJSON should terminate with newline")
|
||||
}
|
||||
}
|
||||
|
||||
func TestPrintJSONUnmarshalableValue(t *testing.T) {
|
||||
var buf bytes.Buffer
|
||||
// Channels are not JSON-marshalable.
|
||||
err := printJSON(&buf, make(chan int))
|
||||
if err == nil {
|
||||
t.Fatal("expected error for unmarshalable value")
|
||||
}
|
||||
}
|
||||
|
||||
func TestPrintVMSummary(t *testing.T) {
|
||||
var buf bytes.Buffer
|
||||
vm := model.VMRecord{
|
||||
ID: "0123456789abcdef",
|
||||
Name: "demo",
|
||||
State: model.VMStateRunning,
|
||||
}
|
||||
vm.Runtime.GuestIP = "172.16.0.5"
|
||||
vm.Runtime.DNSName = "demo.vm"
|
||||
vm.Spec.WorkDiskSizeBytes = 0
|
||||
if err := printVMSummary(&buf, vm); err != nil {
|
||||
t.Fatalf("printVMSummary: %v", err)
|
||||
}
|
||||
got := buf.String()
|
||||
for _, want := range []string{"0123456789ab", "demo", "172.16.0.5", "demo.vm"} {
|
||||
if !strings.Contains(got, want) {
|
||||
t.Errorf("summary missing %q:\n%s", want, got)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func TestPrintImageSummary(t *testing.T) {
|
||||
var buf bytes.Buffer
|
||||
img := model.Image{ID: "img-id", Name: "debian-bookworm", Managed: true, RootfsPath: "/var/rootfs.ext4"}
|
||||
if err := printImageSummary(&buf, img); err != nil {
|
||||
t.Fatalf("printImageSummary: %v", err)
|
||||
}
|
||||
got := buf.String()
|
||||
for _, want := range []string{"debian-bookworm", "true", "/var/rootfs.ext4"} {
|
||||
if !strings.Contains(got, want) {
|
||||
t.Errorf("summary missing %q:\n%s", want, got)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func TestVMImageLabel(t *testing.T) {
|
||||
names := map[string]string{"img-1": "debian"}
|
||||
if got := vmImageLabel("img-1", names); got != "debian" {
|
||||
t.Errorf("got %q, want debian", got)
|
||||
}
|
||||
if got := vmImageLabel("img-2", names); got != "img-2" {
|
||||
t.Errorf("fallback: got %q, want img-2", got)
|
||||
}
|
||||
}
|
||||
|
||||
// failWriter lets us exercise io-error branches of the printers.
|
||||
type failWriter struct{}
|
||||
|
||||
func (failWriter) Write([]byte) (int, error) { return 0, fmt.Errorf("boom") }
|
||||
|
||||
func TestPrintersPropagateWriteErrors(t *testing.T) {
|
||||
sessions := []model.GuestSession{{ID: "id", Name: "n"}}
|
||||
if err := printGuestSessionTable(failWriter{}, sessions); err == nil {
|
||||
t.Error("expected write error from printGuestSessionTable")
|
||||
}
|
||||
kernels := []api.KernelEntry{{Name: "k"}}
|
||||
if err := printKernelListTable(failWriter{}, kernels); err == nil {
|
||||
t.Error("expected write error from printKernelListTable")
|
||||
}
|
||||
catalog := []api.KernelCatalogEntry{{Name: "k"}}
|
||||
if err := printKernelCatalogTable(failWriter{}, catalog); err == nil {
|
||||
t.Error("expected write error from printKernelCatalogTable")
|
||||
}
|
||||
}
|
||||
Loading…
Add table
Add a link
Reference in a new issue