diff --git a/internal/cli/formatters_test.go b/internal/cli/formatters_test.go new file mode 100644 index 0000000..c5833d2 --- /dev/null +++ b/internal/cli/formatters_test.go @@ -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") + } +} diff --git a/internal/paths/layout_test.go b/internal/paths/layout_test.go new file mode 100644 index 0000000..d95ede1 --- /dev/null +++ b/internal/paths/layout_test.go @@ -0,0 +1,136 @@ +package paths + +import ( + "os" + "path/filepath" + "strings" + "testing" +) + +func TestResolveUsesXDGOverrides(t *testing.T) { + dir := t.TempDir() + t.Setenv("XDG_CONFIG_HOME", filepath.Join(dir, "config")) + t.Setenv("XDG_STATE_HOME", filepath.Join(dir, "state")) + t.Setenv("XDG_CACHE_HOME", filepath.Join(dir, "cache")) + t.Setenv("XDG_RUNTIME_DIR", filepath.Join(dir, "run")) + + layout, err := Resolve() + if err != nil { + t.Fatalf("Resolve: %v", err) + } + if layout.ConfigDir != filepath.Join(dir, "config", "banger") { + t.Errorf("ConfigDir = %q", layout.ConfigDir) + } + if layout.StateDir != filepath.Join(dir, "state", "banger") { + t.Errorf("StateDir = %q", layout.StateDir) + } + if layout.CacheDir != filepath.Join(dir, "cache", "banger") { + t.Errorf("CacheDir = %q", layout.CacheDir) + } + if layout.RuntimeDir != filepath.Join(dir, "run", "banger") { + t.Errorf("RuntimeDir = %q", layout.RuntimeDir) + } + if !strings.HasSuffix(layout.SocketPath, "bangerd.sock") { + t.Errorf("SocketPath = %q", layout.SocketPath) + } + if !strings.HasSuffix(layout.DBPath, "state.db") { + t.Errorf("DBPath = %q", layout.DBPath) + } +} + +func TestResolveFallsBackWhenRuntimeUnset(t *testing.T) { + t.Setenv("XDG_RUNTIME_DIR", "") + layout, err := Resolve() + if err != nil { + t.Fatalf("Resolve: %v", err) + } + if !strings.Contains(layout.RuntimeDir, "banger-runtime-") { + t.Errorf("expected fallback runtime dir, got %q", layout.RuntimeDir) + } +} + +func TestEnsureCreatesAllDirs(t *testing.T) { + base := t.TempDir() + layout := Layout{ + ConfigDir: filepath.Join(base, "config"), + StateDir: filepath.Join(base, "state"), + CacheDir: filepath.Join(base, "cache"), + RuntimeDir: filepath.Join(base, "runtime"), + VMsDir: filepath.Join(base, "state/vms"), + ImagesDir: filepath.Join(base, "state/images"), + KernelsDir: filepath.Join(base, "state/kernels"), + OCICacheDir: filepath.Join(base, "cache/oci"), + } + if err := Ensure(layout); err != nil { + t.Fatalf("Ensure: %v", err) + } + for _, dir := range []string{ + layout.ConfigDir, + layout.StateDir, + layout.CacheDir, + layout.RuntimeDir, + layout.VMsDir, + layout.ImagesDir, + layout.KernelsDir, + layout.OCICacheDir, + } { + info, err := os.Stat(dir) + if err != nil { + t.Errorf("stat %q: %v", dir, err) + continue + } + if !info.IsDir() { + t.Errorf("%q is not a directory", dir) + } + } + + // Idempotent. + if err := Ensure(layout); err != nil { + t.Fatalf("Ensure (second run): %v", err) + } +} + +func TestBangerdPathEnvOverride(t *testing.T) { + t.Setenv("BANGER_DAEMON_BIN", "/tmp/custom-bangerd") + got, err := BangerdPath() + if err != nil { + t.Fatalf("BangerdPath: %v", err) + } + if got != "/tmp/custom-bangerd" { + t.Errorf("got %q, want /tmp/custom-bangerd", got) + } +} + +func TestBangerdPathFindsSiblingBinary(t *testing.T) { + t.Setenv("BANGER_DAEMON_BIN", "") + + root := t.TempDir() + sibling := filepath.Join(root, "bangerd") + if err := os.WriteFile(sibling, []byte("#!/bin/sh\n"), 0o755); err != nil { + t.Fatalf("WriteFile: %v", err) + } + original := executablePath + executablePath = func() (string, error) { return filepath.Join(root, "banger"), nil } + t.Cleanup(func() { executablePath = original }) + + got, err := BangerdPath() + if err != nil { + t.Fatalf("BangerdPath: %v", err) + } + if got != sibling { + t.Errorf("got %q, want %q", got, sibling) + } +} + +func TestBangerdPathNotFound(t *testing.T) { + t.Setenv("BANGER_DAEMON_BIN", "") + + root := t.TempDir() + original := executablePath + executablePath = func() (string, error) { return filepath.Join(root, "banger"), nil } + t.Cleanup(func() { executablePath = original }) + + if _, err := BangerdPath(); err == nil { + t.Fatal("expected error when no sibling bangerd exists") + } +} diff --git a/internal/system/extra_test.go b/internal/system/extra_test.go new file mode 100644 index 0000000..ce912e4 --- /dev/null +++ b/internal/system/extra_test.go @@ -0,0 +1,133 @@ +package system + +import ( + "context" + "encoding/json" + "os" + "path/filepath" + "runtime" + "testing" +) + +func TestWriteJSONRoundtrip(t *testing.T) { + path := filepath.Join(t.TempDir(), "out.json") + value := map[string]any{"name": "banger", "n": 42.0} + if err := WriteJSON(path, value); err != nil { + t.Fatalf("WriteJSON: %v", err) + } + data, err := os.ReadFile(path) + if err != nil { + t.Fatalf("ReadFile: %v", err) + } + var got map[string]any + if err := json.Unmarshal(data, &got); err != nil { + t.Fatalf("Unmarshal: %v", err) + } + if got["name"] != "banger" || got["n"].(float64) != 42.0 { + t.Fatalf("decoded = %v", got) + } +} + +func TestWriteJSONErrorsForUnmarshalable(t *testing.T) { + path := filepath.Join(t.TempDir(), "out.json") + if err := WriteJSON(path, make(chan int)); err == nil { + t.Fatal("expected marshal error for channel value") + } + if _, err := os.Stat(path); !os.IsNotExist(err) { + t.Fatalf("expected no file when marshal fails, got %v", err) + } +} + +func TestTailCommand(t *testing.T) { + cmd := TailCommand("/tmp/log.txt", false) + if cmd == nil || cmd.Path == "" { + t.Fatal("TailCommand(false) returned nil/empty") + } + // follow=false → cat, follow=true → tail -f. + if !hasArg(cmd.Args, "/tmp/log.txt") { + t.Fatalf("cat args missing path: %v", cmd.Args) + } + + followCmd := TailCommand("/tmp/log.txt", true) + if !hasArg(followCmd.Args, "-f") { + t.Fatalf("follow cmd missing -f: %v", followCmd.Args) + } + if !hasArg(followCmd.Args, "/tmp/log.txt") { + t.Fatalf("follow cmd missing path: %v", followCmd.Args) + } +} + +func hasArg(args []string, want string) bool { + for _, a := range args { + if a == want { + return true + } + } + return false +} + +func TestReportAddWarnAndHasFailures(t *testing.T) { + var r Report + r.AddPass("a") + r.AddWarn("b", "detail-1", "detail-2") + if r.HasFailures() { + t.Fatal("HasFailures should be false with only pass+warn") + } + if len(r.Checks) != 2 { + t.Fatalf("len(Checks) = %d, want 2", len(r.Checks)) + } + if r.Checks[1].Status != CheckStatusWarn { + t.Fatalf("check[1].Status = %v, want warn", r.Checks[1].Status) + } + if len(r.Checks[1].Details) != 2 { + t.Fatalf("warn details lost: %v", r.Checks[1].Details) + } + + r.AddFail("c") + if !r.HasFailures() { + t.Fatal("HasFailures should be true after AddFail") + } +} + +func TestRequireCommandsMissing(t *testing.T) { + err := RequireCommands(context.Background(), "this-command-cannot-possibly-exist-xyz-123") + if err == nil { + t.Fatal("expected error for missing command") + } +} + +func TestRequireCommandsPresent(t *testing.T) { + // `go` is guaranteed on PATH during test runs. + if err := RequireCommands(context.Background(), "go"); err != nil { + t.Fatalf("RequireCommands(go): %v", err) + } +} + +func TestReadHostResources(t *testing.T) { + if runtime.GOOS != "linux" { + t.Skip("ReadHostResources reads /proc/meminfo; Linux-only") + } + res, err := ReadHostResources() + if err != nil { + t.Fatalf("ReadHostResources: %v", err) + } + if res.CPUCount <= 0 { + t.Errorf("CPUCount = %d, want > 0", res.CPUCount) + } + if res.TotalMemoryBytes <= 0 { + t.Errorf("TotalMemoryBytes = %d, want > 0", res.TotalMemoryBytes) + } +} + +func TestShortIDEdgeCases(t *testing.T) { + if got := ShortID(""); got != "" { + t.Errorf("ShortID('') = %q, want ''", got) + } + if got := ShortID("short"); got != "short" { + t.Errorf("ShortID('short') = %q, want 'short'", got) + } + long := "0123456789abcdef" + if got := ShortID(long); got != "01234567" { + t.Errorf("ShortID(long) = %q, want 01234567", got) + } +} diff --git a/internal/toolingplan/rust_test.go b/internal/toolingplan/rust_test.go new file mode 100644 index 0000000..e474042 --- /dev/null +++ b/internal/toolingplan/rust_test.go @@ -0,0 +1,23 @@ +package toolingplan + +import "testing" + +func TestFirstMeaningfulLine(t *testing.T) { + cases := []struct { + in, want string + }{ + {"", ""}, + {"\n\n\n", ""}, + {" \n \n", ""}, + {"# just a comment\n# another\n", ""}, + {"1.75.0\n", "1.75.0"}, + {" 1.75.0 ", "1.75.0"}, + {"# pinned toolchain\n1.75.0\nmore junk\n", "1.75.0"}, + {"\n\n stable-x86_64-unknown-linux-gnu \n", "stable-x86_64-unknown-linux-gnu"}, + } + for _, tc := range cases { + if got := firstMeaningfulLine(tc.in); got != tc.want { + t.Errorf("firstMeaningfulLine(%q) = %q, want %q", tc.in, got, tc.want) + } + } +} diff --git a/internal/vmdns/remove_test.go b/internal/vmdns/remove_test.go new file mode 100644 index 0000000..09eb302 --- /dev/null +++ b/internal/vmdns/remove_test.go @@ -0,0 +1,93 @@ +package vmdns + +import ( + "testing" +) + +func TestServerRemoveDropsRecord(t *testing.T) { + server := startTestServer(t) + if err := server.Set("devbox.vm", "172.16.0.8"); err != nil { + t.Fatalf("Set: %v", err) + } + if _, ok := server.Lookup("devbox.vm"); !ok { + t.Fatal("record missing before remove") + } + + if err := server.Remove("devbox.vm"); err != nil { + t.Fatalf("Remove: %v", err) + } + if _, ok := server.Lookup("devbox.vm"); ok { + t.Fatal("record still present after Remove") + } +} + +func TestServerRemoveInvalidNameIsNoop(t *testing.T) { + server := startTestServer(t) + // Non-.vm names silently normalize-fail, returning nil. + if err := server.Remove("example.com"); err != nil { + t.Fatalf("Remove: %v", err) + } +} + +func TestServerRemoveNilReceiver(t *testing.T) { + var s *Server + if err := s.Remove("anything.vm"); err != nil { + t.Fatalf("nil Remove: %v", err) + } +} + +func TestServerSetRejectsIPv6(t *testing.T) { + server := startTestServer(t) + if err := server.Set("six.vm", "::1"); err == nil { + t.Fatal("expected error for IPv6 address") + } +} + +func TestServerSetRejectsBadIP(t *testing.T) { + server := startTestServer(t) + if err := server.Set("bad.vm", "not-an-ip"); err == nil { + t.Fatal("expected parse error for bogus IP") + } +} + +func TestServerSetRejectsNonVMName(t *testing.T) { + server := startTestServer(t) + if err := server.Set("example.com", "172.16.0.1"); err == nil { + t.Fatal("expected error for non-.vm name") + } +} + +func TestServerReplaceRejectsBadIP(t *testing.T) { + server := startTestServer(t) + err := server.Replace(map[string]string{"bad.vm": "nope"}) + if err == nil { + t.Fatal("expected parse error") + } +} + +func TestServerReplaceRejectsIPv6(t *testing.T) { + server := startTestServer(t) + err := server.Replace(map[string]string{"six.vm": "::1"}) + if err == nil { + t.Fatal("expected IPv6 rejection") + } +} + +func TestServerNilLookupAndAddr(t *testing.T) { + var s *Server + if _, ok := s.Lookup("x.vm"); ok { + t.Fatal("nil Lookup should return false") + } + if got := s.Addr(); got != "" { + t.Fatalf("nil Addr = %q, want empty", got) + } + if err := s.Close(); err != nil { + t.Fatalf("nil Close: %v", err) + } + if err := s.Set("x.vm", "172.16.0.1"); err != nil { + t.Fatalf("nil Set: %v", err) + } + if err := s.Replace(map[string]string{"x.vm": "172.16.0.1"}); err != nil { + t.Fatalf("nil Replace: %v", err) + } +}