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
1 change: 1 addition & 0 deletions cmd/mutate/main.go
Original file line number Diff line number Diff line change
Expand Up @@ -83,6 +83,7 @@ func main() {
os.Exit(2)
}

mutate.Share(*jobs)
started := time.Now()
t := runAll(chosen, importers, *timeout, *jobs)
reportCosts(time.Since(started))
Expand Down
1 change: 1 addition & 0 deletions internal/mutate/cover.go
Original file line number Diff line number Diff line change
Expand Up @@ -110,6 +110,7 @@ func Cover(dir, timeout string, slots chan struct{}) (*Coverage, error) {
run := exec.Command(bin, "-test.run", "^"+test+"$", "-test.count=1", //nolint:gosec // the binary this built
"-test.timeout", timeout, "-test.coverprofile", profile)
run.Dir = dir
run.Env = testEnv()
// A failure here is the package's own tests' to report, not this gate's; what this
// gate does with it is ask the test about no edit, since it fails whatever the edit -
// and say so, since an edit only it reaches then reads as one nothing holds.
Expand Down
1 change: 1 addition & 0 deletions internal/mutate/mutate.go
Original file line number Diff line number Diff line change
Expand Up @@ -719,6 +719,7 @@ func Run(m Mutation, a Asked) (Outcome, string) {
}
cmd := exec.Command("go", args...) //nolint:gosec // go test over this repository's packages
cmd.Dir = set.dir
cmd.Env = testEnv()
out, err := combined(set.what, cmd)
said.Write(out)
if err == nil {
Expand Down
2 changes: 1 addition & 1 deletion internal/mutate/schema.go
Original file line number Diff line number Diff line change
Expand Up @@ -378,7 +378,7 @@ func Ask(bin, dir string, id int, timeout string, only []string) (Outcome, strin
}
cmd := exec.Command(bin, args...) //nolint:gosec // a test binary this run built
cmd.Dir = dir
cmd.Env = append(os.Environ(), switchEnv+"="+strconv.Itoa(id))
cmd.Env = testEnv(switchEnv + "=" + strconv.Itoa(id))
out, err := combined("schema: run one edit", cmd)
if err != nil {
return Killed, string(out)
Expand Down
29 changes: 29 additions & 0 deletions internal/mutate/share.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,29 @@
package mutate

import (
"os"
"runtime"
"strconv"
)

// testProcs is the GOMAXPROCS of every test process a run starts; 0 leaves each the machine's.
// A test binary takes every core by default, and so does each of its t.Parallel tests: four
// mutations of the control plane's app package asked at once were nineteen processes running and
// a load of 24 on a 20-core machine, though they used a fraction of its CPU.
var testProcs int

// Share divides the machine among jobs test processes run at once, each held to its share of the
// cores. Call it once, before anything is asked.
func Share(jobs int) {
testProcs = max(runtime.NumCPU()/max(jobs, 1), 1)
}

// testEnv is the environment a test process runs in: this one's, its share of the machine, and
// extra.
func testEnv(extra ...string) []string {
env := os.Environ()
if testProcs > 0 {
env = append(env, "GOMAXPROCS="+strconv.Itoa(testProcs))
}
return append(env, extra...)
}
89 changes: 89 additions & 0 deletions internal/mutate/share_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,89 @@
package mutate

import (
"os"
"os/exec"
"path/filepath"
"runtime"
"slices"
"testing"
)

// oneProc is a module whose one test passes only in a process held to one core, which is what
// Share(more jobs than cores) gives every test process.
func oneProc(t *testing.T) string {
t.Helper()
dir := t.TempDir()
for name, body := range map[string]string{
"go.mod": "module example.com/oneproc\n\ngo 1.24\n",
"code.go": "package oneproc\n\nfunc One() int {\n\treturn 1\n}\n",
"code_test.go": "package oneproc\n\nimport (\n\t\"runtime\"\n\t\"testing\"\n)\n\n" +
"func TestHeldToOneCore(t *testing.T) {\n\tif runtime.GOMAXPROCS(0) != 1 || One() != 1 {\n\t\tt.Fatal(\"not held to one core\")\n\t}\n}\n",
} {
if err := os.WriteFile(filepath.Join(dir, name), []byte(body), 0o600); err != nil {
t.Fatal(err)
}
}
return dir
}

// share is Share for one test, the machine's again after it.
func share(t *testing.T, jobs int) {
t.Helper()
Share(jobs)
t.Cleanup(func() { testProcs = 0 })
}

// The machine is divided among the jobs that run at once, and a process never gets less than a
// core.
func TestEachJobIsHeldToItsShareOfTheCores(t *testing.T) {
share(t, 2)
if want := max(runtime.NumCPU()/2, 1); testProcs != want {
t.Errorf("two jobs are held to %d core(s) each, want %d", testProcs, want)
}
// One job, or a -j of nothing, is the whole machine.
for _, jobs := range []int{1, 0} {
share(t, jobs)
if testProcs != runtime.NumCPU() {
t.Errorf("-j %d holds its job to %d core(s), want all %d", jobs, testProcs, runtime.NumCPU())
}
}
share(t, 1<<20)
env := testEnv("A=b")
if !slices.Contains(env, "GOMAXPROCS=1") || env[len(env)-1] != "A=b" {
t.Errorf("more jobs than cores: the environment ends %q", env[max(len(env)-3, 0):])
}
testProcs = 0
if slices.Contains(testEnv(), "GOMAXPROCS=1") && os.Getenv("GOMAXPROCS") != "1" {
t.Error("a run that shares nothing held its tests to one core")
}
}

// Every way a test process is started holds it to its share: measuring coverage, asking a schema
// binary, and building an edit alone.
func TestEveryTestProcessIsHeldToItsShare(t *testing.T) {
dir := oneProc(t)
share(t, 1<<20)

c, err := Cover(dir, "1m", make(chan struct{}, 1))
if err != nil {
t.Fatal(err)
}
if got := c.Reaching(filepath.Join(dir, "code.go"), 4); !slices.Equal(got, []string{"TestHeldToOneCore"}) {
t.Errorf("coverage ran its test on every core: the line is reached by %q", got)
}

bin := filepath.Join(t.TempDir(), "pkg.test")
build := exec.Command("go", "test", "-c", "-o", bin, ".")
build.Dir = dir
if out, err := build.CombinedOutput(); err != nil {
t.Fatalf("%v: %s", err, out)
}
if outcome, out := Ask(bin, dir, 0, "1m", nil); outcome != Survived {
t.Errorf("a schema binary was asked on every core: %s", out)
}

if outcome, out := Run(Mutation{File: filepath.Join(dir, "code.go")}, Asked{Timeout: "1m"}); outcome != Survived {
t.Errorf("an edit built alone was asked on every core: %s", out)
}
}