Commit d06335e2 authored by Russ Cox's avatar Russ Cox

cmd/go: implement go clean -testcache

Ian suggested that since test caching is not expected to be perfect
in all cases, we should allow users to clear the test cache separately
from clearing the entire build cache.

This CL adds 'go clean -testcache' to do that. The implementation
does not actually delete files (for that, use 'go clean -cache').
Instead, it writes down the current time, and future go tests will
ignore any cached test results written before that time.

Change-Id: I4f84065d7dfc2499fa3f203e9ab62e68d7f367c5
Reviewed-on: https://go-review.googlesource.com/78176Reviewed-by: 's avatarIan Lance Taylor <iant@golang.org>
parent 5a7fd403
...@@ -14,7 +14,7 @@ ...@@ -14,7 +14,7 @@
// The commands are: // The commands are:
// //
// build compile packages and dependencies // build compile packages and dependencies
// clean remove object files // clean remove object files and cached files
// doc show documentation for package or symbol // doc show documentation for package or symbol
// env print Go environment information // env print Go environment information
// bug start a bug report // bug start a bug report
...@@ -170,11 +170,11 @@ ...@@ -170,11 +170,11 @@
// See also: go install, go get, go clean. // See also: go install, go get, go clean.
// //
// //
// Remove object files // Remove object files and cached files
// //
// Usage: // Usage:
// //
// go clean [-i] [-r] [-n] [-x] [-cache] [build flags] [packages] // go clean [-i] [-r] [-n] [-x] [-cache] [-testcache] [build flags] [packages]
// //
// Clean removes object files from package source directories. // Clean removes object files from package source directories.
// The go command builds most objects in a temporary directory, // The go command builds most objects in a temporary directory,
...@@ -212,8 +212,10 @@ ...@@ -212,8 +212,10 @@
// //
// The -x flag causes clean to print remove commands as it executes them. // The -x flag causes clean to print remove commands as it executes them.
// //
// The -cache flag causes clean to remove the entire go build cache, // The -cache flag causes clean to remove the entire go build cache.
// in addition to cleaning specified packages (if any). //
// The -testcache flag causes clean to expire all test results in the
// go build cache.
// //
// For more about build flags, see 'go help build'. // For more about build flags, see 'go help build'.
// //
...@@ -576,7 +578,7 @@ ...@@ -576,7 +578,7 @@
// //
// Usage: // Usage:
// //
// go list [-deps] [-e] [-f format] [-json] [build flags] [packages] // go list [-e] [-f format] [-json] [build flags] [packages]
// //
// List lists the packages named by the import paths, one per line. // List lists the packages named by the import paths, one per line.
// //
...@@ -680,9 +682,6 @@ ...@@ -680,9 +682,6 @@
// The -json flag causes the package data to be printed in JSON format // The -json flag causes the package data to be printed in JSON format
// instead of using the template format. // instead of using the template format.
// //
// The -deps flag causes list to add to its output all the dependencies of
// the packages named on the command line.
//
// The -e flag changes the handling of erroneous packages, those that // The -e flag changes the handling of erroneous packages, those that
// cannot be found or are malformed. By default, the list command // cannot be found or are malformed. By default, the list command
// prints an error to standard error for each erroneous package and // prints an error to standard error for each erroneous package and
......
...@@ -4970,6 +4970,10 @@ func TestTestCache(t *testing.T) { ...@@ -4970,6 +4970,10 @@ func TestTestCache(t *testing.T) {
tg.run("test", "-timeout=1ns", "-x", "errors") tg.run("test", "-timeout=1ns", "-x", "errors")
tg.grepStderrNot(`errors\.test`, "incorrectly ran test") tg.grepStderrNot(`errors\.test`, "incorrectly ran test")
tg.run("clean", "-testcache")
tg.run("test", "-x", "errors")
tg.grepStderr(`errors\.test`, "did not run test")
// The -p=1 in the commands below just makes the -x output easier to read. // The -p=1 in the commands below just makes the -x output easier to read.
t.Log("\n\nINITIAL\n\n") t.Log("\n\nINITIAL\n\n")
......
...@@ -81,9 +81,9 @@ func (c *Cache) fileName(id [HashSize]byte, key string) string { ...@@ -81,9 +81,9 @@ func (c *Cache) fileName(id [HashSize]byte, key string) string {
var errMissing = errors.New("cache entry not found") var errMissing = errors.New("cache entry not found")
const ( const (
// action entry file is "v1 <hex id> <hex out> <decimal size space-padded to 20 bytes>\n" // action entry file is "v1 <hex id> <hex out> <decimal size space-padded to 20 bytes> <unixnano space-padded to 20 bytes>\n"
hexSize = HashSize * 2 hexSize = HashSize * 2
entrySize = 2 + 1 + hexSize + 1 + hexSize + 1 + 20 + 1 entrySize = 2 + 1 + hexSize + 1 + hexSize + 1 + 20 + 1 + 20 + 1
) )
// verify controls whether to run the cache in verify mode. // verify controls whether to run the cache in verify mode.
...@@ -117,18 +117,24 @@ func initEnv() { ...@@ -117,18 +117,24 @@ func initEnv() {
// returning the corresponding output ID and file size, if any. // returning the corresponding output ID and file size, if any.
// Note that finding an output ID does not guarantee that the // Note that finding an output ID does not guarantee that the
// saved file for that output ID is still available. // saved file for that output ID is still available.
func (c *Cache) Get(id ActionID) (OutputID, int64, error) { func (c *Cache) Get(id ActionID) (Entry, error) {
if verify { if verify {
return OutputID{}, 0, errMissing return Entry{}, errMissing
} }
return c.get(id) return c.get(id)
} }
type Entry struct {
OutputID OutputID
Size int64
Time time.Time
}
// get is Get but does not respect verify mode, so that Put can use it. // get is Get but does not respect verify mode, so that Put can use it.
func (c *Cache) get(id ActionID) (OutputID, int64, error) { func (c *Cache) get(id ActionID) (Entry, error) {
missing := func() (OutputID, int64, error) { missing := func() (Entry, error) {
fmt.Fprintf(c.log, "%d miss %x\n", c.now().Unix(), id) fmt.Fprintf(c.log, "%d miss %x\n", c.now().Unix(), id)
return OutputID{}, 0, errMissing return Entry{}, errMissing
} }
f, err := os.Open(c.fileName(id, "a")) f, err := os.Open(c.fileName(id, "a"))
if err != nil { if err != nil {
...@@ -139,10 +145,13 @@ func (c *Cache) get(id ActionID) (OutputID, int64, error) { ...@@ -139,10 +145,13 @@ func (c *Cache) get(id ActionID) (OutputID, int64, error) {
if n, err := io.ReadFull(f, entry); n != entrySize || err != io.ErrUnexpectedEOF { if n, err := io.ReadFull(f, entry); n != entrySize || err != io.ErrUnexpectedEOF {
return missing() return missing()
} }
if entry[0] != 'v' || entry[1] != '1' || entry[2] != ' ' || entry[3+hexSize] != ' ' || entry[3+hexSize+1+64] != ' ' || entry[entrySize-1] != '\n' { if entry[0] != 'v' || entry[1] != '1' || entry[2] != ' ' || entry[3+hexSize] != ' ' || entry[3+hexSize+1+hexSize] != ' ' || entry[3+hexSize+1+hexSize+1+20] != ' ' || entry[entrySize-1] != '\n' {
return missing() return missing()
} }
eid, eout, esize := entry[3:3+hexSize], entry[3+hexSize+1:3+hexSize+1+hexSize], entry[3+hexSize+1+hexSize+1:entrySize-1] eid, entry := entry[3:3+hexSize], entry[3+hexSize:]
eout, entry := entry[1:1+hexSize], entry[1+hexSize:]
esize, entry := entry[1:1+20], entry[1+20:]
etime, entry := entry[1:1+20], entry[1+20:]
var buf [HashSize]byte var buf [HashSize]byte
if _, err := hex.Decode(buf[:], eid); err != nil || buf != id { if _, err := hex.Decode(buf[:], eid); err != nil || buf != id {
return missing() return missing()
...@@ -158,6 +167,14 @@ func (c *Cache) get(id ActionID) (OutputID, int64, error) { ...@@ -158,6 +167,14 @@ func (c *Cache) get(id ActionID) (OutputID, int64, error) {
if err != nil || size < 0 { if err != nil || size < 0 {
return missing() return missing()
} }
i = 0
for i < len(etime) && etime[i] == ' ' {
i++
}
tm, err := strconv.ParseInt(string(etime[i:]), 10, 64)
if err != nil || size < 0 {
return missing()
}
fmt.Fprintf(c.log, "%d get %x\n", c.now().Unix(), id) fmt.Fprintf(c.log, "%d get %x\n", c.now().Unix(), id)
...@@ -165,22 +182,22 @@ func (c *Cache) get(id ActionID) (OutputID, int64, error) { ...@@ -165,22 +182,22 @@ func (c *Cache) get(id ActionID) (OutputID, int64, error) {
// so that mtime reflects cache access time. // so that mtime reflects cache access time.
os.Chtimes(c.fileName(id, "a"), c.now(), c.now()) os.Chtimes(c.fileName(id, "a"), c.now(), c.now())
return buf, size, nil return Entry{buf, size, time.Unix(0, tm)}, nil
} }
// GetBytes looks up the action ID in the cache and returns // GetBytes looks up the action ID in the cache and returns
// the corresponding output bytes. // the corresponding output bytes.
// GetBytes should only be used for data that can be expected to fit in memory. // GetBytes should only be used for data that can be expected to fit in memory.
func (c *Cache) GetBytes(id ActionID) ([]byte, error) { func (c *Cache) GetBytes(id ActionID) ([]byte, Entry, error) {
out, _, err := c.Get(id) entry, err := c.Get(id)
if err != nil { if err != nil {
return nil, err return nil, entry, err
} }
data, _ := ioutil.ReadFile(c.OutputFile(out)) data, _ := ioutil.ReadFile(c.OutputFile(entry.OutputID))
if sha256.Sum256(data) != out { if sha256.Sum256(data) != entry.OutputID {
return nil, errMissing return nil, entry, errMissing
} }
return data, nil return data, entry, nil
} }
// OutputFile returns the name of the cache file storing output with the given OutputID. // OutputFile returns the name of the cache file storing output with the given OutputID.
...@@ -208,11 +225,11 @@ func (c *Cache) putIndexEntry(id ActionID, out OutputID, size int64, allowVerify ...@@ -208,11 +225,11 @@ func (c *Cache) putIndexEntry(id ActionID, out OutputID, size int64, allowVerify
// in verify mode we are double-checking that the cache entries // in verify mode we are double-checking that the cache entries
// are entirely reproducible. As just noted, this may be unrealistic // are entirely reproducible. As just noted, this may be unrealistic
// in some cases but the check is also useful for shaking out real bugs. // in some cases but the check is also useful for shaking out real bugs.
entry := []byte(fmt.Sprintf("v1 %x %x %20d\n", id, out, size)) entry := []byte(fmt.Sprintf("v1 %x %x %20d %20d\n", id, out, size, time.Now().UnixNano()))
if verify && allowVerify { if verify && allowVerify {
oldOut, oldSize, err := c.get(id) old, err := c.get(id)
if err == nil && (oldOut != out || oldSize != size) { if err == nil && (old.OutputID != out || old.Size != size) {
fmt.Fprintf(os.Stderr, "go: internal cache error: id=%x changed:<<<\n%s\n>>>\nold: %x %d\nnew: %x %d\n", id, reverseHash(id), out, size, oldOut, oldSize) fmt.Fprintf(os.Stderr, "go: internal cache error: id=%x changed:<<<\n%s\n>>>\nold: %x %d\nnew: %x %d\n", id, reverseHash(id), out, size, old.OutputID, old.Size)
// panic to show stack trace, so we can see what code is generating this cache entry. // panic to show stack trace, so we can see what code is generating this cache entry.
panic("cache verify failed") panic("cache verify failed")
} }
......
...@@ -11,6 +11,7 @@ import ( ...@@ -11,6 +11,7 @@ import (
"os" "os"
"path/filepath" "path/filepath"
"strings" "strings"
"time"
"cmd/go/internal/base" "cmd/go/internal/base"
"cmd/go/internal/cache" "cmd/go/internal/cache"
...@@ -20,8 +21,8 @@ import ( ...@@ -20,8 +21,8 @@ import (
) )
var CmdClean = &base.Command{ var CmdClean = &base.Command{
UsageLine: "clean [-i] [-r] [-n] [-x] [-cache] [build flags] [packages]", UsageLine: "clean [-i] [-r] [-n] [-x] [-cache] [-testcache] [build flags] [packages]",
Short: "remove object files", Short: "remove object files and cached files",
Long: ` Long: `
Clean removes object files from package source directories. Clean removes object files from package source directories.
The go command builds most objects in a temporary directory, The go command builds most objects in a temporary directory,
...@@ -59,8 +60,10 @@ dependencies of the packages named by the import paths. ...@@ -59,8 +60,10 @@ dependencies of the packages named by the import paths.
The -x flag causes clean to print remove commands as it executes them. The -x flag causes clean to print remove commands as it executes them.
The -cache flag causes clean to remove the entire go build cache, The -cache flag causes clean to remove the entire go build cache.
in addition to cleaning specified packages (if any).
The -testcache flag causes clean to expire all test results in the
go build cache.
For more about build flags, see 'go help build'. For more about build flags, see 'go help build'.
...@@ -69,9 +72,10 @@ For more about specifying packages, see 'go help packages'. ...@@ -69,9 +72,10 @@ For more about specifying packages, see 'go help packages'.
} }
var ( var (
cleanI bool // clean -i flag cleanI bool // clean -i flag
cleanR bool // clean -r flag cleanR bool // clean -r flag
cleanCache bool // clean -cache flag cleanCache bool // clean -cache flag
cleanTestcache bool // clean -testcache flag
) )
func init() { func init() {
...@@ -81,6 +85,7 @@ func init() { ...@@ -81,6 +85,7 @@ func init() {
CmdClean.Flag.BoolVar(&cleanI, "i", false, "") CmdClean.Flag.BoolVar(&cleanI, "i", false, "")
CmdClean.Flag.BoolVar(&cleanR, "r", false, "") CmdClean.Flag.BoolVar(&cleanR, "r", false, "")
CmdClean.Flag.BoolVar(&cleanCache, "cache", false, "") CmdClean.Flag.BoolVar(&cleanCache, "cache", false, "")
CmdClean.Flag.BoolVar(&cleanTestcache, "testcache", false, "")
// -n and -x are important enough to be // -n and -x are important enough to be
// mentioned explicitly in the docs but they // mentioned explicitly in the docs but they
...@@ -120,6 +125,19 @@ func runClean(cmd *base.Command, args []string) { ...@@ -120,6 +125,19 @@ func runClean(cmd *base.Command, args []string) {
} }
} }
} }
if cleanTestcache && !cleanCache {
// Instead of walking through the entire cache looking for test results,
// we write a file to the cache indicating that all test results from before
// right now are to be ignored.
dir := cache.DefaultDir()
if dir != "off" {
err := ioutil.WriteFile(filepath.Join(dir, "testexpire.txt"), []byte(fmt.Sprintf("%d\n", time.Now().UnixNano())), 0666)
if err != nil {
base.Errorf("go clean -testcache: %v", err)
}
}
}
} }
var cleaned = map[*load.Package]bool{} var cleaned = map[*load.Package]bool{}
......
...@@ -14,12 +14,14 @@ import ( ...@@ -14,12 +14,14 @@ import (
"go/parser" "go/parser"
"go/token" "go/token"
"io" "io"
"io/ioutil"
"os" "os"
"os/exec" "os/exec"
"path" "path"
"path/filepath" "path/filepath"
"regexp" "regexp"
"sort" "sort"
"strconv"
"strings" "strings"
"sync" "sync"
"text/template" "text/template"
...@@ -476,6 +478,7 @@ var ( ...@@ -476,6 +478,7 @@ var (
pkgs []*load.Package pkgs []*load.Package
testKillTimeout = 10 * time.Minute testKillTimeout = 10 * time.Minute
testCacheExpire time.Time // ignore cached test results before this time
) )
var testMainDeps = []string{ var testMainDeps = []string{
...@@ -554,6 +557,17 @@ func runTest(cmd *base.Command, args []string) { ...@@ -554,6 +557,17 @@ func runTest(cmd *base.Command, args []string) {
testC = true testC = true
} }
// Read testcache expiration time, if present.
// (We implement go clean -testcache by writing an expiration date
// instead of searching out and deleting test result cache entries.)
if dir := cache.DefaultDir(); dir != "off" {
if data, _ := ioutil.ReadFile(filepath.Join(dir, "testexpire.txt")); len(data) > 0 && data[len(data)-1] == '\n' {
if t, err := strconv.ParseInt(string(data[:len(data)-1]), 10, 64); err == nil {
testCacheExpire = time.Unix(0, t)
}
}
}
var b work.Builder var b work.Builder
b.Init() b.Init()
...@@ -1443,10 +1457,13 @@ func (c *runCache) tryCacheWithID(b *work.Builder, a *work.Action, id string) bo ...@@ -1443,10 +1457,13 @@ func (c *runCache) tryCacheWithID(b *work.Builder, a *work.Action, id string) bo
// Parse cached result in preparation for changing run time to "(cached)". // Parse cached result in preparation for changing run time to "(cached)".
// If we can't parse the cached result, don't use it. // If we can't parse the cached result, don't use it.
data, _ := cache.Default().GetBytes(testID) data, entry, _ := cache.Default().GetBytes(testID)
if len(data) == 0 || data[len(data)-1] != '\n' { if len(data) == 0 || data[len(data)-1] != '\n' {
return false return false
} }
if entry.Time.Before(testCacheExpire) {
return false
}
i := bytes.LastIndexByte(data[:len(data)-1], '\n') + 1 i := bytes.LastIndexByte(data[:len(data)-1], '\n') + 1
if !bytes.HasPrefix(data[i:], []byte("ok \t")) { if !bytes.HasPrefix(data[i:], []byte("ok \t")) {
return false return false
......
...@@ -364,18 +364,17 @@ func (b *Builder) useCache(a *Action, p *load.Package, actionHash cache.ActionID ...@@ -364,18 +364,17 @@ func (b *Builder) useCache(a *Action, p *load.Package, actionHash cache.ActionID
// but we're still happy to use results from the build artifact cache. // but we're still happy to use results from the build artifact cache.
if c := cache.Default(); c != nil { if c := cache.Default(); c != nil {
if !cfg.BuildA { if !cfg.BuildA {
outputID, size, err := c.Get(actionHash) entry, err := c.Get(actionHash)
if err == nil { if err == nil {
file := c.OutputFile(outputID) file := c.OutputFile(entry.OutputID)
info, err1 := os.Stat(file) info, err1 := os.Stat(file)
buildID, err2 := buildid.ReadFile(file) buildID, err2 := buildid.ReadFile(file)
if err1 == nil && err2 == nil && info.Size() == size { if err1 == nil && err2 == nil && info.Size() == entry.Size {
stdout, err := c.GetBytes(cache.Subkey(a.actionID, "stdout")) stdout, stdoutEntry, err := c.GetBytes(cache.Subkey(a.actionID, "stdout"))
if err == nil { if err == nil {
if len(stdout) > 0 { if len(stdout) > 0 {
if cfg.BuildX || cfg.BuildN { if cfg.BuildX || cfg.BuildN {
id, _, _ := c.Get(cache.Subkey(a.actionID, "stdout")) b.Showcmd("", "%s # internal", joinUnambiguously(str.StringList("cat", c.OutputFile(stdoutEntry.OutputID))))
b.Showcmd("", "%s # internal", joinUnambiguously(str.StringList("cat", c.OutputFile(id))))
} }
if !cfg.BuildN { if !cfg.BuildN {
b.Print(string(stdout)) b.Print(string(stdout))
......
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment