test: add shared off-test-goroutine assertion helper and convert the concurrency-sensitive suites - #2133
test: add shared off-test-goroutine assertion helper and convert the concurrency-sensitive suites#2133rudrankriyam wants to merge 4 commits into
Conversation
httptest handlers, RoundTrippers, injected stubs, and worker goroutines cannot call t.Fatal: it skips the response write and terminates the goroutine before it signals completion, so the client blocks and the test deadlocks instead of failing. Add internal/handlertest, which records the failure with Errorf and returns a value the fixture hands back — an error, a 500 written to the ResponseWriter, or a synthetic 500 response — so the code under test observes a deterministic failure and unwinds normally.
The availability edit and remove-from-sale commands patch territories from a worker pool, so these RoundTrippers and their response builders run off the test goroutine. The guards were already split across a t.Errorf and a duplicated fmt.Errorf, and territoryAvailabilityResponse still called t.Fatalf, which would terminate a worker before it sends its result and deadlock the run. Report through a single handlertest.Asserter instead: one message, an error the transport hands back, and a 500 response where the builder cannot return one.
The readiness fan-out drives these fixtures from bounded worker goroutines, so a t.Fatalf inside a RoundTripper stops it before it can return a response. Two of them also returned (nil, nil) after the Fatal, which would hand the client a nil response if it ever ran on. Report through handlertest instead and give the caller a value back: an error where the transport can fail the request, a 500 response where the fixture must answer. The unexpected-request branches that answered with a silent 500 now also record why.
…ertest TestConcurrentApplyInitializesOnlyAfterReadingStateUnderRunLock runs executeDistributionApply on a worker goroutine, and its writeRun and reconcileApply stubs called t.Fatal. Forcing either guard proves the hazard: with t.Fatal the worker is terminated before it sends on done and the test hangs until the package timeout panics; with handlertest it fails in milliseconds, naming both the violated guard and the run error it produced.
|
Preview deployment for your docs. Learn more about Mintlify Previews.
💡 Tip: Enable Workflows to automatically generate PRs for you. |
|
No actionable comments were generated in the recent review. 🎉 ℹ️ Recent review info⚙️ Run configurationConfiguration used: defaults Review profile: CHILL Plan: Pro Run ID: 📒 Files selected for processing (8)
Limit details: You’ve used the included review currently available. Your 70 included PR review attempts over the past 7 days set your current allowance at 1 review per hour. 📝 WalkthroughWalkthroughThe PR adds the ChangesSafe test failure handling
Estimated code review effort: 3 (Moderate) | ~20 minutes Merge Risk: ⚪ Minimal · up to This change is merge-ready after normal checks and review; no actionable merge-blocking risk remains. Possibly related PRs
Suggested labels: 🚥 Pre-merge checks | ✅ 5✅ Passed checks (5 passed)
✨ Finishing Touches📝 Generate docstrings
🧪 Generate unit tests (beta)
Usage-based review receipt
Note This review was completed with usage-based billing: files reviewed beyond your plan's included limits are billed at $0.25/file. Track spend and usage in your billing settings. Comment |
Why
Three consecutive review rounds flagged the same defect class:
t.Fatal/t.Fatalfcalled from code that does not run on the goroutine running the test.pricing availability edit,remove-from-sale) — RoundTrippers driven by the territory worker pool.The agreed fix each time was
t.Errorfplus an explicit return. The helper was never built, so the idiom has been re-derived by hand, inconsistently, in every round.t.Fatalrunsruntime.Goexiton whichever goroutine calls it. Inside an httptest handler that skips the response write and leaves the client under test waiting for a reply that never arrives. Inside a worker goroutine it kills the worker before it can send its result, so the test blocks on a channel or wait group until the package timeout panics, with the real assertion buried in the goroutine dump.Measured on
TestConcurrentApplyInitializesOnlyAfterReadingStateUnderRunLock, forcing itswriteRunguard to fire:t.Fatal(before)panic: test timed out after 20s, no assertion messagehandlertest(after)--- FAIL (0.00s)naming both the violated guard and the run error it producedWhat this adds
internal/handlertest: oneAsserter, bound withhandlertest.New(t), covering the three shapes the hazard takes.Errorf(format, args...)errorcarrying the same messageRespond(w, format, args...)Response(format, args...)*http.ResponseEach records once with
Errorf, which is safe from any goroutine, and hands the caller a value so the code under test observes a deterministic failure and unwinds normally. Messages render throughfmt.Errorf, so%wkeeps wrapping the operand and still prints readably in the test log — that collapses thet.Errorf(...)+ duplicatedfmt.Errorf(...)pairs the previous PRs had to write by hand into a single call with one message.internal/handlertest/handlertest_test.goproves the semantics: a deliberately failing handler answers a real client with a 500 inside a bounded deadline (a hang fails the test rather than running to the package timeout), the envelope carries the assertion detail, a failing stub lets its worker goroutine return through its channel, and every path reports exactly once.testing.TBcannot be implemented outsidetesting, soAsserterholds a narrowreporterinterface and the tests inject a recorder that captures reports without failing the run.docs/TESTING.mddocuments the convention so the next reviewer can point at it.Blast radius, measured
AST pass over the whole tree (
go/parser), countingt.Fatal,t.Fatalf, andt.FailNowlexically inside a func literal whose signature isfunc(http.ResponseWriter, *http.Request)orfunc(*http.Request) (*http.Response, error):go func() {}bodies.internal/cli/cmdtest3,472 sites / 254 files,internal/web273 / 19,internal/cli/web74 / 10,internal/cli/apps68 / 2,internal/cli/publish66 / 5,internal/cli/assets59 / 6,internal/cli/shared50 / 5.That number is a ceiling on the pattern, not on the hazard. A Fatal in a RoundTripper only deadlocks when the code under test issues that request off the test goroutine; most of the 3,805 are sequential fixtures where Fatal is merely ugly. It also misses two indirect classes the lexical scan cannot see, both present in this batch: fixture helpers called from inside a RoundTripper, and injected dependency stubs invoked on a worker goroutine.
Deliberately not a mass rewrite. This converts the sharp subset — suites whose production code fans out — and leaves the rest for follow-ups now that the helper and the documented convention exist.
Converted in this batch
internal/cli/cmdtest/pricing_availability_edit_territory_test.goupdateTerritoryAvailabilityTargetspatches from a worker poolt.Errorf+ duplicatedfmt.Errorfpairs collapsed; 1t.Fatalfin the response builder called from the transportinternal/cli/cmdtest/pricing_availability_remove_from_sale_test.got.FatalfinterritoryAvailabilityResponse; 4 silent unexpected-request guards now reportinternal/cli/validate/subscription_fetch_test.got.Fatalfconverted, two of which returned(nil, nil)after the Fatal — a nil response handed to the client if it ever ran oninternal/cli/validate/concurrency_test.goRespondinternal/cli/distribute/orchestration_test.goexecuteDistributionApplyruns on a worker goroutinet.Fatalcalls — the hang measured above13
t.Fatal/t.Fatalfremoved from off-test-goroutine code, 10 report/return pairs collapsed, 7 previously silent guards now name their cause.Not converted
internal/cli/ads/search_optimization_test.go— open PR fix(ads): bound optimization body pagination #2071 owns the file; next batch, after it lands.internal/cli/cmdtest,internal/web, andinternal/cli/webis the natural next sweep, prioritised by whether the command under test fans out.Behavior
Mechanism only, not assertions. Every converted test still fails on exactly the conditions it failed on before — the fixture now reports and answers instead of terminating its goroutine. Where a Fatal guarded an unrecoverable fixture error (a marshal failure, an unexpected request), the replacement still fails the test and additionally propagates a deterministic error or 500 to the code under test, so the outer assertion fails too rather than hanging.
The seven previously silent guards are the one place strictness increases: they returned an error or a 500 without recording anything, so an unexpected request could only surface indirectly. Each converted suite was re-run to confirm it stays green, which also confirms none of them currently hits those branches.
Verification
make build,make format,make check-docs,GOLANGCI_LINT_TIMEOUT="10m --allow-parallel-runners" make lint(0 issues),ASC_BYPASS_KEYCHAIN=1 make test— full suite green.go test ./internal/cli/validate ./internal/cli/cmdtest -run TestPricingAvailability -race -count=2green.ASC_BYPASS_KEYCHAIN=1throughout.Summary by CodeRabbit
Documentation
Tests