From 9cf3ed6a5ce504c2984a008b2421115134bf1a36 Mon Sep 17 00:00:00 2001 From: ruinivist Date: Mon, 16 Mar 2026 23:36:01 +0000 Subject: [PATCH] feat: migrate to use cobra as cli dep --- cmd/init.go | 26 +++---- cmd/record.go | 28 +++---- cmd/root.go | 34 +++++---- cmd/root_test.go | 194 +++++++++++++++++++++++++++++++++++++++++++++++ go.mod | 10 ++- go.sum | 10 +++ 6 files changed, 262 insertions(+), 40 deletions(-) create mode 100644 cmd/root_test.go diff --git a/cmd/init.go b/cmd/init.go index d605561..97ae33b 100644 --- a/cmd/init.go +++ b/cmd/init.go @@ -1,23 +1,23 @@ package cmd import ( - "strings" - "miro/internal/miro" "miro/internal/output" + + "github.com/spf13/cobra" ) -func runInit(args []string) int { - if len(args) != 0 { - output.Printf("unknown init option: %s\n", strings.Join(args, " ")) - return 1 - } +func newInitCommand() *cobra.Command { + return &cobra.Command{ + Use: "init", + Args: cobra.NoArgs, + RunE: func(_ *cobra.Command, _ []string) error { + if err := miro.Init(); err != nil { + return err + } - if err := miro.Init(); err != nil { - output.Printf("%v\n", err) - return 1 + output.Println("Done initialising...") + return nil + }, } - - output.Println("Done initialising...") - return 0 } diff --git a/cmd/record.go b/cmd/record.go index c4b20ab..6c755f5 100644 --- a/cmd/record.go +++ b/cmd/record.go @@ -5,21 +5,23 @@ import ( "miro/internal/miro" "miro/internal/output" + + "github.com/spf13/cobra" ) -func runRecord(args []string) int { - if len(args) != 1 { - output.Println("record requires exactly one path") - return 1 - } +func newRecordCommand() *cobra.Command { + return &cobra.Command{ + Use: "record ", + Args: cobra.ExactArgs(1), + RunE: func(_ *cobra.Command, args []string) error { + path := filepath.Clean(args[0]) + createdPath, err := miro.Record(path) + if err != nil { + return err + } - path := filepath.Clean(args[0]) - createdPath, err := miro.Record(path) - if err != nil { - output.Printf("%v\n", err) - return 1 + output.Println(createdPath) + return nil + }, } - - output.Println(createdPath) - return 0 } diff --git a/cmd/root.go b/cmd/root.go index bb86d74..3f15ad4 100644 --- a/cmd/root.go +++ b/cmd/root.go @@ -1,21 +1,29 @@ package cmd -import "miro/internal/output" +import "github.com/spf13/cobra" -// Run parses top-level CLI args and returns a process exit code. +// Run executes the miro CLI and returns a process exit code. func Run(args []string) int { - if len(args) == 0 { - output.Println("unknown command") + rootCmd := newRootCommand() + rootCmd.SetArgs(args) + + if err := rootCmd.Execute(); err != nil { return 1 } - switch args[0] { - case "init": - return runInit(args[1:]) - case "record": - return runRecord(args[1:]) - default: - output.Printf("unknown command: %s\n", args[0]) - return 1 - } + return 0 +} + +func newRootCommand() *cobra.Command { + rootCmd := &cobra.Command{ + Use: "miro", + Short: "A lean CLI E2E testing framework.", + RunE: func(cmd *cobra.Command, _ []string) error { + return cmd.Help() + }, + } + + rootCmd.AddCommand(newInitCommand(), newRecordCommand()) + + return rootCmd } diff --git a/cmd/root_test.go b/cmd/root_test.go new file mode 100644 index 0000000..2c510e8 --- /dev/null +++ b/cmd/root_test.go @@ -0,0 +1,194 @@ +package cmd + +import ( + "bytes" + "io" + "os" + "path/filepath" + "strings" + "testing" + + "miro/internal/output" +) + +func TestRunShowsHelpWhenNoArgs(t *testing.T) { + stdout, stderr := captureOutput(t, func() { + if got := Run(nil); got != 0 { + t.Fatalf("Run() code = %d, want %d", got, 0) + } + }) + + if !strings.Contains(stdout, "A lean CLI E2E testing framework.") { + t.Fatalf("stdout = %q, want root help", stdout) + } + if !strings.Contains(stdout, "init") || !strings.Contains(stdout, "record") { + t.Fatalf("stdout = %q, want listed subcommands", stdout) + } + if stderr != "" { + t.Fatalf("stderr = %q, want empty", stderr) + } +} + +func TestRunInit(t *testing.T) { + stdout, stderr := captureOutput(t, func() { + if got := Run([]string{"init"}); got != 0 { + t.Fatalf("Run() code = %d, want %d", got, 0) + } + }) + + if stdout != prefixed("Done initialising...\n") { + t.Fatalf("stdout = %q, want %q", stdout, prefixed("Done initialising...\n")) + } + if stderr != "" { + t.Fatalf("stderr = %q, want empty", stderr) + } +} + +func TestRunRecord(t *testing.T) { + root := t.TempDir() + wantDir := filepath.Join(root, "e2e") + if err := os.MkdirAll(wantDir, 0o755); err != nil { + t.Fatalf("MkdirAll() error = %v", err) + } + + withWorkingDir(t, root, func() { + stdout, stderr := captureOutput(t, func() { + if got := Run([]string{"record", "suite/spec"}); got != 0 { + t.Fatalf("Run() code = %d, want %d", got, 0) + } + }) + + createdPath := filepath.Join(wantDir, "suite", "spec") + if stdout != prefixed(createdPath+"\n") { + t.Fatalf("stdout = %q, want %q", stdout, prefixed(createdPath+"\n")) + } + if stderr != "" { + t.Fatalf("stderr = %q, want empty", stderr) + } + + info, err := os.Stat(createdPath) + if err != nil { + t.Fatalf("Stat() error = %v", err) + } + if !info.IsDir() { + t.Fatalf("created path %q is not a directory", createdPath) + } + }) +} + +func TestRunRecordMissingPath(t *testing.T) { + stdout, stderr := captureOutput(t, func() { + if got := Run([]string{"record"}); got != 1 { + t.Fatalf("Run() code = %d, want %d", got, 1) + } + }) + + if stdout != "" { + t.Fatalf("stdout = %q, want empty", stdout) + } + if !strings.Contains(stderr, "accepts 1 arg(s), received 0") { + t.Fatalf("stderr = %q, want argument error", stderr) + } + if !strings.Contains(stderr, "Usage:") || !strings.Contains(stderr, "miro record ") { + t.Fatalf("stderr = %q, want record usage", stderr) + } +} + +func TestRunInitExtraArgs(t *testing.T) { + stdout, stderr := captureOutput(t, func() { + if got := Run([]string{"init", "extra"}); got != 1 { + t.Fatalf("Run() code = %d, want %d", got, 1) + } + }) + + if stdout != "" { + t.Fatalf("stdout = %q, want empty", stdout) + } + if !strings.Contains(stderr, "unknown command \"extra\" for \"miro init\"") { + t.Fatalf("stderr = %q, want extra-arg error", stderr) + } + if !strings.Contains(stderr, "Usage:") || !strings.Contains(stderr, "miro init") { + t.Fatalf("stderr = %q, want init usage", stderr) + } +} + +func TestRunUnknownCommand(t *testing.T) { + stdout, stderr := captureOutput(t, func() { + if got := Run([]string{"wat"}); got != 1 { + t.Fatalf("Run() code = %d, want %d", got, 1) + } + }) + + if stdout != "" { + t.Fatalf("stdout = %q, want empty", stdout) + } + if !strings.Contains(stderr, "unknown command \"wat\" for \"miro\"") { + t.Fatalf("stderr = %q, want unknown command error", stderr) + } +} + +func captureOutput(t *testing.T, fn func()) (string, string) { + t.Helper() + + stdoutReader, stdoutWriter, err := os.Pipe() + if err != nil { + t.Fatalf("os.Pipe() stdout error = %v", err) + } + stderrReader, stderrWriter, err := os.Pipe() + if err != nil { + t.Fatalf("os.Pipe() stderr error = %v", err) + } + + oldStdout := os.Stdout + oldStderr := os.Stderr + os.Stdout = stdoutWriter + os.Stderr = stderrWriter + t.Cleanup(func() { + os.Stdout = oldStdout + os.Stderr = oldStderr + }) + + fn() + + if err := stdoutWriter.Close(); err != nil { + t.Fatalf("stdout close error = %v", err) + } + if err := stderrWriter.Close(); err != nil { + t.Fatalf("stderr close error = %v", err) + } + + var stdoutBuf bytes.Buffer + if _, err := io.Copy(&stdoutBuf, stdoutReader); err != nil { + t.Fatalf("stdout copy error = %v", err) + } + + var stderrBuf bytes.Buffer + if _, err := io.Copy(&stderrBuf, stderrReader); err != nil { + t.Fatalf("stderr copy error = %v", err) + } + + return stdoutBuf.String(), stderrBuf.String() +} + +func prefixed(msg string) string { + return output.Format(msg) +} + +func withWorkingDir(t *testing.T, dir string, fn func()) { + t.Helper() + + wd, err := os.Getwd() + if err != nil { + t.Fatalf("Getwd() error = %v", err) + } + if err := os.Chdir(dir); err != nil { + t.Fatalf("Chdir(%q) error = %v", dir, err) + } + t.Cleanup(func() { + if err := os.Chdir(wd); err != nil { + t.Fatalf("restore working directory: %v", err) + } + }) + + fn() +} diff --git a/go.mod b/go.mod index 6e1ce74..db1ce88 100644 --- a/go.mod +++ b/go.mod @@ -2,4 +2,12 @@ module miro go 1.25.5 -require github.com/BurntSushi/toml v1.5.0 +require ( + github.com/BurntSushi/toml v1.5.0 + github.com/spf13/cobra v1.10.2 +) + +require ( + github.com/inconshreveable/mousetrap v1.1.0 // indirect + github.com/spf13/pflag v1.0.9 // indirect +) diff --git a/go.sum b/go.sum index ff7fd09..b076de0 100644 --- a/go.sum +++ b/go.sum @@ -1,2 +1,12 @@ github.com/BurntSushi/toml v1.5.0 h1:W5quZX/G/csjUnuI8SUYlsHs9M38FC7znL0lIO+DvMg= github.com/BurntSushi/toml v1.5.0/go.mod h1:ukJfTF/6rtPPRCnwkur4qwRxa8vTRFBF0uk2lLoLwho= +github.com/cpuguy83/go-md2man/v2 v2.0.6/go.mod h1:oOW0eioCTA6cOiMLiUPZOpcVxMig6NIQQ7OS05n1F4g= +github.com/inconshreveable/mousetrap v1.1.0 h1:wN+x4NVGpMsO7ErUn/mUI3vEoE6Jt13X2s0bqwp9tc8= +github.com/inconshreveable/mousetrap v1.1.0/go.mod h1:vpF70FUmC8bwa3OWnCshd2FqLfsEA9PFc4w1p2J65bw= +github.com/russross/blackfriday/v2 v2.1.0/go.mod h1:+Rmxgy9KzJVeS9/2gXHxylqXiyQDYRxCVz55jmeOWTM= +github.com/spf13/cobra v1.10.2 h1:DMTTonx5m65Ic0GOoRY2c16WCbHxOOw6xxezuLaBpcU= +github.com/spf13/cobra v1.10.2/go.mod h1:7C1pvHqHw5A4vrJfjNwvOdzYu0Gml16OCs2GRiTUUS4= +github.com/spf13/pflag v1.0.9 h1:9exaQaMOCwffKiiiYk6/BndUBv+iRViNW+4lEMi0PvY= +github.com/spf13/pflag v1.0.9/go.mod h1:McXfInJRrz4CZXVZOBLb0bTZqETkiAhM9Iw0y3An2Bg= +go.yaml.in/yaml/v3 v3.0.4/go.mod h1:DhzuOOF2ATzADvBadXxruRBLzYTpT36CKvDb3+aBEFg= +gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=