Documentation
¶
Overview ¶
Package testutil provides shared resource-accounting helpers for tests that assert a unit of work returns the process to its pre-test baseline: open file descriptors, live goroutines, and retained heap. The helpers were hand-copied across several packages' leak tests; consolidating them keeps every fd/goroutine/ heap sample identical so a baseline taken in one place compares to an after-sample taken in another. The package is importable from non-test code (a test harness that is a regular package, not a _test.go file) as well as from _test.go files.
Index ¶
- func CountGoroutines() int
- func CountOpenFDs(tb testing.TB) int
- func ForcedGCHeapAlloc() uint64
- func MappedResidentBytes(pathSubstring string) (bytes uint64, mappings int, err error)
- func OpenFDCount() (int, error)
- func RequireMappedResidentBytes(tb testing.TB, pathSubstring string) uint64
- func RequireNoGoroutineLeak(tb testing.TB)
- func RequireNoGoroutineOrFDLeak(tb testing.TB)
Constants ¶
This section is empty.
Variables ¶
This section is empty.
Functions ¶
func CountGoroutines ¶
func CountGoroutines() int
CountGoroutines returns the number of goroutines currently running. It is a thin wrapper over runtime.NumGoroutine so a goroutine-leak assertion reads the same helper as its fd and heap counterparts.
func CountOpenFDs ¶
CountOpenFDs returns the open file-descriptor count, failing tb if the sample cannot be read. It is the one-line form a test uses for an fd-leak baseline/after comparison.
func ForcedGCHeapAlloc ¶
func ForcedGCHeapAlloc() uint64
ForcedGCHeapAlloc runs a garbage collection and returns the resulting live heap size (runtime.MemStats.HeapAlloc). Forcing the collection first reclaims unreachable allocations, so two samples taken this way compare RETAINED heap rather than allocation churn — the basis for a heap-leak assertion.
func MappedResidentBytes ¶ added in v0.4.0
MappedResidentBytes returns the total resident bytes of every mapping in this process whose backing path contains pathSubstring, and how many such mappings there were.
The mapping count is returned alongside the bytes because a substring that matches nothing yields zero bytes, and a zero that means "nothing matched" is indistinguishable from a zero that means "nothing is resident" — the second is a real answer and the first is a broken measurement. A caller comparing a before and an after sample should assert the count is the one it expects.
Resident bytes, not mapped bytes, is what a memory claim about a shared-memory region has to be made in: the region is a sparse memfd, so its mapped SIZE is fixed at attach and says nothing about how many of its pages any traffic has actually touched. Rss is reported in kB and converted here.
func OpenFDCount ¶
OpenFDCount returns the number of file descriptors this process currently holds, sampled from /proc/self/fd. It is the primitive the fd-leak assertions build on and the form non-test code uses, reporting a read failure as an error rather than failing a test; test files use CountOpenFDs to fail a testing.TB directly instead.
func RequireMappedResidentBytes ¶ added in v0.4.0
RequireMappedResidentBytes is MappedResidentBytes for a test: it fails tb if the sample cannot be read, or if no mapping matched pathSubstring at all — which would otherwise report zero resident bytes and let a comparison pass over a measurement of nothing.
func RequireNoGoroutineLeak ¶
RequireNoGoroutineLeak registers a t.Cleanup that fails tb unless the process's goroutine count has returned to at or below baseline+goroutineSettleSlack — baseline captured right now, at call time — by the time the test's own cleanup has finished.
Call it FIRST in the test, before starting whatever it will tear down (a host, a process, a supervisor run) AND before subscribing to anything that spawns its own background goroutine (e.g. an event bus's Subscribe): t.Cleanup runs in reverse-registration order, so the cleanup registered first runs last, after the teardown this check depends on; and baselining before such a subscription means its goroutine is counted, not silently absorbed into the baseline where a bug that leaves it running would go unseen.
LIMITS — what this does NOT detect: a leaked child process (it counts this process's own goroutines, nothing about a plugin process that outlives its supervisor); a leaked shared-memory region mapping; retained/growing heap; a goroutine that happens to exit only because an earlier-registered cleanup (e.g. Host.Stop) already ran before this one — this check proves nothing leaked past THAT teardown, not that nothing misbehaved before it; and a goroutine that leaked but is offset by an unrelated one exiting late, since this is a net count, not a per-goroutine identity check. What it DOES see, at goroutineSettleSlack of 0, is a single goroutine that outlives the test that started it — including one leaked once per host, per subscription, or per call without ever growing further.
The registered cleanup polls at a fixed cadence and requires goroutineSettleSamples consecutive samples at or below baseline+slack before treating the count as settled; it fails only once that never happens within the settleMaxWait iteration budget. A transient spike immediately after teardown — a goroutine still unwinding, not one that leaked — therefore never trips it, while a count that stays above baseline+slack for the whole window fails every time: there is no way to assemble goroutineSettleSamples consecutive passes without the count actually having fallen back into the tolerated band. See RequireNoGoroutineOrFDLeak for a test that also drives a real plugin process and wants an fd-count baseline alongside this one.
func RequireNoGoroutineOrFDLeak ¶
RequireNoGoroutineOrFDLeak is RequireNoGoroutineLeak plus an open-fd baseline, for a test that drives a real plugin process lifecycle — spawn, crash, restart, teardown — where the host process's own descriptors (control sockets, region memfds, eventfds) are exactly what such a defect leaves open. Call it FIRST, for the same reverse-registration and pre-subscription reasons RequireNoGoroutineLeak documents.
The name is deliberately narrow, not RequireNoProcessLeak or similar: it checks exactly two counters in THIS process, nothing about the plugin process(es) it spawns. In particular it does NOT detect an orphaned or leaked CHILD process, a leaked shared-memory region mapping, retained/growing heap, or a leak whose fd and goroutine footprint happens to cancel out against something else closing early — the fd dimension below is a net count, not a per-descriptor identity check, so a leaked fd offset by an unrelated extra close reads as zero net change. See RequireNoGoroutineLeak's LIMITS paragraph for the rest, which applies here too.
Both dimensions are always checked and reported even when the goroutine dimension fails first: this uses tb.Errorf internally, not tb.Fatalf, so a goroutine failure does not abort the cleanup before the fd count is sampled and asserted.
Both dimensions require the count to return to AT OR BELOW the baseline: this test's descriptors and its goroutines are alike its own to account for once its host has stopped, so neither has a legitimate reason to sit above baseline. The two differ only in how long they are given to get there — a goroutine may still be unwinding when the fd it held is already closed — which the shared settle loop's consecutive-sample requirement covers for both.
Types ¶
This section is empty.