Permalink
Switch branches/tags
Nothing to show
Find file
ec959be Apr 10, 2017
520 lines (472 sloc) 10.6 KB
// Copyright 2015 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.
// Gt is a wrapper for ``go test'' that caches test results.
//
// The usage of ``gt'' is nearly identical to that of ``go test.''
//
// gt [-f] [-l] [arguments for "go test"]
//
// The difference between ``gt'' and ``go test'' is that when testing
// a list of packages, if a package and its dependencies have not changed
// since the last run, ``gt'' reuses the previous result.
//
// The -f flag causes gt to treat all test results as uncached, as does the
// use of any ``go test'' flag other than -short and -v.
//
// The -l flag causes gt to list the uncached tests it would run.
//
// Cached test results are saved in $CACHE/go-test-cache if $CACHE is set,
// or else $HOME/Library/Caches/go-test-cache on OS X
// and $HOME/.cache/go-test-cache on other systems.
// It is always safe to delete these directories if they become too large.
//
// Gt is an experiment in what it would mean and how well it would work
// to cache test results. If the experiment proves successful, the functionality
// may move into the standard go command.
//
// Examples
//
// Run (and cache) the strings test:
//
// $ gt strings
// ok strings 0.436s
// $
//
// List tests in str... without cached results:
//
// $ gt -l str...
// strconv
// $
//
// Run str... tests:
//
// $ gt str...
// ok strconv 1.548s
// ok strings 0.436s (cached)
// $
//
// Force rerun of both:
//
// $ gt -f str...
// ok strconv 1.795s
// ok strings 0.629s
// $
//
package main // import "rsc.io/gt"
import (
"bufio"
"bytes"
"crypto/sha1"
"encoding/json"
"fmt"
"io"
"io/ioutil"
"log"
"os"
"os/exec"
"path/filepath"
"regexp"
"runtime"
"strings"
"time"
)
func usage() {
fmt.Fprint(os.Stderr, "usage: gt [arguments for \"go test\"]\n")
os.Exit(2)
}
var (
flagV bool
flagShort bool
flagRace bool
flagList bool
flagForce bool
flagTiming bool
failed bool
cacheDir string
start = time.Now()
)
func main() {
log.SetFlags(0)
log.SetPrefix("gt: ")
opts, pkgs := parseFlags()
if len(pkgs) == 0 {
if flagList {
log.Fatal("cannot use -l without package list or with testing flags other than -v and -short")
}
cmd := exec.Command("go", append([]string{"test"}, os.Args[1:]...)...)
cmd.Stdin = os.Stdin
cmd.Stdout = os.Stdout
cmd.Stderr = os.Stderr
err := cmd.Run()
if err != nil {
log.Fatalf("go test: %v", err)
}
return
}
if flagTiming {
log.Printf("%.2fs go list", time.Since(start).Seconds())
}
// Expand pkg list.
out, err := exec.Command("go", append([]string{"list"}, pkgs...)...).CombinedOutput()
if err != nil {
log.Fatalf("go list: %v", err)
}
pkgs = strings.Fields(string(out))
if flagTiming {
log.Printf("%.2fs go list -json", time.Since(start).Seconds())
}
// Build list of all dependencies.
readPkgs(pkgs)
first := true
next := pkgs
for {
var deps []string
for _, path := range next {
p := pkgInfo[path]
if p.Incomplete {
log.Fatalf("go list: errors loading packages")
}
for _, dep := range p.Deps {
if _, ok := pkgInfo[dep]; !ok {
pkgInfo[dep] = nil
deps = append(deps, dep)
}
}
if first {
for _, dep := range p.TestImports {
if _, ok := pkgInfo[dep]; !ok {
pkgInfo[dep] = nil
deps = append(deps, dep)
}
}
for _, dep := range p.XTestImports {
if _, ok := pkgInfo[dep]; !ok {
pkgInfo[dep] = nil
deps = append(deps, dep)
}
}
}
}
if len(deps) == 0 {
break
}
if flagTiming {
log.Printf("%.2fs go list -json", time.Since(start).Seconds())
}
readPkgs(deps)
next = deps
first = false
}
if env := os.Getenv("CACHE"); env != "" {
cacheDir = fmt.Sprintf("%s/go-test-cache", env)
} else if runtime.GOOS == "darwin" {
cacheDir = fmt.Sprintf("%s/Library/Caches/go-test-cache", os.Getenv("HOME"))
} else {
cacheDir = fmt.Sprintf("%s/.cache/go-test-cache", os.Getenv("HOME"))
}
if flagTiming {
log.Printf("%.2fs compute hashes", time.Since(start).Seconds())
}
computeStale(pkgs)
var toRun []string
for _, pkg := range pkgs {
if !haveTestResult(pkg) {
toRun = append(toRun, pkg)
}
}
if flagTiming {
log.Printf("%.2fs ready to run", time.Since(start).Seconds())
}
if flagList {
for _, pkg := range toRun {
fmt.Printf("%s\n", pkg)
}
return
}
var cmd *exec.Cmd
pr, pw := io.Pipe()
r := bufio.NewReader(pr)
if len(toRun) > 0 {
if err := os.MkdirAll(cacheDir, 0700); err != nil {
log.Fatal(err)
}
args := []string{"test"}
args = append(args, opts...)
args = append(args, toRun...)
cmd = exec.Command("go", args...)
cmd.Stdout = pw
cmd.Stderr = pw
if err := cmd.Start(); err != nil {
log.Fatalf("go test: %v", err)
}
}
var cmdErr error
done := make(chan bool)
go func() {
if cmd != nil {
cmdErr = cmd.Wait()
}
pw.Close()
done <- true
}()
for _, pkg := range pkgs {
if len(toRun) > 0 && toRun[0] == pkg {
readTestResult(r, pkg)
toRun = toRun[1:]
} else {
showTestResult(pkg)
}
}
io.Copy(os.Stdout, r)
<-done
if cmdErr != nil && !failed {
log.Fatalf("go test: %v", cmdErr)
}
if flagTiming {
log.Printf("%.2fs done", time.Since(start).Seconds())
}
if failed {
os.Exit(1)
}
}
var (
pkgInfo = map[string]*Package{}
outOfSync bool
)
type Package struct {
Dir string
ImportPath string
Standard bool
Goroot bool
Stale bool
GoFiles []string
CgoFiles []string
CFiles []string
CXXFiles []string
MFiles []string
HFiles []string
SFiles []string
SwigFiles []string
SwigCXXFiles []string
SysoFiles []string
Imports []string
Deps []string
Incomplete bool
TestGoFiles []string
TestImports []string
XTestGoFiles []string
XTestImports []string
testHash string
pkgHash string
}
func readPkgs(pkgs []string) {
out, err := exec.Command("go", append([]string{"list", "-json"}, pkgs...)...).CombinedOutput()
if err != nil {
log.Fatalf("go list: %v\n%s", err, out)
}
dec := json.NewDecoder(bytes.NewReader(out))
for {
var p Package
if err := dec.Decode(&p); err != nil {
if err == io.EOF {
break
}
log.Fatalf("reading go list output: %v", err)
}
pkgInfo[p.ImportPath] = &p
}
}
func computeStale(pkgs []string) {
for _, pkg := range pkgs {
computeTestHash(pkgInfo[pkg])
}
}
func computeTestHash(p *Package) {
if p.testHash != "" {
return
}
p.testHash = "cycle"
computePkgHash(p)
h := sha1.New()
fmt.Fprintf(h, "test\n")
if flagRace {
fmt.Fprintf(h, "-race\n")
}
if flagShort {
fmt.Fprintf(h, "-short\n")
}
if flagV {
fmt.Fprintf(h, "-v\n")
}
fmt.Fprintf(h, "pkg %s\n", p.pkgHash)
for _, imp := range p.TestImports {
p1 := pkgInfo[imp]
computePkgHash(p1)
fmt.Fprintf(h, "testimport %s\n", p1.pkgHash)
}
for _, imp := range p.XTestImports {
p1 := pkgInfo[imp]
computePkgHash(p1)
fmt.Fprintf(h, "xtestimport %s\n", p1.pkgHash)
}
hashFiles(h, p.Dir, p.TestGoFiles)
hashFiles(h, p.Dir, p.XTestGoFiles)
p.testHash = fmt.Sprintf("%x", h.Sum(nil))
}
func computePkgHash(p *Package) {
if p.pkgHash != "" {
return
}
p.pkgHash = "cycle"
h := sha1.New()
fmt.Fprintf(h, "pkg\n")
for _, imp := range p.Deps {
p1 := pkgInfo[imp]
if p1 == nil {
log.Fatalf("lost package: %v for %v", imp, p.ImportPath)
}
computePkgHash(p1)
fmt.Fprintf(h, "import %s\n", p1.pkgHash)
}
hashFiles(h, p.Dir, p.GoFiles)
hashFiles(h, p.Dir, p.CgoFiles)
hashFiles(h, p.Dir, p.CFiles)
hashFiles(h, p.Dir, p.CXXFiles)
hashFiles(h, p.Dir, p.MFiles)
hashFiles(h, p.Dir, p.HFiles)
hashFiles(h, p.Dir, p.SFiles)
hashFiles(h, p.Dir, p.SwigFiles)
hashFiles(h, p.Dir, p.SwigCXXFiles)
hashFiles(h, p.Dir, p.SysoFiles)
p.pkgHash = fmt.Sprintf("%x", h.Sum(nil))
}
func hashFiles(h io.Writer, dir string, files []string) {
for _, file := range files {
f, err := os.Open(filepath.Join(dir, file))
if err != nil {
fmt.Fprintf(h, "%s error\n", file)
continue
}
fmt.Fprintf(h, "file %s\n", file)
n, _ := io.Copy(h, f)
fmt.Fprintf(h, "%d bytes\n", n)
f.Close()
}
}
func cacheFile(p *Package) string {
return filepath.Join(cacheDir, fmt.Sprintf("%s/%s.test", p.testHash[:3], p.testHash[3:]))
}
func haveTestResult(path string) bool {
if flagForce {
return false
}
p := pkgInfo[path]
if p.testHash == "cycle" {
return false
}
fi, err := os.Stat(cacheFile(pkgInfo[path]))
return err == nil && fi.Mode().IsRegular()
}
var fail = []byte("FAIL")
func showTestResult(path string) {
p := pkgInfo[path]
if p.testHash == "cycle" {
return
}
data, err := ioutil.ReadFile(cacheFile(pkgInfo[path]))
if err != nil {
fmt.Printf("%v\n", err)
fmt.Printf("FAIL\t%s\t(cached)\n", path)
return
}
os.Stdout.Write(data)
data = bytes.TrimSpace(data)
i := bytes.LastIndex(data, []byte{'\n'})
line := data[i+1:]
if bytes.HasPrefix(line, fail) {
failed = true
}
}
var endRE = regexp.MustCompile(`\A(\?|ok|FAIL) ? ? ?\t([^ \t]+)\t(([0-9.]+s)?\s*(\[.*\])?)\n\z`)
func readTestResult(r *bufio.Reader, path string) {
var buf bytes.Buffer
for {
line, err := r.ReadString('\n')
os.Stdout.WriteString(line)
if err != nil {
log.Fatalf("reading test output for %s: %v", path, err)
}
if outOfSync {
continue
}
m := endRE.FindStringSubmatch(line)
if m == nil {
buf.WriteString(line)
continue
}
if m[1] == "FAIL" {
failed = true
}
if m[2] != "" && m[2] != path {
log.Fatalf("reading test output for %s: found %s instead", path, m[2])
}
fmt.Fprintf(&buf, "%s (cached)\n", strings.TrimSuffix(line, "\n"))
file := cacheFile(pkgInfo[path])
if err := os.MkdirAll(filepath.Dir(file), 0700); err != nil {
log.Print(err)
} else if err := ioutil.WriteFile(file, buf.Bytes(), 0600); err != nil {
log.Print(err)
}
break
}
}
func parseFlags() (opts, pkgs []string) {
donePkgs := false
for i := 1; i < len(os.Args); i++ {
arg := os.Args[i]
if !strings.HasPrefix(arg, "-") {
if donePkgs {
// additional arguments after pkg list ended
return nil, nil
}
pkgs = append(pkgs, arg)
continue
}
donePkgs = len(pkgs) > 0
if strings.HasPrefix(arg, "--") && arg != "--" && !strings.HasPrefix(arg, "---") {
arg = arg[1:]
}
if arg == "-gt.timing" {
flagTiming = true
continue
}
if arg == "-v" {
flagV = true
opts = append(opts, arg)
donePkgs = len(pkgs) > 0
continue
}
if arg == "-race" {
flagRace = true
opts = append(opts, arg)
continue
}
if arg == "-short" {
flagShort = true
opts = append(opts, arg)
continue
}
if arg == "-f" {
flagForce = true
continue
}
if arg == "-l" {
flagList = true
continue
}
// unrecognized flag
return nil, nil
}
return opts, pkgs
}