From 641fb88623af6d41e3630b08e505addd9f912148 Mon Sep 17 00:00:00 2001 From: David Morrison Date: Thu, 15 Jun 2023 13:32:17 -0700 Subject: [PATCH 1/3] add enforce option and pre-commit hook --- .pre-commit-hooks.yaml | 8 +++++++ .pre-commit-runner.sh | 15 ++++++++++++ README.md | 50 +++++++++++++++++++++++++++++--------- go-carpet.1 | 4 ++++ go-carpet.go | 54 ++++++++++++++++++++++++++++-------------- unix_only_test.go | 24 ++++++++++++------- 6 files changed, 118 insertions(+), 37 deletions(-) create mode 100644 .pre-commit-hooks.yaml create mode 100755 .pre-commit-runner.sh diff --git a/.pre-commit-hooks.yaml b/.pre-commit-hooks.yaml new file mode 100644 index 0000000..792f2fa --- /dev/null +++ b/.pre-commit-hooks.yaml @@ -0,0 +1,8 @@ +- id: go-carpet + name: go-carpet + entry: .pre-commit-runner.sh + language: script + files: \.go + description: enforce a minimum level of coverage on changed files + args: + - "-mincov=50" diff --git a/.pre-commit-runner.sh b/.pre-commit-runner.sh new file mode 100755 index 0000000..d66333a --- /dev/null +++ b/.pre-commit-runner.sh @@ -0,0 +1,15 @@ +#!/usr/bin/bash + +args="" +while [ $# -gt 0 ]; do + if [[ "$1" == -* ]]; then + args+="$1" + else + space_files=$@ + files=${space_files// /,} + break + fi + shift +done + +go-carpet -summary -enforce $args -file $files diff --git a/README.md b/README.md index edc1a63..0a267ca 100644 --- a/README.md +++ b/README.md @@ -22,26 +22,54 @@ Usage usage: go-carpet [options] [paths] -256colors - use more colors on 256-color terminal (indicate the level of coverage) - -args string - pass additional arguments for go test - -file string - comma-separated list of files to test (default: all) - -func string - comma-separated functions list (default: all functions) + use more colors on 256-color terminal (indicate the level of coverage) + -args arguments + pass additional arguments for go test + -enforce + fail if any file's coverage is below mincov + -file files + comma-separated list of files to test (default: all) + -func functions + comma-separated functions list (default: all functions) -include-vendor - include vendor directories for show coverage (Godeps, vendor) + include vendor directories for show coverage (Godeps, vendor) -mincov float - coverage threshold of the file to be displayed (in percent) (default 100) + coverage threshold of the file to be displayed (in percent) (default 100) -summary - only show summary for each file + only show summary for each file -version - get version + get version For view coverage in less, use `-R` option: go-carpet | less -R +As a pre-commit hook +-------------------- + +[Pre-commit](https://pre-commit.com) is a tool that makes it easy to run checks on every Git commit. +You can use `go-carpet` as a pre-commit hook by installing pre-commit and including the following +in your `.pre-commit-config.yaml`: + +``` +repos: + ... + - repo: https://github.com/msoap/go-carpet + rev: v1.11.0 + hooks: + - id: go-carpet +``` + +By default, `go-carpet` will enforce 50% test coverage on every changed file. You can customize +the threshold in your hook with an arguments block as follows: + +``` +args: + - "-mincov=X" +``` + +You must have `go-carpet` installed in your PATH for this pre-commit hook to work. + Install ------- diff --git a/go-carpet.1 b/go-carpet.1 index adfcd17..b883429 100644 --- a/go-carpet.1 +++ b/go-carpet.1 @@ -27,12 +27,16 @@ usage: go\-carpet [options] [paths] use more colors on 256\-color terminal (indicate the level of coverage) \-args string pass additional arguments for go test + \-enforce + fail if any file's coverage is below mincov \-file string comma\-separated list of files to test (default: all) \-func string comma\-separated functions list (default: all functions) \-include\-vendor include vendor directories for show coverage (Godeps, vendor) + \-mincov float + coverage threshold of the file to be displayed (in percent) (default 100) \-summary only show summary for each file \-version diff --git a/go-carpet.go b/go-carpet.go index c266fe9..db89cec 100644 --- a/go-carpet.go +++ b/go-carpet.go @@ -153,10 +153,10 @@ func guessAbsPathInGOPATH(GOPATH, relPath string) (absPath string, err error) { return absPath, err } -func getCoverForDir(coverFileName string, filesFilter []string, config Config) (result []byte, profileBlocks []cover.ProfileBlock, err error) { +func getCoverForDir(coverFileName string, filesFilter []string, config Config) (result []byte, belowMin bool, profileBlocks []cover.ProfileBlock, err error) { coverProfile, err := cover.ParseProfiles(coverFileName) if err != nil { - return result, profileBlocks, err + return result, belowMin, profileBlocks, err } for _, fileProfile := range coverProfile { @@ -179,13 +179,13 @@ func getCoverForDir(coverFileName string, filesFilter []string, config Config) ( } } else if fileName, err = guessAbsPathInGoMod(fileProfile.FileName); err != errIsNotInGoMod { if err != nil { - return result, profileBlocks, err + return result, belowMin, profileBlocks, err } } else { // file in one dir in GOPATH fileName, err = guessAbsPathInGOPATH(os.Getenv("GOPATH"), fileProfile.FileName) if err != nil { - return result, profileBlocks, err + return result, belowMin, profileBlocks, err } } @@ -193,17 +193,19 @@ func getCoverForDir(coverFileName string, filesFilter []string, config Config) ( continue } + // If we get here, we're below minimum specified coverage and we weren't filtered out + belowMin = true var fileBytes []byte fileBytes, err = readFile(fileName) if err != nil { - return result, profileBlocks, err + return result, belowMin, profileBlocks, err } result = append(result, getCoverForFile(fileProfile, fileBytes, config)...) profileBlocks = append(profileBlocks, fileProfile.Blocks...) } - return result, profileBlocks, err + return result, belowMin, profileBlocks, err } func getColorHeader(header string, addUnderiline bool) string { @@ -355,15 +357,16 @@ func getTempFileName() (string, error) { // Config - application config type Config struct { - filesFilterRaw string - filesFilter []string - funcFilterRaw string - funcFilter []string - argsRaw string - minCoverage float64 - colors256 bool - includeVendor bool - summary bool + filesFilterRaw string + filesFilter []string + funcFilterRaw string + funcFilter []string + argsRaw string + minCoverage float64 + colors256 bool + includeVendor bool + summary bool + enforceCoverage bool } var config Config @@ -376,6 +379,7 @@ func init() { flag.BoolVar(&config.includeVendor, "include-vendor", false, "include vendor directories for show coverage (Godeps, vendor)") flag.StringVar(&config.argsRaw, "args", "", "pass additional `arguments` for go test") flag.Float64Var(&config.minCoverage, "mincov", 100.0, "coverage threshold of the file to be displayed (in percent)") + flag.BoolVar(&config.enforceCoverage, "enforce", false, "fail if any file's coverage is below mincov") flag.Usage = func() { fmt.Println(usageMessage) flag.PrintDefaults() @@ -383,13 +387,13 @@ func init() { } } -func main() { +func goCarpet() int { versionFl := flag.Bool("version", false, "get version") flag.Parse() if *versionFl { fmt.Println(version) - os.Exit(0) + return 0 } config.filesFilter = grepEmptyStringSlice(strings.Split(config.filesFilterRaw, ",")) @@ -424,17 +428,21 @@ func main() { log.Fatal(err) } + someFileBelowMin := false for _, path := range testDirs { if err = runGoTest(path, coverFileName, additionalArgs, false); err != nil { log.Print(err) continue } - coverInBytes, profileBlocks, errCover := getCoverForDir(coverFileName, config.filesFilter, config) + coverInBytes, belowMin, profileBlocks, errCover := getCoverForDir(coverFileName, config.filesFilter, config) if errCover != nil { log.Print(errCover) continue } + if belowMin { + someFileBelowMin = true + } _, err = stdOut.Write(coverInBytes) if err != nil { log.Fatal(err) @@ -451,4 +459,14 @@ func main() { log.Fatal(err) } } + + if someFileBelowMin && config.enforceCoverage { + return 1 + } + + return 0 +} + +func main() { + os.Exit(goCarpet()) } diff --git a/unix_only_test.go b/unix_only_test.go index b8f5256..4a8ddaa 100644 --- a/unix_only_test.go +++ b/unix_only_test.go @@ -10,14 +10,14 @@ import ( func Test_getCoverForDir(t *testing.T) { t.Run("error", func(t *testing.T) { - _, _, err := getCoverForDir("./testdata/not_exists.out", []string{}, Config{colors256: false}) + _, _, _, err := getCoverForDir("./testdata/not_exists.out", []string{}, Config{colors256: false}) if err == nil { t.Errorf("1. getCoverForDir() error failed") } }) t.Run("cover", func(t *testing.T) { - bytes, _, err := getCoverForDir("./testdata/cover_00.out", []string{}, Config{colors256: false}) + bytes, _, _, err := getCoverForDir("./testdata/cover_00.out", []string{}, Config{colors256: false}) if err != nil { t.Errorf("2. getCoverForDir() failed: %v", err) } @@ -31,7 +31,7 @@ func Test_getCoverForDir(t *testing.T) { }) t.Run("cover with 256 colors", func(t *testing.T) { - bytes, _, err := getCoverForDir("./testdata/cover_00.out", []string{}, Config{colors256: true}) + bytes, _, _, err := getCoverForDir("./testdata/cover_00.out", []string{}, Config{colors256: true}) if err != nil { t.Errorf("5. getCoverForDir() failed: %v", err) } @@ -45,14 +45,14 @@ func Test_getCoverForDir(t *testing.T) { }) t.Run("cover with 256 colors with error", func(t *testing.T) { - _, _, err := getCoverForDir("./testdata/cover_01.out", []string{}, Config{colors256: true}) + _, _, _, err := getCoverForDir("./testdata/cover_01.out", []string{}, Config{colors256: true}) if err == nil { t.Errorf("8. getCoverForDir() not exists go file") } }) t.Run("cover 01 without 256 colors", func(t *testing.T) { - bytes, _, err := getCoverForDir("./testdata/cover_00.out", []string{"file_01.go"}, Config{colors256: false}) + bytes, _, _, err := getCoverForDir("./testdata/cover_00.out", []string{"file_01.go"}, Config{colors256: false}) if err != nil { t.Errorf("9. getCoverForDir() failed: %v", err) } @@ -66,7 +66,7 @@ func Test_getCoverForDir(t *testing.T) { }) t.Run("cover 02 without 256 colors", func(t *testing.T) { - bytes, _, err := getCoverForDir("./testdata/cover_02.out", []string{}, Config{colors256: false}) + bytes, _, _, err := getCoverForDir("./testdata/cover_02.out", []string{}, Config{colors256: false}) if err != nil { t.Errorf("12. getCoverForDir() failed: %v", err) } @@ -88,7 +88,7 @@ func Test_getCoverForDir_mincov_flag(t *testing.T) { } // cover_00.out has 100% coverage of 2 files - _, profileBlocks, err := getCoverForDir("./testdata/cover_00.out", []string{"file_01.go"}, conf) + _, belowMin, profileBlocks, err := getCoverForDir("./testdata/cover_00.out", []string{"file_01.go"}, conf) if err != nil { t.Errorf("getCoverForDir() failed with error: %s", err) } @@ -99,6 +99,10 @@ func Test_getCoverForDir_mincov_flag(t *testing.T) { if expectLen != actualLen { t.Errorf("1. minimum coverage 100%% should print all the blocks. want %v, got: %v", expectLen, actualLen) } + + if !belowMin { + t.Errorf("1. at least one file was below minimum, but we didn't detect it") + } }) t.Run("covered 100% mincov 50%", func(t *testing.T) { @@ -108,7 +112,7 @@ func Test_getCoverForDir_mincov_flag(t *testing.T) { } // cover_00.out has 100% coverage of 2 files - _, profileBlocks, err := getCoverForDir("./testdata/cover_00.out", []string{"file_01.go"}, conf) + _, belowMin, profileBlocks, err := getCoverForDir("./testdata/cover_00.out", []string{"file_01.go"}, conf) if err != nil { t.Errorf("getCoverForDir() failed with error: %s", err) } @@ -119,5 +123,9 @@ func Test_getCoverForDir_mincov_flag(t *testing.T) { if expectLen != actualLen { t.Errorf("2. minimum coverage 50%% for 100%% covered source should print nothing. want %v, got: %v", expectLen, actualLen) } + + if belowMin { + t.Errorf("2. all files were above minimum coverage but we thought one wasn't") + } }) } From 8857cfe959c7d2ad3694664a11ab0f75bb226cfb Mon Sep 17 00:00:00 2001 From: David Morrison Date: Tue, 1 Aug 2023 14:09:03 -0700 Subject: [PATCH 2/3] fail if tests do not pass --- go-carpet.go | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/go-carpet.go b/go-carpet.go index db89cec..c053339 100644 --- a/go-carpet.go +++ b/go-carpet.go @@ -429,9 +429,11 @@ func goCarpet() int { } someFileBelowMin := false + testsPass := true for _, path := range testDirs { if err = runGoTest(path, coverFileName, additionalArgs, false); err != nil { log.Print(err) + testsPass = false continue } @@ -460,7 +462,7 @@ func goCarpet() int { } } - if someFileBelowMin && config.enforceCoverage { + if !testsPass || (someFileBelowMin && config.enforceCoverage) { return 1 } From c1895968cdd330c9d051e6d3ecf9303f5b2830a9 Mon Sep 17 00:00:00 2001 From: David Morrison Date: Wed, 2 Aug 2023 10:45:01 -0700 Subject: [PATCH 3/3] allow 0% coverage --- go-carpet.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/go-carpet.go b/go-carpet.go index c053339..b24cafb 100644 --- a/go-carpet.go +++ b/go-carpet.go @@ -161,7 +161,7 @@ func getCoverForDir(coverFileName string, filesFilter []string, config Config) ( for _, fileProfile := range coverProfile { // Skip files if minimal coverage is set and is covered more than minimal coverage - if config.minCoverage > 0 && config.minCoverage < 100.0 && getStatForProfileBlocks(fileProfile.Blocks) > config.minCoverage { + if config.minCoverage >= 0 && config.minCoverage <= 100.0 && getStatForProfileBlocks(fileProfile.Blocks) >= config.minCoverage { continue }