New Upstream Snapshot - golang-github-rogpeppe-go-internal
Ready changes
Summary
Merged new upstream version: 1.9.0+git20230209.1.f0583b8 (was: 1.9.0).
Diff
diff --git a/.gitattributes b/.gitattributes
deleted file mode 100644
index 5520980..0000000
--- a/.gitattributes
+++ /dev/null
@@ -1,10 +0,0 @@
-# By default, do not attempt to do any end-of-line conversion upon
-# checkin or checkout.
-* -text
-
-# For specific file-types, force conversion to LF line endings. This
-# can be overridden below or in a more specific .gitattributes if, for
-# example, we want to allow a .txt file to contain a CRLF line ending
-*.go text eol=lf
-*.txt text eol=lf
-*.txtar text eol=lf
diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml
deleted file mode 100644
index 5649f29..0000000
--- a/.github/workflows/test.yml
+++ /dev/null
@@ -1,62 +0,0 @@
-on:
- push:
- branches:
- - master
- pull_request:
- branches:
- - '**'
-name: Test
-jobs:
- test:
- strategy:
- fail-fast: false
- matrix:
- go-version: [1.17.x, 1.18.x]
- os: [ubuntu-latest, macos-latest, windows-latest]
- runs-on: ${{ matrix.os }}
- steps:
- - name: Install Go
- uses: actions/setup-go@v3
- with:
- go-version: ${{ matrix.go-version }}
- - name: Checkout code
- uses: actions/checkout@v3
- - name: Test
- run: |
- go test ./...
- go test -race ./...
-
- - name: Tidy
- if: matrix.os == 'ubuntu-latest' # no need to do this everywhere
- run: |
- go mod tidy
-
- test -z "$(gofmt -d .)" || (gofmt -d . && false)
- test -z "$(git status --porcelain)" || (git status; git diff && false)
-
- test-gotip:
- runs-on: ubuntu-latest
- continue-on-error: true # master breaks sometimes
- steps:
- - name: Install Go
- env:
- GO_COMMIT: 2cfbef438049fd4c3f73d1562773ad1f93900897 # 2022-06-09
- run: |
- cd $HOME
- mkdir $HOME/gotip
- cd $HOME/gotip
-
- wget -O gotip.tar.gz https://go.googlesource.com/go/+archive/${GO_COMMIT}.tar.gz
- tar -xf gotip.tar.gz
- echo "devel go1.19-${GO_COMMIT}" >VERSION
-
- cd src
- ./make.bash
- echo "GOROOT=$HOME/gotip" >>$GITHUB_ENV
- echo "$HOME/gotip/bin" >>$GITHUB_PATH
- - name: Checkout code
- uses: actions/checkout@v3
- - name: Test
- run: |
- go version
- go test ./...
diff --git a/README.md b/README.md
index e21b28d..f2ab973 100644
--- a/README.md
+++ b/README.md
@@ -14,3 +14,7 @@ Included are the following:
- testenv: information on the current testing environment.
- testscript: script-based testing based on txtar files
- txtar: simple text-based file archives for testing.
+
+# Links
+
+- [Test scripts in Go](https://bitfieldconsulting.com/golang/test-scripts)
diff --git a/cmd/testscript/help.go b/cmd/testscript/help.go
index d4808b0..0b3975c 100644
--- a/cmd/testscript/help.go
+++ b/cmd/testscript/help.go
@@ -14,7 +14,7 @@ The testscript command runs github.com/rogpeppe/go-internal/testscript scripts
in a fresh temporary work directory tree.
Usage:
- testscript [-v] [-e VAR[=value]]... [-u] [-work] files...
+ testscript [-v] [-e VAR[=value]]... [-u] [-continue] [-work] files...
The testscript command is designed to make it easy to create self-contained
reproductions of command sequences.
@@ -42,6 +42,9 @@ succeed and the testscript file will be updated to reflect the actual content.
As such, this is the cmd/testcript equivalent of
testscript.Params.UpdateScripts.
+The -continue flag specifies that if an error occurs, the script will continue running.
+All errors will be printed and the exit status will be false.
+
The -work flag prints the temporary work directory path before running each
script, and does not remove that directory when testscript exits.
diff --git a/cmd/testscript/main.go b/cmd/testscript/main.go
index d3b1d25..6b2629d 100644
--- a/cmd/testscript/main.go
+++ b/cmd/testscript/main.go
@@ -65,6 +65,7 @@ func mainerr() (retErr error) {
var envVars envVarsFlag
fUpdate := fs.Bool("u", false, "update archive file if a cmp fails")
fWork := fs.Bool("work", false, "print temporary work directory and do not remove when done")
+ fContinue := fs.Bool("continue", false, "continue running the script if an error occurs")
fVerbose := fs.Bool("v", false, "run tests verbosely")
fs.Var(&envVars, "e", "pass through environment variable to script (can appear multiple times)")
if err := fs.Parse(os.Args[1:]); err != nil {
@@ -101,10 +102,11 @@ func mainerr() (retErr error) {
}
tr := testRunner{
- update: *fUpdate,
- verbose: *fVerbose,
- env: envVars.vals,
- testWork: *fWork,
+ update: *fUpdate,
+ continueOnError: *fContinue,
+ verbose: *fVerbose,
+ env: envVars.vals,
+ testWork: *fWork,
}
dirNames := make(map[string]int)
@@ -139,6 +141,10 @@ type testRunner struct {
// updated in the case of any cmp failures.
update bool
+ // continueOnError indicates that T.FailNow should not panic, allowing the
+ // test script to continue running. Note that T is still marked as failed.
+ continueOnError bool
+
// verbose indicates the running of the script should be noisy.
verbose bool
@@ -203,8 +209,9 @@ func (tr *testRunner) run(runDir, filename string) error {
}
p := testscript.Params{
- Dir: runDir,
- UpdateScripts: tr.update,
+ Dir: runDir,
+ UpdateScripts: tr.update,
+ ContinueOnError: tr.continueOnError,
}
if _, err := exec.LookPath("go"); err == nil {
@@ -271,7 +278,7 @@ func (tr *testRunner) run(runDir, filename string) error {
})
}
- r := runT{
+ r := &runT{
verbose: tr.verbose,
}
@@ -286,6 +293,12 @@ func (tr *testRunner) run(runDir, filename string) error {
}
}()
testscript.RunT(r, p)
+
+ // When continueOnError is true, FailNow does not call panic(failedRun).
+ // We still want err to be set, as the script resulted in a failure.
+ if r.Failed() {
+ err = failedRun
+ }
}()
if err != nil {
@@ -338,40 +351,40 @@ type runT struct {
failed int32
}
-func (r runT) Skip(is ...interface{}) {
+func (r *runT) Skip(is ...interface{}) {
panic(skipRun)
}
-func (r runT) Fatal(is ...interface{}) {
+func (r *runT) Fatal(is ...interface{}) {
r.Log(is...)
r.FailNow()
}
-func (r runT) Parallel() {
+func (r *runT) Parallel() {
// No-op for now; we are currently only running a single script in a
// testscript instance.
}
-func (r runT) Log(is ...interface{}) {
+func (r *runT) Log(is ...interface{}) {
fmt.Print(is...)
}
-func (r runT) FailNow() {
+func (r *runT) FailNow() {
atomic.StoreInt32(&r.failed, 1)
panic(failedRun)
}
-func (r runT) Failed() bool {
+func (r *runT) Failed() bool {
return atomic.LoadInt32(&r.failed) != 0
}
-func (r runT) Run(n string, f func(t testscript.T)) {
+func (r *runT) Run(n string, f func(t testscript.T)) {
// For now we we don't top/tail the run of a subtest. We are currently only
// running a single script in a testscript instance, which means that we
// will only have a single subtest.
f(r)
}
-func (r runT) Verbose() bool {
+func (r *runT) Verbose() bool {
return r.verbose
}
diff --git a/cmd/testscript/testdata/continue.txt b/cmd/testscript/testdata/continue.txt
new file mode 100644
index 0000000..78f6211
--- /dev/null
+++ b/cmd/testscript/testdata/continue.txt
@@ -0,0 +1,16 @@
+# should support -continue
+unquote file.txt
+
+# Running with continue, the testscript command itself
+# should fail, but we should see the results of executing
+# both commands.
+! testscript -continue file.txt
+stdout 'grep banana in'
+stdout 'no match for `banana` found in in'
+stdout 'grep apple in'
+
+-- file.txt --
+>grep banana in
+>grep apple in
+>-- in --
+>apple
diff --git a/cmd/txtar-c/savedir.go b/cmd/txtar-c/savedir.go
index 07f0ab2..2c2246d 100644
--- a/cmd/txtar-c/savedir.go
+++ b/cmd/txtar-c/savedir.go
@@ -10,7 +10,6 @@
//
// See https://godoc.org/github.com/rogpeppe/go-internal/txtar for details of the format
// and how to parse a txtar file.
-//
package main
import (
diff --git a/cmd/txtar-goproxy/main.go b/cmd/txtar-goproxy/main.go
index 161cc76..c4f0553 100644
--- a/cmd/txtar-goproxy/main.go
+++ b/cmd/txtar-goproxy/main.go
@@ -8,9 +8,9 @@
// This allows interactive experimentation with the set of proxied modules.
// For example:
//
-// cd cmd/go
-// go test -proxy=localhost:1234 &
-// export GOPROXY=http://localhost:1234/mod
+// cd cmd/go
+// go test -proxy=localhost:1234 &
+// export GOPROXY=http://localhost:1234/mod
//
// and then run go commands as usual.
package main
diff --git a/cmd/txtar-x/extract.go b/cmd/txtar-x/extract.go
index 38d1a9f..ce3c975 100644
--- a/cmd/txtar-x/extract.go
+++ b/cmd/txtar-x/extract.go
@@ -10,7 +10,6 @@
//
// See https://godoc.org/github.com/rogpeppe/go-internal/txtar for details of the format
// and how to parse a txtar file.
-//
package main
import (
diff --git a/debian/changelog b/debian/changelog
index 8fbc39b..a6d294d 100644
--- a/debian/changelog
+++ b/debian/changelog
@@ -1,3 +1,9 @@
+golang-github-rogpeppe-go-internal (1.9.0+git20230209.1.f0583b8-1) UNRELEASED; urgency=low
+
+ * New upstream snapshot.
+
+ -- Debian Janitor <janitor@jelmer.uk> Sun, 26 Feb 2023 23:29:45 -0000
+
golang-github-rogpeppe-go-internal (1.9.0-1) unstable; urgency=medium
* New upstream version 1.9.0
diff --git a/fmtsort/sort.go b/fmtsort/sort.go
index 0fb5187..7f51854 100644
--- a/fmtsort/sort.go
+++ b/fmtsort/sort.go
@@ -36,19 +36,18 @@ func (o *SortedMap) Swap(i, j int) {
//
// The ordering rules are more general than with Go's < operator:
//
-// - when applicable, nil compares low
-// - ints, floats, and strings order by <
-// - NaN compares less than non-NaN floats
-// - bool compares false before true
-// - complex compares real, then imag
-// - pointers compare by machine address
-// - channel values compare by machine address
-// - structs compare each field in turn
-// - arrays compare each element in turn.
-// Otherwise identical arrays compare by length.
-// - interface values compare first by reflect.Type describing the concrete type
-// and then by concrete value as described in the previous rules.
-//
+// - when applicable, nil compares low
+// - ints, floats, and strings order by <
+// - NaN compares less than non-NaN floats
+// - bool compares false before true
+// - complex compares real, then imag
+// - pointers compare by machine address
+// - channel values compare by machine address
+// - structs compare each field in turn
+// - arrays compare each element in turn.
+// Otherwise identical arrays compare by length.
+// - interface values compare first by reflect.Type describing the concrete type
+// and then by concrete value as described in the previous rules.
func Sort(mapValue reflect.Value) *SortedMap {
if mapValue.Type().Kind() != reflect.Map {
return nil
diff --git a/go.mod b/go.mod
index 3da36bc..459c036 100644
--- a/go.mod
+++ b/go.mod
@@ -1,5 +1,5 @@
module github.com/rogpeppe/go-internal
-go 1.17
+go 1.18
require github.com/pkg/diff v0.0.0-20210226163009-20ebb0f2a09e
diff --git a/gotooltest/setup.go b/gotooltest/setup.go
index 7e56df6..5ce75a1 100644
--- a/gotooltest/setup.go
+++ b/gotooltest/setup.go
@@ -10,6 +10,7 @@ import (
"bytes"
"encoding/json"
"fmt"
+ "os"
"os/exec"
"path/filepath"
"regexp"
@@ -36,12 +37,30 @@ var (
// initGoEnv initialises goEnv. It should only be called using goEnv.once.Do,
// as in Setup.
-func initGoEnv() error {
- var err error
+//
+// Run all of these probe commands in a temporary directory, so as not to make
+// any assumptions about the caller's working directory.
+func initGoEnv() (err error) {
+ td, err := os.MkdirTemp("", "gotooltest-initGoEnv")
+ if err != nil {
+ return fmt.Errorf("failed to create temporary directory for go command tests: %w", err)
+ }
+ defer func() {
+ if rerr := os.RemoveAll(td); rerr != nil && err == nil {
+ err = fmt.Errorf("failed to remove temporary directory for go command tests: %w", rerr)
+ }
+ }()
+
+ // Write a temporary go.mod file in td. This ensures that we create
+ // a porcelain environment in which to run these probe commands.
+ if err := os.WriteFile(filepath.Join(td, "go.mod"), []byte("module gotooltest"), 0600); err != nil {
+ return fmt.Errorf("failed to write temporary go.mod file: %w", err)
+ }
run := func(args ...string) (*bytes.Buffer, *bytes.Buffer, error) {
var stdout, stderr bytes.Buffer
cmd := exec.Command(args[0], args[1:]...)
+ cmd.Dir = td
cmd.Stdout = &stdout
cmd.Stderr = &stderr
return &stdout, &stderr, cmd.Run()
diff --git a/gotooltest/setup_test.go b/gotooltest/setup_test.go
new file mode 100644
index 0000000..c8fc297
--- /dev/null
+++ b/gotooltest/setup_test.go
@@ -0,0 +1,30 @@
+package gotooltest
+
+import (
+ "os"
+ "testing"
+)
+
+func TestInitGoEnv(t *testing.T) {
+ // Set up a temp directory containing a bad go.mod file to
+ // ensure the working directory does not influence the probe
+ // commands run during initGoEnv
+ td := t.TempDir()
+
+ // If these commands fail we are in bigger trouble
+ wd, _ := os.Getwd()
+ os.Chdir(td)
+
+ t.Cleanup(func() {
+ os.Chdir(wd)
+ os.Remove(td)
+ })
+
+ if err := os.WriteFile("go.mod", []byte("this is rubbish"), 0600); err != nil {
+ t.Fatal(err)
+ }
+
+ if err := initGoEnv(); err != nil {
+ t.Fatal(err)
+ }
+}
diff --git a/gotooltest/testdata/cover.txt b/gotooltest/testdata/cover.txt
index b3d13e2..18a3b16 100644
--- a/gotooltest/testdata/cover.txt
+++ b/gotooltest/testdata/cover.txt
@@ -1,7 +1,5 @@
unquote scripts/exec.txt
-[darwin] skip 'Pending a fix for github.com/rogpeppe/go-internal/issues/130'
-
# The module uses testscript itself.
# Use the checked out module, based on where the test binary ran.
go mod edit -replace=github.com/rogpeppe/go-internal=${GOINTERNAL_MODULE}
@@ -10,18 +8,17 @@ go mod tidy
# First, a 'go test' run without coverage.
go test -vet=off
stdout 'PASS'
-! stdout 'total coverage'
+! stdout 'coverage'
# Then, a 'go test' run with -coverprofile.
-# Assuming testscript works well, this results in the basic coverage being 0%,
-# since the test binary does not directly run any non-test code.
-# The total coverage after merging profiles should end up being 100%,
-# as long as all three sub-profiles are accounted for.
+# The total coverage after merging profiles should end up being 100%.
# Marking all printlns as covered requires all edge cases to work well.
+# Go 1.20 learned to produce and merge multiple coverage profiles,
+# so versions before then report a shallow 0% coverage.
go test -vet=off -coverprofile=cover.out -v
stdout 'PASS'
-stdout '^coverage: 0\.0%'
-stdout '^total coverage: 100\.0%'
+[go1.20] stdout 'coverage: 100\.0%'
+[!go1.20] stdout 'coverage: 0\.0%'
! stdout 'malformed coverage' # written by "go test" if cover.out is invalid
exists cover.out
diff --git a/imports/build.go b/imports/build.go
index 5fd0106..891295c 100644
--- a/imports/build.go
+++ b/imports/build.go
@@ -32,7 +32,6 @@ var slashslash = []byte("//")
// the purpose of satisfying build tags, in order to estimate
// (conservatively) whether a file could ever possibly be used
// in any build.
-//
func ShouldBuild(content []byte, tags map[string]bool) bool {
// Pass 1. Identify leading run of // comments and blank lines,
// which must be followed by a blank line.
@@ -96,7 +95,6 @@ func ShouldBuild(content []byte, tags map[string]bool) bool {
// tag (if tags[tag] is true)
// !tag (if tags[tag] is false)
// a comma-separated list of any of these
-//
func matchTags(name string, tags map[string]bool) bool {
if name == "" {
return false
@@ -145,12 +143,12 @@ func matchTag(name string, tags map[string]bool, want bool) bool {
// suffix which does not match the current system.
// The recognized name formats are:
//
-// name_$(GOOS).*
-// name_$(GOARCH).*
-// name_$(GOOS)_$(GOARCH).*
-// name_$(GOOS)_test.*
-// name_$(GOARCH)_test.*
-// name_$(GOOS)_$(GOARCH)_test.*
+// name_$(GOOS).*
+// name_$(GOARCH).*
+// name_$(GOOS)_$(GOARCH).*
+// name_$(GOOS)_test.*
+// name_$(GOARCH)_test.*
+// name_$(GOOS)_$(GOARCH)_test.*
//
// An exception: if GOOS=android, then files with GOOS=linux are also matched.
//
diff --git a/internal/syscall/windows/registry/key.go b/internal/syscall/windows/registry/key.go
index fb89e39..8ac59d2 100644
--- a/internal/syscall/windows/registry/key.go
+++ b/internal/syscall/windows/registry/key.go
@@ -23,7 +23,6 @@
//
// NOTE: This package is a copy of golang.org/x/sys/windows/registry
// with KeyInfo.ModTime removed to prevent dependency cycles.
-//
package registry
import (
diff --git a/lockedfile/internal/filelock/filelock.go b/lockedfile/internal/filelock/filelock.go
index aba3eed..05f27c3 100644
--- a/lockedfile/internal/filelock/filelock.go
+++ b/lockedfile/internal/filelock/filelock.go
@@ -9,6 +9,7 @@ package filelock
import (
"errors"
+ "io/fs"
"os"
)
@@ -24,7 +25,7 @@ type File interface {
Fd() uintptr
// Stat returns the FileInfo structure describing file.
- Stat() (os.FileInfo, error)
+ Stat() (fs.FileInfo, error)
}
// Lock places an advisory write lock on the file, blocking until it can be
@@ -87,7 +88,7 @@ var ErrNotSupported = errors.New("operation not supported")
// underlyingError returns the underlying error for known os error types.
func underlyingError(err error) error {
switch err := err.(type) {
- case *os.PathError:
+ case *fs.PathError:
return err.Err
case *os.LinkError:
return err.Err
diff --git a/lockedfile/internal/filelock/filelock_fcntl.go b/lockedfile/internal/filelock/filelock_fcntl.go
index 4e64481..3098519 100644
--- a/lockedfile/internal/filelock/filelock_fcntl.go
+++ b/lockedfile/internal/filelock/filelock_fcntl.go
@@ -2,8 +2,8 @@
// Use of this source code is governed by a BSD-style
// license that can be found in the LICENSE file.
-//go:build aix || solaris
-// +build aix solaris
+//go:build aix || (solaris && !illumos)
+// +build aix solaris,!illumos
// This code implements the filelock API using POSIX 'fcntl' locks, which attach
// to an (inode, process) pair rather than a file descriptor. To avoid unlocking
@@ -13,17 +13,14 @@
// Most platforms provide some alternative API, such as an 'flock' system call
// or an F_OFD_SETLK command for 'fcntl', that allows for better concurrency and
// does not require per-inode bookkeeping in the application.
-//
-// TODO(golang.org/issue/35618): add a syscall.Flock binding for Illumos and
-// switch it over to use filelock_unix.go.
package filelock
import (
"errors"
"io"
+ "io/fs"
"math/rand"
- "os"
"sync"
"syscall"
"time"
@@ -43,8 +40,6 @@ type inodeLock struct {
queue []<-chan File
}
-type token struct{}
-
var (
mu sync.Mutex
inodes = map[File]inode{}
@@ -65,7 +60,7 @@ func lock(f File, lt lockType) (err error) {
mu.Lock()
if i, dup := inodes[f]; dup && i != ino {
mu.Unlock()
- return &os.PathError{
+ return &fs.PathError{
Op: lt.String(),
Path: f.Name(),
Err: errors.New("inode for file changed since last Lock or RLock"),
@@ -156,7 +151,7 @@ func lock(f File, lt lockType) (err error) {
if err != nil {
unlock(f)
- return &os.PathError{
+ return &fs.PathError{
Op: lt.String(),
Path: f.Name(),
Err: err,
diff --git a/lockedfile/internal/filelock/filelock_other.go b/lockedfile/internal/filelock/filelock_other.go
index cfc5338..70f5d7a 100644
--- a/lockedfile/internal/filelock/filelock_other.go
+++ b/lockedfile/internal/filelock/filelock_other.go
@@ -7,7 +7,7 @@
package filelock
-import "os"
+import "io/fs"
type lockType int8
@@ -17,7 +17,7 @@ const (
)
func lock(f File, lt lockType) error {
- return &os.PathError{
+ return &fs.PathError{
Op: lt.String(),
Path: f.Name(),
Err: ErrNotSupported,
@@ -25,7 +25,7 @@ func lock(f File, lt lockType) error {
}
func unlock(f File) error {
- return &os.PathError{
+ return &fs.PathError{
Op: "Unlock",
Path: f.Name(),
Err: ErrNotSupported,
diff --git a/lockedfile/internal/filelock/filelock_plan9.go b/lockedfile/internal/filelock/filelock_plan9.go
index 5ae3cc2..908afb6 100644
--- a/lockedfile/internal/filelock/filelock_plan9.go
+++ b/lockedfile/internal/filelock/filelock_plan9.go
@@ -7,9 +7,7 @@
package filelock
-import (
- "os"
-)
+import "io/fs"
type lockType int8
@@ -19,7 +17,7 @@ const (
)
func lock(f File, lt lockType) error {
- return &os.PathError{
+ return &fs.PathError{
Op: lt.String(),
Path: f.Name(),
Err: ErrNotSupported,
@@ -27,7 +25,7 @@ func lock(f File, lt lockType) error {
}
func unlock(f File) error {
- return &os.PathError{
+ return &fs.PathError{
Op: "Unlock",
Path: f.Name(),
Err: ErrNotSupported,
diff --git a/lockedfile/internal/filelock/filelock_test.go b/lockedfile/internal/filelock/filelock_test.go
index 925f641..c278636 100644
--- a/lockedfile/internal/filelock/filelock_test.go
+++ b/lockedfile/internal/filelock/filelock_test.go
@@ -2,14 +2,13 @@
// Use of this source code is governed by a BSD-style
// license that can be found in the LICENSE file.
-//go:build !js && !nacl && !plan9
-// +build !js,!nacl,!plan9
+//go:build !js && !plan9
+// +build !js,!plan9
package filelock_test
import (
"fmt"
- "io/ioutil"
"os"
"os/exec"
"path/filepath"
@@ -17,8 +16,6 @@ import (
"testing"
"time"
- "github.com/rogpeppe/go-internal/testenv"
-
"github.com/rogpeppe/go-internal/lockedfile/internal/filelock"
)
@@ -53,9 +50,9 @@ func mustTempFile(t *testing.T) (f *os.File, remove func()) {
t.Helper()
base := filepath.Base(t.Name())
- f, err := ioutil.TempFile("", base)
+ f, err := os.CreateTemp("", base)
if err != nil {
- t.Fatalf(`ioutil.TempFile("", %q) = %v`, base, err)
+ t.Fatalf(`os.CreateTemp("", %q) = %v`, base, err)
}
t.Logf("fd %d = %s", f.Fd(), f.Name())
@@ -161,7 +158,9 @@ func TestRLockExcludesOnlyLock(t *testing.T) {
f2 := mustOpen(t, f.Name())
defer f2.Close()
- if runtime.GOOS == "solaris" || runtime.GOOS == "aix" {
+ doUnlockTF := false
+ switch runtime.GOOS {
+ case "aix", "solaris":
// When using POSIX locks (as on Solaris), we can't safely read-lock the
// same inode through two different descriptors at the same time: when the
// first descriptor is closed, the second descriptor would still be open but
@@ -169,8 +168,9 @@ func TestRLockExcludesOnlyLock(t *testing.T) {
lockF2 := mustBlock(t, "RLock", f2)
unlock(t, f)
lockF2(t)
- } else {
+ default:
rLock(t, f2)
+ doUnlockTF = true
}
other := mustOpen(t, f.Name())
@@ -178,7 +178,7 @@ func TestRLockExcludesOnlyLock(t *testing.T) {
lockOther := mustBlock(t, "Lock", other)
unlock(t, f2)
- if runtime.GOOS != "solaris" && runtime.GOOS != "aix" {
+ if doUnlockTF {
unlock(t, f)
}
lockOther(t)
@@ -186,8 +186,6 @@ func TestRLockExcludesOnlyLock(t *testing.T) {
}
func TestLockNotDroppedByExecCommand(t *testing.T) {
- testenv.MustHaveExec(t)
-
f, remove := mustTempFile(t)
defer remove()
diff --git a/lockedfile/internal/filelock/filelock_unix.go b/lockedfile/internal/filelock/filelock_unix.go
index 09549ef..878a1e7 100644
--- a/lockedfile/internal/filelock/filelock_unix.go
+++ b/lockedfile/internal/filelock/filelock_unix.go
@@ -2,13 +2,13 @@
// Use of this source code is governed by a BSD-style
// license that can be found in the LICENSE file.
-//go:build darwin || dragonfly || freebsd || linux || netbsd || openbsd
-// +build darwin dragonfly freebsd linux netbsd openbsd
+//go:build darwin || dragonfly || freebsd || illumos || linux || netbsd || openbsd
+// +build darwin dragonfly freebsd illumos linux netbsd openbsd
package filelock
import (
- "os"
+ "io/fs"
"syscall"
)
@@ -27,7 +27,7 @@ func lock(f File, lt lockType) (err error) {
}
}
if err != nil {
- return &os.PathError{
+ return &fs.PathError{
Op: lt.String(),
Path: f.Name(),
Err: err,
diff --git a/lockedfile/internal/filelock/filelock_windows.go b/lockedfile/internal/filelock/filelock_windows.go
index 2bd3eb9..1454aca 100644
--- a/lockedfile/internal/filelock/filelock_windows.go
+++ b/lockedfile/internal/filelock/filelock_windows.go
@@ -8,7 +8,7 @@
package filelock
import (
- "os"
+ "io/fs"
"syscall"
"github.com/rogpeppe/go-internal/internal/syscall/windows"
@@ -36,7 +36,7 @@ func lock(f File, lt lockType) error {
err := windows.LockFileEx(syscall.Handle(f.Fd()), uint32(lt), reserved, allBytes, allBytes, ol)
if err != nil {
- return &os.PathError{
+ return &fs.PathError{
Op: lt.String(),
Path: f.Name(),
Err: err,
@@ -49,7 +49,7 @@ func unlock(f File) error {
ol := new(syscall.Overlapped)
err := windows.UnlockFileEx(syscall.Handle(f.Fd()), reserved, allBytes, allBytes, ol)
if err != nil {
- return &os.PathError{
+ return &fs.PathError{
Op: "Unlock",
Path: f.Name(),
Err: err,
diff --git a/lockedfile/lockedfile.go b/lockedfile/lockedfile.go
index 88f95b4..82e1a89 100644
--- a/lockedfile/lockedfile.go
+++ b/lockedfile/lockedfile.go
@@ -9,7 +9,7 @@ package lockedfile
import (
"fmt"
"io"
- "io/ioutil"
+ "io/fs"
"os"
"runtime"
)
@@ -35,7 +35,7 @@ type osFile struct {
// OpenFile is like os.OpenFile, but returns a locked file.
// If flag includes os.O_WRONLY or os.O_RDWR, the file is write-locked;
// otherwise, it is read-locked.
-func OpenFile(name string, flag int, perm os.FileMode) (*File, error) {
+func OpenFile(name string, flag int, perm fs.FileMode) (*File, error) {
var (
f = new(File)
err error
@@ -64,16 +64,16 @@ func Open(name string) (*File, error) {
// Create is like os.Create, but returns a write-locked file.
func Create(name string) (*File, error) {
- return OpenFile(name, os.O_RDWR|os.O_CREATE|os.O_TRUNC, 0o666)
+ return OpenFile(name, os.O_RDWR|os.O_CREATE|os.O_TRUNC, 0666)
}
-// Edit creates the named file with mode 0o666 (before umask),
+// Edit creates the named file with mode 0666 (before umask),
// but does not truncate existing contents.
//
// If Edit succeeds, methods on the returned File can be used for I/O.
// The associated file descriptor has mode O_RDWR and the file is write-locked.
func Edit(name string) (*File, error) {
- return OpenFile(name, os.O_RDWR|os.O_CREATE, 0o666)
+ return OpenFile(name, os.O_RDWR|os.O_CREATE, 0666)
}
// Close unlocks and closes the underlying file.
@@ -82,10 +82,10 @@ func Edit(name string) (*File, error) {
// non-nil error.
func (f *File) Close() error {
if f.closed {
- return &os.PathError{
+ return &fs.PathError{
Op: "close",
Path: f.Name(),
- Err: os.ErrClosed,
+ Err: fs.ErrClosed,
}
}
f.closed = true
@@ -103,12 +103,12 @@ func Read(name string) ([]byte, error) {
}
defer f.Close()
- return ioutil.ReadAll(f)
+ return io.ReadAll(f)
}
// Write opens the named file (creating it with the given permissions if needed),
// then write-locks it and overwrites it with the given content.
-func Write(name string, content io.Reader, perm os.FileMode) (err error) {
+func Write(name string, content io.Reader, perm fs.FileMode) (err error) {
f, err := OpenFile(name, os.O_WRONLY|os.O_CREATE|os.O_TRUNC, perm)
if err != nil {
return err
@@ -120,3 +120,68 @@ func Write(name string, content io.Reader, perm os.FileMode) (err error) {
}
return err
}
+
+// Transform invokes t with the result of reading the named file, with its lock
+// still held.
+//
+// If t returns a nil error, Transform then writes the returned contents back to
+// the file, making a best effort to preserve existing contents on error.
+//
+// t must not modify the slice passed to it.
+func Transform(name string, t func([]byte) ([]byte, error)) (err error) {
+ f, err := Edit(name)
+ if err != nil {
+ return err
+ }
+ defer f.Close()
+
+ old, err := io.ReadAll(f)
+ if err != nil {
+ return err
+ }
+
+ new, err := t(old)
+ if err != nil {
+ return err
+ }
+
+ if len(new) > len(old) {
+ // The overall file size is increasing, so write the tail first: if we're
+ // about to run out of space on the disk, we would rather detect that
+ // failure before we have overwritten the original contents.
+ if _, err := f.WriteAt(new[len(old):], int64(len(old))); err != nil {
+ // Make a best effort to remove the incomplete tail.
+ f.Truncate(int64(len(old)))
+ return err
+ }
+ }
+
+ // We're about to overwrite the old contents. In case of failure, make a best
+ // effort to roll back before we close the file.
+ defer func() {
+ if err != nil {
+ if _, err := f.WriteAt(old, 0); err == nil {
+ f.Truncate(int64(len(old)))
+ }
+ }
+ }()
+
+ if len(new) >= len(old) {
+ if _, err := f.WriteAt(new[:len(old)], 0); err != nil {
+ return err
+ }
+ } else {
+ if _, err := f.WriteAt(new, 0); err != nil {
+ return err
+ }
+ // The overall file size is decreasing, so shrink the file to its final size
+ // after writing. We do this after writing (instead of before) so that if
+ // the write fails, enough filesystem space will likely still be reserved
+ // to contain the previous contents.
+ if err := f.Truncate(int64(len(new))); err != nil {
+ return err
+ }
+ }
+
+ return nil
+}
diff --git a/lockedfile/lockedfile_filelock.go b/lockedfile/lockedfile_filelock.go
index 6a03173..454c3a4 100644
--- a/lockedfile/lockedfile_filelock.go
+++ b/lockedfile/lockedfile_filelock.go
@@ -3,17 +3,17 @@
// license that can be found in the LICENSE file.
//go:build !plan9
-// +build !plan9
package lockedfile
import (
+ "io/fs"
"os"
"github.com/rogpeppe/go-internal/lockedfile/internal/filelock"
)
-func openFile(name string, flag int, perm os.FileMode) (*os.File, error) {
+func openFile(name string, flag int, perm fs.FileMode) (*os.File, error) {
// On BSD systems, we could add the O_SHLOCK or O_EXLOCK flag to the OpenFile
// call instead of locking separately, but we have to support separate locking
// calls for Linux and Windows anyway, so it's simpler to use that approach
diff --git a/lockedfile/lockedfile_plan9.go b/lockedfile/lockedfile_plan9.go
index 02221c5..a2ce794 100644
--- a/lockedfile/lockedfile_plan9.go
+++ b/lockedfile/lockedfile_plan9.go
@@ -3,11 +3,11 @@
// license that can be found in the LICENSE file.
//go:build plan9
-// +build plan9
package lockedfile
import (
+ "io/fs"
"math/rand"
"os"
"strings"
@@ -17,9 +17,9 @@ import (
// Opening an exclusive-use file returns an error.
// The expected error strings are:
//
-// - "open/create -- file is locked" (cwfs, kfs)
-// - "exclusive lock" (fossil)
-// - "exclusive use file already open" (ramfs)
+// - "open/create -- file is locked" (cwfs, kfs)
+// - "exclusive lock" (fossil)
+// - "exclusive use file already open" (ramfs)
var lockedErrStrings = [...]string{
"file is locked",
"exclusive lock",
@@ -42,7 +42,7 @@ func isLocked(err error) bool {
return false
}
-func openFile(name string, flag int, perm os.FileMode) (*os.File, error) {
+func openFile(name string, flag int, perm fs.FileMode) (*os.File, error) {
// Plan 9 uses a mode bit instead of explicit lock/unlock syscalls.
//
// Per http://man.cat-v.org/plan_9/5/stat: “Exclusive use files may be open
@@ -57,8 +57,8 @@ func openFile(name string, flag int, perm os.FileMode) (*os.File, error) {
// have the ModeExclusive bit set. Set it before we call OpenFile, so that we
// can be confident that a successful OpenFile implies exclusive use.
if fi, err := os.Stat(name); err == nil {
- if fi.Mode()&os.ModeExclusive == 0 {
- if err := os.Chmod(name, fi.Mode()|os.ModeExclusive); err != nil {
+ if fi.Mode()&fs.ModeExclusive == 0 {
+ if err := os.Chmod(name, fi.Mode()|fs.ModeExclusive); err != nil {
return nil, err
}
}
@@ -69,7 +69,7 @@ func openFile(name string, flag int, perm os.FileMode) (*os.File, error) {
nextSleep := 1 * time.Millisecond
const maxSleep = 500 * time.Millisecond
for {
- f, err := os.OpenFile(name, flag, perm|os.ModeExclusive)
+ f, err := os.OpenFile(name, flag, perm|fs.ModeExclusive)
if err == nil {
return f, nil
}
diff --git a/lockedfile/lockedfile_test.go b/lockedfile/lockedfile_test.go
index bc51de5..39cd7c9 100644
--- a/lockedfile/lockedfile_test.go
+++ b/lockedfile/lockedfile_test.go
@@ -2,30 +2,27 @@
// Use of this source code is governed by a BSD-style
// license that can be found in the LICENSE file.
-// js and nacl do not support inter-process file locking.
-//go:build !js && !nacl
-// +build !js,!nacl
+// js does not support inter-process file locking.
+//
+//go:build !js
package lockedfile_test
import (
"fmt"
- "io/ioutil"
"os"
"os/exec"
"path/filepath"
"testing"
"time"
- "github.com/rogpeppe/go-internal/testenv"
-
"github.com/rogpeppe/go-internal/lockedfile"
)
func mustTempDir(t *testing.T) (dir string, remove func()) {
t.Helper()
- dir, err := ioutil.TempDir("", filepath.Base(t.Name()))
+ dir, err := os.MkdirTemp("", filepath.Base(t.Name()))
if err != nil {
t.Fatal(err)
}
@@ -157,8 +154,8 @@ func TestCanLockExistingFile(t *testing.T) {
defer remove()
path := filepath.Join(dir, "existing.txt")
- if err := ioutil.WriteFile(path, []byte("ok"), 0o777); err != nil {
- t.Fatalf("ioutil.WriteFile: %v", err)
+ if err := os.WriteFile(path, []byte("ok"), 0777); err != nil {
+ t.Fatalf("os.WriteFile: %v", err)
}
f, err := lockedfile.Edit(path)
@@ -191,8 +188,6 @@ func TestSpuriousEDEADLK(t *testing.T) {
// P.2 unblocks and locks file B.
// P.2 unlocks file B.
- testenv.MustHaveExec(t)
-
dirVar := t.Name() + "DIR"
if dir := os.Getenv(dirVar); dir != "" {
@@ -203,7 +198,7 @@ func TestSpuriousEDEADLK(t *testing.T) {
}
defer b.Close()
- if err := ioutil.WriteFile(filepath.Join(dir, "locked"), []byte("ok"), 0o666); err != nil {
+ if err := os.WriteFile(filepath.Join(dir, "locked"), []byte("ok"), 0666); err != nil {
t.Fatal(err)
}
diff --git a/lockedfile/mutex.go b/lockedfile/mutex.go
index de3be57..180a36c 100644
--- a/lockedfile/mutex.go
+++ b/lockedfile/mutex.go
@@ -7,6 +7,7 @@ package lockedfile
import (
"fmt"
"os"
+ "sync"
)
// A Mutex provides mutual exclusion within and across processes by locking a
@@ -21,7 +22,8 @@ import (
// must not be copied after first use. The Path field must be set before first
// use and must not be change thereafter.
type Mutex struct {
- Path string // The path to the well-known lock file. Must be non-empty.
+ Path string // The path to the well-known lock file. Must be non-empty.
+ mu sync.Mutex // A redundant mutex. The race detector doesn't know about file locking, so in tests we may need to lock something that it understands.
}
// MutexAt returns a new Mutex with Path set to the given non-empty path.
@@ -52,9 +54,14 @@ func (mu *Mutex) Lock() (unlock func(), err error) {
// in the future, it should call OpenFile with O_RDONLY and will require the
// files must be readable, so we should not let the caller make any
// assumptions about Mutex working with write-only files.
- f, err := OpenFile(mu.Path, os.O_RDWR|os.O_CREATE, 0o666)
+ f, err := OpenFile(mu.Path, os.O_RDWR|os.O_CREATE, 0666)
if err != nil {
return nil, err
}
- return func() { f.Close() }, nil
+ mu.mu.Lock()
+
+ return func() {
+ mu.mu.Unlock()
+ f.Close()
+ }, nil
}
diff --git a/lockedfile/transform_test.go b/lockedfile/transform_test.go
new file mode 100644
index 0000000..f7488fa
--- /dev/null
+++ b/lockedfile/transform_test.go
@@ -0,0 +1,105 @@
+// Copyright 2019 The Go Authors. All rights reserved.
+// Use of this source code is governed by a BSD-style
+// license that can be found in the LICENSE file.
+
+// js does not support inter-process file locking.
+//
+//go:build !js
+
+package lockedfile_test
+
+import (
+ "bytes"
+ "encoding/binary"
+ "math/rand"
+ "path/filepath"
+ "testing"
+ "time"
+
+ "github.com/rogpeppe/go-internal/lockedfile"
+)
+
+func isPowerOf2(x int) bool {
+ return x > 0 && x&(x-1) == 0
+}
+
+func roundDownToPowerOf2(x int) int {
+ if x <= 0 {
+ panic("nonpositive x")
+ }
+ bit := 1
+ for x != bit {
+ x = x &^ bit
+ bit <<= 1
+ }
+ return x
+}
+
+func TestTransform(t *testing.T) {
+ dir, remove := mustTempDir(t)
+ defer remove()
+ path := filepath.Join(dir, "blob.bin")
+
+ const maxChunkWords = 8 << 10
+ buf := make([]byte, 2*maxChunkWords*8)
+ for i := uint64(0); i < 2*maxChunkWords; i++ {
+ binary.LittleEndian.PutUint64(buf[i*8:], i)
+ }
+ if err := lockedfile.Write(path, bytes.NewReader(buf[:8]), 0666); err != nil {
+ t.Fatal(err)
+ }
+
+ var attempts int64 = 128
+ if !testing.Short() {
+ attempts *= 16
+ }
+ const parallel = 32
+
+ var sem = make(chan bool, parallel)
+
+ for n := attempts; n > 0; n-- {
+ sem <- true
+ go func() {
+ defer func() { <-sem }()
+
+ time.Sleep(time.Duration(rand.Intn(100)) * time.Microsecond)
+ chunkWords := roundDownToPowerOf2(rand.Intn(maxChunkWords) + 1)
+ offset := rand.Intn(chunkWords)
+
+ err := lockedfile.Transform(path, func(data []byte) (chunk []byte, err error) {
+ chunk = buf[offset*8 : (offset+chunkWords)*8]
+
+ if len(data)&^7 != len(data) {
+ t.Errorf("read %d bytes, but each write is an integer multiple of 8 bytes", len(data))
+ return chunk, nil
+ }
+
+ words := len(data) / 8
+ if !isPowerOf2(words) {
+ t.Errorf("read %d 8-byte words, but each write is a power-of-2 number of words", words)
+ return chunk, nil
+ }
+
+ u := binary.LittleEndian.Uint64(data)
+ for i := 1; i < words; i++ {
+ next := binary.LittleEndian.Uint64(data[i*8:])
+ if next != u+1 {
+ t.Errorf("wrote sequential integers, but read integer out of sequence at offset %d", i)
+ return chunk, nil
+ }
+ u = next
+ }
+
+ return chunk, nil
+ })
+
+ if err != nil {
+ t.Errorf("unexpected error from Transform: %v", err)
+ }
+ }()
+ }
+
+ for n := parallel; n > 0; n-- {
+ sem <- true
+ }
+}
diff --git a/modfile/read.go b/modfile/read.go
index 1d81ff1..0e5fa3e 100644
--- a/modfile/read.go
+++ b/modfile/read.go
@@ -244,7 +244,6 @@ func (x *Line) Span() (start, end Position) {
// "x"
// "y"
// )
-//
type LineBlock struct {
Comments
Start Position
diff --git a/testenv/testenv.go b/testenv/testenv.go
index 8f69fe0..bcac6a5 100644
--- a/testenv/testenv.go
+++ b/testenv/testenv.go
@@ -30,7 +30,7 @@ func Builder() string {
return os.Getenv("GO_BUILDER_NAME")
}
-// HasGoBuild reports whether the current system can build programs with ``go build''
+// HasGoBuild reports whether the current system can build programs with “go build”
// and then run them with os.StartProcess or exec.Command.
func HasGoBuild() bool {
if os.Getenv("GO_GCFLAGS") != "" {
@@ -51,7 +51,7 @@ func HasGoBuild() bool {
return true
}
-// MustHaveGoBuild checks that the current system can build programs with ``go build''
+// MustHaveGoBuild checks that the current system can build programs with “go build”
// and then run them with os.StartProcess or exec.Command.
// If not, MustHaveGoBuild calls t.Skip with an explanation.
func MustHaveGoBuild(t testing.TB) {
@@ -63,13 +63,13 @@ func MustHaveGoBuild(t testing.TB) {
}
}
-// HasGoRun reports whether the current system can run programs with ``go run.''
+// HasGoRun reports whether the current system can run programs with “go run.”
func HasGoRun() bool {
// For now, having go run and having go build are the same.
return HasGoBuild()
}
-// MustHaveGoRun checks that the current system can run programs with ``go run.''
+// MustHaveGoRun checks that the current system can run programs with “go run.”
// If not, MustHaveGoRun calls t.Skip with an explanation.
func MustHaveGoRun(t testing.TB) {
if !HasGoRun() {
diff --git a/testscript/cmd.go b/testscript/cmd.go
index 2cf63f7..87c44dc 100644
--- a/testscript/cmd.go
+++ b/testscript/cmd.go
@@ -253,7 +253,7 @@ func (ts *TestScript) cmdExec(neg bool, args []string) {
if err == nil {
wait := make(chan struct{})
go func() {
- ctxWait(ts.ctxt, cmd)
+ waitOrStop(ts.ctxt, cmd, -1)
close(wait)
}()
ts.background = append(ts.background, backgroundCmd{bgName, cmd, wait, neg})
diff --git a/testscript/cover.go b/testscript/cover.go
deleted file mode 100644
index 181605b..0000000
--- a/testscript/cover.go
+++ /dev/null
@@ -1,280 +0,0 @@
-// Copyright 2018 The Go Authors. All rights reserved.
-// Use of this source code is governed by a BSD-style
-// license that can be found in the LICENSE file.
-
-package testscript
-
-import (
- "bufio"
- "errors"
- "fmt"
- "io"
- "os"
- "path/filepath"
- "regexp"
- "strconv"
- "strings"
- "sync/atomic"
- "testing"
-)
-
-// mergeCoverProfile merges the coverage information in f into
-// cover. It assumes that the coverage information in f is
-// always produced from the same binary for every call.
-func mergeCoverProfile(cover *testing.Cover, path string) error {
- f, err := os.Open(path)
- if err != nil {
- return err
- }
- defer f.Close()
- scanner, err := newProfileScanner(f)
- if err != nil {
- return err
- }
- if scanner.Mode() != testing.CoverMode() {
- return errors.New("unexpected coverage mode in subcommand")
- }
- if cover.Mode == "" {
- cover.Mode = scanner.Mode()
- }
- isCount := cover.Mode == "count"
- if cover.Counters == nil {
- cover.Counters = make(map[string][]uint32)
- cover.Blocks = make(map[string][]testing.CoverBlock)
- }
-
- // Note that we rely on the fact that the coverage is written
- // out file-by-file, with all blocks for a file in sequence.
- var (
- filename string
- blockId uint32
- counters []uint32
- blocks []testing.CoverBlock
- )
- flush := func() {
- if len(counters) > 0 {
- cover.Counters[filename] = counters
- cover.Blocks[filename] = blocks
- }
- }
- for scanner.Scan() {
- block := scanner.Block()
- if scanner.Filename() != filename {
- flush()
- filename = scanner.Filename()
- counters = cover.Counters[filename]
- blocks = cover.Blocks[filename]
- blockId = 0
- } else {
- blockId++
- }
- if int(blockId) >= len(counters) {
- counters = append(counters, block.Count)
- blocks = append(blocks, block.CoverBlock)
- continue
- }
- // TODO check that block.CoverBlock == blocks[blockId] ?
- if isCount {
- counters[blockId] += block.Count
- } else {
- counters[blockId] |= block.Count
- }
- }
- flush()
- if scanner.Err() != nil {
- return fmt.Errorf("error scanning profile: %v", err)
- }
- return nil
-}
-
-func finalizeCoverProfile(dir string) error {
- // Merge all the coverage profiles written by test binary sub-processes,
- // such as those generated by executions of commands.
- var cover testing.Cover
- if err := filepath.Walk(dir, func(path string, info os.FileInfo, err error) error {
- if err != nil {
- return err
- }
- if !info.Mode().IsRegular() {
- return nil
- }
- if err := mergeCoverProfile(&cover, path); err != nil {
- return fmt.Errorf("cannot merge coverage profile from %v: %v", path, err)
- }
- return nil
- }); err != nil {
- return err
- }
- if err := os.RemoveAll(dir); err != nil {
- // The RemoveAll seems to fail very rarely, with messages like
- // "directory not empty". It's unclear why.
- // For now, if it happens again, try to print a bit more info.
- filepath.Walk(dir, func(path string, info os.FileInfo, err error) error {
- if err == nil && !info.IsDir() {
- fmt.Fprintln(os.Stderr, "non-directory found after RemoveAll:", path)
- }
- return nil
- })
- return err
- }
-
- // We need to include our own top-level coverage profile too.
- cprof := coverProfile()
- if err := mergeCoverProfile(&cover, cprof); err != nil {
- return fmt.Errorf("cannot merge coverage profile from %v: %v", cprof, err)
- }
-
- // Finally, write the resulting merged profile.
- f, err := os.Create(cprof)
- if err != nil {
- return fmt.Errorf("cannot create cover profile: %v", err)
- }
- defer f.Close()
- w := bufio.NewWriter(f)
- if err := writeCoverProfile1(w, cover); err != nil {
- return err
- }
- if err := w.Flush(); err != nil {
- return err
- }
- if err := f.Close(); err != nil {
- return err
- }
- return nil
-}
-
-func writeCoverProfile1(w io.Writer, cover testing.Cover) error {
- fmt.Fprintf(w, "mode: %s\n", cover.Mode)
- var active, total int64
- var count uint32
- for name, counts := range cover.Counters {
- blocks := cover.Blocks[name]
- for i := range counts {
- stmts := int64(blocks[i].Stmts)
- total += stmts
- count = atomic.LoadUint32(&counts[i]) // For -mode=atomic.
- if count > 0 {
- active += stmts
- }
- _, err := fmt.Fprintf(w, "%s:%d.%d,%d.%d %d %d\n", name,
- blocks[i].Line0, blocks[i].Col0,
- blocks[i].Line1, blocks[i].Col1,
- stmts,
- count,
- )
- if err != nil {
- return err
- }
- }
- }
- if total == 0 {
- total = 1
- }
- fmt.Printf("total coverage: %.1f%% of statements%s\n", 100*float64(active)/float64(total), cover.CoveredPackages)
- return nil
-}
-
-type profileScanner struct {
- mode string
- err error
- scanner *bufio.Scanner
- filename string
- block coverBlock
-}
-
-type coverBlock struct {
- testing.CoverBlock
- Count uint32
-}
-
-var profileLineRe = regexp.MustCompile(`^(.+):([0-9]+)\.([0-9]+),([0-9]+)\.([0-9]+) ([0-9]+) ([0-9]+)$`)
-
-func toInt(s string) int {
- i, err := strconv.Atoi(s)
- if err != nil {
- panic(err)
- }
- return i
-}
-
-func newProfileScanner(r io.Reader) (*profileScanner, error) {
- s := &profileScanner{
- scanner: bufio.NewScanner(r),
- }
- // First line is "mode: foo", where foo is "set", "count", or "atomic".
- // Rest of file is in the format
- // encoding/base64/base64.go:34.44,37.40 3 1
- // where the fields are: name.go:line.column,line.column numberOfStatements count
- if !s.scanner.Scan() {
- return nil, fmt.Errorf("no lines found in profile: %v", s.Err())
- }
- line := s.scanner.Text()
- mode := strings.TrimPrefix(line, "mode: ")
- if len(mode) == len(line) {
- return nil, fmt.Errorf("bad mode line %q", line)
- }
- s.mode = mode
- return s, nil
-}
-
-// Mode returns the profile's coverage mode (one of "atomic", "count:
-// or "set").
-func (s *profileScanner) Mode() string {
- return s.mode
-}
-
-// Err returns any error encountered when scanning a profile.
-func (s *profileScanner) Err() error {
- if s.err == io.EOF {
- return nil
- }
- return s.err
-}
-
-// Block returns the most recently scanned profile block, or the zero
-// block if Scan has not been called or has returned false.
-func (s *profileScanner) Block() coverBlock {
- if s.err == nil {
- return s.block
- }
- return coverBlock{}
-}
-
-// Filename returns the filename of the most recently scanned profile
-// block, or the empty string if Scan has not been called or has
-// returned false.
-func (s *profileScanner) Filename() string {
- if s.err == nil {
- return s.filename
- }
- return ""
-}
-
-// Scan scans the next line in a coverage profile and reports whether
-// a line was found.
-func (s *profileScanner) Scan() bool {
- if s.err != nil {
- return false
- }
- if !s.scanner.Scan() {
- s.err = io.EOF
- return false
- }
- m := profileLineRe.FindStringSubmatch(s.scanner.Text())
- if m == nil {
- s.err = fmt.Errorf("line %q doesn't match expected format %v", m, profileLineRe)
- return false
- }
- s.filename = m[1]
- s.block = coverBlock{
- CoverBlock: testing.CoverBlock{
- Line0: uint32(toInt(m[2])),
- Col0: uint16(toInt(m[3])),
- Line1: uint32(toInt(m[4])),
- Col1: uint16(toInt(m[5])),
- Stmts: uint16(toInt(m[6])),
- },
- Count: uint32(toInt(m[7])),
- }
- return true
-}
diff --git a/testscript/exe.go b/testscript/exe.go
index 46c611a..ed6bd98 100644
--- a/testscript/exe.go
+++ b/testscript/exe.go
@@ -5,9 +5,6 @@
package testscript
import (
- cryptorand "crypto/rand"
- "flag"
- "fmt"
"io"
"io/ioutil"
"log"
@@ -24,15 +21,8 @@ type TestingM interface {
Run() int
}
-var ignoreMissedCoverage = false
-
-// IgnoreMissedCoverage causes any missed coverage information
-// (for example when a function passed to RunMain
-// calls os.Exit, for example) to be ignored.
-// This function should be called before calling RunMain.
-func IgnoreMissedCoverage() {
- ignoreMissedCoverage = true
-}
+// Deprecated: this option is no longer used.
+func IgnoreMissedCoverage() {}
// RunMain should be called within a TestMain function to allow
// subcommands to be run in the testscript context.
@@ -84,26 +74,6 @@ func RunMain(m TestingM, commands map[string]func() int) (exitCode int) {
}
os.Setenv("PATH", bindir+string(filepath.ListSeparator)+os.Getenv("PATH"))
- flag.Parse()
- // If we are collecting a coverage profile, set up a shared
- // directory for all executed test binary sub-processes to write
- // their profiles to. Before finishing, we'll merge all of those
- // profiles into the main profile.
- if coverProfile() != "" {
- coverdir := filepath.Join(tmpdir, "cover")
- if err := os.MkdirAll(coverdir, 0o777); err != nil {
- log.Printf("could not set up cover directory: %v", err)
- return 2
- }
- os.Setenv("TESTSCRIPT_COVER_DIR", coverdir)
- defer func() {
- if err := finalizeCoverProfile(coverdir); err != nil {
- log.Printf("cannot merge cover profiles: %v", err)
- exitCode = 2
- }
- }()
- }
-
// We're not in a subcommand.
for name := range commands {
name := name
@@ -131,31 +101,7 @@ func RunMain(m TestingM, commands map[string]func() int) (exitCode int) {
}
// The command being registered is being invoked, so run it, then exit.
os.Args[0] = cmdName
- coverdir := os.Getenv("TESTSCRIPT_COVER_DIR")
- if coverdir == "" {
- // No coverage, act as normal.
- return mainf()
- }
-
- // For a command "foo", write ${TESTSCRIPT_COVER_DIR}/foo-${RANDOM}.
- // Note that we do not use ioutil.TempFile as that creates the file.
- // In this case, we want to leave it to -test.coverprofile to create the
- // file, as otherwise we could end up with an empty file.
- // Later, when merging profiles, trying to merge an empty file would
- // result in a confusing error.
- rnd, err := nextRandom()
- if err != nil {
- log.Printf("could not obtain random number: %v", err)
- return 2
- }
- cprof := filepath.Join(coverdir, fmt.Sprintf("%s-%x", cmdName, rnd))
- return runCoverSubcommand(cprof, mainf)
-}
-
-func nextRandom() ([]byte, error) {
- p := make([]byte, 6)
- _, err := cryptorand.Read(p)
- return p, err
+ return mainf()
}
// copyBinary makes a copy of a binary to a new location. It is used as part of
@@ -197,78 +143,6 @@ func copyBinary(from, to string) error {
return err
}
-// runCoverSubcommand runs the given function, then writes any generated
-// coverage information to the cprof file.
-// This is called inside a separately run executable.
-func runCoverSubcommand(cprof string, mainf func() int) (exitCode int) {
- // Change the error handling mode to PanicOnError
- // so that in the common case of calling flag.Parse in main we'll
- // be able to catch the panic instead of just exiting.
- flag.CommandLine.Init(flag.CommandLine.Name(), flag.PanicOnError)
- defer func() {
- panicErr := recover()
- if err, ok := panicErr.(error); ok {
- // The flag package will already have printed this error, assuming,
- // that is, that the error was created in the flag package.
- // TODO check the stack to be sure it was actually raised by the flag package.
- exitCode = 2
- if err == flag.ErrHelp {
- exitCode = 0
- }
- panicErr = nil
- }
- // Set os.Args so that flag.Parse will tell testing the correct
- // coverprofile setting. Unfortunately this isn't sufficient because
- // the testing oackage explicitly avoids calling flag.Parse again
- // if flag.Parsed returns true, so we the coverprofile value directly
- // too.
- os.Args = []string{os.Args[0], "-test.coverprofile=" + cprof}
- setCoverProfile(cprof)
-
- // Suppress the chatty coverage and test report.
- devNull, err := os.Open(os.DevNull)
- if err != nil {
- panic(err)
- }
- os.Stdout = devNull
- os.Stderr = devNull
-
- // Run MainStart (recursively, but it we should be ok) with no tests
- // so that it writes the coverage profile.
- m := mainStart()
- if code := m.Run(); code != 0 && exitCode == 0 {
- exitCode = code
- }
- if _, err := os.Stat(cprof); err != nil {
- log.Printf("failed to write coverage profile %q", cprof)
- }
- if panicErr != nil {
- // The error didn't originate from the flag package (we know that
- // flag.PanicOnError causes an error value that implements error),
- // so carry on panicking.
- panic(panicErr)
- }
- }()
- return mainf()
-}
-
-func coverProfileFlag() flag.Getter {
- f := flag.CommandLine.Lookup("test.coverprofile")
- if f == nil {
- // We've imported testing so it definitely should be there.
- panic("cannot find test.coverprofile flag")
- }
- return f.Value.(flag.Getter)
-}
-
-func coverProfile() string {
- return coverProfileFlag().Get().(string)
-}
-
-func setCoverProfile(cprof string) {
- coverProfileFlag().Set(cprof)
-}
-
type nopTestDeps struct{}
func (nopTestDeps) MatchString(pat, str string) (result bool, err error) {
diff --git a/testscript/testdata/testscript_duplicate_name.txt b/testscript/testdata/testscript_duplicate_name.txt
new file mode 100644
index 0000000..b8fd4c6
--- /dev/null
+++ b/testscript/testdata/testscript_duplicate_name.txt
@@ -0,0 +1,12 @@
+# Check that RequireUniqueNames works;
+# it should reject txtar archives with duplicate names as defined by the host system.
+
+unquote scripts-normalized/testscript.txt
+
+testscript scripts-normalized
+! testscript -unique-names scripts-normalized
+stdout '.* would overwrite .* \(because RequireUniqueNames is enabled\)'
+
+-- scripts-normalized/testscript.txt --
+>-- file --
+>-- dir/../file --
\ No newline at end of file
diff --git a/testscript/testdata/testscript_logging.txt b/testscript/testdata/testscript_logging.txt
new file mode 100644
index 0000000..60975fe
--- /dev/null
+++ b/testscript/testdata/testscript_logging.txt
@@ -0,0 +1,108 @@
+# non-verbose, non-continue
+! testscript scripts
+cmpenv stdout expect-stdout.txt
+
+# verbose
+! testscript -v scripts
+cmpenv stdout expect-stdout-v.txt
+
+# continue
+! testscript -continue scripts
+cmpenv stdout expect-stdout-c.txt
+
+# verbose, continue
+! testscript -v -continue scripts
+cmpenv stdout expect-stdout-vc.txt
+
+-- scripts/testscript.txt --
+# comment 1
+printargs section1
+
+# comment 2
+printargs section2
+
+# comment 3
+printargs section3
+status 1
+
+# comment 4
+printargs section3
+
+# comment 5
+printargs section5
+status 1
+
+-- expect-stdout.txt --
+# comment 1 (0.000s)
+# comment 2 (0.000s)
+# comment 3 (0.000s)
+> printargs section3
+[stdout]
+["printargs" "section3"]
+> status 1
+[exit status 1]
+FAIL: $$WORK${/}scripts${/}testscript.txt:9: unexpected command failure
+-- expect-stdout-v.txt --
+# comment 1 (0.000s)
+> printargs section1
+[stdout]
+["printargs" "section1"]
+# comment 2 (0.000s)
+> printargs section2
+[stdout]
+["printargs" "section2"]
+# comment 3 (0.000s)
+> printargs section3
+[stdout]
+["printargs" "section3"]
+> status 1
+[exit status 1]
+FAIL: $$WORK${/}scripts${/}testscript.txt:9: unexpected command failure
+-- expect-stdout-c.txt --
+# comment 1 (0.000s)
+# comment 2 (0.000s)
+# comment 3 (0.000s)
+> printargs section3
+[stdout]
+["printargs" "section3"]
+> status 1
+[exit status 1]
+FAIL: $$WORK${/}scripts${/}testscript.txt:9: unexpected command failure
+# comment 4 (0.000s)
+> printargs section3
+[stdout]
+["printargs" "section3"]
+# comment 5 (0.000s)
+> printargs section5
+[stdout]
+["printargs" "section5"]
+> status 1
+[exit status 1]
+FAIL: $$WORK${/}scripts${/}testscript.txt:16: unexpected command failure
+-- expect-stdout-vc.txt --
+# comment 1 (0.000s)
+> printargs section1
+[stdout]
+["printargs" "section1"]
+# comment 2 (0.000s)
+> printargs section2
+[stdout]
+["printargs" "section2"]
+# comment 3 (0.000s)
+> printargs section3
+[stdout]
+["printargs" "section3"]
+> status 1
+[exit status 1]
+FAIL: $$WORK${/}scripts${/}testscript.txt:9: unexpected command failure
+# comment 4 (0.000s)
+> printargs section3
+[stdout]
+["printargs" "section3"]
+# comment 5 (0.000s)
+> printargs section5
+[stdout]
+["printargs" "section5"]
+> status 1
+[exit status 1]
+FAIL: $$WORK${/}scripts${/}testscript.txt:16: unexpected command failure
diff --git a/testscript/testscript.go b/testscript/testscript.go
index 2b0cd75..f21d267 100644
--- a/testscript/testscript.go
+++ b/testscript/testscript.go
@@ -10,9 +10,11 @@ package testscript
import (
"bytes"
"context"
+ "errors"
"flag"
"fmt"
"go/build"
+ "io/fs"
"io/ioutil"
"os"
"os/exec"
@@ -21,6 +23,7 @@ import (
"runtime"
"strings"
"sync/atomic"
+ "syscall"
"testing"
"time"
@@ -40,6 +43,16 @@ var execCache par.Cache
// poke at the test file tree afterward.
var testWork = flag.Bool("testwork", false, "")
+// timeSince is defined as a variable so that it can be overridden
+// for the local testscript tests so that we can test against predictable
+// output.
+var timeSince = time.Since
+
+// showVerboseEnv specifies whether the environment should be displayed
+// automatically when in verbose mode. This is set to false for the local testscript tests so we
+// can test against predictable output.
+var showVerboseEnv = true
+
// Env holds the environment to use at the start of a test script invocation.
type Env struct {
// WorkDir holds the path to the root directory of the
@@ -139,11 +152,7 @@ type Params struct {
// $GOTMPDIR/go-test-script*, where $GOTMPDIR defaults to os.TempDir().
WorkdirRoot string
- // IgnoreMissedCoverage specifies that if coverage information
- // is being generated (with the -test.coverprofile flag) and a subcommand
- // function passed to RunMain fails to generate coverage information
- // (for example because the function invoked os.Exit), then the
- // error will be ignored.
+ // Deprecated: this option is no longer used.
IgnoreMissedCoverage bool
// UpdateScripts specifies that if a `cmp` command fails and its second
@@ -161,11 +170,28 @@ type Params struct {
// consistency across test scripts as well as keep separate process
// executions explicit.
RequireExplicitExec bool
+
+ // RequireUniqueNames requires that names in the txtar archive are unique.
+ // By default, later entries silently overwrite earlier ones.
+ RequireUniqueNames bool
+
+ // ContinueOnError causes a testscript to try to continue in
+ // the face of errors. Once an error has occurred, the script
+ // will continue as if in verbose mode.
+ ContinueOnError bool
+
+ // Deadline, if not zero, specifies the time at which the test run will have
+ // exceeded the timeout. It is equivalent to testing.T's Deadline method,
+ // and Run will set it to the method's return value if this field is zero.
+ Deadline time.Time
}
// RunDir runs the tests in the given directory. All files in dir with a ".txt"
// or ".txtar" extension are considered to be test files.
func Run(t *testing.T, p Params) {
+ if deadline, ok := t.Deadline(); ok && p.Deadline.IsZero() {
+ p.Deadline = deadline
+ }
RunT(tshim{t}, p)
}
@@ -240,6 +266,37 @@ func RunT(t T, p Params) {
if err != nil {
t.Fatal(err)
}
+
+ var (
+ ctx = context.Background()
+ gracePeriod = 100 * time.Millisecond
+ cancel context.CancelFunc
+ )
+ if !p.Deadline.IsZero() {
+ timeout := time.Until(p.Deadline)
+
+ // If time allows, increase the termination grace period to 5% of the
+ // remaining time.
+ if gp := timeout / 20; gp > gracePeriod {
+ gracePeriod = gp
+ }
+
+ // When we run commands that execute subprocesses, we want to reserve two
+ // grace periods to clean up. We will send the first termination signal when
+ // the context expires, then wait one grace period for the process to
+ // produce whatever useful output it can (such as a stack trace). After the
+ // first grace period expires, we'll escalate to os.Kill, leaving the second
+ // grace period for the test function to record its output before the test
+ // process itself terminates.
+ timeout -= 2 * gracePeriod
+
+ ctx, cancel = context.WithTimeout(ctx, timeout)
+ // We don't defer cancel() because RunT returns before the sub-tests,
+ // and we don't have access to Cleanup due to the T interface. Instead,
+ // we call it after the refCount goes to zero below.
+ _ = cancel
+ }
+
refCount := int32(len(files))
for _, file := range files {
file := file
@@ -253,7 +310,8 @@ func RunT(t T, p Params) {
name: name,
file: file,
params: p,
- ctxt: context.Background(),
+ ctxt: ctx,
+ gracePeriod: gracePeriod,
deferred: func() {},
scriptFiles: make(map[string]string),
scriptUpdates: make(map[string]string),
@@ -265,8 +323,11 @@ func RunT(t T, p Params) {
removeAll(ts.workdir)
if atomic.AddInt32(&refCount, -1) == 0 {
// This is the last subtest to finish. Remove the
- // parent directory too.
+ // parent directory too, and cancel the context.
os.Remove(testTempDir)
+ if cancel != nil {
+ cancel()
+ }
}
}()
ts.run()
@@ -301,7 +362,8 @@ type TestScript struct {
scriptFiles map[string]string // files stored in the txtar archive (absolute paths -> path in script)
scriptUpdates map[string]string // updates to testscript files via UpdateScripts.
- ctxt context.Context // per TestScript context
+ ctxt context.Context // per TestScript context
+ gracePeriod time.Duration // time between SIGQUIT and SIGKILL
}
type backgroundCmd struct {
@@ -311,9 +373,30 @@ type backgroundCmd struct {
neg bool // if true, cmd should fail
}
+func writeFile(name string, data []byte, perm fs.FileMode, excl bool) error {
+ oflags := os.O_WRONLY | os.O_CREATE | os.O_TRUNC
+ if excl {
+ oflags |= os.O_EXCL
+ }
+ f, err := os.OpenFile(name, oflags, perm)
+ if err != nil {
+ return err
+ }
+ defer f.Close()
+ if _, err := f.Write(data); err != nil {
+ return fmt.Errorf("cannot write file contents: %v", err)
+ }
+ return nil
+}
+
// setup sets up the test execution temporary directory and environment.
// It returns the comment section of the txtar archive.
func (ts *TestScript) setup() string {
+ defer catchFailNow(func() {
+ // There's been a failure in setup; fail immediately regardless
+ // of the ContinueOnError flag.
+ ts.t.FailNow()
+ })
ts.workdir = filepath.Join(ts.testTempDir, "script-"+ts.name)
// Establish a temporary directory in workdir, but use a prefix that ensures
@@ -330,6 +413,7 @@ func (ts *TestScript) setup() string {
Vars: []string{
"WORK=" + ts.workdir, // must be first for ts.abbrev
"PATH=" + os.Getenv("PATH"),
+ "GOTRACEBACK=system",
homeEnvName() + "=/no-home",
tempEnvName() + "=" + tmpDir,
"devnull=" + os.DevNull,
@@ -339,7 +423,7 @@ func (ts *TestScript) setup() string {
// If we are collecting coverage profiles for merging into the main one,
// ensure the environment variable is forwarded to sub-processes.
- "TESTSCRIPT_COVER_DIR=" + os.Getenv("TESTSCRIPT_COVER_DIR"),
+ "GOCOVERDIR=" + os.Getenv("GOCOVERDIR"),
},
WorkDir: ts.workdir,
Values: make(map[interface{}]interface{}),
@@ -366,7 +450,12 @@ func (ts *TestScript) setup() string {
name := ts.MkAbs(ts.expand(f.Name))
ts.scriptFiles[name] = f.Name
ts.Check(os.MkdirAll(filepath.Dir(name), 0o777))
- ts.Check(ioutil.WriteFile(name, f.Data, 0o666))
+ switch err := writeFile(name, f.Data, 0o666, ts.params.RequireUniqueNames); {
+ case ts.params.RequireUniqueNames && errors.Is(err, fs.ErrExist):
+ ts.Check(fmt.Errorf("%s would overwrite %s (because RequireUniqueNames is enabled)", f.Name, name))
+ default:
+ ts.Check(err)
+ }
}
// Run any user-defined setup.
if ts.params.Setup != nil {
@@ -389,8 +478,9 @@ func (ts *TestScript) setup() string {
func (ts *TestScript) run() {
// Truncate log at end of last phase marker,
// discarding details of successful phase.
+ verbose := ts.t.Verbose()
rewind := func() {
- if !ts.t.Verbose() {
+ if !verbose {
ts.log.Truncate(ts.mark)
}
}
@@ -400,12 +490,13 @@ func (ts *TestScript) run() {
if ts.mark > 0 && !ts.start.IsZero() {
afterMark := append([]byte{}, ts.log.Bytes()[ts.mark:]...)
ts.log.Truncate(ts.mark - 1) // cut \n and afterMark
- fmt.Fprintf(&ts.log, " (%.3fs)\n", time.Since(ts.start).Seconds())
+ fmt.Fprintf(&ts.log, " (%.3fs)\n", timeSince(ts.start).Seconds())
ts.log.Write(afterMark)
}
ts.start = time.Time{}
}
+ failed := false
defer func() {
// On a normal exit from the test loop, background processes are cleaned up
// before we print PASS. If we return early (e.g., due to a test failure),
@@ -413,7 +504,7 @@ func (ts *TestScript) run() {
for _, bg := range ts.background {
interruptProcess(bg.cmd.Process)
}
- if ts.t.Verbose() || hasFailed(ts.t) {
+ if ts.t.Verbose() || failed {
// In verbose mode or on test failure, we want to see what happened in the background
// processes too.
ts.waitBackground(false)
@@ -434,7 +525,7 @@ func (ts *TestScript) run() {
script := ts.setup()
// With -v or -testwork, start log with full environment.
- if *testWork || ts.t.Verbose() {
+ if *testWork || (showVerboseEnv && ts.t.Verbose()) {
// Display environment.
ts.cmdEnv(false, nil)
fmt.Fprintf(&ts.log, "\n")
@@ -444,7 +535,6 @@ func (ts *TestScript) run() {
// Run script.
// See testdata/script/README for documentation of script form.
-Script:
for script != "" {
// Extract next line.
ts.lineno++
@@ -458,7 +548,9 @@ Script:
// # is a comment indicating the start of new phase.
if strings.HasPrefix(line, "#") {
// If there was a previous phase, it succeeded,
- // so rewind the log to delete its details (unless -v is in use).
+ // so rewind the log to delete its details (unless -v is in use or
+ // ContinueOnError was enabled and there was a previous error,
+ // causing verbose to be set to true).
// If nothing has happened at all since the mark,
// rewinding is a no-op and adding elapsed time
// for doing nothing is meaningless, so don't.
@@ -473,60 +565,16 @@ Script:
continue
}
- // Parse input line. Ignore blanks entirely.
- args := ts.parse(line)
- if len(args) == 0 {
- continue
- }
-
- // Echo command to log.
- fmt.Fprintf(&ts.log, "> %s\n", line)
-
- // Command prefix [cond] means only run this command if cond is satisfied.
- for strings.HasPrefix(args[0], "[") && strings.HasSuffix(args[0], "]") {
- cond := args[0]
- cond = cond[1 : len(cond)-1]
- cond = strings.TrimSpace(cond)
- args = args[1:]
- if len(args) == 0 {
- ts.Fatalf("missing command after condition")
- }
- want := true
- if strings.HasPrefix(cond, "!") {
- want = false
- cond = strings.TrimSpace(cond[1:])
- }
- ok, err := ts.condition(cond)
- if err != nil {
- ts.Fatalf("bad condition %q: %v", cond, err)
- }
- if ok != want {
- // Don't run rest of line.
- continue Script
+ ok := ts.runLine(line)
+ if !ok {
+ failed = true
+ if ts.params.ContinueOnError {
+ verbose = true
+ } else {
+ ts.t.FailNow()
}
}
- // Command prefix ! means negate the expectations about this command:
- // go command should fail, match should not be found, etc.
- neg := false
- if args[0] == "!" {
- neg = true
- args = args[1:]
- if len(args) == 0 {
- ts.Fatalf("! on line by itself")
- }
- }
-
- // Run command.
- cmd := scriptCmds[args[0]]
- if cmd == nil {
- cmd = ts.params.Cmds[args[0]]
- }
- if cmd == nil {
- ts.Fatalf("unknown command %q", args[0])
- }
- cmd(ts, neg, args[1:])
-
// Command can ask script to stop early.
if ts.stopped {
// Break instead of returning, so that we check the status of any
@@ -540,6 +588,12 @@ Script:
}
ts.cmdWait(false, nil)
+ // If we reached here but we've failed (probably because ContinueOnError
+ // was set), don't wipe the log and print "PASS".
+ if failed {
+ ts.t.FailNow()
+ }
+
// Final phase ended.
rewind()
markTime()
@@ -548,11 +602,65 @@ Script:
}
}
-func hasFailed(t T) bool {
- if t, ok := t.(TFailed); ok {
- return t.Failed()
+func (ts *TestScript) runLine(line string) (runOK bool) {
+ defer catchFailNow(func() {
+ runOK = false
+ })
+
+ // Parse input line. Ignore blanks entirely.
+ args := ts.parse(line)
+ if len(args) == 0 {
+ return true
+ }
+
+ // Echo command to log.
+ fmt.Fprintf(&ts.log, "> %s\n", line)
+
+ // Command prefix [cond] means only run this command if cond is satisfied.
+ for strings.HasPrefix(args[0], "[") && strings.HasSuffix(args[0], "]") {
+ cond := args[0]
+ cond = cond[1 : len(cond)-1]
+ cond = strings.TrimSpace(cond)
+ args = args[1:]
+ if len(args) == 0 {
+ ts.Fatalf("missing command after condition")
+ }
+ want := true
+ if strings.HasPrefix(cond, "!") {
+ want = false
+ cond = strings.TrimSpace(cond[1:])
+ }
+ ok, err := ts.condition(cond)
+ if err != nil {
+ ts.Fatalf("bad condition %q: %v", cond, err)
+ }
+ if ok != want {
+ // Don't run rest of line.
+ return true
+ }
+ }
+
+ // Command prefix ! means negate the expectations about this command:
+ // go command should fail, match should not be found, etc.
+ neg := false
+ if args[0] == "!" {
+ neg = true
+ args = args[1:]
+ if len(args) == 0 {
+ ts.Fatalf("! on line by itself")
+ }
}
- return false
+
+ // Run command.
+ cmd := scriptCmds[args[0]]
+ if cmd == nil {
+ cmd = ts.params.Cmds[args[0]]
+ }
+ if cmd == nil {
+ ts.Fatalf("unknown command %q", args[0])
+ }
+ cmd(ts, neg, args[1:])
+ return true
}
func (ts *TestScript) applyScriptUpdates() {
@@ -570,7 +678,7 @@ func (ts *TestScript) applyScriptUpdates() {
if txtar.NeedsQuote(data) {
data1, err := txtar.Quote(data)
if err != nil {
- ts.t.Fatal(fmt.Sprintf("cannot update script file %q: %v", f.Name, err))
+ ts.Fatalf("cannot update script file %q: %v", f.Name, err)
continue
}
data = data1
@@ -589,6 +697,21 @@ func (ts *TestScript) applyScriptUpdates() {
ts.Logf("%s updated", ts.file)
}
+var failNow = errors.New("fail now!")
+
+// catchFailNow catches any panic from Fatalf and calls
+// f if it did so. It must be called in a defer.
+func catchFailNow(f func()) {
+ e := recover()
+ if e == nil {
+ return
+ }
+ if e != failNow {
+ panic(e)
+ }
+ f()
+}
+
// condition reports whether the given condition is satisfied.
func (ts *TestScript) condition(cond string) (bool, error) {
switch {
@@ -686,7 +809,7 @@ func (ts *TestScript) exec(command string, args ...string) (stdout, stderr strin
cmd.Stdout = &stdoutBuf
cmd.Stderr = &stderrBuf
if err = cmd.Start(); err == nil {
- err = ctxWait(ts.ctxt, cmd)
+ err = waitOrStop(ts.ctxt, cmd, ts.gracePeriod)
}
ts.stdin = ""
return stdoutBuf.String(), stderrBuf.String(), err
@@ -731,21 +854,68 @@ func (ts *TestScript) BackgroundCmds() []*exec.Cmd {
return cmds
}
-// ctxWait is like cmd.Wait, but terminates cmd with os.Interrupt if ctx becomes done.
+// waitOrStop waits for the already-started command cmd by calling its Wait method.
//
-// This differs from exec.CommandContext in that it prefers os.Interrupt over os.Kill.
-// (See https://golang.org/issue/21135.)
-func ctxWait(ctx context.Context, cmd *exec.Cmd) error {
- errc := make(chan error, 1)
- go func() { errc <- cmd.Wait() }()
-
- select {
- case err := <-errc:
- return err
- case <-ctx.Done():
- interruptProcess(cmd.Process)
- return <-errc
+// If cmd does not return before ctx is done, waitOrStop sends it an interrupt
+// signal. If killDelay is positive, waitOrStop waits that additional period for
+// Wait to return before sending os.Kill.
+func waitOrStop(ctx context.Context, cmd *exec.Cmd, killDelay time.Duration) error {
+ if cmd.Process == nil {
+ panic("waitOrStop called with a nil cmd.Process — missing Start call?")
+ }
+
+ errc := make(chan error)
+ go func() {
+ select {
+ case errc <- nil:
+ return
+ case <-ctx.Done():
+ }
+
+ var interrupt os.Signal = syscall.SIGQUIT
+ if runtime.GOOS == "windows" {
+ // Per https://golang.org/pkg/os/#Signal, “Interrupt is not implemented on
+ // Windows; using it with os.Process.Signal will return an error.”
+ // Fall back directly to Kill instead.
+ interrupt = os.Kill
+ }
+
+ err := cmd.Process.Signal(interrupt)
+ if err == nil {
+ err = ctx.Err() // Report ctx.Err() as the reason we interrupted.
+ } else if err == os.ErrProcessDone {
+ errc <- nil
+ return
+ }
+
+ if killDelay > 0 {
+ timer := time.NewTimer(killDelay)
+ select {
+ // Report ctx.Err() as the reason we interrupted the process...
+ case errc <- ctx.Err():
+ timer.Stop()
+ return
+ // ...but after killDelay has elapsed, fall back to a stronger signal.
+ case <-timer.C:
+ }
+
+ // Wait still hasn't returned.
+ // Kill the process harder to make sure that it exits.
+ //
+ // Ignore any error: if cmd.Process has already terminated, we still
+ // want to send ctx.Err() (or the error from the Interrupt call)
+ // to properly attribute the signal that may have terminated it.
+ _ = cmd.Process.Kill()
+ }
+
+ errc <- err
+ }()
+
+ waitErr := cmd.Wait()
+ if interruptErr := <-errc; interruptErr != nil {
+ return interruptErr
}
+ return waitErr
}
// interruptProcess sends os.Interrupt to p if supported, or os.Kill otherwise.
@@ -785,7 +955,10 @@ func (ts *TestScript) expand(s string) string {
// fatalf aborts the test with the given failure message.
func (ts *TestScript) Fatalf(format string, args ...interface{}) {
fmt.Fprintf(&ts.log, "FAIL: %s:%d: %s\n", ts.file, ts.lineno, fmt.Sprintf(format, args...))
- ts.t.FailNow()
+ // This should be caught by the defer inside the TestScript.runLine method.
+ // We do this rather than calling ts.t.FailNow directly because we want to
+ // be able to continue on error when Params.ContinueOnError is set.
+ panic(failNow)
}
// MkAbs interprets file relative to the test script's current directory
diff --git a/testscript/testscript_test.go b/testscript/testscript_test.go
index c09cabb..53f420a 100644
--- a/testscript/testscript_test.go
+++ b/testscript/testscript_test.go
@@ -59,6 +59,11 @@ func signalCatcher() int {
}
func TestMain(m *testing.M) {
+ timeSince = func(t time.Time) time.Duration {
+ return 0
+ }
+
+ showVerboseEnv = false
os.Exit(RunMain(m, map[string]func() int{
"printargs": printArgs,
"fprintargs": fprintArgs,
@@ -130,6 +135,31 @@ func TestEnv(t *testing.T) {
}
}
+func TestSetupFailure(t *testing.T) {
+ dir := t.TempDir()
+ if err := os.WriteFile(filepath.Join(dir, "foo.txt"), nil, 0o666); err != nil {
+ t.Fatal(err)
+ }
+ ft := &fakeT{}
+ func() {
+ defer catchAbort()
+ RunT(ft, Params{
+ Dir: dir,
+ Setup: func(*Env) error {
+ return fmt.Errorf("some failure")
+ },
+ })
+ }()
+ if !ft.failed {
+ t.Fatal("test should have failed because of setup failure")
+ }
+
+ want := regexp.MustCompile(`^FAIL: .*: some failure\n$`)
+ if got := ft.log.String(); !want.MatchString(got) {
+ t.Fatalf("expected msg to match `%v`; got:\n%q", want, got)
+ }
+}
+
func TestScripts(t *testing.T) {
// TODO set temp directory.
testDeferCount := 0
@@ -183,42 +213,40 @@ func TestScripts(t *testing.T) {
fset := flag.NewFlagSet("testscript", flag.ContinueOnError)
fUpdate := fset.Bool("update", false, "update scripts when cmp fails")
fExplicitExec := fset.Bool("explicit-exec", false, "require explicit use of exec for commands")
- fVerbose := fset.Bool("verbose", false, "be verbose with output")
+ fUniqueNames := fset.Bool("unique-names", false, "require unique names in txtar archive")
+ fVerbose := fset.Bool("v", false, "be verbose with output")
+ fContinue := fset.Bool("continue", false, "continue on error")
if err := fset.Parse(args); err != nil {
ts.Fatalf("failed to parse args for testscript: %v", err)
}
if fset.NArg() != 1 {
- ts.Fatalf("testscript [-verbose] [-update] [-explicit-exec] <dir>")
+ ts.Fatalf("testscript [-v] [-continue] [-update] [-explicit-exec] <dir>")
}
dir := fset.Arg(0)
- t := &fakeT{ts: ts, verbose: *fVerbose}
+ t := &fakeT{verbose: *fVerbose}
func() {
- defer func() {
- if err := recover(); err != nil {
- if err != errAbort {
- panic(err)
- }
- }
- }()
+ defer catchAbort()
RunT(t, Params{
Dir: ts.MkAbs(dir),
UpdateScripts: *fUpdate,
RequireExplicitExec: *fExplicitExec,
+ RequireUniqueNames: *fUniqueNames,
Cmds: map[string]func(ts *TestScript, neg bool, args []string){
"some-param-cmd": func(ts *TestScript, neg bool, args []string) {
},
},
+ ContinueOnError: *fContinue,
})
}()
ts.stdout = strings.Replace(t.log.String(), ts.workdir, "$WORK", -1)
if neg {
- if len(t.failMsgs) == 0 {
+ if !t.failed {
ts.Fatalf("testscript unexpectedly succeeded")
}
return
}
- if len(t.failMsgs) > 0 {
- ts.Fatalf("testscript unexpectedly failed with errors: %q", t.failMsgs)
+ if t.failed {
+ ts.Fatalf("testscript unexpectedly failed with errors: %q", &t.log)
}
},
},
@@ -303,24 +331,21 @@ func TestWorkdirRoot(t *testing.T) {
func TestBadDir(t *testing.T) {
ft := new(fakeT)
func() {
- defer func() {
- if err := recover(); err != nil {
- if err != errAbort {
- panic(err)
- }
- }
- }()
+ defer catchAbort()
RunT(ft, Params{
Dir: "thiswillnevermatch",
})
}()
- wantCount := 1
- if got := len(ft.failMsgs); got != wantCount {
- t.Fatalf("expected %v fail message; got %v", wantCount, got)
+ want := regexp.MustCompile(`no txtar nor txt scripts found in dir thiswillnevermatch`)
+ if got := ft.log.String(); !want.MatchString(got) {
+ t.Fatalf("expected msg to match `%v`; got:\n%v", want, got)
}
- wantMsg := regexp.MustCompile(`no txtar nor txt scripts found in dir thiswillnevermatch`)
- if got := ft.failMsgs[0]; !wantMsg.MatchString(got) {
- t.Fatalf("expected msg to match `%v`; got:\n%v", wantMsg, got)
+}
+
+// catchAbort catches the panic raised by fakeT.FailNow.
+func catchAbort() {
+ if err := recover(); err != nil && err != errAbort {
+ panic(err)
}
}
@@ -391,11 +416,9 @@ func waitFile(ts *TestScript, neg bool, args []string) {
}
type fakeT struct {
- ts *TestScript
- log bytes.Buffer
- failMsgs []string
- verbose bool
- failed bool
+ log strings.Builder
+ verbose bool
+ failed bool
}
var errAbort = errors.New("abort test")
@@ -405,9 +428,8 @@ func (t *fakeT) Skip(args ...interface{}) {
}
func (t *fakeT) Fatal(args ...interface{}) {
- t.failed = true
- t.failMsgs = append(t.failMsgs, fmt.Sprint(args...))
- panic(errAbort)
+ t.Log(args...)
+ t.FailNow()
}
func (t *fakeT) Parallel() {}
@@ -417,7 +439,8 @@ func (t *fakeT) Log(args ...interface{}) {
}
func (t *fakeT) FailNow() {
- t.Fatal("failed")
+ t.failed = true
+ panic(errAbort)
}
func (t *fakeT) Run(name string, f func(T)) {
More details
Historical runs
- failed: make[1]: *** [debian/rules:9: override_dh_auto_test] Error 1
- too-many-requests: Unexpected HTTP status 429 for https://salsa.debian.org/go-team/packages/golang-github-rogpeppe-go-internal.git/info/refs?service=git-upload-pack: Unable to handle http code: Too Many Requests
- nothing-to-do: Last upstream version 1.9.0 already imported.
- nothing-to-do: Last upstream version 1.9.0 already imported.
- result-push-failed: Failed to push result branch: IncompleteRead(0 bytes read)
- worker-timeout: No keepalives received in 10:02:16.244665.
- build-failed-stage-explain-bd-uninstallable: build failed stage explain-bd-uninstallable
- worker-timeout: No keepalives received in 10:00:10.187100.
- run-disappeared: Worker started processing new run rather than e8496b16-c96f-4488-93f0-723a8ebf45d5