go.uber.org/yarpc@v1.72.1/internal/cover/main.go (about) 1 // Copyright (c) 2022 Uber Technologies, Inc. 2 // 3 // Permission is hereby granted, free of charge, to any person obtaining a copy 4 // of this software and associated documentation files (the "Software"), to deal 5 // in the Software without restriction, including without limitation the rights 6 // to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 7 // copies of the Software, and to permit persons to whom the Software is 8 // furnished to do so, subject to the following conditions: 9 // 10 // The above copyright notice and this permission notice shall be included in 11 // all copies or substantial portions of the Software. 12 // 13 // THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 14 // IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 15 // FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 16 // AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 17 // LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 18 // OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN 19 // THE SOFTWARE. 20 21 // cover is a tool that runs `go test` with cross-package coverage on this 22 // repository, ignoring any packages that opt out of coverage with .nocover 23 // files. The coverage is written to a coverage.txt file in the current 24 // directory. 25 // 26 // Usage 27 // 28 // Call cover with a list of one or more import paths of packages being 29 // tested. 30 // 31 // cover PKG ... 32 // 33 // This must be run from the root of the project. 34 package main 35 36 import ( 37 "bufio" 38 "errors" 39 "fmt" 40 "io" 41 "io/ioutil" 42 "log" 43 "os" 44 "os/exec" 45 "path/filepath" 46 "strings" 47 48 "golang.org/x/tools/go/packages" 49 ) 50 51 func main() { 52 log.SetFlags(0) 53 if err := run(os.Args[1:]); err != nil { 54 log.Fatal(err) 55 } 56 } 57 58 var ( 59 errUsage = errors.New("usage: cover packages") 60 errNoGoPackage = errors.New("could not find a Go package in the current directory") 61 errNoImportPath = fmt.Errorf("could not determine import path for the Go package in the current directory") 62 ) 63 64 func run(args []string) error { 65 if len(args) == 0 { 66 return errUsage 67 } 68 69 cwd, err := os.Getwd() 70 if err != nil { 71 return fmt.Errorf("could not determine current directory: %v", err) 72 } 73 74 pkgs, err := packages.Load(&packages.Config{ 75 Mode: packages.NeedName, 76 Dir: cwd, 77 }, ".") 78 if err != nil { 79 return err 80 } 81 82 var rootPkg *packages.Package 83 switch len(pkgs) { 84 case 0: 85 return errNoGoPackage 86 case 1: 87 rootPkg = pkgs[0] 88 default: 89 return fmt.Errorf("found %d Go packagess in %q, expected 1", len(pkgs), cwd) 90 } 91 92 rootImportPath := rootPkg.PkgPath 93 if len(rootImportPath) == 0 { 94 return errNoImportPath 95 } 96 97 // All provided packages must be under rootImport. 98 rootPackagePrefix := rootImportPath + "/" 99 for _, importPath := range args { 100 if importPath == rootImportPath { 101 continue 102 } 103 if strings.HasPrefix(importPath, rootPackagePrefix) { 104 continue 105 } 106 return fmt.Errorf("%q is not a subpackage of %q", importPath, rootImportPath) 107 } 108 109 covFile, err := ioutil.TempFile("" /* dir */, "coverage") 110 if err != nil { 111 return fmt.Errorf("failed to create temporary file: %v", err) 112 } 113 covFileName := covFile.Name() 114 defer deleteFile(covFileName) 115 116 if err := covFile.Close(); err != nil { 117 return fmt.Errorf("failed to close %q: %v", covFileName, err) 118 } 119 120 testArgs := []string{ 121 "test", 122 fmt.Sprintf("-coverprofile=%v", covFileName), 123 "-covermode=count", 124 fmt.Sprintf("-coverpkg=%v/...", rootImportPath), 125 } 126 testArgs = append(testArgs, args...) 127 cmd := exec.Command("go", testArgs...) 128 cmd.Stdout = os.Stdout 129 cmd.Stderr = os.Stderr 130 if err := cmd.Run(); err != nil { 131 return fmt.Errorf("go test failed: %v", err) 132 } 133 134 outFileName := filepath.Join(cwd, "coverage.txt") 135 if err := filterIgnoredPackages(cwd, rootImportPath, covFileName, outFileName); err != nil { 136 return fmt.Errorf("could not filter coverage: %v", err) 137 } 138 139 return nil 140 } 141 142 func filterIgnoredPackages(rootDir, rootImportPath, src, dst string) (err error) { 143 r, err := os.Open(src) 144 if err != nil { 145 return fmt.Errorf("could not open %q for reading: %v", src, err) 146 } 147 defer closeFile(src, r) 148 149 w, err := os.OpenFile(dst, os.O_RDWR|os.O_CREATE|os.O_TRUNC, 0644) 150 if err != nil { 151 return fmt.Errorf("could not open %q for writing: %v", dst, err) 152 } 153 defer closeFile(dst, w) 154 155 // Map from import path to whether a package is covered or not. If an 156 // entry doesn't exist in this map, the status for that package isn't 157 // known yet. 158 shouldCover := make(map[string]bool) 159 160 scanner := bufio.NewScanner(r) 161 for scanner.Scan() { 162 line := scanner.Text() 163 164 idx := strings.IndexByte(line, ':') 165 if idx < 0 { 166 if _, err := fmt.Fprintln(w, line); err != nil { 167 return err 168 } 169 } 170 171 file := line[:idx] 172 if strings.Contains(file, "/internal/examples/") || 173 strings.Contains(file, "/internal/tests/") || 174 strings.Contains(file, "/mocks/") || 175 strings.Contains(file, "test/") { 176 continue 177 } 178 179 importPath := filepath.Dir(file) 180 cover, ok := shouldCover[importPath] 181 if !ok { 182 relPath, err := filepath.Rel(rootImportPath, importPath) 183 if err != nil { 184 return fmt.Errorf("could not make %q relative to %q: %v", importPath, rootImportPath, err) 185 } 186 187 _, err = os.Stat(filepath.Join(rootDir, relPath, ".nocover")) 188 189 // cover a package if .nocover doesn't exist 190 cover = os.IsNotExist(err) 191 shouldCover[importPath] = cover 192 } 193 194 if !cover { 195 continue 196 } 197 198 if _, err := fmt.Fprintln(w, line); err != nil { 199 return err 200 } 201 } 202 203 return nil 204 } 205 206 func closeFile(n string, c io.Closer) { 207 if err := c.Close(); err != nil { 208 log.Printf("WARN: Failed to close %q: %v", n, err) 209 } 210 } 211 212 func deleteFile(f string) { 213 if err := os.Remove(f); err != nil { 214 log.Printf("WARN: failed to remove %q: %v", f, err) 215 } 216 }