Skip to content
2 changes: 1 addition & 1 deletion internal/commands/covgate.go
Original file line number Diff line number Diff line change
Expand Up @@ -42,7 +42,7 @@ func NewCovgateCommand() *cobra.Command {
)
fl.IntVarP(
&opts.Parallelism, "parallelism", "p", 0,
"max concurrent package measurements (0 = NumCPU)",
"max concurrent package measurements (0 = GOMAXPROCS)",
)
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 = NumCPU)",
"max concurrent package measurements (0 = GOMAXPROCS)",
)

return cmd
Expand Down
29 changes: 26 additions & 3 deletions internal/services/covgate/covgate.go
Original file line number Diff line number Diff line change
Expand Up @@ -28,14 +28,37 @@ type runner struct {
goModule func() (string, error)
goListPackages func(string) ([]string, error)
measure func(pkg string, testPaths []string) (float64, []byte, error)
parallelism int
}

// effectiveParallelism and childGOMAXPROCS are 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
}

// 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: gocover.Measure,
measure: func(pkg string, testPaths []string) (float64, []byte, error) {
return gocover.MeasureWithEnv(pkg, testPaths, extraEnv)
},
parallelism: parallelism,
}
return r.run(opts)
}
Expand All @@ -46,9 +69,9 @@ func (r *runner) run(opts Opts) error {
}
w := opts.Out

parallelism := opts.Parallelism
parallelism := r.parallelism
if parallelism <= 0 {
parallelism = runtime.NumCPU()
parallelism = effectiveParallelism(opts)
}

_, _ = fmt.Fprintf(
Expand Down
75 changes: 73 additions & 2 deletions internal/services/covgate/covgate_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@ import (
"fmt"
"os"
"path/filepath"
"runtime"
"strings"
"testing"
"time"
Expand Down Expand Up @@ -215,7 +216,7 @@ func TestRun_Parallelism(t *testing.T) {
}
}

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

var buf bytes.Buffer
Expand All @@ -231,7 +232,29 @@ func TestRun_Parallelism_DefaultsToNumCPU(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 (NumCPU): %v", err)
t.Fatalf("unexpected error with Parallelism=0 (GOMAXPROCS): %v", err)
}
}

func TestEffectiveParallelism(t *testing.T) {
//nolint:exhaustruct // test uses partial initialization
if got := effectiveParallelism(Opts{Parallelism: 4}); got != 4 {
t.Errorf("effectiveParallelism(4) = %d, want 4", got)
}
want := runtime.GOMAXPROCS(0)
//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)
}
want := runtime.GOMAXPROCS(0)
if got := childGOMAXPROCS(1); got != want {
t.Errorf("childGOMAXPROCS(1) = %d, want %d", got, want)
}
}

Expand Down Expand Up @@ -338,6 +361,54 @@ func TestRun_PublicWrapper(t *testing.T) {
}
}

func TestRun_PublicWrapper_HappyPath(t *testing.T) {
tmp := t.TempDir()
t.Chdir(tmp)

goMod := "module testmod\n\ngo 1.23\n"
//nolint:gosec // G306: test file
err := os.WriteFile(filepath.Join(tmp, "go.mod"), []byte(goMod), 0o644)
if err != nil {
t.Fatal(err)
}
pkg := filepath.Join(tmp, "mypkg")
//nolint:gosec // G301: test directory
if err := os.MkdirAll(pkg, 0o755); err != nil {
t.Fatal(err)
}
lib := "package mypkg\n\n" +
"func Add(a, b int) int { return a + b }\n"
//nolint:gosec // G306: test file
err = os.WriteFile(filepath.Join(pkg, "lib.go"), []byte(lib), 0o644)
if err != nil {
t.Fatal(err)
}
testSrc := "package mypkg\n\n" +
"import \"testing\"\n\n" +
"func TestAdd(t *testing.T) {\n" +
"\tif Add(1, 2) != 3 { t.Fatal(\"Add broken\") }\n}\n"
//nolint:gosec // G306: test file
err = os.WriteFile(filepath.Join(pkg, "lib_test.go"), []byte(testSrc), 0o644)
if err != nil {
t.Fatal(err)
}

var buf bytes.Buffer
//nolint:exhaustruct // test uses partial initialization
err = Run(Opts{
Packages: "testmod/...",
DefaultThreshold: 80.0,
Out: &buf,
Parallelism: 1,
})
if err != nil {
t.Fatalf("Run: %v\n%s", err, buf.String())
}
if !strings.Contains(buf.String(), "All packages meet") {
t.Errorf("missing success msg: %s", buf.String())
}
}

func TestRun_GoModuleError(t *testing.T) {
var buf bytes.Buffer
//nolint:exhaustruct // test uses partial initialization
Expand Down
2 changes: 1 addition & 1 deletion internal/services/covratchet/.covgate
Original file line number Diff line number Diff line change
@@ -1 +1 @@
97.4
99.5
29 changes: 26 additions & 3 deletions internal/services/covratchet/covratchet.go
Original file line number Diff line number Diff line change
Expand Up @@ -25,14 +25,37 @@ type runner struct {
goModule func() (string, error)
goListPackages func(string) ([]string, error)
measure func(pkg string, testPaths []string) (float64, []byte, error)
parallelism int
}

// effectiveParallelism and childGOMAXPROCS are 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
}

// 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: gocover.Measure,
measure: func(pkg string, testPaths []string) (float64, []byte, error) {
return gocover.MeasureWithEnv(pkg, testPaths, extraEnv)
},
parallelism: parallelism,
}
return r.run(opts)
}
Expand All @@ -43,9 +66,9 @@ func (r *runner) run(opts Opts) error {
}
w := opts.Out

parallelism := opts.Parallelism
parallelism := r.parallelism
if parallelism <= 0 {
parallelism = runtime.NumCPU()
parallelism = effectiveParallelism(opts)
}

_, _ = fmt.Fprintln(w, "Updating .covgate files (ratchet up only)...")
Expand Down
70 changes: 68 additions & 2 deletions internal/services/covratchet/covratchet_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@ import (
"fmt"
"os"
"path/filepath"
"runtime"
"strings"
"testing"

Expand Down Expand Up @@ -386,7 +387,7 @@ func TestRun_Parallelism(t *testing.T) {
}
}

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

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

func TestEffectiveParallelism(t *testing.T) {
//nolint:exhaustruct // test uses partial initialization
if got := effectiveParallelism(Opts{Parallelism: 4}); got != 4 {
t.Errorf("effectiveParallelism(4) = %d, want 4", got)
}
want := runtime.GOMAXPROCS(0)
//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)
}
want := runtime.GOMAXPROCS(0)
if got := childGOMAXPROCS(1); got != want {
t.Errorf("childGOMAXPROCS(1) = %d, want %d", got, want)
}
}

func TestRun_PublicWrapper_HappyPath(t *testing.T) {
tmp := t.TempDir()
t.Chdir(tmp)

goMod := "module testmod\n\ngo 1.23\n"
//nolint:gosec // G306: test file
err := os.WriteFile(filepath.Join(tmp, "go.mod"), []byte(goMod), 0o644)
if err != nil {
t.Fatal(err)
}
pkg := filepath.Join(tmp, "mypkg")
//nolint:gosec // G301: test directory
if err := os.MkdirAll(pkg, 0o755); err != nil {
t.Fatal(err)
}
lib := "package mypkg\n\n" +
"func Add(a, b int) int { return a + b }\n"
//nolint:gosec // G306: test file
err = os.WriteFile(filepath.Join(pkg, "lib.go"), []byte(lib), 0o644)
if err != nil {
t.Fatal(err)
}
testSrc := "package mypkg\n\n" +
"import \"testing\"\n\n" +
"func TestAdd(t *testing.T) {\n" +
"\tif Add(1, 2) != 3 { t.Fatal(\"Add broken\") }\n}\n"
//nolint:gosec // G306: test file
err = os.WriteFile(filepath.Join(pkg, "lib_test.go"), []byte(testSrc), 0o644)
if err != nil {
t.Fatal(err)
}

var buf bytes.Buffer
//nolint:exhaustruct // test uses partial initialization
err = Run(Opts{Packages: "testmod/...", Out: &buf, Parallelism: 1})
if err != nil {
t.Fatalf("Run: %v\n%s", err, buf.String())
}
if !strings.Contains(buf.String(), "Done.") {
t.Errorf("missing 'Done.' summary: %s", buf.String())
}
}

Expand Down
15 changes: 15 additions & 0 deletions internal/services/gocover/gocover.go
Original file line number Diff line number Diff line change
Expand Up @@ -131,6 +131,18 @@ func BuildTestPaths(pkg, relPkg, srcPrefix, testDir string) []string {
// Uses a temp file for the coverage profile, cleaned up
// automatically.
func Measure(pkg string, testPaths []string) (float64, []byte, error) {
return MeasureWithEnv(pkg, testPaths, nil)
}

// MeasureWithEnv is like Measure but appends extraEnv to
// the test subprocess environment when non-empty.
// Entries in extraEnv override earlier definitions of the
// same key (os/exec uses last-wins).
// This lets callers inject variables such as GOMAXPROCS
// into the child go test invocation.
func MeasureWithEnv(
pkg string, testPaths []string, extraEnv []string,
) (float64, []byte, error) {
tmpFile, err := os.CreateTemp("", "miru-coverage-*.out")
if err != nil {
return 0, nil, fmt.Errorf("create temp file: %w", err)
Expand All @@ -144,6 +156,9 @@ func Measure(pkg string, testPaths []string) (float64, []byte, error) {
args = append(args, testPaths...)

testCmd := cmdutil.GoCommand(args...)
if len(extraEnv) > 0 {
testCmd.Env = append(testCmd.Env, extraEnv...)
}
output, testErr := testCmd.CombinedOutput()
if testErr != nil {
return 0, output, testErr
Expand Down
16 changes: 16 additions & 0 deletions internal/services/gocover/gocover_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -288,6 +288,22 @@ func TestMeasure(t *testing.T) {
}
}

func TestMeasureWithEnv_AppliesExtraEnv(t *testing.T) {
makeGoProject(t)

cov, _, err := MeasureWithEnv(
"testmod/mypkg",
[]string{"testmod/mypkg"},
[]string{"GOMAXPROCS=1"},
)
if err != nil {
t.Fatalf("MeasureWithEnv: %v", err)
}
if cov < 100.0 {
t.Errorf("expected 100%%, got %.1f%%", cov)
}
}

func TestMeasure_TestFailure(t *testing.T) {
tmp := t.TempDir()
t.Chdir(tmp)
Expand Down
Loading