Skip to content

Commit 01f8f0c

Browse files
authored
feat: Support more complex stubbing scenarios in run package (#25)
1 parent c43071b commit 01f8f0c

15 files changed

+416
-39
lines changed

go.mod

Lines changed: 6 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,7 @@ go 1.19
44

55
require (
66
github.com/AlecAivazis/survey/v2 v2.3.6
7-
github.com/briandowns/spinner v1.19.0
7+
github.com/briandowns/spinner v1.20.0
88
github.com/creasty/defaults v1.6.0
99
github.com/go-playground/locales v0.14.0
1010
github.com/go-playground/universal-translator v0.18.0
@@ -18,13 +18,13 @@ require (
1818

1919
require (
2020
github.com/davecgh/go-spew v1.1.1 // indirect
21-
github.com/fatih/color v1.7.0 // indirect
21+
github.com/fatih/color v1.13.0 // indirect
2222
github.com/kballard/go-shellquote v0.0.0-20180428030007-95032a82bc51 // indirect
2323
github.com/leodido/go-urn v1.2.1 // indirect
2424
github.com/mattn/go-colorable v0.1.13 // indirect
2525
github.com/pmezard/go-difflib v1.0.0 // indirect
26-
golang.org/x/crypto v0.0.0-20211215153901-e495a2d5b3d3 // indirect
27-
golang.org/x/sys v0.0.0-20220811171246-fbc7d0a398ab // indirect
28-
golang.org/x/term v0.0.0-20210503060354-a79de5458b56 // indirect
29-
golang.org/x/text v0.3.7 // indirect
26+
golang.org/x/crypto v0.4.0 // indirect
27+
golang.org/x/sys v0.3.0 // indirect
28+
golang.org/x/term v0.3.0 // indirect
29+
golang.org/x/text v0.5.0 // indirect
3030
)

go.sum

Lines changed: 19 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -2,8 +2,8 @@ github.com/AlecAivazis/survey/v2 v2.3.6 h1:NvTuVHISgTHEHeBFqt6BHOe4Ny/NwGZr7w+F8
22
github.com/AlecAivazis/survey/v2 v2.3.6/go.mod h1:4AuI9b7RjAR+G7v9+C4YSlX/YL3K3cWNXgWXOhllqvI=
33
github.com/Netflix/go-expect v0.0.0-20220104043353-73e0943537d2 h1:+vx7roKuyA63nhn5WAunQHLTznkw5W8b1Xc0dNjp83s=
44
github.com/Netflix/go-expect v0.0.0-20220104043353-73e0943537d2/go.mod h1:HBCaDeC1lPdgDeDbhX8XFpy1jqjK0IBG8W5K+xYqA0w=
5-
github.com/briandowns/spinner v1.19.0 h1:s8aq38H+Qju89yhp89b4iIiMzMm8YN3p6vGpwyh/a8E=
6-
github.com/briandowns/spinner v1.19.0/go.mod h1:mQak9GHqbspjC/5iUx3qMlIho8xBS/ppAL/hX5SmPJU=
5+
github.com/briandowns/spinner v1.20.0 h1:GQq1Yf1KyzYT8CY19GzWrDKP6hYOFB6J72Ks7d8aO1U=
6+
github.com/briandowns/spinner v1.20.0/go.mod h1:TcwZHb7Wb6vn/+bcVv1UXEzaA4pLS7yznHlkY/HzH44=
77
github.com/creack/pty v1.1.9/go.mod h1:oKZEueFk5CKHvIhNR5MUki03XCEU+Q6VDXinZuGJ33E=
88
github.com/creack/pty v1.1.17 h1:QeVUsEDNrLBW4tMgZHvxy18sKtr6VI492kBhUfhDJNI=
99
github.com/creack/pty v1.1.17/go.mod h1:MOBLtS5ELjhRRrroQr9kyvTxUAFNvYEK993ew/Vr4O4=
@@ -12,8 +12,9 @@ github.com/creasty/defaults v1.6.0/go.mod h1:iGzKe6pbEHnpMPtfDXZEr0NVxWnPTjb1bbD
1212
github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
1313
github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c=
1414
github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
15-
github.com/fatih/color v1.7.0 h1:DkWD4oS2D8LGGgTQ6IvwJJXSL5Vp2ffcQg58nFV38Ys=
1615
github.com/fatih/color v1.7.0/go.mod h1:Zm6kSWBoL9eyXnKyktHP6abPY2pDugNf5KwzbycvMj4=
16+
github.com/fatih/color v1.13.0 h1:8LOYc1KYPPmyKMuN8QV2DNRWNbLo6LZ0iLs8+mlH53w=
17+
github.com/fatih/color v1.13.0/go.mod h1:kLAiJbzzSOZDVNGyDpeOxJ47H46qBXwg5ILebYFFOfk=
1718
github.com/go-playground/assert/v2 v2.0.1 h1:MsBgLAaY856+nPRTKrp3/OZK38U/wa0CcBYNjji3q3A=
1819
github.com/go-playground/assert/v2 v2.0.1/go.mod h1:VDjEfimB/XKnb+ZQfWdccd7VUvScMdVu0Titje2rxJ4=
1920
github.com/go-playground/locales v0.14.0 h1:u50s323jtVGugKlcYeyzC0etD1HifMjqmJqb8WugfUU=
@@ -37,9 +38,12 @@ github.com/kr/text v0.2.0/go.mod h1:eLer722TekiGuMkidMxC/pM04lWEeraHUUmBw8l2grE=
3738
github.com/leodido/go-urn v1.2.1 h1:BqpAaACuzVSgi/VLzGZIobT2z4v53pjosyNd9Yv6n/w=
3839
github.com/leodido/go-urn v1.2.1/go.mod h1:zt4jvISO2HfUBqxjfIshjdMTYS56ZS/qv49ictyFfxY=
3940
github.com/mattn/go-colorable v0.1.2/go.mod h1:U0ppj6V5qS13XJ6of8GYAs25YV2eR4EVcfRqFIhoBtE=
41+
github.com/mattn/go-colorable v0.1.9/go.mod h1:u6P/XSegPjTcexA+o6vUJrdnUu04hMope9wVRipJSqc=
4042
github.com/mattn/go-colorable v0.1.13 h1:fFA4WZxdEF4tXPZVKMLwD8oUnCTTo08duU7wxecdEvA=
4143
github.com/mattn/go-colorable v0.1.13/go.mod h1:7S9/ev0klgBDR4GtXTXX8a3vIGJpMovkB8vQcUbaXHg=
4244
github.com/mattn/go-isatty v0.0.8/go.mod h1:Iq45c/XA43vh69/j3iqttzPXn0bhXyGjM0Hdxcsrc5s=
45+
github.com/mattn/go-isatty v0.0.12/go.mod h1:cbi8OIDigv2wuxKPP5vlRcQ1OAZbq2CE4Kysco4FUpU=
46+
github.com/mattn/go-isatty v0.0.14/go.mod h1:7GGIvUiUoEMVVmxf/4nioHXj79iQHKdU27kJ6hsGG94=
4347
github.com/mattn/go-isatty v0.0.16 h1:bq3VjFmv/sOjHtdEhmkEV4x1AJtvUvOJ2PFAZ5+peKQ=
4448
github.com/mattn/go-isatty v0.0.16/go.mod h1:kYGgaQfpe5nmfYZH+SKPsOc2e4SrIfOl2e/yFXSvRLM=
4549
github.com/mgutz/ansi v0.0.0-20170206155736-9520e82c474b/go.mod h1:01TrycV0kFyexm33Z7vhZRXopbI8J3TDReVlkTgMUxE=
@@ -62,24 +66,32 @@ github.com/stretchr/testify v1.7.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/
6266
github.com/stretchr/testify v1.8.0/go.mod h1:yNjHg4UonilssWZ8iaSj1OCr/vHnekPRkoO+kdMU+MU=
6367
github.com/stretchr/testify v1.8.1 h1:w7B6lhMri9wdJUVmEZPGGhZzrYTPvgJArz7wNPgYKsk=
6468
github.com/stretchr/testify v1.8.1/go.mod h1:w2LPCIKwWwSfY2zedu0+kehJoqGctiVI29o6fzry7u4=
65-
golang.org/x/crypto v0.0.0-20211215153901-e495a2d5b3d3 h1:0es+/5331RGQPcXlMfP+WrnIIS6dNnNRe0WB02W0F4M=
6669
golang.org/x/crypto v0.0.0-20211215153901-e495a2d5b3d3/go.mod h1:IxCIyHEi3zRg3s0A5j5BB6A9Jmi73HwBIUl50j+osU4=
70+
golang.org/x/crypto v0.4.0 h1:UVQgzMY87xqpKNgb+kDsll2Igd33HszWHFLmpaRMq/8=
71+
golang.org/x/crypto v0.4.0/go.mod h1:3quD/ATkf6oY+rnes5c3ExXTbLc8mueNue5/DoinL80=
6772
golang.org/x/net v0.0.0-20211112202133-69e39bad7dc2/go.mod h1:9nx3DQGgdP8bBQD5qxJ1jj9UTztislL4KSBs9R2vV5Y=
6873
golang.org/x/sys v0.0.0-20190222072716-a9d3bda3a223/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
74+
golang.org/x/sys v0.0.0-20200116001909-b77594299b42/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
75+
golang.org/x/sys v0.0.0-20200223170610-d5e6a3e2c0ae/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
6976
golang.org/x/sys v0.0.0-20201119102817-f84b799fce68/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
7077
golang.org/x/sys v0.0.0-20210423082822-04245dca01da/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
7178
golang.org/x/sys v0.0.0-20210615035016-665e8c7367d1/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
79+
golang.org/x/sys v0.0.0-20210630005230-0f9fa26af87c/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
7280
golang.org/x/sys v0.0.0-20210806184541-e5e7981a1069/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
7381
golang.org/x/sys v0.0.0-20220422013727-9388b58f7150/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
74-
golang.org/x/sys v0.0.0-20220811171246-fbc7d0a398ab h1:2QkjZIsXupsJbJIdSjjUOgWK3aEtzyuh2mPt3l/CkeU=
7582
golang.org/x/sys v0.0.0-20220811171246-fbc7d0a398ab/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
83+
golang.org/x/sys v0.3.0 h1:w8ZOecv6NaNa/zC8944JTU3vz4u6Lagfk4RPQxv92NQ=
84+
golang.org/x/sys v0.3.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
7685
golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo=
77-
golang.org/x/term v0.0.0-20210503060354-a79de5458b56 h1:b8jxX3zqjpqb2LklXPzKSGJhzyxCOZSz8ncv8Nv+y7w=
7886
golang.org/x/term v0.0.0-20210503060354-a79de5458b56/go.mod h1:tfny5GFUkzUvx4ps4ajbZsCe5lw1metzhBm9T3x7oIY=
87+
golang.org/x/term v0.1.0/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8=
88+
golang.org/x/term v0.3.0 h1:qoo4akIqOcDME5bhc/NgxUdovd6BSS2uMsVjB56q1xI=
89+
golang.org/x/term v0.3.0/go.mod h1:q750SLmJuPmVoN1blW3UFBPREJfb1KmY3vwxfr+nFDA=
7990
golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
8091
golang.org/x/text v0.3.6/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
81-
golang.org/x/text v0.3.7 h1:olpwvP2KacW1ZWvsR7uQhoyTYvKAupfQrRGBFM352Gk=
8292
golang.org/x/text v0.3.7/go.mod h1:u+2+/6zg+i71rQMx5EYifcz6MCKuco9NR6JIITiCfzQ=
93+
golang.org/x/text v0.5.0 h1:OLmvp0KP+FVG99Ct/qFiL/Fhk4zp4QQnZ7b2U+5piUM=
94+
golang.org/x/text v0.5.0/go.mod h1:mrYo+phRRbMaCq/xk9113O4dZlRixOauAjOtrjsXDZ8=
8395
golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
8496
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
8597
gopkg.in/check.v1 v1.0.0-20180628173108-788fd7840127/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=

run/client.go

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -21,13 +21,13 @@ type Client struct {
2121
// the given arguments.
2222
func (c *Client) Command(name string, arg ...string) *Cmd {
2323
cmd := exec.Command(name, arg...)
24-
return &Cmd{Cmd: cmd, client: c}
24+
return &Cmd{Cmd: cmd, client: c, exitCode: -1}
2525
}
2626

2727
// CommandContext is like Command but includes a context.
2828
func (c *Client) CommandContext(ctx context.Context, name string, arg ...string) *Cmd {
2929
cmd := exec.CommandContext(ctx, name, arg...)
30-
return &Cmd{Cmd: cmd, client: c}
30+
return &Cmd{Cmd: cmd, client: c, exitCode: -1}
3131
}
3232

3333
// IsStubbed returns true if the executor is configured for stubbing.

run/cmd.go

Lines changed: 7 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -12,7 +12,13 @@ import (
1212
type Cmd struct {
1313
*exec.Cmd
1414

15-
client *Client
15+
client *Client
16+
exitCode int // used when stubbing
17+
}
18+
19+
// ExitCode returns the exit code for the command.
20+
func (c *Cmd) ExitCode() int {
21+
return c.client.Executor.ExitCode(c)
1622
}
1723

1824
// Output runs the command and returns its standard output.

run/cmd_test.go

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,17 @@ import (
88
"github.com/stretchr/testify/assert"
99
)
1010

11+
func TestCmd_ExitCode(t *testing.T) {
12+
client := NewClient()
13+
client.Executor = &ExecutorMock{
14+
ExitCodeFunc: func(cmd *Cmd) int {
15+
return 123
16+
},
17+
}
18+
cmd := client.Command("/bin/echo", "foo", "bar")
19+
assert.Equal(t, 123, cmd.ExitCode())
20+
}
21+
1122
func TestCmd_Output(t *testing.T) {
1223
client := NewClient()
1324
client.Executor = &ExecutorMock{

run/default_executor.go

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,10 @@ var (
99
type defaultExecutor struct {
1010
}
1111

12+
func (e *defaultExecutor) ExitCode(cmd *Cmd) int {
13+
return cmd.Cmd.ProcessState.ExitCode()
14+
}
15+
1216
func (e *defaultExecutor) Output(cmd *Cmd) ([]byte, error) {
1317
return cmd.Cmd.Output()
1418
}

run/default_executor_test.go

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,18 @@ import (
66
"github.com/stretchr/testify/assert"
77
)
88

9+
func TestDefaultExecutor_ExitCode(t *testing.T) {
10+
client := NewClient()
11+
assert.Equal(t, DefaultExecutor, client.Executor)
12+
13+
cmd := client.Command("echo", "foo", "bar")
14+
assert.Equal(t, -1, client.Executor.ExitCode(cmd))
15+
16+
err := cmd.Run()
17+
assert.NoError(t, err)
18+
assert.Equal(t, 0, client.Executor.ExitCode(cmd))
19+
}
20+
921
func TestDefaultExecutor_Output(t *testing.T) {
1022
client := NewClient()
1123
assert.Equal(t, DefaultExecutor, client.Executor)

run/errors.go

Lines changed: 31 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,31 @@
1+
package run
2+
3+
import (
4+
"fmt"
5+
)
6+
7+
// NewExitError returns a new exit error for code.
8+
func NewExitError(code int) *ExitError {
9+
return &ExitError{code: code}
10+
}
11+
12+
// ExitError is a replacement for [exec.ExitError] used when stubbing that
13+
// allows for accessing the exit code.
14+
//
15+
// The reason we don't use [exec.ExitError] directly is that it stores the
16+
// exit code inside `os.ProcessState`, which in turn uses `syscall.WaitStatus`,
17+
// and WaitStatus is implemented differently per-system.
18+
type ExitError struct {
19+
Stderr []byte
20+
code int
21+
}
22+
23+
// Code returns the exit code.
24+
func (e *ExitError) Code() int {
25+
return e.code
26+
}
27+
28+
// Error returns the error message.
29+
func (e *ExitError) Error() string {
30+
return fmt.Sprintf("exit status %v", e.Code())
31+
}

run/errors_test.go

Lines changed: 22 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,22 @@
1+
package run
2+
3+
import (
4+
"testing"
5+
6+
"github.com/stretchr/testify/assert"
7+
)
8+
9+
func TestNewExitError(t *testing.T) {
10+
err := NewExitError(0)
11+
assert.Error(t, err)
12+
}
13+
14+
func TestExitError_Code(t *testing.T) {
15+
err := NewExitError(12)
16+
assert.Equal(t, 12, err.Code())
17+
}
18+
19+
func TestExitError_Error(t *testing.T) {
20+
err := NewExitError(12)
21+
assert.Equal(t, "exit status 12", err.Error())
22+
}

run/executor.go

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,9 @@ package run
44

55
// Executor is an interface representing the ability to execute an external command.
66
type Executor interface {
7+
// ExitCode returns the exit code of the exited process, or -1
8+
// if the process hasn't exited or was terminated by a signal.
9+
ExitCode(cmd *Cmd) int
710
// Output runs the command and returns its standard output.
811
Output(cmd *Cmd) ([]byte, error)
912
// Run starts the specified command and waits for it to complete.

run/executor_test.go

Lines changed: 46 additions & 2 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

run/responder.go

Lines changed: 39 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -6,12 +6,12 @@ import (
66
)
77

88
// Responder is a function that returns stubbed command output.
9-
type Responder func(cmd *Cmd) ([]byte, error)
9+
type Responder func(cmd *Cmd) (stdout []byte, stderr []byte, err error)
1010

1111
// ErrorResponse creates a responder that returns err.
1212
func ErrorResponse(err error) Responder {
13-
return func(cmd *Cmd) ([]byte, error) {
14-
return nil, err
13+
return func(cmd *Cmd) ([]byte, []byte, error) {
14+
return nil, nil, err
1515
}
1616
}
1717

@@ -27,7 +27,7 @@ func ErrorResponse(err error) Responder {
2727
// // err == nil
2828
func RegexpResponse(pattern string, index int) Responder {
2929
r := regexp.MustCompile(pattern)
30-
return func(cmd *Cmd) ([]byte, error) {
30+
return func(cmd *Cmd) ([]byte, []byte, error) {
3131
cmdStr := cmd.String()
3232
matches := r.FindStringSubmatch(cmdStr)
3333
if index >= len(matches) {
@@ -40,13 +40,44 @@ func RegexpResponse(pattern string, index int) Responder {
4040
),
4141
)
4242
}
43-
return []byte(matches[index]), nil
43+
return []byte(matches[index]), nil, nil
4444
}
4545
}
4646

47-
// StringResponse creates a responder that returns s.
47+
// StringResponse creates a responder that returns the given string via stdout.
4848
func StringResponse(s string) Responder {
49-
return func(cmd *Cmd) ([]byte, error) {
50-
return []byte(s), nil
49+
return func(cmd *Cmd) ([]byte, []byte, error) {
50+
return []byte(s), nil, nil
5151
}
5252
}
53+
54+
// StdoutResponse creates a responder that returns the given bytes via stdout.
55+
// If the provided exit code is non-zero, then an exit error will also be returned.
56+
func StdoutResponse(stdout []byte, code int) Responder {
57+
return func(cmd *Cmd) ([]byte, []byte, error) {
58+
return stdout, nil, errorForCode(code)
59+
}
60+
}
61+
62+
// StderrResponse creates a responder that returns the given bytes via stderr.
63+
// If the provided exit code is non-zero, then an exit error will also be returned.
64+
func StderrResponse(stderr []byte, code int) Responder {
65+
return func(cmd *Cmd) ([]byte, []byte, error) {
66+
return nil, stderr, errorForCode(code)
67+
}
68+
}
69+
70+
// MuxResponse creates a responder that returns values for both stdout and stderr.
71+
// If the provided exit code is non-zero, then an exit error will also be returned.
72+
func MuxResponse(stdout []byte, stderr []byte, code int) Responder {
73+
return func(cmd *Cmd) ([]byte, []byte, error) {
74+
return stdout, stderr, errorForCode(code)
75+
}
76+
}
77+
78+
func errorForCode(code int) error {
79+
if code == 0 {
80+
return nil
81+
}
82+
return NewExitError(code)
83+
}

0 commit comments

Comments
 (0)