123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209 |
- /*
- Copyright 2016 The Kubernetes Authors.
- Licensed under the Apache License, Version 2.0 (the "License");
- you may not use this file except in compliance with the License.
- You may obtain a copy of the License at
- http://www.apache.org/licenses/LICENSE-2.0
- Unless required by applicable law or agreed to in writing, software
- distributed under the License is distributed on an "AS IS" BASIS,
- WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- See the License for the specific language governing permissions and
- limitations under the License.
- */
- // teststale checks the staleness of a test binary. go test -c builds a test
- // binary but it does no staleness check. In other words, every time one runs
- // go test -c, it compiles the test packages and links the binary even when
- // nothing has changed. This program helps to mitigate that problem by allowing
- // to check the staleness of a given test package and its binary.
- package main
- import (
- "encoding/json"
- "flag"
- "fmt"
- "io"
- "os"
- "os/exec"
- "path/filepath"
- "time"
- "github.com/golang/glog"
- )
- const usageHelp = "" +
- `This program checks the staleness of a given test package and its test
- binary so that one can make a decision about re-building the test binary.
- Usage:
- teststale -binary=/path/to/test/binary -package=package
- Example:
- teststale -binary="$HOME/gosrc/bin/e2e.test" -package="k8s.io/kubernetes/test/e2e"
- `
- var (
- binary = flag.String("binary", "", "filesystem path to the test binary file. Example: \"$HOME/gosrc/bin/e2e.test\"")
- pkgPath = flag.String("package", "", "import path of the test package in the format used while importing packages. Example: \"k8s.io/kubernetes/test/e2e\"")
- )
- func usage() {
- fmt.Fprintln(os.Stderr, usageHelp)
- fmt.Fprintln(os.Stderr, "Flags:")
- flag.PrintDefaults()
- os.Exit(2)
- }
- // golist is an interface emulating the `go list` command to get package information.
- // TODO: Evaluate using `go/build` package instead. It doesn't provide staleness
- // information, but we can probably run `go list` and `go/build.Import()` concurrently
- // in goroutines and merge the results. Evaluate if that's faster.
- type golist interface {
- pkgInfo(pkgPaths []string) ([]pkg, error)
- }
- // execmd implements the `golist` interface.
- type execcmd struct {
- cmd string
- args []string
- env []string
- }
- func (e *execcmd) pkgInfo(pkgPaths []string) ([]pkg, error) {
- args := append(e.args, pkgPaths...)
- cmd := exec.Command(e.cmd, args...)
- cmd.Env = e.env
- stdout, err := cmd.StdoutPipe()
- if err != nil {
- return nil, fmt.Errorf("failed to obtain the metadata output stream: %v", err)
- }
- dec := json.NewDecoder(stdout)
- // Start executing the command
- if err := cmd.Start(); err != nil {
- return nil, fmt.Errorf("command did not start: %v", err)
- }
- var pkgs []pkg
- for {
- var p pkg
- if err := dec.Decode(&p); err == io.EOF {
- break
- } else if err != nil {
- return nil, fmt.Errorf("failed to unmarshal metadata for package %s: %v", p.ImportPath, err)
- }
- pkgs = append(pkgs, p)
- }
- if err := cmd.Wait(); err != nil {
- return nil, fmt.Errorf("command did not complete: %v", err)
- }
- return pkgs, nil
- }
- type pkg struct {
- Dir string
- ImportPath string
- Target string
- Stale bool
- TestGoFiles []string
- TestImports []string
- XTestGoFiles []string
- XTestImports []string
- }
- func (p *pkg) isNewerThan(cmd golist, buildTime time.Time) bool {
- // If the package itself is stale, then we have to rebuild the whole thing anyway.
- if p.Stale {
- return true
- }
- // Test for file staleness
- for _, f := range p.TestGoFiles {
- if isNewerThan(filepath.Join(p.Dir, f), buildTime) {
- glog.V(4).Infof("test Go file %s is stale", f)
- return true
- }
- }
- for _, f := range p.XTestGoFiles {
- if isNewerThan(filepath.Join(p.Dir, f), buildTime) {
- glog.V(4).Infof("external test Go file %s is stale", f)
- return true
- }
- }
- imps := []string{}
- imps = append(imps, p.TestImports...)
- imps = append(imps, p.XTestImports...)
- // This calls `go list` the second time. This is required because the first
- // call to `go list` checks the staleness of the package in question by
- // looking the non-test dependencies, but it doesn't look at the test
- // dependencies. However, it returns the list of test dependencies. This
- // second call to `go list` checks the staleness of all the test
- // dependencies.
- pkgs, err := cmd.pkgInfo(imps)
- if err != nil || len(pkgs) < 1 {
- glog.V(4).Infof("failed to obtain metadata for packages %s: %v", imps, err)
- return true
- }
- for _, p := range pkgs {
- if p.Stale {
- glog.V(4).Infof("import %q is stale", p.ImportPath)
- return true
- }
- }
- return false
- }
- func isNewerThan(filename string, buildTime time.Time) bool {
- stat, err := os.Stat(filename)
- if err != nil {
- return true
- }
- return stat.ModTime().After(buildTime)
- }
- // isTestStale checks if the test binary is stale and needs to rebuilt.
- // Some of the ideas here are inspired by how Go does staleness checks.
- func isTestStale(cmd golist, binPath, pkgPath string) bool {
- bStat, err := os.Stat(binPath)
- if err != nil {
- glog.V(4).Infof("Couldn't obtain the modified time of the binary %s: %v", binPath, err)
- return true
- }
- buildTime := bStat.ModTime()
- pkgs, err := cmd.pkgInfo([]string{pkgPath})
- if err != nil || len(pkgs) < 1 {
- glog.V(4).Infof("Couldn't retrieve test package information for package %s: %v", pkgPath, err)
- return false
- }
- return pkgs[0].isNewerThan(cmd, buildTime)
- }
- func main() {
- flag.Usage = usage
- flag.Parse()
- cmd := &execcmd{
- cmd: "go",
- args: []string{
- "list",
- "-json",
- },
- env: os.Environ(),
- }
- if !isTestStale(cmd, *binary, *pkgPath) {
- os.Exit(1)
- }
- }
|