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: 1 addition & 1 deletion internal/commands/covgate.go
Original file line number Diff line number Diff line change
Expand Up @@ -46,7 +46,7 @@ func NewCovgateCommand() *cobra.Command {
)
fl.IntVarP(
&opts.Parallelism, "parallelism", "p", 0,
"max concurrent package measurements (0 = GOMAXPROCS)",
"max concurrent package measurements (0 = NumCPU; emits progress)",
)
fl.BoolVar(
&opts.TightnessEnabled, "tightness", true,
Expand Down
2 changes: 1 addition & 1 deletion internal/commands/covratchet.go
Original file line number Diff line number Diff line change
Expand Up @@ -39,7 +39,7 @@ func NewCovratchetCommand() *cobra.Command {
)
fl.IntVarP(
&opts.Parallelism, "parallelism", "p", 0,
"max concurrent package measurements (0 = GOMAXPROCS)",
"max concurrent package measurements (0 = NumCPU; emits progress)",
)

return cmd
Expand Down
56 changes: 36 additions & 20 deletions internal/services/covgate/covgate.go
Original file line number Diff line number Diff line change
Expand Up @@ -33,36 +33,26 @@ type runner struct {
goListPackages func(string) ([]string, error)
measure func(pkg string, testPaths []string) (float64, []byte, error)
parallelism int
emitProgress bool
}

// effectiveParallelism and childGOMAXPROCS are intentionally
// duplicated in covratchet; keep them in sync.
// effectiveParallelism is intentionally duplicated in covratchet;
// keep them in sync.
func effectiveParallelism(opts Opts) int {
if opts.Parallelism > 0 {
return opts.Parallelism
}
return runtime.GOMAXPROCS(0)
}

func childGOMAXPROCS(parallelism int) int {
n := runtime.GOMAXPROCS(0) / parallelism
if n < 1 {
return 1
}
return n
return runtime.NumCPU()
}

// Run checks per-package coverage against thresholds.
func Run(opts Opts) error {
parallelism := effectiveParallelism(opts)
extraEnv := []string{fmt.Sprintf("GOMAXPROCS=%d", childGOMAXPROCS(parallelism))}
r := runner{
goModule: gocover.GoModule,
goListPackages: gocover.GoListPackages,
measure: func(pkg string, testPaths []string) (float64, []byte, error) {
return gocover.MeasureWithEnv(pkg, testPaths, extraEnv)
},
parallelism: parallelism,
measure: gocover.Measure,
parallelism: effectiveParallelism(opts),
emitProgress: opts.Parallelism == 0,
}
return r.run(opts)
}
Expand Down Expand Up @@ -99,6 +89,14 @@ func (r *runner) run(opts Opts) error {
return err
}

if r.emitProgress {
_, _ = fmt.Fprintf(
w, "Running %d packages with parallelism=%d; "+
"progress will appear as packages finish:\n",
len(pkgs), parallelism,
)
}

printHeader(w)

ctx := checkPackageCtx{
Expand All @@ -111,7 +109,7 @@ func (r *runner) run(opts Opts) error {
}

start := time.Now()
results := r.runPackages(pkgs, ctx, parallelism)
results := r.runPackages(pkgs, ctx, parallelism, w)
wallTime := time.Since(start)
return r.printResults(w, results, excluded, module, wallTime)
}
Expand Down Expand Up @@ -164,11 +162,13 @@ func (r *runner) applyExclude(
}

func (r *runner) runPackages(
pkgs []string, ctx checkPackageCtx, parallelism int,
pkgs []string, ctx checkPackageCtx, parallelism int, w io.Writer,
) []checkResult {
results := make([]checkResult, len(pkgs))
total := len(pkgs)
results := make([]checkResult, total)
sem := make(chan struct{}, parallelism)
var wg sync.WaitGroup
var progressMu sync.Mutex

for i, pkg := range pkgs {
wg.Add(1)
Expand All @@ -177,12 +177,28 @@ func (r *runner) runPackages(
defer wg.Done()
defer func() { <-sem }()
results[idx] = r.checkPackage(p, ctx)
if r.emitProgress {
progressMu.Lock()
_, _ = fmt.Fprintf(
w, "[%d/%d] %s %s %s\n",
idx+1, total,
progressStatus(results[idx].output),
gocover.RelPkg(p, ctx.module),
fmtDuration(results[idx].duration),
)
progressMu.Unlock()
}
}(i, pkg)
}
wg.Wait()
return results
}

// progressStatus extracts the first whitespace-delimited token of
// the result's first output line, which is the status keyword
// (PASS, FAIL, LOOSE) printed by checkPackage.
func progressStatus(output string) string { return strings.Fields(output)[0] }

func (r *runner) printResults(
w io.Writer,
results []checkResult,
Expand Down
101 changes: 92 additions & 9 deletions internal/services/covgate/covgate_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -437,7 +437,7 @@ func TestRun_Parallelism(t *testing.T) {
}
}

func TestRun_Parallelism_DefaultsToGOMAXPROCS(t *testing.T) {
func TestRun_Parallelism_DefaultsToNumCPU(t *testing.T) {
testutil.MakePkgDir(t, "pkg/a")

var buf bytes.Buffer
Expand All @@ -453,7 +453,7 @@ func TestRun_Parallelism_DefaultsToGOMAXPROCS(t *testing.T) {
//nolint:exhaustruct // test uses partial initialization
err := r.run(Opts{Out: &buf, DefaultThreshold: 80.0, Parallelism: 0})
if err != nil {
t.Fatalf("unexpected error with Parallelism=0 (GOMAXPROCS): %v", err)
t.Fatalf("unexpected error with Parallelism=0 (NumCPU): %v", err)
}
}

Expand All @@ -462,20 +462,103 @@ func TestEffectiveParallelism(t *testing.T) {
if got := effectiveParallelism(Opts{Parallelism: 4}); got != 4 {
t.Errorf("effectiveParallelism(4) = %d, want 4", got)
}
want := runtime.GOMAXPROCS(0)
want := runtime.NumCPU()
//nolint:exhaustruct // test uses partial initialization
if got := effectiveParallelism(Opts{Parallelism: 0}); got != want {
t.Errorf("effectiveParallelism(0) = %d, want %d", got, want)
}
}

func TestChildGOMAXPROCS(t *testing.T) {
if got := childGOMAXPROCS(1 << 30); got != 1 {
t.Errorf("childGOMAXPROCS(1<<30) = %d, want 1 (clamped)", got)
func TestRun_EmitsProgress_WhenAutoParallelism(t *testing.T) {
// Use a single temp dir so both packages share the same cwd.
tmp := t.TempDir()
t.Chdir(tmp)
for _, rel := range []string{"pkg/a", "pkg/b"} {
//nolint:gosec // G301: test directory
if err := os.MkdirAll(filepath.Join(tmp, rel), 0o755); err != nil {
t.Fatal(err)
}
}

var buf bytes.Buffer
//nolint:exhaustruct // test uses partial initialization
r := runner{
goModule: func() (string, error) { return modName, nil },
goListPackages: func(string) ([]string, error) {
return []string{modName + "/pkg/a", modName + "/pkg/b"}, nil
},
measure: fakeMeasure(90.0),
emitProgress: true,
}

//nolint:exhaustruct // test uses partial initialization
err := r.run(Opts{Out: &buf, DefaultThreshold: 80.0, Parallelism: 0})
if err != nil {
t.Fatalf("unexpected error: %v", err)
}
out := buf.String()
if !strings.Contains(out, "[1/2]") {
t.Errorf("output missing [1/2] progress line:\n%s", out)
}
if !strings.Contains(out, "[2/2]") {
t.Errorf("output missing [2/2] progress line:\n%s", out)
}
// Leading announcement should appear before the STATUS header.
idxAnnounce := strings.Index(out, "Running 2 packages with parallelism=")
idxHeader := strings.Index(out, "STATUS")
if idxAnnounce < 0 || idxHeader < 0 || idxAnnounce >= idxHeader {
t.Errorf(
"expected leading announcement before STATUS header "+
"(announce=%d, header=%d):\n%s",
idxAnnounce, idxHeader, out,
)
}
// Both progress lines should appear before the final
// "All packages meet" line that closes the table.
idxProgress1 := strings.Index(out, "[1/2]")
idxProgress2 := strings.Index(out, "[2/2]")
idxFinal := strings.Index(out, "All packages meet")
if idxProgress1 >= idxFinal || idxProgress2 >= idxFinal {
t.Errorf(
"expected progress lines before final summary "+
"(p1=%d, p2=%d, final=%d):\n%s",
idxProgress1, idxProgress2, idxFinal, out,
)
}
}

func TestRun_SuppressesProgress_WhenExplicitParallelism(t *testing.T) {
tmp := t.TempDir()
t.Chdir(tmp)
for _, rel := range []string{"pkg/a", "pkg/b"} {
//nolint:gosec // G301: test directory
if err := os.MkdirAll(filepath.Join(tmp, rel), 0o755); err != nil {
t.Fatal(err)
}
}

var buf bytes.Buffer
//nolint:exhaustruct // test uses partial initialization
r := runner{
goModule: func() (string, error) { return modName, nil },
goListPackages: func(string) ([]string, error) {
return []string{modName + "/pkg/a", modName + "/pkg/b"}, nil
},
measure: fakeMeasure(90.0),
emitProgress: false,
}

//nolint:exhaustruct // test uses partial initialization
err := r.run(Opts{Out: &buf, DefaultThreshold: 80.0, Parallelism: 2})
if err != nil {
t.Fatalf("unexpected error: %v", err)
}
out := buf.String()
if strings.Contains(out, "[1/2]") || strings.Contains(out, "[2/2]") {
t.Errorf("output unexpectedly contains progress prefix:\n%s", out)
}
want := runtime.GOMAXPROCS(0)
if got := childGOMAXPROCS(1); got != want {
t.Errorf("childGOMAXPROCS(1) = %d, want %d", got, want)
if strings.Contains(out, "progress will appear") {
t.Errorf("output unexpectedly contains leading announcement:\n%s", out)
}
}

Expand Down
94 changes: 64 additions & 30 deletions internal/services/covratchet/covratchet.go
Original file line number Diff line number Diff line change
Expand Up @@ -26,36 +26,26 @@ type runner struct {
goListPackages func(string) ([]string, error)
measure func(pkg string, testPaths []string) (float64, []byte, error)
parallelism int
emitProgress bool
}

// effectiveParallelism and childGOMAXPROCS are intentionally
// duplicated in covgate; keep them in sync.
// effectiveParallelism is intentionally duplicated in covgate;
// keep them in sync.
func effectiveParallelism(opts Opts) int {
if opts.Parallelism > 0 {
return opts.Parallelism
}
return runtime.GOMAXPROCS(0)
}

func childGOMAXPROCS(parallelism int) int {
n := runtime.GOMAXPROCS(0) / parallelism
if n < 1 {
return 1
}
return n
return runtime.NumCPU()
}

// Run ratchets up .covgate thresholds.
func Run(opts Opts) error {
parallelism := effectiveParallelism(opts)
extraEnv := []string{fmt.Sprintf("GOMAXPROCS=%d", childGOMAXPROCS(parallelism))}
r := runner{
goModule: gocover.GoModule,
goListPackages: gocover.GoListPackages,
measure: func(pkg string, testPaths []string) (float64, []byte, error) {
return gocover.MeasureWithEnv(pkg, testPaths, extraEnv)
},
parallelism: parallelism,
measure: gocover.Measure,
parallelism: effectiveParallelism(opts),
emitProgress: opts.Parallelism == 0,
}
return r.run(opts)
}
Expand Down Expand Up @@ -84,22 +74,22 @@ func (r *runner) run(opts Opts) error {
return err
}

printHeader(w)
if r.emitProgress {
_, _ = fmt.Fprintf(
w, "Running %d packages with parallelism=%d; "+
"progress will appear as packages finish:\n",
len(pkgs), parallelism,
)
}

results := make([]ratchetResult, len(pkgs))
sem := make(chan struct{}, parallelism)
var wg sync.WaitGroup
printHeader(w)

for i, pkg := range pkgs {
wg.Add(1)
sem <- struct{}{}
go func(idx int, p string) {
defer wg.Done()
defer func() { <-sem }()
results[idx] = r.ratchetPackage(p, module, opts.SrcPrefix, opts.TestDir)
}(i, pkg)
ctx := ratchetCtx{
module: module,
srcPrefix: opts.SrcPrefix,
testDir: opts.TestDir,
}
wg.Wait()
results := r.runPackages(pkgs, ctx, parallelism, w)

updated := 0
unchanged := 0
Expand Down Expand Up @@ -133,6 +123,50 @@ func printHeader(w io.Writer) {
)
}

// ratchetCtx holds the per-run constants threaded into ratchetPackage.
type ratchetCtx struct {
module string
srcPrefix string
testDir string
}

func (r *runner) runPackages(
pkgs []string, ctx ratchetCtx, parallelism int, w io.Writer,
) []ratchetResult {
total := len(pkgs)
results := make([]ratchetResult, total)
sem := make(chan struct{}, parallelism)
var wg sync.WaitGroup
var progressMu sync.Mutex

for i, pkg := range pkgs {
wg.Add(1)
sem <- struct{}{}
go func(idx int, p string) {
defer wg.Done()
defer func() { <-sem }()
results[idx] = r.ratchetPackage(p, ctx.module, ctx.srcPrefix, ctx.testDir)
if r.emitProgress {
progressMu.Lock()
_, _ = fmt.Fprintf(
w, "[%d/%d] %s %s\n",
idx+1, total,
progressStatus(results[idx].output),
gocover.RelPkg(p, ctx.module),
)
progressMu.Unlock()
}
}(i, pkg)
}
wg.Wait()
return results
}

// progressStatus extracts the first whitespace-delimited token of
// the result's first output line, which is the status keyword
// (NEW, UP, OK, FAIL) printed by ratchetPackage.
func progressStatus(output string) string { return strings.Fields(output)[0] }

// ratchetResult holds the output and counts for a single package ratchet.
type ratchetResult struct {
output string
Expand Down
Loading