Commit d2fa3c3cbb
d2fa3c3cbb138c5df0e5ff1a4c971c6b1c6dc573
parent: 89601690f2
Verified · cmc
cmc <hello@cleberg.net> · 2026-09-21 21:17 UTC
e2e: build each binary once per process
startInstance built gitbayd per test, so a full run linked a
byte-identical 35MB binary 208 times. Go's build cache covers the
compile but not the final link, which measured 0.8s each. The gitbay
and gitbay-runner helpers did the same.
One sync.Once per binary, under a directory TestMain owns: t.TempDir
is removed when the test that asked for it ends, and these outlive any
single test. Built lazily, so -run of one test does not link binaries
it has no use for.
Full suite locally: 637s to 401s.
e2e/bin_test.go
added
+67
| @@ -0,0 +1,67 @@ |
| 1 | package e2e |
| 2 | |
| 3 | import ( |
| 4 | "os" |
| 5 | "os/exec" |
| 6 | "path/filepath" |
| 7 | "sync" |
| 8 | "testing" |
| 9 | ) |
| 10 | |
| 11 | // The suite starts an instance per test, and each one needs gitbayd. Built |
| 12 | // per test, that is 200-odd links of a byte-identical 35MB binary: go's |
| 13 | // build cache covers the compile but not the final link, so it cost about |
| 14 | // 0.8s every time, a quarter of the whole run. |
| 15 | // |
| 16 | // Built once per process instead, under a directory TestMain owns. The |
| 17 | // binaries outlive any single test, so t.TempDir is the wrong home for |
| 18 | // them — it is removed when the test that asked for it ends. |
| 19 | var ( |
| 20 | gitbaydBin = &builtBin{name: "gitbayd", pkg: "gitbay.org/gitbay/cmd/gitbayd"} |
| 21 | gitbayBin = &builtBin{name: "gitbay", pkg: "gitbay.org/gitbay/cmd/gitbay"} |
| 22 | runnerBin = &builtBin{name: "gitbay-runner", pkg: "gitbay.org/gitbay/cmd/gitbay-runner"} |
| 23 | ) |
| 24 | |
| 25 | // binDir is where the built binaries live, set by TestMain. |
| 26 | var binDir string |
| 27 | |
| 28 | type builtBin struct { |
| 29 | name string |
| 30 | pkg string |
| 31 | |
| 32 | once sync.Once |
| 33 | path string |
| 34 | out []byte |
| 35 | err error |
| 36 | } |
| 37 | |
| 38 | // get builds the binary on first use and returns the same path thereafter. |
| 39 | // Lazily, so `-run TestOneThing` does not link the two binaries it has no |
| 40 | // use for. |
| 41 | func (b *builtBin) get(t *testing.T) string { |
| 42 | t.Helper() |
| 43 | b.once.Do(func() { |
| 44 | b.path = filepath.Join(binDir, b.name) |
| 45 | cmd := exec.Command("go", "build", "-o", b.path, b.pkg) |
| 46 | cmd.Dir = ".." |
| 47 | b.out, b.err = cmd.CombinedOutput() |
| 48 | }) |
| 49 | if b.err != nil { |
| 50 | t.Fatalf("build %s: %v\n%s", b.name, b.err, b.out) |
| 51 | } |
| 52 | return b.path |
| 53 | } |
| 54 | |
| 55 | func buildGitbayd(t *testing.T) string { return gitbaydBin.get(t) } |
| 56 | func buildGitbayCLI(t *testing.T) string { return gitbayBin.get(t) } |
| 57 | func buildRunner(t *testing.T) string { return runnerBin.get(t) } |
| 58 | |
| 59 | // makeBinDir is called by TestMain. Returned rather than deferred because |
| 60 | // TestMain ends in os.Exit, which runs no deferred functions. |
| 61 | func makeBinDir() (dir string, cleanup func(), err error) { |
| 62 | dir, err = os.MkdirTemp("", "e2e-bin") |
| 63 | if err != nil { |
| 64 | return "", func() {}, err |
| 65 | } |
| 66 | return dir, func() { os.RemoveAll(dir) }, nil |
| 67 | } |
e2e/ci_test.go
−11
| @@ -11,17 +11,6 @@ import ( |
| 11 | 11 | "time" |
| 12 | 12 | ) |
| 13 | 13 | |
| 14 | | func buildRunner(t *testing.T) string { |
| 15 | | t.Helper() |
| 16 | | bin := filepath.Join(t.TempDir(), "gitbay-runner") |
| 17 | | cmd := exec.Command("go", "build", "-o", bin, "gitbay.org/gitbay/cmd/gitbay-runner") |
| 18 | | cmd.Dir = ".." |
| 19 | | if out, err := cmd.CombinedOutput(); err != nil { |
| 20 | | t.Fatalf("build gitbay-runner: %v\n%s", err, out) |
| 21 | | } |
| 22 | | return bin |
| 23 | | } |
| 24 | | |
| 25 | 14 | // runnerOnce processes at most one pending build with the given key. |
| 26 | 15 | func (i *instance) runnerOnce(t *testing.T, key string, extra ...string) string { |
| 27 | 16 | t.Helper() |
e2e/cli_test.go
−11
| @@ -10,17 +10,6 @@ import ( |
| 10 | 10 | "testing" |
| 11 | 11 | ) |
| 12 | 12 | |
| 13 | | func buildGitbayCLI(t *testing.T) string { |
| 14 | | t.Helper() |
| 15 | | bin := filepath.Join(t.TempDir(), "gitbay") |
| 16 | | cmd := exec.Command("go", "build", "-o", bin, "gitbay.org/gitbay/cmd/gitbay") |
| 17 | | cmd.Dir = ".." |
| 18 | | if out, err := cmd.CombinedOutput(); err != nil { |
| 19 | | t.Fatalf("build gitbay: %v\n%s", err, out) |
| 20 | | } |
| 21 | | return bin |
| 22 | | } |
| 23 | | |
| 24 | 13 | // cli runs the forge binary with an isolated config home. |
| 25 | 14 | type cli struct { |
| 26 | 15 | bin string |
e2e/main_test.go
+9 −1
| @@ -35,5 +35,13 @@ func TestMain(m *testing.M) { |
| 35 | 35 | } |
| 36 | 36 | } |
| 37 | 37 | } |
| 38 | | os.Exit(m.Run()) |
| 38 | dir, cleanup, err := makeBinDir() |
| 39 | if err != nil { |
| 40 | fmt.Fprintf(os.Stderr, "e2e: %v\n", err) |
| 41 | os.Exit(1) |
| 42 | } |
| 43 | binDir = dir |
| 44 | code := m.Run() |
| 45 | cleanup() |
| 46 | os.Exit(code) |
| 39 | 47 | } |
e2e/ssh_test.go
−11
| @@ -24,17 +24,6 @@ type instance struct { |
| 24 | 24 | sshDir string // per-user client keys live here |
| 25 | 25 | } |
| 26 | 26 | |
| 27 | | func buildGitbayd(t *testing.T) string { |
| 28 | | t.Helper() |
| 29 | | bin := filepath.Join(t.TempDir(), "gitbayd") |
| 30 | | cmd := exec.Command("go", "build", "-o", bin, "gitbay.org/gitbay/cmd/gitbayd") |
| 31 | | cmd.Dir = ".." |
| 32 | | if out, err := cmd.CombinedOutput(); err != nil { |
| 33 | | t.Fatalf("build gitbayd: %v\n%s", err, out) |
| 34 | | } |
| 35 | | return bin |
| 36 | | } |
| 37 | | |
| 38 | 27 | // freePorts reserves n distinct ports. A port is chosen by binding :0 and |
| 39 | 28 | // reading back what the kernel assigned, so every listener has to stay open |
| 40 | 29 | // until all of them are picked — closing one before picking the next lets |