From a96ef0d3e7ef9730fd4f40e8b4877a44b3f70449 Mon Sep 17 00:00:00 2001 From: Melvin Hillsman Date: Sat, 30 May 2026 01:21:39 -0500 Subject: [PATCH] test(cmd/virtwork): increase test coverage from 11% to 85% Add Ginkgo BDD tests covering all major cmd/virtwork code paths: - initAuditor: --no-audit flag, disabled config, valid/invalid db paths, --audit-db flag override - printSummary/printCleanupPreview: output formatting with various field combinations - runE: config error, invalid workload names, initAuditor error, cluster connection error, full dry-run path with single/multiple workloads and --vm-count - cleanupE: config error, initAuditor error, malformed kubeconfig, empty namespace, dry-run, --run-id filter, --yes skip prompt, user confirm/ decline prompt, --delete-namespace - newRootCmd/newRunCmd/newCleanupCmd: command structure and flag presence - PromptForConfirmation: EOF and reader error cases Add clusterConnect variable to enable injecting a fake client in cleanupE and runE tests without requiring a real cluster. Resolves #142 Signed-off-by: Melvin Hillsman --- cmd/virtwork/cleanupe_test.go | 234 ++++++++++++++++++++++++++++++ cmd/virtwork/cmdstructure_test.go | 96 ++++++++++++ cmd/virtwork/initauditor_test.go | 80 ++++++++++ cmd/virtwork/main.go | 34 ++++- cmd/virtwork/print_test.go | 108 ++++++++++++++ cmd/virtwork/prompt_test.go | 15 ++ cmd/virtwork/rune_test.go | 122 ++++++++++++++++ 7 files changed, 683 insertions(+), 6 deletions(-) create mode 100644 cmd/virtwork/cleanupe_test.go create mode 100644 cmd/virtwork/cmdstructure_test.go create mode 100644 cmd/virtwork/initauditor_test.go create mode 100644 cmd/virtwork/print_test.go create mode 100644 cmd/virtwork/rune_test.go diff --git a/cmd/virtwork/cleanupe_test.go b/cmd/virtwork/cleanupe_test.go new file mode 100644 index 0000000..382deb3 --- /dev/null +++ b/cmd/virtwork/cleanupe_test.go @@ -0,0 +1,234 @@ +// Copyright 2026 Red Hat +// SPDX-License-Identifier: Apache-2.0 + +package main + +import ( + "bytes" + "os" + "path/filepath" + "strings" + + . "github.com/onsi/ginkgo/v2" + . "github.com/onsi/gomega" + metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" + kubevirtv1 "kubevirt.io/api/core/v1" + "sigs.k8s.io/controller-runtime/pkg/client" + "sigs.k8s.io/controller-runtime/pkg/client/fake" + + "github.com/opdev/virtwork/internal/cluster" + "github.com/opdev/virtwork/internal/constants" +) + +func fakeConnectEmpty(_ string) (client.Client, string, error) { + scheme := cluster.NewScheme() + c := fake.NewClientBuilder().WithScheme(scheme).Build() + return c, "fake-context", nil +} + +func fakeConnectWithResources(_ string) (client.Client, string, error) { + scheme := cluster.NewScheme() + vm := &kubevirtv1.VirtualMachine{ + ObjectMeta: metav1.ObjectMeta{ + Name: "virtwork-cpu-0", + Namespace: constants.DefaultNamespace, + Labels: map[string]string{ + constants.LabelManagedBy: constants.ManagedByValue, + constants.LabelRunID: "test-run-001", + }, + }, + } + c := fake.NewClientBuilder(). + WithScheme(scheme). + WithObjects(vm). + Build() + return c, "fake-context", nil +} + +var _ = Describe("cleanupE", func() { + Context("error paths", func() { + It("returns error for nonexistent config file", func() { + rootCmd := newRootCmd() + rootCmd.SetArgs([]string{"cleanup", "--config", "/nonexistent/config.yaml"}) + err := rootCmd.Execute() + Expect(err).To(HaveOccurred()) + Expect(err.Error()).To(ContainSubstring("config")) + }) + + It("returns error when initAuditor fails", func() { + rootCmd := newRootCmd() + rootCmd.SetArgs([]string{ + "cleanup", + "--audit-db", "/nonexistent/deeply/nested/path/db.sqlite", + }) + err := rootCmd.Execute() + Expect(err).To(HaveOccurred()) + Expect(err.Error()).To(ContainSubstring("auditor")) + }) + + It("returns error for malformed kubeconfig", func() { + tmpDir := GinkgoT().TempDir() + badKubeconfig := filepath.Join(tmpDir, "kubeconfig") + Expect(os.WriteFile(badKubeconfig, []byte(`not valid yaml{{{`), 0o600)).To(Succeed()) + + rootCmd := newRootCmd() + rootCmd.SetArgs([]string{ + "cleanup", + "--kubeconfig", badKubeconfig, + "--no-audit", + }) + err := rootCmd.Execute() + Expect(err).To(HaveOccurred()) + Expect(err.Error()).To(ContainSubstring("connecting to cluster")) + }) + }) + + Context("with empty fake client", func() { + var origConnect func(string) (client.Client, string, error) + + BeforeEach(func() { + origConnect = clusterConnect + clusterConnect = fakeConnectEmpty + }) + + AfterEach(func() { + clusterConnect = origConnect + }) + + It("reports nothing to clean up when no managed resources exist", func() { + rootCmd := newRootCmd() + var buf bytes.Buffer + rootCmd.SetOut(&buf) + rootCmd.SetArgs([]string{"cleanup", "--no-audit", "--yes"}) + Expect(rootCmd.Execute()).To(Succeed()) + + output := buf.String() + Expect(output).To(ContainSubstring("nothing to clean up")) + }) + + It("reports nothing in dry-run mode", func() { + rootCmd := newRootCmd() + var buf bytes.Buffer + rootCmd.SetOut(&buf) + rootCmd.SetArgs([]string{"cleanup", "--dry-run", "--no-audit"}) + Expect(rootCmd.Execute()).To(Succeed()) + + output := buf.String() + Expect(output).To(ContainSubstring("nothing to clean up")) + }) + + It("reports nothing with --run-id filter", func() { + rootCmd := newRootCmd() + var buf bytes.Buffer + rootCmd.SetOut(&buf) + rootCmd.SetArgs([]string{ + "cleanup", "--run-id", "nonexistent-run", "--no-audit", + }) + Expect(rootCmd.Execute()).To(Succeed()) + + output := buf.String() + Expect(output).To(ContainSubstring("nothing to clean up")) + }) + + It("covers all three cleanup mode branches", func() { + for _, tc := range []struct { + args []string + }{ + {[]string{"cleanup", "--no-audit", "--yes"}}, + {[]string{"cleanup", "--dry-run", "--no-audit"}}, + {[]string{"cleanup", "--run-id", "x", "--no-audit"}}, + } { + rootCmd := newRootCmd() + var buf bytes.Buffer + rootCmd.SetOut(&buf) + rootCmd.SetArgs(tc.args) + Expect(rootCmd.Execute()).To(Succeed()) + } + }) + }) + + Context("with pre-populated fake client", func() { + var origConnect func(string) (client.Client, string, error) + + BeforeEach(func() { + origConnect = clusterConnect + clusterConnect = fakeConnectWithResources + }) + + AfterEach(func() { + clusterConnect = origConnect + }) + + It("shows preview and skips deletion in dry-run mode", func() { + rootCmd := newRootCmd() + var buf bytes.Buffer + rootCmd.SetOut(&buf) + rootCmd.SetArgs([]string{"cleanup", "--dry-run", "--no-audit"}) + Expect(rootCmd.Execute()).To(Succeed()) + + output := buf.String() + Expect(output).To(ContainSubstring("dry-run mode")) + }) + + It("deletes resources when --yes is set", func() { + rootCmd := newRootCmd() + var buf bytes.Buffer + rootCmd.SetOut(&buf) + rootCmd.SetArgs([]string{"cleanup", "--yes", "--no-audit"}) + Expect(rootCmd.Execute()).To(Succeed()) + + output := buf.String() + Expect(output).To(ContainSubstring("cleanup complete")) + }) + + It("deletes resources when --run-id matches (skips prompt)", func() { + rootCmd := newRootCmd() + var buf bytes.Buffer + rootCmd.SetOut(&buf) + rootCmd.SetArgs([]string{ + "cleanup", "--run-id", "test-run-001", "--no-audit", + }) + Expect(rootCmd.Execute()).To(Succeed()) + + output := buf.String() + Expect(output).To(ContainSubstring("cleanup complete")) + }) + + It("aborts when user declines confirmation prompt", func() { + rootCmd := newRootCmd() + var buf bytes.Buffer + rootCmd.SetOut(&buf) + rootCmd.SetIn(strings.NewReader("no\n")) + rootCmd.SetArgs([]string{"cleanup", "--no-audit"}) + Expect(rootCmd.Execute()).To(Succeed()) + + output := buf.String() + Expect(output).To(ContainSubstring("cleanup aborted")) + }) + + It("proceeds when user confirms at prompt", func() { + rootCmd := newRootCmd() + var buf bytes.Buffer + rootCmd.SetOut(&buf) + rootCmd.SetIn(strings.NewReader("yes\n")) + rootCmd.SetArgs([]string{"cleanup", "--no-audit"}) + Expect(rootCmd.Execute()).To(Succeed()) + + output := buf.String() + Expect(output).To(ContainSubstring("cleanup complete")) + }) + + It("deletes resources with --delete-namespace", func() { + rootCmd := newRootCmd() + var buf bytes.Buffer + rootCmd.SetOut(&buf) + rootCmd.SetArgs([]string{ + "cleanup", "--yes", "--delete-namespace", "--no-audit", + }) + Expect(rootCmd.Execute()).To(Succeed()) + + output := buf.String() + Expect(output).To(ContainSubstring("cleanup complete")) + }) + }) +}) diff --git a/cmd/virtwork/cmdstructure_test.go b/cmd/virtwork/cmdstructure_test.go new file mode 100644 index 0000000..753cb23 --- /dev/null +++ b/cmd/virtwork/cmdstructure_test.go @@ -0,0 +1,96 @@ +// Copyright 2026 Red Hat +// SPDX-License-Identifier: Apache-2.0 + +package main + +import ( + . "github.com/onsi/ginkgo/v2" + . "github.com/onsi/gomega" +) + +var _ = Describe("newRootCmd", func() { + It("creates a command named virtwork", func() { + cmd := newRootCmd() + Expect(cmd.Use).To(Equal("virtwork")) + }) + + It("has run, cleanup, and version subcommands", func() { + cmd := newRootCmd() + names := make([]string, 0) + for _, sub := range cmd.Commands() { + names = append(names, sub.Name()) + } + Expect(names).To(ContainElements("run", "cleanup", "version")) + }) + + It("has persistent flags for namespace, kubeconfig, config, verbose, audit", func() { + cmd := newRootCmd() + Expect(cmd.PersistentFlags().Lookup("namespace")).NotTo(BeNil()) + Expect(cmd.PersistentFlags().Lookup("kubeconfig")).NotTo(BeNil()) + Expect(cmd.PersistentFlags().Lookup("config")).NotTo(BeNil()) + Expect(cmd.PersistentFlags().Lookup("verbose")).NotTo(BeNil()) + Expect(cmd.PersistentFlags().Lookup("audit")).NotTo(BeNil()) + Expect(cmd.PersistentFlags().Lookup("no-audit")).NotTo(BeNil()) + Expect(cmd.PersistentFlags().Lookup("audit-db")).NotTo(BeNil()) + }) + + It("silences usage on error", func() { + cmd := newRootCmd() + Expect(cmd.SilenceUsage).To(BeTrue()) + }) +}) + +var _ = Describe("newRunCmd", func() { + It("creates a command named run", func() { + cmd := newRunCmd() + Expect(cmd.Use).To(Equal("run")) + }) + + It("has expected flags", func() { + cmd := newRunCmd() + Expect(cmd.Flags().Lookup("workloads")).NotTo(BeNil()) + Expect(cmd.Flags().Lookup("vm-count")).NotTo(BeNil()) + Expect(cmd.Flags().Lookup("cpu-cores")).NotTo(BeNil()) + Expect(cmd.Flags().Lookup("memory")).NotTo(BeNil()) + Expect(cmd.Flags().Lookup("dry-run")).NotTo(BeNil()) + Expect(cmd.Flags().Lookup("no-wait")).NotTo(BeNil()) + Expect(cmd.Flags().Lookup("timeout")).NotTo(BeNil()) + Expect(cmd.Flags().Lookup("ssh-user")).NotTo(BeNil()) + Expect(cmd.Flags().Lookup("ssh-password")).NotTo(BeNil()) + Expect(cmd.Flags().Lookup("ssh-key")).NotTo(BeNil()) + Expect(cmd.Flags().Lookup("ssh-key-file")).NotTo(BeNil()) + Expect(cmd.Flags().Lookup("vm-concurrency")).NotTo(BeNil()) + }) + + It("has RunE set to production runE function", func() { + cmd := newRunCmd() + Expect(cmd.RunE).NotTo(BeNil()) + }) +}) + +var _ = Describe("newCleanupCmd", func() { + It("creates a command named cleanup", func() { + cmd := newCleanupCmd() + Expect(cmd.Use).To(Equal("cleanup")) + }) + + It("has expected flags", func() { + cmd := newCleanupCmd() + Expect(cmd.Flags().Lookup("delete-namespace")).NotTo(BeNil()) + Expect(cmd.Flags().Lookup("run-id")).NotTo(BeNil()) + Expect(cmd.Flags().Lookup("dry-run")).NotTo(BeNil()) + Expect(cmd.Flags().Lookup("yes")).NotTo(BeNil()) + }) + + It("has -y shorthand for --yes", func() { + cmd := newCleanupCmd() + flag := cmd.Flags().Lookup("yes") + Expect(flag).NotTo(BeNil()) + Expect(flag.Shorthand).To(Equal("y")) + }) + + It("has RunE set to production cleanupE function", func() { + cmd := newCleanupCmd() + Expect(cmd.RunE).NotTo(BeNil()) + }) +}) diff --git a/cmd/virtwork/initauditor_test.go b/cmd/virtwork/initauditor_test.go new file mode 100644 index 0000000..017b1de --- /dev/null +++ b/cmd/virtwork/initauditor_test.go @@ -0,0 +1,80 @@ +// Copyright 2026 Red Hat +// SPDX-License-Identifier: Apache-2.0 + +package main + +import ( + "os" + "path/filepath" + + . "github.com/onsi/ginkgo/v2" + . "github.com/onsi/gomega" + "github.com/spf13/cobra" + + "github.com/opdev/virtwork/internal/audit" + "github.com/opdev/virtwork/internal/config" +) + +var _ = Describe("initAuditor", func() { + var cmd *cobra.Command + + BeforeEach(func() { + cmd = &cobra.Command{} + cmd.Flags().Bool("no-audit", false, "") + cmd.Flags().String("audit-db", "", "") + }) + + It("returns NoOpAuditor when --no-audit is set", func() { + Expect(cmd.Flags().Set("no-audit", "true")).To(Succeed()) + + cfg := &config.Config{AuditEnabled: true} + auditor, err := initAuditor(cmd, cfg) + Expect(err).NotTo(HaveOccurred()) + Expect(auditor).To(BeAssignableToTypeOf(audit.NoOpAuditor{})) + }) + + It("returns NoOpAuditor when cfg.AuditEnabled is false", func() { + cfg := &config.Config{AuditEnabled: false} + auditor, err := initAuditor(cmd, cfg) + Expect(err).NotTo(HaveOccurred()) + Expect(auditor).To(BeAssignableToTypeOf(audit.NoOpAuditor{})) + }) + + It("returns SQLiteAuditor with default db path from config", func() { + tmpDir := GinkgoT().TempDir() + dbPath := filepath.Join(tmpDir, "audit.db") + + cfg := &config.Config{AuditEnabled: true, AuditDBPath: dbPath} + auditor, err := initAuditor(cmd, cfg) + Expect(err).NotTo(HaveOccurred()) + Expect(auditor).NotTo(BeNil()) + Expect(auditor.Close()).To(Succeed()) + + _, statErr := os.Stat(dbPath) + Expect(statErr).NotTo(HaveOccurred()) + }) + + It("uses --audit-db flag path when set", func() { + tmpDir := GinkgoT().TempDir() + flagPath := filepath.Join(tmpDir, "override.db") + Expect(cmd.Flags().Set("audit-db", flagPath)).To(Succeed()) + + cfg := &config.Config{AuditEnabled: true, AuditDBPath: "/should/not/use/this"} + auditor, err := initAuditor(cmd, cfg) + Expect(err).NotTo(HaveOccurred()) + Expect(auditor).NotTo(BeNil()) + Expect(auditor.Close()).To(Succeed()) + + _, statErr := os.Stat(flagPath) + Expect(statErr).NotTo(HaveOccurred()) + }) + + It("returns error for invalid db path", func() { + cfg := &config.Config{ + AuditEnabled: true, + AuditDBPath: "/nonexistent/deeply/nested/path/audit.db", + } + _, err := initAuditor(cmd, cfg) + Expect(err).To(HaveOccurred()) + }) +}) diff --git a/cmd/virtwork/main.go b/cmd/virtwork/main.go index f3fbc73..bbf40ca 100644 --- a/cmd/virtwork/main.go +++ b/cmd/virtwork/main.go @@ -31,6 +31,8 @@ var ( version = "" commit = "" date = "" + + clusterConnect = cluster.Connect ) func main() { @@ -72,7 +74,13 @@ func newVersionCmd() *cobra.Command { if d == "" { d = "(unknown)" } - _, _ = fmt.Fprintf(cmd.OutOrStdout(), "virtwork version %s\n commit: %s\n built: %s\n", v, c, d) + _, _ = fmt.Fprintf( + cmd.OutOrStdout(), + "virtwork version %s\n commit: %s\n built: %s\n", + v, + c, + d, + ) return nil }, } @@ -130,6 +138,7 @@ func runE(cmd *cobra.Command, args []string) error { return err } + // nolint:dupl verbose, _ := cmd.Flags().GetBool("verbose") logger := logging.NewLogger(cmd.OutOrStdout(), verbose) @@ -139,7 +148,11 @@ func runE(cmd *cobra.Command, args []string) error { } defer func() { if closeErr := auditor.Close(); closeErr != nil { - logger.Warn("audit record failed", slog.String("op", "Close"), slog.String("error", closeErr.Error())) + logger.Warn( + "audit record failed", + slog.String("op", "Close"), + slog.String("error", closeErr.Error()), + ) } }() @@ -149,7 +162,7 @@ func runE(cmd *cobra.Command, args []string) error { var c client.Client if !cfg.DryRun { var contextName string - c, contextName, err = cluster.Connect(cluster.ResolveKubeconfigPath(cfg.KubeconfigPath)) + c, contextName, err = clusterConnect(cluster.ResolveKubeconfigPath(cfg.KubeconfigPath)) if err != nil { return fmt.Errorf("connecting to cluster: %w", err) } @@ -181,7 +194,11 @@ func runE(cmd *cobra.Command, args []string) error { EventType: "execution_started", Message: fmt.Sprintf("Starting %s with run-id %s", cmdName, runID), }); auditErr != nil { - logger.Warn("audit record failed", slog.String("op", "RecordEvent"), slog.String("error", auditErr.Error())) + logger.Warn( + "audit record failed", + slog.String("op", "RecordEvent"), + slog.String("error", auditErr.Error()), + ) } vmCountFlag, _ := cmd.Flags().GetInt("vm-count") @@ -211,6 +228,7 @@ func cleanupE(cmd *cobra.Command, args []string) error { return fmt.Errorf("loading config: %w", err) } + // nolint:dupl verbose, _ := cmd.Flags().GetBool("verbose") logger := logging.NewLogger(cmd.OutOrStdout(), verbose) @@ -220,7 +238,11 @@ func cleanupE(cmd *cobra.Command, args []string) error { } defer func() { if closeErr := auditor.Close(); closeErr != nil { - logger.Warn("audit record failed", slog.String("op", "Close"), slog.String("error", closeErr.Error())) + logger.Warn( + "audit record failed", + slog.String("op", "Close"), + slog.String("error", closeErr.Error()), + ) } }() @@ -238,7 +260,7 @@ func cleanupE(cmd *cobra.Command, args []string) error { cfg.CleanupMode = "all" } - c, contextName, err := cluster.Connect(cluster.ResolveKubeconfigPath(cfg.KubeconfigPath)) + c, contextName, err := clusterConnect(cluster.ResolveKubeconfigPath(cfg.KubeconfigPath)) if err != nil { return fmt.Errorf("connecting to cluster: %w", err) } diff --git a/cmd/virtwork/print_test.go b/cmd/virtwork/print_test.go new file mode 100644 index 0000000..d9e52c3 --- /dev/null +++ b/cmd/virtwork/print_test.go @@ -0,0 +1,108 @@ +// Copyright 2026 Red Hat +// SPDX-License-Identifier: Apache-2.0 + +package main + +import ( + "bytes" + + . "github.com/onsi/ginkgo/v2" + . "github.com/onsi/gomega" + + "github.com/opdev/virtwork/internal/cleanup" + "github.com/opdev/virtwork/internal/config" + "github.com/opdev/virtwork/internal/logging" + "github.com/opdev/virtwork/internal/orchestrator" +) + +var _ = Describe("printSummary", func() { + It("logs run_id, namespace, vm count, service count, secret count, and image", func() { + var buf bytes.Buffer + logger := logging.NewLogger(&buf, false) + + result := &orchestrator.RunResult{ + RunID: "test-run-abc", + VMCount: 5, + ServiceCount: 2, + SecretCount: 3, + } + cfg := &config.Config{ + Namespace: "my-namespace", + ContainerDiskImage: "registry.example.com/vm:latest", + } + + printSummary(logger, result, cfg) + + output := buf.String() + Expect(output).To(ContainSubstring("test-run-abc")) + Expect(output).To(ContainSubstring("my-namespace")) + Expect(output).To(ContainSubstring("deployment summary")) + }) +}) + +var _ = Describe("printCleanupPreview", func() { + It("logs resource counts and namespace", func() { + var buf bytes.Buffer + logger := logging.NewLogger(&buf, false) + + preview := &cleanup.CleanupPreview{ + VMCount: 3, + ServiceCount: 1, + SecretCount: 2, + DVCount: 1, + PVCCount: 1, + TotalCount: 8, + } + + printCleanupPreview(logger, preview, "test-ns", "") + + output := buf.String() + Expect(output).To(ContainSubstring("test-ns")) + Expect(output).To(ContainSubstring("resources to be deleted")) + }) + + It("includes run_id_filter when runID is provided", func() { + var buf bytes.Buffer + logger := logging.NewLogger(&buf, false) + + preview := &cleanup.CleanupPreview{ + VMCount: 1, + TotalCount: 1, + } + + printCleanupPreview(logger, preview, "test-ns", "run-xyz") + + output := buf.String() + Expect(output).To(ContainSubstring("run-xyz")) + }) + + It("includes run_ids when preview has them", func() { + var buf bytes.Buffer + logger := logging.NewLogger(&buf, false) + + preview := &cleanup.CleanupPreview{ + VMCount: 2, + TotalCount: 2, + RunIDs: []string{"id-aaa", "id-bbb"}, + } + + printCleanupPreview(logger, preview, "test-ns", "") + + output := buf.String() + Expect(output).To(ContainSubstring("id-aaa")) + Expect(output).To(ContainSubstring("id-bbb")) + }) + + It("handles zero counts", func() { + var buf bytes.Buffer + logger := logging.NewLogger(&buf, false) + + preview := &cleanup.CleanupPreview{} + + printCleanupPreview(logger, preview, "empty-ns", "") + + output := buf.String() + Expect(output).To(ContainSubstring("empty-ns")) + Expect(output).To(ContainSubstring("resources to be deleted")) + }) +}) diff --git a/cmd/virtwork/prompt_test.go b/cmd/virtwork/prompt_test.go index c1e9d2c..e1c38c2 100644 --- a/cmd/virtwork/prompt_test.go +++ b/cmd/virtwork/prompt_test.go @@ -6,8 +6,11 @@ package main import ( "strings" "testing" + "testing/iotest" ) +var errFakeRead = iotest.ErrTimeout + func TestPromptForConfirmation(t *testing.T) { tests := []struct { name string @@ -20,6 +23,7 @@ func TestPromptForConfirmation(t *testing.T) { {"arbitrary text rejects", "maybe\n", false}, {"YES case-insensitive", "YES\n", true}, {"whitespace trimmed", " yes \n", true}, + {"EOF returns false", "", false}, } for _, tt := range tests { @@ -35,3 +39,14 @@ func TestPromptForConfirmation(t *testing.T) { }) } } + +func TestPromptForConfirmation_ReadError(t *testing.T) { + reader := iotest.ErrReader(errFakeRead) + confirmed, err := PromptForConfirmation(reader) + if err == nil { + t.Fatal("expected error, got nil") + } + if confirmed { + t.Error("expected false on read error") + } +} diff --git a/cmd/virtwork/rune_test.go b/cmd/virtwork/rune_test.go new file mode 100644 index 0000000..1a25606 --- /dev/null +++ b/cmd/virtwork/rune_test.go @@ -0,0 +1,122 @@ +// Copyright 2026 Red Hat +// SPDX-License-Identifier: Apache-2.0 + +package main + +import ( + "bytes" + "os" + "path/filepath" + + . "github.com/onsi/ginkgo/v2" + . "github.com/onsi/gomega" +) + +var _ = Describe("runE", func() { + Context("error paths", func() { + It("returns error for nonexistent config file", func() { + rootCmd := newRootCmd() + rootCmd.SetArgs([]string{"run", "--config", "/nonexistent/config.yaml"}) + err := rootCmd.Execute() + Expect(err).To(HaveOccurred()) + Expect(err.Error()).To(ContainSubstring("config")) + }) + + It("returns error for invalid workload name", func() { + rootCmd := newRootCmd() + rootCmd.SetArgs([]string{ + "run", "--dry-run", "--workloads", "bogus-workload", "--no-audit", + }) + err := rootCmd.Execute() + Expect(err).To(HaveOccurred()) + Expect(err.Error()).To(ContainSubstring("bogus-workload")) + }) + + It("returns error when cluster connection fails in non-dry-run mode", func() { + tmpDir := GinkgoT().TempDir() + badKubeconfig := filepath.Join(tmpDir, "kubeconfig") + Expect(os.WriteFile(badKubeconfig, []byte(`not valid yaml{{{`), 0o600)).To(Succeed()) + + rootCmd := newRootCmd() + rootCmd.SetArgs([]string{ + "run", + "--workloads", "cpu", + "--no-audit", + "--kubeconfig", badKubeconfig, + }) + err := rootCmd.Execute() + Expect(err).To(HaveOccurred()) + Expect(err.Error()).To(ContainSubstring("connecting to cluster")) + }) + + It("returns error when initAuditor fails", func() { + rootCmd := newRootCmd() + rootCmd.SetArgs([]string{ + "run", + "--dry-run", + "--workloads", "cpu", + "--audit-db", "/nonexistent/deeply/nested/path/db.sqlite", + }) + err := rootCmd.Execute() + Expect(err).To(HaveOccurred()) + Expect(err.Error()).To(ContainSubstring("auditor")) + }) + }) + + Context("dry-run mode", func() { + It("succeeds without a cluster connection", func() { + rootCmd := newRootCmd() + var buf bytes.Buffer + rootCmd.SetOut(&buf) + rootCmd.SetArgs([]string{ + "run", "--dry-run", "--workloads", "cpu", "--no-audit", + }) + Expect(rootCmd.Execute()).To(Succeed()) + + output := buf.String() + Expect(output).To(ContainSubstring("virtwork-cpu")) + }) + + It("succeeds with multiple workloads", func() { + rootCmd := newRootCmd() + var buf bytes.Buffer + rootCmd.SetOut(&buf) + rootCmd.SetArgs([]string{ + "run", "--dry-run", "--workloads", "cpu,memory", "--no-audit", + }) + Expect(rootCmd.Execute()).To(Succeed()) + + output := buf.String() + Expect(output).To(ContainSubstring("virtwork-cpu")) + Expect(output).To(ContainSubstring("virtwork-memory")) + }) + + It("prints VM YAML specs to output", func() { + rootCmd := newRootCmd() + var buf bytes.Buffer + rootCmd.SetOut(&buf) + rootCmd.SetArgs([]string{ + "run", "--dry-run", "--workloads", "cpu", "--no-audit", + }) + Expect(rootCmd.Execute()).To(Succeed()) + + output := buf.String() + Expect(output).To(ContainSubstring("kind: VirtualMachine")) + }) + + It("respects --vm-count flag", func() { + rootCmd := newRootCmd() + var buf bytes.Buffer + rootCmd.SetOut(&buf) + rootCmd.SetArgs([]string{ + "run", "--dry-run", "--workloads", "cpu", "--vm-count", "3", "--no-audit", + }) + Expect(rootCmd.Execute()).To(Succeed()) + + output := buf.String() + Expect(output).To(ContainSubstring("virtwork-cpu-0")) + Expect(output).To(ContainSubstring("virtwork-cpu-1")) + Expect(output).To(ContainSubstring("virtwork-cpu-2")) + }) + }) +})