From 8daf3484e94b2a1515bc152f3ca2047910d48e19 Mon Sep 17 00:00:00 2001 From: ehmicky Date: Fri, 21 Jun 2024 08:55:29 +0100 Subject: [PATCH] Allow `verbose` option to be a function for custom logging (#1130) --- docs/api.md | 84 +++++++++++- docs/debugging.md | 90 ++++++++++++ docs/typescript.md | 16 ++- index.d.ts | 1 + lib/arguments/command.js | 4 +- lib/io/contents.js | 2 +- lib/io/output-sync.js | 10 +- lib/methods/main-async.js | 33 ++--- lib/methods/main-sync.js | 35 ++--- lib/return/reject.js | 4 +- lib/stdio/stdio-option.js | 2 +- lib/verbose/complete.js | 32 ++--- lib/verbose/custom.js | 26 ++++ lib/verbose/default.js | 12 +- lib/verbose/error.js | 10 +- lib/verbose/info.js | 20 +-- lib/verbose/ipc.js | 9 +- lib/verbose/log.js | 43 +++--- lib/verbose/output.js | 19 +-- lib/verbose/start.js | 9 +- lib/verbose/values.js | 33 +++++ test-d/arguments/options.test-d.ts | 11 -- test-d/verbose.test-d.ts | 97 +++++++++++++ test/fixtures/nested-pipe-verbose.js | 21 +++ test/fixtures/nested.js | 10 +- test/fixtures/nested/custom-event.js | 3 + test/fixtures/nested/custom-json.js | 3 + test/fixtures/nested/custom-object-stdout.js | 11 ++ test/fixtures/nested/custom-option.js | 3 + test/fixtures/nested/custom-print-function.js | 10 ++ test/fixtures/nested/custom-print-multiple.js | 10 ++ test/fixtures/nested/custom-print.js | 10 ++ test/fixtures/nested/custom-result.js | 3 + test/fixtures/nested/custom-return.js | 5 + test/fixtures/nested/custom-throw.js | 7 + test/fixtures/nested/custom-uppercase.js | 5 + test/fixtures/noop-verbose.js | 12 ++ test/helpers/nested.js | 12 ++ test/helpers/verbose.js | 29 +++- test/verbose/complete.js | 2 +- test/verbose/custom-command.js | 97 +++++++++++++ test/verbose/custom-common.js | 48 +++++++ test/verbose/custom-complete.js | 92 +++++++++++++ test/verbose/custom-error.js | 97 +++++++++++++ test/verbose/custom-event.js | 57 ++++++++ test/verbose/custom-id.js | 35 +++++ test/verbose/custom-ipc.js | 119 ++++++++++++++++ test/verbose/custom-options.js | 47 +++++++ test/verbose/custom-output.js | 128 ++++++++++++++++++ test/verbose/custom-reject.js | 37 +++++ test/verbose/custom-result.js | 76 +++++++++++ test/verbose/custom-start.js | 84 ++++++++++++ test/verbose/custom-throw.js | 67 +++++++++ test/verbose/error.js | 2 +- test/verbose/info.js | 30 ++++ types/arguments/options.d.ts | 7 +- types/return/result.d.ts | 6 +- types/verbose.d.ts | 98 ++++++++++++++ 58 files changed, 1720 insertions(+), 165 deletions(-) create mode 100644 lib/verbose/custom.js create mode 100644 lib/verbose/values.js create mode 100644 test-d/verbose.test-d.ts create mode 100755 test/fixtures/nested-pipe-verbose.js create mode 100644 test/fixtures/nested/custom-event.js create mode 100644 test/fixtures/nested/custom-json.js create mode 100644 test/fixtures/nested/custom-object-stdout.js create mode 100644 test/fixtures/nested/custom-option.js create mode 100644 test/fixtures/nested/custom-print-function.js create mode 100644 test/fixtures/nested/custom-print-multiple.js create mode 100644 test/fixtures/nested/custom-print.js create mode 100644 test/fixtures/nested/custom-result.js create mode 100644 test/fixtures/nested/custom-return.js create mode 100644 test/fixtures/nested/custom-throw.js create mode 100644 test/fixtures/nested/custom-uppercase.js create mode 100755 test/fixtures/noop-verbose.js create mode 100644 test/verbose/custom-command.js create mode 100644 test/verbose/custom-common.js create mode 100644 test/verbose/custom-complete.js create mode 100644 test/verbose/custom-error.js create mode 100644 test/verbose/custom-event.js create mode 100644 test/verbose/custom-id.js create mode 100644 test/verbose/custom-ipc.js create mode 100644 test/verbose/custom-options.js create mode 100644 test/verbose/custom-output.js create mode 100644 test/verbose/custom-reject.js create mode 100644 test/verbose/custom-result.js create mode 100644 test/verbose/custom-start.js create mode 100644 test/verbose/custom-throw.js create mode 100644 types/verbose.d.ts diff --git a/docs/api.md b/docs/api.md index 99f0117a5e..8586f78302 100644 --- a/docs/api.md +++ b/docs/api.md @@ -1033,12 +1033,14 @@ More info [here](ipc.md#send-an-initial-message) and [there](input.md#any-input- ### options.verbose -_Type:_ `'none' | 'short' | 'full'`\ +_Type:_ `'none' | 'short' | 'full' | Function`\ _Default:_ `'none'` If `verbose` is `'short'`, prints the command on [`stderr`](https://en.wikipedia.org/wiki/Standard_streams#Standard_error_(stderr)): its file, arguments, duration and (if it failed) error message. -If `verbose` is `'full'`, the command's [`stdout`](https://en.wikipedia.org/wiki/Standard_streams#Standard_output_(stdout)), `stderr` and [IPC messages](ipc.md) are also printed. +If `verbose` is `'full'` or a function, the command's [`stdout`](https://en.wikipedia.org/wiki/Standard_streams#Standard_output_(stdout)), `stderr` and [IPC messages](ipc.md) are also printed. + +A [function](#verbose-function) can be passed to customize logging. Please see [this page](debugging.md#custom-logging) for more information. By default, this applies to both `stdout` and `stderr`, but [different values can also be passed](output.md#stdoutstderr-specific-options). @@ -1170,6 +1172,84 @@ If `false`, escapes the command arguments on Windows. [More info.](windows.md#cmdexe-escaping) +## Verbose function + +_Type_: `(string, VerboseObject) => string | undefined` + +Function passed to the [`verbose`](#optionsverbose) option to customize logging. + +[More info.](debugging.md#custom-logging) + +### Verbose object + +_Type_: `VerboseObject` or `SyncVerboseObject` + +Subprocess event object, for logging purpose, using the [`verbose`](#optionsverbose) option. + +#### verboseObject.type + +_Type_: `string` + +Event type. This can be: +- `'command'`: subprocess start +- `'output'`: `stdout`/`stderr` [output](output.md#stdout-and-stderr) +- `'ipc'`: IPC [output](ipc.md#retrieve-all-messages) +- `'error'`: subprocess [failure](errors.md#subprocess-failure) +- `'duration'`: subprocess success or failure + +#### verboseObject.message + +_Type_: `string` + +Depending on [`verboseObject.type`](#verboseobjecttype), this is: +- `'command'`: the [`result.escapedCommand`](#resultescapedcommand) +- `'output'`: one line from [`result.stdout`](#resultstdout) or [`result.stderr`](#resultstderr) +- `'ipc'`: one IPC message from [`result.ipcOutput`](#resultipcoutput) +- `'error'`: the [`error.shortMessage`](#errorshortmessage) +- `'duration'`: the [`result.durationMs`](#resultdurationms) + +#### verboseObject.escapedCommand + +_Type_: `string` + +The file and [arguments](input.md#command-arguments) that were run. This is the same as [`result.escapedCommand`](#resultescapedcommand). + +#### verboseObject.options + +_Type_: [`Options`](#options-1) or [`SyncOptions`](#options-1) + +The [options](#options-1) passed to the subprocess. + +#### verboseObject.commandId + +_Type_: `string` + +Serial number identifying the subprocess within the current process. It is incremented from `'0'`. + +This is helpful when multiple subprocesses are running at the same time. + +This is similar to a [PID](https://en.wikipedia.org/wiki/Process_identifier) except it has no maximum limit, which means it never repeats. Also, it is usually shorter. + +#### verboseObject.timestamp + +_Type_: `Date` + +Event date/time. + +#### verboseObject.result + +_Type_: [`Result`](#result), [`SyncResult`](#result) or `undefined` + +Subprocess [result](#result). + +This is `undefined` if [`verboseObject.type`](#verboseobjecttype) is `'command'`, `'output'` or `'ipc'`. + +#### verboseObject.piped + +_Type_: `boolean` + +Whether another subprocess is [piped](pipe.md) into this subprocess. This is `false` when [`result.pipedFrom`](#resultfailed) is empty. + ## Transform options A transform or an [array of transforms](transform.md#combining) can be passed to the [`stdin`](#optionsstdin), [`stdout`](#optionsstdout), [`stderr`](#optionsstderr) or [`stdio`](#optionsstdio) option. diff --git a/docs/debugging.md b/docs/debugging.md index bbe9a7f0f2..b17f4fcaed 100644 --- a/docs/debugging.md +++ b/docs/debugging.md @@ -106,6 +106,96 @@ When printed to a terminal, the verbose mode uses colors. execa verbose output +## Custom logging + +### Verbose function + +The [`verbose`](api.md#optionsverbose) option can be a function to customize logging. + +It is called once per log line. The first argument is the default log line string. The second argument is the same information but as an object instead (documented [here](api.md#verbose-object)). + +If a string is returned, it is printed on `stderr`. If `undefined` is returned, nothing is printed. + +### Filter logs + +```js +import {execa as execa_} from 'execa'; + +// Only print log lines showing the subprocess duration +const execa = execa_({ + verbose(verboseLine, {type}) { + return type === 'duration' ? verboseLine : undefined; + }, +}); +``` + +### Transform logs + +```js +import {execa as execa_} from 'execa'; + +// Prepend current process' PID +const execa = execa_({ + verbose(verboseLine) { + return `[${process.pid}] ${verboseLine}` + }, +}); +``` + +### Custom log format + +```js +import {execa as execa_} from 'execa'; + +// Use a different format for the timestamp +const execa = execa_({ + verbose(verboseLine, {timestamp}) { + return verboseLine.replace(timestampRegExp, timestamp.toISOString()); + }, +}); + +// Timestamp at the start of each log line +const timestampRegExp = /\d{2}:\d{2}:\d{2}\.\d{3}/; +``` + +### JSON logging + +```js +import {execa as execa_} from 'execa'; + +const execa = execa_({ + verbose(verboseLine, verboseObject) { + return JSON.stringify(verboseObject) + }, +}); +``` + +### Advanced logging + +```js +import {execa as execa_} from 'execa'; +import {createLogger, transports} from 'winston'; + +// Log to a file using Winston +const transport = new transports.File({filename: 'logs.txt'}); +const logger = createLogger({transports: [transport]}); + +const execa = execa_({ + verbose(verboseLine, {type, message, ...verboseObject}) { + const level = LOG_LEVELS[type]; + logger[level](message, verboseObject); + }, +}); + +const LOG_LEVELS = { + command: 'info', + output: 'verbose', + ipc: 'verbose', + error: 'error', + duration: 'info', +}; +``` +
[**Next**: 📎 Windows](windows.md)\ diff --git a/docs/typescript.md b/docs/typescript.md index e767e22fbb..21bcda8635 100644 --- a/docs/typescript.md +++ b/docs/typescript.md @@ -8,7 +8,7 @@ ## Available types -The following types can be imported: [`ResultPromise`](api.md#return-value), [`Subprocess`](api.md#subprocess), [`Result`](api.md#result), [`ExecaError`](api.md#execaerror), [`Options`](api.md#options-1), [`StdinOption`](api.md#optionsstdin), [`StdoutStderrOption`](api.md#optionsstdout), [`TemplateExpression`](api.md#execacommand), [`Message`](api.md#subprocesssendmessagemessage-sendmessageoptions), [`ExecaMethod`](api.md#execaoptions), [`ExecaNodeMethod`](api.md#execanodeoptions) and [`ExecaScriptMethod`](api.md#options). +The following types can be imported: [`ResultPromise`](api.md#return-value), [`Subprocess`](api.md#subprocess), [`Result`](api.md#result), [`ExecaError`](api.md#execaerror), [`Options`](api.md#options-1), [`StdinOption`](api.md#optionsstdin), [`StdoutStderrOption`](api.md#optionsstdout), [`TemplateExpression`](api.md#execacommand), [`Message`](api.md#subprocesssendmessagemessage-sendmessageoptions), [`VerboseObject`](api.md#verbose-object), [`ExecaMethod`](api.md#execaoptions), [`ExecaNodeMethod`](api.md#execanodeoptions) and [`ExecaScriptMethod`](api.md#options). ```ts import { @@ -21,6 +21,7 @@ import { type StdoutStderrOption, type TemplateExpression, type Message, + type VerboseObject, type ExecaMethod, } from 'execa'; @@ -32,6 +33,9 @@ const options: Options = { stderr: 'pipe' satisfies StdoutStderrOption, timeout: 1000, ipc: true, + verbose(verboseLine: string, verboseObject: VerboseObject) { + return verboseObject.type === 'duration' ? verboseLine : undefined; + }, }; const task: TemplateExpression = 'build'; const message: Message = 'hello world'; @@ -50,7 +54,7 @@ try { ## Synchronous execution -Their [synchronous](#synchronous-execution) counterparts are [`SyncResult`](api.md#result), [`ExecaSyncError`](api.md#execasyncerror), [`SyncOptions`](api.md#options-1), [`StdinSyncOption`](api.md#optionsstdin), [`StdoutStderrSyncOption`](api.md#optionsstdout), [`TemplateExpression`](api.md#execacommand), [`ExecaSyncMethod`](api.md#execasyncoptions) and [`ExecaScriptSyncMethod`](api.md#syncoptions). +Their [synchronous](#synchronous-execution) counterparts are [`SyncResult`](api.md#result), [`ExecaSyncError`](api.md#execasyncerror), [`SyncOptions`](api.md#options-1), [`StdinSyncOption`](api.md#optionsstdin), [`StdoutStderrSyncOption`](api.md#optionsstdout), [`TemplateExpression`](api.md#execacommand), [`SyncVerboseObject`](api.md#verbose-object), [`ExecaSyncMethod`](api.md#execasyncoptions) and [`ExecaScriptSyncMethod`](api.md#syncoptions). ```ts import { @@ -61,6 +65,7 @@ import { type StdinSyncOption, type StdoutStderrSyncOption, type TemplateExpression, + type SyncVerboseObject, type ExecaSyncMethod, } from 'execa'; @@ -71,6 +76,9 @@ const options: SyncOptions = { stdout: 'pipe' satisfies StdoutStderrSyncOption, stderr: 'pipe' satisfies StdoutStderrSyncOption, timeout: 1000, + verbose(verboseLine: string, verboseObject: SyncVerboseObject) { + return verboseObject.type === 'duration' ? verboseLine : undefined; + }, }; const task: TemplateExpression = 'build'; @@ -93,6 +101,7 @@ import { execa as execa_, ExecaError, type Result, + type VerboseObject, } from 'execa'; const execa = execa_({preferLocal: true}); @@ -107,6 +116,9 @@ const options = { stderr: 'pipe', timeout: 1000, ipc: true, + verbose(verboseLine: string, verboseObject: VerboseObject) { + return verboseObject.type === 'duration' ? verboseLine : undefined; + }, } as const; const task = 'build'; const message = 'hello world'; diff --git a/index.d.ts b/index.d.ts index 723d3e2891..a227299683 100644 --- a/index.d.ts +++ b/index.d.ts @@ -24,3 +24,4 @@ export { getCancelSignal, type Message, } from './types/ipc.js'; +export type {VerboseObject, SyncVerboseObject} from './types/verbose.js'; diff --git a/lib/arguments/command.js b/lib/arguments/command.js index 774f13077e..d1f8e3602b 100644 --- a/lib/arguments/command.js +++ b/lib/arguments/command.js @@ -5,12 +5,12 @@ import {joinCommand} from './escape.js'; import {normalizeFdSpecificOption} from './specific.js'; // Compute `result.command`, `result.escapedCommand` and `verbose`-related information -export const handleCommand = (filePath, rawArguments, {piped, ...rawOptions}) => { +export const handleCommand = (filePath, rawArguments, rawOptions) => { const startTime = getStartTime(); const {command, escapedCommand} = joinCommand(filePath, rawArguments); const verbose = normalizeFdSpecificOption(rawOptions, 'verbose'); const verboseInfo = getVerboseInfo(verbose, escapedCommand, {...rawOptions}); - logCommand(escapedCommand, verboseInfo, piped); + logCommand(escapedCommand, verboseInfo); return { command, escapedCommand, diff --git a/lib/io/contents.js b/lib/io/contents.js index aaadf8b6c1..a8c30768b0 100644 --- a/lib/io/contents.js +++ b/lib/io/contents.js @@ -64,7 +64,7 @@ const logOutputAsync = async ({stream, onStreamEnd, fdNumber, encoding, allMixed stripFinalNewline: true, allMixed, }); - await logLines(linesIterable, stream, verboseInfo); + await logLines(linesIterable, stream, fdNumber, verboseInfo); }; // When using `buffer: false`, users need to read `subprocess.stdout|stderr|all` right away diff --git a/lib/io/output-sync.js b/lib/io/output-sync.js index 508d647c92..b29fe755eb 100644 --- a/lib/io/output-sync.js +++ b/lib/io/output-sync.js @@ -51,6 +51,7 @@ const transformOutputResultSync = ( logOutputSync({ serializedResult, fdNumber, + state, verboseInfo, encoding, stdioItems, @@ -101,7 +102,7 @@ const serializeChunks = ({chunks, objectMode, encoding, lines, stripFinalNewline return {serializedResult}; }; -const logOutputSync = ({serializedResult, fdNumber, verboseInfo, encoding, stdioItems, objectMode}) => { +const logOutputSync = ({serializedResult, fdNumber, state, verboseInfo, encoding, stdioItems, objectMode}) => { if (!shouldLogOutput({ stdioItems, encoding, @@ -112,7 +113,12 @@ const logOutputSync = ({serializedResult, fdNumber, verboseInfo, encoding, stdio } const linesArray = splitLinesSync(serializedResult, false, objectMode); - logLinesSync(linesArray, verboseInfo); + + try { + logLinesSync(linesArray, fdNumber, verboseInfo); + } catch (error) { + state.error ??= error; + } }; // When the `std*` target is a file path/URL or a file descriptor diff --git a/lib/methods/main-async.js b/lib/methods/main-async.js index 7de9120414..473625f539 100644 --- a/lib/methods/main-async.js +++ b/lib/methods/main-async.js @@ -14,7 +14,6 @@ import {pipeOutputAsync} from '../io/output-async.js'; import {subprocessKill} from '../terminate/kill.js'; import {cleanupOnExit} from '../terminate/cleanup.js'; import {pipeToSubprocess} from '../pipe/setup.js'; -import {logEarlyResult} from '../verbose/complete.js'; import {makeAllStream} from '../resolve/all-async.js'; import {waitForSubprocessResult} from '../resolve/wait-subprocess.js'; import {addConvertedStreams} from '../convert/add.js'; @@ -48,25 +47,19 @@ export const execaCoreAsync = (rawFile, rawArguments, rawOptions, createNested) // Compute arguments to pass to `child_process.spawn()` const handleAsyncArguments = (rawFile, rawArguments, rawOptions) => { const {command, escapedCommand, startTime, verboseInfo} = handleCommand(rawFile, rawArguments, rawOptions); - - try { - const {file, commandArguments, options: normalizedOptions} = normalizeOptions(rawFile, rawArguments, rawOptions); - const options = handleAsyncOptions(normalizedOptions); - const fileDescriptors = handleStdioAsync(options, verboseInfo); - return { - file, - commandArguments, - command, - escapedCommand, - startTime, - verboseInfo, - options, - fileDescriptors, - }; - } catch (error) { - logEarlyResult(error, startTime, verboseInfo); - throw error; - } + const {file, commandArguments, options: normalizedOptions} = normalizeOptions(rawFile, rawArguments, rawOptions); + const options = handleAsyncOptions(normalizedOptions); + const fileDescriptors = handleStdioAsync(options, verboseInfo); + return { + file, + commandArguments, + command, + escapedCommand, + startTime, + verboseInfo, + options, + fileDescriptors, + }; }; // Options normalization logic specific to async methods. diff --git a/lib/methods/main-sync.js b/lib/methods/main-sync.js index e068fc840f..a21315bec4 100644 --- a/lib/methods/main-sync.js +++ b/lib/methods/main-sync.js @@ -8,7 +8,6 @@ import {stripNewline} from '../io/strip-newline.js'; import {addInputOptionsSync} from '../io/input-sync.js'; import {transformOutputSync} from '../io/output-sync.js'; import {getMaxBufferSync} from '../io/max-buffer.js'; -import {logEarlyResult} from '../verbose/complete.js'; import {getAllSync} from '../resolve/all-sync.js'; import {getExitResultSync} from '../resolve/exit-sync.js'; @@ -31,26 +30,20 @@ export const execaCoreSync = (rawFile, rawArguments, rawOptions) => { // Compute arguments to pass to `child_process.spawnSync()` const handleSyncArguments = (rawFile, rawArguments, rawOptions) => { const {command, escapedCommand, startTime, verboseInfo} = handleCommand(rawFile, rawArguments, rawOptions); - - try { - const syncOptions = normalizeSyncOptions(rawOptions); - const {file, commandArguments, options} = normalizeOptions(rawFile, rawArguments, syncOptions); - validateSyncOptions(options); - const fileDescriptors = handleStdioSync(options, verboseInfo); - return { - file, - commandArguments, - command, - escapedCommand, - startTime, - verboseInfo, - options, - fileDescriptors, - }; - } catch (error) { - logEarlyResult(error, startTime, verboseInfo); - throw error; - } + const syncOptions = normalizeSyncOptions(rawOptions); + const {file, commandArguments, options} = normalizeOptions(rawFile, rawArguments, syncOptions); + validateSyncOptions(options); + const fileDescriptors = handleStdioSync(options, verboseInfo); + return { + file, + commandArguments, + command, + escapedCommand, + startTime, + verboseInfo, + options, + fileDescriptors, + }; }; // Options normalization logic specific to sync methods diff --git a/lib/return/reject.js b/lib/return/reject.js index 284acea5bc..0f41d6823e 100644 --- a/lib/return/reject.js +++ b/lib/return/reject.js @@ -1,9 +1,9 @@ -import {logFinalResult} from '../verbose/complete.js'; +import {logResult} from '../verbose/complete.js'; // Applies the `reject` option. // Also print the final log line with `verbose`. export const handleResult = (result, verboseInfo, {reject}) => { - logFinalResult(result, verboseInfo); + logResult(result, verboseInfo); if (result.failed && reject) { throw result; diff --git a/lib/stdio/stdio-option.js b/lib/stdio/stdio-option.js index 4d52a351bc..192cea5b4b 100644 --- a/lib/stdio/stdio-option.js +++ b/lib/stdio/stdio-option.js @@ -1,6 +1,6 @@ import {STANDARD_STREAMS_ALIASES} from '../utils/standard-stream.js'; import {normalizeIpcStdioArray} from '../ipc/array.js'; -import {isFullVerbose} from '../verbose/info.js'; +import {isFullVerbose} from '../verbose/values.js'; // Add support for `stdin`/`stdout`/`stderr` as an alias for `stdio`. // Also normalize the `stdio` option. diff --git a/lib/verbose/complete.js b/lib/verbose/complete.js index dd6174057a..8f773fbe86 100644 --- a/lib/verbose/complete.js +++ b/lib/verbose/complete.js @@ -1,38 +1,24 @@ import prettyMs from 'pretty-ms'; -import {escapeLines} from '../arguments/escape.js'; -import {getDurationMs} from '../return/duration.js'; -import {isVerbose} from './info.js'; +import {isVerbose} from './values.js'; import {verboseLog} from './log.js'; import {logError} from './error.js'; -// When `verbose` is `short|full`, print each command's completion, duration and error -export const logFinalResult = ({shortMessage, durationMs, failed}, verboseInfo) => { - logResult(shortMessage, durationMs, verboseInfo, failed); -}; - -// Same but for early validation errors -export const logEarlyResult = (error, startTime, {rawOptions, ...verboseInfo}) => { - const shortMessage = escapeLines(String(error)); - const durationMs = getDurationMs(startTime); - const earlyVerboseInfo = {...verboseInfo, rawOptions: {...rawOptions, reject: true}}; - logResult(shortMessage, durationMs, earlyVerboseInfo, true); -}; - -const logResult = (shortMessage, durationMs, verboseInfo, failed) => { +// When `verbose` is `short|full|custom`, print each command's completion, duration and error +export const logResult = (result, verboseInfo) => { if (!isVerbose(verboseInfo)) { return; } - logError(shortMessage, verboseInfo, failed); - logDuration(durationMs, verboseInfo, failed); + logError(result, verboseInfo); + logDuration(result, verboseInfo); }; -const logDuration = (durationMs, verboseInfo, failed) => { - const logMessage = `(done in ${prettyMs(durationMs)})`; +const logDuration = (result, verboseInfo) => { + const verboseMessage = `(done in ${prettyMs(result.durationMs)})`; verboseLog({ type: 'duration', - logMessage, + verboseMessage, verboseInfo, - failed, + result, }); }; diff --git a/lib/verbose/custom.js b/lib/verbose/custom.js new file mode 100644 index 0000000000..d55ab577ac --- /dev/null +++ b/lib/verbose/custom.js @@ -0,0 +1,26 @@ +import {getVerboseFunction} from './values.js'; + +// Apply the `verbose` function on each line +export const applyVerboseOnLines = (printedLines, verboseInfo, fdNumber) => { + const verboseFunction = getVerboseFunction(verboseInfo, fdNumber); + return printedLines + .map(({verboseLine, verboseObject}) => applyVerboseFunction(verboseLine, verboseObject, verboseFunction)) + .filter(printedLine => printedLine !== undefined) + .map(printedLine => appendNewline(printedLine)) + .join(''); +}; + +const applyVerboseFunction = (verboseLine, verboseObject, verboseFunction) => { + if (verboseFunction === undefined) { + return verboseLine; + } + + const printedLine = verboseFunction(verboseLine, verboseObject); + if (typeof printedLine === 'string') { + return printedLine; + } +}; + +const appendNewline = printedLine => printedLine.endsWith('\n') + ? printedLine + : `${printedLine}\n`; diff --git a/lib/verbose/default.js b/lib/verbose/default.js index 0ee31a52a0..090a367408 100644 --- a/lib/verbose/default.js +++ b/lib/verbose/default.js @@ -6,8 +6,16 @@ import { yellowBright, } from 'yoctocolors'; -// Default logger for the `verbose` option -export const defaultLogger = ({type, message, timestamp, failed, piped, commandId, options: {reject = true}}) => { +// Default when `verbose` is not a function +export const defaultVerboseFunction = ({ + type, + message, + timestamp, + piped, + commandId, + result: {failed = false} = {}, + options: {reject = true}, +}) => { const timestampString = serializeTimestamp(timestamp); const icon = ICONS[type]({failed, reject, piped}); const color = COLORS[type]({reject}); diff --git a/lib/verbose/error.js b/lib/verbose/error.js index a3ca076afe..ed4c4b1ef2 100644 --- a/lib/verbose/error.js +++ b/lib/verbose/error.js @@ -1,13 +1,13 @@ import {verboseLog} from './log.js'; -// When `verbose` is `short|full`, print each command's error when it fails -export const logError = (logMessage, verboseInfo, failed) => { - if (failed) { +// When `verbose` is `short|full|custom`, print each command's error when it fails +export const logError = (result, verboseInfo) => { + if (result.failed) { verboseLog({ type: 'error', - logMessage, + verboseMessage: result.shortMessage, verboseInfo, - failed, + result, }); } }; diff --git a/lib/verbose/info.js b/lib/verbose/info.js index 3623f6da64..0e1afa2930 100644 --- a/lib/verbose/info.js +++ b/lib/verbose/info.js @@ -1,4 +1,4 @@ -import {getFdSpecificValue} from '../arguments/specific.js'; +import {isVerbose, VERBOSE_VALUES, isVerboseFunction} from './values.js'; // Information computed before spawning, used by the `verbose` option export const getVerboseInfo = (verbose, escapedCommand, rawOptions) => { @@ -31,21 +31,9 @@ const validateVerbose = verbose => { throw new TypeError('The "verbose: true" option was renamed to "verbose: \'short\'".'); } - if (!VERBOSE_VALUES.has(fdVerbose)) { - const allowedValues = [...VERBOSE_VALUES].map(allowedValue => `'${allowedValue}'`).join(', '); - throw new TypeError(`The "verbose" option must not be ${fdVerbose}. Allowed values are: ${allowedValues}.`); + if (!VERBOSE_VALUES.includes(fdVerbose) && !isVerboseFunction(fdVerbose)) { + const allowedValues = VERBOSE_VALUES.map(allowedValue => `'${allowedValue}'`).join(', '); + throw new TypeError(`The "verbose" option must not be ${fdVerbose}. Allowed values are: ${allowedValues} or a function.`); } } }; - -const VERBOSE_VALUES = new Set(['none', 'short', 'full']); - -// The `verbose` option can have different values for `stdout`/`stderr` -export const isVerbose = ({verbose}) => getFdGenericVerbose(verbose) !== 'none'; - -// Whether IPC and output and logged -export const isFullVerbose = ({verbose}, fdNumber) => getFdSpecificValue(verbose, fdNumber) === 'full'; - -const getFdGenericVerbose = verbose => verbose.every(fdVerbose => fdVerbose === verbose[0]) - ? verbose[0] - : verbose.find(fdVerbose => fdVerbose !== 'none'); diff --git a/lib/verbose/ipc.js b/lib/verbose/ipc.js index 01a690c463..779052b7cb 100644 --- a/lib/verbose/ipc.js +++ b/lib/verbose/ipc.js @@ -1,14 +1,15 @@ -import {verboseLog, serializeLogMessage} from './log.js'; -import {isFullVerbose} from './info.js'; +import {verboseLog, serializeVerboseMessage} from './log.js'; +import {isFullVerbose} from './values.js'; // When `verbose` is `'full'`, print IPC messages from the subprocess export const shouldLogIpc = verboseInfo => isFullVerbose(verboseInfo, 'ipc'); export const logIpcOutput = (message, verboseInfo) => { - const logMessage = serializeLogMessage(message); + const verboseMessage = serializeVerboseMessage(message); verboseLog({ type: 'ipc', - logMessage, + verboseMessage, + fdNumber: 'ipc', verboseInfo, }); }; diff --git a/lib/verbose/log.js b/lib/verbose/log.js index e67e6d41ec..df0de430d7 100644 --- a/lib/verbose/log.js +++ b/lib/verbose/log.js @@ -1,44 +1,45 @@ import {writeFileSync} from 'node:fs'; import {inspect} from 'node:util'; import {escapeLines} from '../arguments/escape.js'; -import {defaultLogger} from './default.js'; +import {defaultVerboseFunction} from './default.js'; +import {applyVerboseOnLines} from './custom.js'; // Write synchronously to ensure lines are properly ordered and not interleaved with `stdout` -export const verboseLog = ({type, logMessage, verboseInfo, failed, piped}) => { - const logObject = getLogObject({ - type, - failed, - piped, - verboseInfo, - }); - const printedLines = getPrintedLines(logMessage, logObject); - writeFileSync(STDERR_FD, `${printedLines}\n`); +export const verboseLog = ({type, verboseMessage, fdNumber, verboseInfo, result}) => { + const verboseObject = getVerboseObject({type, result, verboseInfo}); + const printedLines = getPrintedLines(verboseMessage, verboseObject); + const finalLines = applyVerboseOnLines(printedLines, verboseInfo, fdNumber); + writeFileSync(STDERR_FD, finalLines); }; -const getLogObject = ({ +const getVerboseObject = ({ type, - failed = false, - piped = false, - verboseInfo: {commandId, rawOptions}, + result, + verboseInfo: {escapedCommand, commandId, rawOptions: {piped = false, ...options}}, }) => ({ type, + escapedCommand, + commandId: `${commandId}`, timestamp: new Date(), - failed, piped, - commandId, - options: rawOptions, + result, + options, }); -const getPrintedLines = (logMessage, logObject) => logMessage +const getPrintedLines = (verboseMessage, verboseObject) => verboseMessage .split('\n') - .map(message => defaultLogger({...logObject, message})) - .join('\n'); + .map(message => getPrintedLine({...verboseObject, message})); + +const getPrintedLine = verboseObject => { + const verboseLine = defaultVerboseFunction(verboseObject); + return {verboseLine, verboseObject}; +}; // Unless a `verbose` function is used, print all logs on `stderr` const STDERR_FD = 2; // Serialize any type to a line string, for logging -export const serializeLogMessage = message => { +export const serializeVerboseMessage = message => { const messageString = typeof message === 'string' ? message : inspect(message); const escapedMessage = escapeLines(messageString); return escapedMessage.replaceAll('\t', ' '.repeat(TAB_SIZE)); diff --git a/lib/verbose/output.js b/lib/verbose/output.js index 74a76678de..c95b6274d9 100644 --- a/lib/verbose/output.js +++ b/lib/verbose/output.js @@ -1,7 +1,7 @@ import {BINARY_ENCODINGS} from '../arguments/encoding-option.js'; import {TRANSFORM_TYPES} from '../stdio/type.js'; -import {verboseLog, serializeLogMessage} from './log.js'; -import {isFullVerbose} from './info.js'; +import {verboseLog, serializeVerboseMessage} from './log.js'; +import {isFullVerbose} from './values.js'; // `ignore` opts-out of `verbose` for a specific stream. // `ipc` cannot use piping. @@ -24,18 +24,18 @@ const fdUsesVerbose = fdNumber => fdNumber === 1 || fdNumber === 2; const PIPED_STDIO_VALUES = new Set(['pipe', 'overlapped']); // `verbose: 'full'` printing logic with async methods -export const logLines = async (linesIterable, stream, verboseInfo) => { +export const logLines = async (linesIterable, stream, fdNumber, verboseInfo) => { for await (const line of linesIterable) { if (!isPipingStream(stream)) { - logLine(line, verboseInfo); + logLine(line, fdNumber, verboseInfo); } } }; // `verbose: 'full'` printing logic with sync methods -export const logLinesSync = (linesArray, verboseInfo) => { +export const logLinesSync = (linesArray, fdNumber, verboseInfo) => { for (const line of linesArray) { - logLine(line, verboseInfo); + logLine(line, fdNumber, verboseInfo); } }; @@ -49,11 +49,12 @@ export const logLinesSync = (linesArray, verboseInfo) => { const isPipingStream = stream => stream._readableState.pipes.length > 0; // When `verbose` is `full`, print stdout|stderr -const logLine = (line, verboseInfo) => { - const logMessage = serializeLogMessage(line); +const logLine = (line, fdNumber, verboseInfo) => { + const verboseMessage = serializeVerboseMessage(line); verboseLog({ type: 'output', - logMessage, + verboseMessage, + fdNumber, verboseInfo, }); }; diff --git a/lib/verbose/start.js b/lib/verbose/start.js index 526f239243..82fd516f21 100644 --- a/lib/verbose/start.js +++ b/lib/verbose/start.js @@ -1,16 +1,15 @@ -import {isVerbose} from './info.js'; +import {isVerbose} from './values.js'; import {verboseLog} from './log.js'; -// When `verbose` is `short|full`, print each command -export const logCommand = (escapedCommand, verboseInfo, piped) => { +// When `verbose` is `short|full|custom`, print each command +export const logCommand = (escapedCommand, verboseInfo) => { if (!isVerbose(verboseInfo)) { return; } verboseLog({ type: 'command', - logMessage: escapedCommand, + verboseMessage: escapedCommand, verboseInfo, - piped, }); }; diff --git a/lib/verbose/values.js b/lib/verbose/values.js new file mode 100644 index 0000000000..2ca75e7fe0 --- /dev/null +++ b/lib/verbose/values.js @@ -0,0 +1,33 @@ +import {getFdSpecificValue} from '../arguments/specific.js'; + +// The `verbose` option can have different values for `stdout`/`stderr` +export const isVerbose = ({verbose}, fdNumber) => getFdVerbose(verbose, fdNumber) !== 'none'; + +// Whether IPC and output and logged +export const isFullVerbose = ({verbose}, fdNumber) => !['none', 'short'].includes(getFdVerbose(verbose, fdNumber)); + +// The `verbose` option can be a function to customize logging +export const getVerboseFunction = ({verbose}, fdNumber) => { + const fdVerbose = getFdVerbose(verbose, fdNumber); + return isVerboseFunction(fdVerbose) ? fdVerbose : undefined; +}; + +// When using `verbose: {stdout, stderr, fd3, ipc}`: +// - `verbose.stdout|stderr|fd3` is used for 'output' +// - `verbose.ipc` is only used for 'ipc' +// - highest `verbose.*` value is used for 'command', 'error' and 'duration' +const getFdVerbose = (verbose, fdNumber) => fdNumber === undefined + ? getFdGenericVerbose(verbose) + : getFdSpecificValue(verbose, fdNumber); + +// When using `verbose: {stdout, stderr, fd3, ipc}` and logging is not specific to a file descriptor. +// We then use the highest `verbose.*` value, using the following order: +// - function > 'full' > 'short' > 'none' +// - if several functions are defined: stdout > stderr > fd3 > ipc +const getFdGenericVerbose = verbose => verbose.find(fdVerbose => isVerboseFunction(fdVerbose)) + ?? VERBOSE_VALUES.findLast(fdVerbose => verbose.includes(fdVerbose)); + +// Whether the `verbose` option is customized using a function +export const isVerboseFunction = fdVerbose => typeof fdVerbose === 'function'; + +export const VERBOSE_VALUES = ['none', 'short', 'full']; diff --git a/test-d/arguments/options.test-d.ts b/test-d/arguments/options.test-d.ts index a5442c052c..7ef0b997ff 100644 --- a/test-d/arguments/options.test-d.ts +++ b/test-d/arguments/options.test-d.ts @@ -220,17 +220,6 @@ execaSync('unicorns', {windowsHide: false as boolean}); expectError(await execa('unicorns', {windowsHide: 'false'})); expectError(execaSync('unicorns', {windowsHide: 'false'})); -await execa('unicorns', {verbose: 'none'}); -execaSync('unicorns', {verbose: 'none'}); -await execa('unicorns', {verbose: 'short'}); -execaSync('unicorns', {verbose: 'short'}); -await execa('unicorns', {verbose: 'full'}); -execaSync('unicorns', {verbose: 'full'}); -expectError(await execa('unicorns', {verbose: 'full' as string})); -expectError(execaSync('unicorns', {verbose: 'full' as string})); -expectError(await execa('unicorns', {verbose: 'other'})); -expectError(execaSync('unicorns', {verbose: 'other'})); - await execa('unicorns', {cleanup: false}); expectError(execaSync('unicorns', {cleanup: false})); await execa('unicorns', {cleanup: false as boolean}); diff --git a/test-d/verbose.test-d.ts b/test-d/verbose.test-d.ts new file mode 100644 index 0000000000..6f846660d5 --- /dev/null +++ b/test-d/verbose.test-d.ts @@ -0,0 +1,97 @@ +import { + expectType, + expectNotType, + expectAssignable, + expectNotAssignable, + expectError, +} from 'tsd'; +import { + execa, + execaSync, + type VerboseObject, + type SyncVerboseObject, + type Options, + type SyncOptions, + type Result, + type SyncResult, +} from '../index.js'; + +await execa('unicorns', {verbose: 'none'}); +execaSync('unicorns', {verbose: 'none'}); +await execa('unicorns', {verbose: 'short'}); +execaSync('unicorns', {verbose: 'short'}); +await execa('unicorns', {verbose: 'full'}); +execaSync('unicorns', {verbose: 'full'}); +expectError(await execa('unicorns', {verbose: 'full' as string})); +expectError(execaSync('unicorns', {verbose: 'full' as string})); +expectError(await execa('unicorns', {verbose: 'other'})); +expectError(execaSync('unicorns', {verbose: 'other'})); + +const voidVerbose = () => { + console.log(''); +}; + +await execa('unicorns', {verbose: voidVerbose}); +execaSync('unicorns', {verbose: voidVerbose}); +await execa('unicorns', {verbose: () => ''}); +execaSync('unicorns', {verbose: () => ''}); +await execa('unicorns', {verbose: voidVerbose as () => never}); +execaSync('unicorns', {verbose: voidVerbose as () => never}); +expectError(await execa('unicorns', {verbose: () => true})); +expectError(execaSync('unicorns', {verbose: () => true})); +expectError(await execa('unicorns', {verbose: () => '' as unknown})); +expectError(execaSync('unicorns', {verbose: () => '' as unknown})); + +await execa('unicorns', {verbose: (verboseLine: string) => ''}); +execaSync('unicorns', {verbose: (verboseLine: string) => ''}); +await execa('unicorns', {verbose: (verboseLine: unknown) => ''}); +execaSync('unicorns', {verbose: (verboseLine: unknown) => ''}); +expectError(await execa('unicorns', {verbose: (verboseLine: boolean) => ''})); +expectError(execaSync('unicorns', {verbose: (verboseLine: boolean) => ''})); +expectError(await execa('unicorns', {verbose: (verboseLine: never) => ''})); +expectError(execaSync('unicorns', {verbose: (verboseLine: never) => ''})); + +await execa('unicorns', {verbose: (verboseLine: string, verboseObject: object) => ''}); +execaSync('unicorns', {verbose: (verboseLine: string, verboseObject: object) => ''}); +await execa('unicorns', {verbose: (verboseLine: string, verboseObject: VerboseObject) => ''}); +execaSync('unicorns', {verbose: (verboseLine: string, verboseObject: VerboseObject) => ''}); +await execa('unicorns', {verbose: (verboseLine: string, verboseObject: unknown) => ''}); +execaSync('unicorns', {verbose: (verboseLine: string, verboseObject: unknown) => ''}); +expectError(await execa('unicorns', {verbose: (verboseLine: string, verboseObject: string) => ''})); +expectError(execaSync('unicorns', {verbose: (verboseLine: string, verboseObject: string) => ''})); +expectError(await execa('unicorns', {verbose: (verboseLine: string, verboseObject: never) => ''})); +expectError(execaSync('unicorns', {verbose: (verboseLine: string, verboseObject: never) => ''})); + +expectError(await execa('unicorns', {verbose: (verboseLine: string, verboseObject: object, other: string) => ''})); +expectError(execaSync('unicorns', {verbose: (verboseLine: string, verboseObject: object, other: string) => ''})); + +await execa('unicorns', { + verbose(verboseLine: string, verboseObject: VerboseObject) { + expectNotType(verboseObject.type); + expectAssignable(verboseObject.type); + expectNotAssignable(verboseObject.type); + expectType(verboseObject.message); + expectType(verboseObject.escapedCommand); + expectType(verboseObject.commandId); + expectType(verboseObject.timestamp); + expectType(verboseObject.result); + expectType(verboseObject.piped); + expectType(verboseObject.options); + expectError(verboseObject.other); + }, +}); +execaSync('unicorns', { + verbose(verboseLine: string, verboseObject: SyncVerboseObject) { + expectNotType(verboseObject.type); + expectAssignable(verboseObject.type); + expectNotAssignable(verboseObject.type); + expectType(verboseObject.message); + expectType(verboseObject.escapedCommand); + expectType(verboseObject.commandId); + expectType(verboseObject.timestamp); + expectType(verboseObject.result); + expectType(verboseObject.piped); + expectType(verboseObject.options); + expectError(verboseObject.other); + }, +}); diff --git a/test/fixtures/nested-pipe-verbose.js b/test/fixtures/nested-pipe-verbose.js new file mode 100755 index 0000000000..cfb063c846 --- /dev/null +++ b/test/fixtures/nested-pipe-verbose.js @@ -0,0 +1,21 @@ +#!/usr/bin/env node +import {execa, getOneMessage, sendMessage} from '../../index.js'; +import {getNestedOptions} from '../helpers/nested.js'; + +const { + file, + commandArguments = [], + options: {destinationFile, destinationArguments, ...options}, + optionsFixture, + optionsInput, +} = await getOneMessage(); + +const commandOptions = await getNestedOptions(options, optionsFixture, optionsInput); + +try { + const result = await execa(file, commandArguments, commandOptions) + .pipe(destinationFile, destinationArguments, commandOptions); + await sendMessage(result); +} catch (error) { + await sendMessage(error); +} diff --git a/test/fixtures/nested.js b/test/fixtures/nested.js index a6a0c9cf1a..01d6147d78 100755 --- a/test/fixtures/nested.js +++ b/test/fixtures/nested.js @@ -5,6 +5,7 @@ import { getOneMessage, sendMessage, } from '../../index.js'; +import {getNestedOptions} from '../helpers/nested.js'; const { isSync, @@ -15,14 +16,7 @@ const { optionsInput, } = await getOneMessage(); -let commandOptions = options; - -// Some subprocess options cannot be serialized between processes. -// For those, we pass a fixture filename instead, which dynamically creates the options. -if (optionsFixture !== undefined) { - const {getOptions} = await import(`./nested/${optionsFixture}`); - commandOptions = {...commandOptions, ...getOptions({...commandOptions, ...optionsInput})}; -} +const commandOptions = await getNestedOptions(options, optionsFixture, optionsInput); try { const result = isSync diff --git a/test/fixtures/nested/custom-event.js b/test/fixtures/nested/custom-event.js new file mode 100644 index 0000000000..2e134301f5 --- /dev/null +++ b/test/fixtures/nested/custom-event.js @@ -0,0 +1,3 @@ +export const getOptions = ({type, eventProperty}) => ({ + verbose: (verboseLine, verboseObject) => verboseObject.type === type ? `${verboseObject[eventProperty]}` : undefined, +}); diff --git a/test/fixtures/nested/custom-json.js b/test/fixtures/nested/custom-json.js new file mode 100644 index 0000000000..16efc23278 --- /dev/null +++ b/test/fixtures/nested/custom-json.js @@ -0,0 +1,3 @@ +export const getOptions = ({type}) => ({ + verbose: (verboseLine, verboseObject) => verboseObject.type === type ? JSON.stringify(verboseObject) : undefined, +}); diff --git a/test/fixtures/nested/custom-object-stdout.js b/test/fixtures/nested/custom-object-stdout.js new file mode 100644 index 0000000000..3af0a1d372 --- /dev/null +++ b/test/fixtures/nested/custom-object-stdout.js @@ -0,0 +1,11 @@ +import {foobarObject} from '../../helpers/input.js'; + +export const getOptions = () => ({ + verbose: (verboseLine, {type}) => type === 'output' ? verboseLine : undefined, + stdout: { + * transform() { + yield foobarObject; + }, + objectMode: true, + }, +}); diff --git a/test/fixtures/nested/custom-option.js b/test/fixtures/nested/custom-option.js new file mode 100644 index 0000000000..8dedd7a604 --- /dev/null +++ b/test/fixtures/nested/custom-option.js @@ -0,0 +1,3 @@ +export const getOptions = ({type, optionName}) => ({ + verbose: (verboseLine, verboseObject) => verboseObject.type === type ? `${verboseObject.options[optionName]}` : undefined, +}); diff --git a/test/fixtures/nested/custom-print-function.js b/test/fixtures/nested/custom-print-function.js new file mode 100644 index 0000000000..7f5af07886 --- /dev/null +++ b/test/fixtures/nested/custom-print-function.js @@ -0,0 +1,10 @@ +export const getOptions = ({type, fdNumber, secondFdNumber}) => ({ + verbose: { + [fdNumber](verboseLine, verboseObject) { + if (verboseObject.type === type) { + console.warn(verboseLine); + } + }, + [secondFdNumber]: 'none', + }, +}); diff --git a/test/fixtures/nested/custom-print-multiple.js b/test/fixtures/nested/custom-print-multiple.js new file mode 100644 index 0000000000..55260b94e5 --- /dev/null +++ b/test/fixtures/nested/custom-print-multiple.js @@ -0,0 +1,10 @@ +export const getOptions = ({type, fdNumber, secondFdNumber}) => ({ + verbose: { + [fdNumber](verboseLine, verboseObject) { + if (verboseObject.type === type) { + console.warn(verboseLine); + } + }, + [secondFdNumber]() {}, + }, +}); diff --git a/test/fixtures/nested/custom-print.js b/test/fixtures/nested/custom-print.js new file mode 100644 index 0000000000..fd181a5d8e --- /dev/null +++ b/test/fixtures/nested/custom-print.js @@ -0,0 +1,10 @@ +export const getOptions = ({type, fdNumber}) => ({ + verbose: setFdSpecific( + fdNumber, + (verboseLine, verboseObject) => verboseObject.type === type ? verboseLine : undefined, + ), +}); + +const setFdSpecific = (fdNumber, option) => fdNumber === undefined + ? option + : {[fdNumber]: option}; diff --git a/test/fixtures/nested/custom-result.js b/test/fixtures/nested/custom-result.js new file mode 100644 index 0000000000..77da34685d --- /dev/null +++ b/test/fixtures/nested/custom-result.js @@ -0,0 +1,3 @@ +export const getOptions = ({type}) => ({ + verbose: (verboseLine, verboseObject) => verboseObject.type === type ? JSON.stringify(verboseObject.result) : undefined, +}); diff --git a/test/fixtures/nested/custom-return.js b/test/fixtures/nested/custom-return.js new file mode 100644 index 0000000000..b32ea4d756 --- /dev/null +++ b/test/fixtures/nested/custom-return.js @@ -0,0 +1,5 @@ +export const getOptions = ({verboseOutput}) => ({ + verbose(verboseLine, {type}) { + return type === 'command' ? verboseOutput : undefined; + }, +}); diff --git a/test/fixtures/nested/custom-throw.js b/test/fixtures/nested/custom-throw.js new file mode 100644 index 0000000000..83f6bb32fc --- /dev/null +++ b/test/fixtures/nested/custom-throw.js @@ -0,0 +1,7 @@ +export const getOptions = ({type, errorMessage}) => ({ + verbose(verboseLine, verboseObject) { + if (verboseObject.type === type) { + throw new Error(errorMessage); + } + }, +}); diff --git a/test/fixtures/nested/custom-uppercase.js b/test/fixtures/nested/custom-uppercase.js new file mode 100644 index 0000000000..63c5a67bae --- /dev/null +++ b/test/fixtures/nested/custom-uppercase.js @@ -0,0 +1,5 @@ +export const getOptions = () => ({ + verbose(verboseLine, {type}) { + return type === 'command' ? verboseLine.replace('noop', 'NOOP') : undefined; + }, +}); diff --git a/test/fixtures/noop-verbose.js b/test/fixtures/noop-verbose.js new file mode 100755 index 0000000000..db1324e89d --- /dev/null +++ b/test/fixtures/noop-verbose.js @@ -0,0 +1,12 @@ +#!/usr/bin/env node +import process from 'node:process'; +import {sendMessage} from '../../index.js'; + +const bytes = process.argv[2]; +console.log(bytes); + +try { + await sendMessage(bytes); +} catch {} + +process.exitCode = 2; diff --git a/test/helpers/nested.js b/test/helpers/nested.js index cc0439f289..a9cc546f2b 100644 --- a/test/helpers/nested.js +++ b/test/helpers/nested.js @@ -4,6 +4,7 @@ import {execa} from '../../index.js'; import {FIXTURES_DIRECTORY_URL} from './fixtures-directory.js'; const WORKER_URL = new URL('worker.js', FIXTURES_DIRECTORY_URL); +const NESTED_URL = new URL('nested/', FIXTURES_DIRECTORY_URL); // Like `execa(file, commandArguments, options)` but spawns inside another parent process. // This is useful when testing logic where Execa modifies the global state. @@ -55,3 +56,14 @@ const spawnWorker = async workerData => { }; export const spawnParentProcess = ({parentFixture, parentOptions, ...ipcInput}) => execa(parentFixture, {...parentOptions, ipcInput}); + +// Some subprocess options cannot be serialized between processes. +// For those, we pass a fixture filename instead, which dynamically creates the options. +export const getNestedOptions = async (options, optionsFixture, optionsInput) => { + if (optionsFixture === undefined) { + return options; + } + + const {getOptions} = await import(new URL(optionsFixture, NESTED_URL)); + return {...options, ...getOptions({...options, ...optionsInput})}; +}; diff --git a/test/helpers/verbose.js b/test/helpers/verbose.js index df07093f5c..104fac26ee 100644 --- a/test/helpers/verbose.js +++ b/test/helpers/verbose.js @@ -27,10 +27,36 @@ export const runWarningSubprocess = async (t, isSync) => { export const runEarlyErrorSubprocess = async (t, isSync) => { const {stderr, nestedResult} = await nestedSubprocess('noop.js', [foobarString], {verbose: 'short', cwd: true, isSync}); t.true(nestedResult instanceof Error); - t.true(stderr.includes('The "cwd" option must')); + t.true(nestedResult.message.startsWith('The "cwd" option must')); return stderr; }; +export const runVerboseSubprocess = ({ + isSync = false, + type, + eventProperty, + optionName, + errorMessage, + fdNumber, + secondFdNumber, + optionsFixture = 'custom-event.js', + output = '. .', + ...options +}) => nestedSubprocess('noop-verbose.js', [output], { + ipc: !isSync, + optionsFixture, + optionsInput: { + type, + eventProperty, + optionName, + errorMessage, + fdNumber, + secondFdNumber, + }, + isSync, + ...options, +}); + export const getCommandLine = stderr => getCommandLines(stderr)[0]; export const getCommandLines = stderr => getNormalizedLines(stderr).filter(line => isCommandLine(line)); const isCommandLine = line => line.includes(' $ ') || line.includes(' | '); @@ -46,6 +72,7 @@ const isErrorLine = line => (line.includes(' × ') || line.includes(' ‼ ')) && export const getCompletionLine = stderr => getCompletionLines(stderr)[0]; export const getCompletionLines = stderr => getNormalizedLines(stderr).filter(line => isCompletionLine(line)); const isCompletionLine = line => line.includes('(done in'); +export const getNormalizedLine = stderr => getNormalizedLines(stderr)[0]; export const getNormalizedLines = stderr => splitLines(normalizeStderr(stderr)); const splitLines = stderr => stderr.split('\n'); diff --git a/test/verbose/complete.js b/test/verbose/complete.js index c4d17d0104..b37e4b6eb1 100644 --- a/test/verbose/complete.js +++ b/test/verbose/complete.js @@ -91,7 +91,7 @@ test('Prints completion after errors, "reject" false, sync', testPrintCompletion const testPrintCompletionEarly = async (t, isSync) => { const stderr = await runEarlyErrorSubprocess(t, isSync); - t.is(getCompletionLine(stderr), `${testTimestamp} [0] × (done in 0ms)`); + t.is(getCompletionLine(stderr), undefined); }; test('Prints completion after early validation errors', testPrintCompletionEarly, false); diff --git a/test/verbose/custom-command.js b/test/verbose/custom-command.js new file mode 100644 index 0000000000..9c47139df0 --- /dev/null +++ b/test/verbose/custom-command.js @@ -0,0 +1,97 @@ +import test from 'ava'; +import {setFixtureDirectory} from '../helpers/fixtures-directory.js'; +import {fullStdio} from '../helpers/stdio.js'; +import { + QUOTE, + getNormalizedLine, + testTimestamp, + runVerboseSubprocess, +} from '../helpers/verbose.js'; + +setFixtureDirectory(); + +const testPrintCommandCustom = async (t, fdNumber, isSync) => { + const {stderr} = await runVerboseSubprocess({ + optionsFixture: 'custom-print.js', + isSync, + type: 'command', + fdNumber, + }); + t.is(getNormalizedLine(stderr), `${testTimestamp} [0] $ noop-verbose.js ${QUOTE}. .${QUOTE}`); +}; + +test('Prints command, verbose custom', testPrintCommandCustom, undefined, false); +test('Prints command, verbose custom, fd-specific stdout', testPrintCommandCustom, 'stdout', false); +test('Prints command, verbose custom, fd-specific stderr', testPrintCommandCustom, 'stderr', false); +test('Prints command, verbose custom, fd-specific fd3', testPrintCommandCustom, 'fd3', false); +test('Prints command, verbose custom, fd-specific ipc', testPrintCommandCustom, 'ipc', false); +test('Prints command, verbose custom, sync', testPrintCommandCustom, undefined, true); +test('Prints command, verbose custom, fd-specific stdout, sync', testPrintCommandCustom, 'stdout', true); +test('Prints command, verbose custom, fd-specific stderr, sync', testPrintCommandCustom, 'stderr', true); +test('Prints command, verbose custom, fd-specific fd3, sync', testPrintCommandCustom, 'fd3', true); +test('Prints command, verbose custom, fd-specific ipc, sync', testPrintCommandCustom, 'ipc', true); + +const testPrintCommandOrder = async (t, fdNumber, secondFdNumber, hasOutput) => { + const {stderr} = await runVerboseSubprocess({ + optionsFixture: 'custom-print-multiple.js', + type: 'command', + fdNumber, + secondFdNumber, + ...fullStdio, + }); + + if (hasOutput) { + t.is(getNormalizedLine(stderr), `${testTimestamp} [0] $ noop-verbose.js ${QUOTE}. .${QUOTE}`); + } else { + t.is(stderr, ''); + } +}; + +test('Prints command, verbose custom, fd-specific stdout+stderr', testPrintCommandOrder, 'stdout', 'stderr', true); +test('Prints command, verbose custom, fd-specific stderr+stdout', testPrintCommandOrder, 'stderr', 'stdout', false); +test('Prints command, verbose custom, fd-specific stdout+fd3', testPrintCommandOrder, 'stdout', 'fd3', true); +test('Prints command, verbose custom, fd-specific fd3+stdout', testPrintCommandOrder, 'fd3', 'stdout', false); +test('Prints command, verbose custom, fd-specific stdout+ipc', testPrintCommandOrder, 'stdout', 'ipc', true); +test('Prints command, verbose custom, fd-specific ipc+stdout', testPrintCommandOrder, 'ipc', 'stdout', false); +test('Prints command, verbose custom, fd-specific stderr+fd3', testPrintCommandOrder, 'stderr', 'fd3', true); +test('Prints command, verbose custom, fd-specific fd3+stderr', testPrintCommandOrder, 'fd3', 'stderr', false); +test('Prints command, verbose custom, fd-specific stderr+ipc', testPrintCommandOrder, 'stderr', 'ipc', true); +test('Prints command, verbose custom, fd-specific ipc+stderr', testPrintCommandOrder, 'ipc', 'stderr', false); +test('Prints command, verbose custom, fd-specific fd3+ipc', testPrintCommandOrder, 'fd3', 'ipc', true); +test('Prints command, verbose custom, fd-specific ipc+fd3', testPrintCommandOrder, 'ipc', 'fd3', false); + +const testPrintCommandFunction = async (t, fdNumber, secondFdNumber) => { + const {stderr} = await runVerboseSubprocess({ + optionsFixture: 'custom-print-function.js', + type: 'command', + fdNumber, + secondFdNumber, + ...fullStdio, + }); + t.is(getNormalizedLine(stderr), `${testTimestamp} [0] $ noop-verbose.js ${QUOTE}. .${QUOTE}`); +}; + +test('Prints command, verbose custom, fd-specific stdout+stderr, single function', testPrintCommandFunction, 'stdout', 'stderr'); +test('Prints command, verbose custom, fd-specific stderr+stdout, single function', testPrintCommandFunction, 'stderr', 'stdout'); +test('Prints command, verbose custom, fd-specific stdout+fd3, single function', testPrintCommandFunction, 'stdout', 'fd3'); +test('Prints command, verbose custom, fd-specific fd3+stdout, single function', testPrintCommandFunction, 'fd3', 'stdout'); +test('Prints command, verbose custom, fd-specific stdout+ipc, single function', testPrintCommandFunction, 'stdout', 'ipc'); +test('Prints command, verbose custom, fd-specific ipc+stdout, single function', testPrintCommandFunction, 'ipc', 'stdout'); +test('Prints command, verbose custom, fd-specific stderr+fd3, single function', testPrintCommandFunction, 'stderr', 'fd3'); +test('Prints command, verbose custom, fd-specific fd3+stderr, single function', testPrintCommandFunction, 'fd3', 'stderr'); +test('Prints command, verbose custom, fd-specific stderr+ipc, single function', testPrintCommandFunction, 'stderr', 'ipc'); +test('Prints command, verbose custom, fd-specific ipc+stderr, single function', testPrintCommandFunction, 'ipc', 'stderr'); +test('Prints command, verbose custom, fd-specific fd3+ipc, single function', testPrintCommandFunction, 'fd3', 'ipc'); +test('Prints command, verbose custom, fd-specific ipc+fd3, single function', testPrintCommandFunction, 'ipc', 'fd3'); + +const testVerboseMessage = async (t, isSync) => { + const {stderr} = await runVerboseSubprocess({ + isSync, + type: 'command', + eventProperty: 'message', + }); + t.is(getNormalizedLine(stderr), `noop-verbose.js ${QUOTE}. .${QUOTE}`); +}; + +test('"verbose" function receives verboseObject.message', testVerboseMessage, false); +test('"verbose" function receives verboseObject.message, sync', testVerboseMessage, true); diff --git a/test/verbose/custom-common.js b/test/verbose/custom-common.js new file mode 100644 index 0000000000..79ac79d3d2 --- /dev/null +++ b/test/verbose/custom-common.js @@ -0,0 +1,48 @@ +import test from 'ava'; +import {setFixtureDirectory} from '../helpers/fixtures-directory.js'; +import {foobarString} from '../helpers/input.js'; +import {nestedSubprocess} from '../helpers/nested.js'; +import {QUOTE, getCommandLine, testTimestamp} from '../helpers/verbose.js'; + +setFixtureDirectory(); + +const testCustomReturn = async (t, verboseOutput, expectedOutput) => { + const {stderr} = await nestedSubprocess( + 'empty.js', + {optionsFixture: 'custom-return.js', optionsInput: {verboseOutput}}, + {stripFinalNewline: false}, + ); + t.is(stderr, expectedOutput); +}; + +test('"verbose" returning a string prints it', testCustomReturn, `${foobarString}\n`, `${foobarString}\n`); +test('"verbose" returning a string without a newline adds it', testCustomReturn, foobarString, `${foobarString}\n`); +test('"verbose" returning a string with multiple newlines keeps them', testCustomReturn, `${foobarString}\n\n`, `${foobarString}\n\n`); +test('"verbose" returning an empty string prints an empty line', testCustomReturn, '', '\n'); +test('"verbose" returning undefined ignores it', testCustomReturn, undefined, ''); +test('"verbose" returning a number ignores it', testCustomReturn, 0, ''); +test('"verbose" returning a bigint ignores it', testCustomReturn, 0n, ''); +test('"verbose" returning a boolean ignores it', testCustomReturn, true, ''); +test('"verbose" returning an object ignores it', testCustomReturn, {}, ''); +test('"verbose" returning an array ignores it', testCustomReturn, [], ''); + +test('"verbose" receives verboseLine string as first argument', async t => { + const {stderr} = await nestedSubprocess('noop.js', [foobarString], {optionsFixture: 'custom-uppercase.js'}); + t.is(getCommandLine(stderr), `${testTimestamp} [0] $ NOOP.js ${foobarString}`); +}); + +test('"verbose" can print as JSON', async t => { + const {stderr} = await nestedSubprocess('noop.js', ['. .'], {optionsFixture: 'custom-json.js', type: 'duration', reject: false}); + const {type, message, escapedCommand, commandId, timestamp, piped, result, options} = JSON.parse(stderr); + t.is(type, 'duration'); + t.true(message.includes('done in')); + t.is(escapedCommand, `noop.js ${QUOTE}. .${QUOTE}`); + t.is(commandId, '0'); + t.true(Number.isInteger(new Date(timestamp).getTime())); + t.false(piped); + t.false(result.failed); + t.is(result.exitCode, 0); + t.is(result.stdout, '. .'); + t.is(result.stderr, ''); + t.false(options.reject); +}); diff --git a/test/verbose/custom-complete.js b/test/verbose/custom-complete.js new file mode 100644 index 0000000000..58d57d5b2a --- /dev/null +++ b/test/verbose/custom-complete.js @@ -0,0 +1,92 @@ +import test from 'ava'; +import {setFixtureDirectory} from '../helpers/fixtures-directory.js'; +import {fullStdio} from '../helpers/stdio.js'; +import {getNormalizedLine, testTimestamp, runVerboseSubprocess} from '../helpers/verbose.js'; + +setFixtureDirectory(); + +const testPrintCompletionCustom = async (t, fdNumber, isSync) => { + const {stderr} = await runVerboseSubprocess({ + optionsFixture: 'custom-print.js', + isSync, + type: 'duration', + fdNumber, + }); + t.is(getNormalizedLine(stderr), `${testTimestamp} [0] × (done in 0ms)`); +}; + +test('Prints completion, verbose custom', testPrintCompletionCustom, undefined, false); +test('Prints completion, verbose custom, fd-specific stdout', testPrintCompletionCustom, 'stdout', false); +test('Prints completion, verbose custom, fd-specific stderr', testPrintCompletionCustom, 'stderr', false); +test('Prints completion, verbose custom, fd-specific fd3', testPrintCompletionCustom, 'fd3', false); +test('Prints completion, verbose custom, fd-specific ipc', testPrintCompletionCustom, 'ipc', false); +test('Prints completion, verbose custom, sync', testPrintCompletionCustom, undefined, true); +test('Prints completion, verbose custom, fd-specific stdout, sync', testPrintCompletionCustom, 'stdout', true); +test('Prints completion, verbose custom, fd-specific stderr, sync', testPrintCompletionCustom, 'stderr', true); +test('Prints completion, verbose custom, fd-specific fd3, sync', testPrintCompletionCustom, 'fd3', true); +test('Prints completion, verbose custom, fd-specific ipc, sync', testPrintCompletionCustom, 'ipc', true); + +const testPrintCompletionOrder = async (t, fdNumber, secondFdNumber, hasOutput) => { + const {stderr} = await runVerboseSubprocess({ + optionsFixture: 'custom-print-multiple.js', + type: 'duration', + fdNumber, + secondFdNumber, + ...fullStdio, + }); + + if (hasOutput) { + t.is(getNormalizedLine(stderr), `${testTimestamp} [0] × (done in 0ms)`); + } else { + t.is(stderr, ''); + } +}; + +test('Prints completion, verbose custom, fd-specific stdout+stderr', testPrintCompletionOrder, 'stdout', 'stderr', true); +test('Prints completion, verbose custom, fd-specific stderr+stdout', testPrintCompletionOrder, 'stderr', 'stdout', false); +test('Prints completion, verbose custom, fd-specific stdout+fd3', testPrintCompletionOrder, 'stdout', 'fd3', true); +test('Prints completion, verbose custom, fd-specific fd3+stdout', testPrintCompletionOrder, 'fd3', 'stdout', false); +test('Prints completion, verbose custom, fd-specific stdout+ipc', testPrintCompletionOrder, 'stdout', 'ipc', true); +test('Prints completion, verbose custom, fd-specific ipc+stdout', testPrintCompletionOrder, 'ipc', 'stdout', false); +test('Prints completion, verbose custom, fd-specific stderr+fd3', testPrintCompletionOrder, 'stderr', 'fd3', true); +test('Prints completion, verbose custom, fd-specific fd3+stderr', testPrintCompletionOrder, 'fd3', 'stderr', false); +test('Prints completion, verbose custom, fd-specific stderr+ipc', testPrintCompletionOrder, 'stderr', 'ipc', true); +test('Prints completion, verbose custom, fd-specific ipc+stderr', testPrintCompletionOrder, 'ipc', 'stderr', false); +test('Prints completion, verbose custom, fd-specific fd3+ipc', testPrintCompletionOrder, 'fd3', 'ipc', true); +test('Prints completion, verbose custom, fd-specific ipc+fd3', testPrintCompletionOrder, 'ipc', 'fd3', false); + +const testPrintCompletionFunction = async (t, fdNumber, secondFdNumber) => { + const {stderr} = await runVerboseSubprocess({ + optionsFixture: 'custom-print-function.js', + type: 'duration', + fdNumber, + secondFdNumber, + ...fullStdio, + }); + t.is(getNormalizedLine(stderr), `${testTimestamp} [0] × (done in 0ms)`); +}; + +test('Prints completion, verbose custom, fd-specific stdout+stderr, single function', testPrintCompletionFunction, 'stdout', 'stderr'); +test('Prints completion, verbose custom, fd-specific stderr+stdout, single function', testPrintCompletionFunction, 'stderr', 'stdout'); +test('Prints completion, verbose custom, fd-specific stdout+fd3, single function', testPrintCompletionFunction, 'stdout', 'fd3'); +test('Prints completion, verbose custom, fd-specific fd3+stdout, single function', testPrintCompletionFunction, 'fd3', 'stdout'); +test('Prints completion, verbose custom, fd-specific stdout+ipc, single function', testPrintCompletionFunction, 'stdout', 'ipc'); +test('Prints completion, verbose custom, fd-specific ipc+stdout, single function', testPrintCompletionFunction, 'ipc', 'stdout'); +test('Prints completion, verbose custom, fd-specific stderr+fd3, single function', testPrintCompletionFunction, 'stderr', 'fd3'); +test('Prints completion, verbose custom, fd-specific fd3+stderr, single function', testPrintCompletionFunction, 'fd3', 'stderr'); +test('Prints completion, verbose custom, fd-specific stderr+ipc, single function', testPrintCompletionFunction, 'stderr', 'ipc'); +test('Prints completion, verbose custom, fd-specific ipc+stderr, single function', testPrintCompletionFunction, 'ipc', 'stderr'); +test('Prints completion, verbose custom, fd-specific fd3+ipc, single function', testPrintCompletionFunction, 'fd3', 'ipc'); +test('Prints completion, verbose custom, fd-specific ipc+fd3, single function', testPrintCompletionFunction, 'ipc', 'fd3'); + +const testVerboseMessage = async (t, isSync) => { + const {stderr} = await runVerboseSubprocess({ + isSync, + type: 'duration', + eventProperty: 'message', + }); + t.is(getNormalizedLine(stderr), '(done in 0ms)'); +}; + +test('"verbose" function receives verboseObject.message', testVerboseMessage, false); +test('"verbose" function receives verboseObject.message, sync', testVerboseMessage, true); diff --git a/test/verbose/custom-error.js b/test/verbose/custom-error.js new file mode 100644 index 0000000000..77854e6925 --- /dev/null +++ b/test/verbose/custom-error.js @@ -0,0 +1,97 @@ +import test from 'ava'; +import {setFixtureDirectory} from '../helpers/fixtures-directory.js'; +import {fullStdio} from '../helpers/stdio.js'; +import { + QUOTE, + getNormalizedLine, + testTimestamp, + runVerboseSubprocess, +} from '../helpers/verbose.js'; + +setFixtureDirectory(); + +const testPrintErrorCustom = async (t, fdNumber, isSync) => { + const {stderr} = await runVerboseSubprocess({ + optionsFixture: 'custom-print.js', + isSync, + type: 'error', + fdNumber, + }); + t.is(getNormalizedLine(stderr), `${testTimestamp} [0] × Command failed with exit code 2: noop-verbose.js ${QUOTE}. .${QUOTE}`); +}; + +test('Prints error, verbose custom', testPrintErrorCustom, undefined, false); +test('Prints error, verbose custom, fd-specific stdout', testPrintErrorCustom, 'stdout', false); +test('Prints error, verbose custom, fd-specific stderr', testPrintErrorCustom, 'stderr', false); +test('Prints error, verbose custom, fd-specific fd3', testPrintErrorCustom, 'fd3', false); +test('Prints error, verbose custom, fd-specific ipc', testPrintErrorCustom, 'ipc', false); +test('Prints error, verbose custom, sync', testPrintErrorCustom, undefined, true); +test('Prints error, verbose custom, fd-specific stdout, sync', testPrintErrorCustom, 'stdout', true); +test('Prints error, verbose custom, fd-specific stderr, sync', testPrintErrorCustom, 'stderr', true); +test('Prints error, verbose custom, fd-specific fd3, sync', testPrintErrorCustom, 'fd3', true); +test('Prints error, verbose custom, fd-specific ipc, sync', testPrintErrorCustom, 'ipc', true); + +const testPrintErrorOrder = async (t, fdNumber, secondFdNumber, hasOutput) => { + const {stderr} = await runVerboseSubprocess({ + optionsFixture: 'custom-print-multiple.js', + type: 'error', + fdNumber, + secondFdNumber, + ...fullStdio, + }); + + if (hasOutput) { + t.is(getNormalizedLine(stderr), `${testTimestamp} [0] × Command failed with exit code 2: noop-verbose.js ${QUOTE}. .${QUOTE}`); + } else { + t.is(stderr, ''); + } +}; + +test('Prints error, verbose custom, fd-specific stdout+stderr', testPrintErrorOrder, 'stdout', 'stderr', true); +test('Prints error, verbose custom, fd-specific stderr+stdout', testPrintErrorOrder, 'stderr', 'stdout', false); +test('Prints error, verbose custom, fd-specific stdout+fd3', testPrintErrorOrder, 'stdout', 'fd3', true); +test('Prints error, verbose custom, fd-specific fd3+stdout', testPrintErrorOrder, 'fd3', 'stdout', false); +test('Prints error, verbose custom, fd-specific stdout+ipc', testPrintErrorOrder, 'stdout', 'ipc', true); +test('Prints error, verbose custom, fd-specific ipc+stdout', testPrintErrorOrder, 'ipc', 'stdout', false); +test('Prints error, verbose custom, fd-specific stderr+fd3', testPrintErrorOrder, 'stderr', 'fd3', true); +test('Prints error, verbose custom, fd-specific fd3+stderr', testPrintErrorOrder, 'fd3', 'stderr', false); +test('Prints error, verbose custom, fd-specific stderr+ipc', testPrintErrorOrder, 'stderr', 'ipc', true); +test('Prints error, verbose custom, fd-specific ipc+stderr', testPrintErrorOrder, 'ipc', 'stderr', false); +test('Prints error, verbose custom, fd-specific fd3+ipc', testPrintErrorOrder, 'fd3', 'ipc', true); +test('Prints error, verbose custom, fd-specific ipc+fd3', testPrintErrorOrder, 'ipc', 'fd3', false); + +const testPrintErrorFunction = async (t, fdNumber, secondFdNumber) => { + const {stderr} = await runVerboseSubprocess({ + optionsFixture: 'custom-print-function.js', + type: 'error', + fdNumber, + secondFdNumber, + ...fullStdio, + }); + t.is(getNormalizedLine(stderr), `${testTimestamp} [0] × Command failed with exit code 2: noop-verbose.js ${QUOTE}. .${QUOTE}`); +}; + +test('Prints error, verbose custom, fd-specific stdout+stderr, single function', testPrintErrorFunction, 'stdout', 'stderr'); +test('Prints error, verbose custom, fd-specific stderr+stdout, single function', testPrintErrorFunction, 'stderr', 'stdout'); +test('Prints error, verbose custom, fd-specific stdout+fd3, single function', testPrintErrorFunction, 'stdout', 'fd3'); +test('Prints error, verbose custom, fd-specific fd3+stdout, single function', testPrintErrorFunction, 'fd3', 'stdout'); +test('Prints error, verbose custom, fd-specific stdout+ipc, single function', testPrintErrorFunction, 'stdout', 'ipc'); +test('Prints error, verbose custom, fd-specific ipc+stdout, single function', testPrintErrorFunction, 'ipc', 'stdout'); +test('Prints error, verbose custom, fd-specific stderr+fd3, single function', testPrintErrorFunction, 'stderr', 'fd3'); +test('Prints error, verbose custom, fd-specific fd3+stderr, single function', testPrintErrorFunction, 'fd3', 'stderr'); +test('Prints error, verbose custom, fd-specific stderr+ipc, single function', testPrintErrorFunction, 'stderr', 'ipc'); +test('Prints error, verbose custom, fd-specific ipc+stderr, single function', testPrintErrorFunction, 'ipc', 'stderr'); +test('Prints error, verbose custom, fd-specific fd3+ipc, single function', testPrintErrorFunction, 'fd3', 'ipc'); +test('Prints error, verbose custom, fd-specific ipc+fd3, single function', testPrintErrorFunction, 'ipc', 'fd3'); + +const testVerboseMessage = async (t, isSync) => { + const {stderr} = await runVerboseSubprocess({ + isSync, + type: 'error', + eventProperty: 'message', + }); + t.is(stderr, `Command failed with exit code 2: noop-verbose.js ${QUOTE}. .${QUOTE}`); +}; + +test('"verbose" function receives verboseObject.message', testVerboseMessage, false); +test('"verbose" function receives verboseObject.message, sync', testVerboseMessage, true); diff --git a/test/verbose/custom-event.js b/test/verbose/custom-event.js new file mode 100644 index 0000000000..d9b6ab9f3d --- /dev/null +++ b/test/verbose/custom-event.js @@ -0,0 +1,57 @@ +import test from 'ava'; +import {setFixtureDirectory} from '../helpers/fixtures-directory.js'; +import {runVerboseSubprocess} from '../helpers/verbose.js'; + +setFixtureDirectory(); + +const testVerboseType = async (t, type, isSync) => { + const {stderr} = await runVerboseSubprocess({isSync, type, eventProperty: 'type'}); + t.is(stderr, type); +}; + +test('"verbose" function receives verboseObject.type "command"', testVerboseType, 'command', false); +test('"verbose" function receives verboseObject.type "output"', testVerboseType, 'output', false); +test('"verbose" function receives verboseObject.type "ipc"', testVerboseType, 'ipc', false); +test('"verbose" function receives verboseObject.type "error"', testVerboseType, 'error', false); +test('"verbose" function receives verboseObject.type "duration"', testVerboseType, 'duration', false); +test('"verbose" function receives verboseObject.type "command", sync', testVerboseType, 'command', true); +test('"verbose" function receives verboseObject.type "output", sync', testVerboseType, 'output', true); +test('"verbose" function receives verboseObject.type "error", sync', testVerboseType, 'error', true); +test('"verbose" function receives verboseObject.type "duration", sync', testVerboseType, 'duration', true); + +const testVerboseTimestamp = async (t, type, isSync) => { + const {stderr} = await runVerboseSubprocess({isSync, type, eventProperty: 'timestamp'}); + t.true(Number.isInteger(new Date(stderr).getTime())); +}; + +test('"verbose" function receives verboseObject.timestamp, "command"', testVerboseTimestamp, 'command', false); +test('"verbose" function receives verboseObject.timestamp, "output"', testVerboseTimestamp, 'output', false); +test('"verbose" function receives verboseObject.timestamp, "ipc"', testVerboseTimestamp, 'ipc', false); +test('"verbose" function receives verboseObject.timestamp, "error"', testVerboseTimestamp, 'error', false); +test('"verbose" function receives verboseObject.timestamp, "duration"', testVerboseTimestamp, 'duration', false); +test('"verbose" function receives verboseObject.timestamp, "command", sync', testVerboseTimestamp, 'command', true); +test('"verbose" function receives verboseObject.timestamp, "output", sync', testVerboseTimestamp, 'output', true); +test('"verbose" function receives verboseObject.timestamp, "error", sync', testVerboseTimestamp, 'error', true); +test('"verbose" function receives verboseObject.timestamp, "duration", sync', testVerboseTimestamp, 'duration', true); + +const testVerbosePiped = async (t, type, isSync, expectedOutputs) => { + const {stderr} = await runVerboseSubprocess({ + isSync, + type, + parentFixture: 'nested-pipe-verbose.js', + destinationFile: 'noop-verbose.js', + destinationArguments: ['. . .'], + eventProperty: 'piped', + }); + t.true(expectedOutputs.map(expectedOutput => expectedOutput.join('\n')).includes(stderr)); +}; + +test('"verbose" function receives verboseObject.piped, "command"', testVerbosePiped, 'command', false, [[false, true]]); +test('"verbose" function receives verboseObject.piped, "output"', testVerbosePiped, 'output', false, [[true]]); +test('"verbose" function receives verboseObject.piped, "ipc"', testVerbosePiped, 'ipc', false, [[false, true], [true, false]]); +test('"verbose" function receives verboseObject.piped, "error"', testVerbosePiped, 'error', false, [[false, true], [true, false]]); +test('"verbose" function receives verboseObject.piped, "duration"', testVerbosePiped, 'duration', false, [[false, true], [true, false]]); +test('"verbose" function receives verboseObject.piped, "command", sync', testVerbosePiped, 'command', true, [[false, true]]); +test('"verbose" function receives verboseObject.piped, "output", sync', testVerbosePiped, 'output', true, [[true]]); +test('"verbose" function receives verboseObject.piped, "error", sync', testVerbosePiped, 'error', true, [[false, true], [true, false]]); +test('"verbose" function receives verboseObject.piped, "duration", sync', testVerbosePiped, 'duration', true, [[false, true], [true, false]]); diff --git a/test/verbose/custom-id.js b/test/verbose/custom-id.js new file mode 100644 index 0000000000..d7bbb9878c --- /dev/null +++ b/test/verbose/custom-id.js @@ -0,0 +1,35 @@ +import test from 'ava'; +import {setFixtureDirectory} from '../helpers/fixtures-directory.js'; +import {QUOTE, runVerboseSubprocess} from '../helpers/verbose.js'; + +setFixtureDirectory(); + +const testVerboseCommandId = async (t, type, isSync) => { + const {stderr} = await runVerboseSubprocess({isSync, type, eventProperty: 'commandId'}); + t.is(stderr, '0'); +}; + +test('"verbose" function receives verboseObject.commandId, "command"', testVerboseCommandId, 'command', false); +test('"verbose" function receives verboseObject.commandId, "output"', testVerboseCommandId, 'output', false); +test('"verbose" function receives verboseObject.commandId, "ipc"', testVerboseCommandId, 'ipc', false); +test('"verbose" function receives verboseObject.commandId, "error"', testVerboseCommandId, 'error', false); +test('"verbose" function receives verboseObject.commandId, "duration"', testVerboseCommandId, 'duration', false); +test('"verbose" function receives verboseObject.commandId, "command", sync', testVerboseCommandId, 'command', true); +test('"verbose" function receives verboseObject.commandId, "output", sync', testVerboseCommandId, 'output', true); +test('"verbose" function receives verboseObject.commandId, "error", sync', testVerboseCommandId, 'error', true); +test('"verbose" function receives verboseObject.commandId, "duration", sync', testVerboseCommandId, 'duration', true); + +const testVerboseEscapedCommand = async (t, type, isSync) => { + const {stderr} = await runVerboseSubprocess({isSync, type, eventProperty: 'escapedCommand'}); + t.is(stderr, `noop-verbose.js ${QUOTE}. .${QUOTE}`); +}; + +test('"verbose" function receives verboseObject.escapedCommand, "command"', testVerboseEscapedCommand, 'command', false); +test('"verbose" function receives verboseObject.escapedCommand, "output"', testVerboseEscapedCommand, 'output', false); +test('"verbose" function receives verboseObject.escapedCommand, "ipc"', testVerboseEscapedCommand, 'ipc', false); +test('"verbose" function receives verboseObject.escapedCommand, "error"', testVerboseEscapedCommand, 'error', false); +test('"verbose" function receives verboseObject.escapedCommand, "duration"', testVerboseEscapedCommand, 'duration', false); +test('"verbose" function receives verboseObject.escapedCommand, "command", sync', testVerboseEscapedCommand, 'command', true); +test('"verbose" function receives verboseObject.escapedCommand, "output", sync', testVerboseEscapedCommand, 'output', true); +test('"verbose" function receives verboseObject.escapedCommand, "error", sync', testVerboseEscapedCommand, 'error', true); +test('"verbose" function receives verboseObject.escapedCommand, "duration", sync', testVerboseEscapedCommand, 'duration', true); diff --git a/test/verbose/custom-ipc.js b/test/verbose/custom-ipc.js new file mode 100644 index 0000000000..48cbb67369 --- /dev/null +++ b/test/verbose/custom-ipc.js @@ -0,0 +1,119 @@ +import {inspect} from 'node:util'; +import test from 'ava'; +import {setFixtureDirectory} from '../helpers/fixtures-directory.js'; +import {fullStdio} from '../helpers/stdio.js'; +import { + getNormalizedLine, + getNormalizedLines, + testTimestamp, + runVerboseSubprocess, +} from '../helpers/verbose.js'; +import {nestedSubprocess} from '../helpers/nested.js'; +import {foobarObject} from '../helpers/input.js'; + +setFixtureDirectory(); + +const testPrintIpcCustom = async (t, fdNumber, hasOutput) => { + const {stderr} = await runVerboseSubprocess({ + optionsFixture: 'custom-print.js', + type: 'ipc', + fdNumber, + ...fullStdio, + }); + + if (hasOutput) { + t.is(getNormalizedLine(stderr), `${testTimestamp} [0] * . .`); + } else { + t.is(stderr, ''); + } +}; + +test('Prints IPC, verbose custom', testPrintIpcCustom, undefined, true); +test('Prints IPC, verbose custom, fd-specific stdout', testPrintIpcCustom, 'stdout', false); +test('Prints IPC, verbose custom, fd-specific stderr', testPrintIpcCustom, 'stderr', false); +test('Prints IPC, verbose custom, fd-specific fd3', testPrintIpcCustom, 'fd3', false); +test('Prints IPC, verbose custom, fd-specific ipc', testPrintIpcCustom, 'ipc', true); + +const testPrintIpcOrder = async (t, fdNumber, secondFdNumber, hasOutput) => { + const {stderr} = await runVerboseSubprocess({ + optionsFixture: 'custom-print-multiple.js', + type: 'ipc', + fdNumber, + secondFdNumber, + ...fullStdio, + }); + + if (hasOutput) { + t.is(getNormalizedLine(stderr), `${testTimestamp} [0] * . .`); + } else { + t.is(stderr, ''); + } +}; + +test('Prints IPC, verbose custom, fd-specific stdout+stderr', testPrintIpcOrder, 'stdout', 'stderr', false); +test('Prints IPC, verbose custom, fd-specific stderr+stdout', testPrintIpcOrder, 'stderr', 'stdout', false); +test('Prints IPC, verbose custom, fd-specific stdout+fd3', testPrintIpcOrder, 'stdout', 'fd3', false); +test('Prints IPC, verbose custom, fd-specific fd3+stdout', testPrintIpcOrder, 'fd3', 'stdout', false); +test('Prints IPC, verbose custom, fd-specific stdout+ipc', testPrintIpcOrder, 'stdout', 'ipc', false); +test('Prints IPC, verbose custom, fd-specific ipc+stdout', testPrintIpcOrder, 'ipc', 'stdout', true); +test('Prints IPC, verbose custom, fd-specific stderr+fd3', testPrintIpcOrder, 'stderr', 'fd3', false); +test('Prints IPC, verbose custom, fd-specific fd3+stderr', testPrintIpcOrder, 'fd3', 'stderr', false); +test('Prints IPC, verbose custom, fd-specific stderr+ipc', testPrintIpcOrder, 'stderr', 'ipc', false); +test('Prints IPC, verbose custom, fd-specific ipc+stderr', testPrintIpcOrder, 'ipc', 'stderr', true); +test('Prints IPC, verbose custom, fd-specific fd3+ipc', testPrintIpcOrder, 'fd3', 'ipc', false); +test('Prints IPC, verbose custom, fd-specific ipc+fd3', testPrintIpcOrder, 'ipc', 'fd3', true); + +const testPrintIpcFunction = async (t, fdNumber, secondFdNumber, hasOutput) => { + const {stderr} = await runVerboseSubprocess({ + optionsFixture: 'custom-print-function.js', + type: 'ipc', + fdNumber, + secondFdNumber, + ...fullStdio, + }); + + if (hasOutput) { + t.is(getNormalizedLine(stderr), `${testTimestamp} [0] * . .`); + } else { + t.is(stderr, ''); + } +}; + +test('Prints IPC, verbose custom, fd-specific stdout+stderr, single function', testPrintIpcFunction, 'stdout', 'stderr', false); +test('Prints IPC, verbose custom, fd-specific stderr+stdout, single function', testPrintIpcFunction, 'stderr', 'stdout', false); +test('Prints IPC, verbose custom, fd-specific stdout+fd3, single function', testPrintIpcFunction, 'stdout', 'fd3', false); +test('Prints IPC, verbose custom, fd-specific fd3+stdout, single function', testPrintIpcFunction, 'fd3', 'stdout', false); +test('Prints IPC, verbose custom, fd-specific stdout+ipc, single function', testPrintIpcFunction, 'stdout', 'ipc', false); +test('Prints IPC, verbose custom, fd-specific ipc+stdout, single function', testPrintIpcFunction, 'ipc', 'stdout', true); +test('Prints IPC, verbose custom, fd-specific stderr+fd3, single function', testPrintIpcFunction, 'stderr', 'fd3', false); +test('Prints IPC, verbose custom, fd-specific fd3+stderr, single function', testPrintIpcFunction, 'fd3', 'stderr', false); +test('Prints IPC, verbose custom, fd-specific stderr+ipc, single function', testPrintIpcFunction, 'stderr', 'ipc', false); +test('Prints IPC, verbose custom, fd-specific ipc+stderr, single function', testPrintIpcFunction, 'ipc', 'stderr', true); +test('Prints IPC, verbose custom, fd-specific fd3+ipc, single function', testPrintIpcFunction, 'fd3', 'ipc', false); +test('Prints IPC, verbose custom, fd-specific ipc+fd3, single function', testPrintIpcFunction, 'ipc', 'fd3', true); + +test('"verbose" function receives verboseObject.message', async t => { + const {stderr} = await runVerboseSubprocess({ + type: 'ipc', + eventProperty: 'message', + }); + t.is(stderr, '. .'); +}); + +test('"verbose" function receives verboseObject.message line-wise', async t => { + const {stderr} = await runVerboseSubprocess({ + optionsFixture: 'custom-print.js', + type: 'ipc', + output: '.\n.', + }); + t.deepEqual(getNormalizedLines(stderr), [`${testTimestamp} [0] * .`, `${testTimestamp} [0] * .`]); +}); + +test('"verbose" function receives verboseObject.message serialized', async t => { + const {stderr} = await nestedSubprocess('ipc-echo.js', { + ipcInput: foobarObject, + optionsFixture: 'custom-print.js', + optionsInput: {type: 'ipc'}, + }); + t.is(getNormalizedLine(stderr), `${testTimestamp} [0] * ${inspect(foobarObject)}`); +}); diff --git a/test/verbose/custom-options.js b/test/verbose/custom-options.js new file mode 100644 index 0000000000..a1d8f53b73 --- /dev/null +++ b/test/verbose/custom-options.js @@ -0,0 +1,47 @@ +import test from 'ava'; +import {setFixtureDirectory} from '../helpers/fixtures-directory.js'; +import {runVerboseSubprocess} from '../helpers/verbose.js'; + +setFixtureDirectory(); + +const testVerboseOptionsExplicit = async (t, type, isSync) => { + const maxBuffer = 1000; + const {stderr} = await runVerboseSubprocess({ + isSync, + type, + optionsFixture: 'custom-option.js', + optionName: 'maxBuffer', + maxBuffer, + }); + t.is(stderr, `${maxBuffer}`); +}; + +test('"verbose" function receives verboseObject.options explicitly set, "command"', testVerboseOptionsExplicit, 'command', false); +test('"verbose" function receives verboseObject.options explicitly set, "output"', testVerboseOptionsExplicit, 'output', false); +test('"verbose" function receives verboseObject.options explicitly set, "ipc"', testVerboseOptionsExplicit, 'ipc', false); +test('"verbose" function receives verboseObject.options explicitly set, "error"', testVerboseOptionsExplicit, 'error', false); +test('"verbose" function receives verboseObject.options explicitly set, "duration"', testVerboseOptionsExplicit, 'duration', false); +test('"verbose" function receives verboseObject.options explicitly set, "command", sync', testVerboseOptionsExplicit, 'command', true); +test('"verbose" function receives verboseObject.options explicitly set, "output", sync', testVerboseOptionsExplicit, 'output', true); +test('"verbose" function receives verboseObject.options explicitly set, "error", sync', testVerboseOptionsExplicit, 'error', true); +test('"verbose" function receives verboseObject.options explicitly set, "duration", sync', testVerboseOptionsExplicit, 'duration', true); + +const testVerboseOptionsDefault = async (t, type, isSync) => { + const {stderr} = await runVerboseSubprocess({ + isSync, + type, + optionsFixture: 'custom-option.js', + optionName: 'maxBuffer', + }); + t.is(stderr, 'undefined'); +}; + +test('"verbose" function receives verboseObject.options before default values and normalization, "command"', testVerboseOptionsDefault, 'command', false); +test('"verbose" function receives verboseObject.options before default values and normalization, "output"', testVerboseOptionsDefault, 'output', false); +test('"verbose" function receives verboseObject.options before default values and normalization, "ipc"', testVerboseOptionsDefault, 'ipc', false); +test('"verbose" function receives verboseObject.options before default values and normalization, "error"', testVerboseOptionsDefault, 'error', false); +test('"verbose" function receives verboseObject.options before default values and normalization, "duration"', testVerboseOptionsDefault, 'duration', false); +test('"verbose" function receives verboseObject.options before default values and normalization, "command", sync', testVerboseOptionsDefault, 'command', true); +test('"verbose" function receives verboseObject.options before default values and normalization, "output", sync', testVerboseOptionsDefault, 'output', true); +test('"verbose" function receives verboseObject.options before default values and normalization, "error", sync', testVerboseOptionsDefault, 'error', true); +test('"verbose" function receives verboseObject.options before default values and normalization, "duration", sync', testVerboseOptionsDefault, 'duration', true); diff --git a/test/verbose/custom-output.js b/test/verbose/custom-output.js new file mode 100644 index 0000000000..8f298e33ad --- /dev/null +++ b/test/verbose/custom-output.js @@ -0,0 +1,128 @@ +import {inspect} from 'node:util'; +import test from 'ava'; +import {setFixtureDirectory} from '../helpers/fixtures-directory.js'; +import {fullStdio} from '../helpers/stdio.js'; +import { + getNormalizedLine, + getNormalizedLines, + testTimestamp, + runVerboseSubprocess, +} from '../helpers/verbose.js'; +import {nestedSubprocess} from '../helpers/nested.js'; +import {foobarObject} from '../helpers/input.js'; + +setFixtureDirectory(); + +const testPrintOutputCustom = async (t, fdNumber, isSync, hasOutput) => { + const {stderr} = await runVerboseSubprocess({ + optionsFixture: 'custom-print.js', + isSync, + type: 'output', + fdNumber, + }); + + if (hasOutput) { + t.is(getNormalizedLine(stderr), `${testTimestamp} [0] . .`); + } else { + t.is(stderr, ''); + } +}; + +test('Prints stdout, verbose custom', testPrintOutputCustom, undefined, false, true); +test('Prints stdout, verbose custom, fd-specific stdout', testPrintOutputCustom, 'stdout', false, true); +test('Prints stdout, verbose custom, fd-specific stderr', testPrintOutputCustom, 'stderr', false, false); +test('Prints stdout, verbose custom, fd-specific fd3', testPrintOutputCustom, 'fd3', false, false); +test('Prints stdout, verbose custom, fd-specific ipc', testPrintOutputCustom, 'ipc', false, false); +test('Prints stdout, verbose custom, sync', testPrintOutputCustom, undefined, true, true); +test('Prints stdout, verbose custom, fd-specific stdout, sync', testPrintOutputCustom, 'stdout', true, true); +test('Prints stdout, verbose custom, fd-specific stderr, sync', testPrintOutputCustom, 'stderr', true, false); +test('Prints stdout, verbose custom, fd-specific fd3, sync', testPrintOutputCustom, 'fd3', true, false); +test('Prints stdout, verbose custom, fd-specific ipc, sync', testPrintOutputCustom, 'ipc', true, false); + +const testPrintOutputOrder = async (t, fdNumber, secondFdNumber, hasOutput) => { + const {stderr} = await runVerboseSubprocess({ + optionsFixture: 'custom-print-multiple.js', + type: 'output', + fdNumber, + secondFdNumber, + ...fullStdio, + }); + + if (hasOutput) { + t.is(getNormalizedLine(stderr), `${testTimestamp} [0] . .`); + } else { + t.is(stderr, ''); + } +}; + +test('Prints stdout, verbose custom, fd-specific stdout+stderr', testPrintOutputOrder, 'stdout', 'stderr', true); +test('Prints stdout, verbose custom, fd-specific stderr+stdout', testPrintOutputOrder, 'stderr', 'stdout', false); +test('Prints stdout, verbose custom, fd-specific stdout+fd3', testPrintOutputOrder, 'stdout', 'fd3', true); +test('Prints stdout, verbose custom, fd-specific fd3+stdout', testPrintOutputOrder, 'fd3', 'stdout', false); +test('Prints stdout, verbose custom, fd-specific stdout+ipc', testPrintOutputOrder, 'stdout', 'ipc', true); +test('Prints stdout, verbose custom, fd-specific ipc+stdout', testPrintOutputOrder, 'ipc', 'stdout', false); +test('Prints stdout, verbose custom, fd-specific stderr+fd3', testPrintOutputOrder, 'stderr', 'fd3', false); +test('Prints stdout, verbose custom, fd-specific fd3+stderr', testPrintOutputOrder, 'fd3', 'stderr', false); +test('Prints stdout, verbose custom, fd-specific stderr+ipc', testPrintOutputOrder, 'stderr', 'ipc', false); +test('Prints stdout, verbose custom, fd-specific ipc+stderr', testPrintOutputOrder, 'ipc', 'stderr', false); +test('Prints stdout, verbose custom, fd-specific fd3+ipc', testPrintOutputOrder, 'fd3', 'ipc', false); +test('Prints stdout, verbose custom, fd-specific ipc+fd3', testPrintOutputOrder, 'ipc', 'fd3', false); + +const testPrintOutputFunction = async (t, fdNumber, secondFdNumber, hasOutput) => { + const {stderr} = await runVerboseSubprocess({ + optionsFixture: 'custom-print-function.js', + type: 'output', + fdNumber, + secondFdNumber, + ...fullStdio, + }); + + if (hasOutput) { + t.is(getNormalizedLine(stderr), `${testTimestamp} [0] . .`); + } else { + t.is(stderr, ''); + } +}; + +test('Prints stdout, verbose custom, fd-specific stdout+stderr, single function', testPrintOutputFunction, 'stdout', 'stderr', true); +test('Prints stdout, verbose custom, fd-specific stderr+stdout, single function', testPrintOutputFunction, 'stderr', 'stdout', false); +test('Prints stdout, verbose custom, fd-specific stdout+fd3, single function', testPrintOutputFunction, 'stdout', 'fd3', true); +test('Prints stdout, verbose custom, fd-specific fd3+stdout, single function', testPrintOutputFunction, 'fd3', 'stdout', false); +test('Prints stdout, verbose custom, fd-specific stdout+ipc, single function', testPrintOutputFunction, 'stdout', 'ipc', true); +test('Prints stdout, verbose custom, fd-specific ipc+stdout, single function', testPrintOutputFunction, 'ipc', 'stdout', false); +test('Prints stdout, verbose custom, fd-specific stderr+fd3, single function', testPrintOutputFunction, 'stderr', 'fd3', false); +test('Prints stdout, verbose custom, fd-specific fd3+stderr, single function', testPrintOutputFunction, 'fd3', 'stderr', false); +test('Prints stdout, verbose custom, fd-specific stderr+ipc, single function', testPrintOutputFunction, 'stderr', 'ipc', false); +test('Prints stdout, verbose custom, fd-specific ipc+stderr, single function', testPrintOutputFunction, 'ipc', 'stderr', false); +test('Prints stdout, verbose custom, fd-specific fd3+ipc, single function', testPrintOutputFunction, 'fd3', 'ipc', false); +test('Prints stdout, verbose custom, fd-specific ipc+fd3, single function', testPrintOutputFunction, 'ipc', 'fd3', false); + +const testVerboseMessage = async (t, isSync) => { + const {stderr} = await runVerboseSubprocess({ + isSync, + type: 'output', + eventProperty: 'message', + }); + t.is(stderr, '. .'); +}; + +test('"verbose" function receives verboseObject.message', testVerboseMessage, false); +test('"verbose" function receives verboseObject.message, sync', testVerboseMessage, true); + +const testPrintOutputMultiline = async (t, isSync) => { + const {stderr} = await runVerboseSubprocess({ + optionsFixture: 'custom-print.js', + isSync, + type: 'output', + output: '.\n.', + }); + t.deepEqual(getNormalizedLines(stderr), [`${testTimestamp} [0] .`, `${testTimestamp} [0] .`]); +}; + +test('"verbose" function receives verboseObject.message line-wise', testPrintOutputMultiline, false); +test('"verbose" function receives verboseObject.message line-wise, sync', testPrintOutputMultiline, true); + +test('"verbose" function receives verboseObject.message serialized', async t => { + const {stderr} = await nestedSubprocess('noop.js', {optionsFixture: 'custom-object-stdout.js'}); + t.is(getNormalizedLine(stderr), `${testTimestamp} [0] ${inspect(foobarObject)}`); +}); diff --git a/test/verbose/custom-reject.js b/test/verbose/custom-reject.js new file mode 100644 index 0000000000..1fb25d66e9 --- /dev/null +++ b/test/verbose/custom-reject.js @@ -0,0 +1,37 @@ +import test from 'ava'; +import {setFixtureDirectory} from '../helpers/fixtures-directory.js'; +import {runVerboseSubprocess} from '../helpers/verbose.js'; +import {earlyErrorOptions, earlyErrorOptionsSync} from '../helpers/early-error.js'; + +setFixtureDirectory(); + +// eslint-disable-next-line max-params +const testVerboseReject = async (t, type, options, isSync, expectedOutput) => { + const {stderr} = await runVerboseSubprocess({ + isSync, + type, + optionsFixture: 'custom-option.js', + optionName: 'reject', + ...options, + }); + t.is(stderr, expectedOutput.map(String).join('\n')); +}; + +test('"verbose" function receives verboseObject.options.reject, "command"', testVerboseReject, 'command', {}, false, [undefined]); +test('"verbose" function receives verboseObject.options.reject, "output"', testVerboseReject, 'output', {}, false, [undefined]); +test('"verbose" function receives verboseObject.options.reject, "ipc"', testVerboseReject, 'ipc', {}, false, [undefined]); +test('"verbose" function receives verboseObject.options.reject, "error"', testVerboseReject, 'error', {}, false, [undefined]); +test('"verbose" function receives verboseObject.options.reject, "duration"', testVerboseReject, 'duration', {}, false, [undefined]); +test('"verbose" function receives verboseObject.options.reject, "command", spawn error', testVerboseReject, 'command', earlyErrorOptions, false, [undefined]); +test('"verbose" function receives verboseObject.options.reject, "output", spawn error', testVerboseReject, 'output', earlyErrorOptions, false, []); +test('"verbose" function receives verboseObject.options.reject, "ipc", spawn error', testVerboseReject, 'ipc', earlyErrorOptions, false, []); +test('"verbose" function receives verboseObject.options.reject, "error", spawn error', testVerboseReject, 'error', earlyErrorOptions, false, [undefined, undefined]); +test('"verbose" function receives verboseObject.options.reject, "duration", spawn error', testVerboseReject, 'duration', earlyErrorOptions, false, [undefined]); +test('"verbose" function receives verboseObject.options.reject, "command", sync', testVerboseReject, 'command', {}, true, [undefined]); +test('"verbose" function receives verboseObject.options.reject, "output", sync', testVerboseReject, 'output', {}, true, [undefined]); +test('"verbose" function receives verboseObject.options.reject, "error", sync', testVerboseReject, 'error', {}, true, [undefined]); +test('"verbose" function receives verboseObject.options.reject, "duration", sync', testVerboseReject, 'duration', {}, true, [undefined]); +test('"verbose" function receives verboseObject.options.reject, "command", spawn error, sync', testVerboseReject, 'command', earlyErrorOptionsSync, true, [undefined]); +test('"verbose" function receives verboseObject.options.reject, "output", spawn error, sync', testVerboseReject, 'output', earlyErrorOptionsSync, true, []); +test('"verbose" function receives verboseObject.options.reject, "error", spawn error, sync', testVerboseReject, 'error', earlyErrorOptionsSync, true, [undefined, undefined]); +test('"verbose" function receives verboseObject.options.reject, "duration", spawn error, sync', testVerboseReject, 'duration', earlyErrorOptionsSync, true, [undefined]); diff --git a/test/verbose/custom-result.js b/test/verbose/custom-result.js new file mode 100644 index 0000000000..0945ddb58d --- /dev/null +++ b/test/verbose/custom-result.js @@ -0,0 +1,76 @@ +import test from 'ava'; +import {setFixtureDirectory} from '../helpers/fixtures-directory.js'; +import {runVerboseSubprocess} from '../helpers/verbose.js'; +import { + earlyErrorOptions, + earlyErrorOptionsSync, + expectedEarlyError, + expectedEarlyErrorSync, +} from '../helpers/early-error.js'; + +setFixtureDirectory(); + +const testVerboseResultEnd = async (t, type, isSync) => { + const {stderr: parentStderr} = await runVerboseSubprocess({ + isSync, + type, + optionsFixture: 'custom-result.js', + }); + const {failed, exitCode, stdout, stderr, ipcOutput, durationMs} = JSON.parse(parentStderr); + t.true(failed); + t.is(exitCode, 2); + t.is(stdout, '. .'); + t.is(stderr, ''); + t.is(typeof durationMs, 'number'); + t.deepEqual(ipcOutput, isSync ? [] : ['. .']); +}; + +test('"verbose" function receives verboseObject.result, "error"', testVerboseResultEnd, 'error', false); +test('"verbose" function receives verboseObject.result, "duration"', testVerboseResultEnd, 'duration', false); +test('"verbose" function receives verboseObject.result, "error", sync', testVerboseResultEnd, 'error', true); +test('"verbose" function receives verboseObject.result, "duration", sync', testVerboseResultEnd, 'duration', true); + +// eslint-disable-next-line max-params +const testVerboseResultEndSpawn = async (t, type, options, expectedOutput, isSync) => { + const {stderr: parentStderr} = await runVerboseSubprocess({ + isSync, + type, + optionsFixture: 'custom-result.js', + ...options, + }); + const lastLine = parentStderr.split('\n').at(-1); + const result = JSON.parse(lastLine); + t.like(result, expectedOutput); + t.true(result.failed); + t.is(result.exitCode, undefined); + t.is(result.stdout, undefined); + t.is(result.stderr, undefined); + t.is(typeof result.durationMs, 'number'); + t.deepEqual(result.ipcOutput, []); +}; + +test('"verbose" function receives verboseObject.result, "error", spawn error', testVerboseResultEndSpawn, 'error', earlyErrorOptions, expectedEarlyError, false); +test('"verbose" function receives verboseObject.result, "duration", spawn error', testVerboseResultEndSpawn, 'duration', earlyErrorOptions, expectedEarlyError, false); +test('"verbose" function receives verboseObject.result, "error", spawn error, sync', testVerboseResultEndSpawn, 'error', earlyErrorOptionsSync, expectedEarlyErrorSync, true); +test('"verbose" function receives verboseObject.result, "duration", spawn error, sync', testVerboseResultEndSpawn, 'duration', earlyErrorOptionsSync, expectedEarlyErrorSync, true); + +const testVerboseResultStart = async (t, type, options, isSync) => { + const {stderr: parentStderr} = await runVerboseSubprocess({ + isSync, + type, + optionsFixture: 'custom-result.js', + ...options, + }); + t.is(parentStderr, ''); +}; + +test('"verbose" function does not receive verboseObject.result, "command"', testVerboseResultStart, 'command', {}, false); +test('"verbose" function does not receive verboseObject.result, "output"', testVerboseResultStart, 'output', {}, false); +test('"verbose" function does not receive verboseObject.result, "ipc"', testVerboseResultStart, 'ipc', {}, false); +test('"verbose" function does not receive verboseObject.result, "command", spawn error', testVerboseResultStart, 'command', earlyErrorOptions, false); +test('"verbose" function does not receive verboseObject.result, "output", spawn error', testVerboseResultStart, 'output', earlyErrorOptions, false); +test('"verbose" function does not receive verboseObject.result, "ipc", spawn error', testVerboseResultStart, 'ipc', earlyErrorOptions, false); +test('"verbose" function does not receive verboseObject.result, "command", sync', testVerboseResultStart, 'command', {}, true); +test('"verbose" function does not receive verboseObject.result, "output", sync', testVerboseResultStart, 'output', {}, true); +test('"verbose" function does not receive verboseObject.result, "command", spawn error, sync', testVerboseResultStart, 'command', earlyErrorOptionsSync, true); +test('"verbose" function does not receive verboseObject.result, "output", spawn error, sync', testVerboseResultStart, 'output', earlyErrorOptionsSync, true); diff --git a/test/verbose/custom-start.js b/test/verbose/custom-start.js new file mode 100644 index 0000000000..cb7904e043 --- /dev/null +++ b/test/verbose/custom-start.js @@ -0,0 +1,84 @@ +import test from 'ava'; +import {setFixtureDirectory} from '../helpers/fixtures-directory.js'; +import {fullStdio} from '../helpers/stdio.js'; +import { + QUOTE, + getNormalizedLine, + testTimestamp, + runVerboseSubprocess, +} from '../helpers/verbose.js'; + +setFixtureDirectory(); + +const testPrintCommandCustom = async (t, fdNumber, worker, isSync) => { + const {stderr} = await runVerboseSubprocess({ + optionsFixture: 'custom-print.js', + worker, + isSync, + type: 'command', + fdNumber, + }); + t.is(getNormalizedLine(stderr), `${testTimestamp} [0] $ noop-verbose.js ${QUOTE}. .${QUOTE}`); +}; + +test('Prints command, verbose custom', testPrintCommandCustom, undefined, false, false); +test('Prints command, verbose custom, fd-specific stdout', testPrintCommandCustom, 'stdout', false, false); +test('Prints command, verbose custom, fd-specific stderr', testPrintCommandCustom, 'stderr', false, false); +test('Prints command, verbose custom, fd-specific fd3', testPrintCommandCustom, 'fd3', false, false); +test('Prints command, verbose custom, fd-specific ipc', testPrintCommandCustom, 'ipc', false, false); +test('Prints command, verbose custom, sync', testPrintCommandCustom, undefined, false, true); +test('Prints command, verbose custom, fd-specific stdout, sync', testPrintCommandCustom, 'stdout', false, true); +test('Prints command, verbose custom, fd-specific stderr, sync', testPrintCommandCustom, 'stderr', false, true); +test('Prints command, verbose custom, fd-specific fd3, sync', testPrintCommandCustom, 'fd3', false, true); +test('Prints command, verbose custom, fd-specific ipc, sync', testPrintCommandCustom, 'ipc', false, true); +test('Prints command, verbose custom, worker', testPrintCommandCustom, undefined, true, false); +test('Prints command, verbose custom, fd-specific stdout, worker', testPrintCommandCustom, 'stdout', true, false); +test('Prints command, verbose custom, fd-specific stderr, worker', testPrintCommandCustom, 'stderr', true, false); +test('Prints command, verbose custom, fd-specific fd3, worker', testPrintCommandCustom, 'fd3', true, false); +test('Prints command, verbose custom, fd-specific ipc, worker', testPrintCommandCustom, 'ipc', true, false); +test('Prints command, verbose custom, worker, sync', testPrintCommandCustom, undefined, true, true); +test('Prints command, verbose custom, fd-specific stdout, worker, sync', testPrintCommandCustom, 'stdout', true, true); +test('Prints command, verbose custom, fd-specific stderr, worker, sync', testPrintCommandCustom, 'stderr', true, true); +test('Prints command, verbose custom, fd-specific fd3, worker, sync', testPrintCommandCustom, 'fd3', true, true); +test('Prints command, verbose custom, fd-specific ipc, worker, sync', testPrintCommandCustom, 'ipc', true, true); + +const testPrintCommandOrder = async (t, fdNumber, secondFdNumber, hasOutput) => { + const {stderr} = await runVerboseSubprocess({ + optionsFixture: 'custom-print-multiple.js', + type: 'command', + fdNumber, + secondFdNumber, + ...fullStdio, + }); + + if (hasOutput) { + t.is(getNormalizedLine(stderr), `${testTimestamp} [0] $ noop-verbose.js ${QUOTE}. .${QUOTE}`); + } else { + t.is(stderr, ''); + } +}; + +test('Prints command, verbose custom, fd-specific stdout+stderr', testPrintCommandOrder, 'stdout', 'stderr', true); +test('Prints command, verbose custom, fd-specific stderr+stdout', testPrintCommandOrder, 'stderr', 'stdout', false); +test('Prints command, verbose custom, fd-specific stdout+fd3', testPrintCommandOrder, 'stdout', 'fd3', true); +test('Prints command, verbose custom, fd-specific fd3+stdout', testPrintCommandOrder, 'fd3', 'stdout', false); +test('Prints command, verbose custom, fd-specific stdout+ipc', testPrintCommandOrder, 'stdout', 'ipc', true); +test('Prints command, verbose custom, fd-specific ipc+stdout', testPrintCommandOrder, 'ipc', 'stdout', false); +test('Prints command, verbose custom, fd-specific stderr+fd3', testPrintCommandOrder, 'stderr', 'fd3', true); +test('Prints command, verbose custom, fd-specific fd3+stderr', testPrintCommandOrder, 'fd3', 'stderr', false); +test('Prints command, verbose custom, fd-specific stderr+ipc', testPrintCommandOrder, 'stderr', 'ipc', true); +test('Prints command, verbose custom, fd-specific ipc+stderr', testPrintCommandOrder, 'ipc', 'stderr', false); +test('Prints command, verbose custom, fd-specific fd3+ipc', testPrintCommandOrder, 'fd3', 'ipc', true); +test('Prints command, verbose custom, fd-specific ipc+fd3', testPrintCommandOrder, 'ipc', 'fd3', false); + +const testVerboseMessage = async (t, isSync) => { + const {stderr} = await runVerboseSubprocess({ + isSync, + type: 'command', + eventProperty: 'message', + }); + t.is(stderr, `noop-verbose.js ${QUOTE}. .${QUOTE}`); +}; + +test('"verbose" function receives verboseObject.message', testVerboseMessage, false); +test('"verbose" function receives verboseObject.message, sync', testVerboseMessage, true); diff --git a/test/verbose/custom-throw.js b/test/verbose/custom-throw.js new file mode 100644 index 0000000000..afeab7ba03 --- /dev/null +++ b/test/verbose/custom-throw.js @@ -0,0 +1,67 @@ +import test from 'ava'; +import {setFixtureDirectory} from '../helpers/fixtures-directory.js'; +import {foobarString} from '../helpers/input.js'; +import {runVerboseSubprocess} from '../helpers/verbose.js'; +import {earlyErrorOptions, earlyErrorOptionsSync} from '../helpers/early-error.js'; + +setFixtureDirectory(); + +const testCommandThrowPropagate = async (t, type, options, isSync) => { + const {nestedResult} = await runVerboseSubprocess({ + isSync, + type, + optionsFixture: 'custom-throw.js', + errorMessage: foobarString, + ...options, + }); + t.true(nestedResult instanceof Error); + t.is(nestedResult.message, foobarString); +}; + +test('Propagate verbose exception in "verbose" function, "command"', testCommandThrowPropagate, 'command', {}, false); +test('Propagate verbose exception in "verbose" function, "error"', testCommandThrowPropagate, 'error', {}, false); +test('Propagate verbose exception in "verbose" function, "duration"', testCommandThrowPropagate, 'duration', {}, false); +test('Propagate verbose exception in "verbose" function, "command", spawn error', testCommandThrowPropagate, 'command', earlyErrorOptions, false); +test('Propagate verbose exception in "verbose" function, "error", spawn error', testCommandThrowPropagate, 'error', earlyErrorOptions, false); +test('Propagate verbose exception in "verbose" function, "duration", spawn error', testCommandThrowPropagate, 'duration', earlyErrorOptions, false); +test('Propagate verbose exception in "verbose" function, "command", sync', testCommandThrowPropagate, 'command', {}, true); +test('Propagate verbose exception in "verbose" function, "error", sync', testCommandThrowPropagate, 'error', {}, true); +test('Propagate verbose exception in "verbose" function, "duration", sync', testCommandThrowPropagate, 'duration', {}, true); +test('Propagate verbose exception in "verbose" function, "command", spawn error, sync', testCommandThrowPropagate, 'command', earlyErrorOptionsSync, true); +test('Propagate verbose exception in "verbose" function, "error", spawn error, sync', testCommandThrowPropagate, 'error', earlyErrorOptionsSync, true); +test('Propagate verbose exception in "verbose" function, "duration", spawn error, sync', testCommandThrowPropagate, 'duration', earlyErrorOptionsSync, true); + +const testCommandThrowHandle = async (t, type, isSync) => { + const {nestedResult} = await runVerboseSubprocess({ + isSync, + type, + optionsFixture: 'custom-throw.js', + errorMessage: foobarString, + }); + t.true(nestedResult instanceof Error); + t.true(nestedResult.stack.startsWith(isSync ? 'ExecaSyncError' : 'ExecaError')); + t.true(nestedResult.cause instanceof Error); + t.is(nestedResult.cause.message, foobarString); +}; + +test('Handle exceptions in "verbose" function, "output"', testCommandThrowHandle, 'output', false); +test('Handle exceptions in "verbose" function, "ipc"', testCommandThrowHandle, 'ipc', false); +test('Handle exceptions in "verbose" function, "output", sync', testCommandThrowHandle, 'output', true); + +const testCommandThrowWrap = async (t, type, options, isSync) => { + const {nestedResult} = await runVerboseSubprocess({ + isSync, + type, + optionsFixture: 'custom-throw.js', + errorMessage: foobarString, + ...options, + }); + t.true(nestedResult instanceof Error); + t.true(nestedResult.stack.startsWith(isSync ? 'ExecaSyncError' : 'ExecaError')); + t.true(nestedResult.cause instanceof Error); + t.not(nestedResult.cause.message, foobarString); +}; + +test('Propagate wrapped exception in "verbose" function, "output", spawn error', testCommandThrowWrap, 'output', earlyErrorOptions, false); +test('Propagate wrapped exception in "verbose" function, "ipc", spawn error', testCommandThrowWrap, 'ipc', earlyErrorOptions, false); +test('Propagate wrapped exception in "verbose" function, "output", spawn error, sync', testCommandThrowWrap, 'output', earlyErrorOptionsSync, true); diff --git a/test/verbose/error.js b/test/verbose/error.js index 9d2d2f97c7..4b018d984d 100644 --- a/test/verbose/error.js +++ b/test/verbose/error.js @@ -83,7 +83,7 @@ test('Does not print error if none, sync', testPrintNoError, true); const testPrintErrorEarly = async (t, isSync) => { const stderr = await runEarlyErrorSubprocess(t, isSync); - t.is(getErrorLine(stderr), `${testTimestamp} [0] × TypeError: The "cwd" option must be a string or a file URL: true.`); + t.is(getErrorLine(stderr), undefined); }; test('Prints early validation error', testPrintErrorEarly, false); diff --git a/test/verbose/info.js b/test/verbose/info.js index a3a0d2793c..5c9188f28c 100644 --- a/test/verbose/info.js +++ b/test/verbose/info.js @@ -10,6 +10,7 @@ import { getNormalizedLines, testTimestamp, } from '../helpers/verbose.js'; +import {earlyErrorOptions, earlyErrorOptionsSync} from '../helpers/early-error.js'; setFixtureDirectory(); @@ -60,3 +61,32 @@ test('Does not allow "verbose: true"', testInvalidVerbose, true, invalidTrueMess test('Does not allow "verbose: true", sync', testInvalidVerbose, true, invalidTrueMessage, execaSync); test('Does not allow "verbose: \'unknown\'"', testInvalidVerbose, 'unknown', invalidUnknownMessage, execa); test('Does not allow "verbose: \'unknown\'", sync', testInvalidVerbose, 'unknown', invalidUnknownMessage, execaSync); + +const testValidationError = async (t, isSync) => { + const {stderr, nestedResult} = await nestedSubprocess('empty.js', {verbose: 'full', isSync, timeout: []}); + t.deepEqual(getNormalizedLines(stderr), [`${testTimestamp} [0] $ empty.js`]); + t.true(nestedResult instanceof Error); +}; + +test('Prints validation errors', testValidationError, false); +test('Prints validation errors, sync', testValidationError, true); + +test('Prints early spawn errors', async t => { + const {stderr} = await nestedSubprocess('empty.js', {...earlyErrorOptions, verbose: 'full'}); + t.deepEqual(getNormalizedLines(stderr), [ + `${testTimestamp} [0] $ empty.js`, + `${testTimestamp} [0] × Command failed with ERR_INVALID_ARG_TYPE: empty.js`, + `${testTimestamp} [0] × The "options.detached" property must be of type boolean. Received type string ('true')`, + `${testTimestamp} [0] × (done in 0ms)`, + ]); +}); + +test('Prints early spawn errors, sync', async t => { + const {stderr} = await nestedSubprocess('empty.js', {...earlyErrorOptionsSync, verbose: 'full', isSync: true}); + t.deepEqual(getNormalizedLines(stderr), [ + `${testTimestamp} [0] $ empty.js`, + `${testTimestamp} [0] × Command failed with ERR_OUT_OF_RANGE: empty.js`, + `${testTimestamp} [0] × The value of "options.maxBuffer" is out of range. It must be a positive number. Received false`, + `${testTimestamp} [0] × (done in 0ms)`, + ]); +}); diff --git a/types/arguments/options.d.ts b/types/arguments/options.d.ts index d11cd1d30c..4a3e78d3b6 100644 --- a/types/arguments/options.d.ts +++ b/types/arguments/options.d.ts @@ -4,6 +4,7 @@ import type {Readable} from 'node:stream'; import type {Unless} from '../utils.js'; import type {Message} from '../ipc.js'; import type {StdinOptionCommon, StdoutStderrOptionCommon, StdioOptionsProperty} from '../stdio/type.js'; +import type {VerboseOption} from '../verbose.js'; import type {FdGenericOption} from './specific.js'; import type {EncodingOption} from './encoding-option.js'; @@ -226,13 +227,15 @@ export type CommonOptions = { /** If `verbose` is `'short'`, prints the command on [`stderr`](https://en.wikipedia.org/wiki/Standard_streams#Standard_error_(stderr)): its file, arguments, duration and (if it failed) error message. - If `verbose` is `'full'`, the command's [`stdout`](https://en.wikipedia.org/wiki/Standard_streams#Standard_output_(stdout)), `stderr` and IPC messages are also printed. + If `verbose` is `'full'` or a function, the command's [`stdout`](https://en.wikipedia.org/wiki/Standard_streams#Standard_output_(stdout)), `stderr` and IPC messages are also printed. + + A function can be passed to customize logging. By default, this applies to both `stdout` and `stderr`, but different values can also be passed. @default 'none' */ - readonly verbose?: FdGenericOption<'none' | 'short' | 'full'>; + readonly verbose?: VerboseOption; /** Setting this to `false` resolves the result's promise with the error instead of rejecting it. diff --git a/types/return/result.d.ts b/types/return/result.d.ts index 2121f354be..4164f0915f 100644 --- a/types/return/result.d.ts +++ b/types/return/result.d.ts @@ -179,9 +179,9 @@ export declare abstract class CommonResult< stack?: Error['stack']; } -type SuccessResult< - IsSync extends boolean, - OptionsType extends CommonOptions, +export type SuccessResult< + IsSync extends boolean = boolean, + OptionsType extends CommonOptions = CommonOptions, > = InstanceType> & OmitErrorIfReject; type OmitErrorIfReject = { diff --git a/types/verbose.d.ts b/types/verbose.d.ts new file mode 100644 index 0000000000..28ad4bdf66 --- /dev/null +++ b/types/verbose.d.ts @@ -0,0 +1,98 @@ +import type {FdGenericOption} from './arguments/specific.js'; +import type {Options, SyncOptions} from './arguments/options.js'; +import type {Result, SyncResult} from './return/result.js'; + +type VerboseOption = FdGenericOption< +| 'none' +| 'short' +| 'full' +| VerboseFunction +>; + +type VerboseFunction = (verboseLine: string, verboseObject: MinimalVerboseObject) => string | void; + +type GenericVerboseObject = { + /** + Event type. This can be: + - `'command'`: subprocess start + - `'output'`: `stdout`/`stderr` output + - `'ipc'`: IPC output + - `'error'`: subprocess failure + - `'duration'`: subprocess success or failure + */ + type: 'command' | 'output' | 'ipc' | 'error' | 'duration'; + + /** + Depending on `verboseObject.type`, this is: + - `'command'`: the `result.escapedCommand` + - `'output'`: one line from `result.stdout` or `result.stderr` + - `'ipc'`: one IPC message from `result.ipcOutput` + - `'error'`: the `error.shortMessage` + - `'duration'`: the `result.durationMs` + */ + message: string; + + /** + The file and arguments that were run. This is the same as `result.escapedCommand`. + */ + escapedCommand: string; + + /** + Serial number identifying the subprocess within the current process. It is incremented from `'0'`. + + This is helpful when multiple subprocesses are running at the same time. + + This is similar to a [PID](https://en.wikipedia.org/wiki/Process_identifier) except it has no maximum limit, which means it never repeats. Also, it is usually shorter. + */ + commandId: string; + + /** + Event date/time. + */ + timestamp: Date; + + /** + Whether another subprocess is piped into this subprocess. This is `false` when `result.pipedFrom` is empty. + */ + piped: boolean; +}; + +type MinimalVerboseObject = GenericVerboseObject & { + // We cannot use the `CommonOptions` type because it would make this type recursive + options: object; + result?: never; +}; + +/** +Subprocess event object, for logging purpose, using the `verbose` option and `execa()`. +*/ +export type VerboseObject = GenericVerboseObject & { + /** + The options passed to the subprocess. + */ + options: Options; + + /** + Subprocess result. + + This is `undefined` if `verboseObject.type` is `'command'`, `'output'` or `'ipc'`. + */ + result?: Result; +}; + +/** +Subprocess event object, for logging purpose, using the `verbose` option and `execaSync()`. +*/ +export type SyncVerboseObject = GenericVerboseObject & { + /** + The options passed to the subprocess. + */ + options: SyncOptions; + + /** + Subprocess result. + + This is `undefined` if `verboseObject.type` is `'command'`, `'output'` or `'ipc'`. + */ + result?: SyncResult; +};