Repository navigation
ci: run coverage in set mode and -race on concurrent packages - #1311
fuad-daoud wants to merge 32 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.
The calls to mark are not dead code. write and read both run it, so under coverage they share its counter, and that shared update is what hides the race from a run with coverage merged in. Without the calls the race is reported anyway, and scripts/race-reach.sh would pass a run with coverage merged in. The hiding also depends on where the calls sit: moving write's call before its store, or read's after its load, brings the race back under coverage. The comment on mark now says both, so neither reads as something to tidy away.
|
git grep -l 't\.Parallel()' -- '*_test.go' ':!**/testdata/**' | xargs -n1 dirname | sort -u |
grep -vxE 'barrier|cmd/morphic|compilers/openapi/internal/load|engine(/.*)?|internal/ctxtest|internal/leakcheck|ir/irfacts' | wc -lI planted one unsynchronized package-level
var danglingProps int // above checkPropIDRefs
for _, ref := range dangling {
danglingProps++
So on these two, head is no worse than Where the gap is stated too narrowly:
Rules: CLAUDE.md's "Correctness comes first", and "A limitation that is deliberately out of scope must be stated in the code and in the PR body". Direction, as a draft:
The first fixes the gap. The second makes it a stated trade. |
|
Rule: CLAUDE.md's "Derive counts; never maintain them … If a claim about code must be written down, name the revision it was true at." Fix: name the run and head each count was read from, or drop the counts. The gate already derives the first one on every run. |
Summary
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.make coveragerunsgo test ./... -covermode=setwithout-race.cmd/morphic-covergatereads only whether a block ran, so its verdict is unchanged: 100% of 16,833 statements on this branch.make raceruns-racealone, on the packages listed inscripts/race-packages.txt: the ones whose tests run morphic code on more than one goroutine (load,irfacts,engine/...,cmd/morphic,barrier,leakcheck,ctxtest). It skipsTestLoad_ACancelAtAnyCheckEndsTheLoadCleanlyandTestLoad_DoesNextToNothingOnceItRefusesACancel, which test sequential cancellation and still run at full size inmake coverage.make raceproves it reaches: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 fail the gate: dropping-race, and merging coverage back into the race run (the plant is reported 6 of 6 under-raceand 0 of 6 with-covermode=atomicadded). ci: run coverage in set mode, and -race only where code is concurrent #1271 records that the hiding depends on layout, so a later toolchain could weaken the coverage half; it can never redden a correct recipe.TestRaceList_NamesEveryConcurrentPackagefails when a package whose production code takes a lock, an atomic or agostatement, or whose tests start goroutines, is neither listed nor exempt with its reason, and when an entry or exemption is stale. It reads the same file the Makefile does.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:
-race, inmake coverage.internal/harnessandinternal/census: tooling whose tests callharness.Checkand(*Census).Addfrom parallel goroutines. A race there goes unreported. The list file says so.TestEngine_ConcurrentRunSharesOneEnginepassesRunOptions{}, which leavesVerifyoff. It race-checks the compile and the passes; other listed tests reachVerify.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"] 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: two runs"] direction TB a1["make coverage<br/>go test ./... -covermode=set"] --> a2["100% gate, same verdict<br/>77–85s"] a3["make race<br/>-race on scripts/race-packages.txt"] --> a4["only packages that run code concurrently<br/>24–95s"] a3 --> a5["race-reach.sh: a planted race<br/>must be reported"] a6["TestRaceList_NamesEveryConcurrentPackage"] -. "holds the list to the tree" .-> a3 end before ~~~ afterWhat it brings: the race detector is no longer silenced by coverage and is proven to report a planted race, and the suite job stops paying for it on packages whose code never runs concurrently. On the fork's measurements, the race run fell from 284–386s (combined) to 24–95s, with coverage alone at 77–85s.
Test plan
make gatepasses on this branch, pinned to 4 cores (GOMAXPROCS=4 taskset -c …), on the tree merged withmainat 39afce9.mainat 7cf3f95 is merged in since; on that merge the race-list rule passes,mainadds no race-detector mention and no production package that takes a lock or starts a goroutine, and CI on this head runs the gate.coveragestep endsCoverage gate passed: all 16833 statements covered.. Theracestep runs exactly the seven listed patterns (11 packages) with the two tests skipped, then the reach check.make race's reach: removing-racefrom the recipe, adding-covermode=atomicto it, or making the plant race-free, each failsscripts/race-reach.sh.TestRaceList_*andTestRaceListViolations_*each fail under 12 planted defects: an entry dropped, an unlistedsyncimport, an unlistedgostatement, a stale exemption, the/boundary in matching removed, test-only imports counted, and others. Each exemption's reason was read against every goroutine site in its package.TestRun_JudgesAProfilegained amode: setrow, which reddens when set-mode blocks are judged missed.-race,race detectorandcovermodemention outsidetestdatawas read and classified: those in listed packages stay, and those in packages that left the race run were reworded.mainhas saved a cache under the new steps. The first pull-request run restores a cache built for the combined run.