Skip to content

Instantly share code, notes, and snippets.

@dmohs
Last active June 5, 2021 00:00
Show Gist options
  • Save dmohs/297cf26901b52e43fe09ab38564761c8 to your computer and use it in GitHub Desktop.
Save dmohs/297cf26901b52e43fe09ab38564761c8 to your computer and use it in GitHub Desktop.
Contract tests for NodeJs streams.

NodeJs Streams Contract Tests

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.

Synopsis

  1. Clone the gist repo.

  2. npm install

  3. npm test

{
"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"
}
}
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