internal/test/test.go

Summary

Maintainability
A
0 mins
Test Coverage
// Copyright 2017 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.

//lint:file-ignore U1000 unused fns we might want to use later.

package test

import (
    "bytes"
    "flag"
    "fmt"
    "go/format"
    "io"
    "io/ioutil"
    "os"
    "os/exec"
    "path/filepath"
    "regexp"
    "runtime"
    "strings"
    "sync"
    "testing"

    "github.com/pkg/errors"
)

var (
    // ExeSuffix is the suffix of executable files; ".exe" on Windows.
    ExeSuffix string
    mu        sync.Mutex
    // PrintLogs controls logging of test commands.
    PrintLogs = flag.Bool("logs", false, "log stdin/stdout of test commands")
    // UpdateGolden controls updating test fixtures.
    UpdateGolden = flag.Bool("update", false, "update golden files")
)

const (
    manifestName = "Gopkg.toml"
    lockName     = "Gopkg.lock"
)

func init() {
    switch runtime.GOOS {
    case "windows":
        ExeSuffix = ".exe"
    }
}

// Helper with utilities for testing.
type Helper struct {
    t              *testing.T
    temps          []string
    wd             string
    origWd         string
    env            []string
    tempdir        string
    ran            bool
    inParallel     bool
    stdout, stderr bytes.Buffer
}

// NewHelper initializes a new helper for testing.
func NewHelper(t *testing.T) *Helper {
    wd, err := os.Getwd()
    if err != nil {
        panic(err)
    }
    return &Helper{t: t, origWd: wd}
}

// Must gives a fatal error if err is not nil.
func (h *Helper) Must(err error) {
    if err != nil {
        h.t.Fatalf("%+v", err)
    }
}

// check gives a test non-fatal error if err is not nil.
func (h *Helper) check(err error) {
    if err != nil {
        h.t.Errorf("%+v", err)
    }
}

// Parallel runs the test in parallel by calling t.Parallel.
func (h *Helper) Parallel() {
    if h.ran {
        h.t.Fatalf("%+v", errors.New("internal testsuite error: call to parallel after run"))
    }
    if h.wd != "" {
        h.t.Fatalf("%+v", errors.New("internal testsuite error: call to parallel after cd"))
    }
    for _, e := range h.env {
        if strings.HasPrefix(e, "GOROOT=") || strings.HasPrefix(e, "GOPATH=") || strings.HasPrefix(e, "GOBIN=") {
            val := e[strings.Index(e, "=")+1:]
            if strings.HasPrefix(val, "testdata") || strings.HasPrefix(val, "./testdata") {
                h.t.Fatalf("%+v", errors.Errorf("internal testsuite error: call to parallel with testdata in environment (%s)", e))
            }
        }
    }
    h.inParallel = true
    h.t.Parallel()
}

// pwd returns the current directory.
func (h *Helper) pwd() string {
    wd, err := os.Getwd()
    if err != nil {
        h.t.Fatalf("%+v", errors.Wrap(err, "could not get working directory"))
    }
    return wd
}

// Cd changes the current directory to the named directory. Note that
// using this means that the test must not be run in parallel with any
// other tests.
func (h *Helper) Cd(dir string) {
    if h.inParallel {
        h.t.Fatalf("%+v", errors.New("internal testsuite error: changing directory when running in parallel"))
    }
    if h.wd == "" {
        h.wd = h.pwd()
    }
    abs, err := filepath.Abs(dir)
    if err == nil {
        h.Setenv("PWD", abs)
    }

    err = os.Chdir(dir)
    h.Must(errors.Wrapf(err, "Unable to cd to %s", dir))
}

// Setenv sets an environment variable to use when running the test go
// command.
func (h *Helper) Setenv(name, val string) {
    if h.inParallel && (name == "GOROOT" || name == "GOPATH" || name == "GOBIN") && (strings.HasPrefix(val, "testdata") || strings.HasPrefix(val, "./testdata")) {
        h.t.Fatalf("%+v", errors.Errorf("internal testsuite error: call to setenv with testdata (%s=%s) after parallel", name, val))
    }
    h.unsetenv(name)
    h.env = append(h.env, name+"="+val)
}

// unsetenv removes an environment variable.
func (h *Helper) unsetenv(name string) {
    if h.env == nil {
        h.env = append([]string(nil), os.Environ()...)
    }
    for i, v := range h.env {
        if strings.HasPrefix(v, name+"=") {
            h.env = append(h.env[:i], h.env[i+1:]...)
            break
        }
    }
}

// DoRun runs the test go command, recording stdout and stderr and
// returning exit status.
func (h *Helper) DoRun(args []string) error {
    if h.inParallel {
        for _, arg := range args {
            if strings.HasPrefix(arg, "testdata") || strings.HasPrefix(arg, "./testdata") {
                h.t.Fatalf("%+v", errors.New("internal testsuite error: parallel run using testdata"))
            }
        }
    }
    if *PrintLogs {
        h.t.Logf("running testdep %v", args)
    }
    var prog string
    if h.wd == "" {
        prog = "./testdep" + ExeSuffix
    } else {
        prog = filepath.Join(h.wd, "testdep"+ExeSuffix)
    }
    newargs := args
    if args[0] != "check" {
        newargs = append([]string{args[0], "-v"}, args[1:]...)
    }

    cmd := exec.Command(prog, newargs...)
    h.stdout.Reset()
    h.stderr.Reset()
    cmd.Stdout = &h.stdout
    cmd.Stderr = &h.stderr
    cmd.Env = h.env
    status := cmd.Run()
    if *PrintLogs {
        if h.stdout.Len() > 0 {
            h.t.Log("standard output:")
            h.t.Log(h.stdout.String())
        }
        if h.stderr.Len() > 0 {
            h.t.Log("standard error:")
            h.t.Log(h.stderr.String())
        }
    }
    h.ran = true
    return errors.Wrapf(status, "Error running %s\n%s", strings.Join(newargs, " "), h.stderr.String())
}

// Run runs the test go command, and expects it to succeed.
func (h *Helper) Run(args ...string) {
    if runtime.GOOS == "windows" {
        mu.Lock()
        defer mu.Unlock()
    }
    if status := h.DoRun(args); status != nil {
        h.t.Logf("go %v failed unexpectedly: %v", args, status)
        h.t.FailNow()
    }
}

// runFail runs the test go command, and expects it to fail.
func (h *Helper) runFail(args ...string) {
    if status := h.DoRun(args); status == nil {
        h.t.Fatalf("%+v", errors.New("testgo succeeded unexpectedly"))
    } else {
        h.t.Log("testgo failed as expected:", status)
    }
}

// RunGo runs a go command, and expects it to succeed.
func (h *Helper) RunGo(args ...string) {
    cmd := exec.Command("go", args...)
    h.stdout.Reset()
    h.stderr.Reset()
    cmd.Stdout = &h.stdout
    cmd.Stderr = &h.stderr
    cmd.Dir = h.wd
    cmd.Env = h.env
    status := cmd.Run()
    if h.stdout.Len() > 0 {
        h.t.Log("go standard output:")
        h.t.Log(h.stdout.String())
    }
    if h.stderr.Len() > 0 {
        h.t.Log("go standard error:")
        h.t.Log(h.stderr.String())
    }
    if status != nil {
        h.t.Logf("go %v failed unexpectedly: %v", args, status)
        h.t.FailNow()
    }
}

// NeedsExternalNetwork makes sure the tests needing external network will not
// be run when executing tests in short mode.
func NeedsExternalNetwork(t *testing.T) {
    if testing.Short() {
        t.Skip("skipping test: no external network in -short mode")
    }
}

// NeedsGit will make sure the tests that require git will be skipped if the
// git binary is not available.
func NeedsGit(t *testing.T) {
    if _, err := exec.LookPath("git"); err != nil {
        t.Skip("skipping because git binary not found")
    }
}

// RunGit runs a git command, and expects it to succeed.
func (h *Helper) RunGit(dir string, args ...string) {
    cmd := exec.Command("git", args...)
    h.stdout.Reset()
    h.stderr.Reset()
    cmd.Stdout = &h.stdout
    cmd.Stderr = &h.stderr
    cmd.Dir = dir
    cmd.Env = h.env
    status := cmd.Run()
    if *PrintLogs {
        if h.stdout.Len() > 0 {
            h.t.Logf("git %v standard output:", args)
            h.t.Log(h.stdout.String())
        }
        if h.stderr.Len() > 0 {
            h.t.Logf("git %v standard error:", args)
            h.t.Log(h.stderr.String())
        }
    }
    if status != nil {
        h.t.Logf("git %v failed unexpectedly: %v", args, status)
        h.t.FailNow()
    }
}

// getStdout returns standard output of the testgo run as a string.
func (h *Helper) getStdout() string {
    if !h.ran {
        h.t.Fatalf("%+v", errors.New("internal testsuite error: stdout called before run"))
    }
    return h.stdout.String()
}

// getStderr returns standard error of the testgo run as a string.
func (h *Helper) getStderr() string {
    if !h.ran {
        h.t.Fatalf("%+v", errors.New("internal testsuite error: stdout called before run"))
    }
    return h.stderr.String()
}

// doGrepMatch looks for a regular expression in a buffer, and returns
// whether it is found. The regular expression is matched against
// each line separately, as with the grep command.
func (h *Helper) doGrepMatch(match string, b *bytes.Buffer) bool {
    if !h.ran {
        h.t.Fatalf("%+v", errors.New("internal testsuite error: grep called before run"))
    }
    re := regexp.MustCompile(match)
    for _, ln := range bytes.Split(b.Bytes(), []byte{'\n'}) {
        if re.Match(ln) {
            return true
        }
    }
    return false
}

// doGrep looks for a regular expression in a buffer and fails if it
// is not found. The name argument is the name of the output we are
// searching, "output" or "error".  The msg argument is logged on
// failure.
func (h *Helper) doGrep(match string, b *bytes.Buffer, name, msg string) {
    if !h.doGrepMatch(match, b) {
        h.t.Log(msg)
        h.t.Logf("pattern %v not found in standard %s", match, name)
        h.t.FailNow()
    }
}

// grepStdout looks for a regular expression in the test run's
// standard output and fails, logging msg, if it is not found.
func (h *Helper) grepStdout(match, msg string) {
    h.doGrep(match, &h.stdout, "output", msg)
}

// grepStderr looks for a regular expression in the test run's
// standard error and fails, logging msg, if it is not found.
func (h *Helper) grepStderr(match, msg string) {
    h.doGrep(match, &h.stderr, "error", msg)
}

// grepBoth looks for a regular expression in the test run's standard
// output or stand error and fails, logging msg, if it is not found.
func (h *Helper) grepBoth(match, msg string) {
    if !h.doGrepMatch(match, &h.stdout) && !h.doGrepMatch(match, &h.stderr) {
        h.t.Log(msg)
        h.t.Logf("pattern %v not found in standard output or standard error", match)
        h.t.FailNow()
    }
}

// doGrepNot looks for a regular expression in a buffer and fails if
// it is found. The name and msg arguments are as for doGrep.
func (h *Helper) doGrepNot(match string, b *bytes.Buffer, name, msg string) {
    if h.doGrepMatch(match, b) {
        h.t.Log(msg)
        h.t.Logf("pattern %v found unexpectedly in standard %s", match, name)
        h.t.FailNow()
    }
}

// grepStdoutNot looks for a regular expression in the test run's
// standard output and fails, logging msg, if it is found.
func (h *Helper) grepStdoutNot(match, msg string) {
    h.doGrepNot(match, &h.stdout, "output", msg)
}

// grepStderrNot looks for a regular expression in the test run's
// standard error and fails, logging msg, if it is found.
func (h *Helper) grepStderrNot(match, msg string) {
    h.doGrepNot(match, &h.stderr, "error", msg)
}

// grepBothNot looks for a regular expression in the test run's
// standard output or stand error and fails, logging msg, if it is
// found.
func (h *Helper) grepBothNot(match, msg string) {
    if h.doGrepMatch(match, &h.stdout) || h.doGrepMatch(match, &h.stderr) {
        h.t.Log(msg)
        h.t.Fatalf("%+v", errors.Errorf("pattern %v found unexpectedly in standard output or standard error", match))
    }
}

// doGrepCount counts the number of times a regexp is seen in a buffer.
func (h *Helper) doGrepCount(match string, b *bytes.Buffer) int {
    if !h.ran {
        h.t.Fatalf("%+v", errors.New("internal testsuite error: doGrepCount called before run"))
    }
    re := regexp.MustCompile(match)
    c := 0
    for _, ln := range bytes.Split(b.Bytes(), []byte{'\n'}) {
        if re.Match(ln) {
            c++
        }
    }
    return c
}

// grepCountBoth returns the number of times a regexp is seen in both
// standard output and standard error.
func (h *Helper) grepCountBoth(match string) int {
    return h.doGrepCount(match, &h.stdout) + h.doGrepCount(match, &h.stderr)
}

// creatingTemp records that the test plans to create a temporary file
// or directory. If the file or directory exists already, it will be
// removed. When the test completes, the file or directory will be
// removed if it exists.
func (h *Helper) creatingTemp(path string) {
    if filepath.IsAbs(path) && !strings.HasPrefix(path, h.tempdir) {
        h.t.Fatalf("%+v", errors.Errorf("internal testsuite error: creatingTemp(%q) with absolute path not in temporary directory", path))
    }
    // If we have changed the working directory, make sure we have
    // an absolute path, because we are going to change directory
    // back before we remove the temporary.
    if h.wd != "" && !filepath.IsAbs(path) {
        path = filepath.Join(h.pwd(), path)
    }
    h.Must(os.RemoveAll(path))
    h.temps = append(h.temps, path)
}

// makeTempdir makes a temporary directory for a run of testgo. If
// the temporary directory was already created, this does nothing.
func (h *Helper) makeTempdir() {
    if h.tempdir == "" {
        var err error
        h.tempdir, err = ioutil.TempDir("", "gotest")
        h.Must(err)
    }
}

// TempFile adds a temporary file for a run of testgo.
func (h *Helper) TempFile(path, contents string) {
    h.makeTempdir()
    h.Must(os.MkdirAll(filepath.Join(h.tempdir, filepath.Dir(path)), 0755))
    bytes := []byte(contents)
    if strings.HasSuffix(path, ".go") {
        formatted, err := format.Source(bytes)
        if err == nil {
            bytes = formatted
        }
    }
    h.Must(ioutil.WriteFile(filepath.Join(h.tempdir, path), bytes, 0644))
}

// WriteTestFile writes a file to the testdata directory from memory.  src is
// relative to ./testdata.
func (h *Helper) WriteTestFile(src string, content string) error {
    err := ioutil.WriteFile(filepath.Join(h.origWd, "testdata", src), []byte(content), 0666)
    return err
}

// GetFile reads a file into memory
func (h *Helper) GetFile(path string) io.ReadCloser {
    content, err := os.Open(path)
    if err != nil {
        h.t.Fatalf("%+v", errors.Wrapf(err, "Unable to open file: %s", path))
    }
    return content
}

// GetTestFile reads a file from the testdata directory into memory.  src is
// relative to ./testdata.
func (h *Helper) GetTestFile(src string) io.ReadCloser {
    fullPath := filepath.Join(h.origWd, "testdata", src)
    return h.GetFile(fullPath)
}

// GetTestFileString reads a file from the testdata directory into memory.  src is
// relative to ./testdata.
func (h *Helper) GetTestFileString(src string) string {
    srcf := h.GetTestFile(src)
    defer srcf.Close()
    content, err := ioutil.ReadAll(srcf)
    if err != nil {
        h.t.Fatalf("%+v", err)
    }
    return string(content)
}

// TempCopy copies a temporary file from testdata into the temporary directory.
// dest is relative to the temp directory location, and src is relative to
// ./testdata.
func (h *Helper) TempCopy(dest, src string) {
    in := h.GetTestFile(src)
    defer in.Close()
    h.TempDir(filepath.Dir(dest))
    out, err := os.Create(filepath.Join(h.tempdir, dest))
    if err != nil {
        panic(err)
    }
    defer out.Close()
    io.Copy(out, in)
}

// TempDir adds a temporary directory for a run of testgo.
func (h *Helper) TempDir(path string) {
    h.makeTempdir()
    fullPath := filepath.Join(h.tempdir, path)
    if err := os.MkdirAll(fullPath, 0755); err != nil && !os.IsExist(err) {
        h.t.Fatalf("%+v", errors.Errorf("Unable to create temp directory: %s", fullPath))
    }
}

// Path returns the absolute pathname to file with the temporary
// directory.
func (h *Helper) Path(name string) string {
    if h.tempdir == "" {
        h.t.Fatalf("%+v", errors.Errorf("internal testsuite error: path(%q) with no tempdir", name))
    }

    var joined string
    if name == "." {
        joined = h.tempdir
    } else {
        joined = filepath.Join(h.tempdir, name)
    }

    // Ensure it's the absolute, symlink-less path we're returning
    abs, err := filepath.EvalSymlinks(joined)
    if err != nil {
        h.t.Fatalf("%+v", errors.Wrapf(err, "internal testsuite error: could not get absolute path for dir(%q)", joined))
    }
    return abs
}

// MustExist fails if path does not exist.
func (h *Helper) MustExist(path string) {
    if err := h.ShouldExist(path); err != nil {
        h.t.Fatalf("%+v", err)
    }
}

// ShouldExist returns an error if path does not exist.
func (h *Helper) ShouldExist(path string) error {
    if !h.Exist(path) {
        return errors.Errorf("%s does not exist but should", path)
    }

    return nil
}

// Exist returns whether or not a path exists
func (h *Helper) Exist(path string) bool {
    if _, err := os.Stat(path); err != nil {
        if os.IsNotExist(err) {
            return false
        }
        h.t.Fatalf("%+v", errors.Wrapf(err, "Error checking if path exists: %s", path))
    }

    return true
}

// MustNotExist fails if path exists.
func (h *Helper) MustNotExist(path string) {
    if err := h.ShouldNotExist(path); err != nil {
        h.t.Fatalf("%+v", err)
    }
}

// ShouldNotExist returns an error if path exists.
func (h *Helper) ShouldNotExist(path string) error {
    if h.Exist(path) {
        return errors.Errorf("%s exists but should not", path)
    }

    return nil
}

// Cleanup cleans up a test that runs testgo.
func (h *Helper) Cleanup() {
    if h.wd != "" {
        if err := os.Chdir(h.wd); err != nil {
            // We are unlikely to be able to continue.
            fmt.Fprintln(os.Stderr, "could not restore working directory, crashing:", err)
            os.Exit(2)
        }
    }
    // NOTE(mattn): It seems that sometimes git.exe is not dead
    // when cleanup() is called. But we do not know any way to wait for it.
    if runtime.GOOS == "windows" {
        mu.Lock()
        exec.Command(`taskkill`, `/F`, `/IM`, `git.exe`).Run()
        mu.Unlock()
    }
    for _, path := range h.temps {
        h.check(os.RemoveAll(path))
    }
    if h.tempdir != "" {
        h.check(os.RemoveAll(h.tempdir))
    }
}

// ReadManifest returns the manifest in the current directory.
func (h *Helper) ReadManifest() string {
    m := filepath.Join(h.pwd(), manifestName)
    h.MustExist(m)

    f, err := ioutil.ReadFile(m)
    h.Must(err)
    return string(f)
}

// ReadLock returns the lock in the current directory.
func (h *Helper) ReadLock() string {
    l := filepath.Join(h.pwd(), lockName)
    h.MustExist(l)

    f, err := ioutil.ReadFile(l)
    h.Must(err)
    return string(f)
}

// GetCommit treats repo as a path to a git repository and returns the current
// revision.
func (h *Helper) GetCommit(repo string) string {
    repoPath := h.Path("pkg/dep/sources/https---" + strings.Replace(repo, "/", "-", -1))
    cmd := exec.Command("git", "rev-parse", "HEAD")
    cmd.Dir = repoPath
    out, err := cmd.CombinedOutput()
    if err != nil {
        h.t.Fatalf("%+v", errors.Wrapf(err, "git commit failed: out -> %s", string(out)))
    }
    return strings.TrimSpace(string(out))
}