Skip to content

Commit 2c69381

Browse files
authored
test(debugger): enforce teardown process isolation (#7660)
* test(debugger): enforce teardown process isolation Wait for the debugger target app to exit in `afterEach` before stopping the fake agent and starting the next test. Use a bounded SIGTERM wait with a SIGKILL fallback, and fail explicitly if the child still does not exit, to prevent cross-test span leakage. * test(integration): share process stop helper Move robust child-process teardown into `integration-tests/helpers` as `stopProc` and use it in debugger test utils. This centralizes SIGTERM/SIGKILL shutdown handling and helps prevent cross-test leakage from lingering processes.
1 parent 34bf8e5 commit 2c69381

File tree

2 files changed

+59
-3
lines changed

2 files changed

+59
-3
lines changed

integration-tests/debugger/utils.js

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -9,7 +9,7 @@ const { randomUUID } = require('crypto')
99
const Axios = require('axios')
1010

1111
const { assertObjectContains, assertUUID } = require('../helpers')
12-
const { sandboxCwd, useSandbox, FakeAgent, spawnProc } = require('../helpers')
12+
const { sandboxCwd, useSandbox, FakeAgent, spawnProc, stopProc } = require('../helpers')
1313
const { generateProbeConfig } = require('../../packages/dd-trace/test/debugger/devtools_client/utils')
1414
const { version } = require('../../package.json')
1515

@@ -222,7 +222,7 @@ function setup ({ env, testApp, testAppSource, dependencies, silent, stdioHandle
222222
})
223223

224224
afterEach(async function () {
225-
t.proc?.kill()
225+
await stopProc(t.proc)
226226
await t.agent?.stop()
227227
})
228228

integration-tests/helpers/index.js

Lines changed: 57 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -28,6 +28,7 @@ let shouldKill
2828
const ANY_STRING = Symbol('test.ANY_STRING')
2929
const ANY_NUMBER = Symbol('test.ANY_NUMBER')
3030
const ANY_VALUE = Symbol('test.ANY_VALUE')
31+
const defaultStopProcTimeoutMs = 2_000
3132

3233
/**
3334
* @param {string} filename
@@ -65,7 +66,7 @@ async function runAndCheckOutput (filename, cwd, expectedOut, expectedSource) {
6566

6667
if (expectedSource) {
6768
assert.match(out, new RegExp(`instrumentation source: ${expectedSource}`),
68-
`Expected the process to output "${expectedSource}", but logs only contain: "${out}"`)
69+
`Expected the process to output "${expectedSource}", but logs only contain: "${out}"`)
6970
}
7071
return pid
7172
}
@@ -261,6 +262,60 @@ function spawnProcAndExpectExit (filename, options = {}, stdioHandler, stderrHan
261262
})
262263
}
263264

265+
/**
266+
* Stop a process and wait for it to fully exit.
267+
*
268+
* Sends `SIGTERM` first, waits up to `timeoutMs`, and escalates to `SIGKILL` if needed.
269+
*
270+
* @param {childProcess.ChildProcess|undefined} proc - Process to stop.
271+
* @param {object} [options] - Stop options.
272+
* @param {number} [options.timeoutMs=defaultStopProcTimeoutMs] - Max wait per signal in milliseconds.
273+
* @returns {Promise<void>}
274+
*/
275+
async function stopProc (proc, { timeoutMs = defaultStopProcTimeoutMs } = {}) {
276+
if (!proc) return
277+
if (proc.exitCode !== null || proc.signalCode !== null) return
278+
279+
proc.kill()
280+
281+
const exitedAfterSigterm = await waitForProcExit(proc, timeoutMs)
282+
if (exitedAfterSigterm) return
283+
284+
proc.kill('SIGKILL')
285+
const exitedAfterSigkill = await waitForProcExit(proc, timeoutMs)
286+
287+
if (!exitedAfterSigkill) {
288+
throw new Error(`Process ${proc.pid} did not exit after SIGKILL`)
289+
}
290+
}
291+
292+
/**
293+
* Wait for a process to exit for up to `timeoutMs`.
294+
*
295+
* @param {childProcess.ChildProcess} proc - Process to wait for.
296+
* @param {number} timeoutMs - Max time to wait in milliseconds.
297+
* @returns {Promise<boolean>} `true` if the process exited before timeout.
298+
*/
299+
function waitForProcExit (proc, timeoutMs) {
300+
if (proc.exitCode !== null || proc.signalCode !== null) {
301+
return Promise.resolve(true)
302+
}
303+
304+
return new Promise((resolve) => {
305+
const timeout = setTimeout(() => {
306+
proc.removeListener('exit', onExit)
307+
resolve(false)
308+
}, timeoutMs)
309+
310+
proc.once('exit', onExit)
311+
312+
function onExit () {
313+
clearTimeout(timeout)
314+
resolve(true)
315+
}
316+
})
317+
}
318+
264319
/**
265320
* Internal implementation for spawnProc and spawnProcAndAllowExit.
266321
*
@@ -954,6 +1009,7 @@ module.exports = {
9541009
hookFile,
9551010
assertObjectContains,
9561011
assertUUID,
1012+
stopProc,
9571013
spawnProc,
9581014
spawnProcAndExpectExit,
9591015
telemetryForwarder,

0 commit comments

Comments
 (0)