Documentation
¶
Overview ¶
Package childguard fails a test binary that leaves a process behind whose command line names a marker the caller chooses.
The marker is what makes this a guard rather than a census, and there are two in this tree: the directory the interpreter's process-substitution pipes live in, and the directory internal/plugin's fixtures live in. Both are *paths*, which is not a coincidence — a path is the one thing a stray carries in its command line that says whose it was.
A process substitution is a pipe and a command started beside the one that was given its path. If the shell stops without closing its end, or without waiting for what it started, the command it started stays: a `cat` blocked in open(2) on a FIFO no writer was coming to, sleeping, killable, and costing 0% of a processor. Harmless one at a time and unbounded over a day of test runs — thirteen were found alive at once, aged half an hour to five hours, and they had to be killed by hand (#972).
That exact shape cannot arise any more: the pipes are anonymous since #2893, so there is no open(2) for a command to wait in and no name for a stray to carry. What the marker still finds is the directory a `=(cmd)` writes its file in, which is the same construct and the same lifetime — and the guard is kept rather than retired because its subject is the class of leak, not the one spelling that produced the first thirteen.
Nothing reported them, and that is the part worth fixing. The test that leaked most sharply was the one asserting that the directory is cleaned up: it asserted cleanup while leaking, passed, and said nothing. Correcting the three call sites that were leaking on the day would not stop the fourth, which is the argument #860 and #890 both landed on and the reason this is a guard around the whole run rather than a kill in three tests.
What it guards, and what it does not ¶
A surviving *descendant of this test binary* whose command line names the marker. Both halves are load-bearing:
- A descendant, because a leak is this run's doing. Several agents run these suites at once on one machine, and reporting somebody else's stray would make the guard cry wolf where a real one is hardest to see.
- Naming the marker, because it is not a census of every child. Some tests mean to leave a process running — a job started with `&` and not waited for is the subject of several — and a guard that fired on those would be turned off within the day. The marker is what separates a leak from a background job that was the point.
So this does not catch every kind of stray child. It catches the class the leak belonged to, at every site rather than three, which is what makes it a guard rather than a patch.
And it cannot catch a run that was killed ¶
The census is taken after the run returns. A test binary that is killed rather than finished — a `go test` timeout, a SIGKILL — never reaches it, so nothing is reported however many strays there are; and a killed run is the one that leaves the most, because no deferred shutdown runs either.
That is not a gap to be closed here. It is the reason a child of these tests has to be able to end *itself*: #1093 was seven orphaned plugin fixtures spinning at 11% of a core each, from a run that was killed, and what fixed it was the fixture noticing its own end of input rather than anything this package could have said afterwards. A guard reports the ordinary leak. Self-defense is what covers the other kind.
Index ¶
Constants ¶
const Grace = 5 * time.Second
Grace is how long a straggler is given to finish before it is called a leak.
A substitution's own goroutine can still be closing its end of the pipe when the last test returns, and the command reading that end has to notice the end-of-file and exit — microseconds, but not zero, and a guard that reported those would be reporting the framework's scheduling. Long enough to cover that under load; short enough that a run which really did leak is not made to sit through it more than once.
const PipeMarker = "sh-procsub"
PipeMarker is the name the interpreter gives the directory its substitution pipes live in, and so the string a command line has to contain for the command to be holding one.
Here rather than in the interpreter because every package whose tests run a shell wants the same guard, and only one of them can own the constant. The interpreter keeps its own — a directory prefix is its business, not a test helper's — and its tests pin the two together, so a prefix that changed in one place fails the build rather than leaving the guard quietly finding nothing.
It is not the only marker, and a marker belongs to whoever writes the path. internal/plugin's fixtures are named by their own directory and that constant lives there, pinned to a real fixture path by a test of its own for the same reason this one is pinned to the interpreter's prefix.
Variables ¶
This section is empty.
Functions ¶
func Wrap ¶
Wrap returns a run that fails if the tests leave a process whose command line contains marker.
A package guards itself with
func TestMain(m *testing.M) { os.Exit(treeguard.Run(childguard.Wrap(m, childguard.PipeMarker))) }
and the status is the inner run's unless something was left behind, in which case a passing package still fails — the same rule treeguard follows, and for the same reason: the damage is silent precisely because the test that did it passed.
Types ¶
type Process ¶
Process is one surviving process: what it is, and enough to find it with.
func Holding ¶
Holding is the surviving descendants of root whose command line names marker, right now and without waiting.
Exported for the test whose subject is the cleanup itself: asserting that a directory was removed says nothing about whether anything is still reading what was in it, which is exactly the gap that let the sharpest leak pass.
func Unreaped ¶
Unreaped is the descendants of root that have exited and that nobody has called wait on — zombies.
A separate question from Holding and deliberately not folded into it. A stray holds a descriptor, a pipe and memory, and lasts until something kills it; a zombie holds a process-table slot and is cleared by the kernel when this binary exits. The second is bounded where the first is not, which is why #972 shipped the marker census without this one.
It is here now because the bound is not the whole story. "Started and never waited for" is the same sentence as a stray's, and a shell that does it at a prompt rather than in a test accumulates zombies over a session. Measured on interp's suite, a clean run ended with 55 of them — every one from a test fake standing in for the front end's waitpid and doing none of the waiting, and none of them the shell's (#1006). At zero it is worth keeping at zero, which a census can say and a review cannot.
No marker, unlike Holding, and it needs none: a marker exists to tell a leak from a background job that was the point, and a *live* background job is not defunct. A zombie is unambiguous.