Skip to content

Commit 8629132

Browse files
committed
Fix result.all corrupting objects when only one file descriptor is buffered
When only `stdout` or `stderr` is buffered, `result.all` is read from that descriptor's own stream rather than the merged one. It was still converted as if it were mixed, which changed its object-mode chunks.
1 parent f6de9be commit 8629132

2 files changed

Lines changed: 81 additions & 2 deletions

File tree

‎lib/resolve/all-async.js‎

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -32,7 +32,8 @@ export const waitForAllStream = ({subprocess, all, encoding, buffer, maxBuffer,
3232
maxBuffer: maxBuffer[1] + maxBuffer[2],
3333
lines: lines[1] || lines[2],
3434
allFds: getAllFds(subprocess, buffer),
35-
allMixed: getAllMixed(subprocess, all),
35+
// A single buffered descriptor keeps its own object mode, even when the merged stream has mixed modes.
36+
allMixed: stream === all && getAllMixed(subprocess, all),
3637
stripFinalNewline,
3738
verboseInfo,
3839
streamInfo,

‎test/transform/generator-all.js‎

Lines changed: 79 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2,8 +2,9 @@ import assert from 'node:assert/strict';
22
import {Buffer} from 'node:buffer';
33
import test from 'node:test';
44
import {execa, execaSync} from '../../index.js';
5+
import {assertRejects} from '../helpers/assert.js';
56
import {setFixtureDirectory} from '../helpers/fixtures-directory.js';
6-
import {foobarObject} from '../helpers/input.js';
7+
import {foobarObject, foobarString} from '../helpers/input.js';
78
import {
89
outputObjectGenerator,
910
uppercaseGenerator,
@@ -14,6 +15,83 @@ setFixtureDirectory();
1415

1516
const textEncoder = new TextEncoder();
1617

18+
test('result.all preserves object-mode bytes when stderr is not buffered', async () => {
19+
const chunk = new Uint8Array([97, 98]);
20+
const {stdout, all} = await execa('noop-both.js', ['x'], {
21+
all: true,
22+
buffer: {stderr: false},
23+
stdout: {
24+
objectMode: true,
25+
* transform() {
26+
yield chunk;
27+
},
28+
},
29+
});
30+
assert.deepEqual(stdout, [chunk]);
31+
assert.deepEqual(all, [chunk]);
32+
assert.equal(all[0], chunk);
33+
});
34+
35+
const testSingleBufferedObjectMode = async (bufferedDescriptor, failure) => {
36+
const chunks = [new Uint8Array([97, 98]), Buffer.from('cd'), 'first\nsecond\n', ['nested'], {value: 1}];
37+
const unbufferedDescriptor = bufferedDescriptor === 'stdout' ? 'stderr' : 'stdout';
38+
const subprocess = execa(failure ? 'all-fail.js' : 'noop-both.js', {
39+
all: true,
40+
lines: true,
41+
buffer: {[unbufferedDescriptor]: false},
42+
[bufferedDescriptor]: {
43+
objectMode: true,
44+
* transform() {},
45+
* final() {
46+
yield * chunks;
47+
},
48+
},
49+
});
50+
const result = failure ? await assertRejects(subprocess, {exitCode: 1}) : await subprocess;
51+
assert.equal(result.failed, failure);
52+
assert.equal(result[unbufferedDescriptor], undefined);
53+
assert.deepEqual(result[bufferedDescriptor], chunks);
54+
assert.deepEqual(result.all, chunks);
55+
for (const [index, chunk] of chunks.entries()) {
56+
assert.equal(result.all[index], chunk);
57+
}
58+
};
59+
60+
const testSingleBufferedTextMode = async (bufferedDescriptor, encoding, failure) => {
61+
const unbufferedDescriptor = bufferedDescriptor === 'stdout' ? 'stderr' : 'stdout';
62+
const subprocess = execa(failure ? 'all-fail.js' : 'noop-both.js', {
63+
all: true,
64+
encoding,
65+
stripFinalNewline: false,
66+
buffer: {[unbufferedDescriptor]: false},
67+
[unbufferedDescriptor]: outputObjectGenerator(),
68+
});
69+
const result = failure ? await assertRejects(subprocess, {exitCode: 1}) : await subprocess;
70+
const failureOutput = bufferedDescriptor === 'stdout' ? 'std\nout' : 'std\nerr';
71+
const output = `${failure ? failureOutput : foobarString}\n`;
72+
const expected = encoding === 'buffer' ? textEncoder.encode(output) : Buffer.from(output).toString(encoding);
73+
assert.equal(result[unbufferedDescriptor], undefined);
74+
assert.deepEqual(result[bufferedDescriptor], expected);
75+
assert.deepEqual(result.all, expected);
76+
};
77+
78+
// Object-mode chunks are passed through unchanged, so they are not affected by the `encoding` option
79+
for (const bufferedDescriptor of ['stdout', 'stderr']) {
80+
for (const failure of [false, true]) {
81+
const resultType = failure ? 'error' : 'result';
82+
test(`${resultType}.all preserves object-mode chunks when only ${bufferedDescriptor} is buffered`, () => testSingleBufferedObjectMode(bufferedDescriptor, failure));
83+
}
84+
}
85+
86+
for (const bufferedDescriptor of ['stdout', 'stderr']) {
87+
for (const encoding of ['utf8', 'buffer', 'hex']) {
88+
for (const failure of [false, true]) {
89+
const resultType = failure ? 'error' : 'result';
90+
test(`${resultType}.all decodes text when only ${bufferedDescriptor} is buffered and the other descriptor is in object mode, encoding "${encoding}"`, () => testSingleBufferedTextMode(bufferedDescriptor, encoding, failure));
91+
}
92+
}
93+
}
94+
1795
const getAllStdioOption = (stdioOption, encoding, objectMode) => {
1896
if (stdioOption) {
1997
return 'pipe';

0 commit comments

Comments
 (0)