github.com/hikaru7719/go@v0.0.0-20181025140707-c8b2ac68906a/src/cmd/go/script_test.go (about) 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 5 // Script-driven tests. 6 // See testdata/script/README for an overview. 7 8 package main_test 9 10 import ( 11 "bytes" 12 "fmt" 13 "internal/testenv" 14 "io/ioutil" 15 "os" 16 "os/exec" 17 "path/filepath" 18 "regexp" 19 "runtime" 20 "strconv" 21 "strings" 22 "testing" 23 "time" 24 25 "cmd/go/internal/imports" 26 "cmd/go/internal/par" 27 "cmd/go/internal/txtar" 28 ) 29 30 // TestScript runs the tests in testdata/script/*.txt. 31 func TestScript(t *testing.T) { 32 testenv.MustHaveGoBuild(t) 33 if skipExternal { 34 t.Skipf("skipping external tests on %s/%s", runtime.GOOS, runtime.GOARCH) 35 } 36 37 files, err := filepath.Glob("testdata/script/*.txt") 38 if err != nil { 39 t.Fatal(err) 40 } 41 for _, file := range files { 42 file := file 43 name := strings.TrimSuffix(filepath.Base(file), ".txt") 44 t.Run(name, func(t *testing.T) { 45 t.Parallel() 46 ts := &testScript{t: t, name: name, file: file} 47 ts.setup() 48 if !*testWork { 49 defer removeAll(ts.workdir) 50 } 51 ts.run() 52 }) 53 } 54 } 55 56 // A testScript holds execution state for a single test script. 57 type testScript struct { 58 t *testing.T 59 workdir string // temporary work dir ($WORK) 60 log bytes.Buffer // test execution log (printed at end of test) 61 mark int // offset of next log truncation 62 cd string // current directory during test execution; initially $WORK/gopath/src 63 name string // short name of test ("foo") 64 file string // full file name ("testdata/script/foo.txt") 65 lineno int // line number currently executing 66 line string // line currently executing 67 env []string // environment list (for os/exec) 68 envMap map[string]string // environment mapping (matches env) 69 stdout string // standard output from last 'go' command; for 'stdout' command 70 stderr string // standard error from last 'go' command; for 'stderr' command 71 stopped bool // test wants to stop early 72 start time.Time // time phase started 73 } 74 75 var extraEnvKeys = []string{ 76 "SYSTEMROOT", // must be preserved on Windows to find DLLs; golang.org/issue/25210 77 } 78 79 // setup sets up the test execution temporary directory and environment. 80 func (ts *testScript) setup() { 81 StartProxy() 82 ts.workdir = filepath.Join(testTmpDir, "script-"+ts.name) 83 ts.check(os.MkdirAll(filepath.Join(ts.workdir, "tmp"), 0777)) 84 ts.check(os.MkdirAll(filepath.Join(ts.workdir, "gopath/src"), 0777)) 85 ts.cd = filepath.Join(ts.workdir, "gopath/src") 86 ts.env = []string{ 87 "WORK=" + ts.workdir, // must be first for ts.abbrev 88 "PATH=" + testBin + string(filepath.ListSeparator) + os.Getenv("PATH"), 89 homeEnvName() + "=/no-home", 90 "CCACHE_DISABLE=1", // ccache breaks with non-existent HOME 91 "GOARCH=" + runtime.GOARCH, 92 "GOCACHE=" + testGOCACHE, 93 "GOOS=" + runtime.GOOS, 94 "GOPATH=" + filepath.Join(ts.workdir, "gopath"), 95 "GOPROXY=" + proxyURL, 96 "GOROOT=" + testGOROOT, 97 tempEnvName() + "=" + filepath.Join(ts.workdir, "tmp"), 98 "devnull=" + os.DevNull, 99 ":=" + string(os.PathListSeparator), 100 } 101 102 if runtime.GOOS == "plan9" { 103 ts.env = append(ts.env, "path="+testBin+string(filepath.ListSeparator)+os.Getenv("path")) 104 } 105 106 if runtime.GOOS == "windows" { 107 ts.env = append(ts.env, "exe=.exe") 108 } else { 109 ts.env = append(ts.env, "exe=") 110 } 111 for _, key := range extraEnvKeys { 112 if val := os.Getenv(key); val != "" { 113 ts.env = append(ts.env, key+"="+val) 114 } 115 } 116 117 ts.envMap = make(map[string]string) 118 for _, kv := range ts.env { 119 if i := strings.Index(kv, "="); i >= 0 { 120 ts.envMap[kv[:i]] = kv[i+1:] 121 } 122 } 123 } 124 125 var execCache par.Cache 126 127 // run runs the test script. 128 func (ts *testScript) run() { 129 // Truncate log at end of last phase marker, 130 // discarding details of successful phase. 131 rewind := func() { 132 if !testing.Verbose() { 133 ts.log.Truncate(ts.mark) 134 } 135 } 136 137 // Insert elapsed time for phase at end of phase marker 138 markTime := func() { 139 if ts.mark > 0 && !ts.start.IsZero() { 140 afterMark := append([]byte{}, ts.log.Bytes()[ts.mark:]...) 141 ts.log.Truncate(ts.mark - 1) // cut \n and afterMark 142 fmt.Fprintf(&ts.log, " (%.3fs)\n", time.Since(ts.start).Seconds()) 143 ts.log.Write(afterMark) 144 } 145 ts.start = time.Time{} 146 } 147 148 defer func() { 149 markTime() 150 // Flush testScript log to testing.T log. 151 ts.t.Log("\n" + ts.abbrev(ts.log.String())) 152 }() 153 154 // Unpack archive. 155 a, err := txtar.ParseFile(ts.file) 156 ts.check(err) 157 for _, f := range a.Files { 158 name := ts.mkabs(ts.expand(f.Name)) 159 ts.check(os.MkdirAll(filepath.Dir(name), 0777)) 160 ts.check(ioutil.WriteFile(name, f.Data, 0666)) 161 } 162 163 // With -v or -testwork, start log with full environment. 164 if *testWork || testing.Verbose() { 165 // Display environment. 166 ts.cmdEnv(false, nil) 167 fmt.Fprintf(&ts.log, "\n") 168 ts.mark = ts.log.Len() 169 } 170 171 // Run script. 172 // See testdata/script/README for documentation of script form. 173 script := string(a.Comment) 174 Script: 175 for script != "" { 176 // Extract next line. 177 ts.lineno++ 178 var line string 179 if i := strings.Index(script, "\n"); i >= 0 { 180 line, script = script[:i], script[i+1:] 181 } else { 182 line, script = script, "" 183 } 184 185 // # is a comment indicating the start of new phase. 186 if strings.HasPrefix(line, "#") { 187 // If there was a previous phase, it succeeded, 188 // so rewind the log to delete its details (unless -v is in use). 189 // If nothing has happened at all since the mark, 190 // rewinding is a no-op and adding elapsed time 191 // for doing nothing is meaningless, so don't. 192 if ts.log.Len() > ts.mark { 193 rewind() 194 markTime() 195 } 196 // Print phase heading and mark start of phase output. 197 fmt.Fprintf(&ts.log, "%s\n", line) 198 ts.mark = ts.log.Len() 199 ts.start = time.Now() 200 continue 201 } 202 203 // Parse input line. Ignore blanks entirely. 204 args := ts.parse(line) 205 if len(args) == 0 { 206 continue 207 } 208 209 // Echo command to log. 210 fmt.Fprintf(&ts.log, "> %s\n", line) 211 212 // Command prefix [cond] means only run this command if cond is satisfied. 213 for strings.HasPrefix(args[0], "[") && strings.HasSuffix(args[0], "]") { 214 cond := args[0] 215 cond = cond[1 : len(cond)-1] 216 cond = strings.TrimSpace(cond) 217 args = args[1:] 218 if len(args) == 0 { 219 ts.fatalf("missing command after condition") 220 } 221 want := true 222 if strings.HasPrefix(cond, "!") { 223 want = false 224 cond = strings.TrimSpace(cond[1:]) 225 } 226 // Known conds are: $GOOS, $GOARCH, runtime.Compiler, and 'short' (for testing.Short). 227 // 228 // NOTE: If you make changes here, update testdata/script/README too! 229 // 230 ok := false 231 switch cond { 232 case runtime.GOOS, runtime.GOARCH, runtime.Compiler: 233 ok = true 234 case "short": 235 ok = testing.Short() 236 case "cgo": 237 ok = canCgo 238 case "msan": 239 ok = canMSan 240 case "race": 241 ok = canRace 242 case "net": 243 ok = testenv.HasExternalNetwork() 244 case "link": 245 ok = testenv.HasLink() 246 case "symlink": 247 ok = testenv.HasSymlink() 248 default: 249 if strings.HasPrefix(cond, "exec:") { 250 prog := cond[len("exec:"):] 251 ok = execCache.Do(prog, func() interface{} { 252 _, err := exec.LookPath(prog) 253 return err == nil 254 }).(bool) 255 break 256 } 257 if !imports.KnownArch[cond] && !imports.KnownOS[cond] && cond != "gc" && cond != "gccgo" { 258 ts.fatalf("unknown condition %q", cond) 259 } 260 } 261 if ok != want { 262 // Don't run rest of line. 263 continue Script 264 } 265 } 266 267 // Command prefix ! means negate the expectations about this command: 268 // go command should fail, match should not be found, etc. 269 neg := false 270 if args[0] == "!" { 271 neg = true 272 args = args[1:] 273 if len(args) == 0 { 274 ts.fatalf("! on line by itself") 275 } 276 } 277 278 // Run command. 279 cmd := scriptCmds[args[0]] 280 if cmd == nil { 281 ts.fatalf("unknown command %q", args[0]) 282 } 283 cmd(ts, neg, args[1:]) 284 285 // Command can ask script to stop early. 286 if ts.stopped { 287 return 288 } 289 } 290 291 // Final phase ended. 292 rewind() 293 markTime() 294 fmt.Fprintf(&ts.log, "PASS\n") 295 } 296 297 // scriptCmds are the script command implementations. 298 // Keep list and the implementations below sorted by name. 299 // 300 // NOTE: If you make changes here, update testdata/script/README too! 301 // 302 var scriptCmds = map[string]func(*testScript, bool, []string){ 303 "addcrlf": (*testScript).cmdAddcrlf, 304 "cd": (*testScript).cmdCd, 305 "cmp": (*testScript).cmdCmp, 306 "cp": (*testScript).cmdCp, 307 "env": (*testScript).cmdEnv, 308 "exec": (*testScript).cmdExec, 309 "exists": (*testScript).cmdExists, 310 "go": (*testScript).cmdGo, 311 "grep": (*testScript).cmdGrep, 312 "mkdir": (*testScript).cmdMkdir, 313 "rm": (*testScript).cmdRm, 314 "skip": (*testScript).cmdSkip, 315 "stale": (*testScript).cmdStale, 316 "stderr": (*testScript).cmdStderr, 317 "stdout": (*testScript).cmdStdout, 318 "stop": (*testScript).cmdStop, 319 "symlink": (*testScript).cmdSymlink, 320 } 321 322 // addcrlf adds CRLF line endings to the named files. 323 func (ts *testScript) cmdAddcrlf(neg bool, args []string) { 324 if len(args) == 0 { 325 ts.fatalf("usage: addcrlf file...") 326 } 327 328 for _, file := range args { 329 file = ts.mkabs(file) 330 data, err := ioutil.ReadFile(file) 331 ts.check(err) 332 ts.check(ioutil.WriteFile(file, bytes.ReplaceAll(data, []byte("\n"), []byte("\r\n")), 0666)) 333 } 334 } 335 336 // cd changes to a different directory. 337 func (ts *testScript) cmdCd(neg bool, args []string) { 338 if neg { 339 ts.fatalf("unsupported: ! cd") 340 } 341 if len(args) != 1 { 342 ts.fatalf("usage: cd dir") 343 } 344 345 dir := args[0] 346 if !filepath.IsAbs(dir) { 347 dir = filepath.Join(ts.cd, dir) 348 } 349 info, err := os.Stat(dir) 350 if os.IsNotExist(err) { 351 ts.fatalf("directory %s does not exist", dir) 352 } 353 ts.check(err) 354 if !info.IsDir() { 355 ts.fatalf("%s is not a directory", dir) 356 } 357 ts.cd = dir 358 fmt.Fprintf(&ts.log, "%s\n", ts.cd) 359 } 360 361 // cmp compares two files. 362 func (ts *testScript) cmdCmp(neg bool, args []string) { 363 if neg { 364 // It would be strange to say "this file can have any content except this precise byte sequence". 365 ts.fatalf("unsupported: ! cmp") 366 } 367 if len(args) != 2 { 368 ts.fatalf("usage: cmp file1 file2") 369 } 370 371 name1, name2 := args[0], args[1] 372 var text1, text2 string 373 if name1 == "stdout" { 374 text1 = ts.stdout 375 } else if name1 == "stderr" { 376 text1 = ts.stderr 377 } else { 378 data, err := ioutil.ReadFile(ts.mkabs(name1)) 379 ts.check(err) 380 text1 = string(data) 381 } 382 383 data, err := ioutil.ReadFile(ts.mkabs(name2)) 384 ts.check(err) 385 text2 = string(data) 386 387 if text1 == text2 { 388 return 389 } 390 391 fmt.Fprintf(&ts.log, "[diff -%s +%s]\n%s\n", name1, name2, diff(text1, text2)) 392 ts.fatalf("%s and %s differ", name1, name2) 393 } 394 395 // cp copies files, maybe eventually directories. 396 func (ts *testScript) cmdCp(neg bool, args []string) { 397 if neg { 398 ts.fatalf("unsupported: ! cp") 399 } 400 if len(args) < 2 { 401 ts.fatalf("usage: cp src... dst") 402 } 403 404 dst := ts.mkabs(args[len(args)-1]) 405 info, err := os.Stat(dst) 406 dstDir := err == nil && info.IsDir() 407 if len(args) > 2 && !dstDir { 408 ts.fatalf("cp: destination %s is not a directory", dst) 409 } 410 411 for _, arg := range args[:len(args)-1] { 412 src := ts.mkabs(arg) 413 info, err := os.Stat(src) 414 ts.check(err) 415 data, err := ioutil.ReadFile(src) 416 ts.check(err) 417 targ := dst 418 if dstDir { 419 targ = filepath.Join(dst, filepath.Base(src)) 420 } 421 ts.check(ioutil.WriteFile(targ, data, info.Mode()&0777)) 422 } 423 } 424 425 // env displays or adds to the environment. 426 func (ts *testScript) cmdEnv(neg bool, args []string) { 427 if neg { 428 ts.fatalf("unsupported: ! env") 429 } 430 if len(args) == 0 { 431 printed := make(map[string]bool) // env list can have duplicates; only print effective value (from envMap) once 432 for _, kv := range ts.env { 433 k := kv[:strings.Index(kv, "=")] 434 if !printed[k] { 435 fmt.Fprintf(&ts.log, "%s=%s\n", k, ts.envMap[k]) 436 } 437 } 438 return 439 } 440 for _, env := range args { 441 i := strings.Index(env, "=") 442 if i < 0 { 443 // Display value instead of setting it. 444 fmt.Fprintf(&ts.log, "%s=%s\n", env, ts.envMap[env]) 445 continue 446 } 447 ts.env = append(ts.env, env) 448 ts.envMap[env[:i]] = env[i+1:] 449 } 450 } 451 452 // exec runs the given command. 453 func (ts *testScript) cmdExec(neg bool, args []string) { 454 if len(args) < 1 { 455 ts.fatalf("usage: exec program [args...]") 456 } 457 var err error 458 ts.stdout, ts.stderr, err = ts.exec(args[0], args[1:]...) 459 if ts.stdout != "" { 460 fmt.Fprintf(&ts.log, "[stdout]\n%s", ts.stdout) 461 } 462 if ts.stderr != "" { 463 fmt.Fprintf(&ts.log, "[stderr]\n%s", ts.stderr) 464 } 465 if err != nil { 466 fmt.Fprintf(&ts.log, "[%v]\n", err) 467 if !neg { 468 ts.fatalf("unexpected command failure") 469 } 470 } else { 471 if neg { 472 ts.fatalf("unexpected command success") 473 } 474 } 475 } 476 477 // exists checks that the list of files exists. 478 func (ts *testScript) cmdExists(neg bool, args []string) { 479 var readonly bool 480 if len(args) > 0 && args[0] == "-readonly" { 481 readonly = true 482 args = args[1:] 483 } 484 if len(args) == 0 { 485 ts.fatalf("usage: exists [-readonly] file...") 486 } 487 488 for _, file := range args { 489 file = ts.mkabs(file) 490 info, err := os.Stat(file) 491 if err == nil && neg { 492 what := "file" 493 if info.IsDir() { 494 what = "directory" 495 } 496 ts.fatalf("%s %s unexpectedly exists", what, file) 497 } 498 if err != nil && !neg { 499 ts.fatalf("%s does not exist", file) 500 } 501 if err == nil && !neg && readonly && info.Mode()&0222 != 0 { 502 ts.fatalf("%s exists but is writable", file) 503 } 504 } 505 } 506 507 // go runs the go command. 508 func (ts *testScript) cmdGo(neg bool, args []string) { 509 ts.cmdExec(neg, append([]string{testGo}, args...)) 510 } 511 512 // mkdir creates directories. 513 func (ts *testScript) cmdMkdir(neg bool, args []string) { 514 if neg { 515 ts.fatalf("unsupported: ! mkdir") 516 } 517 if len(args) < 1 { 518 ts.fatalf("usage: mkdir dir...") 519 } 520 for _, arg := range args { 521 ts.check(os.MkdirAll(ts.mkabs(arg), 0777)) 522 } 523 } 524 525 // rm removes files or directories. 526 func (ts *testScript) cmdRm(neg bool, args []string) { 527 if neg { 528 ts.fatalf("unsupported: ! rm") 529 } 530 if len(args) < 1 { 531 ts.fatalf("usage: rm file...") 532 } 533 for _, arg := range args { 534 file := ts.mkabs(arg) 535 removeAll(file) // does chmod and then attempts rm 536 ts.check(os.RemoveAll(file)) // report error 537 } 538 } 539 540 // skip marks the test skipped. 541 func (ts *testScript) cmdSkip(neg bool, args []string) { 542 if len(args) > 1 { 543 ts.fatalf("usage: skip [msg]") 544 } 545 if neg { 546 ts.fatalf("unsupported: ! skip") 547 } 548 if len(args) == 1 { 549 ts.t.Skip(args[0]) 550 } 551 ts.t.Skip() 552 } 553 554 // stale checks that the named build targets are stale. 555 func (ts *testScript) cmdStale(neg bool, args []string) { 556 if len(args) == 0 { 557 ts.fatalf("usage: stale target...") 558 } 559 tmpl := "{{if .Error}}{{.ImportPath}}: {{.Error.Err}}{else}}" 560 if neg { 561 tmpl += "{{if .Stale}}{{.ImportPath}} is unexpectedly stale{{end}}" 562 } else { 563 tmpl += "{{if not .Stale}}{{.ImportPath}} is unexpectedly NOT stale{{end}}" 564 } 565 tmpl += "{{end}}" 566 goArgs := append([]string{"list", "-e", "-f=" + tmpl}, args...) 567 stdout, stderr, err := ts.exec(testGo, goArgs...) 568 if err != nil { 569 ts.fatalf("go list: %v\n%s%s", err, stdout, stderr) 570 } 571 if stdout != "" { 572 ts.fatalf("%s", stdout) 573 } 574 } 575 576 // stdout checks that the last go command standard output matches a regexp. 577 func (ts *testScript) cmdStdout(neg bool, args []string) { 578 scriptMatch(ts, neg, args, ts.stdout, "stdout") 579 } 580 581 // stderr checks that the last go command standard output matches a regexp. 582 func (ts *testScript) cmdStderr(neg bool, args []string) { 583 scriptMatch(ts, neg, args, ts.stderr, "stderr") 584 } 585 586 // grep checks that file content matches a regexp. 587 // Like stdout/stderr and unlike Unix grep, it accepts Go regexp syntax. 588 func (ts *testScript) cmdGrep(neg bool, args []string) { 589 scriptMatch(ts, neg, args, "", "grep") 590 } 591 592 // scriptMatch implements both stdout and stderr. 593 func scriptMatch(ts *testScript, neg bool, args []string, text, name string) { 594 n := 0 595 if len(args) >= 1 && strings.HasPrefix(args[0], "-count=") { 596 if neg { 597 ts.fatalf("cannot use -count= with negated match") 598 } 599 var err error 600 n, err = strconv.Atoi(args[0][len("-count="):]) 601 if err != nil { 602 ts.fatalf("bad -count=: %v", err) 603 } 604 if n < 1 { 605 ts.fatalf("bad -count=: must be at least 1") 606 } 607 args = args[1:] 608 } 609 610 extraUsage := "" 611 want := 1 612 if name == "grep" { 613 extraUsage = " file" 614 want = 2 615 } 616 if len(args) != want { 617 ts.fatalf("usage: %s [-count=N] 'pattern'%s", name, extraUsage) 618 } 619 620 pattern := args[0] 621 re, err := regexp.Compile(`(?m)` + pattern) 622 ts.check(err) 623 624 isGrep := name == "grep" 625 if isGrep { 626 name = args[1] // for error messages 627 data, err := ioutil.ReadFile(ts.mkabs(args[1])) 628 ts.check(err) 629 text = string(data) 630 } 631 632 // Matching against workdir would be misleading. 633 text = strings.ReplaceAll(text, ts.workdir, "$WORK") 634 635 if neg { 636 if re.MatchString(text) { 637 if isGrep { 638 fmt.Fprintf(&ts.log, "[%s]\n%s\n", name, text) 639 } 640 ts.fatalf("unexpected match for %#q found in %s: %s", pattern, name, re.FindString(text)) 641 } 642 } else { 643 if !re.MatchString(text) { 644 if isGrep { 645 fmt.Fprintf(&ts.log, "[%s]\n%s\n", name, text) 646 } 647 ts.fatalf("no match for %#q found in %s", pattern, name) 648 } 649 if n > 0 { 650 count := len(re.FindAllString(text, -1)) 651 if count != n { 652 if isGrep { 653 fmt.Fprintf(&ts.log, "[%s]\n%s\n", name, text) 654 } 655 ts.fatalf("have %d matches for %#q, want %d", count, pattern, n) 656 } 657 } 658 } 659 } 660 661 // stop stops execution of the test (marking it passed). 662 func (ts *testScript) cmdStop(neg bool, args []string) { 663 if neg { 664 ts.fatalf("unsupported: ! stop") 665 } 666 if len(args) > 1 { 667 ts.fatalf("usage: stop [msg]") 668 } 669 if len(args) == 1 { 670 fmt.Fprintf(&ts.log, "stop: %s\n", args[0]) 671 } else { 672 fmt.Fprintf(&ts.log, "stop\n") 673 } 674 ts.stopped = true 675 } 676 677 // symlink creates a symbolic link. 678 func (ts *testScript) cmdSymlink(neg bool, args []string) { 679 if neg { 680 ts.fatalf("unsupported: ! symlink") 681 } 682 if len(args) != 3 || args[1] != "->" { 683 ts.fatalf("usage: symlink file -> target") 684 } 685 // Note that the link target args[2] is not interpreted with mkabs: 686 // it will be interpreted relative to the directory file is in. 687 ts.check(os.Symlink(args[2], ts.mkabs(args[0]))) 688 } 689 690 // Helpers for command implementations. 691 692 // abbrev abbreviates the actual work directory in the string s to the literal string "$WORK". 693 func (ts *testScript) abbrev(s string) string { 694 s = strings.ReplaceAll(s, ts.workdir, "$WORK") 695 if *testWork { 696 // Expose actual $WORK value in environment dump on first line of work script, 697 // so that the user can find out what directory -testwork left behind. 698 s = "WORK=" + ts.workdir + "\n" + strings.TrimPrefix(s, "WORK=$WORK\n") 699 } 700 return s 701 } 702 703 // check calls ts.fatalf if err != nil. 704 func (ts *testScript) check(err error) { 705 if err != nil { 706 ts.fatalf("%v", err) 707 } 708 } 709 710 // exec runs the given command line (an actual subprocess, not simulated) 711 // in ts.cd with environment ts.env and then returns collected standard output and standard error. 712 func (ts *testScript) exec(command string, args ...string) (stdout, stderr string, err error) { 713 cmd := exec.Command(command, args...) 714 cmd.Dir = ts.cd 715 cmd.Env = append(ts.env, "PWD="+ts.cd) 716 var stdoutBuf, stderrBuf strings.Builder 717 cmd.Stdout = &stdoutBuf 718 cmd.Stderr = &stderrBuf 719 err = cmd.Run() 720 return stdoutBuf.String(), stderrBuf.String(), err 721 } 722 723 // expand applies environment variable expansion to the string s. 724 func (ts *testScript) expand(s string) string { 725 return os.Expand(s, func(key string) string { return ts.envMap[key] }) 726 } 727 728 // fatalf aborts the test with the given failure message. 729 func (ts *testScript) fatalf(format string, args ...interface{}) { 730 fmt.Fprintf(&ts.log, "FAIL: %s:%d: %s\n", ts.file, ts.lineno, fmt.Sprintf(format, args...)) 731 ts.t.FailNow() 732 } 733 734 // mkabs interprets file relative to the test script's current directory 735 // and returns the corresponding absolute path. 736 func (ts *testScript) mkabs(file string) string { 737 if filepath.IsAbs(file) { 738 return file 739 } 740 return filepath.Join(ts.cd, file) 741 } 742 743 // parse parses a single line as a list of space-separated arguments 744 // subject to environment variable expansion (but not resplitting). 745 // Single quotes around text disable splitting and expansion. 746 // To embed a single quote, double it: 'Don''t communicate by sharing memory.' 747 func (ts *testScript) parse(line string) []string { 748 ts.line = line 749 750 var ( 751 args []string 752 arg string // text of current arg so far (need to add line[start:i]) 753 start = -1 // if >= 0, position where current arg text chunk starts 754 quoted = false // currently processing quoted text 755 ) 756 for i := 0; ; i++ { 757 if !quoted && (i >= len(line) || line[i] == ' ' || line[i] == '\t' || line[i] == '\r' || line[i] == '#') { 758 // Found arg-separating space. 759 if start >= 0 { 760 arg += ts.expand(line[start:i]) 761 args = append(args, arg) 762 start = -1 763 arg = "" 764 } 765 if i >= len(line) || line[i] == '#' { 766 break 767 } 768 continue 769 } 770 if i >= len(line) { 771 ts.fatalf("unterminated quoted argument") 772 } 773 if line[i] == '\'' { 774 if !quoted { 775 // starting a quoted chunk 776 if start >= 0 { 777 arg += ts.expand(line[start:i]) 778 } 779 start = i + 1 780 quoted = true 781 continue 782 } 783 // 'foo''bar' means foo'bar, like in rc shell and Pascal. 784 if i+1 < len(line) && line[i+1] == '\'' { 785 arg += line[start:i] 786 start = i + 1 787 i++ // skip over second ' before next iteration 788 continue 789 } 790 // ending a quoted chunk 791 arg += line[start:i] 792 start = i + 1 793 quoted = false 794 continue 795 } 796 // found character worth saving; make sure we're saving 797 if start < 0 { 798 start = i 799 } 800 } 801 return args 802 } 803 804 // diff returns a formatted diff of the two texts, 805 // showing the entire text and the minimum line-level 806 // additions and removals to turn text1 into text2. 807 // (That is, lines only in text1 appear with a leading -, 808 // and lines only in text2 appear with a leading +.) 809 func diff(text1, text2 string) string { 810 if text1 != "" && !strings.HasSuffix(text1, "\n") { 811 text1 += "(missing final newline)" 812 } 813 lines1 := strings.Split(text1, "\n") 814 lines1 = lines1[:len(lines1)-1] // remove empty string after final line 815 if text2 != "" && !strings.HasSuffix(text2, "\n") { 816 text2 += "(missing final newline)" 817 } 818 lines2 := strings.Split(text2, "\n") 819 lines2 = lines2[:len(lines2)-1] // remove empty string after final line 820 821 // Naive dynamic programming algorithm for edit distance. 822 // https://en.wikipedia.org/wiki/Wagner–Fischer_algorithm 823 // dist[i][j] = edit distance between lines1[:len(lines1)-i] and lines2[:len(lines2)-j] 824 // (The reversed indices make following the minimum cost path 825 // visit lines in the same order as in the text.) 826 dist := make([][]int, len(lines1)+1) 827 for i := range dist { 828 dist[i] = make([]int, len(lines2)+1) 829 if i == 0 { 830 for j := range dist[0] { 831 dist[0][j] = j 832 } 833 continue 834 } 835 for j := range dist[i] { 836 if j == 0 { 837 dist[i][0] = i 838 continue 839 } 840 cost := dist[i][j-1] + 1 841 if cost > dist[i-1][j]+1 { 842 cost = dist[i-1][j] + 1 843 } 844 if lines1[len(lines1)-i] == lines2[len(lines2)-j] { 845 if cost > dist[i-1][j-1] { 846 cost = dist[i-1][j-1] 847 } 848 } 849 dist[i][j] = cost 850 } 851 } 852 853 var buf strings.Builder 854 i, j := len(lines1), len(lines2) 855 for i > 0 || j > 0 { 856 cost := dist[i][j] 857 if i > 0 && j > 0 && cost == dist[i-1][j-1] && lines1[len(lines1)-i] == lines2[len(lines2)-j] { 858 fmt.Fprintf(&buf, " %s\n", lines1[len(lines1)-i]) 859 i-- 860 j-- 861 } else if i > 0 && cost == dist[i-1][j]+1 { 862 fmt.Fprintf(&buf, "-%s\n", lines1[len(lines1)-i]) 863 i-- 864 } else { 865 fmt.Fprintf(&buf, "+%s\n", lines2[len(lines2)-j]) 866 j-- 867 } 868 } 869 return buf.String() 870 } 871 872 var diffTests = []struct { 873 text1 string 874 text2 string 875 diff string 876 }{ 877 {"a b c", "a b d e f", "a b -c +d +e +f"}, 878 {"", "a b c", "+a +b +c"}, 879 {"a b c", "", "-a -b -c"}, 880 {"a b c", "d e f", "-a -b -c +d +e +f"}, 881 {"a b c d e f", "a b d e f", "a b -c d e f"}, 882 {"a b c e f", "a b c d e f", "a b c +d e f"}, 883 } 884 885 func TestDiff(t *testing.T) { 886 for _, tt := range diffTests { 887 // Turn spaces into \n. 888 text1 := strings.ReplaceAll(tt.text1, " ", "\n") 889 if text1 != "" { 890 text1 += "\n" 891 } 892 text2 := strings.ReplaceAll(tt.text2, " ", "\n") 893 if text2 != "" { 894 text2 += "\n" 895 } 896 out := diff(text1, text2) 897 // Cut final \n, cut spaces, turn remaining \n into spaces. 898 out = strings.ReplaceAll(strings.ReplaceAll(strings.TrimSuffix(out, "\n"), " ", ""), "\n", " ") 899 if out != tt.diff { 900 t.Errorf("diff(%q, %q) = %q, want %q", text1, text2, out, tt.diff) 901 } 902 } 903 }