1// Copyright 2018 The Go Authors. All rights reserved. 2// Use of this source code is governed by a BSD-style 3// license that can be found in the LICENSE file. 4 5package testscript 6 7import ( 8 "bufio" 9 "bytes" 10 "fmt" 11 "io/ioutil" 12 "os" 13 "os/exec" 14 "path/filepath" 15 "regexp" 16 "strconv" 17 "strings" 18 19 "github.com/pkg/diff" 20 "github.com/rogpeppe/go-internal/txtar" 21) 22 23// scriptCmds are the script command implementations. 24// Keep list and the implementations below sorted by name. 25// 26// NOTE: If you make changes here, update doc.go. 27// 28var scriptCmds = map[string]func(*TestScript, bool, []string){ 29 "cd": (*TestScript).cmdCd, 30 "chmod": (*TestScript).cmdChmod, 31 "cmp": (*TestScript).cmdCmp, 32 "cmpenv": (*TestScript).cmdCmpenv, 33 "cp": (*TestScript).cmdCp, 34 "env": (*TestScript).cmdEnv, 35 "exec": (*TestScript).cmdExec, 36 "exists": (*TestScript).cmdExists, 37 "grep": (*TestScript).cmdGrep, 38 "mkdir": (*TestScript).cmdMkdir, 39 "rm": (*TestScript).cmdRm, 40 "unquote": (*TestScript).cmdUnquote, 41 "skip": (*TestScript).cmdSkip, 42 "stdin": (*TestScript).cmdStdin, 43 "stderr": (*TestScript).cmdStderr, 44 "stdout": (*TestScript).cmdStdout, 45 "stop": (*TestScript).cmdStop, 46 "symlink": (*TestScript).cmdSymlink, 47 "unix2dos": (*TestScript).cmdUNIX2DOS, 48 "wait": (*TestScript).cmdWait, 49} 50 51// cd changes to a different directory. 52func (ts *TestScript) cmdCd(neg bool, args []string) { 53 if neg { 54 ts.Fatalf("unsupported: ! cd") 55 } 56 if len(args) != 1 { 57 ts.Fatalf("usage: cd dir") 58 } 59 60 dir := args[0] 61 if !filepath.IsAbs(dir) { 62 dir = filepath.Join(ts.cd, dir) 63 } 64 info, err := os.Stat(dir) 65 if os.IsNotExist(err) { 66 ts.Fatalf("directory %s does not exist", dir) 67 } 68 ts.Check(err) 69 if !info.IsDir() { 70 ts.Fatalf("%s is not a directory", dir) 71 } 72 ts.cd = dir 73 ts.Logf("%s\n", ts.cd) 74} 75 76func (ts *TestScript) cmdChmod(neg bool, args []string) { 77 if neg { 78 ts.Fatalf("unsupported: ! chmod") 79 } 80 if len(args) != 2 { 81 ts.Fatalf("usage: chmod perm paths...") 82 } 83 perm, err := strconv.ParseUint(args[0], 8, 32) 84 if err != nil || perm&uint64(os.ModePerm) != perm { 85 ts.Fatalf("invalid mode: %s", args[0]) 86 } 87 for _, arg := range args[1:] { 88 path := arg 89 if !filepath.IsAbs(path) { 90 path = filepath.Join(ts.cd, arg) 91 } 92 err := os.Chmod(path, os.FileMode(perm)) 93 ts.Check(err) 94 } 95} 96 97// cmp compares two files. 98func (ts *TestScript) cmdCmp(neg bool, args []string) { 99 if neg { 100 // It would be strange to say "this file can have any content except this precise byte sequence". 101 ts.Fatalf("unsupported: ! cmp") 102 } 103 if len(args) != 2 { 104 ts.Fatalf("usage: cmp file1 file2") 105 } 106 107 ts.doCmdCmp(args, false) 108} 109 110// cmpenv compares two files with environment variable substitution. 111func (ts *TestScript) cmdCmpenv(neg bool, args []string) { 112 if neg { 113 ts.Fatalf("unsupported: ! cmpenv") 114 } 115 if len(args) != 2 { 116 ts.Fatalf("usage: cmpenv file1 file2") 117 } 118 ts.doCmdCmp(args, true) 119} 120 121func (ts *TestScript) doCmdCmp(args []string, env bool) { 122 name1, name2 := args[0], args[1] 123 text1 := ts.ReadFile(name1) 124 125 absName2 := ts.MkAbs(name2) 126 data, err := ioutil.ReadFile(absName2) 127 ts.Check(err) 128 text2 := string(data) 129 if env { 130 text2 = ts.expand(text2) 131 } 132 if text1 == text2 { 133 return 134 } 135 if ts.params.UpdateScripts && !env { 136 if scriptFile, ok := ts.scriptFiles[absName2]; ok { 137 ts.scriptUpdates[scriptFile] = text1 138 return 139 } 140 // The file being compared against isn't in the txtar archive, so don't 141 // update the script. 142 } 143 144 var sb strings.Builder 145 if err := diff.Text(name1, name2, text1, text2, &sb); err != nil { 146 ts.Check(err) 147 } 148 149 ts.Logf("%s", sb.String()) 150 ts.Fatalf("%s and %s differ", name1, name2) 151} 152 153// cp copies files, maybe eventually directories. 154func (ts *TestScript) cmdCp(neg bool, args []string) { 155 if neg { 156 ts.Fatalf("unsupported: ! cp") 157 } 158 if len(args) < 2 { 159 ts.Fatalf("usage: cp src... dst") 160 } 161 162 dst := ts.MkAbs(args[len(args)-1]) 163 info, err := os.Stat(dst) 164 dstDir := err == nil && info.IsDir() 165 if len(args) > 2 && !dstDir { 166 ts.Fatalf("cp: destination %s is not a directory", dst) 167 } 168 169 for _, arg := range args[:len(args)-1] { 170 var ( 171 src string 172 data []byte 173 mode os.FileMode 174 ) 175 switch arg { 176 case "stdout": 177 src = arg 178 data = []byte(ts.stdout) 179 mode = 0666 180 case "stderr": 181 src = arg 182 data = []byte(ts.stderr) 183 mode = 0666 184 default: 185 src = ts.MkAbs(arg) 186 info, err := os.Stat(src) 187 ts.Check(err) 188 mode = info.Mode() & 0777 189 data, err = ioutil.ReadFile(src) 190 ts.Check(err) 191 } 192 targ := dst 193 if dstDir { 194 targ = filepath.Join(dst, filepath.Base(src)) 195 } 196 ts.Check(ioutil.WriteFile(targ, data, mode)) 197 } 198} 199 200// env displays or adds to the environment. 201func (ts *TestScript) cmdEnv(neg bool, args []string) { 202 if neg { 203 ts.Fatalf("unsupported: ! env") 204 } 205 if len(args) == 0 { 206 printed := make(map[string]bool) // env list can have duplicates; only print effective value (from envMap) once 207 for _, kv := range ts.env { 208 k := envvarname(kv[:strings.Index(kv, "=")]) 209 if !printed[k] { 210 printed[k] = true 211 ts.Logf("%s=%s\n", k, ts.envMap[k]) 212 } 213 } 214 return 215 } 216 for _, env := range args { 217 i := strings.Index(env, "=") 218 if i < 0 { 219 // Display value instead of setting it. 220 ts.Logf("%s=%s\n", env, ts.Getenv(env)) 221 continue 222 } 223 ts.Setenv(env[:i], env[i+1:]) 224 } 225} 226 227// exec runs the given command. 228func (ts *TestScript) cmdExec(neg bool, args []string) { 229 if len(args) < 1 || (len(args) == 1 && args[0] == "&") { 230 ts.Fatalf("usage: exec program [args...] [&]") 231 } 232 233 var err error 234 if len(args) > 0 && args[len(args)-1] == "&" { 235 var cmd *exec.Cmd 236 cmd, err = ts.execBackground(args[0], args[1:len(args)-1]...) 237 if err == nil { 238 wait := make(chan struct{}) 239 go func() { 240 ctxWait(ts.ctxt, cmd) 241 close(wait) 242 }() 243 ts.background = append(ts.background, backgroundCmd{cmd, wait, neg}) 244 } 245 ts.stdout, ts.stderr = "", "" 246 } else { 247 ts.stdout, ts.stderr, err = ts.exec(args[0], args[1:]...) 248 if ts.stdout != "" { 249 fmt.Fprintf(&ts.log, "[stdout]\n%s", ts.stdout) 250 } 251 if ts.stderr != "" { 252 fmt.Fprintf(&ts.log, "[stderr]\n%s", ts.stderr) 253 } 254 if err == nil && neg { 255 ts.Fatalf("unexpected command success") 256 } 257 } 258 259 if err != nil { 260 fmt.Fprintf(&ts.log, "[%v]\n", err) 261 if ts.ctxt.Err() != nil { 262 ts.Fatalf("test timed out while running command") 263 } else if !neg { 264 ts.Fatalf("unexpected command failure") 265 } 266 } 267} 268 269// exists checks that the list of files exists. 270func (ts *TestScript) cmdExists(neg bool, args []string) { 271 var readonly bool 272 if len(args) > 0 && args[0] == "-readonly" { 273 readonly = true 274 args = args[1:] 275 } 276 if len(args) == 0 { 277 ts.Fatalf("usage: exists [-readonly] file...") 278 } 279 280 for _, file := range args { 281 file = ts.MkAbs(file) 282 info, err := os.Stat(file) 283 if err == nil && neg { 284 what := "file" 285 if info.IsDir() { 286 what = "directory" 287 } 288 ts.Fatalf("%s %s unexpectedly exists", what, file) 289 } 290 if err != nil && !neg { 291 ts.Fatalf("%s does not exist", file) 292 } 293 if err == nil && !neg && readonly && info.Mode()&0222 != 0 { 294 ts.Fatalf("%s exists but is writable", file) 295 } 296 } 297} 298 299// mkdir creates directories. 300func (ts *TestScript) cmdMkdir(neg bool, args []string) { 301 if neg { 302 ts.Fatalf("unsupported: ! mkdir") 303 } 304 if len(args) < 1 { 305 ts.Fatalf("usage: mkdir dir...") 306 } 307 for _, arg := range args { 308 ts.Check(os.MkdirAll(ts.MkAbs(arg), 0777)) 309 } 310} 311 312// unquote unquotes files. 313func (ts *TestScript) cmdUnquote(neg bool, args []string) { 314 if neg { 315 ts.Fatalf("unsupported: ! unquote") 316 } 317 for _, arg := range args { 318 file := ts.MkAbs(arg) 319 data, err := ioutil.ReadFile(file) 320 ts.Check(err) 321 data, err = txtar.Unquote(data) 322 ts.Check(err) 323 err = ioutil.WriteFile(file, data, 0666) 324 ts.Check(err) 325 } 326} 327 328// rm removes files or directories. 329func (ts *TestScript) cmdRm(neg bool, args []string) { 330 if neg { 331 ts.Fatalf("unsupported: ! rm") 332 } 333 if len(args) < 1 { 334 ts.Fatalf("usage: rm file...") 335 } 336 for _, arg := range args { 337 file := ts.MkAbs(arg) 338 removeAll(file) // does chmod and then attempts rm 339 ts.Check(os.RemoveAll(file)) // report error 340 } 341} 342 343// skip marks the test skipped. 344func (ts *TestScript) cmdSkip(neg bool, args []string) { 345 if len(args) > 1 { 346 ts.Fatalf("usage: skip [msg]") 347 } 348 if neg { 349 ts.Fatalf("unsupported: ! skip") 350 } 351 352 // Before we mark the test as skipped, shut down any background processes and 353 // make sure they have returned the correct status. 354 for _, bg := range ts.background { 355 interruptProcess(bg.cmd.Process) 356 } 357 ts.cmdWait(false, nil) 358 359 if len(args) == 1 { 360 ts.t.Skip(args[0]) 361 } 362 ts.t.Skip() 363} 364 365func (ts *TestScript) cmdStdin(neg bool, args []string) { 366 if neg { 367 ts.Fatalf("unsupported: ! stdin") 368 } 369 if len(args) != 1 { 370 ts.Fatalf("usage: stdin filename") 371 } 372 ts.stdin = ts.ReadFile(args[0]) 373} 374 375// stdout checks that the last go command standard output matches a regexp. 376func (ts *TestScript) cmdStdout(neg bool, args []string) { 377 scriptMatch(ts, neg, args, ts.stdout, "stdout") 378} 379 380// stderr checks that the last go command standard output matches a regexp. 381func (ts *TestScript) cmdStderr(neg bool, args []string) { 382 scriptMatch(ts, neg, args, ts.stderr, "stderr") 383} 384 385// grep checks that file content matches a regexp. 386// Like stdout/stderr and unlike Unix grep, it accepts Go regexp syntax. 387func (ts *TestScript) cmdGrep(neg bool, args []string) { 388 scriptMatch(ts, neg, args, "", "grep") 389} 390 391// stop stops execution of the test (marking it passed). 392func (ts *TestScript) cmdStop(neg bool, args []string) { 393 if neg { 394 ts.Fatalf("unsupported: ! stop") 395 } 396 if len(args) > 1 { 397 ts.Fatalf("usage: stop [msg]") 398 } 399 if len(args) == 1 { 400 ts.Logf("stop: %s\n", args[0]) 401 } else { 402 ts.Logf("stop\n") 403 } 404 ts.stopped = true 405} 406 407// symlink creates a symbolic link. 408func (ts *TestScript) cmdSymlink(neg bool, args []string) { 409 if neg { 410 ts.Fatalf("unsupported: ! symlink") 411 } 412 if len(args) != 3 || args[1] != "->" { 413 ts.Fatalf("usage: symlink file -> target") 414 } 415 // Note that the link target args[2] is not interpreted with MkAbs: 416 // it will be interpreted relative to the directory file is in. 417 ts.Check(os.Symlink(args[2], ts.MkAbs(args[0]))) 418} 419 420// cmdUNIX2DOS converts files from UNIX line endings to DOS line endings. 421func (ts *TestScript) cmdUNIX2DOS(neg bool, args []string) { 422 if neg { 423 ts.Fatalf("unsupported: ! unix2dos") 424 } 425 if len(args) < 1 { 426 ts.Fatalf("usage: unix2dos paths...") 427 } 428 for _, arg := range args { 429 filename := ts.MkAbs(arg) 430 data, err := ioutil.ReadFile(filename) 431 ts.Check(err) 432 dosData, err := unix2DOS(data) 433 ts.Check(err) 434 if err := ioutil.WriteFile(filename, dosData, 0666); err != nil { 435 ts.Fatalf("%s: %v", filename, err) 436 } 437 } 438} 439 440// Tait waits for background commands to exit, setting stderr and stdout to their result. 441func (ts *TestScript) cmdWait(neg bool, args []string) { 442 if neg { 443 ts.Fatalf("unsupported: ! wait") 444 } 445 if len(args) > 0 { 446 ts.Fatalf("usage: wait") 447 } 448 449 var stdouts, stderrs []string 450 for _, bg := range ts.background { 451 <-bg.wait 452 453 args := append([]string{filepath.Base(bg.cmd.Args[0])}, bg.cmd.Args[1:]...) 454 fmt.Fprintf(&ts.log, "[background] %s: %v\n", strings.Join(args, " "), bg.cmd.ProcessState) 455 456 cmdStdout := bg.cmd.Stdout.(*strings.Builder).String() 457 if cmdStdout != "" { 458 fmt.Fprintf(&ts.log, "[stdout]\n%s", cmdStdout) 459 stdouts = append(stdouts, cmdStdout) 460 } 461 462 cmdStderr := bg.cmd.Stderr.(*strings.Builder).String() 463 if cmdStderr != "" { 464 fmt.Fprintf(&ts.log, "[stderr]\n%s", cmdStderr) 465 stderrs = append(stderrs, cmdStderr) 466 } 467 468 if bg.cmd.ProcessState.Success() { 469 if bg.neg { 470 ts.Fatalf("unexpected command success") 471 } 472 } else { 473 if ts.ctxt.Err() != nil { 474 ts.Fatalf("test timed out while running command") 475 } else if !bg.neg { 476 ts.Fatalf("unexpected command failure") 477 } 478 } 479 } 480 481 ts.stdout = strings.Join(stdouts, "") 482 ts.stderr = strings.Join(stderrs, "") 483 ts.background = nil 484} 485 486// scriptMatch implements both stdout and stderr. 487func scriptMatch(ts *TestScript, neg bool, args []string, text, name string) { 488 n := 0 489 if len(args) >= 1 && strings.HasPrefix(args[0], "-count=") { 490 if neg { 491 ts.Fatalf("cannot use -count= with negated match") 492 } 493 var err error 494 n, err = strconv.Atoi(args[0][len("-count="):]) 495 if err != nil { 496 ts.Fatalf("bad -count=: %v", err) 497 } 498 if n < 1 { 499 ts.Fatalf("bad -count=: must be at least 1") 500 } 501 args = args[1:] 502 } 503 504 extraUsage := "" 505 want := 1 506 if name == "grep" { 507 extraUsage = " file" 508 want = 2 509 } 510 if len(args) != want { 511 ts.Fatalf("usage: %s [-count=N] 'pattern'%s", name, extraUsage) 512 } 513 514 pattern := args[0] 515 re, err := regexp.Compile(`(?m)` + pattern) 516 ts.Check(err) 517 518 isGrep := name == "grep" 519 if isGrep { 520 name = args[1] // for error messages 521 data, err := ioutil.ReadFile(ts.MkAbs(args[1])) 522 ts.Check(err) 523 text = string(data) 524 } 525 526 if neg { 527 if re.MatchString(text) { 528 if isGrep { 529 ts.Logf("[%s]\n%s\n", name, text) 530 } 531 ts.Fatalf("unexpected match for %#q found in %s: %s", pattern, name, re.FindString(text)) 532 } 533 } else { 534 if !re.MatchString(text) { 535 if isGrep { 536 ts.Logf("[%s]\n%s\n", name, text) 537 } 538 ts.Fatalf("no match for %#q found in %s", pattern, name) 539 } 540 if n > 0 { 541 count := len(re.FindAllString(text, -1)) 542 if count != n { 543 if isGrep { 544 ts.Logf("[%s]\n%s\n", name, text) 545 } 546 ts.Fatalf("have %d matches for %#q, want %d", count, pattern, n) 547 } 548 } 549 } 550} 551 552// unix2DOS returns data with UNIX line endings converted to DOS line endings. 553func unix2DOS(data []byte) ([]byte, error) { 554 sb := &strings.Builder{} 555 s := bufio.NewScanner(bytes.NewReader(data)) 556 for s.Scan() { 557 if _, err := sb.Write(s.Bytes()); err != nil { 558 return nil, err 559 } 560 if _, err := sb.WriteString("\r\n"); err != nil { 561 return nil, err 562 } 563 } 564 if err := s.Err(); err != nil { 565 return nil, err 566 } 567 return []byte(sb.String()), nil 568} 569