From 52d5dd8dbaad949b08dd20b8e7fea86da7247eb8 Mon Sep 17 00:00:00 2001 From: Joyee Cheung Date: Fri, 4 Aug 2023 19:54:59 +0200 Subject: [PATCH] test: add expectSyncExitWithoutError() and expectSyncExit() utils These can be used to check the state and the output of a child process launched with `spawnSync()`. They log additional information about the child process when the check fails to facilitate debugging test failures. PR-URL: https://github.com/nodejs/node/pull/49020 Reviewed-By: Luigi Pinca --- test/common/README.md | 38 +++++++++++++++++ test/common/child_process.js | 80 ++++++++++++++++++++++++++++++++++++ 2 files changed, 118 insertions(+) diff --git a/test/common/README.md b/test/common/README.md index 35c6d835b3ddc8..3f68c5d8f5788f 100644 --- a/test/common/README.md +++ b/test/common/README.md @@ -6,6 +6,7 @@ This directory contains modules used to test the Node.js implementation. * [ArrayStream module](#arraystream-module) * [Benchmark module](#benchmark-module) +* [Child process module](#child-process-module) * [Common module API](#common-module-api) * [Countdown module](#countdown-module) * [CPU Profiler module](#cpu-profiler-module) @@ -35,6 +36,42 @@ The `benchmark` module is used by tests to run benchmarks. * `env` [\][] Environment variables to be applied during the run. +## Child Process Module + +The `child_process` module is used by tests that launch child processes. + +### `expectSyncExit(child, options)` + +Checks if a _synchronous_ child process runs in the way expected. If it does +not, print the stdout and stderr output from the child process and additional +information about it to the stderr of the current process before throwing +and error. This helps gathering more information about test failures +coming from child processes. + +* `child` [\][]: a `ChildProcess` instance + returned by `child_process.spawnSync()`. +* `options` [\][] + * `status` [\][] Expected `child.status` + * `signal` [\][] | `null` Expected `child.signal` + * `stderr` [\][] | [\][] | + [\][] Optional. If it's a string, check that the output + to the stderr of the child process is exactly the same as the string. If + it's a regular expression, check that the stderr matches it. If it's a + function, invoke it with the stderr output as a string and check + that it returns true. The function can just throw errors (e.g. assertion + errors) to provide more information if the check fails. + * `stdout` [\][] | [\][] | + [\][] Optional. Similar to `stderr` but for the stdout. + * `trim` [\][] Optional. Whether this method should trim + out the whitespace characters when checking `stderr` and `stdout` outputs. + Defaults to `false`. + +### `expectSyncExitWithoutError(child[, options])` + +Similar to `expectSyncExit()` with the `status` expected to be 0 and +`signal` expected to be `null`. Any other optional options are passed +into `expectSyncExit()`. + ## Common Module API The `common` module is used by tests for consistency across repeated @@ -1086,6 +1123,7 @@ See [the WPT tests README][] for details. []: https://developer.mozilla.org/en-US/docs/Web/API/ArrayBufferView []: https://nodejs.org/api/buffer.html#buffer_class_buffer []: https://developer.mozilla.org/en-US/docs/Web/API/BufferSource +[]: ../../doc/api/child_process.md#class-childprocess []: https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Error []: https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Function []: https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Object diff --git a/test/common/child_process.js b/test/common/child_process.js index 799c963a7ed7dc..a53dddc19f3216 100644 --- a/test/common/child_process.js +++ b/test/common/child_process.js @@ -2,6 +2,7 @@ const assert = require('assert'); const common = require('./'); +const util = require('util'); // Workaround for Windows Server 2008R2 // When CMD is used to launch a process and CMD is killed too quickly, the @@ -41,9 +42,88 @@ function logAfterTime(time) { }, time); } +function checkOutput(str, check) { + if ((check instanceof RegExp && !check.test(str)) || + (typeof check === 'string' && check !== str)) { + return { passed: false, reason: `did not match ${util.inspect(check)}` }; + } + if (typeof check === 'function') { + try { + check(str); + } catch (error) { + return { + passed: false, + reason: `did not match expectation, checker throws:\n${util.inspect(error)}`, + }; + } + } + return { passed: true }; +} + +function expectSyncExit(child, { + status, + signal, + stderr: stderrCheck, + stdout: stdoutCheck, + trim = false, +}) { + const failures = []; + let stderrStr, stdoutStr; + if (status !== undefined && child.status !== status) { + failures.push(`- process terminated with status ${child.status}, expected ${status}`); + } + if (signal !== undefined && child.signal !== signal) { + failures.push(`- process terminated with signal ${child.signal}, expected ${signal}`); + } + + function logAndThrow() { + const tag = `[process ${child.pid}]:`; + console.error(`${tag} --- stderr ---`); + console.error(stderrStr === undefined ? child.stderr.toString() : stderrStr); + console.error(`${tag} --- stdout ---`); + console.error(stdoutStr === undefined ? child.stdout.toString() : stdoutStr); + console.error(`${tag} status = ${child.status}, signal = ${child.signal}`); + throw new Error(`${failures.join('\n')}`); + } + + // If status and signal are not matching expectations, fail early. + if (failures.length !== 0) { + logAndThrow(); + } + + if (stderrCheck !== undefined) { + stderrStr = child.stderr.toString(); + const { passed, reason } = checkOutput(trim ? stderrStr.trim() : stderrStr, stderrCheck); + if (!passed) { + failures.push(`- stderr ${reason}`); + } + } + if (stdoutCheck !== undefined) { + stdoutStr = child.stdout.toString(); + const { passed, reason } = checkOutput(trim ? stdoutStr.trim() : stdoutStr, stdoutCheck); + if (!passed) { + failures.push(`- stdout ${reason}`); + } + } + if (failures.length !== 0) { + logAndThrow(); + } + return { child, stderr: stderrStr, stdout: stdoutStr }; +} + +function expectSyncExitWithoutError(child, options) { + return expectSyncExit(child, { + status: 0, + signal: null, + ...options, + }); +} + module.exports = { cleanupStaleProcess, logAfterTime, kExpiringChildRunTime, kExpiringParentTimer, + expectSyncExit, + expectSyncExitWithoutError, };