├── testdata ├── values.txt ├── defer.txt ├── commandstatus.txt ├── command.txt ├── cmpenv.txt ├── regexpquote.txt ├── nothing.txt ├── setenv.txt ├── interrupt.txt ├── execguard.txt ├── stdin.txt ├── hello.txt ├── cpstdout.txt ├── setupfiles.txt ├── readfile.txt ├── exists.txt ├── testscript_update_script_not_in_archive.txt ├── evalsymlink.txt ├── testscript_update_script.txt ├── testscript_update_script_stderr.txt ├── testscript_update_script_quote.txt ├── wait.txt └── exec_path_change.txt ├── internal ├── imports │ ├── testdata │ │ └── import1 │ │ │ ├── x.go │ │ │ ├── x_darwin.go │ │ │ ├── x_windows.go │ │ │ └── x1.go │ ├── scan_test.go │ ├── scan.go │ ├── read_test.go │ ├── build.go │ └── read.go ├── os │ └── execpath │ │ ├── exec.go │ │ ├── lp_js.go │ │ ├── lp_plan9.go │ │ ├── lp_unix.go │ │ └── lp_windows.go ├── testenv │ ├── testenv_cgo.go │ ├── testenv_notwin.go │ ├── testenv_windows.go │ └── testenv.go ├── textutil │ ├── doc.go │ ├── diff_test.go │ └── diff.go └── par │ ├── work_test.go │ └── work.go ├── go.mod ├── envvarname.go ├── envvarname_windows.go ├── README.md ├── go.sum ├── LICENSE ├── txtar ├── archive_test.go └── archive.go ├── exe.go ├── cover.go ├── testscript_test.go ├── doc.go ├── cmd.go └── testscript.go /testdata/values.txt: -------------------------------------------------------------------------------- 1 | test-values 2 | -------------------------------------------------------------------------------- /testdata/defer.txt: -------------------------------------------------------------------------------- 1 | testdefer 2 | testdefer 3 | testdefer 4 | -------------------------------------------------------------------------------- /testdata/commandstatus.txt: -------------------------------------------------------------------------------- 1 | ! status 1 2 | ! status 2 3 | status 0 4 | -------------------------------------------------------------------------------- /internal/imports/testdata/import1/x.go: -------------------------------------------------------------------------------- 1 | package x 2 | 3 | import "import1" 4 | -------------------------------------------------------------------------------- /internal/imports/testdata/import1/x_darwin.go: -------------------------------------------------------------------------------- 1 | package xxxx 2 | 3 | import "import3" 4 | -------------------------------------------------------------------------------- /internal/imports/testdata/import1/x_windows.go: -------------------------------------------------------------------------------- 1 | package x 2 | 3 | import "import2" 4 | -------------------------------------------------------------------------------- /testdata/command.txt: -------------------------------------------------------------------------------- 1 | printargs a b 'c d' 2 | stdout '\["printargs" "a" "b" "c d"\]\n' 3 | -------------------------------------------------------------------------------- /go.mod: -------------------------------------------------------------------------------- 1 | module github.com/rogpeppe/testscript 2 | 3 | go 1.13 4 | 5 | require gopkg.in/errgo.v2 v2.1.0 6 | -------------------------------------------------------------------------------- /testdata/cmpenv.txt: -------------------------------------------------------------------------------- 1 | env $=$ 2 | cmpenv file1 file2 3 | 4 | -- file1 -- 5 | $i 6 | -- file2 -- 7 | $$i 8 | -------------------------------------------------------------------------------- /testdata/regexpquote.txt: -------------------------------------------------------------------------------- 1 | env XXX='hello)' 2 | grep ^${XXX@R}$ file.txt 3 | 4 | -- file.txt -- 5 | hello) 6 | -------------------------------------------------------------------------------- /testdata/nothing.txt: -------------------------------------------------------------------------------- 1 | # Intentionally blank file, used to test that -testwork doesn't remove the work directory 2 | -------------------------------------------------------------------------------- /envvarname.go: -------------------------------------------------------------------------------- 1 | // +build !windows 2 | 3 | package testscript 4 | 5 | func envvarname(k string) string { 6 | return k 7 | } 8 | -------------------------------------------------------------------------------- /testdata/setenv.txt: -------------------------------------------------------------------------------- 1 | setSpecialVal 2 | exists $SPECIALVAL.txt 3 | ensureSpecialVal 4 | 5 | -- 42.txt -- 6 | Douglas Adams 7 | -------------------------------------------------------------------------------- /testdata/interrupt.txt: -------------------------------------------------------------------------------- 1 | [windows] skip 2 | 3 | signalcatcher & 4 | waitfile catchsignal 5 | interrupt 6 | wait 7 | stdout 'caught interrupt' 8 | -------------------------------------------------------------------------------- /envvarname_windows.go: -------------------------------------------------------------------------------- 1 | package testscript 2 | 3 | import "strings" 4 | 5 | func envvarname(k string) string { 6 | return strings.ToLower(k) 7 | } 8 | -------------------------------------------------------------------------------- /testdata/execguard.txt: -------------------------------------------------------------------------------- 1 | [exec:nosuchcommand] exec nosuchcommand 2 | [!exec:cat] stop 3 | exec cat foo 4 | stdout 'foo\n' 5 | 6 | -- foo -- 7 | foo 8 | -------------------------------------------------------------------------------- /testdata/stdin.txt: -------------------------------------------------------------------------------- 1 | stdin hello.txt 2 | [exec:cat] exec cat 3 | stdout hello 4 | [exec:cat] exec cat 5 | ! stdout hello 6 | 7 | -- hello.txt -- 8 | hello 9 | 10 | -------------------------------------------------------------------------------- /testdata/hello.txt: -------------------------------------------------------------------------------- 1 | [!exec:cat] stop 2 | 3 | # hello world 4 | exec cat hello.text 5 | stdout 'hello world\n' 6 | ! stderr . 7 | 8 | -- hello.text -- 9 | hello world 10 | -------------------------------------------------------------------------------- /testdata/cpstdout.txt: -------------------------------------------------------------------------------- 1 | [!exec:cat] stop 2 | 3 | # hello world 4 | exec cat hello.text 5 | cp stdout got 6 | cmp got hello.text 7 | 8 | -- hello.text -- 9 | hello world 10 | 11 | -------------------------------------------------------------------------------- /internal/imports/testdata/import1/x1.go: -------------------------------------------------------------------------------- 1 | // +build blahblh 2 | // +build linux 3 | // +build !linux 4 | // +build windows 5 | // +build darwin 6 | 7 | package x 8 | 9 | import "import4" 10 | -------------------------------------------------------------------------------- /testdata/setupfiles.txt: -------------------------------------------------------------------------------- 1 | # check that the Setup function saw the unarchived files, 2 | # including the temp directory that's always created. 3 | setup-filenames a b tmp 4 | 5 | -- a -- 6 | -- b/c -- 7 | -------------------------------------------------------------------------------- /testdata/readfile.txt: -------------------------------------------------------------------------------- 1 | echo stdout stdout 2 | testreadfile stdout 3 | 4 | echo stderr stderr 5 | testreadfile stderr 6 | 7 | testreadfile x/somefile 8 | 9 | -- x/somefile -- 10 | x/somefile 11 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # testscript - end-to-end testing of Go commands 2 | 3 | The `go` tool uses an internal DSL for testing. It works really well and we 4 | want it for our own commands, so we've factored it out here. 5 | -------------------------------------------------------------------------------- /internal/os/execpath/exec.go: -------------------------------------------------------------------------------- 1 | package execpath 2 | 3 | import "os/exec" 4 | 5 | type Error = exec.Error 6 | 7 | // ErrNotFound is the error resulting if a path search failed to find an executable file. 8 | var ErrNotFound = exec.ErrNotFound 9 | -------------------------------------------------------------------------------- /testdata/exists.txt: -------------------------------------------------------------------------------- 1 | chmod 444 foo_r 2 | exists foo 3 | ! exists unfoo 4 | # TODO The following line fails but probably should not. 5 | # ! exists -readonly foo 6 | exists foo_r 7 | exists -readonly foo_r 8 | 9 | -- foo -- 10 | foo 11 | 12 | -- foo_r -- 13 | -------------------------------------------------------------------------------- /internal/testenv/testenv_cgo.go: -------------------------------------------------------------------------------- 1 | // Copyright 2017 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 | // +build cgo 6 | 7 | package testenv 8 | 9 | func init() { 10 | haveCGO = true 11 | } 12 | -------------------------------------------------------------------------------- /testdata/testscript_update_script_not_in_archive.txt: -------------------------------------------------------------------------------- 1 | unquote scripts/testscript.txt 2 | cp scripts/testscript.txt unchanged 3 | ! testscript-update scripts 4 | cmp scripts/testscript.txt unchanged 5 | 6 | -- scripts/testscript.txt -- 7 | >echo stdout right 8 | >cp file expect 9 | >cmp stdout expect 10 | > 11 | >-- file -- 12 | >wrong 13 | -------------------------------------------------------------------------------- /testdata/evalsymlink.txt: -------------------------------------------------------------------------------- 1 | # If ioutil.TempDir returns a sym linked dir (default behaviour in macOS for example) the 2 | # matcher will have problems with external programs that uses the real path. 3 | # This script tests that $WORK is matched in a consistent way (also see #79). 4 | [windows] skip 5 | exec pwd 6 | stdout ^$WORK$ 7 | exec pwd -P 8 | stdout ^$WORK$ 9 | -------------------------------------------------------------------------------- /testdata/testscript_update_script.txt: -------------------------------------------------------------------------------- 1 | unquote scripts/testscript.txt 2 | unquote testscript-new.txt 3 | testscript-update scripts 4 | cmp scripts/testscript.txt testscript-new.txt 5 | 6 | -- scripts/testscript.txt -- 7 | >echo stdout right 8 | >cmp stdout expect 9 | > 10 | >-- expect -- 11 | >wrong 12 | -- testscript-new.txt -- 13 | >echo stdout right 14 | >cmp stdout expect 15 | > 16 | >-- expect -- 17 | >right 18 | -------------------------------------------------------------------------------- /internal/textutil/doc.go: -------------------------------------------------------------------------------- 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 | // package textutil contains text processing utilities. 6 | // 7 | // This package came to life as a result of refactoring code common to 8 | // internal packages we have factored out of the Go repo. 9 | package textutil 10 | -------------------------------------------------------------------------------- /testdata/testscript_update_script_stderr.txt: -------------------------------------------------------------------------------- 1 | unquote scripts/testscript.txt 2 | unquote testscript-new.txt 3 | testscript-update scripts 4 | cmp scripts/testscript.txt testscript-new.txt 5 | 6 | -- scripts/testscript.txt -- 7 | >echo stderr right 8 | >cmp stderr expect 9 | > 10 | >-- expect -- 11 | >wrong 12 | -- testscript-new.txt -- 13 | >echo stderr right 14 | >cmp stderr expect 15 | > 16 | >-- expect -- 17 | >right 18 | -------------------------------------------------------------------------------- /testdata/testscript_update_script_quote.txt: -------------------------------------------------------------------------------- 1 | unquote scripts/testscript.txt 2 | unquote testscript-new.txt 3 | testscript-update scripts 4 | cmp scripts/testscript.txt testscript-new.txt 5 | 6 | -- scripts/testscript.txt -- 7 | >echo stdout '-- lookalike --' 8 | >cmp stdout expect 9 | > 10 | >-- expect -- 11 | >wrong 12 | -- testscript-new.txt -- 13 | >echo stdout '-- lookalike --' 14 | >cmp stdout expect 15 | > 16 | >-- expect -- 17 | >>-- lookalike -- 18 | -------------------------------------------------------------------------------- /internal/testenv/testenv_notwin.go: -------------------------------------------------------------------------------- 1 | // Copyright 2016 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 | // +build !windows 6 | 7 | package testenv 8 | 9 | import ( 10 | "runtime" 11 | ) 12 | 13 | func hasSymlink() (ok bool, reason string) { 14 | switch runtime.GOOS { 15 | case "android", "nacl", "plan9": 16 | return false, "" 17 | } 18 | 19 | return true, "" 20 | } 21 | -------------------------------------------------------------------------------- /testdata/wait.txt: -------------------------------------------------------------------------------- 1 | [!exec:echo] skip 2 | [!exec:false] skip 3 | 4 | exec echo foo 5 | stdout foo 6 | 7 | exec echo foo & 8 | exec echo bar & 9 | ! exec false & 10 | 11 | # Starting a background process should clear previous output. 12 | ! stdout foo 13 | 14 | # Wait should set the output to the concatenated outputs of the background 15 | # programs, in the order in which they were started. 16 | wait 17 | stdout 'foo\nbar' 18 | 19 | # The end of the test should interrupt or kill any remaining background 20 | # programs. 21 | [!exec:sleep] skip 22 | ! exec sleep 86400 & 23 | -------------------------------------------------------------------------------- /testdata/exec_path_change.txt: -------------------------------------------------------------------------------- 1 | # If the PATH environment variable is set in the testscript.Params.Setup phase 2 | # or set directly within a script, exec should honour that PATH 3 | 4 | [!windows] env HOME=$WORK/home 5 | [windows] env HOME=$WORK\home 6 | [windows] env USERPROFILE=$WORK\home 7 | [windows] env LOCALAPPDATA=$WORK\appdata 8 | 9 | cd go 10 | exec go$exe version 11 | stdout 'go version' 12 | exec go$exe build 13 | [!windows] env PATH=$WORK/go${:}$PATH 14 | [windows] env PATH=$WORK\go${:}$PATH 15 | exec go$exe version 16 | stdout 'This is not go' 17 | 18 | -- go/go.mod -- 19 | module example.com/go 20 | 21 | -- go/main.go -- 22 | package main 23 | 24 | import "fmt" 25 | 26 | func main() { 27 | fmt.Println("This is not go") 28 | } 29 | -------------------------------------------------------------------------------- /internal/os/execpath/lp_js.go: -------------------------------------------------------------------------------- 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 | // +build js,wasm 6 | 7 | package execpath 8 | 9 | // Look searches for an executable named file, using getenv to look up 10 | // environment variables. If getenv is nil, os.Getenv will be used. If file 11 | // contains a slash, it is tried directly and getenv will not be called. The 12 | // result may be an absolute path or a path relative to the current directory. 13 | func Look(file string, getenv func(string) string) (string, error) { 14 | // Wasm can not execute processes, so act as if there are no executables at all. 15 | return "", &Error{file, ErrNotFound} 16 | } 17 | -------------------------------------------------------------------------------- /go.sum: -------------------------------------------------------------------------------- 1 | github.com/kr/pretty v0.1.0 h1:L/CwN0zerZDmRFUapSPitk6f+Q3+0za1rQkzVuMiMFI= 2 | github.com/kr/pretty v0.1.0/go.mod h1:dAy3ld7l9f0ibDNOQOHHMYYIIbhfbHSm3C4ZsoJORNo= 3 | github.com/kr/pty v1.1.1/go.mod h1:pFQYn66WHrOpPYNljwOMqo10TkYh1fy3cYio2l3bCsQ= 4 | github.com/kr/text v0.1.0 h1:45sCR5RtlFHMR4UwH9sdQ5TC8v0qDQCHnXt+kaKSTVE= 5 | github.com/kr/text v0.1.0/go.mod h1:4Jbv+DJW3UT/LiOwJeYQe1efqtUx/iVham/4vfdArNI= 6 | gopkg.in/check.v1 v1.0.0-20180628173108-788fd7840127 h1:qIbj1fsPNlZgppZ+VLlY7N33q108Sa+fhmuc+sWQYwY= 7 | gopkg.in/check.v1 v1.0.0-20180628173108-788fd7840127/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= 8 | gopkg.in/errgo.v2 v2.1.0 h1:0vLT13EuvQ0hNvakwLuFZ/jYrLp5F3kcWHXdRggjCE8= 9 | gopkg.in/errgo.v2 v2.1.0/go.mod h1:hNsd1EY+bozCKY1Ytp96fpM3vjJbqLJn88ws8XvfDNI= 10 | -------------------------------------------------------------------------------- /internal/testenv/testenv_windows.go: -------------------------------------------------------------------------------- 1 | // Copyright 2016 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 | package testenv 6 | 7 | import ( 8 | "io/ioutil" 9 | "os" 10 | "path/filepath" 11 | "sync" 12 | "syscall" 13 | ) 14 | 15 | var symlinkOnce sync.Once 16 | var winSymlinkErr error 17 | 18 | func initWinHasSymlink() { 19 | tmpdir, err := ioutil.TempDir("", "symtest") 20 | if err != nil { 21 | panic("failed to create temp directory: " + err.Error()) 22 | } 23 | defer os.RemoveAll(tmpdir) 24 | 25 | err = os.Symlink("target", filepath.Join(tmpdir, "symlink")) 26 | if err != nil { 27 | err = err.(*os.LinkError).Err 28 | switch err { 29 | case syscall.EWINDOWS, syscall.ERROR_PRIVILEGE_NOT_HELD: 30 | winSymlinkErr = err 31 | } 32 | } 33 | } 34 | 35 | func hasSymlink() (ok bool, reason string) { 36 | symlinkOnce.Do(initWinHasSymlink) 37 | 38 | switch winSymlinkErr { 39 | case nil: 40 | return true, "" 41 | case syscall.EWINDOWS: 42 | return false, ": symlinks are not supported on your version of Windows" 43 | case syscall.ERROR_PRIVILEGE_NOT_HELD: 44 | return false, ": you don't have enough privileges to create symlinks" 45 | } 46 | 47 | return false, "" 48 | } 49 | -------------------------------------------------------------------------------- /internal/textutil/diff_test.go: -------------------------------------------------------------------------------- 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 | package textutil_test 6 | 7 | import ( 8 | "strings" 9 | "testing" 10 | 11 | "github.com/rogpeppe/testscript/internal/textutil" 12 | ) 13 | 14 | var diffTests = []struct { 15 | text1 string 16 | text2 string 17 | diff string 18 | }{ 19 | {"a b c", "a b d e f", "a b -c +d +e +f"}, 20 | {"", "a b c", "+a +b +c"}, 21 | {"a b c", "", "-a -b -c"}, 22 | {"a b c", "d e f", "-a -b -c +d +e +f"}, 23 | {"a b c d e f", "a b d e f", "a b -c d e f"}, 24 | {"a b c e f", "a b c d e f", "a b c +d e f"}, 25 | } 26 | 27 | func TestDiff(t *testing.T) { 28 | for _, tt := range diffTests { 29 | // Turn spaces into \n. 30 | text1 := strings.Replace(tt.text1, " ", "\n", -1) 31 | if text1 != "" { 32 | text1 += "\n" 33 | } 34 | text2 := strings.Replace(tt.text2, " ", "\n", -1) 35 | if text2 != "" { 36 | text2 += "\n" 37 | } 38 | out := textutil.Diff(text1, text2) 39 | // Cut final \n, cut spaces, turn remaining \n into spaces. 40 | out = strings.Replace(strings.Replace(strings.TrimSuffix(out, "\n"), " ", "", -1), "\n", " ", -1) 41 | if out != tt.diff { 42 | t.Errorf("diff(%q, %q) = %q, want %q", text1, text2, out, tt.diff) 43 | } 44 | } 45 | } 46 | -------------------------------------------------------------------------------- /internal/os/execpath/lp_plan9.go: -------------------------------------------------------------------------------- 1 | // Copyright 2011 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 | package execpath 6 | 7 | import ( 8 | "os" 9 | "path/filepath" 10 | "strings" 11 | ) 12 | 13 | func findExecutable(file string) error { 14 | d, err := os.Stat(file) 15 | if err != nil { 16 | return err 17 | } 18 | if m := d.Mode(); !m.IsDir() && m&0111 != 0 { 19 | return nil 20 | } 21 | return os.ErrPermission 22 | } 23 | 24 | // Look searches for an executable named file, using getenv to look up 25 | // environment variables. If getenv is nil, os.Getenv will be used. If file 26 | // contains a slash, it is tried directly and getenv will not be called. The 27 | // result may be an absolute path or a path relative to the current directory. 28 | func Look(file string, getenv func(string) string) (string, error) { 29 | if getenv == nil { 30 | getenv = os.Getenv 31 | } 32 | 33 | // skip the path lookup for these prefixes 34 | skip := []string{"/", "#", "./", "../"} 35 | 36 | for _, p := range skip { 37 | if strings.HasPrefix(file, p) { 38 | err := findExecutable(file) 39 | if err == nil { 40 | return file, nil 41 | } 42 | return "", &Error{file, err} 43 | } 44 | } 45 | 46 | path := getenv("path") 47 | for _, dir := range filepath.SplitList(path) { 48 | path := filepath.Join(dir, file) 49 | if err := findExecutable(path); err == nil { 50 | return path, nil 51 | } 52 | } 53 | return "", &Error{file, ErrNotFound} 54 | } 55 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | Copyright (c) 2018 The Go Authors. All rights reserved. 2 | 3 | Redistribution and use in source and binary forms, with or without 4 | modification, are permitted provided that the following conditions are 5 | met: 6 | 7 | * Redistributions of source code must retain the above copyright 8 | notice, this list of conditions and the following disclaimer. 9 | * Redistributions in binary form must reproduce the above 10 | copyright notice, this list of conditions and the following disclaimer 11 | in the documentation and/or other materials provided with the 12 | distribution. 13 | * Neither the name of Google Inc. nor the names of its 14 | contributors may be used to endorse or promote products derived from 15 | this software without specific prior written permission. 16 | 17 | THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS 18 | "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT 19 | LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR 20 | A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT 21 | OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, 22 | SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT 23 | LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, 24 | DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY 25 | THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT 26 | (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE 27 | OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. 28 | -------------------------------------------------------------------------------- /internal/os/execpath/lp_unix.go: -------------------------------------------------------------------------------- 1 | // Copyright 2010 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 | // +build aix darwin dragonfly freebsd linux nacl netbsd openbsd solaris 6 | 7 | package execpath 8 | 9 | import ( 10 | "os" 11 | "path/filepath" 12 | "strings" 13 | ) 14 | 15 | func findExecutable(file string) error { 16 | d, err := os.Stat(file) 17 | if err != nil { 18 | return err 19 | } 20 | if m := d.Mode(); !m.IsDir() && m&0111 != 0 { 21 | return nil 22 | } 23 | return os.ErrPermission 24 | } 25 | 26 | // Look searches for an executable named file, using getenv to look up 27 | // environment variables. If getenv is nil, os.Getenv will be used. If file 28 | // contains a slash, it is tried directly and getenv will not be called. The 29 | // result may be an absolute path or a path relative to the current directory. 30 | func Look(file string, getenv func(string) string) (string, error) { 31 | if getenv == nil { 32 | getenv = os.Getenv 33 | } 34 | 35 | // NOTE(rsc): I wish we could use the Plan 9 behavior here 36 | // (only bypass the path if file begins with / or ./ or ../) 37 | // but that would not match all the Unix shells. 38 | 39 | if strings.Contains(file, "/") { 40 | err := findExecutable(file) 41 | if err == nil { 42 | return file, nil 43 | } 44 | return "", &Error{file, err} 45 | } 46 | path := getenv("PATH") 47 | for _, dir := range filepath.SplitList(path) { 48 | if dir == "" { 49 | // Unix shell semantics: path element "" means "." 50 | dir = "." 51 | } 52 | path := filepath.Join(dir, file) 53 | if err := findExecutable(path); err == nil { 54 | return path, nil 55 | } 56 | } 57 | return "", &Error{file, ErrNotFound} 58 | } 59 | -------------------------------------------------------------------------------- /internal/par/work_test.go: -------------------------------------------------------------------------------- 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 | package par 6 | 7 | import ( 8 | "sync/atomic" 9 | "testing" 10 | "time" 11 | ) 12 | 13 | func TestWork(t *testing.T) { 14 | var w Work 15 | 16 | const N = 10000 17 | n := int32(0) 18 | w.Add(N) 19 | w.Do(100, func(x interface{}) { 20 | atomic.AddInt32(&n, 1) 21 | i := x.(int) 22 | if i >= 2 { 23 | w.Add(i - 1) 24 | w.Add(i - 2) 25 | } 26 | w.Add(i >> 1) 27 | w.Add((i >> 1) ^ 1) 28 | }) 29 | if n != N+1 { 30 | t.Fatalf("ran %d items, expected %d", n, N+1) 31 | } 32 | } 33 | 34 | func TestWorkParallel(t *testing.T) { 35 | for tries := 0; tries < 10; tries++ { 36 | var w Work 37 | const N = 100 38 | for i := 0; i < N; i++ { 39 | w.Add(i) 40 | } 41 | start := time.Now() 42 | var n int32 43 | w.Do(N, func(x interface{}) { 44 | time.Sleep(1 * time.Millisecond) 45 | atomic.AddInt32(&n, +1) 46 | }) 47 | if n != N { 48 | t.Fatalf("par.Work.Do did not do all the work") 49 | } 50 | if time.Since(start) < N/2*time.Millisecond { 51 | return 52 | } 53 | } 54 | t.Fatalf("par.Work.Do does not seem to be parallel") 55 | } 56 | 57 | func TestCache(t *testing.T) { 58 | var cache Cache 59 | 60 | n := 1 61 | v := cache.Do(1, func() interface{} { n++; return n }) 62 | if v != 2 { 63 | t.Fatalf("cache.Do(1) did not run f") 64 | } 65 | v = cache.Do(1, func() interface{} { n++; return n }) 66 | if v != 2 { 67 | t.Fatalf("cache.Do(1) ran f again!") 68 | } 69 | v = cache.Do(2, func() interface{} { n++; return n }) 70 | if v != 3 { 71 | t.Fatalf("cache.Do(2) did not run f") 72 | } 73 | v = cache.Do(1, func() interface{} { n++; return n }) 74 | if v != 2 { 75 | t.Fatalf("cache.Do(1) did not returned saved value from original cache.Do(1)") 76 | } 77 | } 78 | -------------------------------------------------------------------------------- /internal/imports/scan_test.go: -------------------------------------------------------------------------------- 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 | package imports 6 | 7 | import ( 8 | "path/filepath" 9 | "reflect" 10 | "runtime" 11 | "testing" 12 | 13 | "github.com/rogpeppe/testscript/internal/testenv" 14 | ) 15 | 16 | func TestScan(t *testing.T) { 17 | testenv.MustHaveGoBuild(t) 18 | 19 | imports, testImports, err := ScanDir(filepath.Join(runtime.GOROOT(), "src/encoding/json"), nil) 20 | if err != nil { 21 | t.Fatal(err) 22 | } 23 | foundBase64 := false 24 | for _, p := range imports { 25 | if p == "encoding/base64" { 26 | foundBase64 = true 27 | } 28 | if p == "encoding/binary" { 29 | // A dependency but not an import 30 | t.Errorf("json reported as importing encoding/binary but does not") 31 | } 32 | if p == "net/http" { 33 | // A test import but not an import 34 | t.Errorf("json reported as importing encoding/binary but does not") 35 | } 36 | } 37 | if !foundBase64 { 38 | t.Errorf("json missing import encoding/base64 (%q)", imports) 39 | } 40 | 41 | foundHTTP := false 42 | for _, p := range testImports { 43 | if p == "net/http" { 44 | foundHTTP = true 45 | } 46 | if p == "unicode/utf16" { 47 | // A package import but not a test import 48 | t.Errorf("json reported as test-importing unicode/utf16 but does not") 49 | } 50 | } 51 | if !foundHTTP { 52 | t.Errorf("json missing test import net/http (%q)", testImports) 53 | } 54 | } 55 | 56 | func TestScanStar(t *testing.T) { 57 | testenv.MustHaveGoBuild(t) 58 | 59 | imports, _, err := ScanDir("testdata/import1", map[string]bool{"*": true}) 60 | if err != nil { 61 | t.Fatal(err) 62 | } 63 | 64 | want := []string{"import1", "import2", "import3", "import4"} 65 | if !reflect.DeepEqual(imports, want) { 66 | t.Errorf("ScanDir testdata/import1:\nhave %v\nwant %v", imports, want) 67 | } 68 | } 69 | -------------------------------------------------------------------------------- /internal/os/execpath/lp_windows.go: -------------------------------------------------------------------------------- 1 | // Copyright 2010 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 | package execpath 6 | 7 | import ( 8 | "os" 9 | "path/filepath" 10 | "strings" 11 | ) 12 | 13 | func chkStat(file string) error { 14 | d, err := os.Stat(file) 15 | if err != nil { 16 | return err 17 | } 18 | if d.IsDir() { 19 | return os.ErrPermission 20 | } 21 | return nil 22 | } 23 | 24 | func hasExt(file string) bool { 25 | i := strings.LastIndex(file, ".") 26 | if i < 0 { 27 | return false 28 | } 29 | return strings.LastIndexAny(file, `:\/`) < i 30 | } 31 | 32 | func findExecutable(file string, exts []string) (string, error) { 33 | if len(exts) == 0 { 34 | return file, chkStat(file) 35 | } 36 | if hasExt(file) { 37 | if chkStat(file) == nil { 38 | return file, nil 39 | } 40 | } 41 | for _, e := range exts { 42 | if f := file + e; chkStat(f) == nil { 43 | return f, nil 44 | } 45 | } 46 | return "", os.ErrNotExist 47 | } 48 | 49 | // Look searches for an executable named file, using getenv to look up 50 | // environment variables. If getenv is nil, os.Getenv will be used. If file 51 | // contains a slash, it is tried directly and getenv will not be called. The 52 | // result may be an absolute path or a path relative to the current directory. 53 | // Look also uses PATHEXT environment variable to match 54 | // a suitable candidate. 55 | func Look(file string, getenv func(string) string) (string, error) { 56 | if getenv == nil { 57 | getenv = os.Getenv 58 | } 59 | var exts []string 60 | x := getenv(`PATHEXT`) 61 | if x != "" { 62 | for _, e := range strings.Split(strings.ToLower(x), `;`) { 63 | if e == "" { 64 | continue 65 | } 66 | if e[0] != '.' { 67 | e = "." + e 68 | } 69 | exts = append(exts, e) 70 | } 71 | } else { 72 | exts = []string{".com", ".exe", ".bat", ".cmd"} 73 | } 74 | 75 | if strings.ContainsAny(file, `:\/`) { 76 | if f, err := findExecutable(file, exts); err == nil { 77 | return f, nil 78 | } else { 79 | return "", &Error{file, err} 80 | } 81 | } 82 | if f, err := findExecutable(filepath.Join(".", file), exts); err == nil { 83 | return f, nil 84 | } 85 | path := getenv("path") 86 | for _, dir := range filepath.SplitList(path) { 87 | if f, err := findExecutable(filepath.Join(dir, file), exts); err == nil { 88 | return f, nil 89 | } 90 | } 91 | return "", &Error{file, ErrNotFound} 92 | } 93 | -------------------------------------------------------------------------------- /internal/textutil/diff.go: -------------------------------------------------------------------------------- 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 | package textutil 6 | 7 | import ( 8 | "fmt" 9 | "strings" 10 | ) 11 | 12 | // Diff returns a formatted diff of the two texts, 13 | // showing the entire text and the minimum line-level 14 | // additions and removals to turn text1 into text2. 15 | // (That is, lines only in text1 appear with a leading -, 16 | // and lines only in text2 appear with a leading +.) 17 | func Diff(text1, text2 string) string { 18 | if text1 != "" && !strings.HasSuffix(text1, "\n") { 19 | text1 += "(missing final newline)" 20 | } 21 | lines1 := strings.Split(text1, "\n") 22 | lines1 = lines1[:len(lines1)-1] // remove empty string after final line 23 | if text2 != "" && !strings.HasSuffix(text2, "\n") { 24 | text2 += "(missing final newline)" 25 | } 26 | lines2 := strings.Split(text2, "\n") 27 | lines2 = lines2[:len(lines2)-1] // remove empty string after final line 28 | 29 | // Naive dynamic programming algorithm for edit distance. 30 | // https://en.wikipedia.org/wiki/Wagner–Fischer_algorithm 31 | // dist[i][j] = edit distance between lines1[:len(lines1)-i] and lines2[:len(lines2)-j] 32 | // (The reversed indices make following the minimum cost path 33 | // visit lines in the same order as in the text.) 34 | dist := make([][]int, len(lines1)+1) 35 | for i := range dist { 36 | dist[i] = make([]int, len(lines2)+1) 37 | if i == 0 { 38 | for j := range dist[0] { 39 | dist[0][j] = j 40 | } 41 | continue 42 | } 43 | for j := range dist[i] { 44 | if j == 0 { 45 | dist[i][0] = i 46 | continue 47 | } 48 | cost := dist[i][j-1] + 1 49 | if cost > dist[i-1][j]+1 { 50 | cost = dist[i-1][j] + 1 51 | } 52 | if lines1[len(lines1)-i] == lines2[len(lines2)-j] { 53 | if cost > dist[i-1][j-1] { 54 | cost = dist[i-1][j-1] 55 | } 56 | } 57 | dist[i][j] = cost 58 | } 59 | } 60 | 61 | var buf strings.Builder 62 | i, j := len(lines1), len(lines2) 63 | for i > 0 || j > 0 { 64 | cost := dist[i][j] 65 | if i > 0 && j > 0 && cost == dist[i-1][j-1] && lines1[len(lines1)-i] == lines2[len(lines2)-j] { 66 | fmt.Fprintf(&buf, " %s\n", lines1[len(lines1)-i]) 67 | i-- 68 | j-- 69 | } else if i > 0 && cost == dist[i-1][j]+1 { 70 | fmt.Fprintf(&buf, "-%s\n", lines1[len(lines1)-i]) 71 | i-- 72 | } else { 73 | fmt.Fprintf(&buf, "+%s\n", lines2[len(lines2)-j]) 74 | j-- 75 | } 76 | } 77 | return buf.String() 78 | } 79 | -------------------------------------------------------------------------------- /internal/imports/scan.go: -------------------------------------------------------------------------------- 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 | package imports 6 | 7 | import ( 8 | "fmt" 9 | "io/ioutil" 10 | "os" 11 | "path/filepath" 12 | "sort" 13 | "strconv" 14 | "strings" 15 | ) 16 | 17 | func ScanDir(dir string, tags map[string]bool) ([]string, []string, error) { 18 | infos, err := ioutil.ReadDir(dir) 19 | if err != nil { 20 | return nil, nil, err 21 | } 22 | var files []string 23 | for _, info := range infos { 24 | name := info.Name() 25 | if info.Mode().IsRegular() && !strings.HasPrefix(name, "_") && strings.HasSuffix(name, ".go") && MatchFile(name, tags) { 26 | files = append(files, filepath.Join(dir, name)) 27 | } 28 | } 29 | return scanFiles(files, tags, false) 30 | } 31 | 32 | func ScanFiles(files []string, tags map[string]bool) ([]string, []string, error) { 33 | return scanFiles(files, tags, true) 34 | } 35 | 36 | func scanFiles(files []string, tags map[string]bool, explicitFiles bool) ([]string, []string, error) { 37 | imports := make(map[string]bool) 38 | testImports := make(map[string]bool) 39 | numFiles := 0 40 | Files: 41 | for _, name := range files { 42 | r, err := os.Open(name) 43 | if err != nil { 44 | return nil, nil, err 45 | } 46 | var list []string 47 | data, err := ReadImports(r, false, &list) 48 | r.Close() 49 | if err != nil { 50 | return nil, nil, fmt.Errorf("reading %s: %v", name, err) 51 | } 52 | 53 | // import "C" is implicit requirement of cgo tag. 54 | // When listing files on the command line (explicitFiles=true) 55 | // we do not apply build tag filtering but we still do apply 56 | // cgo filtering, so no explicitFiles check here. 57 | // Why? Because we always have, and it's not worth breaking 58 | // that behavior now. 59 | for _, path := range list { 60 | if path == `"C"` && !tags["cgo"] && !tags["*"] { 61 | continue Files 62 | } 63 | } 64 | 65 | if !explicitFiles && !ShouldBuild(data, tags) { 66 | continue 67 | } 68 | numFiles++ 69 | m := imports 70 | if strings.HasSuffix(name, "_test.go") { 71 | m = testImports 72 | } 73 | for _, p := range list { 74 | q, err := strconv.Unquote(p) 75 | if err != nil { 76 | continue 77 | } 78 | m[q] = true 79 | } 80 | } 81 | if numFiles == 0 { 82 | return nil, nil, ErrNoGo 83 | } 84 | return keys(imports), keys(testImports), nil 85 | } 86 | 87 | var ErrNoGo = fmt.Errorf("no Go source files") 88 | 89 | func keys(m map[string]bool) []string { 90 | var list []string 91 | for k := range m { 92 | list = append(list, k) 93 | } 94 | sort.Strings(list) 95 | return list 96 | } 97 | -------------------------------------------------------------------------------- /internal/par/work.go: -------------------------------------------------------------------------------- 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 | // Package par implements parallel execution helpers. 6 | package par 7 | 8 | import ( 9 | "math/rand" 10 | "sync" 11 | "sync/atomic" 12 | ) 13 | 14 | // Work manages a set of work items to be executed in parallel, at most once each. 15 | // The items in the set must all be valid map keys. 16 | type Work struct { 17 | f func(interface{}) // function to run for each item 18 | running int // total number of runners 19 | 20 | mu sync.Mutex 21 | added map[interface{}]bool // items added to set 22 | todo []interface{} // items yet to be run 23 | wait sync.Cond // wait when todo is empty 24 | waiting int // number of runners waiting for todo 25 | } 26 | 27 | func (w *Work) init() { 28 | if w.added == nil { 29 | w.added = make(map[interface{}]bool) 30 | } 31 | } 32 | 33 | // Add adds item to the work set, if it hasn't already been added. 34 | func (w *Work) Add(item interface{}) { 35 | w.mu.Lock() 36 | w.init() 37 | if !w.added[item] { 38 | w.added[item] = true 39 | w.todo = append(w.todo, item) 40 | if w.waiting > 0 { 41 | w.wait.Signal() 42 | } 43 | } 44 | w.mu.Unlock() 45 | } 46 | 47 | // Do runs f in parallel on items from the work set, 48 | // with at most n invocations of f running at a time. 49 | // It returns when everything added to the work set has been processed. 50 | // At least one item should have been added to the work set 51 | // before calling Do (or else Do returns immediately), 52 | // but it is allowed for f(item) to add new items to the set. 53 | // Do should only be used once on a given Work. 54 | func (w *Work) Do(n int, f func(item interface{})) { 55 | if n < 1 { 56 | panic("par.Work.Do: n < 1") 57 | } 58 | if w.running >= 1 { 59 | panic("par.Work.Do: already called Do") 60 | } 61 | 62 | w.running = n 63 | w.f = f 64 | w.wait.L = &w.mu 65 | 66 | for i := 0; i < n-1; i++ { 67 | go w.runner() 68 | } 69 | w.runner() 70 | } 71 | 72 | // runner executes work in w until both nothing is left to do 73 | // and all the runners are waiting for work. 74 | // (Then all the runners return.) 75 | func (w *Work) runner() { 76 | for { 77 | // Wait for something to do. 78 | w.mu.Lock() 79 | for len(w.todo) == 0 { 80 | w.waiting++ 81 | if w.waiting == w.running { 82 | // All done. 83 | w.wait.Broadcast() 84 | w.mu.Unlock() 85 | return 86 | } 87 | w.wait.Wait() 88 | w.waiting-- 89 | } 90 | 91 | // Pick something to do at random, 92 | // to eliminate pathological contention 93 | // in case items added at about the same time 94 | // are most likely to contend. 95 | i := rand.Intn(len(w.todo)) 96 | item := w.todo[i] 97 | w.todo[i] = w.todo[len(w.todo)-1] 98 | w.todo = w.todo[:len(w.todo)-1] 99 | w.mu.Unlock() 100 | 101 | w.f(item) 102 | } 103 | } 104 | 105 | // Cache runs an action once per key and caches the result. 106 | type Cache struct { 107 | m sync.Map 108 | } 109 | 110 | type cacheEntry struct { 111 | done uint32 112 | mu sync.Mutex 113 | result interface{} 114 | } 115 | 116 | // Do calls the function f if and only if Do is being called for the first time with this key. 117 | // No call to Do with a given key returns until the one call to f returns. 118 | // Do returns the value returned by the one call to f. 119 | func (c *Cache) Do(key interface{}, f func() interface{}) interface{} { 120 | entryIface, ok := c.m.Load(key) 121 | if !ok { 122 | entryIface, _ = c.m.LoadOrStore(key, new(cacheEntry)) 123 | } 124 | e := entryIface.(*cacheEntry) 125 | if atomic.LoadUint32(&e.done) == 0 { 126 | e.mu.Lock() 127 | if atomic.LoadUint32(&e.done) == 0 { 128 | e.result = f() 129 | atomic.StoreUint32(&e.done, 1) 130 | } 131 | e.mu.Unlock() 132 | } 133 | return e.result 134 | } 135 | 136 | // Get returns the cached result associated with key. 137 | // It returns nil if there is no such result. 138 | // If the result for key is being computed, Get does not wait for the computation to finish. 139 | func (c *Cache) Get(key interface{}) interface{} { 140 | entryIface, ok := c.m.Load(key) 141 | if !ok { 142 | return nil 143 | } 144 | e := entryIface.(*cacheEntry) 145 | if atomic.LoadUint32(&e.done) == 0 { 146 | return nil 147 | } 148 | return e.result 149 | } 150 | -------------------------------------------------------------------------------- /internal/imports/read_test.go: -------------------------------------------------------------------------------- 1 | // Copyright 2012 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 | // Copied from Go distribution src/go/build/read.go. 6 | 7 | package imports 8 | 9 | import ( 10 | "io" 11 | "strings" 12 | "testing" 13 | ) 14 | 15 | const quote = "`" 16 | 17 | type readTest struct { 18 | // Test input contains ℙ where readImports should stop. 19 | in string 20 | err string 21 | } 22 | 23 | var readImportsTests = []readTest{ 24 | { 25 | `package p`, 26 | "", 27 | }, 28 | { 29 | `package p; import "x"`, 30 | "", 31 | }, 32 | { 33 | `package p; import . "x"`, 34 | "", 35 | }, 36 | { 37 | `package p; import "x";ℙvar x = 1`, 38 | "", 39 | }, 40 | { 41 | `package p 42 | 43 | // comment 44 | 45 | import "x" 46 | import _ "x" 47 | import a "x" 48 | 49 | /* comment */ 50 | 51 | import ( 52 | "x" /* comment */ 53 | _ "x" 54 | a "x" // comment 55 | ` + quote + `x` + quote + ` 56 | _ /*comment*/ ` + quote + `x` + quote + ` 57 | a ` + quote + `x` + quote + ` 58 | ) 59 | import ( 60 | ) 61 | import () 62 | import()import()import() 63 | import();import();import() 64 | 65 | ℙvar x = 1 66 | `, 67 | "", 68 | }, 69 | } 70 | 71 | var readCommentsTests = []readTest{ 72 | { 73 | `ℙpackage p`, 74 | "", 75 | }, 76 | { 77 | `ℙpackage p; import "x"`, 78 | "", 79 | }, 80 | { 81 | `ℙpackage p; import . "x"`, 82 | "", 83 | }, 84 | { 85 | `// foo 86 | 87 | /* bar */ 88 | 89 | /* quux */ // baz 90 | 91 | /*/ zot */ 92 | 93 | // asdf 94 | ℙHello, world`, 95 | "", 96 | }, 97 | } 98 | 99 | func testRead(t *testing.T, tests []readTest, read func(io.Reader) ([]byte, error)) { 100 | for i, tt := range tests { 101 | var in, testOut string 102 | j := strings.Index(tt.in, "ℙ") 103 | if j < 0 { 104 | in = tt.in 105 | testOut = tt.in 106 | } else { 107 | in = tt.in[:j] + tt.in[j+len("ℙ"):] 108 | testOut = tt.in[:j] 109 | } 110 | r := strings.NewReader(in) 111 | buf, err := read(r) 112 | if err != nil { 113 | if tt.err == "" { 114 | t.Errorf("#%d: err=%q, expected success (%q)", i, err, string(buf)) 115 | continue 116 | } 117 | if !strings.Contains(err.Error(), tt.err) { 118 | t.Errorf("#%d: err=%q, expected %q", i, err, tt.err) 119 | continue 120 | } 121 | continue 122 | } 123 | if err == nil && tt.err != "" { 124 | t.Errorf("#%d: success, expected %q", i, tt.err) 125 | continue 126 | } 127 | 128 | out := string(buf) 129 | if out != testOut { 130 | t.Errorf("#%d: wrong output:\nhave %q\nwant %q\n", i, out, testOut) 131 | } 132 | } 133 | } 134 | 135 | func TestReadImports(t *testing.T) { 136 | testRead(t, readImportsTests, func(r io.Reader) ([]byte, error) { return ReadImports(r, true, nil) }) 137 | } 138 | 139 | func TestReadComments(t *testing.T) { 140 | testRead(t, readCommentsTests, ReadComments) 141 | } 142 | 143 | var readFailuresTests = []readTest{ 144 | { 145 | `package`, 146 | "syntax error", 147 | }, 148 | { 149 | "package p\n\x00\nimport `math`\n", 150 | "unexpected NUL in input", 151 | }, 152 | { 153 | `package p; import`, 154 | "syntax error", 155 | }, 156 | { 157 | `package p; import "`, 158 | "syntax error", 159 | }, 160 | { 161 | "package p; import ` \n\n", 162 | "syntax error", 163 | }, 164 | { 165 | `package p; import "x`, 166 | "syntax error", 167 | }, 168 | { 169 | `package p; import _`, 170 | "syntax error", 171 | }, 172 | { 173 | `package p; import _ "`, 174 | "syntax error", 175 | }, 176 | { 177 | `package p; import _ "x`, 178 | "syntax error", 179 | }, 180 | { 181 | `package p; import .`, 182 | "syntax error", 183 | }, 184 | { 185 | `package p; import . "`, 186 | "syntax error", 187 | }, 188 | { 189 | `package p; import . "x`, 190 | "syntax error", 191 | }, 192 | { 193 | `package p; import (`, 194 | "syntax error", 195 | }, 196 | { 197 | `package p; import ("`, 198 | "syntax error", 199 | }, 200 | { 201 | `package p; import ("x`, 202 | "syntax error", 203 | }, 204 | { 205 | `package p; import ("x"`, 206 | "syntax error", 207 | }, 208 | } 209 | 210 | func TestReadFailures(t *testing.T) { 211 | // Errors should be reported (true arg to readImports). 212 | testRead(t, readFailuresTests, func(r io.Reader) ([]byte, error) { return ReadImports(r, true, nil) }) 213 | } 214 | 215 | func TestReadFailuresIgnored(t *testing.T) { 216 | // Syntax errors should not be reported (false arg to readImports). 217 | // Instead, entire file should be the output and no error. 218 | // Convert tests not to return syntax errors. 219 | tests := make([]readTest, len(readFailuresTests)) 220 | copy(tests, readFailuresTests) 221 | for i := range tests { 222 | tt := &tests[i] 223 | if !strings.Contains(tt.err, "NUL") { 224 | tt.err = "" 225 | } 226 | } 227 | testRead(t, tests, func(r io.Reader) ([]byte, error) { return ReadImports(r, false, nil) }) 228 | } 229 | -------------------------------------------------------------------------------- /txtar/archive_test.go: -------------------------------------------------------------------------------- 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 | package txtar 6 | 7 | import ( 8 | "bytes" 9 | "fmt" 10 | "io/ioutil" 11 | "os" 12 | "reflect" 13 | "testing" 14 | ) 15 | 16 | var tests = []struct { 17 | name string 18 | text string 19 | parsed *Archive 20 | }{ 21 | // General test 22 | { 23 | name: "basic", 24 | text: `comment1 25 | comment2 26 | -- file1 -- 27 | File 1 text. 28 | -- foo --- 29 | More file 1 text. 30 | -- file 2 -- 31 | File 2 text. 32 | -- empty -- 33 | -- noNL -- 34 | hello world`, 35 | parsed: &Archive{ 36 | Comment: []byte("comment1\ncomment2\n"), 37 | Files: []File{ 38 | {"file1", []byte("File 1 text.\n-- foo ---\nMore file 1 text.\n")}, 39 | {"file 2", []byte("File 2 text.\n")}, 40 | {"empty", []byte{}}, 41 | {"noNL", []byte("hello world\n")}, 42 | }, 43 | }, 44 | }, 45 | // Test CRLF input 46 | { 47 | name: "basic", 48 | text: "blah\r\n-- hello --\r\nhello\r\n", 49 | parsed: &Archive{ 50 | Comment: []byte("blah\r\n"), 51 | Files: []File{ 52 | {"hello", []byte("hello\r\n")}, 53 | }, 54 | }, 55 | }, 56 | } 57 | 58 | func Test(t *testing.T) { 59 | for _, tt := range tests { 60 | t.Run(tt.name, func(t *testing.T) { 61 | a := Parse([]byte(tt.text)) 62 | if !reflect.DeepEqual(a, tt.parsed) { 63 | t.Fatalf("Parse: wrong output:\nhave:\n%s\nwant:\n%s", shortArchive(a), shortArchive(tt.parsed)) 64 | } 65 | text := Format(a) 66 | a = Parse(text) 67 | if !reflect.DeepEqual(a, tt.parsed) { 68 | t.Fatalf("Parse after Format: wrong output:\nhave:\n%s\nwant:\n%s", shortArchive(a), shortArchive(tt.parsed)) 69 | } 70 | }) 71 | } 72 | } 73 | 74 | func shortArchive(a *Archive) string { 75 | var buf bytes.Buffer 76 | fmt.Fprintf(&buf, "comment: %q\n", a.Comment) 77 | for _, f := range a.Files { 78 | fmt.Fprintf(&buf, "file %q: %q\n", f.Name, f.Data) 79 | } 80 | return buf.String() 81 | } 82 | 83 | func TestWrite(t *testing.T) { 84 | td, err := ioutil.TempDir("", "") 85 | if err != nil { 86 | t.Fatalf("failed to create temp dir: %v", err) 87 | } 88 | defer os.RemoveAll(td) 89 | 90 | good := &Archive{Files: []File{File{Name: "good.txt"}}} 91 | if err := Write(good, td); err != nil { 92 | t.Fatalf("expected no error; got %v", err) 93 | } 94 | 95 | badRel := &Archive{Files: []File{File{Name: "../bad.txt"}}} 96 | want := `"../bad.txt": outside parent directory` 97 | if err := Write(badRel, td); err == nil || err.Error() != want { 98 | t.Fatalf("expected %v; got %v", want, err) 99 | } 100 | 101 | badAbs := &Archive{Files: []File{File{Name: "/bad.txt"}}} 102 | want = `"/bad.txt": outside parent directory` 103 | if err := Write(badAbs, td); err == nil || err.Error() != want { 104 | t.Fatalf("expected %v; got %v", want, err) 105 | } 106 | } 107 | 108 | var unquoteErrorTests = []struct { 109 | testName string 110 | data string 111 | expectError string 112 | }{{ 113 | testName: "no final newline", 114 | data: ">hello", 115 | expectError: `data does not appear to be quoted`, 116 | }, { 117 | testName: "no initial >", 118 | data: "hello\n", 119 | expectError: `data does not appear to be quoted`, 120 | }} 121 | 122 | func TestUnquote(t *testing.T) { 123 | for _, test := range unquoteErrorTests { 124 | t.Run(test.testName, func(t *testing.T) { 125 | _, err := Unquote([]byte(test.data)) 126 | if err == nil { 127 | t.Fatalf("unexpected success") 128 | } 129 | if err.Error() != test.expectError { 130 | t.Fatalf("unexpected error; got %q want %q", err, test.expectError) 131 | } 132 | }) 133 | } 134 | } 135 | 136 | var quoteTests = []struct { 137 | testName string 138 | data string 139 | expect string 140 | expectError string 141 | }{{ 142 | testName: "empty", 143 | data: "", 144 | expect: "", 145 | }, { 146 | testName: "one line", 147 | data: "foo\n", 148 | expect: ">foo\n", 149 | }, { 150 | testName: "several lines", 151 | data: "foo\nbar\n-- baz --\n", 152 | expect: ">foo\n>bar\n>-- baz --\n", 153 | }, { 154 | testName: "bad data", 155 | data: "foo\xff\n", 156 | expectError: `data contains non-UTF-8 characters`, 157 | }, { 158 | testName: "no final newline", 159 | data: "foo", 160 | expectError: `data has no final newline`, 161 | }} 162 | 163 | func TestQuote(t *testing.T) { 164 | for _, test := range quoteTests { 165 | t.Run(test.testName, func(t *testing.T) { 166 | got, err := Quote([]byte(test.data)) 167 | if test.expectError != "" { 168 | if err == nil { 169 | t.Fatalf("unexpected success") 170 | } 171 | if err.Error() != test.expectError { 172 | t.Fatalf("unexpected error; got %q want %q", err, test.expectError) 173 | } 174 | return 175 | } 176 | if err != nil { 177 | t.Fatalf("quote error: %v", err) 178 | } 179 | if string(got) != test.expect { 180 | t.Fatalf("unexpected result; got %q want %q", got, test.expect) 181 | } 182 | orig, err := Unquote(got) 183 | if err != nil { 184 | t.Fatal(err) 185 | } 186 | if string(orig) != test.data { 187 | t.Fatalf("round trip failed; got %q want %q", orig, test.data) 188 | } 189 | }) 190 | } 191 | } 192 | -------------------------------------------------------------------------------- /internal/imports/build.go: -------------------------------------------------------------------------------- 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 | // Copied from Go distribution src/go/build/build.go, syslist.go 6 | 7 | package imports 8 | 9 | import ( 10 | "bytes" 11 | "strings" 12 | "unicode" 13 | ) 14 | 15 | var slashslash = []byte("//") 16 | 17 | // ShouldBuild reports whether it is okay to use this file, 18 | // The rule is that in the file's leading run of // comments 19 | // and blank lines, which must be followed by a blank line 20 | // (to avoid including a Go package clause doc comment), 21 | // lines beginning with '// +build' are taken as build directives. 22 | // 23 | // The file is accepted only if each such line lists something 24 | // matching the file. For example: 25 | // 26 | // // +build windows linux 27 | // 28 | // marks the file as applicable only on Windows and Linux. 29 | // 30 | // If tags["*"] is true, then ShouldBuild will consider every 31 | // build tag except "ignore" to be both true and false for 32 | // the purpose of satisfying build tags, in order to estimate 33 | // (conservatively) whether a file could ever possibly be used 34 | // in any build. 35 | // 36 | func ShouldBuild(content []byte, tags map[string]bool) bool { 37 | // Pass 1. Identify leading run of // comments and blank lines, 38 | // which must be followed by a blank line. 39 | end := 0 40 | p := content 41 | for len(p) > 0 { 42 | line := p 43 | if i := bytes.IndexByte(line, '\n'); i >= 0 { 44 | line, p = line[:i], p[i+1:] 45 | } else { 46 | p = p[len(p):] 47 | } 48 | line = bytes.TrimSpace(line) 49 | if len(line) == 0 { // Blank line 50 | end = len(content) - len(p) 51 | continue 52 | } 53 | if !bytes.HasPrefix(line, slashslash) { // Not comment line 54 | break 55 | } 56 | } 57 | content = content[:end] 58 | 59 | // Pass 2. Process each line in the run. 60 | p = content 61 | allok := true 62 | for len(p) > 0 { 63 | line := p 64 | if i := bytes.IndexByte(line, '\n'); i >= 0 { 65 | line, p = line[:i], p[i+1:] 66 | } else { 67 | p = p[len(p):] 68 | } 69 | line = bytes.TrimSpace(line) 70 | if !bytes.HasPrefix(line, slashslash) { 71 | continue 72 | } 73 | line = bytes.TrimSpace(line[len(slashslash):]) 74 | if len(line) > 0 && line[0] == '+' { 75 | // Looks like a comment +line. 76 | f := strings.Fields(string(line)) 77 | if f[0] == "+build" { 78 | ok := false 79 | for _, tok := range f[1:] { 80 | if matchTags(tok, tags) { 81 | ok = true 82 | } 83 | } 84 | if !ok { 85 | allok = false 86 | } 87 | } 88 | } 89 | } 90 | 91 | return allok 92 | } 93 | 94 | // matchTags reports whether the name is one of: 95 | // 96 | // tag (if tags[tag] is true) 97 | // !tag (if tags[tag] is false) 98 | // a comma-separated list of any of these 99 | // 100 | func matchTags(name string, tags map[string]bool) bool { 101 | if name == "" { 102 | return false 103 | } 104 | if i := strings.Index(name, ","); i >= 0 { 105 | // comma-separated list 106 | ok1 := matchTags(name[:i], tags) 107 | ok2 := matchTags(name[i+1:], tags) 108 | return ok1 && ok2 109 | } 110 | if strings.HasPrefix(name, "!!") { // bad syntax, reject always 111 | return false 112 | } 113 | if strings.HasPrefix(name, "!") { // negation 114 | return len(name) > 1 && matchTag(name[1:], tags, false) 115 | } 116 | return matchTag(name, tags, true) 117 | } 118 | 119 | // matchTag reports whether the tag name is valid and satisfied by tags[name]==want. 120 | func matchTag(name string, tags map[string]bool, want bool) bool { 121 | // Tags must be letters, digits, underscores or dots. 122 | // Unlike in Go identifiers, all digits are fine (e.g., "386"). 123 | for _, c := range name { 124 | if !unicode.IsLetter(c) && !unicode.IsDigit(c) && c != '_' && c != '.' { 125 | return false 126 | } 127 | } 128 | 129 | if tags["*"] && name != "" && name != "ignore" { 130 | // Special case for gathering all possible imports: 131 | // if we put * in the tags map then all tags 132 | // except "ignore" are considered both present and not 133 | // (so we return true no matter how 'want' is set). 134 | return true 135 | } 136 | 137 | have := tags[name] 138 | if name == "linux" { 139 | have = have || tags["android"] 140 | } 141 | return have == want 142 | } 143 | 144 | // MatchFile returns false if the name contains a $GOOS or $GOARCH 145 | // suffix which does not match the current system. 146 | // The recognized name formats are: 147 | // 148 | // name_$(GOOS).* 149 | // name_$(GOARCH).* 150 | // name_$(GOOS)_$(GOARCH).* 151 | // name_$(GOOS)_test.* 152 | // name_$(GOARCH)_test.* 153 | // name_$(GOOS)_$(GOARCH)_test.* 154 | // 155 | // An exception: if GOOS=android, then files with GOOS=linux are also matched. 156 | // 157 | // If tags["*"] is true, then MatchFile will consider all possible 158 | // GOOS and GOARCH to be available and will consequently 159 | // always return true. 160 | func MatchFile(name string, tags map[string]bool) bool { 161 | if tags["*"] { 162 | return true 163 | } 164 | if dot := strings.Index(name, "."); dot != -1 { 165 | name = name[:dot] 166 | } 167 | 168 | // Before Go 1.4, a file called "linux.go" would be equivalent to having a 169 | // build tag "linux" in that file. For Go 1.4 and beyond, we require this 170 | // auto-tagging to apply only to files with a non-empty prefix, so 171 | // "foo_linux.go" is tagged but "linux.go" is not. This allows new operating 172 | // systems, such as android, to arrive without breaking existing code with 173 | // innocuous source code in "android.go". The easiest fix: cut everything 174 | // in the name before the initial _. 175 | i := strings.Index(name, "_") 176 | if i < 0 { 177 | return true 178 | } 179 | name = name[i:] // ignore everything before first _ 180 | 181 | l := strings.Split(name, "_") 182 | if n := len(l); n > 0 && l[n-1] == "test" { 183 | l = l[:n-1] 184 | } 185 | n := len(l) 186 | if n >= 2 && KnownOS[l[n-2]] && KnownArch[l[n-1]] { 187 | return tags[l[n-2]] && tags[l[n-1]] 188 | } 189 | if n >= 1 && KnownOS[l[n-1]] { 190 | return tags[l[n-1]] 191 | } 192 | if n >= 1 && KnownArch[l[n-1]] { 193 | return tags[l[n-1]] 194 | } 195 | return true 196 | } 197 | 198 | var KnownOS = make(map[string]bool) 199 | var KnownArch = make(map[string]bool) 200 | 201 | func init() { 202 | for _, v := range strings.Fields(goosList) { 203 | KnownOS[v] = true 204 | } 205 | for _, v := range strings.Fields(goarchList) { 206 | KnownArch[v] = true 207 | } 208 | } 209 | 210 | const goosList = "android darwin dragonfly freebsd js linux nacl netbsd openbsd plan9 solaris windows zos " 211 | const goarchList = "386 amd64 amd64p32 arm armbe arm64 arm64be ppc64 ppc64le mips mipsle mips64 mips64le mips64p32 mips64p32le ppc riscv riscv64 s390 s390x sparc sparc64 wasm " 212 | -------------------------------------------------------------------------------- /internal/imports/read.go: -------------------------------------------------------------------------------- 1 | // Copyright 2012 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 | // Copied from Go distribution src/go/build/read.go. 6 | 7 | package imports 8 | 9 | import ( 10 | "bufio" 11 | "errors" 12 | "io" 13 | "unicode/utf8" 14 | ) 15 | 16 | type importReader struct { 17 | b *bufio.Reader 18 | buf []byte 19 | peek byte 20 | err error 21 | eof bool 22 | nerr int 23 | } 24 | 25 | func isIdent(c byte) bool { 26 | return 'A' <= c && c <= 'Z' || 'a' <= c && c <= 'z' || '0' <= c && c <= '9' || c == '_' || c >= utf8.RuneSelf 27 | } 28 | 29 | var ( 30 | errSyntax = errors.New("syntax error") 31 | errNUL = errors.New("unexpected NUL in input") 32 | ) 33 | 34 | // syntaxError records a syntax error, but only if an I/O error has not already been recorded. 35 | func (r *importReader) syntaxError() { 36 | if r.err == nil { 37 | r.err = errSyntax 38 | } 39 | } 40 | 41 | // readByte reads the next byte from the input, saves it in buf, and returns it. 42 | // If an error occurs, readByte records the error in r.err and returns 0. 43 | func (r *importReader) readByte() byte { 44 | c, err := r.b.ReadByte() 45 | if err == nil { 46 | r.buf = append(r.buf, c) 47 | if c == 0 { 48 | err = errNUL 49 | } 50 | } 51 | if err != nil { 52 | if err == io.EOF { 53 | r.eof = true 54 | } else if r.err == nil { 55 | r.err = err 56 | } 57 | c = 0 58 | } 59 | return c 60 | } 61 | 62 | // peekByte returns the next byte from the input reader but does not advance beyond it. 63 | // If skipSpace is set, peekByte skips leading spaces and comments. 64 | func (r *importReader) peekByte(skipSpace bool) byte { 65 | if r.err != nil { 66 | if r.nerr++; r.nerr > 10000 { 67 | panic("go/build: import reader looping") 68 | } 69 | return 0 70 | } 71 | 72 | // Use r.peek as first input byte. 73 | // Don't just return r.peek here: it might have been left by peekByte(false) 74 | // and this might be peekByte(true). 75 | c := r.peek 76 | if c == 0 { 77 | c = r.readByte() 78 | } 79 | for r.err == nil && !r.eof { 80 | if skipSpace { 81 | // For the purposes of this reader, semicolons are never necessary to 82 | // understand the input and are treated as spaces. 83 | switch c { 84 | case ' ', '\f', '\t', '\r', '\n', ';': 85 | c = r.readByte() 86 | continue 87 | 88 | case '/': 89 | c = r.readByte() 90 | if c == '/' { 91 | for c != '\n' && r.err == nil && !r.eof { 92 | c = r.readByte() 93 | } 94 | } else if c == '*' { 95 | var c1 byte 96 | for (c != '*' || c1 != '/') && r.err == nil { 97 | if r.eof { 98 | r.syntaxError() 99 | } 100 | c, c1 = c1, r.readByte() 101 | } 102 | } else { 103 | r.syntaxError() 104 | } 105 | c = r.readByte() 106 | continue 107 | } 108 | } 109 | break 110 | } 111 | r.peek = c 112 | return r.peek 113 | } 114 | 115 | // nextByte is like peekByte but advances beyond the returned byte. 116 | func (r *importReader) nextByte(skipSpace bool) byte { 117 | c := r.peekByte(skipSpace) 118 | r.peek = 0 119 | return c 120 | } 121 | 122 | // readKeyword reads the given keyword from the input. 123 | // If the keyword is not present, readKeyword records a syntax error. 124 | func (r *importReader) readKeyword(kw string) { 125 | r.peekByte(true) 126 | for i := 0; i < len(kw); i++ { 127 | if r.nextByte(false) != kw[i] { 128 | r.syntaxError() 129 | return 130 | } 131 | } 132 | if isIdent(r.peekByte(false)) { 133 | r.syntaxError() 134 | } 135 | } 136 | 137 | // readIdent reads an identifier from the input. 138 | // If an identifier is not present, readIdent records a syntax error. 139 | func (r *importReader) readIdent() { 140 | c := r.peekByte(true) 141 | if !isIdent(c) { 142 | r.syntaxError() 143 | return 144 | } 145 | for isIdent(r.peekByte(false)) { 146 | r.peek = 0 147 | } 148 | } 149 | 150 | // readString reads a quoted string literal from the input. 151 | // If an identifier is not present, readString records a syntax error. 152 | func (r *importReader) readString(save *[]string) { 153 | switch r.nextByte(true) { 154 | case '`': 155 | start := len(r.buf) - 1 156 | for r.err == nil { 157 | if r.nextByte(false) == '`' { 158 | if save != nil { 159 | *save = append(*save, string(r.buf[start:])) 160 | } 161 | break 162 | } 163 | if r.eof { 164 | r.syntaxError() 165 | } 166 | } 167 | case '"': 168 | start := len(r.buf) - 1 169 | for r.err == nil { 170 | c := r.nextByte(false) 171 | if c == '"' { 172 | if save != nil { 173 | *save = append(*save, string(r.buf[start:])) 174 | } 175 | break 176 | } 177 | if r.eof || c == '\n' { 178 | r.syntaxError() 179 | } 180 | if c == '\\' { 181 | r.nextByte(false) 182 | } 183 | } 184 | default: 185 | r.syntaxError() 186 | } 187 | } 188 | 189 | // readImport reads an import clause - optional identifier followed by quoted string - 190 | // from the input. 191 | func (r *importReader) readImport(imports *[]string) { 192 | c := r.peekByte(true) 193 | if c == '.' { 194 | r.peek = 0 195 | } else if isIdent(c) { 196 | r.readIdent() 197 | } 198 | r.readString(imports) 199 | } 200 | 201 | // ReadComments is like ioutil.ReadAll, except that it only reads the leading 202 | // block of comments in the file. 203 | func ReadComments(f io.Reader) ([]byte, error) { 204 | r := &importReader{b: bufio.NewReader(f)} 205 | r.peekByte(true) 206 | if r.err == nil && !r.eof { 207 | // Didn't reach EOF, so must have found a non-space byte. Remove it. 208 | r.buf = r.buf[:len(r.buf)-1] 209 | } 210 | return r.buf, r.err 211 | } 212 | 213 | // ReadImports is like ioutil.ReadAll, except that it expects a Go file as input 214 | // and stops reading the input once the imports have completed. 215 | func ReadImports(f io.Reader, reportSyntaxError bool, imports *[]string) ([]byte, error) { 216 | r := &importReader{b: bufio.NewReader(f)} 217 | 218 | r.readKeyword("package") 219 | r.readIdent() 220 | for r.peekByte(true) == 'i' { 221 | r.readKeyword("import") 222 | if r.peekByte(true) == '(' { 223 | r.nextByte(false) 224 | for r.peekByte(true) != ')' && r.err == nil { 225 | r.readImport(imports) 226 | } 227 | r.nextByte(false) 228 | } else { 229 | r.readImport(imports) 230 | } 231 | } 232 | 233 | // If we stopped successfully before EOF, we read a byte that told us we were done. 234 | // Return all but that last byte, which would cause a syntax error if we let it through. 235 | if r.err == nil && !r.eof { 236 | return r.buf[:len(r.buf)-1], nil 237 | } 238 | 239 | // If we stopped for a syntax error, consume the whole file so that 240 | // we are sure we don't change the errors that go/parser returns. 241 | if r.err == errSyntax && !reportSyntaxError { 242 | r.err = nil 243 | for r.err == nil && !r.eof { 244 | r.readByte() 245 | } 246 | } 247 | 248 | return r.buf, r.err 249 | } 250 | -------------------------------------------------------------------------------- /exe.go: -------------------------------------------------------------------------------- 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 | package testscript 6 | 7 | import ( 8 | "flag" 9 | "fmt" 10 | "io" 11 | "log" 12 | "os" 13 | "sync/atomic" 14 | "testing" 15 | ) 16 | 17 | var profileId int32 = 0 18 | 19 | // TestingM is implemented by *testing.M. It's defined as an interface 20 | // to allow testscript to co-exist with other testing frameworks 21 | // that might also wish to call M.Run. 22 | type TestingM interface { 23 | Run() int 24 | } 25 | 26 | var ignoreMissedCoverage = false 27 | 28 | // IgnoreMissedCoverage causes any missed coverage information 29 | // (for example when a function passed to RunMain 30 | // calls os.Exit, for example) to be ignored. 31 | // This function should be called before calling RunMain. 32 | func IgnoreMissedCoverage() { 33 | ignoreMissedCoverage = true 34 | } 35 | 36 | // RunMain should be called within a TestMain function to allow 37 | // subcommands to be run in the testscript context. 38 | // 39 | // The commands map holds the set of command names, each 40 | // with an associated run function which should return the 41 | // code to pass to os.Exit. It's OK for a command function to 42 | // exit itself, but this may result in loss of coverage information. 43 | // 44 | // When Run is called, these commands will be available as 45 | // testscript commands; note that these commands behave like 46 | // commands run with the "exec" command: they set stdout 47 | // and stderr, and can be run in the background by passing "&" 48 | // as a final argument. 49 | // 50 | // This function returns an exit code to pass to os.Exit, after calling m.Run. 51 | func RunMain(m TestingM, commands map[string]func() int) (exitCode int) { 52 | goCoverProfileMerge() 53 | cmdName := os.Getenv("TESTSCRIPT_COMMAND") 54 | if cmdName == "" { 55 | defer func() { 56 | if err := finalizeCoverProfile(); err != nil { 57 | log.Printf("cannot merge cover profiles: %v", err) 58 | exitCode = 2 59 | } 60 | }() 61 | // We're not in a subcommand. 62 | for name := range commands { 63 | name := name 64 | scriptCmds[name] = func(ts *TestScript, neg bool, args []string) { 65 | path, err := os.Executable() 66 | if err != nil { 67 | ts.Fatalf("cannot determine path to test binary: %v", err) 68 | } 69 | id := atomic.AddInt32(&profileId, 1) - 1 70 | oldEnvLen := len(ts.env) 71 | cprof := coverFilename(id) 72 | ts.env = append(ts.env, 73 | "TESTSCRIPT_COMMAND="+name, 74 | "TESTSCRIPT_COVERPROFILE="+cprof, 75 | ) 76 | ts.cmdExec(neg, append([]string{path}, args...)) 77 | ts.env = ts.env[0:oldEnvLen] 78 | if cprof == "" { 79 | return 80 | } 81 | f, err := os.Open(cprof) 82 | if err != nil { 83 | if ignoreMissedCoverage { 84 | return 85 | } 86 | ts.Fatalf("command %s (args %q) failed to generate coverage information", name, args) 87 | return 88 | } 89 | coverChan <- f 90 | } 91 | } 92 | return m.Run() 93 | } 94 | mainf := commands[cmdName] 95 | if mainf == nil { 96 | log.Printf("unknown command name %q", cmdName) 97 | return 2 98 | } 99 | // The command being registered is being invoked, so run it, then exit. 100 | os.Args[0] = cmdName 101 | cprof := os.Getenv("TESTSCRIPT_COVERPROFILE") 102 | if cprof == "" { 103 | // No coverage, act as normal. 104 | return mainf() 105 | } 106 | return runCoverSubcommand(cprof, mainf) 107 | } 108 | 109 | // runCoverSubcommand runs the given function, then writes any generated 110 | // coverage information to the cprof file. 111 | // This is called inside a separately run executable. 112 | func runCoverSubcommand(cprof string, mainf func() int) (exitCode int) { 113 | // Change the error handling mode to PanicOnError 114 | // so that in the common case of calling flag.Parse in main we'll 115 | // be able to catch the panic instead of just exiting. 116 | flag.CommandLine.Init(flag.CommandLine.Name(), flag.PanicOnError) 117 | defer func() { 118 | panicErr := recover() 119 | if _, ok := panicErr.(error); ok { 120 | // The flag package will already have printed this error, assuming, 121 | // that is, that the error was created in the flag package. 122 | // TODO check the stack to be sure it was actually raised by the flag package. 123 | exitCode = 2 124 | panicErr = nil 125 | } 126 | // Set os.Args so that flag.Parse will tell testing the correct 127 | // coverprofile setting. Unfortunately this isn't sufficient because 128 | // the testing oackage explicitly avoids calling flag.Parse again 129 | // if flag.Parsed returns true, so we the coverprofile value directly 130 | // too. 131 | os.Args = []string{os.Args[0], "-test.coverprofile=" + cprof} 132 | setCoverProfile(cprof) 133 | 134 | // Suppress the chatty coverage and test report. 135 | devNull, err := os.Open(os.DevNull) 136 | if err != nil { 137 | panic(err) 138 | } 139 | os.Stdout = devNull 140 | os.Stderr = devNull 141 | 142 | // Run MainStart (recursively, but it we should be ok) with no tests 143 | // so that it writes the coverage profile. 144 | m := testing.MainStart(nopTestDeps{}, nil, nil, nil) 145 | if code := m.Run(); code != 0 && exitCode == 0 { 146 | exitCode = code 147 | } 148 | if _, err := os.Stat(cprof); err != nil { 149 | log.Printf("failed to write coverage profile %q", cprof) 150 | } 151 | if panicErr != nil { 152 | // The error didn't originate from the flag package (we know that 153 | // flag.PanicOnError causes an error value that implements error), 154 | // so carry on panicking. 155 | panic(panicErr) 156 | } 157 | }() 158 | return mainf() 159 | } 160 | 161 | func coverFilename(id int32) string { 162 | if cprof := coverProfile(); cprof != "" { 163 | return fmt.Sprintf("%s_%d", cprof, id) 164 | } 165 | return "" 166 | } 167 | 168 | func coverProfileFlag() flag.Getter { 169 | f := flag.CommandLine.Lookup("test.coverprofile") 170 | if f == nil { 171 | // We've imported testing so it definitely should be there. 172 | panic("cannot find test.coverprofile flag") 173 | } 174 | return f.Value.(flag.Getter) 175 | } 176 | 177 | func coverProfile() string { 178 | return coverProfileFlag().Get().(string) 179 | } 180 | 181 | func setCoverProfile(cprof string) { 182 | coverProfileFlag().Set(cprof) 183 | } 184 | 185 | type nopTestDeps struct{} 186 | 187 | func (nopTestDeps) MatchString(pat, str string) (result bool, err error) { 188 | return false, nil 189 | } 190 | 191 | func (nopTestDeps) StartCPUProfile(w io.Writer) error { 192 | return nil 193 | } 194 | 195 | func (nopTestDeps) StopCPUProfile() {} 196 | 197 | func (nopTestDeps) WriteProfileTo(name string, w io.Writer, debug int) error { 198 | return nil 199 | } 200 | func (nopTestDeps) ImportPath() string { 201 | return "" 202 | } 203 | func (nopTestDeps) StartTestLog(w io.Writer) {} 204 | 205 | func (nopTestDeps) StopTestLog() error { 206 | return nil 207 | } 208 | 209 | // Note: WriteHeapProfile is needed for Go 1.10 but not Go 1.11. 210 | func (nopTestDeps) WriteHeapProfile(io.Writer) error { 211 | // Not needed for Go 1.10. 212 | return nil 213 | } 214 | -------------------------------------------------------------------------------- /cover.go: -------------------------------------------------------------------------------- 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 | package testscript 6 | 7 | import ( 8 | "bufio" 9 | "fmt" 10 | "io" 11 | "log" 12 | "os" 13 | "regexp" 14 | "strconv" 15 | "strings" 16 | "sync/atomic" 17 | "testing" 18 | 19 | "gopkg.in/errgo.v2/fmt/errors" 20 | ) 21 | 22 | // mergeCoverProfile merges the coverage information in f into 23 | // cover. It assumes that the coverage information in f is 24 | // always produced from the same binary for every call. 25 | func mergeCoverProfile(cover *testing.Cover, r io.Reader) error { 26 | scanner, err := newProfileScanner(r) 27 | if err != nil { 28 | return errors.Wrap(err) 29 | } 30 | if scanner.Mode() != testing.CoverMode() { 31 | return errors.Newf("unexpected coverage mode in subcommand") 32 | } 33 | if cover.Mode == "" { 34 | cover.Mode = scanner.Mode() 35 | } 36 | isCount := cover.Mode == "count" 37 | if cover.Counters == nil { 38 | cover.Counters = make(map[string][]uint32) 39 | cover.Blocks = make(map[string][]testing.CoverBlock) 40 | } 41 | 42 | // Note that we rely on the fact that the coverage is written 43 | // out file-by-file, with all blocks for a file in sequence. 44 | var ( 45 | filename string 46 | blockId uint32 47 | counters []uint32 48 | blocks []testing.CoverBlock 49 | ) 50 | flush := func() { 51 | if len(counters) > 0 { 52 | cover.Counters[filename] = counters 53 | cover.Blocks[filename] = blocks 54 | } 55 | } 56 | for scanner.Scan() { 57 | block := scanner.Block() 58 | if scanner.Filename() != filename { 59 | flush() 60 | filename = scanner.Filename() 61 | counters = cover.Counters[filename] 62 | blocks = cover.Blocks[filename] 63 | blockId = 0 64 | } else { 65 | blockId++ 66 | } 67 | if int(blockId) >= len(counters) { 68 | counters = append(counters, block.Count) 69 | blocks = append(blocks, block.CoverBlock) 70 | continue 71 | } 72 | // TODO check that block.CoverBlock == blocks[blockId] ? 73 | if isCount { 74 | counters[blockId] += block.Count 75 | } else { 76 | counters[blockId] |= block.Count 77 | } 78 | } 79 | flush() 80 | if scanner.Err() != nil { 81 | return errors.Notef(err, nil, "error scanning profile") 82 | } 83 | return nil 84 | } 85 | 86 | var ( 87 | coverChan chan *os.File 88 | coverDone chan testing.Cover 89 | ) 90 | 91 | func goCoverProfileMerge() { 92 | if coverChan != nil { 93 | panic("RunMain called twice!") 94 | } 95 | coverChan = make(chan *os.File) 96 | coverDone = make(chan testing.Cover) 97 | go mergeCoverProfiles() 98 | } 99 | 100 | func mergeCoverProfiles() { 101 | var cover testing.Cover 102 | for f := range coverChan { 103 | if err := mergeCoverProfile(&cover, f); err != nil { 104 | log.Printf("cannot merge coverage profile from %v: %v", f.Name(), err) 105 | } 106 | f.Close() 107 | os.Remove(f.Name()) 108 | } 109 | coverDone <- cover 110 | } 111 | 112 | func finalizeCoverProfile() error { 113 | cprof := coverProfile() 114 | if cprof == "" { 115 | return nil 116 | } 117 | f, err := os.Open(cprof) 118 | if err != nil { 119 | return errors.Notef(err, nil, "cannot open existing cover profile") 120 | } 121 | coverChan <- f 122 | close(coverChan) 123 | cover := <-coverDone 124 | f, err = os.Create(cprof) 125 | if err != nil { 126 | return errors.Notef(err, nil, "cannot create cover profile") 127 | } 128 | defer f.Close() 129 | w := bufio.NewWriter(f) 130 | if err := writeCoverProfile1(w, cover); err != nil { 131 | return errors.Wrap(err) 132 | } 133 | if err := w.Flush(); err != nil { 134 | return errors.Wrap(err) 135 | } 136 | if err := f.Close(); err != nil { 137 | return errors.Wrap(err) 138 | } 139 | return nil 140 | } 141 | 142 | func writeCoverProfile1(w io.Writer, cover testing.Cover) error { 143 | fmt.Fprintf(w, "mode: %s\n", cover.Mode) 144 | var active, total int64 145 | var count uint32 146 | for name, counts := range cover.Counters { 147 | blocks := cover.Blocks[name] 148 | for i := range counts { 149 | stmts := int64(blocks[i].Stmts) 150 | total += stmts 151 | count = atomic.LoadUint32(&counts[i]) // For -mode=atomic. 152 | if count > 0 { 153 | active += stmts 154 | } 155 | _, err := fmt.Fprintf(w, "%s:%d.%d,%d.%d %d %d\n", name, 156 | blocks[i].Line0, blocks[i].Col0, 157 | blocks[i].Line1, blocks[i].Col1, 158 | stmts, 159 | count, 160 | ) 161 | if err != nil { 162 | return errors.Wrap(err) 163 | } 164 | } 165 | } 166 | if total == 0 { 167 | total = 1 168 | } 169 | fmt.Printf("total coverage: %.1f%% of statements%s\n", 100*float64(active)/float64(total), cover.CoveredPackages) 170 | return nil 171 | } 172 | 173 | type profileScanner struct { 174 | mode string 175 | err error 176 | scanner *bufio.Scanner 177 | filename string 178 | block coverBlock 179 | } 180 | 181 | type coverBlock struct { 182 | testing.CoverBlock 183 | Count uint32 184 | } 185 | 186 | var profileLineRe = regexp.MustCompile(`^(.+):([0-9]+)\.([0-9]+),([0-9]+)\.([0-9]+) ([0-9]+) ([0-9]+)$`) 187 | 188 | func toInt(s string) int { 189 | i, err := strconv.Atoi(s) 190 | if err != nil { 191 | panic(err) 192 | } 193 | return i 194 | } 195 | 196 | func newProfileScanner(r io.Reader) (*profileScanner, error) { 197 | s := &profileScanner{ 198 | scanner: bufio.NewScanner(r), 199 | } 200 | // First line is "mode: foo", where foo is "set", "count", or "atomic". 201 | // Rest of file is in the format 202 | // encoding/base64/base64.go:34.44,37.40 3 1 203 | // where the fields are: name.go:line.column,line.column numberOfStatements count 204 | if !s.scanner.Scan() { 205 | return nil, errors.Newf("no lines found in profile: %v", s.Err()) 206 | } 207 | line := s.scanner.Text() 208 | mode := strings.TrimPrefix(line, "mode: ") 209 | if len(mode) == len(line) { 210 | return nil, fmt.Errorf("bad mode line %q", line) 211 | } 212 | s.mode = mode 213 | return s, nil 214 | } 215 | 216 | // Mode returns the profile's coverage mode (one of "atomic", "count: 217 | // or "set"). 218 | func (s *profileScanner) Mode() string { 219 | return s.mode 220 | } 221 | 222 | // Err returns any error encountered when scanning a profile. 223 | func (s *profileScanner) Err() error { 224 | if s.err == io.EOF { 225 | return nil 226 | } 227 | return s.err 228 | } 229 | 230 | // Block returns the most recently scanned profile block, or the zero 231 | // block if Scan has not been called or has returned false. 232 | func (s *profileScanner) Block() coverBlock { 233 | if s.err == nil { 234 | return s.block 235 | } 236 | return coverBlock{} 237 | } 238 | 239 | // Filename returns the filename of the most recently scanned profile 240 | // block, or the empty string if Scan has not been called or has 241 | // returned false. 242 | func (s *profileScanner) Filename() string { 243 | if s.err == nil { 244 | return s.filename 245 | } 246 | return "" 247 | } 248 | 249 | // Scan scans the next line in a coverage profile and reports whether 250 | // a line was found. 251 | func (s *profileScanner) Scan() bool { 252 | if s.err != nil { 253 | return false 254 | } 255 | if !s.scanner.Scan() { 256 | s.err = io.EOF 257 | return false 258 | } 259 | m := profileLineRe.FindStringSubmatch(s.scanner.Text()) 260 | if m == nil { 261 | s.err = errors.Newf("line %q doesn't match expected format %v", m, profileLineRe) 262 | return false 263 | } 264 | s.filename = m[1] 265 | s.block = coverBlock{ 266 | CoverBlock: testing.CoverBlock{ 267 | Line0: uint32(toInt(m[2])), 268 | Col0: uint16(toInt(m[3])), 269 | Line1: uint32(toInt(m[4])), 270 | Col1: uint16(toInt(m[5])), 271 | Stmts: uint16(toInt(m[6])), 272 | }, 273 | Count: uint32(toInt(m[7])), 274 | } 275 | return true 276 | } 277 | -------------------------------------------------------------------------------- /txtar/archive.go: -------------------------------------------------------------------------------- 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 | // Package txtar implements a trivial text-based file archive format. 6 | // 7 | // The goals for the format are: 8 | // 9 | // - be trivial enough to create and edit by hand. 10 | // - be able to store trees of text files describing go command test cases. 11 | // - diff nicely in git history and code reviews. 12 | // 13 | // Non-goals include being a completely general archive format, 14 | // storing binary data, storing file modes, storing special files like 15 | // symbolic links, and so on. 16 | // 17 | // Txtar format 18 | // 19 | // A txtar archive is zero or more comment lines and then a sequence of file entries. 20 | // Each file entry begins with a file marker line of the form "-- FILENAME --" 21 | // and is followed by zero or more file content lines making up the file data. 22 | // The comment or file content ends at the next file marker line. 23 | // The file marker line must begin with the three-byte sequence "-- " 24 | // and end with the three-byte sequence " --", but the enclosed 25 | // file name can be surrounding by additional white space, 26 | // all of which is stripped. 27 | // 28 | // If the txtar file is missing a trailing newline on the final line, 29 | // parsers should consider a final newline to be present anyway. 30 | // 31 | // There are no possible syntax errors in a txtar archive. 32 | package txtar 33 | 34 | import ( 35 | "bytes" 36 | "errors" 37 | "fmt" 38 | "io/ioutil" 39 | "os" 40 | "path/filepath" 41 | "strings" 42 | "unicode/utf8" 43 | ) 44 | 45 | // An Archive is a collection of files. 46 | type Archive struct { 47 | Comment []byte 48 | Files []File 49 | } 50 | 51 | // A File is a single file in an archive. 52 | type File struct { 53 | Name string // name of file ("foo/bar.txt") 54 | Data []byte // text content of file 55 | } 56 | 57 | // Format returns the serialized form of an Archive. 58 | // It is assumed that the Archive data structure is well-formed: 59 | // a.Comment and all a.File[i].Data contain no file marker lines, 60 | // and all a.File[i].Name is non-empty. 61 | func Format(a *Archive) []byte { 62 | var buf bytes.Buffer 63 | buf.Write(fixNL(a.Comment)) 64 | for _, f := range a.Files { 65 | fmt.Fprintf(&buf, "-- %s --\n", f.Name) 66 | buf.Write(fixNL(f.Data)) 67 | } 68 | return buf.Bytes() 69 | } 70 | 71 | // ParseFile parses the named file as an archive. 72 | func ParseFile(file string) (*Archive, error) { 73 | data, err := ioutil.ReadFile(file) 74 | if err != nil { 75 | return nil, err 76 | } 77 | return Parse(data), nil 78 | } 79 | 80 | // Parse parses the serialized form of an Archive. 81 | // The returned Archive holds slices of data. 82 | func Parse(data []byte) *Archive { 83 | a := new(Archive) 84 | var name string 85 | a.Comment, name, data = findFileMarker(data) 86 | for name != "" { 87 | f := File{name, nil} 88 | f.Data, name, data = findFileMarker(data) 89 | a.Files = append(a.Files, f) 90 | } 91 | return a 92 | } 93 | 94 | // NeedsQuote reports whether the given data needs to 95 | // be quoted before it's included as a txtar file. 96 | func NeedsQuote(data []byte) bool { 97 | _, _, after := findFileMarker(data) 98 | return after != nil 99 | } 100 | 101 | // Quote quotes the data so that it can be safely stored in a txtar 102 | // file. This copes with files that contain lines that look like txtar 103 | // separators. 104 | // 105 | // The original data can be recovered with Unquote. It returns an error 106 | // if the data cannot be quoted (for example because it has no final 107 | // newline or it holds unprintable characters) 108 | func Quote(data []byte) ([]byte, error) { 109 | if len(data) == 0 { 110 | return nil, nil 111 | } 112 | if data[len(data)-1] != '\n' { 113 | return nil, errors.New("data has no final newline") 114 | } 115 | if !utf8.Valid(data) { 116 | return nil, fmt.Errorf("data contains non-UTF-8 characters") 117 | } 118 | var nd []byte 119 | prev := byte('\n') 120 | for _, b := range data { 121 | if prev == '\n' { 122 | nd = append(nd, '>') 123 | } 124 | nd = append(nd, b) 125 | prev = b 126 | } 127 | return nd, nil 128 | } 129 | 130 | // Unquote unquotes data as quoted by Quote. 131 | func Unquote(data []byte) ([]byte, error) { 132 | if len(data) == 0 { 133 | return nil, nil 134 | } 135 | if data[0] != '>' || data[len(data)-1] != '\n' { 136 | return nil, errors.New("data does not appear to be quoted") 137 | } 138 | data = bytes.Replace(data, []byte("\n>"), []byte("\n"), -1) 139 | data = bytes.TrimPrefix(data, []byte(">")) 140 | return data, nil 141 | } 142 | 143 | var ( 144 | newlineMarker = []byte("\n-- ") 145 | marker = []byte("-- ") 146 | markerEnd = []byte(" --") 147 | ) 148 | 149 | // findFileMarker finds the next file marker in data, 150 | // extracts the file name, and returns the data before the marker, 151 | // the file name, and the data after the marker. 152 | // If there is no next marker, findFileMarker returns before = fixNL(data), name = "", after = nil. 153 | func findFileMarker(data []byte) (before []byte, name string, after []byte) { 154 | var i int 155 | for { 156 | if name, after = isMarker(data[i:]); name != "" { 157 | return data[:i], name, after 158 | } 159 | j := bytes.Index(data[i:], newlineMarker) 160 | if j < 0 { 161 | return fixNL(data), "", nil 162 | } 163 | i += j + 1 // positioned at start of new possible marker 164 | } 165 | } 166 | 167 | // isMarker checks whether data begins with a file marker line. 168 | // If so, it returns the name from the line and the data after the line. 169 | // Otherwise it returns name == "" with an unspecified after. 170 | func isMarker(data []byte) (name string, after []byte) { 171 | if !bytes.HasPrefix(data, marker) { 172 | return "", nil 173 | } 174 | if i := bytes.IndexByte(data, '\n'); i >= 0 { 175 | data, after = data[:i], data[i+1:] 176 | if data[i-1] == '\r' { 177 | data = data[:len(data)-1] 178 | } 179 | } 180 | if !bytes.HasSuffix(data, markerEnd) { 181 | return "", nil 182 | } 183 | return strings.TrimSpace(string(data[len(marker) : len(data)-len(markerEnd)])), after 184 | } 185 | 186 | // If data is empty or ends in \n, fixNL returns data. 187 | // Otherwise fixNL returns a new slice consisting of data with a final \n added. 188 | func fixNL(data []byte) []byte { 189 | if len(data) == 0 || data[len(data)-1] == '\n' { 190 | return data 191 | } 192 | d := make([]byte, len(data)+1) 193 | copy(d, data) 194 | d[len(data)] = '\n' 195 | return d 196 | } 197 | 198 | // Write writes each File in an Archive to the given directory, returning any 199 | // errors encountered. An error is also returned in the event a file would be 200 | // written outside of dir. 201 | func Write(a *Archive, dir string) error { 202 | for _, f := range a.Files { 203 | fp := filepath.Clean(filepath.FromSlash(f.Name)) 204 | if isAbs(fp) || strings.HasPrefix(fp, ".."+string(filepath.Separator)) { 205 | return fmt.Errorf("%q: outside parent directory", f.Name) 206 | } 207 | fp = filepath.Join(dir, fp) 208 | 209 | if err := os.MkdirAll(filepath.Dir(fp), 0777); err != nil { 210 | return err 211 | } 212 | // Avoid overwriting existing files by using O_EXCL. 213 | out, err := os.OpenFile(fp, os.O_WRONLY|os.O_CREATE|os.O_EXCL, 0666) 214 | if err != nil { 215 | return err 216 | } 217 | 218 | _, err = out.Write(f.Data) 219 | cerr := out.Close() 220 | if err != nil { 221 | return err 222 | } 223 | if cerr != nil { 224 | return cerr 225 | } 226 | } 227 | return nil 228 | } 229 | 230 | func isAbs(p string) bool { 231 | // Note: under Windows, filepath.IsAbs(`\foo`) returns false, 232 | // so we need to check for that case specifically. 233 | return filepath.IsAbs(p) || strings.HasPrefix(p, string(filepath.Separator)) 234 | } 235 | -------------------------------------------------------------------------------- /testscript_test.go: -------------------------------------------------------------------------------- 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 | package testscript 6 | 7 | import ( 8 | "errors" 9 | "fmt" 10 | "io/ioutil" 11 | "os" 12 | "os/exec" 13 | "os/signal" 14 | "path/filepath" 15 | "reflect" 16 | "regexp" 17 | "strconv" 18 | "strings" 19 | "testing" 20 | "time" 21 | ) 22 | 23 | func printArgs() int { 24 | fmt.Printf("%q\n", os.Args) 25 | return 0 26 | } 27 | 28 | func echo() int { 29 | s := strings.Join(os.Args[2:], " ") 30 | switch os.Args[1] { 31 | case "stdout": 32 | fmt.Println(s) 33 | case "stderr": 34 | fmt.Fprintln(os.Stderr, s) 35 | } 36 | return 0 37 | } 38 | 39 | func exitWithStatus() int { 40 | n, _ := strconv.Atoi(os.Args[1]) 41 | return n 42 | } 43 | 44 | func signalCatcher() int { 45 | // Note: won't work under Windows. 46 | c := make(chan os.Signal, 1) 47 | signal.Notify(c, os.Interrupt) 48 | // Create a file so that the test can know that 49 | // we will catch the signal. 50 | if err := ioutil.WriteFile("catchsignal", nil, 0666); err != nil { 51 | fmt.Println(err) 52 | return 1 53 | } 54 | <-c 55 | fmt.Println("caught interrupt") 56 | return 0 57 | } 58 | 59 | func TestMain(m *testing.M) { 60 | os.Exit(RunMain(m, map[string]func() int{ 61 | "printargs": printArgs, 62 | "echo": echo, 63 | "status": exitWithStatus, 64 | "signalcatcher": signalCatcher, 65 | })) 66 | } 67 | 68 | func TestCRLFInput(t *testing.T) { 69 | td, err := ioutil.TempDir("", "") 70 | if err != nil { 71 | t.Fatalf("failed to create TempDir: %v", err) 72 | } 73 | defer func() { 74 | os.RemoveAll(td) 75 | }() 76 | tf := filepath.Join(td, "script.txt") 77 | contents := []byte("exists output.txt\r\n-- output.txt --\r\noutput contents") 78 | if err := ioutil.WriteFile(tf, contents, 0644); err != nil { 79 | t.Fatalf("failed to write to %v: %v", tf, err) 80 | } 81 | t.Run("_", func(t *testing.T) { 82 | Run(t, Params{Dir: td}) 83 | }) 84 | } 85 | 86 | func TestScripts(t *testing.T) { 87 | // TODO set temp directory. 88 | testDeferCount := 0 89 | var setupFilenames []string 90 | Run(t, Params{ 91 | Dir: "testdata", 92 | Cmds: map[string]func(ts *TestScript, neg bool, args []string){ 93 | "setSpecialVal": setSpecialVal, 94 | "ensureSpecialVal": ensureSpecialVal, 95 | "interrupt": interrupt, 96 | "waitfile": waitFile, 97 | "testdefer": func(ts *TestScript, neg bool, args []string) { 98 | testDeferCount++ 99 | n := testDeferCount 100 | ts.Defer(func() { 101 | if testDeferCount != n { 102 | t.Errorf("defers not run in reverse order; got %d want %d", testDeferCount, n) 103 | } 104 | testDeferCount-- 105 | }) 106 | }, 107 | "setup-filenames": func(ts *TestScript, neg bool, args []string) { 108 | if !reflect.DeepEqual(args, setupFilenames) { 109 | ts.Fatalf("setup did not see expected files; got %q want %q", setupFilenames, args) 110 | } 111 | }, 112 | "test-values": func(ts *TestScript, neg bool, args []string) { 113 | if ts.Value("somekey") != 1234 { 114 | ts.Fatalf("test-values did not see expected value") 115 | } 116 | }, 117 | "testreadfile": func(ts *TestScript, neg bool, args []string) { 118 | if len(args) != 1 { 119 | ts.Fatalf("testreadfile ") 120 | } 121 | got := ts.ReadFile(args[0]) 122 | want := args[0] + "\n" 123 | if got != want { 124 | ts.Fatalf("reading %q; got %q want %q", args[0], got, want) 125 | } 126 | }, 127 | "testscript-update": func(ts *TestScript, neg bool, args []string) { 128 | // Run testscript in testscript. Oooh! Meta! 129 | if len(args) != 1 { 130 | ts.Fatalf("testscript ") 131 | } 132 | t := &fakeT{ts: ts} 133 | func() { 134 | defer func() { 135 | if err := recover(); err != nil { 136 | if err != errAbort { 137 | panic(err) 138 | } 139 | } 140 | }() 141 | RunT(t, Params{ 142 | Dir: ts.MkAbs(args[0]), 143 | UpdateScripts: true, 144 | }) 145 | }() 146 | if neg { 147 | if len(t.failMsgs) == 0 { 148 | ts.Fatalf("testscript-update unexpectedly succeeded") 149 | } 150 | return 151 | } 152 | if len(t.failMsgs) > 0 { 153 | ts.Fatalf("testscript-update unexpectedly failed with errors: %q", t.failMsgs) 154 | } 155 | }, 156 | }, 157 | Setup: func(env *Env) error { 158 | infos, err := ioutil.ReadDir(env.WorkDir) 159 | if err != nil { 160 | return fmt.Errorf("cannot read workdir: %v", err) 161 | } 162 | setupFilenames = nil 163 | for _, info := range infos { 164 | setupFilenames = append(setupFilenames, info.Name()) 165 | } 166 | env.Values["somekey"] = 1234 167 | env.Vars = append(env.Vars, 168 | "GONOSUMDB=*", 169 | ) 170 | return nil 171 | }, 172 | }) 173 | if testDeferCount != 0 { 174 | t.Fatalf("defer mismatch; got %d want 0", testDeferCount) 175 | } 176 | // TODO check that the temp directory has been removed. 177 | } 178 | 179 | // TestTestwork tests that using the flag -testwork will make sure the work dir isn't removed 180 | // after the test is done. It uses an empty testscript file that doesn't do anything. 181 | func TestTestwork(t *testing.T) { 182 | out, err := exec.Command("go", "test", ".", "-testwork", "-v", "-run", "TestScripts/^nothing$").CombinedOutput() 183 | if err != nil { 184 | t.Fatal(err) 185 | } 186 | 187 | re := regexp.MustCompile(`\s+WORK=(\S+)`) 188 | match := re.FindAllStringSubmatch(string(out), -1) 189 | 190 | // Ensure that there is only one line with one match 191 | if len(match) != 1 || len(match[0]) != 2 { 192 | t.Fatalf("failed to extract WORK directory") 193 | } 194 | 195 | var fi os.FileInfo 196 | if fi, err = os.Stat(match[0][1]); err != nil { 197 | t.Fatalf("failed to stat expected work directory %v: %v", match[0][1], err) 198 | } 199 | 200 | if !fi.IsDir() { 201 | t.Fatalf("expected persisted workdir is not a directory: %v", match[0][1]) 202 | } 203 | } 204 | 205 | func setSpecialVal(ts *TestScript, neg bool, args []string) { 206 | ts.Setenv("SPECIALVAL", "42") 207 | } 208 | 209 | func ensureSpecialVal(ts *TestScript, neg bool, args []string) { 210 | want := "42" 211 | if got := ts.Getenv("SPECIALVAL"); got != want { 212 | ts.Fatalf("expected SPECIALVAL to be %q; got %q", want, got) 213 | } 214 | } 215 | 216 | // interrupt interrupts the current background command. 217 | // Note that this will not work under Windows. 218 | func interrupt(ts *TestScript, neg bool, args []string) { 219 | if neg { 220 | ts.Fatalf("interrupt does not support neg") 221 | } 222 | if len(args) > 0 { 223 | ts.Fatalf("unexpected args found") 224 | } 225 | bg := ts.BackgroundCmds() 226 | if got, want := len(bg), 1; got != want { 227 | ts.Fatalf("unexpected background cmd count; got %d want %d", got, want) 228 | } 229 | bg[0].Process.Signal(os.Interrupt) 230 | } 231 | 232 | func waitFile(ts *TestScript, neg bool, args []string) { 233 | if neg { 234 | ts.Fatalf("waitfile does not support neg") 235 | } 236 | if len(args) != 1 { 237 | ts.Fatalf("usage: waitfile file") 238 | } 239 | path := ts.MkAbs(args[0]) 240 | for i := 0; i < 100; i++ { 241 | _, err := os.Stat(path) 242 | if err == nil { 243 | return 244 | } 245 | if !os.IsNotExist(err) { 246 | ts.Fatalf("unexpected stat error: %v", err) 247 | } 248 | time.Sleep(10 * time.Millisecond) 249 | } 250 | ts.Fatalf("timed out waiting for %q to be created", path) 251 | } 252 | 253 | type fakeT struct { 254 | ts *TestScript 255 | failMsgs []string 256 | } 257 | 258 | var errAbort = errors.New("abort test") 259 | 260 | func (t *fakeT) Skip(args ...interface{}) { 261 | panic(errAbort) 262 | } 263 | 264 | func (t *fakeT) Fatal(args ...interface{}) { 265 | t.failMsgs = append(t.failMsgs, fmt.Sprint(args...)) 266 | panic(errAbort) 267 | } 268 | 269 | func (t *fakeT) Parallel() {} 270 | 271 | func (t *fakeT) Log(args ...interface{}) { 272 | t.ts.Logf("testscript: %v", fmt.Sprint(args...)) 273 | } 274 | 275 | func (t *fakeT) FailNow() { 276 | t.Fatal("failed") 277 | } 278 | 279 | func (t *fakeT) Run(name string, f func(T)) { 280 | f(t) 281 | } 282 | 283 | func (t *fakeT) Verbose() bool { 284 | return false 285 | } 286 | -------------------------------------------------------------------------------- /internal/testenv/testenv.go: -------------------------------------------------------------------------------- 1 | // Copyright 2015 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 | // Package testenv provides information about what functionality 6 | // is available in different testing environments run by the Go team. 7 | // 8 | // It is an internal package because these details are specific 9 | // to the Go team's test setup (on build.golang.org) and not 10 | // fundamental to tests in general. 11 | package testenv 12 | 13 | import ( 14 | "errors" 15 | "flag" 16 | "os" 17 | "os/exec" 18 | "path/filepath" 19 | "runtime" 20 | "strconv" 21 | "strings" 22 | "testing" 23 | ) 24 | 25 | // Builder reports the name of the builder running this test 26 | // (for example, "linux-amd64" or "windows-386-gce"). 27 | // If the test is not running on the build infrastructure, 28 | // Builder returns the empty string. 29 | func Builder() string { 30 | return os.Getenv("GO_BUILDER_NAME") 31 | } 32 | 33 | // HasGoBuild reports whether the current system can build programs with ``go build'' 34 | // and then run them with os.StartProcess or exec.Command. 35 | func HasGoBuild() bool { 36 | if os.Getenv("GO_GCFLAGS") != "" { 37 | // It's too much work to require every caller of the go command 38 | // to pass along "-gcflags="+os.Getenv("GO_GCFLAGS"). 39 | // For now, if $GO_GCFLAGS is set, report that we simply can't 40 | // run go build. 41 | return false 42 | } 43 | switch runtime.GOOS { 44 | case "android", "nacl", "js": 45 | return false 46 | case "darwin": 47 | if strings.HasPrefix(runtime.GOARCH, "arm") { 48 | return false 49 | } 50 | } 51 | return true 52 | } 53 | 54 | // MustHaveGoBuild checks that the current system can build programs with ``go build'' 55 | // and then run them with os.StartProcess or exec.Command. 56 | // If not, MustHaveGoBuild calls t.Skip with an explanation. 57 | func MustHaveGoBuild(t testing.TB) { 58 | if os.Getenv("GO_GCFLAGS") != "" { 59 | t.Skipf("skipping test: 'go build' not compatible with setting $GO_GCFLAGS") 60 | } 61 | if !HasGoBuild() { 62 | t.Skipf("skipping test: 'go build' not available on %s/%s", runtime.GOOS, runtime.GOARCH) 63 | } 64 | } 65 | 66 | // HasGoRun reports whether the current system can run programs with ``go run.'' 67 | func HasGoRun() bool { 68 | // For now, having go run and having go build are the same. 69 | return HasGoBuild() 70 | } 71 | 72 | // MustHaveGoRun checks that the current system can run programs with ``go run.'' 73 | // If not, MustHaveGoRun calls t.Skip with an explanation. 74 | func MustHaveGoRun(t testing.TB) { 75 | if !HasGoRun() { 76 | t.Skipf("skipping test: 'go run' not available on %s/%s", runtime.GOOS, runtime.GOARCH) 77 | } 78 | } 79 | 80 | // GoToolPath reports the path to the Go tool. 81 | // It is a convenience wrapper around GoTool. 82 | // If the tool is unavailable GoToolPath calls t.Skip. 83 | // If the tool should be available and isn't, GoToolPath calls t.Fatal. 84 | func GoToolPath(t testing.TB) string { 85 | MustHaveGoBuild(t) 86 | path, err := GoTool() 87 | if err != nil { 88 | t.Fatal(err) 89 | } 90 | return path 91 | } 92 | 93 | // GoTool reports the path to the Go tool. 94 | func GoTool() (string, error) { 95 | if !HasGoBuild() { 96 | return "", errors.New("platform cannot run go tool") 97 | } 98 | var exeSuffix string 99 | if runtime.GOOS == "windows" { 100 | exeSuffix = ".exe" 101 | } 102 | path := filepath.Join(runtime.GOROOT(), "bin", "go"+exeSuffix) 103 | if _, err := os.Stat(path); err == nil { 104 | return path, nil 105 | } 106 | goBin, err := exec.LookPath("go" + exeSuffix) 107 | if err != nil { 108 | return "", errors.New("cannot find go tool: " + err.Error()) 109 | } 110 | return goBin, nil 111 | } 112 | 113 | // HasExec reports whether the current system can start new processes 114 | // using os.StartProcess or (more commonly) exec.Command. 115 | func HasExec() bool { 116 | switch runtime.GOOS { 117 | case "nacl", "js": 118 | return false 119 | case "darwin": 120 | if strings.HasPrefix(runtime.GOARCH, "arm") { 121 | return false 122 | } 123 | } 124 | return true 125 | } 126 | 127 | // HasSrc reports whether the entire source tree is available under GOROOT. 128 | func HasSrc() bool { 129 | switch runtime.GOOS { 130 | case "nacl": 131 | return false 132 | case "darwin": 133 | if strings.HasPrefix(runtime.GOARCH, "arm") { 134 | return false 135 | } 136 | } 137 | return true 138 | } 139 | 140 | // MustHaveExec checks that the current system can start new processes 141 | // using os.StartProcess or (more commonly) exec.Command. 142 | // If not, MustHaveExec calls t.Skip with an explanation. 143 | func MustHaveExec(t testing.TB) { 144 | if !HasExec() { 145 | t.Skipf("skipping test: cannot exec subprocess on %s/%s", runtime.GOOS, runtime.GOARCH) 146 | } 147 | } 148 | 149 | // HasExternalNetwork reports whether the current system can use 150 | // external (non-localhost) networks. 151 | func HasExternalNetwork() bool { 152 | return !testing.Short() && runtime.GOOS != "nacl" && runtime.GOOS != "js" 153 | } 154 | 155 | // MustHaveExternalNetwork checks that the current system can use 156 | // external (non-localhost) networks. 157 | // If not, MustHaveExternalNetwork calls t.Skip with an explanation. 158 | func MustHaveExternalNetwork(t testing.TB) { 159 | if runtime.GOOS == "nacl" || runtime.GOOS == "js" { 160 | t.Skipf("skipping test: no external network on %s", runtime.GOOS) 161 | } 162 | if testing.Short() { 163 | t.Skipf("skipping test: no external network in -short mode") 164 | } 165 | } 166 | 167 | var haveCGO bool 168 | 169 | // HasCGO reports whether the current system can use cgo. 170 | func HasCGO() bool { 171 | return haveCGO 172 | } 173 | 174 | // MustHaveCGO calls t.Skip if cgo is not available. 175 | func MustHaveCGO(t testing.TB) { 176 | if !haveCGO { 177 | t.Skipf("skipping test: no cgo") 178 | } 179 | } 180 | 181 | // HasSymlink reports whether the current system can use os.Symlink. 182 | func HasSymlink() bool { 183 | ok, _ := hasSymlink() 184 | return ok 185 | } 186 | 187 | // MustHaveSymlink reports whether the current system can use os.Symlink. 188 | // If not, MustHaveSymlink calls t.Skip with an explanation. 189 | func MustHaveSymlink(t testing.TB) { 190 | ok, reason := hasSymlink() 191 | if !ok { 192 | t.Skipf("skipping test: cannot make symlinks on %s/%s%s", runtime.GOOS, runtime.GOARCH, reason) 193 | } 194 | } 195 | 196 | // HasLink reports whether the current system can use os.Link. 197 | func HasLink() bool { 198 | // From Android release M (Marshmallow), hard linking files is blocked 199 | // and an attempt to call link() on a file will return EACCES. 200 | // - https://code.google.com/p/android-developer-preview/issues/detail?id=3150 201 | return runtime.GOOS != "plan9" && runtime.GOOS != "android" 202 | } 203 | 204 | // MustHaveLink reports whether the current system can use os.Link. 205 | // If not, MustHaveLink calls t.Skip with an explanation. 206 | func MustHaveLink(t testing.TB) { 207 | if !HasLink() { 208 | t.Skipf("skipping test: hardlinks are not supported on %s/%s", runtime.GOOS, runtime.GOARCH) 209 | } 210 | } 211 | 212 | var flaky = flag.Bool("flaky", false, "run known-flaky tests too") 213 | 214 | func SkipFlaky(t testing.TB, issue int) { 215 | t.Helper() 216 | if !*flaky { 217 | t.Skipf("skipping known flaky test without the -flaky flag; see golang.org/issue/%d", issue) 218 | } 219 | } 220 | 221 | func SkipFlakyNet(t testing.TB) { 222 | t.Helper() 223 | if v, _ := strconv.ParseBool(os.Getenv("GO_BUILDER_FLAKY_NET")); v { 224 | t.Skip("skipping test on builder known to have frequent network failures") 225 | } 226 | } 227 | 228 | // CleanCmdEnv will fill cmd.Env with the environment, excluding certain 229 | // variables that could modify the behavior of the Go tools such as 230 | // GODEBUG and GOTRACEBACK. 231 | func CleanCmdEnv(cmd *exec.Cmd) *exec.Cmd { 232 | if cmd.Env != nil { 233 | panic("environment already set") 234 | } 235 | for _, env := range os.Environ() { 236 | // Exclude GODEBUG from the environment to prevent its output 237 | // from breaking tests that are trying to parse other command output. 238 | if strings.HasPrefix(env, "GODEBUG=") { 239 | continue 240 | } 241 | // Exclude GOTRACEBACK for the same reason. 242 | if strings.HasPrefix(env, "GOTRACEBACK=") { 243 | continue 244 | } 245 | cmd.Env = append(cmd.Env, env) 246 | } 247 | return cmd 248 | } 249 | -------------------------------------------------------------------------------- /doc.go: -------------------------------------------------------------------------------- 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 | /* 6 | Package testscript provides support for defining filesystem-based tests by 7 | creating scripts in a directory. 8 | 9 | To invoke the tests, call testscript.Run. For example: 10 | 11 | func TestFoo(t *testing.T) { 12 | testscript.Run(t, testscript.Params{ 13 | Dir: "testdata", 14 | }) 15 | } 16 | 17 | A testscript directory holds test scripts *.txt run during 'go test'. 18 | Each script defines a subtest; the exact set of allowable commands in a 19 | script are defined by the parameters passed to the Run function. 20 | To run a specific script foo.txt 21 | 22 | go test cmd/go -run=TestName/^foo$ 23 | 24 | where TestName is the name of the test that Run is called from. 25 | 26 | To define an executable command (or several) that can be run as part of the script, 27 | call RunMain with the functions that implement the command's functionality. 28 | The command functions will be called in a separate process, so are 29 | free to mutate global variables without polluting the top level test binary. 30 | 31 | func TestMain(m *testing.M) { 32 | os.Exit(testscript.RunMain(m, map[string] func() int{ 33 | "testscript": testscriptMain, 34 | })) 35 | } 36 | 37 | In general script files should have short names: a few words, not whole sentences. 38 | The first word should be the general category of behavior being tested, 39 | often the name of a subcommand to be tested or a concept (vendor, pattern). 40 | 41 | Each script is a text archive (go doc github.com/rogpeppe/testscript//txtar). 42 | The script begins with an actual command script to run 43 | followed by the content of zero or more supporting files to 44 | create in the script's temporary file system before it starts executing. 45 | 46 | As an example: 47 | 48 | # hello world 49 | exec cat hello.text 50 | stdout 'hello world\n' 51 | ! stderr . 52 | 53 | -- hello.text -- 54 | hello world 55 | 56 | Each script runs in a fresh temporary work directory tree, available to scripts as $WORK. 57 | Scripts also have access to these other environment variables: 58 | 59 | HOME=/no-home 60 | PATH= 61 | TMPDIR=$WORK/tmp 62 | devnull= 63 | goversion= 64 | 65 | The environment variable $exe (lowercase) is an empty string on most 66 | systems, ".exe" on Windows. 67 | 68 | The script's supporting files are unpacked relative to $WORK 69 | and then the script begins execution in that 70 | directory as well. Thus the example above runs in $WORK 71 | with $WORK/hello.txt containing the listed contents. 72 | 73 | The lines at the top of the script are a sequence of commands to be 74 | executed by a small script engine in the testscript package (not the system 75 | shell). The script stops and the overall test fails if any particular 76 | command fails. 77 | 78 | Each line is parsed into a sequence of space-separated command words, 79 | with environment variable expansion and # marking an end-of-line comment. 80 | Adding single quotes around text keeps spaces in that text from being 81 | treated as word separators and also disables environment variable 82 | expansion. Inside a single-quoted block of text, a repeated single 83 | quote indicates a literal single quote, as in: 84 | 85 | 'Don''t communicate by sharing memory.' 86 | 87 | A line beginning with # is a comment and conventionally explains what is 88 | being done or tested at the start of a new phase in the script. 89 | 90 | A special form of environment variable syntax can be used to quote 91 | regexp metacharacters inside environment variables. The "@R" suffix 92 | is special, and indicates that the variable should be quoted. 93 | 94 | ${VAR@R} 95 | 96 | The command prefix ! indicates that the command on the rest of the line 97 | (typically go or a matching predicate) must fail, not succeed. Only certain 98 | commands support this prefix. They are indicated below by [!] in the synopsis. 99 | 100 | The command prefix [cond] indicates that the command on the rest of the line 101 | should only run when the condition is satisfied. The predefined conditions are: 102 | 103 | - [short] for testing.Short() 104 | - [net] for whether the external network can be used 105 | - [link] for whether the OS has hard link support 106 | - [symlink] for whether the OS has symbolic link support 107 | - [exec:prog] for whether prog is available for execution (found by exec.LookPath) 108 | 109 | A condition can be negated: [!short] means to run the rest of the line 110 | when testing.Short() is false. 111 | 112 | Additional conditions can be added by passing a function to Params.Condition. 113 | 114 | The predefined commands are: 115 | 116 | - cd dir 117 | Change to the given directory for future commands. 118 | 119 | - chmod mode file 120 | 121 | Change the permissions of file or directory to the given octal mode (000 to 777). 122 | 123 | - cmp file1 file2 124 | Check that the named files have the same content. 125 | By convention, file1 is the actual data and file2 the expected data. 126 | File1 can be "stdout" or "stderr" to use the standard output or standard error 127 | from the most recent exec or wait command. 128 | (If the files have differing content, the failure prints a diff.) 129 | 130 | - cmpenv file1 file2 131 | Like cmp, but environment variables in file2 are substituted before the 132 | comparison. For example, $GOOS is replaced by the target GOOS. 133 | 134 | - cp src... dst 135 | Copy the listed files to the target file or existing directory. 136 | src can include "stdout" or "stderr" to use the standard output or standard error 137 | from the most recent exec or go command. 138 | 139 | - env [key=value...] 140 | With no arguments, print the environment (useful for debugging). 141 | Otherwise add the listed key=value pairs to the environment. 142 | 143 | - [!] exec program [args...] [&] 144 | Run the given executable program with the arguments. 145 | It must (or must not) succeed. 146 | Note that 'exec' does not terminate the script (unlike in Unix shells). 147 | 148 | If the last token is '&', the program executes in the background. The standard 149 | output and standard error of the previous command is cleared, but the output 150 | of the background process is buffered — and checking of its exit status is 151 | delayed — until the next call to 'wait', 'skip', or 'stop' or the end of the 152 | test. At the end of the test, any remaining background processes are 153 | terminated using os.Interrupt (if supported) or os.Kill. 154 | 155 | Standard input can be provided using the stdin command; this will be 156 | cleared after exec has been called. 157 | 158 | - [!] exists [-readonly] file... 159 | Each of the listed files or directories must (or must not) exist. 160 | If -readonly is given, the files or directories must be unwritable. 161 | 162 | - [!] grep [-count=N] pattern file 163 | The file's content must (or must not) match the regular expression pattern. 164 | For positive matches, -count=N specifies an exact number of matches to require. 165 | 166 | - mkdir path... 167 | Create the listed directories, if they do not already exists. 168 | 169 | - unquote file... 170 | Rewrite each file by replacing any leading ">" characters from 171 | each line. This enables a file to contain substrings that look like 172 | txtar file markers. 173 | See also https://godoc.org/github.com/rogpeppe/testscript//txtar#Unquote 174 | 175 | - rm file... 176 | Remove the listed files or directories. 177 | 178 | - skip [message] 179 | Mark the test skipped, including the message if given. 180 | 181 | - stdin file 182 | Set the standard input for the next exec command to the contents of the given file. 183 | 184 | - [!] stderr [-count=N] pattern 185 | Apply the grep command (see above) to the standard error 186 | from the most recent exec or wait command. 187 | 188 | - [!] stdout [-count=N] pattern 189 | Apply the grep command (see above) to the standard output 190 | from the most recent exec or wait command. 191 | 192 | - stop [message] 193 | Stop the test early (marking it as passing), including the message if given. 194 | 195 | - symlink file -> target 196 | Create file as a symlink to target. The -> (like in ls -l output) is required. 197 | 198 | - wait 199 | Wait for all 'exec' and 'go' commands started in the background (with the '&' 200 | token) to exit, and display success or failure status for them. 201 | After a call to wait, the 'stderr' and 'stdout' commands will apply to the 202 | concatenation of the corresponding streams of the background commands, 203 | in the order in which those commands were started. 204 | 205 | When TestScript runs a script and the script fails, by default TestScript shows 206 | the execution of the most recent phase of the script (since the last # comment) 207 | and only shows the # comments for earlier phases. For example, here is a 208 | multi-phase script with a bug in it (TODO: make this example less go-command 209 | specific): 210 | 211 | # GOPATH with p1 in d2, p2 in d2 212 | env GOPATH=$WORK/d1${:}$WORK/d2 213 | 214 | # build & install p1 215 | env 216 | go install -i p1 217 | ! stale p1 218 | ! stale p2 219 | 220 | # modify p2 - p1 should appear stale 221 | cp $WORK/p2x.go $WORK/d2/src/p2/p2.go 222 | stale p1 p2 223 | 224 | # build & install p1 again 225 | go install -i p11 226 | ! stale p1 227 | ! stale p2 228 | 229 | -- $WORK/d1/src/p1/p1.go -- 230 | package p1 231 | import "p2" 232 | func F() { p2.F() } 233 | -- $WORK/d2/src/p2/p2.go -- 234 | package p2 235 | func F() {} 236 | -- $WORK/p2x.go -- 237 | package p2 238 | func F() {} 239 | func G() {} 240 | 241 | The bug is that the final phase installs p11 instead of p1. The test failure looks like: 242 | 243 | $ go test -run=Script 244 | --- FAIL: TestScript (3.75s) 245 | --- FAIL: TestScript/install_rebuild_gopath (0.16s) 246 | script_test.go:223: 247 | # GOPATH with p1 in d2, p2 in d2 (0.000s) 248 | # build & install p1 (0.087s) 249 | # modify p2 - p1 should appear stale (0.029s) 250 | # build & install p1 again (0.022s) 251 | > go install -i p11 252 | [stderr] 253 | can't load package: package p11: cannot find package "p11" in any of: 254 | /Users/rsc/go/src/p11 (from $GOROOT) 255 | $WORK/d1/src/p11 (from $GOPATH) 256 | $WORK/d2/src/p11 257 | [exit status 1] 258 | FAIL: unexpected go command failure 259 | 260 | script_test.go:73: failed at testdata/script/install_rebuild_gopath.txt:15 in $WORK/gopath/src 261 | 262 | FAIL 263 | exit status 1 264 | FAIL cmd/go 4.875s 265 | $ 266 | 267 | Note that the commands in earlier phases have been hidden, so that the relevant 268 | commands are more easily found, and the elapsed time for a completed phase 269 | is shown next to the phase heading. To see the entire execution, use "go test -v", 270 | which also adds an initial environment dump to the beginning of the log. 271 | 272 | Note also that in reported output, the actual name of the per-script temporary directory 273 | has been consistently replaced with the literal string $WORK. 274 | 275 | If Params.TestWork is true, it causes each test to log the name of its $WORK directory and other 276 | environment variable settings and also to leave that directory behind when it exits, 277 | for manual debugging of failing tests: 278 | 279 | $ go test -run=Script -work 280 | --- FAIL: TestScript (3.75s) 281 | --- FAIL: TestScript/install_rebuild_gopath (0.16s) 282 | script_test.go:223: 283 | WORK=/tmp/cmd-go-test-745953508/script-install_rebuild_gopath 284 | GOARCH= 285 | GOCACHE=/Users/rsc/Library/Caches/go-build 286 | GOOS= 287 | GOPATH=$WORK/gopath 288 | GOROOT=/Users/rsc/go 289 | HOME=/no-home 290 | TMPDIR=$WORK/tmp 291 | exe= 292 | 293 | # GOPATH with p1 in d2, p2 in d2 (0.000s) 294 | # build & install p1 (0.085s) 295 | # modify p2 - p1 should appear stale (0.030s) 296 | # build & install p1 again (0.019s) 297 | > go install -i p11 298 | [stderr] 299 | can't load package: package p11: cannot find package "p11" in any of: 300 | /Users/rsc/go/src/p11 (from $GOROOT) 301 | $WORK/d1/src/p11 (from $GOPATH) 302 | $WORK/d2/src/p11 303 | [exit status 1] 304 | FAIL: unexpected go command failure 305 | 306 | script_test.go:73: failed at testdata/script/install_rebuild_gopath.txt:15 in $WORK/gopath/src 307 | 308 | FAIL 309 | exit status 1 310 | FAIL cmd/go 4.875s 311 | $ 312 | 313 | $ WORK=/tmp/cmd-go-test-745953508/script-install_rebuild_gopath 314 | $ cd $WORK/d1/src/p1 315 | $ cat p1.go 316 | package p1 317 | import "p2" 318 | func F() { p2.F() } 319 | $ 320 | */ 321 | package testscript 322 | -------------------------------------------------------------------------------- /cmd.go: -------------------------------------------------------------------------------- 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 | package testscript 6 | 7 | import ( 8 | "fmt" 9 | "io/ioutil" 10 | "os" 11 | "os/exec" 12 | "path/filepath" 13 | "regexp" 14 | "strconv" 15 | "strings" 16 | 17 | "github.com/rogpeppe/testscript/internal/textutil" 18 | "github.com/rogpeppe/testscript/txtar" 19 | ) 20 | 21 | // scriptCmds are the script command implementations. 22 | // Keep list and the implementations below sorted by name. 23 | // 24 | // NOTE: If you make changes here, update doc.go. 25 | // 26 | var scriptCmds = map[string]func(*TestScript, bool, []string){ 27 | "cd": (*TestScript).cmdCd, 28 | "chmod": (*TestScript).cmdChmod, 29 | "cmp": (*TestScript).cmdCmp, 30 | "cmpenv": (*TestScript).cmdCmpenv, 31 | "cp": (*TestScript).cmdCp, 32 | "env": (*TestScript).cmdEnv, 33 | "exec": (*TestScript).cmdExec, 34 | "exists": (*TestScript).cmdExists, 35 | "grep": (*TestScript).cmdGrep, 36 | "mkdir": (*TestScript).cmdMkdir, 37 | "rm": (*TestScript).cmdRm, 38 | "unquote": (*TestScript).cmdUnquote, 39 | "skip": (*TestScript).cmdSkip, 40 | "stdin": (*TestScript).cmdStdin, 41 | "stderr": (*TestScript).cmdStderr, 42 | "stdout": (*TestScript).cmdStdout, 43 | "stop": (*TestScript).cmdStop, 44 | "symlink": (*TestScript).cmdSymlink, 45 | "wait": (*TestScript).cmdWait, 46 | } 47 | 48 | // cd changes to a different directory. 49 | func (ts *TestScript) cmdCd(neg bool, args []string) { 50 | if neg { 51 | ts.Fatalf("unsupported: ! cd") 52 | } 53 | if len(args) != 1 { 54 | ts.Fatalf("usage: cd dir") 55 | } 56 | 57 | dir := args[0] 58 | if !filepath.IsAbs(dir) { 59 | dir = filepath.Join(ts.cd, dir) 60 | } 61 | info, err := os.Stat(dir) 62 | if os.IsNotExist(err) { 63 | ts.Fatalf("directory %s does not exist", dir) 64 | } 65 | ts.Check(err) 66 | if !info.IsDir() { 67 | ts.Fatalf("%s is not a directory", dir) 68 | } 69 | ts.cd = dir 70 | ts.Logf("%s\n", ts.cd) 71 | } 72 | 73 | func (ts *TestScript) cmdChmod(neg bool, args []string) { 74 | if len(args) != 2 { 75 | ts.Fatalf("usage: chmod mode file") 76 | } 77 | mode, err := strconv.ParseInt(args[0], 8, 32) 78 | if err != nil { 79 | ts.Fatalf("bad file mode %q: %v", args[0], err) 80 | } 81 | if mode > 0777 { 82 | ts.Fatalf("unsupported file mode %.3o", mode) 83 | } 84 | err = os.Chmod(ts.MkAbs(args[1]), os.FileMode(mode)) 85 | if neg { 86 | if err == nil { 87 | ts.Fatalf("unexpected chmod success") 88 | } 89 | return 90 | } 91 | if err != nil { 92 | ts.Fatalf("unexpected chmod failure: %v", err) 93 | } 94 | } 95 | 96 | // cmp compares two files. 97 | func (ts *TestScript) cmdCmp(neg bool, args []string) { 98 | if neg { 99 | // It would be strange to say "this file can have any content except this precise byte sequence". 100 | ts.Fatalf("unsupported: ! cmp") 101 | } 102 | if len(args) != 2 { 103 | ts.Fatalf("usage: cmp file1 file2") 104 | } 105 | 106 | ts.doCmdCmp(args, false) 107 | } 108 | 109 | // cmpenv compares two files with environment variable substitution. 110 | func (ts *TestScript) cmdCmpenv(neg bool, args []string) { 111 | if neg { 112 | ts.Fatalf("unsupported: ! cmpenv") 113 | } 114 | if len(args) != 2 { 115 | ts.Fatalf("usage: cmpenv file1 file2") 116 | } 117 | ts.doCmdCmp(args, true) 118 | } 119 | 120 | func (ts *TestScript) doCmdCmp(args []string, env bool) { 121 | name1, name2 := args[0], args[1] 122 | text1 := ts.ReadFile(name1) 123 | 124 | absName2 := ts.MkAbs(name2) 125 | data, err := ioutil.ReadFile(absName2) 126 | ts.Check(err) 127 | text2 := string(data) 128 | if env { 129 | text2 = ts.expand(text2) 130 | } 131 | if text1 == text2 { 132 | return 133 | } 134 | if ts.params.UpdateScripts && !env && (args[0] == "stdout" || args[0] == "stderr") { 135 | if scriptFile, ok := ts.scriptFiles[absName2]; ok { 136 | ts.scriptUpdates[scriptFile] = text1 137 | return 138 | } 139 | // The file being compared against isn't in the txtar archive, so don't 140 | // update the script. 141 | } 142 | 143 | ts.Logf("[diff -%s +%s]\n%s\n", name1, name2, textutil.Diff(text1, text2)) 144 | ts.Fatalf("%s and %s differ", name1, name2) 145 | } 146 | 147 | // cp copies files, maybe eventually directories. 148 | func (ts *TestScript) cmdCp(neg bool, args []string) { 149 | if neg { 150 | ts.Fatalf("unsupported: ! cp") 151 | } 152 | if len(args) < 2 { 153 | ts.Fatalf("usage: cp src... dst") 154 | } 155 | 156 | dst := ts.MkAbs(args[len(args)-1]) 157 | info, err := os.Stat(dst) 158 | dstDir := err == nil && info.IsDir() 159 | if len(args) > 2 && !dstDir { 160 | ts.Fatalf("cp: destination %s is not a directory", dst) 161 | } 162 | 163 | for _, arg := range args[:len(args)-1] { 164 | var ( 165 | src string 166 | data []byte 167 | mode os.FileMode 168 | ) 169 | switch arg { 170 | case "stdout": 171 | src = arg 172 | data = []byte(ts.stdout) 173 | mode = 0666 174 | case "stderr": 175 | src = arg 176 | data = []byte(ts.stderr) 177 | mode = 0666 178 | default: 179 | src = ts.MkAbs(arg) 180 | info, err := os.Stat(src) 181 | ts.Check(err) 182 | mode = info.Mode() & 0777 183 | data, err = ioutil.ReadFile(src) 184 | ts.Check(err) 185 | } 186 | targ := dst 187 | if dstDir { 188 | targ = filepath.Join(dst, filepath.Base(src)) 189 | } 190 | ts.Check(ioutil.WriteFile(targ, data, mode)) 191 | } 192 | } 193 | 194 | // env displays or adds to the environment. 195 | func (ts *TestScript) cmdEnv(neg bool, args []string) { 196 | if neg { 197 | ts.Fatalf("unsupported: ! env") 198 | } 199 | if len(args) == 0 { 200 | printed := make(map[string]bool) // env list can have duplicates; only print effective value (from envMap) once 201 | for _, kv := range ts.env { 202 | k := envvarname(kv[:strings.Index(kv, "=")]) 203 | if !printed[k] { 204 | printed[k] = true 205 | ts.Logf("%s=%s\n", k, ts.envMap[k]) 206 | } 207 | } 208 | return 209 | } 210 | for _, env := range args { 211 | i := strings.Index(env, "=") 212 | if i < 0 { 213 | // Display value instead of setting it. 214 | ts.Logf("%s=%s\n", env, ts.Getenv(env)) 215 | continue 216 | } 217 | ts.Setenv(env[:i], env[i+1:]) 218 | } 219 | } 220 | 221 | // exec runs the given command. 222 | func (ts *TestScript) cmdExec(neg bool, args []string) { 223 | if len(args) < 1 || (len(args) == 1 && args[0] == "&") { 224 | ts.Fatalf("usage: exec program [args...] [&]") 225 | } 226 | 227 | var err error 228 | if len(args) > 0 && args[len(args)-1] == "&" { 229 | var cmd *exec.Cmd 230 | cmd, err = ts.execBackground(args[0], args[1:len(args)-1]...) 231 | if err == nil { 232 | wait := make(chan struct{}) 233 | go func() { 234 | ctxWait(ts.ctxt, cmd) 235 | close(wait) 236 | }() 237 | ts.background = append(ts.background, backgroundCmd{cmd, wait, neg}) 238 | } 239 | ts.stdout, ts.stderr = "", "" 240 | } else { 241 | ts.stdout, ts.stderr, err = ts.exec(args[0], args[1:]...) 242 | if ts.stdout != "" { 243 | fmt.Fprintf(&ts.log, "[stdout]\n%s", ts.stdout) 244 | } 245 | if ts.stderr != "" { 246 | fmt.Fprintf(&ts.log, "[stderr]\n%s", ts.stderr) 247 | } 248 | if err == nil && neg { 249 | ts.Fatalf("unexpected command success") 250 | } 251 | } 252 | 253 | if err != nil { 254 | fmt.Fprintf(&ts.log, "[%v]\n", err) 255 | if ts.ctxt.Err() != nil { 256 | ts.Fatalf("test timed out while running command") 257 | } else if !neg { 258 | ts.Fatalf("unexpected command failure") 259 | } 260 | } 261 | } 262 | 263 | // exists checks that the list of files exists. 264 | func (ts *TestScript) cmdExists(neg bool, args []string) { 265 | var readonly bool 266 | if len(args) > 0 && args[0] == "-readonly" { 267 | readonly = true 268 | args = args[1:] 269 | } 270 | if len(args) == 0 { 271 | ts.Fatalf("usage: exists [-readonly] file...") 272 | } 273 | 274 | for _, file := range args { 275 | file = ts.MkAbs(file) 276 | info, err := os.Stat(file) 277 | if err == nil && neg { 278 | what := "file" 279 | if info.IsDir() { 280 | what = "directory" 281 | } 282 | ts.Fatalf("%s %s unexpectedly exists", what, file) 283 | } 284 | if err != nil && !neg { 285 | ts.Fatalf("%s does not exist", file) 286 | } 287 | if err == nil && !neg && readonly && info.Mode()&0222 != 0 { 288 | ts.Fatalf("%s exists but is writable", file) 289 | } 290 | } 291 | } 292 | 293 | // mkdir creates directories. 294 | func (ts *TestScript) cmdMkdir(neg bool, args []string) { 295 | if neg { 296 | ts.Fatalf("unsupported: ! mkdir") 297 | } 298 | if len(args) < 1 { 299 | ts.Fatalf("usage: mkdir dir...") 300 | } 301 | for _, arg := range args { 302 | ts.Check(os.MkdirAll(ts.MkAbs(arg), 0777)) 303 | } 304 | } 305 | 306 | // unquote unquotes files. 307 | func (ts *TestScript) cmdUnquote(neg bool, args []string) { 308 | if neg { 309 | ts.Fatalf("unsupported: ! unquote") 310 | } 311 | for _, arg := range args { 312 | file := ts.MkAbs(arg) 313 | data, err := ioutil.ReadFile(file) 314 | ts.Check(err) 315 | data, err = txtar.Unquote(data) 316 | ts.Check(err) 317 | err = ioutil.WriteFile(file, data, 0666) 318 | ts.Check(err) 319 | } 320 | } 321 | 322 | // rm removes files or directories. 323 | func (ts *TestScript) cmdRm(neg bool, args []string) { 324 | if neg { 325 | ts.Fatalf("unsupported: ! rm") 326 | } 327 | if len(args) < 1 { 328 | ts.Fatalf("usage: rm file...") 329 | } 330 | for _, arg := range args { 331 | file := ts.MkAbs(arg) 332 | removeAll(file) // does chmod and then attempts rm 333 | ts.Check(os.RemoveAll(file)) // report error 334 | } 335 | } 336 | 337 | // skip marks the test skipped. 338 | func (ts *TestScript) cmdSkip(neg bool, args []string) { 339 | if len(args) > 1 { 340 | ts.Fatalf("usage: skip [msg]") 341 | } 342 | if neg { 343 | ts.Fatalf("unsupported: ! skip") 344 | } 345 | 346 | // Before we mark the test as skipped, shut down any background processes and 347 | // make sure they have returned the correct status. 348 | for _, bg := range ts.background { 349 | interruptProcess(bg.cmd.Process) 350 | } 351 | ts.cmdWait(false, nil) 352 | 353 | if len(args) == 1 { 354 | ts.t.Skip(args[0]) 355 | } 356 | ts.t.Skip() 357 | } 358 | 359 | func (ts *TestScript) cmdStdin(neg bool, args []string) { 360 | if neg { 361 | ts.Fatalf("unsupported: ! stdin") 362 | } 363 | if len(args) != 1 { 364 | ts.Fatalf("usage: stdin filename") 365 | } 366 | data, err := ioutil.ReadFile(ts.MkAbs(args[0])) 367 | ts.Check(err) 368 | ts.stdin = string(data) 369 | } 370 | 371 | // stdout checks that the last go command standard output matches a regexp. 372 | func (ts *TestScript) cmdStdout(neg bool, args []string) { 373 | scriptMatch(ts, neg, args, ts.stdout, "stdout") 374 | } 375 | 376 | // stderr checks that the last go command standard output matches a regexp. 377 | func (ts *TestScript) cmdStderr(neg bool, args []string) { 378 | scriptMatch(ts, neg, args, ts.stderr, "stderr") 379 | } 380 | 381 | // grep checks that file content matches a regexp. 382 | // Like stdout/stderr and unlike Unix grep, it accepts Go regexp syntax. 383 | func (ts *TestScript) cmdGrep(neg bool, args []string) { 384 | scriptMatch(ts, neg, args, "", "grep") 385 | } 386 | 387 | // stop stops execution of the test (marking it passed). 388 | func (ts *TestScript) cmdStop(neg bool, args []string) { 389 | if neg { 390 | ts.Fatalf("unsupported: ! stop") 391 | } 392 | if len(args) > 1 { 393 | ts.Fatalf("usage: stop [msg]") 394 | } 395 | if len(args) == 1 { 396 | ts.Logf("stop: %s\n", args[0]) 397 | } else { 398 | ts.Logf("stop\n") 399 | } 400 | ts.stopped = true 401 | } 402 | 403 | // symlink creates a symbolic link. 404 | func (ts *TestScript) cmdSymlink(neg bool, args []string) { 405 | if neg { 406 | ts.Fatalf("unsupported: ! symlink") 407 | } 408 | if len(args) != 3 || args[1] != "->" { 409 | ts.Fatalf("usage: symlink file -> target") 410 | } 411 | // Note that the link target args[2] is not interpreted with MkAbs: 412 | // it will be interpreted relative to the directory file is in. 413 | ts.Check(os.Symlink(args[2], ts.MkAbs(args[0]))) 414 | } 415 | 416 | // Tait waits for background commands to exit, setting stderr and stdout to their result. 417 | func (ts *TestScript) cmdWait(neg bool, args []string) { 418 | if neg { 419 | ts.Fatalf("unsupported: ! wait") 420 | } 421 | if len(args) > 0 { 422 | ts.Fatalf("usage: wait") 423 | } 424 | 425 | var stdouts, stderrs []string 426 | for _, bg := range ts.background { 427 | <-bg.wait 428 | 429 | args := append([]string{filepath.Base(bg.cmd.Args[0])}, bg.cmd.Args[1:]...) 430 | fmt.Fprintf(&ts.log, "[background] %s: %v\n", strings.Join(args, " "), bg.cmd.ProcessState) 431 | 432 | cmdStdout := bg.cmd.Stdout.(*strings.Builder).String() 433 | if cmdStdout != "" { 434 | fmt.Fprintf(&ts.log, "[stdout]\n%s", cmdStdout) 435 | stdouts = append(stdouts, cmdStdout) 436 | } 437 | 438 | cmdStderr := bg.cmd.Stderr.(*strings.Builder).String() 439 | if cmdStderr != "" { 440 | fmt.Fprintf(&ts.log, "[stderr]\n%s", cmdStderr) 441 | stderrs = append(stderrs, cmdStderr) 442 | } 443 | 444 | if bg.cmd.ProcessState.Success() { 445 | if bg.neg { 446 | ts.Fatalf("unexpected command success") 447 | } 448 | } else { 449 | if ts.ctxt.Err() != nil { 450 | ts.Fatalf("test timed out while running command") 451 | } else if !bg.neg { 452 | ts.Fatalf("unexpected command failure") 453 | } 454 | } 455 | } 456 | 457 | ts.stdout = strings.Join(stdouts, "") 458 | ts.stderr = strings.Join(stderrs, "") 459 | ts.background = nil 460 | } 461 | 462 | // scriptMatch implements both stdout and stderr. 463 | func scriptMatch(ts *TestScript, neg bool, args []string, text, name string) { 464 | n := 0 465 | if len(args) >= 1 && strings.HasPrefix(args[0], "-count=") { 466 | if neg { 467 | ts.Fatalf("cannot use -count= with negated match") 468 | } 469 | var err error 470 | n, err = strconv.Atoi(args[0][len("-count="):]) 471 | if err != nil { 472 | ts.Fatalf("bad -count=: %v", err) 473 | } 474 | if n < 1 { 475 | ts.Fatalf("bad -count=: must be at least 1") 476 | } 477 | args = args[1:] 478 | } 479 | 480 | extraUsage := "" 481 | want := 1 482 | if name == "grep" { 483 | extraUsage = " file" 484 | want = 2 485 | } 486 | if len(args) != want { 487 | ts.Fatalf("usage: %s [-count=N] 'pattern'%s", name, extraUsage) 488 | } 489 | 490 | pattern := args[0] 491 | re, err := regexp.Compile(`(?m)` + pattern) 492 | ts.Check(err) 493 | 494 | isGrep := name == "grep" 495 | if isGrep { 496 | name = args[1] // for error messages 497 | data, err := ioutil.ReadFile(ts.MkAbs(args[1])) 498 | ts.Check(err) 499 | text = string(data) 500 | } 501 | 502 | if neg { 503 | if re.MatchString(text) { 504 | if isGrep { 505 | ts.Logf("[%s]\n%s\n", name, text) 506 | } 507 | ts.Fatalf("unexpected match for %#q found in %s: %s", pattern, name, re.FindString(text)) 508 | } 509 | } else { 510 | if !re.MatchString(text) { 511 | if isGrep { 512 | ts.Logf("[%s]\n%s\n", name, text) 513 | } 514 | ts.Fatalf("no match for %#q found in %s", pattern, name) 515 | } 516 | if n > 0 { 517 | count := len(re.FindAllString(text, -1)) 518 | if count != n { 519 | if isGrep { 520 | ts.Logf("[%s]\n%s\n", name, text) 521 | } 522 | ts.Fatalf("have %d matches for %#q, want %d", count, pattern, n) 523 | } 524 | } 525 | } 526 | } 527 | -------------------------------------------------------------------------------- /testscript.go: -------------------------------------------------------------------------------- 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 testscript 9 | 10 | import ( 11 | "bytes" 12 | "context" 13 | "flag" 14 | "fmt" 15 | "io/ioutil" 16 | "os" 17 | "os/exec" 18 | "path/filepath" 19 | "regexp" 20 | "runtime" 21 | "strings" 22 | "sync/atomic" 23 | "testing" 24 | "time" 25 | 26 | "github.com/rogpeppe/testscript/internal/imports" 27 | "github.com/rogpeppe/testscript/internal/os/execpath" 28 | "github.com/rogpeppe/testscript/internal/par" 29 | "github.com/rogpeppe/testscript/internal/testenv" 30 | "github.com/rogpeppe/testscript/txtar" 31 | ) 32 | 33 | var execCache par.Cache 34 | 35 | // If -testwork is specified, the test prints the name of the temp directory 36 | // and does not remove it when done, so that a programmer can 37 | // poke at the test file tree afterward. 38 | var testWork = flag.Bool("testwork", false, "") 39 | 40 | // Env holds the environment to use at the start of a test script invocation. 41 | type Env struct { 42 | // WorkDir holds the path to the root directory of the 43 | // extracted files. 44 | WorkDir string 45 | // Vars holds the initial set environment variables that will be passed to the 46 | // testscript commands. 47 | Vars []string 48 | // Cd holds the initial current working directory. 49 | Cd string 50 | // Values holds a map of arbitrary values for use by custom 51 | // testscript commands. This enables Setup to pass arbitrary 52 | // values (not just strings) through to custom commands. 53 | Values map[interface{}]interface{} 54 | 55 | ts *TestScript 56 | } 57 | 58 | // Value returns a value from Env.Values, or nil if no 59 | // value was set by Setup. 60 | func (ts *TestScript) Value(key interface{}) interface{} { 61 | return ts.values[key] 62 | } 63 | 64 | // Defer arranges for f to be called at the end 65 | // of the test. If Defer is called multiple times, the 66 | // defers are executed in reverse order (similar 67 | // to Go's defer statement) 68 | func (e *Env) Defer(f func()) { 69 | e.ts.Defer(f) 70 | } 71 | 72 | // Params holds parameters for a call to Run. 73 | type Params struct { 74 | // Dir holds the name of the directory holding the scripts. 75 | // All files in the directory with a .txt suffix will be considered 76 | // as test scripts. By default the current directory is used. 77 | // Dir is interpreted relative to the current test directory. 78 | Dir string 79 | 80 | // Setup is called, if not nil, to complete any setup required 81 | // for a test. The WorkDir and Vars fields will have already 82 | // been initialized and all the files extracted into WorkDir, 83 | // and Cd will be the same as WorkDir. 84 | // The Setup function may modify Vars and Cd as it wishes. 85 | Setup func(*Env) error 86 | 87 | // Condition is called, if not nil, to determine whether a particular 88 | // condition is true. It's called only for conditions not in the 89 | // standard set, and may be nil. 90 | Condition func(cond string) (bool, error) 91 | 92 | // Cmds holds a map of commands available to the script. 93 | // It will only be consulted for commands not part of the standard set. 94 | Cmds map[string]func(ts *TestScript, neg bool, args []string) 95 | 96 | // TestWork specifies that working directories should be 97 | // left intact for later inspection. 98 | TestWork bool 99 | 100 | // IgnoreMissedCoverage specifies that if coverage information 101 | // is being generated (with the -test.coverprofile flag) and a subcommand 102 | // function passed to RunMain fails to generate coverage information 103 | // (for example because the function invoked os.Exit), then the 104 | // error will be ignored. 105 | IgnoreMissedCoverage bool 106 | 107 | // UpdateScripts specifies that if a `cmp` command fails and 108 | // its first argument is `stdout` or `stderr` and its second argument 109 | // refers to a file inside the testscript file, the command will succeed 110 | // and the testscript file will be updated to reflect the actual output. 111 | // 112 | // The content will be quoted with txtar.Quote if needed; 113 | // a manual change will be needed if it is not unquoted in the 114 | // script. 115 | UpdateScripts bool 116 | } 117 | 118 | // RunDir runs the tests in the given directory. All files in dir with a ".txt" 119 | // are considered to be test files. 120 | func Run(t *testing.T, p Params) { 121 | RunT(tshim{t}, p) 122 | } 123 | 124 | // T holds all the methods of the *testing.T type that 125 | // are used by testscript. 126 | type T interface { 127 | Skip(...interface{}) 128 | Fatal(...interface{}) 129 | Parallel() 130 | Log(...interface{}) 131 | FailNow() 132 | Run(string, func(T)) 133 | // Verbose is usually implemented by the testing package 134 | // directly rather than on the *testing.T type. 135 | Verbose() bool 136 | } 137 | 138 | type tshim struct { 139 | *testing.T 140 | } 141 | 142 | func (t tshim) Run(name string, f func(T)) { 143 | t.T.Run(name, func(t *testing.T) { 144 | f(tshim{t}) 145 | }) 146 | } 147 | 148 | func (t tshim) Verbose() bool { 149 | return testing.Verbose() 150 | } 151 | 152 | // RunT is like Run but uses an interface type instead of the concrete *testing.T 153 | // type to make it possible to use testscript functionality outside of go test. 154 | func RunT(t T, p Params) { 155 | files, err := filepath.Glob(filepath.Join(p.Dir, "*.txt")) 156 | if err != nil { 157 | t.Fatal(err) 158 | } 159 | testTempDir, err := ioutil.TempDir(os.Getenv("GOTMPDIR"), "go-test-script") 160 | if err != nil { 161 | t.Fatal(err) 162 | } 163 | // The temp dir returned by ioutil.TempDir might be a sym linked dir (default 164 | // behaviour in macOS). That could mess up matching that includes $WORK if, 165 | // for example, an external program outputs resolved paths. Evaluating the 166 | // dir here will ensure consistency. 167 | testTempDir, err = filepath.EvalSymlinks(testTempDir) 168 | if err != nil { 169 | t.Fatal(err) 170 | } 171 | refCount := int32(len(files)) 172 | for _, file := range files { 173 | file := file 174 | name := strings.TrimSuffix(filepath.Base(file), ".txt") 175 | t.Run(name, func(t T) { 176 | t.Parallel() 177 | ts := &TestScript{ 178 | t: t, 179 | testTempDir: testTempDir, 180 | name: name, 181 | file: file, 182 | params: p, 183 | ctxt: context.Background(), 184 | deferred: func() {}, 185 | scriptFiles: make(map[string]string), 186 | scriptUpdates: make(map[string]string), 187 | } 188 | defer func() { 189 | if p.TestWork || *testWork { 190 | return 191 | } 192 | removeAll(ts.workdir) 193 | if atomic.AddInt32(&refCount, -1) == 0 { 194 | // This is the last subtest to finish. Remove the 195 | // parent directory too. 196 | os.Remove(testTempDir) 197 | } 198 | }() 199 | ts.run() 200 | }) 201 | } 202 | } 203 | 204 | // A TestScript holds execution state for a single test script. 205 | type TestScript struct { 206 | params Params 207 | t T 208 | testTempDir string 209 | workdir string // temporary work dir ($WORK) 210 | log bytes.Buffer // test execution log (printed at end of test) 211 | mark int // offset of next log truncation 212 | cd string // current directory during test execution; initially $WORK/gopath/src 213 | name string // short name of test ("foo") 214 | file string // full file name ("testdata/script/foo.txt") 215 | lineno int // line number currently executing 216 | line string // line currently executing 217 | env []string // environment list (for os/exec) 218 | envMap map[string]string // environment mapping (matches env; on Windows keys are lowercase) 219 | values map[interface{}]interface{} // values for custom commands 220 | stdin string // standard input to next 'go' command; set by 'stdin' command. 221 | stdout string // standard output from last 'go' command; for 'stdout' command 222 | stderr string // standard error from last 'go' command; for 'stderr' command 223 | stopped bool // test wants to stop early 224 | start time.Time // time phase started 225 | background []backgroundCmd // backgrounded 'exec' and 'go' commands 226 | deferred func() // deferred cleanup actions. 227 | archive *txtar.Archive // the testscript being run. 228 | scriptFiles map[string]string // files stored in the txtar archive (absolute paths -> path in script) 229 | scriptUpdates map[string]string // updates to testscript files via UpdateScripts. 230 | 231 | ctxt context.Context // per TestScript context 232 | } 233 | 234 | type backgroundCmd struct { 235 | cmd *exec.Cmd 236 | wait <-chan struct{} 237 | neg bool // if true, cmd should fail 238 | } 239 | 240 | // setup sets up the test execution temporary directory and environment. 241 | // It returns the comment section of the txtar archive. 242 | func (ts *TestScript) setup() string { 243 | ts.workdir = filepath.Join(ts.testTempDir, "script-"+ts.name) 244 | ts.Check(os.MkdirAll(filepath.Join(ts.workdir, "tmp"), 0777)) 245 | env := &Env{ 246 | Vars: []string{ 247 | "WORK=" + ts.workdir, // must be first for ts.abbrev 248 | "PATH=" + os.Getenv("PATH"), 249 | homeEnvName() + "=/no-home", 250 | tempEnvName() + "=" + filepath.Join(ts.workdir, "tmp"), 251 | "devnull=" + os.DevNull, 252 | ":=" + string(os.PathListSeparator), 253 | }, 254 | WorkDir: ts.workdir, 255 | Values: make(map[interface{}]interface{}), 256 | Cd: ts.workdir, 257 | ts: ts, 258 | } 259 | // Must preserve SYSTEMROOT on Windows: https://github.com/golang/go/issues/25513 et al 260 | if runtime.GOOS == "windows" { 261 | env.Vars = append(env.Vars, 262 | "SYSTEMROOT="+os.Getenv("SYSTEMROOT"), 263 | "exe=.exe", 264 | ) 265 | } else { 266 | env.Vars = append(env.Vars, 267 | "exe=", 268 | ) 269 | } 270 | ts.cd = env.Cd 271 | // Unpack archive. 272 | a, err := txtar.ParseFile(ts.file) 273 | ts.Check(err) 274 | ts.archive = a 275 | for _, f := range a.Files { 276 | name := ts.MkAbs(ts.expand(f.Name)) 277 | ts.scriptFiles[name] = f.Name 278 | ts.Check(os.MkdirAll(filepath.Dir(name), 0777)) 279 | ts.Check(ioutil.WriteFile(name, f.Data, 0666)) 280 | } 281 | // Run any user-defined setup. 282 | if ts.params.Setup != nil { 283 | ts.Check(ts.params.Setup(env)) 284 | } 285 | ts.cd = env.Cd 286 | ts.env = env.Vars 287 | ts.values = env.Values 288 | 289 | ts.envMap = make(map[string]string) 290 | for _, kv := range ts.env { 291 | if i := strings.Index(kv, "="); i >= 0 { 292 | ts.envMap[envvarname(kv[:i])] = kv[i+1:] 293 | } 294 | } 295 | return string(a.Comment) 296 | } 297 | 298 | // run runs the test script. 299 | func (ts *TestScript) run() { 300 | // Truncate log at end of last phase marker, 301 | // discarding details of successful phase. 302 | rewind := func() { 303 | if !ts.t.Verbose() { 304 | ts.log.Truncate(ts.mark) 305 | } 306 | } 307 | 308 | // Insert elapsed time for phase at end of phase marker 309 | markTime := func() { 310 | if ts.mark > 0 && !ts.start.IsZero() { 311 | afterMark := append([]byte{}, ts.log.Bytes()[ts.mark:]...) 312 | ts.log.Truncate(ts.mark - 1) // cut \n and afterMark 313 | fmt.Fprintf(&ts.log, " (%.3fs)\n", time.Since(ts.start).Seconds()) 314 | ts.log.Write(afterMark) 315 | } 316 | ts.start = time.Time{} 317 | } 318 | 319 | defer func() { 320 | // On a normal exit from the test loop, background processes are cleaned up 321 | // before we print PASS. If we return early (e.g., due to a test failure), 322 | // don't print anything about the processes that were still running. 323 | for _, bg := range ts.background { 324 | interruptProcess(bg.cmd.Process) 325 | } 326 | for _, bg := range ts.background { 327 | <-bg.wait 328 | } 329 | ts.background = nil 330 | 331 | markTime() 332 | // Flush testScript log to testing.T log. 333 | ts.t.Log("\n" + ts.abbrev(ts.log.String())) 334 | }() 335 | defer func() { 336 | ts.deferred() 337 | }() 338 | script := ts.setup() 339 | 340 | // With -v or -testwork, start log with full environment. 341 | if *testWork || ts.t.Verbose() { 342 | // Display environment. 343 | ts.cmdEnv(false, nil) 344 | fmt.Fprintf(&ts.log, "\n") 345 | ts.mark = ts.log.Len() 346 | } 347 | defer ts.applyScriptUpdates() 348 | 349 | // Run script. 350 | // See testdata/script/README for documentation of script form. 351 | Script: 352 | for script != "" { 353 | // Extract next line. 354 | ts.lineno++ 355 | var line string 356 | if i := strings.Index(script, "\n"); i >= 0 { 357 | line, script = script[:i], script[i+1:] 358 | } else { 359 | line, script = script, "" 360 | } 361 | 362 | // # is a comment indicating the start of new phase. 363 | if strings.HasPrefix(line, "#") { 364 | // If there was a previous phase, it succeeded, 365 | // so rewind the log to delete its details (unless -v is in use). 366 | // If nothing has happened at all since the mark, 367 | // rewinding is a no-op and adding elapsed time 368 | // for doing nothing is meaningless, so don't. 369 | if ts.log.Len() > ts.mark { 370 | rewind() 371 | markTime() 372 | } 373 | // Print phase heading and mark start of phase output. 374 | fmt.Fprintf(&ts.log, "%s\n", line) 375 | ts.mark = ts.log.Len() 376 | ts.start = time.Now() 377 | continue 378 | } 379 | 380 | // Parse input line. Ignore blanks entirely. 381 | args := ts.parse(line) 382 | if len(args) == 0 { 383 | continue 384 | } 385 | 386 | // Echo command to log. 387 | fmt.Fprintf(&ts.log, "> %s\n", line) 388 | 389 | // Command prefix [cond] means only run this command if cond is satisfied. 390 | for strings.HasPrefix(args[0], "[") && strings.HasSuffix(args[0], "]") { 391 | cond := args[0] 392 | cond = cond[1 : len(cond)-1] 393 | cond = strings.TrimSpace(cond) 394 | args = args[1:] 395 | if len(args) == 0 { 396 | ts.Fatalf("missing command after condition") 397 | } 398 | want := true 399 | if strings.HasPrefix(cond, "!") { 400 | want = false 401 | cond = strings.TrimSpace(cond[1:]) 402 | } 403 | ok, err := ts.condition(cond) 404 | if err != nil { 405 | ts.Fatalf("bad condition %q: %v", cond, err) 406 | } 407 | if ok != want { 408 | // Don't run rest of line. 409 | continue Script 410 | } 411 | } 412 | 413 | // Command prefix ! means negate the expectations about this command: 414 | // go command should fail, match should not be found, etc. 415 | neg := false 416 | if args[0] == "!" { 417 | neg = true 418 | args = args[1:] 419 | if len(args) == 0 { 420 | ts.Fatalf("! on line by itself") 421 | } 422 | } 423 | 424 | // Run command. 425 | cmd := scriptCmds[args[0]] 426 | if cmd == nil { 427 | cmd = ts.params.Cmds[args[0]] 428 | } 429 | if cmd == nil { 430 | ts.Fatalf("unknown command %q", args[0]) 431 | } 432 | cmd(ts, neg, args[1:]) 433 | 434 | // Command can ask script to stop early. 435 | if ts.stopped { 436 | // Break instead of returning, so that we check the status of any 437 | // background processes and print PASS. 438 | break 439 | } 440 | } 441 | 442 | for _, bg := range ts.background { 443 | interruptProcess(bg.cmd.Process) 444 | } 445 | ts.cmdWait(false, nil) 446 | 447 | // Final phase ended. 448 | rewind() 449 | markTime() 450 | if !ts.stopped { 451 | fmt.Fprintf(&ts.log, "PASS\n") 452 | } 453 | } 454 | 455 | func (ts *TestScript) applyScriptUpdates() { 456 | if len(ts.scriptUpdates) == 0 { 457 | return 458 | } 459 | for name, content := range ts.scriptUpdates { 460 | found := false 461 | for i := range ts.archive.Files { 462 | f := &ts.archive.Files[i] 463 | if f.Name != name { 464 | continue 465 | } 466 | data := []byte(content) 467 | if txtar.NeedsQuote(data) { 468 | data1, err := txtar.Quote(data) 469 | if err != nil { 470 | ts.t.Fatal(fmt.Sprintf("cannot update script file %q: %v", f.Name, err)) 471 | continue 472 | } 473 | data = data1 474 | } 475 | f.Data = data 476 | found = true 477 | } 478 | // Sanity check. 479 | if !found { 480 | panic("script update file not found") 481 | } 482 | } 483 | if err := ioutil.WriteFile(ts.file, txtar.Format(ts.archive), 0666); err != nil { 484 | ts.t.Fatal("cannot update script: ", err) 485 | } 486 | ts.Logf("%s updated", ts.file) 487 | } 488 | 489 | // condition reports whether the given condition is satisfied. 490 | func (ts *TestScript) condition(cond string) (bool, error) { 491 | switch cond { 492 | case "short": 493 | return testing.Short(), nil 494 | case "net": 495 | return testenv.HasExternalNetwork(), nil 496 | case "link": 497 | return testenv.HasLink(), nil 498 | case "symlink": 499 | return testenv.HasSymlink(), nil 500 | case runtime.GOOS, runtime.GOARCH: 501 | return true, nil 502 | default: 503 | if imports.KnownArch[cond] || imports.KnownOS[cond] { 504 | return false, nil 505 | } 506 | if strings.HasPrefix(cond, "exec:") { 507 | prog := cond[len("exec:"):] 508 | ok := execCache.Do(prog, func() interface{} { 509 | _, err := execpath.Look(prog, ts.Getenv) 510 | return err == nil 511 | }).(bool) 512 | return ok, nil 513 | } 514 | if ts.params.Condition != nil { 515 | return ts.params.Condition(cond) 516 | } 517 | ts.Fatalf("unknown condition %q", cond) 518 | panic("unreachable") 519 | } 520 | } 521 | 522 | // Helpers for command implementations. 523 | 524 | // abbrev abbreviates the actual work directory in the string s to the literal string "$WORK". 525 | func (ts *TestScript) abbrev(s string) string { 526 | s = strings.Replace(s, ts.workdir, "$WORK", -1) 527 | if *testWork { 528 | // Expose actual $WORK value in environment dump on first line of work script, 529 | // so that the user can find out what directory -testwork left behind. 530 | s = "WORK=" + ts.workdir + "\n" + strings.TrimPrefix(s, "WORK=$WORK\n") 531 | } 532 | return s 533 | } 534 | 535 | // Defer arranges for f to be called at the end 536 | // of the test. If Defer is called multiple times, the 537 | // defers are executed in reverse order (similar 538 | // to Go's defer statement) 539 | func (ts *TestScript) Defer(f func()) { 540 | old := ts.deferred 541 | ts.deferred = func() { 542 | defer old() 543 | f() 544 | } 545 | } 546 | 547 | // Check calls ts.Fatalf if err != nil. 548 | func (ts *TestScript) Check(err error) { 549 | if err != nil { 550 | ts.Fatalf("%v", err) 551 | } 552 | } 553 | 554 | // Logf appends the given formatted message to the test log transcript. 555 | func (ts *TestScript) Logf(format string, args ...interface{}) { 556 | format = strings.TrimSuffix(format, "\n") 557 | fmt.Fprintf(&ts.log, format, args...) 558 | ts.log.WriteByte('\n') 559 | } 560 | 561 | // exec runs the given command line (an actual subprocess, not simulated) 562 | // in ts.cd with environment ts.env and then returns collected standard output and standard error. 563 | func (ts *TestScript) exec(command string, args ...string) (stdout, stderr string, err error) { 564 | cmd, err := ts.buildExecCmd(command, args...) 565 | if err != nil { 566 | return "", "", err 567 | } 568 | cmd.Dir = ts.cd 569 | cmd.Env = append(ts.env, "PWD="+ts.cd) 570 | cmd.Stdin = strings.NewReader(ts.stdin) 571 | var stdoutBuf, stderrBuf strings.Builder 572 | cmd.Stdout = &stdoutBuf 573 | cmd.Stderr = &stderrBuf 574 | if err = cmd.Start(); err == nil { 575 | err = ctxWait(ts.ctxt, cmd) 576 | } 577 | ts.stdin = "" 578 | return stdoutBuf.String(), stderrBuf.String(), err 579 | } 580 | 581 | // execBackground starts the given command line (an actual subprocess, not simulated) 582 | // in ts.cd with environment ts.env. 583 | func (ts *TestScript) execBackground(command string, args ...string) (*exec.Cmd, error) { 584 | cmd, err := ts.buildExecCmd(command, args...) 585 | if err != nil { 586 | return nil, err 587 | } 588 | cmd.Dir = ts.cd 589 | cmd.Env = append(ts.env, "PWD="+ts.cd) 590 | var stdoutBuf, stderrBuf strings.Builder 591 | cmd.Stdin = strings.NewReader(ts.stdin) 592 | cmd.Stdout = &stdoutBuf 593 | cmd.Stderr = &stderrBuf 594 | ts.stdin = "" 595 | return cmd, cmd.Start() 596 | } 597 | 598 | func (ts *TestScript) buildExecCmd(command string, args ...string) (*exec.Cmd, error) { 599 | if filepath.Base(command) == command { 600 | if lp, err := execpath.Look(command, ts.Getenv); err != nil { 601 | return nil, err 602 | } else { 603 | command = lp 604 | } 605 | } 606 | return exec.Command(command, args...), nil 607 | } 608 | 609 | // BackgroundCmds returns a slice containing all the commands that have 610 | // been started in the background since the most recent wait command, or 611 | // the start of the script if wait has not been called. 612 | func (ts *TestScript) BackgroundCmds() []*exec.Cmd { 613 | cmds := make([]*exec.Cmd, len(ts.background)) 614 | for i, b := range ts.background { 615 | cmds[i] = b.cmd 616 | } 617 | return cmds 618 | } 619 | 620 | // ctxWait is like cmd.Wait, but terminates cmd with os.Interrupt if ctx becomes done. 621 | // 622 | // This differs from exec.CommandContext in that it prefers os.Interrupt over os.Kill. 623 | // (See https://golang.org/issue/21135.) 624 | func ctxWait(ctx context.Context, cmd *exec.Cmd) error { 625 | errc := make(chan error, 1) 626 | go func() { errc <- cmd.Wait() }() 627 | 628 | select { 629 | case err := <-errc: 630 | return err 631 | case <-ctx.Done(): 632 | interruptProcess(cmd.Process) 633 | return <-errc 634 | } 635 | } 636 | 637 | // interruptProcess sends os.Interrupt to p if supported, or os.Kill otherwise. 638 | func interruptProcess(p *os.Process) { 639 | if err := p.Signal(os.Interrupt); err != nil { 640 | // Per https://golang.org/pkg/os/#Signal, “Interrupt is not implemented on 641 | // Windows; using it with os.Process.Signal will return an error.” 642 | // Fall back to Kill instead. 643 | p.Kill() 644 | } 645 | } 646 | 647 | // Exec runs the given command and saves its stdout and stderr so 648 | // they can be inspected by subsequent script commands. 649 | func (ts *TestScript) Exec(command string, args ...string) error { 650 | var err error 651 | ts.stdout, ts.stderr, err = ts.exec(command, args...) 652 | if ts.stdout != "" { 653 | ts.Logf("[stdout]\n%s", ts.stdout) 654 | } 655 | if ts.stderr != "" { 656 | ts.Logf("[stderr]\n%s", ts.stderr) 657 | } 658 | return err 659 | } 660 | 661 | // expand applies environment variable expansion to the string s. 662 | func (ts *TestScript) expand(s string) string { 663 | return os.Expand(s, func(key string) string { 664 | if key1 := strings.TrimSuffix(key, "@R"); len(key1) != len(key) { 665 | return regexp.QuoteMeta(ts.Getenv(key1)) 666 | } 667 | return ts.Getenv(key) 668 | }) 669 | } 670 | 671 | // fatalf aborts the test with the given failure message. 672 | func (ts *TestScript) Fatalf(format string, args ...interface{}) { 673 | fmt.Fprintf(&ts.log, "FAIL: %s:%d: %s\n", ts.file, ts.lineno, fmt.Sprintf(format, args...)) 674 | ts.t.FailNow() 675 | } 676 | 677 | // MkAbs interprets file relative to the test script's current directory 678 | // and returns the corresponding absolute path. 679 | func (ts *TestScript) MkAbs(file string) string { 680 | if filepath.IsAbs(file) { 681 | return file 682 | } 683 | return filepath.Join(ts.cd, file) 684 | } 685 | 686 | // ReadFile returns the contents of the file with the 687 | // given name, intepreted relative to the test script's 688 | // current directory. It interprets "stdout" and "stderr" to 689 | // mean the standard output or standard error from 690 | // the most recent exec or wait command respectively. 691 | // 692 | // If the file cannot be read, the script fails. 693 | func (ts *TestScript) ReadFile(file string) string { 694 | switch file { 695 | case "stdout": 696 | return ts.stdout 697 | case "stderr": 698 | return ts.stderr 699 | default: 700 | file = ts.MkAbs(file) 701 | data, err := ioutil.ReadFile(file) 702 | ts.Check(err) 703 | return string(data) 704 | } 705 | } 706 | 707 | // Setenv sets the value of the environment variable named by the key. 708 | func (ts *TestScript) Setenv(key, value string) { 709 | ts.env = append(ts.env, key+"="+value) 710 | ts.envMap[envvarname(key)] = value 711 | } 712 | 713 | // Getenv gets the value of the environment variable named by the key. 714 | func (ts *TestScript) Getenv(key string) string { 715 | return ts.envMap[envvarname(key)] 716 | } 717 | 718 | // parse parses a single line as a list of space-separated arguments 719 | // subject to environment variable expansion (but not resplitting). 720 | // Single quotes around text disable splitting and expansion. 721 | // To embed a single quote, double it: 'Don''t communicate by sharing memory.' 722 | func (ts *TestScript) parse(line string) []string { 723 | ts.line = line 724 | 725 | var ( 726 | args []string 727 | arg string // text of current arg so far (need to add line[start:i]) 728 | start = -1 // if >= 0, position where current arg text chunk starts 729 | quoted = false // currently processing quoted text 730 | ) 731 | for i := 0; ; i++ { 732 | if !quoted && (i >= len(line) || line[i] == ' ' || line[i] == '\t' || line[i] == '\r' || line[i] == '#') { 733 | // Found arg-separating space. 734 | if start >= 0 { 735 | arg += ts.expand(line[start:i]) 736 | args = append(args, arg) 737 | start = -1 738 | arg = "" 739 | } 740 | if i >= len(line) || line[i] == '#' { 741 | break 742 | } 743 | continue 744 | } 745 | if i >= len(line) { 746 | ts.Fatalf("unterminated quoted argument") 747 | } 748 | if line[i] == '\'' { 749 | if !quoted { 750 | // starting a quoted chunk 751 | if start >= 0 { 752 | arg += ts.expand(line[start:i]) 753 | } 754 | start = i + 1 755 | quoted = true 756 | continue 757 | } 758 | // 'foo''bar' means foo'bar, like in rc shell and Pascal. 759 | if i+1 < len(line) && line[i+1] == '\'' { 760 | arg += line[start:i] 761 | start = i + 1 762 | i++ // skip over second ' before next iteration 763 | continue 764 | } 765 | // ending a quoted chunk 766 | arg += line[start:i] 767 | start = i + 1 768 | quoted = false 769 | continue 770 | } 771 | // found character worth saving; make sure we're saving 772 | if start < 0 { 773 | start = i 774 | } 775 | } 776 | return args 777 | } 778 | 779 | func removeAll(dir string) error { 780 | // module cache has 0444 directories; 781 | // make them writable in order to remove content. 782 | filepath.Walk(dir, func(path string, info os.FileInfo, err error) error { 783 | if err != nil { 784 | return nil // ignore errors walking in file system 785 | } 786 | if info.IsDir() { 787 | os.Chmod(path, 0777) 788 | } 789 | return nil 790 | }) 791 | return os.RemoveAll(dir) 792 | } 793 | 794 | func homeEnvName() string { 795 | switch runtime.GOOS { 796 | case "windows": 797 | return "USERPROFILE" 798 | case "plan9": 799 | return "home" 800 | default: 801 | return "HOME" 802 | } 803 | } 804 | 805 | func tempEnvName() string { 806 | switch runtime.GOOS { 807 | case "windows": 808 | return "TMP" 809 | case "plan9": 810 | return "TMPDIR" // actually plan 9 doesn't have one at all but this is fine 811 | default: 812 | return "TMPDIR" 813 | } 814 | } 815 | --------------------------------------------------------------------------------