diff --git a/README.md b/README.md index 418948d..4f383ec 100644 --- a/README.md +++ b/README.md @@ -20,6 +20,13 @@ error rather than a panic. ### Notest comments Blocks or files with a `// notest` comment are excluded. +### Generated code +Generated code files, that contain the respective comment line that is +specified by the [Go Team](https://github.com/golang/go/issues/41196) in +[`go generate`](https://golang.org/s/generatedcode). + +This exclude is disabled by default. Use flag `-g` to enable this behavior. + ### Blocks returning a error tested to be non-nil We only exclude blocks where the error being returned has been tested to be non-nil, so: @@ -114,6 +121,11 @@ courtney -t="-count=2" -t="-parallel=4" All the output from the `go test -v` command is shown. +### Exclude generated code: -g +`Exclude generated code from coverage` + +All generated code is excluded from coverage. + # Output Courtney will fail if the tests fail. If the tests succeed, it will create or overwrite a `coverage.out` file in the current directory. diff --git a/courtney.go b/courtney.go index 28025a4..bd8d96c 100644 --- a/courtney.go +++ b/courtney.go @@ -27,19 +27,21 @@ func main() { argsFlag := new(argsValue) flag.Var(argsFlag, "t", "Argument to pass to the 'go test' command. Can be used more than once.") loadFlag := flag.String("l", "", "Load coverage file(s) instead of running 'go test'") + excludeGeneratedCodeFlag := flag.Bool("g", false, "Exclude generated code from coverage") flag.Parse() setup := &shared.Setup{ - Env: env, - Paths: patsy.NewCache(env), - Enforce: *enforceFlag, - Verbose: *verboseFlag, - Short: *shortFlag, - Timeout: *timeoutFlag, - Output: *outputFlag, - TestArgs: argsFlag.args, - Load: *loadFlag, + Env: env, + Paths: patsy.NewCache(env), + Enforce: *enforceFlag, + Verbose: *verboseFlag, + Short: *shortFlag, + Timeout: *timeoutFlag, + Output: *outputFlag, + TestArgs: argsFlag.args, + Load: *loadFlag, + ExcludeGeneratedCode: *excludeGeneratedCodeFlag, } if err := Run(setup); err != nil { fmt.Printf("%+v", err) diff --git a/scanner/generated.go b/scanner/generated.go new file mode 100644 index 0000000..1884852 --- /dev/null +++ b/scanner/generated.go @@ -0,0 +1,36 @@ +// Copyright (c) 2013 The Go Authors. All rights reserved. +// +// Use of this source code is governed by a BSD-style +// license that can be found in the LICENSE file or at +// https://developers.google.com/open-source/licenses/bsd. + +// Taken from https://github.com/golang/lint/blob/85993ffd0a6cd043291f3f63d45d656d97b165bd/lint.go + +// Once https://github.com/golang/go/issues/28089 is implemented (proposal is accepted), +// this can be replaced with the official implementation. + +package scanner + +import ( + "bufio" + "bytes" +) + +var ( + genHdr = []byte("// Code generated ") + genFtr = []byte(" DO NOT EDIT.") +) + +// isGenerated reports whether the source file is generated code +// according the rules from https://golang.org/s/generatedcode. +// +func isGenerated(src []byte) bool { + sc := bufio.NewScanner(bytes.NewReader(src)) + for sc.Scan() { + b := sc.Bytes() + if bytes.HasPrefix(b, genHdr) && bytes.HasSuffix(b, genFtr) && len(b) >= len(genHdr)+len(genFtr) { + return true + } + } + return false +} diff --git a/scanner/generated_test.go b/scanner/generated_test.go new file mode 100644 index 0000000..81bf8e3 --- /dev/null +++ b/scanner/generated_test.go @@ -0,0 +1,38 @@ +// Copyright (c) 2013 The Go Authors. All rights reserved. +// +// Use of this source code is governed by a BSD-style +// license that can be found in the LICENSE file or at +// https://developers.google.com/open-source/licenses/bsd. + +// Taken from https://github.com/golang/lint/blob/85993ffd0a6cd043291f3f63d45d656d97b165bd/lint_test.go + +package scanner + +import "testing" + +func TestIsGenerated(t *testing.T) { + tests := []struct { + source string + generated bool + }{ + {"// Code Generated by some tool. DO NOT EDIT.", false}, + {"// Code generated by some tool. DO NOT EDIT.", true}, + {"// Code generated by some tool. DO NOT EDIT", false}, + {"// Code generated DO NOT EDIT.", true}, + {"// Code generated DO NOT EDIT.", false}, + {"\t\t// Code generated by some tool. DO NOT EDIT.\npackage foo\n", false}, + {"// Code generated by some tool. DO NOT EDIT.\npackage foo\n", true}, + {"package foo\n// Code generated by some tool. DO NOT EDIT.\ntype foo int\n", true}, + {"package foo\n // Code generated by some tool. DO NOT EDIT.\ntype foo int\n", false}, + {"package foo\n// Code generated by some tool. DO NOT EDIT. \ntype foo int\n", false}, + {"package foo\ntype foo int\n// Code generated by some tool. DO NOT EDIT.\n", true}, + {"package foo\ntype foo int\n// Code generated by some tool. DO NOT EDIT.", true}, + } + + for i, test := range tests { + got := isGenerated([]byte(test.source)) + if got != test.generated { + t.Errorf("test %d, isGenerated() = %v, want %v", i, got, test.generated) + } + } +} diff --git a/scanner/scanner.go b/scanner/scanner.go index ee01f74..1c186c1 100644 --- a/scanner/scanner.go +++ b/scanner/scanner.go @@ -1,11 +1,13 @@ package scanner import ( + "bytes" "fmt" "go/ast" "go/constant" "go/token" "go/types" + "os" "github.com/dave/astrid" "github.com/dave/brenda" @@ -129,6 +131,22 @@ func (p *PackageMap) ScanPackage() error { return errors.WithStack(err) } } + + if p.setup.ExcludeGeneratedCode { + // Exclude complete files, if the code is generated. + for _, f := range p.pkg.GoFiles { + body, err := os.ReadFile(f) + if err != nil { + return errors.WithStack(err) + } + if !isGenerated(body) { + continue + } + for i := range bytes.Split(body, []byte("\n")) { + p.addExclude(f, i+1) + } + } + } return nil } diff --git a/scanner/scanner_test.go b/scanner/scanner_test.go index b5a612f..5e9bf38 100644 --- a/scanner/scanner_test.go +++ b/scanner/scanner_test.go @@ -686,6 +686,17 @@ func TestComments(t *testing.T) { test(t, tests) } +func TestGenerated(t *testing.T) { + tests := map[string]string{ + "generated": `// Code generated by courtney. DO NOT EDIT. + package foo // * + func Baz() int { // * + return 0 // * + } // *`, + } + test(t, tests) +} + func test(t *testing.T, tests map[string]string) { for name, source := range tests { env := vos.Mock() @@ -704,8 +715,9 @@ func test(t *testing.T, tests map[string]string) { paths := patsy.NewCache(env) setup := &shared.Setup{ - Env: env, - Paths: paths, + Env: env, + Paths: paths, + ExcludeGeneratedCode: true, } if err := setup.Parse([]string{ppath}); err != nil { t.Fatalf("Error parsing args in %s: %+v", name, err) @@ -726,7 +738,8 @@ func test(t *testing.T, tests map[string]string) { for i, line := range strings.Split(source, "\n") { expected := strings.HasSuffix(line, "// *") || strings.HasSuffix(line, "//notest") || - strings.HasSuffix(line, "// notest") + strings.HasSuffix(line, "// notest") || + strings.HasSuffix(line, "// Code generated by courtney. DO NOT EDIT.") if result[i+1] != expected { t.Fatalf("Unexpected state in %s, line %d: %s\n", name, i, strconv.Quote(strings.Trim(line, "\t"))) } diff --git a/shared/shared.go b/shared/shared.go index 82be997..678ea2f 100644 --- a/shared/shared.go +++ b/shared/shared.go @@ -10,16 +10,17 @@ import ( // Setup holds globals, environment and command line flags for the courtney // command type Setup struct { - Env vos.Env - Paths *patsy.Cache - Enforce bool - Verbose bool - Short bool - Timeout string - Load string - Output string - TestArgs []string - Packages []PackageSpec + Env vos.Env + Paths *patsy.Cache + Enforce bool + Verbose bool + Short bool + Timeout string + Load string + Output string + TestArgs []string + ExcludeGeneratedCode bool + Packages []PackageSpec } // PackageSpec identifies a package by dir and path