github.com/ungtb10d/cli/v2@v2.0.0-20221110210412-98537dd9d6a1/internal/run/stub.go (about) 1 package run 2 3 import ( 4 "fmt" 5 "os/exec" 6 "path/filepath" 7 "regexp" 8 "strings" 9 ) 10 11 type T interface { 12 Helper() 13 Errorf(string, ...interface{}) 14 } 15 16 // Stub installs a catch-all for all external commands invoked from gh. It returns a restore func that, when 17 // invoked from tests, fails the current test if some stubs that were registered were never matched. 18 func Stub() (*CommandStubber, func(T)) { 19 cs := &CommandStubber{} 20 teardown := setPrepareCmd(func(cmd *exec.Cmd) Runnable { 21 s := cs.find(cmd.Args) 22 if s == nil { 23 panic(fmt.Sprintf("no exec stub for `%s`", strings.Join(cmd.Args, " "))) 24 } 25 for _, c := range s.callbacks { 26 c(cmd.Args) 27 } 28 s.matched = true 29 return s 30 }) 31 32 return cs, func(t T) { 33 defer teardown() 34 var unmatched []string 35 for _, s := range cs.stubs { 36 if s.matched { 37 continue 38 } 39 unmatched = append(unmatched, s.pattern.String()) 40 } 41 if len(unmatched) == 0 { 42 return 43 } 44 t.Helper() 45 t.Errorf("unmatched stubs (%d): %s", len(unmatched), strings.Join(unmatched, ", ")) 46 } 47 } 48 49 func setPrepareCmd(fn func(*exec.Cmd) Runnable) func() { 50 origPrepare := PrepareCmd 51 PrepareCmd = func(cmd *exec.Cmd) Runnable { 52 // normalize git executable name for consistency in tests 53 if baseName := filepath.Base(cmd.Args[0]); baseName == "git" || baseName == "git.exe" { 54 cmd.Args[0] = "git" 55 } 56 return fn(cmd) 57 } 58 return func() { 59 PrepareCmd = origPrepare 60 } 61 } 62 63 // CommandStubber stubs out invocations to external commands. 64 type CommandStubber struct { 65 stubs []*commandStub 66 } 67 68 // Register a stub for an external command. Pattern is a regular expression, output is the standard output 69 // from a command. Pass callbacks to inspect raw arguments that the command was invoked with. 70 func (cs *CommandStubber) Register(pattern string, exitStatus int, output string, callbacks ...CommandCallback) { 71 if len(pattern) < 1 { 72 panic("cannot use empty regexp pattern") 73 } 74 cs.stubs = append(cs.stubs, &commandStub{ 75 pattern: regexp.MustCompile(pattern), 76 exitStatus: exitStatus, 77 stdout: output, 78 callbacks: callbacks, 79 }) 80 } 81 82 func (cs *CommandStubber) find(args []string) *commandStub { 83 line := strings.Join(args, " ") 84 for _, s := range cs.stubs { 85 if !s.matched && s.pattern.MatchString(line) { 86 return s 87 } 88 } 89 return nil 90 } 91 92 type CommandCallback func([]string) 93 94 type commandStub struct { 95 pattern *regexp.Regexp 96 matched bool 97 exitStatus int 98 stdout string 99 callbacks []CommandCallback 100 } 101 102 // Run satisfies Runnable 103 func (s *commandStub) Run() error { 104 if s.exitStatus != 0 { 105 return fmt.Errorf("%s exited with status %d", s.pattern, s.exitStatus) 106 } 107 return nil 108 } 109 110 // Output satisfies Runnable 111 func (s *commandStub) Output() ([]byte, error) { 112 if s.exitStatus != 0 { 113 return []byte(nil), fmt.Errorf("%s exited with status %d", s.pattern, s.exitStatus) 114 } 115 return []byte(s.stdout), nil 116 }