diff --git a/.github/workflows/test-core.yaml b/.github/workflows/test-core.yaml index 91ec0c464..18f6def30 100644 --- a/.github/workflows/test-core.yaml +++ b/.github/workflows/test-core.yaml @@ -42,6 +42,7 @@ jobs: cache-key-suffix: -checks - name: Run make check run: | + make missing make bootstrap make check compile: @@ -87,42 +88,51 @@ jobs: fail-fast: false matrix: pkg: - - acl + - acl/... - client - - client/allocdir - - client/allochealth - - client/allocrunner - - client/allocwatcher - - client/config - - client/consul - - client/devicemanager - - client/dynamicplugins - - client/fingerprint + - client/allocdir/... + - client/allochealth/... + - client/allocrunner/... + - client/allocwatcher/... + - client/config/... + - client/consul/... + - client/devicemanager/... + - client/dynamicplugins/... + - client/fingerprint/... + - client/interfaces/... - client/lib/... - - client/logmon - - client/pluginmanager - - client/state - - client/stats - - client/structs - - client/taskenv + - client/logmon/... + - client/pluginmanager/... + - client/servers/... + - client/serviceregistration/... + - client/state/... + - client/stats/... + - client/structs/... + - client/taskenv/... - command - - command/agent - - drivers/docker - - drivers/exec - - drivers/java - - drivers/rawexec + - command/agent/... + - command/raft_tools/... + - drivers/docker/... + - drivers/exec/... + - drivers/java/... + - drivers/mock/... + - drivers/rawexec/... + - drivers/shared/... + - drivers/qemu/... - helper/... - internal/... - jobspec/... - lib/... - nomad - - nomad/deploymentwatcher - - nomad/stream - - nomad/structs - - nomad/volumewatcher + - nomad/deploymentwatcher/... + - nomad/drainer/... + - nomad/state/... + - nomad/stream/... + - nomad/structs/... + - nomad/volumewatcher/... - plugins/... - scheduler/... - - testutil + - testutil/... steps: - uses: actions/checkout@v2 - uses: magnetikonline/action-golang-cache@v1 diff --git a/GNUmakefile b/GNUmakefile index 3f841e489..d06334e5b 100644 --- a/GNUmakefile +++ b/GNUmakefile @@ -410,6 +410,7 @@ ui-screenshots-local: @echo "==> Collecting UI screenshots (local)..." @cd scripts/screenshots/src && SCREENSHOTS_DIR="../screenshots" node index.js +.PHONY: version version: ifneq (,$(wildcard version/version_ent.go)) @$(CURDIR)/scripts/version.sh version/version.go version/version_ent.go @@ -417,4 +418,8 @@ else @$(CURDIR)/scripts/version.sh version/version.go version/version.go endif -.PHONY: version +.PHONY: missing +missing: + @echo "==> Checking for packages not being tested ..." + @go run -modfile tools/go.mod tools/missing/main.go \ + .github/workflows/test-core.yaml diff --git a/tools/go.mod b/tools/go.mod index 7f48040c0..dd1d3a7f9 100644 --- a/tools/go.mod +++ b/tools/go.mod @@ -2,6 +2,16 @@ module github.com/hashicorp/nomad/tools go 1.17 -require github.com/aws/aws-sdk-go v1.37.26 +require ( + github.com/aws/aws-sdk-go v1.37.26 + github.com/stretchr/testify v1.7.1 + gophers.dev/pkgs/ignore v0.3.1 + gopkg.in/yaml.v2 v2.2.8 +) -require github.com/jmespath/go-jmespath v0.4.0 // indirect +require ( + github.com/davecgh/go-spew v1.1.0 // indirect + github.com/jmespath/go-jmespath v0.4.0 // indirect + github.com/pmezard/go-difflib v1.0.0 // indirect + gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c // indirect +) diff --git a/tools/go.sum b/tools/go.sum index c3efdec0f..6a3300de7 100644 --- a/tools/go.sum +++ b/tools/go.sum @@ -10,6 +10,8 @@ github.com/pkg/errors v0.9.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINE github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= +github.com/stretchr/testify v1.7.1 h1:5TQK59W5E3v0r2duFAb7P95B6hEeOyEnHRa8MjYSMTY= +github.com/stretchr/testify v1.7.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w= golang.org/x/crypto v0.0.0-20200622213623-75b288015ac9/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto= golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= @@ -20,6 +22,11 @@ golang.org/x/sys v0.0.0-20200930185726-fdedc70b468f/go.mod h1:h1NjWce9XRLGQEsW7w golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= +gophers.dev/pkgs/ignore v0.3.1 h1:MmNywpk5VAxWQ7/Yz9E1NmEEHLTh0hRWLiHENdVy4Ls= +gophers.dev/pkgs/ignore v0.3.1/go.mod h1:HwTn4Fc9oicp2TxFV/8P0CswwfsVQczZvG6ZTy2TRXA= +gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405 h1:yhCVgyC4o1eVCa2tZl7eS0r+SDo693bJlVdllGtEeKM= gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= gopkg.in/yaml.v2 v2.2.8 h1:obN1ZagJSUGI0Ek/LBmuj4SNLPfIny3KsKFopxRdj10= gopkg.in/yaml.v2 v2.2.8/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= +gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c h1:dUUwHk2QECo/6vqA44rthZ8ie2QXMNeKRTHCNY2nXvo= +gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= diff --git a/tools/missing/main.go b/tools/missing/main.go new file mode 100644 index 000000000..f13413f7b --- /dev/null +++ b/tools/missing/main.go @@ -0,0 +1,178 @@ +package main + +import ( + "errors" + "fmt" + "io" + "io/fs" + "os" + "path/filepath" + "sort" + "strings" + + "gophers.dev/pkgs/ignore" + "gopkg.in/yaml.v2" +) + +func main() { + if err := run(os.Args[1:]); err != nil { + _, _ = fmt.Fprintf(os.Stderr, "FAIL: %s\n", err) + os.Exit(1) + } +} + +type YamlFile struct { + Jobs struct { + TestPackages struct { + Strategy struct { + Matrix struct { + Packages []string `yaml:"pkg"` + } `yaml:"matrix"` + } `yaml:"strategy"` + } `yaml:"tests-pkgs"` + } `yaml:"jobs"` +} + +func run(args []string) error { + if len(args) != 1 { + return errors.New("requires filename") + } + + f, err := os.Open(args[0]) + if err != nil { + return err + } + defer ignore.Close(f) + + coverage, err := inMatrix(f) + if err != nil { + return err + } + + packages, err := inCode(".") + if err != nil { + return err + } + + var isMissing []string + for _, pkg := range packages { + if !isCovered(coverage, pkg) { + isMissing = append(isMissing, pkg) + } + } + + sort.Strings(isMissing) + for _, pkg := range isMissing { + _, _ = fmt.Fprintf(os.Stderr, "missing: %s\n", pkg) + } + + if len(isMissing) > 0 { + return fmt.Errorf("detected %d packages not tested", len(isMissing)) + } + + return nil +} + +// isCovered returns true if pkg is covered by a package in coverage. +func isCovered(coverage []string, pkg string) bool { + for _, p := range coverage { + if isCoveredOne(p, pkg) { + return true + } + } + return false +} + +// isCoveredOne returns true if p covers pkg. +// +// p may be a complete path, or a prefix ending with recursive '...' +func isCoveredOne(p string, pkg string) bool { + if p == pkg { + return true + } + + if strings.HasSuffix(p, "/...") { + prefix := strings.TrimSuffix(p, "/...") + if strings.HasPrefix(pkg, prefix) { + return true + } + } + return false +} + +func inMatrix(r io.Reader) ([]string, error) { + var yFile YamlFile + if err := yaml.NewDecoder(r).Decode(&yFile); err != nil { + return nil, err + } + p := yFile.Jobs.TestPackages.Strategy.Matrix.Packages + return p, nil +} + +type nothing struct{} + +var null = nothing{} + +// uninteresting lists remaining packages that contain Go code but still +// do not need to be covered by test cases. +var uninteresting = []string{ + // module + "api", + + // main + ".", + + // testing helpers + "ci", + "client/testutil", + "client/vaultclient", + "e2e", + "nomad/mock", + "plugins/csi/fake", + + // not core code + "demo", + "tools", + "version", +} + +func skip(p string) bool { + for _, prefix := range uninteresting { + if strings.HasPrefix(p, prefix) { + return true + } + } + return false +} + +func inCode(root string) ([]string, error) { + m := map[string]nothing{} + + err := filepath.Walk(root, func(path string, info fs.FileInfo, err error) error { + if info.IsDir() { + return nil + } + + if skip(path) { + return nil + } + + if ext := filepath.Ext(path); ext == ".go" { + m[filepath.Dir(path)] = null + } + + return nil + }) + if err != nil { + return nil, err + } + + delete(m, ".") // package main + + var packages []string + for p := range m { + packages = append(packages, p) + } + sort.Strings(packages) + return packages, nil +} diff --git a/tools/missing/main_test.go b/tools/missing/main_test.go new file mode 100644 index 000000000..7b405b258 --- /dev/null +++ b/tools/missing/main_test.go @@ -0,0 +1,21 @@ +package main + +import ( + "testing" + + "github.com/stretchr/testify/require" +) + +func Test_isCoveredOne(t *testing.T) { + try := func(p string, exp bool) { + result := isCoveredOne(p, "foo/bar") + require.Equal(t, exp, result) + } + try("baz", false) + try("foo", false) + try("foo/bar/baz", false) + try("foo/bar", true) + try("foo/bar/...", true) + try("foo/...", true) + try("abc/...", false) +}