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  }