Skip to content

Commit 9c54833

Browse files
committed
Run input transform final() when there is no input source
Synchronous input transforms were skipped when no `input`, `inputFile` or `stdin` source was provided, which meant their `final()` did not run and their errors were not reported.
1 parent cc7bcab commit 9c54833

2 files changed

Lines changed: 104 additions & 2 deletions

File tree

‎lib/io/input-sync.js‎

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -17,7 +17,8 @@ const getInputFdNumbers = fileDescriptors => new Set(Object.entries(fileDescript
1717
const addInputOptionSync = (fileDescriptors, fdNumber, options) => {
1818
const {stdioItems} = fileDescriptors[fdNumber];
1919
const allStdioItems = stdioItems.filter(({contents}) => contents !== undefined);
20-
if (allStdioItems.length === 0) {
20+
// Even without an input source, generators must run so that `final` can produce input or report errors.
21+
if (allStdioItems.length === 0 && stdioItems.every(({type}) => type !== 'generator')) {
2122
return;
2223
}
2324

‎test/transform/generator-final.js‎

Lines changed: 102 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,12 @@ import assert from 'node:assert/strict';
22
import test from 'node:test';
33
import {execa, execaSync} from '../../index.js';
44
import {foobarString} from '../helpers/input.js';
5-
import {getOutputAsyncGenerator, getOutputGenerator, convertTransformToFinal} from '../helpers/generator.js';
5+
import {
6+
getOutputAsyncGenerator,
7+
getOutputGenerator,
8+
convertTransformToFinal,
9+
throwingGenerator,
10+
} from '../helpers/generator.js';
611
import {setFixtureDirectory} from '../helpers/fixtures-directory.js';
712

813
setFixtureDirectory();
@@ -34,3 +39,99 @@ const testFinalNoOutput = async (final, execaMethod) => {
3439
test('Generators "final" can be used without "transform" nor output', () => testFinalNoOutput(getOutputGenerator, execa));
3540
test('Generators "final" can be used without "transform" nor output, sync', () => testFinalNoOutput(getOutputGenerator, execaSync));
3641
test('Generators "final" can be used without "transform" nor output, async', () => testFinalNoOutput(getOutputAsyncGenerator, execa));
42+
43+
const testInputFinalEmpty = options => {
44+
let finalCount = 0;
45+
const chunks = [];
46+
const {stdout} = execaSync('stdin.js', {
47+
...options,
48+
stdin: {
49+
* transform(chunk) {
50+
chunks.push(chunk);
51+
yield chunk;
52+
},
53+
* final() {
54+
finalCount += 1;
55+
yield foobarString;
56+
},
57+
binary: true,
58+
},
59+
});
60+
assert.equal(stdout, foobarString);
61+
assert.equal(finalCount, 1);
62+
assert.deepEqual(chunks, []);
63+
};
64+
65+
test('Input generators "final" runs without an input source, sync', () => testInputFinalEmpty({}));
66+
test('Input generators "final" runs with an empty string input source, sync', () => testInputFinalEmpty({input: ''}));
67+
test('Input generators "final" runs with an empty Uint8Array input source, sync', () => testInputFinalEmpty({input: new Uint8Array()}));
68+
69+
test('Input generators "final" runs once with multiple empty iterable input sources, sync', () => {
70+
let finalCount = 0;
71+
const {stdout} = execaSync('stdin.js', {
72+
stdin: [[], [], {
73+
* final() {
74+
finalCount += 1;
75+
yield foobarString;
76+
},
77+
}],
78+
});
79+
assert.equal(stdout, foobarString);
80+
assert.equal(finalCount, 1);
81+
});
82+
83+
test('Input generators "final" can produce binary input without an input source, sync', () => {
84+
const {stdout} = execaSync('stdin.js', {
85+
stdin: {
86+
* final() {
87+
yield new TextEncoder().encode('first\nsecond');
88+
},
89+
binary: true,
90+
},
91+
});
92+
assert.equal(stdout, 'first\nsecond');
93+
});
94+
95+
test('Input generators "final" output passes through the next transform without an input source, sync', () => {
96+
const calls = [];
97+
const {stdout} = execaSync('stdin.js', {
98+
stdin: [
99+
{
100+
* final() {
101+
yield 'first';
102+
yield 'second';
103+
},
104+
},
105+
{
106+
* transform(line) {
107+
calls.push(line);
108+
yield line.toUpperCase();
109+
},
110+
* final() {
111+
yield 'last';
112+
},
113+
},
114+
],
115+
});
116+
assert.equal(stdout, 'FIRST\nSECOND\nlast');
117+
assert.deepEqual(calls, ['first', 'second']);
118+
});
119+
120+
test('Input generators "final" errors are reported without an input source, sync', () => {
121+
const cause = new Error('Input final failed');
122+
assert.throws(() => {
123+
execaSync('stdin.js', {
124+
stdin: {final: throwingGenerator(cause)().transform},
125+
});
126+
}, {cause});
127+
});
128+
129+
test('Input generators "final" errors respect reject: false without an input source, sync', () => {
130+
const cause = new Error('Input final failed');
131+
const result = execaSync('stdin.js', {
132+
stdin: {final: throwingGenerator(cause)().transform},
133+
reject: false,
134+
});
135+
assert.equal(result.failed, true);
136+
assert.equal(result.cause, cause);
137+
});

0 commit comments

Comments
 (0)