These tests make assertions about some of the details regarding streams when they are connected to files and used within an async/await context. These details help me produce better error messages.
Last active
June 5, 2021 00:00
-
-
Save dmohs/297cf26901b52e43fe09ab38564761c8 to your computer and use it in GitHub Desktop.
Contract tests for NodeJs streams.
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
{ | |
"name": "nodejs-stream-contract-tests", | |
"version": "1.0.0", | |
"description": "Contract tests for NodeJs streams.", | |
"main": "index.js", | |
"scripts": { | |
"test": "mocha tests.js" | |
}, | |
"author": "David E. Mohs", | |
"license": "MIT", | |
"dependencies": { | |
"mocha": "^8.4.0" | |
} | |
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
const assert = require('assert').strict | |
const fs = require('fs') | |
const fsp = fs.promises | |
const os = require('os') | |
const path = require('path') | |
const withTempFile = f => withTempDir(dir => f(path.join(dir, 'file'))) | |
const withTempDir = async f => { | |
const dir = await fsp.mkdtemp(await fsp.realpath(os.tmpdir()) + path.sep) | |
try { | |
return await f(dir) | |
} finally { | |
fsp.rmdir(dir, {recursive: true}) | |
} | |
} | |
function promiseEvent(event, object) { | |
return new Promise((resolve, reject) => { | |
object.on(event, (...args) => { | |
if (args.length === 0) { return resolve(null) } | |
if (args.length === 1) { return resolve(args[0]) } | |
return resolve(args) | |
}) | |
// object.on('error', e => { console.error('error on stream', e) }) | |
object.on('error', reject) | |
}) | |
} | |
// These handlers are crucial. Otherwise the exceptions can go unreported, impeding debugging. | |
process.on('uncaughtException', (err, origin) => { | |
console.warn('Caught exception. Details below.') | |
console.error(err) | |
}) | |
process.on('unhandledRejection', (reason, promise) => { | |
console.warn('Caught unhandled rejection.') | |
throw reason | |
}) | |
describe('File Streams', function() { | |
it('should copy via pipe', function(done) { | |
withTempFile(async dst => { | |
const srcStats = await fsp.lstat(__filename) | |
const readStream = fs.createReadStream(__filename) | |
const writeStream = fs.createWriteStream(dst) | |
const pipe = readStream.pipe(writeStream) | |
// pipe returns the last stream for chaining. | |
assert.strictEqual(pipe, writeStream) | |
const dstStatsBegin = await fsp.lstat(dst) | |
// Haven't awaited the copy, so nothing done yet. | |
assert.strictEqual(0, dstStatsBegin.size) | |
// This times out. Docs say, "The 'end' event will not be emitted unless the data is | |
// completely consumed." I would expect the pipe to be flowing while awaiting this, | |
// so I'm not sure why it isn't completely consumed. Same result listening for 'close'. | |
// HANGS // await promiseEvent('end', readStream) | |
await promiseEvent('finish', pipe) | |
// Not necessary, but let's be certain. | |
await promiseEvent('close', readStream) | |
const dstStats = await fsp.lstat(dst) | |
assert.strictEqual(srcStats.size, dstStats.size) | |
done() | |
}) | |
}) | |
it('should fail verbosely if src doesn\'t exist', function(done) { | |
withTempFile(async nullSrc => { | |
// And now we play the "when will it throw" game. | |
// Will it throw here? No. | |
const readStream = fs.createReadStream(nullSrc) | |
// Here? No. | |
const pipe = readStream.pipe(fs.createWriteStream('/dev/null')) | |
// How about we listen for the 'finish' event on the pipe? Nope, wrong stream. | |
// NOPE // await promiseEvent('finish', pipe) | |
// Readable does not have the event we want. The event we want is from the fs.ReadStream | |
// subclass of Readable, which has the 'open' event. | |
await assert.rejects( | |
_ => promiseEvent('open', readStream), | |
{errno: -2, code: 'ENOENT', syscall: 'open', path: nullSrc} | |
) | |
done() | |
}) | |
}) | |
it('should fail early if src doesn\'t exist', function(done) { | |
withTempFile(async nullSrc => { | |
const src = nullSrc+'/foo/bar' | |
const readStream = fs.createReadStream(src) | |
await assert.rejects( | |
_ => promiseEvent('open', readStream), | |
{errno: -2, code: 'ENOENT', syscall: 'open', path: src} | |
) | |
done() | |
}) | |
}) | |
it('should fail verbosely if dst can\'t be written', async function() { | |
const readStream = fs.createReadStream(__filename) | |
const nullDst = '/dev/null/should-not-exist' | |
const pipe = readStream.pipe(fs.createWriteStream(nullDst)) | |
// In this case, the stream we're watching is the one throwing the error. | |
await assert.rejects( | |
_ => promiseEvent('finish', pipe), | |
{errno: -20, code: 'ENOTDIR', syscall: 'open', path: nullDst} | |
) | |
}) | |
}) |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment