github.com/jmigpin/editor@v1.6.0/util/testutil/script.go (about) 1 package testutil 2 3 import ( 4 "bufio" 5 "bytes" 6 "context" 7 "errors" 8 "fmt" 9 "os" 10 "os/exec" 11 "path/filepath" 12 "strconv" 13 "strings" 14 "testing" 15 16 "github.com/jmigpin/editor/util/iout" 17 "github.com/jmigpin/editor/util/osutil" 18 "golang.org/x/tools/txtar" 19 ) 20 21 // based on txtar (txt archive) 22 type Script struct { 23 ScriptsDir string 24 Args []string 25 Cmds []*ScriptCmd // user cmds (provided) 26 Work bool // don't remove work dir at end 27 28 ScriptStart func(*testing.T) error // each script init 29 ScriptStop func(*testing.T) error // each script close 30 31 ucmds map[string]*ScriptCmd // user cmds (mapped) 32 icmds map[string]*ScriptCmd // internal cmds 33 34 workDir string 35 lastCmdStd [2][]byte // stdin, stdout 36 lastCmd struct { 37 stdout []byte 38 stderr []byte 39 err []byte 40 } 41 } 42 43 func NewScript(args []string) *Script { 44 return &Script{Args: args} 45 } 46 47 //---------- 48 49 func (scr *Script) log(t *testing.T, s string) { 50 s = strings.TrimRight(s, "\n") // remove newlines 51 t.Log(s) // adds one newline 52 } 53 func (scr *Script) logf(t *testing.T, f string, args ...interface{}) { 54 scr.log(t, fmt.Sprintf(f, args...)) 55 } 56 57 //---------- 58 59 func (scr *Script) Run(t *testing.T) { 60 // internal cmds 61 icmds := []*ScriptCmd{ 62 &ScriptCmd{"ucmd", scr.icUCmd}, // run user cmd 63 &ScriptCmd{"exec", scr.icExec}, 64 &ScriptCmd{"contains", scr.icContains}, 65 &ScriptCmd{"setenv", scr.icSetEnv}, 66 &ScriptCmd{"fail", scr.icFail}, 67 &ScriptCmd{"cd", scr.icChangeDir}, 68 } 69 scr.icmds = mapScriptCmds(icmds) 70 // user cmds 71 scr.ucmds = mapScriptCmds(scr.Cmds) 72 73 if err := scr.runDir(t, scr.ScriptsDir); err != nil { 74 t.Fatal(err) 75 } 76 } 77 func (scr *Script) runDir(t *testing.T, dir string) error { 78 des, err := os.ReadDir(dir) 79 if err != nil { 80 return err 81 } 82 for _, de := range des { 83 if de.IsDir() { 84 continue 85 } 86 filename := filepath.Join(dir, de.Name()) 87 if err := scr.runFile(t, filename); err != nil { 88 return err 89 } 90 } 91 return nil 92 } 93 func (scr *Script) runFile(t1 *testing.T, filename string) error { 94 err0 := error(nil) 95 name := filepath.Base(filename) 96 ok := t1.Run(name, func(t2 *testing.T) { 97 // running as a sub-test 98 scr.logf(t2, "script: %v", filename) 99 100 ar, err := txtar.ParseFile(filename) 101 if err != nil { 102 err0 = err // stop testing by returning an error 103 return 104 } 105 106 func() { // run in func to use defer inside 107 if scr.ScriptStart != nil { 108 if err := scr.ScriptStart(t2); err != nil { 109 t2.Fatal(err) 110 } 111 } 112 if scr.ScriptStop != nil { 113 defer func() { 114 if err := scr.ScriptStop(t2); err != nil { 115 t2.Fatal(err) 116 } 117 }() 118 } 119 120 if err := scr.runScript(t2, filename, ar); err != nil { 121 t2.Logf("FAIL: %v", err) 122 //t2.Fail() // continues testing 123 t2.Fatal() // also seems to continue, need t1 124 t1.Fatal() // stop testing 125 } 126 }() 127 }) 128 _ = ok 129 return err0 130 } 131 func (scr *Script) runScript(t *testing.T, filename string, ar *txtar.Archive) error { 132 // create working dir 133 // TODO: review, not working properly 134 //dir, err := os.MkdirTemp(t.TempDir(), "editor_testutil_work.*") 135 dir, err := os.MkdirTemp("", "editor_testutilscript*") 136 if err != nil { 137 return err 138 } 139 scr.workDir = dir 140 t.Setenv("WORK", scr.workDir) 141 scr.logf(t, "script_workdir: %v", scr.workDir) 142 defer func() { 143 if scr.Work { 144 scr.logf(t, "workDir not cleaned") 145 } else { 146 _ = os.RemoveAll(scr.workDir) 147 } 148 }() 149 150 // keep/restore current dir 151 keepDir, err := os.Getwd() 152 if err != nil { 153 return err 154 } 155 defer os.Chdir(keepDir) 156 157 // switch to working dir 158 if err := os.Chdir(scr.workDir); err != nil { 159 return err 160 } 161 162 // setup tmp dir in workdir for program to create its own tmp files 163 scriptTmpDir := filepath.Join(scr.workDir, "tmp") 164 t.Setenv("TMPDIR", scriptTmpDir) 165 if err := iout.MkdirAll(scriptTmpDir); err != nil { 166 return err 167 } 168 169 for _, f := range ar.Files { 170 if err := scr.writeToTmp(f.Name, f.Data); err != nil { 171 return err 172 } 173 } 174 175 // run script 176 rd := bytes.NewReader(ar.Comment) 177 scanner := bufio.NewScanner(rd) 178 line := 0 179 for scanner.Scan() { 180 line++ 181 txt := strings.TrimSpace(scanner.Text()) 182 // comments 183 if strings.HasPrefix(txt, "#") { 184 continue 185 } 186 // empty lines 187 if txt == "" { 188 continue 189 } 190 // as least an arg after empty lines check 191 args := scr.splitArgs(txt) 192 193 cmd, ok := scr.icmds[args[0]] 194 if !ok { 195 err := fmt.Errorf("cmd not found: %v", args[0]) 196 return &lineError{filename, line, err} 197 } 198 scr.logf(t, "%v: %v", args[0], args[1:]) 199 if err := cmd.Fn(t, args); err != nil { 200 return &lineError{filename, line, err} 201 } 202 } 203 if err := scanner.Err(); err != nil { 204 return &lineError{filename, line, err} 205 } 206 return nil 207 } 208 209 //---------- 210 211 func (scr *Script) splitArgs(s string) []string { 212 quoted := false 213 escape := false 214 a := strings.FieldsFunc(s, func(r rune) bool { 215 if r == '\\' { 216 escape = true 217 return false 218 } 219 if escape { 220 escape = false 221 return false 222 } 223 if r == '"' { 224 quoted = !quoted 225 } 226 return !quoted && r == ' ' 227 }) 228 return a 229 } 230 231 //---------- 232 233 func (scr *Script) collectOutput(t *testing.T, fn func() error) error { 234 stdout, stderr, err := CollectLog(t, fn) 235 236 scr.lastCmd.stdout = stdout 237 scr.lastCmd.stderr = stderr 238 scr.lastCmd.err = nil 239 if err != nil { 240 scr.lastCmd.err = []byte(err.Error()) 241 } 242 243 return err 244 } 245 246 //---------- 247 248 func (scr *Script) writeToTmp(filename string, data []byte) error { 249 filename2 := filepath.Join(scr.workDir, filename) 250 return iout.MkdirAllWriteFile(filename2, data, 0o644) 251 } 252 253 //---------- 254 255 func (scr *Script) icExec(t *testing.T, args []string) error { 256 args = args[1:] // drop "exec" 257 if len(args) <= 0 { 258 return fmt.Errorf("expecting args, got %v", len(args)) 259 } 260 ctx := context.Background() 261 ec := exec.CommandContext(ctx, args[0], args[1:]...) 262 263 //ec.Dir = // commented: dir set with os.Chdir previously 264 265 return scr.collectOutput(t, func() error { 266 // setup cmd stdout inside collectoutput 267 // TODO: stdin? 268 ec.Stdout = os.Stdout 269 ec.Stderr = os.Stderr 270 271 ci := osutil.NewCmdI(ec) 272 ci = osutil.NewSetSidCmd(ctx, ci) 273 ci = osutil.NewShellCmd(ci) 274 return osutil.RunCmdI(ci) 275 }) 276 } 277 278 //---------- 279 280 func (scr *Script) icUCmd(t *testing.T, args []string) error { 281 args = args[1:] // drop "cmd" 282 cmd, ok := scr.ucmds[args[0]] 283 if !ok { 284 return fmt.Errorf("cmd not found: %v", args[0]) 285 } 286 return scr.collectOutput(t, func() error { 287 return cmd.Fn(t, args) 288 }) 289 } 290 291 //---------- 292 293 func (scr *Script) icContains(t *testing.T, args []string) error { 294 args = args[1:] // drop "contains" 295 if len(args) != 2 { 296 return fmt.Errorf("expecting 2 args, got %v", args) 297 } 298 299 data, ok := scr.lastCmdContent(args[0]) 300 if !ok { 301 return fmt.Errorf("unknown content: %v", args[0]) 302 } 303 304 // pattern 305 u, err := strconv.Unquote(args[1]) 306 if err != nil { 307 return err 308 } 309 pattern := u 310 311 if !bytes.Contains(data, []byte(pattern)) { 312 //return fmt.Errorf("contains: no match:\npattern=[%v]\ndata=[%v]", pattern, string(data)) 313 return fmt.Errorf("contains: no match") 314 } 315 return nil 316 } 317 318 //---------- 319 320 func (scr *Script) icSetEnv(t *testing.T, args []string) error { 321 args = args[1:] // drop "setenv" 322 if len(args) != 1 && len(args) != 2 { 323 return fmt.Errorf("expecting 1 or 2 args, got %v", args) 324 } 325 v := "" // allow setting to empty 326 if len(args) == 2 { 327 v = args[1] 328 329 // allow env expansion when setting env vars 330 v = os.Expand(v, os.Getenv) 331 332 // allow expansion of lastcmd 333 data, ok := scr.lastCmdContent(v) 334 if ok { 335 v = string(data) 336 } 337 } 338 t.Setenv(args[0], v) 339 return nil 340 } 341 342 //---------- 343 344 func (scr *Script) icFail(t *testing.T, args []string) error { 345 args = args[1:] // drop "fail" 346 if len(args) < 1 { 347 return fmt.Errorf("expecting at least 1 arg, got %v", args) 348 } 349 cmd, ok := scr.icmds[args[0]] 350 if !ok { 351 return fmt.Errorf("cmd not found: %v", args[0]) 352 } 353 err := cmd.Fn(t, args) 354 if err == nil { 355 return fmt.Errorf("expected failure but got no error") 356 } 357 scr.logf(t, "fail ok: %v", err) 358 return nil 359 } 360 361 //---------- 362 363 func (scr *Script) icChangeDir(t *testing.T, args []string) error { 364 args = args[1:] // drop "cd" 365 if len(args) != 1 { 366 return fmt.Errorf("expecting 1 arg, got %v", args) 367 } 368 dir := args[0] 369 return os.Chdir(dir) 370 } 371 372 //---------- 373 374 func (scr *Script) lastCmdContent(name string) ([]byte, bool) { 375 switch name { 376 case "stdout": 377 return scr.lastCmd.stdout, true 378 case "stderr": 379 return scr.lastCmd.stderr, true 380 case "error": 381 return scr.lastCmd.err, true 382 } 383 return nil, false 384 } 385 386 //---------- 387 //---------- 388 //---------- 389 390 type ScriptCmd struct { 391 Name string 392 Fn func(t *testing.T, args []string) error 393 } 394 395 func mapScriptCmds(w []*ScriptCmd) map[string]*ScriptCmd { 396 m := map[string]*ScriptCmd{} 397 for _, cmd := range w { 398 m[cmd.Name] = cmd 399 } 400 return m 401 } 402 403 //---------- 404 405 type lineError struct { 406 filename string 407 line int 408 err error 409 } 410 411 func (le *lineError) Error() string { 412 return fmt.Sprintf("%v:%v: %v", le.filename, le.line, le.err) 413 } 414 func (le *lineError) Is(err error) bool { 415 return errors.Is(le.err, err) 416 } 417 418 //---------- 419 //---------- 420 //----------