Skip to content

Commit f9783fe

Browse files
committed
Add exec.Command stub mechanism that matches by arguments
Our previous command stub mechanism matches stubs sequentially, which leads to brittle tests when the exec calls get reordered or removed in the implementation.
1 parent f17d967 commit f9783fe

File tree

1 file changed

+91
-0
lines changed

1 file changed

+91
-0
lines changed

internal/run/stub.go

Lines changed: 91 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,91 @@
1+
package run
2+
3+
import (
4+
"fmt"
5+
"os/exec"
6+
"regexp"
7+
"strings"
8+
)
9+
10+
type T interface {
11+
Helper()
12+
Errorf(string, ...interface{})
13+
}
14+
15+
func Stub() (*CommandStubber, func(T)) {
16+
cs := &CommandStubber{}
17+
teardown := SetPrepareCmd(func(cmd *exec.Cmd) Runnable {
18+
s := cs.find(cmd.Args)
19+
if s == nil {
20+
panic(fmt.Sprintf("no exec stub for `%s`", strings.Join(cmd.Args, " ")))
21+
}
22+
for _, c := range s.callbacks {
23+
c(cmd.Args)
24+
}
25+
s.matched = true
26+
return s
27+
})
28+
29+
return cs, func(t T) {
30+
defer teardown()
31+
var unmatched []string
32+
for _, s := range cs.stubs {
33+
if s.matched {
34+
continue
35+
}
36+
unmatched = append(unmatched, s.pattern.String())
37+
}
38+
if len(unmatched) == 0 {
39+
return
40+
}
41+
t.Helper()
42+
t.Errorf("umatched stubs (%d): %s", len(unmatched), strings.Join(unmatched, ", "))
43+
}
44+
}
45+
46+
type CommandStubber struct {
47+
stubs []*commandStub
48+
}
49+
50+
func (cs *CommandStubber) Register(p string, exitStatus int, output string, callbacks ...CommandCallback) {
51+
cs.stubs = append(cs.stubs, &commandStub{
52+
pattern: regexp.MustCompile(p),
53+
exitStatus: exitStatus,
54+
stdout: output,
55+
callbacks: callbacks,
56+
})
57+
}
58+
59+
func (cs *CommandStubber) find(args []string) *commandStub {
60+
line := strings.Join(args, " ")
61+
for _, s := range cs.stubs {
62+
if !s.matched && s.pattern.MatchString(line) {
63+
return s
64+
}
65+
}
66+
return nil
67+
}
68+
69+
type CommandCallback func([]string)
70+
71+
type commandStub struct {
72+
pattern *regexp.Regexp
73+
matched bool
74+
exitStatus int
75+
stdout string
76+
callbacks []CommandCallback
77+
}
78+
79+
func (s *commandStub) Run() error {
80+
if s.exitStatus != 0 {
81+
return fmt.Errorf("%s exited with status %d", s.pattern, s.exitStatus)
82+
}
83+
return nil
84+
}
85+
86+
func (s *commandStub) Output() ([]byte, error) {
87+
if s.exitStatus != 0 {
88+
return []byte(nil), fmt.Errorf("%s exited with status %d", s.pattern, s.exitStatus)
89+
}
90+
return []byte(s.stdout), nil
91+
}

0 commit comments

Comments
 (0)