fix(test): stop lane-runtime leaking a live server on every run
The two `upLane qc option` tests booted a real stack and then only released the slot. `downLane` locates a service's pid file through the lane's slot directory, so releasing the slot first orphaned the child with nothing left able to reach it — one `python3 -m http.server` survived every run, holding a port from a pool that is only ten wide. Thirteen had accumulated; the eleventh run onwards fails with EPORTBUSY in whichever test boots next, which reads as an unrelated flake. Both tests now stop the stack before releasing the slot, and assert the port went quiet — so a teardown that breaks again fails here rather than leaking into the next run. A suite-level `after` covers the case a test throws before its own teardown; it runs before SUITE_ROOT is removed, since the pid files it needs live inside it.
This commit is contained in:
@@ -29,7 +29,30 @@ const profileLib = require("../lib/lane-profile");
|
|||||||
const runtime = require("../lib/lane-runtime");
|
const runtime = require("../lib/lane-runtime");
|
||||||
const { isListening } = require("../lib/ports");
|
const { isListening } = require("../lib/ports");
|
||||||
|
|
||||||
after(() => fsMod.rmSync(SUITE_ROOT, { recursive: true, force: true }));
|
/**
|
||||||
|
* Lanes whose boot hook actually spawned a detached process. `harness_spawn`
|
||||||
|
* children outlive the hook by design, so a test that throws before its own
|
||||||
|
* `downLane` — or never calls one — leaves a real server holding a real port
|
||||||
|
* AFTER the suite exits. The port pool is only ten wide per name, so ten such
|
||||||
|
* runs exhaust it and every later boot fails with EPORTBUSY: a leak in these
|
||||||
|
* tests reads as a failure in whichever test runs next.
|
||||||
|
*/
|
||||||
|
const bootedLanes = new Set();
|
||||||
|
|
||||||
|
// One teardown, in this order on purpose: downLane reads the pid files under
|
||||||
|
// LANES_ROOT, which lives inside SUITE_ROOT — removing the tree first would
|
||||||
|
// leave nothing to kill the children with.
|
||||||
|
after(async () => {
|
||||||
|
for (const id of bootedLanes) {
|
||||||
|
try {
|
||||||
|
const lane = lanesLib.getLane(id);
|
||||||
|
if (lane) await runtime.downLane(lane);
|
||||||
|
} catch {
|
||||||
|
/* already stopped, or torn down by the test itself */
|
||||||
|
}
|
||||||
|
}
|
||||||
|
fsMod.rmSync(SUITE_ROOT, { recursive: true, force: true });
|
||||||
|
});
|
||||||
|
|
||||||
let laneSeq = 0;
|
let laneSeq = 0;
|
||||||
/** A lane row backed by a real directory, so profile lookup and mkdir work. */
|
/** A lane row backed by a real directory, so profile lookup and mkdir work. */
|
||||||
@@ -274,6 +297,7 @@ describe("lifecycle", () => {
|
|||||||
const lane = makeLane();
|
const lane = makeLane();
|
||||||
serverProfile(lane, 19100);
|
serverProfile(lane, 19100);
|
||||||
|
|
||||||
|
bootedLanes.add(lane.id);
|
||||||
const facts = await runtime.upLane(lanesLib.getLane(lane.id));
|
const facts = await runtime.upLane(lanesLib.getLane(lane.id));
|
||||||
assert.equal(facts.available, true);
|
assert.equal(facts.available, true);
|
||||||
assert.equal(facts.up, true);
|
assert.equal(facts.up, true);
|
||||||
@@ -301,6 +325,7 @@ describe("lifecycle", () => {
|
|||||||
assert.equal(afterDown.up, false);
|
assert.equal(afterDown.up, false);
|
||||||
assert.deepEqual(afterDown.services, []);
|
assert.deepEqual(afterDown.services, []);
|
||||||
|
|
||||||
|
bootedLanes.delete(lane.id);
|
||||||
slots.releaseSlot(lane.id);
|
slots.releaseSlot(lane.id);
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -377,12 +402,22 @@ describe("upLane qc option", () => {
|
|||||||
].join("\n"),
|
].join("\n"),
|
||||||
});
|
});
|
||||||
|
|
||||||
|
bootedLanes.add(lane.id);
|
||||||
await runtime.upLane(lanesLib.getLane(lane.id), { qc: true });
|
await runtime.upLane(lanesLib.getLane(lane.id), { qc: true });
|
||||||
const booted = lanesLib.getLane(lane.id);
|
const booted = lanesLib.getLane(lane.id);
|
||||||
const markerPath = pathMod.join(booted.cwd, "qc-marker.txt");
|
const markerPath = pathMod.join(booted.cwd, "qc-marker.txt");
|
||||||
const marker = fsMod.readFileSync(markerPath, "utf8");
|
const marker = fsMod.readFileSync(markerPath, "utf8");
|
||||||
assert.match(marker, /QC=from-qc/);
|
assert.match(marker, /QC=from-qc/);
|
||||||
|
|
||||||
|
// Stop the stack BEFORE releasing the slot: downLane locates the pid file
|
||||||
|
// through the lane's slot directory, so a released slot orphans a running
|
||||||
|
// child with no way left to reach it. Asserting the port went quiet is what
|
||||||
|
// makes a broken teardown fail HERE instead of leaking a live server into
|
||||||
|
// the next run's port pool.
|
||||||
|
const bootedPort = booted.ports.web;
|
||||||
|
await runtime.downLane(lanesLib.getLane(lane.id));
|
||||||
|
assert.equal(await isListening(bootedPort), false, "boot hook's child outlived downLane");
|
||||||
|
bootedLanes.delete(lane.id);
|
||||||
slots.releaseSlot(lane.id);
|
slots.releaseSlot(lane.id);
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -408,12 +443,17 @@ describe("upLane qc option", () => {
|
|||||||
].join("\n"),
|
].join("\n"),
|
||||||
});
|
});
|
||||||
|
|
||||||
|
bootedLanes.add(lane.id);
|
||||||
await runtime.upLane(lanesLib.getLane(lane.id));
|
await runtime.upLane(lanesLib.getLane(lane.id));
|
||||||
const booted = lanesLib.getLane(lane.id);
|
const booted = lanesLib.getLane(lane.id);
|
||||||
const markerPath = pathMod.join(booted.cwd, "qc-marker-no-qc.txt");
|
const markerPath = pathMod.join(booted.cwd, "qc-marker-no-qc.txt");
|
||||||
const marker = fsMod.readFileSync(markerPath, "utf8");
|
const marker = fsMod.readFileSync(markerPath, "utf8");
|
||||||
assert.match(marker, /QC=not-set/);
|
assert.match(marker, /QC=not-set/);
|
||||||
|
|
||||||
|
const bootedPort = booted.ports.web;
|
||||||
|
await runtime.downLane(lanesLib.getLane(lane.id));
|
||||||
|
assert.equal(await isListening(bootedPort), false, "boot hook's child outlived downLane");
|
||||||
|
bootedLanes.delete(lane.id);
|
||||||
slots.releaseSlot(lane.id);
|
slots.releaseSlot(lane.id);
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|||||||
Reference in New Issue
Block a user