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/integration_test.go +73−1
| @@ -38,8 +38,24 @@ import ( | |||
| 38 | 38 | "github.com/rodent-software/vole/internal/server" | |
| 39 | 39 | ) | |
| 40 | 40 | ||
| 41 | // binPath is the binary under test, built once by TestMain. It is read by every test | ||
| 42 | // and written by none, which is what makes it safe for them to run concurrently. | ||
| 41 | 43 | var binPath string | |
| 42 | 44 | ||
| 45 | // Every test here calls t.Parallel(). These are integration tests: each one starts a | ||
| 46 | // server, pushes over SSH and runs `git` subprocesses, so it spends almost all of its | ||
| 47 | // time waiting on child processes and almost none of it on CPU. Serially that is ~150 | ||
| 48 | // seconds; sixteen at a time it is ~16. Anything added here must keep that possible: | ||
| 49 | // | ||
| 50 | // - no t.Setenv — it panics in a parallel test, and it is the wrong tool anyway. | ||
| 51 | // Pass environment through cmd.Env (see ciEnv) so tests cannot see each other. | ||
| 52 | // - no fixed paths. t.TempDir() and 127.0.0.1:0 give each test its own instance. | ||
| 53 | // - no shared package-level state beyond the read-only binPath. | ||
| 54 | // - no ordering assumptions between tests. `go test -shuffle=on` must pass. | ||
| 55 | // | ||
| 56 | // A genuinely slow *wait* is the one thing parallelism cannot hide, because it sets the | ||
| 57 | // floor for the whole package: prefer the shortest sleep a fixture can get away with, | ||
| 58 | // and say in a comment what it is waiting for. | ||
| 43 | 59 | func TestMain(m *testing.M) { | |
| 44 | 60 | dir, err := os.MkdirTemp("", "vole-int-bin-") | |
| 45 | 61 | if err != nil { | |
| @@ -326,6 +342,7 @@ func (f *fixture) cli(dir string, args ...string) (string, string, error) { | |||
| 326 | 342 | // TestPushOverSSHThenReadOverBothTransports is the core loop: writes need a key, | |
| 327 | 343 | // reads do not. | |
| 328 | 344 | func TestPushOverSSHThenReadOverBothTransports(t *testing.T) { | |
| 345 | t.Parallel() | ||
| 329 | 346 | f := newFixture(t) | |
| 330 | 347 | f.publish(f.repo) | |
| 331 | 348 | ||
| @@ -362,6 +379,7 @@ func TestPushOverSSHThenReadOverBothTransports(t *testing.T) { | |||
| 362 | 379 | // TestPrivateRepoIsInvisible covers the visibility rule: private means absent, | |
| 363 | 380 | // not forbidden. | |
| 364 | 381 | func TestPrivateRepoIsInvisible(t *testing.T) { | |
| 382 | t.Parallel() | ||
| 365 | 383 | f := newFixture(t) | |
| 366 | 384 | work := workRepo(t, map[string]string{"secret.md": "classified\n"}) | |
| 367 | 385 | if _, errs, err := f.git(work, "push", f.sshURL(f.repo), "main:refs/heads/main"); err != nil { | |
| @@ -397,6 +415,7 @@ func TestPrivateRepoIsInvisible(t *testing.T) { | |||
| 397 | 415 | } | |
| 398 | 416 | ||
| 399 | 417 | func TestPushOverHTTPRefused(t *testing.T) { | |
| 418 | t.Parallel() | ||
| 400 | 419 | f := newFixture(t) | |
| 401 | 420 | f.publish(f.repo) | |
| 402 | 421 | work := workRepo(t, map[string]string{"a.txt": "a\n"}) | |
| @@ -406,6 +425,7 @@ func TestPushOverHTTPRefused(t *testing.T) { | |||
| 406 | 425 | } | |
| 407 | 426 | ||
| 408 | 427 | func TestRebuildIndexAfterDelete(t *testing.T) { | |
| 428 | t.Parallel() | ||
| 409 | 429 | f := newFixture(t) | |
| 410 | 430 | f.publish(f.repo) | |
| 411 | 431 | before, err := f.srv.Index.List() | |
| @@ -452,6 +472,7 @@ func TestRebuildIndexAfterDelete(t *testing.T) { | |||
| 452 | 472 | // one SQLite file, a consistent copy of it is complete, and no system repository | |
| 453 | 473 | // or key directory exists beside it. | |
| 454 | 474 | func TestRosterIsOneDatabaseFile(t *testing.T) { | |
| 475 | t.Parallel() | ||
| 455 | 476 | f := newFixture(t) | |
| 456 | 477 | f.publish(f.repo) | |
| 457 | 478 | work := workRepo(t, map[string]string{"readme.md": "x\n"}) | |
| @@ -486,6 +507,7 @@ func TestRosterIsOneDatabaseFile(t *testing.T) { | |||
| 486 | 507 | // a hand-edited key is accepted, because there is no signed history to compare | |
| 487 | 508 | // against. What we keep is that the change is unambiguous and reloadable. | |
| 488 | 509 | func TestRosterIsReloadable(t *testing.T) { | |
| 510 | t.Parallel() | ||
| 489 | 511 | f := newFixture(t) | |
| 490 | 512 | // A key dropped in by hand is picked up on reload. | |
| 491 | 513 | pub, _ := keypair(t, filepath.Join(f.cfg.DataDir, "id_second")) | |
| @@ -544,6 +566,7 @@ func issueFiles(id, title, desc string) map[string]string { | |||
| 544 | 566 | // TestIssuePushedThenRendered is the whole option-A loop: a user with an editor | |
| 545 | 567 | // and a key files a bug, and the read-only web shows it. | |
| 546 | 568 | func TestIssuePushedThenRendered(t *testing.T) { | |
| 569 | t.Parallel() | ||
| 547 | 570 | f := newFixture(t) | |
| 548 | 571 | f.publish(f.repo) | |
| 549 | 572 | ||
| @@ -588,6 +611,7 @@ func TestIssuePushedThenRendered(t *testing.T) { | |||
| 588 | 611 | // TestIssueMarkdownIsSanitised: issue bodies arrive by push, so an issue is | |
| 589 | 612 | // untrusted input rendered into a public page. | |
| 590 | 613 | func TestIssueMarkdownIsSanitised(t *testing.T) { | |
| 614 | t.Parallel() | ||
| 591 | 615 | f := newFixture(t) | |
| 592 | 616 | f.publish(f.repo) | |
| 593 | 617 | desc := "hello\n\n<script>alert('xss')</script>\n\n[click](javascript:alert(1))\n" | |
| @@ -610,6 +634,7 @@ func TestIssueMarkdownIsSanitised(t *testing.T) { | |||
| 610 | 634 | // TestInvalidIssuePushRejected proves validation happens at the boundary: the | |
| 611 | 635 | // push fails, the message says why, and the branch does not move. | |
| 612 | 636 | func TestInvalidIssuePushRejected(t *testing.T) { | |
| 637 | t.Parallel() | ||
| 613 | 638 | f := newFixture(t) | |
| 614 | 639 | f.publish(f.repo) | |
| 615 | 640 | ||
| @@ -654,6 +679,7 @@ func TestInvalidIssuePushRejected(t *testing.T) { | |||
| 654 | 679 | // TestIssuesSurviveIndexRebuild: issue rows are derived, so deleting the index | |
| 655 | 680 | // must not lose them. | |
| 656 | 681 | func TestIssuesSurviveIndexRebuild(t *testing.T) { | |
| 682 | t.Parallel() | ||
| 657 | 683 | f := newFixture(t) | |
| 658 | 684 | f.publish(f.repo) | |
| 659 | 685 | work := f.issuesRepo(t, issueFiles(testIssueID, "persisted", "body\n")) | |
| @@ -686,6 +712,7 @@ func TestIssuesSurviveIndexRebuild(t *testing.T) { | |||
| 686 | 712 | // TestIssueCLIEndToEnd drives the sugar path: a clone with a remote, the CLI | |
| 687 | 713 | // writing files and committing, and the push landing on the server. | |
| 688 | 714 | func TestIssueCLIEndToEnd(t *testing.T) { | |
| 715 | t.Parallel() | ||
| 689 | 716 | f := newFixture(t) | |
| 690 | 717 | f.publish(f.repo) | |
| 691 | 718 | // A commit on main so the clone is not empty and has a default branch. | |
| @@ -858,6 +885,7 @@ func TestIssueCLIEndToEnd(t *testing.T) { | |||
| 858 | 885 | // on every issue it filed, silently. The key is the identity; the instance is the | |
| 859 | 886 | // only thing that knows which account it belongs to. | |
| 860 | 887 | func TestWhoAmIAnswersFromTheInstance(t *testing.T) { | |
| 888 | t.Parallel() | ||
| 861 | 889 | f := newFixtureWith(t, func(c *config.Config) { c.PublicURL = "http://forge.test" }) | |
| 862 | 890 | f.publish(f.repo) | |
| 863 | 891 | work := workRepo(t, map[string]string{"readme.md": "hi\n"}) | |
| @@ -934,6 +962,7 @@ func TestWhoAmIAnswersFromTheInstance(t *testing.T) { | |||
| 934 | 962 | // TestReadCommandsNeedNoIdentity: `issue list` and `issue show` read the branch, | |
| 935 | 963 | // so they must keep working where the instance cannot be reached at all. | |
| 936 | 964 | func TestReadCommandsNeedNoIdentity(t *testing.T) { | |
| 965 | t.Parallel() | ||
| 937 | 966 | f := newFixture(t) | |
| 938 | 967 | f.publish(f.repo) | |
| 939 | 968 | work := workRepo(t, map[string]string{"readme.md": "hi\n"}) | |
| @@ -963,6 +992,7 @@ func TestReadCommandsNeedNoIdentity(t *testing.T) { | |||
| 963 | 992 | // and there is no flag for saying otherwise. `--author` was a name you could typo | |
| 964 | 993 | // into a round trip; the key already answers the question. | |
| 965 | 994 | func TestRequestAuthorComesFromTheKey(t *testing.T) { | |
| 995 | t.Parallel() | ||
| 966 | 996 | f := newFixture(t) | |
| 967 | 997 | f.publish(f.repo) | |
| 968 | 998 | work := workRepo(t, map[string]string{"readme.md": "hi\n"}) | |
| @@ -1010,6 +1040,7 @@ func TestRequestAuthorComesFromTheKey(t *testing.T) { | |||
| 1010 | 1040 | // tip it had never updated, and the retry reported a "lost race" that never | |
| 1011 | 1041 | // happened and re-applied onto the same stale base. | |
| 1012 | 1042 | func TestAURLRemoteIsFetchedBeforeWriting(t *testing.T) { | |
| 1043 | t.Parallel() | ||
| 1013 | 1044 | f := newFixture(t) | |
| 1014 | 1045 | f.publish(f.repo) | |
| 1015 | 1046 | fresh := func() string { | |
| @@ -1039,6 +1070,7 @@ func TestAURLRemoteIsFetchedBeforeWriting(t *testing.T) { | |||
| 1039 | 1070 | // minute with the instance, and four of the five things they might want to do are | |
| 1040 | 1071 | // writes the web cannot take. The page has to hand them the command. | |
| 1041 | 1072 | func TestEmptyStatesPrintAWorkingCommand(t *testing.T) { | |
| 1073 | t.Parallel() | ||
| 1042 | 1074 | f := newFixture(t) | |
| 1043 | 1075 | f.publish(f.repo) | |
| 1044 | 1076 | for _, c := range []struct{ path, want string }{ | |
| @@ -1060,6 +1092,7 @@ func TestEmptyStatesPrintAWorkingCommand(t *testing.T) { | |||
| 1060 | 1092 | // answer comes from `git merge-tree` in the repository — nothing is written — and | |
| 1061 | 1093 | // it is only claimed when the question could actually be asked. | |
| 1062 | 1094 | func TestRequestPageSaysWhenItWouldConflict(t *testing.T) { | |
| 1095 | t.Parallel() | ||
| 1063 | 1096 | f := newFixture(t) | |
| 1064 | 1097 | f.publish(f.repo) | |
| 1065 | 1098 | ||
| @@ -1171,6 +1204,7 @@ func TestRequestPageSaysWhenItWouldConflict(t *testing.T) { | |||
| 1171 | 1204 | // at the boundary, and owned by whoever wrote it. Without this, the only ways to | |
| 1172 | 1205 | // say something about a request are editing its body or approving it. | |
| 1173 | 1206 | func TestRequestDiscussionIsPushedAndOwned(t *testing.T) { | |
| 1207 | t.Parallel() | ||
| 1174 | 1208 | f := newFixture(t) | |
| 1175 | 1209 | writerKey := f.addWriter(t) // wilbur: write access, not admin | |
| 1176 | 1210 | f.publish(f.repo) | |
| @@ -1270,6 +1304,7 @@ func TestRequestDiscussionIsPushedAndOwned(t *testing.T) { | |||
| 1270 | 1304 | // moment the file changes, instead of sliding to a line that now says something | |
| 1271 | 1305 | // else. | |
| 1272 | 1306 | func TestReviewCommentAnchorsALine(t *testing.T) { | |
| 1307 | t.Parallel() | ||
| 1273 | 1308 | f := newFixture(t) | |
| 1274 | 1309 | f.publish(f.repo) | |
| 1275 | 1310 | ||
| @@ -1388,6 +1423,7 @@ func (f *fixture) serverIssues(t *testing.T) string { | |||
| 1388 | 1423 | ||
| 1389 | 1424 | // TestIssueFiltersAndSearch covers the SQL paths behind the list page. | |
| 1390 | 1425 | func TestIssueFiltersAndSearch(t *testing.T) { | |
| 1426 | t.Parallel() | ||
| 1391 | 1427 | f := newFixture(t) | |
| 1392 | 1428 | f.publish(f.repo) | |
| 1393 | 1429 | files := map[string]string{} | |
| @@ -1431,6 +1467,7 @@ func TestIssueFiltersAndSearch(t *testing.T) { | |||
| 1431 | 1467 | // click, a filter that narrows the list, and — because it names a person — a | |
| 1432 | 1468 | // value the push boundary refuses when it could never be an account. | |
| 1433 | 1469 | func TestIssueAssigneesRenderedFilteredAndValidated(t *testing.T) { | |
| 1470 | t.Parallel() | ||
| 1434 | 1471 | f := newFixture(t) | |
| 1435 | 1472 | f.publish(f.repo) | |
| 1436 | 1473 | const ( | |
| @@ -1519,6 +1556,7 @@ func commentFile(author, created, body string) string { | |||
| 1519 | 1556 | // file. Closing and relabelling somebody else's issue is triage, not authorship, | |
| 1520 | 1557 | // and refusing it would make the shared fields useless. | |
| 1521 | 1558 | func TestCommentEditsBelongToTheirAuthor(t *testing.T) { | |
| 1559 | t.Parallel() | ||
| 1522 | 1560 | f := newFixture(t) | |
| 1523 | 1561 | writerKey := f.addWriter(t) // wilbur: write access, not admin | |
| 1524 | 1562 | f.publish(f.repo) | |
| @@ -1603,6 +1641,7 @@ func TestCommentEditsBelongToTheirAuthor(t *testing.T) { | |||
| 1603 | 1641 | // TestCrossReferencesLinksIssues: a #<ulid> in a body becomes a link, and a | |
| 1604 | 1642 | // commit mentioning an issue shows as a backlink. | |
| 1605 | 1643 | func TestCrossReferencesLinksIssues(t *testing.T) { | |
| 1644 | t.Parallel() | ||
| 1606 | 1645 | f := newFixture(t) | |
| 1607 | 1646 | f.publish(f.repo) | |
| 1608 | 1647 | const target = "01M5JR2JK29KKMAA1T9MDNTPC9" | |
| @@ -1662,6 +1701,7 @@ func min(a, b int) int { | |||
| 1662 | 1701 | // TestMetadataRejectsMergeCommits: metadata history is linear by policy, so a | |
| 1663 | 1702 | // merge node is refused rather than quietly accepted. | |
| 1664 | 1703 | func TestMetadataRejectsMergeCommits(t *testing.T) { | |
| 1704 | t.Parallel() | ||
| 1665 | 1705 | f := newFixture(t) | |
| 1666 | 1706 | f.publish(f.repo) | |
| 1667 | 1707 | ||
| @@ -1699,6 +1739,7 @@ func TestMetadataRejectsMergeCommits(t *testing.T) { | |||
| 1699 | 1739 | // TestMetadataForcePushIsAdminOnly: rewriting discussion content is a redaction | |
| 1700 | 1740 | // tool, not a normal operation. | |
| 1701 | 1741 | func TestMetadataForcePushIsAdminOnly(t *testing.T) { | |
| 1742 | t.Parallel() | ||
| 1702 | 1743 | f := newFixture(t) | |
| 1703 | 1744 | f.publish(f.repo) | |
| 1704 | 1745 | writerKey := f.addWriter(t) | |
| @@ -1739,6 +1780,7 @@ func TestMetadataForcePushIsAdminOnly(t *testing.T) { | |||
| 1739 | 1780 | // TestMetadataRejectsUnknownTopLevel: the layout is closed, so a typo is caught | |
| 1740 | 1781 | // rather than silently ignored by the web. | |
| 1741 | 1782 | func TestMetadataRejectsUnknownTopLevel(t *testing.T) { | |
| 1783 | t.Parallel() | ||
| 1742 | 1784 | f := newFixture(t) | |
| 1743 | 1785 | f.publish(f.repo) | |
| 1744 | 1786 | work := t.TempDir() | |
| @@ -1769,6 +1811,7 @@ func TestMetadataRejectsUnknownTopLevel(t *testing.T) { | |||
| 1769 | 1811 | // TestConcurrentCLIWritesAllLand: two people filing issues at once must both | |
| 1770 | 1812 | // succeed. The CLI re-applies on a lost race instead of failing. | |
| 1771 | 1813 | func TestConcurrentCLIWritesAllLand(t *testing.T) { | |
| 1814 | t.Parallel() | ||
| 1772 | 1815 | f := newFixture(t) | |
| 1773 | 1816 | f.publish(f.repo) | |
| 1774 | 1817 | if _, errs, err := f.git(workRepo(t, map[string]string{"readme.md": "hi\n"}), | |
| @@ -2022,6 +2065,7 @@ func issueFile(title, author, state, created string, labels []string, body strin | |||
| 2022 | 2065 | // writer cannot land on a protected branch until a change request for that exact | |
| 2023 | 2066 | // commit has the required approvals, and an admin still bypasses. | |
| 2024 | 2067 | func TestProtectedBranchRequiresApprovedRequest(t *testing.T) { | |
| 2068 | t.Parallel() | ||
| 2025 | 2069 | f := newFixture(t) | |
| 2026 | 2070 | writerKey := f.addWriter(t) | |
| 2027 | 2071 | f.publish(f.repo) | |
| @@ -2091,6 +2135,7 @@ func TestProtectedBranchRequiresApprovedRequest(t *testing.T) { | |||
| 2091 | 2135 | // TestApprovalIsBoundToTheCommit is the property that makes approvals mean | |
| 2092 | 2136 | // something: approve A, amend to B, and the approval no longer applies. | |
| 2093 | 2137 | func TestApprovalIsBoundToTheCommit(t *testing.T) { | |
| 2138 | t.Parallel() | ||
| 2094 | 2139 | f := newFixture(t) | |
| 2095 | 2140 | writerKey := f.addWriter(t) | |
| 2096 | 2141 | f.publish(f.repo) | |
| @@ -2132,6 +2177,7 @@ func TestApprovalIsBoundToTheCommit(t *testing.T) { | |||
| 2132 | 2177 | // TestSelfApprovalRejected: the metadata push itself fails, so the approval never | |
| 2133 | 2178 | // exists to be counted. | |
| 2134 | 2179 | func TestSelfApprovalRejected(t *testing.T) { | |
| 2180 | t.Parallel() | ||
| 2135 | 2181 | f := newFixture(t) | |
| 2136 | 2182 | writerKey := f.addWriter(t) | |
| 2137 | 2183 | f.publish(f.repo) | |
| @@ -2159,6 +2205,7 @@ func TestSelfApprovalRejected(t *testing.T) { | |||
| 2159 | 2205 | // the branch tip and the approved head is allowed; smuggling an extra commit into | |
| 2160 | 2206 | // that merge is not. | |
| 2161 | 2207 | func TestMergeCommitShapeAllowed(t *testing.T) { | |
| 2208 | t.Parallel() | ||
| 2162 | 2209 | f := newFixture(t) | |
| 2163 | 2210 | writerKey := f.addWriter(t) | |
| 2164 | 2211 | f.publish(f.repo) | |
| @@ -2212,6 +2259,7 @@ func TestMergeCommitShapeAllowed(t *testing.T) { | |||
| 2212 | 2259 | // TestImpersonatedApprovalRejected: an approval file is only written by the person | |
| 2213 | 2260 | // it names, otherwise "approval" means nothing. | |
| 2214 | 2261 | func TestImpersonatedApprovalRejected(t *testing.T) { | |
| 2262 | t.Parallel() | ||
| 2215 | 2263 | f := newFixture(t) | |
| 2216 | 2264 | writerKey := f.addWriter(t) | |
| 2217 | 2265 | f.publish(f.repo) | |
| @@ -2244,6 +2292,7 @@ func TestImpersonatedApprovalRejected(t *testing.T) { | |||
| 2244 | 2292 | // TestApprovalIsBoundToTheBase: repointing a request at another branch must not | |
| 2245 | 2293 | // carry its approvals along. | |
| 2246 | 2294 | func TestApprovalIsBoundToTheBase(t *testing.T) { | |
| 2295 | t.Parallel() | ||
| 2247 | 2296 | f := newFixture(t) | |
| 2248 | 2297 | writerKey := f.addWriter(t) | |
| 2249 | 2298 | f.publish(f.repo) | |
| @@ -2292,6 +2341,7 @@ func TestApprovalIsBoundToTheBase(t *testing.T) { | |||
| 2292 | 2341 | // its named principal must also cover the push that creates the branch, when there | |
| 2293 | 2342 | // is no previous commit to diff against. | |
| 2294 | 2343 | func TestFirstPushCannotSeedForeignApprovals(t *testing.T) { | |
| 2344 | t.Parallel() | ||
| 2295 | 2345 | f := newFixture(t) | |
| 2296 | 2346 | writerKey := f.addWriter(t) | |
| 2297 | 2347 | f.publish(f.repo) | |
| @@ -2320,6 +2370,7 @@ func TestFirstPushCannotSeedForeignApprovals(t *testing.T) { | |||
| 2320 | 2370 | // *edited* rather than added, so a check that only inspects added paths misses it. | |
| 2321 | 2371 | // This is the exploit: approve commit A, then rewrite the approval to name B. | |
| 2322 | 2372 | func TestModifiedApprovalCannotBeForged(t *testing.T) { | |
| 2373 | t.Parallel() | ||
| 2323 | 2374 | f := newFixture(t) | |
| 2324 | 2375 | writerKey := f.addWriter(t) | |
| 2325 | 2376 | f.publish(f.repo) | |
| @@ -2376,6 +2427,7 @@ func TestModifiedApprovalCannotBeForged(t *testing.T) { | |||
| 2376 | 2427 | // TestApprovalDeletionIsOwnerOrAdmin: deleting a file forges nothing, but it | |
| 2377 | 2428 | // erases someone else's statement, so a writer may not do it to another person. | |
| 2378 | 2429 | func TestApprovalDeletionIsOwnerOrAdmin(t *testing.T) { | |
| 2430 | t.Parallel() | ||
| 2379 | 2431 | f := newFixture(t) | |
| 2380 | 2432 | writerKey := f.addWriter(t) | |
| 2381 | 2433 | f.publish(f.repo) | |
| @@ -2443,6 +2495,7 @@ func TestApprovalDeletionIsOwnerOrAdmin(t *testing.T) { | |||
| 2443 | 2495 | // changing a person's name must not change what they may do, nor who owns their | |
| 2444 | 2496 | // past approvals. | |
| 2445 | 2497 | func TestRenameKeepsAccessAndApprovalOwnership(t *testing.T) { | |
| 2498 | t.Parallel() | ||
| 2446 | 2499 | f := newFixture(t) | |
| 2447 | 2500 | writerKey := f.addWriter(t) | |
| 2448 | 2501 | f.publish(f.repo) | |
| @@ -2506,6 +2559,7 @@ func TestRenameKeepsAccessAndApprovalOwnership(t *testing.T) { | |||
| 2506 | 2559 | // TestRetiredNameCannotBeReissued: a departure must not hand someone a retired | |
| 2507 | 2560 | // person's access and approvals. | |
| 2508 | 2561 | func TestRetiredNameCannotBeReissued(t *testing.T) { | |
| 2562 | t.Parallel() | ||
| 2509 | 2563 | f := newFixture(t) | |
| 2510 | 2564 | writerKey := f.addWriter(t) | |
| 2511 | 2565 | f.publish(f.repo) | |
| @@ -2530,6 +2584,7 @@ func TestRetiredNameCannotBeReissued(t *testing.T) { | |||
| 2530 | 2584 | // introduced it, naming the file and the reason, instead of sitting in the tree | |
| 2531 | 2585 | // and never running. | |
| 2532 | 2586 | func TestInvalidWorkflowRejectedAtPush(t *testing.T) { | |
| 2587 | t.Parallel() | ||
| 2533 | 2588 | f := newFixture(t) | |
| 2534 | 2589 | f.publish(f.repo) | |
| 2535 | 2590 | ||
| @@ -2596,6 +2651,7 @@ func workflowFile(body string) map[string]string { | |||
| 2596 | 2651 | // TestPushRunsWorkflow is the end-to-end criterion: a push queues a job, a runner | |
| 2597 | 2652 | // leases it, executes the pinned steps and reports the log. | |
| 2598 | 2653 | func TestPushRunsWorkflow(t *testing.T) { | |
| 2654 | t.Parallel() | ||
| 2599 | 2655 | f := newFixtureWith(t, func(c *config.Config) { c.LeaseSeconds = 2 }) | |
| 2600 | 2656 | f.publish(f.repo) | |
| 2601 | 2657 | ||
| @@ -2682,6 +2738,7 @@ jobs: | |||
| 2682 | 2738 | // matches nothing looks exactly like a quiet instance, so a runner that started | |
| 2683 | 2739 | // happily and idled would be indistinguishable from a healthy one. | |
| 2684 | 2740 | func TestARepoFilterOutsideTheGrantIsRefusedAtStartup(t *testing.T) { | |
| 2741 | t.Parallel() | ||
| 2685 | 2742 | f := newFixture(t) | |
| 2686 | 2743 | f.publish(f.repo) | |
| 2687 | 2744 | ||
| @@ -2703,6 +2760,7 @@ func TestARepoFilterOutsideTheGrantIsRefusedAtStartup(t *testing.T) { | |||
| 2703 | 2760 | // on the metadata branch, so a plain clone carries it, and the queue's finished | |
| 2704 | 2761 | // half can be rebuilt from git after the database is deleted. | |
| 2705 | 2762 | func TestRunRecordIsPortable(t *testing.T) { | |
| 2763 | t.Parallel() | ||
| 2706 | 2764 | f := newFixtureWith(t, func(c *config.Config) { c.LeaseSeconds = 2 }) | |
| 2707 | 2765 | f.publish(f.repo) | |
| 2708 | 2766 | ||
| @@ -2830,6 +2888,7 @@ jobs: | |||
| 2830 | 2888 | // TestKilledRunnerIsRequeued is the other half of the contract: killing a runner | |
| 2831 | 2889 | // mid-job returns the job to the queue and another runner finishes it. | |
| 2832 | 2890 | func TestKilledRunnerIsRequeued(t *testing.T) { | |
| 2891 | t.Parallel() | ||
| 2833 | 2892 | if testing.Short() { | |
| 2834 | 2893 | t.Skip("waits for a lease to expire and a janitor tick; slow under -race") | |
| 2835 | 2894 | } | |
| @@ -2841,7 +2900,11 @@ jobs: | |||
| 2841 | 2900 | slow: | |
| 2842 | 2901 | steps: | |
| 2843 | 2902 | - name: hold the lease | |
| 2844 | run: sleep 8 | ||
| 2903 | # Long enough that the runner is certainly still working when it is killed | ||
| 2904 | # (the kill happens as soon as the lease is visible, so the margin is the whole | ||
| 2905 | # step), and no longer: this same step also runs on the retry, so every second | ||
| 2906 | # here is a second added to the test. | ||
| 2907 | run: sleep 3 | ||
| 2845 | 2908 | `)) | |
| 2846 | 2909 | if _, errs, err := f.git(work, "push", f.sshURL(f.repo), "main:refs/heads/main"); err != nil { | |
| 2847 | 2910 | t.Fatalf("push: %s", errs) | |
| @@ -2893,6 +2956,7 @@ jobs: | |||
| 2893 | 2956 | // A run still waiting when a newer tip arrives is canceled rather than started — the | |
| 2894 | 2957 | // pinning question only exists for a run that got as far as executing. | |
| 2895 | 2958 | func TestQueuedRunUsesTheWorkflowItWasQueuedFrom(t *testing.T) { | |
| 2959 | t.Parallel() | ||
| 2896 | 2960 | f := newFixtureWith(t, func(c *config.Config) { c.LeaseSeconds = 5 }) | |
| 2897 | 2961 | f.publish(f.repo) | |
| 2898 | 2962 | ||
| @@ -3046,6 +3110,7 @@ func (f *fixture) openAndApprove(t *testing.T, head, author string) { | |||
| 3046 | 3110 | // TestProtectedBranchRequiresGreenChecks: approvals say a person read the change, | |
| 3047 | 3111 | // checks say it works, and a protected branch wants both. | |
| 3048 | 3112 | func TestProtectedBranchRequiresGreenChecks(t *testing.T) { | |
| 3113 | t.Parallel() | ||
| 3049 | 3114 | f := newFixtureWith(t, func(c *config.Config) { c.LeaseSeconds = 2 }) | |
| 3050 | 3115 | writerKey := f.addWriter(t) | |
| 3051 | 3116 | f.publish(f.repo) | |
| @@ -3080,6 +3145,7 @@ func TestProtectedBranchRequiresGreenChecks(t *testing.T) { | |||
| 3080 | 3145 | // the account that reported it. A required check is a claim about a commit, and | |
| 3081 | 3146 | // without this rule any writer could satisfy it by running CI on their own machine. | |
| 3082 | 3147 | func TestOnlyAllowedRunnersGreenLightABranch(t *testing.T) { | |
| 3148 | t.Parallel() | ||
| 3083 | 3149 | f := newFixtureWith(t, func(c *config.Config) { c.LeaseSeconds = 2 }) | |
| 3084 | 3150 | writerKey := f.addWriter(t) | |
| 3085 | 3151 | wilbur := f.accountID("wilbur") | |
| @@ -3157,6 +3223,7 @@ func TestOnlyAllowedRunnersGreenLightABranch(t *testing.T) { | |||
| 3157 | 3223 | // TestACLRunnersEditsTheList: three shapes, because replacing a list to add a sixth | |
| 3158 | 3224 | // name is how a list gets shortened by mistake. | |
| 3159 | 3225 | func TestACLRunnersEditsTheList(t *testing.T) { | |
| 3226 | t.Parallel() | ||
| 3160 | 3227 | f := newFixture(t) | |
| 3161 | 3228 | f.addWriter(t) | |
| 3162 | 3229 | f.publish(f.repo) | |
| @@ -3248,6 +3315,7 @@ func TestACLRunnersEditsTheList(t *testing.T) { | |||
| 3248 | 3315 | ||
| 3249 | 3316 | // TestFailingCheckBlocksTheMerge: a green approval does not outvote a red build. | |
| 3250 | 3317 | func TestFailingCheckBlocksTheMerge(t *testing.T) { | |
| 3318 | t.Parallel() | ||
| 3251 | 3319 | f := newFixtureWith(t, func(c *config.Config) { c.LeaseSeconds = 2 }) | |
| 3252 | 3320 | writerKey := f.addWriter(t) | |
| 3253 | 3321 | f.publish(f.repo) | |
| @@ -3285,6 +3353,7 @@ func TestFailingCheckBlocksTheMerge(t *testing.T) { | |||
| 3285 | 3353 | // TestMissingRunBlocksWithAHint: a workflow that only runs on main can never | |
| 3286 | 3354 | // produce a result for a feature branch, and the message has to say so. | |
| 3287 | 3355 | func TestMissingRunBlocksWithAHint(t *testing.T) { | |
| 3356 | t.Parallel() | ||
| 3288 | 3357 | f := newFixtureWith(t, func(c *config.Config) { c.LeaseSeconds = 2 }) | |
| 3289 | 3358 | writerKey := f.addWriter(t) | |
| 3290 | 3359 | f.publish(f.repo) | |
| @@ -3338,6 +3407,7 @@ func repoID(t *testing.T, f *fixture) string { | |||
| 3338 | 3407 | } | |
| 3339 | 3408 | ||
| 3340 | 3409 | func TestHealthzReportsUsers(t *testing.T) { | |
| 3410 | t.Parallel() | ||
| 3341 | 3411 | f := newFixture(t) | |
| 3342 | 3412 | code, body := f.get("/healthz") | |
| 3343 | 3413 | if code != 200 { | |
| @@ -3354,6 +3424,7 @@ var _ = acl.Read | |||
| 3354 | 3424 | // label. The end-to-end shape matters here: the whole point is that nothing runs, which | |
| 3355 | 3425 | // has to be distinguishable from "a runner started and failed". | |
| 3356 | 3426 | func TestRunsOnWaitsForARunnerThatCarriesTheLabel(t *testing.T) { | |
| 3427 | t.Parallel() | ||
| 3357 | 3428 | f := newFixtureWith(t, func(c *config.Config) { c.LeaseSeconds = 5 }) | |
| 3358 | 3429 | f.publish(f.repo) | |
| 3359 | 3430 | ||
| @@ -3410,6 +3481,7 @@ jobs: | |||
| 3410 | 3481 | // refuses it before it dials. A machine advertising a label no workflow can ask for is | |
| 3411 | 3482 | // a typo with no symptom. | |
| 3412 | 3483 | func TestABadRunnerLabelIsRefusedBeforeConnecting(t *testing.T) { | |
| 3484 | t.Parallel() | ||
| 3413 | 3485 | f := newFixture(t) | |
| 3414 | 3486 | cmd := exec.Command(binPath, "runner", "--remote", f.sshBase(), "--once", | |
| 3415 | 3487 | "--dir", t.TempDir(), "--label", "Linux") | |