├── .gitignore ├── .travis.yml ├── LICENSE ├── README.md ├── courtney.go ├── courtney_test.go ├── go.mod ├── go.sum ├── scanner ├── scanner.go └── scanner_test.go ├── shared ├── shared.go └── shared_test.go └── tester ├── logger ├── logger.go └── logger_test.go ├── merge ├── LICENCE ├── README.md └── merge.go ├── tester.go └── tester_test.go /.gitignore: -------------------------------------------------------------------------------- 1 | *.iml 2 | 3 | # Compiled Object files, Static and Dynamic libs (Shared Objects) 4 | *.o 5 | *.a 6 | *.so 7 | 8 | # Folders 9 | _obj 10 | _test 11 | vendor/ 12 | 13 | # Architecture specific extensions/prefixes 14 | *.[568vq] 15 | [568vq].out 16 | 17 | *.cgo1.go 18 | *.cgo2.c 19 | _cgo_defun.c 20 | _cgo_gotypes.go 21 | _cgo_export.* 22 | 23 | _testmain.go 24 | 25 | *.exe 26 | *.test 27 | *.prof 28 | 29 | .DS_Store 30 | .idea/ 31 | coverage.out 32 | -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | language: go 2 | go: 3 | - 1.x 4 | - 1.16 5 | - 1.15 6 | notifications: 7 | email: 8 | recipients: dave@brophy.uk 9 | on_failure: always 10 | install: 11 | - go get -u github.com/mattn/goveralls # only for coveralls.io 12 | - go get -u github.com/dave/courtney 13 | - go get -t -v ./... 14 | script: 15 | - courtney -e -v 16 | after_success: 17 | - bash <(curl -s https://codecov.io/bash) # only for codecov.io 18 | - goveralls -coverprofile=coverage.out -service=travis-ci # only for coveralls.io 19 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2017 David Brophy 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | [![Build Status](https://travis-ci.org/dave/courtney.svg?branch=master)](https://travis-ci.org/dave/courtney) [![Go Report Card](https://goreportcard.com/badge/github.com/dave/courtney)](https://goreportcard.com/report/github.com/dave/courtney) [![codecov](https://codecov.io/gh/dave/courtney/branch/master/graph/badge.svg)](https://codecov.io/gh/dave/courtney) 2 | 3 | # Courtney 4 | 5 | Courtney makes your code coverage more meaningful, by excluding some of the 6 | less important parts. 7 | 8 | 1. Packages are tested with coverage. 9 | 2. Coverage files are merged. 10 | 3. Some code is less important to test. This is excluded from the coverage file. 11 | 4. Optionally we enforce that all remaining code is covered. 12 | 13 | # Excludes 14 | What do we exclude from the coverage report? 15 | 16 | ### Blocks including a panic 17 | If you need to test that your code panics correctly, it should probably be an 18 | error rather than a panic. 19 | 20 | ### Notest comments 21 | Blocks or files with a `// notest` comment are excluded. 22 | 23 | ### Blocks returning a error tested to be non-nil 24 | We only exclude blocks where the error being returned has been tested to be 25 | non-nil, so: 26 | 27 | ```go 28 | err := foo() 29 | if err != nil { 30 | return err // excluded 31 | } 32 | ``` 33 | 34 | ... however: 35 | 36 | ```go 37 | if i == 0 { 38 | return errors.New("...") // not excluded 39 | } 40 | ``` 41 | 42 | All errors are originally created with code similar to `errors.New`, which is 43 | not excluded from the coverage report - it's important your tests hit these. 44 | 45 | It's less important your tests cover all the points that an existing non-nil 46 | error is passed back, so these are excluded. 47 | 48 | A few more rules: 49 | * If multiple return values are returned, error must be the last, and all 50 | others must be nil or zero values. 51 | * We also exclude blocks returning an error which is the result of a function 52 | taking a non-nil error as a parameter, e.g. `errors.Wrap(err, "...")`. 53 | * We also exclude blocks containing a bare return statement, where the function 54 | has named result parameters, and the last result is an error that has been 55 | tested non-nil. Be aware that in this scenario no attempt is made to verify 56 | that the other result parameters are zero values. 57 | 58 | # Limitations 59 | * Having test coverage doesn't mean your code is well tested. 60 | * It's up to you to make sure that your tests explore the appropriate edge 61 | cases. 62 | 63 | # Install 64 | ``` 65 | go get -u github.com/dave/courtney 66 | ``` 67 | 68 | # Usage 69 | Run the courtney command followed by a list of packages. Use `.` for the 70 | package in the current directory, and adding `/...` tests all sub-packages 71 | recursively. If no packages are provided, the default is `./...`. 72 | 73 | To test the current package, and all sub-packages recursively: 74 | ``` 75 | courtney 76 | ``` 77 | 78 | To test just the current package: 79 | ``` 80 | courtney . 81 | ``` 82 | 83 | To test the `a` package, it's sub-packages and the `b` package: 84 | ``` 85 | courtney github.com/dave/a/... github.com/dave/b 86 | ``` 87 | 88 | # Options 89 | ### Enforce: -e 90 | `Enforce 100% code coverage.` 91 | 92 | The command will exit with an error if any code remains uncovered. Combining a 93 | CI system with a fully tested package and the `-e` flag is extremely useful. It 94 | ensures any pull request has tests that cover all new code. For example, [here 95 | is a PR](https://github.com/dave/courtney/pull/5) for this project that lacks 96 | tests. As you can see the Travis build failed with a descriptive error. 97 | 98 | ### Output: -o 99 | `Override coverage file location.` 100 | 101 | Provide a custom location for the coverage file. The default is `./coverage.out`. 102 | 103 | ### Test flags: -t 104 | `Argument to pass to the 'go test' command.` 105 | 106 | If you have special arguments to pass to the `go test` command, add them here. 107 | Add one `-t` flag per argument e.g. 108 | ``` 109 | courtney -t="-count=2" -t="-parallel=4" 110 | ``` 111 | 112 | ### Verbose: -v 113 | `Verbose output` 114 | 115 | All the output from the `go test -v` command is shown. 116 | 117 | # Output 118 | Courtney will fail if the tests fail. If the tests succeed, it will create or 119 | overwrite a `coverage.out` file in the current directory. 120 | 121 | # Continuous integration 122 | To upload your coverage to [codecov.io](https://codecov.io/) via 123 | [travis](https://travis-ci.org/), use a `.travis.yml` file something like this: 124 | 125 | ```yml 126 | language: go 127 | go: 128 | - 1.x 129 | notifications: 130 | email: 131 | recipients: 132 | on_failure: always 133 | install: 134 | - go get -u github.com/dave/courtney 135 | - go get -t -v ./... 136 | script: 137 | - courtney 138 | after_success: 139 | - bash <(curl -s https://codecov.io/bash) 140 | ``` 141 | 142 | For [coveralls.io](https://coveralls.io/), use something like this: 143 | 144 | ```yml 145 | language: go 146 | go: 147 | - 1.x 148 | notifications: 149 | email: 150 | recipients: 151 | on_failure: always 152 | install: 153 | - go get -u github.com/mattn/goveralls 154 | - go get -u github.com/dave/courtney 155 | - go get -t -v ./... 156 | script: 157 | - courtney 158 | after_success: 159 | - goveralls -coverprofile=coverage.out -service=travis-ci 160 | ``` -------------------------------------------------------------------------------- /courtney.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "flag" 5 | "fmt" 6 | "os" 7 | "strings" 8 | 9 | "github.com/pkg/errors" 10 | 11 | "github.com/dave/courtney/scanner" 12 | "github.com/dave/courtney/shared" 13 | "github.com/dave/courtney/tester" 14 | "github.com/dave/patsy" 15 | "github.com/dave/patsy/vos" 16 | ) 17 | 18 | func main() { 19 | // notest 20 | env := vos.Os() 21 | 22 | enforceFlag := flag.Bool("e", false, "Enforce 100% code coverage") 23 | verboseFlag := flag.Bool("v", false, "Verbose output") 24 | shortFlag := flag.Bool("short", false, "Pass the short flag to the go test command") 25 | timeoutFlag := flag.String("timeout", "", "Pass the timeout flag to the go test command") 26 | outputFlag := flag.String("o", "", "Override coverage file location") 27 | argsFlag := new(argsValue) 28 | flag.Var(argsFlag, "t", "Argument to pass to the 'go test' command. Can be used more than once.") 29 | loadFlag := flag.String("l", "", "Load coverage file(s) instead of running 'go test'") 30 | 31 | flag.Parse() 32 | 33 | setup := &shared.Setup{ 34 | Env: env, 35 | Paths: patsy.NewCache(env), 36 | Enforce: *enforceFlag, 37 | Verbose: *verboseFlag, 38 | Short: *shortFlag, 39 | Timeout: *timeoutFlag, 40 | Output: *outputFlag, 41 | TestArgs: argsFlag.args, 42 | Load: *loadFlag, 43 | } 44 | if err := Run(setup); err != nil { 45 | fmt.Printf("%+v", err) 46 | os.Exit(1) 47 | } 48 | } 49 | 50 | // Run initiates the command with the provided setup 51 | func Run(setup *shared.Setup) error { 52 | if err := setup.Parse(flag.Args()); err != nil { 53 | return errors.Wrapf(err, "Parse") 54 | } 55 | 56 | s := scanner.New(setup) 57 | if err := s.LoadProgram(); err != nil { 58 | return errors.Wrapf(err, "LoadProgram") 59 | } 60 | if err := s.ScanPackages(); err != nil { 61 | return errors.Wrapf(err, "ScanPackages") 62 | } 63 | 64 | t := tester.New(setup) 65 | if setup.Load == "" { 66 | if err := t.Test(); err != nil { 67 | return errors.Wrapf(err, "Test") 68 | } 69 | } else { 70 | if err := t.Load(); err != nil { 71 | return errors.Wrapf(err, "Load") 72 | } 73 | } 74 | if err := t.ProcessExcludes(s.Excludes); err != nil { 75 | return errors.Wrapf(err, "ProcessExcludes") 76 | } 77 | if err := t.Save(); err != nil { 78 | return errors.Wrapf(err, "Save") 79 | } 80 | if err := t.Enforce(); err != nil { 81 | return errors.Wrapf(err, "Enforce") 82 | } 83 | 84 | return nil 85 | } 86 | 87 | type argsValue struct { 88 | args []string 89 | } 90 | 91 | var _ flag.Value = (*argsValue)(nil) 92 | 93 | func (v *argsValue) String() string { 94 | // notest 95 | if v == nil { 96 | return "" 97 | } 98 | return strings.Join(v.args, " ") 99 | } 100 | func (v *argsValue) Set(s string) error { 101 | // notest 102 | v.args = append(v.args, s) 103 | return nil 104 | } 105 | -------------------------------------------------------------------------------- /courtney_test.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "fmt" 5 | "testing" 6 | 7 | "io/ioutil" 8 | "path/filepath" 9 | 10 | "bytes" 11 | "strings" 12 | 13 | "os" 14 | 15 | "github.com/dave/courtney/shared" 16 | "github.com/dave/patsy" 17 | "github.com/dave/patsy/builder" 18 | "github.com/dave/patsy/vos" 19 | ) 20 | 21 | func TestRun(t *testing.T) { 22 | for _, gomod := range []bool{true, false} { 23 | t.Run(fmt.Sprintf("gomod=%v", gomod), func(t *testing.T) { 24 | name := "run" 25 | env := vos.Mock() 26 | b, err := builder.New(env, "ns", gomod) 27 | if err != nil { 28 | t.Fatalf("Error creating builder in %s: %s", name, err) 29 | } 30 | defer b.Cleanup() 31 | 32 | _, pdir, err := b.Package("a", map[string]string{ 33 | "a.go": `package a 34 | 35 | func Foo(i int) int { 36 | i++ 37 | return i 38 | } 39 | 40 | func Bar(i int) int { 41 | i++ 42 | return i 43 | } 44 | `, 45 | "a_test.go": `package a 46 | 47 | import "testing" 48 | 49 | func TestFoo(t *testing.T) { 50 | i := Foo(1) 51 | if i != 2 { 52 | t.Fail() 53 | } 54 | } 55 | `, 56 | }) 57 | if err != nil { 58 | t.Fatalf("Error creating builder in %s: %s", name, err) 59 | } 60 | 61 | if err := env.Setwd(pdir); err != nil { 62 | t.Fatalf("Error in Setwd in %s: %s", name, err) 63 | } 64 | 65 | sout := &bytes.Buffer{} 66 | serr := &bytes.Buffer{} 67 | env.Setstdout(sout) 68 | env.Setstderr(serr) 69 | 70 | setup := &shared.Setup{ 71 | Env: env, 72 | Paths: patsy.NewCache(env), 73 | Enforce: true, 74 | Verbose: true, 75 | } 76 | err = Run(setup) 77 | if err == nil { 78 | t.Fatalf("Error in %s. Run should error.", name) 79 | } 80 | expected := `Error - untested code: 81 | ns/a/a.go:8-11: 82 | func Bar(i int) int { 83 | i++ 84 | return i 85 | }` 86 | if !strings.Contains(err.Error(), expected) { 87 | t.Fatalf("Error in %s err. Got: \n%s\nExpected to contain: \n%s\n", name, err.Error(), expected) 88 | } 89 | 90 | coverage, err := ioutil.ReadFile(filepath.Join(pdir, "coverage.out")) 91 | if err != nil { 92 | t.Fatalf("Error reading coverage file in %s: %s", name, err) 93 | } 94 | expected = `mode: set 95 | ns/a/a.go:3.24,6.5 2 1 96 | ns/a/a.go:8.24,11.5 2 0 97 | ` 98 | if string(coverage) != expected { 99 | t.Fatalf("Error in %s coverage. Got: \n%s\nExpected: \n%s\n", name, string(coverage), expected) 100 | } 101 | 102 | setup = &shared.Setup{ 103 | Env: env, 104 | Paths: patsy.NewCache(env), 105 | } 106 | if err := Run(setup); err != nil { 107 | t.Fatalf("Error running program (second try) in %s: %s", name, err) 108 | } 109 | }) 110 | } 111 | } 112 | 113 | func TestRun_load(t *testing.T) { 114 | for _, gomod := range []bool{true, false} { 115 | t.Run(fmt.Sprintf("gomod=%v", gomod), func(t *testing.T) { 116 | name := "load" 117 | env := vos.Mock() 118 | b, err := builder.New(env, "ns", gomod) 119 | if err != nil { 120 | t.Fatalf("Error creating builder in %s: %s", name, err) 121 | } 122 | defer b.Cleanup() 123 | 124 | _, pdir, err := b.Package("a", map[string]string{ 125 | "a.go": `package a 126 | 127 | func Foo(i int) int { 128 | i++ 129 | return i 130 | } 131 | 132 | func Bar(i int) int { 133 | i++ 134 | return i 135 | } 136 | `, 137 | "a_test.go": `package a 138 | 139 | import "testing" 140 | 141 | func TestFoo(t *testing.T) { 142 | // In "load" mode, this test will not run. 143 | t.Fail() 144 | } 145 | `, 146 | "a.out": `mode: set 147 | ns/a/a.go:3.24,6.5 2 1 148 | ns/a/a.go:8.24,11.5 2 0 149 | `, 150 | "b.out": `mode: set 151 | ns/a/a.go:3.24,6.5 2 0 152 | ns/a/a.go:8.24,11.5 2 1 153 | `, 154 | }) 155 | if err != nil { 156 | t.Fatalf("Error creating builder in %s: %s", name, err) 157 | } 158 | 159 | if err := env.Setwd(pdir); err != nil { 160 | t.Fatalf("Error in Setwd in %s: %s", name, err) 161 | } 162 | 163 | // annoyingly, filepath.Glob in "Load" method does not respect the mocked 164 | // vos working directory 165 | if err := os.Chdir(pdir); err != nil { 166 | t.Fatalf("Error in os.Chdir in %s: %s", name, err) 167 | } 168 | 169 | sout := &bytes.Buffer{} 170 | serr := &bytes.Buffer{} 171 | env.Setstdout(sout) 172 | env.Setstderr(serr) 173 | 174 | setup := &shared.Setup{ 175 | Env: env, 176 | Paths: patsy.NewCache(env), 177 | Load: "*.out", 178 | } 179 | if err := Run(setup); err != nil { 180 | t.Fatalf("Error running program in %s: %s", name, err) 181 | } 182 | 183 | coverage, err := ioutil.ReadFile(filepath.Join(pdir, "coverage.out")) 184 | if err != nil { 185 | t.Fatalf("Error reading coverage file in %s: %s", name, err) 186 | } 187 | expected := `mode: set 188 | ns/a/a.go:3.24,6.5 2 1 189 | ns/a/a.go:8.24,11.5 2 1 190 | ` 191 | if string(coverage) != expected { 192 | t.Fatalf("Error in %s coverage. Got: \n%s\nExpected: \n%s\n", name, string(coverage), expected) 193 | } 194 | }) 195 | } 196 | } 197 | -------------------------------------------------------------------------------- /go.mod: -------------------------------------------------------------------------------- 1 | module github.com/dave/courtney 2 | 3 | go 1.12 4 | 5 | require ( 6 | github.com/dave/astrid v0.0.0-20170323122508-8c2895878b14 7 | github.com/dave/brenda v1.1.0 8 | github.com/dave/patsy v0.0.0-20210517141501-957256f50cba 9 | github.com/pkg/errors v0.9.1 10 | golang.org/x/tools v0.30.0 11 | ) 12 | -------------------------------------------------------------------------------- /go.sum: -------------------------------------------------------------------------------- 1 | github.com/dave/astrid v0.0.0-20170323122508-8c2895878b14 h1:YI1gOOdmMk3xodBao7fehcvoZsEeOyy/cfhlpCSPgM4= 2 | github.com/dave/astrid v0.0.0-20170323122508-8c2895878b14/go.mod h1:Sth2QfxfATb/nW4EsrSi2KyJmbcniZ8TgTaji17D6ms= 3 | github.com/dave/brenda v1.1.0 h1:Sl1LlwXnbw7xMhq3y2x11McFu43AjDcwkllxxgZ3EZw= 4 | github.com/dave/brenda v1.1.0/go.mod h1:4wCUr6gSlu5/1Tk7akE5X7UorwiQ8Rij0SKH3/BGMOM= 5 | github.com/dave/patsy v0.0.0-20210517141501-957256f50cba h1:1o36L4EKbZzazMk8iGC4kXpVnZ6TPxR2mZ9qVKjNNAs= 6 | github.com/dave/patsy v0.0.0-20210517141501-957256f50cba/go.mod h1:qfR88CgEGLoiqDaE+xxDCi5QA5v4vUoW0UCX2Nd5Tlc= 7 | github.com/google/go-cmp v0.6.0 h1:ofyhxvXcZhMsU5ulbFiLKl/XBFqE1GSq7atu8tAmTRI= 8 | github.com/google/go-cmp v0.6.0/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY= 9 | github.com/pkg/errors v0.9.1 h1:FEBLx1zS214owpjy7qsBeixbURkuhQAwrK5UwLGTwt4= 10 | github.com/pkg/errors v0.9.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0= 11 | github.com/yuin/goldmark v1.4.13/go.mod h1:6yULJ656Px+3vBD8DxQVa3kxgyrAnzto9xy5taEt/CY= 12 | golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w= 13 | golang.org/x/crypto v0.0.0-20210921155107-089bfa567519/go.mod h1:GvvjBRRGRdwPK5ydBHafDWAxML/pGHZbMvKqRZ5+Abc= 14 | golang.org/x/crypto v0.13.0/go.mod h1:y6Z2r+Rw4iayiXXAIxJIDAJ1zMW4yaTpebo8fPOliYc= 15 | golang.org/x/crypto v0.19.0/go.mod h1:Iy9bg/ha4yyC70EfRS8jz+B6ybOBKMaSxLj6P6oBDfU= 16 | golang.org/x/crypto v0.23.0/go.mod h1:CKFgDieR+mRhux2Lsu27y0fO304Db0wZe70UKqHu0v8= 17 | golang.org/x/crypto v0.33.0/go.mod h1:bVdXmD7IV/4GdElGPozy6U7lWdRXA4qyRVGJV57uQ5M= 18 | golang.org/x/mod v0.6.0-dev.0.20220419223038-86c51ed26bb4/go.mod h1:jJ57K6gSWd91VN4djpZkiMVwK6gcyfeH4XE8wZrZaV4= 19 | golang.org/x/mod v0.8.0/go.mod h1:iBbtSCu2XBx23ZKBPSOrRkjjQPZFPuis4dIYUhu/chs= 20 | golang.org/x/mod v0.12.0/go.mod h1:iBbtSCu2XBx23ZKBPSOrRkjjQPZFPuis4dIYUhu/chs= 21 | golang.org/x/mod v0.15.0/go.mod h1:hTbmBsO62+eylJbnUtE2MGJUyE7QWk4xUqPFrRgJ+7c= 22 | golang.org/x/mod v0.17.0/go.mod h1:hTbmBsO62+eylJbnUtE2MGJUyE7QWk4xUqPFrRgJ+7c= 23 | golang.org/x/mod v0.23.0 h1:Zb7khfcRGKk+kqfxFaP5tZqCnDZMjC5VtUBs87Hr6QM= 24 | golang.org/x/mod v0.23.0/go.mod h1:6SkKJ3Xj0I0BrPOZoBy3bdMptDDU9oJrpohJ3eWZ1fY= 25 | golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= 26 | golang.org/x/net v0.0.0-20210226172049-e18ecbb05110/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg= 27 | golang.org/x/net v0.0.0-20220722155237-a158d28d115b/go.mod h1:XRhObCWvk6IyKnWLug+ECip1KBveYUHfp+8e9klMJ9c= 28 | golang.org/x/net v0.6.0/go.mod h1:2Tu9+aMcznHK/AK1HMvgo6xiTLG5rD5rZLDS+rp2Bjs= 29 | golang.org/x/net v0.10.0/go.mod h1:0qNGK6F8kojg2nk9dLZ2mShWaEBan6FAoqfSigmmuDg= 30 | golang.org/x/net v0.15.0/go.mod h1:idbUs1IY1+zTqbi8yxTbhexhEEk5ur9LInksu6HrEpk= 31 | golang.org/x/net v0.21.0/go.mod h1:bIjVDfnllIU7BJ2DNgfnXvpSvtn8VRwhlsaeUTyUS44= 32 | golang.org/x/net v0.25.0/go.mod h1:JkAGAh7GEvH74S6FOH42FLoXpXbE/aqXSrIQjXgsiwM= 33 | golang.org/x/net v0.35.0/go.mod h1:EglIi67kWsHKlRzzVMUD93VMSWGFOMSZgxFjparz1Qk= 34 | golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= 35 | golang.org/x/sync v0.0.0-20220722155255-886fb9371eb4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= 36 | golang.org/x/sync v0.1.0/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= 37 | golang.org/x/sync v0.3.0/go.mod h1:FU7BRWz2tNW+3quACPkgCx/L+uEAv1htQ0V83Z9Rj+Y= 38 | golang.org/x/sync v0.6.0/go.mod h1:Czt+wKu1gCyEFDUtn0jG5QVvpJ6rzVqr5aXyt9drQfk= 39 | golang.org/x/sync v0.7.0/go.mod h1:Czt+wKu1gCyEFDUtn0jG5QVvpJ6rzVqr5aXyt9drQfk= 40 | golang.org/x/sync v0.11.0 h1:GGz8+XQP4FvTTrjZPzNKTMFtSXH80RAzG+5ghFPgK9w= 41 | golang.org/x/sync v0.11.0/go.mod h1:Czt+wKu1gCyEFDUtn0jG5QVvpJ6rzVqr5aXyt9drQfk= 42 | golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= 43 | golang.org/x/sys v0.0.0-20201119102817-f84b799fce68/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= 44 | golang.org/x/sys v0.0.0-20210615035016-665e8c7367d1/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= 45 | golang.org/x/sys v0.0.0-20220520151302-bc2c85ada10a/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= 46 | golang.org/x/sys v0.0.0-20220722155257-8c9f86f7a55f/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= 47 | golang.org/x/sys v0.5.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= 48 | golang.org/x/sys v0.8.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= 49 | golang.org/x/sys v0.12.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= 50 | golang.org/x/sys v0.17.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= 51 | golang.org/x/sys v0.20.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= 52 | golang.org/x/sys v0.30.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= 53 | golang.org/x/telemetry v0.0.0-20240228155512-f48c80bd79b2/go.mod h1:TeRTkGYfJXctD9OcfyVLyj2J3IxLnKwHJR8f4D8a3YE= 54 | golang.org/x/telemetry v0.0.0-20240521205824-bda55230c457/go.mod h1:pRgIJT+bRLFKnoM1ldnzKoxTIn14Yxz928LQRYYgIN0= 55 | golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo= 56 | golang.org/x/term v0.0.0-20210927222741-03fcf44c2211/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8= 57 | golang.org/x/term v0.5.0/go.mod h1:jMB1sMXY+tzblOD4FWmEbocvup2/aLOaQEp7JmGp78k= 58 | golang.org/x/term v0.8.0/go.mod h1:xPskH00ivmX89bAKVGSKKtLOWNx2+17Eiy94tnKShWo= 59 | golang.org/x/term v0.12.0/go.mod h1:owVbMEjm3cBLCHdkQu9b1opXd4ETQWc3BhuQGKgXgvU= 60 | golang.org/x/term v0.17.0/go.mod h1:lLRBjIVuehSbZlaOtGMbcMncT+aqLLLmKrsjNrUguwk= 61 | golang.org/x/term v0.20.0/go.mod h1:8UkIAJTvZgivsXaD6/pH6U9ecQzZ45awqEOzuCvwpFY= 62 | golang.org/x/term v0.29.0/go.mod h1:6bl4lRlvVuDgSf3179VpIxBF0o10JUpXWOnI7nErv7s= 63 | golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= 64 | golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= 65 | golang.org/x/text v0.3.7/go.mod h1:u+2+/6zg+i71rQMx5EYifcz6MCKuco9NR6JIITiCfzQ= 66 | golang.org/x/text v0.7.0/go.mod h1:mrYo+phRRbMaCq/xk9113O4dZlRixOauAjOtrjsXDZ8= 67 | golang.org/x/text v0.9.0/go.mod h1:e1OnstbJyHTd6l/uOt8jFFHp6TRDWZR/bV3emEE/zU8= 68 | golang.org/x/text v0.13.0/go.mod h1:TvPlkZtksWOMsz7fbANvkp4WM8x/WCo/om8BMLbz+aE= 69 | golang.org/x/text v0.14.0/go.mod h1:18ZOQIKpY8NJVqYksKHtTdi31H5itFRjB5/qKTNYzSU= 70 | golang.org/x/text v0.15.0/go.mod h1:18ZOQIKpY8NJVqYksKHtTdi31H5itFRjB5/qKTNYzSU= 71 | golang.org/x/text v0.22.0/go.mod h1:YRoo4H8PVmsu+E3Ou7cqLVH8oXWIHVoX0jqUWALQhfY= 72 | golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= 73 | golang.org/x/tools v0.0.0-20191119224855-298f0cb1881e/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= 74 | golang.org/x/tools v0.1.12/go.mod h1:hNGJHUnrk76NpqgfD5Aqm5Crs+Hm0VOH/i9J2+nxYbc= 75 | golang.org/x/tools v0.6.0/go.mod h1:Xwgl3UAJ/d3gWutnCtw505GrjyAbvKui8lOU390QaIU= 76 | golang.org/x/tools v0.13.0/go.mod h1:HvlwmtVNQAhOuCjW7xxvovg8wbNq7LwfXh/k7wXUl58= 77 | golang.org/x/tools v0.21.1-0.20240508182429-e35e4ccd0d2d/go.mod h1:aiJjzUbINMkxbQROHiO6hDPo2LHcIPhhQsa9DLh0yGk= 78 | golang.org/x/tools v0.30.0 h1:BgcpHewrV5AUp2G9MebG4XPFI1E2W41zU1SaqVA9vJY= 79 | golang.org/x/tools v0.30.0/go.mod h1:c347cR/OJfw5TI+GfX7RUPNMdDRRbjvYTS0jPyvsVtY= 80 | golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= 81 | -------------------------------------------------------------------------------- /scanner/scanner.go: -------------------------------------------------------------------------------- 1 | package scanner 2 | 3 | import ( 4 | "fmt" 5 | "go/ast" 6 | "go/constant" 7 | "go/token" 8 | "go/types" 9 | "strings" 10 | 11 | "github.com/dave/astrid" 12 | "github.com/dave/brenda" 13 | "github.com/dave/courtney/shared" 14 | "github.com/pkg/errors" 15 | "golang.org/x/tools/go/packages" 16 | ) 17 | 18 | // CodeMap scans a number of packages for code to exclude 19 | type CodeMap struct { 20 | setup *shared.Setup 21 | pkgs []*packages.Package 22 | Excludes map[string]map[int]bool 23 | } 24 | 25 | // PackageMap scans a single package for code to exclude 26 | type PackageMap struct { 27 | *CodeMap 28 | pkg *packages.Package 29 | fset *token.FileSet 30 | } 31 | 32 | // FileMap scans a single file for code to exclude 33 | type FileMap struct { 34 | *PackageMap 35 | file *ast.File 36 | matcher *astrid.Matcher 37 | } 38 | 39 | type packageId struct { 40 | path string 41 | name string 42 | } 43 | 44 | // New returns a CoseMap with the provided setup 45 | func New(setup *shared.Setup) *CodeMap { 46 | return &CodeMap{ 47 | setup: setup, 48 | Excludes: make(map[string]map[int]bool), 49 | } 50 | } 51 | 52 | func (c *CodeMap) addExclude(fpath string, line int) { 53 | if c.Excludes[fpath] == nil { 54 | c.Excludes[fpath] = make(map[int]bool) 55 | } 56 | c.Excludes[fpath][line] = true 57 | } 58 | 59 | // LoadProgram uses the loader package to load and process the source for a 60 | // number or packages. 61 | func (c *CodeMap) LoadProgram() error { 62 | var patterns []string 63 | for _, p := range c.setup.Packages { 64 | patterns = append(patterns, p.Path) 65 | } 66 | wd, err := c.setup.Env.Getwd() 67 | if err != nil { 68 | return errors.WithStack(err) 69 | } 70 | 71 | cfg := &packages.Config{ 72 | Dir: wd, 73 | Mode: packages.NeedName | packages.NeedFiles | packages.NeedCompiledGoFiles | 74 | packages.NeedImports | packages.NeedTypes | packages.NeedTypesSizes | 75 | packages.NeedSyntax | packages.NeedTypesInfo, 76 | Env: c.setup.Env.Environ(), 77 | } 78 | 79 | // add a recover to catch a panic and add some context to the error 80 | defer func() { 81 | if panicErr := recover(); panicErr != nil { 82 | panic(fmt.Sprintf("%+v", errors.Errorf("Panic in packages.Load: %s", panicErr))) 83 | } 84 | }() 85 | 86 | pkgs, err := packages.Load(cfg, patterns...) 87 | /* 88 | ctxt := build.Default 89 | ctxt.GOPATH = c.setup.Env.Getenv("GOPATH") 90 | 91 | conf := loader.Config{Build: &ctxt, Cwd: wd, ParserMode: parser.ParseComments} 92 | 93 | for _, p := range c.setup.Packages { 94 | conf.Import(p.Path) 95 | } 96 | prog, err := conf.Load() 97 | */ 98 | if err != nil { 99 | return errors.Wrap(err, "Error loading config") 100 | } 101 | c.pkgs = pkgs 102 | return nil 103 | } 104 | 105 | // ScanPackages scans the imported packages 106 | func (c *CodeMap) ScanPackages() error { 107 | for _, p := range c.pkgs { 108 | pm := &PackageMap{ 109 | CodeMap: c, 110 | pkg: p, 111 | fset: p.Fset, 112 | } 113 | if err := pm.ScanPackage(); err != nil { 114 | return errors.WithStack(err) 115 | } 116 | } 117 | return nil 118 | } 119 | 120 | // ScanPackage scans a single package 121 | func (p *PackageMap) ScanPackage() error { 122 | for _, f := range p.pkg.Syntax { 123 | 124 | fm := &FileMap{ 125 | PackageMap: p, 126 | file: f, 127 | matcher: astrid.NewMatcher(p.pkg.TypesInfo.Uses, p.pkg.TypesInfo.Defs), 128 | } 129 | if err := fm.FindExcludes(); err != nil { 130 | return errors.WithStack(err) 131 | } 132 | } 133 | return nil 134 | } 135 | 136 | // FindExcludes scans a single file to find code to exclude from coverage files 137 | func (f *FileMap) FindExcludes() error { 138 | var err error 139 | 140 | ast.Inspect(f.file, func(node ast.Node) bool { 141 | if err != nil { 142 | // notest 143 | return false 144 | } 145 | b, inner := f.inspectNode(node) 146 | if inner != nil { 147 | // notest 148 | err = inner 149 | return false 150 | } 151 | return b 152 | }) 153 | if err != nil { 154 | return errors.WithStack(err) 155 | } 156 | for _, cg := range f.file.Comments { 157 | f.inspectComment(cg) 158 | } 159 | return nil 160 | } 161 | 162 | func (f *FileMap) findScope(node ast.Node, filter func(ast.Node) bool) ast.Node { 163 | inside := func(node, holder ast.Node) bool { 164 | return node != nil && holder != nil && node.Pos() > holder.Pos() && node.Pos() <= holder.End() 165 | } 166 | var scopes []ast.Node 167 | ast.Inspect(f.file, func(scope ast.Node) bool { 168 | if inside(node, scope) { 169 | scopes = append(scopes, scope) 170 | return true 171 | } 172 | return false 173 | }) 174 | // find the last matching scope 175 | for i := len(scopes) - 1; i >= 0; i-- { 176 | if filter == nil || filter(scopes[i]) { 177 | return scopes[i] 178 | } 179 | } 180 | // notest 181 | return nil 182 | } 183 | 184 | func (f *FileMap) inspectComment(cg *ast.CommentGroup) { 185 | for _, cm := range cg.List { 186 | if !strings.HasPrefix(cm.Text, "//notest") && !strings.HasPrefix(cm.Text, "// notest") { 187 | continue 188 | } 189 | 190 | // get the parent scope 191 | scope := f.findScope(cm, nil) 192 | 193 | // scope can be nil if the comment is in an empty file... in that 194 | // case we don't need any excludes. 195 | if scope != nil { 196 | comment := f.fset.Position(cm.Pos()) 197 | start := f.fset.Position(scope.Pos()) 198 | end := f.fset.Position(scope.End()) 199 | endLine := end.Line 200 | if _, ok := scope.(*ast.CaseClause); ok { 201 | // case block needs an extra line... 202 | endLine++ 203 | } 204 | for line := comment.Line; line < endLine; line++ { 205 | f.addExclude(start.Filename, line) 206 | } 207 | } 208 | } 209 | } 210 | 211 | func (f *FileMap) inspectNode(node ast.Node) (bool, error) { 212 | if node == nil { 213 | return true, nil 214 | } 215 | switch n := node.(type) { 216 | case *ast.CallExpr: 217 | if id, ok := n.Fun.(*ast.Ident); ok && id.Name == "panic" { 218 | pos := f.fset.Position(n.Pos()) 219 | f.addExclude(pos.Filename, pos.Line) 220 | } 221 | case *ast.IfStmt: 222 | if err := f.inspectIf(n); err != nil { 223 | return false, err 224 | } 225 | case *ast.SwitchStmt: 226 | if n.Tag != nil { 227 | // we are only concerned with switch statements with no tag 228 | // expression e.g. switch { ... } 229 | return true, nil 230 | } 231 | var falseExpr []ast.Expr 232 | var defaultClause *ast.CaseClause 233 | for _, s := range n.Body.List { 234 | cc := s.(*ast.CaseClause) 235 | if cc.List == nil { 236 | // save the default clause until the end 237 | defaultClause = cc 238 | continue 239 | } 240 | if err := f.inspectCase(cc, falseExpr...); err != nil { 241 | return false, err 242 | } 243 | falseExpr = append(falseExpr, f.boolOr(cc.List)) 244 | } 245 | if defaultClause != nil { 246 | if err := f.inspectCase(defaultClause, falseExpr...); err != nil { 247 | return false, err 248 | } 249 | } 250 | } 251 | return true, nil 252 | } 253 | 254 | func (f *FileMap) inspectCase(stmt *ast.CaseClause, falseExpr ...ast.Expr) error { 255 | s := brenda.NewSolver(f.fset, f.pkg.TypesInfo.Uses, f.pkg.TypesInfo.Defs, f.boolOr(stmt.List), falseExpr...) 256 | if err := s.SolveTrue(); err != nil { 257 | return errors.WithStack(err) 258 | } 259 | f.processResults(s, &ast.BlockStmt{List: stmt.Body}) 260 | return nil 261 | } 262 | 263 | func (f *FileMap) boolOr(list []ast.Expr) ast.Expr { 264 | if len(list) == 0 { 265 | return nil 266 | } 267 | if len(list) == 1 { 268 | return list[0] 269 | } 270 | current := list[0] 271 | for i := 1; i < len(list); i++ { 272 | current = &ast.BinaryExpr{X: current, Y: list[i], Op: token.LOR} 273 | } 274 | return current 275 | } 276 | 277 | func (f *FileMap) inspectIf(stmt *ast.IfStmt, falseExpr ...ast.Expr) error { 278 | 279 | // main if block 280 | s := brenda.NewSolver(f.fset, f.pkg.TypesInfo.Uses, f.pkg.TypesInfo.Defs, stmt.Cond, falseExpr...) 281 | if err := s.SolveTrue(); err != nil { 282 | return errors.WithStack(err) 283 | } 284 | f.processResults(s, stmt.Body) 285 | 286 | switch e := stmt.Else.(type) { 287 | case *ast.BlockStmt: 288 | 289 | // else block 290 | s := brenda.NewSolver(f.fset, f.pkg.TypesInfo.Uses, f.pkg.TypesInfo.Defs, stmt.Cond, falseExpr...) 291 | if err := s.SolveFalse(); err != nil { 292 | return errors.WithStack(err) 293 | } 294 | f.processResults(s, e) 295 | 296 | case *ast.IfStmt: 297 | 298 | // else if block 299 | falseExpr = append(falseExpr, stmt.Cond) 300 | if err := f.inspectIf(e, falseExpr...); err != nil { 301 | return errors.WithStack(err) 302 | } 303 | } 304 | return nil 305 | } 306 | 307 | func (f *FileMap) processResults(s *brenda.Solver, block *ast.BlockStmt) { 308 | for expr, match := range s.Components { 309 | if !match.Match && !match.Inverse { 310 | continue 311 | } 312 | 313 | found, op, expr := f.isErrorComparison(expr) 314 | if !found { 315 | continue 316 | } 317 | if op == token.NEQ && match.Match || op == token.EQL && match.Inverse { 318 | ast.Inspect(block, f.inspectNodeForReturn(expr)) 319 | ast.Inspect(block, f.inspectNodeForWrap(block, expr)) 320 | } 321 | } 322 | } 323 | 324 | func (f *FileMap) isErrorComparison(e ast.Expr) (found bool, sign token.Token, expr ast.Expr) { 325 | if b, ok := e.(*ast.BinaryExpr); ok { 326 | if b.Op != token.NEQ && b.Op != token.EQL { 327 | return 328 | } 329 | xErr := f.isError(b.X) 330 | yNil := f.isNil(b.Y) 331 | 332 | if xErr && yNil { 333 | return true, b.Op, b.X 334 | } 335 | yErr := f.isError(b.Y) 336 | xNil := f.isNil(b.X) 337 | if yErr && xNil { 338 | return true, b.Op, b.Y 339 | } 340 | } 341 | return 342 | } 343 | 344 | func (f *FileMap) inspectNodeForReturn(search ast.Expr) func(node ast.Node) bool { 345 | return func(node ast.Node) bool { 346 | if node == nil { 347 | return true 348 | } 349 | switch n := node.(type) { 350 | case *ast.ReturnStmt: 351 | if f.isErrorReturn(n, search) { 352 | pos := f.fset.Position(n.Pos()) 353 | f.addExclude(pos.Filename, pos.Line) 354 | } 355 | } 356 | return true 357 | } 358 | } 359 | 360 | func (f *FileMap) inspectNodeForWrap(block *ast.BlockStmt, search ast.Expr) func(node ast.Node) bool { 361 | return func(node ast.Node) bool { 362 | if node == nil { 363 | return true 364 | } 365 | switch n := node.(type) { 366 | case *ast.DeclStmt: 367 | // covers the case: 368 | // var e = foo() 369 | // and 370 | // var e error = foo() 371 | gd, ok := n.Decl.(*ast.GenDecl) 372 | if !ok { 373 | // notest 374 | return true 375 | } 376 | if gd.Tok != token.VAR { 377 | // notest 378 | return true 379 | } 380 | if len(gd.Specs) != 1 { 381 | // notest 382 | return true 383 | } 384 | spec, ok := gd.Specs[0].(*ast.ValueSpec) 385 | if !ok { 386 | // notest 387 | return true 388 | } 389 | if len(spec.Names) != 1 || len(spec.Values) != 1 { 390 | // notest 391 | return true 392 | } 393 | newSearch := spec.Names[0] 394 | 395 | if f.isErrorCall(spec.Values[0], search) { 396 | ast.Inspect(block, f.inspectNodeForReturn(newSearch)) 397 | } 398 | 399 | case *ast.AssignStmt: 400 | if len(n.Lhs) != 1 || len(n.Rhs) != 1 { 401 | // notest 402 | return true 403 | } 404 | newSearch := n.Lhs[0] 405 | 406 | if f.isErrorCall(n.Rhs[0], search) { 407 | ast.Inspect(block, f.inspectNodeForReturn(newSearch)) 408 | } 409 | } 410 | return true 411 | } 412 | } 413 | 414 | func (f *FileMap) isErrorCall(expr, search ast.Expr) bool { 415 | n, ok := expr.(*ast.CallExpr) 416 | if !ok { 417 | return false 418 | } 419 | if !f.isError(n) { 420 | // never gets here, but leave it in for completeness 421 | // notest 422 | return false 423 | } 424 | for _, arg := range n.Args { 425 | if f.matcher.Match(arg, search) { 426 | return true 427 | } 428 | } 429 | return false 430 | } 431 | 432 | func (f *FileMap) isErrorReturnNamedResultParameters(r *ast.ReturnStmt, search ast.Expr) bool { 433 | // covers the syntax: 434 | // func a() (err error) { 435 | // if err != nil { 436 | // return 437 | // } 438 | // } 439 | scope := f.findScope(r, func(n ast.Node) bool { 440 | switch n.(type) { 441 | case *ast.FuncDecl, *ast.FuncLit: 442 | return true 443 | } 444 | return false 445 | }) 446 | var t *ast.FuncType 447 | switch s := scope.(type) { 448 | case *ast.FuncDecl: 449 | t = s.Type 450 | case *ast.FuncLit: 451 | t = s.Type 452 | } 453 | if t.Results == nil { 454 | return false 455 | } 456 | last := t.Results.List[len(t.Results.List)-1] 457 | if last.Names == nil { 458 | // anonymous returns - shouldn't be able to get here because a bare 459 | // return statement with either have zero results or named results. 460 | // notest 461 | return false 462 | } 463 | id := last.Names[len(last.Names)-1] 464 | return f.matcher.Match(id, search) 465 | } 466 | 467 | func (f *FileMap) isErrorReturn(r *ast.ReturnStmt, search ast.Expr) bool { 468 | if len(r.Results) == 0 { 469 | return f.isErrorReturnNamedResultParameters(r, search) 470 | } 471 | 472 | last := r.Results[len(r.Results)-1] 473 | 474 | // check the last result is an error 475 | if !f.isError(last) { 476 | return false 477 | } 478 | 479 | // check all the other results are nil or zero 480 | for i, v := range r.Results { 481 | if i == len(r.Results)-1 { 482 | // ignore the last item 483 | break 484 | } 485 | if !f.isZero(v) { 486 | return false 487 | } 488 | } 489 | 490 | return f.matcher.Match(last, search) || f.isErrorCall(last, search) 491 | } 492 | 493 | func (f *FileMap) isError(v ast.Expr) bool { 494 | if n, ok := f.pkg.TypesInfo.TypeOf(v).(*types.Named); ok { 495 | o := n.Obj() 496 | return o != nil && o.Pkg() == nil && o.Name() == "error" 497 | } 498 | return false 499 | } 500 | 501 | func (f *FileMap) isNil(v ast.Expr) bool { 502 | t := f.pkg.TypesInfo.Types[v] 503 | return t.IsNil() 504 | } 505 | 506 | func (f *FileMap) isZero(v ast.Expr) bool { 507 | t := f.pkg.TypesInfo.Types[v] 508 | if t.IsNil() { 509 | return true 510 | } 511 | if t.Value != nil { 512 | // constant 513 | switch t.Value.Kind() { 514 | case constant.Bool: 515 | return constant.BoolVal(t.Value) == false 516 | case constant.String: 517 | return constant.StringVal(t.Value) == "" 518 | case constant.Int, constant.Float, constant.Complex: 519 | return constant.Sign(t.Value) == 0 520 | default: 521 | // notest 522 | return false 523 | } 524 | } 525 | if t.IsValue() { 526 | if cl, ok := v.(*ast.CompositeLit); ok { 527 | for _, e := range cl.Elts { 528 | if kve, ok := e.(*ast.KeyValueExpr); ok { 529 | e = kve.Value 530 | } 531 | if !f.isZero(e) { 532 | return false 533 | } 534 | } 535 | } 536 | } 537 | return true 538 | } 539 | -------------------------------------------------------------------------------- /scanner/scanner_test.go: -------------------------------------------------------------------------------- 1 | package scanner_test 2 | 3 | import ( 4 | "regexp" 5 | "strconv" 6 | "strings" 7 | "testing" 8 | 9 | "path/filepath" 10 | 11 | "github.com/dave/courtney/scanner" 12 | "github.com/dave/courtney/shared" 13 | "github.com/dave/patsy" 14 | "github.com/dave/patsy/builder" 15 | "github.com/dave/patsy/vos" 16 | ) 17 | 18 | func TestSingle(t *testing.T) { 19 | tests := map[string]string{ 20 | "single": `package a 21 | 22 | func wrap(error) error 23 | 24 | func a() error { 25 | var a bool 26 | var err error 27 | if err != nil { 28 | if a { // this line will not be excluded! 29 | return wrap(err) // * 30 | } 31 | return wrap(err) // * 32 | } 33 | return nil 34 | } 35 | `, 36 | } 37 | test(t, tests) 38 | } 39 | 40 | func TestSwitchCase(t *testing.T) { 41 | tests := map[string]string{ 42 | "simple switch": `package a 43 | 44 | func a() error { 45 | var err error 46 | switch { 47 | case err != nil: 48 | return err // * 49 | } 50 | return nil 51 | } 52 | `, 53 | "switch multi": `package a 54 | 55 | func a() error { 56 | var a bool 57 | var err error 58 | switch { 59 | case err == nil, a: 60 | return err 61 | default: 62 | return err // * 63 | } 64 | return nil 65 | } 66 | `, 67 | "simple switch ignored": `package a 68 | 69 | func a() error { 70 | var a bool 71 | var err error 72 | switch a { 73 | case err != nil: 74 | return err 75 | } 76 | return nil 77 | } 78 | `, 79 | "complex switch": `package a 80 | 81 | func foo() error { 82 | var err error 83 | var b, c bool 84 | var d int 85 | switch { 86 | case err == nil && (b && d > 0) || c: 87 | return err 88 | case d <= 0 || c: 89 | return err 90 | case b: 91 | return err // * 92 | } 93 | return err 94 | } 95 | `, 96 | } 97 | test(t, tests) 98 | } 99 | 100 | func TestNamedParameters(t *testing.T) { 101 | tests := map[string]string{ 102 | "named parameters simple": `package a 103 | 104 | func a() (err error) { 105 | if err != nil { 106 | return // * 107 | } 108 | return 109 | } 110 | `, 111 | "named parameters ignored": `package a 112 | 113 | func a() { 114 | var err error 115 | if err != nil { 116 | return 117 | } 118 | return 119 | } 120 | `, 121 | "named parameters 2": `package a 122 | 123 | func a() (i int, err error) { 124 | i = 1 125 | if err != nil { 126 | return // * 127 | } 128 | return 129 | } 130 | `, 131 | "named parameters must be last": `package a 132 | 133 | func a() (err error, i int) { 134 | i = 1 135 | if err != nil { 136 | return 137 | } 138 | return 139 | } 140 | `, 141 | "named parameters must be not nil": `package a 142 | 143 | func a() (err error) { 144 | return 145 | } 146 | `, 147 | "named parameters func lit": `package a 148 | 149 | func a() { 150 | func () (err error) { 151 | if err != nil { 152 | return // * 153 | } 154 | return 155 | }() 156 | } 157 | `, 158 | } 159 | test(t, tests) 160 | } 161 | 162 | func TestBool(t *testing.T) { 163 | tests := map[string]string{ 164 | "wrap1": `package a 165 | 166 | func a() error { 167 | var wrap func(error) error 168 | var err error 169 | if err != nil { 170 | return wrap(err) // * 171 | } 172 | return nil 173 | } 174 | `, 175 | "wrap ignored": `package a 176 | 177 | func a() int { 178 | var wrap func(error) int 179 | var err error 180 | if err != nil { 181 | return wrap(err) 182 | } 183 | return 0 184 | } 185 | `, 186 | "wrap2": `package a 187 | 188 | func a() error { 189 | var wrap func(error) error 190 | var err error 191 | if err != nil { 192 | w := wrap(err) 193 | return w // * 194 | } 195 | return nil 196 | } 197 | `, 198 | "wrap3": `package a 199 | 200 | func a() error { 201 | var wrap func(error) error 202 | var err error 203 | var w error 204 | if err != nil { 205 | w = wrap(err) 206 | return w // * 207 | } 208 | return nil 209 | } 210 | `, 211 | "wrap4": `package a 212 | 213 | func a() error { 214 | var wrap func(error) error 215 | var err error 216 | if err != nil { 217 | var w = wrap(err) 218 | return w // * 219 | } 220 | return nil 221 | } 222 | `, 223 | "wrap5": `package a 224 | 225 | func a() error { 226 | var wrap func(error) error 227 | var err error 228 | if err != nil { 229 | var w error = wrap(err) 230 | return w // * 231 | } 232 | return nil 233 | } 234 | `, 235 | "wrap no tuple": `package a 236 | 237 | func a() (int, error) { 238 | var wrap func(error) (int, error) 239 | var err error 240 | if err != nil { 241 | return wrap(err) 242 | } 243 | return 0, nil 244 | } 245 | `, 246 | "logical and first": `package a 247 | 248 | import "fmt" 249 | 250 | func a() error { 251 | _, err := fmt.Println() 252 | if err != nil && 1 == 1 { 253 | return err // * 254 | } 255 | return nil 256 | } 257 | `, 258 | "logical and second": `package a 259 | 260 | import "fmt" 261 | 262 | func a() error { 263 | _, err := fmt.Println() 264 | if 1 == 1 && err != nil { 265 | return err // * 266 | } 267 | return nil 268 | } 269 | `, 270 | "logical and third": `package a 271 | 272 | import "fmt" 273 | 274 | func a() error { 275 | _, err := fmt.Println() 276 | if 1 == 1 && 2 == 2 && err != nil { 277 | return err // * 278 | } 279 | return nil 280 | } 281 | `, 282 | "logical and brackets": `package a 283 | 284 | import "fmt" 285 | 286 | func a() error { 287 | _, err := fmt.Println() 288 | if 1 == 1 && (2 == 2 && err != nil) { 289 | return err // * 290 | } 291 | return nil 292 | } 293 | `, 294 | "logical or first": `package a 295 | 296 | import "fmt" 297 | 298 | func a() error { 299 | _, err := fmt.Println() 300 | if err == nil || 1 == 1 { 301 | return err 302 | } else { 303 | return err // * 304 | } 305 | return nil 306 | } 307 | `, 308 | "logical or second": `package a 309 | 310 | import "fmt" 311 | 312 | func a() error { 313 | _, err := fmt.Println() 314 | if 1 == 1 || err == nil { 315 | return err 316 | } else { 317 | return err // * 318 | } 319 | return nil 320 | } 321 | `, 322 | "logical or third": `package a 323 | 324 | import "fmt" 325 | 326 | func a() error { 327 | _, err := fmt.Println() 328 | if 1 == 1 || 2 == 2 || err == nil { 329 | return err 330 | } else { 331 | return err // * 332 | } 333 | return nil 334 | } 335 | `, 336 | "logical or brackets": `package a 337 | 338 | import "fmt" 339 | 340 | func a() error { 341 | _, err := fmt.Println() 342 | if 1 == 1 || (2 == 2 || err == nil) { 343 | return err 344 | } else { 345 | return err // * 346 | } 347 | return nil 348 | } 349 | `, 350 | "complex": `package a 351 | 352 | func foo() error { 353 | var err error 354 | var b, c bool 355 | var d int 356 | if err == nil && (b && d > 0) || c { 357 | return err 358 | } else if d <= 0 || c { 359 | return err 360 | } else if b { 361 | return err // * 362 | } 363 | return err 364 | } 365 | `, 366 | } 367 | test(t, tests) 368 | } 369 | 370 | func TestGeneral(t *testing.T) { 371 | tests := map[string]string{ 372 | "simple": `package a 373 | 374 | import "fmt" 375 | 376 | func a() error { 377 | _, err := fmt.Println() 378 | if err != nil { 379 | return err // * 380 | } 381 | return nil 382 | } 383 | `, 384 | "wrong way round": `package a 385 | 386 | import "fmt" 387 | 388 | func a() error { 389 | _, err := fmt.Println() 390 | if nil != err { 391 | return err // * 392 | } 393 | return nil 394 | } 395 | `, 396 | "not else block": `package a 397 | 398 | import "fmt" 399 | 400 | func a() error { 401 | _, err := fmt.Println() 402 | if err != nil { 403 | return err // * 404 | } else { 405 | return err 406 | } 407 | return nil 408 | } 409 | `, 410 | "any name": `package a 411 | 412 | import "fmt" 413 | 414 | func a() error { 415 | _, foo := fmt.Println() 416 | if foo != nil { 417 | return foo // * 418 | } 419 | return nil 420 | } 421 | `, 422 | "don't mark if ==": `package a 423 | 424 | import "fmt" 425 | 426 | func a() error { 427 | _, err := fmt.Println() 428 | if err == nil { 429 | return err 430 | } 431 | return nil 432 | } 433 | `, 434 | "use else block if err == nil": `package a 435 | 436 | import "fmt" 437 | 438 | func a() error { 439 | _, err := fmt.Println() 440 | if err == nil { 441 | return err 442 | } else { 443 | return err // * 444 | } 445 | return nil 446 | } 447 | `, 448 | "support if with init form": `package a 449 | 450 | import "fmt" 451 | 452 | func a() error { 453 | if _, err := fmt.Println(); err != nil { 454 | return err // * 455 | } 456 | return nil 457 | } 458 | `, 459 | "only in if block": `package foo 460 | 461 | import "fmt" 462 | 463 | func Baz() error { 464 | return fmt.Errorf("foo") 465 | } 466 | `, 467 | } 468 | test(t, tests) 469 | } 470 | 471 | func TestZeroValues(t *testing.T) { 472 | tests := map[string]string{ 473 | "only return if all other return vars are zero": `package a 474 | 475 | import "fmt" 476 | 477 | type iface interface{} 478 | 479 | type strct struct { 480 | a int 481 | b string 482 | } 483 | 484 | func Foo() (iface, bool, int, string, float32, strct, strct, error) { 485 | if _, err := fmt.Println(); err != nil { 486 | return 1, false, 0, "", 0.0, strct{0, ""}, strct{a: 0, b: ""}, err 487 | } 488 | if _, err := fmt.Println(); err != nil { 489 | return nil, true, 0, "", 0.0, strct{0, ""}, strct{a: 0, b: ""}, err 490 | } 491 | if _, err := fmt.Println(); err != nil { 492 | return nil, false, 1, "", 0.0, strct{0, ""}, strct{a: 0, b: ""}, err 493 | } 494 | if _, err := fmt.Println(); err != nil { 495 | return nil, false, 0, "a", 0.0, strct{0, ""}, strct{a: 0, b: ""}, err 496 | } 497 | if _, err := fmt.Println(); err != nil { 498 | return nil, false, 0, "", 1.0, strct{0, ""}, strct{a: 0, b: ""}, err 499 | } 500 | if _, err := fmt.Println(); err != nil { 501 | return nil, false, 0, "", 0.0, strct{1, ""}, strct{a: 0, b: ""}, err 502 | } 503 | if _, err := fmt.Println(); err != nil { 504 | return nil, false, 0, "", 0.0, strct{0, "a"}, strct{a: 0, b: ""}, err 505 | } 506 | if _, err := fmt.Println(); err != nil { 507 | return nil, false, 0, "", 0.0, strct{0, ""}, strct{a: 1, b: ""}, err 508 | } 509 | if _, err := fmt.Println(); err != nil { 510 | return nil, false, 0, "", 0.0, strct{0, ""}, strct{a: 0, b: "a"}, err 511 | } 512 | if _, err := fmt.Println(); err != nil { 513 | return nil, false, 0, "", 0.0, strct{0, ""}, strct{a: 0, b: ""}, err // * 514 | } 515 | return nil, false, 0, "", 0.0, strct{0, ""}, strct{a: 0, b: ""}, nil 516 | } 517 | `, 518 | } 519 | test(t, tests) 520 | } 521 | 522 | func TestSelectorExpressions(t *testing.T) { 523 | tests := map[string]string{ 524 | "selector expression": `package foo 525 | 526 | func Baz() error { 527 | type T struct { 528 | Err error 529 | } 530 | var b T 531 | if b.Err != nil { 532 | return b.Err // * 533 | } 534 | return nil 535 | } 536 | `, 537 | } 538 | test(t, tests) 539 | } 540 | 541 | func TestFunctionExpressions(t *testing.T) { 542 | tests := map[string]string{ 543 | "function expression": `package foo 544 | 545 | func Baz() error { 546 | var f func(int) error 547 | if f(5) != nil { 548 | return f(5) // * 549 | } 550 | return nil 551 | } 552 | `, 553 | "function expression params": `package foo 554 | 555 | func Baz() error { 556 | var f func(int) error 557 | if f(4) != nil { 558 | return f(5) 559 | } 560 | return nil 561 | } 562 | `, 563 | "function expression params 2": `package foo 564 | 565 | func Baz() error { 566 | var f func(...int) error 567 | if f(4) != nil { 568 | return f(4, 4) 569 | } 570 | return nil 571 | } 572 | `, 573 | "function expression elipsis": `package foo 574 | 575 | func Baz() error { 576 | var f func(...interface{}) error 577 | var a []interface{} 578 | if f(a) != nil { 579 | return f(a...) 580 | } 581 | return nil 582 | } 583 | `, 584 | "function expression elipsis 2": `package foo 585 | 586 | func Baz() error { 587 | var f func(...interface{}) error 588 | var a []interface{} 589 | if f(a) != nil { 590 | return f(a) // * 591 | } 592 | return nil 593 | } 594 | `, 595 | } 596 | test(t, tests) 597 | } 598 | 599 | func TestPanic(t *testing.T) { 600 | tests := map[string]string{ 601 | "panic": `package foo 602 | 603 | func Baz() error { 604 | panic("") // * 605 | } 606 | `, 607 | } 608 | test(t, tests) 609 | } 610 | 611 | func TestComments(t *testing.T) { 612 | tests := map[string]string{ 613 | "scope": `package foo 614 | 615 | func Baz() int { 616 | i := 1 617 | if i > 1 { 618 | return i 619 | } 620 | 621 | //notest 622 | // * 623 | if i > 2 { // * 624 | return i // * 625 | } // * 626 | return 0 // * 627 | } 628 | `, 629 | "scope if": `package foo 630 | 631 | func Baz(i int) int { 632 | if i > 2 { 633 | //notest 634 | return i // * 635 | } 636 | return 0 637 | } 638 | `, 639 | "scope file": `package foo 640 | 641 | //notest 642 | // * 643 | func Baz(i int) int { // * 644 | if i > 2 { // * 645 | return i // * 646 | } // * 647 | return 0 // * 648 | } // * 649 | // * 650 | func Foo(i int) int { // * 651 | return 0 // * 652 | } 653 | `, 654 | "complex comments": `package foo 655 | 656 | type Logger struct { 657 | Enabled bool 658 | } 659 | func (l Logger) Print(i ...interface{}) {} 660 | 661 | func Foo() { 662 | var logger Logger 663 | var tokens []interface{} 664 | if logger.Enabled { 665 | // notest 666 | for i, token := range tokens { // * 667 | logger.Print("[", i, "] ", token) // * 668 | } // * 669 | } 670 | } 671 | `, 672 | "case block": `package foo 673 | 674 | func Foo() bool { 675 | switch { 676 | case true: 677 | // notest 678 | if true { // * 679 | return true // * 680 | } // * 681 | return false // * 682 | } 683 | return false 684 | } 685 | `, 686 | "case block with explanation comment": `package foo 687 | 688 | func Foo() bool { 689 | switch { 690 | case true: 691 | // notest // this condition is always true 692 | if true { // * 693 | return true // * 694 | } // * 695 | return false // * 696 | } 697 | return false 698 | } 699 | `, 700 | } 701 | test(t, tests) 702 | } 703 | 704 | func test(t *testing.T, tests map[string]string) { 705 | for name, source := range tests { 706 | env := vos.Mock() 707 | b, err := builder.New(env, "ns", true) 708 | if err != nil { 709 | t.Fatalf("Error creating builder in %s: %+v", name, err) 710 | } 711 | defer b.Cleanup() 712 | 713 | ppath, pdir, err := b.Package("a", map[string]string{ 714 | "a.go": source, 715 | }) 716 | if err != nil { 717 | t.Fatalf("Error creating package in %s: %+v", name, err) 718 | } 719 | 720 | paths := patsy.NewCache(env) 721 | setup := &shared.Setup{ 722 | Env: env, 723 | Paths: paths, 724 | } 725 | if err := setup.Parse([]string{ppath}); err != nil { 726 | t.Fatalf("Error parsing args in %s: %+v", name, err) 727 | } 728 | 729 | cm := scanner.New(setup) 730 | 731 | if err := cm.LoadProgram(); err != nil { 732 | t.Fatalf("Error loading program in %s: %+v", name, err) 733 | } 734 | 735 | if err := cm.ScanPackages(); err != nil { 736 | t.Fatalf("Error scanning packages in %s: %+v", name, err) 737 | } 738 | 739 | result := cm.Excludes[filepath.Join(pdir, "a.go")] 740 | 741 | // matches strings like: 742 | // - //notest$ 743 | // - // notest$ 744 | // - //notest // because this is glue code$ 745 | // - // notest // because this is glue code$ 746 | notest := regexp.MustCompile("//\\s?notest(\\s//\\s?.*)?$") 747 | 748 | for i, line := range strings.Split(source, "\n") { 749 | expected := strings.HasSuffix(line, "// *") || notest.MatchString(line) 750 | if result[i+1] != expected { 751 | t.Fatalf("Unexpected state in %s, line %d: %s\n", name, i, strconv.Quote(strings.Trim(line, "\t"))) 752 | } 753 | } 754 | } 755 | } 756 | -------------------------------------------------------------------------------- /shared/shared.go: -------------------------------------------------------------------------------- 1 | package shared 2 | 3 | import ( 4 | "strings" 5 | 6 | "github.com/dave/patsy" 7 | "github.com/dave/patsy/vos" 8 | ) 9 | 10 | // Setup holds globals, environment and command line flags for the courtney 11 | // command 12 | type Setup struct { 13 | Env vos.Env 14 | Paths *patsy.Cache 15 | Enforce bool 16 | Verbose bool 17 | Short bool 18 | Timeout string 19 | Load string 20 | Output string 21 | TestArgs []string 22 | Packages []PackageSpec 23 | } 24 | 25 | // PackageSpec identifies a package by dir and path 26 | type PackageSpec struct { 27 | Dir string 28 | Path string 29 | } 30 | 31 | // Parse parses a slice of strings into the Packages slice 32 | func (s *Setup) Parse(args []string) error { 33 | if len(args) == 0 { 34 | args = []string{"./..."} 35 | } 36 | packages := map[string]string{} 37 | for _, ppath := range args { 38 | ppath = strings.TrimSuffix(ppath, "/") 39 | 40 | paths, err := s.Paths.Dirs(ppath) 41 | if err != nil { 42 | return err 43 | } 44 | 45 | for importPath, dir := range paths { 46 | packages[importPath] = dir 47 | } 48 | } 49 | for ppath, dir := range packages { 50 | s.Packages = append(s.Packages, PackageSpec{Path: ppath, Dir: dir}) 51 | } 52 | return nil 53 | } 54 | -------------------------------------------------------------------------------- /shared/shared_test.go: -------------------------------------------------------------------------------- 1 | package shared_test 2 | 3 | import ( 4 | "fmt" 5 | "testing" 6 | 7 | "github.com/dave/courtney/shared" 8 | "github.com/dave/patsy" 9 | "github.com/dave/patsy/builder" 10 | "github.com/dave/patsy/vos" 11 | ) 12 | 13 | func TestParseArgs(t *testing.T) { 14 | for _, gomod := range []bool{true, false} { 15 | t.Run(fmt.Sprintf("gomod=%v", gomod), func(t *testing.T) { 16 | env := vos.Mock() 17 | b, err := builder.New(env, "ns", gomod) 18 | if err != nil { 19 | t.Fatal(fmt.Sprintf("%+v", err)) 20 | } 21 | defer b.Cleanup() 22 | 23 | apath, adir, err := b.Package("a", map[string]string{ 24 | "a.go": `package a`, 25 | }) 26 | bpath, bdir, err := b.Package("a/b", map[string]string{ 27 | "b.go": `package b`, 28 | }) 29 | cpath, cdir, err := b.Package("a/c", map[string]string{ 30 | "c.go": `package c`, 31 | }) 32 | if err != nil { 33 | t.Fatal(fmt.Sprintf("%+v", err)) 34 | } 35 | 36 | paths := patsy.NewCache(env) 37 | 38 | if err := env.Setwd(adir); err != nil { 39 | t.Fatal(fmt.Sprintf("%+v", err)) 40 | } 41 | 42 | expectedA := shared.PackageSpec{ 43 | Dir: adir, 44 | Path: apath, 45 | } 46 | expectedB := shared.PackageSpec{ 47 | Dir: bdir, 48 | Path: bpath, 49 | } 50 | expectedC := shared.PackageSpec{ 51 | Dir: cdir, 52 | Path: cpath, 53 | } 54 | 55 | setup := shared.Setup{ 56 | Env: env, 57 | Paths: paths, 58 | } 59 | if err := setup.Parse([]string{"."}); err != nil { 60 | t.Fatal(fmt.Sprintf("%+v", err)) 61 | } 62 | if len(setup.Packages) != 1 { 63 | t.Fatalf("Error in ParseArgs - wrong number of packages. Expected 1, got %d", len(setup.Packages)) 64 | } 65 | if setup.Packages[0] != expectedA { 66 | t.Fatalf("Error in ParseArgs - wrong package. Expected %#v. Got %#v.", expectedA, setup.Packages[0]) 67 | } 68 | 69 | setup = shared.Setup{ 70 | Env: env, 71 | Paths: paths, 72 | } 73 | if err := setup.Parse(nil); err != nil { 74 | t.Fatal(fmt.Sprintf("%+v", err)) 75 | } 76 | if len(setup.Packages) != 3 { 77 | t.Fatalf("Error in ParseArgs - wrong number of packages. Expected 3, got %d", len(setup.Packages)) 78 | } 79 | if setup.Packages[0] != expectedA && setup.Packages[0] != expectedB && setup.Packages[0] != expectedC { 80 | t.Fatal("Error in ParseArgs - wrong package.") 81 | } 82 | if setup.Packages[1] != expectedA && setup.Packages[1] != expectedB && setup.Packages[1] != expectedC { 83 | t.Fatal("Error in ParseArgs - wrong package.") 84 | } 85 | if setup.Packages[2] != expectedA && setup.Packages[2] != expectedB && setup.Packages[2] != expectedC { 86 | t.Fatal("Error in ParseArgs - wrong package.") 87 | } 88 | 89 | if err := env.Setwd(bdir); err != nil { 90 | t.Fatal(fmt.Sprintf("%+v", err)) 91 | } 92 | 93 | setup = shared.Setup{ 94 | Env: env, 95 | Paths: paths, 96 | } 97 | if err := setup.Parse([]string{"."}); err != nil { 98 | t.Fatal(fmt.Sprintf("%+v", err)) 99 | } 100 | if len(setup.Packages) != 1 { 101 | t.Fatalf("Error in ParseArgs - wrong number of packages. Expected 1, got %d", len(setup.Packages)) 102 | } 103 | if setup.Packages[0] != expectedB { 104 | t.Fatalf("Error in ParseArgs - wrong package. Expected %#v. Got %#v.", expectedB, setup.Packages[0]) 105 | } 106 | 107 | setup = shared.Setup{ 108 | Env: env, 109 | Paths: paths, 110 | } 111 | // should correctly strip "/" suffix 112 | if err := setup.Parse([]string{"ns/a/b/"}); err != nil { 113 | t.Fatal(fmt.Sprintf("%+v", err)) 114 | } 115 | if len(setup.Packages) != 1 { 116 | t.Fatalf("Error in ParseArgs - wrong number of packages. Expected 1, got %d", len(setup.Packages)) 117 | } 118 | if setup.Packages[0] != expectedB { 119 | t.Fatalf("Error in ParseArgs - wrong package. Expected %#v. Got %#v.", expectedB, setup.Packages[0]) 120 | } 121 | }) 122 | } 123 | } 124 | -------------------------------------------------------------------------------- /tester/logger/logger.go: -------------------------------------------------------------------------------- 1 | package logger 2 | 3 | import ( 4 | "bytes" 5 | "io" 6 | ) 7 | 8 | // Log returns a buffer and two Writers. Data written to the writers is 9 | // combined and stored in the buffer. If verbose = true, it is also written to 10 | // the two provided Writers. 11 | func Log(verbose bool, stdout io.Writer, stderr io.Writer) (log *bytes.Buffer, loggedStdout io.Writer, loggedStderr io.Writer) { 12 | log = &bytes.Buffer{} 13 | if verbose { 14 | loggedStdout = MultiWriter(stdout, log) 15 | loggedStderr = MultiWriter(stderr, log) 16 | } else { 17 | loggedStdout = log 18 | loggedStderr = log 19 | } 20 | return 21 | } 22 | 23 | // MultiWriter creates a writer that duplicates its writes to all the 24 | // provided writers, similar to the Unix tee(1) command. 25 | func MultiWriter(primary io.Writer, writers ...io.Writer) io.Writer { 26 | w := make([]io.Writer, len(writers)) 27 | copy(w, writers) 28 | return &multiWriter{primary: primary, writers: w} 29 | } 30 | 31 | type multiWriter struct { 32 | primary io.Writer 33 | writers []io.Writer 34 | } 35 | 36 | // Write writes to the writers. 37 | func (t *multiWriter) Write(p []byte) (n int, err error) { 38 | for _, w := range t.writers { 39 | w.Write(p) 40 | } 41 | return t.primary.Write(p) 42 | } 43 | -------------------------------------------------------------------------------- /tester/logger/logger_test.go: -------------------------------------------------------------------------------- 1 | package logger 2 | 3 | import ( 4 | "bytes" 5 | "testing" 6 | 7 | "os" 8 | ) 9 | 10 | func TestLogger(t *testing.T) { 11 | _, o, e := Log(false, os.Stdout, os.Stderr) 12 | _, ok := o.(*bytes.Buffer) 13 | if !ok { 14 | t.Fatal("Stdout is not a *Buffer") 15 | } 16 | _, ok = e.(*bytes.Buffer) 17 | if !ok { 18 | t.Fatal("Stderr is not a *Buffer") 19 | } 20 | 21 | _, o, e = Log(true, os.Stdout, os.Stderr) 22 | _, ok = o.(*multiWriter) 23 | if !ok { 24 | t.Fatal("Stdout is not a *multiWriter") 25 | } 26 | _, ok = e.(*multiWriter) 27 | if !ok { 28 | t.Fatal("Stderr is not a *multiWriter") 29 | } 30 | } 31 | 32 | func TestMultiWriter(t *testing.T) { 33 | var p, w1, w2 []byte 34 | pb := bytes.NewBuffer(p) 35 | w1b := bytes.NewBuffer(w1) 36 | w2b := bytes.NewBuffer(w2) 37 | mw := MultiWriter(pb, w1b, w2b) 38 | mw.Write([]byte("a")) 39 | mw.Write([]byte("b")) 40 | if pb.String() != "ab" { 41 | t.Fatalf("pb expected 'ab', got '%s'", pb.String()) 42 | } 43 | if w1b.String() != "ab" { 44 | t.Fatalf("w1b expected 'ab', got '%s'", pb.String()) 45 | } 46 | if w2b.String() != "ab" { 47 | t.Fatalf("w2b expected 'ab', got '%s'", pb.String()) 48 | } 49 | } 50 | -------------------------------------------------------------------------------- /tester/merge/LICENCE: -------------------------------------------------------------------------------- 1 | Copyright (c) 2015, Wade Simmons 2 | All rights reserved. 3 | 4 | Redistribution and use in source and binary forms, with or without 5 | modification, are permitted provided that the following conditions are met: 6 | 7 | 1. Redistributions of source code must retain the above copyright notice, this 8 | list of conditions and the following disclaimer. 9 | 2. Redistributions in binary form must reproduce the above copyright notice, 10 | this list of conditions and the following disclaimer in the documentation 11 | and/or other materials provided with the distribution. 12 | 13 | THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND 14 | ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED 15 | WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE 16 | DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE LIABLE FOR 17 | ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES 18 | (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; 19 | LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND 20 | ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT 21 | (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS 22 | SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. -------------------------------------------------------------------------------- /tester/merge/README.md: -------------------------------------------------------------------------------- 1 | # merge 2 | 3 | This package was adapted from [gocovmerge](https://github.com/wadey/gocovmerge). 4 | Thanks [Wade](https://github.com/wadey)! -------------------------------------------------------------------------------- /tester/merge/merge.go: -------------------------------------------------------------------------------- 1 | package merge 2 | 3 | import ( 4 | "fmt" 5 | "io" 6 | "sort" 7 | 8 | "github.com/pkg/errors" 9 | "golang.org/x/tools/cover" 10 | ) 11 | 12 | // notest 13 | 14 | // AddProfile adds and merges a profile to a slice of profiles 15 | func AddProfile(profiles []*cover.Profile, p *cover.Profile) ([]*cover.Profile, error) { 16 | i := sort.Search(len(profiles), func(i int) bool { return profiles[i].FileName >= p.FileName }) 17 | if i < len(profiles) && profiles[i].FileName == p.FileName { 18 | if err := mergeProfiles(profiles[i], p); err != nil { 19 | return nil, err 20 | } 21 | } else { 22 | profiles = append(profiles, nil) 23 | copy(profiles[i+1:], profiles[i:]) 24 | profiles[i] = p 25 | } 26 | return profiles, nil 27 | } 28 | 29 | // DumpProfiles writes a slice of profiles to a writer in the standard format. 30 | func DumpProfiles(profiles []*cover.Profile, out io.Writer) { 31 | if len(profiles) == 0 { 32 | return 33 | } 34 | fmt.Fprintf(out, "mode: %s\n", profiles[0].Mode) 35 | for _, p := range profiles { 36 | for _, b := range p.Blocks { 37 | fmt.Fprintf(out, "%s:%d.%d,%d.%d %d %d\n", p.FileName, b.StartLine, b.StartCol, b.EndLine, b.EndCol, b.NumStmt, b.Count) 38 | } 39 | } 40 | } 41 | 42 | func mergeProfiles(p *cover.Profile, merge *cover.Profile) error { 43 | if p.Mode != merge.Mode { 44 | return errors.New("cannot merge profiles with different modes") 45 | } 46 | // Since the blocks are sorted, we can keep track of where the last block 47 | // was inserted and only look at the blocks after that as targets for merge 48 | startIndex := 0 49 | var err error 50 | for _, b := range merge.Blocks { 51 | if startIndex, err = mergeProfileBlock(p, b, startIndex); err != nil { 52 | return err 53 | } 54 | } 55 | return nil 56 | } 57 | 58 | func mergeProfileBlock(p *cover.Profile, pb cover.ProfileBlock, startIndex int) (int, error) { 59 | sortFunc := func(i int) bool { 60 | pi := p.Blocks[i+startIndex] 61 | return pi.StartLine >= pb.StartLine && (pi.StartLine != pb.StartLine || pi.StartCol >= pb.StartCol) 62 | } 63 | 64 | i := 0 65 | if sortFunc(i) != true { 66 | i = sort.Search(len(p.Blocks)-startIndex, sortFunc) 67 | } 68 | i += startIndex 69 | if i < len(p.Blocks) && p.Blocks[i].StartLine == pb.StartLine && p.Blocks[i].StartCol == pb.StartCol { 70 | if p.Blocks[i].EndLine != pb.EndLine || p.Blocks[i].EndCol != pb.EndCol { 71 | return 0, errors.Errorf("OVERLAP MERGE: %v %v %v", p.FileName, p.Blocks[i], pb) 72 | } 73 | switch p.Mode { 74 | case "set": 75 | p.Blocks[i].Count |= pb.Count 76 | case "count", "atomic": 77 | p.Blocks[i].Count += pb.Count 78 | default: 79 | return 0, errors.Errorf("unsupported covermode: '%s'", p.Mode) 80 | } 81 | } else { 82 | if i > 0 { 83 | pa := p.Blocks[i-1] 84 | if pa.EndLine >= pb.EndLine && (pa.EndLine != pb.EndLine || pa.EndCol > pb.EndCol) { 85 | return 0, errors.Errorf("OVERLAP BEFORE: %v %v %v", p.FileName, pa, pb) 86 | } 87 | } 88 | if i < len(p.Blocks)-1 { 89 | pa := p.Blocks[i+1] 90 | if pa.StartLine <= pb.StartLine && (pa.StartLine != pb.StartLine || pa.StartCol < pb.StartCol) { 91 | return 0, errors.Errorf("OVERLAP AFTER: %v %v %v", p.FileName, pa, pb) 92 | } 93 | } 94 | p.Blocks = append(p.Blocks, cover.ProfileBlock{}) 95 | copy(p.Blocks[i+1:], p.Blocks[i:]) 96 | p.Blocks[i] = pb 97 | } 98 | return i + 1, nil 99 | } 100 | -------------------------------------------------------------------------------- /tester/tester.go: -------------------------------------------------------------------------------- 1 | package tester 2 | 3 | import ( 4 | "crypto/md5" 5 | "fmt" 6 | "io/ioutil" 7 | "os" 8 | "os/exec" 9 | "path/filepath" 10 | "regexp" 11 | "strings" 12 | 13 | "github.com/dave/courtney/shared" 14 | "github.com/dave/courtney/tester/logger" 15 | "github.com/dave/courtney/tester/merge" 16 | "github.com/pkg/errors" 17 | "golang.org/x/tools/cover" 18 | ) 19 | 20 | // New creates a new Tester with the provided setup 21 | func New(setup *shared.Setup) *Tester { 22 | t := &Tester{ 23 | setup: setup, 24 | } 25 | return t 26 | } 27 | 28 | // Tester runs tests and merges coverage files 29 | type Tester struct { 30 | setup *shared.Setup 31 | cover string 32 | Results []*cover.Profile 33 | } 34 | 35 | // Load loads pre-prepared coverage files instead of running 'go test' 36 | func (t *Tester) Load() error { 37 | files, err := filepath.Glob(t.setup.Load) 38 | if err != nil { 39 | return errors.Wrap(err, "Error loading coverage files") 40 | } 41 | for _, fpath := range files { 42 | if err := t.processCoverageFile(fpath); err != nil { 43 | return err 44 | } 45 | } 46 | return nil 47 | } 48 | 49 | // Test initiates the tests and merges the coverage files 50 | func (t *Tester) Test() error { 51 | 52 | var err error 53 | if t.cover, err = ioutil.TempDir("", "coverage"); err != nil { 54 | return errors.Wrap(err, "Error creating temporary coverage dir") 55 | } 56 | defer os.RemoveAll(t.cover) 57 | 58 | for _, spec := range t.setup.Packages { 59 | if err := t.processDir(spec.Dir); err != nil { 60 | return err 61 | } 62 | } 63 | 64 | return nil 65 | } 66 | 67 | // Save saves the coverage file 68 | func (t *Tester) Save() error { 69 | if len(t.Results) == 0 { 70 | fmt.Fprintln(t.setup.Env.Stdout(), "No results") 71 | return nil 72 | } 73 | currentDir, err := t.setup.Env.Getwd() 74 | if err != nil { 75 | return errors.Wrap(err, "Error getting working dir") 76 | } 77 | out := filepath.Join(currentDir, "coverage.out") 78 | if t.setup.Output != "" { 79 | out = t.setup.Output 80 | } 81 | f, err := os.Create(out) 82 | if err != nil { 83 | return errors.Wrapf(err, "Error creating output coverage file %s", out) 84 | } 85 | defer f.Close() 86 | merge.DumpProfiles(t.Results, f) 87 | return nil 88 | } 89 | 90 | // Enforce returns an error if code is untested if the -e command line option 91 | // is set 92 | func (t *Tester) Enforce() error { 93 | if !t.setup.Enforce { 94 | return nil 95 | } 96 | untested := make(map[string][]cover.ProfileBlock) 97 | for _, r := range t.Results { 98 | for _, b := range r.Blocks { 99 | if b.Count == 0 { 100 | if len(untested[r.FileName]) > 0 { 101 | // check if the new block is directly after the last one 102 | last := untested[r.FileName][len(untested[r.FileName])-1] 103 | if b.StartLine <= last.EndLine+1 { 104 | last.EndLine = b.EndLine 105 | last.EndCol = b.EndCol 106 | untested[r.FileName][len(untested[r.FileName])-1] = last 107 | continue 108 | } 109 | } 110 | untested[r.FileName] = append(untested[r.FileName], b) 111 | } 112 | } 113 | } 114 | 115 | if len(untested) == 0 { 116 | return nil 117 | } 118 | 119 | var s string 120 | for name, blocks := range untested { 121 | fpath, err := t.setup.Paths.FilePath(name) 122 | if err != nil { 123 | return err 124 | } 125 | by, err := ioutil.ReadFile(fpath) 126 | if err != nil { 127 | return errors.Wrapf(err, "Error reading source file %s", fpath) 128 | } 129 | lines := strings.Split(string(by), "\n") 130 | for _, b := range blocks { 131 | s += fmt.Sprintf("%s:%d-%d:\n", name, b.StartLine, b.EndLine) 132 | undented := undent(lines[b.StartLine-1 : b.EndLine]) 133 | s += strings.Join(undented, "\n") 134 | } 135 | } 136 | return errors.Errorf("Error - untested code:\n%s", s) 137 | 138 | } 139 | 140 | // ProcessExcludes uses the output from the scanner package and removes blocks 141 | // from the merged coverage file. 142 | func (t *Tester) ProcessExcludes(excludes map[string]map[int]bool) error { 143 | var processed []*cover.Profile 144 | 145 | for _, p := range t.Results { 146 | 147 | // Filenames in t.Results are in go package form. We need to convert to 148 | // filepaths before use 149 | fpath, err := t.setup.Paths.FilePath(p.FileName) 150 | if err != nil { 151 | return err 152 | } 153 | 154 | f, ok := excludes[fpath] 155 | if !ok { 156 | // no excludes in this file - add the profile unchanged 157 | processed = append(processed, p) 158 | continue 159 | } 160 | var blocks []cover.ProfileBlock 161 | for _, b := range p.Blocks { 162 | excluded := false 163 | for line := b.StartLine; line <= b.EndLine; line++ { 164 | if ex, ok := f[line]; ok && ex { 165 | excluded = true 166 | break 167 | } 168 | } 169 | if !excluded || b.Count > 0 { 170 | // include blocks that are not excluded 171 | // also include any blocks that have coverage 172 | blocks = append(blocks, b) 173 | } 174 | } 175 | profile := &cover.Profile{ 176 | FileName: p.FileName, 177 | Mode: p.Mode, 178 | Blocks: blocks, 179 | } 180 | processed = append(processed, profile) 181 | } 182 | t.Results = processed 183 | return nil 184 | } 185 | 186 | func (t *Tester) processDir(dir string) error { 187 | 188 | coverfile := filepath.Join( 189 | t.cover, 190 | fmt.Sprintf("%x", md5.Sum([]byte(dir)))+".out", 191 | ) 192 | 193 | files, err := ioutil.ReadDir(dir) 194 | if err != nil { 195 | return errors.Wrapf(err, "Error reading files from %s", dir) 196 | } 197 | 198 | foundTest := false 199 | for _, f := range files { 200 | if strings.HasSuffix(f.Name(), "_test.go") { 201 | foundTest = true 202 | } 203 | } 204 | if !foundTest { 205 | // notest 206 | return nil 207 | } 208 | 209 | combined, stdout, stderr := logger.Log( 210 | t.setup.Verbose, 211 | t.setup.Env.Stdout(), 212 | t.setup.Env.Stderr(), 213 | ) 214 | 215 | var args []string 216 | var pkgs []string 217 | for _, s := range t.setup.Packages { 218 | pkgs = append(pkgs, s.Path) 219 | } 220 | args = append(args, "test") 221 | if t.setup.Short { 222 | // notest 223 | // TODO: add test 224 | args = append(args, "-short") 225 | } 226 | if t.setup.Timeout != "" { 227 | // notest 228 | // TODO: add test 229 | args = append(args, "-timeout", t.setup.Timeout) 230 | } 231 | args = append(args, fmt.Sprintf("-coverpkg=%s", strings.Join(pkgs, ","))) 232 | args = append(args, fmt.Sprintf("-coverprofile=%s", coverfile)) 233 | if t.setup.Verbose { 234 | args = append(args, "-v") 235 | } 236 | if len(t.setup.TestArgs) > 0 { 237 | // notest 238 | args = append(args, t.setup.TestArgs...) 239 | } 240 | if t.setup.Verbose { 241 | fmt.Fprintf( 242 | t.setup.Env.Stdout(), 243 | "Running test: %s\n", 244 | strings.Join(append([]string{"go"}, args...), " "), 245 | ) 246 | } 247 | 248 | exe := exec.Command("go", args...) 249 | exe.Dir = dir 250 | exe.Env = t.setup.Env.Environ() 251 | exe.Stdout = stdout 252 | exe.Stderr = stderr 253 | err = exe.Run() 254 | if strings.Contains(combined.String(), "no buildable Go source files in") { 255 | // notest 256 | return nil 257 | } 258 | if err != nil { 259 | // TODO: Remove when https://github.com/dave/courtney/issues/4 is fixed 260 | // notest 261 | if t.setup.Verbose { 262 | // They will already have seen the output 263 | return errors.Wrap(err, "Error executing test") 264 | } 265 | return errors.Wrapf(err, "Error executing test \nOutput:[\n%s]\n", combined.String()) 266 | } 267 | return t.processCoverageFile(coverfile) 268 | } 269 | 270 | func (t *Tester) processCoverageFile(filename string) error { 271 | profiles, err := cover.ParseProfiles(filename) 272 | if err != nil { 273 | return err 274 | } 275 | for _, p := range profiles { 276 | if t.Results, err = merge.AddProfile(t.Results, p); err != nil { 277 | return err 278 | } 279 | } 280 | return nil 281 | } 282 | 283 | func undent(lines []string) []string { 284 | 285 | indentRegex := regexp.MustCompile("[^\t]") 286 | mindent := -1 287 | 288 | for _, line := range lines { 289 | loc := indentRegex.FindStringIndex(line) 290 | if len(loc) == 0 { 291 | // notest 292 | // string is empty? 293 | continue 294 | } 295 | if mindent == -1 || loc[0] < mindent { 296 | mindent = loc[0] 297 | } 298 | } 299 | 300 | var out []string 301 | for _, line := range lines { 302 | if line == "" { 303 | // notest 304 | out = append(out, "") 305 | } else { 306 | out = append(out, "\t"+line[mindent:]) 307 | } 308 | } 309 | return out 310 | } 311 | -------------------------------------------------------------------------------- /tester/tester_test.go: -------------------------------------------------------------------------------- 1 | package tester_test 2 | 3 | import ( 4 | "bytes" 5 | "fmt" 6 | "io/ioutil" 7 | "path" 8 | "path/filepath" 9 | "reflect" 10 | "regexp" 11 | "strconv" 12 | "strings" 13 | "testing" 14 | 15 | "github.com/dave/courtney/shared" 16 | "github.com/dave/courtney/tester" 17 | "github.com/dave/patsy" 18 | "github.com/dave/patsy/builder" 19 | "github.com/dave/patsy/vos" 20 | "golang.org/x/tools/cover" 21 | ) 22 | 23 | func TestTester_ProcessExcludes(t *testing.T) { 24 | for _, gomod := range []bool{true, false} { 25 | t.Run(fmt.Sprintf("gomod=%v", gomod), func(t *testing.T) { 26 | env := vos.Mock() 27 | b, err := builder.New(env, "ns", gomod) 28 | if err != nil { 29 | t.Fatalf("Error creating builder in %s", err) 30 | } 31 | defer b.Cleanup() 32 | 33 | _, pdir, err := b.Package("a", map[string]string{ 34 | "a.go": `package a`, 35 | }) 36 | if err != nil { 37 | t.Fatalf("Error creating temp package: %s", err) 38 | } 39 | 40 | setup := &shared.Setup{ 41 | Env: env, 42 | Paths: patsy.NewCache(env), 43 | } 44 | ts := tester.New(setup) 45 | ts.Results = []*cover.Profile{ 46 | { 47 | FileName: "ns/a/a.go", 48 | Blocks: []cover.ProfileBlock{ 49 | {Count: 1, StartLine: 1, EndLine: 10}, 50 | {Count: 0, StartLine: 11, EndLine: 20}, 51 | {Count: 1, StartLine: 21, EndLine: 30}, 52 | {Count: 0, StartLine: 31, EndLine: 40}, 53 | }, 54 | }, 55 | } 56 | excludes := map[string]map[int]bool{ 57 | filepath.Join(pdir, "a.go"): { 58 | 25: true, 59 | 35: true, 60 | }, 61 | } 62 | expected := []cover.ProfileBlock{ 63 | {Count: 1, StartLine: 1, EndLine: 10}, 64 | {Count: 0, StartLine: 11, EndLine: 20}, 65 | {Count: 1, StartLine: 21, EndLine: 30}, 66 | } 67 | if err := ts.ProcessExcludes(excludes); err != nil { 68 | t.Fatalf("Processing excludes: %s", err) 69 | } 70 | if !reflect.DeepEqual(ts.Results[0].Blocks, expected) { 71 | t.Fatalf("Processing excludes - got:\n%#v\nexpected:\n%#v\n", ts.Results[0].Blocks, expected) 72 | } 73 | }) 74 | } 75 | } 76 | 77 | func TestTester_Enforce(t *testing.T) { 78 | for _, gomod := range []bool{true, false} { 79 | t.Run(fmt.Sprintf("gomod=%v", gomod), func(t *testing.T) { 80 | env := vos.Mock() 81 | setup := &shared.Setup{ 82 | Env: env, 83 | Paths: patsy.NewCache(env), 84 | Enforce: true, 85 | } 86 | b, err := builder.New(env, "ns", gomod) 87 | if err != nil { 88 | t.Fatalf("Error creating builder: %s", err) 89 | } 90 | defer b.Cleanup() 91 | 92 | _, _, _ = b.Package("a", map[string]string{ 93 | "a.go": "package a\n1\n2\n3\n4\n5\n6\n7\n8\n9\n10\n11\n12\n13\n14\n15\n16\n17\n18\n19\n20", 94 | }) 95 | 96 | ts := tester.New(setup) 97 | ts.Results = []*cover.Profile{ 98 | { 99 | FileName: "ns/a/a.go", 100 | Mode: "b", 101 | Blocks: []cover.ProfileBlock{ 102 | {Count: 1}, 103 | }, 104 | }, 105 | } 106 | if err := ts.Enforce(); err != nil { 107 | t.Fatalf("Error enforcing: %s", err) 108 | } 109 | 110 | ts.Results[0].Blocks = []cover.ProfileBlock{ 111 | {Count: 1, StartLine: 1, EndLine: 2}, 112 | {Count: 0, StartLine: 6, EndLine: 11}, 113 | } 114 | err = ts.Enforce() 115 | if err == nil { 116 | t.Fatal("Error enforcing - should get error, got nil") 117 | } 118 | expected := "Error - untested code:\nns/a/a.go:6-11:\n\t5\n\t6\n\t7\n\t8\n\t9\n\t10" 119 | if err.Error() != expected { 120 | t.Fatalf("Error enforcing - got \n%s\nexpected:\n%s\n", strconv.Quote(err.Error()), strconv.Quote(expected)) 121 | } 122 | 123 | // check that blocks next to each other are merged 124 | ts.Results[0].Blocks = []cover.ProfileBlock{ 125 | {Count: 1, StartLine: 1, EndLine: 2}, 126 | {Count: 0, StartLine: 6, EndLine: 11}, 127 | {Count: 0, StartLine: 12, EndLine: 16}, 128 | {Count: 0, StartLine: 18, EndLine: 21}, 129 | } 130 | err = ts.Enforce() 131 | if err == nil { 132 | t.Fatal("Error enforcing - should get error, got nil") 133 | } 134 | expected = "Error - untested code:\nns/a/a.go:6-16:\n\t5\n\t6\n\t7\n\t8\n\t9\n\t10\n\t11\n\t12\n\t13\n\t14\n\t15ns/a/a.go:18-21:\n\t17\n\t18\n\t19\n\t20" 135 | if err.Error() != expected { 136 | t.Fatalf("Error enforcing - got \n%s\nexpected:\n%s\n", strconv.Quote(err.Error()), strconv.Quote(expected)) 137 | } 138 | }) 139 | } 140 | } 141 | 142 | func TestTester_Save_output(t *testing.T) { 143 | env := vos.Mock() 144 | dir, err := ioutil.TempDir("", "") 145 | if err != nil { 146 | t.Fatalf("Error creating temp dir: %s", err) 147 | } 148 | out := filepath.Join(dir, "foo.bar") 149 | setup := &shared.Setup{ 150 | Env: env, 151 | Paths: patsy.NewCache(env), 152 | Output: out, 153 | } 154 | ts := tester.New(setup) 155 | ts.Results = []*cover.Profile{ 156 | { 157 | FileName: "a", 158 | Mode: "b", 159 | Blocks: []cover.ProfileBlock{{}}, 160 | }, 161 | } 162 | if err := ts.Save(); err != nil { 163 | t.Fatalf("Error saving: %s", err) 164 | } 165 | if _, err := ioutil.ReadFile(out); err != nil { 166 | t.Fatalf("Error loading coverage: %s", err) 167 | } 168 | } 169 | 170 | func TestTester_Save_no_results(t *testing.T) { 171 | env := vos.Mock() 172 | sout := &bytes.Buffer{} 173 | serr := &bytes.Buffer{} 174 | env.Setstdout(sout) 175 | env.Setstderr(serr) 176 | setup := &shared.Setup{ 177 | Env: env, 178 | Paths: patsy.NewCache(env), 179 | } 180 | ts := tester.New(setup) 181 | if err := ts.Save(); err != nil { 182 | t.Fatalf("Error saving: %s", err) 183 | } 184 | expected := "No results\n" 185 | if sout.String() != expected { 186 | t.Fatalf("Error saving, stdout: got:\n%s\nexpected:\n%s\n", sout.String(), expected) 187 | } 188 | } 189 | 190 | func TestTester_Test(t *testing.T) { 191 | 192 | type args []string 193 | type files map[string]string 194 | type packages map[string]files 195 | type test struct { 196 | args args 197 | packages packages 198 | } 199 | 200 | tests := map[string]test{ 201 | "simple": { 202 | args: args{"ns/..."}, 203 | packages: packages{ 204 | "a": files{ 205 | "a.go": `package a 206 | func Foo(i int) int { 207 | i++ // 0 208 | return i 209 | } 210 | `, 211 | "a_test.go": `package a`, 212 | }, 213 | }, 214 | }, 215 | "simple test": { 216 | args: args{"ns/..."}, 217 | packages: packages{ 218 | "a": files{ 219 | "a.go": `package a 220 | 221 | func Foo(i int) int { 222 | i++ // 1 223 | return i 224 | } 225 | 226 | func Bar(i int) int { 227 | i++ // 0 228 | return i 229 | } 230 | `, 231 | "a_test.go": `package a 232 | 233 | import "testing" 234 | 235 | func TestFoo(t *testing.T) { 236 | i := Foo(1) 237 | if i != 2 { 238 | t.Fail() 239 | } 240 | } 241 | `, 242 | }, 243 | }, 244 | }, 245 | "cross package test": { 246 | args: args{"ns/a", "ns/b"}, 247 | packages: packages{ 248 | "a": files{ 249 | "a.go": `package a 250 | 251 | func Foo(i int) int { 252 | i++ // 1 253 | return i 254 | } 255 | 256 | func Bar(i int) int { 257 | i++ // 1 258 | return i 259 | } 260 | `, 261 | "a_test.go": `package a 262 | 263 | import "testing" 264 | 265 | func TestFoo(t *testing.T) { 266 | i := Foo(1) 267 | if i != 2 { 268 | t.Fail() 269 | } 270 | } 271 | `, 272 | }, 273 | "b": files{ 274 | "b_exclude.go": `package b`, 275 | "b_test.go": `package b 276 | 277 | import ( 278 | "testing" 279 | "ns/a" 280 | ) 281 | 282 | func TestBar(t *testing.T) { 283 | i := a.Bar(1) 284 | if i != 2 { 285 | t.Fail() 286 | } 287 | } 288 | `, 289 | }, 290 | }, 291 | }, 292 | } 293 | 294 | for name, test := range tests { 295 | for _, gomod := range []bool{true, false} { 296 | t.Run(fmt.Sprintf("%s,gomod=%v", name, gomod), func(t *testing.T) { 297 | env := vos.Mock() 298 | b, err := builder.New(env, "ns", gomod) 299 | if err != nil { 300 | t.Fatalf("Error creating builder in %s: %+v", name, err) 301 | } 302 | defer b.Cleanup() 303 | 304 | for pname, files := range test.packages { 305 | if _, _, err := b.Package(pname, files); err != nil { 306 | t.Fatalf("Error creating package %s in %s: %+v", pname, name, err) 307 | } 308 | } 309 | 310 | paths := patsy.NewCache(env) 311 | 312 | setup := &shared.Setup{ 313 | Env: env, 314 | Paths: paths, 315 | } 316 | if err := setup.Parse(test.args); err != nil { 317 | t.Fatalf("Error in '%s' parsing args: %+v", name, err) 318 | } 319 | 320 | ts := tester.New(setup) 321 | 322 | if err := ts.Test(); err != nil { 323 | t.Fatalf("Error in '%s' while running test: %+v", name, err) 324 | } 325 | 326 | fmt.Printf("Results: %#v\n", ts.Results) 327 | 328 | filesInOutput := map[string]bool{} 329 | for _, p := range ts.Results { 330 | 331 | filesInOutput[p.FileName] = true 332 | pkg, fname := path.Split(p.FileName) 333 | pkg = strings.TrimSuffix(pkg, "/") 334 | dir, err := patsy.Dir(env, pkg) 335 | if err != nil { 336 | t.Fatalf("Error in '%s' while getting dir from package: %+v", name, err) 337 | } 338 | src, err := ioutil.ReadFile(filepath.Join(dir, fname)) 339 | if err != nil { 340 | t.Fatalf("Error in '%s' while opening coverage: %+v", name, err) 341 | } 342 | lines := strings.Split(string(src), "\n") 343 | matched := map[int]bool{} 344 | for _, b := range p.Blocks { 345 | if !strings.HasSuffix(lines[b.StartLine], fmt.Sprintf("// %d", b.Count)) { 346 | t.Fatalf("Error in '%s' - incorrect count %d at %s line %d", name, b.Count, p.FileName, b.StartLine) 347 | } 348 | matched[b.StartLine] = true 349 | } 350 | for i, line := range lines { 351 | if annotatedLine.MatchString(line) { 352 | if _, ok := matched[i]; !ok { 353 | t.Fatalf("Error in '%s' - annotated line doesn't match a coverage block as %s line %d", name, p.FileName, i) 354 | } 355 | } 356 | } 357 | } 358 | fmt.Printf("%#v\n", filesInOutput) 359 | for pname, files := range test.packages { 360 | for fname := range files { 361 | if strings.HasSuffix(fname, ".mod") { 362 | continue 363 | } 364 | if strings.HasSuffix(fname, "_test.go") { 365 | continue 366 | } 367 | if strings.HasSuffix(fname, "_exclude.go") { 368 | // so we can have simple source files with no logic 369 | // blocks 370 | continue 371 | } 372 | fullFilename := path.Join("ns", pname, fname) 373 | fmt.Println(fullFilename) 374 | if _, ok := filesInOutput[fullFilename]; !ok { 375 | t.Fatalf("Error in '%s' - %s does not appear in coverge output", name, fullFilename) 376 | } 377 | } 378 | } 379 | }) 380 | } 381 | } 382 | } 383 | 384 | var annotatedLine = regexp.MustCompile(`// \d+$`) 385 | --------------------------------------------------------------------------------