rindle
Version:
Collection of utilities for working with Streams
487 lines (419 loc) • 13.3 kB
JavaScript
/*
* The MIT License
*
* Copyright (c) 2015 Juan Cruz Viotti. https://jviotti.github.io
*
* Permission is hereby granted, free of charge, to any person obtaining a copy
* of this software and associated documentation files (the "Software"), to deal
* in the Software without restriction, including without limitation the rights
* to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
* copies of the Software, and to permit persons to whom the Software is
* furnished to do so, subject to the following conditions:
*
* The above copyright notice and this permission notice shall be included in
* all copies or substantial portions of the Software.
*
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
* FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
* AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
* LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
* OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
* THE SOFTWARE.
*/
var chai = require('chai');
var sinon = require('sinon');
var _ = require('lodash');
var fs = require('fs');
var StreamReadable = require('stream').Readable;
var StreamPassThrough = require('stream').PassThrough;
var EventEmitter = require('events').EventEmitter;
const { setTimeout: delay } = require('timers/promises');
var tmp = require('tmp');
tmp.setGracefulCleanup();
chai.use(require('chai-as-promised'));
chai.use(require('sinon-chai'));
var { expect } = chai;
var rindle = require('../lib/rindle');
describe('Rindle:', function () {
'use strict';
describe('.wait()', function () {
describe('given a stream that emits a close event with no result', function () {
beforeEach(function () {
this.stream = new EventEmitter();
setTimeout(
_.bind(function () {
this.stream.emit('close');
}, this),
100,
);
});
it('should not reject', async function () {
await rindle.wait(this.stream);
});
it('should return no result', async function () {
const result = await rindle.wait(this.stream);
expect(result).to.deep.equal([]);
});
});
describe('given a stream that emits a close event with a result', function () {
beforeEach(function () {
this.stream = new EventEmitter();
setTimeout(
_.bind(function () {
this.stream.emit('close', 'foo');
}, this),
100,
);
});
it('should return the result', async function () {
const result = await rindle.wait(this.stream);
expect(result).to.deep.equal(['foo']);
});
});
describe('given a stream that emits a close event with multiple results', function () {
beforeEach(function () {
this.stream = new EventEmitter();
setTimeout(
_.bind(function () {
this.stream.emit('close', 'foo', 'bar', 'baz');
}, this),
100,
);
});
it('should return all the results', async function () {
const result = await rindle.wait(this.stream);
expect(result).to.deep.equal(['foo', 'bar', 'baz']);
});
});
describe('given a stream that emits an end event with no result', function () {
beforeEach(function () {
this.stream = new EventEmitter();
setTimeout(
_.bind(function () {
this.stream.emit('end');
}, this),
100,
);
});
it('should return no error', async function () {
await rindle.wait(this.stream);
});
});
describe('given a stream that emits a done event with no result', function () {
beforeEach(function () {
this.stream = new EventEmitter();
setTimeout(
_.bind(function () {
this.stream.emit('done');
}, this),
100,
);
});
it('should return no error', async function () {
await rindle.wait(this.stream);
});
});
describe('given a stream that emits an error event', function () {
beforeEach(function () {
this.stream = new EventEmitter();
setTimeout(
_.bind(function () {
this.stream.emit('error', new Error('stream error'));
}, this),
100,
);
});
it('should yield the error', async function () {
try {
await rindle.wait(this.stream);
throw new Error('No error was thrown but was expected');
} catch (error) {
expect(error).to.be.an.instanceof(Error);
expect(error.message).to.equal('stream error');
}
});
});
});
describe('.extract()', function () {
describe('given a stream that emits data', function () {
beforeEach(function () {
this.stream = new StreamReadable({
encoding: 'utf8',
});
this.stream._read = function () {
this.push('Hello');
this.push(' ');
this.push('World');
this.push(null);
};
});
it('should yield the stream data', async function () {
rindle.extract(this.stream, function (error, data) {
expect(error).to.not.exist;
expect(data).to.equal('Hello World');
});
});
});
describe('given a stream that throws an error', function () {
beforeEach(function () {
this.stream = new StreamReadable({
encoding: 'utf8',
});
this.stream._read = function () {
// If we don't emit an error event with a slight timeout
// then the error is emitted before an error listener
// is attached. This causes the error to be thrown
// directly in Node v0.10.
_.defer(
_.bind(function () {
this.emit('error', new Error('stream error'));
}, this),
);
};
});
it('should yield the error', async function () {
try {
await rindle.extract(this.stream);
throw new Error('No error was thrown but was expected');
} catch (error) {
expect(error).to.be.an.instanceof(Error);
expect(error.message).to.equal('stream error');
}
});
});
});
describe('.bifurcate()', function () {
describe('given a stream that emits data', function () {
beforeEach(function () {
this.stream = new StreamReadable({
encoding: 'utf8',
});
this.stream._read = function () {
this.push('Hello');
this.push(' ');
this.push('World');
this.push(null);
};
});
describe('given two duplex streams', function () {
beforeEach(function () {
this.output1 = new StreamPassThrough();
this.output2 = new StreamPassThrough();
});
it('should pipe all data to both streams', function () {
rindle.bifurcate(this.stream, this.output1, this.output2);
return Promise.all([
rindle.extract(this.output1),
rindle.extract(this.output2),
]).then(function ([one, two]) {
expect(one).to.equal('Hello World');
expect(two).to.equal('Hello World');
});
});
});
describe('given two file-system writable streams', function () {
beforeEach(function () {
this.output1Path = tmp.tmpNameSync();
this.output2Path = tmp.tmpNameSync();
this.output1 = fs.createWriteStream(this.output1Path);
this.output2 = fs.createWriteStream(this.output2Path);
});
it('should write all data to both files', function () {
return rindle.bifurcate(
this.stream,
this.output1,
this.output2,
_.bind(function (error) {
return Promise.all([
fs.promises.readFile(this.output1Path, {
encoding: 'utf8',
}),
fs.promises.readFile(this.output2Path, {
encoding: 'utf8',
}),
]).then(function ([one, two]) {
expect(one).to.equal('Hello World');
expect(two).to.equal('Hello World');
});
}, this),
);
});
});
});
});
describe('.pipeWithEvents()', function () {
describe('given a stream which emits various events', function () {
beforeEach(function () {
this.stream = new StreamReadable({
encoding: 'utf8',
});
this.stream._read = function () {
this.push('Hello');
this.push(' ');
this.push('World');
this.push(null);
};
_.defer(
_.bind(function () {
this.stream.emit('foo');
this.stream.emit('bar');
this.stream.emit('baz');
}, this),
);
});
it('should be able to pipe all events', async function () {
var output = new StreamPassThrough();
var fooSpy = sinon.spy();
var barSpy = sinon.spy();
var bazSpy = sinon.spy();
output.on('foo', fooSpy);
output.on('bar', barSpy);
output.on('baz', bazSpy);
var pipe = rindle.pipeWithEvents(this.stream, output, [
'foo',
'bar',
'baz',
]);
const data = await rindle.extract(pipe);
await delay(100);
expect(data).to.equal('Hello World');
expect(fooSpy).to.have.been.calledOnce;
expect(barSpy).to.have.been.calledOnce;
expect(bazSpy).to.have.been.calledOnce;
});
it('should be able to pipe some events', async function () {
var output = new StreamPassThrough();
var fooSpy = sinon.spy();
var barSpy = sinon.spy();
var bazSpy = sinon.spy();
output.on('foo', fooSpy);
output.on('bar', barSpy);
output.on('baz', bazSpy);
var pipe = rindle.pipeWithEvents(this.stream, output, ['bar']);
const data = await rindle.extract(pipe);
await delay(100);
expect(data).to.equal('Hello World');
expect(fooSpy).to.not.have.been.called;
expect(barSpy).to.have.been.calledOnce;
expect(bazSpy).to.not.have.been.called;
});
});
describe('given a stream which emits events with data', function () {
beforeEach(function () {
this.stream = new StreamReadable({
encoding: 'utf8',
});
this.stream._read = function () {
this.push('Hello');
this.push(' ');
this.push('World');
this.push(null);
};
_.defer(
_.bind(function () {
this.stream.emit('foo', 'bar', 'baz');
this.stream.emit('hello', 'world');
}, this),
);
});
it('should pipe the events along with their corresponding data', async function () {
var output = new StreamPassThrough();
var fooSpy = sinon.spy();
var helloSpy = sinon.spy();
output.on('foo', fooSpy);
output.on('hello', helloSpy);
var pipe = rindle.pipeWithEvents(this.stream, output, ['foo', 'hello']);
const data = await rindle.extract(pipe);
await delay(100);
expect(data).to.equal('Hello World');
expect(fooSpy).to.have.been.calledOnce;
expect(fooSpy).to.have.been.calledWith('bar', 'baz');
expect(helloSpy).to.have.been.calledOnce;
expect(helloSpy).to.have.been.calledWith('world');
});
});
describe('given a stream which emits an event multiple times', function () {
beforeEach(function () {
this.stream = new StreamReadable({
encoding: 'utf8',
});
this.stream._read = function () {
this.push('Hello');
this.push(' ');
this.push('World');
this.push(null);
};
_.defer(
_.bind(function () {
this.stream.emit('foo', 'bar');
this.stream.emit('foo', 'baz');
this.stream.emit('foo', 'qux');
}, this),
);
});
it('should pipe the events the corresponding parts', async function () {
var output = new StreamPassThrough();
var fooSpy = sinon.spy();
output.on('foo', fooSpy);
var pipe = rindle.pipeWithEvents(this.stream, output, ['foo']);
const data = await rindle.extract(pipe);
await delay(100);
expect(data).to.equal('Hello World');
expect(fooSpy).to.have.been.calledThrice;
});
});
});
describe('.onEvent()', function () {
describe('given a stream that emits an event with no arguments', function () {
beforeEach(function () {
this.stream = new EventEmitter();
_.defer(
_.bind(function () {
this.stream.emit('foo');
}, this),
);
});
it('should eventually resolve to undefined', function () {
var promise = rindle.onEvent(this.stream, 'foo');
expect(promise).to.eventually.be.undefined;
});
});
describe('given a stream that emits an event with one argument', function () {
beforeEach(function () {
this.stream = new EventEmitter();
_.defer(
_.bind(function () {
this.stream.emit('foo', 'bar');
}, this),
);
});
it('should eventually resolve with the argument', function () {
var promise = rindle.onEvent(this.stream, 'foo');
expect(promise).to.eventually.equal('bar');
});
});
describe('given a stream that emits an event with multiple argument', function () {
beforeEach(function () {
this.stream = new EventEmitter();
_.defer(
_.bind(function () {
this.stream.emit('foo', 'bar', 'baz', 'qux');
}, this),
);
});
it('should eventually resolve with all the arguments', async function () {
var promise = rindle.onEvent(this.stream, 'foo');
await expect(promise).to.eventually.become(['bar', 'baz', 'qux']);
});
it('should be able to spread all the arguments', async function () {
const result = await rindle.onEvent(this.stream, 'foo');
expect(result).to.deep.equal(['bar', 'baz', 'qux']);
});
it('should be able to spread all the arguments with a callback', async function () {
const result = await rindle.onEvent(this.stream, 'foo');
expect(result).to.deep.equal(['bar', 'baz', 'qux']);
});
});
});
});