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  }