151 lines
3.6 KiB
Go
151 lines
3.6 KiB
Go
package run
|
|
|
|
import (
|
|
"fmt"
|
|
"os/exec"
|
|
"path/filepath"
|
|
"regexp"
|
|
"strings"
|
|
)
|
|
|
|
const (
|
|
gitAuthRE = `-c credential(?:\..+)?\.helper= -c credential(?:\..+)?\.helper=!"[^"]+" auth git-credential `
|
|
)
|
|
|
|
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 exec 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")
|
|
}
|
|
if strings.HasPrefix(pattern, "git") {
|
|
pattern = addGitAuthentication(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
|
|
}
|
|
|
|
type errWithExitCode struct {
|
|
message string
|
|
exitCode int
|
|
}
|
|
|
|
func (e errWithExitCode) Error() string {
|
|
return e.message
|
|
}
|
|
|
|
func (e errWithExitCode) ExitCode() int {
|
|
return e.exitCode
|
|
}
|
|
|
|
// Run satisfies Runnable
|
|
func (s *commandStub) Run() error {
|
|
if s.exitStatus != 0 {
|
|
// It's nontrivial to construct a fake `exec.ExitError` instance, so we return an error type
|
|
// that has the `ExitCode() int` method.
|
|
return errWithExitCode{
|
|
message: fmt.Sprintf("%s exited with status %d", s.pattern, s.exitStatus),
|
|
exitCode: s.exitStatus,
|
|
}
|
|
}
|
|
return nil
|
|
}
|
|
|
|
// Output satisfies Runnable
|
|
func (s *commandStub) Output() ([]byte, error) {
|
|
if err := s.Run(); err != nil {
|
|
return []byte(nil), err
|
|
}
|
|
return []byte(s.stdout), nil
|
|
}
|
|
|
|
// Inject git authentication string for specific git commands.
|
|
func addGitAuthentication(s string) string {
|
|
pattern := regexp.MustCompile(`( fetch | pull | push | clone | remote add.+-f | submodule )`)
|
|
loc := pattern.FindStringIndex(s)
|
|
if loc == nil {
|
|
return s
|
|
}
|
|
return s[:loc[0]+1] + gitAuthRE + s[loc[0]+1:]
|
|
}
|