Skip to content

Commit

Permalink
cmd/testscript: initial commit (#41)
Browse files Browse the repository at this point in the history
  • Loading branch information
myitcv committed Jan 9, 2019
1 parent 4bbc89b commit dc3eec0
Show file tree
Hide file tree
Showing 16 changed files with 629 additions and 86 deletions.
117 changes: 117 additions & 0 deletions cmd/testscript/help.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,117 @@
package main

import (
"fmt"
"io"
)

func mainUsage(f io.Writer) {
fmt.Fprint(f, mainHelp)
}

var mainHelp = `
The testscript command runs github.com/rogpeppe/go-internal/testscript scripts
in a fresh temporary work directory tree.
Usage:
testscript [-v] files...
The testscript command is designed to make it easy to create self-contained
reproductions of command sequences.
Each file is opened as a script and run as described in the documentation for
github.com/rogpeppe/go-internal/testscript. The special filename "-" is
interpreted as the standard input.
As a special case, supporting files/directories in the .gomodproxy subdirectory
will be served via a github.com/rogpeppe/go-internal/goproxytest server which
is available to each script via the GOPROXY environment variable. The contents
of the .gomodproxy subdirectory are not available to the script except via the
proxy server. See the documentation for
github.com/rogpeppe/go-internal/goproxytest for details on the format of these
files/directories.
Examples
========
The following example, fruit.txt, shows a simple reproduction that includes
.gomodproxy supporting files:
go get -m fruit.com
go list fruit.com/...
stdout 'fruit.com/fruit'
-- go.mod --
module mod
-- .gomodproxy/fruit.com_v1.0.0/.mod --
module fruit.com
-- .gomodproxy/fruit.com_v1.0.0/.info --
{"Version":"v1.0.0","Time":"2018-10-22T18:45:39Z"}
-- .gomodproxy/fruit.com_v1.0.0/fruit/fruit.go --
package fruit
const Name = "Apple"
Running testscript -v fruit.txt we get:
...
> go get -m fruit.com
[stderr]
go: finding fruit.com v1.0.0
> go list fruit.com/...
[stdout]
fruit.com/fruit
[stderr]
go: downloading fruit.com v1.0.0
> stdout 'fruit.com/fruit'
PASS
The following example, goimports.txt, shows a simple reproduction involving
goimports:
go install golang.org/x/tools/cmd/goimports
# check goimports help information
exec goimports -d main.go
stdout 'import "math"'
-- go.mod --
module mod
require golang.org/x/tools v0.0.0-20181221235234-d00ac6d27372
-- main.go --
package mod
const Pi = math.Pi
Running testscript -v goimports.txt we get:
...
> go install golang.org/x/tools/cmd/goimports
[stderr]
go: finding golang.org/x/tools v0.0.0-20181221235234-d00ac6d27372
go: downloading golang.org/x/tools v0.0.0-20181221235234-d00ac6d27372
# check goimports help information (0.015s)
> exec goimports -d main.go
[stdout]
diff -u main.go.orig main.go
--- main.go.orig 2019-01-08 16:03:35.861907738 +0000
+++ main.go 2019-01-08 16:03:35.861907738 +0000
@@ -1,3 +1,5 @@
package mod
+import "math"
+
const Pi = math.Pi
> stdout 'import "math"'
PASS
`[1:]
222 changes: 222 additions & 0 deletions cmd/testscript/main.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,222 @@
// 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 main

import (
"errors"
"flag"
"fmt"
"io/ioutil"
"os"
"os/exec"
"path/filepath"
"strconv"
"strings"

"github.com/rogpeppe/go-internal/goproxytest"
"github.com/rogpeppe/go-internal/gotooltest"
"github.com/rogpeppe/go-internal/testscript"
"github.com/rogpeppe/go-internal/txtar"
)

const (
// goModProxyDir is the special subdirectory in a txtar script's supporting files
// within which we expect to find github.com/rogpeppe/go-internal/goproxytest
// directories.
goModProxyDir = ".gomodproxy"
)

func main() {
os.Exit(main1())
}

func main1() int {
if err := mainerr(); err != nil {
fmt.Fprintln(os.Stderr, err)
return 1
}
return 0
}

func mainerr() (retErr error) {
fs := flag.NewFlagSet(os.Args[0], flag.ContinueOnError)
fs.Usage = func() {
mainUsage(os.Stderr)
}
fWork := fs.Bool("work", false, "print temporary work directory and do not remove when done")
fVerbose := fs.Bool("v", false, "run tests verbosely")
if err := fs.Parse(os.Args[1:]); err != nil {
return err
}

td, err := ioutil.TempDir("", "testscript")
if err != nil {
return fmt.Errorf("unable to create temp dir: %v", err)
}
fmt.Printf("temporary work directory: %v\n", td)
if !*fWork {
defer os.RemoveAll(td)
}

files := fs.Args()
if len(files) == 0 {
files = []string{"-"}
}

for i, fileName := range files {
// TODO make running files concurrent by default? If we do, note we'll need to do
// something smarter with the runner stdout and stderr below
runDir := filepath.Join(td, strconv.Itoa(i))
if err := os.Mkdir(runDir, 0777); err != nil {
return fmt.Errorf("failed to create a run directory within %v for %v: %v", td, fileName, err)
}
if err := run(runDir, fileName, *fVerbose); err != nil {
return err
}
}

return nil
}

var (
failedRun = errors.New("failed run")
skipRun = errors.New("skip")
)

type runner struct {
verbose bool
}

func (r runner) Skip(is ...interface{}) {
panic(skipRun)
}

func (r runner) Fatal(is ...interface{}) {
r.Log(is...)
r.FailNow()
}

func (r runner) Parallel() {
// No-op for now; we are currently only running a single script in a
// testscript instance.
}

func (r runner) Log(is ...interface{}) {
fmt.Print(is...)
}

func (r runner) FailNow() {
panic(failedRun)
}

func (r runner) 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 runner) Verbose() bool {
return r.verbose
}

func run(runDir, fileName string, verbose bool) error {
var ar *txtar.Archive
var err error

mods := filepath.Join(runDir, goModProxyDir)

if err := os.MkdirAll(mods, 0777); err != nil {
return fmt.Errorf("failed to create goModProxy dir: %v", err)
}

if fileName == "-" {
fileName = "<stdin>"
byts, err := ioutil.ReadAll(os.Stdin)
if err != nil {
return fmt.Errorf("failed to read from stdin: %v", err)
}
ar = txtar.Parse(byts)
} else {
ar, err = txtar.ParseFile(fileName)
}

if err != nil {
return fmt.Errorf("failed to txtar parse %v: %v", fileName, err)
}

var script, gomodProxy txtar.Archive
script.Comment = ar.Comment

for _, f := range ar.Files {
fp := filepath.Clean(filepath.FromSlash(f.Name))
parts := strings.Split(fp, string(os.PathSeparator))

if len(parts) > 1 && parts[0] == goModProxyDir {
gomodProxy.Files = append(gomodProxy.Files, f)
} else {
script.Files = append(script.Files, f)
}
}

if txtar.Write(&gomodProxy, runDir); err != nil {
return fmt.Errorf("failed to write .gomodproxy files: %v", err)
}

if err := ioutil.WriteFile(filepath.Join(runDir, "script.txt"), txtar.Format(&script), 0666); err != nil {
return fmt.Errorf("failed to write script for %v: %v", fileName, err)
}

p := testscript.Params{
Dir: runDir,
}

if len(gomodProxy.Files) > 0 {
srv, err := goproxytest.NewServer(mods, "")
if err != nil {
return fmt.Errorf("cannot start proxy for %v: %v", fileName, err)
}
defer srv.Close()

currSetup := p.Setup

p.Setup = func(env *testscript.Env) error {
env.Vars = append(env.Vars, "GOPROXY="+srv.URL)
if currSetup != nil {
return currSetup(env)
}
return nil
}
}

if _, err := exec.LookPath("go"); err == nil {
if err := gotooltest.Setup(&p); err != nil {
return fmt.Errorf("failed to setup go tool for %v run: %v", fileName, err)
}
}

r := runner{
verbose: verbose,
}

func() {
defer func() {
switch recover() {
case nil, skipRun:
case failedRun:
err = failedRun
default:
panic(fmt.Errorf("unexpected panic: %v [%T]", err, err))
}
}()
testscript.RunT(r, p)
}()

if err != nil {
return fmt.Errorf("error running %v in %v\n", fileName, runDir)
}

return nil
}

0 comments on commit dc3eec0

Please sign in to comment.