|
1 | 1 | import process from 'node:process'; |
| 2 | +import {spawn} from 'node:child_process'; |
| 3 | +import {once} from 'node:events'; |
| 4 | +import {setTimeout as delay} from 'node:timers/promises'; |
2 | 5 | import test from 'ava'; |
3 | 6 | import {execa} from 'execa'; |
4 | 7 | import exitHook, {asyncExitHook} from './index.js'; |
5 | 8 |
|
| 9 | +const isContinuousIntegration = Boolean(process.env.CI); |
| 10 | + |
6 | 11 | test('main', async t => { |
7 | 12 | const {stdout, stderr, exitCode} = await execa(process.execPath, ['./fixtures/sync.js']); |
8 | 13 | t.is(stdout, 'foo\nbar'); |
@@ -155,6 +160,105 @@ test('type enforcing', t => { |
155 | 160 | }); |
156 | 161 | }); |
157 | 162 |
|
| 163 | +test('flushes stdout before exit with async hook', async t => { |
| 164 | + const lineCount = 10_000; |
| 165 | + const {stdout, exitCode} = await execa(process.execPath, ['./fixtures/flush-stdout.js', String(lineCount)]); |
| 166 | + const lines = stdout.split('\n').filter(Boolean); |
| 167 | + t.is(lines.length, lineCount); |
| 168 | + t.is(exitCode, 0); |
| 169 | +}); |
| 170 | + |
| 171 | +test('flushes stdout before exit with short wait and backpressure', async t => { |
| 172 | + const lineCount = 20_000; |
| 173 | + const childProcess = spawn(process.execPath, ['./fixtures/flush-stdout-short-wait.js', String(lineCount)], { |
| 174 | + stdio: ['ignore', 'pipe', 'ignore'], |
| 175 | + }); |
| 176 | + |
| 177 | + childProcess.stdout.setEncoding('utf8'); |
| 178 | + |
| 179 | + let stdout = ''; |
| 180 | + childProcess.stdout.on('data', data => { |
| 181 | + stdout += data; |
| 182 | + }); |
| 183 | + |
| 184 | + childProcess.stdout.pause(); |
| 185 | + await delay(200); |
| 186 | + childProcess.stdout.resume(); |
| 187 | + |
| 188 | + const [exitCode] = await once(childProcess, 'close'); |
| 189 | + t.is(exitCode, 0); |
| 190 | + |
| 191 | + const lines = stdout.split('\n').filter(Boolean); |
| 192 | + t.is(lines.length, lineCount); |
| 193 | +}); |
| 194 | + |
| 195 | +test('flushes stdout and stderr before exit with short wait and backpressure', async t => { |
| 196 | + const lineCount = 10_000; |
| 197 | + const childProcess = spawn(process.execPath, ['./fixtures/flush-stdout-stderr-short-wait.js', String(lineCount)], { |
| 198 | + stdio: ['ignore', 'pipe', 'pipe'], |
| 199 | + }); |
| 200 | + |
| 201 | + childProcess.stdout.setEncoding('utf8'); |
| 202 | + childProcess.stderr.setEncoding('utf8'); |
| 203 | + |
| 204 | + let stdout = ''; |
| 205 | + let stderr = ''; |
| 206 | + childProcess.stdout.on('data', data => { |
| 207 | + stdout += data; |
| 208 | + }); |
| 209 | + childProcess.stderr.on('data', data => { |
| 210 | + stderr += data; |
| 211 | + }); |
| 212 | + |
| 213 | + childProcess.stdout.pause(); |
| 214 | + childProcess.stderr.pause(); |
| 215 | + await delay(200); |
| 216 | + childProcess.stdout.resume(); |
| 217 | + childProcess.stderr.resume(); |
| 218 | + |
| 219 | + const [exitCode] = await once(childProcess, 'close'); |
| 220 | + t.is(exitCode, 0); |
| 221 | + |
| 222 | + const stdoutLines = stdout.split('\n').filter(Boolean); |
| 223 | + const stderrLines = stderr.split('\n').filter(Boolean); |
| 224 | + t.is(stdoutLines.length, lineCount); |
| 225 | + t.is(stderrLines.length, lineCount); |
| 226 | +}); |
| 227 | + |
| 228 | +test('flush timeout prevents hanging when stdout is blocked', async t => { |
| 229 | + t.timeout(isContinuousIntegration ? 15_000 : 8000); |
| 230 | + |
| 231 | + const startTime = Date.now(); |
| 232 | + const childProcess = spawn(process.execPath, ['./fixtures/flush-stdout-timeout.js'], { |
| 233 | + stdio: ['ignore', 'pipe', 'ignore'], |
| 234 | + }); |
| 235 | + const closePromise = once(childProcess, 'close'); |
| 236 | + |
| 237 | + childProcess.stdout.pause(); |
| 238 | + await delay(1500); |
| 239 | + childProcess.stdout.resume(); |
| 240 | + |
| 241 | + const [exitCode] = await closePromise; |
| 242 | + const elapsedMilliseconds = Date.now() - startTime; |
| 243 | + t.is(exitCode, 0); |
| 244 | + t.true(elapsedMilliseconds >= 900); |
| 245 | +}); |
| 246 | + |
| 247 | +test('flushes stdout before exit with sync hook', async t => { |
| 248 | + const lineCount = 10_000; |
| 249 | + const {stdout, exitCode} = await execa(process.execPath, ['./fixtures/flush-stdout-sync.js', String(lineCount)]); |
| 250 | + const lines = stdout.split('\n').filter(Boolean); |
| 251 | + t.is(lines.length, lineCount); |
| 252 | + t.is(exitCode, 0); |
| 253 | +}); |
| 254 | + |
| 255 | +test('does not throw when stdio is closed before flush', async t => { |
| 256 | + const {stdout, stderr, exitCode} = await execa(process.execPath, ['./fixtures/closed-stdio.js']); |
| 257 | + t.is(stdout, ''); |
| 258 | + t.is(stderr, ''); |
| 259 | + t.is(exitCode, 0); |
| 260 | +}); |
| 261 | + |
158 | 262 | const signalTests = [ |
159 | 263 | ['SIGINT', 130], |
160 | 264 | ['SIGTERM', 143], |
|
0 commit comments