teststale.go 5.5 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209
  1. /*
  2. Copyright 2016 The Kubernetes Authors.
  3. Licensed under the Apache License, Version 2.0 (the "License");
  4. you may not use this file except in compliance with the License.
  5. You may obtain a copy of the License at
  6. http://www.apache.org/licenses/LICENSE-2.0
  7. Unless required by applicable law or agreed to in writing, software
  8. distributed under the License is distributed on an "AS IS" BASIS,
  9. WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
  10. See the License for the specific language governing permissions and
  11. limitations under the License.
  12. */
  13. // teststale checks the staleness of a test binary. go test -c builds a test
  14. // binary but it does no staleness check. In other words, every time one runs
  15. // go test -c, it compiles the test packages and links the binary even when
  16. // nothing has changed. This program helps to mitigate that problem by allowing
  17. // to check the staleness of a given test package and its binary.
  18. package main
  19. import (
  20. "encoding/json"
  21. "flag"
  22. "fmt"
  23. "io"
  24. "os"
  25. "os/exec"
  26. "path/filepath"
  27. "time"
  28. "github.com/golang/glog"
  29. )
  30. const usageHelp = "" +
  31. `This program checks the staleness of a given test package and its test
  32. binary so that one can make a decision about re-building the test binary.
  33. Usage:
  34. teststale -binary=/path/to/test/binary -package=package
  35. Example:
  36. teststale -binary="$HOME/gosrc/bin/e2e.test" -package="k8s.io/kubernetes/test/e2e"
  37. `
  38. var (
  39. binary = flag.String("binary", "", "filesystem path to the test binary file. Example: \"$HOME/gosrc/bin/e2e.test\"")
  40. pkgPath = flag.String("package", "", "import path of the test package in the format used while importing packages. Example: \"k8s.io/kubernetes/test/e2e\"")
  41. )
  42. func usage() {
  43. fmt.Fprintln(os.Stderr, usageHelp)
  44. fmt.Fprintln(os.Stderr, "Flags:")
  45. flag.PrintDefaults()
  46. os.Exit(2)
  47. }
  48. // golist is an interface emulating the `go list` command to get package information.
  49. // TODO: Evaluate using `go/build` package instead. It doesn't provide staleness
  50. // information, but we can probably run `go list` and `go/build.Import()` concurrently
  51. // in goroutines and merge the results. Evaluate if that's faster.
  52. type golist interface {
  53. pkgInfo(pkgPaths []string) ([]pkg, error)
  54. }
  55. // execmd implements the `golist` interface.
  56. type execcmd struct {
  57. cmd string
  58. args []string
  59. env []string
  60. }
  61. func (e *execcmd) pkgInfo(pkgPaths []string) ([]pkg, error) {
  62. args := append(e.args, pkgPaths...)
  63. cmd := exec.Command(e.cmd, args...)
  64. cmd.Env = e.env
  65. stdout, err := cmd.StdoutPipe()
  66. if err != nil {
  67. return nil, fmt.Errorf("failed to obtain the metadata output stream: %v", err)
  68. }
  69. dec := json.NewDecoder(stdout)
  70. // Start executing the command
  71. if err := cmd.Start(); err != nil {
  72. return nil, fmt.Errorf("command did not start: %v", err)
  73. }
  74. var pkgs []pkg
  75. for {
  76. var p pkg
  77. if err := dec.Decode(&p); err == io.EOF {
  78. break
  79. } else if err != nil {
  80. return nil, fmt.Errorf("failed to unmarshal metadata for package %s: %v", p.ImportPath, err)
  81. }
  82. pkgs = append(pkgs, p)
  83. }
  84. if err := cmd.Wait(); err != nil {
  85. return nil, fmt.Errorf("command did not complete: %v", err)
  86. }
  87. return pkgs, nil
  88. }
  89. type pkg struct {
  90. Dir string
  91. ImportPath string
  92. Target string
  93. Stale bool
  94. TestGoFiles []string
  95. TestImports []string
  96. XTestGoFiles []string
  97. XTestImports []string
  98. }
  99. func (p *pkg) isNewerThan(cmd golist, buildTime time.Time) bool {
  100. // If the package itself is stale, then we have to rebuild the whole thing anyway.
  101. if p.Stale {
  102. return true
  103. }
  104. // Test for file staleness
  105. for _, f := range p.TestGoFiles {
  106. if isNewerThan(filepath.Join(p.Dir, f), buildTime) {
  107. glog.V(4).Infof("test Go file %s is stale", f)
  108. return true
  109. }
  110. }
  111. for _, f := range p.XTestGoFiles {
  112. if isNewerThan(filepath.Join(p.Dir, f), buildTime) {
  113. glog.V(4).Infof("external test Go file %s is stale", f)
  114. return true
  115. }
  116. }
  117. imps := []string{}
  118. imps = append(imps, p.TestImports...)
  119. imps = append(imps, p.XTestImports...)
  120. // This calls `go list` the second time. This is required because the first
  121. // call to `go list` checks the staleness of the package in question by
  122. // looking the non-test dependencies, but it doesn't look at the test
  123. // dependencies. However, it returns the list of test dependencies. This
  124. // second call to `go list` checks the staleness of all the test
  125. // dependencies.
  126. pkgs, err := cmd.pkgInfo(imps)
  127. if err != nil || len(pkgs) < 1 {
  128. glog.V(4).Infof("failed to obtain metadata for packages %s: %v", imps, err)
  129. return true
  130. }
  131. for _, p := range pkgs {
  132. if p.Stale {
  133. glog.V(4).Infof("import %q is stale", p.ImportPath)
  134. return true
  135. }
  136. }
  137. return false
  138. }
  139. func isNewerThan(filename string, buildTime time.Time) bool {
  140. stat, err := os.Stat(filename)
  141. if err != nil {
  142. return true
  143. }
  144. return stat.ModTime().After(buildTime)
  145. }
  146. // isTestStale checks if the test binary is stale and needs to rebuilt.
  147. // Some of the ideas here are inspired by how Go does staleness checks.
  148. func isTestStale(cmd golist, binPath, pkgPath string) bool {
  149. bStat, err := os.Stat(binPath)
  150. if err != nil {
  151. glog.V(4).Infof("Couldn't obtain the modified time of the binary %s: %v", binPath, err)
  152. return true
  153. }
  154. buildTime := bStat.ModTime()
  155. pkgs, err := cmd.pkgInfo([]string{pkgPath})
  156. if err != nil || len(pkgs) < 1 {
  157. glog.V(4).Infof("Couldn't retrieve test package information for package %s: %v", pkgPath, err)
  158. return false
  159. }
  160. return pkgs[0].isNewerThan(cmd, buildTime)
  161. }
  162. func main() {
  163. flag.Usage = usage
  164. flag.Parse()
  165. cmd := &execcmd{
  166. cmd: "go",
  167. args: []string{
  168. "list",
  169. "-json",
  170. },
  171. env: os.Environ(),
  172. }
  173. if !isTestStale(cmd, *binary, *pkgPath) {
  174. os.Exit(1)
  175. }
  176. }