Skip to content

Commit ff3f5a8

Browse files
committed
Improve test coverage for error situations
1 parent 3a978bd commit ff3f5a8

File tree

3 files changed

+119
-20
lines changed

3 files changed

+119
-20
lines changed

‎README.md‎

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,7 @@
33
[![codecov](https://codecov.io/gh/bep/simplecobra/branch/master/graph/badge.svg)](https://codecov.io/gh/bep/simplecobra)
44
[![GoDoc](https://godoc.org/github.com/bep/simplecobra?status.svg)](https://godoc.org/github.com/bep/simplecobra)
55

6-
So, [Cobra](https://github.com/spf13/cobra) is a Go CLI library with a feature set that's hard to resist for bigger applications (autocomplete, docs auto generation etc.). But it's also rather complex to use beyond the simplest of applications. This package is built to aid rewriting [Hugo's](https://github.com/gohugoio/hugo) commands package to something that's easier to understand and maintain.
6+
So, [Cobra](https://github.com/spf13/cobra) is a Go CLI library with a feature set that's hard to resist for bigger applications (autocompletion, docs and man pages auto generation etc.). But it's also complex to use beyond the simplest of applications. This package was built to help rewriting [Hugo's](https://github.com/gohugoio/hugo) commands package to something that's easier to understand and maintain.
77

88
I welcome suggestions to improve/simplify this further, but the core idea is that the command graph gets built in one go with a tree of struct pointers implementing a simple `Commander` interface:
99

@@ -70,7 +70,7 @@ func main() {
7070

7171
You have access to the `*cobra.Command` pointer so there's not much you cannot do with this project compared to the more low-level Cobra, but there's one small, but imortant difference:
7272

73-
Cobra only treats the first level of misspelled commands as an `unknown command` with "Did you mean this?" suggestions, see [see this issue](https://github.com/spf13/cobra/pull/1500) for more context. The reason this is, is because of the ambiguity between sub command names and command arguments, but that is throwing away a very useful feature for not a very good reason. We recently rewrote [Hugo's CLI](https://github.com/gohugoio/hugo) using this poackage, and found only one sub command that needed to be adjusted to avoid this ambiguity.
73+
Cobra only treats the first level of misspelled commands as an `unknown command` with "Did you mean this?" suggestions, see [see this issue](https://github.com/spf13/cobra/pull/1500) for more context. The reason this is, is because of the ambiguity between sub command names and command arguments, but that is throwing away a very useful feature for a not very good reason. We recently rewrote [Hugo's CLI](https://github.com/gohugoio/hugo) using this poackage, and found only one sub command that needed to be adjusted to avoid this ambiguity.
7474

7575

7676

‎simplecobra.go‎

Lines changed: 18 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -99,11 +99,7 @@ func (c *Commandeer) init() error {
9999
}
100100

101101
type runErr struct {
102-
err error
103-
}
104-
105-
func (r *runErr) Error() string {
106-
return fmt.Sprintf("run error: %v", r.err)
102+
error
107103
}
108104

109105
func (c *Commandeer) compile() error {
@@ -115,7 +111,7 @@ func (c *Commandeer) compile() error {
115111
Use: fmt.Sprintf("%s %s", c.Command.Name(), useCommandFlagsArgs),
116112
RunE: func(cmd *cobra.Command, args []string) error {
117113
if err := c.Command.Run(cmd.Context(), c, args); err != nil {
118-
return &runErr{err: err}
114+
return &runErr{error: err}
119115
}
120116
return nil
121117
},
@@ -128,7 +124,9 @@ func (c *Commandeer) compile() error {
128124
}
129125

130126
// This is where the flags, short and long description etc. are added
131-
c.Command.WithCobraCommand(c.CobraCommand)
127+
if err := c.Command.WithCobraCommand(c.CobraCommand); err != nil {
128+
return err
129+
}
132130

133131
// Add commands recursively.
134132
for _, cc := range c.commandeers {
@@ -149,6 +147,10 @@ type Exec struct {
149147
// Execute executes the command tree starting from the root command.
150148
// The args are usually filled with os.Args[1:].
151149
func (r *Exec) Execute(ctx context.Context, args []string) (*Commandeer, error) {
150+
if args == nil {
151+
// Cobra falls back to os.Args[1:] if args is nil.
152+
args = []string{}
153+
}
152154
r.c.CobraCommand.SetArgs(args)
153155
cobraCommand, err := r.c.CobraCommand.ExecuteContextC(ctx)
154156
var cd *Commandeer
@@ -177,20 +179,24 @@ func (r *Exec) Execute(ctx context.Context, args []string) (*Commandeer, error)
177179
}
178180

179181
// CommandError is returned when a command fails because of a user error (unknown command, invalid flag etc.).
182+
// All other errors comes from the execution of the command.
180183
type CommandError struct {
181184
Err error
182185
}
183186

187+
// Error implements error.
184188
func (e *CommandError) Error() string {
185189
return fmt.Sprintf("command error: %v", e.Err)
186190
}
187191

192+
// Is reports whether e is of type *CommandError.
193+
func (*CommandError) Is(e error) bool {
194+
_, ok := e.(*CommandError)
195+
return ok
196+
}
197+
188198
// IsCommandError reports whether any error in err's tree matches CommandError.
189199
func IsCommandError(err error) bool {
190-
switch err.(type) {
191-
case *CommandError:
192-
return true
193-
}
194200
return errors.Is(err, &CommandError{})
195201
}
196202

@@ -200,7 +206,7 @@ func wrapErr(err error) error {
200206
}
201207

202208
if rerr, ok := err.(*runErr); ok {
203-
err = rerr.err
209+
return rerr.error
204210
}
205211

206212
// All other errors are coming from Cobra.
@@ -234,9 +240,6 @@ func findSuggestions(cmd *cobra.Command, arg string) string {
234240
if cmd.DisableSuggestions {
235241
return ""
236242
}
237-
if cmd.SuggestionsMinimumDistance <= 0 {
238-
cmd.SuggestionsMinimumDistance = 2
239-
}
240243
suggestionsString := ""
241244
if suggestions := cmd.SuggestionsFor(arg); len(suggestions) > 0 {
242245
suggestionsString += "\n\nDid you mean this?\n"

‎simplecobra_test.go‎

Lines changed: 99 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -2,8 +2,10 @@ package simplecobra_test
22

33
import (
44
"context"
5+
"errors"
56
"fmt"
67
"log"
8+
"os"
79
"testing"
810

911
"github.com/bep/simplecobra"
@@ -121,6 +123,24 @@ func TestErrors(t *testing.T) {
121123
c.Assert(simplecobra.IsCommandError(err), qt.Equals, true)
122124
})
123125

126+
c.Run("disable suggestions", func(c *qt.C) {
127+
r := &rootCommand{name: "root",
128+
commands: []simplecobra.Commander{
129+
&lvl1Command{name: "foo", disableSuggestions: true,
130+
commands: []simplecobra.Commander{
131+
&lvl2Command{name: "bar"},
132+
},
133+
},
134+
},
135+
}
136+
x, err := simplecobra.New(r)
137+
c.Assert(err, qt.IsNil)
138+
_, err = x.Execute(context.Background(), []string{"foo", "bars"})
139+
c.Assert(err, qt.Not(qt.IsNil))
140+
c.Assert(err.Error(), qt.Contains, "unknown")
141+
c.Assert(err.Error(), qt.Not(qt.Contains), "Did you mean this?")
142+
})
143+
124144
c.Run("unknown flag", func(c *qt.C) {
125145
x, err := simplecobra.New(testCommands())
126146
c.Assert(err, qt.IsNil)
@@ -130,6 +150,63 @@ func TestErrors(t *testing.T) {
130150
c.Assert(simplecobra.IsCommandError(err), qt.Equals, true)
131151
})
132152

153+
c.Run("fail New in root command", func(c *qt.C) {
154+
r := &rootCommand{name: "root", failWithCobraCommand: true,
155+
commands: []simplecobra.Commander{
156+
&lvl1Command{name: "foo"},
157+
},
158+
}
159+
_, err := simplecobra.New(r)
160+
c.Assert(err, qt.IsNotNil)
161+
})
162+
163+
c.Run("fail New in sub command", func(c *qt.C) {
164+
r := &rootCommand{name: "root",
165+
commands: []simplecobra.Commander{
166+
&lvl1Command{name: "foo", failWithCobraCommand: true},
167+
},
168+
}
169+
_, err := simplecobra.New(r)
170+
c.Assert(err, qt.IsNotNil)
171+
})
172+
173+
c.Run("fail run root command", func(c *qt.C) {
174+
r := &rootCommand{name: "root", failRun: true,
175+
commands: []simplecobra.Commander{
176+
&lvl1Command{name: "foo"},
177+
},
178+
}
179+
x, err := simplecobra.New(r)
180+
c.Assert(err, qt.IsNil)
181+
_, err = x.Execute(context.Background(), nil)
182+
c.Assert(err, qt.IsNotNil)
183+
c.Assert(err.Error(), qt.Equals, "failRun")
184+
})
185+
186+
c.Run("fail init sub command", func(c *qt.C) {
187+
r := &rootCommand{name: "root",
188+
commands: []simplecobra.Commander{
189+
&lvl1Command{name: "foo", failInit: true},
190+
},
191+
}
192+
x, err := simplecobra.New(r)
193+
c.Assert(err, qt.IsNil)
194+
_, err = x.Execute(context.Background(), []string{"foo"})
195+
c.Assert(err, qt.IsNotNil)
196+
197+
})
198+
199+
}
200+
201+
func TestIsCommandError(t *testing.T) {
202+
c := qt.New(t)
203+
cerr := &simplecobra.CommandError{Err: errors.New("foo")}
204+
c.Assert(simplecobra.IsCommandError(os.ErrNotExist), qt.Equals, false)
205+
c.Assert(simplecobra.IsCommandError(nil), qt.Equals, false)
206+
c.Assert(simplecobra.IsCommandError(errors.New("foo")), qt.Equals, false)
207+
c.Assert(simplecobra.IsCommandError(cerr), qt.Equals, true)
208+
c.Assert(simplecobra.IsCommandError(fmt.Errorf("foo: %w", cerr)), qt.Equals, true)
209+
133210
}
134211

135212
func Example() {
@@ -176,9 +253,11 @@ type rootCommand struct {
176253
localFlagNameC string
177254

178255
// For testing.
179-
ctx context.Context
180-
initThis *simplecobra.Commandeer
181-
initRunner *simplecobra.Commandeer
256+
ctx context.Context
257+
initThis *simplecobra.Commandeer
258+
initRunner *simplecobra.Commandeer
259+
failWithCobraCommand bool
260+
failRun bool
182261

183262
// Sub commands.
184263
commands []simplecobra.Commander
@@ -202,11 +281,17 @@ func (c *rootCommand) Name() string {
202281
}
203282

204283
func (c *rootCommand) Run(ctx context.Context, cd *simplecobra.Commandeer, args []string) error {
284+
if c.failRun {
285+
return errors.New("failRun")
286+
}
205287
c.ctx = ctx
206288
return nil
207289
}
208290

209291
func (c *rootCommand) WithCobraCommand(cmd *cobra.Command) error {
292+
if c.failWithCobraCommand {
293+
return errors.New("failWithCobraCommand")
294+
}
210295
localFlags := cmd.Flags()
211296
persistentFlags := cmd.PersistentFlags()
212297

@@ -223,6 +308,10 @@ type lvl1Command struct {
223308
localFlagName string
224309
localFlagNameC string
225310

311+
failInit bool
312+
failWithCobraCommand bool
313+
disableSuggestions bool
314+
226315
rootCmd *rootCommand
227316

228317
commands []simplecobra.Commander
@@ -235,6 +324,9 @@ func (c *lvl1Command) Commands() []simplecobra.Commander {
235324
}
236325

237326
func (c *lvl1Command) Init(this, runner *simplecobra.Commandeer) error {
327+
if c.failInit {
328+
return fmt.Errorf("failInit")
329+
}
238330
c.isInit = true
239331
c.localFlagNameC = c.localFlagName + "_lvl1Command_compiled"
240332
c.rootCmd = this.Root.Command.(*rootCommand)
@@ -251,6 +343,10 @@ func (c *lvl1Command) Run(ctx context.Context, cd *simplecobra.Commandeer, args
251343
}
252344

253345
func (c *lvl1Command) WithCobraCommand(cmd *cobra.Command) error {
346+
if c.failWithCobraCommand {
347+
return errors.New("failWithCobraCommand")
348+
}
349+
cmd.DisableSuggestions = c.disableSuggestions
254350
localFlags := cmd.Flags()
255351
localFlags.StringVar(&c.localFlagName, "localFlagName", "", "set localFlagName for lvl1Command")
256352
return nil

0 commit comments

Comments
 (0)