Skip to content

Commit e0b5dbc

Browse files
committed
Fix execaSync() output transforms
With a binary encoding, no line is split, so transforms wrongly ran on the empty output when the subprocess produced none. That empty chunk is now not passed to them. A transform failing on one file descriptor also prevented the other ones from being redirected to their target. Those errors are now collected per file descriptor, so each one keeps writing its own files.
1 parent 004f3b3 commit e0b5dbc

3 files changed

Lines changed: 58 additions & 17 deletions

File tree

‎lib/io/output-sync.js‎

Lines changed: 19 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -13,22 +13,23 @@ export const transformOutputSync = ({fileDescriptors, syncResult: {output}, opti
1313
return {output: Array.from({length: 3})};
1414
}
1515

16-
const state = {};
1716
const outputFiles = new Set();
17+
// Transforms, `verbose` and file writes apply to a single file descriptor each, so their errors must not affect the other ones
18+
const errors = [];
1819
const transformedOutput = output.map((result, fdNumber) =>
1920
transformOutputResultSync({
2021
result,
2122
fileDescriptors,
2223
fdNumber,
23-
state,
24+
errors,
2425
outputFiles,
2526
verboseInfo,
2627
}, options));
27-
return {output: transformedOutput, ...state};
28+
return {output: transformedOutput, error: errors.find(error => error !== undefined)};
2829
};
2930

3031
const transformOutputResultSync = (
31-
{result, fileDescriptors, fdNumber, state, outputFiles, verboseInfo},
32+
{result, fileDescriptors, fdNumber, errors, outputFiles, verboseInfo},
3233
{buffer, encoding, lines, stripFinalNewline, maxBuffer},
3334
) => {
3435
if (result === null) {
@@ -38,7 +39,8 @@ const transformOutputResultSync = (
3839
const truncatedResult = truncateMaxBufferSync(result, fdNumber, {maxBuffer, buffer});
3940
const uint8ArrayResult = bufferToUint8Array(truncatedResult);
4041
const {stdioItems, objectMode} = fileDescriptors[fdNumber];
41-
const chunks = runOutputGeneratorsSync([uint8ArrayResult], stdioItems, encoding, state);
42+
// Empty output must not be passed as a chunk: with a binary encoding, no line is split, so transforms would wrongly run on it
43+
const {chunks, error: transformError} = runOutputGeneratorsSync(uint8ArrayResult.length === 0 ? [] : [uint8ArrayResult], stdioItems, encoding);
4244
const {serializedResult, finalResult = serializedResult} = serializeChunks({
4345
chunks,
4446
objectMode,
@@ -48,37 +50,38 @@ const transformOutputResultSync = (
4850
fdNumber,
4951
});
5052

51-
logOutputSync({
53+
const logError = logOutputSync({
5254
serializedResult,
5355
fdNumber,
54-
state,
5556
verboseInfo,
5657
encoding,
5758
stdioItems,
5859
objectMode,
5960
});
6061

62+
const error = transformError ?? logError;
63+
errors.push(error);
64+
6165
const returnedResult = buffer[fdNumber] ? finalResult : undefined;
6266

6367
try {
64-
if (state.error === undefined) {
68+
if (error === undefined) {
6569
writeToFiles(serializedResult, stdioItems, outputFiles);
6670
}
6771

6872
return returnedResult;
69-
} catch (error) {
70-
state.error = error;
73+
} catch (writeError) {
74+
errors.push(writeError);
7175
return returnedResult;
7276
}
7377
};
7478

7579
// Applies transform generators to `stdout`/`stderr`
76-
const runOutputGeneratorsSync = (chunks, stdioItems, encoding, state) => {
80+
const runOutputGeneratorsSync = (chunks, stdioItems, encoding) => {
7781
try {
78-
return runGeneratorsSync(chunks, stdioItems, encoding, false);
82+
return {chunks: runGeneratorsSync(chunks, stdioItems, encoding, false)};
7983
} catch (error) {
80-
state.error = error;
81-
return chunks;
84+
return {chunks, error};
8285
}
8386
};
8487

@@ -102,7 +105,7 @@ const serializeChunks = ({chunks, objectMode, encoding, lines, stripFinalNewline
102105
return {serializedResult};
103106
};
104107

105-
const logOutputSync = ({serializedResult, fdNumber, state, verboseInfo, encoding, stdioItems, objectMode}) => {
108+
const logOutputSync = ({serializedResult, fdNumber, verboseInfo, encoding, stdioItems, objectMode}) => {
106109
if (!shouldLogOutput({
107110
stdioItems,
108111
encoding,
@@ -117,7 +120,7 @@ const logOutputSync = ({serializedResult, fdNumber, state, verboseInfo, encoding
117120
try {
118121
logLinesSync(linesArray, fdNumber, verboseInfo);
119122
} catch (error) {
120-
state.error ??= error;
123+
return error;
121124
}
122125
};
123126

‎test/transform/generator-error.js‎

Lines changed: 22 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,10 @@
11
import {once} from 'node:events';
2+
import {readFile, rm} from 'node:fs/promises';
23
import test from 'ava';
4+
import tempfile from 'tempfile';
35
import {execa, execaSync} from '../../index.js';
46
import {foobarString} from '../helpers/input.js';
5-
import {noopGenerator, infiniteGenerator, convertTransformToFinal} from '../helpers/generator.js';
7+
import {noopGenerator, infiniteGenerator, convertTransformToFinal, throwingGenerator} from '../helpers/generator.js';
68
import {generatorsMap} from '../helpers/map.js';
79
import {setFixtureDirectory} from '../helpers/fixtures-directory.js';
810
import {getEarlyErrorSubprocess, expectedEarlyError} from '../helpers/early-error.js';
@@ -89,3 +91,22 @@ test('Generators are destroyed on early subprocess exit', async t => {
8991
const error = await t.throwsAsync(getEarlyErrorSubprocess({stdout: infiniteGenerator()}));
9092
t.like(error, expectedEarlyError);
9193
});
94+
95+
// A transform failing on one file descriptor must not prevent the other ones from being redirected to their target
96+
const testTransformErrorOtherFd = async (t, execaMethod) => {
97+
const filePath = tempfile();
98+
const cause = new Error(foobarString);
99+
const runSubprocess = () => execaMethod('noop-both.js', [foobarString], {
100+
stdout: throwingGenerator(cause)(),
101+
stderr: {file: filePath},
102+
});
103+
104+
await (execaMethod === execa
105+
? t.throwsAsync(runSubprocess())
106+
: t.throws(runSubprocess));
107+
t.is(await readFile(filePath, 'utf8'), `${foobarString}\n`);
108+
await rm(filePath);
109+
};
110+
111+
test('Transform errors on stdout do not skip the stderr file', testTransformErrorOtherFd, execa);
112+
test('Transform errors on stdout do not skip the stderr file, sync', testTransformErrorOtherFd, execaSync);

‎test/transform/split-binary.js‎

Lines changed: 17 additions & 0 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

0 commit comments

Comments
 (0)