pipette
Version:
Stream and pipe utilities for Node
632 lines (520 loc) • 17.7 kB
JavaScript
// Copyright 2012 The Obvious Corporation.
/*
* Modules used
*/
"use strict";
var assert = require("assert");
var events = require("events");
var Slicer = require("../").Slicer;
var CallbackCollector = require("./cbcoll").CallbackCollector;
var emit = require("./emit").emit;
/*
* Tests
*/
/**
* Makes sure the constructor doesn't fail off the bat.
*/
function constructor() {
var emitter = new events.EventEmitter();
new Slicer(emitter);
new Slicer(emitter, {});
new Slicer(emitter, { incomingEncoding: "hex" });
}
/**
* Tests expected constructor failures.
*/
function constructorFailures() {
function f1() {
new Slicer();
}
assert.throws(f1, /Missing source/);
function f2() {
new Slicer("non-emitter");
}
assert.throws(f2, /Source not an EventEmitter/);
function f3() {
new Slicer(new events.EventEmitter(), { incomingEncoding: "bad-encoding" });
}
assert.throws(f3, /Bad value for option: incomingEncoding/);
function f4() {
new Slicer(new events.EventEmitter(), { frobnitz: "fizmo" });
}
assert.throws(f4, /Unknown option: frobnitz/);
}
/**
* Tests the transition from readable to un-readable. This also checks
* to make sure the upstream source is un-listened immediately upon
* receipt of an end-type event.
*/
function readableTransition() {
var theData = new Buffer("Scones are delicious.");
tryWith(false, "end");
tryWith(false, "close");
tryWith(false, "close", new Error("oy"));
tryWith(false, "error", new Error("oof"));
tryWith(true, "end");
tryWith(true, "close");
tryWith(true, "close", new Error("oy"));
tryWith(true, "close", true);
tryWith(true, "error", new Error("oof"));
function tryWith(doData, endEvent, endArg) {
var source = new events.EventEmitter();
var slicer = new Slicer(source);
var coll = new CallbackCollector();
assert.ok(slicer.readable);
if (doData) {
source.emit("data", theData);
assert.ok(slicer.readable);
}
emit(source, endEvent, endArg);
assert.equal(source.listeners("close").length, 0);
assert.equal(source.listeners("data").length, 0);
assert.equal(source.listeners("end").length, 0);
assert.equal(source.listeners("error").length, 0);
if (doData) {
assert.ok(slicer.readable);
slicer.readAll(coll.callback);
assert.equal(coll.callbacks.length, 1);
coll.assertCallback(0, false, theData.length, theData, 0);
}
assert.ok(!slicer.readable);
// Also make sure that a pending error is "readable".
if (endArg) {
coll.reset();
slicer.readAll(coll.callback);
assert.equal(coll.callbacks.length, 1);
coll.assertCallback(0, true, 0, new Buffer(0), 0);
}
}
}
/**
* Tests that `destroy()` properly forces pending reads to get called
* back and un-listens to the upstream source.
*/
function destroy() {
var theData = new Buffer("muffins");
for (var i = 1; i <= 10; i++) {
tryWith(i);
}
function tryWith(count) {
var source = new events.EventEmitter();
var slicer = new Slicer(source);
var coll = new CallbackCollector();
for (var i = 0; i < count; i++) {
slicer.read(10, coll.callback);
}
assert.equal(coll.callbacks.length, 0);
source.emit("data", theData);
assert.equal(coll.callbacks.length, 0);
slicer.destroy();
assert.ok(!slicer.readable);
assert.equal(source.listeners("close").length, 0);
assert.equal(source.listeners("data").length, 0);
assert.equal(source.listeners("end").length, 0);
assert.equal(source.listeners("error").length, 0);
coll.assertCallback(0, true, theData.length, theData, 0);
for (var i = 1; i < count; i++) {
coll.assertCallback(i, true, 0, new Buffer(0), 0);
}
}
}
/**
* Tests that `getError()` and `gotError()` work as expected.
*/
function getErrorGotError() {
tryWith(false, false, "end");
tryWith(false, false, "close");
tryWith(false, false, "close", false);
tryWith(false, true, "end");
tryWith(false, true, "close");
tryWith(false, true, "close", false);
tryWith(true, false, "error");
tryWith(true, false, "error", false);
tryWith(true, false, "error", new Error("yikes"));
tryWith(true, false, "close", true);
tryWith(true, false, "close", new Error("stuff"));
tryWith(true, true, "error");
tryWith(true, true, "error", new Error("spaztastic"));
tryWith(true, true, "close", true);
function tryWith(expectError, doData, endEvent, endArg) {
var source = new events.EventEmitter();
var slicer = new Slicer(source);
assert.ok(!slicer.gotError());
assert.equal(slicer.getError(), undefined);
if (doData) {
source.emit("data", "I'm a sucker for a good biscuit.");
assert.ok(!slicer.gotError());
assert.equal(slicer.getError(), undefined);
}
emit(source, endEvent, endArg);
if (expectError) {
assert.ok(slicer.gotError());
assert.equal(slicer.getError(), endArg);
} else {
assert.ok(!slicer.gotError());
assert.equal(slicer.getError(), undefined);
}
}
}
/**
* Tests the no-data case of `readAll()`.
*/
function readAllNoData() {
var theData = new Buffer("scone");
tryWith(false, 1);
tryWith(false, 2);
tryWith(false, 10);
tryWith(true, 1);
tryWith(true, 2);
tryWith(true, 10);
function tryWith(doData, count) {
var source = new events.EventEmitter();
var slicer = new Slicer(source);
var coll = new CallbackCollector();
if (doData) {
slicer.read(theData.length, coll.callback);
assert.equal(coll.callbacks.length, 0);
}
for (var i = 0; i < count; i++) {
slicer.readAll(coll.callback);
}
if (doData) {
assert.equal(coll.callbacks.length, 0);
source.emit("data", theData);
coll.assertCallback(0, false, theData.length, theData, 0);
coll.callbacks.shift();
}
assert.equal(coll.callbacks.length, count);
for (var i = 0; i < count; i++) {
coll.assertCallback(i, false, 0, new Buffer(0), 0);
}
}
}
/**
* Tests the immediately-available data case of `readAll()`.
*/
function readAllImmediateData() {
var theData = new Buffer("Who wants a cupcake?");
var source = new events.EventEmitter();
var slicer = new Slicer(source);
var coll = new CallbackCollector();
source.emit("data", theData);
slicer.readAll(coll.callback);
slicer.readAll(coll.callback);
assert.equal(coll.callbacks.length, 2);
coll.assertCallback(0, false, theData.length, theData, 0);
coll.assertCallback(1, false, 0, new Buffer(0), 0);
}
/**
* Tests the case of a `readAll()` that grabs the end chunk of
* some data behind a fixed-length `read()`.
*/
function readAllAfterReadWithLength() {
var theData = new Buffer("Try the shortbread.");
var data0 = theData.slice(0, 10);
var data1 = theData.slice(10);
var source = new events.EventEmitter();
var slicer = new Slicer(source);
var coll = new CallbackCollector();
slicer.read(10, coll.callback);
slicer.readAll(coll.callback);
assert.equal(coll.callbacks.length, 0);
source.emit("data", theData);
assert.equal(coll.callbacks.length, 2);
coll.assertCallback(0, false, data0.length, data0, 0);
coll.assertCallback(1, false, data1.length, data1, 0);
}
/**
* Tests that `read()` with length 0 always succeeds when it's
* first in the queue.
*/
function readWithZeroLength() {
var theData = new Buffer("Banana nut muffins: total travesty");
var source = new events.EventEmitter();
var slicer = new Slicer(source);
var coll = new CallbackCollector();
// Test with an empty read queue and nothing pending.
slicer.read(0, coll.callback);
assert.equal(coll.callbacks.length, 1);
coll.assertCallback(0, false, 0, new Buffer(0), 0);
coll.reset();
// Test with an empty read queue and some data pending.
source.emit("data", theData);
slicer.read(0, coll.callback);
assert.equal(coll.callbacks.length, 1);
coll.assertCallback(0, false, 0, new Buffer(0), 0);
coll.reset();
// Test with a non-empty read queue and an initial read that
// doesn't end up consuming all the data.
slicer = new Slicer(source);
slicer.read(10, coll.callback);
slicer.read(0, coll.callback);
source.emit("data", theData);
assert.equal(coll.callbacks.length, 2);
coll.assertCallback(0, false, 10, theData.slice(0, 10), 0);
coll.assertCallback(1, false, 0, new Buffer(0), 0);
coll.reset();
// Test with a non-empty read queue and an initial read that
// *does* end up consuming all the data.
slicer = new Slicer(source);
slicer.read(theData.length, coll.callback);
slicer.read(0, coll.callback);
source.emit("data", theData);
assert.equal(coll.callbacks.length, 2);
coll.assertCallback(0, false, theData.length, theData, 0);
coll.assertCallback(1, false, 0, new Buffer(0), 0);
}
/**
* Tests a spectrum of cases of reading, where the requested
* size of the reads and the size of the buffers passed through in
* data events vary. This is meant to cover cases where the length
* to be read is larger, the same, and smaller than the length of
* the buffers being emitted in events.
*/
function readLengthSpectrum() {
var EMIT_COUNT = 200;
for (var readLength = 1; readLength < 100; readLength += 7) {
for (var emitLength = 1; emitLength < 100; emitLength += 7) {
tryWith(readLength, emitLength);
}
}
function tryWith(readLength, emitLength) {
var source = new events.EventEmitter();
var slicer = new Slicer(source);
var coll = new CallbackCollector();
var buffer = makeEmitBuf(EMIT_COUNT * emitLength);
doEmit(source, buffer, emitLength);
while (buffer.length >= readLength) {
var expectBuf = buffer.slice(0, readLength);
slicer.read(readLength, coll.callback);
assert.equal(coll.callbacks.length, 1);
coll.assertCallback(0, false, readLength, expectBuf, 0);
coll.reset();
buffer = buffer.slice(readLength);
}
}
function doEmit(source, buffer, emitLength) {
while (buffer.length !== 0) {
source.emit("data", buffer.slice(0, emitLength));
buffer = buffer.slice(emitLength);
}
}
function makeEmitBuf(length) {
var result = new Buffer(length);
var ch = 0x41; // 'A'
for (var i = 0; i < length; i++) {
result[i] = ch;
ch++;
if (ch > 0x5a /* 'Z' */) {
ch = 0x41;
}
}
return result;
}
}
/**
* Tests the various argument options of `readInto()`.
*/
function readInto() {
var theData = new Buffer("Strawberry cupcakes: surprisingly delicious");
var source = new events.EventEmitter();
var slicer = new Slicer(source);
var coll = new CallbackCollector();
var target = new Buffer(10);
target.fill(0x61);
// Test a zero-length read. The callback's offset should correspond.
source.emit("data", theData);
slicer.readInto(target, 5, 0, coll.callback);
assert.equal(coll.callbacks.length, 1);
coll.assertCallback(0, false, 0, target, 5);
assert.strictEqual(coll.callbacks[0].buffer, target);
slicer = new Slicer(source);
coll.reset();
// Test reading the entire buffer.
source.emit("data", theData);
slicer.readInto(target, 0, undefined, coll.callback);
assert.equal(coll.callbacks.length, 1);
coll.assertCallback(0, false, 10, theData.slice(0, 10), 0);
assert.strictEqual(coll.callbacks[0].buffer, target);
slicer = new Slicer(source);
coll.reset();
target.fill(0x61);
// Test reading from the middle to the end of the buffer, with
// `undefined` length.
source.emit("data", theData);
slicer.readInto(target, 3, undefined, coll.callback);
assert.equal(coll.callbacks.length, 1);
coll.assertCallback(0, false, 7, target, 3);
assert.equal(target, "aaaStrawbe");
assert.strictEqual(coll.callbacks[0].buffer, target);
slicer = new Slicer(source);
coll.reset();
target.fill(0x61);
// Test reading from the middle to the end of the buffer, with
// precisely-correct length.
source.emit("data", theData);
slicer.readInto(target, 6, undefined, coll.callback);
assert.equal(coll.callbacks.length, 1);
coll.assertCallback(0, false, 4, target, 6);
assert.equal(target, "aaaaaaStra");
assert.strictEqual(coll.callbacks[0].buffer, target);
}
/**
* Tests that a length-specified read at the end of the stream ends up
* succeeding as a partial read or an error read (as appropriate).
*/
function partialRead() {
tryWith("end");
tryWith("close");
tryWith("close", true);
tryWith("close", new Error("yowtch"));
tryWith("error");
tryWith("error", false);
tryWith("error", new Error("craziness"));
function tryWith(endEvent, endArg) {
var theData = new Buffer("ice cream");
var source = new events.EventEmitter();
var slicer = new Slicer(source);
var coll = new CallbackCollector();
var expectError = (endEvent === "error") || (endArg !== undefined);
slicer.read(1000, coll.callback);
slicer.read(1, coll.callback);
slicer.readAll(coll.callback);
source.emit("data", theData);
assert.equal(coll.callbacks.length, 0);
emit(source, endEvent, endArg);
assert.equal(coll.callbacks.length, 3);
coll.assertCallback(0, true, theData.length, theData, 0);
coll.assertCallback(1, true, 0, new Buffer(0), 0);
coll.assertCallback(2, expectError, 0, new Buffer(0), 0);
}
}
/**
* Tests that the initial incoming data encoding works as expected.
*/
function constructorEncodings() {
tryWith("ascii", "muffin");
tryWith("base64", new Buffer("biscuit").toString("base64"));
tryWith("hex", new Buffer("scone").toString("hex"));
tryWith("ucs2", "cupcake");
tryWith("utf16le", "croissant");
tryWith("utf8", "bear claw");
function tryWith(encodingName, dataString) {
var source = new events.EventEmitter();
var slicer = new Slicer(source, { incomingEncoding: encodingName });
var coll = new CallbackCollector();
if (encodingName === "utf16le") {
// For compatibility with Node 0.6.*.
encodingName = "ucs2";
}
var expectData = new Buffer(dataString, encodingName);
source.emit("data", dataString);
slicer.readAll(coll.callback);
assert.equal(coll.callbacks.length, 1);
coll.assertCallback(0, false, expectData.length, expectData, 0);
}
}
/**
* Tests that `setIncomingEncoding()` works as expected, particularly
* that it applies only to subsequently-received `data` events.
*/
function setIncomingEncoding() {
var source = new events.EventEmitter();
var slicer = new Slicer(source);
var coll = new CallbackCollector();
var expectData = new Buffer(0);
addExpect("ascii", "dark chocolate.");
addExpect("base64", new Buffer("milk chocolate. ").toString("base64"));
addExpect("hex", new Buffer("white chocolate. ").toString("hex"));
addExpect("ucs2", "German white chocolate, with almonds. ");
addExpect("utf16le", "extra dark chocolate. ");
addExpect("utf8", "caramel.");
slicer.readAll(coll.callback);
assert.equal(coll.callbacks.length, 1);
coll.assertCallback(0, false, expectData.length, expectData, 0);
function addExpect(encodingName, dataString) {
slicer.setIncomingEncoding(encodingName);
source.emit("data", dataString);
if (encodingName === "utf16le") {
// For compatibility with Node 0.6.*.
encodingName = "ucs2";
}
var buf = new Buffer(dataString, encodingName);
var newExpect = new Buffer(expectData.length + buf.length);
expectData.copy(newExpect);
buf.copy(newExpect, expectData.length);
expectData = newExpect;
}
}
/**
* Makes sure callbacks aren't getting unintentionally reused. (The
* other multi-callback test cases end up intentionally reusing
* callbacks, for programmer convenience.)
*/
function noCallbackReuse() {
var theData = new Buffer("cannelle");
var source = new events.EventEmitter();
var slicer = new Slicer(source);
var colls = [];
for (var i = 0; i < 20; i++) {
colls[i] = new CallbackCollector();
slicer.read(1, colls[i].callback);
}
source.emit("data", theData);
source.emit("end");
for (i = 0; i < colls.length; i++) {
var one = colls[i];
var expectError = (i >= theData.length);
var expectBuf = expectError ? new Buffer(0) : theData.slice(i, i + 1);
assert.equal(one.callbacks.length, 1);
one.assertCallback(0, expectError, expectBuf.length, expectBuf, 0);
}
}
/**
* Makes sure that callbacks aren't called with anything other than
* a default `this`.
*/
function callbackThis() {
var source = new events.EventEmitter();
var slicer = new Slicer(source);
var count = 0;
source.emit("data", "sufficiently tasty muffins");
// These are meant to cover all the various ways a callback might be
// triggered.
slicer.read(1, callback);
slicer.read(0, callback);
slicer.readInto(new Buffer(5), 0, 5, callback);
slicer.readAll(callback);
slicer.read(0, callback);
source.emit("error", new Error("Insufficient tastiness after all!"));
slicer.read(0, callback);
slicer.read(1, callback);
slicer.readAll(callback);
slicer.readInto(new Buffer(1), 0, 1, callback);
assert.equal(count, 9, "Missing callback.");
function callback(/*ignored*/) {
count++;
assert.equal(this, undefined, "Bogus `this` in callback.");
}
}
function test() {
constructor();
constructorFailures();
readableTransition();
destroy();
getErrorGotError();
readAllNoData();
readAllImmediateData();
readAllAfterReadWithLength();
readWithZeroLength();
readLengthSpectrum();
readInto();
partialRead();
constructorEncodings();
setIncomingEncoding();
noCallbackReuse();
callbackThis();
}
module.exports = {
test: test
};