vole
Clone
Read only, no credentials:
https://rodent.software/git/vole.git
Write over SSH with a registered key:
git clone ssh://git@0.0.0.0: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 · 16d 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/web/diff_test.go +7
| @@ -28,6 +28,7 @@ index 3333333..0000000 | |||
| 28 | 28 | ` | |
| 29 | 29 | ||
| 30 | 30 | func TestParseDiff(t *testing.T) { | |
| 31 | t.Parallel() | ||
| 31 | 32 | files := parseDiff(patchTwoFiles) | |
| 32 | 33 | if len(files) != 2 { | |
| 33 | 34 | t.Fatalf("parsed %d files, want 2: %+v", len(files), files) | |
| @@ -62,6 +63,7 @@ func TestParseDiff(t *testing.T) { | |||
| 62 | 63 | } | |
| 63 | 64 | ||
| 64 | 65 | func TestParseDiffOfABinaryFile(t *testing.T) { | |
| 66 | t.Parallel() | ||
| 65 | 67 | files := parseDiff(`diff --git a/logo.png b/logo.png | |
| 66 | 68 | index 4444444..5555555 100644 | |
| 67 | 69 | Binary files a/logo.png and b/logo.png differ | |
| @@ -72,6 +74,7 @@ Binary files a/logo.png and b/logo.png differ | |||
| 72 | 74 | } | |
| 73 | 75 | ||
| 74 | 76 | func TestParseDiffOfANewFile(t *testing.T) { | |
| 77 | t.Parallel() | ||
| 75 | 78 | files := parseDiff(`diff --git a/new.txt b/new.txt | |
| 76 | 79 | new file mode 100644 | |
| 77 | 80 | index 0000000..6666666 | |
| @@ -91,6 +94,7 @@ index 0000000..6666666 | |||
| 91 | 94 | } | |
| 92 | 95 | ||
| 93 | 96 | func TestParseDiffOfARename(t *testing.T) { | |
| 97 | t.Parallel() | ||
| 94 | 98 | files := parseDiff(`diff --git a/old.md b/new.md | |
| 95 | 99 | similarity index 100% | |
| 96 | 100 | rename from old.md | |
| @@ -107,6 +111,7 @@ rename to new.md | |||
| 107 | 111 | } | |
| 108 | 112 | ||
| 109 | 113 | func TestParseDiffIgnoresPlumbing(t *testing.T) { | |
| 114 | t.Parallel() | ||
| 110 | 115 | files := parseDiff(`diff --git a/app.go b/app.go | |
| 111 | 116 | index aaa..bbb 100644 | |
| 112 | 117 | --- a/app.go | |
| @@ -128,6 +133,7 @@ index aaa..bbb 100644 | |||
| 128 | 133 | } | |
| 129 | 134 | ||
| 130 | 135 | func TestParseDiffToleratesATruncatedPatch(t *testing.T) { | |
| 136 | t.Parallel() | ||
| 131 | 137 | // A cut patch ends mid-hunk: it must render what it has, not panic or claim | |
| 132 | 138 | // the missing lines were unchanged. | |
| 133 | 139 | files := parseDiff(`diff --git a/big.txt b/big.txt | |
| @@ -145,6 +151,7 @@ func TestParseDiffToleratesATruncatedPatch(t *testing.T) { | |||
| 145 | 151 | } | |
| 146 | 152 | ||
| 147 | 153 | func TestDiffSummary(t *testing.T) { | |
| 154 | t.Parallel() | ||
| 148 | 155 | added, removed := diffSummary(parseDiff(patchTwoFiles)) | |
| 149 | 156 | if added != 2 || removed != 3 { | |
| 150 | 157 | t.Fatalf("summary = +%d −%d, want +2 −3", added, removed) | |