Repository navigation
ci: run coverage in set mode and -race in a run of its own - #1311
Open
fuad-daoud wants to merge 62 commits into
Open
fuad-daoud wants to merge 62 commits into
fuad-daoud wants to merge 62 commits into
Conversation
`make coverage` now runs the suite once in set mode, without -race. cmd/morphic-covergate reads only whether a block ran, so set mode gives it the same verdict, and a new row in its test pins that reading. `make race` runs -race alone over the packages in scripts/race-packages.txt, the ones whose code runs on more than one goroutine. Combined with atomic coverage the detector missed a race planted for the purpose that it caught alone, so the two no longer share a run. `make gate` runs both. The race run skips the two load tests of sequential cancellation, which the detector learns nothing from; they still run in full in coverage. Refs #1271
internal/harness and internal/census stay off scripts/race-packages.txt, as #1271 decided. Their tests call harness.Check and (*Census).Add on parallel goroutines, so with -race gone from make coverage a race in either is no longer detected. The list file now says so.
The suite job now runs coverage and the race detector as separate steps, `make coverage` then `make race`. The job id is kept, so its cache key is unchanged.
A directory is flagged as running concurrently when a non-test file imports sync or sync/atomic, or any file, tests included, starts a goroutine by a go statement or a single-argument .Go call. Test-only sync imports and t.Parallel do not count. Every flagged directory must be covered by an entry of scripts/race-packages.txt or exempt in raceExempt with a reason. An entry or exemption that flags nothing fails too, as does a directory that is both listed and exempt. The test reads the same file the Makefile does, through os.ReadFile, so Go's test cache re-runs it when the list is edited.
go vet replays a cached finding with the path of the tree that produced it, so the second tree to vet identical content saw an absolute path and the plant check refused its own plant.
A sibling directory whose name only starts with a p/... entry's name, such as px beside p/..., is not covered by it. go test ./p/... does not run that sibling, so the rule must report it, and a planted case now holds that. A race-list line naming a path outside the root (./../a) or the root as a dot (./.) is refused, and each refusal now has a row of its own.
Two Go comments described the combined run: Engine's doc said -race was what the coverage gate uses, and the detect test said -race was the gate's only test step. The first now names `make race`; the second gives the 64 MiB case's cost under the coverage run's set mode, measured, in place of a -race figure that no longer describes any gate step. CLAUDE.md, docs/architecture.md and CONTRIBUTING.md now list the rule that holds scripts/race-packages.txt to the packages that run concurrently, and CONTRIBUTING's compiler recipe names its test.
Three comments described the combined run, not two: the first sweep matched exact phrases and missed detect_test.go:112, where TestCompiler_SourceLimitReadsTheCallersOptions justified its design by a gate race run that no longer covers compilers/openapi. It now cites the set-mode cost that TestDetect_ReadsASourceWithinTheCallersByteBudget measures. CLAUDE.md, docs/architecture.md and CONTRIBUTING.md now state the race-list rule by what TestRaceList_NamesEveryConcurrentPackage counts: a sync or sync/atomic import in a non-test file, or a go statement or single-argument .Go(...) call in any file. Packages whose tests only run in parallel are not flagged. CONTRIBUTING names the raceExempt route.
The skills now name the set-mode coverage build and the race build of the packages `make race` lists, in place of the combined `-race -covermode=atomic` run that `make coverage` no longer performs.
The allocation-count rule and its summary in the performance skill now name the race run as a separate `go test -race` run, because `go test` refuses `-race` together with `-covermode=set`.
Rewrap the childTimeout and probeMaxStack comments as whole paragraphs after their rewording; no words change.
make race passed with -race dropped from its recipe, as the packages it lists are race-free. scripts/race-reach.sh runs the recipe's own command over a race planted in internal/archtest/testdata/racereach and fails unless a data race naming the plant is reported, as scripts/vet-rules.sh does for make vet.
The figure of about 2 s and 430 MiB held for one source shape. A 65 MiB comment costs 1.2 s and 337 MiB; 65 MiB of paths entries costs 9.8 s and 2028 MiB. Say what holds for any shape.
Both readers of the list, the Makefile's sed and the archtest parser, take an indented # line as a comment.
The plant is test code, which coverage does not instrument, so atomic coverage cannot hide it the way it hid the same race in production code. The comments no longer say otherwise.
The reach script checks only the command passed to it. The Makefile now says that the recipe's first line is held only while it runs $(RACE_TEST).
The reach script holds only the command it is passed. The race recipe's first line is the empty-list guard, so the comment now names the line it means: the test of $(RACE_PACKAGES), which is held only while it runs $(RACE_TEST).
The plant's accesses were test code, which coverage does not instrument, so a race run with atomic coverage merged back in still passed the reach check: the regression GitHub #1271 removed. They now live in racereach.go, which coverage does instrument, so the script also refuses that run. The package is ruled with an empty allowlist, as production Go under testdata is. The coverage half depends on how the toolchain lays out instrumented code, so a later toolchain may stop hiding the plant. That weakens the check and never fails a correct recipe.
The skills' citation rule is references/<file>.md#<key>, the form skills_lint resolves; a bare anchor passes the lint whatever it names.
ParseProfiles ORs the counts of a set-mode profile and sums an atomic one's, so the comment named a merge the gate's profiles no longer undergo.
13 tasks done
callsParallel matching any no-argument method and raceCovers ignoring the subtree suffix both stayed green. The pipeline-roots test now plants a test whose only call is t.Helper() and an entry without "/..." beside a child directory, so each mutant reddens it. Also drops raceEntry's dir == "" check, which filepath.IsLocal already makes, reuses writeDocFile in writeParallelTest, and returns a found flag instead of "" from pipelineRootOf and importsSync.
An entry such as ./ir/... passed the race rule whenever it covered one package with a signal and no racePipelineOnly entry lay under it, so the list could grow from a dozen packages to every package under a root without the rule noticing, while the same widening spelled package by package was refused. A subtree entry under a root is now reported, and a plant pins it.
Three raceExempt reasons named work their tests do not do: the covergate's tallyOf test builds its profiles in memory, the decls tests share one fixture, and the exectest suite never starts a child. Each reason now says what the parallel tests call and over what.
The comment on RACE_TEST named compilers/openapi/e2e as the package that takes most of the 300s timeout on four busy cores. Since the two cancel tests joined race-pipeline, internal/load takes as long and has been seen past the limit as well, so the comment names both.
fuad-daoud
marked this pull request as ready for review
November 5, 2026 23:44
This was referenced Nov 6, 2026
skills: trial-merge-open-prs omits a change adding a package and building from the merged tree
#1556
Open
fuad-daoud
marked this pull request as draft
November 6, 2026 07:20
startsGoroutine, which feeds both the leak-check guard and the race-list rule, read a go statement and a single-argument .Go(...) call only. A package starting a goroutine through time.AfterFunc, context.AfterFunc, TryGo, or a Go method taken as a value joined no race list and drew no entry, so a race in it was found by neither run. Match a go statement or a selector named Go, TryGo or AfterFunc, called or passed as a value. The match stays syntactic, so a false positive costs one extra TestMain and a miss is what the guard prevents. No file in the tree differs between the two readings. Add a table test over every spelling, and state the wider reading where CLAUDE.md and docs/architecture.md describe the rule.
startsGoroutine assigned its flag on every selector, so a Go, TryGo or AfterFunc selector was forgotten as soon as the next selector in the walk was not one: g.Go(s.run) and time.AfterFunc(time.Second, f) read as starting no goroutine, which left the leak check and both race tables blind to them. The flag now only rises. Three rows join the spelling table, each with a sibling selector the earlier rows lacked. The method value row also regressed against the check this one replaced.
fuad-daoud
marked this pull request as ready for review
November 6, 2026 15:24
This was referenced Nov 7, 2026
docs: CONTRIBUTING.md says make gate keeps CI's order, which two side-by-side jobs do not have
#1581
Open
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Summary
In short: CI's slowest step ran every test under the race detector with atomic coverage, which is slow and can hide a data race. This splits it: coverage runs without
-race,-raceruns on each pull request over the packages that run code concurrently, and the whole pipeline is raced after each push tomain.make coverageran the whole suite once under-race -covermode=atomic. That run was the slowest step of every pull request, and atomic coverage under-racecan hide a data race: #1271 has a planted race that-racereports 6 of 6 times and-race -covermode=atomicreports 0 of 6. This splits the run in two, and races the whole pipeline after merge.make coveragerunsgo test ./... -covermode=setwithout-race.cmd/morphic-covergatereads only whether a block ran, so its verdict is unchanged. The gate prints the statement count it judged on every run.make race, on every pull request, runs-racealone on the patterns inscripts/race-packages.txt: the packages whose production code importssyncorsync/atomic, those with a file that starts a goroutine (agostatement, or a selector namedGo,TryGoorAfterFunc), and, outside the pipeline, those whose tests run in parallel, bar eachraceExemptandracePipelineOnlyentry. It skipsTestLoad_ACancelAtAnyCheckEndsTheLoadCleanlyandTestLoad_DoesNextToNothingOnceItRefusesACancel, which cancel a load at every check and cost most ofload's race time;make race-pipelinestill races them, and they run at full size inmake coverage. Among its tests isTestEngine_ConcurrentRunSharesOneEngine, which compiles the conformance corpus through one engine on 8 goroutines.make race-pipelineadds every package under the trees inscripts/race-pipeline-roots.txt(compilers,ir,pass), whose parallel tests run pipeline code on many goroutines. Thesuitejob runs it on each push tomain, after the cache save, so it adds nothing to a pull request's wait and a race it finds costs no pull request its cache.make gateleaves it out, and every statement of whatmake gateand CI run says so.scripts/race-reach.shruns the race command on a planted race underinternal/archtest/testdata/racereachand fails unless it is reported. The plant's racing accesses sit in a non-test file, so both regressions inRACE_TESTfail the gate: dropping-race, and merging coverage back into the race run.TestRaceList_NamesEveryConcurrentPackagereads the same two files the Makefile does. It fails when a package with one of the signals is on no list, when an entry or exemption is stale, when a package under a root is exempt from both runs, when a subtree entry (./x/...) lies under a root, whichmake racenames package by package, and when a package is both listed and deferred. A package under a root with a signal of its own thatmake raceleaves to the push run is aracePipelineOnlyentry with its reason; one elsewhere that neither run covers is araceExemptentry with its reason; and an entry of either table with a blank reason fails the rule.suitejob runscoverageandraceas separate steps, under a name that says so. Its id stayssuite, so its cache key is unchanged.What the race run gives up, deliberately:
make race-pipelineon each push tomain, and not on the pull request. A reviewer's plant on such a path, inir/irverify/ids.go'sprimIDViolation, is reported bymain's atomic run 7 of 9 times, bymake race0 of 3, and bymake race-pipelineon its run. The narrow list keeps the pull request's race step at 67–110s on this branch's Actions runs; the cost of the wide one is below.make raceskips twoloadtests that cancel a load at every check. Four cancel returns only they reach (load.go:248,bindings.go:145,overlays.go:82,phases.go:174) are raced bymake race-pipelinealone, after merge, and run at full size inmake coverage.raceExemptentry ininternal/archtest/racelist_test.gowith what its tests run,internal/harnessandinternal/census(ci: run coverage in set mode, and -race only where code is concurrent #1271) andinternal/archtest(its go/types loads race, test(archtest): the recursion pin's load can trip a go/types data race #855) among them. Every test still runs, without-race, inmake coverage.A
scripts/vet-rules.shfix that #1271 neither needs nor exposes was moved out to #1305.Closes #1271.
Before and after
flowchart LR subgraph before["Before: one combined run, every pull request"] direction TB b1["make coverage<br/>go test ./... -race -covermode=atomic"] --> b2["every package under the race detector<br/>284–386s on a one-file change"] b1 -. "atomic counters can act as<br/>synchronisation the detector trusts" .-> b3["some races go unreported"] end subgraph after["After: three runs"] direction TB a1["make coverage, every pull request<br/>go test ./... -covermode=set"] --> a2["100% gate, same verdict"] a3["make race, every pull request<br/>-race on scripts/race-packages.txt"] --> a4["packages that run code concurrently<br/>plus a planted race that must be reported"] a5["make race-pipeline, each push to main<br/>adds compilers, ir and pass"] --> a6["races only the pipeline's own<br/>parallel tests reach, after merge"] a7["TestRaceList_NamesEveryConcurrentPackage"] -. "holds both lists to the tree" .-> a3 end before ~~~ afterWhat it brings: coverage no longer silences the race detector, a pull request's race step stays at the narrow list's size, and the whole pipeline is raced on every merge, with each run proven to report a planted race.
Test plan
make gatepassed on16b38e3c, withmainatd3ce8e8fmerged in, on 8 pinned cores. Later commits changemake race's skip, the rule's test, comments and documents, including a reason check on both exemption tables, the comments onchildTimeoutandprobeTimeout, and the helper both the rule and the goroutine-leak check read,startsGoroutine, which now also countstime.AfterFunc,context.AfterFunc,TryGoand aGomethod value, each beside any other selector;go build ./...,go test ./internal/archtest ./internal/harness ./compilers/openapi/internal/canaryandmake vet lint lint-plantedpass on them, andskills_lint.pyis clean. CI on this head, withmainat2a0996a6merged in, runs the gate.The workflow's
coverageandracesteps ran on Actions in a test pull request on a fork (check: #1271 workflow on CI fuad-daoud/morphic#1): runs 41360252466 and 41365106637, on5f5b8198andce531939. The push-only step has not run on Actions:actionlintis clean on it, andmake -n race-pipelineraces the list's patterns plus the three roots, then runs the reach check.Each target's reach: removing
-racefromRACE_TEST, adding-covermode=atomicto it, or making the plant race-free, each failsscripts/race-reach.sh.Plants,
go clean -testcachebefore each run, in a throwaway copy:make racemake race-pipelineir/irverify/bigval.go, thebigval-not-numericarm ofappendBigValir/irverify/ids.go, first inprimIDViolationmain's combined run reports the first 0 of 3 and the second 7 of 9.The race-list rule reddens under each planted mutant: a root dropped or narrowed,
./barrieror./internal/leakcheckdropped from the list, a staleraceExemptorracePipelineOnlyentry, a pipeline package moved toraceExempt, a package both listed and deferred, the wide list (./ir/...,./pass,./compilers/...beside these) restored, with and without the deferrals that otherwise redden it, the parallel-test signal counted under the roots or never counted, a blank reason in either table, and one more for each of the rule's other branches. A comment edit to either file survives.TestStartsGoroutine_RecognizesEverySpellingfails 11 of its 17 rows on the helper that read onlygostatements and.Go(…)calls; a package whose production code callstime.AfterFuncreddens bothTestRaceList_NamesEveryConcurrentPackageandTestGoroutineLeak_EveryGoStatementHasALeakCheck. No file in the tree reads differently under the wider match.Each
racePipelineOnlyreason was read against every goroutine site in its package, and a planted race inCompileis reported fromcompilers/openapi/e2e's parallel tests under-race, which is why that package's reason defers them to the push run rather than calling them sequential.Cost,
go clean -testcachebefore each run, 4 pinned cores of a loaded AMD Ryzen 7 5800X:make race70–78s,make race-pipeline269–361s withload's two cancel tests still skipped, its slowest packagecompilers/openapi/e2eat 175–287s against-timeout 300s. With them in,loadalone takes 129–191s under-race. The commandmake race-pipelineruns, on 4 busy cores, timed out at 300s inloadandcompilers/openapi/e2eat a load average falling from 79 to 55, and with the timeout raised took 345s and 289s at a load average of about 25. On Actions,main's combined run at0d09a96d, which holds those two tests under-race, tookloadto 143s andcompilers/openapi/e2eto 92s, so the push run is expected to clear the limit; it has not run there. On Actions the 48-package race step took 327s with the race build cached.After merge: read that the first push to
mainranmake race-pipeline, its time against thesuitejob's 30 minutes, andload's andcompilers/openapi/e2e's against-timeout 300s. A-timeoutpanic is test: make coverage's 300s timeout is under twice what ir and load take #1248's, a child read as hung test: child-process probes read a child slowed by load as hung #1010's; one naming a race is the step working. test: move fuzz targets to packages that link no test tooling #1381 adds./internal/ircasesand./internal/testspectoscripts/race-packages.txtwhen it merges this, as their parallel tests call for; if it lands first, this branch adds them when it mergesmain.At merge, set the squash body by hand: a merge commit on this branch,
16b38e3c, carries attribution trailers that a default squash would paste ontomain.