Sign in

Search issue titles and bodies, change requests and comments. Private repositories you cannot read are not included.

vole
Clone

Read only, no credentials:

https://rodent.software/vole.git

Write over SSH with a registered key:

git clone ssh://git@rodent.software:2222/vole.git

A free and open-source forge that lives in your Git repo.

Run the integration tests in parallel: 168s of tests becomes 16s

44530e746def72216bf02e88716f121936576ab7 · Keenan Nemetz · 17d ago · parent 6c03e461

Run the integration tests in parallel: 168s of tests becomes 16s

The suite was serial, and it is the wrong shape to be serial. 95 tests in
internal/server each start a server, push over SSH and shell out to git, so
they spend nearly all their time waiting on child processes and almost none
on CPU: the package took 148s wall for 23s of user time. Sixteen cores were
sitting idle while tests waited for git.

Every test now calls t.Parallel(). Nothing else about the fixtures had to
change, because they were already isolated the way parallelism needs: each
gets t.TempDir(), binds 127.0.0.1:0, and passes its environment through
cmd.Env rather than the process's. binPath is built once by TestMain and is
read-only.

Measured on 16 cores: internal/server 148.4s -> 12.8s, the whole suite
168s -> 16s, and `scripts/ci.sh` (gofmt, vet, build, link check, then the
whole suite under -race) 19.5s end to end. Verified stable across six
repeated runs, two `-shuffle=on` runs and two `-race` runs, with no data
races reported, so the parallelism is exposing isolation the fixtures
already had rather than luck.

The critical path is now one test, TestKilledRunnerIsRequeued, because it is
genuinely a timing test: it waits for a lease to expire and for the requeue
sweep. Its fixture workflow held the step for `sleep 8`, which also runs on
the retry, so eight of its fourteen seconds were the fixture waiting for
itself. Three seconds keeps the property it is testing — the runner must
still be working when it is killed, and the kill happens as soon as the lease
is visible — and takes the test to 9.7s.

What is left is fixture setup, and most of it is SQLite schema creation:
runs.Open is ~89ms and index.Open ~58ms on a *fresh* file. Measured, that is
not the eleven ALTER TABLE statements that are expected to fail on "duplicate
column" — on a warm connection those cost 2us for all eleven. It is the
CREATE TABLE / CREATE INDEX work itself, and modernc's pure-Go engine is much
slower at DDL than C SQLite. Reopening an already-created database is 408us,
so this is a cost paid once per database file, not once per open: in
production it is `vole init` and nowhere else. Tests pay it 95 times because
every fixture starts from an empty data directory.

Left alone deliberately, for two reasons. It is ~1s of wall time across the
suite, and the only fixes available are test-side (build the empty databases
once in TestMain and copy the files per fixture) or would mean changing
storage engines — which is the wrong conclusion from 150ms, and is not what
those 89ms are about.

The invariant is written down at the top of internal/server's test file, with
the rules a new test has to keep: no t.Setenv, no fixed paths, no shared
state beyond binPath, no ordering assumptions, and the shortest sleep a
fixture can get away with — because a test's own wait sets the floor for the
whole package and parallelism cannot hide it.

internal/gitx and cmd/vole keep `t.Setenv` tests, which cannot be parallel,
so those packages are unchanged. They take 0.2s and 0.5s.

internal/server/releases_test.go +12

@@ -64,6 +64,7 @@ func releaseFixture(t *testing.T) *fixture {
64 64 }
65 65
66 66 func TestReleaseListAndNotes(t *testing.T) {
67 t.Parallel()
67 68 f := releaseFixture(t)
68 69
69 70 code, body := f.get("/r/" + f.repo + "/releases")
@@ -116,6 +117,7 @@ func TestReleaseListAndNotes(t *testing.T) {
116 117 // a tag message is pushed content, so it goes through the same renderer as every
117 118 // other pushed body.
118 119 func TestReleaseNotesAreSanitised(t *testing.T) {
120 t.Parallel()
119 121 f := newFixture(t)
120 122 f.publish(f.repo)
121 123 work := workRepo(t, map[string]string{"readme.md": "x\n"})
@@ -143,6 +145,7 @@ func TestReleaseNotesAreSanitised(t *testing.T) {
143 145 // copies pointer files verbatim, so a tarball of an LFS repository is not the
144 146 // bytes the reader expects. The page has to say so.
145 147 func TestReleasePageWarnsAboutLFSPointers(t *testing.T) {
148 t.Parallel()
146 149 f := newFixture(t)
147 150 f.publish(f.repo)
148 151 work := workRepo(t, map[string]string{
@@ -170,6 +173,7 @@ func TestReleasePageWarnsAboutLFSPointers(t *testing.T) {
170 173 }
171 174
172 175 func TestReleaseArchiveIsGeneratedFromTheTag(t *testing.T) {
176 t.Parallel()
173 177 f := releaseFixture(t)
174 178
175 179 code, body := f.get("/r/" + f.repo + "/releases/v1.0.0/archive.tar")
@@ -217,6 +221,7 @@ func TestReleaseArchiveIsGeneratedFromTheTag(t *testing.T) {
217 221 // tarball cacheable without a store behind it: an ETag derived from the tag
218 222 // object, and a 304 when the client already has those bytes.
219 223 func TestReleaseArchiveValidators(t *testing.T) {
224 t.Parallel()
220 225 f := releaseFixture(t)
221 226
222 227 url := f.http.URL + "/r/" + f.repo + "/releases/v1.0.0/archive.tar"
@@ -253,6 +258,7 @@ func TestReleaseArchiveValidators(t *testing.T) {
253 258 }
254 259
255 260 func TestReleasePagesAreNotPublicForPrivateRepos(t *testing.T) {
261 t.Parallel()
256 262 f := newFixture(t)
257 263 // Deliberately not published: an anonymous reader may not see it exists.
258 264 work := workRepo(t, map[string]string{"readme.md": "secret\n"})
@@ -325,6 +331,7 @@ func attachArtifact(t *testing.T, f *fixture, tag, name string, data []byte) (st
325 331 }
326 332
327 333 func TestReleaseArtifactDownload(t *testing.T) {
334 t.Parallel()
328 335 f := releaseFixture(t)
329 336 data := []byte("a release binary, not source code\n")
330 337 oid, _ := attachArtifact(t, f, "v1.0.0", "tool-linux-amd64", data)
@@ -401,6 +408,7 @@ func TestReleaseArtifactDownload(t *testing.T) {
401 408 // to read the pointer and check the store, or the release would advertise a
402 409 // download that 404s.
403 410 func TestReleasePointerWithoutObjectIsRefused(t *testing.T) {
411 t.Parallel()
404 412 f := releaseFixture(t)
405 413 meta := f.newMetaRepo(t)
406 414 missing := strings.Repeat("ab", 32)
@@ -422,6 +430,7 @@ func TestReleasePointerWithoutObjectIsRefused(t *testing.T) {
422 430 // pointer the whole record: the sweep marks objects named by pointer blobs in any
423 431 // tree reachable from any ref, and the metadata branch is one of those refs.
424 432 func TestGCCountsReleaseArtifactsAsReachable(t *testing.T) {
433 t.Parallel()
425 434 f := releaseFixture(t)
426 435 keep := []byte("published artifact")
427 436 drop := []byte("an upload nobody referenced")
@@ -449,6 +458,7 @@ func TestGCCountsReleaseArtifactsAsReachable(t *testing.T) {
449 458 }
450 459
451 460 func TestReleaseArtifactsArePrivateForPrivateRepos(t *testing.T) {
461 t.Parallel()
452 462 f := newFixture(t)
453 463 work := workRepo(t, map[string]string{"readme.md": "secret\n"})
454 464 if _, errs, err := f.git(work, "push", f.sshURL(f.repo), "main:refs/heads/main"); err != nil {
@@ -509,6 +519,7 @@ func pointerFor(t *testing.T, f *fixture, data []byte) string {
509 519 // pointer cannot be re-pointed. A link that can change bytes is worse than no
510 520 // link, and a writer is the person most likely to do it by accident.
511 521 func TestPublishedReleaseIsImmutable(t *testing.T) {
522 t.Parallel()
512 523 f := newFixture(t)
513 524 f.publish(f.repo)
514 525 work := workRepo(t, map[string]string{"a.txt": "one\n"})
@@ -576,6 +587,7 @@ func TestPublishedReleaseIsImmutable(t *testing.T) {
576 587 // mistyped tag unrepairable, and an unaudited override leaves no trace of the
577 588 // bytes that changed under somebody's link.
578 589 func TestAdminCanRepairARelease(t *testing.T) {
590 t.Parallel()
579 591 f := newFixture(t)
580 592 f.publish(f.repo)
581 593 work := workRepo(t, map[string]string{"a.txt": "one\n"})