From 38a5af0beed655fa1186bde8dbbdae69ac3600ae Mon Sep 17 00:00:00 2001 From: nlf Date: Thu, 27 Oct 2022 13:27:07 -0700 Subject: [PATCH] chore: refactor tests to use spawk --- package.json | 1 + test/index.js | 413 ++++++++++++++++++++++++++++++++++++++++++ test/promise-spawn.js | 226 ----------------------- 3 files changed, 414 insertions(+), 226 deletions(-) create mode 100644 test/index.js delete mode 100644 test/promise-spawn.js diff --git a/package.json b/package.json index 679c498..715417d 100644 --- a/package.json +++ b/package.json @@ -34,6 +34,7 @@ "@npmcli/eslint-config": "^4.0.0", "@npmcli/template-oss": "4.8.0", "minipass": "^3.1.1", + "spawk": "^1.7.1", "tap": "^16.0.1" }, "engines": { diff --git a/test/index.js b/test/index.js new file mode 100644 index 0000000..a9bd8f4 --- /dev/null +++ b/test/index.js @@ -0,0 +1,413 @@ +'use strict' + +const spawk = require('spawk') +const t = require('tap') + +const promiseSpawn = require('../lib/index.js') + +spawk.preventUnmatched() +t.afterEach(() => { + spawk.clean() +}) + +t.test('defaults to returning buffers', async (t) => { + const proc = spawk.spawn('pass', [], {}) + .stdout(Buffer.from('OK\n')) + + const result = await promiseSpawn('pass', []) + t.hasStrict(result, { + code: 0, + signal: null, + stdout: Buffer.from('OK\n'), + stderr: Buffer.from(''), + }) + + t.ok(proc.called) +}) + +t.test('extra context is returned', async (t) => { + const proc = spawk.spawn('pass', [], {}) + .stdout(Buffer.from('OK\n')) + + const result = await promiseSpawn('pass', [], {}, { extra: 'property' }) + t.hasStrict(result, { + code: 0, + signal: null, + stdout: Buffer.from('OK\n'), + stderr: Buffer.from(''), + extra: 'property', + }) + + t.ok(proc.called) +}) + +t.test('stdioString returns trimmed strings', async (t) => { + const proc = spawk.spawn('pass', [], {}) + .stdout(Buffer.from('OK\n')) + + const result = await promiseSpawn('pass', [], { stdioString: true }) + t.hasStrict(result, { + code: 0, + signal: null, + stdout: 'OK', + stderr: '', + }) + + t.ok(proc.called) +}) + +t.test('stdout and stderr are null when stdio is inherit', async (t) => { + const proc = spawk.spawn('pass', [], { stdio: 'inherit' }) + .stdout(Buffer.from('OK\n')) + + const result = await promiseSpawn('pass', [], { stdio: 'inherit' }) + t.hasStrict(result, { + code: 0, + signal: null, + stdout: null, + stderr: null, + }) + + t.ok(proc.called) +}) + +t.test('stdout and stderr are null when stdio is inherit and stdioString is set', async (t) => { + const proc = spawk.spawn('pass', [], { stdio: 'inherit' }) + .stdout(Buffer.from('OK\n')) + + const result = await promiseSpawn('pass', [], { stdio: 'inherit', stdioString: true }) + t.hasStrict(result, { + code: 0, + signal: null, + stdout: null, + stderr: null, + }) + + t.ok(proc.called) +}) + +t.test('stdout is null when stdio is [pipe, inherit, pipe]', async (t) => { + const proc = spawk.spawn('pass', [], { stdio: ['pipe', 'inherit', 'pipe'] }) + .stdout(Buffer.from('OK\n')) + + const result = await promiseSpawn('pass', [], { stdio: ['pipe', 'inherit', 'pipe'] }) + t.hasStrict(result, { + code: 0, + signal: null, + stdout: null, + stderr: Buffer.from(''), + }) + + t.ok(proc.called) +}) + +t.test('stderr is null when stdio is [pipe, pipe, inherit]', async (t) => { + const proc = spawk.spawn('pass', [], { stdio: ['pipe', 'pipe', 'inherit'] }) + .stdout(Buffer.from('OK\n')) + + const result = await promiseSpawn('pass', [], { stdio: ['pipe', 'pipe', 'inherit'] }) + t.hasStrict(result, { + code: 0, + signal: null, + stdout: Buffer.from('OK\n'), + stderr: null, + }) + + t.ok(proc.called) +}) + +t.test('exposes stdin', async (t) => { + const proc = spawk.spawn('stdin', [], {}) + const p = promiseSpawn('stdin', []) + process.nextTick(() => { + p.process.stdin.pipe(p.process.stdout) + p.stdin.end('hello') + }) + + const result = await p + t.hasStrict(result, { + code: 0, + signal: null, + stdout: Buffer.from('hello'), + stderr: Buffer.from(''), + }) + + t.ok(proc.called) +}) + +t.test('exposes process', async (t) => { + const proc = spawk.spawn('proc', [], {}) + .exitOnSignal('SIGFAKE') + + const p = promiseSpawn('proc', []) + process.nextTick(() => p.process.kill('SIGFAKE')) + + // there are no signals in windows, so we expect a different result + if (process.platform === 'win32') { + await t.rejects(p, { + code: 1, + signal: null, + stdout: Buffer.from(''), + stderr: Buffer.from(''), + }) + } else { + await t.rejects(p, { + code: null, + signal: 'SIGFAKE', + stdout: Buffer.from(''), + stderr: Buffer.from(''), + }) + } + + t.ok(proc.called) +}) + +t.test('rejects when spawn errors', async (t) => { + const proc = spawk.spawn('notfound', [], {}) + .spawnError(new Error('command not found')) + + await t.rejects(promiseSpawn('notfound', []), { + message: 'command not found', + stdout: Buffer.from(''), + stderr: Buffer.from(''), + }) + + t.ok(proc.called) +}) + +t.test('spawn error includes extra', async (t) => { + const proc = spawk.spawn('notfound', [], {}) + .spawnError(new Error('command not found')) + + await t.rejects(promiseSpawn('notfound', [], {}, { extra: 'property' }), { + message: 'command not found', + stdout: Buffer.from(''), + stderr: Buffer.from(''), + extra: 'property', + }) + + t.ok(proc.called) +}) + +t.test('spawn error respects stdioString', async (t) => { + const proc = spawk.spawn('notfound', [], {}) + .spawnError(new Error('command not found')) + + await t.rejects(promiseSpawn('notfound', [], { stdioString: true }), { + message: 'command not found', + stdout: '', + stderr: '', + }) + + t.ok(proc.called) +}) + +t.test('spawn error respects stdio as inherit', async (t) => { + const proc = spawk.spawn('notfound', [], { stdio: 'inherit' }) + .spawnError(new Error('command not found')) + + await t.rejects(promiseSpawn('notfound', [], { stdio: 'inherit' }), { + message: 'command not found', + stdout: null, + stderr: null, + }) + + t.ok(proc.called) +}) + +t.test('rejects when command fails', async (t) => { + const proc = spawk.spawn('fail', [], {}) + .stderr(Buffer.from('Error!\n')) + .exit(1) + + await t.rejects(promiseSpawn('fail', []), { + message: 'command failed', + code: 1, + stdout: Buffer.from(''), + stderr: Buffer.from('Error!\n'), + }) + + t.ok(proc.called) +}) + +t.test('failed command returns extra', async (t) => { + const proc = spawk.spawn('fail', [], {}) + .stderr(Buffer.from('Error!\n')) + .exit(1) + + await t.rejects(promiseSpawn('fail', [], {}, { extra: 'property' }), { + message: 'command failed', + code: 1, + stdout: Buffer.from(''), + stderr: Buffer.from('Error!\n'), + extra: 'property', + }) + + t.ok(proc.called) +}) + +t.test('failed command respects stdioString', async (t) => { + const proc = spawk.spawn('fail', [], {}) + .stderr(Buffer.from('Error!\n')) + .exit(1) + + await t.rejects(promiseSpawn('fail', [], { stdioString: true }), { + message: 'command failed', + code: 1, + stdout: '', + stderr: 'Error!', + }) + + t.ok(proc.called) +}) + +t.test('failed command respects stdio as inherit', async (t) => { + const proc = spawk.spawn('fail', [], { stdio: 'inherit' }) + .stderr(Buffer.from('Error!\n')) + .exit(1) + + await t.rejects(promiseSpawn('fail', [], { stdio: 'inherit' }), { + message: 'command failed', + code: 1, + stdout: null, + stderr: null, + }) + + t.ok(proc.called) +}) + +t.test('rejects when signal kills child', async (t) => { + const proc = spawk.spawn('signal', [], {}) + .signal('SIGFAKE') + + const p = promiseSpawn('signal', []) + // there are no signals in windows, so we expect a different result + if (process.platform === 'win32') { + await t.rejects(p, { + code: 1, + signal: null, + stdout: Buffer.from(''), + stderr: Buffer.from(''), + }) + } else { + await t.rejects(p, { + code: null, + signal: 'SIGFAKE', + stdout: Buffer.from(''), + stderr: Buffer.from(''), + }) + } + + t.ok(proc.called) +}) + +t.test('signal death includes extra', async (t) => { + const proc = spawk.spawn('signal', [], {}) + .signal('SIGFAKE') + + const p = promiseSpawn('signal', [], {}, { extra: 'property' }) + // there are no signals in windows, so we expect a different result + if (process.platform === 'win32') { + await t.rejects(p, { + code: 1, + signal: null, + stdout: Buffer.from(''), + stderr: Buffer.from(''), + extra: 'property', + }) + } else { + await t.rejects(p, { + code: null, + signal: 'SIGFAKE', + stdout: Buffer.from(''), + stderr: Buffer.from(''), + extra: 'property', + }) + } + + t.ok(proc.called) +}) + +t.test('signal death respects stdioString', async (t) => { + const proc = spawk.spawn('signal', [], {}) + .signal('SIGFAKE') + + const p = promiseSpawn('signal', [], { stdioString: true }) + // there are no signals in windows, so we expect a different result + if (process.platform === 'win32') { + await t.rejects(p, { + code: 1, + signal: null, + stdout: '', + stderr: '', + }) + } else { + await t.rejects(p, { + code: null, + signal: 'SIGFAKE', + stdout: '', + stderr: '', + }) + } + + t.ok(proc.called) +}) + +t.test('signal death respects stdio as inherit', async (t) => { + const proc = spawk.spawn('signal', [], { stdio: 'inherit' }) + .signal('SIGFAKE') + + const p = promiseSpawn('signal', [], { stdio: 'inherit' }) + // there are no signals in windows, so we expect a different result + if (process.platform === 'win32') { + await t.rejects(p, { + code: 1, + signal: null, + stdout: null, + stderr: null, + }) + } else { + await t.rejects(p, { + code: null, + signal: 'SIGFAKE', + stdout: null, + stderr: null, + }) + } + + t.ok(proc.called) +}) + +t.test('rejects when stdout errors', async (t) => { + const proc = spawk.spawn('stdout-err', [], {}) + + const p = promiseSpawn('stdout-err', []) + process.nextTick(() => p.process.stdout.emit('error', new Error('stdout err'))) + + await t.rejects(p, { + message: 'stdout err', + code: null, + signal: null, + stdout: Buffer.from(''), + stderr: Buffer.from(''), + }) + + t.ok(proc.called) +}) + +t.test('rejects when stderr errors', async (t) => { + const proc = spawk.spawn('stderr-err', [], {}) + + const p = promiseSpawn('stderr-err', []) + process.nextTick(() => p.process.stderr.emit('error', new Error('stderr err'))) + + await t.rejects(p, { + message: 'stderr err', + code: null, + signal: null, + stdout: Buffer.from(''), + stderr: Buffer.from(''), + }) + + t.ok(proc.called) +}) diff --git a/test/promise-spawn.js b/test/promise-spawn.js deleted file mode 100644 index eb28cbf..0000000 --- a/test/promise-spawn.js +++ /dev/null @@ -1,226 +0,0 @@ -const t = require('tap') -const Minipass = require('minipass') -const EE = require('events') - -const isPipe = (stdio = 'pipe', fd) => - stdio === 'pipe' || stdio === null ? true - : Array.isArray(stdio) ? isPipe(stdio[fd], fd) - : false - -class MockProc extends EE { - constructor (cmd, args, opts) { - super() - this.cmd = cmd - this.args = args - this.opts = opts - this.stdin = isPipe(opts.stdio, 0) ? new Minipass() : null - this.stdout = isPipe(opts.stdio, 1) ? new Minipass() : null - this.stderr = isPipe(opts.stdio, 2) ? new Minipass() : null - this.code = null - this.signal = null - process.nextTick(() => this.run()) - } - - exit (code) { - this.code = code - this.emit('exit', this.code, this.signal) - if (this.stdout && this.stderr) { - let stdoutEnded = false - let stderrEnded = false - this.stdout.on('end', () => { - stdoutEnded = true - if (stderrEnded) { - this.emit('close', this.code, this.signal) - } - }) - this.stderr.on('end', () => { - stderrEnded = true - if (stdoutEnded) { - this.emit('close', this.code, this.signal) - } - }) - this.stdout.end() - this.stderr.end() - } else { - this.emit('close', this.code, this.signal) - } - } - - kill (signal) { - this.signal = signal - this.exit(null) - } - - writeOut (m) { - this.stdout && this.stdout.write(m) - } - - writeErr (m) { - this.stderr && this.stderr.write(m) - } - - run () { - switch (this.cmd) { - case 'cat': - this.stdin.on('data', c => this.writeOut(c)) - this.stdin.on('end', () => this.exit(0)) - return - case 'not found': - return this.emit('error', new Error('command not found')) - case 'signal': - this.writeOut('stdout') - this.writeErr('stderr') - return this.kill('SIGFAKE') - case 'pass': - this.writeOut('OK :)') - return this.exit(0) - case 'pass-nl': - this.writeOut('OK :)\n') - return this.exit(0) - case 'fail': - this.writeOut('not ok :(') - this.writeErr('Some kind of helpful error') - return this.exit(1) - case 'whoami': - this.writeOut(`UID ${this.opts.uid}\n`) - this.writeOut(`GID ${this.opts.gid}\n`) - return this.exit(0) - case 'stdout-fail': - this.stdout.emit('error', new Error('stdout error')) - return this.exit(1) - case 'stderr-fail': - this.stderr.emit('error', new Error('stderr error')) - return this.exit(1) - } - } -} - -const promiseSpawn = t.mock('../', { - child_process: { - spawn: (cmd, args, opts) => new MockProc(cmd, args, opts), - }, -}) - -t.test('not found', t => t.rejects(promiseSpawn('not found', [], {}), { - message: 'command not found', -})) - -t.test('not found, with extra', - t => t.rejects(promiseSpawn('not found', [], { stdioString: true }, { a: 1 }), { - message: 'command not found', - stdout: '', - stderr: '', - a: 1, - })) - -t.test('pass', t => t.resolveMatch(promiseSpawn('pass', [], { stdioString: true }, { a: 1 }), { - code: 0, - signal: null, - stdout: 'OK :)', - stderr: '', - a: 1, -})) - -t.test('pass trim output', t => t.resolveMatch(promiseSpawn('pass-nl', [], { stdioString: true }), { - code: 0, - signal: null, - stdout: 'OK :)', - stderr: '', -})) - -t.test('pass, default opts', t => t.resolveMatch(promiseSpawn('pass', []), { - code: 0, - signal: null, -})) - -t.test('pass, share stdio', - t => t.resolveMatch(promiseSpawn('pass', [], { stdio: 'inherit' }, { a: 1 }), { - code: 0, - signal: null, - stdout: null, - stderr: null, - a: 1, - })) - -t.test('pass, share stdout', - t => t.resolveMatch( - promiseSpawn('pass', [], { stdioString: true, stdio: ['pipe', 'inherit', 'pipe'] }, { a: 1 }), { - code: 0, - signal: null, - stdout: null, - stderr: '', - a: 1, - })) - -t.test('pass, share stderr', - t => t.resolveMatch( - promiseSpawn('pass', [], { stdioString: true, stdio: ['pipe', 'pipe', 'inherit'] }, { a: 1 }), { - code: 0, - signal: null, - stdout: 'OK :)', - stderr: null, - a: 1, - })) - -t.test('fail', t => t.rejects(promiseSpawn('fail', [], {}, { a: 1 }), { - message: 'command failed', - code: 1, - signal: null, - stdout: Buffer.from('not ok :('), - stderr: Buffer.from('Some kind of helpful error'), - a: 1, -})) - -t.test('fail, shared stdio', - t => t.rejects(promiseSpawn('fail', [], { stdio: 'inherit' }, { a: 1 }), { - message: 'command failed', - code: 1, - signal: null, - stdout: null, - stderr: null, - a: 1, - })) - -t.test('signal', t => t.rejects(promiseSpawn('signal', [], {}, { a: 1 }), { - message: 'command failed', - code: null, - signal: 'SIGFAKE', - stdout: Buffer.from('stdout'), - stderr: Buffer.from('stderr'), - a: 1, -})) - -t.test('stdio errors', t => { - t.rejects(promiseSpawn('stdout-fail', [], {}), { - message: 'stdout error', - }) - t.rejects(promiseSpawn('stderr-fail', [], {}), { - message: 'stderr error', - }) - t.end() -}) - -t.test('expose process stdin', t => { - const p = promiseSpawn('cat', [], { stdio: 'pipe' }) - t.resolveMatch(p, { - code: 0, - signal: null, - stdout: Buffer.from('hello'), - stderr: Buffer.alloc(0), - }) - t.end() - p.stdin.write('hell') - setTimeout(() => p.stdin.end('o')) -}) - -t.test('expose process', t => { - const p = promiseSpawn('cat', [], { stdio: 'pipe' }) - t.resolveMatch(p, { - code: 0, - signal: null, - stdout: Buffer.alloc(0), - stderr: Buffer.alloc(0), - }) - t.end() - setTimeout(() => p.process.exit(0)) -})