├── .github └── workflows │ ├── go.yml │ └── release.yml ├── .goreleaser.yml ├── LICENSE ├── Makefile ├── README.md ├── ast.go ├── ast_test.go ├── doc.go ├── go-carpet.1 ├── go-carpet.go ├── go-carpet_test.go ├── go.mod ├── go.sum ├── mod.go ├── mod_test.go ├── terminal_posix.go ├── terminal_windows.go ├── testdata ├── Godeps │ └── empty_test.go ├── colored_00.txt ├── colored_01.txt ├── colored_02.txt ├── colored_03.txt ├── cover_00.out ├── cover_01.out ├── cover_02.out ├── empty_test.go ├── file_00.golang ├── file_01.golang ├── src │ └── file.golang ├── testdata │ └── empty_test.go └── vendor │ ├── dir │ └── empty_test.go │ └── empty_test.go ├── unix_only_test.go ├── utils.go └── utils_test.go /.github/workflows/go.yml: -------------------------------------------------------------------------------- 1 | name: Go 2 | 3 | on: 4 | push: 5 | branches: [ master ] 6 | pull_request: 7 | branches: [ master ] 8 | schedule: 9 | - cron: '0 12 * * 0' 10 | 11 | jobs: 12 | 13 | build: 14 | runs-on: ubuntu-latest 15 | strategy: 16 | matrix: 17 | go: ['1.22.x', '1.23.x'] 18 | steps: 19 | - uses: actions/checkout@v4 20 | 21 | - name: Set up Go 22 | uses: actions/setup-go@v5 23 | with: 24 | go-version: ${{ matrix.go }} 25 | 26 | - name: Install errcheck 27 | run: go install github.com/kisielk/errcheck@latest 28 | 29 | - name: errcheck 30 | run: errcheck -verbose ./... 31 | 32 | - name: gofmt check 33 | run: diff <(gofmt -d .) <(echo -n "") 34 | 35 | - name: Test 36 | run: go test -race -v ./... 37 | 38 | - name: Coveralls 39 | if: ${{ startsWith(matrix.go, '1.23') && github.event_name == 'push' }} 40 | env: 41 | COVERALLS_TOKEN: ${{ secrets.GITHUB_TOKEN }} 42 | run: | 43 | go install github.com/mattn/goveralls@latest && \ 44 | go test -covermode=count -coverprofile=profile.cov ./... && \ 45 | goveralls -coverprofile=profile.cov -service=github 46 | -------------------------------------------------------------------------------- /.github/workflows/release.yml: -------------------------------------------------------------------------------- 1 | name: goreleaser 2 | 3 | on: 4 | push: 5 | tags: 6 | - 'v*' 7 | 8 | permissions: 9 | contents: write 10 | 11 | jobs: 12 | goreleaser: 13 | runs-on: ubuntu-latest 14 | steps: 15 | - name: Checkout 16 | uses: actions/checkout@v4 17 | with: 18 | fetch-depth: 0 19 | - name: Set up Go 20 | uses: actions/setup-go@v5 21 | with: 22 | go-version: 1.x 23 | - name: Run GoReleaser 24 | uses: goreleaser/goreleaser-action@v5 25 | with: 26 | distribution: goreleaser 27 | version: latest 28 | args: release --rm-dist 29 | env: 30 | GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} 31 | - name: Post release 32 | run: ls -l ./dist/* 33 | -------------------------------------------------------------------------------- /.goreleaser.yml: -------------------------------------------------------------------------------- 1 | release: 2 | name_template: "{{ .Version }} - {{ .Date }}" 3 | draft: true 4 | header: | 5 | [![Github Releases ({{ .Tag }})](https://img.shields.io/github/downloads/msoap/go-carpet/{{ .Tag }}/total.svg)](https://github.com/msoap/go-carpet/releases/latest) [![Github All Releases](https://img.shields.io/github/downloads/msoap/go-carpet/total.svg)](https://github.com/msoap/go-carpet/releases) 6 | 7 | builds: 8 | - env: 9 | - CGO_ENABLED=0 10 | goos: 11 | - linux 12 | - darwin 13 | - windows 14 | goarch: 15 | - 386 16 | - amd64 17 | - arm 18 | - arm64 19 | ignore: 20 | - goos: windows 21 | goarch: arm 22 | flags: 23 | - -trimpath 24 | ldflags: 25 | - -s -w -X main.version={{ .Version }} 26 | 27 | nfpms: 28 | - 29 | homepage: https://github.com/msoap/{{ .ProjectName }} 30 | description: Show test coverage for Go source files. 31 | license: MIT 32 | formats: 33 | - deb 34 | - rpm 35 | bindir: /usr/bin 36 | contents: 37 | - src: go-carpet.1 38 | dst: /usr/share/man/man1/go-carpet.1 39 | - src: LICENSE 40 | dst: /usr/share/doc/go-carpet/copyright 41 | - src: README.md 42 | dst: /usr/share/doc/go-carpet/README.md 43 | 44 | archives: 45 | - 46 | format_overrides: 47 | - goos: windows 48 | format: zip 49 | files: 50 | - README* 51 | - LICENSE* 52 | - "*.1" 53 | 54 | checksum: 55 | name_template: 'checksums.txt' 56 | 57 | snapshot: 58 | name_template: "{{ .Tag }}" 59 | 60 | changelog: 61 | sort: desc 62 | filters: 63 | exclude: 64 | - '^docs:' 65 | - '^test:' 66 | - '^Merge branch' 67 | - '^go fmt' 68 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | The MIT License (MIT) 2 | 3 | Copyright (c) 2016 Serhii Mudryk 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 | 23 | -------------------------------------------------------------------------------- /Makefile: -------------------------------------------------------------------------------- 1 | APP_NAME := go-carpet 2 | APP_DESCRIPTION := $$(awk 'NR == 10, NR == 16' README.md) 3 | APP_URL := https://github.com/msoap/$(APP_NAME) 4 | APP_MAINTAINER := $$(git show HEAD | awk '$$1 == "Author:" {print $$2 " " $$3 " " $$4}') 5 | GIT_TAG := $$(git describe --tags --abbrev=0) 6 | 7 | test: 8 | go test -v -cover -race ./... 9 | 10 | lint: 11 | golint ./... 12 | go vet ./... 13 | errcheck ./... 14 | 15 | run: 16 | go run . -256colors 17 | 18 | update-from-github: 19 | go get -u github.com/msoap/$(APP_NAME) 20 | 21 | gometalinter: 22 | gometalinter --vendor --cyclo-over=20 --line-length=150 --dupl-threshold=150 --min-occurrences=2 --enable=misspell --deadline=10m --exclude=SA1022 ./... 23 | 24 | generate-manpage: 25 | cat README.md | grep -v "^\[" | grep -v Screenshot > $(APP_NAME).md 26 | docker run --rm -v $$PWD:/app -w /app msoap/ruby-ronn ronn $(APP_NAME).md 27 | mv ./$(APP_NAME) ./$(APP_NAME).1 28 | rm ./$(APP_NAME).{md,html} 29 | 30 | create-debian-amd64-package: 31 | GOOS=linux GOARCH=amd64 go build -ldflags="-w -s" -o $(APP_NAME) 32 | docker run --rm -v $$PWD:/app -w /app msoap/ruby-fpm \ 33 | fpm -s dir -t deb --force --name $(APP_NAME) -v $(GIT_TAG) \ 34 | --license="$$(head -1 LICENSE)" \ 35 | --url=$(APP_URL) \ 36 | --description="$(APP_DESCRIPTION)" \ 37 | --maintainer="$(APP_MAINTAINER)" \ 38 | --category=network \ 39 | ./$(APP_NAME)=/usr/bin/ \ 40 | ./$(APP_NAME).1=/usr/share/man/man1/ \ 41 | LICENSE=/usr/share/doc/$(APP_NAME)/copyright \ 42 | README.md=/usr/share/doc/$(APP_NAME)/ 43 | rm $(APP_NAME) 44 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | go-carpet - show test coverage for Go source files 2 | ================================================== 3 | 4 | [![Go Reference](https://pkg.go.dev/badge/github.com/msoap/go-carpet.svg)](https://pkg.go.dev/github.com/msoap/go-carpet) 5 | [![Go](https://github.com/msoap/go-carpet/actions/workflows/go.yml/badge.svg)](https://github.com/msoap/go-carpet/actions/workflows/go.yml) 6 | [![Coverage Status](https://coveralls.io/repos/github/msoap/go-carpet/badge.svg?branch=master)](https://coveralls.io/github/msoap/go-carpet?branch=master) 7 | [![Report Card](https://goreportcard.com/badge/github.com/msoap/go-carpet)](https://goreportcard.com/report/github.com/msoap/go-carpet) 8 | [![Homebrew formula exists](https://img.shields.io/badge/homebrew-🍺-d7af72.svg)](https://github.com/msoap/go-carpet#install) 9 | 10 | To view the test coverage in the terminal, just run `go-carpet`. 11 | 12 | It works outside of the `GOPATH` directory. And it works recursively for multiple packages. 13 | 14 | With `-256colors` option, shades of green indicate the level of coverage. 15 | 16 | By default skip vendor directories (Godeps,vendor), otherwise use `-include-vendor` option. 17 | 18 | The `-mincov` option allows you to specify a coverage threshold to limit the files to be displayed. 19 | 20 | Usage 21 | ----- 22 | 23 | usage: go-carpet [options] [paths] 24 | -256colors 25 | use more colors on 256-color terminal (indicate the level of coverage) 26 | -args string 27 | pass additional arguments for go test 28 | -file string 29 | comma-separated list of files to test (default: all) 30 | -func string 31 | comma-separated functions list (default: all functions) 32 | -include-vendor 33 | include vendor directories for show coverage (Godeps, vendor) 34 | -mincov float 35 | coverage threshold of the file to be displayed (in percent) (default 100) 36 | -summary 37 | only show summary for each file 38 | -version 39 | get version 40 | 41 | For view coverage in less, use `-R` option: 42 | 43 | go-carpet | less -R 44 | 45 | Install 46 | ------- 47 | 48 | From source: 49 | 50 | go install github.com/msoap/go-carpet@latest 51 | 52 | Download binaries from: [releases](https://github.com/msoap/go-carpet/releases) (OS X/Linux/Windows) 53 | 54 | Install from homebrew (OS X): 55 | 56 | brew tap msoap/tools 57 | brew install go-carpet 58 | # update: 59 | brew upgrade go-carpet 60 | 61 | ### Screenshot 62 | 63 | screen shot 2016-03-06 64 | 65 | See also 66 | -------- 67 | 68 | * [blog.golang.org](https://blog.golang.org/cover) - the cover story 69 | * [gocover.io](https://gocover.io) - simple Go test coverage service 70 | * [coveralls.io](https://coveralls.io) - test coverage service 71 | * [package cover](https://godoc.org/golang.org/x/tools/cover) - golang.org/x/tools/cover 72 | * [gotests](https://github.com/cweill/gotests) - Go commandline tool that generates table driven tests 73 | * [docker-golang-checks](https://github.com/msoap/docker-golang-checks) - Go-code checks Docker image 74 | -------------------------------------------------------------------------------- /ast.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "go/ast" 5 | "go/parser" 6 | "go/token" 7 | ) 8 | 9 | // Func - one go function in source 10 | type Func struct { 11 | Name string 12 | Begin, End int 13 | } 14 | 15 | // getGolangFuncs - parse golang source file and get all functions 16 | // 17 | // funcs, err := getGolangFuncs(goFileContentInBytes) 18 | func getGolangFuncs(fileContent []byte) (result []Func, err error) { 19 | fset := token.NewFileSet() 20 | astFile, err := parser.ParseFile(fset, "", fileContent, 0) 21 | if err != nil { 22 | return result, err 23 | } 24 | 25 | ast.Inspect(astFile, func(nodeRaw ast.Node) bool { 26 | switch node := nodeRaw.(type) { 27 | case *ast.FuncDecl: 28 | result = append(result, Func{ 29 | Name: node.Name.String(), 30 | Begin: int(node.Pos()), 31 | End: int(node.End()), 32 | }) 33 | } 34 | 35 | return true 36 | }) 37 | 38 | return result, nil 39 | } 40 | -------------------------------------------------------------------------------- /ast_test.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "reflect" 5 | "testing" 6 | ) 7 | 8 | const testGolangSrc = `package somepkg 9 | 10 | import "fmt" 11 | 12 | type T int 13 | 14 | func (r T) String() string { 15 | return fmt.Sprintf("%v", r) 16 | } 17 | 18 | func fn() string { 19 | return "Hello" 20 | } 21 | ` 22 | 23 | func Test_getGolangFuncs(t *testing.T) { 24 | tests := []struct { 25 | name string 26 | fileContent []byte 27 | wantResult []Func 28 | wantErr bool 29 | }{ 30 | { 31 | name: "without error", 32 | fileContent: []byte(testGolangSrc), 33 | wantResult: []Func{ 34 | {Name: "String", Begin: 44, End: 103}, 35 | {Name: "fn", Begin: 105, End: 141}, 36 | }, 37 | wantErr: false, 38 | }, 39 | { 40 | name: "with error", 41 | fileContent: []byte("..."), 42 | wantResult: nil, 43 | wantErr: true, 44 | }, 45 | } 46 | 47 | for _, tt := range tests { 48 | t.Run(tt.name, func(t *testing.T) { 49 | gotResult, err := getGolangFuncs(tt.fileContent) 50 | if (err != nil) != tt.wantErr { 51 | t.Errorf("getGolangFuncs() error = %v, wantErr %v", err, tt.wantErr) 52 | return 53 | } 54 | if !reflect.DeepEqual(gotResult, tt.wantResult) { 55 | t.Errorf("getGolangFuncs() = %#v, want %#v", gotResult, tt.wantResult) 56 | } 57 | }) 58 | } 59 | } 60 | -------------------------------------------------------------------------------- /doc.go: -------------------------------------------------------------------------------- 1 | /* 2 | go-carpet - show test coverage for Go source files 3 | 4 | It works not only in the directory GOPATH. And it works recursively for multiple packages. 5 | With -256colors option, shades of green indicate the level of coverage. 6 | 7 | Install/update: 8 | 9 | go get -u github.com/msoap/go-carpet 10 | ln -s $GOPATH/bin/go-carpet ~/bin/go-carpet 11 | 12 | Usage: 13 | 14 | go-carpet [-options] [paths] 15 | options: 16 | -256colors - use more colors on 256-color terminal (indicate the level of coverage) 17 | -args - pass additional arguments for go test (for example "-short" or "-i -timeout t") 18 | -file string - comma-separated list of files to test (default: all) 19 | -func string - comma-separated functions list (default: all functions) 20 | -include-vendor - include vendor directories for show coverage (Godeps, vendor) 21 | -summary - only show summary for each file 22 | -version - get version 23 | 24 | Source: https://github.com/msoap/go-carpet 25 | */ 26 | package main 27 | -------------------------------------------------------------------------------- /go-carpet.1: -------------------------------------------------------------------------------- 1 | .\" generated with Ronn/v0.7.3 2 | .\" http://github.com/rtomayko/ronn/tree/0.7.3 3 | . 4 | .TH "GO\-CARPET" "" "January 2022" "" "" 5 | . 6 | .SH "NAME" 7 | \fBgo\-carpet\fR \- show test coverage for Go source files 8 | . 9 | .P 10 | To view the test coverage in the terminal, just run \fBgo\-carpet\fR\. 11 | . 12 | .P 13 | It works outside of the \fBGOPATH\fR directory\. And it works recursively for multiple packages\. 14 | . 15 | .P 16 | With \fB\-256colors\fR option, shades of green indicate the level of coverage\. 17 | . 18 | .P 19 | By default skip vendor directories (Godeps,vendor), otherwise use \fB\-include\-vendor\fR option\. 20 | . 21 | .SH "Usage" 22 | . 23 | .nf 24 | 25 | usage: go\-carpet [options] [paths] 26 | \-256colors 27 | use more colors on 256\-color terminal (indicate the level of coverage) 28 | \-args string 29 | pass additional arguments for go test 30 | \-file string 31 | comma\-separated list of files to test (default: all) 32 | \-func string 33 | comma\-separated functions list (default: all functions) 34 | \-include\-vendor 35 | include vendor directories for show coverage (Godeps, vendor) 36 | \-summary 37 | only show summary for each file 38 | \-version 39 | get version 40 | . 41 | .fi 42 | . 43 | .P 44 | For view coverage in less, use \fB\-R\fR option: 45 | . 46 | .IP "" 4 47 | . 48 | .nf 49 | 50 | go\-carpet | less \-R 51 | . 52 | .fi 53 | . 54 | .IP "" 0 55 | . 56 | .SH "Install" 57 | From source: 58 | . 59 | .IP "" 4 60 | . 61 | .nf 62 | 63 | go install github\.com/msoap/go\-carpet@latest 64 | . 65 | .fi 66 | . 67 | .IP "" 0 68 | . 69 | .P 70 | Download binaries from: releases \fIhttps://github\.com/msoap/go\-carpet/releases\fR (OS X/Linux/Windows) 71 | . 72 | .P 73 | Install from homebrew (OS X): 74 | . 75 | .IP "" 4 76 | . 77 | .nf 78 | 79 | brew tap msoap/tools 80 | brew install go\-carpet 81 | # update: 82 | brew upgrade go\-carpet 83 | . 84 | .fi 85 | . 86 | .IP "" 0 87 | . 88 | .P 89 | . 90 | .SH "See also" 91 | . 92 | .IP "\(bu" 4 93 | blog\.golang\.org \fIhttps://blog\.golang\.org/cover\fR \- the cover story 94 | . 95 | .IP "\(bu" 4 96 | gocover\.io \fIhttps://gocover\.io\fR \- simple Go test coverage service 97 | . 98 | .IP "\(bu" 4 99 | coveralls\.io \fIhttps://coveralls\.io\fR \- test coverage service 100 | . 101 | .IP "\(bu" 4 102 | package cover \fIhttps://godoc\.org/golang\.org/x/tools/cover\fR \- golang\.org/x/tools/cover 103 | . 104 | .IP "\(bu" 4 105 | gotests \fIhttps://github\.com/cweill/gotests\fR \- Go commandline tool that generates table driven tests 106 | . 107 | .IP "\(bu" 4 108 | docker\-golang\-checks \fIhttps://github\.com/msoap/docker\-golang\-checks\fR \- Go\-code checks Docker image 109 | . 110 | .IP "" 0 111 | 112 | -------------------------------------------------------------------------------- /go-carpet.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "flag" 5 | "fmt" 6 | "go/build" 7 | "io" 8 | "log" 9 | "os" 10 | "os/exec" 11 | "path/filepath" 12 | "regexp" 13 | "runtime" 14 | "strings" 15 | 16 | "github.com/mgutz/ansi" 17 | "golang.org/x/tools/cover" 18 | ) 19 | 20 | const ( 21 | usageMessage = `go-carpet - show test coverage for Go source files 22 | 23 | usage: go-carpet [options] [paths]` 24 | 25 | version = "1.9.0" 26 | 27 | // predefined go test options 28 | goTestCoverProfile = "-coverprofile" 29 | goTestCoverMode = "-covermode" 30 | ) 31 | 32 | var ( 33 | reNewLine = regexp.MustCompile("\n") 34 | reWindowsPathFix = regexp.MustCompile(`^_\\([A-Z])_`) 35 | 36 | // vendors directories for skip 37 | vendorDirs = []string{"Godeps", "vendor", ".vendor", "_vendor"} 38 | 39 | // directories for skip 40 | skipDirs = []string{"testdata"} 41 | 42 | errIsNotInGoMod = fmt.Errorf("is not in go modules") 43 | ) 44 | 45 | func getDirsWithTests(includeVendor bool, roots ...string) (result []string, err error) { 46 | if len(roots) == 0 { 47 | roots = []string{"."} 48 | } 49 | 50 | dirs := map[string]struct{}{} 51 | for _, root := range roots { 52 | err = filepath.Walk(root, func(path string, _ os.FileInfo, _ error) error { 53 | if strings.HasSuffix(path, "_test.go") { 54 | dirs[filepath.Dir(path)] = struct{}{} 55 | } 56 | return nil 57 | }) 58 | if err != nil { 59 | return result, err 60 | } 61 | } 62 | 63 | result = make([]string, 0, len(dirs)) 64 | for dir := range dirs { 65 | if !includeVendor && isSliceInStringPrefix(dir, vendorDirs) || isSliceInStringPrefix(dir, skipDirs) { 66 | continue 67 | } 68 | result = append(result, "./"+dir) 69 | } 70 | 71 | return result, nil 72 | } 73 | 74 | func readFile(fileName string) (result []byte, err error) { 75 | fileReader, err := os.Open(fileName) 76 | if err != nil { 77 | return result, err 78 | } 79 | 80 | result, err = io.ReadAll(fileReader) 81 | if err == nil { 82 | err = fileReader.Close() 83 | } 84 | 85 | return result, err 86 | } 87 | 88 | func getShadeOfGreen(normCover float64) string { 89 | /* 90 | Get all colors for 255-colors terminal: 91 | gommand 'for i := 0; i < 256; i++ {fmt.Println(i, ansi.ColorCode(strconv.Itoa(i)) + "String" + ansi.ColorCode("reset"))}' 92 | */ 93 | var tenShadesOfGreen = [...]string{ 94 | "29", 95 | "30", 96 | "34", 97 | "36", 98 | "40", 99 | "42", 100 | "46", 101 | "48", 102 | "50", 103 | "51", 104 | } 105 | if normCover < 0 { 106 | normCover = 0 107 | } 108 | if normCover > 1 { 109 | normCover = 1 110 | } 111 | index := int((normCover - 0.00001) * float64(len(tenShadesOfGreen))) 112 | return tenShadesOfGreen[index] 113 | } 114 | 115 | func runGoTest(path string, coverFileName string, goTestArgs []string, hideStderr bool) error { 116 | args := []string{"test", goTestCoverProfile + "=" + coverFileName, goTestCoverMode + "=count"} 117 | args = append(args, goTestArgs...) 118 | args = append(args, path) 119 | osExec := exec.Command("go", args...) // #nosec 120 | if !hideStderr { 121 | osExec.Stderr = os.Stderr 122 | } 123 | 124 | if output, err := osExec.Output(); err != nil { 125 | fmt.Print(string(output)) 126 | return err 127 | } 128 | 129 | return nil 130 | } 131 | 132 | func guessAbsPathInGOPATH(GOPATH, relPath string) (absPath string, err error) { 133 | if GOPATH == "" { 134 | GOPATH = build.Default.GOPATH 135 | if GOPATH == "" { 136 | return "", fmt.Errorf("GOPATH is not set") 137 | } 138 | } 139 | 140 | gopathChunks := strings.Split(GOPATH, string(os.PathListSeparator)) 141 | for _, gopathChunk := range gopathChunks { 142 | guessAbsPath := filepath.Join(gopathChunk, "src", relPath) 143 | if _, err = os.Stat(guessAbsPath); err == nil { 144 | absPath = guessAbsPath 145 | break 146 | } 147 | } 148 | 149 | if absPath == "" { 150 | return "", fmt.Errorf("file '%s' not found in GOPATH", relPath) 151 | } 152 | 153 | return absPath, err 154 | } 155 | 156 | func getCoverForDir(coverFileName string, filesFilter []string, config Config) (result []byte, profileBlocks []cover.ProfileBlock, err error) { 157 | coverProfile, err := cover.ParseProfiles(coverFileName) 158 | if err != nil { 159 | return result, profileBlocks, err 160 | } 161 | 162 | for _, fileProfile := range coverProfile { 163 | // Skip files if minimal coverage is set and is covered more than minimal coverage 164 | if config.minCoverage > 0 && config.minCoverage < 100.0 && getStatForProfileBlocks(fileProfile.Blocks) > config.minCoverage { 165 | continue 166 | } 167 | 168 | var fileName string 169 | if strings.HasPrefix(fileProfile.FileName, "/") { 170 | // TODO: what about windows? 171 | fileName = fileProfile.FileName 172 | } else if strings.HasPrefix(fileProfile.FileName, "_") { 173 | // absolute path (or relative in tests) 174 | if runtime.GOOS != "windows" { 175 | fileName = strings.TrimLeft(fileProfile.FileName, "_") 176 | } else { 177 | // "_\C_\Users\..." -> "C:\Users\..." 178 | fileName = reWindowsPathFix.ReplaceAllString(fileProfile.FileName, "$1:") 179 | } 180 | } else if fileName, err = guessAbsPathInGoMod(fileProfile.FileName); err != errIsNotInGoMod { 181 | if err != nil { 182 | return result, profileBlocks, err 183 | } 184 | } else { 185 | // file in one dir in GOPATH 186 | fileName, err = guessAbsPathInGOPATH(os.Getenv("GOPATH"), fileProfile.FileName) 187 | if err != nil { 188 | return result, profileBlocks, err 189 | } 190 | } 191 | 192 | if len(filesFilter) > 0 && !isSliceInString(fileName, filesFilter) { 193 | continue 194 | } 195 | 196 | var fileBytes []byte 197 | fileBytes, err = readFile(fileName) 198 | if err != nil { 199 | return result, profileBlocks, err 200 | } 201 | 202 | result = append(result, getCoverForFile(fileProfile, fileBytes, config)...) 203 | profileBlocks = append(profileBlocks, fileProfile.Blocks...) 204 | } 205 | 206 | return result, profileBlocks, err 207 | } 208 | 209 | func getColorHeader(header string, addUnderiline bool) string { 210 | result := ansi.ColorCode("yellow") + 211 | header + ansi.ColorCode("reset") + "\n" 212 | 213 | if addUnderiline { 214 | result += ansi.ColorCode("black+h") + 215 | strings.Repeat("~", len(header)) + 216 | ansi.ColorCode("reset") + "\n" 217 | } 218 | 219 | return result 220 | } 221 | 222 | // algorithms from Go-sources: 223 | // 224 | // src/cmd/cover/html.go::percentCovered() 225 | // src/testing/cover.go::coverReport() 226 | func getStatForProfileBlocks(fileProfileBlocks []cover.ProfileBlock) (stat float64) { 227 | var total, covered int64 228 | for _, profileBlock := range fileProfileBlocks { 229 | total += int64(profileBlock.NumStmt) 230 | if profileBlock.Count > 0 { 231 | covered += int64(profileBlock.NumStmt) 232 | } 233 | } 234 | if total > 0 { 235 | stat = float64(covered) / float64(total) * 100.0 236 | } 237 | 238 | return stat 239 | } 240 | 241 | func getCoverForFile(fileProfile *cover.Profile, fileBytes []byte, config Config) (result []byte) { 242 | stat := getStatForProfileBlocks(fileProfile.Blocks) 243 | 244 | textRanges, err := getFileFuncRanges(fileBytes, config.funcFilter) 245 | if err != nil { 246 | return result 247 | } 248 | 249 | var fileNameDisplay string 250 | if len(config.funcFilter) == 0 { 251 | fileNameDisplay = fmt.Sprintf("%s - %.1f%%", strings.TrimLeft(fileProfile.FileName, "_"), stat) 252 | } else { 253 | fileNameDisplay = strings.TrimLeft(fileProfile.FileName, "_") 254 | } 255 | 256 | if config.summary { 257 | return []byte(fileNameDisplay + "\n") 258 | } 259 | 260 | result = append(result, []byte(getColorHeader(fileNameDisplay, true))...) 261 | 262 | boundaries := fileProfile.Boundaries(fileBytes) 263 | 264 | for _, textRange := range textRanges { 265 | fileBytesPart := fileBytes[textRange.begin:textRange.end] 266 | curOffset := 0 267 | coverColor := "" 268 | 269 | for _, boundary := range boundaries { 270 | if boundary.Offset < textRange.begin || boundary.Offset > textRange.end { 271 | // skip boundary which is not in filter function 272 | continue 273 | } 274 | 275 | boundaryOffset := boundary.Offset - textRange.begin 276 | 277 | if boundaryOffset > curOffset { 278 | nextChunk := fileBytesPart[curOffset:boundaryOffset] 279 | // Add ansi color code in begin of each line (this fixed view in "less -R") 280 | if coverColor != "" && coverColor != ansi.ColorCode("reset") { 281 | nextChunk = reNewLine.ReplaceAllLiteral(nextChunk, []byte(ansi.ColorCode("reset")+"\n"+coverColor)) 282 | } 283 | result = append(result, nextChunk...) 284 | } 285 | 286 | switch { 287 | case boundary.Start && boundary.Count > 0: 288 | coverColor = ansi.ColorCode("green") 289 | if config.colors256 { 290 | coverColor = ansi.ColorCode(getShadeOfGreen(boundary.Norm)) 291 | } 292 | case boundary.Start && boundary.Count == 0: 293 | coverColor = ansi.ColorCode("red") 294 | case !boundary.Start: 295 | coverColor = ansi.ColorCode("reset") 296 | } 297 | result = append(result, []byte(coverColor)...) 298 | 299 | curOffset = boundaryOffset 300 | } 301 | if curOffset < len(fileBytesPart) { 302 | result = append(result, fileBytesPart[curOffset:]...) 303 | } 304 | 305 | result = append(result, []byte("\n")...) 306 | } 307 | 308 | return result 309 | } 310 | 311 | type textRange struct { 312 | begin, end int 313 | } 314 | 315 | func getFileFuncRanges(fileBytes []byte, funcs []string) (result []textRange, err error) { 316 | if len(funcs) == 0 { 317 | return []textRange{{ 318 | begin: 0, 319 | end: len(fileBytes), 320 | }}, nil 321 | } 322 | 323 | golangFuncs, err := getGolangFuncs(fileBytes) 324 | if err != nil { 325 | return nil, err 326 | } 327 | 328 | for _, existsFunc := range golangFuncs { 329 | for _, filterFuncName := range funcs { 330 | if existsFunc.Name == filterFuncName { 331 | result = append(result, textRange{begin: existsFunc.Begin - 1, end: existsFunc.End - 1}) 332 | } 333 | } 334 | } 335 | 336 | if len(result) == 0 { 337 | return nil, fmt.Errorf("filter by functions: %v - not found", funcs) 338 | } 339 | 340 | return result, nil 341 | } 342 | 343 | func getTempFileName() (string, error) { 344 | tmpFile, err := os.CreateTemp(".", "go-carpet-coverage-out-") 345 | if err != nil { 346 | return "", err 347 | } 348 | err = tmpFile.Close() 349 | if err != nil { 350 | return "", err 351 | } 352 | 353 | return tmpFile.Name(), nil 354 | } 355 | 356 | // Config - application config 357 | type Config struct { 358 | filesFilterRaw string 359 | filesFilter []string 360 | funcFilterRaw string 361 | funcFilter []string 362 | argsRaw string 363 | minCoverage float64 364 | colors256 bool 365 | includeVendor bool 366 | summary bool 367 | } 368 | 369 | var config Config 370 | 371 | func init() { 372 | flag.StringVar(&config.filesFilterRaw, "file", "", "comma-separated list of `files` to test (default: all)") 373 | flag.StringVar(&config.funcFilterRaw, "func", "", "comma-separated `functions` list (default: all functions)") 374 | flag.BoolVar(&config.colors256, "256colors", false, "use more colors on 256-color terminal (indicate the level of coverage)") 375 | flag.BoolVar(&config.summary, "summary", false, "only show summary for each file") 376 | flag.BoolVar(&config.includeVendor, "include-vendor", false, "include vendor directories for show coverage (Godeps, vendor)") 377 | flag.StringVar(&config.argsRaw, "args", "", "pass additional `arguments` for go test") 378 | flag.Float64Var(&config.minCoverage, "mincov", 100.0, "coverage threshold of the file to be displayed (in percent)") 379 | flag.Usage = func() { 380 | fmt.Println(usageMessage) 381 | flag.PrintDefaults() 382 | os.Exit(0) 383 | } 384 | } 385 | 386 | func main() { 387 | versionFl := flag.Bool("version", false, "get version") 388 | flag.Parse() 389 | 390 | if *versionFl { 391 | fmt.Println(version) 392 | os.Exit(0) 393 | } 394 | 395 | config.filesFilter = grepEmptyStringSlice(strings.Split(config.filesFilterRaw, ",")) 396 | config.funcFilter = grepEmptyStringSlice(strings.Split(config.funcFilterRaw, ",")) 397 | additionalArgs, err := parseAdditionalArgs(config.argsRaw, []string{goTestCoverProfile, goTestCoverMode}) 398 | if err != nil { 399 | log.Fatal(err) 400 | } 401 | 402 | testDirs := flag.Args() 403 | 404 | coverFileName, err := getTempFileName() 405 | if err != nil { 406 | log.Fatal(err) 407 | } 408 | defer func() { 409 | err = os.RemoveAll(coverFileName) 410 | if err != nil { 411 | log.Fatal(err) 412 | } 413 | }() 414 | 415 | stdOut := getColorWriter() 416 | allProfileBlocks := []cover.ProfileBlock{} 417 | 418 | if len(testDirs) > 0 { 419 | testDirs, err = getDirsWithTests(config.includeVendor, testDirs...) 420 | } else { 421 | testDirs, err = getDirsWithTests(config.includeVendor, ".") 422 | } 423 | if err != nil { 424 | log.Fatal(err) 425 | } 426 | 427 | for _, path := range testDirs { 428 | if err = runGoTest(path, coverFileName, additionalArgs, false); err != nil { 429 | log.Print(err) 430 | continue 431 | } 432 | 433 | coverInBytes, profileBlocks, errCover := getCoverForDir(coverFileName, config.filesFilter, config) 434 | if errCover != nil { 435 | log.Print(errCover) 436 | continue 437 | } 438 | _, err = stdOut.Write(coverInBytes) 439 | if err != nil { 440 | log.Fatal(err) 441 | } 442 | 443 | allProfileBlocks = append(allProfileBlocks, profileBlocks...) 444 | } 445 | 446 | if len(allProfileBlocks) > 0 && len(config.funcFilter) == 0 { 447 | stat := getStatForProfileBlocks(allProfileBlocks) 448 | totalCoverage := fmt.Sprintf("Coverage: %.1f%% of statements", stat) 449 | _, err = stdOut.Write([]byte(getColorHeader(totalCoverage, false))) 450 | if err != nil { 451 | log.Fatal(err) 452 | } 453 | } 454 | } 455 | -------------------------------------------------------------------------------- /go-carpet_test.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "os" 5 | "path/filepath" 6 | "testing" 7 | 8 | "github.com/mgutz/ansi" 9 | "golang.org/x/tools/cover" 10 | ) 11 | 12 | func assertDontPanic(t *testing.T, fn func(), name string) { 13 | defer func() { 14 | if recoverInfo := recover(); recoverInfo != nil { 15 | t.Errorf("The code panic: %s\npanic: %s", name, recoverInfo) 16 | } 17 | }() 18 | fn() 19 | } 20 | 21 | // usage: 22 | // 23 | // defer testChdir(t, "/path")() 24 | func testChdir(t *testing.T, dir string) func() { 25 | cwd, err := os.Getwd() 26 | if err != nil { 27 | t.Fatal(err) 28 | } 29 | 30 | if err := os.Chdir(dir); err != nil { 31 | t.Fatal(err) 32 | } 33 | 34 | return func() { 35 | err := os.Chdir(cwd) 36 | if err != nil { 37 | t.Fatal(err) 38 | } 39 | } 40 | } 41 | 42 | func Test_readFile(t *testing.T) { 43 | file, err := readFile("go-carpet_test.go") 44 | if err != nil { 45 | t.Errorf("readFile(): got error: %s", err) 46 | } 47 | if len(file) == 0 { 48 | t.Errorf("readFile(): file empty") 49 | } 50 | if string(file[:12]) != "package main" { 51 | t.Errorf("readFile(): failed read first line") 52 | } 53 | 54 | _, err = readFile("dont exists file") 55 | if err == nil { 56 | t.Errorf("File exists error:") 57 | } 58 | } 59 | 60 | func Test_getDirsWithTests(t *testing.T) { 61 | dirs, err := getDirsWithTests(false, ".") 62 | if len(dirs) == 0 || err != nil { 63 | t.Errorf("getDirsWithTests(): dir list is empty") 64 | } 65 | dirs, err = getDirsWithTests(false) 66 | if len(dirs) == 0 || err != nil { 67 | t.Errorf("getDirsWithTests(): dir list is empty") 68 | } 69 | dirs, err = getDirsWithTests(false, ".", ".") 70 | if len(dirs) != 1 || err != nil { 71 | t.Errorf("getDirsWithTests(): the same directory failed") 72 | } 73 | 74 | defer testChdir(t, "./testdata")() 75 | dirs, err = getDirsWithTests(false, ".") 76 | if len(dirs) != 1 || err != nil { 77 | t.Errorf("getDirsWithTests(): without vendor dirs") 78 | } 79 | 80 | dirs, err = getDirsWithTests(true, ".") 81 | if len(dirs) != 4 || err != nil { 82 | t.Errorf("getDirsWithTests(): with vendor dirs") 83 | } 84 | } 85 | 86 | func Test_getTempFileName(t *testing.T) { 87 | tmpFileName, err := getTempFileName() 88 | if err != nil { 89 | t.Errorf("getTempFileName() got error") 90 | } 91 | defer func() { 92 | err = os.RemoveAll(tmpFileName) 93 | if err != nil { 94 | t.Errorf("getTempFileName() RemoveAll failed") 95 | } 96 | }() 97 | 98 | if len(tmpFileName) == 0 { 99 | t.Errorf("getTempFileName() failed") 100 | } 101 | 102 | // on RO-dir 103 | defer testChdir(t, "/")() 104 | _, err = getTempFileName() 105 | if err == nil { 106 | t.Errorf("getTempFileName() not got error") 107 | } 108 | } 109 | 110 | func Test_getShadeOfGreen(t *testing.T) { 111 | testData := []struct { 112 | normCover float64 113 | result string 114 | }{ 115 | { 116 | normCover: 0, 117 | result: "29", 118 | }, 119 | { 120 | normCover: 1, 121 | result: "51", 122 | }, 123 | { 124 | normCover: 0.99999, 125 | result: "51", 126 | }, 127 | { 128 | normCover: 0.5, 129 | result: "40", 130 | }, 131 | { 132 | normCover: -1, 133 | result: "29", 134 | }, 135 | { 136 | normCover: 11, 137 | result: "51", 138 | }, 139 | { 140 | normCover: 100500, 141 | result: "51", 142 | }, 143 | } 144 | 145 | for i, item := range testData { 146 | result := getShadeOfGreen(item.normCover) 147 | if result != item.result { 148 | t.Errorf("\n%d.\nexpected: %v\nreal : %v", i, item.result, result) 149 | } 150 | } 151 | } 152 | 153 | func Test_getColorWriter(t *testing.T) { 154 | assertDontPanic(t, func() { getColorWriter() }, "getColorWriter()") 155 | } 156 | 157 | func Test_getColorHeader(t *testing.T) { 158 | result := getColorHeader("filename.go", true) 159 | expected := ansi.ColorCode("yellow") + "filename.go" + ansi.ColorCode("reset") + "\n" + 160 | ansi.ColorCode("black+h") + "~~~~~~~~~~~" + ansi.ColorCode("reset") + "\n" 161 | 162 | if result != expected { 163 | t.Errorf("1. getColorHeader() failed") 164 | } 165 | 166 | result = getColorHeader("filename.go", false) 167 | expected = ansi.ColorCode("yellow") + "filename.go" + ansi.ColorCode("reset") + "\n" 168 | 169 | if result != expected { 170 | t.Errorf("2. getColorHeader() failed") 171 | } 172 | } 173 | 174 | func Test_getCoverForFile(t *testing.T) { 175 | fileProfile := &cover.Profile{ 176 | FileName: "filename.go", 177 | Mode: "count", 178 | Blocks: []cover.ProfileBlock{ 179 | { 180 | StartLine: 2, 181 | StartCol: 5, 182 | EndLine: 2, 183 | EndCol: 10, 184 | NumStmt: 1, 185 | Count: 1, 186 | }, 187 | }, 188 | } 189 | fileContent := []byte("1 line\n123 green 456\n3 line red and other") 190 | 191 | coloredBytes := getCoverForFile(fileProfile, fileContent, Config{colors256: false}) 192 | expectOut := getColorHeader("filename.go - 100.0%", true) + 193 | "1 line\n" + 194 | "123 " + ansi.ColorCode("green") + "green" + ansi.ColorCode("reset") + " 456\n" + 195 | "3 line red and other\n" 196 | if string(coloredBytes) != expectOut { 197 | t.Errorf("1. getCoverForFile() failed") 198 | } 199 | 200 | // with red blocks 201 | fileProfile.Blocks = append(fileProfile.Blocks, 202 | cover.ProfileBlock{ 203 | StartLine: 3, 204 | StartCol: 8, 205 | EndLine: 3, 206 | EndCol: 11, 207 | NumStmt: 0, 208 | Count: 0, 209 | }, 210 | ) 211 | coloredBytes = getCoverForFile(fileProfile, fileContent, Config{colors256: false}) 212 | expectOut = getColorHeader("filename.go - 100.0%", true) + 213 | "1 line\n" + 214 | "123 " + ansi.ColorCode("green") + "green" + ansi.ColorCode("reset") + " 456\n" + 215 | "3 line " + ansi.ColorCode("red") + "red" + ansi.ColorCode("reset") + " and other\n" 216 | if string(coloredBytes) != expectOut { 217 | t.Errorf("2. getCoverForFile() failed") 218 | } 219 | 220 | // 256 colors 221 | coloredBytes = getCoverForFile(fileProfile, fileContent, Config{colors256: true}) 222 | expectOut = getColorHeader("filename.go - 100.0%", true) + 223 | "1 line\n" + 224 | "123 " + ansi.ColorCode("48") + "green" + ansi.ColorCode("reset") + " 456\n" + 225 | "3 line " + ansi.ColorCode("red") + "red" + ansi.ColorCode("reset") + " and other\n" 226 | if string(coloredBytes) != expectOut { 227 | t.Errorf("3. getCoverForFile() failed") 228 | } 229 | 230 | coloredBytes = getCoverForFile(fileProfile, fileContent, Config{summary: true}) 231 | expectOut = "filename.go - 100.0%\n" 232 | if string(coloredBytes) != expectOut { 233 | t.Errorf("4. getCoverForFile() failed; got:\n%s\nwant:\n%s", coloredBytes, expectOut) 234 | } 235 | } 236 | 237 | func Test_runGoTest(t *testing.T) { 238 | err := runGoTest("./not exists dir", "", []string{}, true) 239 | if err == nil { 240 | t.Errorf("runGoTest() error failed") 241 | } 242 | } 243 | 244 | func Test_guessAbsPathInGOPATH(t *testing.T) { 245 | GOPATH := "" 246 | absPath, err := guessAbsPathInGOPATH(GOPATH, "file.golang") 247 | if absPath != "" || err == nil { 248 | t.Errorf("1. guessAbsPathInGOPATH() empty GOPATH") 249 | } 250 | 251 | cwd, _ := os.Getwd() 252 | 253 | GOPATH = filepath.Join(cwd, "testdata") 254 | absPath, err = guessAbsPathInGOPATH(GOPATH, "file.golang") 255 | if err != nil { 256 | t.Errorf("2. guessAbsPathInGOPATH() error: %s", err) 257 | } 258 | if absPath != filepath.Join(cwd, "testdata", "src", "file.golang") { 259 | t.Errorf("3. guessAbsPathInGOPATH() empty GOPATH") 260 | } 261 | 262 | GOPATH = filepath.Join(cwd, "testdata") + string(os.PathListSeparator) + "/tmp" 263 | absPath, err = guessAbsPathInGOPATH(GOPATH, "file.golang") 264 | if err != nil { 265 | t.Errorf("4. guessAbsPathInGOPATH() error: %s", err) 266 | } 267 | if absPath != filepath.Join(cwd, "testdata", "src", "file.golang") { 268 | t.Errorf("5. guessAbsPathInGOPATH() empty GOPATH") 269 | } 270 | 271 | GOPATH = "/tmp" + string(os.PathListSeparator) + "/" 272 | absPath, err = guessAbsPathInGOPATH(GOPATH, "file.golang") 273 | if absPath != "" || err == nil { 274 | t.Errorf("6. guessAbsPathInGOPATH() file not in GOPATH") 275 | } 276 | } 277 | 278 | func Test_getStatForProfileBlocks(t *testing.T) { 279 | profileBlocks := []cover.ProfileBlock{ 280 | { 281 | StartLine: 2, 282 | StartCol: 5, 283 | EndLine: 2, 284 | EndCol: 10, 285 | NumStmt: 1, 286 | Count: 1, 287 | }, 288 | } 289 | 290 | stat := getStatForProfileBlocks(profileBlocks) 291 | if stat != 100.0 { 292 | t.Errorf("1. getStatForProfileBlocks() failed") 293 | } 294 | 295 | profileBlocks = append(profileBlocks, 296 | cover.ProfileBlock{ 297 | StartLine: 3, 298 | StartCol: 5, 299 | EndLine: 3, 300 | EndCol: 10, 301 | NumStmt: 1, 302 | Count: 0, 303 | }, 304 | ) 305 | stat = getStatForProfileBlocks(profileBlocks) 306 | if stat != 50.0 { 307 | t.Errorf("2. getStatForProfileBlocks() failed") 308 | } 309 | 310 | profileBlocks = append(profileBlocks, 311 | cover.ProfileBlock{ 312 | StartLine: 4, 313 | StartCol: 5, 314 | EndLine: 4, 315 | EndCol: 10, 316 | NumStmt: 1, 317 | Count: 0, 318 | }, 319 | cover.ProfileBlock{ 320 | StartLine: 4, 321 | StartCol: 5, 322 | EndLine: 4, 323 | EndCol: 10, 324 | NumStmt: 1, 325 | Count: 0, 326 | }, 327 | ) 328 | stat = getStatForProfileBlocks(profileBlocks) 329 | if stat != 25.0 { 330 | t.Errorf("3. getStatForProfileBlocks() failed") 331 | } 332 | } 333 | -------------------------------------------------------------------------------- /go.mod: -------------------------------------------------------------------------------- 1 | module github.com/msoap/go-carpet 2 | 3 | go 1.19 4 | 5 | require ( 6 | github.com/mattn/go-colorable v0.1.12 7 | github.com/mattn/go-shellwords v1.0.12 8 | github.com/mgutz/ansi v0.0.0-20200706080929-d51e80ef957d 9 | github.com/msoap/byline v1.1.1 10 | golang.org/x/tools v0.1.12 11 | ) 12 | 13 | require ( 14 | github.com/mattn/go-isatty v0.0.14 // indirect 15 | golang.org/x/sys v0.0.0-20220722155257-8c9f86f7a55f // indirect 16 | ) 17 | -------------------------------------------------------------------------------- /go.sum: -------------------------------------------------------------------------------- 1 | github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c= 2 | github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= 3 | github.com/mattn/go-colorable v0.1.12 h1:jF+Du6AlPIjs2BiUiQlKOX0rt3SujHxPnksPKZbaA40= 4 | github.com/mattn/go-colorable v0.1.12/go.mod h1:u5H1YNBxpqRaxsYJYSkiCWKzEfiAb1Gb520KVy5xxl4= 5 | github.com/mattn/go-isatty v0.0.14 h1:yVuAays6BHfxijgZPzw+3Zlu5yQgKGP2/hcQbHb7S9Y= 6 | github.com/mattn/go-isatty v0.0.14/go.mod h1:7GGIvUiUoEMVVmxf/4nioHXj79iQHKdU27kJ6hsGG94= 7 | github.com/mattn/go-shellwords v1.0.12 h1:M2zGm7EW6UQJvDeQxo4T51eKPurbeFbe8WtebGE2xrk= 8 | github.com/mattn/go-shellwords v1.0.12/go.mod h1:EZzvwXDESEeg03EKmM+RmDnNOPKG4lLtQsUlTZDWQ8Y= 9 | github.com/mgutz/ansi v0.0.0-20200706080929-d51e80ef957d h1:5PJl274Y63IEHC+7izoQE9x6ikvDFZS2mDVS3drnohI= 10 | github.com/mgutz/ansi v0.0.0-20200706080929-d51e80ef957d/go.mod h1:01TrycV0kFyexm33Z7vhZRXopbI8J3TDReVlkTgMUxE= 11 | github.com/msoap/byline v1.1.1 h1:imxWvm9wIHNGePF/peiOxcL1vgVLK3/qKsMW75XZn9c= 12 | github.com/msoap/byline v1.1.1/go.mod h1:E2oCrXddpzrmu4NmrwEv4Qiyweo62Yp3+w3IN3X2sq8= 13 | github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= 14 | github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= 15 | github.com/stretchr/objx v0.1.1/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= 16 | github.com/stretchr/testify v1.2.2 h1:bSDNvY7ZPG5RlJ8otE/7V6gMiyenm9RtJ7IUVIAoJ1w= 17 | github.com/stretchr/testify v1.2.2/go.mod h1:a8OnRcib4nhh0OaRAV+Yts87kKdq0PP7pXfy6kDkUVs= 18 | golang.org/x/sys v0.0.0-20210630005230-0f9fa26af87c/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= 19 | golang.org/x/sys v0.0.0-20210927094055-39ccf1dd6fa6/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= 20 | golang.org/x/sys v0.0.0-20220722155257-8c9f86f7a55f h1:v4INt8xihDGvnrfjMDVXGxw9wrfxYyCjk0KbXjhR55s= 21 | golang.org/x/sys v0.0.0-20220722155257-8c9f86f7a55f/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= 22 | golang.org/x/tools v0.1.12 h1:VveCTK38A2rkS8ZqFY25HIDFscX5X9OoEhJd3quQmXU= 23 | golang.org/x/tools v0.1.12/go.mod h1:hNGJHUnrk76NpqgfD5Aqm5Crs+Hm0VOH/i9J2+nxYbc= 24 | -------------------------------------------------------------------------------- /mod.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "fmt" 5 | "io" 6 | "log" 7 | "os" 8 | "os/exec" 9 | "path" 10 | "strings" 11 | 12 | "github.com/msoap/byline" 13 | ) 14 | 15 | var goModFilename *string 16 | 17 | func getGoModFilename() string { 18 | if goModFilename != nil { 19 | return *goModFilename 20 | } 21 | 22 | file := "" 23 | out, err := exec.Command("go", "env", "GOMOD").Output() 24 | if err != nil { 25 | log.Printf("failed to load 'go env GOMOD' content: %s", err) 26 | goModFilename = &file 27 | return "" 28 | } 29 | 30 | file = strings.TrimSpace(string(out)) 31 | goModFilename = &file 32 | 33 | return file 34 | } 35 | 36 | func guessAbsPathInGoMod(relPath string) (string, error) { 37 | modFilename := getGoModFilename() 38 | if modFilename == "" { 39 | return "", errIsNotInGoMod 40 | } 41 | 42 | modFile, err := os.Open(modFilename) 43 | if err != nil { 44 | return "", err 45 | } 46 | defer func() { 47 | if err := modFile.Close(); err != nil { 48 | log.Printf("failed to close %s file: %s", modFilename, err) 49 | } 50 | }() 51 | 52 | moduleName := "" 53 | if err := byline.NewReader(modFile).AWKMode(func(_ string, fields []string, vars byline.AWKVars) (string, error) { 54 | if vars.NF == 2 && fields[0] == "module" && fields[1] != "" { 55 | moduleName = fields[1] 56 | return "", io.EOF 57 | } 58 | 59 | return "", nil 60 | }).Discard(); err != nil { 61 | return "", err 62 | } 63 | if moduleName == "" { 64 | return "", errIsNotInGoMod 65 | } 66 | 67 | absPath := path.Dir(modFilename) + strings.TrimPrefix(relPath, moduleName) 68 | if stat, err := os.Stat(absPath); err != nil { 69 | return "", err 70 | } else if !stat.Mode().IsRegular() { 71 | return "", fmt.Errorf("%s is not regular file", absPath) 72 | } 73 | 74 | return absPath, nil 75 | } 76 | -------------------------------------------------------------------------------- /mod_test.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "os" 5 | "testing" 6 | ) 7 | 8 | func Test_guessAbsPathInGoMod(t *testing.T) { 9 | if err := os.Setenv("GO111MODULE", "on"); err != nil { 10 | t.Fatalf("failed to set env: %s", err) 11 | } 12 | 13 | t.Run("empty", func(t *testing.T) { 14 | if _, err := guessAbsPathInGoMod(""); err == nil { 15 | t.Errorf("failed to test empty file") 16 | } 17 | }) 18 | 19 | t.Run("real", func(t *testing.T) { 20 | gotAbsPath, err := guessAbsPathInGoMod("github.com/msoap/go-carpet/terminal_posix.go") 21 | if err != nil { 22 | t.Errorf("failed to test real file: %s", err) 23 | } 24 | 25 | if _, err := os.Stat(gotAbsPath); err != nil { 26 | t.Errorf("failed to test real file: %s", err) 27 | } 28 | }) 29 | 30 | t.Run("not exists", func(t *testing.T) { 31 | _, err := guessAbsPathInGoMod("github.com/msoap/go-carpet/terminal_posix_another_file.go") 32 | if err == nil { 33 | t.Errorf("failed to test not exists file") 34 | } 35 | }) 36 | } 37 | -------------------------------------------------------------------------------- /terminal_posix.go: -------------------------------------------------------------------------------- 1 | //go:build !windows 2 | 3 | package main 4 | 5 | import ( 6 | "io" 7 | "os" 8 | ) 9 | 10 | func getColorWriter() io.Writer { 11 | return (io.Writer)(os.Stdout) 12 | } 13 | -------------------------------------------------------------------------------- /terminal_windows.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "io" 5 | 6 | "github.com/mattn/go-colorable" 7 | ) 8 | 9 | func getColorWriter() io.Writer { 10 | return colorable.NewColorableStdout() 11 | } 12 | -------------------------------------------------------------------------------- /testdata/Godeps/empty_test.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import "testing" 4 | 5 | func Test_empty(t *testing.T) { 6 | } 7 | -------------------------------------------------------------------------------- /testdata/colored_00.txt: -------------------------------------------------------------------------------- 1 | ./testdata/file_00.golang - 100.0% 2 | ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ 3 | package main 4 | 5 | func readFile(fileName string) (result []byte, err error) { 6 | fileReader, err := os.Open(fileName) 7 | if err != nil { 8 | return result, err 9 | } 10 | defer fileReader.Close() 11 | 12 | result, err = ioutil.ReadAll(fileReader) 13 | return result, err 14 | } 15 | 16 | ./testdata/file_01.golang - 100.0% 17 | ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ 18 | package main 19 | 20 | // isStringInSlice - one of the elements of the array contained in the string 21 | func isSliceInString(src string, slice []string) bool { 22 | for _, dst := range slice { 23 |  if strings.Contains(src, dst) { 24 |  return true 25 |  } 26 |  } 27 | return false 28 | } 29 | 30 | -------------------------------------------------------------------------------- /testdata/colored_01.txt: -------------------------------------------------------------------------------- 1 | ./testdata/file_00.golang - 100.0% 2 | ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ 3 | package main 4 | 5 | func readFile(fileName string) (result []byte, err error) { 6 | fileReader, err := os.Open(fileName) 7 | if err != nil { 8 | return result, err 9 | } 10 | defer fileReader.Close() 11 | 12 | result, err = ioutil.ReadAll(fileReader) 13 | return result, err 14 | } 15 | 16 | ./testdata/file_01.golang - 100.0% 17 | ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ 18 | package main 19 | 20 | // isStringInSlice - one of the elements of the array contained in the string 21 | func isSliceInString(src string, slice []string) bool { 22 | for _, dst := range slice { 23 |  if strings.Contains(src, dst) { 24 |  return true 25 |  } 26 |  } 27 | return false 28 | } 29 | 30 | -------------------------------------------------------------------------------- /testdata/colored_02.txt: -------------------------------------------------------------------------------- 1 | ./testdata/file_01.golang - 100.0% 2 | ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ 3 | package main 4 | 5 | // isStringInSlice - one of the elements of the array contained in the string 6 | func isSliceInString(src string, slice []string) bool { 7 | for _, dst := range slice { 8 |  if strings.Contains(src, dst) { 9 |  return true 10 |  } 11 |  } 12 | return false 13 | } 14 | 15 | -------------------------------------------------------------------------------- /testdata/colored_03.txt: -------------------------------------------------------------------------------- 1 | github.com/msoap/go-carpet/terminal_posix.go - 100.0% 2 | ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ 3 | //go:build !windows 4 | 5 | package main 6 | 7 | import ( 8 | "io" 9 | "os" 10 | ) 11 | 12 | func getColorWriter() io.Writer { 13 | return (io.Writer)(os.Stdout) 14 | } 15 | 16 | -------------------------------------------------------------------------------- /testdata/cover_00.out: -------------------------------------------------------------------------------- 1 | mode: count 2 | _./testdata/file_00.golang:4.2,4.38 1 2 3 | _./testdata/file_00.golang:5.2,5.17 0 0 4 | _./testdata/file_01.golang:5.28,9.3 1 1 5 | _./testdata/file_01.golang:10.2,10.14 0 0 6 | -------------------------------------------------------------------------------- /testdata/cover_01.out: -------------------------------------------------------------------------------- 1 | mode: count 2 | _./testdata/file_not_exists.golang:4.2,4.38 1 2 3 | -------------------------------------------------------------------------------- /testdata/cover_02.out: -------------------------------------------------------------------------------- 1 | mode: count 2 | github.com/msoap/go-carpet/terminal_posix.go:12.2,12.31 1 1 3 | -------------------------------------------------------------------------------- /testdata/empty_test.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import "testing" 4 | 5 | func Test_empty(t *testing.T) { 6 | } 7 | -------------------------------------------------------------------------------- /testdata/file_00.golang: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | func readFile(fileName string) (result []byte, err error) { 4 | fileReader, err := os.Open(fileName) 5 | if err != nil { 6 | return result, err 7 | } 8 | defer fileReader.Close() 9 | 10 | result, err = ioutil.ReadAll(fileReader) 11 | return result, err 12 | } 13 | -------------------------------------------------------------------------------- /testdata/file_01.golang: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | // isStringInSlice - one of the elements of the array contained in the string 4 | func isSliceInString(src string, slice []string) bool { 5 | for _, dst := range slice { 6 | if strings.Contains(src, dst) { 7 | return true 8 | } 9 | } 10 | return false 11 | } 12 | -------------------------------------------------------------------------------- /testdata/src/file.golang: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "fmt" 5 | ) 6 | 7 | func main() { 8 | fmt.Println("Hello world") 9 | } 10 | -------------------------------------------------------------------------------- /testdata/testdata/empty_test.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import "testing" 4 | 5 | func Test_empty(t *testing.T) { 6 | } 7 | -------------------------------------------------------------------------------- /testdata/vendor/dir/empty_test.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import "testing" 4 | 5 | func Test_empty(t *testing.T) { 6 | } 7 | -------------------------------------------------------------------------------- /testdata/vendor/empty_test.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import "testing" 4 | 5 | func Test_empty(t *testing.T) { 6 | } 7 | -------------------------------------------------------------------------------- /unix_only_test.go: -------------------------------------------------------------------------------- 1 | //go:build !windows 2 | // +build !windows 3 | 4 | package main 5 | 6 | import ( 7 | "reflect" 8 | "testing" 9 | ) 10 | 11 | func Test_getCoverForDir(t *testing.T) { 12 | t.Run("error", func(t *testing.T) { 13 | _, _, err := getCoverForDir("./testdata/not_exists.out", []string{}, Config{colors256: false}) 14 | if err == nil { 15 | t.Errorf("1. getCoverForDir() error failed") 16 | } 17 | }) 18 | 19 | t.Run("cover", func(t *testing.T) { 20 | bytes, _, err := getCoverForDir("./testdata/cover_00.out", []string{}, Config{colors256: false}) 21 | if err != nil { 22 | t.Errorf("2. getCoverForDir() failed: %v", err) 23 | } 24 | expect, err := readFile("./testdata/colored_00.txt") 25 | if err != nil { 26 | t.Errorf("3. getCoverForDir() failed: %v", err) 27 | } 28 | if !reflect.DeepEqual(bytes, expect) { 29 | t.Errorf("4. getCoverForDir() not equal") 30 | } 31 | }) 32 | 33 | t.Run("cover with 256 colors", func(t *testing.T) { 34 | bytes, _, err := getCoverForDir("./testdata/cover_00.out", []string{}, Config{colors256: true}) 35 | if err != nil { 36 | t.Errorf("5. getCoverForDir() failed: %v", err) 37 | } 38 | expect, err := readFile("./testdata/colored_01.txt") 39 | if err != nil { 40 | t.Errorf("6. getCoverForDir() failed: %v", err) 41 | } 42 | if !reflect.DeepEqual(bytes, expect) { 43 | t.Errorf("7. getCoverForDir() not equal") 44 | } 45 | }) 46 | 47 | t.Run("cover with 256 colors with error", func(t *testing.T) { 48 | _, _, err := getCoverForDir("./testdata/cover_01.out", []string{}, Config{colors256: true}) 49 | if err == nil { 50 | t.Errorf("8. getCoverForDir() not exists go file") 51 | } 52 | }) 53 | 54 | t.Run("cover 01 without 256 colors", func(t *testing.T) { 55 | bytes, _, err := getCoverForDir("./testdata/cover_00.out", []string{"file_01.go"}, Config{colors256: false}) 56 | if err != nil { 57 | t.Errorf("9. getCoverForDir() failed: %v", err) 58 | } 59 | expect, err := readFile("./testdata/colored_02.txt") 60 | if err != nil { 61 | t.Errorf("10. getCoverForDir() failed: %v", err) 62 | } 63 | if !reflect.DeepEqual(bytes, expect) { 64 | t.Errorf("11. getCoverForDir() not equal") 65 | } 66 | }) 67 | 68 | t.Run("cover 02 without 256 colors", func(t *testing.T) { 69 | bytes, _, err := getCoverForDir("./testdata/cover_02.out", []string{}, Config{colors256: false}) 70 | if err != nil { 71 | t.Errorf("12. getCoverForDir() failed: %v", err) 72 | } 73 | expect, err := readFile("./testdata/colored_03.txt") 74 | if err != nil { 75 | t.Errorf("13. getCoverForDir() failed: %v", err) 76 | } 77 | if !reflect.DeepEqual(bytes, expect) { 78 | t.Errorf("14. getCoverForDir() not equal\ngot:\n%s\nexpect:\n%s\n", bytes, expect) 79 | } 80 | }) 81 | } 82 | 83 | func Test_getCoverForDir_mincov_flag(t *testing.T) { 84 | t.Run("covered 100% mincov 100%", func(t *testing.T) { 85 | conf := Config{ 86 | colors256: false, 87 | minCoverage: 100.0, 88 | } 89 | 90 | // cover_00.out has 100% coverage of 2 files 91 | _, profileBlocks, err := getCoverForDir("./testdata/cover_00.out", []string{"file_01.go"}, conf) 92 | if err != nil { 93 | t.Errorf("getCoverForDir() failed with error: %s", err) 94 | } 95 | 96 | expectLen := 2 97 | actualLen := len(profileBlocks) 98 | 99 | if expectLen != actualLen { 100 | t.Errorf("1. minimum coverage 100%% should print all the blocks. want %v, got: %v", expectLen, actualLen) 101 | } 102 | }) 103 | 104 | t.Run("covered 100% mincov 50%", func(t *testing.T) { 105 | conf := Config{ 106 | colors256: false, 107 | minCoverage: 50.0, 108 | } 109 | 110 | // cover_00.out has 100% coverage of 2 files 111 | _, profileBlocks, err := getCoverForDir("./testdata/cover_00.out", []string{"file_01.go"}, conf) 112 | if err != nil { 113 | t.Errorf("getCoverForDir() failed with error: %s", err) 114 | } 115 | 116 | expectLen := 0 117 | actualLen := len(profileBlocks) 118 | 119 | if expectLen != actualLen { 120 | t.Errorf("2. minimum coverage 50%% for 100%% covered source should print nothing. want %v, got: %v", expectLen, actualLen) 121 | } 122 | }) 123 | } 124 | -------------------------------------------------------------------------------- /utils.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "fmt" 5 | "log" 6 | "strings" 7 | 8 | shellwords "github.com/mattn/go-shellwords" 9 | ) 10 | 11 | // isSliceInString - one of the elements of the array contained in the string 12 | func isSliceInString(src string, slice []string) bool { 13 | for _, dst := range slice { 14 | if strings.Contains(src, dst) { 15 | return true 16 | } 17 | } 18 | return false 19 | } 20 | 21 | // isSliceInStringPrefix - one of the elements of the array is are prefix in the string 22 | func isSliceInStringPrefix(src string, slice []string) bool { 23 | for _, dst := range slice { 24 | if strings.HasPrefix(src, dst) { 25 | return true 26 | } 27 | } 28 | return false 29 | } 30 | 31 | // grepEmptyStringSlice - return slice with non-empty strings 32 | func grepEmptyStringSlice(inSlice []string) []string { 33 | result := []string{} 34 | for _, item := range inSlice { 35 | if len(item) > 0 { 36 | result = append(result, item) 37 | } 38 | } 39 | return result 40 | } 41 | 42 | // parse additional args for pass to go test 43 | func parseAdditionalArgs(argsRaw string, excludes []string) (resultArgs []string, err error) { 44 | if argsRaw != "" { 45 | args, err := shellwords.Parse(argsRaw) 46 | if err != nil { 47 | return resultArgs, fmt.Errorf("args %q parse failed: %s", argsRaw, err) 48 | } 49 | 50 | NEXTARG: 51 | for _, arg := range args { 52 | for _, excludeArg := range excludes { 53 | if excludeArg != "" && strings.HasPrefix(arg, excludeArg) { 54 | log.Printf("arg: %q is not allowed, skip", arg) 55 | continue NEXTARG 56 | } 57 | } 58 | resultArgs = append(resultArgs, arg) 59 | } 60 | } 61 | 62 | return resultArgs, nil 63 | } 64 | -------------------------------------------------------------------------------- /utils_test.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "io" 5 | "log" 6 | "os" 7 | "reflect" 8 | "testing" 9 | ) 10 | 11 | func Test_isSliceInString(t *testing.T) { 12 | testData := []struct { 13 | src string 14 | slice []string 15 | result bool 16 | }{ 17 | { 18 | src: "one/file.go", 19 | slice: []string{"one.go", "file.go"}, 20 | result: true, 21 | }, 22 | { 23 | src: "path/path/file.go", 24 | slice: []string{"one.go", "path/file.go"}, 25 | result: true, 26 | }, 27 | { 28 | src: "one/file.go", 29 | slice: []string{"one.go", "two.go"}, 30 | result: false, 31 | }, 32 | { 33 | src: "one/file.go", 34 | slice: []string{}, 35 | result: false, 36 | }, 37 | } 38 | 39 | for i, item := range testData { 40 | result := isSliceInString(item.src, item.slice) 41 | if result != item.result { 42 | t.Errorf("\n%d. isSliceInString()\nexpected: %v\nreal :%v", i, item.result, result) 43 | } 44 | } 45 | } 46 | 47 | func Test_isSliceInStringPrefix(t *testing.T) { 48 | testData := []struct { 49 | src string 50 | slice []string 51 | result bool 52 | }{ 53 | { 54 | src: "one/file.go", 55 | slice: []string{"vendor", "Godeps"}, 56 | result: false, 57 | }, 58 | { 59 | src: "vendor/path/file.go", 60 | slice: []string{"vendor", "Godeps"}, 61 | result: true, 62 | }, 63 | { 64 | src: "Godeps/path/file.go", 65 | slice: []string{"vendor", "Godeps"}, 66 | result: true, 67 | }, 68 | { 69 | src: "one/file.go", 70 | slice: []string{}, 71 | result: false, 72 | }, 73 | } 74 | 75 | for i, item := range testData { 76 | result := isSliceInStringPrefix(item.src, item.slice) 77 | if result != item.result { 78 | t.Errorf("\n%d. isSliceInStringPrefix()\nexpected: %v\nreal :%v", i, item.result, result) 79 | } 80 | } 81 | } 82 | 83 | func Test_grepEmptyStringSlice(t *testing.T) { 84 | testData := []struct { 85 | inSlice []string 86 | result []string 87 | }{ 88 | { 89 | inSlice: []string{}, 90 | result: []string{}, 91 | }, 92 | { 93 | inSlice: nil, 94 | result: []string{}, 95 | }, 96 | { 97 | inSlice: []string{""}, 98 | result: []string{}, 99 | }, 100 | { 101 | inSlice: []string{"A", "B"}, 102 | result: []string{"A", "B"}, 103 | }, 104 | { 105 | inSlice: []string{"A", "", "B"}, 106 | result: []string{"A", "B"}, 107 | }, 108 | { 109 | inSlice: []string{"", "", "B"}, 110 | result: []string{"B"}, 111 | }, 112 | } 113 | 114 | for i, item := range testData { 115 | result := grepEmptyStringSlice(item.inSlice) 116 | 117 | if !reflect.DeepEqual(result, item.result) { 118 | t.Errorf("\n%d. grepEmptyStringSlice()\nexpected: %#v\nreal :%#v", i, item.result, result) 119 | } 120 | } 121 | } 122 | 123 | func Test_parseAdditionalArgs(t *testing.T) { 124 | tests := []struct { 125 | name string 126 | argsRaw string 127 | excludes []string 128 | wantResultArgs []string 129 | wantErr bool 130 | }{ 131 | { 132 | name: "empty args", 133 | argsRaw: "", 134 | excludes: []string{}, 135 | wantResultArgs: nil, 136 | wantErr: false, 137 | }, 138 | { 139 | name: "with error", 140 | argsRaw: "str '...", 141 | excludes: []string{}, 142 | wantResultArgs: nil, 143 | wantErr: true, 144 | }, 145 | { 146 | name: "one args", 147 | argsRaw: `-short`, 148 | excludes: []string{}, 149 | wantResultArgs: []string{"-short"}, 150 | wantErr: false, 151 | }, 152 | { 153 | name: "all args", 154 | argsRaw: `-short -option`, 155 | excludes: []string{}, 156 | wantResultArgs: []string{"-short", "-option"}, 157 | wantErr: false, 158 | }, 159 | { 160 | name: "with excludes", 161 | argsRaw: `-short -ex=23 -new '-ex2 "ex word"' -option`, 162 | excludes: []string{"-ex", "-ex2"}, 163 | wantResultArgs: []string{"-short", "-new", "-option"}, 164 | wantErr: false, 165 | }, 166 | } 167 | 168 | log.SetOutput(io.Discard) 169 | 170 | for _, tt := range tests { 171 | t.Run(tt.name, func(t *testing.T) { 172 | gotResultArgs, err := parseAdditionalArgs(tt.argsRaw, tt.excludes) 173 | if (err != nil) != tt.wantErr { 174 | t.Errorf("parseAdditionalArgs() error = %v, wantErr %v", err, tt.wantErr) 175 | return 176 | } 177 | if !reflect.DeepEqual(gotResultArgs, tt.wantResultArgs) { 178 | t.Errorf("parseAdditionalArgs() = %v, want %v", gotResultArgs, tt.wantResultArgs) 179 | } 180 | }) 181 | } 182 | 183 | log.SetOutput(os.Stdout) 184 | } 185 | --------------------------------------------------------------------------------