Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 2 additions & 0 deletions internal/commands/commands_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -341,6 +341,7 @@ func TestNewCovgateCommand_Flags(t *testing.T) {
wantType string
}{
{"packages", "string"},
{"exclude", "string"},
{"src-prefix", "string"},
{"test-dir", "string"},
{"default-threshold", "float64"},
Expand All @@ -365,6 +366,7 @@ func TestNewCovgateCommand_FlagDefaults(t *testing.T) {

stringDefaults := map[string]string{
"packages": "./...",
"exclude": "",
"src-prefix": "pkg",
"test-dir": "",
}
Expand Down
4 changes: 4 additions & 0 deletions internal/commands/covgate.go
Original file line number Diff line number Diff line change
Expand Up @@ -28,6 +28,10 @@ func NewCovgateCommand() *cobra.Command {

fl := cmd.Flags()
fl.StringVar(&opts.Packages, "packages", "./...", "Go package pattern for go list")
fl.StringVar(
&opts.Exclude, "exclude", "",
"comma-separated list of Go list patterns to exclude from coverage measurement",
)
fl.StringVar(
&opts.SrcPrefix, "src-prefix", "pkg",
"source prefix for mapping external test dirs",
Expand Down
55 changes: 54 additions & 1 deletion internal/services/covgate/covgate.go
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,11 @@ import (

// Opts holds the options for the covgate service.
type Opts struct {
Packages string
Packages string
// Exclude is a comma-separated list of Go list patterns whose
// matched import paths are removed from the measurement set
// before tests run. Empty means no exclusion.
Exclude string
SrcPrefix string
TestDir string
DefaultThreshold float64
Expand Down Expand Up @@ -90,6 +94,11 @@ func (r *runner) run(opts Opts) error {
return err
}

pkgs, err = r.applyExclude(pkgs, opts.Exclude, w)
if err != nil {
return err
}

printHeader(w)

ctx := checkPackageCtx{
Expand All @@ -107,6 +116,50 @@ func (r *runner) run(opts Opts) error {
return r.printResults(w, results, wallTime)
}

// applyExclude removes packages matched by the comma-separated
// exclude patterns from pkgs. It preserves the original order of
// pkgs and prints a one-line notice to w when any package is
// actually removed. An empty (or whitespace-only) exclude string
// returns pkgs unchanged with no output.
func (r *runner) applyExclude(
pkgs []string, exclude string, w io.Writer,
) ([]string, error) {
if strings.TrimSpace(exclude) == "" {
return pkgs, nil
}

excluded := make(map[string]struct{})
for _, raw := range strings.Split(exclude, ",") {
entry := strings.TrimSpace(raw)
if entry == "" {
continue
}
matched, err := r.goListPackages(entry)
if err != nil {
return nil, fmt.Errorf("resolve exclude %q: %w", entry, err)
}
for _, p := range matched {
excluded[p] = struct{}{}
}
}

kept := make([]string, 0, len(pkgs))
for _, p := range pkgs {
if _, drop := excluded[p]; drop {
continue
}
kept = append(kept, p)
}

if removed := len(pkgs) - len(kept); removed > 0 {
_, _ = fmt.Fprintf(
w, "Excluded %d package(s) from coverage measurement\n",
removed,
)
}
return kept, nil
}

func (r *runner) runPackages(
pkgs []string, ctx checkPackageCtx, parallelism int,
) []checkResult {
Expand Down
152 changes: 152 additions & 0 deletions internal/services/covgate/covgate_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -164,6 +164,158 @@ func TestRun_WithFailure(t *testing.T) {
}
}

func TestRun_Exclude(t *testing.T) {
const (
pkgA = "example.com/mod/pkg/a"
pkgB = "example.com/mod/pkg/b"
pkgC = "example.com/mod/pkg/c"
)
allThree := []string{pkgA, pkgB, pkgC}

cases := []struct {
name string
exclude string
lookup map[string][]string
wantContains []string
wantNotContain []string
}{
{
name: "NoExclude",
exclude: "",
lookup: map[string][]string{"./...": allThree},
wantContains: []string{"pkg/a", "pkg/b", "pkg/c"},
wantNotContain: []string{"Excluded"},
},
{
name: "Subset",
exclude: "./pkg/b",
lookup: map[string][]string{"./...": allThree, "./pkg/b": {pkgB}},
wantContains: []string{
"pkg/a",
"pkg/c",
"Excluded 1 package(s) from coverage measurement",
},
wantNotContain: []string{"pkg/b"},
},
{
name: "NoOpPattern",
exclude: "./does-not-exist/...",
lookup: map[string][]string{
"./...": allThree,
"./does-not-exist/...": {},
},
wantContains: []string{"pkg/a", "pkg/b", "pkg/c"},
wantNotContain: []string{"Excluded"},
},
{
// Includes empty entries before, between, and after
// non-empty ones so the trim/skip-empty path is
// exercised end-to-end.
name: "MultiplePatternsWithWhitespace",
exclude: ", ./pkg/a, , ./pkg/c,",
lookup: map[string][]string{
"./...": allThree,
"./pkg/a": {pkgA},
"./pkg/c": {pkgC},
},
wantContains: []string{
"pkg/b",
"Excluded 2 package(s) from coverage measurement",
},
wantNotContain: []string{"pkg/a", "pkg/c"},
},
{
name: "AllPackages",
exclude: "./...",
lookup: map[string][]string{"./...": allThree},
wantContains: []string{
"Excluded 3 package(s) from coverage measurement",
"All packages meet minimum coverage requirement",
"Total time:",
},
wantNotContain: []string{"pkg/a", "pkg/b", "pkg/c"},
},
}

for _, tc := range cases {
t.Run(tc.name, func(t *testing.T) {
lookup := tc.lookup
var buf bytes.Buffer
//nolint:exhaustruct // test uses partial initialization
r := runner{
goModule: func() (string, error) { return modName, nil },
goListPackages: func(pattern string) ([]string, error) {
out, ok := lookup[pattern]
if !ok {
return nil, fmt.Errorf("unexpected pattern %q", pattern)
}
return out, nil
},
measure: fakeMeasure(90.0),
}

//nolint:exhaustruct // test uses partial initialization
err := r.run(Opts{
Out: &buf,
DefaultThreshold: 80.0,
Packages: "./...",
Exclude: tc.exclude,
})
if err != nil {
t.Fatalf("unexpected error: %v", err)
}

out := buf.String()
for _, want := range tc.wantContains {
if !strings.Contains(out, want) {
t.Errorf("output missing %q:\n%s", want, out)
}
}
for _, unwanted := range tc.wantNotContain {
if strings.Contains(out, unwanted) {
t.Errorf("output unexpectedly contains %q:\n%s", unwanted, out)
}
}
})
}
}

func TestRun_Exclude_GoListError(t *testing.T) {
var buf bytes.Buffer
//nolint:exhaustruct // test uses partial initialization
r := runner{
goModule: func() (string, error) { return modName, nil },
goListPackages: func(pattern string) ([]string, error) {
if pattern == "./..." {
return []string{"example.com/mod/pkg/a"}, nil
}
return nil, fmt.Errorf("list failed")
},
measure: fakeMeasure(90.0),
}

//nolint:exhaustruct // test uses partial initialization
err := r.run(Opts{
Out: &buf,
DefaultThreshold: 80.0,
Packages: "./...",
Exclude: "./pkg/bogus",
})
if err == nil {
t.Fatal("expected error from exclude pattern resolution")
}
msg := err.Error()
if !strings.Contains(msg, "./pkg/bogus") {
t.Errorf("error missing pattern %q: %v", "./pkg/bogus", err)
}
if !strings.Contains(msg, "list failed") {
t.Errorf("error missing wrapped cause %q: %v", "list failed", err)
}
if !strings.Contains(msg, "resolve exclude") {
t.Errorf("error missing context prefix %q: %v", "resolve exclude", err)
}
}

func TestRun_Parallelism(t *testing.T) {
// Use a single temp dir so all three packages share the same cwd.
tmp := t.TempDir()
Expand Down
Loading