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 }