github.com/ActiveState/cli@v0.0.0-20240508170324-6801f60cd051/internal/testhelpers/e2e/spawn.go (about)

     1  package e2e
     2  
     3  import (
     4  	"fmt"
     5  	"path/filepath"
     6  	"regexp"
     7  	"runtime"
     8  	"strings"
     9  	"time"
    10  
    11  	"github.com/ActiveState/termtest"
    12  
    13  	"github.com/ActiveState/cli/internal/errs"
    14  	"github.com/ActiveState/cli/internal/osutils"
    15  )
    16  
    17  type SpawnedCmd struct {
    18  	*termtest.TermTest
    19  	opts SpawnOpts
    20  }
    21  
    22  func (s *SpawnedCmd) WorkDirectory() string {
    23  	return s.TermTest.Cmd().Dir
    24  }
    25  
    26  func (s *SpawnedCmd) Wait() error {
    27  	return s.TermTest.Wait(30 * time.Second)
    28  }
    29  
    30  func (s *SpawnedCmd) Executable() string {
    31  	return s.TermTest.Cmd().Path
    32  }
    33  
    34  // StrippedSnapshot returns the snapshot with trimmed whitespace and stripped line endings
    35  // Mainly intended for JSON parsing
    36  func (s *SpawnedCmd) StrippedSnapshot() string {
    37  	return strings.Trim(strings.ReplaceAll(s.TermTest.Snapshot(), "\n", ""), "\x00\x20\x0a\x0d")
    38  }
    39  
    40  // ExpectRe takes a string rather than an already compiled regex, so that we can handle regex compilation failures
    41  // through our error handling chain rather than have it fail on eg. a panic through regexp.MustCompile, or needing
    42  // to manually error check it before sending it to ExpectRe.
    43  func (s *SpawnedCmd) ExpectRe(v string, opts ...termtest.SetExpectOpt) error {
    44  	expectOpts, err := termtest.NewExpectOpts(opts...)
    45  	if err != nil {
    46  		err = fmt.Errorf("could not create expect options: %w", err)
    47  		return s.ExpectErrorHandler(&err, expectOpts)
    48  	}
    49  
    50  	rx, err := regexp.Compile(v)
    51  	if err != nil {
    52  		err = errs.Wrap(err, "ExpectRe received invalid regex string")
    53  		return s.ExpectErrorHandler(&err, expectOpts)
    54  	}
    55  	return s.TermTest.ExpectRe(rx, opts...)
    56  }
    57  
    58  func (s *SpawnedCmd) ExpectInput(opts ...termtest.SetExpectOpt) error {
    59  	expectOpts, err := termtest.NewExpectOpts(opts...)
    60  	if err != nil {
    61  		err = fmt.Errorf("could not create expect options: %w", err)
    62  		return s.ExpectErrorHandler(&err, expectOpts)
    63  	}
    64  
    65  	cmdName := strings.TrimSuffix(strings.ToLower(filepath.Base(s.Cmd().Path)), ".exe")
    66  
    67  	shellName := ""
    68  	envMap := osutils.EnvSliceToMap(s.Cmd().Env)
    69  	if v, ok := envMap["SHELL"]; ok {
    70  		shellName = strings.TrimSuffix(strings.ToLower(filepath.Base(v)), ".exe")
    71  	}
    72  
    73  	send := `echo $'expect\'input from posix shell'`
    74  	expect := `expect'input from posix shell`
    75  	if cmdName != "bash" && shellName != "bash" && runtime.GOOS == "windows" {
    76  		send = `echo ^<expect input from cmd prompt^>`
    77  		expect = `<expect input from cmd prompt>`
    78  	}
    79  
    80  	// Termtest internal functions already implement error handling
    81  	if err := s.SendLine(send); err != nil {
    82  		return fmt.Errorf("could not send line to terminal: %w", err)
    83  	}
    84  
    85  	return s.Expect(expect, opts...)
    86  }
    87  
    88  func (s *SpawnedCmd) Send(value string) error {
    89  	if runtime.GOOS == "windows" {
    90  		// Work around race condition - on Windows it appears more likely to happen
    91  		// https://activestatef.atlassian.net/browse/DX-2171
    92  		time.Sleep(100 * time.Millisecond)
    93  	}
    94  	return s.TermTest.Send(value)
    95  }
    96  
    97  func (s *SpawnedCmd) SendLine(value string) error {
    98  	if runtime.GOOS == "windows" {
    99  		// Work around race condition - on Windows it appears more likely to happen
   100  		// https://activestatef.atlassian.net/browse/DX-2171
   101  		time.Sleep(100 * time.Millisecond)
   102  	}
   103  	return s.TermTest.SendLine(value)
   104  }
   105  
   106  func (s *SpawnedCmd) SendEnter() error {
   107  	return s.SendLine("")
   108  }
   109  
   110  func (s *SpawnedCmd) SendKeyUp() error {
   111  	return s.Send(string([]byte{0033, '[', 'A'}))
   112  }
   113  
   114  func (s *SpawnedCmd) SendKeyDown() error {
   115  	return s.Send(string([]byte{0033, '[', 'B'}))
   116  }
   117  
   118  func (s *SpawnedCmd) SendKeyRight() error {
   119  	return s.Send(string([]byte{0033, '[', 'C'}))
   120  }
   121  
   122  func (s *SpawnedCmd) SendKeyLeft() error {
   123  	return s.Send(string([]byte{0033, '[', 'D'}))
   124  }
   125  
   126  type SpawnOpts struct {
   127  	Args           []string
   128  	Env            []string
   129  	Dir            string
   130  	TermtestOpts   []termtest.SetOpt
   131  	HideCmdArgs    bool
   132  	RunInsideShell bool
   133  }
   134  
   135  func NewSpawnOpts() SpawnOpts {
   136  	return SpawnOpts{
   137  		RunInsideShell: false,
   138  	}
   139  }
   140  
   141  type SpawnOptSetter func(opts *SpawnOpts)
   142  
   143  func OptArgs(args ...string) SpawnOptSetter {
   144  	return func(opts *SpawnOpts) {
   145  		opts.Args = args
   146  	}
   147  }
   148  
   149  func OptWD(wd string) SpawnOptSetter {
   150  	return func(opts *SpawnOpts) {
   151  		opts.Dir = wd
   152  	}
   153  }
   154  
   155  func OptAppendEnv(env ...string) SpawnOptSetter {
   156  	return func(opts *SpawnOpts) {
   157  		opts.Env = append(opts.Env, env...)
   158  	}
   159  }
   160  
   161  func OptTermTest(opt ...termtest.SetOpt) SpawnOptSetter {
   162  	return func(opts *SpawnOpts) {
   163  		opts.TermtestOpts = append(opts.TermtestOpts, opt...)
   164  	}
   165  }
   166  
   167  func OptHideArgs() SpawnOptSetter {
   168  	return func(opts *SpawnOpts) {
   169  		opts.HideCmdArgs = true
   170  	}
   171  }
   172  
   173  func OptRunInsideShell(v bool) SpawnOptSetter {
   174  	return func(opts *SpawnOpts) {
   175  		opts.RunInsideShell = v
   176  	}
   177  }