116 lines
2.8 KiB
Go
116 lines
2.8 KiB
Go
package run
|
|
|
|
import (
|
|
"fmt"
|
|
"os/exec"
|
|
"path/filepath"
|
|
"regexp"
|
|
"strings"
|
|
)
|
|
|
|
type T interface {
|
|
Helper()
|
|
Errorf(string, ...interface{})
|
|
}
|
|
|
|
// Stub installs a catch-all for all external commands invoked from gh. It returns a restore func that, when
|
|
// invoked from tests, fails the current test if some stubs that were registered were never matched.
|
|
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("unmatched stubs (%d): %s", len(unmatched), strings.Join(unmatched, ", "))
|
|
}
|
|
}
|
|
|
|
func setPrepareCmd(fn func(*exec.Cmd) Runnable) func() {
|
|
origPrepare := PrepareCmd
|
|
PrepareCmd = func(cmd *exec.Cmd) Runnable {
|
|
// normalize git executable name for consistency in tests
|
|
if baseName := filepath.Base(cmd.Args[0]); baseName == "git" || baseName == "git.exe" {
|
|
cmd.Args[0] = "git"
|
|
}
|
|
return fn(cmd)
|
|
}
|
|
return func() {
|
|
PrepareCmd = origPrepare
|
|
}
|
|
}
|
|
|
|
// CommandStubber stubs out invocations to external commands.
|
|
type CommandStubber struct {
|
|
stubs []*commandStub
|
|
}
|
|
|
|
// Register a stub for an external command. Pattern is a regular expression, output is the standard output
|
|
// from a command. Pass callbacks to inspect raw arguments that the command was invoked with.
|
|
func (cs *CommandStubber) Register(pattern string, exitStatus int, output string, callbacks ...CommandCallback) {
|
|
if len(pattern) < 1 {
|
|
panic("cannot use empty regexp pattern")
|
|
}
|
|
cs.stubs = append(cs.stubs, &commandStub{
|
|
pattern: regexp.MustCompile(pattern),
|
|
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
|
|
}
|
|
|
|
// Run satisfies Runnable
|
|
func (s *commandStub) Run() error {
|
|
if s.exitStatus != 0 {
|
|
return fmt.Errorf("%s exited with status %d", s.pattern, s.exitStatus)
|
|
}
|
|
return nil
|
|
}
|
|
|
|
// Output satisfies Runnable
|
|
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
|
|
}
|