From f9783fe812da432e149e1eea6a3e9f3c7d574f22 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Mislav=20Marohni=C4=87?= Date: Wed, 23 Sep 2020 18:38:29 +0200 Subject: [PATCH] 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. --- internal/run/stub.go | 91 ++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 91 insertions(+) create mode 100644 internal/run/stub.go diff --git a/internal/run/stub.go b/internal/run/stub.go new file mode 100644 index 000000000..9bd6e279b --- /dev/null +++ b/internal/run/stub.go @@ -0,0 +1,91 @@ +package run + +import ( + "fmt" + "os/exec" + "regexp" + "strings" +) + +type T interface { + Helper() + Errorf(string, ...interface{}) +} + +func Stub() (*CommandStubber, func(T)) { + cs := &CommandStubber{} + teardown := SetPrepareCmd(func(cmd *exec.Cmd) Runnable { + s := cs.find(cmd.Args) + if s == nil { + panic(fmt.Sprintf("no exec stub for `%s`", strings.Join(cmd.Args, " "))) + } + for _, c := range s.callbacks { + c(cmd.Args) + } + s.matched = true + return s + }) + + return cs, func(t T) { + defer teardown() + var unmatched []string + for _, s := range cs.stubs { + if s.matched { + continue + } + unmatched = append(unmatched, s.pattern.String()) + } + if len(unmatched) == 0 { + return + } + t.Helper() + t.Errorf("umatched stubs (%d): %s", len(unmatched), strings.Join(unmatched, ", ")) + } +} + +type CommandStubber struct { + stubs []*commandStub +} + +func (cs *CommandStubber) Register(p string, exitStatus int, output string, callbacks ...CommandCallback) { + cs.stubs = append(cs.stubs, &commandStub{ + pattern: regexp.MustCompile(p), + exitStatus: exitStatus, + stdout: output, + callbacks: callbacks, + }) +} + +func (cs *CommandStubber) find(args []string) *commandStub { + line := strings.Join(args, " ") + for _, s := range cs.stubs { + if !s.matched && s.pattern.MatchString(line) { + return s + } + } + return nil +} + +type CommandCallback func([]string) + +type commandStub struct { + pattern *regexp.Regexp + matched bool + exitStatus int + stdout string + callbacks []CommandCallback +} + +func (s *commandStub) Run() error { + if s.exitStatus != 0 { + return fmt.Errorf("%s exited with status %d", s.pattern, s.exitStatus) + } + return nil +} + +func (s *commandStub) Output() ([]byte, error) { + if s.exitStatus != 0 { + return []byte(nil), fmt.Errorf("%s exited with status %d", s.pattern, s.exitStatus) + } + return []byte(s.stdout), nil +}