I’m facing an issue with Jest where I’m trying to enforce a timeout on its execution and kill it along with all its spawned child processes if it exceeds this timeout.
When the javascript heap space runs out in the environment where the tests are running, jest will not stop running.
Here’s what I’ve tried so far:
timedrunner.js
<code>import args from "command-line-args";
import { execa } from "execa";
import chalk from "chalk";
import process from "node:process";
import treeKill from "tree-kill";
import terminate from "terminate";
const optionDefinitions = [
{ name: "delay", alias: "d", type: Number },
{ name: "command", alias: "c", type: String }
];
const options = args(optionDefinitions);
const delay = options.delay;
const command = options.command;
console.log(
chalk.yellow(
`setting ${delay}s auto kill timeout for running command ${chalk.green(`'${command}'`)}`
)
);
const controller = new AbortController();
setTimeout(() => {
console.log(
chalk.red(
`timeout exceeded running command ${chalk.bold(`'${command}'`)}. exiting...`
)
);
controller.abort();
}, delay * 1000);
try {
execa({
shell: true,
stdio: "inherit",
verbose: "full",
timeout: delay * 1000,
detached: false,
cancelSignal: controller.signal,
forceKillAfterDelay: true,
cleanup: true
})`${command}`;
} catch (err) {
execa({
shell: true,
stdio: "inherit",
verbose: "full",
timeout: 2000,
detached: false,
forceKillAfterDelay: true,
cleanup: true
})`ps aux | grep jest | awk '{print $2}' | xargs kill -9`;
}
</code>
<code>import args from "command-line-args";
import { execa } from "execa";
import chalk from "chalk";
import process from "node:process";
import treeKill from "tree-kill";
import terminate from "terminate";
const optionDefinitions = [
{ name: "delay", alias: "d", type: Number },
{ name: "command", alias: "c", type: String }
];
const options = args(optionDefinitions);
const delay = options.delay;
const command = options.command;
console.log(
chalk.yellow(
`setting ${delay}s auto kill timeout for running command ${chalk.green(`'${command}'`)}`
)
);
const controller = new AbortController();
setTimeout(() => {
console.log(
chalk.red(
`timeout exceeded running command ${chalk.bold(`'${command}'`)}. exiting...`
)
);
controller.abort();
}, delay * 1000);
try {
execa({
shell: true,
stdio: "inherit",
verbose: "full",
timeout: delay * 1000,
detached: false,
cancelSignal: controller.signal,
forceKillAfterDelay: true,
cleanup: true
})`${command}`;
} catch (err) {
execa({
shell: true,
stdio: "inherit",
verbose: "full",
timeout: 2000,
detached: false,
forceKillAfterDelay: true,
cleanup: true
})`ps aux | grep jest | awk '{print $2}' | xargs kill -9`;
}
</code>
import args from "command-line-args";
import { execa } from "execa";
import chalk from "chalk";
import process from "node:process";
import treeKill from "tree-kill";
import terminate from "terminate";
const optionDefinitions = [
{ name: "delay", alias: "d", type: Number },
{ name: "command", alias: "c", type: String }
];
const options = args(optionDefinitions);
const delay = options.delay;
const command = options.command;
console.log(
chalk.yellow(
`setting ${delay}s auto kill timeout for running command ${chalk.green(`'${command}'`)}`
)
);
const controller = new AbortController();
setTimeout(() => {
console.log(
chalk.red(
`timeout exceeded running command ${chalk.bold(`'${command}'`)}. exiting...`
)
);
controller.abort();
}, delay * 1000);
try {
execa({
shell: true,
stdio: "inherit",
verbose: "full",
timeout: delay * 1000,
detached: false,
cancelSignal: controller.signal,
forceKillAfterDelay: true,
cleanup: true
})`${command}`;
} catch (err) {
execa({
shell: true,
stdio: "inherit",
verbose: "full",
timeout: 2000,
detached: false,
forceKillAfterDelay: true,
cleanup: true
})`ps aux | grep jest | awk '{print $2}' | xargs kill -9`;
}
package.json
<code>...
"test": "react-app-rewired test --verbose --silent --watchAll=false",
"test:prebuild": " node timed-runner.js --delay 2 --command 'npm run test'",
...
</code>
<code>...
"test": "react-app-rewired test --verbose --silent --watchAll=false",
"test:prebuild": " node timed-runner.js --delay 2 --command 'npm run test'",
...
</code>
...
"test": "react-app-rewired test --verbose --silent --watchAll=false",
"test:prebuild": " node timed-runner.js --delay 2 --command 'npm run test'",
...
running
<code>❯ npm run test:prebuild
> [email protected] test:prebuild
> node ./scripts/utils/timeout.js -d 2 -c 'npm run test -- --workerThreads=1'
setting 2s auto kill timeout for running command 'npm run test -- --workerThreads=1'
t::0> react-app-rewired test --verbose --silent --watchAll=false --workerThreads=1
t::0> PASS Test1
t::0> PASS Test2
t::1> PASS Test3
t::2> PASS Test3
t::3> timeout exceeded running command 'npm run test'. exiting...
████████████████████████████████████████timeout exceeded running command 'npm run test'. exiting...
[23:55:24.469] [0] ✘ Command was canceled: 'npm run test'
[23:55:24.469] [0] ✘ This operation was aborted
[23:55:24.469] [0] ✘ (done in 5s)
file:ui/node_modules/execa/lib/return/final-error.js:6
return new ErrorClass(message, options);
^
ExecaError: Command was canceled: 'npm run test'
This operation was aborted
at getFinalError (file:ui/node_modules/execa/lib/return/final-error.js:6:9)
at makeError (file:ui/node_modules/execa/lib/return/result.js:108:16)
at getAsyncResult (file:ui/node_modules/execa/lib/methods/main-async.js:174:4)
at handlePromise (file:ui/node_modules/execa/lib/methods/main-async.js:157:17)
at process.processTicksAndRejections (node:internal/process/task_queues:95:5) {
shortMessage: "Command was canceled: 'npm run test'nThis operation was aborted",
originalMessage: 'This operation was aborted',
command: 'npm run test',
escapedCommand: "'npm run test'",
cwd: 'ui',
durationMs: 5009.989291,
failed: true,
timedOut: false,
isCanceled: true,
isGracefullyCanceled: false,
isTerminated: true,
isMaxBuffer: false,
isForcefullyTerminated: false,
>
</code>
<code>❯ npm run test:prebuild
> [email protected] test:prebuild
> node ./scripts/utils/timeout.js -d 2 -c 'npm run test -- --workerThreads=1'
setting 2s auto kill timeout for running command 'npm run test -- --workerThreads=1'
t::0> react-app-rewired test --verbose --silent --watchAll=false --workerThreads=1
t::0> PASS Test1
t::0> PASS Test2
t::1> PASS Test3
t::2> PASS Test3
t::3> timeout exceeded running command 'npm run test'. exiting...
████████████████████████████████████████timeout exceeded running command 'npm run test'. exiting...
[23:55:24.469] [0] ✘ Command was canceled: 'npm run test'
[23:55:24.469] [0] ✘ This operation was aborted
[23:55:24.469] [0] ✘ (done in 5s)
file:ui/node_modules/execa/lib/return/final-error.js:6
return new ErrorClass(message, options);
^
ExecaError: Command was canceled: 'npm run test'
This operation was aborted
at getFinalError (file:ui/node_modules/execa/lib/return/final-error.js:6:9)
at makeError (file:ui/node_modules/execa/lib/return/result.js:108:16)
at getAsyncResult (file:ui/node_modules/execa/lib/methods/main-async.js:174:4)
at handlePromise (file:ui/node_modules/execa/lib/methods/main-async.js:157:17)
at process.processTicksAndRejections (node:internal/process/task_queues:95:5) {
shortMessage: "Command was canceled: 'npm run test'nThis operation was aborted",
originalMessage: 'This operation was aborted',
command: 'npm run test',
escapedCommand: "'npm run test'",
cwd: 'ui',
durationMs: 5009.989291,
failed: true,
timedOut: false,
isCanceled: true,
isGracefullyCanceled: false,
isTerminated: true,
isMaxBuffer: false,
isForcefullyTerminated: false,
>
</code>
❯ npm run test:prebuild
> [email protected] test:prebuild
> node ./scripts/utils/timeout.js -d 2 -c 'npm run test -- --workerThreads=1'
setting 2s auto kill timeout for running command 'npm run test -- --workerThreads=1'
t::0> react-app-rewired test --verbose --silent --watchAll=false --workerThreads=1
t::0> PASS Test1
t::0> PASS Test2
t::1> PASS Test3
t::2> PASS Test3
t::3> timeout exceeded running command 'npm run test'. exiting...
████████████████████████████████████████timeout exceeded running command 'npm run test'. exiting...
[23:55:24.469] [0] ✘ Command was canceled: 'npm run test'
[23:55:24.469] [0] ✘ This operation was aborted
[23:55:24.469] [0] ✘ (done in 5s)
file:ui/node_modules/execa/lib/return/final-error.js:6
return new ErrorClass(message, options);
^
ExecaError: Command was canceled: 'npm run test'
This operation was aborted
at getFinalError (file:ui/node_modules/execa/lib/return/final-error.js:6:9)
at makeError (file:ui/node_modules/execa/lib/return/result.js:108:16)
at getAsyncResult (file:ui/node_modules/execa/lib/methods/main-async.js:174:4)
at handlePromise (file:ui/node_modules/execa/lib/methods/main-async.js:157:17)
at process.processTicksAndRejections (node:internal/process/task_queues:95:5) {
shortMessage: "Command was canceled: 'npm run test'nThis operation was aborted",
originalMessage: 'This operation was aborted',
command: 'npm run test',
escapedCommand: "'npm run test'",
cwd: 'ui',
durationMs: 5009.989291,
failed: true,
timedOut: false,
isCanceled: true,
isGracefullyCanceled: false,
isTerminated: true,
isMaxBuffer: false,
isForcefullyTerminated: false,
>
Even after grep and kill, the main process with node is still in action